Skip to content

Commit 0b3fec6

Browse files
author
Vadim Demedes
committed
run tests in a single process
1 parent 947f207 commit 0b3fec6

11 files changed

+13502
-7592
lines changed

lib/cli.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ exports.run = () => {
7171
'verbose',
7272
'watch',
7373
'update-snapshots',
74-
'color'
74+
'color',
75+
'single'
7576
],
7677
default: {
7778
cache: conf.cache,
@@ -150,7 +151,8 @@ exports.run = () => {
150151
concurrency: conf.concurrency ? parseInt(conf.concurrency, 10) : 0,
151152
updateSnapshots: conf.updateSnapshots,
152153
snapshotDir: conf.snapshotDir ? path.resolve(projectDir, conf.snapshotDir) : null,
153-
color: conf.color
154+
color: conf.color,
155+
fork: !conf.single
154156
});
155157

156158
let reporter;

lib/fork-test-worker.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use strict';
2+
3+
// Check if the test is being run without AVA CLI
4+
{
5+
const path = require('path');
6+
const chalk = require('chalk');
7+
8+
const isForked = typeof process.send === 'function';
9+
if (!isForked) {
10+
const fp = path.relative('.', process.argv[1]);
11+
12+
console.log();
13+
console.error('Test files must be run with the AVA CLI:\n\n ' + chalk.grey.dim('$') + ' ' + chalk.cyan('ava ' + fp) + '\n');
14+
15+
process.exit(1); // eslint-disable-line unicorn/no-process-exit
16+
}
17+
}
18+
19+
const run = require('./test-worker');
20+
21+
const opts = JSON.parse(process.argv[2]);
22+
23+
// Adapter for simplified communication between AVA and worker
24+
const ipcMain = {
25+
send: (name, data) => {
26+
process.send({
27+
name: `ava-${name}`,
28+
data,
29+
ava: true
30+
});
31+
},
32+
on: (name, listener) => process.on(name, listener),
33+
// `process.channel` was added in Node.js 7.1.0, but the channel was available
34+
// through an undocumented API as `process._channel`.
35+
ipcChannel: process.channel || process._channel
36+
};
37+
38+
run({
39+
ipcMain,
40+
opts,
41+
isForked: true
42+
});

lib/fork.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const fs = require('fs');
55
const Promise = require('bluebird');
66
const debug = require('debug')('ava');
77
const AvaError = require('./ava-error');
8+
const single = require('./single-test-worker');
89

910
if (fs.realpathSync(__filename) !== __filename) {
1011
console.warn('WARNING: `npm link ava` and the `--preserve-symlink` flag are incompatible. We have detected that AVA is linked via `npm link`, and that you are using either an early version of Node 6, or the `--preserve-symlink` flag. This breaks AVA. You should upgrade to Node 6.2.0+, avoid the `--preserve-symlink` flag, or avoid using `npm link ava`.');
@@ -24,6 +25,19 @@ if (env.NODE_PATH) {
2425
// the presence of this variable allows it to require this one instead
2526
env.AVA_PATH = path.resolve(__dirname, '..');
2627

28+
const fork = (opts, env, execArgv) => {
29+
const args = [JSON.stringify(opts), opts.color ? '--color' : '--no-color'];
30+
31+
const ps = childProcess.fork(path.join(__dirname, 'fork-test-worker.js'), args, {
32+
cwd: opts.projectDir,
33+
silent: true,
34+
env,
35+
execArgv: execArgv || process.execArgv
36+
});
37+
38+
return ps;
39+
};
40+
2741
module.exports = (file, opts, execArgv) => {
2842
opts = Object.assign({
2943
file,
@@ -34,14 +48,13 @@ module.exports = (file, opts, execArgv) => {
3448
} : false
3549
}, opts);
3650

37-
const args = [JSON.stringify(opts), opts.color ? '--color' : '--no-color'];
51+
let ps;
3852

39-
const ps = childProcess.fork(path.join(__dirname, 'test-worker.js'), args, {
40-
cwd: opts.projectDir,
41-
silent: true,
42-
env,
43-
execArgv: execArgv || process.execArgv
44-
});
53+
if (opts.fork) {
54+
ps = fork(opts, env, execArgv || process.execArgv);
55+
} else {
56+
ps = single(opts);
57+
}
4558

4659
const relFile = path.relative('.', file);
4760

lib/main.js

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
'use strict';
2-
const worker = require('./test-worker');
3-
const adapter = require('./process-adapter');
42
const serializeError = require('./serialize-error');
53
const globals = require('./globals');
64
const Runner = require('./runner');
75

8-
const opts = globals.options;
6+
const testPath = module.parent.parent.filename;
7+
const opts = global.__shared.options;
8+
99
const runner = new Runner({
1010
bail: opts.failFast,
1111
failWithoutAssertions: opts.failWithoutAssertions,
12-
file: opts.file,
12+
file: testPath,
1313
match: opts.match,
1414
projectDir: opts.projectDir,
1515
serial: opts.serial,
1616
updateSnapshots: opts.updateSnapshots,
1717
snapshotDir: opts.snapshotDir
1818
});
1919

20-
worker.setRunner(runner);
20+
const ipcMain = global.__shared.ipcMain[testPath];
21+
const ipcWorker = global.__shared.ipcWorker[testPath];
22+
global.__shared.runner[testPath] = runner;
2123

2224
// If fail-fast is enabled, use this variable to detect
2325
// that no more tests should be logged
@@ -43,7 +45,7 @@ function test(props) {
4345
props.error = null;
4446
}
4547

46-
adapter.send('test', props);
48+
ipcMain.send('test', props);
4749

4850
if (hasError && opts.failFast) {
4951
isFailed = true;
@@ -53,33 +55,28 @@ function test(props) {
5355

5456
function exit() {
5557
// Reference the IPC channel now that tests have finished running.
56-
adapter.ipcChannel.ref();
58+
ipcMain.ipcChannel.ref();
5759

5860
const stats = runner.buildStats();
59-
adapter.send('results', {stats});
61+
ipcMain.send('results', {stats});
6062
}
6163

6264
globals.setImmediate(() => {
6365
const hasExclusive = runner.tests.hasExclusive;
6466
const numberOfTests = runner.tests.testCount;
6567

6668
if (numberOfTests === 0) {
67-
adapter.send('no-tests', {avaRequired: true});
69+
ipcMain.send('no-tests', {avaRequired: true});
6870
return;
6971
}
7072

71-
adapter.send('stats', {
72-
testCount: numberOfTests,
73-
hasExclusive
74-
});
75-
7673
runner.on('test', test);
7774

78-
process.on('ava-run', options => {
75+
ipcWorker.on('ava-run', options => {
7976
// Unreference the IPC channel. This stops it from keeping the event loop
8077
// busy, which means the `beforeExit` event can be used to detect when tests
8178
// stall.
82-
adapter.ipcChannel.unref();
79+
ipcMain.ipcChannel.unref();
8380

8481
runner.run(options)
8582
.then(() => {
@@ -92,9 +89,14 @@ globals.setImmediate(() => {
9289
});
9390
});
9491

95-
process.on('ava-init-exit', () => {
92+
ipcWorker.on('ava-init-exit', () => {
9693
exit();
9794
});
95+
96+
ipcMain.send('stats', {
97+
testCount: numberOfTests,
98+
hasExclusive
99+
});
98100
});
99101

100102
module.exports = runner.chain;

lib/process-adapter.js

Lines changed: 33 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,63 +4,43 @@ const path = require('path');
44
const debug = require('debug')('ava');
55
const sourceMapSupport = require('source-map-support');
66
const installPrecompiler = require('require-precompiled');
7-
8-
// Parse and re-emit AVA messages
9-
process.on('message', message => {
10-
if (!message.ava) {
11-
return;
12-
}
13-
14-
process.emit(message.name, message.data);
15-
});
16-
17-
exports.send = (name, data) => {
18-
process.send({
19-
name: `ava-${name}`,
20-
data,
21-
ava: true
22-
});
23-
};
24-
25-
// `process.channel` was added in Node.js 7.1.0, but the channel was available
26-
// through an undocumented API as `process._channel`.
27-
exports.ipcChannel = process.channel || process._channel;
28-
29-
const opts = JSON.parse(process.argv[2]);
30-
exports.opts = opts;
7+
const mem = require('mem');
318

329
// Fake TTY support
33-
if (opts.tty) {
34-
process.stdout.isTTY = true;
35-
process.stdout.columns = opts.tty.columns || 80;
36-
process.stdout.rows = opts.tty.rows;
10+
exports.setupFakeTTY = mem(opts => {
11+
if (opts.tty) {
12+
process.stdout.isTTY = true;
13+
process.stdout.columns = opts.tty.columns || 80;
14+
process.stdout.rows = opts.tty.rows;
15+
16+
const tty = require('tty');
17+
const isatty = tty.isatty;
18+
19+
tty.isatty = function (fd) {
20+
if (fd === 1 || fd === process.stdout) {
21+
return true;
22+
}
3723

38-
const tty = require('tty');
39-
const isatty = tty.isatty;
24+
return isatty(fd);
25+
};
26+
}
27+
});
4028

41-
tty.isatty = function (fd) {
42-
if (fd === 1 || fd === process.stdout) {
43-
return true;
29+
exports.setupTimeRequire = mem(opts => {
30+
if (debug.enabled) {
31+
// Forward the `@ladjs/time-require` `--sorted` flag.
32+
// Intended for internal optimization tests only.
33+
if (opts._sorted) {
34+
process.argv.push('--sorted');
4435
}
4536

46-
return isatty(fd);
47-
};
48-
}
49-
50-
if (debug.enabled) {
51-
// Forward the `@ladjs/time-require` `--sorted` flag.
52-
// Intended for internal optimization tests only.
53-
if (opts._sorted) {
54-
process.argv.push('--sorted');
37+
require('@ladjs/time-require'); // eslint-disable-line import/no-unassigned-import
5538
}
56-
57-
require('@ladjs/time-require'); // eslint-disable-line import/no-unassigned-import
58-
}
39+
});
5940

6041
const sourceMapCache = new Map();
61-
const cacheDir = opts.cacheDir;
6242

63-
exports.installSourceMapSupport = () => {
43+
exports.installSourceMapSupport = mem(() => {
6444
sourceMapSupport.install({
6545
environment: 'node',
6646
handleUncaughtExceptions: false,
@@ -73,21 +53,22 @@ exports.installSourceMapSupport = () => {
7353
}
7454
}
7555
});
76-
};
56+
});
7757

78-
exports.installPrecompilerHook = () => {
58+
exports.installPrecompilerHook = mem(opts => {
7959
installPrecompiler(filename => {
8060
const precompiled = opts.precompiled[filename];
8161

8262
if (precompiled) {
83-
sourceMapCache.set(filename, path.join(cacheDir, `${precompiled}.js.map`));
84-
return fs.readFileSync(path.join(cacheDir, `${precompiled}.js`), 'utf8');
63+
sourceMapCache.set(filename, path.join(opts.cacheDir, `${precompiled}.js.map`));
64+
return fs.readFileSync(path.join(opts.cacheDir, `${precompiled}.js`), 'utf8');
8565
}
8666

8767
return null;
8868
});
89-
};
69+
});
9070

71+
// TODO: Detect which dependencies belong to other test files in single mode
9172
exports.installDependencyTracking = (dependencies, testPath) => {
9273
Object.keys(require.extensions).forEach(ext => {
9374
const wrappedHandler = require.extensions[ext];

lib/single-test-worker.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use strict';
2+
const EventEmitter = require('events');
3+
const stream = require('stream');
4+
const vm = require('vm');
5+
const run = require('./test-worker');
6+
7+
// Required to prevent warnings from Node.js, because in single mode each test file
8+
// attaches its own `uncaughtException` and `unhandledRejection` listeners.
9+
process.setMaxListeners(Infinity);
10+
11+
module.exports = opts => {
12+
// Fake child process
13+
const ps = new EventEmitter();
14+
ps.stdout = new stream.PassThrough();
15+
ps.stderr = new stream.PassThrough();
16+
17+
// Adapter for simplified communication between AVA and worker
18+
const ipcMain = new EventEmitter();
19+
20+
// Incoming message from AVA to worker
21+
ps.send = data => {
22+
ipcMain.emit('message', data);
23+
};
24+
25+
// Fake IPC channel
26+
ipcMain.ipcChannel = {
27+
ref: () => {},
28+
unref: () => {}
29+
};
30+
31+
// Outgoing message from worker to AVA
32+
ipcMain.send = (name, data) => {
33+
ps.emit('message', {
34+
name: `ava-${name}`,
35+
data,
36+
ava: true
37+
});
38+
};
39+
40+
// Fake `process.exit()`
41+
ipcMain.exit = code => {
42+
ps.emit('exit', code);
43+
};
44+
45+
setImmediate(() => {
46+
run({
47+
ipcMain,
48+
opts,
49+
isForked: false
50+
});
51+
});
52+
53+
return ps;
54+
};

0 commit comments

Comments
 (0)