1 | 'use strict';
|
2 |
|
3 | const Fs = require('fs');
|
4 | const Path = require('path');
|
5 |
|
6 | const Bossy = require('@hapi/bossy');
|
7 | const FindRc = require('find-rc');
|
8 | const Hoek = require('@hapi/hoek');
|
9 |
|
10 | const Coverage = require('./coverage');
|
11 | const Pkg = require('../package.json');
|
12 | const Runner = require('./runner');
|
13 | const Transform = require('./transform');
|
14 | const Utils = require('./utils');
|
15 |
|
16 |
|
17 |
|
18 |
|
19 | const internals = {};
|
20 |
|
21 |
|
22 | internals.rcPath = FindRc('lab');
|
23 | internals.rc = internals.rcPath ? require(internals.rcPath) : {};
|
24 |
|
25 |
|
26 | exports.run = function () {
|
27 |
|
28 | const settings = internals.options();
|
29 |
|
30 | settings.coveragePath = Path.join(process.cwd(), settings['coverage-path'] || '');
|
31 | settings.coverageExclude = ['node_modules', 'test', 'test_runner'];
|
32 | if (settings['coverage-exclude']) {
|
33 | settings.coverageExclude = settings.coverageExclude.concat(settings['coverage-exclude']);
|
34 | }
|
35 |
|
36 | settings.lintingPath = process.cwd();
|
37 |
|
38 | if (settings.coverage) {
|
39 | Coverage.instrument(settings);
|
40 | }
|
41 | else if (settings.transform) {
|
42 | Transform.install(settings);
|
43 | }
|
44 |
|
45 | if (settings.environment) {
|
46 | process.env.NODE_ENV = settings.environment;
|
47 | }
|
48 |
|
49 | if (settings.sourcemaps) {
|
50 | let sourceMapOptions = {};
|
51 |
|
52 | if (settings.transform) {
|
53 | sourceMapOptions = {
|
54 | retrieveFile: Transform.retrieveFile
|
55 | };
|
56 | }
|
57 |
|
58 | require('source-map-support').install(sourceMapOptions);
|
59 | }
|
60 |
|
61 | const scripts = internals.traverse(settings.paths, settings);
|
62 | return Runner.report(scripts, settings);
|
63 | };
|
64 |
|
65 |
|
66 | internals.traverse = function (paths, options) {
|
67 |
|
68 | let nextPath = null;
|
69 | const traverse = function (path) {
|
70 |
|
71 | let files = [];
|
72 | nextPath = path;
|
73 |
|
74 | const pathStat = Fs.statSync(path);
|
75 | if (pathStat.isFile()) {
|
76 | return path;
|
77 | }
|
78 |
|
79 | Fs.readdirSync(path).forEach((filename) => {
|
80 |
|
81 | nextPath = Path.join(path, filename);
|
82 | const stat = Fs.statSync(nextPath);
|
83 | if (stat.isDirectory() &&
|
84 | !options.flat) {
|
85 |
|
86 | files = files.concat(traverse(nextPath, options));
|
87 | return;
|
88 | }
|
89 |
|
90 | if (stat.isFile() &&
|
91 | options.pattern.test(filename) &&
|
92 | Path.basename(nextPath)[0] !== '.') {
|
93 |
|
94 | files.push(nextPath);
|
95 | }
|
96 | });
|
97 |
|
98 | return files;
|
99 | };
|
100 |
|
101 | let testFiles = [];
|
102 | try {
|
103 | paths.forEach((path) => {
|
104 |
|
105 | testFiles = testFiles.concat(traverse(path));
|
106 | });
|
107 | }
|
108 | catch (err) {
|
109 | if (err.code !== 'ENOENT') {
|
110 | throw err;
|
111 | }
|
112 |
|
113 | console.error('Could not find test file or directory \'' + nextPath + '\'.');
|
114 | process.exit(1);
|
115 | }
|
116 |
|
117 | if (options.pattern && !testFiles.length) {
|
118 | console.log('The pattern provided (-P or --pattern) didn\'t match any files.');
|
119 | process.exit(0);
|
120 | }
|
121 |
|
122 | testFiles = testFiles.map((path) => {
|
123 |
|
124 | return Path.resolve(path);
|
125 | });
|
126 |
|
127 | const scripts = [];
|
128 | testFiles.forEach((file) => {
|
129 |
|
130 | global._labScriptRun = false;
|
131 | file = Path.resolve(file);
|
132 |
|
133 | try {
|
134 | require(file);
|
135 | }
|
136 | catch (ex) {
|
137 | console.error(`Error requiring file: ${file}`);
|
138 | console.error(`${ex.message}`);
|
139 | console.error(`${ex.stack}`);
|
140 | return process.exit(1);
|
141 | }
|
142 |
|
143 | const pkg = require(file);
|
144 |
|
145 | if (pkg.lab &&
|
146 | pkg.lab._root) {
|
147 |
|
148 | scripts.push(pkg.lab);
|
149 |
|
150 | if (pkg.lab._cli) {
|
151 | Utils.applyOptions(options, pkg.lab._cli);
|
152 | }
|
153 | }
|
154 | else if (global._labScriptRun) {
|
155 | console.error(`The file: ${file} includes a lab script that is not exported via exports.lab`);
|
156 | return process.exit(1);
|
157 | }
|
158 | });
|
159 |
|
160 | return scripts;
|
161 | };
|
162 |
|
163 |
|
164 | internals.options = function () {
|
165 |
|
166 | const definition = {
|
167 | assert: {
|
168 | alias: 'a',
|
169 | type: 'string',
|
170 | description: 'specify an assertion library module path to require and make available under Lab.assertions',
|
171 | default: null
|
172 | },
|
173 | bail: {
|
174 | type: 'boolean',
|
175 | description: 'exit the process with a non zero exit code on the first test failure',
|
176 | default: null
|
177 | },
|
178 | colors: {
|
179 | alias: 'C',
|
180 | type: 'boolean',
|
181 | description: 'enable color output (defaults to terminal capabilities)',
|
182 | default: null
|
183 | },
|
184 | 'context-timeout': {
|
185 | alias: 'M',
|
186 | type: 'number',
|
187 | description: 'timeout for before, after, beforeEach, afterEach in milliseconds',
|
188 | default: null
|
189 | },
|
190 | coverage: {
|
191 | alias: 'c',
|
192 | type: 'boolean',
|
193 | description: 'enable code coverage analysis',
|
194 | default: null
|
195 | },
|
196 | 'coverage-all': {
|
197 | type: 'boolean',
|
198 | description: 'include all files in coveragePath in report',
|
199 | default: null
|
200 | },
|
201 | 'coverage-exclude': {
|
202 | type: 'string',
|
203 | description: 'set code coverage excludes',
|
204 | multiple: true,
|
205 | default: null
|
206 | },
|
207 | 'coverage-flat': {
|
208 | type: 'boolean',
|
209 | description: 'prevent recursive inclusion of all files in coveragePath in report',
|
210 | default: null
|
211 | },
|
212 | 'coverage-module': {
|
213 | type: 'string',
|
214 | description: 'enable coverage on external module',
|
215 | multiple: true,
|
216 | default: null
|
217 | },
|
218 | 'coverage-path': {
|
219 | type: 'string',
|
220 | description: 'set code coverage path',
|
221 | default: null
|
222 | },
|
223 | 'coverage-pattern': {
|
224 | type: 'string',
|
225 | description: 'file pattern to use for locating files for coverage',
|
226 | default: null
|
227 | },
|
228 | 'default-plan-threshold': {
|
229 | alias: 'p',
|
230 | type: 'number',
|
231 | description: 'minimum plan threshold to apply to all tests that don\'t define any plan',
|
232 | default: null
|
233 | },
|
234 | dry: {
|
235 | alias: 'd',
|
236 | type: 'boolean',
|
237 | description: 'skip all tests (dry run)',
|
238 | default: null
|
239 | },
|
240 | environment: {
|
241 | alias: 'e',
|
242 | type: 'string',
|
243 | description: 'value to set NODE_ENV before tests',
|
244 | default: null
|
245 | },
|
246 | flat: {
|
247 | alias: 'f',
|
248 | type: 'boolean',
|
249 | description: 'prevent recursive collection of tests within the provided path',
|
250 | default: null
|
251 | },
|
252 | globals: {
|
253 | alias: ['I', 'ignore'],
|
254 | type: 'string',
|
255 | description: 'ignore a list of globals for the leak detection (comma separated)',
|
256 | default: null
|
257 | },
|
258 | grep: {
|
259 | alias: 'g',
|
260 | type: 'string',
|
261 | description: 'only run tests matching the given pattern which is internally compiled to a RegExp',
|
262 | default: null
|
263 | },
|
264 | help: {
|
265 | alias: 'h',
|
266 | type: 'boolean',
|
267 | description: 'display usage options',
|
268 | default: null
|
269 | },
|
270 | id: {
|
271 | alias: 'i',
|
272 | type: 'range',
|
273 | description: 'test identifier',
|
274 | default: null
|
275 | },
|
276 | inspect: {
|
277 | type: 'boolean',
|
278 | description: 'starts lab with the node.js native debugger',
|
279 | default: null
|
280 | },
|
281 | leaks: {
|
282 | alias: 'l',
|
283 | type: 'boolean',
|
284 | description: 'disable global variable leaks detection',
|
285 | default: null
|
286 | },
|
287 | lint: {
|
288 | alias: 'L',
|
289 | type: 'boolean',
|
290 | description: 'enable linting',
|
291 | default: null
|
292 | },
|
293 | linter: {
|
294 | alias: 'n',
|
295 | type: 'string',
|
296 | description: 'linter path to use',
|
297 | default: null
|
298 | },
|
299 | 'lint-fix': {
|
300 | type: 'boolean',
|
301 | description: 'apply any fixes from the linter.',
|
302 | default: null
|
303 | },
|
304 | 'lint-options': {
|
305 | type: 'string',
|
306 | description: 'specify options to pass to linting program. It must be a string that is JSON.parse(able).',
|
307 | default: null
|
308 | },
|
309 | 'lint-errors-threshold': {
|
310 | type: 'number',
|
311 | description: 'linter errors threshold in absolute value',
|
312 | default: null
|
313 | },
|
314 | 'lint-warnings-threshold': {
|
315 | type: 'number',
|
316 | description: 'linter warnings threshold in absolute value',
|
317 | default: null
|
318 | },
|
319 | output: {
|
320 | alias: 'o',
|
321 | type: 'string',
|
322 | description: 'file path to write test results',
|
323 | multiple: true,
|
324 | default: null
|
325 | },
|
326 | pattern: {
|
327 | alias: 'P',
|
328 | type: 'string',
|
329 | description: 'file pattern to use for locating tests',
|
330 | default: null
|
331 | },
|
332 | reporter: {
|
333 | alias: 'r',
|
334 | type: 'string',
|
335 | description: 'reporter type [console, html, json, tap, lcov, clover, junit]',
|
336 | multiple: true,
|
337 | default: null
|
338 | },
|
339 | seed: {
|
340 | type: 'string',
|
341 | description: 'use this seed to randomize the order with `--shuffle`. This is useful to debug order dependent test failures',
|
342 | default: null
|
343 | },
|
344 | shuffle: {
|
345 | type: 'boolean',
|
346 | description: 'shuffle script execution order',
|
347 | default: null
|
348 | },
|
349 | silence: {
|
350 | alias: 's',
|
351 | type: 'boolean',
|
352 | description: 'silence test output',
|
353 | default: null
|
354 | },
|
355 | 'silent-skips': {
|
356 | alias: 'k',
|
357 | type: 'boolean',
|
358 | description: 'don’t output skipped tests',
|
359 | default: null
|
360 | },
|
361 | sourcemaps: {
|
362 | alias: ['S', 'sourcemaps'],
|
363 | type: 'boolean',
|
364 | description: 'enable support for sourcemaps',
|
365 | default: null
|
366 | },
|
367 | threshold: {
|
368 | alias: 't',
|
369 | type: 'number',
|
370 | description: 'code coverage threshold percentage',
|
371 | default: null
|
372 | },
|
373 | timeout: {
|
374 | alias: 'm',
|
375 | type: 'number',
|
376 | description: 'timeout for each test in milliseconds',
|
377 | default: null
|
378 | },
|
379 | transform: {
|
380 | alias: ['T', 'transform'],
|
381 | type: 'string',
|
382 | description: 'javascript file that exports an array of objects ie. [ { ext: ".js", transform: function (content, filename) { ... } } ]',
|
383 | default: null
|
384 | },
|
385 | types: {
|
386 | alias: ['Y', 'types'],
|
387 | type: 'boolean',
|
388 | description: 'test types definitions',
|
389 | default: null
|
390 | },
|
391 | 'types-test': {
|
392 | type: 'string',
|
393 | description: 'location of types definitions test file',
|
394 | default: null
|
395 | },
|
396 | verbose: {
|
397 | alias: 'v',
|
398 | type: 'boolean',
|
399 | description: 'verbose test output',
|
400 | default: null
|
401 | },
|
402 | version: {
|
403 | alias: 'V',
|
404 | type: 'boolean',
|
405 | description: 'version information',
|
406 | default: null
|
407 | }
|
408 | };
|
409 |
|
410 | const defaults = {
|
411 | bail: false,
|
412 | coverage: false,
|
413 | dry: false,
|
414 | environment: 'test',
|
415 | flat: false,
|
416 | leaks: true,
|
417 | lint: false,
|
418 | linter: 'eslint',
|
419 | 'lint-fix': false,
|
420 | 'lint-errors-threshold': 0,
|
421 | 'lint-warnings-threshold': 0,
|
422 | paths: ['test'],
|
423 | reporter: 'console',
|
424 | shuffle: false,
|
425 | silence: false,
|
426 | 'silent-skips': false,
|
427 | sourcemaps: false,
|
428 | 'context-timeout': 0,
|
429 | timeout: 2000,
|
430 | verbose: false
|
431 | };
|
432 |
|
433 | const argv = Bossy.parse(definition);
|
434 |
|
435 | if (argv instanceof Error) {
|
436 | console.error(Bossy.usage(definition, 'lab [options] [path]'));
|
437 | console.error('\n' + argv.message);
|
438 | process.exit(1);
|
439 | }
|
440 |
|
441 | if (argv.help) {
|
442 | console.log(Bossy.usage(definition, 'lab [options] [path]'));
|
443 | process.exit(0);
|
444 | }
|
445 |
|
446 | if (argv.version) {
|
447 | console.log(Pkg.version);
|
448 | process.exit(0);
|
449 | }
|
450 |
|
451 | const options = Utils.mergeOptions(defaults, internals.rc);
|
452 | options.paths = argv._ ? [].concat(argv._) : options.paths;
|
453 |
|
454 | const keys = ['assert', 'bail', 'colors', 'context-timeout', 'coverage', 'coverage-exclude',
|
455 | 'coverage-path', 'coverage-all', 'coverage-flat', 'coverage-module', 'coverage-pattern', 'default-plan-threshold', 'dry', 'environment', 'flat', 'globals', 'grep',
|
456 | 'lint', 'lint-errors-threshold', 'lint-fix', 'lint-options', 'lint-warnings-threshold',
|
457 | 'linter', 'output', 'pattern', 'reporter', 'seed', 'shuffle', 'silence',
|
458 | 'silent-skips', 'sourcemaps', 'threshold', 'timeout', 'transform', 'types', 'types-test', 'verbose'];
|
459 |
|
460 | for (let i = 0; i < keys.length; ++i) {
|
461 | if (argv.hasOwnProperty(keys[i]) && argv[keys[i]] !== undefined && argv[keys[i]] !== null) {
|
462 | options[keys[i]] = argv[keys[i]];
|
463 | }
|
464 | }
|
465 |
|
466 | if (typeof argv.leaks === 'boolean') {
|
467 | options.leaks = !argv.leaks;
|
468 | }
|
469 |
|
470 | if (argv.id) {
|
471 | options.ids = argv.id;
|
472 | }
|
473 |
|
474 | if (Array.isArray(options.reporter) && options.output) {
|
475 | if (!Array.isArray(options.output) || options.output.length !== options.reporter.length) {
|
476 | console.error(Bossy.usage(definition, 'lab [options] [path]'));
|
477 | process.exit(1);
|
478 | }
|
479 | }
|
480 |
|
481 | if (!options.output) {
|
482 | options.output = process.stdout;
|
483 | }
|
484 |
|
485 | if (options.assert) {
|
486 | options.assert = require(options.assert);
|
487 | require('./').assertions = options.assert;
|
488 | }
|
489 |
|
490 | if (options.globals) {
|
491 | options.globals = options.globals.trim().split(',');
|
492 | }
|
493 |
|
494 | if (options.silence) {
|
495 | options.progress = 0;
|
496 | }
|
497 | else if (options.verbose) {
|
498 | options.progress = 2;
|
499 | }
|
500 |
|
501 | if (options['types-test']) {
|
502 | options.types = true;
|
503 | }
|
504 |
|
505 | const pattern = options.pattern ? '.*' + options.pattern + '.*?' : '';
|
506 |
|
507 | let exts = '\\.(js)$';
|
508 | if (options.transform) {
|
509 | const transform = require(Path.resolve(options.transform));
|
510 |
|
511 | Hoek.assert(Array.isArray(transform) && transform.length > 0, 'transform module must export an array of objects {ext: ".js", transform: null or function (content, filename)}');
|
512 | options.transform = transform;
|
513 |
|
514 | const includes = 'js|' + transform.map(internals.mapTransform).join('|');
|
515 | exts = '\\.(' + includes + ')$';
|
516 | }
|
517 |
|
518 | options.pattern = new RegExp(pattern + exts);
|
519 |
|
520 | 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);
|
521 | options.coveragePattern = new RegExp(options['coverage-pattern'] || pattern + exts);
|
522 |
|
523 | if (options['coverage-pattern'] && !options['coverage-all']) {
|
524 | console.error('The "coverage-pattern" option can only be used with "coverage-all"');
|
525 | process.exit(1);
|
526 | }
|
527 |
|
528 | if (options['coverage-flat'] && !options['coverage-all']) {
|
529 | console.error('The "coverage-flat" option can only be used with "coverage-all"');
|
530 | process.exit(1);
|
531 | }
|
532 |
|
533 | return options;
|
534 | };
|
535 |
|
536 | internals.mapTransform = function (transform) {
|
537 |
|
538 | return transform.ext.substr(1).replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&');
|
539 | };
|