From a21d3700145c2f55b325419739c73b205bb8fe2d Mon Sep 17 00:00:00 2001 From: Matteo Stella Date: Tue, 30 Jun 2026 14:04:46 +0200 Subject: [PATCH 1/4] Cover example runner timeout parsing The example test runner documents --timeout, but the option did not have focused coverage. This extracts argument parsing into a pure helper so the documented forms can be checked without starting examples. While making the module importable, this also aligns the multi-handle example type with the cmd field the runner already uses. Issue: https://github.com/fedify-dev/fedify/issues/887 Assisted-by: Codex:gpt-5 --- CHANGES.md | 9 +++ deno.json | 1 + examples/test-examples/mod.test.ts | 35 +++++++++++ examples/test-examples/mod.ts | 95 +++++++++++++++++------------- 4 files changed, 98 insertions(+), 42 deletions(-) create mode 100644 examples/test-examples/mod.test.ts diff --git a/CHANGES.md b/CHANGES.md index 679cf4860..ae8a18179 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,15 @@ Version 2.4.0 To be released. +### Documentation and examples + + - Added focused coverage for the example test runner's documented + `--timeout` option so contributor checks catch regressions in its CLI + parsing. [[#887], [#910] by Matteo Stella] + +[#887]: https://github.com/fedify-dev/fedify/issues/887 +[#910]: https://github.com/fedify-dev/fedify/pull/910 + Version 2.3.1 ------------- 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..363a96c77 --- /dev/null +++ b/examples/test-examples/mod.test.ts @@ -0,0 +1,35 @@ +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"]); + }); +}); diff --git a/examples/test-examples/mod.ts b/examples/test-examples/mod.ts index c15abd073..df5b7575b 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,33 @@ type TestResult = | { name: string; status: "fail"; error: string; output: string } | { name: string; status: "skip"; reason: string }; +interface CliOptions { + defaultTimeoutMs: number; + debugMode: boolean; + filterNames: Set; +} + +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" && i + 1 < args.length) { + defaultTimeoutMs = Number(args[++i]); + } else if (arg.startsWith("--timeout=")) { + defaultTimeoutMs = Number(arg.slice("--timeout=".length)); + } 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 +737,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 +870,4 @@ async function main(): Promise { Deno.exit(0); } -await main(); +if (import.meta.main) await main(); From 7a5e2ecd4904cf83be42234b60efe84984d61064 Mon Sep 17 00:00:00 2001 From: Matteo Stella Date: Tue, 30 Jun 2026 14:13:49 +0200 Subject: [PATCH 2/4] Validate example runner timeout values Invalid --timeout values could previously become NaN and skip a following flag. Keep the default unless the option receives a positive finite value, and cover the review case. Addresses review feedback on https://github.com/fedify-dev/fedify/pull/910. Assisted-by: Codex:gpt-5 --- examples/test-examples/mod.test.ts | 14 ++++++++++++++ examples/test-examples/mod.ts | 17 ++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/examples/test-examples/mod.test.ts b/examples/test-examples/mod.test.ts index 363a96c77..a9ce0db05 100644 --- a/examples/test-examples/mod.test.ts +++ b/examples/test-examples/mod.test.ts @@ -32,4 +32,18 @@ describe("parseCliArgs", () => { strictEqual(options.debugMode, true); deepStrictEqual([...options.filterNames], ["express", "koa"]); }); + + it("ignores invalid timeout values without skipping flags", () => { + const options = parseCliArgs([ + "--timeout", + "--debug", + "--timeout=", + "--timeout=-1", + "express", + ]); + + strictEqual(options.defaultTimeoutMs, 10_000); + strictEqual(options.debugMode, true); + deepStrictEqual([...options.filterNames], ["express"]); + }); }); diff --git a/examples/test-examples/mod.ts b/examples/test-examples/mod.ts index df5b7575b..2bc323ab2 100644 --- a/examples/test-examples/mod.ts +++ b/examples/test-examples/mod.ts @@ -128,6 +128,13 @@ interface CliOptions { 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; +} + export function parseCliArgs(args: readonly string[]): CliOptions { let defaultTimeoutMs = 10_000; let debugMode = false; @@ -135,10 +142,14 @@ export function parseCliArgs(args: readonly string[]): CliOptions { for (let i = 0; i < args.length; i++) { const arg = args[i]; - if (arg === "--timeout" && i + 1 < args.length) { - defaultTimeoutMs = Number(args[++i]); + if (arg === "--timeout") { + const timeoutMs = parseTimeoutMs(args[i + 1]); + if (timeoutMs == null) continue; + defaultTimeoutMs = timeoutMs; + i++; } else if (arg.startsWith("--timeout=")) { - defaultTimeoutMs = Number(arg.slice("--timeout=".length)); + const timeoutMs = parseTimeoutMs(arg.slice("--timeout=".length)); + if (timeoutMs != null) defaultTimeoutMs = timeoutMs; } else if (arg === "--debug" || arg === "-d") { debugMode = true; } else if (!arg.startsWith("--")) { From 1be51705dfcb8a99636c34e21d5de1ff372e4879 Mon Sep 17 00:00:00 2001 From: Matteo Stella Date: Tue, 30 Jun 2026 14:21:05 +0200 Subject: [PATCH 3/4] Consume invalid timeout values Invalid non-flag values after --timeout should not become example filters. Consume those values while leaving real flags available for normal parsing. Addresses follow-up review feedback on https://github.com/fedify-dev/fedify/pull/910. Assisted-by: Codex:gpt-5 --- examples/test-examples/mod.test.ts | 16 ++++++++++++++++ examples/test-examples/mod.ts | 16 ++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/examples/test-examples/mod.test.ts b/examples/test-examples/mod.test.ts index a9ce0db05..71287a230 100644 --- a/examples/test-examples/mod.test.ts +++ b/examples/test-examples/mod.test.ts @@ -37,6 +37,8 @@ describe("parseCliArgs", () => { const options = parseCliArgs([ "--timeout", "--debug", + "--timeout", + "-d", "--timeout=", "--timeout=-1", "express", @@ -46,4 +48,18 @@ describe("parseCliArgs", () => { 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 2bc323ab2..c21c5c14f 100644 --- a/examples/test-examples/mod.ts +++ b/examples/test-examples/mod.ts @@ -135,6 +135,10 @@ function parseTimeoutMs(value: string | undefined): number | 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; @@ -143,10 +147,14 @@ export function parseCliArgs(args: readonly string[]): CliOptions { for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === "--timeout") { - const timeoutMs = parseTimeoutMs(args[i + 1]); - if (timeoutMs == null) continue; - defaultTimeoutMs = timeoutMs; - i++; + 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; From debb92257b8571a9ae395a0cdc1b0671154500d0 Mon Sep 17 00:00:00 2001 From: Matteo Stella Date: Tue, 30 Jun 2026 14:25:38 +0200 Subject: [PATCH 4/4] Remove changelog entry The example runner test coverage does not change user-facing API, so leave CHANGES.md untouched. Addresses maintainer feedback on https://github.com/fedify-dev/fedify/pull/910. Assisted-by: Codex:gpt-5 --- CHANGES.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ae8a18179..679cf4860 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,15 +8,6 @@ Version 2.4.0 To be released. -### Documentation and examples - - - Added focused coverage for the example test runner's documented - `--timeout` option so contributor checks catch regressions in its CLI - parsing. [[#887], [#910] by Matteo Stella] - -[#887]: https://github.com/fedify-dev/fedify/issues/887 -[#910]: https://github.com/fedify-dev/fedify/pull/910 - Version 2.3.1 -------------