Skip to content

Commit fb5884a

Browse files
MoLowdanielleadams
authored andcommitted
assert: add assert.Snapshot
PR-URL: #44095 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent 2a75bce commit fb5884a

20 files changed

+380
-0
lines changed

doc/api/assert.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2006,6 +2006,32 @@ argument, then `error` is assumed to be omitted and the string will be used for
20062006
example in [`assert.throws()`][] carefully if using a string as the second
20072007
argument gets considered.
20082008

2009+
## `assert.snapshot(value, name)`
2010+
2011+
<!-- YAML
2012+
added: REPLACEME
2013+
-->
2014+
2015+
> Stability: 1 - Experimental
2016+
2017+
* `value` {any} the value to snapshot
2018+
* `name` {string} the name of snapshot.
2019+
* Returns: {Promise}
2020+
2021+
reads a snapshot from a file, and compares `value` to the snapshot.
2022+
`value` is serialized with [`util.inspect()`][]
2023+
If the value is not strictly equal to the snapshot,
2024+
`assert.snapshot()` will return a rejected `Promise`
2025+
with an [`AssertionError`][].
2026+
2027+
If the snapshot file does not exist, the snapshot is written.
2028+
2029+
In case it is needed to force a snapshot update,
2030+
use [`--update-assert-snapshot`][];
2031+
2032+
By default, a snapshot is read and written to a file,
2033+
using the same name as the main entrypoint with `.snapshot` as the extension.
2034+
20092035
## `assert.strictEqual(actual, expected[, message])`
20102036

20112037
<!-- YAML
@@ -2442,6 +2468,7 @@ argument.
24422468
[Object wrappers]: https://developer.mozilla.org/en-US/docs/Glossary/Primitive#Primitive_wrapper_objects_in_JavaScript
24432469
[Object.prototype.toString()]: https://tc39.github.io/ecma262/#sec-object.prototype.tostring
24442470
[`!=` operator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Inequality
2471+
[`--update-assert-snapshot`]: cli.md#--update-assert-snapshot
24452472
[`===` operator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality
24462473
[`==` operator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Equality
24472474
[`AssertionError`]: #class-assertassertionerror
@@ -2473,5 +2500,6 @@ argument.
24732500
[`process.on('exit')`]: process.md#event-exit
24742501
[`tracker.calls()`]: #trackercallsfn-exact
24752502
[`tracker.verify()`]: #trackerverify
2503+
[`util.inspect()`]: util.md#utilinspectobject-options
24762504
[enumerable "own" properties]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties
24772505
[prototype-spec]: https://tc39.github.io/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots

doc/api/cli.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,6 +1461,14 @@ occurs. One of the following modes can be chosen:
14611461
If a rejection happens during the command line entry point's ES module static
14621462
loading phase, it will always raise it as an uncaught exception.
14631463

1464+
### `--update-assert-snapshot`
1465+
1466+
<!-- YAML
1467+
added: REPLACEME
1468+
-->
1469+
1470+
Force updating snapshot files for [`assert.snapshot()`][]
1471+
14641472
### `--use-bundled-ca`, `--use-openssl-ca`
14651473

14661474
<!-- YAML
@@ -1819,6 +1827,7 @@ Node.js options that are allowed are:
18191827
* `--trace-warnings`
18201828
* `--track-heap-objects`
18211829
* `--unhandled-rejections`
1830+
* `--update-assert-snapshot`
18221831
* `--use-bundled-ca`
18231832
* `--use-largepages`
18241833
* `--use-openssl-ca`
@@ -2189,6 +2198,7 @@ done
21892198
[`NO_COLOR`]: https://no-color.org
21902199
[`SlowBuffer`]: buffer.md#class-slowbuffer
21912200
[`YoungGenerationSizeFromSemiSpaceSize`]: https://chromium.googlesource.com/v8/v8.git/+/refs/tags/10.3.129/src/heap/heap.cc#328
2201+
[`assert.snapshot()`]: assert.md#assertsnapshotvalue-name
21922202
[`dns.lookup()`]: dns.md#dnslookuphostname-options-callback
21932203
[`dns.setDefaultResultOrder()`]: dns.md#dnssetdefaultresultorderorder
21942204
[`dnsPromises.lookup()`]: dns.md#dnspromiseslookuphostname-options

doc/api/errors.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,13 @@ A special type of error that can be triggered whenever Node.js detects an
705705
exceptional logic violation that should never occur. These are raised typically
706706
by the `node:assert` module.
707707

708+
<a id="ERR_ASSERT_SNAPSHOT_NOT_SUPPORTED"></a>
709+
710+
### `ERR_ASSERT_SNAPSHOT_NOT_SUPPORTED`
711+
712+
An attempt was made to use `assert.snapshot()` in an environment that
713+
does not support snapshots, such as the REPL, or when using `node --eval`.
714+
708715
<a id="ERR_ASYNC_CALLBACK"></a>
709716

710717
### `ERR_ASYNC_CALLBACK`

lib/assert.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,9 @@ assert.doesNotMatch = function doesNotMatch(string, regexp, message) {
10521052

10531053
assert.CallTracker = CallTracker;
10541054

1055+
const snapshot = require('internal/assert/snapshot');
1056+
assert.snapshot = snapshot;
1057+
10551058
/**
10561059
* Expose a strict only variant of assert.
10571060
* @param {...any} args

lib/internal/assert/snapshot.js

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
'use strict';
2+
3+
const {
4+
ArrayPrototypeJoin,
5+
ArrayPrototypeMap,
6+
ArrayPrototypeSlice,
7+
RegExp,
8+
SafeMap,
9+
SafeSet,
10+
StringPrototypeSplit,
11+
StringPrototypeReplace,
12+
Symbol,
13+
} = primordials;
14+
15+
const { codes: { ERR_ASSERT_SNAPSHOT_NOT_SUPPORTED } } = require('internal/errors');
16+
const AssertionError = require('internal/assert/assertion_error');
17+
const { inspect } = require('internal/util/inspect');
18+
const { getOptionValue } = require('internal/options');
19+
const { validateString } = require('internal/validators');
20+
const { once } = require('events');
21+
const { createReadStream, createWriteStream } = require('fs');
22+
const path = require('path');
23+
const assert = require('assert');
24+
25+
const kUpdateSnapshot = getOptionValue('--update-assert-snapshot');
26+
const kInitialSnapshot = Symbol('kInitialSnapshot');
27+
const kDefaultDelimiter = '\n#*#*#*#*#*#*#*#*#*#*#*#\n';
28+
const kDefaultDelimiterRegex = new RegExp(kDefaultDelimiter.replaceAll('*', '\\*').replaceAll('\n', '\r?\n'), 'g');
29+
const kKeyDelimiter = /:\r?\n/g;
30+
31+
function getSnapshotPath() {
32+
if (process.mainModule) {
33+
const { dir, name } = path.parse(process.mainModule.filename);
34+
return path.join(dir, `${name}.snapshot`);
35+
}
36+
if (!process.argv[1]) {
37+
throw new ERR_ASSERT_SNAPSHOT_NOT_SUPPORTED();
38+
}
39+
const { dir, name } = path.parse(process.argv[1]);
40+
return path.join(dir, `${name}.snapshot`);
41+
}
42+
43+
function getSource() {
44+
return createReadStream(getSnapshotPath(), { encoding: 'utf8' });
45+
}
46+
47+
let _target;
48+
function getTarget() {
49+
_target ??= createWriteStream(getSnapshotPath(), { encoding: 'utf8' });
50+
return _target;
51+
}
52+
53+
function serializeName(name) {
54+
validateString(name, 'name');
55+
return StringPrototypeReplace(`${name}`, kKeyDelimiter, '_');
56+
}
57+
58+
let writtenNames;
59+
let snapshotValue;
60+
let counter = 0;
61+
62+
async function writeSnapshot({ name, value }) {
63+
const target = getTarget();
64+
if (counter > 1) {
65+
target.write(kDefaultDelimiter);
66+
}
67+
writtenNames = writtenNames || new SafeSet();
68+
if (writtenNames.has(name)) {
69+
throw new AssertionError({ message: `Snapshot "${name}" already used` });
70+
}
71+
writtenNames.add(name);
72+
const drained = target.write(`${name}:\n${value}`);
73+
await drained || once(target, 'drain');
74+
}
75+
76+
async function getSnapshot() {
77+
if (snapshotValue !== undefined) {
78+
return snapshotValue;
79+
}
80+
if (kUpdateSnapshot) {
81+
snapshotValue = kInitialSnapshot;
82+
return kInitialSnapshot;
83+
}
84+
try {
85+
const source = getSource();
86+
let data = '';
87+
for await (const line of source) {
88+
data += line;
89+
}
90+
snapshotValue = new SafeMap(
91+
ArrayPrototypeMap(
92+
StringPrototypeSplit(data, kDefaultDelimiterRegex),
93+
(item) => {
94+
const arr = StringPrototypeSplit(item, kKeyDelimiter);
95+
return [
96+
arr[0],
97+
ArrayPrototypeJoin(ArrayPrototypeSlice(arr, 1), ':\n'),
98+
];
99+
}
100+
));
101+
} catch (e) {
102+
if (e.code === 'ENOENT') {
103+
snapshotValue = kInitialSnapshot;
104+
} else {
105+
throw e;
106+
}
107+
}
108+
return snapshotValue;
109+
}
110+
111+
112+
async function snapshot(input, name) {
113+
const snapshot = await getSnapshot();
114+
counter = counter + 1;
115+
name = serializeName(name);
116+
117+
const value = inspect(input);
118+
if (snapshot === kInitialSnapshot) {
119+
await writeSnapshot({ name, value });
120+
} else if (snapshot.has(name)) {
121+
const expected = snapshot.get(name);
122+
// eslint-disable-next-line no-restricted-syntax
123+
assert.strictEqual(value, expected);
124+
} else {
125+
throw new AssertionError({ message: `Snapshot "${name}" does not exist`, actual: inspect(snapshot) });
126+
}
127+
}
128+
129+
module.exports = snapshot;

lib/internal/errors.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,8 @@ module.exports = {
936936
E('ERR_AMBIGUOUS_ARGUMENT', 'The "%s" argument is ambiguous. %s', TypeError);
937937
E('ERR_ARG_NOT_ITERABLE', '%s must be iterable', TypeError);
938938
E('ERR_ASSERTION', '%s', Error);
939+
E('ERR_ASSERT_SNAPSHOT_NOT_SUPPORTED',
940+
'Snapshot is not supported in this context ', TypeError);
939941
E('ERR_ASYNC_CALLBACK', '%s must be a function', TypeError);
940942
E('ERR_ASYNC_TYPE', 'Invalid name for async "type": %s', TypeError);
941943
E('ERR_BROTLI_INVALID_PARAM', '%s is not a valid Brotli parameter', RangeError);

src/node_options.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
618618
&EnvironmentOptions::force_repl);
619619
AddAlias("-i", "--interactive");
620620

621+
AddOption("--update-assert-snapshot",
622+
"update assert snapshot files",
623+
&EnvironmentOptions::update_assert_snapshot,
624+
kAllowedInEnvironment);
625+
621626
AddOption("--napi-modules", "", NoOp{}, kAllowedInEnvironment);
622627

623628
AddOption("--tls-keylog",

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ class EnvironmentOptions : public Options {
136136
bool preserve_symlinks = false;
137137
bool preserve_symlinks_main = false;
138138
bool prof_process = false;
139+
bool update_assert_snapshot = false;
139140
#if HAVE_INSPECTOR
140141
std::string cpu_prof_dir;
141142
static const uint64_t kDefaultCpuProfInterval = 1000;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import assert from 'node:assert';
2+
3+
await assert.snapshot("test", "name");
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import assert from 'node:assert';
2+
3+
await assert.snapshot("test", "name");
4+
await assert.snapshot("test", "another name");

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy