Skip to content

Instantly share code, notes, and snippets.

@dcousineau
Last active April 26, 2016 15:10
Show Gist options
  • Save dcousineau/768b01a30ea281b44cd7c882a0610386 to your computer and use it in GitHub Desktop.
Save dcousineau/768b01a30ea281b44cd7c882a0610386 to your computer and use it in GitHub Desktop.

Initial proof of concept. Just swap api.js with this whole file in the latest master branch.

You can see a video of the code working in a minimal "perfect" case environment: http://ooh.dcousineau.sexy/3b3T360U0l1B

TODOs:

  • Pull new promise into separate function, swap behavior on CLI flag
  • Quadruple confirm error handling
  • Utilize .dispose in bluebird? (to ensure teardown lifecycle)
  • Test with the watcher
'use strict';
var EventEmitter = require('events').EventEmitter;
var path = require('path');
var util = require('util');
var Promise = require('bluebird');
var objectAssign = require('object-assign');
var commonPathPrefix = require('common-path-prefix');
var resolveCwd = require('resolve-cwd');
var uniqueTempDir = require('unique-temp-dir');
var findCacheDir = require('find-cache-dir');
var debounce = require('lodash.debounce');
var ms = require('ms');
var AvaError = require('./lib/ava-error');
var fork = require('./lib/fork');
var CachingPrecompiler = require('./lib/caching-precompiler');
var AvaFiles = require('./lib/ava-files');
var RunStatus = require('./lib/run-status');
Promise.config({
// Enable cancellation
cancellation: true
});
function Api(options) {
if (!(this instanceof Api)) {
throw new TypeError('Class constructor Api cannot be invoked without \'new\'');
}
EventEmitter.call(this);
this.options = options || {};
this.options.match = this.options.match || [];
this.options.require = (this.options.require || []).map(function (moduleId) {
var ret = resolveCwd(moduleId);
if (ret === null) {
throw new Error('Could not resolve required module \'' + moduleId + '\'');
}
return ret;
});
Object.keys(Api.prototype).forEach(function (key) {
this[key] = this[key].bind(this);
}, this);
}
util.inherits(Api, EventEmitter);
module.exports = Api;
Api.prototype._runFile = function (file, runStatus) {
var hash = this.precompiler.precompileFile(file);
var precompiled = {};
precompiled[file] = hash;
var options = objectAssign({}, this.options, {
precompiled: precompiled
});
var emitter = fork(file, options);
runStatus.observeFork(emitter);
return emitter;
};
Api.prototype._onTimeout = function (runStatus) {
var timeout = ms(this.options.timeout);
var message = 'Exited because no new tests completed within the last ' + timeout + 'ms of inactivity';
runStatus.handleExceptions({
exception: new AvaError(message),
file: undefined
});
runStatus.emit('timeout');
};
Api.prototype.run = function (files, options) {
var self = this;
return new AvaFiles(files)
.findTestFiles()
.then(function (files) {
return self._run(files, options);
});
};
Api.prototype._run = function (files, _options) {
var self = this;
var runStatus = new RunStatus({
prefixTitles: this.options.explicitTitles || files.length > 1,
runOnlyExclusive: _options && _options.runOnlyExclusive,
base: path.relative('.', commonPathPrefix(files)) + path.sep
});
if (self.options.timeout) {
var timeout = ms(self.options.timeout);
runStatus._restartTimer = debounce(function () {
self._onTimeout(runStatus);
}, timeout);
runStatus._restartTimer();
runStatus.on('test', runStatus._restartTimer);
}
self.emit('test-run', runStatus, files);
if (files.length === 0) {
runStatus.handleExceptions({
exception: new AvaError('Couldn\'t find any files to test'),
file: undefined
});
return Promise.resolve([]);
}
var cacheEnabled = self.options.cacheEnabled !== false;
var cacheDir = (cacheEnabled && findCacheDir({name: 'ava', files: files})) ||
uniqueTempDir();
self.options.cacheDir = cacheDir;
self.precompiler = new CachingPrecompiler(cacheDir, self.options.babelConfig);
self.fileCount = files.length;
var tests = new Array(self.fileCount);
// TODO: thid should be cleared at the end of the run
runStatus.on('timeout', function () {
tests.forEach(function (fork) {
fork.exit();
});
});
////
// Begin @dcousineau's noodling
////
const emptyResults = {
stats: {
testCount: 0,
passCount: 0,
skipCount: 0,
todoCount: 0,
failCount: 0
},
tests: []
};
return new Promise(function (resolve) {
Promise.map(files, (file, index) => {
function runner(test, resolve, reject) {
return function() {
self.emit('ready');
var options = {};
test.run(options)
.then(resolve)
.catch(function (err) {
// The test failed catastrophically. Flag it up as an
// exception, then return an empty result. Other tests may
// continue to run.
runStatus.handleExceptions({
exception: err,
file: path.relative('.', file)
});
resolve(emptyResults);
});
};
}
try {
var test = tests[index] = self._runFile(file, runStatus);
return new Promise((resolve, reject) => {
test.on('stats', runner(test, resolve, reject));
test.catch(runner(test, resolve, reject));
});
} catch (err) {
runStatus.handleExceptions({
exception: err,
file: path.relative('.', file)
});
reject(err);
}
}, {concurrency: 5})
.then(results => {
// cancel debounced _onTimeout() from firing
if (self.options.timeout) {
runStatus._restartTimer.cancel();
}
runStatus.processResults(results);
resolve(runStatus);
})
.catch(err => {
runStatus.processResults([]);
resolve(runStatus);
});
});
////
// End @dcousineau's noodling
////
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment