Add Cassandra 4.0 to the Jenkins build and test fixes (#352)

* Limit cloud test runs
* Examples log a message for not supported runtimes
* Add C* 4.0 to build
diff --git a/build.yaml b/build.yaml
index fcdafea..135920d 100644
--- a/build.yaml
+++ b/build.yaml
@@ -5,13 +5,13 @@
     matrix:
       exclude:
         - nodejs: ['8']
-          # Include 2.1, dse-6.0
-          cassandra: ['3.11', 'dse-5.1', 'dse-6.7', 'dse-6.8']
+          # Include 2.1, dse-6.0, dse-6.7
+          cassandra: ['3.11', '4.0', 'dse-5.1', 'dse-6.8']
         - nodejs: ['10']
           # Include 3.11, dse-6.8
-          cassandra: ['2.1', 'dse-5.1', 'dse-6.0', 'dse-6.7']
+          cassandra: ['2.1', '4.0', 'dse-5.1', 'dse-6.0', 'dse-6.7']
         - nodejs: ['12']
-          # Include dse-5.1, dse-6.7
+          # Include 4.0, dse-5.1, dse-6.7
           # Examples are run against DSE 6.7
           cassandra: ['2.1', '3.11', 'dse-6.0', 'dse-6.8']
   nightly:
@@ -35,10 +35,11 @@
 cassandra:
   - '2.1'
   - '3.11'
-  - dse-5.1
-  - dse-6.0
-  - dse-6.7
-  - dse-6.8
+  - '4.0'
+  - 'dse-5.1'
+  - 'dse-6.0'
+  - 'dse-6.7'
+  - 'dse-6.8'
 build:
   - type: envinject
     properties: |
diff --git a/examples/runner.js b/examples/runner.js
index dfa527d..50f7ee5 100644
--- a/examples/runner.js
+++ b/examples/runner.js
@@ -31,7 +31,8 @@
 }
 
 if (+process.versions.node.split('.')[0] < 10) {
-  throw new Error('Examples were not executed as they were designed to run against Node.js 10+');
+  console.log('Examples were not executed as they were designed to run against Node.js 10+');
+  return;
 }
 
 const runnerFileName = path.basename(module.filename);
diff --git a/test/integration/short/client-execute-prepared-tests.js b/test/integration/short/client-execute-prepared-tests.js
index dc9976d..6fe91f5 100644
--- a/test/integration/short/client-execute-prepared-tests.js
+++ b/test/integration/short/client-execute-prepared-tests.js
@@ -35,11 +35,16 @@
   describe('#execute(query, params, {prepare: 1}, callback)', function () {
     const commonTable = commonKs + '.' + helper.getRandomName('table');
     const commonTable2 = commonKs + '.' + helper.getRandomName('table');
+    const yaml = ['batch_size_warn_threshold_in_kb:5'];
+
+    if (!helper.isDse() && helper.isCassandraGreaterThan('4.0')) {
+      yaml.push('enable_materialized_views:true');
+    }
 
     const setupInfo = helper.setup(3, {
       keyspace: commonKs,
       queries: [ helper.createTableWithClusteringKeyCql(commonTable), helper.createTableCql(commonTable2) ],
-      ccmOptions: { yaml: ['batch_size_warn_threshold_in_kb:5'] }
+      ccmOptions: { yaml }
     });
 
     it('should execute a prepared query with parameters on all hosts', function (done) {
diff --git a/test/integration/short/cloud/cloud-tests.js b/test/integration/short/cloud/cloud-tests.js
index a4e0a30..2c90f16 100644
--- a/test/integration/short/cloud/cloud-tests.js
+++ b/test/integration/short/cloud/cloud-tests.js
@@ -34,8 +34,8 @@
 vdescribe('dse-6.7', 'Cloud support', function () {
   // Only run tests with few versions of DSE as SNI project has a fixed C*/DSE version
 
-  if (helper.isWin()) {
-    // Skip altogether for AppVeyor
+  if (helper.isWin() || process.versions.node.split('.')[0] < 10) {
+    // Skip altogether for AppVeyor and Node.js runtime below 10
     return;
   }
 
diff --git a/test/integration/short/control-connection-tests.js b/test/integration/short/control-connection-tests.js
index 4ba9a27..99a4047 100644
--- a/test/integration/short/control-connection-tests.js
+++ b/test/integration/short/control-connection-tests.js
@@ -135,6 +135,15 @@
 
       await cc.init();
       await new Promise(r => cc.options.policies.loadBalancing.init(null, cc.hosts, r));
+
+      if (helper.isCassandraGreaterThan('4.0')) {
+        // To avoid issue "Not enough live nodes to maintain replication factor"
+        await cc.query("ALTER KEYSPACE system_traces" +
+          " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}");
+        await cc.query("ALTER KEYSPACE system_distributed" +
+          " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}");
+      }
+
       await util.promisify(helper.ccmHelper.decommissionNode)(2);
 
       await helper.wait.forNodeToBeRemoved(cc.hosts, 2);
diff --git a/test/integration/short/metadata-tests.js b/test/integration/short/metadata-tests.js
index 002c925..aa34494 100644
--- a/test/integration/short/metadata-tests.js
+++ b/test/integration/short/metadata-tests.js
@@ -27,10 +27,19 @@
 
 describe('metadata', function () {
   this.timeout(240000);
-  const setupInfo = helper.setup('2:0', { ccmOptions: {
-    vnodes: true,
-    yaml: helper.isDseGreaterThan('6') ? ['cdc_enabled:true'] : null
-  }});
+
+  const yaml = [];
+
+  if (helper.isDseGreaterThan('6')) {
+    yaml.push('cdc_enabled:true');
+  }
+
+  if (!helper.isDse() && helper.isCassandraGreaterThan('4.0')) {
+    yaml.push('enable_materialized_views:true');
+  }
+
+  const setupInfo = helper.setup('2:0', { ccmOptions: { vnodes: true, yaml }});
+
   describe('Metadata', function () {
     describe('#keyspaces', function () {
       it('should keep keyspace information up to date', function (done) {
diff --git a/test/test-helper.js b/test/test-helper.js
index e39f2d4..6716b48 100644
--- a/test/test-helper.js
+++ b/test/test-helper.js
@@ -444,6 +444,11 @@
     return helper.versionCompare(serverInfo.version, version);
   },
 
+  /** Determines if the current server is a DSE instance. */
+  isDse: function () {
+    return this.getServerInfo().isDse;
+  },
+
   /**
    * Determines if the current C* or DSE instance version is greater than or equals to the C* version provided
    * @param {String} version The version in string format, dot separated.