Skip to content

Commit

Permalink
Detect more forms of snapshot file corruption
Browse files Browse the repository at this point in the history
  • Loading branch information
ninevra committed Jun 10, 2021
1 parent c57067b commit a8c908e
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 7 deletions.
2 changes: 1 addition & 1 deletion lib/reporters/improper-usage-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Visit the following URL for more details:
if (assertion === 'snapshot') {
const {name, snapPath} = error.improperUsage;

if (name === 'ChecksumError') {
if (name === 'ChecksumError' || name === 'InvalidSnapshotError') {
return `The snapshot file is corrupted.
File path: ${chalk.yellow(snapPath)}
Expand Down
22 changes: 16 additions & 6 deletions lib/snapshot-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ export class VersionMismatchError extends SnapshotError {
}
}

export class InvalidSnapshotError extends SnapshotError {
constructor(snapPath) {
super('Invalid snapshot file', snapPath);
this.name = 'InvalidSnapshotError';
}
}

const LEGACY_SNAPSHOT_HEADER = Buffer.from('// Jest Snapshot v1');
function isLegacySnapshot(buffer) {
return LEGACY_SNAPSHOT_HEADER.equals(buffer.slice(0, LEGACY_SNAPSHOT_HEADER.byteLength));
Expand Down Expand Up @@ -194,7 +201,12 @@ function decodeSnapshots(buffer, snapPath) {

// The version starts after the readable prefix, which is ended by a newline
// byte (0x0A).
const versionOffset = buffer.indexOf(0x0A) + 1;
const newline = buffer.indexOf(0x0A);
if (newline === -1) {
throw new InvalidSnapshotError(snapPath);
}

const versionOffset = newline + 1;
const version = buffer.readUInt16LE(versionOffset);
if (version !== VERSION) {
throw new VersionMismatchError(snapPath, version);
Expand Down Expand Up @@ -469,11 +481,9 @@ export function load({file, fixedLocation, projectDir, recordNewSnapshots, updat
blocksByTitle = new Map();

if (!updating) { // Discard all decoding errors when updating snapshots
if (error instanceof SnapshotError) {
snapshotError = error;
} else {
throw error;
}
snapshotError = error instanceof SnapshotError ?
error :
new InvalidSnapshotError(paths.snapPath);
}
}

Expand Down
59 changes: 59 additions & 0 deletions test/snapshot-tests/corrupt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {promises as fs} from 'fs';
import path from 'path';

import test from '@ava/test';

import {cwd, fixture} from '../helpers/exec.js';
import {withTemporaryFixture} from '../helpers/with-temporary-fixture.js';

function countMatches(string, regex) {
return [...string.matchAll(regex)].length;
}

function countStringMatches(string, patternString) {
if (patternString.length === 0) {
throw new RangeError('Pattern must be non-empty');
}

let index = string.indexOf(patternString);
let matches = 0;
while (index !== -1 && index < string.length) {
matches++;
index = string.indexOf(patternString, index + patternString.length);
}

return matches;
}

test('snapshot corruption is reported to the console', async t => {
await withTemporaryFixture(cwd('corrupt'), async cwd => {
const snapPath = path.join(cwd, 'test.js.snap');
await fs.writeFile(snapPath, Uint8Array.of(0x00));
const result = await t.throwsAsync(fixture([], {cwd}));
t.snapshot(result.stats.failed, 'failed tests');
t.is(countMatches(result.stdout, /The snapshot file is corrupted./g), 2);
t.is(countMatches(result.stdout, /File path:/g), 2);
t.is(
countMatches(
result.stdout,
/Please run AVA again with the .*--update-snapshots.* flag to recreate it\./g
),
2
);
t.is(countStringMatches(result.stdout, snapPath), 2);
});
});

test('with --update-snapshots, corrupt snapshot files are overwritten', async t => {
await withTemporaryFixture(cwd('corrupt'), async cwd => {
const snapPath = path.join(cwd, 'test.js.snap');
const env = {
AVA_FORCE_CI: 'not-ci'
};
await fs.writeFile(snapPath, Uint8Array.of(0x00));
await fixture(['--update-snapshots'], {cwd, env});

const snapContents = await fs.readFile(snapPath);
t.not(snapContents.length, 1);
});
});
1 change: 1 addition & 0 deletions test/snapshot-tests/fixtures/corrupt/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
9 changes: 9 additions & 0 deletions test/snapshot-tests/fixtures/corrupt/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const test = require(process.env.TEST_AVA_IMPORT_FROM);

test('a snapshot', t => {
t.snapshot('foo');
});

test('a snapshot with a message', t => {
t.snapshot('foo', 'a message');
});
20 changes: 20 additions & 0 deletions test/snapshot-tests/snapshots/corrupt.js.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Snapshot report for `test/snapshot-tests/corrupt.js`

The actual snapshot is saved in `corrupt.js.snap`.

Generated by [AVA](https://avajs.dev).

## snapshot corruption is reported to the console

> failed tests
[
{
file: 'test.js',
title: 'a snapshot',
},
{
file: 'test.js',
title: 'a snapshot with a message',
},
]
Binary file added test/snapshot-tests/snapshots/corrupt.js.snap
Binary file not shown.

0 comments on commit a8c908e

Please sign in to comment.