From 3a4dc16e1db3d5efeadc53e7694ad0f5cadd41fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 22 Oct 2026 08:36:35 +0200 Subject: [PATCH] test(guards): read each file and the schema once in the backup plan check The two-ended checks ask every declared file about every model and each of its relations, and read the schema and the files again for each question. With the backup split into more files that took two seconds a case and timed out on a loaded runner; the reads are memoised now. --- .../backup-plan-classification.test.ts | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/src/__tests__/backup-plan-classification.test.ts b/src/__tests__/backup-plan-classification.test.ts index e41ddd97c..9151ef24f 100644 --- a/src/__tests__/backup-plan-classification.test.ts +++ b/src/__tests__/backup-plan-classification.test.ts @@ -208,8 +208,45 @@ const ROUND_TRIP_TEST = "tests/integration/backup-round-trip.test.ts"; const REPO_ROOT = resolve(__dirname, "../.."); +/** + * Every read below is memoised: the two-ended checks ask each declared file + * about each model and each of its relations, and reading the schema and the + * files again for every question made those checks slow enough to time out on + * a loaded runner. + */ +const sourceCache = new Map(); + function read(relativePath: string): string { - return readFileSync(resolve(REPO_ROOT, relativePath), "utf8"); + let text = sourceCache.get(relativePath); + if (text === undefined) { + text = readFileSync(resolve(REPO_ROOT, relativePath), "utf8"); + sourceCache.set(relativePath, text); + } + return text; +} + +const flatCache = new Map(); + +/** A declared file, whitespace-flattened so a call broken across lines matches. */ +function flat(relativePath: string): string { + let text = flatCache.get(relativePath); + if (text === undefined) { + text = read(relativePath).replace(/\s+/g, " "); + flatCache.set(relativePath, text); + } + return text; +} + +let schemaModelBodies: Array<[string, string]> | null = null; + +/** Every schema model's name and body, read once. */ +function modelBodies(): Array<[string, string]> { + schemaModelBodies ??= [ + ...readFileSync(SCHEMA_PATH, "utf8").matchAll( + /^model\s+(\w+)\s*\{([\s\S]*?)^\}/gm, + ), + ].map(([, name, body]) => [name, body]); + return schemaModelBodies; } /** `Measurement` → `measurement`, the Prisma delegate name. */ @@ -238,11 +275,8 @@ function relationParents(model: string): Array<{ field: string; parent: string; }> { - const source = readFileSync(SCHEMA_PATH, "utf8"); const found: Array<{ field: string; parent: string }> = []; - for (const [, parent, body] of source.matchAll( - /^model\s+(\w+)\s*\{([\s\S]*?)^\}/gm, - )) { + for (const [parent, body] of modelBodies()) { for (const line of body.split("\n")) { const match = /^\s*(\w+)\s+(\w+)(\[\])?\??\s*(@|$)/.exec(line); if (match && match[2] === model) { @@ -271,9 +305,8 @@ function callsDelegate(source: string, model: string, ops: RegExp): boolean { /** Every model a relation field of this name points at, across the schema. */ function targetsOfRelationField(field: string): Set { - const source = readFileSync(SCHEMA_PATH, "utf8"); const targets = new Set(); - for (const [, body] of source.matchAll(/^model\s+\w+\s*\{([\s\S]*?)^\}/gm)) { + for (const [, body] of modelBodies()) { for (const line of body.split("\n")) { const match = /^\s*(\w+)\s+(\w+)(\[\])?\??\s*(@|$)/.exec(line); if (match && match[1] === field) targets.add(match[2]); @@ -301,8 +334,7 @@ function touches( relationOps: RegExp, ): boolean { return files.some((file) => { - // Whitespace-flattened so a call broken across lines still matches. - const source = read(file).replace(/\s+/g, " "); + const source = flat(file); if (callsDelegate(source, model, delegateOps)) return true; return relationParents(model).some(({ field, parent }) => { if (