mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
tests/quarantine/quarantine.json is the only way to take a flaky unit or integration test out of a run. An entry names the file, the full test name, the issue, the reason, the day it was added and the day it expires, at most 14 days later. Both setup files skip a listed test while its entry is live, reported as skipped with its issue rather than retried into green. Past its expiry date the test runs again, and quarantine-guard.test.ts fails until the entry is removed; it also fails on an entry naming a file or test that does not exist. The list starts empty.
79 lines
2.6 KiB
TypeScript
79 lines
2.6 KiB
TypeScript
/**
|
|
* The flaky-test quarantine: a list of tests skipped while their flake is
|
|
* fixed, each with an issue and an expiry date.
|
|
*
|
|
* A quarantined test is reported as skipped rather than retried into green,
|
|
* so the run stays honest about what it did not check, and the entry cannot
|
|
* outlive its excuse: once `expires` has passed the test runs again (and
|
|
* `quarantine-guard.test.ts` goes red until the entry is removed). Both
|
|
* suites apply the list from their setup files.
|
|
*/
|
|
import { readFileSync } from "node:fs";
|
|
import { relative, resolve } from "node:path";
|
|
|
|
export interface QuarantineEntry {
|
|
/** Path from the repository root, e.g. `tests/integration/foo.test.ts`. */
|
|
file: string;
|
|
/** Full name: describe blocks and the test, joined with " > ". */
|
|
test: string;
|
|
/** Where the flake is tracked: `#123` or a URL. */
|
|
issue: string;
|
|
/** What fails and how often, in a sentence. */
|
|
reason: string;
|
|
/** ISO date the entry was added. */
|
|
added: string;
|
|
/** ISO date after which the test runs again; at most 14 days after `added`. */
|
|
expires: string;
|
|
}
|
|
|
|
export const QUARANTINE_FILE = resolve(import.meta.dirname, "quarantine.json");
|
|
export const MAX_QUARANTINE_DAYS = 14;
|
|
const ROOT = resolve(import.meta.dirname, "..", "..");
|
|
|
|
export function readQuarantine(path = QUARANTINE_FILE): QuarantineEntry[] {
|
|
const parsed = JSON.parse(readFileSync(path, "utf8")) as {
|
|
entries?: QuarantineEntry[];
|
|
};
|
|
return parsed.entries ?? [];
|
|
}
|
|
|
|
/** True while the entry's expiry date (inclusive) has not passed. */
|
|
export function isActive(
|
|
entry: QuarantineEntry,
|
|
today: Date = new Date(),
|
|
): boolean {
|
|
return today.toISOString().slice(0, 10) <= entry.expires;
|
|
}
|
|
|
|
let cache: QuarantineEntry[] | null = null;
|
|
|
|
export interface TaskLike {
|
|
name: string;
|
|
suite?: TaskLike | null;
|
|
file?: (TaskLike & { filepath?: string }) | null;
|
|
}
|
|
|
|
/** Describe blocks and the test, outermost first; the file task is the root. */
|
|
export function fullName(task: TaskLike): string {
|
|
const names: string[] = [];
|
|
let current: TaskLike | null | undefined = task;
|
|
while (current && current !== current.file) {
|
|
names.unshift(current.name);
|
|
current = current.suite;
|
|
}
|
|
return names.join(" > ");
|
|
}
|
|
|
|
/** The active entry naming this test, if any. */
|
|
export function quarantineEntryFor(task: TaskLike): QuarantineEntry | null {
|
|
cache ??= readQuarantine();
|
|
if (cache.length === 0) return null;
|
|
const filepath = task.file?.filepath;
|
|
if (!filepath) return null;
|
|
const file = relative(ROOT, filepath).split("\\").join("/");
|
|
const name = fullName(task);
|
|
return (
|
|
cache.find((e) => e.file === file && e.test === name && isActive(e)) ?? null
|
|
);
|
|
}
|