-
Notifications
You must be signed in to change notification settings - Fork 29.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
assert: add snapshot assertion #44095
Changes from 7 commits
e45d3b1
95e5182
f8c945c
617e3bc
2a7a5db
fb0f0f7
d78f55f
b06b89a
0123718
441c4d5
be22762
97c3db2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
'use strict'; | ||
|
||
const { | ||
ArrayPrototypeMap, | ||
Boolean, | ||
RegExp, | ||
SafeMap, | ||
SafeSet, | ||
StringPrototypeIndexOf, | ||
StringPrototypeSlice, | ||
StringPrototypeSplit, | ||
StringPrototypeReplace, | ||
Symbol, | ||
} = primordials; | ||
|
||
const { EOL } = require('internal/constants'); | ||
const { codes: { ERR_ASSERT_SNAPSHOT_NOT_SUPPORTED } } = require('internal/errors'); | ||
const AssertionError = require('internal/assert/assertion_error'); | ||
const { inspect } = require('internal/util/inspect'); | ||
const { validateString } = require('internal/validators'); | ||
const { once } = require('events'); | ||
const { createReadStream, createWriteStream } = require('fs'); | ||
const path = require('path'); | ||
const assert = require('assert'); | ||
|
||
const kUpdateSnapshot = Boolean(process.env.NODE_UPDATE_SNAPSHOT); | ||
const kInitialSnapshot = Symbol('kInitialSnapshot'); | ||
const kDefaultDelimiter = '\n#*#*#*#*#*#*#*#*#*#*#*#\n'; | ||
const kDefaultDelimiterRegex = new RegExp(kDefaultDelimiter.replaceAll('*', '\\*').replaceAll('\n', '\r?\n'), 'g'); | ||
|
||
function getSnapshotPath() { | ||
if (process.mainModule) { | ||
const { dir, name } = path.parse(process.mainModule.filename); | ||
return path.join(dir, `${name}.snapshot`); | ||
} | ||
if (!process.argv[1]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this to check if we're in the REPL? wouldn't it be better to just check if there is no There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. esm does not have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't read the code, but reminder that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it is documented to not be supported at this stage. |
||
throw new ERR_ASSERT_SNAPSHOT_NOT_SUPPORTED(); | ||
} | ||
const { dir, name } = path.parse(process.argv[1]); | ||
return path.join(dir, `${name}.snapshot`); | ||
} | ||
|
||
function getSource() { | ||
return createReadStream(getSnapshotPath(), { encoding: 'utf8' }); | ||
} | ||
|
||
let _target; | ||
function getTarget() { | ||
_target ??= createWriteStream(getSnapshotPath(), { encoding: 'utf8' }); | ||
return _target; | ||
} | ||
|
||
function serializeName(name) { | ||
validateString(name, 'name'); | ||
return StringPrototypeReplace(`${name}`, /:\r?\n/g, '_'); | ||
} | ||
|
||
let writtenNames; | ||
let snapshotValue; | ||
let counter = 0; | ||
|
||
async function writeSnapshot({ name, value }) { | ||
const target = getTarget(); | ||
if (counter > 1) { | ||
target.write(kDefaultDelimiter); | ||
} | ||
writtenNames = writtenNames || new SafeSet(); | ||
if (writtenNames.has(name)) { | ||
throw new AssertionError({ message: `Snapshot "${name}" already used` }); | ||
} | ||
writtenNames.add(name); | ||
const drained = target.write(`${name}:\n${value}`); | ||
await drained || once(target, 'drain'); | ||
} | ||
|
||
async function getSnapshot() { | ||
if (snapshotValue !== undefined) { | ||
return snapshotValue; | ||
} | ||
if (kUpdateSnapshot) { | ||
snapshotValue = kInitialSnapshot; | ||
return kInitialSnapshot; | ||
} | ||
try { | ||
const source = getSource(); | ||
let data = ''; | ||
MoLow marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for await (const line of source) { | ||
data += line; | ||
} | ||
snapshotValue = new SafeMap( | ||
ArrayPrototypeMap( | ||
StringPrototypeSplit(data, kDefaultDelimiterRegex), | ||
(item) => { | ||
const keyDelimiter = StringPrototypeIndexOf(item, `:${EOL}`); | ||
return [StringPrototypeSlice(item, 0, keyDelimiter), StringPrototypeSlice(item, keyDelimiter + 2)]; | ||
} | ||
)); | ||
} catch (e) { | ||
if (e.code === 'ENOENT') { | ||
snapshotValue = kInitialSnapshot; | ||
} else { | ||
throw e; | ||
} | ||
} | ||
return snapshotValue; | ||
} | ||
|
||
|
||
async function snapshot(input, name) { | ||
const snapshot = await getSnapshot(); | ||
counter = counter + 1; | ||
name = serializeName(name); | ||
|
||
const value = inspect(input); | ||
if (snapshot === kInitialSnapshot) { | ||
await writeSnapshot({ name, value }); | ||
} else if (snapshot.has(name)) { | ||
const expected = snapshot.get(name); | ||
// eslint-disable-next-line no-restricted-syntax | ||
assert.strictEqual(value, expected); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's kind of a bummer that this does string comparison and not |
||
} else { | ||
throw new AssertionError({ message: `Snapshot "${name}" does not exist` }); | ||
} | ||
} | ||
|
||
module.exports = snapshot; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import assert from 'node:assert'; | ||
|
||
await assert.snapshot("test", "name"); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
name: | ||
'test' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import assert from 'node:assert'; | ||
|
||
await assert.snapshot("test", "name"); | ||
await assert.snapshot("test", "another name"); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
name: | ||
'test' | ||
#*#*#*#*#*#*#*#*#*#*#*# | ||
another name: | ||
'test' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import assert from 'node:assert'; | ||
|
||
await assert.snapshot("test", "another name"); | ||
await assert.snapshot("test", "non existing"); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
another name: | ||
'test' | ||
#*#*#*#*#*#*#*#*#*#*#*# | ||
name: | ||
'test' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import assert from 'node:assert'; | ||
|
||
function random() { | ||
return `Random Value: ${Math.random()}`; | ||
} | ||
function transform(value) { | ||
return value.replaceAll(/Random Value: \d+\.+\d+/g, 'Random Value: *'); | ||
} | ||
|
||
await assert.snapshot(transform(random()), 'random1'); | ||
await assert.snapshot(transform(random()), 'random2'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
random1: | ||
'Random Value: *' | ||
#*#*#*#*#*#*#*#*#*#*#*# | ||
random2: | ||
'Random Value: *' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import assert from 'node:assert'; | ||
|
||
function fn() { | ||
this.should.be.a.fn(); | ||
return false; | ||
} | ||
|
||
await assert.snapshot(fn, 'function'); | ||
await assert.snapshot({ foo: "bar" }, 'object'); | ||
await assert.snapshot(new Set([1, 2, 3]), 'set'); | ||
await assert.snapshot(new Map([['one', 1], ['two', 2]]), 'map'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
function: | ||
[Function: fn] | ||
#*#*#*#*#*#*#*#*#*#*#*# | ||
object: | ||
{ foo: 'bar' } | ||
#*#*#*#*#*#*#*#*#*#*#*# | ||
set: | ||
Set(3) { 1, 2, 3 } | ||
#*#*#*#*#*#*#*#*#*#*#*# | ||
map: | ||
Map(2) { 'one' => 1, 'two' => 2 } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import assert from 'node:assert'; | ||
|
||
await assert.snapshot("test", "snapshot"); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
snapshot: | ||
'test' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import assert from 'node:assert'; | ||
|
||
await assert.snapshot("changed", "snapshot"); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
snapshot: | ||
'original' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we using an environment variable for this? Node has been trying to limit new environment variables in favor of CLI flags (which can also be passed via the
NODE_OPTIONS
environment variable).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updating to use
--update-assert-snapshot