UNPKG

@napi-rs/cli

Version:

Cli tools for napi-rs

178 lines (147 loc) 5.66 kB
import { existsSync, type BigIntStats } from 'node:fs' import { lstat, mkdtemp, readdir, readFile, rename, rm, writeFile, } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import ava, { type TestFn } from 'ava' import { retireFailedSnapshotLeftover, snapshotLeftoverIsTransactionOwned, statIdentitiesMatch, updatePackageJson, } from '../misc.js' async function fileIdentityStrings(path: string) { const stats = await lstat(path, { bigint: true }) return { dev: String(stats.dev), ino: String(stats.ino) } } const test = ava as TestFn<{ tmpDir: string }> test.beforeEach(async (t) => { t.context = { tmpDir: await mkdtemp(join(tmpdir(), 'napi-rs-misc-spec-')), } }) test.afterEach.always(async (t) => { if (existsSync(t.context.tmpDir)) { await rm(t.context.tmpDir, { recursive: true, force: true }) } }) test('updatePackageJson merges nested objects instead of overwriting them', async (t) => { const packageJsonPath = join(t.context.tmpDir, 'package.json') await writeFile( packageJsonPath, JSON.stringify( { name: 'fixture', version: '1.0.0', optionalDependencies: { fsevents: '^2.3.3', }, }, null, 2, ), ) await updatePackageJson(packageJsonPath, { optionalDependencies: { '@napi-rs/fixture-darwin-arm64': '1.0.1', }, }) const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) t.deepEqual(packageJson.optionalDependencies, { fsevents: '^2.3.3', '@napi-rs/fixture-darwin-arm64': '1.0.1', }) }) test('statIdentitiesMatch distinguishes inodes that collide as lossy Numbers', (t) => { const dev = 1n const ino = 2n ** 53n const owned = { dev, ino } const successor = { dev, ino: ino + 1n } // The defect being guarded against: past Number.MAX_SAFE_INTEGER two // distinct 64-bit identifiers collapse onto the same JS double, so a // numeric Stats.dev/ino comparison cannot tell these files apart. t.is(Number(owned.ino), Number(successor.ino)) t.false(statIdentitiesMatch(owned, successor)) t.false(statIdentitiesMatch({ dev: dev + 1n, ino }, owned)) t.true(statIdentitiesMatch(owned, { dev, ino })) t.false(statIdentitiesMatch(owned, undefined)) }) test('snapshotLeftoverIsTransactionOwned rejects Number-colliding successors', (t) => { const identity = { dev: '1', ino: String(2n ** 53n) } const ownedStats = { isFile: () => true, dev: 1n, ino: 2n ** 53n, } as unknown as BigIntStats const successorStats = { isFile: () => true, dev: 1n, ino: 2n ** 53n + 1n, } as unknown as BigIntStats t.true(snapshotLeftoverIsTransactionOwned(ownedStats, identity)) t.false(snapshotLeftoverIsTransactionOwned(successorStats, identity)) t.false(snapshotLeftoverIsTransactionOwned(undefined, identity)) }) test('retireFailedSnapshotLeftover removes the transaction-owned inode', async (t) => { const destination = join(t.context.tmpDir, 'leftover.tmp') await writeFile(destination, 'partial snapshot') const identity = await fileIdentityStrings(destination) const result = await retireFailedSnapshotLeftover(destination, identity) t.deepEqual(result, { outcome: 'removed' }) t.false(existsSync(destination)) t.deepEqual(await readdir(t.context.tmpDir), []) }) test('retireFailedSnapshotLeftover reports a missing leftover', async (t) => { const destination = join(t.context.tmpDir, 'leftover.tmp') await writeFile(destination, 'partial snapshot') const identity = await fileIdentityStrings(destination) await rm(destination) const result = await retireFailedSnapshotLeftover(destination, identity) t.deepEqual(result, { outcome: 'missing' }) t.deepEqual(await readdir(t.context.tmpDir), []) }) test('retireFailedSnapshotLeftover keeps a pre-existing non-owned successor', async (t) => { const destination = join(t.context.tmpDir, 'leftover.tmp') await writeFile(destination, 'partial snapshot') const identity = await fileIdentityStrings(destination) // Replace the owned inode with a distinct one before cleanup runs. The // successor is created as a sibling first so it deterministically has a // different inode, then atomically renamed over the destination. const successor = join(t.context.tmpDir, 'successor.tmp') await writeFile(successor, 'successor content') await rename(successor, destination) const result = await retireFailedSnapshotLeftover(destination, identity) t.deepEqual(result, { outcome: 'kept' }) t.is(await readFile(destination, 'utf8'), 'successor content') t.deepEqual(await readdir(t.context.tmpDir), ['leftover.tmp']) }) test('retireFailedSnapshotLeftover restores a successor swapped in during the race window', async (t) => { const destination = join(t.context.tmpDir, 'leftover.tmp') await writeFile(destination, 'partial snapshot') const identity = await fileIdentityStrings(destination) const successor = join(t.context.tmpDir, 'successor.tmp') await writeFile(successor, 'successor content') // Swap the successor onto the pathname after the ownership pre-check and // before the retirement rename — the exact interval in which the previous // lstat-then-unlink cleanup would have deleted a file the transaction never // owned. const result = await retireFailedSnapshotLeftover( destination, identity, async () => { await rename(successor, destination) }, ) t.deepEqual(result, { outcome: 'kept' }) t.is(await readFile(destination, 'utf8'), 'successor content') t.deepEqual(await readdir(t.context.tmpDir), ['leftover.tmp']) })