Skip to content

Commit

Permalink
run tests in a single process
Browse files Browse the repository at this point in the history
  • Loading branch information
Vadim Demedes committed Jan 21, 2018
1 parent 947f207 commit 0b3fec6
Show file tree
Hide file tree
Showing 11 changed files with 13,502 additions and 7,592 deletions.
6 changes: 4 additions & 2 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ exports.run = () => {
'verbose',
'watch',
'update-snapshots',
'color'
'color',
'single'
],
default: {
cache: conf.cache,
Expand Down Expand Up @@ -150,7 +151,8 @@ exports.run = () => {
concurrency: conf.concurrency ? parseInt(conf.concurrency, 10) : 0,
updateSnapshots: conf.updateSnapshots,
snapshotDir: conf.snapshotDir ? path.resolve(projectDir, conf.snapshotDir) : null,
color: conf.color
color: conf.color,
fork: !conf.single
});

let reporter;
Expand Down
42 changes: 42 additions & 0 deletions lib/fork-test-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';

// Check if the test is being run without AVA CLI
{
const path = require('path');
const chalk = require('chalk');

const isForked = typeof process.send === 'function';
if (!isForked) {
const fp = path.relative('.', process.argv[1]);

console.log();
console.error('Test files must be run with the AVA CLI:\n\n ' + chalk.grey.dim('$') + ' ' + chalk.cyan('ava ' + fp) + '\n');

process.exit(1); // eslint-disable-line unicorn/no-process-exit
}
}

const run = require('./test-worker');

const opts = JSON.parse(process.argv[2]);

// Adapter for simplified communication between AVA and worker
const ipcMain = {
send: (name, data) => {
process.send({
name: `ava-${name}`,
data,
ava: true
});
},
on: (name, listener) => process.on(name, listener),
// `process.channel` was added in Node.js 7.1.0, but the channel was available
// through an undocumented API as `process._channel`.
ipcChannel: process.channel || process._channel
};

run({
ipcMain,
opts,
isForked: true
});
27 changes: 20 additions & 7 deletions lib/fork.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const fs = require('fs');
const Promise = require('bluebird');
const debug = require('debug')('ava');
const AvaError = require('./ava-error');
const single = require('./single-test-worker');

if (fs.realpathSync(__filename) !== __filename) {
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`.');
Expand All @@ -24,6 +25,19 @@ if (env.NODE_PATH) {
// the presence of this variable allows it to require this one instead
env.AVA_PATH = path.resolve(__dirname, '..');

const fork = (opts, env, execArgv) => {
const args = [JSON.stringify(opts), opts.color ? '--color' : '--no-color'];

const ps = childProcess.fork(path.join(__dirname, 'fork-test-worker.js'), args, {
cwd: opts.projectDir,
silent: true,
env,
execArgv: execArgv || process.execArgv
});

return ps;
};

module.exports = (file, opts, execArgv) => {
opts = Object.assign({
file,
Expand All @@ -34,14 +48,13 @@ module.exports = (file, opts, execArgv) => {
} : false
}, opts);

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

const ps = childProcess.fork(path.join(__dirname, 'test-worker.js'), args, {
cwd: opts.projectDir,
silent: true,
env,
execArgv: execArgv || process.execArgv
});
if (opts.fork) {
ps = fork(opts, env, execArgv || process.execArgv);
} else {
ps = single(opts);
}

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

Expand Down
36 changes: 19 additions & 17 deletions lib/main.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
'use strict';
const worker = require('./test-worker');
const adapter = require('./process-adapter');
const serializeError = require('./serialize-error');
const globals = require('./globals');
const Runner = require('./runner');

const opts = globals.options;
const testPath = module.parent.parent.filename;
const opts = global.__shared.options;

const runner = new Runner({
bail: opts.failFast,
failWithoutAssertions: opts.failWithoutAssertions,
file: opts.file,
file: testPath,
match: opts.match,
projectDir: opts.projectDir,
serial: opts.serial,
updateSnapshots: opts.updateSnapshots,
snapshotDir: opts.snapshotDir
});

worker.setRunner(runner);
const ipcMain = global.__shared.ipcMain[testPath];
const ipcWorker = global.__shared.ipcWorker[testPath];
global.__shared.runner[testPath] = runner;

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

adapter.send('test', props);
ipcMain.send('test', props);

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

function exit() {
// Reference the IPC channel now that tests have finished running.
adapter.ipcChannel.ref();
ipcMain.ipcChannel.ref();

const stats = runner.buildStats();
adapter.send('results', {stats});
ipcMain.send('results', {stats});
}

globals.setImmediate(() => {
const hasExclusive = runner.tests.hasExclusive;
const numberOfTests = runner.tests.testCount;

if (numberOfTests === 0) {
adapter.send('no-tests', {avaRequired: true});
ipcMain.send('no-tests', {avaRequired: true});
return;
}

adapter.send('stats', {
testCount: numberOfTests,
hasExclusive
});

runner.on('test', test);

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

runner.run(options)
.then(() => {
Expand All @@ -92,9 +89,14 @@ globals.setImmediate(() => {
});
});

process.on('ava-init-exit', () => {
ipcWorker.on('ava-init-exit', () => {
exit();
});

ipcMain.send('stats', {
testCount: numberOfTests,
hasExclusive
});
});

module.exports = runner.chain;
Expand Down
85 changes: 33 additions & 52 deletions lib/process-adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,63 +4,43 @@ const path = require('path');
const debug = require('debug')('ava');
const sourceMapSupport = require('source-map-support');
const installPrecompiler = require('require-precompiled');

// Parse and re-emit AVA messages
process.on('message', message => {
if (!message.ava) {
return;
}

process.emit(message.name, message.data);
});

exports.send = (name, data) => {
process.send({
name: `ava-${name}`,
data,
ava: true
});
};

// `process.channel` was added in Node.js 7.1.0, but the channel was available
// through an undocumented API as `process._channel`.
exports.ipcChannel = process.channel || process._channel;

const opts = JSON.parse(process.argv[2]);
exports.opts = opts;
const mem = require('mem');

// Fake TTY support
if (opts.tty) {
process.stdout.isTTY = true;
process.stdout.columns = opts.tty.columns || 80;
process.stdout.rows = opts.tty.rows;
exports.setupFakeTTY = mem(opts => {
if (opts.tty) {
process.stdout.isTTY = true;
process.stdout.columns = opts.tty.columns || 80;
process.stdout.rows = opts.tty.rows;

const tty = require('tty');
const isatty = tty.isatty;

tty.isatty = function (fd) {
if (fd === 1 || fd === process.stdout) {
return true;
}

const tty = require('tty');
const isatty = tty.isatty;
return isatty(fd);
};
}
});

tty.isatty = function (fd) {
if (fd === 1 || fd === process.stdout) {
return true;
exports.setupTimeRequire = mem(opts => {
if (debug.enabled) {
// Forward the `@ladjs/time-require` `--sorted` flag.
// Intended for internal optimization tests only.
if (opts._sorted) {
process.argv.push('--sorted');
}

return isatty(fd);
};
}

if (debug.enabled) {
// Forward the `@ladjs/time-require` `--sorted` flag.
// Intended for internal optimization tests only.
if (opts._sorted) {
process.argv.push('--sorted');
require('@ladjs/time-require'); // eslint-disable-line import/no-unassigned-import
}

require('@ladjs/time-require'); // eslint-disable-line import/no-unassigned-import
}
});

const sourceMapCache = new Map();
const cacheDir = opts.cacheDir;

exports.installSourceMapSupport = () => {
exports.installSourceMapSupport = mem(() => {
sourceMapSupport.install({
environment: 'node',
handleUncaughtExceptions: false,
Expand All @@ -73,21 +53,22 @@ exports.installSourceMapSupport = () => {
}
}
});
};
});

exports.installPrecompilerHook = () => {
exports.installPrecompilerHook = mem(opts => {
installPrecompiler(filename => {
const precompiled = opts.precompiled[filename];

if (precompiled) {
sourceMapCache.set(filename, path.join(cacheDir, `${precompiled}.js.map`));
return fs.readFileSync(path.join(cacheDir, `${precompiled}.js`), 'utf8');
sourceMapCache.set(filename, path.join(opts.cacheDir, `${precompiled}.js.map`));
return fs.readFileSync(path.join(opts.cacheDir, `${precompiled}.js`), 'utf8');
}

return null;
});
};
});

// TODO: Detect which dependencies belong to other test files in single mode
exports.installDependencyTracking = (dependencies, testPath) => {
Object.keys(require.extensions).forEach(ext => {
const wrappedHandler = require.extensions[ext];
Expand Down
54 changes: 54 additions & 0 deletions lib/single-test-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use strict';
const EventEmitter = require('events');
const stream = require('stream');
const vm = require('vm');
const run = require('./test-worker');

// Required to prevent warnings from Node.js, because in single mode each test file
// attaches its own `uncaughtException` and `unhandledRejection` listeners.
process.setMaxListeners(Infinity);

module.exports = opts => {
// Fake child process
const ps = new EventEmitter();
ps.stdout = new stream.PassThrough();
ps.stderr = new stream.PassThrough();

// Adapter for simplified communication between AVA and worker
const ipcMain = new EventEmitter();

// Incoming message from AVA to worker
ps.send = data => {
ipcMain.emit('message', data);
};

// Fake IPC channel
ipcMain.ipcChannel = {
ref: () => {},
unref: () => {}
};

// Outgoing message from worker to AVA
ipcMain.send = (name, data) => {
ps.emit('message', {
name: `ava-${name}`,
data,
ava: true
});
};

// Fake `process.exit()`
ipcMain.exit = code => {
ps.emit('exit', code);
};

setImmediate(() => {
run({
ipcMain,
opts,
isForked: false
});
});

return ps;
};
Loading

0 comments on commit 0b3fec6

Please sign in to comment.