1 | 'use strict';
|
2 |
|
3 |
|
4 |
|
5 | const Fs = require('fs');
|
6 | const Path = require('path');
|
7 |
|
8 | const Bossy = require('bossy');
|
9 | const FindRc = require('find-rc');
|
10 | const Hoek = require('hoek');
|
11 |
|
12 | const Coverage = require('./coverage');
|
13 | const Pkg = require('../package.json');
|
14 | const Runner = require('./runner');
|
15 | const Transform = require('./transform');
|
16 | const Utils = require('./utils');
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | const internals = {};
|
24 |
|
25 | internals.rcPath = FindRc('lab');
|
26 | internals.rc = internals.rcPath ? require(internals.rcPath) : {};
|
27 |
|
28 |
|
29 | exports.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 |
|
69 | internals.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 |
|
167 | internals.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 |
|
491 | internals.mapTransform = function (transform) {
|
492 |
|
493 | return transform.ext.substr(1).replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&');
|
494 | };
|