From a8b0dc846415aa79087133a25dcf3fb191cce1c0 Mon Sep 17 00:00:00 2001 From: make-github-pseudonymous-again <5165674+make-github-pseudonymous-again@users.noreply.github.com> Date: Thu, 11 Apr 2024 07:13:59 +0200 Subject: [PATCH] WIP --- lib/snapshot-manager.js | 73 ++++++++++--------- .../magic-assert-buffers.js | 5 +- package-lock.json | 12 +++ package.json | 1 + test-tap/helper/report.js | 11 +-- test-tap/helper/tty-stream.js | 26 ++++--- test-tap/integration/snapshots.js | 12 +-- test-tap/reporters/default.js | 4 +- test-tap/reporters/tap.js | 4 +- test/helpers/exec.js | 4 +- test/snapshot-removal/test.js | 5 +- test/snapshot-tests/fixtures/large/test.js | 4 +- test/snapshot-workflow/invalid-snapfile.js | 3 +- 13 files changed, 90 insertions(+), 74 deletions(-) diff --git a/lib/snapshot-manager.js b/lib/snapshot-manager.js index 68d7f8af7..d2d960acb 100644 --- a/lib/snapshot-manager.js +++ b/lib/snapshot-manager.js @@ -1,4 +1,3 @@ -import {Buffer} from 'node:buffer'; import crypto from 'node:crypto'; import fs from 'node:fs'; import {findSourceMap} from 'node:module'; @@ -16,17 +15,17 @@ import {snapshotManager as concordanceOptions} from './concordance-options.js'; import slash from './slash.cjs'; // Increment if encoding layout or Concordance serialization versions change. Previous AVA versions will not be able to -// decode buffers generated by a newer version, so changing this value will require a major version bump of AVA itself. +// decode byte arrays generated by a newer version, so changing this value will require a major version bump of AVA itself. // The version is encoded as an unsigned 16 bit integer. const VERSION = 3; -const VERSION_HEADER = Buffer.alloc(2); -VERSION_HEADER.writeUInt16LE(VERSION); +const VERSION_HEADER = new Uint8Array(2); +new DataView(VERSION_HEADER).setUint16(0, VERSION, true); // The decoder matches on the trailing newline byte (0x0A). -const READABLE_PREFIX = Buffer.from(`AVA Snapshot v${VERSION}\n`, 'ascii'); -const REPORT_SEPARATOR = Buffer.from('\n\n', 'ascii'); -const REPORT_TRAILING_NEWLINE = Buffer.from('\n', 'ascii'); +const READABLE_PREFIX = new TextEncoder().encode(`AVA Snapshot v${VERSION}\n`); +const REPORT_SEPARATOR = new TextEncoder().encode('\n\n'); +const REPORT_TRAILING_NEWLINE = new TextEncoder().encode('\n'); const SHA_256_HASH_LENGTH = 32; @@ -61,9 +60,9 @@ export class InvalidSnapshotError extends SnapshotError { } } -const LEGACY_SNAPSHOT_HEADER = Buffer.from('// Jest Snapshot v1'); -function isLegacySnapshot(buffer) { - return LEGACY_SNAPSHOT_HEADER.equals(buffer.slice(0, LEGACY_SNAPSHOT_HEADER.byteLength)); +const LEGACY_SNAPSHOT_HEADER = new TextEncoder().encode('// Jest Snapshot v1'); +function isLegacySnapshot(array) { + return LEGACY_SNAPSHOT_HEADER.equals(array.subarray(0, LEGACY_SNAPSHOT_HEADER.byteLength)); } export class LegacyError extends SnapshotError { @@ -101,7 +100,7 @@ function formatEntry(snapshot, index) { } function combineEntries({blocks}) { - const combined = new BufferBuilder(); + const combined = new Uint8ArrayBuilder(); for (const {title, snapshots} of blocks) { const last = snapshots.at(-1); @@ -120,7 +119,7 @@ function combineEntries({blocks}) { } function generateReport(relFile, snapFile, snapshots) { - return new BufferBuilder() + return new Uint8ArrayBuilder() .write(`# Snapshot report for \`${slash(relFile)}\` The actual snapshot is saved in \`${snapFile}\`. @@ -128,34 +127,35 @@ The actual snapshot is saved in \`${snapFile}\`. Generated by [AVA](https://avajs.dev).`) .append(combineEntries(snapshots)) .write(REPORT_TRAILING_NEWLINE) - .toBuffer(); + .toUint8Array(); } -class BufferBuilder { +class Uint8ArrayBuilder { constructor() { - this.buffers = []; + this.arrays = []; this.byteOffset = 0; } append(builder) { - this.buffers.push(...builder.buffers); + this.arrays.push(...builder.arrays); this.byteOffset += builder.byteOffset; return this; } write(data) { if (typeof data === 'string') { - this.write(Buffer.from(data, 'utf8')); + const encoder = new TextEncoder(); + this.write(encoder.encode(data)); } else { - this.buffers.push(data); + this.arrays.push(data); this.byteOffset += data.byteLength; } return this; } - toBuffer() { - return Buffer.concat(this.buffers, this.byteOffset); + toUint8Array() { + return concatUint8Arrays(this.arrays, this.byteOffset); } } @@ -190,7 +190,7 @@ async function encodeSnapshots(snapshotData) { const compressed = zlib.gzipSync(encoded); compressed[9] = 0x03; // Override the GZip header containing the OS to always be Linux const sha256sum = crypto.createHash('sha256').update(compressed).digest(); - return Buffer.concat([ + return concatUint8Arrays([ READABLE_PREFIX, VERSION_HEADER, sha256sum, @@ -198,38 +198,39 @@ async function encodeSnapshots(snapshotData) { ], READABLE_PREFIX.byteLength + VERSION_HEADER.byteLength + SHA_256_HASH_LENGTH + compressed.byteLength); } -export function extractCompressedSnapshot(buffer, snapPath) { - if (isLegacySnapshot(buffer)) { +export function extractCompressedSnapshot(array, snapPath) { + if (isLegacySnapshot(array)) { throw new LegacyError(snapPath); } // The version starts after the readable prefix, which is ended by a newline // byte (0x0A). - const newline = buffer.indexOf(0x0A); + const newline = array.indexOf(0x0A); if (newline === -1) { throw new InvalidSnapshotError(snapPath); } + const view = new DataView(array); const versionOffset = newline + 1; - const version = buffer.readUInt16LE(versionOffset); + const version = view.getUint16(versionOffset, true); if (version !== VERSION) { throw new VersionMismatchError(snapPath, version); } const sha256sumOffset = versionOffset + 2; const compressedOffset = sha256sumOffset + SHA_256_HASH_LENGTH; - const compressed = buffer.slice(compressedOffset); + const compressed = array.subarray(compressedOffset); return { version, compressed, sha256sumOffset, compressedOffset, }; } -function decodeSnapshots(buffer, snapPath) { - const {compressed, sha256sumOffset, compressedOffset} = extractCompressedSnapshot(buffer, snapPath); +function decodeSnapshots(array, snapPath) { + const {compressed, sha256sumOffset, compressedOffset} = extractCompressedSnapshot(array, snapPath); const sha256sum = crypto.createHash('sha256').update(compressed).digest(); - const expectedSum = buffer.slice(sha256sumOffset, compressedOffset); + const expectedSum = array.subarray(sha256sumOffset, compressedOffset); if (!sha256sum.equals(expectedSum)) { throw new ChecksumError(snapPath); } @@ -373,16 +374,16 @@ class Manager { ), }; - const buffer = await encodeSnapshots(snapshots); - const reportBuffer = generateReport(relFile, snapFile, snapshots); + const array = await encodeSnapshots(snapshots); + const reportArray = generateReport(relFile, snapFile, snapshots); await fs.promises.mkdir(dir, {recursive: true}); const temporaryFiles = []; const tmpfileCreated = file => temporaryFiles.push(file); await Promise.all([ - writeFileAtomic(snapPath, buffer, {tmpfileCreated}), - writeFileAtomic(reportPath, reportBuffer, {tmpfileCreated}), + writeFileAtomic(snapPath, array, {tmpfileCreated}), + writeFileAtomic(reportPath, reportArray, {tmpfileCreated}), ]); return { changedFiles: [snapPath, reportPath], @@ -470,9 +471,9 @@ export function load({file, fixedLocation, projectDir, recordNewSnapshots, updat } const paths = determineSnapshotPaths({file, fixedLocation, projectDir}); - const buffer = tryRead(paths.snapPath); + const array = tryRead(paths.snapPath); - if (!buffer) { + if (!array) { return new Manager({ recordNewSnapshots, updating, @@ -486,7 +487,7 @@ export function load({file, fixedLocation, projectDir, recordNewSnapshots, updat let snapshotError; try { - const data = decodeSnapshots(buffer, paths.snapPath); + const data = decodeSnapshots(array, paths.snapPath); blocksByTitle = new Map(data.blocks.map(({title, ...block}) => [title, block])); } catch (error) { blocksByTitle = new Map(); diff --git a/media/screenshot-fixtures/magic-assert-buffers.js b/media/screenshot-fixtures/magic-assert-buffers.js index 33213930a..0702f4014 100644 --- a/media/screenshot-fixtures/magic-assert-buffers.js +++ b/media/screenshot-fixtures/magic-assert-buffers.js @@ -1,7 +1,8 @@ import test from 'ava'; +import {hexToUint8Array} from 'uint8array-extras'; test('buffers', t => { - const actual = Buffer.from('decafbadcab00d1e'.repeat(4), 'hex') - const expected = Buffer.from('cab00d1edecafbad' + 'decafbadcab00d1e'.repeat(3), 'hex') + const actual = hexToUint8Array('decafbadcab00d1e'.repeat(4)) + const expected = hexToUint8Array('cab00d1edecafbad' + 'decafbadcab00d1e'.repeat(3)) t.deepEqual(actual, expected) }); diff --git a/package-lock.json b/package-lock.json index a8427df3e..fc6ada639 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "strip-ansi": "^7.1.0", "supertap": "^3.0.1", "temp-dir": "^3.0.0", + "uint8array-extras": "^1.1.0", "write-file-atomic": "^5.0.1", "yargs": "^17.7.2" }, @@ -11402,6 +11403,17 @@ "node": ">=14.17" } }, + "node_modules/uint8array-extras": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.1.0.tgz", + "integrity": "sha512-CVaBSyOmGoFHu+zOVPbetXEXykOd8KHVBHLlqvmaMWpwcq3rewj18xVNbU5uzf48hclnNQhfNaNany2cMHFK/g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 4a058c817..8c32fb13c 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "strip-ansi": "^7.1.0", "supertap": "^3.0.1", "temp-dir": "^3.0.0", + "uint8array-extras": "^1.1.0", "write-file-atomic": "^5.0.1", "yargs": "^17.7.2" }, diff --git a/test-tap/helper/report.js b/test-tap/helper/report.js index e616e52a4..8d7a9e0c6 100644 --- a/test-tap/helper/report.js +++ b/test-tap/helper/report.js @@ -8,25 +8,26 @@ import Api from '../../lib/api.js'; import {_testOnlyReplaceWorkerPath} from '../../lib/fork.js'; import {normalizeGlobs} from '../../lib/globs.js'; import pkg from '../../lib/pkg.cjs'; +import {uint8ArrayToString} from 'uint8array-extras'; _testOnlyReplaceWorkerPath(new URL('report-worker.js', import.meta.url)); const exports = {}; export default exports; -exports.assert = (t, logFile, buffer) => { +exports.assert = (t, logFile, array) => { let existing = null; try { existing = fs.readFileSync(logFile); } catch {} if (existing === null || process.env.UPDATE_REPORTER_LOG) { - fs.writeFileSync(logFile, buffer); - existing = buffer; + fs.writeFileSync(logFile, array); + existing = array; } - const expected = existing.toString('utf8'); - const actual = buffer.toString('utf8'); + const expected = uint8ArrayToString(existing); + const actual = uint8ArrayToString(array); if (actual === expected) { t.pass(); } else { diff --git a/test-tap/helper/tty-stream.js b/test-tap/helper/tty-stream.js index 885c9435f..a5a012513 100644 --- a/test-tap/helper/tty-stream.js +++ b/test-tap/helper/tty-stream.js @@ -1,7 +1,7 @@ -import {Buffer} from 'node:buffer'; import stream from 'node:stream'; import ansiEscapes from 'ansi-escapes'; +import {stringToUint8Array, concatUint8Arrays, uint8ArrayToString} from 'uint8array-extras'; export default class TTYStream extends stream.Writable { constructor(options) { @@ -17,7 +17,7 @@ export default class TTYStream extends stream.Writable { _write(chunk, encoding, callback) { if (this.spinnerActivity.length > 0) { - this.chunks.push(Buffer.concat(this.spinnerActivity), TTYStream.SEPARATOR); + this.chunks.push(concatUint8Arrays(this.spinnerActivity), TTYStream.SEPARATOR); this.spinnerActivity = []; } @@ -26,7 +26,7 @@ export default class TTYStream extends stream.Writable { // chunks. if (string !== '' || chunk.length === 0) { this.chunks.push( - Buffer.from(string, 'utf8'), + stringToUint8Array(string), TTYStream.SEPARATOR, ); } @@ -36,33 +36,37 @@ export default class TTYStream extends stream.Writable { _writev(chunks, callback) { if (this.spinnerActivity.length > 0) { - this.chunks.push(Buffer.concat(this.spinnerActivity), TTYStream.SEPARATOR); + this.chunks.push(concatUint8Arrays(this.spinnerActivity), TTYStream.SEPARATOR); this.spinnerActivity = []; } for (const object of chunks) { - this.chunks.push(Buffer.from(this.sanitizers.reduce((string, sanitizer) => sanitizer(string), object.chunk.toString('utf8')), 'utf8')); // eslint-disable-line unicorn/no-array-reduce + this.chunks.push(stringToUint8Array(this.sanitizers.reduce((string, sanitizer) => sanitizer(string), uint8ArrayToString(object.chunk)))); // eslint-disable-line unicorn/no-array-reduce } this.chunks.push(TTYStream.SEPARATOR); callback(); } - asBuffer() { - return Buffer.concat(this.chunks); + asUint8Array() { + return concatUint8Arrays(this.chunks); + } + + toString() { + return uint8ArrayToString(array); } clearLine() { - this.spinnerActivity.push(Buffer.from(ansiEscapes.eraseLine, 'ascii')); + this.spinnerActivity.push(stringToUint8Array(ansiEscapes.eraseLine)); } cursorTo(x, y) { - this.spinnerActivity.push(Buffer.from(ansiEscapes.cursorTo(x, y), 'ascii')); + this.spinnerActivity.push(stringToUint8Array(ansiEscapes.cursorTo(x, y))); } moveCursor(dx, dy) { - this.spinnerActivity.push(Buffer.from(ansiEscapes.cursorMove(dx, dy), 'ascii')); + this.spinnerActivity.push(stringToUint8Array(ansiEscapes.cursorMove(dx, dy))); } } -TTYStream.SEPARATOR = Buffer.from('---tty-stream-chunk-separator\n', 'utf8'); +TTYStream.SEPARATOR = stringToUint8Array('---tty-stream-chunk-separator\n'); diff --git a/test-tap/integration/snapshots.js b/test-tap/integration/snapshots.js index 36461129b..b78293d6a 100644 --- a/test-tap/integration/snapshots.js +++ b/test-tap/integration/snapshots.js @@ -1,4 +1,3 @@ -import {Buffer} from 'node:buffer'; import fs from 'node:fs'; import path from 'node:path'; import {fileURLToPath} from 'node:url'; @@ -10,6 +9,7 @@ import {temporaryDirectory} from 'tempy'; import {extractCompressedSnapshot} from '../../lib/snapshot-manager.js'; import {execCli} from '../helper/cli.js'; +import {stringToUint8Array} from 'uint8array-extras'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); @@ -82,7 +82,7 @@ test('two', t => { test('outdated snapshot version is reported to the console', t => { const snapPath = path.join(__dirname, '..', 'fixture', 'snapshots', 'test.cjs.snap'); - fs.writeFileSync(snapPath, Buffer.from([0x0A, 0x00, 0x00])); + fs.writeFileSync(snapPath, stringToUint8Array([0x0A, 0x00, 0x00])); execCli(['test.cjs'], {dirname: 'fixture/snapshots'}, (error, stdout) => { t.ok(error); @@ -96,7 +96,7 @@ test('outdated snapshot version is reported to the console', t => { test('outdated snapshot version can be updated', t => { const snapPath = path.join(__dirname, '..', 'fixture', 'snapshots', 'test.cjs.snap'); - fs.writeFileSync(snapPath, Buffer.from([0x0A, 0x00, 0x00])); + fs.writeFileSync(snapPath, stringToUint8Array([0x0A, 0x00, 0x00])); execCli(['test.cjs', '--update-snapshots'], {dirname: 'fixture/snapshots', env: {AVA_FORCE_CI: 'not-ci'}}, (error, stdout) => { t.error(error); @@ -107,7 +107,7 @@ test('outdated snapshot version can be updated', t => { test('newer snapshot version is reported to the console', t => { const snapPath = path.join(__dirname, '..', 'fixture', 'snapshots', 'test.cjs.snap'); - fs.writeFileSync(snapPath, Buffer.from([0x0A, 0xFF, 0xFF])); + fs.writeFileSync(snapPath, stringToUint8Array([0x0A, 0xFF, 0xFF])); execCli(['test.cjs'], {dirname: 'fixture/snapshots'}, (error, stdout) => { t.ok(error); @@ -121,7 +121,7 @@ test('newer snapshot version is reported to the console', t => { test('snapshot corruption is reported to the console', t => { const snapPath = path.join(__dirname, '..', 'fixture', 'snapshots', 'test.cjs.snap'); - fs.writeFileSync(snapPath, Buffer.from([0x0A, 0x03, 0x00])); + fs.writeFileSync(snapPath, stringToUint8Array([0x0A, 0x03, 0x00])); execCli(['test.cjs'], {dirname: 'fixture/snapshots'}, (error, stdout) => { t.ok(error); @@ -135,7 +135,7 @@ test('snapshot corruption is reported to the console', t => { test('legacy snapshot files are reported to the console', t => { const snapPath = path.join(__dirname, '..', 'fixture', 'snapshots', 'test.cjs.snap'); - fs.writeFileSync(snapPath, Buffer.from('// Jest Snapshot v1, https://goo.gl/fbAQLP\n')); + fs.writeFileSync(snapPath, stringToUint8Array('// Jest Snapshot v1, https://goo.gl/fbAQLP\n')); execCli(['test.cjs'], {dirname: 'fixture/snapshots'}, (error, stdout) => { t.ok(error); diff --git a/test-tap/reporters/default.js b/test-tap/reporters/default.js index e14d843a8..0b19be65c 100644 --- a/test-tap/reporters/default.js +++ b/test-tap/reporters/default.js @@ -32,9 +32,9 @@ test(async t => { return report[type](reporter) .then(() => { tty.end(); - return tty.asBuffer(); + return tty.asUint8Array(); }) - .then(buffer => report.assert(t, logFile, buffer)) + .then(array => report.assert(t, logFile, array)) .catch(t.threw); }; diff --git a/test-tap/reporters/tap.js b/test-tap/reporters/tap.js index 4935d8520..20cac7d66 100644 --- a/test-tap/reporters/tap.js +++ b/test-tap/reporters/tap.js @@ -30,9 +30,9 @@ test(async t => { return report[type](reporter) .then(() => { tty.end(); - return tty.asBuffer(); + return tty.asUint8Array(); }) - .then(buffer => report.assert(t, logFile, buffer)) + .then(array => report.assert(t, logFile, array)) .catch(t.threw); }; diff --git a/test/helpers/exec.js b/test/helpers/exec.js index 9c0691439..01ad1cfb2 100644 --- a/test/helpers/exec.js +++ b/test/helpers/exec.js @@ -1,4 +1,3 @@ -import {Buffer} from 'node:buffer'; import {on} from 'node:events'; import path from 'node:path'; import {Writable} from 'node:stream'; @@ -6,6 +5,7 @@ import {fileURLToPath, pathToFileURL} from 'node:url'; import test from '@ava/test'; import {execaNode} from 'execa'; +import {stringToUint8Array} from 'uint8array-extras'; const cliPath = fileURLToPath(new URL('../../entrypoints/cli.mjs', import.meta.url)); const ttySimulator = fileURLToPath(new URL('simulate-tty.cjs', import.meta.url)); @@ -35,7 +35,7 @@ const compareStatObjects = (a, b) => { export const cwd = (...paths) => path.join(path.dirname(fileURLToPath(test.meta.file)), 'fixtures', ...paths); export const cleanOutput = string => string.replace(/^\W+/, '').replaceAll(/\W+\n+$/g, '').trim(); -const NO_FORWARD_PREFIX = Buffer.from('🤗', 'utf8'); +const NO_FORWARD_PREFIX = stringToUint8Array('🤗'); const forwardErrorOutput = chunk => { if (chunk.length < 4 || NO_FORWARD_PREFIX.compare(chunk, 0, 4) !== 0) { diff --git a/test/snapshot-removal/test.js b/test/snapshot-removal/test.js index 3d43ff271..41935ac6a 100644 --- a/test/snapshot-removal/test.js +++ b/test/snapshot-removal/test.js @@ -1,4 +1,3 @@ -import {Buffer} from 'node:buffer'; import {promises as fs} from 'node:fs'; import path from 'node:path'; @@ -54,7 +53,7 @@ test.serial('removing non-existent snapshots doesn\'t throw', async t => { test.serial('without --update-snapshots, invalid .snaps are retained', async t => { await withTemporaryFixture(cwd('no-snapshots'), async cwd => { const snapPath = path.join(cwd, 'test.js.snap'); - const invalid = Buffer.of(0x0A, 0x00, 0x00); + const invalid = new Uint8Array([0x0A, 0x00, 0x00]); await fs.writeFile(snapPath, invalid); await fixture([], {cwd}); @@ -67,7 +66,7 @@ test.serial('without --update-snapshots, invalid .snaps are retained', async t = test.serial('with --update-snapshots, invalid .snaps are removed', async t => { await withTemporaryFixture(cwd('no-snapshots'), async cwd => { const snapPath = path.join(cwd, 'test.js.snap'); - const invalid = Buffer.of(0x0A, 0x00, 0x00); + const invalid = new Uint8Array([0x0A, 0x00, 0x00]); await fs.writeFile(snapPath, invalid); await fixture(['--update-snapshots'], {cwd}); diff --git a/test/snapshot-tests/fixtures/large/test.js b/test/snapshot-tests/fixtures/large/test.js index 3a2a2c4c8..a4c1e8100 100644 --- a/test/snapshot-tests/fixtures/large/test.js +++ b/test/snapshot-tests/fixtures/large/test.js @@ -1,9 +1,7 @@ -const {Buffer} = require('node:buffer'); - const test = require(process.env.TEST_AVA_REQUIRE_FROM); for (let i = 0; i < 2; i++) { test(`large snapshot ${i}`, t => { - t.snapshot(Buffer.alloc(1024 * 16)); + t.snapshot(new Uint8Array(1024 * 16)); }); } diff --git a/test/snapshot-workflow/invalid-snapfile.js b/test/snapshot-workflow/invalid-snapfile.js index a1abd6956..dcbe47f94 100644 --- a/test/snapshot-workflow/invalid-snapfile.js +++ b/test/snapshot-workflow/invalid-snapfile.js @@ -1,4 +1,3 @@ -import {Buffer} from 'node:buffer'; import {promises as fs} from 'node:fs'; import path from 'node:path'; @@ -13,7 +12,7 @@ test.serial('With invalid .snap file and --update-snapshots, skipped snaps are o const snapPath = path.join(cwd, 'test.js.snap'); const reportPath = path.join(cwd, 'test.js.md'); - await fs.writeFile(snapPath, Buffer.of(0x0A, 0x00, 0x00)); + await fs.writeFile(snapPath, new Uint8Array([0x0A, 0x00, 0x00])); const result = await fixture(['--update-snapshots'], {cwd, env}); const report = await fs.readFile(reportPath, 'utf8');