diff --git a/CHANGELOG.md b/CHANGELOG.md index d28b0cac0..4841ef0e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -273,6 +273,14 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md ### Development +- The CCTV launcher and preview-server tests resolve their temporary fixture + root through `fs.realpath`, so they pass on macOS, where the system temp + directory is reached through a symlink and the paths the tests compare would + otherwise differ. The launcher, preview-server and tool-project tests share one + helper that resolves the root, and a case that builds a symlinked temp root + explicitly keeps it covered on Linux, whose own temp root is not symlinked. + Contributed by VassagoDevteam (#301). + - Extract application lifecycle and viewer exports. Split standalone startup into scene setup, controls, layer registration, tools and loading UI. Startup failure and terminal shutdown release acquired resources and cancel delayed work. diff --git a/scripts/format-scope.json b/scripts/format-scope.json index be3cba0ed..10ab26731 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -7,6 +7,8 @@ "scripts/check-package-boundaries.mjs", "scripts/precompute-cctv-heights.mjs", "src/tooling/precomputeCctvHeights.test.mjs", + "src/tooling/fixtureRoot.mjs", + "src/tooling/fixtureRoot.test.mjs", "src/tooling/format.test.mjs", "src/tooling/packageBoundaries.test.mjs", "src/data/infrastructure.js", diff --git a/src/devCctv.test.mjs b/src/devCctv.test.mjs index 0b62da61c..c4f66f832 100644 --- a/src/devCctv.test.mjs +++ b/src/devCctv.test.mjs @@ -6,12 +6,15 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; +import { makeFixtureRoot } from './tooling/fixtureRoot.mjs'; const run = promisify(execFile); const bashTest = process.platform === 'win32' ? test.skip : test; async function launch(overrides = {}, dotenv = '', omitCctv = false) { - const root = await fs.mkdtemp(path.join(os.tmpdir(), 'gev-cctv-launch-')); + // Physical path: the launched process reports its cwd resolved, and macOS + // reaches the temp directory through a symlink. + const root = await makeFixtureRoot('gev-cctv-launch-'); try { await fs.mkdir(path.join(root, 'scripts')); await fs.mkdir(path.join(root, 'bin')); diff --git a/src/toolProjectRoot.test.mjs b/src/toolProjectRoot.test.mjs index c00b52db5..0e766e485 100644 --- a/src/toolProjectRoot.test.mjs +++ b/src/toolProjectRoot.test.mjs @@ -8,6 +8,7 @@ import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import { projectRoot } from '../scripts/project-root.mjs'; import { inspectSetup } from '../scripts/setup-doctor.mjs'; +import { makeFixtureRoot } from './tooling/fixtureRoot.mjs'; const run = promisify(execFile); const sourceRoot = fileURLToPath(new URL('../', import.meta.url)); @@ -60,7 +61,9 @@ const bashTest = process.platform === 'win32' ? test.skip : test; bashTest( 'launcher reads the selected project keys and starts its command while source checks use the installation', async () => { - const root = await mkdtemp(path.join(os.tmpdir(), 'gev other project-')); + // Physical path: the launched process reports its cwd resolved, and macOS + // reaches the temp directory through a symlink. + const root = await makeFixtureRoot('gev other project-'); try { const bin = path.join(root, 'bin'); await mkdir(bin); diff --git a/src/tooling/fixtureRoot.mjs b/src/tooling/fixtureRoot.mjs new file mode 100644 index 000000000..8624e682b --- /dev/null +++ b/src/tooling/fixtureRoot.mjs @@ -0,0 +1,21 @@ +import { mkdtemp, realpath } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; + +/** + * Create a temporary directory for a test fixture, resolved to its physical + * path. + * + * The system temp directory is reached through a symlink on macOS + * (`/var` -> `/private/var`, `/tmp` -> `/private/tmp`). A test that gives the + * logical path to something that reports paths back — a launched process's + * working directory, a bundler's project root — then compares two spellings of + * the same directory and fails. Resolving the root once, here, keeps both sides + * on the physical path. + * + * @param {string} prefix - Directory name prefix, e.g. `gev-preview-`. + * @returns {Promise} The fixture root, with no symlinked component. + */ +export async function makeFixtureRoot(prefix) { + return realpath(await mkdtemp(path.join(tmpdir(), prefix))); +} diff --git a/src/tooling/fixtureRoot.test.mjs b/src/tooling/fixtureRoot.test.mjs new file mode 100644 index 000000000..0d9770046 --- /dev/null +++ b/src/tooling/fixtureRoot.test.mjs @@ -0,0 +1,55 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { mkdir, mkdtemp, realpath, rm, symlink } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { makeFixtureRoot } from './fixtureRoot.mjs'; + +// Creating a symlink needs a privilege Windows does not grant by default, and +// Windows reads its temp directory from other variables than TMPDIR. The case +// therefore covers macOS and Linux; a Windows equivalent would have to build a +// directory junction and set TEMP. +const symlinkTest = process.platform === 'win32' ? test.skip : test; + +symlinkTest( + 'a fixture root is physical even when the temp root is a symlink', + async (t) => { + // macOS reaches its own temp directory through a symlink and Linux does + // not, so build one explicitly: without it this check would pass on the + // Linux runner while every Mac contributor still saw the failure it exists + // to prevent. + const scratch = await realpath( + await mkdtemp(path.join(tmpdir(), 'gev-fixture-root-')), + ); + t.after(() => rm(scratch, { recursive: true, force: true })); + const physical = path.join(scratch, 'physical'); + const viaLink = path.join(scratch, 'link'); + await mkdir(physical); + await symlink(physical, viaLink); + + const before = process.env.TMPDIR; + process.env.TMPDIR = viaLink; + t.after(() => { + if (before === undefined) delete process.env.TMPDIR; + else process.env.TMPDIR = before; + }); + // Fail rather than pass vacuously: if the temp root is not the symlink, + // the condition under test was never set up. + assert.equal( + tmpdir(), + viaLink, + 'the symlinked temp root was not in force, so nothing was exercised', + ); + + const root = await makeFixtureRoot('gev-fixture-case-'); + assert.equal( + root, + await realpath(root), + 'the fixture root still contains a symlink', + ); + assert.ok( + root.startsWith(`${physical}${path.sep}`), + `${root} is not under the physical temp root ${physical}`, + ); + }, +); diff --git a/src/tooling/previewServing.test.mjs b/src/tooling/previewServing.test.mjs index 65993acef..87a49b196 100644 --- a/src/tooling/previewServing.test.mjs +++ b/src/tooling/previewServing.test.mjs @@ -1,11 +1,11 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { mkdtemp, writeFile, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; +import { writeFile, rm } from 'node:fs/promises'; import path from 'node:path'; import { build, createServer, preview } from 'vite'; import { localProviderPlugins } from '../../server/providers/local.js'; import { apiNotFoundPlugin } from '../../server/standalone/api-not-found.js'; +import { makeFixtureRoot } from './fixtureRoot.mjs'; test('data providers have both hooks; credential editing stays development-only', () => { for (const plugin of localProviderPlugins()) { @@ -27,7 +27,9 @@ test('data providers have both hooks; credential editing stays development-only' }); test('real dev and built-preview servers serve provider JSON and terminate unknown APIs', async (t) => { - const root = await mkdtemp(path.join(tmpdir(), 'gev-preview-')); + // Physical path: Vite's root and the files written under it must agree on one + // spelling, and macOS reaches the temp directory through a symlink. + const root = await makeFixtureRoot('gev-preview-'); t.after(() => rm(root, { recursive: true, force: true })); await writeFile( path.join(root, 'index.html'),