Files
regex-tools/src/regex/execution/EngineSupervisor.test.ts

169 lines
4.9 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;
postMessage(message: unknown): void {
const request = message as WorkerRequest<EngineWorkerOperation>;
this.operations.push(request.payload);
const payload: EngineWorkerResult =
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;
}
}
describe("engine worker selection", () => {
it("registers the shipped ECMAScript and PCRE2 execution workers", () => {
expect(
ENGINE_WORKER_REGISTRY.registrations.map(
(registration) => registration.flavour,
),
).toEqual(["ecmascript", "pcre2"]);
expect(ENGINE_WORKER_REGISTRY.require("pcre2").label).toBe("PCRE2 engine");
});
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`,
createWorker: () => {
const worker = new ResponsiveWorker();
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).toHaveLength(2);
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();
});
});