diff --git a/deno.json b/deno.json index 4755d4c7f..fb0f749c6 100644 --- a/deno.json +++ b/deno.json @@ -121,6 +121,7 @@ }, "test": { "include": [ + "./examples/test-examples", "./packages" ], "exclude": [ diff --git a/examples/test-examples/mod.test.ts b/examples/test-examples/mod.test.ts new file mode 100644 index 000000000..71287a230 --- /dev/null +++ b/examples/test-examples/mod.test.ts @@ -0,0 +1,65 @@ +import { deepStrictEqual, strictEqual } from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { parseCliArgs } from "./mod.ts"; + +describe("parseCliArgs", () => { + it("uses the documented default timeout", () => { + const options = parseCliArgs([]); + + strictEqual(options.defaultTimeoutMs, 10_000); + strictEqual(options.debugMode, false); + deepStrictEqual([...options.filterNames], []); + }); + + it("honors --timeout with a separate value", () => { + const options = parseCliArgs(["--timeout", "2500", "hono-sample"]); + + strictEqual(options.defaultTimeoutMs, 2500); + strictEqual(options.debugMode, false); + deepStrictEqual([...options.filterNames], ["hono-sample"]); + }); + + it("honors --timeout=value and debug aliases", () => { + const options = parseCliArgs([ + "--timeout=1500", + "-d", + "express", + "koa", + ]); + + strictEqual(options.defaultTimeoutMs, 1500); + strictEqual(options.debugMode, true); + deepStrictEqual([...options.filterNames], ["express", "koa"]); + }); + + it("ignores invalid timeout values without skipping flags", () => { + const options = parseCliArgs([ + "--timeout", + "--debug", + "--timeout", + "-d", + "--timeout=", + "--timeout=-1", + "express", + ]); + + strictEqual(options.defaultTimeoutMs, 10_000); + strictEqual(options.debugMode, true); + deepStrictEqual([...options.filterNames], ["express"]); + }); + + it("consumes invalid timeout values without treating them as filters", () => { + const options = parseCliArgs([ + "--timeout", + "abc", + "--timeout", + "-1", + "express", + ]); + + strictEqual(options.defaultTimeoutMs, 10_000); + strictEqual(options.debugMode, false); + deepStrictEqual([...options.filterNames], ["express"]); + }); +}); diff --git a/examples/test-examples/mod.ts b/examples/test-examples/mod.ts index c15abd073..c21c5c14f 100644 --- a/examples/test-examples/mod.ts +++ b/examples/test-examples/mod.ts @@ -33,34 +33,31 @@ const REPO_ROOT = fromFileUrl(new URL("../../", import.meta.url)); // ─── Logging ────────────────────────────────────────────────────────────────── // -// We configure logtape before everything else so that log calls in helpers -// work even if they execute at module-initialization time. -// // All test-runner logs live under the ["fedify", "examples"] category. // Library-internal fedify logs are intentionally excluded so they don't flood // the output. Pass --debug to lower the level from "info" to "debug". -const debugMode = Deno.args.includes("--debug") || Deno.args.includes("-d"); - -await configure({ - sinks: { console: getConsoleSink() }, - filters: {}, - loggers: [ - { - category: ["fedify", "examples"], - lowestLevel: debugMode ? "debug" : "info", - sinks: ["console"], - filters: [], - }, - { - // Suppress logtape's own meta-logs unless something is wrong. - category: ["logtape", "meta"], - lowestLevel: "warning", - sinks: ["console"], - filters: [], - }, - ], -}); +async function configureLogging(debugMode: boolean): Promise { + await configure({ + sinks: { console: getConsoleSink() }, + filters: {}, + loggers: [ + { + category: ["fedify", "examples"], + lowestLevel: debugMode ? "debug" : "info", + sinks: ["console"], + filters: [], + }, + { + // Suppress logtape's own meta-logs unless something is wrong. + category: ["logtape", "meta"], + lowestLevel: "warning", + sinks: ["console"], + filters: [], + }, + ], + }); +} const logger = getLogger(["fedify", "examples", "test-runner"]); @@ -108,7 +105,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; @@ -125,6 +122,52 @@ type TestResult = | { name: string; status: "fail"; error: string; output: string } | { name: string; status: "skip"; reason: string }; +interface CliOptions { + defaultTimeoutMs: number; + debugMode: boolean; + filterNames: Set; +} + +function parseTimeoutMs(value: string | undefined): number | undefined { + if (value == null || value.trim() === "") return undefined; + const timeoutMs = Number(value); + if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return undefined; + return timeoutMs; +} + +function isCliFlag(value: string): boolean { + return value === "-d" || value.startsWith("--"); +} + +export function parseCliArgs(args: readonly string[]): CliOptions { + let defaultTimeoutMs = 10_000; + let debugMode = false; + const filterNames = new Set(); + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === "--timeout") { + const rawValue = args[i + 1]; + const timeoutMs = parseTimeoutMs(rawValue); + if (timeoutMs != null) { + defaultTimeoutMs = timeoutMs; + i++; + } else if (rawValue != null && !isCliFlag(rawValue)) { + i++; + } + } else if (arg.startsWith("--timeout=")) { + const timeoutMs = parseTimeoutMs(arg.slice("--timeout=".length)); + if (timeoutMs != null) defaultTimeoutMs = timeoutMs; + } else if (arg === "--debug" || arg === "-d") { + debugMode = true; + } else if (!arg.startsWith("--")) { + filterNames.add(arg); + } + } + + return { defaultTimeoutMs, debugMode, filterNames }; +} + // ─── Example Registry ───────────────────────────────────────────────────────── // // Every example directory under examples/ must be registered in exactly one of @@ -713,23 +756,10 @@ function printInlineResult(result: TestResult): void { // ─── Main ───────────────────────────────────────────────────────────────────── -async function main(): Promise { +async function main(args: readonly string[] = Deno.args): Promise { // ── Parse CLI arguments ────────────────────────────────────────────────── - let defaultTimeoutMs = 10_000; - const filterNames = new Set(); - - for (let i = 0; i < Deno.args.length; i++) { - const arg = Deno.args[i]; - if (arg === "--timeout" && i + 1 < Deno.args.length) { - defaultTimeoutMs = Number(Deno.args[++i]); - } else if (arg.startsWith("--timeout=")) { - defaultTimeoutMs = Number(arg.slice("--timeout=".length)); - } else if (arg === "--debug" || arg === "-d") { - // already handled above at module level - } else if (!arg.startsWith("--")) { - filterNames.add(arg); - } - } + const { debugMode, defaultTimeoutMs, filterNames } = parseCliArgs(args); + await configureLogging(debugMode); const shouldRun = (name: string) => filterNames.size === 0 || filterNames.has(name); @@ -859,4 +889,4 @@ async function main(): Promise { Deno.exit(0); } -await main(); +if (import.meta.main) await main();