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