UNPKG

15.9 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');
14const Utils = require('./utils');
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 && !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
164internals.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
536internals.mapTransform = function (transform) {
537
538 return transform.ext.substr(1).replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&');
539};