mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
c84d0efb1a
The Postgres probe only ratcheted the methods it managed to call. A method it could not build arguments for, one that threw before its first statement and one that timed out were counted and logged but held to nothing, so a new repository method with a callback parameter could land SQLite-only SQL unseen, and the part of the code the probe reaches could shrink without anyone noticing. pg-probe-baseline.json gains an `uncovered` list of those methods. A method outside it that goes unmeasured fails the run, and so does an entry whose method is measured now or gone, so the list only shrinks. It is seeded from the same first measurement as `failing`; a baseline without the key reads as unmeasured. The failing count also counted every refusal, including data errors the sample values cause (a "probe" string in a date column, SQLSTATE 22007), so renaming a parameter could fail the gate on a change that never touched the SQL. Only classes 42 and 0A count now; the rest is reported in the log and the step summary without being held. The docs said the probe measures every repository statement. They now say it measures what each method reaches with sample arguments against empty tables, and that the methods it cannot reach are held to the list.
116 lines
6.5 KiB
TypeScript
116 lines
6.5 KiB
TypeScript
/**
|
|
* The Postgres probe's ratchet (scripts/pg-probe/baseline.ts). The probe
|
|
* itself only runs in CI against a Postgres service, so the rules that decide
|
|
* whether that job passes are pinned here: a method may not fail more
|
|
* statements than its entry, a stale entry fails until it is lowered, a
|
|
* method the probe did not measure must be listed as uncovered and the list
|
|
* only shrinks, only SQL errors count, an unmeasured baseline has nothing to
|
|
* compare (the run fails on it, see report.test.ts), and --update never
|
|
* raises or adds.
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { compareWithBaseline, countsTowardRatchet, formatBaseline, lowerBaseline, parseBaseline } from '../../../../scripts/pg-probe/baseline';
|
|
|
|
const now = (entries: Record<string, number>): Map<string, number> => new Map(Object.entries(entries));
|
|
const none = new Set<string>();
|
|
|
|
describe('pg-probe baseline', () => {
|
|
it('PGPROBE-001: parses a measured and an unmeasured baseline; a missing uncovered list reads as unmeasured', () => {
|
|
expect(parseBaseline('{"failing": null, "uncovered": null}', 'b.json')).toEqual({ failing: null, uncovered: null });
|
|
expect(parseBaseline('{"failing": null}', 'b.json')).toEqual({ failing: null, uncovered: null });
|
|
expect(parseBaseline('{"failing": {"TagsRepository.listByUser": 2}, "uncovered": ["A.b"]}', 'b.json')).toEqual({
|
|
failing: { 'TagsRepository.listByUser': 2 },
|
|
uncovered: ['A.b'],
|
|
});
|
|
expect(parseBaseline('{"failing": {}}', 'b.json')).toEqual({ failing: {}, uncovered: null });
|
|
});
|
|
|
|
it('PGPROBE-002: refuses a malformed baseline instead of reading it as empty', () => {
|
|
expect(() => parseBaseline('nope', 'b.json')).toThrow(/b.json is not valid JSON/);
|
|
expect(() => parseBaseline('{}', 'b.json')).toThrow(/"failing" key/);
|
|
expect(() => parseBaseline('[]', 'b.json')).toThrow(/"failing" key/);
|
|
expect(() => parseBaseline('{"failing": []}', 'b.json')).toThrow(/null or an object/);
|
|
expect(() => parseBaseline('{"failing": {"listByUser": 1}}', 'b.json')).toThrow(/not a Class.method key/);
|
|
expect(() => parseBaseline('{"failing": {"A.b": 0}}', 'b.json')).toThrow(/expected a positive integer/);
|
|
expect(() => parseBaseline('{"failing": {"A.b": 1.5}}', 'b.json')).toThrow(/expected a positive integer/);
|
|
expect(() => parseBaseline('{"failing": {}, "uncovered": {}}', 'b.json')).toThrow(/"uncovered" must be null or an array/);
|
|
expect(() => parseBaseline('{"failing": {}, "uncovered": ["nodot"]}', 'b.json')).toThrow(/expected a Class.method name/);
|
|
expect(() => parseBaseline('{"failing": {}, "uncovered": [1]}', 'b.json')).toThrow(/expected a Class.method name/);
|
|
expect(() => parseBaseline('{"failing": {}, "uncovered": ["A.b", "A.b"]}', 'b.json')).toThrow(/lists A.b twice/);
|
|
});
|
|
|
|
it('PGPROBE-003: an unmeasured baseline (either part null) is flagged instead of compared', () => {
|
|
const flagged = { unseeded: true, grown: [], stale: [], newlyUncovered: [], nowCovered: [] };
|
|
expect(compareWithBaseline({ failing: null, uncovered: null }, now({ 'A.b': 3 }), new Set(['C.d']))).toEqual(flagged);
|
|
expect(compareWithBaseline({ failing: { 'A.b': 1 }, uncovered: null }, now({ 'A.b': 3 }), none)).toEqual(flagged);
|
|
expect(compareWithBaseline({ failing: null, uncovered: [] }, now({ 'A.b': 3 }), none)).toEqual(flagged);
|
|
});
|
|
|
|
it('PGPROBE-004: a method failing more statements than its entry (or any without one) fails', () => {
|
|
const verdict = compareWithBaseline({ failing: { 'A.b': 1 }, uncovered: [] }, now({ 'A.b': 2, 'C.d': 1, 'E.f': 0 }), none);
|
|
expect(verdict.grown).toEqual([
|
|
{ method: 'A.b', allowed: 1, now: 2 },
|
|
{ method: 'C.d', allowed: 0, now: 1 },
|
|
]);
|
|
expect(verdict.stale).toEqual([]);
|
|
});
|
|
|
|
it('PGPROBE-005: an entry above what its method fails now is stale, also when the method is gone', () => {
|
|
const verdict = compareWithBaseline({ failing: { 'A.b': 2, 'Gone.away': 1 }, uncovered: [] }, now({ 'A.b': 1 }), none);
|
|
expect(verdict.grown).toEqual([]);
|
|
expect(verdict.stale).toEqual([
|
|
{ method: 'A.b', allowed: 2, now: 1 },
|
|
{ method: 'Gone.away', allowed: 1, now: 0 },
|
|
]);
|
|
});
|
|
|
|
it('PGPROBE-009: a method the probe did not measure must be listed, and a listed method measured now or gone is stale', () => {
|
|
const verdict = compareWithBaseline(
|
|
{ failing: {}, uncovered: ['Kept.out', 'Now.measured', 'Gone.away'] },
|
|
now({ 'Now.measured': 0 }),
|
|
new Set(['Kept.out', 'New.callback', 'Old.throws']),
|
|
);
|
|
expect(verdict).toEqual({
|
|
unseeded: false,
|
|
grown: [],
|
|
stale: [],
|
|
newlyUncovered: ['New.callback', 'Old.throws'],
|
|
nowCovered: ['Gone.away', 'Now.measured'],
|
|
});
|
|
expect(compareWithBaseline({ failing: {}, uncovered: ['A.b'] }, now({}), new Set(['A.b']))).toMatchObject({ newlyUncovered: [], nowCovered: [] });
|
|
});
|
|
|
|
it('PGPROBE-006: --update lowers and drops entries but never raises or adds one, in either part', () => {
|
|
const lowered = lowerBaseline(
|
|
{ failing: { 'A.b': 2, 'C.d': 1, 'Gone.away': 1 }, uncovered: ['Kept.out', 'Now.measured'] },
|
|
now({ 'A.b': 1, 'C.d': 5, 'New.one': 3 }),
|
|
new Set(['Kept.out', 'New.callback']),
|
|
);
|
|
expect(lowered).toEqual({ failing: { 'A.b': 1, 'C.d': 1 }, uncovered: ['Kept.out'] });
|
|
});
|
|
|
|
it('PGPROBE-007: the first --update seeds an unmeasured baseline with what fails and what went unmeasured, sorted', () => {
|
|
expect(lowerBaseline({ failing: null, uncovered: null }, now({ 'Z.z': 1, 'A.a': 2, 'M.m': 0 }), new Set(['Y.y', 'B.b']))).toEqual({
|
|
failing: { 'A.a': 2, 'Z.z': 1 },
|
|
uncovered: ['B.b', 'Y.y'],
|
|
});
|
|
expect(lowerBaseline({ failing: { 'A.a': 3 }, uncovered: null }, now({ 'A.a': 2, 'N.n': 1 }), new Set(['B.b']))).toEqual({
|
|
failing: { 'A.a': 2 },
|
|
uncovered: ['B.b'],
|
|
});
|
|
});
|
|
|
|
it('PGPROBE-010: only syntax and feature errors count toward the ratchet, not data errors from the sample values', () => {
|
|
for (const code of ['42601', '42883', '42804', '42P01', '42703', '0A000']) expect(countsTowardRatchet(code)).toBe(true);
|
|
for (const code of ['22P02', '22007', '22008', '23505', '57014', 'NOSQLSTATE']) expect(countsTowardRatchet(code)).toBe(false);
|
|
});
|
|
|
|
it('PGPROBE-008: the checked-in baseline parses and formats back to itself', () => {
|
|
const file = path.join(__dirname, '../../../../scripts/pg-probe-baseline.json');
|
|
const text = readFileSync(file, 'utf8');
|
|
expect(formatBaseline(parseBaseline(text, file))).toBe(text.replace(/\r\n/g, '\n'));
|
|
});
|
|
});
|