mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-30 07:14:39 +08:00
Keep self-hosting emission tests from blocking the test worker (#883)
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { expect, test } from "vitest";
|
||||
import { analyze, compile, compileC, deserializeModule, serializeModule } from "@scriptc/compiler";
|
||||
import { compile, compileC, deserializeModule, serializeModule } from "@scriptc/compiler";
|
||||
import { emitLlvmModule } from "../../packages/compiler/src/backend/llvm/emitter.js";
|
||||
import { scalarizeNumericRecords } from "../../packages/compiler/src/ir/scalar-records.js";
|
||||
import { everyStmtList } from "../../packages/compiler/src/ir/traverse.js";
|
||||
import { type IrModule } from "../../packages/compiler/src/ir/ir.js";
|
||||
import { analyzeInChild, compileInChild } from "./self-hosting-compiler-process.js";
|
||||
import { spawnResult } from "./spawn-result.js";
|
||||
import {
|
||||
moduleUsesCopying,
|
||||
moduleUsesDynInvoke,
|
||||
@@ -53,8 +54,8 @@ interface Emission {
|
||||
block: string;
|
||||
}
|
||||
|
||||
test("the production layout and metadata stage lowers statically", () => {
|
||||
const { coverage } = analyze(entry, { dynamic: false });
|
||||
test("the production layout and metadata stage lowers statically", async () => {
|
||||
const coverage = await analyzeInChild(entry, { dynamic: false });
|
||||
expect(coverage.preflightFailed).toBe(false);
|
||||
expect(coverage.diagnostics).toEqual([]);
|
||||
expect(coverage.stats.statementsTotal).toBeGreaterThan(1200);
|
||||
@@ -72,7 +73,7 @@ for (const backend of ["llvm"] as const) {
|
||||
const executable = (name: string): string =>
|
||||
join(dir, name + (process.platform === "win32" ? ".exe" : ""));
|
||||
try {
|
||||
const built = await compile(entry, {
|
||||
const built = await compileInChild(entry, {
|
||||
outDir: dir,
|
||||
outPath: executable("stage"),
|
||||
backend,
|
||||
@@ -83,14 +84,23 @@ for (const backend of ["llvm"] as const) {
|
||||
if (!built.ok)
|
||||
throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
expect(built.backend).toBe(backend);
|
||||
const run = (mod: IrModule, request: EmissionRequest, name: string, status = 0): string => {
|
||||
const run = async (
|
||||
mod: IrModule,
|
||||
request: EmissionRequest,
|
||||
name: string,
|
||||
status = 0,
|
||||
): Promise<string> => {
|
||||
const input = join(dir, "input.json");
|
||||
const config = join(dir, "config.json");
|
||||
writeFileSync(input, serializeModule(mod));
|
||||
writeFileSync(config, JSON.stringify(request));
|
||||
const args = [input, config];
|
||||
const oracle = spawnSync(process.execPath, ["--import", "tsx", entry, ...args], options);
|
||||
const native = spawnSync(built.binaryPath, args, options);
|
||||
const oracle = await spawnResult(
|
||||
process.execPath,
|
||||
["--import", "tsx", entry, ...args],
|
||||
options,
|
||||
);
|
||||
const native = await spawnResult(built.binaryPath, args, options);
|
||||
for (const result of [oracle, native]) {
|
||||
expect(result.error, name).toBeUndefined();
|
||||
expect(result.signal, `${name}: ${result.stderr}`).toBeNull();
|
||||
@@ -106,7 +116,7 @@ for (const backend of ["llvm"] as const) {
|
||||
};
|
||||
|
||||
for (const item of emissionCases()) {
|
||||
const text = run(item.module, item.request, item.name);
|
||||
const text = await run(item.module, item.request, item.name);
|
||||
const emitted = JSON.parse(text) as Emission;
|
||||
const definitions = [
|
||||
...emitted.declarations,
|
||||
@@ -143,12 +153,12 @@ for (const backend of ["llvm"] as const) {
|
||||
loc: { file: invalid.sourceFile, start: 0, end: 0 },
|
||||
},
|
||||
];
|
||||
expect(run(invalid, emissionRequest(), "missing base", 1)).toContain(
|
||||
expect(await run(invalid, emissionRequest(), "missing base", 1)).toContain(
|
||||
"undeclared base class Missing",
|
||||
);
|
||||
const absent = emissionRequest();
|
||||
absent.classObjects = ["Missing"];
|
||||
expect(run(emissionModule(), absent, "missing class object", 1)).toContain(
|
||||
expect(await run(emissionModule(), absent, "missing class object", 1)).toContain(
|
||||
"class object for unknown class Missing",
|
||||
);
|
||||
|
||||
@@ -176,7 +186,7 @@ for (const backend of ["llvm"] as const) {
|
||||
},
|
||||
];
|
||||
for (const { request, message } of invalidRequests) {
|
||||
expect(run(emissionModule(), request, message, 1)).toContain(message);
|
||||
expect(await run(emissionModule(), request, message, 1)).toContain(message);
|
||||
}
|
||||
const unsupported = emissionModule();
|
||||
unsupported.unions = [
|
||||
@@ -186,7 +196,7 @@ for (const backend of ["llvm"] as const) {
|
||||
},
|
||||
];
|
||||
expect(
|
||||
run(
|
||||
await run(
|
||||
unsupported,
|
||||
{ ...emissionRequest(), joinUnions: ["containers"] },
|
||||
"unsupported union join",
|
||||
@@ -225,7 +235,7 @@ for (const backend of ["llvm"] as const) {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const native = JSON.parse(run(mod, request, source)) as Emission;
|
||||
const native = JSON.parse(await run(mod, request, source)) as Emission;
|
||||
// Splice the native-generated sections into the real program. The
|
||||
// full emitter still runs on Node; these type and helper definitions
|
||||
// are supplied by the compiled production stage under test.
|
||||
@@ -256,8 +266,8 @@ for (const backend of ["llvm"] as const) {
|
||||
regex: moduleUsesRegex(mod),
|
||||
copying: moduleUsesCopying(mod),
|
||||
});
|
||||
const oracle = spawnSync(process.execPath, [sourcePath], options);
|
||||
const program = spawnSync(outPath, [], options);
|
||||
const oracle = await spawnResult(process.execPath, [sourcePath], options);
|
||||
const program = await spawnResult(outPath, [], options);
|
||||
for (const result of [oracle, program]) {
|
||||
expect(result.error, source).toBeUndefined();
|
||||
expect(result.signal, `${source}: ${result.stderr}`).toBeNull();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { analyze, compile } from "@scriptc/compiler";
|
||||
import { expect, test } from "vitest";
|
||||
import { ts7Executable } from "../../packages/compiler/src/frontend/ts7/rpc-api.js";
|
||||
|
||||
import { npmFetchCases } from "../../packages/compiler/test/npm-fetch-cases.js";
|
||||
import { analyzeInChild, compileInChild } from "./self-hosting-compiler-process.js";
|
||||
import { spawnResult } from "./spawn-result.js";
|
||||
|
||||
const root = join(import.meta.dirname, "../..");
|
||||
const entry = join(root, "tests/fixtures/self-hosting/frontend-services.ts");
|
||||
@@ -69,17 +69,23 @@ for (const backend of ["llvm"] as const) {
|
||||
);
|
||||
try {
|
||||
const object = join(directory, "process.o");
|
||||
execFileSync("clang", [
|
||||
"-std=c11",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
...(sanitize ? ["-fsanitize=address"] : []),
|
||||
"-c",
|
||||
join(nativeSources, "ts7-process.c"),
|
||||
"-o",
|
||||
object,
|
||||
]);
|
||||
const clang = await spawnResult(
|
||||
"clang",
|
||||
[
|
||||
"-std=c11",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
...(sanitize ? ["-fsanitize=address"] : []),
|
||||
"-c",
|
||||
join(nativeSources, "ts7-process.c"),
|
||||
"-o",
|
||||
object,
|
||||
],
|
||||
{ cwd: root, timeout: 120_000 },
|
||||
);
|
||||
expect(clang.error).toBeUndefined();
|
||||
expect(clang.status, clang.stderr.toString()).toBe(0);
|
||||
const profile = join(directory, "ffi.json");
|
||||
writeFileSync(
|
||||
profile,
|
||||
@@ -88,13 +94,13 @@ for (const backend of ["llvm"] as const) {
|
||||
libraries: [object],
|
||||
}),
|
||||
);
|
||||
const { coverage } = analyze(entry, { dynamic: false, ffiProfilePath: profile });
|
||||
const coverage = await analyzeInChild(entry, { dynamic: false, ffiProfilePath: profile });
|
||||
expect(coverage.preflightFailed, JSON.stringify(coverage.diagnostics)).toBe(false);
|
||||
expect(coverage.diagnostics).toEqual([]);
|
||||
expect(coverage.stats.statementsFailed).toBe(0);
|
||||
expect(coverage.stats.statementsIsland).toBe(0);
|
||||
expect(coverage.stats.functionsSkipped).toBe(0);
|
||||
const built = await compile(entry, {
|
||||
const built = await compileInChild(entry, {
|
||||
backend,
|
||||
dynamic: false,
|
||||
optimization: "dev",
|
||||
@@ -108,25 +114,25 @@ for (const backend of ["llvm"] as const) {
|
||||
const request = join(directory, "request.json");
|
||||
writeFileSync(request, JSON.stringify(input(directory)));
|
||||
const expected = join(directory, "node.json");
|
||||
const node = spawnSync(
|
||||
const node = await spawnResult(
|
||||
process.execPath,
|
||||
["--import", "tsx", oracle, ts7Executable(), request, expected],
|
||||
{ encoding: "utf8", timeout: 45_000 },
|
||||
{ cwd: root, timeout: 45_000 },
|
||||
);
|
||||
expect(node.error, node.stderr).toBeUndefined();
|
||||
expect(node.status, node.stderr).toBe(0);
|
||||
expect(node.stdout).toBe("");
|
||||
expect(node.stderr).toBe("");
|
||||
expect(node.error, node.stderr.toString()).toBeUndefined();
|
||||
expect(node.status, node.stderr.toString()).toBe(0);
|
||||
expect(node.stdout.toString()).toBe("");
|
||||
expect(node.stderr.toString()).toBe("");
|
||||
const report = join(directory, "native.json");
|
||||
const run = spawnSync(built.binaryPath, [ts7Executable(), request, report], {
|
||||
encoding: "utf8",
|
||||
const run = await spawnResult(built.binaryPath, [ts7Executable(), request, report], {
|
||||
cwd: root,
|
||||
timeout: 45_000,
|
||||
});
|
||||
expect(run.error, run.stderr).toBeUndefined();
|
||||
expect(run.signal, run.stderr).toBeNull();
|
||||
expect(run.status, run.stderr).toBe(0);
|
||||
expect(run.stdout).toBe(node.stdout);
|
||||
expect(run.stderr).toBe(node.stderr);
|
||||
expect(run.error, run.stderr.toString()).toBeUndefined();
|
||||
expect(run.signal, run.stderr.toString()).toBeNull();
|
||||
expect(run.status, run.stderr.toString()).toBe(0);
|
||||
expect(run.stdout.toString()).toBe(node.stdout.toString());
|
||||
expect(run.stderr.toString()).toBe(node.stderr.toString());
|
||||
const result = JSON.parse(readFileSync(report, "utf8"));
|
||||
expect(result).toEqual(JSON.parse(readFileSync(expected, "utf8")));
|
||||
expect(result.fetch).toEqual(
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { analyzeInChild, compileInChild } from "./self-hosting-compiler-process.js";
|
||||
import { spawnResult } from "./spawn-result.js";
|
||||
import { nativeCodegenTarget } from "../../packages/compiler/src/backend/targets.js";
|
||||
import type { NativeLinkFeatures } from "../../packages/compiler/src/backend/native-link-info.js";
|
||||
import type { RuntimePackManifest } from "../../packages/compiler/src/backend/runtime-pack-core.js";
|
||||
@@ -143,7 +143,7 @@ for (const backend of ["llvm"] as const) {
|
||||
default_target: target.helper.defaultTarget,
|
||||
data_layout: target.helper.defaultDataLayout,
|
||||
};
|
||||
const check = (
|
||||
const check = async (
|
||||
status: number,
|
||||
message: string,
|
||||
action = "stage",
|
||||
@@ -153,11 +153,11 @@ for (const backend of ["llvm"] as const) {
|
||||
input,
|
||||
JSON.stringify({ toolchain, stage, features, action, version: helperVersion }),
|
||||
);
|
||||
const options = { cwd: root, timeout: 30_000, maxBuffer: 1024 * 1024 };
|
||||
const options = { cwd: root, timeout: 30_000 };
|
||||
const results = [];
|
||||
for (const native of [false, true]) {
|
||||
rmSync(stage, { recursive: true, force: true });
|
||||
const result = spawnSync(
|
||||
const result = await spawnResult(
|
||||
native ? built.binaryPath : process.execPath,
|
||||
native ? [input] : ["--import", "tsx", entry, input],
|
||||
native ? { ...options, env: { ...process.env, PATH: "" } } : options,
|
||||
@@ -171,24 +171,24 @@ for (const backend of ["llvm"] as const) {
|
||||
expect(results[1]!.stdout).toEqual(results[0]!.stdout);
|
||||
expect(results[1]!.stderr).toEqual(results[0]!.stderr);
|
||||
};
|
||||
check(0, "base.o");
|
||||
await check(0, "base.o");
|
||||
expect(readFileSync(join(stage, artifact.path))).toEqual(bytes);
|
||||
// A later replacement of the installed input cannot change staged bytes.
|
||||
writeFileSync(join(pack, artifact.path), Buffer.from("changed"));
|
||||
expect(readFileSync(join(stage, artifact.path))).toEqual(bytes);
|
||||
check(1, "hash mismatch");
|
||||
await check(1, "hash mismatch");
|
||||
expect(existsSync(join(stage, artifact.path))).toBe(false);
|
||||
writeFileSync(join(pack, artifact.path), bytes);
|
||||
rmSync(join(pack, "license.txt"));
|
||||
check(1, "license payload is incomplete");
|
||||
await check(1, "license payload is incomplete");
|
||||
writeFileSync(join(pack, "license.txt"), "license");
|
||||
rmSync(join(pack, artifact.path));
|
||||
check(1, "artifact is missing");
|
||||
await check(1, "artifact is missing");
|
||||
writeFileSync(join(pack, artifact.path), bytes);
|
||||
writeFileSync(toolchain, JSON.stringify({ ...config, compiler_version: "other" }));
|
||||
check(1, "version mismatch");
|
||||
await check(1, "version mismatch");
|
||||
writeFileSync(toolchain, JSON.stringify(config));
|
||||
check(0, "1 1.2.3 22.1.8", "helper");
|
||||
await check(0, "1 1.2.3 22.1.8", "helper");
|
||||
for (const [field, value] of Object.entries({
|
||||
protocol_version: "2",
|
||||
llvm_version: "0",
|
||||
@@ -199,7 +199,7 @@ for (const backend of ["llvm"] as const) {
|
||||
data_layout: "other",
|
||||
host_triple: null,
|
||||
})) {
|
||||
check(1, field, "helper", { ...version, [field]: value });
|
||||
await check(1, field, "helper", { ...version, [field]: value });
|
||||
}
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
export interface SpawnResult {
|
||||
error: Error | undefined;
|
||||
signal: NodeJS.Signals | null;
|
||||
status: number | null;
|
||||
stdout: Buffer;
|
||||
stderr: Buffer;
|
||||
}
|
||||
|
||||
/** spawnSync's result without blocking the worker: a test that runs child
|
||||
* processes back to back with spawnSync starves Vitest's worker RPC, which
|
||||
* fails the run with an unhandled timeout even when every test passes. */
|
||||
export function spawnResult(
|
||||
file: string,
|
||||
args: readonly string[],
|
||||
options: { cwd: string; timeout: number; env?: NodeJS.ProcessEnv },
|
||||
): Promise<SpawnResult> {
|
||||
return new Promise((resolve) => {
|
||||
const stdout: Buffer[] = [];
|
||||
const stderr: Buffer[] = [];
|
||||
let error: Error | undefined;
|
||||
const child = spawn(file, args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
timeout: options.timeout,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
child.stdout.on("data", (chunk: Buffer) => stdout.push(chunk));
|
||||
child.stderr.on("data", (chunk: Buffer) => stderr.push(chunk));
|
||||
child.on("error", (cause) => {
|
||||
error = cause;
|
||||
});
|
||||
child.on("close", (status, signal) =>
|
||||
resolve({
|
||||
error,
|
||||
signal,
|
||||
status,
|
||||
stdout: Buffer.concat(stdout),
|
||||
stderr: Buffer.concat(stderr),
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user