rea/tests/coverageMonotonicity.test.ts

261 lines
7.6 KiB
TypeScript

import { createHash } from "node:crypto";
import { fc, it } from "@fast-check/vitest";
import { describe, expect } from "vitest";
import type { BinaryTarget } from "../src/domain/binaryTarget.js";
import {
assessManagedGraphOmissions,
managedGraphEvidenceCoverage,
totalManagedGraphOmitted,
type ManagedGraphProjectionLimits,
} from "../src/domain/managedApplicationGraphCoverage.js";
import {
assessKnownPageCoverage,
type KnownCollectionPage,
} from "../src/domain/knownPageCoverage.js";
import { inspectManagedMembersBytes } from "../src/dotnet/ManagedMemberInspector.js";
import { inspectManagedNativeBoundariesBytes } from "../src/dotnet/ManagedNativeBoundaryInspector.js";
import { buildManagedPeFixture } from "./fixtures/managedPe.js";
const MEMBER_LIMITS = {
typeOffset: 0,
typeLimit: 100,
methodOffset: 0,
methodLimit: 100,
fieldOffset: 0,
fieldLimit: 100,
memberRefOffset: 0,
memberRefLimit: 100,
edgeOffset: 0,
edgeLimit: 100,
instructionAnchorLimit: 100,
maxMetadataBytes: 1024 * 1024,
maxTableRows: 1_000,
maxHeapItemBytes: 1024 * 1024,
maxMethodBodyBytes: 1024 * 1024,
maxMethodInstructions: 1_000,
};
const BOUNDARY_LIMITS = {
moduleRefOffset: 0,
moduleRefLimit: 100,
importOffset: 0,
importLimit: 100,
implementationOffset: 0,
implementationLimit: 100,
maxMetadataBytes: 1024 * 1024,
maxTableRows: 1_000,
maxHeapItemBytes: 1024 * 1024,
};
const limitPair = fc
.tuple(fc.integer({ min: 1, max: 12 }), fc.integer({ min: 1, max: 12 }))
.map(
([first, second]) =>
[Math.min(first, second), Math.max(first, second)] as const,
);
describe("coverage monotonicity", () => {
it.prop([fc.integer({ min: 0, max: 200 }), limitPair])(
"never makes a smaller source page more complete",
(total, [smallerLimit, largerLimit]) => {
const smaller = assessKnownPageCoverage(sourcePage(total, smallerLimit));
const larger = assessKnownPageCoverage(sourcePage(total, largerLimit));
expect(smaller.includedCount).toBeLessThanOrEqual(larger.includedCount);
expect(smaller.omittedCount).toBeGreaterThanOrEqual(larger.omittedCount);
expect(smaller.sourceOmittedCount).toBeGreaterThanOrEqual(
larger.sourceOmittedCount,
);
if (smaller.complete) expect(larger.complete).toBe(true);
},
);
it.prop([fc.integer({ min: 0, max: 200 }), limitPair])(
"never makes a smaller downstream projection more complete",
(total, [smallerLimit, largerLimit]) => {
const page = sourcePage(total, Math.max(1, total));
const smaller = assessKnownPageCoverage(page, smallerLimit);
const larger = assessKnownPageCoverage(page, largerLimit);
expect(smaller.includedCount).toBeLessThanOrEqual(larger.includedCount);
expect(smaller.omittedCount).toBeGreaterThanOrEqual(larger.omittedCount);
expect(smaller.sourceComplete).toBe(larger.sourceComplete);
if (smaller.complete) expect(larger.complete).toBe(true);
},
);
it.prop([
fc.array(fc.integer({ min: 0, max: 12 }), {
minLength: 5,
maxLength: 5,
}),
fc.array(limitPair, { minLength: 5, maxLength: 5 }),
])(
"does not improve aggregate managed graph coverage when projection limits shrink",
(totals, pairs) => {
const sources = managedSources(totals);
const smaller = assessManagedGraphOmissions(
sources,
graphLimits(pairs, 0),
);
const larger = assessManagedGraphOmissions(
sources,
graphLimits(pairs, 1),
);
const smallerCoverage = managedGraphEvidenceCoverage(smaller);
const largerCoverage = managedGraphEvidenceCoverage(larger);
expect(totalManagedGraphOmitted(smaller)).toBeGreaterThanOrEqual(
totalManagedGraphOmitted(larger),
);
expect(smaller.types).toBeGreaterThanOrEqual(larger.types);
expect(smaller.methods).toBeGreaterThanOrEqual(larger.methods);
expect(smaller.fields).toBeGreaterThanOrEqual(larger.fields);
expect(smaller.pinvokeImports).toBeGreaterThanOrEqual(
larger.pinvokeImports,
);
expect(smaller.nativeImplementations).toBeGreaterThanOrEqual(
larger.nativeImplementations,
);
if (smallerCoverage.status === "complete")
expect(largerCoverage.status).toBe("complete");
},
);
it("does not let wider projection limits repair parser-unknown input", () => {
const sources = managedSources([2, 2, 2, 2, 2]);
const partialSources = {
...sources,
members: {
...sources.members,
coverage: {
state: "partial" as const,
issues: [
{
code: "limit-exceeded" as const,
scope: "method.0x06000001.body.instructions",
offset: 0x0a00,
detail: "instruction limit reached",
},
],
},
},
};
const narrow = managedGraphEvidenceCoverage(
assessManagedGraphOmissions(partialSources, constantGraphLimits(1)),
);
const wide = managedGraphEvidenceCoverage(
assessManagedGraphOmissions(partialSources, constantGraphLimits(100)),
);
expect(narrow).toMatchObject({
status: "partial",
truncated: false,
omitted_count: null,
});
expect(wide).toEqual(narrow);
});
});
const sourcePage = (
total: number,
limit: number,
): KnownCollectionPage<number> => {
const returned = Math.min(total, limit);
return {
items: Array.from({ length: returned }, (_, index) => index),
offset: 0,
limit,
total,
returned,
complete: returned === total,
};
};
const fixtureBytes = buildManagedPeFixture({
pinvoke: {
moduleName: "user32.dll",
importName: "MessageBoxW",
mappingFlags: 0x0345,
},
readyToRun: true,
});
const fixtureTarget: BinaryTarget = {
path: "/fixture/ManagedCoverage.exe",
sha256: createHash("sha256").update(fixtureBytes).digest("hex"),
kind: "executable",
format: "pe",
architecture: "x86",
availableArchitectures: ["x86"],
};
const fixtureMembers = inspectManagedMembersBytes(
fixtureBytes,
fixtureTarget,
MEMBER_LIMITS,
);
const fixtureBoundaries = inspectManagedNativeBoundariesBytes(
fixtureBytes,
fixtureTarget,
BOUNDARY_LIMITS,
);
const managedSources = (totals: readonly number[]) => ({
artifact: null,
members: {
...fixtureMembers,
types: repeatPage(fixtureMembers.types, totals[0] ?? 0),
methods: repeatPage(fixtureMembers.methods, totals[1] ?? 0),
fields: repeatPage(fixtureMembers.fields, totals[2] ?? 0),
},
boundaries: {
...fixtureBoundaries,
pinvoke_imports: repeatPage(
fixtureBoundaries.pinvoke_imports,
totals[3] ?? 0,
),
native_implementations: repeatPage(
fixtureBoundaries.native_implementations,
totals[4] ?? 0,
),
},
});
const repeatPage = <Item, Page extends KnownCollectionPage<Item>>(
page: Page,
total: number,
) => {
const first = page.items[0];
const items =
first === undefined ? [] : Array.from({ length: total }, () => first);
return {
...page,
items,
offset: 0,
limit: Math.max(1, total),
total,
returned: total,
complete: true,
};
};
const graphLimits = (
pairs: readonly (readonly [number, number])[],
index: 0 | 1,
): ManagedGraphProjectionLimits => ({
max_types: pairs[0]?.[index] ?? 1,
max_methods: pairs[1]?.[index] ?? 1,
max_fields: pairs[2]?.[index] ?? 1,
max_pinvoke_imports: pairs[3]?.[index] ?? 1,
max_native_implementations: pairs[4]?.[index] ?? 1,
});
const constantGraphLimits = (value: number): ManagedGraphProjectionLimits => ({
max_types: value,
max_methods: value,
max_fields: value,
max_pinvoke_imports: value,
max_native_implementations: value,
});