diff --git a/lib/run-status.js b/lib/run-status.js index 6526f7bdc..609c79a87 100644 --- a/lib/run-status.js +++ b/lib/run-status.js @@ -73,6 +73,7 @@ class RunStatus extends EventEmitter { } handleTeardown(data) { this.emit('dependencies', data.file, data.dependencies, this); + this.emit('touchedFiles', data.touchedFiles); } handleStats(stats) { this.emit('stats', stats, this); diff --git a/lib/runner.js b/lib/runner.js index d2533c3a2..7a561c298 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -195,7 +195,10 @@ class Runner extends EventEmitter { saveSnapshotState() { if (this.snapshots) { - this.snapshots.save(); + const files = this.snapshots.save(); + if (files) { + this.emit('touched', files); + } } else if (this.updateSnapshots) { // TODO: There may be unused snapshot files if no test caused the // snapshots to be loaded. Prune them. But not if tests (including hooks!) diff --git a/lib/snapshot-manager.js b/lib/snapshot-manager.js index 8396d21b7..e2cd3a25d 100644 --- a/lib/snapshot-manager.js +++ b/lib/snapshot-manager.js @@ -5,11 +5,11 @@ const fs = require('fs'); const path = require('path'); const zlib = require('zlib'); +const writeFileAtomic = require('@ava/write-file-atomic'); const concordance = require('concordance'); const indentString = require('indent-string'); const makeDir = require('make-dir'); const md5Hex = require('md5-hex'); -const writeFileAtomic = require('write-file-atomic'); const concordanceOptions = require('./concordance-options').snapshotManager; @@ -333,7 +333,7 @@ class Manager { save() { if (!this.hasChanges) { - return; + return null; } const snapPath = path.join(this.dir, this.snapFile); @@ -346,8 +346,10 @@ class Manager { generateReport(this.relFile, this.snapFile, this.reportEntries); makeDir.sync(this.dir); - writeFileAtomic.sync(snapPath, buffer); - writeFileAtomic.sync(reportPath, reportBuffer); + const tmpSnapPath = writeFileAtomic.sync(snapPath, buffer); + const tmpReportPath = writeFileAtomic.sync(reportPath, reportBuffer); + + return [tmpSnapPath, tmpReportPath, snapPath, reportPath]; } } diff --git a/lib/test-worker.js b/lib/test-worker.js index aa1ca998c..10faa10c0 100644 --- a/lib/test-worker.js +++ b/lib/test-worker.js @@ -42,10 +42,17 @@ const testPath = opts.file; const dependencies = []; adapter.installDependencyTracking(dependencies, testPath); +const touchedFiles = new Set(); + // Set when main.js is required (since test files should have `require('ava')`). let runner = null; exports.setRunner = newRunner => { runner = newRunner; + runner.on('touched', files => { + for (const file of files) { + touchedFiles.add(file); + } + }); }; require(testPath); @@ -123,8 +130,9 @@ process.on('ava-teardown', () => { // Include dependencies in the final teardown message. This ensures the full // set of dependencies is included no matter how the process exits, unless - // it flat out crashes. - adapter.send('teardown', {dependencies}); + // it flat out crashes. Also include any files that AVA touched during the + // test run. This allows the watcher to ignore modifications to those files. + adapter.send('teardown', {dependencies, touchedFiles: Array.from(touchedFiles)}); }); process.on('ava-exit', () => { diff --git a/lib/watcher.js b/lib/watcher.js index 66dc9f1e5..d07a155a8 100644 --- a/lib/watcher.js +++ b/lib/watcher.js @@ -116,6 +116,7 @@ class Watcher { } } + this.touchedFiles.clear(); this.busy = api.run(specificFiles || files, {runOnlyExclusive}) .then(runStatus => { runStatus.previousFailCount = this.sumPreviousFailures(currentVector); @@ -130,6 +131,9 @@ class Watcher { this.testDependencies = []; this.trackTestDependencies(api, sources); + this.touchedFiles = new Set(); + this.trackTouchedFiles(api); + this.filesWithExclusiveTests = []; this.trackExclusivity(api); @@ -184,6 +188,15 @@ class Watcher { this.testDependencies.push(new TestDependency(file, sources)); } } + trackTouchedFiles(api) { + api.on('test-run', runStatus => { + runStatus.on('touchedFiles', files => { + for (const file of files) { + this.touchedFiles.add(nodePath.relative(process.cwd(), file)); + } + }); + }); + } trackExclusivity(api) { api.on('stats', stats => { this.updateExclusivity(stats.file, stats.hasExclusive); @@ -284,7 +297,14 @@ class Watcher { const dirtyStates = this.dirtyStates; this.dirtyStates = {}; - const dirtyPaths = Object.keys(dirtyStates); + 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; + }); const dirtyTests = dirtyPaths.filter(this.avaFiles.isTest); const dirtySources = diff(dirtyPaths, dirtyTests); const addedOrChangedTests = dirtyTests.filter(path => dirtyStates[path] !== 'unlink'); diff --git a/package-lock.json b/package-lock.json index a024f06b8..60f4874a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,11 @@ "resolved": "https://registry.npmjs.org/@ava/babel-preset-transform-test-files/-/babel-preset-transform-test-files-3.0.0.tgz", "integrity": "sha1-ze0RlqjY2TgaUJJAq5LpGl7Aafc=" }, + "@ava/write-file-atomic": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ava/write-file-atomic/-/write-file-atomic-2.2.0.tgz", + "integrity": "sha512-BTNB3nGbEfJT+69wuqXFr/bQH7Vr7ihx2xGOMNqPgDGhwspoZhiWumDDZNjBy7AScmqS5CELIOGtPVXESyrnDA==" + }, "@concordance/react": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@concordance/react/-/react-0.2.0.tgz", diff --git a/package.json b/package.json index d198603d1..43ec27d81 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "dependencies": { "@ava/babel-preset-stage-4": "^1.1.0", "@ava/babel-preset-transform-test-files": "^3.0.0", + "@ava/write-file-atomic": "^2.2.0", "@concordance/react": "^0.2.0", "ansi-escapes": "^2.0.0", "ansi-styles": "^3.1.0", @@ -170,8 +171,7 @@ "time-require": "^0.1.2", "trim-off-newlines": "^1.0.1", "unique-temp-dir": "^1.0.0", - "update-notifier": "^2.1.0", - "write-file-atomic": "^1.3.4" + "update-notifier": "^2.1.0" }, "devDependencies": { "cli-table2": "^0.2.0", diff --git a/test/cli.js b/test/cli.js index 5a92ec3c7..6e008da9a 100644 --- a/test/cli.js +++ b/test/cli.js @@ -307,6 +307,32 @@ test('watcher reruns test files when source dependencies change', t => { }); }); +test('watcher does not rerun test files when they write snapshot files', t => { + let killed = false; + + const child = execCli(['--verbose', '--watch', '--update-snapshots', 'test.js'], {dirname: 'fixture/snapshots'}, err => { + t.ok(killed); + t.ifError(err); + t.end(); + }); + + let buffer = ''; + let passedFirst = false; + child.stderr.on('data', str => { + buffer += str; + if (/2 tests passed/.test(buffer) && !passedFirst) { + buffer = ''; + passedFirst = true; + setTimeout(() => { + child.kill(); + killed = true; + }, 500); + } else if (passedFirst && !killed) { + t.is(buffer.replace(/\s/g, ''), ''); + } + }); +}); + test('`"tap": true` config is ignored when --watch is given', t => { let killed = false; diff --git a/test/fixture/snapshots/package.json b/test/fixture/snapshots/package.json new file mode 100644 index 000000000..2213e8988 --- /dev/null +++ b/test/fixture/snapshots/package.json @@ -0,0 +1,5 @@ +{ + "ava": { + "source": "**/*" + } +}