Skip to content

Commit

Permalink
Remove snapshot files when a test file stops using snapshots
Browse files Browse the repository at this point in the history
Fixes #1424.

Co-authored-by: Mark Wubben <mark@novemberborn.net>
  • Loading branch information
ninevra and novemberborn committed Dec 31, 2020
1 parent 98595da commit 4f093ab
Show file tree
Hide file tree
Showing 36 changed files with 710 additions and 5 deletions.
25 changes: 21 additions & 4 deletions lib/runner.js
Expand Up @@ -29,6 +29,8 @@ class Runner extends Emittery {

this.activeRunnables = new Set();
this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this);
this.skippedSnapshots = false;
this.boundSkipSnapshot = this.skipSnapshot.bind(this);
this.interrupted = false;
this.snapshots = null;
this.nextTaskIndex = 0;
Expand Down Expand Up @@ -199,8 +201,19 @@ class Runner extends Emittery {
return this.snapshots.compare(options);
}

skipSnapshot() {
this.skippedSnapshots = true;
}

saveSnapshotState() {
if (this.updateSnapshots && (this.runOnlyExclusive || this.skippingTests)) {
if (
this.updateSnapshots &&
(
this.runOnlyExclusive ||
this.skippingTests ||
this.skippedSnapshots
)
) {
return {cannotSave: true};
}

Expand All @@ -209,9 +222,11 @@ class Runner extends Emittery {
}

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!)
// were skipped. Perhaps emit a warning if this occurs?
return {touchedFiles: snapshotManager.cleanSnapshots({
file: this.file,
fixedLocation: this.snapshotDir,
projectDir: this.projectDir
})};
}

return {};
Expand Down Expand Up @@ -297,6 +312,7 @@ class Runner extends Emittery {
task.implementation :
t => task.implementation.apply(null, [t].concat(task.args)),
compareTestSnapshot: this.boundCompareTestSnapshot,
skipSnapshot: this.boundSkipSnapshot,
updateSnapshots: this.updateSnapshots,
metadata: {...task.metadata, associatedTaskIndex},
powerAssert: this.powerAssert,
Expand Down Expand Up @@ -349,6 +365,7 @@ class Runner extends Emittery {
task.implementation :
t => task.implementation.apply(null, [t].concat(task.args)),
compareTestSnapshot: this.boundCompareTestSnapshot,
skipSnapshot: this.boundSkipSnapshot,
updateSnapshots: this.updateSnapshots,
metadata: task.metadata,
powerAssert: this.powerAssert,
Expand Down
39 changes: 38 additions & 1 deletion lib/snapshot-manager.js
Expand Up @@ -449,12 +449,49 @@ const determineSnapshotDir = mem(({file, fixedLocation, projectDir}) => {

exports.determineSnapshotDir = determineSnapshotDir;

function load({file, fixedLocation, projectDir, recordNewSnapshots, updating}) {
function determineSnapshotPaths({file, fixedLocation, projectDir}) {
const dir = determineSnapshotDir({file, fixedLocation, projectDir});
const relFile = path.relative(projectDir, resolveSourceFile(file));
const name = path.basename(relFile);
const reportFile = `${name}.md`;
const snapFile = `${name}.snap`;

return {
dir,
relFile,
snapFile,
reportFile
};
}

function cleanFile(file) {
try {
fs.unlinkSync(file);
return [file];
} catch (error) {
if (error.code === 'ENOENT') {
return [];
}

throw error;
}
}

// Remove snapshot and report if they exist. Returns an array containing the
// paths of the touched files.
function cleanSnapshots({file, fixedLocation, projectDir}) {
const {dir, snapFile, reportFile} = determineSnapshotPaths({file, fixedLocation, projectDir});

return [
...cleanFile(path.join(dir, snapFile)),
...cleanFile(path.join(dir, reportFile))
];
}

exports.cleanSnapshots = cleanSnapshots;

function load({file, fixedLocation, projectDir, recordNewSnapshots, updating}) {
const {dir, relFile, snapFile, reportFile} = determineSnapshotPaths({file, fixedLocation, projectDir});
const snapPath = path.join(dir, snapFile);

let appendOnly = !updating;
Expand Down
4 changes: 4 additions & 0 deletions lib/test.js
Expand Up @@ -249,6 +249,10 @@ class Test {
};

this.skipSnapshot = () => {
if (typeof options.skipSnapshot === 'function') {
options.skipSnapshot();
}

if (options.updateSnapshots) {
this.addFailedAssertion(new Error('Snapshot assertions cannot be skipped when updating snapshots'));
} else {
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -123,6 +123,7 @@
"delay": "^4.4.0",
"esm": "^3.2.25",
"execa": "^5.0.0",
"fs-extra": "^9.0.1",
"get-stream": "^6.0.0",
"it-first": "^1.0.4",
"proxyquire": "^2.1.3",
Expand Down
@@ -0,0 +1 @@
{}
21 changes: 21 additions & 0 deletions test-tap/fixture/snapshots/watcher-rerun-unlink/test.js
@@ -0,0 +1,21 @@
const test = require('../../../..');

if (process.env.TEMPLATE) {
test('test title', t => {
t.snapshot({foo: 'bar'});
t.snapshot({answer: 42});
t.pass();
});

test('another test', t => {
t.snapshot(new Map());
});
} else {
test('test title', t => {
t.pass();
});

test('another test', t => {
t.pass();
});
}
46 changes: 46 additions & 0 deletions test-tap/integration/watcher.js
Expand Up @@ -82,6 +82,52 @@ test('watcher does not rerun test files when they write snapshot files', t => {
});
});

test('watcher does not rerun test files when they unlink snapshot files', t => {
// Run fixture as template to generate snapshots
execCli(
['--update-snapshots'],
{
dirname: 'fixture/snapshots/watcher-rerun-unlink',
env: {AVA_FORCE_CI: 'not-ci', TEMPLATE: 'true'}
},
err => {
t.ifError(err);

// Run fixture in watch mode; snapshots should be removed, and watcher should not rerun
let killed = false;

const child = execCli(
['--verbose', '--watch', '--update-snapshots', 'test.js'],
{
dirname: 'fixture/snapshots/watcher-rerun-unlink',
env: {AVA_FORCE_CI: 'not-ci'}
},
err => {
t.ok(killed);
t.ifError(err);
t.end();
}
);

let buffer = '';
let passedFirst = false;
child.stdout.on('data', string => {
buffer += string;
if (buffer.includes('2 tests passed') && !passedFirst) {
buffer = '';
passedFirst = true;
setTimeout(() => {
child.kill();
killed = true;
}, 500);
} else if (passedFirst && !killed) {
t.is(buffer.replace(/\s/g, '').replace(END_MESSAGE.replace(/\s/g, ''), ''), '');
}
});
}
);
});

test('watcher does not rerun test files when ignored files change', t => {
let killed = false;

Expand Down
30 changes: 30 additions & 0 deletions test-tap/test.js
Expand Up @@ -696,6 +696,36 @@ test('snapshot assertion can be skipped', t => {
});
});

// Snapshots reused from test/assert.js
test('snapshot assertions call options.skipSnapshot when skipped', async t => {
const projectDir = path.join(__dirname, 'fixture');
const manager = snapshotManager.load({
file: path.join(projectDir, 'assert.js'),
projectDir,
fixedLocation: null,
updating: false
});

const skipSnapshot = sinon.spy();

const test = new Test({
compareTestSnapshot: options => manager.compare(options),
skipSnapshot,
updateSnapshots: false,
metadata: {},
title: 'passes',
fn(t) {
t.snapshot.skip({not: {a: 'match'}});
t.snapshot.skip({not: {b: 'match'}});
t.snapshot(React.createElement(HelloMessage, {name: 'Sindre'}));
}
});

await test.run();

t.true(skipSnapshot.calledTwice);
});

test('snapshot assertion cannot be skipped when updating snapshots', t => {
return new Test({
updateSnapshots: true,
Expand Down
@@ -0,0 +1,5 @@
{
"ava": {
"snapshotDir": "fixedSnapshotDir"
}
}
22 changes: 22 additions & 0 deletions test/snapshot-removal/fixtures/fixed-snapshot-dir/test.js
@@ -0,0 +1,22 @@
const test = require(process.env.AVA_PATH); // This fixture is copied to a temporary directory, so require AVA through its configured path.

if (process.env.TEMPLATE) {
test('some snapshots', t => {
t.snapshot('foo');
t.snapshot('bar');
t.pass();
});

test('another snapshot', t => {
t.snapshot('baz');
t.pass();
});
} else {
test('some snapshots', t => {
t.pass();
});

test('another snapshot', t => {
t.pass();
});
}
1 change: 1 addition & 0 deletions test/snapshot-removal/fixtures/no-snapshots/package.json
@@ -0,0 +1 @@
{}
5 changes: 5 additions & 0 deletions test/snapshot-removal/fixtures/no-snapshots/test.js
@@ -0,0 +1,5 @@
const test = require('ava');

test('without snapshots', t => {
t.pass();
});
1 change: 1 addition & 0 deletions test/snapshot-removal/fixtures/only-test/package.json
@@ -0,0 +1 @@
{}
22 changes: 22 additions & 0 deletions test/snapshot-removal/fixtures/only-test/test.js
@@ -0,0 +1,22 @@
const test = require(process.env.AVA_PATH); // This fixture is copied to a temporary directory, so require AVA through its configured path.

if (process.env.TEMPLATE) {
test('some snapshots', t => {
t.snapshot('foo');
t.snapshot('bar');
t.pass();
});

test('another snapshot', t => {
t.snapshot('baz');
t.pass();
});
} else {
test.only('some snapshots', t => {
t.pass();
});

test('another snapshot', t => {
t.pass();
});
}
1 change: 1 addition & 0 deletions test/snapshot-removal/fixtures/removal/package.json
@@ -0,0 +1 @@
{}
22 changes: 22 additions & 0 deletions test/snapshot-removal/fixtures/removal/test.js
@@ -0,0 +1,22 @@
const test = require(process.env.AVA_PATH); // This fixture is copied to a temporary directory, so require AVA through its configured path.

if (process.env.TEMPLATE) {
test('some snapshots', t => {
t.snapshot('foo');
t.snapshot('bar');
t.pass();
});

test('another snapshot', t => {
t.snapshot('baz');
t.pass();
});
} else {
test('some snapshots', t => {
t.pass();
});

test('another snapshot', t => {
t.pass();
});
}
@@ -0,0 +1 @@
{}
23 changes: 23 additions & 0 deletions test/snapshot-removal/fixtures/skipped-snapshots-in-try/test.js
@@ -0,0 +1,23 @@
const test = require(process.env.AVA_PATH); // This fixture is copied to a temporary directory, so require AVA through its configured path.

if (process.env.TEMPLATE) {
test('skipped snapshots in try', async t => {
const attempt = await t.try(tt => {
tt.snapshot('in try');
});

attempt.commit();

t.pass();
});
} else {
test('skipped snapshots in try', async t => {
const attempt = await t.try(tt => {
tt.snapshot.skip('in try');
});

attempt.discard();

t.pass();
});
}
@@ -0,0 +1 @@
{}
23 changes: 23 additions & 0 deletions test/snapshot-removal/fixtures/skipped-snapshots/test.js
@@ -0,0 +1,23 @@
const test = require(process.env.AVA_PATH); // This fixture is copied to a temporary directory, so require AVA through its configured path.

if (process.env.TEMPLATE) {
test('some snapshots', t => {
t.snapshot('foo');
t.snapshot('bar');
t.pass();
});

test('another snapshot', t => {
t.snapshot('baz');
t.pass();
});
} else {
test('some snapshots', t => {
t.snapshot.skip('foo');
t.pass();
});

test('another snapshot', t => {
t.pass();
});
}
1 change: 1 addition & 0 deletions test/snapshot-removal/fixtures/skipped-tests/package.json
@@ -0,0 +1 @@
{}

0 comments on commit 4f093ab

Please sign in to comment.