UNPKG

lab

Version:
521 lines (448 loc) 15.3 kB
'use strict'; // Load modules const Fs = require('fs'); const Path = require('path'); const Bossy = require('bossy'); const FindRc = require('find-rc'); const Hoek = require('hoek'); const Coverage = require('./coverage'); const Pkg = require('../package.json'); const Runner = require('./runner'); const Transform = require('./transform'); const Utils = require('./utils'); // .labrc configuration will be required if it exists // index.js required below if setting assertions module // Declare internals const internals = {}; internals.rcPath = FindRc('lab'); internals.rc = internals.rcPath ? require(internals.rcPath) : {}; exports.run = function () { const settings = internals.options(); settings.coveragePath = Path.join(process.cwd(), settings['coverage-path'] || ''); settings.coverageExclude = ['node_modules', 'test', 'test_runner']; if (settings['coverage-exclude']) { settings.coverageExclude = settings.coverageExclude.concat(settings['coverage-exclude']); } settings.lintingPath = process.cwd(); if (settings.coverage) { Coverage.instrument(settings); } else if (settings.transform) { Transform.install(settings); } if (settings.environment) { process.env.NODE_ENV = settings.environment; } if (settings.sourcemaps) { let sourceMapOptions = {}; if (settings.transform) { sourceMapOptions = { retrieveFile: Transform.retrieveFile }; } require('source-map-support').install(sourceMapOptions); } const scripts = internals.traverse(settings.paths, settings); return Runner.report(scripts, settings); }; internals.traverse = function (paths, options) { let nextPath = null; const traverse = function (path) { let files = []; nextPath = path; const pathStat = Fs.statSync(path); if (pathStat.isFile()) { return path; } Fs.readdirSync(path).forEach((filename) => { nextPath = Path.join(path, filename); const stat = Fs.statSync(nextPath); if (stat.isDirectory() && !options.flat) { files = files.concat(traverse(nextPath, options)); return; } if (stat.isFile() && options.pattern.test(filename) && Path.basename(nextPath)[0] !== '.') { files.push(nextPath); } }); return files; }; let testFiles = []; try { paths.forEach((path) => { testFiles = testFiles.concat(traverse(path)); }); } catch (err) { if (err.code !== 'ENOENT') { throw err; } console.error('Could not find test file or directory \'' + nextPath + '\'.'); process.exit(1); } if (options.pattern && !testFiles.length) { console.log('The pattern provided (-P or --pattern) didn\'t match any files.'); process.exit(0); } testFiles = testFiles.map((path) => { return Path.resolve(path); }); const scripts = []; testFiles.forEach((file) => { global._labScriptRun = false; file = Path.resolve(file); try { require(file); } catch (ex) { console.error(`Error requiring file: ${file}`); console.error(`${ex.message}`); console.error(`${ex.stack}`); return process.exit(1); } const pkg = require(file); if (pkg.lab && pkg.lab._root) { scripts.push(pkg.lab); if (pkg.lab._cli) { Utils.applyOptions(options, pkg.lab._cli); } } else if (global._labScriptRun) { console.error(`The file: ${file} includes a lab script that is not exported via exports.lab`); return process.exit(1); } }); return scripts; }; internals.options = function () { const definition = { assert: { alias: 'a', type: 'string', description: 'specify an assertion library module path to require and make available under Lab.assertions', default: null }, bail: { type: 'boolean', description: 'exit the process with a non zero exit code on the first test failure', default: null }, colors: { alias: 'C', type: 'boolean', description: 'enable color output (defaults to terminal capabilities)', default: null }, 'context-timeout': { alias: 'M', type: 'number', description: 'timeout for before, after, beforeEach, afterEach in milliseconds', default: null }, coverage: { alias: 'c', type: 'boolean', description: 'enable code coverage analysis', default: null }, 'coverage-path': { type: 'string', description: 'set code coverage path', default: null }, 'coverage-exclude': { type: 'string', description: 'set code coverage excludes', multiple: true, default: null }, 'coverage-all': { type: 'boolean', description: 'include all files in coveragePath in report', default: null }, 'coverage-flat': { type: 'boolean', description: 'prevent recursive inclusion of all files in coveragePath in report', default: null }, 'coverage-pattern': { type: 'string', description: 'file pattern to use for locating files for coverage', default: null }, 'default-plan-threshold': { alias: 'p', type: 'number', description: 'minimum plan threshold to apply to all tests that don\'t define any plan', default: null }, dry: { alias: 'd', type: 'boolean', description: 'skip all tests (dry run)', default: null }, environment: { alias: 'e', type: 'string', description: 'value to set NODE_ENV before tests', default: null }, flat: { alias: 'f', type: 'boolean', description: 'prevent recursive collection of tests within the provided path', default: null }, globals: { alias: ['I', 'ignore'], type: 'string', description: 'ignore a list of globals for the leak detection (comma separated)', default: null }, grep: { alias: 'g', type: 'string', description: 'only run tests matching the given pattern which is internally compiled to a RegExp', default: null }, help: { alias: 'h', type: 'boolean', description: 'display usage options', default: null }, id: { alias: 'i', type: 'range', description: 'test identifier', default: null }, inspect: { type: 'boolean', description: 'starts lab with the node.js native debugger', default: null }, leaks: { alias: 'l', type: 'boolean', description: 'disable global variable leaks detection', default: null }, lint: { alias: 'L', type: 'boolean', description: 'enable linting', default: null }, linter: { alias: 'n', type: 'string', description: 'linter path to use', default: null }, 'lint-fix': { type: 'boolean', description: 'apply any fixes from the linter.', default: null }, 'lint-options': { type: 'string', description: 'specify options to pass to linting program. It must be a string that is JSON.parse(able).', default: null }, 'lint-errors-threshold': { type: 'number', description: 'linter errors threshold in absolute value', default: null }, 'lint-warnings-threshold': { type: 'number', description: 'linter warnings threshold in absolute value', default: null }, output: { alias: 'o', type: 'string', description: 'file path to write test results', multiple: true, default: null }, pattern: { alias: 'P', type: 'string', description: 'file pattern to use for locating tests', default: null }, reporter: { alias: 'r', type: 'string', description: 'reporter type [console, html, json, tap, lcov, clover, junit]', multiple: true, default: null }, seed: { type: 'string', description: 'use this seed to randomize the order with `--shuffle`. This is useful to debug order dependent test failures', default: null }, shuffle: { type: 'boolean', description: 'shuffle script execution order', default: null }, silence: { alias: 's', type: 'boolean', description: 'silence test output', default: null }, 'silent-skips': { alias: 'k', type: 'boolean', description: 'don’t output skipped tests', default: null }, sourcemaps: { alias: ['S', 'sourcemaps'], type: 'boolean', description: 'enable support for sourcemaps', default: null }, threshold: { alias: 't', type: 'number', description: 'code coverage threshold percentage', default: null }, timeout: { alias: 'm', type: 'number', description: 'timeout for each test in milliseconds', default: null }, transform: { alias: ['T', 'transform'], type: 'string', description: 'javascript file that exports an array of objects ie. [ { ext: ".js", transform: function (content, filename) { ... } } ]', default: null }, verbose: { alias: 'v', type: 'boolean', description: 'verbose test output', default: null }, version: { alias: 'V', type: 'boolean', description: 'version information', default: null } }; const defaults = { bail: false, coverage: false, dry: false, environment: 'test', flat: false, leaks: true, lint: false, linter: 'eslint', 'lint-fix': false, 'lint-errors-threshold': 0, 'lint-warnings-threshold': 0, paths: ['test'], reporter: 'console', shuffle: false, silence: false, 'silent-skips': false, sourcemaps: false, 'context-timeout': 0, timeout: 2000, verbose: false }; const argv = Bossy.parse(definition); if (argv instanceof Error) { console.error(Bossy.usage(definition, 'lab [options] [path]')); console.error('\n' + argv.message); process.exit(1); } if (argv.help) { console.log(Bossy.usage(definition, 'lab [options] [path]')); process.exit(0); } if (argv.version) { console.log(Pkg.version); process.exit(0); } const options = Utils.mergeOptions(defaults, internals.rc); options.paths = argv._ ? [].concat(argv._) : options.paths; const keys = ['assert', 'bail', 'colors', 'context-timeout', 'coverage', 'coverage-exclude', 'coverage-path', 'coverage-all', 'coverage-flat', 'coverage-pattern', 'default-plan-threshold', 'dry', 'environment', 'flat', 'globals', 'grep', 'lint', 'lint-errors-threshold', 'lint-fix', 'lint-options', 'lint-warnings-threshold', 'linter', 'output', 'pattern', 'reporter', 'seed', 'shuffle', 'silence', 'silent-skips', 'sourcemaps', 'threshold', 'timeout', 'transform', 'verbose']; for (let i = 0; i < keys.length; ++i) { if (argv.hasOwnProperty(keys[i]) && argv[keys[i]] !== undefined && argv[keys[i]] !== null) { options[keys[i]] = argv[keys[i]]; } } if (typeof argv.leaks === 'boolean') { options.leaks = !argv.leaks; } if (argv.id) { options.ids = argv.id; } if (Array.isArray(options.reporter) && options.output) { if (!Array.isArray(options.output) || options.output.length !== options.reporter.length) { console.error(Bossy.usage(definition, 'lab [options] [path]')); process.exit(1); } } if (!options.output) { options.output = process.stdout; } if (options.assert) { options.assert = require(options.assert); require('./').assertions = options.assert; } if (options.globals) { options.globals = options.globals.trim().split(','); } if (options.silence) { options.progress = 0; } else if (options.verbose) { options.progress = 2; } const pattern = options.pattern ? '.*' + options.pattern + '.*?' : ''; let exts = '\\.(js)$'; if (options.transform) { const transform = require(Path.resolve(options.transform)); Hoek.assert(Array.isArray(transform) && transform.length > 0, 'transform module must export an array of objects {ext: ".js", transform: null or function (content, filename)}'); options.transform = transform; const includes = 'js|' + transform.map(internals.mapTransform).join('|'); exts = '\\.(' + includes + ')$'; } options.pattern = new RegExp(pattern + exts); options.coverage = (options.coverage || options.threshold > 0 || options['coverage-all'] || options.reporter.indexOf('html') !== -1 || options.reporter.indexOf('lcov') !== -1 || options.reporter.indexOf('clover') !== -1); options.coveragePattern = new RegExp(options['coverage-pattern'] || pattern + exts); if (options['coverage-pattern'] && !options['coverage-all']) { console.error('The "coverage-pattern" option can only be used with "coverage-all"'); process.exit(1); } if (options['coverage-flat'] && !options['coverage-all']) { console.error('The "coverage-flat" option can only be used with "coverage-all"'); process.exit(1); } return options; }; internals.mapTransform = function (transform) { return transform.ext.substr(1).replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&'); };