GeezerVison is showing github.com — assert: add assert.Snapshot · nodejs/node@fb5884a
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/assert.md +28Lines changed: 28 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2006,6 +2006,32 @@ argument, then `error` is assumed to be omitted and the string will be used for | 2006 | 2006 | example in [`assert.throws()`][] carefully if using a string as the second | 2007 | 2007 | argument gets considered. | 2008 | 2008 |
| | 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 | + | 2009 | 2035 | ## `assert.strictEqual(actual, expected[, message])` | 2010 | 2036 |
| 2011 | 2037 | <!-- YAML | @@ -2442,6 +2468,7 @@ argument. | 2442 | 2468 | [Object wrappers]: https://developer.mozilla.org/en-US/docs/Glossary/Primitive#Primitive_wrapper_objects_in_JavaScript | 2443 | 2469 | [Object.prototype.toString()]: https://tc39.github.io/ecma262/#sec-object.prototype.tostring | 2444 | 2470 | [`!=` operator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Inequality | | 2471 | +[`--update-assert-snapshot`]: cli.md#--update-assert-snapshot | 2445 | 2472 | [`===` operator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality | 2446 | 2473 | [`==` operator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Equality | 2447 | 2474 | [`AssertionError`]: #class-assertassertionerror | @@ -2473,5 +2500,6 @@ argument. | 2473 | 2500 | [`process.on('exit')`]: process.md#event-exit | 2474 | 2501 | [`tracker.calls()`]: #trackercallsfn-exact | 2475 | 2502 | [`tracker.verify()`]: #trackerverify | | 2503 | +[`util.inspect()`]: util.md#utilinspectobject-options | 2476 | 2504 | [enumerable "own" properties]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties | 2477 | 2505 | [prototype-spec]: https://tc39.github.io/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots |
Collapse file Copy file name to clipboardExpand all lines: doc/api/cli.md +10Lines changed: 10 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1461,6 +1461,14 @@ occurs. One of the following modes can be chosen: | 1461 | 1461 | If a rejection happens during the command line entry point's ES module static | 1462 | 1462 | loading phase, it will always raise it as an uncaught exception. | 1463 | 1463 |
| | 1464 | +### `--update-assert-snapshot` | | 1465 | + | | 1466 | +<!-- YAML | | 1467 | +added: REPLACEME | | 1468 | +--> | | 1469 | + | | 1470 | +Force updating snapshot files for [`assert.snapshot()`][] | | 1471 | + | 1464 | 1472 | ### `--use-bundled-ca`, `--use-openssl-ca` | 1465 | 1473 |
| 1466 | 1474 | <!-- YAML | @@ -1819,6 +1827,7 @@ Node.js options that are allowed are: | 1819 | 1827 | * `--trace-warnings` | 1820 | 1828 | * `--track-heap-objects` | 1821 | 1829 | * `--unhandled-rejections` | | 1830 | +* `--update-assert-snapshot` | 1822 | 1831 | * `--use-bundled-ca` | 1823 | 1832 | * `--use-largepages` | 1824 | 1833 | * `--use-openssl-ca` | @@ -2189,6 +2198,7 @@ done | 2189 | 2198 | [`NO_COLOR`]: https://no-color.org | 2190 | 2199 | [`SlowBuffer`]: buffer.md#class-slowbuffer | 2191 | 2200 | [`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 | 2192 | 2202 | [`dns.lookup()`]: dns.md#dnslookuphostname-options-callback | 2193 | 2203 | [`dns.setDefaultResultOrder()`]: dns.md#dnssetdefaultresultorderorder | 2194 | 2204 | [`dnsPromises.lookup()`]: dns.md#dnspromiseslookuphostname-options | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/errors.md +7Lines changed: 7 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -705,6 +705,13 @@ A special type of error that can be triggered whenever Node.js detects an | 705 | 705 | exceptional logic violation that should never occur. These are raised typically | 706 | 706 | by the `node:assert` module. | 707 | 707 |
| | 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 | + | 708 | 715 | <a id="ERR_ASYNC_CALLBACK"></a> | 709 | 716 |
| 710 | 717 | ### `ERR_ASYNC_CALLBACK` | |
Collapse file Copy file name to clipboardExpand all lines: lib/assert.js +3Lines changed: 3 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1052,6 +1052,9 @@ assert.doesNotMatch = function doesNotMatch(string, regexp, message) { | 1052 | 1052 |
| 1053 | 1053 | assert.CallTracker = CallTracker; | 1054 | 1054 |
| | 1055 | +const snapshot = require('internal/assert/snapshot'); | | 1056 | +assert.snapshot = snapshot; | | 1057 | + | 1055 | 1058 | /** | 1056 | 1059 | * Expose a strict only variant of assert. | 1057 | 1060 | * @param {...any} args | |
Collapse file Copy file name to clipboard +129Lines changed: 129 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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; |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/errors.js +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -936,6 +936,8 @@ module.exports = { | 936 | 936 | E('ERR_AMBIGUOUS_ARGUMENT', 'The "%s" argument is ambiguous. %s', TypeError); | 937 | 937 | E('ERR_ARG_NOT_ITERABLE', '%s must be iterable', TypeError); | 938 | 938 | E('ERR_ASSERTION', '%s', Error); | | 939 | +E('ERR_ASSERT_SNAPSHOT_NOT_SUPPORTED', | | 940 | + 'Snapshot is not supported in this context ', TypeError); | 939 | 941 | E('ERR_ASYNC_CALLBACK', '%s must be a function', TypeError); | 940 | 942 | E('ERR_ASYNC_TYPE', 'Invalid name for async "type": %s', TypeError); | 941 | 943 | E('ERR_BROTLI_INVALID_PARAM', '%s is not a valid Brotli parameter', RangeError); | |
Collapse file Copy file name to clipboardExpand all lines: src/node_options.cc +5Lines changed: 5 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -618,6 +618,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { | 618 | 618 | &EnvironmentOptions::force_repl); | 619 | 619 | AddAlias("-i", "--interactive"); | 620 | 620 |
| | 621 | + AddOption("--update-assert-snapshot", | | 622 | + "update assert snapshot files", | | 623 | + &EnvironmentOptions::update_assert_snapshot, | | 624 | + kAllowedInEnvironment); | | 625 | + | 621 | 626 | AddOption("--napi-modules", "", NoOp{}, kAllowedInEnvironment); | 622 | 627 |
| 623 | 628 | AddOption("--tls-keylog", | |
Collapse file Copy file name to clipboardExpand all lines: src/node_options.h +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -136,6 +136,7 @@ class EnvironmentOptions : public Options { | 136 | 136 | bool preserve_symlinks = false; | 137 | 137 | bool preserve_symlinks_main = false; | 138 | 138 | bool prof_process = false; | | 139 | + bool update_assert_snapshot = false; | 139 | 140 | #if HAVE_INSPECTOR | 140 | 141 | std::string cpu_prof_dir; | 141 | 142 | static const uint64_t kDefaultCpuProfInterval = 1000; | |
Collapse file Copy file name to clipboard +3Lines changed: 3 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +import assert from 'node:assert'; | | 2 | + | | 3 | +await assert.snapshot("test", "name"); |
Collapse file Copy file name to clipboard +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +import assert from 'node:assert'; | | 2 | + | | 3 | +await assert.snapshot("test", "name"); | | 4 | +await assert.snapshot("test", "another name"); |
|