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 Coverage = require('./coverage');
11const Pkg = require('../package.json');
12const Runner = require('./runner');
13const Transform = require('./transform');
14
15// .labrc configuration will be required if it exists
16// index.js required below if setting assertions module
17
18
19const internals = {};
20
21
22internals.rcPath = FindRc('lab');
23internals.rc = internals.rcPath ? require(internals.rcPath) : {};
24
25
26exports.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
66internals.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
166internals.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
551internals.mapTransform = function (transform) {
552
553 return transform.ext.substr(1).replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&');
554};