Skip to content

Commit 37ab212

Browse files
authored
Merge pull request #267 from leocagli/test/required-fixture-groups
test(fixtures): fail when a required fixture group is missing
2 parents d91e5af + 351ea73 commit 37ab212

2 files changed

Lines changed: 130 additions & 20 deletions

File tree

scripts/check-fixture-sizes.mjs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Copyright (c) 2026 Sub Rosa contributors
2-
import { readdirSync, statSync } from "node:fs";
2+
import { existsSync, readdirSync, statSync } from "node:fs";
33
import { join, relative } from "node:path";
44

55
const GROUPS = [
@@ -37,8 +37,11 @@ function walk(dir, include) {
3737
files.push(full);
3838
}
3939
}
40-
} catch {
41-
// directory does not exist
40+
} catch (error) {
41+
// Un directorio ausente es normal al recorrer en profundidad. Cualquier
42+
// otro error, como un permiso denegado, no lo es: tragarselo hacia que un
43+
// grupo ilegible se viera igual que uno vacio.
44+
if (error.code !== "ENOENT" && error.code !== "ENOTDIR") throw error;
4245
}
4346
return files;
4447
}
@@ -59,7 +62,15 @@ function checkGroup(group) {
5962
const totalOk = totalBytes <= group.totalBytes;
6063
const allFilesOk = files.every((f) => f.ok);
6164

62-
return { label: group.label, files, totalBytes, totalOk, ok: allFilesOk && totalOk };
65+
return {
66+
label: group.label,
67+
dir: group.dir,
68+
dirExists: existsSync(group.dir),
69+
files,
70+
totalBytes,
71+
totalOk,
72+
ok: allFilesOk && totalOk,
73+
};
6374
}
6475

6576
function main() {
@@ -68,8 +79,25 @@ function main() {
6879
for (const group of GROUPS) {
6980
const result = checkGroup(group);
7081

82+
// Todo grupo configurado en GROUPS es obligatorio. Antes un grupo ausente
83+
// o vacio salia por SKIP y el proceso terminaba en 0, asi que borrar un
84+
// grupo entero desactivaba en silencio el presupuesto que lo cuidaba.
85+
//
86+
// Los dos casos se informan por separado a proposito: "no existe" y "esta
87+
// vacio" se arreglan distinto, y un solo mensaje para los dos te obliga a
88+
// ir a mirar cual de los dos fue.
89+
if (!result.dirExists) {
90+
console.log(` [FAIL] ${group.label} — required fixture directory is missing: ${group.dir}`);
91+
allPassed = false;
92+
continue;
93+
}
94+
7195
if (result.files.length === 0) {
72-
console.log(` [SKIP] ${group.label} — directory not found or empty`);
96+
console.log(
97+
` [FAIL] ${group.label} — required fixture directory has no matching files: ` +
98+
`${group.dir} (expected files matching ${group.include})`,
99+
);
100+
allPassed = false;
73101
continue;
74102
}
75103

@@ -102,7 +130,7 @@ function main() {
102130
console.log("All fixture size budgets are within limits.");
103131
process.exit(0);
104132
} else {
105-
console.log("Some fixture size budgets are exceeded.");
133+
console.log("Fixture check failed: a budget was exceeded or a required group is missing.");
106134
console.log("To update budgets, edit GROUPS in scripts/check-fixture-sizes.mjs.");
107135
process.exit(1);
108136
}
Lines changed: 96 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,108 @@
11
// Copyright (c) 2026 Sub Rosa contributors
22
import { execFileSync } from "node:child_process";
3+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4+
import { tmpdir } from "node:os";
5+
import { join } from "node:path";
36
import { describe, it } from "node:test";
47
import assert from "node:assert/strict";
58

9+
const CHECKER = new URL("check-fixture-sizes.mjs", import.meta.url).pathname;
10+
const RAIZ = new URL("..", import.meta.url).pathname;
11+
12+
/** Corre el checker y devuelve salida y codigo, sin tirar cuando falla. */
13+
function correr(cwd) {
14+
try {
15+
const stdout = execFileSync("node", [CHECKER], { encoding: "utf-8", cwd });
16+
return { code: 0, stdout };
17+
} catch (error) {
18+
return { code: error.status, stdout: error.stdout ?? "" };
19+
}
20+
}
21+
22+
/**
23+
* Un proyecto de mentira con los tres grupos que declara GROUPS.
24+
*
25+
* `omitir` no crea ese directorio; `vaciar` lo crea sin archivos que matcheen.
26+
* Son los dos casos que antes salian por SKIP con exit 0.
27+
*/
28+
function proyecto({ omitir = null, vaciar = null } = {}) {
29+
const raiz = mkdtempSync(join(tmpdir(), "fixture-check-"));
30+
const grupos = [
31+
["services/receipt-cli/src/fixtures", "a.json", "{}"],
32+
["contracts/round/test_snapshots/test", "b.json", "{}"],
33+
["apps/web/src/demo", "c.ts", "export const x = 1;\n"],
34+
];
35+
for (const [dir, archivo, contenido] of grupos) {
36+
if (dir === omitir) continue;
37+
mkdirSync(join(raiz, dir), { recursive: true });
38+
if (dir === vaciar) continue;
39+
writeFileSync(join(raiz, dir, archivo), contenido);
40+
}
41+
return raiz;
42+
}
43+
644
describe("check-fixture-sizes", () => {
745
it("exits 0 when all fixture sizes are within budget", () => {
8-
const result = execFileSync(
9-
"node",
10-
[new URL("check-fixture-sizes.mjs", import.meta.url).pathname],
11-
{ encoding: "utf-8", cwd: new URL("..", import.meta.url).pathname },
12-
);
13-
assert.match(result, /All fixture size budgets are within limits/);
46+
const { code, stdout } = correr(RAIZ);
47+
assert.equal(code, 0);
48+
assert.match(stdout, /All fixture size budgets are within limits/);
1449
});
1550

1651
it("reports all three fixture groups", () => {
17-
const result = execFileSync(
18-
"node",
19-
[new URL("check-fixture-sizes.mjs", import.meta.url).pathname],
20-
{ encoding: "utf-8", cwd: new URL("..", import.meta.url).pathname },
21-
);
22-
assert.match(result, /Receipt fixtures/);
23-
assert.match(result, /Contract test snapshots/);
24-
assert.match(result, /Demo trace outputs/);
52+
const { stdout } = correr(RAIZ);
53+
assert.match(stdout, /Receipt fixtures/);
54+
assert.match(stdout, /Contract test snapshots/);
55+
assert.match(stdout, /Demo trace outputs/);
56+
});
57+
58+
it("passes on a temporary project where every required group is present", () => {
59+
const raiz = proyecto();
60+
try {
61+
const { code } = correr(raiz);
62+
assert.equal(code, 0, "un proyecto con los tres grupos completos tiene que pasar");
63+
} finally {
64+
rmSync(raiz, { recursive: true, force: true });
65+
}
66+
});
67+
68+
it("fails when a required group directory is missing", () => {
69+
const raiz = proyecto({ omitir: "services/receipt-cli/src/fixtures" });
70+
try {
71+
const { code, stdout } = correr(raiz);
72+
// Este es el agujero: antes salia [SKIP] y terminaba en 0, asi que
73+
// borrar un grupo entero desactivaba su presupuesto sin que nadie viera.
74+
assert.notEqual(code, 0, "un grupo ausente tiene que dar exit distinto de 0");
75+
assert.match(stdout, /required fixture directory is missing/);
76+
assert.match(stdout, /Receipt fixtures/);
77+
assert.doesNotMatch(stdout, /\[SKIP\]/);
78+
} finally {
79+
rmSync(raiz, { recursive: true, force: true });
80+
}
81+
});
82+
83+
it("fails when a required group directory exists but has no matching files", () => {
84+
const raiz = proyecto({ vaciar: "apps/web/src/demo" });
85+
try {
86+
const { code, stdout } = correr(raiz);
87+
assert.notEqual(code, 0, "un grupo vacio tiene que dar exit distinto de 0");
88+
assert.match(stdout, /has no matching files/);
89+
assert.match(stdout, /Demo trace outputs/);
90+
} finally {
91+
rmSync(raiz, { recursive: true, force: true });
92+
}
93+
});
94+
95+
it("tells the two failure paths apart", () => {
96+
// Se arreglan distinto: uno se restaura, el otro se repuebla. Un mensaje
97+
// unico para los dos te obliga a ir a mirar cual fue.
98+
const ausente = proyecto({ omitir: "contracts/round/test_snapshots/test" });
99+
const vacio = proyecto({ vaciar: "contracts/round/test_snapshots/test" });
100+
try {
101+
assert.match(correr(ausente).stdout, /is missing/);
102+
assert.match(correr(vacio).stdout, /has no matching files/);
103+
} finally {
104+
rmSync(ausente, { recursive: true, force: true });
105+
rmSync(vacio, { recursive: true, force: true });
106+
}
25107
});
26108
});

0 commit comments

Comments
 (0)