1'use strict';
2const common = require('.');
3const path = require('node:path');
4const test = require('node:test');
5const fs = require('node:fs/promises');
6const assert = require('node:assert/strict');
7
8const stackFramesRegexp = /(?<=\n)(\s+)((.+?)\s+\()?(?:\(?(.+?):(\d+)(?::(\d+))?)\)?(\s+\{)?(\[\d+m)?(\n|$)/g;
9const windowNewlineRegexp = /\r/g;
10
11function replaceNodeVersion(str) {
12  return str.replaceAll(process.version, '*');
13}
14
15function replaceStackTrace(str, replacement = '$1*$7$8\n') {
16  return str.replace(stackFramesRegexp, replacement);
17}
18
19function replaceWindowsLineEndings(str) {
20  return str.replace(windowNewlineRegexp, '');
21}
22
23function replaceWindowsPaths(str) {
24  return common.isWindows ? str.replaceAll(path.win32.sep, path.posix.sep) : str;
25}
26
27function replaceFullPaths(str) {
28  return str.replaceAll(process.cwd(), '');
29}
30
31function transform(...args) {
32  return (str) => args.reduce((acc, fn) => fn(acc), str);
33}
34
35function getSnapshotPath(filename) {
36  const { name, dir } = path.parse(filename);
37  return path.resolve(dir, `${name}.snapshot`);
38}
39
40async function assertSnapshot(actual, filename = process.argv[1]) {
41  const snapshot = getSnapshotPath(filename);
42  if (process.env.NODE_REGENERATE_SNAPSHOTS) {
43    await fs.writeFile(snapshot, actual);
44  } else {
45    let expected;
46    try {
47      expected = await fs.readFile(snapshot, 'utf8');
48    } catch (e) {
49      if (e.code === 'ENOENT') {
50        console.log(
51          'Snapshot file does not exist. You can create a new one by running the test with NODE_REGENERATE_SNAPSHOTS=1',
52        );
53      }
54      throw e;
55    }
56    assert.strictEqual(actual, replaceWindowsLineEndings(expected));
57  }
58}
59
60/**
61 * Spawn a process and assert its output against a snapshot.
62 * if you want to automatically update the snapshot, run tests with NODE_REGENERATE_SNAPSHOTS=1
63 * transform is a function that takes the output and returns a string that will be compared against the snapshot
64 * this is useful for normalizing output such as stack traces
65 * there are some predefined transforms in this file such as replaceStackTrace and replaceWindowsLineEndings
66 * both of which can be used as an example for writing your own
67 * compose multiple transforms by passing them as arguments to the transform function:
68 * assertSnapshot.transform(assertSnapshot.replaceStackTrace, assertSnapshot.replaceWindowsLineEndings)
69 * @param {string} filename
70 * @param {function(string): string} [transform]
71 * @param {object} [options] - control how the child process is spawned
72 * @param {boolean} [options.tty] - whether to spawn the process in a pseudo-tty
73 * @returns {Promise<void>}
74 */
75async function spawnAndAssert(filename, transform = (x) => x, { tty = false, ...options } = {}) {
76  if (tty && common.isWindows) {
77    test({ skip: 'Skipping pseudo-tty tests, as pseudo terminals are not available on Windows.' });
78    return;
79  }
80  const flags = common.parseTestFlags(filename);
81  const executable = tty ? 'tools/pseudo-tty.py' : process.execPath;
82  const args = tty ? [process.execPath, ...flags, filename] : [...flags, filename];
83  const { stdout, stderr } = await common.spawnPromisified(executable, args, options);
84  await assertSnapshot(transform(`${stdout}${stderr}`), filename);
85}
86
87module.exports = {
88  assertSnapshot,
89  getSnapshotPath,
90  replaceFullPaths,
91  replaceNodeVersion,
92  replaceStackTrace,
93  replaceWindowsLineEndings,
94  replaceWindowsPaths,
95  spawnAndAssert,
96  transform,
97};
98