136 lines
3.7 KiB
TypeScript
136 lines
3.7 KiB
TypeScript
import { signal, computed, effect } from "@preact/signals-core";
|
|
import { DirectedGraph } from "graphology";
|
|
|
|
const IDLE: NodeStatus = "idle";
|
|
const WAITING: NodeStatus = "waiting";
|
|
const READY: NodeStatus = "ready";
|
|
const RUNNING: NodeStatus = "running";
|
|
const COMPLETED: NodeStatus = "completed";
|
|
const FAILED: NodeStatus = "failed";
|
|
const ABORTED: NodeStatus = "aborted";
|
|
const SKIPPED: NodeStatus = "skipped";
|
|
|
|
type NodeStatus =
|
|
| "idle"
|
|
| "waiting"
|
|
| "ready"
|
|
| "running"
|
|
| "completed"
|
|
| "failed"
|
|
| "aborted"
|
|
| "skipped";
|
|
|
|
export interface TestReactiveRoot {
|
|
statusMap: Map<string, ReturnType<typeof signal<NodeStatus>>>;
|
|
preconditions: Map<string, ReturnType<typeof computed<boolean>>>;
|
|
blockedByFailure: Map<string, ReturnType<typeof computed<boolean>>>;
|
|
dispose: () => void;
|
|
}
|
|
|
|
export function createTestReactiveRoot(graph: DirectedGraph): TestReactiveRoot {
|
|
const statusMap = new Map<string, ReturnType<typeof signal<NodeStatus>>>();
|
|
const preconditions = new Map<string, ReturnType<typeof computed<boolean>>>();
|
|
const blockedByFailure = new Map<string, ReturnType<typeof computed<boolean>>>();
|
|
const disposers: (() => void)[] = [];
|
|
|
|
for (const node of graph.nodes()) {
|
|
const predecessors = graph.inNeighbors(node);
|
|
|
|
const status = signal<NodeStatus>(IDLE);
|
|
const preconditionsComputed = computed(() => {
|
|
return predecessors.every((pred: string) => {
|
|
const predStatus = statusMap.get(pred);
|
|
return (
|
|
predStatus !== undefined &&
|
|
(predStatus.value === COMPLETED || predStatus.value === SKIPPED)
|
|
);
|
|
});
|
|
});
|
|
const blockedByFailureComputed = computed(() => {
|
|
return predecessors.some((pred: string) => {
|
|
const predStatus = statusMap.get(pred);
|
|
return (
|
|
predStatus !== undefined &&
|
|
(predStatus.value === FAILED || predStatus.value === ABORTED)
|
|
);
|
|
});
|
|
});
|
|
|
|
const blockerDisposer = effect(() => {
|
|
if (blockedByFailureComputed.value) {
|
|
if (status.value === IDLE || status.value === WAITING) {
|
|
status.value = ABORTED;
|
|
}
|
|
}
|
|
});
|
|
|
|
disposers.push(blockerDisposer);
|
|
statusMap.set(node, status);
|
|
preconditions.set(node, preconditionsComputed);
|
|
blockedByFailure.set(node, blockedByFailureComputed);
|
|
}
|
|
|
|
return {
|
|
statusMap,
|
|
preconditions,
|
|
blockedByFailure,
|
|
dispose: () => {
|
|
for (const disposer of disposers) {
|
|
disposer();
|
|
}
|
|
statusMap.clear();
|
|
preconditions.clear();
|
|
blockedByFailure.clear();
|
|
},
|
|
};
|
|
}
|
|
|
|
export function assertStatus(
|
|
root: TestReactiveRoot,
|
|
nodeId: string,
|
|
expected: NodeStatus,
|
|
): void {
|
|
const status = root.statusMap.get(nodeId);
|
|
if (!status) {
|
|
throw new Error(`Node ${nodeId} not found in statusMap`);
|
|
}
|
|
if (status.value !== expected) {
|
|
throw new Error(
|
|
`Node ${nodeId} status: expected ${expected}, got ${status.value}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export function assertPreconditions(
|
|
root: TestReactiveRoot,
|
|
nodeId: string,
|
|
expected: boolean,
|
|
): void {
|
|
const preconditions = root.preconditions.get(nodeId);
|
|
if (!preconditions) {
|
|
throw new Error(`Node ${nodeId} not found in preconditions`);
|
|
}
|
|
if (preconditions.value !== expected) {
|
|
throw new Error(
|
|
`Node ${nodeId} preconditions: expected ${expected}, got ${preconditions.value}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export function assertBlockedByFailure(
|
|
root: TestReactiveRoot,
|
|
nodeId: string,
|
|
expected: boolean,
|
|
): void {
|
|
const blocked = root.blockedByFailure.get(nodeId);
|
|
if (!blocked) {
|
|
throw new Error(`Node ${nodeId} not found in blockedByFailure`);
|
|
}
|
|
if (blocked.value !== expected) {
|
|
throw new Error(
|
|
`Node ${nodeId} blockedByFailure: expected ${expected}, got ${blocked.value}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export type { NodeStatus }; |