diff --git a/deno.json b/deno.json index a5b026089..cdef4dd87 100644 --- a/deno.json +++ b/deno.json @@ -39,6 +39,7 @@ "./examples/lint/oxlint", "./examples/monitoring", "./examples/rfc-9421-test", + "./examples/test-examples", "./test/smoke/harness" ], "imports": { diff --git a/examples/test-examples/deno.json b/examples/test-examples/deno.json new file mode 100644 index 000000000..1f84a0d13 --- /dev/null +++ b/examples/test-examples/deno.json @@ -0,0 +1,6 @@ +{ + "tasks": { + "test": "deno test --allow-all .", + "start": "deno run --allow-all mod.ts" + } +} diff --git a/examples/test-examples/mod.test.ts b/examples/test-examples/mod.test.ts new file mode 100644 index 000000000..78b6e947a --- /dev/null +++ b/examples/test-examples/mod.test.ts @@ -0,0 +1,46 @@ +/** + * Registry-consistency tests for the example test runner. + * + * Every example directory under examples/ must be registered in exactly one + * of the registries in mod.ts (SERVER_EXAMPLES, SCRIPT_EXAMPLES, + * MULTI_HANDLE_EXAMPLES, or SKIPPED_EXAMPLES). These tests fail when a new + * example is added without being registered, so it cannot silently miss + * automated checks. + * + * Usage (from repository root): + * deno test --allow-all examples/test-examples + */ +import { fromFileUrl } from "@std/path"; +import assert from "node:assert/strict"; +import test from "node:test"; + +import { getRegisteredExampleNames, scanUnregisteredExamples } from "./mod.ts"; + +const EXAMPLES_DIR = fromFileUrl(new URL("../", import.meta.url)); + +test("Every example in the examples/ directory is registered in test-examples", async () => { + const unregistered = await scanUnregisteredExamples(); + assert.deepEqual( + unregistered, + [], + `Unregistered example directories found: ${unregistered.join(", ")}`, + ); +}); + +test("Every registered example has a matching examples/ directory", async () => { + const directories = new Set(); + for await (const entry of Deno.readDir(EXAMPLES_DIR)) { + if (entry.isDirectory) directories.add(entry.name); + } + const unmatchedExamples = [...getRegisteredExampleNames()] + .filter((name) => !directories.has(name)) + .sort(); + + assert.deepEqual( + unmatchedExamples, + [], + `Registered examples without a matching directory found: ${ + unmatchedExamples.join(", ") + }`, + ); +}); diff --git a/examples/test-examples/mod.ts b/examples/test-examples/mod.ts index 7edb03497..f4a420135 100644 --- a/examples/test-examples/mod.ts +++ b/examples/test-examples/mod.ts @@ -108,7 +108,7 @@ interface MultiHandleExample { name: string; dir: string; /** Command prefix—the handle is appended as the final argument. */ - cmdPrefix?: string[]; + cmd: string[]; /** Handles to try, in order. Pass if any succeeds. */ handles: string[]; description: string; @@ -294,6 +294,19 @@ const SERVER_EXAMPLES: ServerExample[] = [ readyUrl: "http://localhost:3000/", readyTimeout: 30_000, }, + { + // SolidStart sample using @fedify/solidstart; actor path is + // /users/{identifier}. Built with vinxi build; served with vinxi start + // on port 3000. + name: "solidstart", + dir: "solidstart", + buildCmd: ["pnpm", "build"], + startCmd: ["pnpm", "start"], + port: 3000, + actor: "demo", + readyUrl: "http://localhost:3000/", + readyTimeout: 30_000, + }, ]; const SCRIPT_EXAMPLES: ScriptExample[] = [ @@ -350,6 +363,32 @@ const SKIPPED_EXAMPLES: SkippedExample[] = [ }, ]; +export function getRegisteredExampleNames(): Set { + return new Set([ + ...SERVER_EXAMPLES, + ...SCRIPT_EXAMPLES, + ...MULTI_HANDLE_EXAMPLES, + ...SKIPPED_EXAMPLES, + ].map((e) => e.name)); +} + +/** + * Scans the examples/ directory and returns the names of sub-directories + * that are not registered in any of the registries above. + */ +export async function scanUnregisteredExamples(): Promise { + const registeredNames = getRegisteredExampleNames(); + const unregistered: string[] = []; + for await (const entry of Deno.readDir(EXAMPLES_DIR)) { + if (!entry.isDirectory) continue; + if (entry.name === "test-examples") continue; + if (!registeredNames.has(entry.name)) { + unregistered.push(entry.name); + } + } + return unregistered; +} + // ─── ANSI Colors ────────────────────────────────────────────────────────────── const c = { @@ -805,24 +844,8 @@ async function main(): Promise { } // ── Unregistered examples ──────────────────────────────────────────────── - // Collect every example name that appears in any registry. - const registeredNames = new Set([ - ...SERVER_EXAMPLES.map((e) => e.name), - ...SCRIPT_EXAMPLES.map((e) => e.name), - ...MULTI_HANDLE_EXAMPLES.map((e) => e.name), - ...SKIPPED_EXAMPLES.map((e) => e.name), - ]); - // Scan the examples/ directory for sub-directories that are not registered. - const unregistered: string[] = []; - for await (const entry of Deno.readDir(EXAMPLES_DIR)) { - if (!entry.isDirectory) continue; - // The test-examples directory is the test runner itself—skip it. - if (entry.name === "test-examples") continue; - if (!registeredNames.has(entry.name)) { - unregistered.push(entry.name); - } - } + const unregistered = await scanUnregisteredExamples(); // ── Summary ────────────────────────────────────────────────────────────── const passed = results.filter((r) => r.status === "pass"); @@ -871,4 +894,8 @@ async function main(): Promise { Deno.exit(0); } -await main(); +// Run the test runner only when this module is executed directly, so that +// mod.test.ts can import the registry helpers above without side effects. +if (import.meta.main) { + await main(); +}