diff --git a/scripts/mirror-vgi-bench-media.test.ts b/scripts/mirror-vgi-bench-media.test.ts index e9dc7038..0d278f5e 100644 --- a/scripts/mirror-vgi-bench-media.test.ts +++ b/scripts/mirror-vgi-bench-media.test.ts @@ -1,9 +1,11 @@ import { describe, expect, test } from "bun:test"; import { VGI_BENCH_DEFAULT_REVISION } from "../src/benchmarks/vgi-bench/benchmark"; +import { assertFailure, assertSuccess } from "../src/internal/testing"; import type { ManifestEntry } from "./mirror-vgi-bench-media"; import { candidateSources, + describeReadOptionsError, extensionOf, hashManifest, readOptions, @@ -42,7 +44,9 @@ describe("extensionOf", () => { describe("readOptions", () => { test("applies defaults", () => { - const options = readOptions([]); + const result = readOptions([]); + assertSuccess(result); + const options = result.success; expect(options.concurrency).toBe(8); expect(options.revision).toBe(VGI_BENCH_DEFAULT_REVISION); expect(options.limit).toBeUndefined(); @@ -51,7 +55,7 @@ describe("readOptions", () => { }); test("parses provided flags", () => { - const options = readOptions([ + const result = readOptions([ "--concurrency=3", "--limit=5", "--out=manifest.json", @@ -59,7 +63,8 @@ describe("readOptions", () => { "--force", "--dry-run", ]); - expect(options).toEqual({ + assertSuccess(result); + expect(result.success).toEqual({ concurrency: 3, limit: 5, out: "manifest.json", @@ -69,9 +74,41 @@ describe("readOptions", () => { }); }); - test("rejects invalid numeric flags", () => { - expect(() => readOptions(["--concurrency=0"])).toThrow(); - expect(() => readOptions(["--limit=abc"])).toThrow(); + test("fails with concurrency_not_positive_integer for --concurrency=0", () => { + const result = readOptions(["--concurrency=0"]); + assertFailure(result); + expect(result.failure).toEqual({ + kind: "concurrency_not_positive_integer", + }); + }); + + test("fails with limit_not_positive_integer for --limit=abc", () => { + const result = readOptions(["--limit=abc"]); + assertFailure(result); + expect(result.failure).toEqual({ kind: "limit_not_positive_integer" }); + }); + + test("reports the first failing flag when both are invalid", () => { + const result = readOptions(["--concurrency=1.5", "--limit=0"]); + assertFailure(result); + expect(result.failure).toEqual({ + kind: "concurrency_not_positive_integer", + }); + }); +}); + +describe("describeReadOptionsError", () => { + test.each([ + [ + { kind: "concurrency_not_positive_integer" }, + "--concurrency must be a positive integer", + ], + [ + { kind: "limit_not_positive_integer" }, + "--limit must be a positive integer", + ], + ] as const)("renders %p as the CLI message", (error, message) => { + expect(describeReadOptionsError(error)).toBe(message); }); }); diff --git a/scripts/mirror-vgi-bench-media.ts b/scripts/mirror-vgi-bench-media.ts index 31a7d4ec..db23b20a 100644 --- a/scripts/mirror-vgi-bench-media.ts +++ b/scripts/mirror-vgi-bench-media.ts @@ -1,4 +1,5 @@ import { S3Client } from "bun"; +import * as Result from "effect/Result"; import { VGI_BENCH_CONFIG, @@ -88,7 +89,13 @@ type MirrorOutcome = | { readonly kind: "mirrored"; readonly entry: ManifestEntry } | { readonly kind: "unresolved"; readonly reason: string }; -export function readOptions(argv: readonly string[]): MirrorOptions { +export type ReadOptionsError = + | { readonly kind: "concurrency_not_positive_integer" } + | { readonly kind: "limit_not_positive_integer" }; + +export function readOptions( + argv: readonly string[] +): Result.Result { const flag = (name: string): string | undefined => { const prefixed = `--${name}=`; const match = argv.find((arg) => arg.startsWith(prefixed)); @@ -96,21 +103,35 @@ export function readOptions(argv: readonly string[]): MirrorOptions { }; const concurrency = Number(flag("concurrency") ?? "8"); if (!Number.isInteger(concurrency) || concurrency < 1) { - throw new Error("--concurrency must be a positive integer"); + return Result.fail({ kind: "concurrency_not_positive_integer" }); } const rawLimit = flag("limit"); const limit = rawLimit === undefined ? undefined : Number(rawLimit); if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) { - throw new Error("--limit must be a positive integer"); + return Result.fail({ kind: "limit_not_positive_integer" }); } - return { + return Result.succeed({ concurrency, out: flag("out") ?? "vgi-bench-media-manifest.json", force: argv.includes("--force"), dryRun: argv.includes("--dry-run"), revision: flag("revision") ?? VGI_BENCH_DEFAULT_REVISION, limit, - }; + }); +} + +export function describeReadOptionsError(error: ReadOptionsError): string { + switch (error.kind) { + case "concurrency_not_positive_integer": { + return "--concurrency must be a positive integer"; + } + case "limit_not_positive_integer": { + return "--limit must be a positive integer"; + } + default: { + return error satisfies never; + } + } } export type { ManifestEntry }; @@ -238,7 +259,12 @@ export function hashManifest(entries: readonly ManifestEntry[]): string { async function main(): Promise { const env = readMediaMirrorEnv(); - const options = readOptions(process.argv.slice(2)); + const parsed = readOptions(process.argv.slice(2)); + if (Result.isFailure(parsed)) { + process.stderr.write(`${describeReadOptionsError(parsed.failure)}\n`); + process.exit(1); + } + const options = parsed.success; const s3 = new S3Client(env); const allVideos = await fetchSourceVideos( options.revision,