Skip to content

Refactor IPC, status and reporters #1722

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Apr 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 73 additions & 84 deletions api.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
'use strict';
const EventEmitter = require('events');
const path = require('path');
const fs = require('fs');
const os = require('os');
Expand All @@ -14,10 +13,11 @@ const arrify = require('arrify');
const ms = require('ms');
const babelConfigHelper = require('./lib/babel-config');
const CachingPrecompiler = require('./lib/caching-precompiler');
const Emittery = require('./lib/emittery');
const RunStatus = require('./lib/run-status');
const AvaError = require('./lib/ava-error');
const AvaFiles = require('./lib/ava-files');
const fork = require('./lib/fork');
const serializeError = require('./lib/serialize-error');

function resolveModules(modules) {
return arrify(modules).map(name => {
Expand All @@ -31,7 +31,7 @@ function resolveModules(modules) {
});
}

class Api extends EventEmitter {
class Api extends Emittery {
constructor(options) {
super();

Expand All @@ -46,19 +46,13 @@ class Api extends EventEmitter {
// Each run will have its own status. It can only be created when test files
// have been found.
let runStatus;

// Irrespectively, perform some setup now, before finding test files.
const handleError = exception => {
runStatus.handleExceptions({
exception,
file: exception.file ? path.relative(process.cwd(), exception.file) : undefined
});
};

// Track active forks and manage timeouts.
const failFast = apiOptions.failFast === true;
let bailed = false;
const pendingForks = new Set();
const pendingWorkers = new Set();
const timedOutWorkerFiles = new Set();
let restartTimer;
if (apiOptions.timeout) {
const timeout = ms(apiOptions.timeout);
Expand All @@ -70,11 +64,12 @@ class Api extends EventEmitter {
bailed = true;
}

for (const fork of pendingForks) {
fork.exit();
for (const worker of pendingWorkers) {
timedOutWorkerFiles.add(worker.file);
worker.exit();
}

handleError(new AvaError(`Exited because no new tests completed within the last ${timeout}ms of inactivity`));
runStatus.emitStateChange({type: 'timeout', period: timeout});
}, timeout);
} else {
restartTimer = Object.assign(() => {}, {cancel() {}});
Expand All @@ -83,39 +78,48 @@ class Api extends EventEmitter {
// Find all test files.
return new AvaFiles({cwd: apiOptions.resolveTestsFrom, files}).findTestFiles()
.then(files => {
runStatus = new RunStatus({
runOnlyExclusive: runtimeOptions.runOnlyExclusive,
prefixTitles: apiOptions.explicitTitles || files.length > 1,
base: path.relative(process.cwd(), commonPathPrefix(files)) + path.sep,
failFast,
fileCount: files.length,
updateSnapshots: runtimeOptions.updateSnapshots
runStatus = new RunStatus(files.length);

const emittedRun = this.emit('run', {
clearLogOnNextRun: runtimeOptions.clearLogOnNextRun === true,
failFastEnabled: failFast,
filePathPrefix: commonPathPrefix(files),
files,
matching: apiOptions.match.length > 0,
previousFailures: runtimeOptions.previousFailures || 0,
runOnlyExclusive: runtimeOptions.runOnlyExclusive === true,
runVector: runtimeOptions.runVector || 0,
status: runStatus
});

runStatus.on('test', restartTimer);
if (failFast) {
// Prevent new test files from running once a test has failed.
runStatus.on('test', test => {
if (test.error) {
bailed = true;

for (const fork of pendingForks) {
fork.notifyOfPeerFailure();
}
}
// Bail out early if no files were found.
if (files.length === 0) {
return emittedRun.then(() => {
return runStatus;
});
}

this.emit('test-run', runStatus, files);
runStatus.on('stateChange', record => {
if (record.testFile && !timedOutWorkerFiles.has(record.testFile)) {
// Restart the timer whenever there is activity from workers that
// haven't already timed out.
restartTimer();
}

// Bail out early if no files were found.
if (files.length === 0) {
handleError(new AvaError('Couldn\'t find any files to test'));
return runStatus;
}
if (failFast && (record.type === 'hook-failed' || record.type === 'test-failed' || record.type === 'worker-failed')) {
// Prevent new test files from running once a test has failed.
bailed = true;

// Try to stop currently scheduled tests.
for (const worker of pendingWorkers) {
worker.notifyOfPeerFailure();
}
}
});

// Set up a fresh precompiler for each test run.
return this._setupPrecompiler()
return emittedRun
.then(() => this._setupPrecompiler())
.then(precompilation => {
if (!precompilation) {
return null;
Expand Down Expand Up @@ -156,59 +160,44 @@ class Api extends EventEmitter {
// No new files should be run once a test has timed out or failed,
// and failFast is enabled.
if (bailed) {
return null;
return;
}

let forked;
return Bluebird.resolve(
this._computeForkExecArgv().then(execArgv => {
const options = Object.assign({}, apiOptions, {
// If we're looking for matches, run every single test process in exclusive-only mode
runOnlyExclusive: apiOptions.match.length > 0 || runtimeOptions.runOnlyExclusive === true
});
if (precompilation) {
options.cacheDir = precompilation.cacheDir;
options.precompiled = precompilation.map;
} else {
options.precompiled = {};
}
if (runtimeOptions.updateSnapshots) {
// Don't use in Object.assign() since it'll override options.updateSnapshots even when false.
options.updateSnapshots = true;
}

forked = fork(file, options, execArgv);
pendingForks.add(forked);
runStatus.observeFork(forked);
restartTimer();
return forked;
}).catch(err => {
// Prevent new test files from running.
if (failFast) {
bailed = true;
}
handleError(Object.assign(err, {file}));
return null;
})
).finally(() => {
pendingForks.delete(forked);
return this._computeForkExecArgv().then(execArgv => {
const options = Object.assign({}, apiOptions, {
// If we're looking for matches, run every single test process in exclusive-only mode
runOnlyExclusive: apiOptions.match.length > 0 || runtimeOptions.runOnlyExclusive === true
});
if (precompilation) {
options.cacheDir = precompilation.cacheDir;
options.precompiled = precompilation.map;
} else {
options.precompiled = {};
}
if (runtimeOptions.updateSnapshots) {
// Don't use in Object.assign() since it'll override options.updateSnapshots even when false.
options.updateSnapshots = true;
}

const worker = fork(file, options, execArgv);
runStatus.observeWorker(worker, file);

pendingWorkers.add(worker);
worker.promise.then(() => { // eslint-disable-line max-nested-callbacks
pendingWorkers.delete(worker);
});

restartTimer();

return worker.promise;
});
}, {concurrency});
})
.catch(err => {
handleError(err);
return [];
runStatus.emitStateChange({type: 'internal-error', err: serializeError('Internal error', false, err)});
})
.then(results => {
.then(() => {
restartTimer.cancel();

// Filter out undefined results (e.g. for files that were skipped after a timeout)
results = results.filter(Boolean);
if (apiOptions.match.length > 0 && !runStatus.hasExclusive) {
handleError(new AvaError('Couldn\'t find any matching tests'));
}

runStatus.processResults(results);
return runStatus;
});
});
Expand Down
11 changes: 1 addition & 10 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,5 @@ const importLocal = require('import-local');
if (importLocal(__filename)) {
debug('Using local install of AVA');
} else {
if (debug.enabled) {
require('@ladjs/time-require'); // eslint-disable-line import/no-unassigned-import
}

try {
require('./lib/cli').run();
} catch (err) {
console.error(`\n ${err.message}`);
process.exit(1);
}
require('./lib/cli').run();
}
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
if (process.env.AVA_PATH && process.env.AVA_PATH !== __dirname) {
module.exports = require(process.env.AVA_PATH);
} else {
module.exports = require('./lib/main');
module.exports = require('./lib/worker/main');
}
10 changes: 0 additions & 10 deletions lib/ava-error.js

This file was deleted.

6 changes: 2 additions & 4 deletions lib/babel-config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
'use strict';
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const figures = require('figures');
const configManager = require('hullabaloo-config-manager');
const md5Hex = require('md5-hex');
const makeDir = require('make-dir');
const colors = require('./colors');
const chalk = require('./chalk').get();

const stage4Path = require.resolve('../stage-4');
const syntaxAsyncGeneratorsPath = require.resolve('@babel/plugin-syntax-async-generators');
Expand All @@ -23,7 +21,7 @@ function validate(conf) {
}

if (!conf || typeof conf !== 'object' || !conf.testOptions || typeof conf.testOptions !== 'object' || Array.isArray(conf.testOptions) || Object.keys(conf).length > 1) {
throw new Error(`${colors.error(figures.cross)} Unexpected Babel configuration for AVA. See ${chalk.underline('https://github.com/avajs/ava/blob/master/docs/recipes/babel.md')} for allowed values.`);
throw new Error(`Unexpected Babel configuration for AVA. See ${chalk.underline('https://github.com/avajs/ava/blob/master/docs/recipes/babel.md')} for allowed values.`);
}

return conf;
Expand Down
4 changes: 2 additions & 2 deletions lib/beautify-stack.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ const debug = require('debug')('ava');
// Ignore unimportant stack trace lines
let ignoreStackLines = [];

const avaInternals = /\/ava\/(?:lib\/)?[\w-]+\.js:\d+:\d+\)?$/;
const avaDependencies = /\/node_modules\/(?:bluebird|empower-core|(?:ava\/node_modules\/)?(?:babel-runtime|core-js))\//;
const avaInternals = /\/ava\/(?:lib\/|lib\/worker\/)?[\w-]+\.js:\d+:\d+\)?$/;
const avaDependencies = /\/node_modules\/(?:append-transform|bluebird|empower-core|nyc|require-precompiled|(?:ava\/node_modules\/)?(?:babel-runtime|core-js))\//;
const stackFrameLine = /^.+( \(.+:\d+:\d+\)|:\d+:\d+)$/;

if (!debug.enabled) {
Expand Down
16 changes: 16 additions & 0 deletions lib/chalk.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';
const Chalk = require('chalk').constructor;

let ctx = null;
exports.get = () => {
if (!ctx) {
throw new Error('Chalk has not yet been configured');
}
return ctx;
};
exports.set = options => {
if (ctx) {
throw new Error('Chalk has already been configured');
}
ctx = new Chalk(options);
};
Loading