UNPKG

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