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