refactor(test): migrate bun-install tests to concurrent execution (#25895)

This commit is contained in:
robobun
2026-01-08 01:06:03 -08:00
committed by GitHub
parent bf1e4922b4
commit a1f1252771
3 changed files with 9045 additions and 8317 deletions

View File

@@ -34,3 +34,38 @@ error: "workspaces.packages" expects an array of strings, e.g.
`;
exports[`should handle modified git resolutions in bun.lock 1`] = `"{"lockfileVersion":0,"configVersion":1,"workspaces":{"":{"dependencies":{"jquery":"3.7.1"}}},"packages":{"jquery":["jquery@git+ssh://git@github.com/dylan-conway/install-test-8.git#3a1288830817d13da39e9231302261896f8721ea",{},"3a1288830817d13da39e9231302261896f8721ea"]}}"`;
exports[`bun-install should report error on invalid format for package.json 1`] = `
"1 | foo
^
error: Unexpected foo
at [dir]/package.json:1:1
ParserError: failed to parse '[dir]/package.json'
"
`;
exports[`bun-install should report error on invalid format for dependencies 1`] = `
"1 | {"name":"foo","version":"0.0.1","dependencies":[]}
^
error: dependencies expects a map of specifiers, e.g.
"dependencies": {
<green>"bun"<r>: <green>"latest"<r>
}
at [dir]/package.json:1:33
"
`;
exports[`bun-install should report error on invalid format for workspaces 1`] = `
"1 | {"name":"foo","version":"0.0.1","workspaces":{"packages":{"bar":true}}}
^
error: "workspaces.packages" expects an array of strings, e.g.
"workspaces": {
"packages": [
"path/to/package"
]
}
at [dir]/package.json:1:58
"
`;
exports[`bun-install should handle modified git resolutions in bun.lock 1`] = `"{"lockfileVersion":0,"configVersion":1,"workspaces":{"":{"dependencies":{"jquery":"3.7.1"}}},"packages":{"jquery":["jquery@git+ssh://git@github.com/dylan-conway/install-test-8.git#3a1288830817d13da39e9231302261896f8721ea",{},"3a1288830817d13da39e9231302261896f8721ea"]}}"`;

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,30 @@
* This file can be directly run
*
* PACKAGE_DIR_TO_USE=(realpath .) bun test/cli/install/dummy.registry.ts
*
* ## Concurrent Test Support
*
* This module supports running tests concurrently by using URL prefixes to isolate
* each test's registry requests. Each test gets a unique context with:
* - Its own handler for registry requests
* - Its own package_dir (temp directory)
* - Its own request counter
* - A unique registry URL with a prefix (e.g., http://localhost:PORT/test-123/)
*
* ### Usage for concurrent tests:
* ```typescript
* it("my test", async () => {
* const ctx = await createTestContext({ linker: "hoisted" });
* try {
* const urls: string[] = [];
* setContextHandler(ctx, dummyRegistry(urls, ctx));
* // Use ctx.package_dir, ctx.registry_url, ctx.requested
* await writeFile(join(ctx.package_dir, "package.json"), ...);
* } finally {
* destroyTestContext(ctx);
* }
* });
* ```
*/
import { file, Server } from "bun";
import { tmpdirSync } from "harness";
@@ -19,24 +43,202 @@ type Pkg = {
tarball: string;
};
};
let handler: Handler;
let server: Server;
export let package_dir: string;
export let requested: number;
export let root_url: string;
export let check_npm_auth_type = { check: true };
export async function write(path: string, content: string | object) {
if (!package_dir) throw new Error("writeToPackageDir() must be called in a test");
// ============================================================================
// Concurrent Test Context Support
// ============================================================================
await Bun.write(join(package_dir, path), typeof content === "string" ? content : JSON.stringify(content));
/** Global counter for generating unique test IDs */
let testIdCounter = 0;
/**
* Context for a single test, containing all per-test state.
* Use this for concurrent test execution.
*/
export interface TestContext {
/** Unique identifier for this test context (e.g., "test-1") */
id: string;
/** The package directory for this test (a unique temp directory) */
package_dir: string;
/** Number of requests made to this test's handler */
requested: number;
/** The handler for this test's registry requests */
handler: Handler;
/** The registry URL for this test (includes prefix, e.g., http://localhost:PORT/test-1/) */
registry_url: string;
}
export function read(path: string) {
return Bun.file(join(package_dir, path));
/** Map of test ID prefix -> test context */
const testContexts = new Map<string, TestContext>();
/** Default handler for unmatched requests */
function defaultHandler(): Response {
return new Response("Tea Break~", { status: 418 });
}
export function dummyRegistry(urls: string[], info: any = { "0.0.2": {} }, numberOfTimesTo500PerURL = 0) {
/**
* Extract the test ID prefix from a URL path.
* URL format: /test-123/package-name or /test-123/@scope%2fpackage-name
*/
function extractTestPrefix(url: string): { prefix: string; remainingPath: string } | null {
const urlObj = new URL(url);
const path = urlObj.pathname;
// Match /test-N/ followed by anything
const match = path.match(/^\/(test-\d+)(\/.*)?$/);
if (match) {
return {
prefix: match[1],
remainingPath: match[2] || "/",
};
}
return null;
}
/**
* Creates a new isolated test context for concurrent test execution.
* Each context has its own handler, package_dir, and request counter.
*
* The bunfig.toml is automatically created with the prefixed registry URL.
*
* @param opts - Optional configuration for the test context
* @returns A new TestContext that should be used for all test operations
*/
export async function createTestContext(opts?: { linker: "hoisted" | "isolated" }): Promise<TestContext> {
const id = `test-${++testIdCounter}`;
const pkg_dir = tmpdirSync();
const ctx: TestContext = {
id,
package_dir: pkg_dir,
requested: 0,
handler: defaultHandler,
registry_url: `${root_url}/${id}/`,
};
testContexts.set(id, ctx);
// Create bunfig.toml with the prefixed registry URL
await writeFile(
join(pkg_dir, "bunfig.toml"),
`
[install]
cache = false
registry = "${ctx.registry_url}"
saveTextLockfile = false
${opts ? `linker = "${opts.linker}"` : ""}
`,
);
return ctx;
}
/**
* Cleans up a test context after the test is done.
* This removes the context from the registry so requests won't be routed to it.
*/
export function destroyTestContext(ctx: TestContext): void {
testContexts.delete(ctx.id);
}
/**
* Sets the handler for a specific test context.
* The handler will receive all requests that have this context's URL prefix.
*/
export function setContextHandler(ctx: TestContext, newHandler: Handler): void {
ctx.handler = newHandler;
}
/**
* Creates a dummy registry handler for a specific test context.
* This is the concurrent-safe version that uses the context's registry_url for tarballs.
*
* @param ctx - The test context (provides registry_url for tarball URLs)
* @param urls - Array to collect requested URLs (passed by reference)
* @param info - Package version info (default: { "0.0.2": {} })
* @param numberOfTimesTo500PerURL - Number of times to return 500 before success (for retry testing)
*/
export function dummyRegistryForContext(
ctx: TestContext,
urls: string[],
info: any = { "0.0.2": {} },
numberOfTimesTo500PerURL = 0,
): Handler {
let retryCountsByURL = new Map<string, number>();
const _handler: Handler = async request => {
urls.push(request.url);
const url = request.url.replaceAll("%2f", "/");
let status = 200;
if (numberOfTimesTo500PerURL > 0) {
let currentCount = retryCountsByURL.get(request.url);
if (currentCount === undefined) {
retryCountsByURL.set(request.url, numberOfTimesTo500PerURL);
status = 500;
} else {
retryCountsByURL.set(request.url, currentCount - 1);
status = currentCount > 0 ? 500 : 200;
}
}
expect(request.method).toBe("GET");
if (url.endsWith(".tgz")) {
return new Response(file(join(import.meta.dir, basename(url).toLowerCase())), { status });
}
expect(request.headers.get("accept")).toBe(
"application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*",
);
if (check_npm_auth_type.check) {
expect(request.headers.get("npm-auth-type")).toBe(null);
}
expect(await request.text()).toBe("");
// For context-based requests, strip the test prefix
const urlObj = new URL(url);
const pathAfterPrefix = urlObj.pathname.replace(`/${ctx.id}/`, "/");
const name = pathAfterPrefix.slice(1); // Remove leading slash
const versions: Record<string, Pkg> = {};
let version;
for (version in info) {
if (!/^[0-9]/.test(version)) continue;
versions[version] = {
name,
version,
dist: {
tarball: `${ctx.registry_url}${name}-${info[version].as ?? version}.tgz`,
},
...info[version],
};
}
return new Response(
JSON.stringify({
name,
versions,
"dist-tags": {
latest: info.latest ?? version,
},
}),
{ status },
);
};
return _handler;
}
/**
* Creates a dummy registry handler (legacy version for backward compatibility).
*
* @param urls - Array to collect requested URLs (passed by reference)
* @param info - Package version info (default: { "0.0.2": {} })
* @param numberOfTimesTo500PerURL - Number of times to return 500 before success (for retry testing)
*/
export function dummyRegistry(urls: string[], info: any = { "0.0.2": {} }, numberOfTimesTo500PerURL = 0): Handler {
let retryCountsByURL = new Map<string, number>();
const _handler: Handler = async request => {
urls.push(request.url);
@@ -96,19 +298,58 @@ export function dummyRegistry(urls: string[], info: any = { "0.0.2": {} }, numbe
return _handler;
}
// ============================================================================
// Legacy API (for backward compatibility with non-concurrent tests)
// ============================================================================
/** @deprecated Use createTestContext() for concurrent tests */
export let package_dir: string;
/** @deprecated Use ctx.requested for concurrent tests */
export let requested: number;
/** Legacy handler for non-prefixed requests */
let legacyHandler: Handler = defaultHandler;
export async function write(path: string, content: string | object) {
if (!package_dir) throw new Error("writeToPackageDir() must be called in a test");
await Bun.write(join(package_dir, path), typeof content === "string" ? content : JSON.stringify(content));
}
export function read(path: string) {
return Bun.file(join(package_dir, path));
}
/** @deprecated Use setContextHandler() for concurrent tests */
export function setHandler(newHandler: Handler) {
handler = newHandler;
legacyHandler = newHandler;
}
function resetHandler() {
setHandler(() => new Response("Tea Break~", { status: 418 }));
setHandler(defaultHandler);
}
export function dummyBeforeAll() {
server = Bun.serve({
async fetch(request) {
const url = request.url;
// Check if this is a prefixed request (for concurrent tests)
const prefixInfo = extractTestPrefix(url);
if (prefixInfo) {
const ctx = testContexts.get(prefixInfo.prefix);
if (ctx) {
ctx.requested++;
return await ctx.handler(request);
}
// Unknown test prefix - return 404
return new Response(`Unknown test prefix: ${prefixInfo.prefix}`, { status: 404 });
}
// Legacy non-prefixed request
requested++;
return await handler(request);
return await legacyHandler(request);
},
port: 0,
});
@@ -117,6 +358,7 @@ export function dummyBeforeAll() {
export function dummyAfterAll() {
server.stop();
testContexts.clear();
}
export function getPort() {
@@ -126,6 +368,8 @@ export function getPort() {
let packageDirGetter: () => string = () => {
return tmpdirSync();
};
/** @deprecated Use createTestContext() for concurrent tests */
export async function dummyBeforeEach(opts?: { linker: "hoisted" | "isolated" }) {
resetHandler();
requested = 0;
@@ -142,6 +386,7 @@ ${opts ? `linker = "${opts.linker}"` : ""}
);
}
/** @deprecated Use destroyTestContext() for concurrent tests */
export async function dummyAfterEach() {
resetHandler();
}