/* jshint strict: false, sub: true */ /* global print db */ 'use strict'; // ////////////////////////////////////////////////////////////////////////////// // / DISCLAIMER // / // / Copyright 2016 ArangoDB GmbH, Cologne, Germany // / Copyright 2014 triagens GmbH, Cologne, Germany // / // / Licensed under the Apache License, Version 2.0 (the "License") // / you may not use this file except in compliance with the License. // / You may obtain a copy of the License at // / // / http://www.apache.org/licenses/LICENSE-2.0 // / // / Unless required by applicable law or agreed to in writing, software // / distributed under the License is distributed on an "AS IS" BASIS, // / WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // / See the License for the specific language governing permissions and // / limitations under the License. // / // / Copyright holder is ArangoDB GmbH, Cologne, Germany // / // / @author Max Neunhoeffer // ////////////////////////////////////////////////////////////////////////////// /* Modules: */ const _ = require('lodash'); const fs = require('fs'); const pu = require('@arangodb/process-utils'); const yaml = require('js-yaml'); const toArgv = require('internal').toArgv; const time = require('internal').time; const sleep = require('internal').sleep; const download = require('internal').download; /* Constants: */ // const BLUE = require('internal').COLORS.COLOR_BLUE; // const CYAN = require('internal').COLORS.COLOR_CYAN; // const GREEN = require('internal').COLORS.COLOR_GREEN; const RED = require('internal').COLORS.COLOR_RED; const RESET = require('internal').COLORS.COLOR_RESET; // const YELLOW = require('internal').COLORS.COLOR_YELLOW; let didSplitBuckets = false; // ////////////////////////////////////////////////////////////////////////////// // / @brief get the items uniq to arr1 or arr2 // ////////////////////////////////////////////////////////////////////////////// function diffArray (arr1, arr2) { return arr1.concat(arr2).filter(function (val) { if (!(arr1.includes(val) && arr2.includes(val))) { return val; } }); } // ////////////////////////////////////////////////////////////////////////////// // / @brief build a unix path // ////////////////////////////////////////////////////////////////////////////// function makePathUnix (path) { return fs.join.apply(null, path.split('/')); } // ////////////////////////////////////////////////////////////////////////////// // / @brief build a generic path // ////////////////////////////////////////////////////////////////////////////// function makePathGeneric (path) { return fs.join.apply(null, path.split(fs.pathSeparator)); } // ////////////////////////////////////////////////////////////////////////////// // / @brief runs a list of tests // ////////////////////////////////////////////////////////////////////////////// function performTests (options, testList, testname, runFn, serverOptions, startStopHandlers) { if (options.testBuckets && !didSplitBuckets) { throw new Error("You parametrized to split buckets, but this testsuite doesn't support it!!!"); } if (testList.length === 0) { print('Testsuite is empty!'); return { 'EMPTY TESTSUITE': { status: false, message: 'no testsuites found!' } }; } if (serverOptions === undefined) { serverOptions = {}; } let env = {}; let customInstanceInfos = {}; if (startStopHandlers !== undefined && startStopHandlers.hasOwnProperty('preStart')) { customInstanceInfos['preStart'] = startStopHandlers.preStart(options, serverOptions, customInstanceInfos, startStopHandlers); if (customInstanceInfos.preStart.state === false) { return { setup: { status: false, message: 'custom preStart failed!' + customInstanceInfos.preStart.message } }; } _.defaults(env, customInstanceInfos.preStart.env); } let instanceInfo = pu.startInstance('tcp', options, serverOptions, testname); if (instanceInfo === false) { if (startStopHandlers !== undefined && startStopHandlers.hasOwnProperty('startFailed')) { customInstanceInfos['startFailed'] = startStopHandlers.startFailed(options, serverOptions, customInstanceInfos, startStopHandlers); } return { setup: { status: false, message: 'failed to start server!' } }; } if (startStopHandlers !== undefined && startStopHandlers.hasOwnProperty('postStart')) { customInstanceInfos['postStart'] = startStopHandlers.postStart(options, serverOptions, instanceInfo, customInstanceInfos, startStopHandlers); if (customInstanceInfos.postStart.state === false) { pu.shutdownInstance(instanceInfo, options); return { setup: { status: false, message: 'custom postStart failed: ' + customInstanceInfos.postStart.message } }; } _.defaults(env, customInstanceInfos.postStart.env); } let results = {}; let continueTesting = true; let count = 0; let forceTerminate = false; let graphCount = 0; for (let i = 0; i < testList.length; i++) { let te = testList[i]; let filtered = {}; if (filterTestcaseByOptions(te, options, filtered)) { let first = true; let loopCount = 0; count += 1; let collectionsBefore = []; db._collections().forEach(collection => { collectionsBefore.push(collection._name); }); while (first || options.loopEternal) { if (!continueTesting) { print('oops! Skipping, ' + te + ' server is gone.'); results[te] = { status: false, message: instanceInfo.exitStatus }; instanceInfo.exitStatus = 'server is gone.'; break; } print('\n' + Date() + ' ' + runFn.info + ': Trying', te, '...'); let reply = runFn(options, instanceInfo, te, env); if (reply.hasOwnProperty('forceTerminate')) { continueTesting = false; forceTerminate = true; continue; } else if (reply.hasOwnProperty('status')) { results[te] = reply; if (results[te].status === false) { options.cleanup = false; } if (!reply.status && !options.force) { break; } } else { results[te] = { status: false, message: reply }; if (!options.force) { break; } } continueTesting = pu.arangod.check.instanceAlive(instanceInfo, options); // Check whether some collections were left behind, and if mark test as failed. let collectionsAfter = []; db._collections().forEach(collection => { collectionsAfter.push(collection._name); }); let delta = diffArray(collectionsBefore, collectionsAfter).filter(function(name) { return (name[0] !== '_'); // exclude system collections from the comparison }); print(delta); if (delta.length !== 0) { results[te] = { status: false, message: 'Cleanup missing - test left over collections: ' + delta + '. Original test status: ' + JSON.stringify(results[te]) }; collectionsBefore = []; db._collections().forEach(collection => { collectionsBefore.push(collection._name); }); } let graphs = db._collection('_graphs'); if (graphs && graphs.count() !== graphCount) { results[te] = { status: false, message: 'Cleanup of graphs missing - found graph definitions: [ ' + JSON.stringify(graphs.toArray()) + ' ] - Original test status: ' + JSON.stringify(results[te]) }; graphCount = graphs.count(); } if (startStopHandlers !== undefined && startStopHandlers.hasOwnProperty('alive')) { customInstanceInfos['alive'] = startStopHandlers.alive(options, serverOptions, instanceInfo, customInstanceInfos, startStopHandlers); if (customInstanceInfos.alive.state === false) { continueTesting = false; results.setup.message = 'custom preStop failed!'; } collectionsBefore = []; db._collections().forEach(collection => { collectionsBefore.push(collection._name); }); } first = false; if (options.loopEternal) { if (loopCount % options.loopSleepWhen === 0) { print('sleeping...'); sleep(options.loopSleepSec); print('continuing.'); } ++loopCount; } } } else { if (options.extremeVerbosity) { print('Skipped ' + te + ' because of ' + filtered.filter); } } } if (count === 0) { results['ALLTESTS'] = { status: false, skipped: true }; results.status = false; print(RED + 'No testcase matched the filter.' + RESET); } print('Shutting down...'); if (startStopHandlers !== undefined && startStopHandlers.hasOwnProperty('preStop')) { customInstanceInfos['preStop'] = startStopHandlers.preStop(options, serverOptions, instanceInfo, customInstanceInfos, startStopHandlers); if (customInstanceInfos.preStop.state === false) { results.setup.status = false; results.setup.message = 'custom preStop failed!'; } } // pass on JWT secret let clonedOpts = _.clone(options); if (serverOptions['server.jwt-secret'] && !clonedOpts['server.jwt-secret']) { clonedOpts['server.jwt-secret'] = serverOptions['server.jwt-secret']; } pu.shutdownInstance(instanceInfo, clonedOpts, forceTerminate); if (startStopHandlers !== undefined && startStopHandlers.hasOwnProperty('postStop')) { customInstanceInfos['postStop'] = startStopHandlers.postStop(options, serverOptions, instanceInfo, customInstanceInfos, startStopHandlers); if (customInstanceInfos.postStop.state === false) { results.setup.status = false; results.setup.message = 'custom postStop failed!'; } } print('done.'); return results; } // ////////////////////////////////////////////////////////////////////////////// // / @brief filter test-cases according to options // ////////////////////////////////////////////////////////////////////////////// function filterTestcaseByOptions (testname, options, whichFilter) { // These filters require a proper setup, Even if we filter by testcase: if ((testname.indexOf('-mmfiles') !== -1) && options.storageEngine === 'rocksdb') { whichFilter.filter = 'skip when running as rocksdb'; return false; } if ((testname.indexOf('-rocksdb') !== -1) && options.storageEngine === 'mmfiles') { whichFilter.filter = 'skip when running as mmfiles'; return false; } if (options.replication) { whichFilter.filter = 'replication'; if (options.hasOwnProperty('test') && (typeof (options.test) !== 'undefined')) { whichFilter.filter = 'testcase'; return ((testname.search(options.test) >= 0) && (testname.indexOf('replication') !== -1)); } else { return testname.indexOf('replication') !== -1; } } else if (testname.indexOf('replication') !== -1) { whichFilter.filter = 'replication'; return false; } if ((testname.indexOf('-cluster') !== -1) && !options.cluster) { whichFilter.filter = 'noncluster'; return false; } if (testname.indexOf('-noncluster') !== -1 && options.cluster) { whichFilter.filter = 'cluster'; return false; } if (testname.indexOf('-arangosearch') !== -1 && !options.arangosearch) { whichFilter.filter = 'arangosearch'; return false; } // if we filter, we don't care about the other filters below: if (options.hasOwnProperty('test') && (typeof (options.test) !== 'undefined')) { whichFilter.filter = 'testcase'; return testname.search(options.test) >= 0; } if (testname.indexOf('-timecritical') !== -1 && options.skipTimeCritical) { whichFilter.filter = 'timecritical'; return false; } if (testname.indexOf('-nightly') !== -1 && options.skipNightly && !options.onlyNightly) { whichFilter.filter = 'skip nightly'; return false; } if (testname.indexOf('-geo') !== -1 && options.skipGeo) { whichFilter.filter = 'geo'; return false; } if (testname.indexOf('-nondeterministic') !== -1 && options.skipNondeterministic) { whichFilter.filter = 'nondeterministic'; return false; } if (testname.indexOf('-graph') !== -1 && options.skipGraph) { whichFilter.filter = 'graph'; return false; } if (testname.indexOf('-disabled') !== -1) { whichFilter.filter = 'disabled'; return false; } if ((testname.indexOf('-memoryintense') !== -1) && options.skipMemoryIntense) { whichFilter.filter = 'memoryintense'; return false; } if (testname.indexOf('-nightly') === -1 && options.onlyNightly) { whichFilter.filter = 'only nightly'; return false; } if ((testname.indexOf('-novalgrind') !== -1) && options.valgrind) { whichFilter.filter = 'skip in valgrind'; return false; } return true; } // ////////////////////////////////////////////////////////////////////////////// // / @brief split into buckets // ////////////////////////////////////////////////////////////////////////////// function splitBuckets (options, cases) { if (!options.testBuckets || cases.length === 0) { return cases; } didSplitBuckets = true; let m = cases.length; let n = options.testBuckets.split('/'); let r = parseInt(n[0]); let s = parseInt(n[1]); if (r < 1) { r = 1; } if (r === 1) { return cases; } if (s < 0) { s = 0; } if (r <= s) { s = r - 1; } let result = []; for (let i = s % m; i < cases.length; i = i + r) { result.push(cases[i]); } return result; } function doOnePathInner (path) { return _.filter(fs.list(makePathUnix(path)), function (p) { return (p.substr(-3) === '.js'); }) .map(function (x) { return fs.join(makePathUnix(path), x); }).sort(); } function scanTestPath (path) { var community = doOnePathInner(path); if (global.ARANGODB_CLIENT_VERSION(true)['enterprise-version']) { return community.concat(doOnePathInner('enterprise/' + path)); } else { return community; } } // ////////////////////////////////////////////////////////////////////////////// // / @brief runs a remote unittest file using /_admin/execute // ////////////////////////////////////////////////////////////////////////////// function runThere (options, instanceInfo, file) { try { let testCode; let mochaGrep = options.mochaGrep ? ', ' + JSON.stringify(options.mochaGrep) : ''; if (file.indexOf('-spec') === -1) { testCode = 'const runTest = require("jsunity").runTest; ' + 'return runTest(' + JSON.stringify(file) + ', true);'; } else { testCode = 'const runTest = require("@arangodb/mocha-runner"); ' + 'return runTest(' + JSON.stringify(file) + ', true' + mochaGrep + ');'; } testCode = 'global.instanceInfo = ' + JSON.stringify(instanceInfo) + ';\n' + testCode; let httpOptions = pu.makeAuthorizationHeaders(options); httpOptions.method = 'POST'; httpOptions.timeout = 1800; if (options.valgrind) { httpOptions.timeout *= 2; } httpOptions.returnBodyOnError = true; const reply = download(instanceInfo.url + '/_admin/execute?returnAsJSON=true', testCode, httpOptions); if (!reply.error && reply.code === 200) { return JSON.parse(reply.body); } else { if ((reply.code === 500) && reply.hasOwnProperty('message') && (reply.message === 'Request timeout reached')) { return { status: false, message: reply.message, forceTerminate: true }; } else if (reply.hasOwnProperty('body')) { return { status: false, message: reply.body }; } else { return { status: false, message: yaml.safeDump(reply) }; } } } catch (ex) { return { status: false, message: ex.message || String(ex), stack: ex.stack }; } } runThere.info = 'runThere'; // ////////////////////////////////////////////////////////////////////////////// // / @brief runs a local unittest file using arangosh // ////////////////////////////////////////////////////////////////////////////// function runInArangosh (options, instanceInfo, file, addArgs) { let args = pu.makeArgs.arangosh(options); args['server.endpoint'] = instanceInfo.endpoint; args['javascript.unit-tests'] = fs.join(pu.TOP_DIR, file); if (!options.verbose) { args['log.level'] = 'warning'; } if (addArgs !== undefined) { args = Object.assign(args, addArgs); } require('internal').env.INSTANCEINFO = JSON.stringify(instanceInfo); let rc = pu.executeAndWait(pu.ARANGOSH_BIN, toArgv(args), options, 'arangosh', instanceInfo.rootDir); let result; try { result = JSON.parse(fs.read(instanceInfo.rootDir + '/testresult.json')); fs.remove(instanceInfo.rootDir + '/testresult.json'); } catch (x) { if (options.extremeVerbosity) { print('failed to read ' + instanceInfo.rootDir + '/testresult.json'); } return rc; } if ((typeof result[0] === 'object') && result[0].hasOwnProperty('status')) { return result[0]; } else { rc.failed = rc.status ? 0 : 1; return rc; } } runInArangosh.info = 'arangosh'; function makeResults (testname, instanceInfo) { const startTime = time(); return function (status, message) { let duration = time() - startTime; let results; if (status) { let result; try { result = JSON.parse(fs.read(instanceInfo.rootDir + '/testresult.json')); if ((typeof result[0] === 'object') && result[0].hasOwnProperty('status')) { results = result[0]; } } catch (x) {} } if (results === undefined) { results = { status: status, duration: duration, total: 1, failed: status ? 0 : 1, 'testing.js': { status: status, duration: duration } }; if (message) { results['testing.js'].message = message; } } let full = {}; full[testname] = results; return full; }; } exports.runThere = runThere; exports.runInArangosh = runInArangosh; exports.makePathUnix = makePathUnix; exports.makePathGeneric = makePathGeneric; exports.performTests = performTests; exports.filterTestcaseByOptions = filterTestcaseByOptions; exports.splitBuckets = splitBuckets; exports.doOnePathInner = doOnePathInner; exports.scanTestPath = scanTestPath; exports.makeResults = makeResults; exports.diffArray = diffArray;