wallet-core/node_modules/ava/lib/watcher.js

359 lines
8.9 KiB
JavaScript
Raw Normal View History

2017-05-28 00:38:50 +02:00
'use strict';
const nodePath = require('path');
const debug = require('debug')('ava:watcher');
const diff = require('lodash.difference');
const chokidar = require('chokidar');
const flatten = require('arr-flatten');
const union = require('array-union');
const uniq = require('array-uniq');
const AvaFiles = require('./ava-files');
function rethrowAsync(err) {
// Don't swallow exceptions. Note that any
// expected error should already have been logged
setImmediate(() => {
throw err;
});
}
2017-08-14 05:01:11 +02:00
const MIN_DEBOUNCE_DELAY = 10;
const INITIAL_DEBOUNCE_DELAY = 100;
2017-05-28 00:38:50 +02:00
class Debouncer {
constructor(watcher) {
this.watcher = watcher;
this.timer = null;
this.repeat = false;
}
2017-08-14 05:01:11 +02:00
debounce(delay) {
2017-05-28 00:38:50 +02:00
if (this.timer) {
this.again = true;
return;
}
2017-08-14 05:01:11 +02:00
delay = delay ? Math.max(delay, MIN_DEBOUNCE_DELAY) : INITIAL_DEBOUNCE_DELAY;
2017-05-28 00:38:50 +02:00
const timer = setTimeout(() => {
this.watcher.busy.then(() => {
// Do nothing if debouncing was canceled while waiting for the busy
// promise to fulfil
if (this.timer !== timer) {
return;
}
if (this.again) {
this.timer = null;
this.again = false;
2017-08-14 05:01:11 +02:00
this.debounce(delay / 2);
2017-05-28 00:38:50 +02:00
} else {
this.watcher.runAfterChanges();
this.timer = null;
this.again = false;
}
});
2017-08-14 05:01:11 +02:00
}, delay);
2017-05-28 00:38:50 +02:00
this.timer = timer;
}
cancel() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
this.again = false;
}
}
}
class TestDependency {
constructor(file, sources) {
this.file = file;
this.sources = sources;
}
contains(source) {
return this.sources.indexOf(source) !== -1;
}
}
class Watcher {
constructor(logger, api, files, sources) {
this.debouncer = new Debouncer(this);
this.avaFiles = new AvaFiles({
files,
sources
});
this.clearLogOnNextRun = true;
this.runVector = 0;
2017-08-14 05:01:11 +02:00
this.previousFiles = files;
this.run = (specificFiles, updateSnapshots) => {
2017-05-28 00:38:50 +02:00
if (this.runVector > 0) {
const cleared = this.clearLogOnNextRun && logger.clear();
if (!cleared) {
logger.reset();
logger.section();
}
this.clearLogOnNextRun = true;
logger.reset();
logger.start();
}
this.runVector += 1;
const currentVector = this.runVector;
let runOnlyExclusive = false;
if (specificFiles) {
const exclusiveFiles = specificFiles.filter(file => this.filesWithExclusiveTests.indexOf(file) !== -1);
runOnlyExclusive = exclusiveFiles.length !== this.filesWithExclusiveTests.length;
if (runOnlyExclusive) {
// The test files that previously contained exclusive tests are always
// run, together with the remaining specific files.
const remainingFiles = diff(specificFiles, exclusiveFiles);
specificFiles = this.filesWithExclusiveTests.concat(remainingFiles);
}
}
2017-08-14 05:01:11 +02:00
this.touchedFiles.clear();
this.previousFiles = specificFiles || files;
this.busy = api.run(this.previousFiles, {runOnlyExclusive, updateSnapshots: updateSnapshots === true})
2017-05-28 00:38:50 +02:00
.then(runStatus => {
runStatus.previousFailCount = this.sumPreviousFailures(currentVector);
logger.finish(runStatus);
const badCounts = runStatus.failCount + runStatus.rejectionCount + runStatus.exceptionCount;
this.clearLogOnNextRun = this.clearLogOnNextRun && badCounts === 0;
})
.catch(rethrowAsync);
};
this.testDependencies = [];
this.trackTestDependencies(api, sources);
2017-08-14 05:01:11 +02:00
this.touchedFiles = new Set();
this.trackTouchedFiles(api);
2017-05-28 00:38:50 +02:00
this.filesWithExclusiveTests = [];
this.trackExclusivity(api);
this.filesWithFailures = [];
this.trackFailures(api);
this.dirtyStates = {};
this.watchFiles();
this.rerunAll();
}
watchFiles() {
const patterns = this.avaFiles.getChokidarPatterns();
chokidar.watch(patterns.paths, {
ignored: patterns.ignored,
ignoreInitial: true
}).on('all', (event, path) => {
if (event === 'add' || event === 'change' || event === 'unlink') {
debug('Detected %s of %s', event, path);
this.dirtyStates[path] = event;
this.debouncer.debounce();
}
});
}
trackTestDependencies(api) {
const relative = absPath => nodePath.relative(process.cwd(), absPath);
api.on('test-run', runStatus => {
runStatus.on('dependencies', (file, dependencies) => {
const sourceDeps = dependencies.map(relative).filter(this.avaFiles.isSource);
this.updateTestDependencies(file, sourceDeps);
});
});
}
updateTestDependencies(file, sources) {
if (sources.length === 0) {
this.testDependencies = this.testDependencies.filter(dep => dep.file !== file);
return;
}
const isUpdate = this.testDependencies.some(dep => {
if (dep.file !== file) {
return false;
}
dep.sources = sources;
return true;
});
if (!isUpdate) {
this.testDependencies.push(new TestDependency(file, sources));
}
}
2017-08-14 05:01:11 +02:00
trackTouchedFiles(api) {
api.on('test-run', runStatus => {
runStatus.on('touchedFiles', files => {
for (const file of files) {
this.touchedFiles.add(nodePath.relative(process.cwd(), file));
}
});
});
}
2017-05-28 00:38:50 +02:00
trackExclusivity(api) {
api.on('stats', stats => {
this.updateExclusivity(stats.file, stats.hasExclusive);
});
}
updateExclusivity(file, hasExclusiveTests) {
const index = this.filesWithExclusiveTests.indexOf(file);
if (hasExclusiveTests && index === -1) {
this.filesWithExclusiveTests.push(file);
} else if (!hasExclusiveTests && index !== -1) {
this.filesWithExclusiveTests.splice(index, 1);
}
}
trackFailures(api) {
api.on('test-run', (runStatus, files) => {
files.forEach(file => {
this.pruneFailures(nodePath.relative(process.cwd(), file));
});
const currentVector = this.runVector;
runStatus.on('error', err => {
this.countFailure(err.file, currentVector);
});
runStatus.on('test', result => {
if (result.error) {
this.countFailure(result.file, currentVector);
}
});
});
}
pruneFailures(file) {
this.filesWithFailures = this.filesWithFailures.filter(state => state.file !== file);
}
countFailure(file, vector) {
const isUpdate = this.filesWithFailures.some(state => {
if (state.file !== file) {
return false;
}
state.count++;
return true;
});
if (!isUpdate) {
this.filesWithFailures.push({
file,
vector,
count: 1
});
}
}
sumPreviousFailures(beforeVector) {
let total = 0;
this.filesWithFailures.forEach(state => {
if (state.vector < beforeVector) {
total += state.count;
}
});
return total;
}
cleanUnlinkedTests(unlinkedTests) {
unlinkedTests.forEach(testFile => {
this.updateTestDependencies(testFile, []);
this.updateExclusivity(testFile, false);
this.pruneFailures(testFile);
});
}
observeStdin(stdin) {
stdin.resume();
stdin.setEncoding('utf8');
stdin.on('data', data => {
data = data.trim().toLowerCase();
2017-08-14 05:01:11 +02:00
if (data !== 'r' && data !== 'rs' && data !== 'u') {
2017-05-28 00:38:50 +02:00
return;
}
// Cancel the debouncer, it might rerun specific tests whereas *all* tests
// need to be rerun
this.debouncer.cancel();
this.busy.then(() => {
// Cancel the debouncer again, it might have restarted while waiting for
// the busy promise to fulfil
this.debouncer.cancel();
this.clearLogOnNextRun = false;
2017-08-14 05:01:11 +02:00
if (data === 'u') {
this.updatePreviousSnapshots();
} else {
this.rerunAll();
}
2017-05-28 00:38:50 +02:00
});
});
}
rerunAll() {
this.dirtyStates = {};
this.run();
}
2017-08-14 05:01:11 +02:00
updatePreviousSnapshots() {
this.dirtyStates = {};
this.run(this.previousFiles, true);
}
2017-05-28 00:38:50 +02:00
runAfterChanges() {
const dirtyStates = this.dirtyStates;
this.dirtyStates = {};
2017-08-14 05:01:11 +02:00
const dirtyPaths = Object.keys(dirtyStates).filter(path => {
if (this.touchedFiles.has(path)) {
debug('Ignoring known touched file %s', path);
this.touchedFiles.delete(path);
return false;
}
return true;
});
2017-05-28 00:38:50 +02:00
const dirtyTests = dirtyPaths.filter(this.avaFiles.isTest);
const dirtySources = diff(dirtyPaths, dirtyTests);
const addedOrChangedTests = dirtyTests.filter(path => dirtyStates[path] !== 'unlink');
const unlinkedTests = diff(dirtyTests, addedOrChangedTests);
this.cleanUnlinkedTests(unlinkedTests);
// No need to rerun tests if the only change is that tests were deleted
if (unlinkedTests.length === dirtyPaths.length) {
return;
}
if (dirtySources.length === 0) {
// Run any new or changed tests
this.run(addedOrChangedTests);
return;
}
// Try to find tests that depend on the changed source files
const testsBySource = dirtySources.map(path => {
return this.testDependencies.filter(dep => dep.contains(path)).map(dep => {
debug('%s is a dependency of %s', path, dep.file);
return dep.file;
});
}, this).filter(tests => tests.length > 0);
// Rerun all tests if source files were changed that could not be traced to
// specific tests
if (testsBySource.length !== dirtySources.length) {
2017-08-14 05:01:11 +02:00
debug('Sources remain that cannot be traced to specific tests: %O', dirtySources);
debug('Rerunning all tests');
2017-05-28 00:38:50 +02:00
this.run();
return;
}
// Run all affected tests
this.run(union(addedOrChangedTests, uniq(flatten(testsBySource))));
}
}
module.exports = Watcher;