Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions tests/bun/npm-protocol-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { JSDOM } from "npm:jsdom@28.0.0";
import { assertEquals } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import { rewriteNpmProtocolImports } from "./npm-protocol-imports.ts";
import { bunPreloadRewriteFilter, rewriteBunPreloadSource } from "./preload-rewrite.ts";

describe("Bun npm protocol resolution", () => {
it("loads versioned scoped and unscoped npm imports", () => {
Expand Down Expand Up @@ -30,4 +31,45 @@ describe("Bun npm protocol resolution", () => {
].join("\n"),
);
});

it("rewrites extension and test sources after normalizing path separators", () => {
const extensionSource =
'import { defineExtension } from "veryfront/extensions";\nexport const marker = "kept";\n';
const testSource =
'import { BasicTracerProvider } from "npm:@opentelemetry/sdk-trace-base@2.9.0";\nexport const marker = "kept";\n';

for (
const extensionPath of [
"/repo/extensions/ext-yaml/src/adapter.ts",
String.raw`C:\repo\extensions\ext-yaml\src\adapter.ts`,
]
) {
assertEquals(bunPreloadRewriteFilter.test(extensionPath), true);
assertEquals(
rewriteBunPreloadSource(extensionPath, extensionSource, (source) =>
source.replace(
'"veryfront/extensions"',
'"../../../src/extensions/types.ts"',
)),
'import { defineExtension } from "../../../src/extensions/types.ts";\nexport const marker = "kept";\n',
);
}

for (
const testPath of [
"/repo/tests/bun/npm-protocol-resolution.test.ts",
String.raw`C:\repo\tests\bun\npm-protocol-resolution.test.ts`,
"/repo/extensions/fixtures/npm-protocol-resolution.test.ts",
String.raw`C:\repo\extensions\fixtures\npm-protocol-resolution.test.ts`,
]
) {
assertEquals(bunPreloadRewriteFilter.test(testPath), true);
assertEquals(
rewriteBunPreloadSource(testPath, testSource, () => {
throw new Error("test files must not use extension import rewriting");
}),
'import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";\nexport const marker = "kept";\n',
);
}
});
});
27 changes: 27 additions & 0 deletions tests/bun/preload-rewrite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { rewriteNpmProtocolImports } from "./npm-protocol-imports.ts";

export const bunPreloadRewriteFilter =
/(\.test\.[cm]?[jt]sx?|[/\\]extensions[/\\]ext-[^/\\]+[/\\]src[/\\].*\.[cm]?[jt]sx?)$/;

function normalizePreloadPath(path: string): string {
return path.replaceAll("\\", "/");
}

function isExtensionSourcePath(path: string): boolean {
return /(?:^|\/)extensions\/ext-[^/]+\/src\/.*\.[cm]?[jt]sx?$/.test(path);
}

export function rewriteBunPreloadSource(
path: string,
source: string,
rewriteExtensionImports: (source: string) => string | null,
): string {
const posixPath = normalizePreloadPath(path);
let contents = isExtensionSourcePath(posixPath)
? rewriteExtensionImports(source) ?? source
: source;
if (/\.test\.[cm]?[jt]sx?$/.test(posixPath)) {
contents = rewriteNpmProtocolImports(contents) ?? contents;
}
return contents;
}
25 changes: 12 additions & 13 deletions tests/bun/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { plugin } from "bun";
import { existsSync, readFileSync, statSync } from "fs";
import { dirname, extname, relative, resolve, sep } from "path";
import { fileURLToPath } from "url";
import { rewriteModuleSpecifiers, rewriteNpmProtocolImports } from "./npm-protocol-imports.ts";
import { rewriteModuleSpecifiers } from "./npm-protocol-imports.ts";
import { bunPreloadRewriteFilter, rewriteBunPreloadSource } from "./preload-rewrite.ts";

const projectRoot = resolve(import.meta.dir, "../..");

Expand Down Expand Up @@ -144,21 +145,19 @@ plugin({
// import-looking fixture strings and comments untouched.
build.onLoad(
{
filter:
/(\.test\.[cm]?[jt]sx?|[/\\]extensions[/\\]ext-[^/\\]+[/\\]src[/\\].*\.[cm]?[jt]sx?)$/,
filter: bunPreloadRewriteFilter,
},
(args) => {
const posixPath = args.path.split(sep).join("/");
const source = readFileSync(args.path, "utf8");
let contents = posixPath.includes("/extensions/")
? rewriteModuleSpecifiers(
source,
(specifier) => workspaceModuleSpecifier(args.path, specifier),
) ?? source
: source;
if (/\.test\.[cm]?[jt]sx?$/.test(posixPath)) {
contents = rewriteNpmProtocolImports(contents) ?? contents;
}
const contents = rewriteBunPreloadSource(
args.path,
source,
(extensionSource) =>
rewriteModuleSpecifiers(
extensionSource,
(specifier) => workspaceModuleSpecifier(args.path, specifier),
),
);
const extension = extname(args.path).toLowerCase();
const loader = extension === ".tsx"
? "tsx"
Expand Down
17 changes: 16 additions & 1 deletion tests/bun/runner-args.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import { spawnSync } from "node:child_process";
import { EventEmitter } from "node:events";
import { readFileSync } from "node:fs";
import test from "node:test";
import { fileURLToPath } from "node:url";
import {
buildBunTestArgs,
buildIsolatedBunTestRuns,
registerBunWorkspaceCleanup,
} from "./runner-args.mjs";

const runTestsPath = fileURLToPath(new URL("./run-tests.mjs", import.meta.url));

test("buildBunTestArgs caps concurrency without enabling concurrent test semantics", () => {
const args = buildBunTestArgs(["one.test.ts", "two.test.ts"], 3);

Expand Down Expand Up @@ -50,7 +53,7 @@ test("the Bun runner drains child output and exits naturally", () => {
test("the Bun runner fails loudly when filters select no files", () => {
const result = spawnSync(
process.execPath,
[new URL("./run-tests.mjs", import.meta.url).pathname],
[runTestsPath],
{
env: { ...process.env, BUN_TEST_INCLUDE: "missing-bun-fixture.test.ts" },
encoding: "utf8",
Expand All @@ -62,6 +65,18 @@ test("the Bun runner fails loudly when filters select no files", () => {
assert.doesNotMatch(result.stdout, /0 passed, 0 failed/);
});

test("the empty-selection spawn path is decoded through fileURLToPath", () => {
const source = readFileSync(fileURLToPath(import.meta.url), "utf8");
const runnerPathnameAccess = 'run-tests.mjs", import.meta.url)' +
".pathname";

assert.match(
source,
/const runTestsPath = fileURLToPath\(new URL\("\.\/run-tests\.mjs", import\.meta\.url\)\)/,
);
assert.equal(source.includes(runnerPathnameAccess), false);
});

test("Bun workspace cleanup runs before termination signals are re-raised", () => {
const runtimeProcess = new EventEmitter();
runtimeProcess.pid = 123;
Expand Down
161 changes: 134 additions & 27 deletions tests/bun/workspace-packages.mjs
Original file line number Diff line number Diff line change
@@ -1,31 +1,109 @@
import { existsSync, mkdirSync, readFileSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
import {
existsSync,
mkdirSync,
readFileSync,
renameSync,
rmdirSync,
rmSync,
writeFileSync,
} from "node:fs";
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { randomUUID } from "node:crypto";
import { createHash, randomUUID } from "node:crypto";
import { ensureDirectoryLink } from "../ensure-npm-links.mjs";

const MARKER_NAME = ".veryfront-bun-workspace-package.json";
const LOCK_NAME = ".veryfront-bun-workspace-packages.lock";
const RECLAIMER_GUARD_PREFIX = ".veryfront-bun-workspace-packages.reclaiming-";
const LOCK_OWNER = "veryfront-bun-tests";

function readJson(path) {
return JSON.parse(readFileSync(path, "utf8"));
}

function activePreparationError() {
return new Error("Bun workspace package preparation is already active");
}

function waitForReclaimRaceTestBarrier() {
const barrierPath = process.env.VF_BUN_WORKSPACE_RECLAIM_BARRIER_PATH;
if (!barrierPath) return;
mkdirSync(barrierPath, { recursive: true });
writeFileSync(
join(barrierPath, `ready-${process.pid}-${randomUUID()}`),
"ready\n",
{ flag: "wx" },
);
const releasePath = join(barrierPath, "release");
const deadline = Date.now() + 10_000;
const signal = new Int32Array(new SharedArrayBuffer(4));
while (!existsSync(releasePath)) {
if (Date.now() >= deadline) {
throw new Error("Timed out waiting for the Bun workspace reclaim barrier");
}
Atomics.wait(signal, 0, 0, 10);
}
}

function reclaimerGuardPath(nodeModulesPath, token) {
const digest = createHash("sha256").update(token).digest("hex");
return join(nodeModulesPath, `${RECLAIMER_GUARD_PREFIX}${digest}`);
}

function createPreparationLock(nodeModulesPath, lockPath, token) {
const stagingPath = join(
nodeModulesPath,
`${LOCK_NAME}.staging-${process.pid}-${token}`,
);
try {
mkdirSync(stagingPath);
writeFileSync(
join(stagingPath, MARKER_NAME),
`${JSON.stringify({ owner: LOCK_OWNER, pid: process.pid, token })}\n`,
);
if (process.env.VF_BUN_WORKSPACE_INTERRUPT_BEFORE_LOCK_PUBLISH === "1") {
process.exit(18);
}
if (existsSync(lockPath)) {
const conflict = new Error("Bun workspace package lock already exists");
conflict.code = "EEXIST";
throw conflict;
}
renameSync(stagingPath, lockPath);
} catch (error) {
rmSync(stagingPath, { recursive: true, force: true });
throw error;
}
}

export function isDirectoryPathConflict(error, path) {
return existsSync(path) &&
["EACCES", "EEXIST", "ENOTEMPTY", "EPERM"].includes(error?.code);
}

function acquirePreparationLock(nodeModulesPath) {
mkdirSync(nodeModulesPath, { recursive: true });
const lockPath = join(nodeModulesPath, LOCK_NAME);
const token = randomUUID();
let reclaimedGuardPath;
try {
mkdirSync(lockPath);
createPreparationLock(nodeModulesPath, lockPath, token);
} catch (error) {
if (error?.code === "EEXIST") {
if (!reclaimStalePreparationLock(lockPath)) {
throw new Error("Bun workspace package preparation is already active");
if (isDirectoryPathConflict(error, lockPath)) {
reclaimedGuardPath = reclaimStalePreparationLock(
nodeModulesPath,
lockPath,
);
if (!reclaimedGuardPath) {
throw activePreparationError();
}
try {
mkdirSync(lockPath);
createPreparationLock(nodeModulesPath, lockPath, token);
} catch (retryError) {
if (retryError?.code === "EEXIST") {
throw new Error("Bun workspace package preparation is already active");
if (isDirectoryPathConflict(retryError, lockPath)) {
// This generation tombstone must remain permanent. Otherwise an
// arbitrarily delayed stale-generation reader could reuse it after a
// fresh runner acquires lockPath and move that fresh live lock.
throw activePreparationError();
}
throw retryError;
}
Expand All @@ -34,41 +112,70 @@ function acquirePreparationLock(nodeModulesPath) {
}
}

const token = randomUUID();
try {
writeFileSync(
join(lockPath, MARKER_NAME),
`${JSON.stringify({ owner: LOCK_OWNER, pid: process.pid, token })}\n`,
);
} catch (error) {
rmSync(lockPath, { recursive: true, force: true });
throw error;
}

return { lockPath, token };
}

export function reclaimStalePreparationLock(lockPath, runtimeProcess = process) {
function isOwnedLockMarker(marker) {
return marker?.owner === LOCK_OWNER &&
Number.isSafeInteger(marker.pid) &&
marker.pid >= 1 &&
typeof marker.token === "string" &&
marker.token.length > 0;
}

function sameLockGeneration(left, right) {
return left?.owner === right?.owner &&
left?.pid === right?.pid &&
left?.token === right?.token;
}

function reclaimStalePreparationLock(
nodeModulesPath,
lockPath,
runtimeProcess = process,
) {
let marker;
try {
marker = readJson(join(lockPath, MARKER_NAME));
} catch {
return false;
}

if (marker?.owner !== LOCK_OWNER) {
return false;
}
if (!Number.isSafeInteger(marker.pid) || marker.pid < 1) {
if (!isOwnedLockMarker(marker)) {
return false;
}
try {
runtimeProcess.kill(marker.pid, 0);
return false;
} catch (error) {
if (error?.code !== "ESRCH") return false;
rmSync(lockPath, { recursive: true, force: true });
return true;
waitForReclaimRaceTestBarrier();
const guardPath = reclaimerGuardPath(nodeModulesPath, marker.token);
try {
renameSync(lockPath, guardPath);
} catch (renameError) {
if (
renameError?.code === "ENOENT" ||
isDirectoryPathConflict(renameError, guardPath)
) {
return false;
}
throw renameError;
}

let claimedMarker;
try {
claimedMarker = readJson(join(guardPath, MARKER_NAME));
} catch {
return false;
}
if (!sameLockGeneration(marker, claimedMarker)) {
return false;
}
// Lock markers are immutable after publication. Matching the generation
// after the atomic rename proves this process claimed the stale directory;
// keep that path as a permanent tombstone for delayed readers.
return guardPath;
}
}

Expand Down
Loading