diff --git a/package.json b/package.json index 0a47eb8f..99a5fd4a 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,8 @@ "debug": "^4.3.4", "pony-cause": "^2.1.10", "semver": "^7.5.4", - "superstruct": "^1.0.3" + "superstruct": "^1.0.3", + "uuid": "^9.0.1" }, "devDependencies": { "@lavamoat/allow-scripts": "^2.3.1", @@ -72,6 +73,7 @@ "@types/jest": "^28.1.7", "@types/jest-when": "^3.5.3", "@types/node": "^17.0.23", + "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^5.43.0", "@typescript-eslint/parser": "^5.43.0", "depcheck": "^1.4.7", diff --git a/src/fs.test.ts b/src/fs.test.ts index bfab32ed..cffb5a88 100644 --- a/src/fs.test.ts +++ b/src/fs.test.ts @@ -3,6 +3,7 @@ import { when } from 'jest-when'; import os from 'os'; import path from 'path'; import util from 'util'; +import * as uuid from 'uuid'; import { createSandbox, @@ -18,6 +19,16 @@ import { const { withinSandbox } = createSandbox('utils'); +// Clone the `uuid` module so that we can spy on its exports +jest.mock('uuid', () => { + return { + // This is how to mock an ES-compatible module in Jest. + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + ...jest.requireActual('uuid'), + }; +}); + describe('fs', () => { describe('readFile', () => { it('reads the contents of the given file as a UTF-8-encoded string', async () => { @@ -674,9 +685,14 @@ describe('fs', () => { }); it('does not create the sandbox directory immediately', async () => { + jest.spyOn(uuid, 'v4').mockReturnValue('AAAA-AAAA-AAAA-AAAA'); createSandbox('utils-fs'); - const sandboxDirectoryPath = path.join(os.tmpdir(), 'utils-fs'); + const sandboxDirectoryPath = path.join( + os.tmpdir(), + 'utils-fs', + 'AAAA-AAAA-AAAA-AAAA', + ); await expect(fs.promises.stat(sandboxDirectoryPath)).rejects.toThrow( 'ENOENT', @@ -686,16 +702,15 @@ describe('fs', () => { describe('withinSandbox', () => { it('creates the sandbox directory and keeps it around before its given function ends', async () => { expect.assertions(1); - - const nowTimestamp = new Date('2023-01-01').getTime(); - jest.setSystemTime(nowTimestamp); + jest.spyOn(uuid, 'v4').mockReturnValue('AAAA-AAAA-AAAA-AAAA'); const { withinSandbox: withinTestSandbox } = createSandbox('utils-fs'); - const sandboxDirectoryPath = path.join( - os.tmpdir(), - `utils-fs--${nowTimestamp}`, - ); await withinTestSandbox(async () => { + const sandboxDirectoryPath = path.join( + os.tmpdir(), + 'utils-fs', + 'AAAA-AAAA-AAAA-AAAA', + ); expect(await fs.promises.stat(sandboxDirectoryPath)).toStrictEqual( expect.anything(), ); @@ -703,32 +718,32 @@ describe('fs', () => { }); it('removes the sandbox directory after its given function ends', async () => { - const nowTimestamp = new Date('2023-01-01').getTime(); - jest.setSystemTime(nowTimestamp); + jest.spyOn(uuid, 'v4').mockReturnValue('AAAA-AAAA-AAAA-AAAA'); const { withinSandbox: withinTestSandbox } = createSandbox('utils-fs'); - const sandboxDirectoryPath = path.join( - os.tmpdir(), - `utils-fs--${nowTimestamp}`, - ); await withinTestSandbox(async () => { // do nothing }); + const sandboxDirectoryPath = path.join( + os.tmpdir(), + 'utils-fs', + 'AAAA-AAAA-AAAA-AAAA', + ); await expect(fs.promises.stat(sandboxDirectoryPath)).rejects.toThrow( 'ENOENT', ); }); it('throws if the sandbox directory already exists', async () => { - const nowTimestamp = new Date('2023-01-01').getTime(); - jest.setSystemTime(nowTimestamp); + jest.spyOn(uuid, 'v4').mockReturnValue('AAAA-AAAA-AAAA-AAAA'); const { withinSandbox: withinTestSandbox } = createSandbox('utils-fs'); + const sandboxDirectoryPath = path.join( os.tmpdir(), - `utils-fs--${nowTimestamp}`, + 'utils-fs', + 'AAAA-AAAA-AAAA-AAAA', ); - try { await fs.promises.mkdir(sandboxDirectoryPath); diff --git a/src/fs.ts b/src/fs.ts index 0f6b4ef3..6df5b60e 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -4,6 +4,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; +import * as uuid from 'uuid'; import { isErrorWithCode, wrapError } from './errors'; import type { Json } from './json'; @@ -240,8 +241,7 @@ export async function forceRemove(entryPath: string): Promise { * ``` */ export function createSandbox(projectName: string): FileSandbox { - const timestamp = new Date().getTime(); - const directoryPath = path.join(os.tmpdir(), `${projectName}--${timestamp}`); + const directoryPath = path.join(os.tmpdir(), projectName, uuid.v4()); return { directoryPath, diff --git a/yarn.lock b/yarn.lock index 2c3c6ca8..93b09137 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1226,6 +1226,7 @@ __metadata: "@types/jest": ^28.1.7 "@types/jest-when": ^3.5.3 "@types/node": ^17.0.23 + "@types/uuid": ^9.0.8 "@typescript-eslint/eslint-plugin": ^5.43.0 "@typescript-eslint/parser": ^5.43.0 debug: ^4.3.4 @@ -1253,6 +1254,7 @@ __metadata: tsup: ^7.2.0 typedoc: ^0.23.15 typescript: ~4.8.4 + uuid: ^9.0.1 languageName: unknown linkType: soft @@ -1694,6 +1696,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:^9.0.8": + version: 9.0.8 + resolution: "@types/uuid@npm:9.0.8" + checksum: b8c60b7ba8250356b5088302583d1704a4e1a13558d143c549c408bf8920535602ffc12394ede77f8a8083511b023704bc66d1345792714002bfa261b17c5275 + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 21.0.0 resolution: "@types/yargs-parser@npm:21.0.0" @@ -7673,6 +7682,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^9.0.1": + version: 9.0.1 + resolution: "uuid@npm:9.0.1" + bin: + uuid: dist/bin/uuid + checksum: 39931f6da74e307f51c0fb463dc2462807531dc80760a9bff1e35af4316131b4fc3203d16da60ae33f07fdca5b56f3f1dd662da0c99fea9aaeab2004780cc5f4 + languageName: node + linkType: hard + "v8-compile-cache-lib@npm:^3.0.1": version: 3.0.1 resolution: "v8-compile-cache-lib@npm:3.0.1"