Skip to content

Commit e66b54c

Browse files
ninevrasindresorhusnovemberborn
authored
Deterministic snapshot files, declaration-ordered snapshot reports
Ensures that *.snap files are deterministic by sorting entry blocks by the hash of their test name or id. Ensures that *.md snapshot report files are sorted in as close to declaration order as is reasonably possible. Closes #2311 Closes #2324 Co-authored-by: Sindre Sorhus <[email protected]> Co-authored-by: Mark Wubben <[email protected]>
1 parent 73015e5 commit e66b54c

File tree

17 files changed

+547
-14
lines changed

17 files changed

+547
-14
lines changed

lib/runner.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class Runner extends Emittery {
3131
this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this);
3232
this.interrupted = false;
3333
this.snapshots = null;
34+
this.nextTaskIndex = 0;
3435
this.tasks = {
3536
after: [],
3637
afterAlways: [],
@@ -76,6 +77,8 @@ class Runner extends Emittery {
7677
});
7778
}
7879

80+
metadata.taskIndex = this.nextTaskIndex++;
81+
7982
const {args, buildTitle, implementations, rawTitle} = parseTestArgs(testArgs);
8083

8184
if (this.checkSelectedByLineNumbers) {
@@ -285,7 +288,7 @@ class Runner extends Emittery {
285288
return result;
286289
}
287290

288-
async runHooks(tasks, contextRef, titleSuffix, testPassed) {
291+
async runHooks(tasks, contextRef, {titleSuffix, testPassed, associatedTaskIndex} = {}) {
289292
const hooks = tasks.map(task => new Runnable({
290293
contextRef,
291294
experiments: this.experiments,
@@ -295,7 +298,7 @@ class Runner extends Emittery {
295298
t => task.implementation.apply(null, [t].concat(task.args)),
296299
compareTestSnapshot: this.boundCompareTestSnapshot,
297300
updateSnapshots: this.updateSnapshots,
298-
metadata: task.metadata,
301+
metadata: {...task.metadata, associatedTaskIndex},
299302
powerAssert: this.powerAssert,
300303
title: `${task.title}${titleSuffix || ''}`,
301304
isHook: true,
@@ -326,7 +329,14 @@ class Runner extends Emittery {
326329

327330
async runTest(task, contextRef) {
328331
const hookSuffix = ` for ${task.title}`;
329-
let hooksOk = await this.runHooks(this.tasks.beforeEach, contextRef, hookSuffix);
332+
let hooksOk = await this.runHooks(
333+
this.tasks.beforeEach,
334+
contextRef,
335+
{
336+
titleSuffix: hookSuffix,
337+
associatedTaskIndex: task.metadata.taskIndex
338+
}
339+
);
330340

331341
let testOk = false;
332342
if (hooksOk) {
@@ -358,7 +368,14 @@ class Runner extends Emittery {
358368
logs: result.logs
359369
});
360370

361-
hooksOk = await this.runHooks(this.tasks.afterEach, contextRef, hookSuffix, testOk);
371+
hooksOk = await this.runHooks(
372+
this.tasks.afterEach,
373+
contextRef,
374+
{
375+
titleSuffix: hookSuffix,
376+
testPassed: testOk,
377+
associatedTaskIndex: task.metadata.taskIndex
378+
});
362379
} else {
363380
this.emit('stateChange', {
364381
type: 'test-failed',
@@ -372,7 +389,14 @@ class Runner extends Emittery {
372389
}
373390
}
374391

375-
const alwaysOk = await this.runHooks(this.tasks.afterEachAlways, contextRef, hookSuffix, testOk);
392+
const alwaysOk = await this.runHooks(
393+
this.tasks.afterEachAlways,
394+
contextRef,
395+
{
396+
titleSuffix: hookSuffix,
397+
testPassed: testOk,
398+
associatedTaskIndex: task.metadata.taskIndex
399+
});
376400
return alwaysOk && hooksOk && testOk;
377401
}
378402

lib/snapshot-manager.js

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,32 @@ function combineEntries(entries) {
104104
const buffers = [];
105105
let byteLength = 0;
106106

107-
const sortedKeys = [...entries.keys()].sort();
107+
const sortedKeys = [...entries.keys()].sort((keyA, keyB) => {
108+
const [a, b] = [entries.get(keyA), entries.get(keyB)];
109+
const taskDifference = a.taskIndex - b.taskIndex;
110+
111+
if (taskDifference !== 0) {
112+
return taskDifference;
113+
}
114+
115+
const [assocA, assocB] = [a.associatedTaskIndex, b.associatedTaskIndex];
116+
if (assocA !== undefined && assocB !== undefined) {
117+
const assocDifference = assocA - assocB;
118+
119+
if (assocDifference !== 0) {
120+
return assocDifference;
121+
}
122+
}
123+
124+
return a.snapIndex - b.snapIndex;
125+
});
126+
108127
for (const key of sortedKeys) {
109128
const keyBuffer = Buffer.from(`\n\n## ${key}\n\n`, 'utf8');
110129
buffers.push(keyBuffer);
111130
byteLength += keyBuffer.byteLength;
112131

113-
const formattedEntries = entries.get(key);
132+
const formattedEntries = entries.get(key).buffers;
114133
const last = formattedEntries[formattedEntries.length - 1];
115134
for (const entry of formattedEntries) {
116135
buffers.push(entry);
@@ -176,10 +195,11 @@ function encodeSnapshots(buffersByHash) {
176195
byteOffset += 2;
177196

178197
const entries = [];
179-
for (const pair of buffersByHash) {
180-
const hash = pair[0];
181-
const snapshotBuffers = pair[1];
182-
198+
// Maps can't have duplicate keys, so all items in [...buffersByHash.keys()]
199+
// are unique, so sortedHashes should be deterministic.
200+
const sortedHashes = [...buffersByHash.keys()].sort();
201+
const sortedBuffersByHash = [...sortedHashes.map(hash => [hash, buffersByHash.get(hash)])];
202+
for (const [hash, snapshotBuffers] of sortedBuffersByHash) {
183203
buffers.push(Buffer.from(hash, 'hex'));
184204
byteOffset += MD5_HASH_LENGTH;
185205

@@ -332,6 +352,7 @@ class Manager {
332352
const descriptor = concordance.describe(options.expected, concordanceOptions);
333353
const snapshot = concordance.serialize(descriptor);
334354
const entry = formatEntry(options.label, descriptor);
355+
const {taskIndex, snapIndex, associatedTaskIndex} = options;
335356

336357
return () => { // Must be called in order!
337358
this.hasChanges = true;
@@ -353,9 +374,9 @@ class Manager {
353374
snapshots.push(snapshot);
354375

355376
if (this.reportEntries.has(options.belongsTo)) {
356-
this.reportEntries.get(options.belongsTo).push(entry);
377+
this.reportEntries.get(options.belongsTo).buffers.push(entry);
357378
} else {
358-
this.reportEntries.set(options.belongsTo, [entry]);
379+
this.reportEntries.set(options.belongsTo, {buffers: [entry], taskIndex, snapIndex, associatedTaskIndex});
359380
}
360381
};
361382
}

lib/test.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,17 @@ class Test {
230230
const index = id ? 0 : this.nextSnapshotIndex++;
231231
const label = id ? '' : message || `Snapshot ${index + 1}`; // Human-readable labels start counting at 1.
232232

233-
const {record, ...result} = options.compareTestSnapshot({belongsTo, deferRecording, expected, index, label});
233+
const {taskIndex, associatedTaskIndex} = this.metadata;
234+
const {record, ...result} = options.compareTestSnapshot({
235+
belongsTo,
236+
deferRecording,
237+
expected,
238+
index,
239+
label,
240+
taskIndex,
241+
snapIndex: this.snapshotCount,
242+
associatedTaskIndex
243+
});
234244
if (record) {
235245
this.deferredSnapshotRecordings.push(record);
236246
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const test = require('ava');
2+
3+
const reverse = process.env.INTERTEST_ORDER_REVERSE;
4+
5+
// Functions which resolve the corresponding promise to undefined
6+
let resolveOne;
7+
let resolveTwo;
8+
9+
// Promises with triggerable resolution
10+
const one = new Promise(resolve => {
11+
resolveOne = resolve;
12+
});
13+
14+
const two = new Promise(resolve => {
15+
resolveTwo = resolve;
16+
});
17+
18+
// Test cases which await the triggerable promises
19+
test('one', async t => {
20+
await one;
21+
t.snapshot('one');
22+
resolveTwo();
23+
});
24+
test('two', async t => {
25+
await two;
26+
t.snapshot('two');
27+
resolveOne();
28+
});
29+
30+
// Start resolution
31+
if (reverse) {
32+
resolveTwo();
33+
} else {
34+
resolveOne();
35+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
const test = require('ava');
2+
3+
const id = index => `index: ${index}`;
4+
const randomDelay = () => new Promise(resolve => {
5+
setTimeout(resolve, Math.random() * 1000);
6+
});
7+
8+
test.before(async t => {
9+
await randomDelay();
10+
t.snapshot(id(-2), 'in a before hook');
11+
});
12+
13+
test.beforeEach(async t => {
14+
await randomDelay();
15+
t.snapshot(id(-1.5), 'in a beforeEach hook');
16+
});
17+
18+
test.afterEach(async t => {
19+
await randomDelay();
20+
t.snapshot(id(-1), 'in an afterEach hook');
21+
});
22+
23+
test.afterEach.always(async t => {
24+
await randomDelay();
25+
t.snapshot(id(-0.5), 'in an afterEachAlways hook');
26+
});
27+
28+
test('B - declare some snapshots', async t => {
29+
await randomDelay();
30+
t.snapshot(id(0));
31+
t.snapshot(id(1), 'has a message');
32+
t.snapshot(id(2), 'also has a message');
33+
t.snapshot(id(3), {id: 'has an ID'});
34+
});
35+
36+
test('A - declare some more snapshots', async t => {
37+
await randomDelay();
38+
t.snapshot(id(4));
39+
});
40+
41+
test('C - declare some snapshots in a try()', async t => {
42+
await randomDelay();
43+
t.snapshot(id(5), 'outer');
44+
(await t.try('trying', t => {
45+
t.snapshot(id(6), 'inner');
46+
})).commit();
47+
t.snapshot(id(7), 'outer again');
48+
});
49+
50+
test('E - discard some snapshots in a try()', async t => {
51+
await randomDelay();
52+
t.snapshot(id(8), 'outer');
53+
(await t.try('trying', t => {
54+
t.snapshot(id(9), 'inner');
55+
})).discard();
56+
t.snapshot(id(10), 'outer again');
57+
});
58+
59+
test('D - more snapshots with IDs', async t => {
60+
await randomDelay();
61+
t.snapshot(id(11), {id: 'the first in test D'});
62+
t.snapshot(id(12));
63+
// These have to be reported in reverse declaration order, because they can't
64+
// be reported under the same header
65+
t.snapshot(id(14), {id: 'the second-to-last in test D'});
66+
t.snapshot(id(13));
67+
});

0 commit comments

Comments
 (0)