diff --git a/packages/rstack/rstack.config.ts b/packages/rstack/rstack.config.ts index d7777d8..789bc1f 100644 --- a/packages/rstack/rstack.config.ts +++ b/packages/rstack/rstack.config.ts @@ -7,6 +7,8 @@ define.test(async () => { process.env.NO_COLOR = '1'; return { + // Temporary projects may contain files that match Rstest's test glob. + exclude: ['**/test-temp-*/**'], extends: withRslibConfig(), source: { tsconfigPath: './tests/tsconfig.json', diff --git a/packages/rstack/tests/cli/fmt/index.test.ts b/packages/rstack/tests/cli/fmt/index.test.ts index 6312782..283773f 100644 --- a/packages/rstack/tests/cli/fmt/index.test.ts +++ b/packages/rstack/tests/cli/fmt/index.test.ts @@ -6,11 +6,10 @@ import { RSTACK_BIN_PATH } from '#test-helpers'; let projectPath: string; -const writeProjectFile = (filePath: string, content: string): string => { +const writeProjectFile = (filePath: string, content: string): void => { const absolutePath = path.join(projectPath, filePath); mkdirSync(path.dirname(absolutePath), { recursive: true }); writeFileSync(absolutePath, content); - return absolutePath; }; const readProjectFile = (filePath: string): string => @@ -30,7 +29,9 @@ const runCLI = (args: string[]) => { const runFmt = (args: string[] = []) => runCLI(['fmt', ...args]); beforeEach(() => { - projectPath = mkdtempSync(path.join(import.meta.dirname, 'fmt-project-')); + projectPath = mkdtempSync(path.join(import.meta.dirname, 'test-temp-fmt-')); + // Prevent repository-level ignore rules from affecting the fixture. + mkdirSync(path.join(projectPath, '.git')); writeProjectFile('rstack.config.ts', 'export {};\n'); }); diff --git a/packages/rstack/tests/cli/setup/index.test.ts b/packages/rstack/tests/cli/setup/index.test.ts index 2429e69..da7c13a 100644 --- a/packages/rstack/tests/cli/setup/index.test.ts +++ b/packages/rstack/tests/cli/setup/index.test.ts @@ -1,6 +1,5 @@ import { spawnSync } from 'node:child_process'; import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; import path from 'node:path'; import { afterEach, beforeEach } from 'rstack/test'; import { RSTACK_BIN_PATH, test } from '#test-helpers'; @@ -30,9 +29,11 @@ const runSetup = (args: string[], runCwd: string = cwd) => }); beforeEach(() => { - cwd = mkdtempSync(path.join(tmpdir(), 'rstack setup ')); + cwd = mkdtempSync(path.join(import.meta.dirname, 'test-temp-rstack setup ')); env = { ...process.env, + // Keep Git from treating the fixture as part of this repository. + GIT_CEILING_DIRECTORIES: import.meta.dirname, GIT_CONFIG_GLOBAL: path.join(cwd, 'global.gitconfig'), GIT_CONFIG_NOSYSTEM: '1', }; @@ -106,7 +107,9 @@ test('installs a custom hooks directory from a nested project', ({ execCli, expe }); test('skips non-Git directories without creating files', ({ execCli, expect }) => { - expect(execCli('setup', { cwd })).toContain('Git hooks setup skipped: not a Git repository.'); + expect(execCli('setup', { cwd, env })).toContain( + 'Git hooks setup skipped: not a Git repository.', + ); expect(existsSync(path.join(cwd, '.rstack'))).toBe(false); }); diff --git a/packages/rstack/tests/cli/staged/fmt.test.ts b/packages/rstack/tests/cli/staged/fmt.test.ts index 1ce8fa1..e9c4829 100644 --- a/packages/rstack/tests/cli/staged/fmt.test.ts +++ b/packages/rstack/tests/cli/staged/fmt.test.ts @@ -35,7 +35,7 @@ const runStaged = () => }); beforeEach(() => { - projectPath = mkdtempSync(path.join(import.meta.dirname, 'staged-fmt-project-')); + projectPath = mkdtempSync(path.join(import.meta.dirname, 'test-temp-staged-fmt-')); env = { ...process.env, GIT_CONFIG_GLOBAL: path.join(projectPath, 'global.gitconfig'), diff --git a/packages/rstack/tests/config/reload-app-config/index.test.ts b/packages/rstack/tests/config/reload-app-config/index.test.ts index d4bcfb3..cdf6206 100644 --- a/packages/rstack/tests/config/reload-app-config/index.test.ts +++ b/packages/rstack/tests/config/reload-app-config/index.test.ts @@ -1,4 +1,4 @@ -import { rm, writeFile } from 'node:fs/promises'; +import { writeFile } from 'node:fs/promises'; import path from 'node:path'; import { getRandomPort, waitForFile } from '@rstackjs/test-utils'; import { test } from '#test-helpers'; @@ -11,8 +11,6 @@ test('should restart dev server and reload config when Rstack config changes', a const dist2 = await prepareDist('dist-2'); const configFile = path.join(import.meta.dirname, 'test-temp-rstack.config.ts'); - await rm(configFile, { force: true }); - await writeFile( configFile, `import { define } from 'rstack'; diff --git a/packages/rstack/tests/exports/lint-subpath/index.test.ts b/packages/rstack/tests/exports/lint-subpath/index.test.ts index 75061e7..90136f1 100644 --- a/packages/rstack/tests/exports/lint-subpath/index.test.ts +++ b/packages/rstack/tests/exports/lint-subpath/index.test.ts @@ -7,6 +7,5 @@ test('should expose lint APIs from `rstack/lint`', async () => { for (const method of commonLintMethods) { expect(lint).toHaveProperty(method); - expect(typeof lint[method]).toBeTruthy(); } }); diff --git a/packages/rstack/tests/exports/test-subpath/index.test.ts b/packages/rstack/tests/exports/test-subpath/index.test.ts index 191be88..74d693a 100644 --- a/packages/rstack/tests/exports/test-subpath/index.test.ts +++ b/packages/rstack/tests/exports/test-subpath/index.test.ts @@ -7,6 +7,5 @@ test('should expose test APIs from `rstack/test`', async () => { for (const method of commonTestMethods) { expect(test).toHaveProperty(method); - expect(typeof test[method]).toBeTruthy(); } }); diff --git a/packages/rstack/tests/fmt/discoverPaths.test.ts b/packages/rstack/tests/fmt/discoverPaths.test.ts index 64f2add..7de16a3 100644 --- a/packages/rstack/tests/fmt/discoverPaths.test.ts +++ b/packages/rstack/tests/fmt/discoverPaths.test.ts @@ -1,31 +1,14 @@ -import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; +import { symlinkSync } from 'node:fs'; import path from 'node:path'; import { expect, test } from 'rstack/test'; import { discoverFmtPaths } from '../../src/fmt/discoverPaths.ts'; - -const withProject = async (callback: (rootPath: string) => Promise): Promise => { - const rootPath = mkdtempSync(path.join(tmpdir(), 'rstack fmt ')); - - try { - await callback(rootPath); - } finally { - rmSync(rootPath, { force: true, recursive: true }); - } -}; - -const writeProjectFile = (rootPath: string, filePath: string, content = ''): string => { - const absolutePath = path.join(rootPath, filePath); - mkdirSync(path.dirname(absolutePath), { recursive: true }); - writeFileSync(absolutePath, content); - return absolutePath; -}; +import { withTempProject, writeProjectFile } from './helpers.ts'; const relativePaths = (rootPath: string, files: string[]): string[] => files.map((filePath) => path.relative(rootPath, filePath)); test('discovers non-binary files in stable order and skips hard-ignored paths', async () => { - await withProject(async (rootPath) => { + await withTempProject(async (rootPath) => { writeProjectFile(rootPath, 'b.ts'); writeProjectFile(rootPath, 'a.js'); writeProjectFile(rootPath, 'folder with spaces/c.ts'); @@ -50,7 +33,7 @@ test('discovers non-binary files in stable order and skips hard-ignored paths', }); test('combines files, directories, and globs without duplicates', async () => { - await withProject(async (rootPath) => { + await withTempProject(async (rootPath) => { const firstFilePath = writeProjectFile(rootPath, 'src/a.ts'); writeProjectFile(rootPath, 'src/b.js'); writeProjectFile(rootPath, 'test/c.ts'); @@ -90,8 +73,7 @@ test('combines files, directories, and globs without duplicates', async () => { }); test('applies nested gitignore rules with child negation', async () => { - await withProject(async (rootPath) => { - mkdirSync(path.join(rootPath, '.git')); + await withTempProject(async (rootPath) => { writeProjectFile(rootPath, '.gitignore', '*.js\ndist/\n'); writeProjectFile(rootPath, 'src/.gitignore', '!keep.js\n'); writeProjectFile(rootPath, 'dist/.gitignore', '!keep.js\n'); @@ -114,8 +96,7 @@ test('applies nested gitignore rules with child negation', async () => { }); test('lets explicit files bypass gitignore', async () => { - await withProject(async (rootPath) => { - mkdirSync(path.join(rootPath, '.git')); + await withTempProject(async (rootPath) => { writeProjectFile(rootPath, '.gitignore', '/generated/\n'); const keepPath = writeProjectFile(rootPath, 'generated/keep.ts'); writeProjectFile(rootPath, 'src/index.ts'); @@ -132,7 +113,7 @@ test('lets explicit files bypass gitignore', async () => { }); test.runIf(process.platform !== 'win32')('does not follow file or directory symlinks', async () => { - await withProject(async (rootPath) => { + await withTempProject(async (rootPath) => { const targetPath = writeProjectFile(rootPath, 'target/index.ts'); symlinkSync(path.join(rootPath, 'target'), path.join(rootPath, 'linked-directory')); symlinkSync(targetPath, path.join(rootPath, 'linked-file.ts')); diff --git a/packages/rstack/tests/fmt/discovery.test.ts b/packages/rstack/tests/fmt/discovery.test.ts index 4b8e866..e90fb40 100644 --- a/packages/rstack/tests/fmt/discovery.test.ts +++ b/packages/rstack/tests/fmt/discovery.test.ts @@ -1,27 +1,10 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; +import { mkdirSync } from 'node:fs'; import path from 'node:path'; import { expect, test } from 'rstack/test'; import { normalizeFmtConfig } from '../../src/fmt/config.ts'; import { discoverFmtFiles } from '../../src/fmt/discovery.ts'; import type { FmtConfig } from '../../src/fmt/types.ts'; - -const withProject = async (callback: (rootPath: string) => Promise): Promise => { - const rootPath = mkdtempSync(path.join(tmpdir(), 'rstack fmt ')); - - try { - await callback(rootPath); - } finally { - rmSync(rootPath, { force: true, recursive: true }); - } -}; - -const writeProjectFile = (rootPath: string, filePath: string, content = ''): string => { - const absolutePath = path.join(rootPath, filePath); - mkdirSync(path.dirname(absolutePath), { recursive: true }); - writeFileSync(absolutePath, content); - return absolutePath; -}; +import { withTempProject, writeProjectFile } from './helpers.ts'; const discover = async (cwd: string, patterns?: string[], config?: FmtConfig, configRoot = cwd) => discoverFmtFiles({ @@ -34,7 +17,7 @@ const relativePaths = (rootPath: string, files: Awaited path.relative(rootPath, file.path)); test('applies config ignore patterns to discovered and explicit files', async () => { - await withProject(async (rootPath) => { + await withTempProject(async (rootPath) => { const keepPath = writeProjectFile(rootPath, 'generated/keep.ts'); const blockedPath = writeProjectFile(rootPath, 'generated/blocked.ts'); writeProjectFile(rootPath, 'src/index.ts'); @@ -52,7 +35,7 @@ test('applies config ignore patterns to discovered and explicit files', async () }); test('applies config ignore patterns outside the config root', async () => { - await withProject(async (rootPath) => { + await withTempProject(async (rootPath) => { const configRoot = path.join(rootPath, 'project'); const filePath = writeProjectFile(rootPath, 'shared/index.ts'); mkdirSync(configRoot); @@ -64,7 +47,7 @@ test('applies config ignore patterns outside the config root', async () => { }); test('resolves parsers and accepts unknown extensions with an explicit parser', async () => { - await withProject(async (rootPath) => { + await withTempProject(async (rootPath) => { writeProjectFile(rootPath, 'index.ts'); writeProjectFile(rootPath, 'source.custom'); writeProjectFile(rootPath, 'unknown.extension'); diff --git a/packages/rstack/tests/fmt/helpers.ts b/packages/rstack/tests/fmt/helpers.ts new file mode 100644 index 0000000..89d280c --- /dev/null +++ b/packages/rstack/tests/fmt/helpers.ts @@ -0,0 +1,23 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; + +export const withTempProject = async ( + callback: (rootPath: string) => Promise, +): Promise => { + const rootPath = mkdtempSync(path.join(import.meta.dirname, 'test-temp-fmt-')); + // Prevent repository-level ignore rules from affecting the fixture. + mkdirSync(path.join(rootPath, '.git')); + + try { + await callback(rootPath); + } finally { + rmSync(rootPath, { force: true, recursive: true }); + } +}; + +export const writeProjectFile = (rootPath: string, filePath: string, content = ''): string => { + const absolutePath = path.join(rootPath, filePath); + mkdirSync(path.dirname(absolutePath), { recursive: true }); + writeFileSync(absolutePath, content); + return absolutePath; +}; diff --git a/packages/rstack/tests/fmt/runner.test.ts b/packages/rstack/tests/fmt/runner.test.ts index f789e21..07377e8 100644 --- a/packages/rstack/tests/fmt/runner.test.ts +++ b/packages/rstack/tests/fmt/runner.test.ts @@ -1,27 +1,9 @@ -import { - chmodSync, - mkdtempSync, - readFileSync, - rmSync, - statSync, - utimesSync, - writeFileSync, -} from 'node:fs'; -import { tmpdir } from 'node:os'; +import { chmodSync, readFileSync, statSync, utimesSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import { expect, test } from 'rstack/test'; import { runFmtFiles } from '../../src/fmt/runner.ts'; import type { FmtFileRequest, FmtMode } from '../../src/fmt/types.ts'; - -const withProject = async (callback: (rootPath: string) => Promise): Promise => { - const rootPath = mkdtempSync(path.join(tmpdir(), 'rstack fmt runner ')); - - try { - await callback(rootPath); - } finally { - rmSync(rootPath, { force: true, recursive: true }); - } -}; +import { withTempProject } from './helpers.ts'; const createRequest = (filePath: string): FmtFileRequest => ({ path: filePath, @@ -40,7 +22,7 @@ const run = (files: FmtFileRequest[], mode: FmtMode = 'write') => }); test('does not rewrite unchanged files', async () => { - await withProject(async (rootPath) => { + await withTempProject(async (rootPath) => { const filePath = path.join(rootPath, 'unchanged.ts'); const timestamp = new Date('2020-01-01T00:00:00.000Z'); writeFileSync(filePath, 'const value = 1;\n'); @@ -60,7 +42,7 @@ test('does not rewrite unchanged files', async () => { }); test('writes changed files', async () => { - await withProject(async (rootPath) => { + await withTempProject(async (rootPath) => { const filePath = path.join(rootPath, 'changed.ts'); writeFileSync(filePath, 'const value=1'); @@ -75,7 +57,7 @@ test('writes changed files', async () => { }); test.runIf(process.platform !== 'win32')('preserves file mode when writing', async () => { - await withProject(async (rootPath) => { + await withTempProject(async (rootPath) => { const filePath = path.join(rootPath, 'executable.ts'); writeFileSync(filePath, 'const value=1'); chmodSync(filePath, 0o744); @@ -88,7 +70,7 @@ test.runIf(process.platform !== 'win32')('preserves file mode when writing', asy for (const mode of ['check', 'list-different'] as const) { test(`${mode} reports differences without writing`, async () => { - await withProject(async (rootPath) => { + await withTempProject(async (rootPath) => { const filePath = path.join(rootPath, 'different.ts'); const source = 'const value=1'; writeFileSync(filePath, source); @@ -105,7 +87,7 @@ for (const mode of ['check', 'list-different'] as const) { } test('continues after a file fails and gives errors exit-code precedence', async () => { - await withProject(async (rootPath) => { + await withTempProject(async (rootPath) => { const invalidPath = path.join(rootPath, 'invalid.ts'); const validPath = path.join(rootPath, 'valid.ts'); writeFileSync(invalidPath, 'const value = ;'); diff --git a/packages/rstack/tests/helpers/cliTest.ts b/packages/rstack/tests/helpers/cliTest.ts index cb583d4..f3ff322 100644 --- a/packages/rstack/tests/helpers/cliTest.ts +++ b/packages/rstack/tests/helpers/cliTest.ts @@ -26,11 +26,10 @@ export type CliTestFixtures = { type CliTest = ReturnType>; function makeBox(title: string) { - const header = `-------- Logs from: "${title}" --------`; - const footer = `-------- Logs from: "${title}" --------`; + const border = `-------- Logs from: "${title}" --------`; return { - header: `\n${header}\n`, - footer: `${footer}\n`, + header: `\n${border}\n`, + footer: `${border}\n`, }; } @@ -114,9 +113,8 @@ export const test: CliTest = baseTest.extend({ } }, execCliAsync: async ({ exec }, use) => { - const execCliAsync: Exec = (command, options = {}) => { - return exec(`node "${RSTACK_BIN_PATH}" ${command}`, options); - }; + const execCliAsync: Exec = (command, options = {}) => + exec(`node "${RSTACK_BIN_PATH}" ${command}`, options); await use(execCliAsync); }, }); diff --git a/packages/rstack/tests/setup/helpers.ts b/packages/rstack/tests/setup/helpers.ts index 141df8e..533cb08 100644 --- a/packages/rstack/tests/setup/helpers.ts +++ b/packages/rstack/tests/setup/helpers.ts @@ -1,6 +1,5 @@ import { type SpawnSyncReturns, spawnSync } from 'node:child_process'; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; import path from 'node:path'; const hooksDir = '.rstack/hooks'; @@ -22,10 +21,15 @@ export const runGit = (cwd: string, args: string[]): string => { }; export const withDirectory = (callback: (cwd: string) => void): void => { - const cwd = mkdtempSync(path.join(tmpdir(), 'rstack hooks ')); + const cwd = mkdtempSync(path.join(import.meta.dirname, 'test-temp-rstack hooks ')); + const gitCeilingDirectories = process.env.GIT_CEILING_DIRECTORIES; + // Keep Git from treating the temporary directory as part of this repository. + process.env.GIT_CEILING_DIRECTORIES = import.meta.dirname; + try { callback(cwd); } finally { + restoreEnv('GIT_CEILING_DIRECTORIES', gitCeilingDirectories); rmSync(cwd, { force: true, recursive: true }); } }; diff --git a/packages/rstack/tests/setup/hooks.test.ts b/packages/rstack/tests/setup/hooks.test.ts index ab11a43..9c81021 100644 --- a/packages/rstack/tests/setup/hooks.test.ts +++ b/packages/rstack/tests/setup/hooks.test.ts @@ -1,9 +1,9 @@ import { spawnSync } from 'node:child_process'; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; +import { mkdirSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import { expect, test } from 'rstack/test'; import { createHookFiles } from '../../src/setup/hooks.ts'; +import { withDirectory } from './helpers.ts'; test('generates the dispatcher and all client-side Git hook shims', () => { const { runner, ...shims } = createHookFiles(); @@ -29,18 +29,17 @@ test('generates the dispatcher and all client-side Git hook shims', () => { }); test.runIf(process.platform !== 'win32')('runs generated hooks', () => { - const directory = mkdtempSync(path.join(tmpdir(), 'rstack hooks ')); - const hooksDirectory = path.join(directory, 'hooks with spaces'); - const generatedDirectory = path.join(hooksDirectory, '_'); - const generatedHook = path.join(generatedDirectory, 'pre-commit'); - const userHook = path.join(hooksDirectory, 'pre-commit'); - const files = createHookFiles(); - const env: NodeJS.ProcessEnv = { - ...process.env, - XDG_CONFIG_HOME: path.join(directory, 'config'), - }; + withDirectory((directory) => { + const hooksDirectory = path.join(directory, 'hooks with spaces'); + const generatedDirectory = path.join(hooksDirectory, '_'); + const generatedHook = path.join(generatedDirectory, 'pre-commit'); + const userHook = path.join(hooksDirectory, 'pre-commit'); + const files = createHookFiles(); + const env: NodeJS.ProcessEnv = { + ...process.env, + XDG_CONFIG_HOME: path.join(directory, 'config'), + }; - try { mkdirSync(generatedDirectory, { recursive: true }); writeFileSync(path.join(generatedDirectory, 'runner'), files.runner); writeFileSync(generatedHook, files['pre-commit']); @@ -72,7 +71,5 @@ printf 'unreachable\\n' expect(errexitResult.status).toBe(1); expect(errexitResult.stdout).toBe('Rstack - pre-commit hook failed (code 1)\n'); - } finally { - rmSync(directory, { force: true, recursive: true }); - } + }); });