Merge pull request #659 from kaoru0822-kitauji/fix/function-collation-ties
fix(comparison): break function collection collation ties
This commit is contained in:
commit
c3d013b2fd
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from "./functionComparison.js";
|
||||
import { createEvidence, type Evidence } from "./evidence.js";
|
||||
import { functionDossierSchema } from "./hopperValues.js";
|
||||
import { canonicalDigest, canonicalJson } from "./comparisonSemantics.js";
|
||||
import { jsonValueSchema } from "./jsonValue.js";
|
||||
|
||||
const dossier = (
|
||||
|
|
@ -172,6 +173,79 @@ describe("function comparison normalized identity", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("function collection collation ties", () => {
|
||||
const composed = { address: "0x2000", name: "caf\u00e9" };
|
||||
const decomposed = { address: "0x3000", name: "cafe\u0301" };
|
||||
const calling = (
|
||||
callees: readonly { readonly address: string; readonly name: string }[],
|
||||
) =>
|
||||
functionDossierSchema.parse({
|
||||
...dossier("return 0;", "0x1000"),
|
||||
callees,
|
||||
});
|
||||
|
||||
it("ignores reordered callees whose distinct names collate equally", () => {
|
||||
const result = compareFunctions(
|
||||
observe("b", calling([composed, decomposed])),
|
||||
observe("c", calling([decomposed, composed])),
|
||||
);
|
||||
expect(result.status).toBe("unchanged");
|
||||
const calls = result.dimensions.find(
|
||||
({ dimension }) => dimension === "calls",
|
||||
);
|
||||
expect(calls).toMatchObject({
|
||||
status: "unchanged",
|
||||
left_count: 2,
|
||||
right_count: 2,
|
||||
});
|
||||
expect(calls?.left_digest).toBe(calls?.right_digest);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["remove composed", [decomposed]],
|
||||
["remove decomposed", [composed]],
|
||||
["replace composed", [decomposed, decomposed]],
|
||||
["replace decomposed", [composed, composed]],
|
||||
] as const)("preserves an exact name change: %s", (_label, changed) => {
|
||||
const result = compareFunctions(
|
||||
observe("b", calling([composed, decomposed])),
|
||||
observe("c", calling(changed)),
|
||||
);
|
||||
expect(result.status).toBe("changed");
|
||||
const calls = result.dimensions.find(
|
||||
({ dimension }) => dimension === "calls",
|
||||
);
|
||||
expect(calls).toMatchObject({ status: "changed" });
|
||||
expect(calls?.left_digest).not.toBe(calls?.right_digest);
|
||||
});
|
||||
|
||||
it("preserves existing digests when canonical records do not collate equally", () => {
|
||||
const callees = ["zeta", "Alpha", "alpha", "_helper", "beta"].map(
|
||||
(name, index) => ({
|
||||
address: `0x${(0x2000 + index).toString(16)}`,
|
||||
name,
|
||||
}),
|
||||
);
|
||||
const legacyProjection = callees
|
||||
.map(({ name }) => ({ direction: "out", name }))
|
||||
.sort((left, right) =>
|
||||
canonicalJson(left).localeCompare(canonicalJson(right)),
|
||||
);
|
||||
const result = compareFunctions(
|
||||
observe("b", calling(callees)),
|
||||
observe("c", calling([...callees].reverse())),
|
||||
);
|
||||
const calls = result.dimensions.find(
|
||||
({ dimension }) => dimension === "calls",
|
||||
);
|
||||
expect(calls).toMatchObject({
|
||||
status: "unchanged",
|
||||
left_digest: canonicalDigest(legacyProjection),
|
||||
right_digest: canonicalDigest(legacyProjection),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("function comparison CFG address normalization", () => {
|
||||
it("matches CFG successors by numeric address", () => {
|
||||
const make = (base: "0x1000" | "0x2000") =>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import canonicalize from "canonicalize";
|
||||
|
||||
import { compareCodePoints } from "./canonicalOrdering.js";
|
||||
|
||||
import type {
|
||||
FunctionCollection,
|
||||
FunctionSnapshot,
|
||||
|
|
@ -176,9 +178,15 @@ export const stringAndNameProjection = (
|
|||
};
|
||||
|
||||
export const sorted = (values: readonly unknown[]): readonly unknown[] =>
|
||||
[...values].sort((left, right) =>
|
||||
canonicalJson(left).localeCompare(canonicalJson(right)),
|
||||
);
|
||||
[...values].sort((left, right) => {
|
||||
const leftJson = canonicalJson(left);
|
||||
const rightJson = canonicalJson(right);
|
||||
// Preserve existing collation while ordering distinct values within a tie.
|
||||
return (
|
||||
leftJson.localeCompare(rightJson) ||
|
||||
compareCodePoints(leftJson, rightJson)
|
||||
);
|
||||
});
|
||||
|
||||
export const combineCoverage = <Item>(
|
||||
left: FunctionCollection<Item>,
|
||||
|
|
|
|||
Loading…
Reference in New Issue