Files
2026-05-27 23:29:53 +01:00

68 lines
2.3 KiB
TypeScript

import { describe, expect, it } from "@suaveplan/testing/runner";
import fc from "fast-check";
import { topologicalSort } from "../src/topo-sort/topo-sort.js";
import type { PluginNode } from "../src/types/api-plugin.js";
/** Generate an acyclic dep graph: each node's deps only point to earlier ids. */
const acyclicGraphArb = fc
.array(fc.string({ minLength: 1, maxLength: 4 }), {
minLength: 1,
maxLength: 20,
})
.map((rawIds) => {
const ids = Array.from(new Set(rawIds));
return ids;
})
.filter((ids) => ids.length > 0)
.chain((ids) =>
fc
.tuple(
...ids.map((_, i) =>
fc.subarray(ids.slice(0, i), { maxLength: i })
)
)
.map((depsPerNode) =>
ids.map<PluginNode<null>>((id, i) => ({
id,
data: null,
dependencies: depsPerNode[i] ?? [],
}))
)
);
describe("topologicalSort — property tests", () => {
it("sorted order satisfies dep invariant", { timeout: 30000 }, () => {
fc.assert(
fc.property(acyclicGraphArb, (nodes) => {
const { sorted } = topologicalSort(nodes);
const positions = new Map<string, number>();
sorted.forEach((n, i) => positions.set(n.id, i));
for (const node of sorted) {
for (const dep of node.dependencies) {
const depPos = positions.get(dep);
const nodePos = positions.get(node.id);
// dep must appear before node (if dep is in the graph)
if (depPos !== undefined && nodePos !== undefined) {
expect(depPos).toBeLessThan(nodePos);
}
}
}
}),
{ numRuns: 200 }
);
});
it("sort is deterministic across repeated calls", {
timeout: 30000,
}, () => {
fc.assert(
fc.property(acyclicGraphArb, (nodes) => {
const a = topologicalSort(nodes).sorted.map((n) => n.id);
const b = topologicalSort(nodes).sorted.map((n) => n.id);
expect(a).toEqual(b);
}),
{ numRuns: 200 }
);
});
});