diff --git a/.ci/jobs/defaults.yml b/.ci/jobs/defaults.yml index 1ccaa8b08..5b4b1f68c 100644 --- a/.ci/jobs/defaults.yml +++ b/.ci/jobs/defaults.yml @@ -65,3 +65,6 @@ publishers: - email: recipients: infra-root+build@elastic.co + # - junit: + # results: "*-junit.xml" + # allow-empty-results: true diff --git a/.gitignore b/.gitignore index b726b15e7..a5768f2f9 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,5 @@ elasticsearch* api/generated.d.ts test/benchmarks/macro/fixtures/* + +*-junit.xml diff --git a/package.json b/package.json index 054398efe..41eef9a94 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "test": "npm run lint && npm run test:unit && npm run test:behavior && npm run test:types", "test:unit": "tap test/unit/*.test.js -t 300 --no-coverage", "test:behavior": "tap test/behavior/*.test.js -t 300 --no-coverage", - "test:integration": "tap test/integration/index.js -T --harmony --no-esm --no-coverage", + "test:integration": "tap test/integration/index.js -T --no-coverage", + "test:integration:report": "npm run test:integration | tap-mocha-reporter xunit > $WORKSPACE/test-report-junit.xml", "test:types": "tsc --project ./test/types/tsconfig.json", "test:coverage": "nyc npm run test:unit && nyc report --reporter=text-lcov > coverage.lcov && codecov", "lint": "standard", @@ -56,6 +57,7 @@ "standard": "^12.0.1", "stoppable": "^1.1.0", "tap": "^13.0.1", + "tap-mocha-reporter": "^4.0.1", "typescript": "^3.4.5", "workq": "^2.1.0" }, diff --git a/scripts/es-docker-platinum.sh b/scripts/es-docker-platinum.sh index 9b6b69494..03171a275 100755 --- a/scripts/es-docker-platinum.sh +++ b/scripts/es-docker-platinum.sh @@ -9,6 +9,11 @@ testnodecrt="/.ci/certs/testnode.crt" testnodekey="/.ci/certs/testnode.key" cacrt="/.ci/certs/ca.crt" +# pass `--clean` to reemove the old snapshot +if [ "$1" != "" ]; then + docker rmi $(docker images --format '{{.Repository}}:{{.Tag}}' | grep '8.0.0-SNAPSHOT') +fi + exec docker run \ --rm \ -e "node.attr.testattr=test" \ diff --git a/scripts/es-docker.sh b/scripts/es-docker.sh index b50050389..794e77684 100755 --- a/scripts/es-docker.sh +++ b/scripts/es-docker.sh @@ -4,6 +4,11 @@ # to delete an old image and download again # the latest snapshot. +# pass `--clean` to reemove the old snapshot +if [ "$1" != "" ]; then + docker rmi $(docker images --format '{{.Repository}}:{{.Tag}}' | grep '8.0.0-SNAPSHOT') +fi + exec docker run \ --rm \ -e "node.attr.testattr=test" \ diff --git a/test/integration/helper.js b/test/integration/helper.js index ded90388d..0cfb8eba3 100644 --- a/test/integration/helper.js +++ b/test/integration/helper.js @@ -58,11 +58,11 @@ const esDefaultUsers = [ 'remote_monitoring_user' ] -function runInParallel (client, operation, options) { +function runInParallel (client, operation, options, clientOptions) { if (options.length === 0) return Promise.resolve() const operations = options.map(opts => { const api = delve(client, operation).bind(client) - return api(opts) + return api(opts, clientOptions) }) return Promise.all(operations) @@ -82,4 +82,10 @@ function delve (obj, key, def, p) { return (obj === undefined || p < key.length) ? def : obj } -module.exports = { runInParallel, esDefaultRoles, esDefaultUsers, delve } +function to (promise) { + return promise.then(data => [null, data], err => [err, undefined]) +} + +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) + +module.exports = { runInParallel, esDefaultRoles, esDefaultUsers, delve, to, sleep } diff --git a/test/integration/index.js b/test/integration/index.js index bfacb9490..2280de5ef 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -19,32 +19,32 @@ 'use strict' -const assert = require('assert') const { readFileSync, accessSync, mkdirSync, readdirSync, statSync } = require('fs') const { join, sep } = require('path') const yaml = require('js-yaml') const Git = require('simple-git') -const ora = require('ora') const tap = require('tap') const { Client } = require('../../index') const TestRunner = require('./test-runner') +const { sleep } = require('./helper') const esRepo = 'https://github.com/elastic/elasticsearch.git' const esFolder = join(__dirname, '..', '..', 'elasticsearch') const yamlFolder = join(esFolder, 'rest-api-spec', 'src', 'main', 'resources', 'rest-api-spec', 'test') const xPackYamlFolder = join(esFolder, 'x-pack', 'plugin', 'src', 'test', 'resources', 'rest-api-spec', 'test') -const customSkips = [ + +const ossSkips = { // TODO: remove this once 'arbitrary_key' is implemented // https://github.com/elastic/elasticsearch/pull/41492 - 'indices.split/30_copy_settings.yml', + 'indices.split/30_copy_settings.yml': ['*'], // skipping because we are booting ES with `discovery.type=single-node` // and this test will fail because of this configuration - 'nodes.stats/30_discovery.yml', + 'nodes.stats/30_discovery.yml': ['*'], // the expected error is returning a 503, // which triggers a retry and the node to be marked as dead - 'search.aggregation/240_max_buckets.yml' -] -const platinumBlackList = { + 'search.aggregation/240_max_buckets.yml': ['*'] +} +const xPackBlackList = { // file path: test name 'cat.aliases/10_basic.yml': ['Empty cluster'], 'index/10_with_id.yml': ['Index with ID'], @@ -81,278 +81,276 @@ const platinumBlackList = { 'xpack/15_basic.yml': ['*'] } -function Runner (opts) { - if (!(this instanceof Runner)) { - return new Runner(opts) - } - opts = opts || {} - - assert(opts.node, 'Missing base node') - this.bailout = opts.bailout - const options = { node: opts.node } - if (opts.isPlatinum) { - options.ssl = { - // NOTE: this path works only if we run - // the suite with npm scripts - ca: readFileSync('.ci/certs/ca.crt', 'utf8'), - rejectUnauthorized: false - } - } - this.client = new Client(options) - this.log = ora('Loading yaml suite').start() -} - -Runner.prototype.waitCluster = function (callback, times = 0) { - this.log.text = 'Waiting for ElasticSearch' - this.client.cluster.health( - { waitForStatus: 'green', timeout: '50s' }, - (err, res) => { - if (err && ++times < 10) { - setTimeout(() => { - this.waitCluster(callback, times) - }, 5000) - } else { - callback(err) +class Runner { + constructor (opts = {}) { + const options = { node: opts.node } + if (opts.isXPack) { + options.ssl = { + ca: readFileSync(join(__dirname, '..', '..', '.ci', 'certs', 'ca.crt'), 'utf8'), + rejectUnauthorized: false } } - ) -} + this.client = new Client(options) + console.log('Loading yaml suite') + } -/** - * Runs the test suite - */ -Runner.prototype.start = function (opts) { - const parse = this.parse.bind(this) - const client = this.client - - // client.on('response', (err, meta) => { - // console.log('Request', meta.request) - // if (err) { - // console.log('Error', err) - // } else { - // console.log('Response', JSON.stringify(meta.response, null, 2)) - // } - // console.log() - // }) - - this.waitCluster(err => { - if (err) { - this.log.fail(err.message) + async waitCluster (client, times = 0) { + try { + await client.cluster.health({ waitForStatus: 'green', timeout: '50s' }) + } catch (err) { + if (++times < 10) { + await sleep(5000) + return this.waitCluster(client, times) + } + console.error(err) process.exit(1) } - // Get the build hash of Elasticsearch - client.info((err, { body }) => { - if (err) { - this.log.fail(err.message) - process.exit(1) - } - const { number: version, build_hash: sha } = body.version + } - // Set the repository to the given sha and run the test suite - this.withSHA(sha, () => { - this.log.succeed(`Testing ${opts.isPlatinum ? 'platinum' : 'oss'} api...`) - runTest.call(this, version) - }) - }) - }) + async start ({ isXPack }) { + const { client } = this + const parse = this.parse.bind(this) - function runTest (version) { - const files = [] + console.log('Waiting for Elasticsearch') + await this.waitCluster(client) + + const { body } = await client.info() + const { number: version, build_hash: sha } = body.version + + console.log(`Checking out sha ${sha}...`) + await this.withSHA(sha) + + console.log(`Testing ${isXPack ? 'XPack' : 'oss'} api...`) + + const folders = [] .concat(getAllFiles(yamlFolder)) - .concat(opts.isPlatinum ? getAllFiles(xPackYamlFolder) : []) + .concat(isXPack ? getAllFiles(xPackYamlFolder) : []) .filter(t => !/(README|TODO)/g.test(t)) + // we cluster the array based on the folder names, + // to provide a better test log output + .reduce((arr, file) => { + const path = file.slice(file.indexOf('/rest-api-spec/test'), file.lastIndexOf('/')) + var inserted = false + for (var i = 0; i < arr.length; i++) { + if (arr[i][0].includes(path)) { + inserted = true + arr[i].push(file) + break + } + } + if (!inserted) arr.push([file]) + return arr + }, []) - files.forEach(runTestFile.bind(this)) - function runTestFile (file) { - for (var i = 0; i < customSkips.length; i++) { - if (file.endsWith(customSkips[i])) return - } - // create a subtest for the specific folder - tap.test(file.slice(file.indexOf(`${sep}elasticsearch${sep}`)), { jobs: 1 }, tap1 => { - // read the yaml file - const data = readFileSync(file, 'utf8') - // get the test yaml (as object), some file has multiple yaml documents inside, - // every document is separated by '---', so we split on the separator - // and then we remove the empty strings, finally we parse them - const tests = data - .split('\n---\n') - .map(s => s.trim()) - .filter(Boolean) - .map(parse) + for (const folder of folders) { + // pretty name + const apiName = folder[0].slice( + folder[0].indexOf(`${sep}rest-api-spec${sep}test`) + 19, + folder[0].lastIndexOf(sep) + ) + tap.test(`Testing ${apiName}`, { bail: true, timeout: 0 }, t => { + for (const file of folder) { + const data = readFileSync(file, 'utf8') + // get the test yaml (as object), some file has multiple yaml documents inside, + // every document is separated by '---', so we split on the separator + // and then we remove the empty strings, finally we parse them + const tests = data + .split('\n---\n') + .map(s => s.trim()) + .filter(Boolean) + .map(parse) + + t.test( + file.slice(file.lastIndexOf(apiName)), + testFile(file, tests) + ) + } + t.end() + }) + } + + function testFile (file, tests) { + return t => { // get setup and teardown if present var setupTest = null var teardownTest = null - tests.forEach(test => { + for (const test of tests) { if (test.setup) setupTest = test.setup if (test.teardown) teardownTest = test.teardown - }) + } - // run the tests tests.forEach(test => { const name = Object.keys(test)[0] if (name === 'setup' || name === 'teardown') return - // should skip the test inside `platinumBlackList` - // if we are testing the platinum apis - if (opts.isPlatinum) { - const list = Object.keys(platinumBlackList) - for (i = 0; i < list.length; i++) { - const platTest = platinumBlackList[list[i]] - for (var j = 0; j < platTest.length; j++) { - if (file.endsWith(list[i]) && (name === platTest[j] || platTest[j] === '*')) { - const testName = file.slice(file.indexOf(`${sep}elasticsearch${sep}`)) + ' / ' + name - tap.skip(`Skipping test ${testName} because is blacklisted in the platinum test`) - return - } - } - } - } + if (shouldSkip(t, isXPack, file, name)) return + // create a subtest for the specific folder + test file + test name - tap1.test(name, { jobs: 1, bail: this.bailout }, tap2 => { - const testRunner = TestRunner({ + t.test(name, async t => { + const testRunner = new TestRunner({ client, version, - tap: tap2, - isPlatinum: file.includes('x-pack') + tap: t, + isXPack: file.includes('x-pack') }) - testRunner.run(setupTest, test[name], teardownTest, () => tap2.end()) + await testRunner.run(setupTest, test[name], teardownTest) }) }) - - tap1.end() - }) + t.end() + } } } -} -/** - * Parses a given yaml document - * @param {string} yaml document - * @returns {object} - */ -Runner.prototype.parse = function (data) { - try { - var doc = yaml.safeLoad(data) - } catch (err) { - this.log.fail(err.message) - return - } - return doc -} - -/** - * Returns the filtered content of a given folder - * @param {string} folder - * @returns {Array} The content of the given folder - */ -Runner.prototype.getTest = function (folder) { - const tests = readdirSync(folder) - return tests.filter(t => !/(README|TODO)/g.test(t)) -} - -/** - * Sets the elasticsearch repository to the given sha. - * If the repository is not present in `esFolder` it will - * clone the repository and the checkout the sha. - * If the repository is already present but it cannot checkout to - * the given sha, it will perform a pull and then try again. - * @param {string} sha - * @param {function} callback - */ -Runner.prototype.withSHA = function (sha, callback) { - var fresh = false - var retry = 0 - var log = this.log - - if (!this.pathExist(esFolder)) { - if (!this.createFolder(esFolder)) { - log.fail('Failed folder creation') + parse (data) { + try { + var doc = yaml.safeLoad(data) + } catch (err) { + console.error(err) return } - fresh = true + return doc } - const git = Git(esFolder) - - if (fresh) { - clone(checkout) - } else { - checkout() + getTest (folder) { + const tests = readdirSync(folder) + return tests.filter(t => !/(README|TODO)/g.test(t)) } - function checkout () { - log.text = `Checking out sha '${sha}'` - git.checkout(sha, err => { - if (err) { - if (retry++ > 0) { - log.fail(`Cannot checkout sha '${sha}'`) - return + /** + * Sets the elasticsearch repository to the given sha. + * If the repository is not present in `esFolder` it will + * clone the repository and the checkout the sha. + * If the repository is already present but it cannot checkout to + * the given sha, it will perform a pull and then try again. + * @param {string} sha + * @param {function} callback + */ + withSHA (sha) { + return new Promise((resolve, reject) => { + _withSHA.call(this, err => err ? reject(err) : resolve()) + }) + + function _withSHA (callback) { + var fresh = false + var retry = 0 + + if (!this.pathExist(esFolder)) { + if (!this.createFolder(esFolder)) { + return callback(new Error('Failed folder creation')) } - return pull(checkout) + fresh = true } - callback() - }) - } - function pull (cb) { - log.text = 'Pulling elasticsearch repository...' - git.pull(err => { - if (err) { - log.fail(err.message) - return + const git = Git(esFolder) + + if (fresh) { + clone(checkout) + } else { + checkout() } - cb() - }) - } - function clone (cb) { - log.text = 'Cloning elasticsearch repository...' - git.clone(esRepo, esFolder, err => { - if (err) { - log.fail(err.message) - return + function checkout () { + console.log(`Checking out sha '${sha}'`) + git.checkout(sha, err => { + if (err) { + if (retry++ > 0) { + return callback(err) + } + return pull(checkout) + } + callback() + }) } - cb() - }) - } -} -/** - * Checks if the given path exists - * @param {string} path - * @returns {boolean} true if exists, false if not - */ -Runner.prototype.pathExist = function (path) { - try { - accessSync(path) - return true - } catch (err) { - return false - } -} + function pull (cb) { + console.log('Pulling elasticsearch repository...') + git.pull(err => { + if (err) { + return callback(err) + } + cb() + }) + } -/** - * Creates the given folder - * @param {string} name - * @returns {boolean} true on success, false on failure - */ -Runner.prototype.createFolder = function (name) { - try { - mkdirSync(name) - return true - } catch (err) { - return false + function clone (cb) { + console.log('Cloning elasticsearch repository...') + git.clone(esRepo, esFolder, err => { + if (err) { + return callback(err) + } + cb() + }) + } + } + } + + /** + * Checks if the given path exists + * @param {string} path + * @returns {boolean} true if exists, false if not + */ + pathExist (path) { + try { + accessSync(path) + return true + } catch (err) { + return false + } + } + + /** + * Creates the given folder + * @param {string} name + * @returns {boolean} true on success, false on failure + */ + createFolder (name) { + try { + mkdirSync(name) + return true + } catch (err) { + return false + } } } if (require.main === module) { - const url = process.env.TEST_ES_SERVER || 'http://localhost:9200' + const node = process.env.TEST_ES_SERVER || 'http://localhost:9200' const opts = { - node: url, - isPlatinum: url.indexOf('@') > -1 + node, + isXPack: node.indexOf('@') > -1 } - const runner = Runner(opts) - runner.start(opts) + const runner = new Runner(opts) + runner.start(opts).catch(console.log) +} + +const shouldSkip = (t, isXPack, file, name) => { + var list = Object.keys(ossSkips) + for (var i = 0; i < list.length; i++) { + const ossTest = ossSkips[list[i]] + for (var j = 0; j < ossTest.length; j++) { + if (file.endsWith(list[i]) && (name === ossTest[j] || ossTest[j] === '*')) { + const testName = file.slice(file.indexOf(`${sep}elasticsearch${sep}`)) + ' / ' + name + t.comment(`Skipping test ${testName} because is blacklisted in the oss test`) + return true + } + } + } + + if (file.includes('x-pack') || isXPack) { + list = Object.keys(xPackBlackList) + for (i = 0; i < list.length; i++) { + const platTest = xPackBlackList[list[i]] + for (j = 0; j < platTest.length; j++) { + if (file.endsWith(list[i]) && (name === platTest[j] || platTest[j] === '*')) { + const testName = file.slice(file.indexOf(`${sep}elasticsearch${sep}`)) + ' / ' + name + t.comment(`Skipping test ${testName} because is blacklisted in the XPack test`) + return true + } + } + } + } + + return false } const getAllFiles = dir => diff --git a/test/integration/test-runner.js b/test/integration/test-runner.js index d02495264..eed24ea95 100644 --- a/test/integration/test-runner.js +++ b/test/integration/test-runner.js @@ -19,13 +19,14 @@ 'use strict' +/* eslint camelcase: 0 */ + const t = require('tap') const semver = require('semver') -const workq = require('workq') const helper = require('./helper') const { ConfigurationError } = require('../../lib/errors') -const { delve } = helper +const { delve, to } = helper const supportedFeatures = [ 'gtelte', @@ -38,169 +39,153 @@ const supportedFeatures = [ 'catch_unauthorized' ] -function TestRunner (opts) { - if (!(this instanceof TestRunner)) { - return new TestRunner(opts) +class TestRunner { + constructor (opts = {}) { + opts = opts || {} + + this.client = opts.client + this.esVersion = opts.version + this.response = null + this.stash = new Map() + this.tap = opts.tap || t + this.isXPack = opts.isXPack } - opts = opts || {} - this.client = opts.client - this.esVersion = opts.version - this.response = null - this.stash = new Map() - this.tap = opts.tap || t - this.isPlatinum = opts.isPlatinum - this.q = opts.q || workq() -} + /** + * Runs a cleanup, removes all indices, aliases, templates, and snapshots + * @returns {Promise} + */ + async cleanup () { + this.tap.comment('Cleanup') -/** - * Runs a cleanup, removes all indices and templates - * @param {queue} - * @param {function} done - */ -TestRunner.prototype.cleanup = function (q, done) { - this.tap.comment('Cleanup') + this.response = null + this.stash = new Map() - this.response = null - this.stash = new Map() - - q.add((q, done) => { - this.client.indices.delete({ index: '*' }, { ignore: 404 }, err => { + try { + await this.client.indices.delete({ index: '_all' }, { ignore: 404 }) + } catch (err) { this.tap.error(err, 'should not error: indices.delete') - done() - }) - }) + } - q.add((q, done) => { - this.client.indices.deleteTemplate({ name: '*' }, { ignore: 404 }, err => { + try { + await this.client.indices.deleteAlias({ index: '_all', name: '_all' }, { ignore: 404 }) + } catch (err) { + this.tap.error(err, 'should not error: indices.deleteAlias') + } + + try { + const { body: templates } = await this.client.indices.getTemplate() + await helper.runInParallel( + this.client, 'indices.deleteTemplate', + Object.keys(templates).map(t => ({ name: t })) + ) + } catch (err) { this.tap.error(err, 'should not error: indices.deleteTemplate') - done() - }) - }) + } - q.add((q, done) => { - this.client.snapshot.delete({ repository: '*', snapshot: '*' }, { ignore: 404 }, err => { - this.tap.error(err, 'should not error: snapshot.delete') - done() - }) - }) + try { + const { body: repositories } = await this.client.snapshot.getRepository() + for (const repository of Object.keys(repositories)) { + const { body: snapshots } = await this.client.snapshot.get({ repository, snapshot: '_all' }) + await helper.runInParallel( + this.client, 'snapshot.delete', + Object.keys(snapshots).map(snapshot => ({ snapshot, repository })), + { ignore: [404] } + ) + await this.client.snapshot.deleteRepository({ repository }, { ignore: [404] }) + } + } catch (err) { + this.tap.error(err, 'should not error: snapshot.delete / snapshot.deleteRepository') + } + } - q.add((q, done) => { - this.client.snapshot.deleteRepository({ repository: '*' }, { ignore: 404 }, err => { - this.tap.error(err, 'should not error: snapshot.deleteRepository') - done() - }) - }) + /** + * Runs some additional API calls to prepare ES for the xpack test, + * This set of calls should be executed before the final clenup. + * @returns {Promise} + */ + async cleanupXPack () { + this.tap.comment('XPack Cleanup') - done() -} - -/** - * Runs some additional API calls to prepare ES for the Platinum test, - * This set of calls should be executed before the final clenup. - * @param {queue} - * @param {function} done - */ -TestRunner.prototype.cleanupPlatinum = function (q, done) { - this.tap.comment('Platinum Cleanup') - - q.add((q, done) => { - this.client.security.getRole((err, { body }) => { - this.tap.error(err, 'should not error: security.getRole') + try { + const { body } = await this.client.security.getRole() const roles = Object.keys(body).filter(n => helper.esDefaultRoles.indexOf(n) === -1) - helper.runInParallel( + await helper.runInParallel( this.client, 'security.deleteRole', - roles.map(r => ({ name: r, refresh: 'wait_for' })) + roles.map(r => ({ name: r })) ) - .then(() => done()) - .catch(err => this.tap.error(err, 'should not error: security.deleteRole')) - }) - }) + } catch (err) { + this.tap.error(err, 'should not error: security role cleanup') + } - q.add((q, done) => { - this.client.security.getUser((err, { body }) => { - this.tap.error(err, 'should not error: security.getUser') + try { + const { body } = await this.client.security.getUser() const users = Object.keys(body).filter(n => helper.esDefaultUsers.indexOf(n) === -1) - helper.runInParallel( + await helper.runInParallel( this.client, 'security.deleteUser', - users.map(r => ({ username: r, refresh: 'wait_for' })) + users.map(r => ({ username: r })) ) - .then(() => done()) - .catch(err => this.tap.error(err, 'should not error: security.deleteUser')) - }) - }) + } catch (err) { + this.tap.error(err, 'should not error: security user cleanup') + } - q.add((q, done) => { - this.client.security.getPrivileges((err, { body }) => { - this.tap.error(err, 'should not error: security.getPrivileges') + try { + const { body } = await this.client.security.getPrivileges() const privileges = [] Object.keys(body).forEach(app => { Object.keys(body[app]).forEach(priv => { privileges.push({ name: body[app][priv].name, - application: body[app][priv].application, - refresh: 'wait_for' + application: body[app][priv].application }) }) }) - helper.runInParallel(this.client, 'security.deletePrivileges', privileges) - .then(() => done()) - .catch(err => this.tap.error(err, 'should not error: security.deletePrivileges')) - }) - }) + await helper.runInParallel(this.client, 'security.deletePrivileges', privileges) + } catch (err) { + this.tap.error(err, 'should not error: security privileges cleanup') + } - q.add((q, done) => { - this.client.ml.stopDatafeed({ datafeedId: '*', force: true }, err => { - this.tap.error(err, 'should not error: ml.stopDatafeed') - this.client.ml.getDatafeeds({ datafeedId: '*' }, (err, { body }) => { - this.tap.error(err, 'should error: not ml.getDatafeeds') - const feeds = body.datafeeds.map(f => f.datafeed_id) - helper.runInParallel( - this.client, 'ml.deleteDatafeed', - feeds.map(f => ({ datafeedId: f })) - ) - .then(() => done()) - .catch(err => this.tap.error(err, 'should not error: ml.deleteDatafeed')) - }) - }) - }) + try { + await this.client.ml.stopDatafeed({ datafeedId: '*', force: true }) + const { body } = await this.client.ml.getDatafeeds({ datafeedId: '*' }) + const feeds = body.datafeeds.map(f => f.datafeed_id) + await helper.runInParallel( + this.client, 'ml.deleteDatafeed', + feeds.map(f => ({ datafeedId: f })) + ) + } catch (err) { + this.tap.error(err, 'should error: not ml datafeed cleanup') + } - q.add((q, done) => { - this.client.ml.closeJob({ jobId: '*', force: true }, err => { - this.tap.error(err, 'should not error: ml.closeJob') - this.client.ml.getJobs({ jobId: '*' }, (err, { body }) => { - this.tap.error(err, 'should not error: ml.getJobs') - const jobs = body.jobs.map(j => j.job_id) - helper.runInParallel( - this.client, 'ml.deleteJob', - jobs.map(j => ({ jobId: j, waitForCompletion: true, force: true })) - ) - .then(() => done()) - .catch(err => this.tap.error(err, 'should not error: ml.deleteJob')) - }) - }) - }) + try { + await this.client.ml.closeJob({ jobId: '*', force: true }) + const { body } = await this.client.ml.getJobs({ jobId: '*' }) + const jobs = body.jobs.map(j => j.job_id) + await helper.runInParallel( + this.client, 'ml.deleteJob', + jobs.map(j => ({ jobId: j, waitForCompletion: true, force: true })) + ) + } catch (err) { + this.tap.error(err, 'should not error: ml job cleanup') + } - q.add((q, done) => { - this.client.rollup.getJobs({ id: '_all' }, (err, { body }) => { - this.tap.error(err, 'should not error: rollup.getJobs') + try { + const { body } = await this.client.rollup.getJobs({ id: '_all' }) const jobs = body.jobs.map(j => j.config.id) - helper.runInParallel( + await helper.runInParallel( this.client, 'rollup.stopJob', jobs.map(j => ({ id: j, waitForCompletion: true })) ) - .then(() => helper.runInParallel( - this.client, 'rollup.deleteJob', - jobs.map(j => ({ id: j })) - )) - .then(() => done()) - .catch(err => this.tap.error(err, 'should not error: rollup.stopJob/deleteJob')) - }) - }) + await helper.runInParallel( + this.client, 'rollup.deleteJob', + jobs.map(j => ({ id: j })) + ) + } catch (err) { + this.tap.error(err, 'should not error: rollup jobs cleanup') + } - q.add((q, done) => { - this.client.tasks.list((err, { body }) => { - this.tap.error(err, 'should not error: tasks.list') + try { + const { body } = await this.client.tasks.list() const tasks = Object.keys(body.nodes) .reduce((acc, node) => { const { tasks } = body.nodes[node] @@ -210,289 +195,280 @@ TestRunner.prototype.cleanupPlatinum = function (q, done) { return acc }, []) - helper.runInParallel( + await helper.runInParallel( this.client, 'tasks.cancel', tasks.map(id => ({ taskId: id })) ) - .then(() => done()) - .catch(err => this.tap.error(err, 'should not error: tasks.cancel')) - }) - }) + } catch (err) { + this.tap.error(err, 'should not error: tasks cleanup') + } - q.add((q, done) => { - this.client.indices.delete({ index: '.ml-*' }, { ignore: 404 }, err => { - this.tap.error(err, 'should not error: indices.delete (ml indices)') - done() - }) - }) + try { + await this.client.ilm.removePolicy({ index: '_all' }) + } catch (err) { + this.tap.error(err, 'should not error: ilm.removePolicy') + } - done() -} - -/** - * Runs the given test. - * It runs the test components in the following order: - * - setup - * - the actual test - * - teardown - * - cleanup -* Internally uses a queue to guarantee the order of the test sections. - * @param {object} setup (null if not needed) - * @param {object} test - * @oaram {object} teardown (null if not needed) - * @param {function} end - */ -TestRunner.prototype.run = function (setup, test, teardown, end) { - // if we should skip a feature in the setup/teardown section - // we should skip the entire test file - const skip = getSkip(setup) || getSkip(teardown) - if (skip && this.shouldSkip(skip)) { - this.skip(skip) - return end() + // refresh the all indexes + try { + await this.client.indices.refresh({ index: '_all' }) + } catch (err) { + this.tap.error(err, 'should not error: indices.refresh') + } } - if (this.isPlatinum) { - this.tap.comment('Creating x-pack user') - // Some platinum test requires this user - this.q.add((q, done) => { - this.client.security.putUser({ - username: 'x_pack_rest_user', - body: { password: 'x-pack-test-password', roles: ['superuser'] } - }, (err, { body }) => { + /** + * Runs the given test. + * It runs the test components in the following order: + * - skip check + * - xpack user + * - setup + * - the actual test + * - teardown + * - xpack cleanup + * - cleanup + * @param {object} setup (null if not needed) + * @param {object} test + * @oaram {object} teardown (null if not needed) + * @returns {Promise} + */ + async run (setup, test, teardown) { + // if we should skip a feature in the setup/teardown section + // we should skip the entire test file + const skip = getSkip(setup) || getSkip(teardown) + if (skip && this.shouldSkip(skip)) { + this.skip(skip) + return + } + + if (this.isXPack) { + // Some xpack test requires this user + this.tap.comment('Creating x-pack user') + try { + await this.client.security.putUser({ + username: 'x_pack_rest_user', + body: { password: 'x-pack-test-password', roles: ['superuser'] } + }) + } catch (err) { this.tap.error(err, 'should not error: security.putUser') - done() - }) - }) + } + } + + if (setup) await this.exec('Setup', setup) + + await this.exec('Test', test) + + if (teardown) await this.exec('Teardown', teardown) + + if (this.isXPack) await this.cleanupXPack() + + await this.cleanup() } - if (setup) { - this.q.add((q, done) => { - this.exec('Setup', setup, q, done) - }) - } - - this.q.add((q, done) => { - this.exec('Test', test, q, done) - }) - - if (teardown) { - this.q.add((q, done) => { - this.exec('Teardown', teardown, q, done) - }) - } - - if (this.isPlatinum) { - this.q.add((q, done) => { - this.cleanupPlatinum(q, done) - }) - } - - this.q.add((q, done) => { - this.cleanup(q, done) - }) - - this.q.add((q, done) => end() && done()) -} - -/** - * Logs a skip - * @param {object} the actions - * @returns {TestRunner} - */ -TestRunner.prototype.skip = function (action) { - if (action.reason && action.version) { - this.tap.skip(`Skip: ${action.reason} (${action.version})`) - } else if (action.features) { - this.tap.skip(`Skip: ${JSON.stringify(action.features)})`) - } else { - this.tap.skip('Skipped') - } - return this -} - -/** - * Decides if a test should be skipped - * @param {object} the actions - * @returns {boolean} - */ -TestRunner.prototype.shouldSkip = function (action) { - var shouldSkip = false - // skip based on the version - if (action.version) { - if (action.version.trim() === 'all') return true - const [min, max] = action.version.split('-').map(v => v.trim()) - // if both `min` and `max` are specified - if (min && max) { - shouldSkip = semver.satisfies(this.esVersion, action.version) - // if only `min` is specified - } else if (min) { - shouldSkip = semver.gte(this.esVersion, min) - // if only `max` is specified - } else if (max) { - shouldSkip = semver.lte(this.esVersion, max) - // something went wrong! + /** + * Logs a skip + * @param {object} the actions + * @returns {TestRunner} + */ + skip (action) { + if (action.reason && action.version) { + this.tap.comment(`Skip: ${action.reason} (${action.version})`) + } else if (action.features) { + this.tap.comment(`Skip: ${JSON.stringify(action.features)})`) } else { - throw new Error(`skip: Bad version range: ${action.version}`) + this.tap.comment('Skipped') } + return this } - if (shouldSkip) return true + /** + * Decides if a test should be skipped + * @param {object} the actions + * @returns {boolean} + */ + shouldSkip (action) { + var shouldSkip = false + // skip based on the version + if (action.version) { + if (action.version.trim() === 'all') return true + const [min, max] = action.version.split('-').map(v => v.trim()) + // if both `min` and `max` are specified + if (min && max) { + shouldSkip = semver.satisfies(this.esVersion, action.version) + // if only `min` is specified + } else if (min) { + shouldSkip = semver.gte(this.esVersion, min) + // if only `max` is specified + } else if (max) { + shouldSkip = semver.lte(this.esVersion, max) + // something went wrong! + } else { + throw new Error(`skip: Bad version range: ${action.version}`) + } + } - if (action.features) { - if (!Array.isArray(action.features)) action.features = [action.features] - // returns true if one of the features is not present in the supportedFeatures - shouldSkip = !!action.features.filter(f => !~supportedFeatures.indexOf(f)).length + if (shouldSkip) return true + + if (action.features) { + if (!Array.isArray(action.features)) action.features = [action.features] + // returns true if one of the features is not present in the supportedFeatures + shouldSkip = !!action.features.filter(f => !~supportedFeatures.indexOf(f)).length + } + + if (shouldSkip) return true + + return false } - if (shouldSkip) return true + /** + * Updates the array syntax of keys and values + * eg: 'hits.hits.1.stuff' to 'hits.hits[1].stuff' + * @param {object} the action to update + * @returns {obj} the updated action + */ + updateArraySyntax (obj) { + const newObj = {} - return false -} + for (const key in obj) { + const newKey = key.replace(/\.\d{1,}\./g, v => `[${v.slice(1, -1)}].`) + const val = obj[key] -/** - * Updates the array syntax of keys and values - * eg: 'hits.hits.1.stuff' to 'hits.hits[1].stuff' - * @param {object} the action to update - * @returns {obj} the updated action - */ -TestRunner.prototype.updateArraySyntax = function (obj) { - const newObj = {} - - for (const key in obj) { - const newKey = key.replace(/\.\d{1,}\./g, v => `[${v.slice(1, -1)}].`) - const val = obj[key] - - if (typeof val === 'string') { - newObj[newKey] = val.replace(/\.\d{1,}\./g, v => `[${v.slice(1, -1)}].`) - } else if (val !== null && typeof val === 'object') { - newObj[newKey] = this.updateArraySyntax(val) - } else { - newObj[newKey] = val + if (typeof val === 'string') { + newObj[newKey] = val.replace(/\.\d{1,}\./g, v => `[${v.slice(1, -1)}].`) + } else if (val !== null && typeof val === 'object') { + newObj[newKey] = this.updateArraySyntax(val) + } else { + newObj[newKey] = val + } } + + return newObj } - return newObj -} + /** + * Fill the stashed values of a command + * let's say the we have stashed the `master` value, + * is_true: nodes.$master.transport.profiles + * becomes + * is_true: nodes.new_value.transport.profiles + * @param {object|string} the action to update + * @returns {object|string} the updated action + */ + fillStashedValues (obj) { + if (typeof obj === 'string') { + return getStashedValues.call(this, obj) + } + // iterate every key of the object + for (const key in obj) { + const val = obj[key] + // if the key value is a string, and the string includes '${' + // that we must update the content of '${...}'. + // eg: 'Basic ${auth}' we search the stahed value 'auth' + // and the resulting value will be 'Basic valueOfAuth' + if (typeof val === 'string' && val.includes('${')) { + const start = val.indexOf('${') + const end = val.indexOf('}', val.indexOf('${')) + const stashedKey = val.slice(start + 2, end) + const stashed = this.stash.get(stashedKey) + obj[key] = val.slice(0, start) + stashed + val.slice(end + 1) + continue + } + // handle json strings, eg: '{"hello":"$world"}' + if (typeof val === 'string' && val.includes('"$')) { + const start = val.indexOf('"$') + const end = val.indexOf('"', start + 1) + const stashedKey = val.slice(start + 2, end) + const stashed = '"' + this.stash.get(stashedKey) + '"' + obj[key] = val.slice(0, start) + stashed + val.slice(end + 1) + continue + } + // if the key value is a string, and the string includes '$' + // we run the "update value" code + if (typeof val === 'string' && val.includes('$')) { + // update the key value + obj[key] = getStashedValues.call(this, val) + continue + } -/** - * Fill the stashed values of a command - * let's say the we have stashed the `master` value, - * is_true: nodes.$master.transport.profiles - * becomes - * is_true: nodes.new_value.transport.profiles - * @param {object|string} the action to update - * @returns {object|string} the updated action - */ -TestRunner.prototype.fillStashedValues = function (obj) { - if (typeof obj === 'string') { - return getStashedValues.call(this, obj) - } - // iterate every key of the object - for (const key in obj) { - const val = obj[key] - // if the key value is a string, and the string includes '${' - // that we must update the content of '${...}'. - // eg: 'Basic ${auth}' we search the stahed value 'auth' - // and the resulting value will be 'Basic valueOfAuth' - if (typeof val === 'string' && val.includes('${')) { - const start = val.indexOf('${') - const end = val.indexOf('}', val.indexOf('${')) - const stashedKey = val.slice(start + 2, end) - const stashed = this.stash.get(stashedKey) - obj[key] = val.slice(0, start) + stashed + val.slice(end + 1) - continue - } - // handle json strings, eg: '{"hello":"$world"}' - if (typeof val === 'string' && val.includes('"$')) { - const start = val.indexOf('"$') - const end = val.indexOf('"', start + 1) - const stashedKey = val.slice(start + 2, end) - const stashed = '"' + this.stash.get(stashedKey) + '"' - obj[key] = val.slice(0, start) + stashed + val.slice(end + 1) - continue - } - // if the key value is a string, and the string includes '$' - // we run the "update value" code - if (typeof val === 'string' && val.includes('$')) { - // update the key value - obj[key] = getStashedValues.call(this, val) - continue + // go deep in the object + if (val !== null && typeof val === 'object') { + this.fillStashedValues(val) + } } - // go deep in the object - if (val !== null && typeof val === 'object') { - this.fillStashedValues(val) - } - } + return obj - return obj - - function getStashedValues (str) { - const arr = str - // we split the string on the dots - // handle the key with a dot inside that is not a part of the path - .split(/(? { - if (part[0] === '$') { - const stashed = this.stash.get(part.slice(1)) - if (stashed == null) { - throw new Error(`Cannot find stashed value '${part}' for '${JSON.stringify(obj)}'`) + function getStashedValues (str) { + const arr = str + // we split the string on the dots + // handle the key with a dot inside that is not a part of the path + .split(/(? { + if (part[0] === '$') { + const stashed = this.stash.get(part.slice(1)) + if (stashed == null) { + throw new Error(`Cannot find stashed value '${part}' for '${JSON.stringify(obj)}'`) + } + return stashed } - return stashed - } - return part - }) + return part + }) - // recreate the string value only if the array length is higher than one - // otherwise return the first element which in some test this could be a number, - // and call `.join` will coerce it to a string. - return arr.length > 1 ? arr.join('.') : arr[0] + // recreate the string value only if the array length is higher than one + // otherwise return the first element which in some test this could be a number, + // and call `.join` will coerce it to a string. + return arr.length > 1 ? arr.join('.') : arr[0] + } } -} -/** - * Stashes a value - * @param {string} the key to search in the previous response - * @param {string} the name to identify the stashed value - * @returns {TestRunner} - */ -TestRunner.prototype.set = function (key, name) { - this.stash.set(name, delve(this.response, key)) - return this -} - -/** - * Applies a given transformation and stashes the result. - * @param {string} the name to identify the stashed value - * @param {string} the transformation function as string - * @returns {TestRunner} - */ -TestRunner.prototype.transform_and_set = function (name, transform) { - if (/base64EncodeCredentials/.test(transform)) { - const [user, password] = transform - .slice(transform.indexOf('(') + 1, -1) - .replace(/ /g, '') - .split(',') - const userAndPassword = `${delve(this.response, user)}:${delve(this.response, password)}` - this.stash.set(name, Buffer.from(userAndPassword).toString('base64')) - } else { - throw new Error(`Unknown transform: '${transform}'`) + /** + * Stashes a value + * @param {string} the key to search in the previous response + * @param {string} the name to identify the stashed value + * @returns {TestRunner} + */ + set (key, name) { + this.stash.set(name, delve(this.response, key)) + return this } - return this -} -/** - * Runs a client command - * @param {object} the action to perform - * @param {Queue} - */ -TestRunner.prototype.do = function (action, done) { - const cmd = this.parseDo(action) - const api = delve(this.client, cmd.method).bind(this.client) - const options = { ignore: cmd.params.ignore, headers: action.headers } - if (cmd.params.ignore) delete cmd.params.ignore - api(cmd.params, options, (err, { body, warnings }) => { + /** + * Applies a given transformation and stashes the result. + * @param {string} the name to identify the stashed value + * @param {string} the transformation function as string + * @returns {TestRunner} + */ + transform_and_set (name, transform) { + if (/base64EncodeCredentials/.test(transform)) { + const [user, password] = transform + .slice(transform.indexOf('(') + 1, -1) + .replace(/ /g, '') + .split(',') + const userAndPassword = `${delve(this.response, user)}:${delve(this.response, password)}` + this.stash.set(name, Buffer.from(userAndPassword).toString('base64')) + } else { + throw new Error(`Unknown transform: '${transform}'`) + } + return this + } + + /** + * Runs a client command + * @param {object} the action to perform + * @returns {Promise} + */ + async do (action) { + const cmd = this.parseDo(action) + const api = delve(this.client, cmd.method).bind(this.client) + + const options = { ignore: cmd.params.ignore, headers: action.headers } + if (cmd.params.ignore) delete cmd.params.ignore + + const [err, result] = await to(api(cmd.params, options)) + var warnings = result ? result.warnings : null + var body = result ? result.body : null + if (action.warnings && warnings === null) { this.tap.fail('We should get a warning header', action.warnings) } else if (!action.warnings && warnings !== null) { @@ -542,53 +518,39 @@ TestRunner.prototype.do = function (action, done) { this.tap.error(err, `should not error: ${cmd.method}`, action) this.response = body } + } - done() - }) -} - -/** - * Runs an actual test - * @param {string} the name of the test - * @param {object} the actions to perform - * @param {Queue} - */ -TestRunner.prototype.exec = function (name, actions, q, done) { - this.tap.comment(name) - for (var i = 0; i < actions.length; i++) { - const action = actions[i] - - if (action.skip) { - if (this.shouldSkip(action.skip)) { - this.skip(this.fillStashedValues(action.skip)) - break + /** + * Runs an actual test + * @param {string} the name of the test + * @param {object} the actions to perform + * @returns {Promise} + */ + async exec (name, actions) { + this.tap.comment(name) + for (const action of actions) { + if (action.skip) { + if (this.shouldSkip(action.skip)) { + this.skip(this.fillStashedValues(action.skip)) + break + } } - } - if (action.do) { - q.add((q, done) => { - this.do(this.fillStashedValues(action.do), done) - }) - } + if (action.do) { + await this.do(this.fillStashedValues(action.do)) + } - if (action.set) { - q.add((q, done) => { + if (action.set) { const key = Object.keys(action.set)[0] this.set(this.fillStashedValues(key), action.set[key]) - done() - }) - } + } - if (action.transform_and_set) { - q.add((q, done) => { + if (action.transform_and_set) { const key = Object.keys(action.transform_and_set)[0] this.transform_and_set(key, action.transform_and_set[key]) - done() - }) - } + } - if (action.match) { - q.add((q, done) => { + if (action.match) { const key = Object.keys(action.match)[0] this.match( // in some cases, the yaml refers to the body with an empty string @@ -600,56 +562,41 @@ TestRunner.prototype.exec = function (name, actions, q, done) { : this.fillStashedValues(action.match)[key], action.match ) - done() - }) - } + } - if (action.lt) { - q.add((q, done) => { + if (action.lt) { const key = Object.keys(action.lt)[0] this.lt( delve(this.response, this.fillStashedValues(key)), this.fillStashedValues(action.lt)[key] ) - done() - }) - } + } - if (action.gt) { - q.add((q, done) => { + if (action.gt) { const key = Object.keys(action.gt)[0] this.gt( delve(this.response, this.fillStashedValues(key)), this.fillStashedValues(action.gt)[key] ) - done() - }) - } + } - if (action.lte) { - q.add((q, done) => { + if (action.lte) { const key = Object.keys(action.lte)[0] this.lte( delve(this.response, this.fillStashedValues(key)), this.fillStashedValues(action.lte)[key] ) - done() - }) - } + } - if (action.gte) { - q.add((q, done) => { + if (action.gte) { const key = Object.keys(action.gte)[0] this.gte( delve(this.response, this.fillStashedValues(key)), this.fillStashedValues(action.gte)[key] ) - done() - }) - } + } - if (action.length) { - q.add((q, done) => { + if (action.length) { const key = Object.keys(action.length)[0] this.length( key === '$body' || key === '' @@ -659,233 +606,227 @@ TestRunner.prototype.exec = function (name, actions, q, done) { ? action.length[key] : this.fillStashedValues(action.length)[key] ) - done() - }) - } + } - if (action.is_true) { - q.add((q, done) => { + if (action.is_true) { const isTrue = this.fillStashedValues(action.is_true) this.is_true( delve(this.response, isTrue), isTrue ) - done() - }) - } + } - if (action.is_false) { - q.add((q, done) => { + if (action.is_false) { const isFalse = this.fillStashedValues(action.is_false) this.is_false( delve(this.response, isFalse), isFalse ) - done() - }) - } - } - done() -} - -/** - * Asserts that the given value is truthy - * @param {any} the value to check - * @param {string} an optional message - */ -TestRunner.prototype.is_true = function (val, msg) { - this.tap.true(val, `expect truthy value: ${msg} - value: ${JSON.stringify(val)}`) - return this -} - -/** - * Asserts that the given value is falsey - * @param {any} the value to check - * @param {string} an optional message - */ -TestRunner.prototype.is_false = function (val, msg) { - this.tap.false(val, `expect falsey value: ${msg} - value: ${JSON.stringify(val)}`) - return this -} - -/** - * Asserts that two values are the same - * @param {any} the first value - * @param {any} the second value - * @returns {TestRunner} - */ -TestRunner.prototype.match = function (val1, val2, action) { - // both values are objects - if (typeof val1 === 'object' && typeof val2 === 'object') { - this.tap.strictDeepEqual(val1, val2, action) - // the first value is the body as string and the second a pattern string - } else if ( - typeof val1 === 'string' && typeof val2 === 'string' && - val2.startsWith('/') && (val2.endsWith('/\n') || val2.endsWith('/')) - ) { - const regStr = val2 - // match all comments within a "regexp" match arg - .replace(/([\S\s]?)#[^\n]*\n/g, (match, prevChar) => { - return prevChar === '\\' ? match : `${prevChar}\n` - }) - // remove all whitespace from the expression, all meaningful - // whitespace is represented with \s - .replace(/\s/g, '') - .slice(1, -1) - // 'm' adds the support for multiline regex - this.tap.match(val1, new RegExp(regStr, 'm'), `should match pattern provided: ${val2}, action: ${JSON.stringify(action)}`) - // everything else - } else { - this.tap.strictEqual(val1, val2, `should be equal: ${val1} - ${val2}, action: ${JSON.stringify(action)}`) - } - return this -} - -/** - * Asserts that the first value is less than the second - * It also verifies that the two values are numbers - * @param {any} the first value - * @param {any} the second value - * @returns {TestRunner} - */ -TestRunner.prototype.lt = function (val1, val2) { - ;[val1, val2] = getNumbers(val1, val2) - this.tap.true(val1 < val2) - return this -} - -/** - * Asserts that the first value is greater than the second - * It also verifies that the two values are numbers - * @param {any} the first value - * @param {any} the second value - * @returns {TestRunner} - */ -TestRunner.prototype.gt = function (val1, val2) { - ;[val1, val2] = getNumbers(val1, val2) - this.tap.true(val1 > val2) - return this -} - -/** - * Asserts that the first value is less than or equal the second - * It also verifies that the two values are numbers - * @param {any} the first value - * @param {any} the second value - * @returns {TestRunner} - */ -TestRunner.prototype.lte = function (val1, val2) { - ;[val1, val2] = getNumbers(val1, val2) - this.tap.true(val1 <= val2) - return this -} - -/** - * Asserts that the first value is greater than or equal the second - * It also verifies that the two values are numbers - * @param {any} the first value - * @param {any} the second value - * @returns {TestRunner} -*/ -TestRunner.prototype.gte = function (val1, val2) { - ;[val1, val2] = getNumbers(val1, val2) - this.tap.true(val1 >= val2) - return this -} - -/** - * Asserts that the given value has the specified length - * @param {string|object|array} the object to check - * @param {number} the expected length - * @returns {TestRunner} - */ -TestRunner.prototype.length = function (val, len) { - if (typeof val === 'string' || Array.isArray(val)) { - this.tap.strictEqual(val.length, len) - } else if (typeof val === 'object' && val !== null) { - this.tap.strictEqual(Object.keys(val).length, len) - } else { - this.tap.fail(`length: the given value is invalid: ${val}`) - } - return this -} - -/** - * Gets a `do` action object and returns a structured object, - * where the action is the key and the parameter is the value. - * Eg: - * { - * 'indices.create': { - * 'index': 'test' - * }, - * 'warnings': [ - * '[index] is deprecated' - * ] - * } - * becomes - * { - * method: 'indices.create', - * params: { - * index: 'test' - * }, - * warnings: [ - * '[index] is deprecated' - * ] - * } - * @param {object} - * @returns {object} - */ -TestRunner.prototype.parseDo = function (action) { - return Object.keys(action).reduce((acc, val) => { - switch (val) { - case 'catch': - acc.catch = action.catch - break - case 'warnings': - acc.warnings = action.warnings - break - case 'node_selector': - acc.node_selector = action.node_selector - break - default: - // converts underscore to camelCase - // eg: put_mapping => putMapping - acc.method = val.replace(/_([a-z])/g, g => g[1].toUpperCase()) - acc.params = camelify(action[val]) - } - return acc - }, {}) - - function camelify (obj) { - const newObj = {} - - // TODO: add camelCase support for this fields - const doNotCamelify = ['copy_settings'] - - for (const key in obj) { - const val = obj[key] - var newKey = key - if (!~doNotCamelify.indexOf(key)) { - // if the key starts with `_` we should not camelify the first occurence - // eg: _source_include => _sourceInclude - newKey = key[0] === '_' - ? '_' + key.slice(1).replace(/_([a-z])/g, k => k[1].toUpperCase()) - : key.replace(/_([a-z])/g, k => k[1].toUpperCase()) - } - - if ( - val !== null && - typeof val === 'object' && - !Array.isArray(val) && - key !== 'body' - ) { - newObj[newKey] = camelify(val) - } else { - newObj[newKey] = val } } + } - return newObj + /** + * Asserts that the given value is truthy + * @param {any} the value to check + * @param {string} an optional message + * @returns {TestRunner} + */ + is_true (val, msg) { + this.tap.true(val, `expect truthy value: ${msg} - value: ${JSON.stringify(val)}`) + return this + } + + /** + * Asserts that the given value is falsey + * @param {any} the value to check + * @param {string} an optional message + * @returns {TestRunner} + */ + is_false (val, msg) { + this.tap.false(val, `expect falsey value: ${msg} - value: ${JSON.stringify(val)}`) + return this + } + + /** + * Asserts that two values are the same + * @param {any} the first value + * @param {any} the second value + * @returns {TestRunner} + */ + match (val1, val2, action) { + // both values are objects + if (typeof val1 === 'object' && typeof val2 === 'object') { + this.tap.strictDeepEqual(val1, val2, action) + // the first value is the body as string and the second a pattern string + } else if ( + typeof val1 === 'string' && typeof val2 === 'string' && + val2.startsWith('/') && (val2.endsWith('/\n') || val2.endsWith('/')) + ) { + const regStr = val2 + // match all comments within a "regexp" match arg + .replace(/([\S\s]?)#[^\n]*\n/g, (match, prevChar) => { + return prevChar === '\\' ? match : `${prevChar}\n` + }) + // remove all whitespace from the expression, all meaningful + // whitespace is represented with \s + .replace(/\s/g, '') + .slice(1, -1) + // 'm' adds the support for multiline regex + this.tap.match(val1, new RegExp(regStr, 'm'), `should match pattern provided: ${val2}, action: ${JSON.stringify(action)}`) + // everything else + } else { + this.tap.strictEqual(val1, val2, `should be equal: ${val1} - ${val2}, action: ${JSON.stringify(action)}`) + } + return this + } + + /** + * Asserts that the first value is less than the second + * It also verifies that the two values are numbers + * @param {any} the first value + * @param {any} the second value + * @returns {TestRunner} + */ + lt (val1, val2) { + ;[val1, val2] = getNumbers(val1, val2) + this.tap.true(val1 < val2) + return this + } + + /** + * Asserts that the first value is greater than the second + * It also verifies that the two values are numbers + * @param {any} the first value + * @param {any} the second value + * @returns {TestRunner} + */ + gt (val1, val2) { + ;[val1, val2] = getNumbers(val1, val2) + this.tap.true(val1 > val2) + return this + } + + /** + * Asserts that the first value is less than or equal the second + * It also verifies that the two values are numbers + * @param {any} the first value + * @param {any} the second value + * @returns {TestRunner} + */ + lte (val1, val2) { + ;[val1, val2] = getNumbers(val1, val2) + this.tap.true(val1 <= val2) + return this + } + + /** + * Asserts that the first value is greater than or equal the second + * It also verifies that the two values are numbers + * @param {any} the first value + * @param {any} the second value + * @returns {TestRunner} + */ + gte (val1, val2) { + ;[val1, val2] = getNumbers(val1, val2) + this.tap.true(val1 >= val2) + return this + } + + /** + * Asserts that the given value has the specified length + * @param {string|object|array} the object to check + * @param {number} the expected length + * @returns {TestRunner} + */ + length (val, len) { + if (typeof val === 'string' || Array.isArray(val)) { + this.tap.strictEqual(val.length, len) + } else if (typeof val === 'object' && val !== null) { + this.tap.strictEqual(Object.keys(val).length, len) + } else { + this.tap.fail(`length: the given value is invalid: ${val}`) + } + return this + } + + /** + * Gets a `do` action object and returns a structured object, + * where the action is the key and the parameter is the value. + * Eg: + * { + * 'indices.create': { + * 'index': 'test' + * }, + * 'warnings': [ + * '[index] is deprecated' + * ] + * } + * becomes + * { + * method: 'indices.create', + * params: { + * index: 'test' + * }, + * warnings: [ + * '[index] is deprecated' + * ] + * } + * @param {object} + * @returns {object} + */ + parseDo (action) { + return Object.keys(action).reduce((acc, val) => { + switch (val) { + case 'catch': + acc.catch = action.catch + break + case 'warnings': + acc.warnings = action.warnings + break + case 'node_selector': + acc.node_selector = action.node_selector + break + default: + // converts underscore to camelCase + // eg: put_mapping => putMapping + acc.method = val.replace(/_([a-z])/g, g => g[1].toUpperCase()) + acc.params = camelify(action[val]) + } + return acc + }, {}) + + function camelify (obj) { + const newObj = {} + + // TODO: add camelCase support for this fields + const doNotCamelify = ['copy_settings'] + + for (const key in obj) { + const val = obj[key] + var newKey = key + if (!~doNotCamelify.indexOf(key)) { + // if the key starts with `_` we should not camelify the first occurence + // eg: _source_include => _sourceInclude + newKey = key[0] === '_' + ? '_' + key.slice(1).replace(/_([a-z])/g, k => k[1].toUpperCase()) + : key.replace(/_([a-z])/g, k => k[1].toUpperCase()) + } + + if ( + val !== null && + typeof val === 'object' && + !Array.isArray(val) && + key !== 'body' + ) { + newObj[newKey] = camelify(val) + } else { + newObj[newKey] = val + } + } + + return newObj + } } }