|
1 | 1 | // Copyright (c) 2026 Sub Rosa contributors |
2 | 2 | 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"; |
3 | 6 | import { describe, it } from "node:test"; |
4 | 7 | import assert from "node:assert/strict"; |
5 | 8 |
|
| 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 | + |
6 | 44 | describe("check-fixture-sizes", () => { |
7 | 45 | 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/); |
14 | 49 | }); |
15 | 50 |
|
16 | 51 | 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 | + } |
25 | 107 | }); |
26 | 108 | }); |
0 commit comments