Skip to content

Commit a8c908e

Browse files
authored
Detect more forms of snapshot file corruption
1 parent c57067b commit a8c908e

File tree

7 files changed

+106
-7
lines changed

7 files changed

+106
-7
lines changed

lib/reporters/improper-usage-messages.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Visit the following URL for more details:
2020
if (assertion === 'snapshot') {
2121
const {name, snapPath} = error.improperUsage;
2222

23-
if (name === 'ChecksumError') {
23+
if (name === 'ChecksumError' || name === 'InvalidSnapshotError') {
2424
return `The snapshot file is corrupted.
2525
2626
File path: ${chalk.yellow(snapPath)}

lib/snapshot-manager.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ export class VersionMismatchError extends SnapshotError {
5252
}
5353
}
5454

55+
export class InvalidSnapshotError extends SnapshotError {
56+
constructor(snapPath) {
57+
super('Invalid snapshot file', snapPath);
58+
this.name = 'InvalidSnapshotError';
59+
}
60+
}
61+
5562
const LEGACY_SNAPSHOT_HEADER = Buffer.from('// Jest Snapshot v1');
5663
function isLegacySnapshot(buffer) {
5764
return LEGACY_SNAPSHOT_HEADER.equals(buffer.slice(0, LEGACY_SNAPSHOT_HEADER.byteLength));
@@ -194,7 +201,12 @@ function decodeSnapshots(buffer, snapPath) {
194201

195202
// The version starts after the readable prefix, which is ended by a newline
196203
// byte (0x0A).
197-
const versionOffset = buffer.indexOf(0x0A) + 1;
204+
const newline = buffer.indexOf(0x0A);
205+
if (newline === -1) {
206+
throw new InvalidSnapshotError(snapPath);
207+
}
208+
209+
const versionOffset = newline + 1;
198210
const version = buffer.readUInt16LE(versionOffset);
199211
if (version !== VERSION) {
200212
throw new VersionMismatchError(snapPath, version);
@@ -469,11 +481,9 @@ export function load({file, fixedLocation, projectDir, recordNewSnapshots, updat
469481
blocksByTitle = new Map();
470482

471483
if (!updating) { // Discard all decoding errors when updating snapshots
472-
if (error instanceof SnapshotError) {
473-
snapshotError = error;
474-
} else {
475-
throw error;
476-
}
484+
snapshotError = error instanceof SnapshotError ?
485+
error :
486+
new InvalidSnapshotError(paths.snapPath);
477487
}
478488
}
479489

test/snapshot-tests/corrupt.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {promises as fs} from 'fs';
2+
import path from 'path';
3+
4+
import test from '@ava/test';
5+
6+
import {cwd, fixture} from '../helpers/exec.js';
7+
import {withTemporaryFixture} from '../helpers/with-temporary-fixture.js';
8+
9+
function countMatches(string, regex) {
10+
return [...string.matchAll(regex)].length;
11+
}
12+
13+
function countStringMatches(string, patternString) {
14+
if (patternString.length === 0) {
15+
throw new RangeError('Pattern must be non-empty');
16+
}
17+
18+
let index = string.indexOf(patternString);
19+
let matches = 0;
20+
while (index !== -1 && index < string.length) {
21+
matches++;
22+
index = string.indexOf(patternString, index + patternString.length);
23+
}
24+
25+
return matches;
26+
}
27+
28+
test('snapshot corruption is reported to the console', async t => {
29+
await withTemporaryFixture(cwd('corrupt'), async cwd => {
30+
const snapPath = path.join(cwd, 'test.js.snap');
31+
await fs.writeFile(snapPath, Uint8Array.of(0x00));
32+
const result = await t.throwsAsync(fixture([], {cwd}));
33+
t.snapshot(result.stats.failed, 'failed tests');
34+
t.is(countMatches(result.stdout, /The snapshot file is corrupted./g), 2);
35+
t.is(countMatches(result.stdout, /File path:/g), 2);
36+
t.is(
37+
countMatches(
38+
result.stdout,
39+
/Please run AVA again with the .*--update-snapshots.* flag to recreate it\./g
40+
),
41+
2
42+
);
43+
t.is(countStringMatches(result.stdout, snapPath), 2);
44+
});
45+
});
46+
47+
test('with --update-snapshots, corrupt snapshot files are overwritten', async t => {
48+
await withTemporaryFixture(cwd('corrupt'), async cwd => {
49+
const snapPath = path.join(cwd, 'test.js.snap');
50+
const env = {
51+
AVA_FORCE_CI: 'not-ci'
52+
};
53+
await fs.writeFile(snapPath, Uint8Array.of(0x00));
54+
await fixture(['--update-snapshots'], {cwd, env});
55+
56+
const snapContents = await fs.readFile(snapPath);
57+
t.not(snapContents.length, 1);
58+
});
59+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const test = require(process.env.TEST_AVA_IMPORT_FROM);
2+
3+
test('a snapshot', t => {
4+
t.snapshot('foo');
5+
});
6+
7+
test('a snapshot with a message', t => {
8+
t.snapshot('foo', 'a message');
9+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Snapshot report for `test/snapshot-tests/corrupt.js`
2+
3+
The actual snapshot is saved in `corrupt.js.snap`.
4+
5+
Generated by [AVA](https://avajs.dev).
6+
7+
## snapshot corruption is reported to the console
8+
9+
> failed tests
10+
11+
[
12+
{
13+
file: 'test.js',
14+
title: 'a snapshot',
15+
},
16+
{
17+
file: 'test.js',
18+
title: 'a snapshot with a message',
19+
},
20+
]
246 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)