mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 10:59:27 +00:00
05ca03569e
This commit refactors the internals of snapshot tests to get the name of the test file from the test context instead of passing it to the SnapshotManager constructor. This is prep work for supporting running test files in the test runner process. PR-URL: https://github.com/nodejs/node/pull/53853 Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
253 lines
6.8 KiB
JavaScript
253 lines
6.8 KiB
JavaScript
'use strict';
|
|
const {
|
|
ArrayPrototypeJoin,
|
|
ArrayPrototypeMap,
|
|
ArrayPrototypeSlice,
|
|
ArrayPrototypeSort,
|
|
JSONStringify,
|
|
ObjectKeys,
|
|
SafeMap,
|
|
String,
|
|
StringPrototypeReplaceAll,
|
|
} = primordials;
|
|
const {
|
|
codes: {
|
|
ERR_INVALID_STATE,
|
|
},
|
|
} = require('internal/errors');
|
|
const { emitExperimentalWarning, kEmptyObject } = require('internal/util');
|
|
let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
|
|
debug = fn;
|
|
});
|
|
const {
|
|
validateArray,
|
|
validateFunction,
|
|
validateObject,
|
|
} = require('internal/validators');
|
|
const { strictEqual } = require('assert');
|
|
const { mkdirSync, readFileSync, writeFileSync } = require('fs');
|
|
const { dirname } = require('path');
|
|
const { createContext, runInContext } = require('vm');
|
|
const kExperimentalWarning = 'Snapshot testing';
|
|
const kMissingSnapshotTip = 'Missing snapshots can be generated by rerunning ' +
|
|
'the command with the --test-update-snapshots flag.';
|
|
const defaultSerializers = [
|
|
(value) => { return JSONStringify(value, null, 2); },
|
|
];
|
|
|
|
function defaultResolveSnapshotPath(testPath) {
|
|
if (typeof testPath !== 'string') {
|
|
return testPath;
|
|
}
|
|
|
|
return `${testPath}.snapshot`;
|
|
}
|
|
|
|
let resolveSnapshotPathFn = defaultResolveSnapshotPath;
|
|
let serializerFns = defaultSerializers;
|
|
|
|
function setResolveSnapshotPath(fn) {
|
|
emitExperimentalWarning(kExperimentalWarning);
|
|
validateFunction(fn, 'fn');
|
|
resolveSnapshotPathFn = fn;
|
|
}
|
|
|
|
function setDefaultSnapshotSerializers(serializers) {
|
|
emitExperimentalWarning(kExperimentalWarning);
|
|
validateFunctionArray(serializers, 'serializers');
|
|
serializerFns = ArrayPrototypeSlice(serializers);
|
|
}
|
|
|
|
class SnapshotFile {
|
|
constructor(snapshotFile) {
|
|
this.snapshotFile = snapshotFile;
|
|
this.snapshots = { __proto__: null };
|
|
this.nameCounts = new SafeMap();
|
|
this.loaded = false;
|
|
}
|
|
|
|
getSnapshot(id) {
|
|
if (!(id in this.snapshots)) {
|
|
const err = new ERR_INVALID_STATE(`Snapshot '${id}' not found in ` +
|
|
`'${this.snapshotFile}.' ${kMissingSnapshotTip}`);
|
|
err.snapshot = id;
|
|
err.filename = this.snapshotFile;
|
|
throw err;
|
|
}
|
|
|
|
return this.snapshots[id];
|
|
}
|
|
|
|
setSnapshot(id, value) {
|
|
this.snapshots[templateEscape(id)] = value;
|
|
}
|
|
|
|
nextId(name) {
|
|
const count = this.nameCounts.get(name) ?? 1;
|
|
this.nameCounts.set(name, count + 1);
|
|
return `${name} ${count}`;
|
|
}
|
|
|
|
readFile() {
|
|
if (this.loaded) {
|
|
debug('skipping read of snapshot file');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const source = readFileSync(this.snapshotFile, 'utf8');
|
|
const context = { __proto__: null, exports: { __proto__: null } };
|
|
|
|
createContext(context);
|
|
runInContext(source, context);
|
|
|
|
if (context.exports === null || typeof context.exports !== 'object') {
|
|
throw new ERR_INVALID_STATE(
|
|
`Malformed snapshot file '${this.snapshotFile}'.`,
|
|
);
|
|
}
|
|
|
|
for (const key in context.exports) {
|
|
this.snapshots[key] = templateEscape(context.exports[key]);
|
|
}
|
|
this.loaded = true;
|
|
} catch (err) {
|
|
let msg = `Cannot read snapshot file '${this.snapshotFile}.'`;
|
|
|
|
if (err?.code === 'ENOENT') {
|
|
msg += ` ${kMissingSnapshotTip}`;
|
|
}
|
|
|
|
const error = new ERR_INVALID_STATE(msg);
|
|
error.cause = err;
|
|
error.filename = this.snapshotFile;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
writeFile() {
|
|
try {
|
|
const keys = ArrayPrototypeSort(ObjectKeys(this.snapshots));
|
|
const snapshotStrings = ArrayPrototypeMap(keys, (key) => {
|
|
return `exports[\`${key}\`] = \`${this.snapshots[key]}\`;\n`;
|
|
});
|
|
const output = ArrayPrototypeJoin(snapshotStrings, '\n');
|
|
mkdirSync(dirname(this.snapshotFile), { __proto__: null, recursive: true });
|
|
writeFileSync(this.snapshotFile, output, 'utf8');
|
|
} catch (err) {
|
|
const msg = `Cannot write snapshot file '${this.snapshotFile}.'`;
|
|
const error = new ERR_INVALID_STATE(msg);
|
|
error.cause = err;
|
|
error.filename = this.snapshotFile;
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
class SnapshotManager {
|
|
constructor(updateSnapshots) {
|
|
// A manager instance will only read or write snapshot files based on the
|
|
// updateSnapshots argument.
|
|
this.updateSnapshots = updateSnapshots;
|
|
this.cache = new SafeMap();
|
|
}
|
|
|
|
resolveSnapshotFile(entryFile) {
|
|
let snapshotFile = this.cache.get(entryFile);
|
|
|
|
if (snapshotFile === undefined) {
|
|
const resolved = resolveSnapshotPathFn(entryFile);
|
|
|
|
if (typeof resolved !== 'string') {
|
|
const err = new ERR_INVALID_STATE('Invalid snapshot filename.');
|
|
err.filename = resolved;
|
|
throw err;
|
|
}
|
|
|
|
snapshotFile = new SnapshotFile(resolved);
|
|
snapshotFile.loaded = this.updateSnapshots;
|
|
this.cache.set(entryFile, snapshotFile);
|
|
}
|
|
|
|
return snapshotFile;
|
|
}
|
|
|
|
serialize(input, serializers = serializerFns) {
|
|
try {
|
|
let value = input;
|
|
|
|
for (let i = 0; i < serializers.length; ++i) {
|
|
const fn = serializers[i];
|
|
value = fn(value);
|
|
}
|
|
|
|
return `\n${templateEscape(value)}\n`;
|
|
} catch (err) {
|
|
const error = new ERR_INVALID_STATE(
|
|
'The provided serializers did not generate a string.',
|
|
);
|
|
error.input = input;
|
|
error.cause = err;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
writeSnapshotFiles() {
|
|
if (!this.updateSnapshots) {
|
|
debug('skipping write of snapshot files');
|
|
return;
|
|
}
|
|
|
|
this.cache.forEach((snapshotFile) => {
|
|
snapshotFile.writeFile();
|
|
});
|
|
}
|
|
|
|
createAssert() {
|
|
const manager = this;
|
|
|
|
return function snapshotAssertion(actual, options = kEmptyObject) {
|
|
emitExperimentalWarning(kExperimentalWarning);
|
|
validateObject(options, 'options');
|
|
const {
|
|
serializers = serializerFns,
|
|
} = options;
|
|
validateFunctionArray(serializers, 'options.serializers');
|
|
const { filePath, fullName } = this;
|
|
const snapshotFile = manager.resolveSnapshotFile(filePath);
|
|
const value = manager.serialize(actual, serializers);
|
|
const id = snapshotFile.nextId(fullName);
|
|
|
|
if (manager.updateSnapshots) {
|
|
snapshotFile.setSnapshot(id, value);
|
|
} else {
|
|
snapshotFile.readFile();
|
|
strictEqual(value, snapshotFile.getSnapshot(id));
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
function validateFunctionArray(fns, name) {
|
|
validateArray(fns, name);
|
|
for (let i = 0; i < fns.length; ++i) {
|
|
validateFunction(fns[i], `${name}[${i}]`);
|
|
}
|
|
}
|
|
|
|
function templateEscape(str) {
|
|
let result = String(str);
|
|
result = StringPrototypeReplaceAll(result, '\\', '\\\\');
|
|
result = StringPrototypeReplaceAll(result, '`', '\\`');
|
|
result = StringPrototypeReplaceAll(result, '${', '\\${');
|
|
return result;
|
|
}
|
|
|
|
module.exports = {
|
|
SnapshotManager,
|
|
defaultResolveSnapshotPath, // Exported for testing only.
|
|
defaultSerializers, // Exported for testing only.
|
|
setDefaultSnapshotSerializers,
|
|
setResolveSnapshotPath,
|
|
};
|