UNPKG

16.1 kBJavaScriptView Raw
1'use strict';
2
3const Fs = require('fs');
4const Path = require('path');
5
6const Bossy = require('@hapi/bossy');
7const FindRc = require('find-rc');
8const Hoek = require('@hapi/hoek');
9
10const Modules = require('./modules');
11const Pkg = require('../package.json');
12const Runner = require('./runner');
13
14// .labrc configuration will be required if it exists
15// index.js required below if setting assertions module
16
17
18const internals = {};
19
20
21internals.rcPath = FindRc('lab');
22internals.rc = internals.rcPath ? require(internals.rcPath) : {};
23
24
25exports.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
65internals.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
165internals.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
550internals.mapTransform = function (transform) {
551
552 return transform.ext.substr(1).replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&');
553};