275 lines
8.1 KiB
TypeScript
275 lines
8.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type {
|
|
RegexExecutionRequest,
|
|
RegexExecutionResult,
|
|
} from "../model/match";
|
|
import type { RegexFlavourId } from "../model/flavour";
|
|
import { EngineSupervisor } from "./EngineSupervisor";
|
|
import {
|
|
ENGINE_WORKER_REGISTRY,
|
|
EngineWorkerRegistry,
|
|
} from "./engine-registry";
|
|
import type { WorkerLike } from "./WorkerSupervisor";
|
|
import {
|
|
WORKER_PROTOCOL_VERSION,
|
|
type EngineWorkerOperation,
|
|
type EngineWorkerResult,
|
|
type WorkerRequest,
|
|
type WorkerResponse,
|
|
} from "./worker-protocol";
|
|
|
|
function request(flavour: RegexFlavourId): RegexExecutionRequest {
|
|
return {
|
|
flavour,
|
|
flavourVersion: "fixture",
|
|
pattern: "a",
|
|
flags: [],
|
|
options: {},
|
|
subject: "a",
|
|
captureMetadata: [],
|
|
scanAll: false,
|
|
maximumMatches: 10,
|
|
maximumCaptureRows: 10,
|
|
};
|
|
}
|
|
|
|
function result(flavour: RegexFlavourId): RegexExecutionResult {
|
|
return {
|
|
accepted: true,
|
|
engine: {
|
|
flavour,
|
|
adapterVersion: "fixture",
|
|
engineName: `${flavour} fixture`,
|
|
engineVersion: "fixture",
|
|
offsetUnit: "utf16",
|
|
capabilities: {
|
|
compilation: true,
|
|
matching: true,
|
|
replacement: false,
|
|
namedCaptures: false,
|
|
captureHistory: false,
|
|
actualTrace: false,
|
|
benchmark: false,
|
|
},
|
|
},
|
|
flags: {
|
|
userFlags: "",
|
|
effectiveFlags: "",
|
|
internallyAddedIndicesFlag: false,
|
|
internallyAddedGlobalFlag: false,
|
|
},
|
|
matches: [],
|
|
diagnostics: [],
|
|
elapsedMs: 0,
|
|
truncated: false,
|
|
};
|
|
}
|
|
|
|
class ResponsiveWorker implements WorkerLike {
|
|
onmessage: ((event: MessageEvent<unknown>) => void) | null = null;
|
|
onerror: ((event: ErrorEvent) => void) | null = null;
|
|
onmessageerror: ((event: MessageEvent<unknown>) => void) | null = null;
|
|
readonly operations: EngineWorkerOperation[] = [];
|
|
terminateCalls = 0;
|
|
private readonly flavour: RegexFlavourId;
|
|
|
|
constructor(flavour: RegexFlavourId) {
|
|
this.flavour = flavour;
|
|
}
|
|
|
|
postMessage(message: unknown): void {
|
|
const request = message as WorkerRequest<EngineWorkerOperation>;
|
|
this.operations.push(request.payload);
|
|
const payload: EngineWorkerResult =
|
|
request.payload.kind === "load"
|
|
? {
|
|
kind: "load",
|
|
info: result(this.flavour).engine,
|
|
}
|
|
: request.payload.kind === "execute"
|
|
? {
|
|
kind: "execute",
|
|
result: result(request.payload.request.flavour),
|
|
}
|
|
: {
|
|
kind: "replace",
|
|
result: {
|
|
execution: result(request.payload.request.flavour),
|
|
output: "",
|
|
outputBytes: 0,
|
|
outputTruncated: false,
|
|
truncated: false,
|
|
},
|
|
};
|
|
const response: WorkerResponse<EngineWorkerResult> = {
|
|
protocolVersion: WORKER_PROTOCOL_VERSION,
|
|
requestId: request.requestId,
|
|
generation: request.generation,
|
|
ok: true,
|
|
payload,
|
|
};
|
|
this.onmessage?.({ data: response } as MessageEvent<unknown>);
|
|
}
|
|
|
|
terminate(): void {
|
|
this.terminateCalls += 1;
|
|
}
|
|
}
|
|
|
|
class StallingWorker implements WorkerLike {
|
|
onmessage: ((event: MessageEvent<unknown>) => void) | null = null;
|
|
onerror: ((event: ErrorEvent) => void) | null = null;
|
|
onmessageerror: ((event: MessageEvent<unknown>) => void) | null = null;
|
|
readonly operations: EngineWorkerOperation[] = [];
|
|
terminateCalls = 0;
|
|
|
|
postMessage(message: unknown): void {
|
|
const request = message as WorkerRequest<EngineWorkerOperation>;
|
|
this.operations.push(request.payload);
|
|
}
|
|
|
|
terminate(): void {
|
|
this.terminateCalls += 1;
|
|
}
|
|
}
|
|
|
|
describe("engine worker selection", () => {
|
|
it("registers every shipped execution worker with a startup budget", () => {
|
|
expect(
|
|
ENGINE_WORKER_REGISTRY.registrations.map(
|
|
(registration) => registration.flavour,
|
|
),
|
|
).toEqual(["ecmascript", "pcre2", "python", "java"]);
|
|
expect(ENGINE_WORKER_REGISTRY.require("pcre2").label).toBe("PCRE2 engine");
|
|
expect(ENGINE_WORKER_REGISTRY.require("python").startupTimeoutMs).toBe(
|
|
30_000,
|
|
);
|
|
});
|
|
|
|
it("selects and reuses a dedicated supervisor per registered flavour", async () => {
|
|
const workers = new Map<RegexFlavourId, ResponsiveWorker[]>();
|
|
const registration = (flavour: RegexFlavourId) => ({
|
|
flavour,
|
|
label: `${flavour} fixture`,
|
|
startupTimeoutMs: 100,
|
|
createWorker: () => {
|
|
const worker = new ResponsiveWorker(flavour);
|
|
workers.set(flavour, [...(workers.get(flavour) ?? []), worker]);
|
|
return worker;
|
|
},
|
|
});
|
|
const supervisor = new EngineSupervisor(
|
|
new EngineWorkerRegistry([
|
|
registration("ecmascript"),
|
|
registration("pcre2"),
|
|
]),
|
|
);
|
|
|
|
await expect(
|
|
supervisor.execute(request("ecmascript"), 100),
|
|
).resolves.toEqual(
|
|
expect.objectContaining({
|
|
engine: expect.objectContaining({ flavour: "ecmascript" }),
|
|
}),
|
|
);
|
|
await expect(supervisor.execute(request("pcre2"), 100)).resolves.toEqual(
|
|
expect.objectContaining({
|
|
engine: expect.objectContaining({ flavour: "pcre2" }),
|
|
}),
|
|
);
|
|
await supervisor.execute(request("ecmascript"), 100);
|
|
|
|
expect(workers.get("ecmascript")).toHaveLength(1);
|
|
expect(workers.get("pcre2")).toHaveLength(1);
|
|
expect(
|
|
workers
|
|
.get("ecmascript")?.[0]
|
|
?.operations.map((operation) => operation.kind),
|
|
).toEqual(["load", "execute", "execute"]);
|
|
expect(
|
|
workers.get("pcre2")?.[0]?.operations.map((operation) => operation.kind),
|
|
).toEqual(["load", "execute"]);
|
|
|
|
supervisor.dispose();
|
|
expect(workers.get("ecmascript")?.[0]?.terminateCalls).toBe(1);
|
|
expect(workers.get("pcre2")?.[0]?.terminateCalls).toBe(1);
|
|
});
|
|
|
|
it("fails closed before constructing a worker for an unavailable flavour", async () => {
|
|
const supervisor = new EngineSupervisor(new EngineWorkerRegistry([]));
|
|
|
|
await expect(supervisor.execute(request("pcre2"), 100)).rejects.toThrow(
|
|
/No execution worker is registered/u,
|
|
);
|
|
supervisor.dispose();
|
|
});
|
|
|
|
it("lets the newest request supersede a shared ready-engine continuation", async () => {
|
|
const worker = new ResponsiveWorker("python");
|
|
const supervisor = new EngineSupervisor(
|
|
new EngineWorkerRegistry([
|
|
{
|
|
flavour: "python",
|
|
label: "Python fixture",
|
|
startupTimeoutMs: 100,
|
|
createWorker: () => worker,
|
|
},
|
|
]),
|
|
);
|
|
await supervisor.load("python");
|
|
|
|
const older = supervisor.execute(request("python"), 100);
|
|
const newer = supervisor.execute(request("python"), 100);
|
|
|
|
await expect(older).rejects.toMatchObject({ kind: "cancelled" });
|
|
await expect(newer).resolves.toEqual(
|
|
expect.objectContaining({
|
|
engine: expect.objectContaining({ flavour: "python" }),
|
|
}),
|
|
);
|
|
expect(worker.operations.map((operation) => operation.kind)).toEqual([
|
|
"load",
|
|
"execute",
|
|
]);
|
|
supervisor.dispose();
|
|
});
|
|
|
|
it("restarts a superseded cold runtime under the startup budget before execution", async () => {
|
|
const coldWorker = new StallingWorker();
|
|
const readyWorker = new ResponsiveWorker("python");
|
|
let factoryCalls = 0;
|
|
const supervisor = new EngineSupervisor(
|
|
new EngineWorkerRegistry([
|
|
{
|
|
flavour: "python",
|
|
label: "Python fixture",
|
|
startupTimeoutMs: 100,
|
|
createWorker: () => {
|
|
factoryCalls += 1;
|
|
return factoryCalls === 1 ? coldWorker : readyWorker;
|
|
},
|
|
},
|
|
]),
|
|
);
|
|
|
|
const older = supervisor.execute(request("python"), 1);
|
|
const newer = supervisor.execute(request("python"), 1);
|
|
|
|
await expect(older).rejects.toMatchObject({ kind: "cancelled" });
|
|
await expect(newer).resolves.toEqual(
|
|
expect.objectContaining({
|
|
engine: expect.objectContaining({ flavour: "python" }),
|
|
}),
|
|
);
|
|
expect(coldWorker.operations.map((operation) => operation.kind)).toEqual([
|
|
"load",
|
|
]);
|
|
expect(coldWorker.terminateCalls).toBe(1);
|
|
expect(readyWorker.operations.map((operation) => operation.kind)).toEqual([
|
|
"load",
|
|
"execute",
|
|
]);
|
|
supervisor.dispose();
|
|
});
|
|
});
|