From cc636e1fc187222f6ccdce7e4bd3ad121b14229f Mon Sep 17 00:00:00 2001 From: Brandon Ong Date: Thu, 20 Aug 2026 07:00:33 +0000 Subject: [PATCH] feat(cli): accept multiple files in `hflow doctor` `hflow doctor` took exactly one file, so validating a directory of episodes needed a shell loop whose exit code only reflected the last invocation. The `file` argument is now `nargs="+"`: each file is diagnosed in turn, its report printed (`summary()` already leads with `doctor: `, so no extra prefix is needed), and the command exits 1 if any file is non-conforming, 0 when all conform. A missing file still prints the one-line errno message and exits 2 without diagnosing the rest. Closes #29 Claude-Session: https://claude.ai/code/session_01AEdPuniM5w5DjWBSP9q39X --- docs/FORMAT.md | 5 +++-- src/hflow/cli.py | 24 +++++++++++++++--------- tests/test_doctor.py | 10 ++++++++++ 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/docs/FORMAT.md b/docs/FORMAT.md index 10f7549d8..45d1eab52 100644 --- a/docs/FORMAT.md +++ b/docs/FORMAT.md @@ -127,8 +127,9 @@ A file claiming this convention must satisfy, in increasing strictness: 4. **Video constraints**: every `foxglove.CompressedVideo` message is one Annex B access unit beginning with an AUD; every IDR access unit contains SPS and PPS; no B-frames; `format="h264"`. 5. **Stamps**: `provenance/v1` present with `schema_version` and `pipeline_version`. -`hflow doctor ` executes these checks and exits with status 0 for -a conforming file or 1 when it reports violations. +`hflow doctor [more.mcap ...]` executes these checks against every +file given, printing one report each, and exits with status 0 when all files +conform or 1 when any reports violations. ## References diff --git a/src/hflow/cli.py b/src/hflow/cli.py index 240840dc4..0fb5c8274 100644 --- a/src/hflow/cli.py +++ b/src/hflow/cli.py @@ -92,10 +92,12 @@ def _build_parser() -> argparse.ArgumentParser: description=( "Validate container integrity, metadata stamps, chunk-group layout, " "and in-band video constraints (docs/FORMAT.md, executable form). " - "Exit code 0 when conforming, 1 when not." + "Accepts multiple files; exit code 0 when all conform, 1 when any does not." ), ) - doctor_parser.add_argument("file", help="the local path or object-store URL to check") + doctor_parser.add_argument( + "file", nargs="+", help="the local paths or object-store URLs to check" + ) up_parser = subparsers.add_parser( "up", @@ -490,13 +492,17 @@ def _command_curate(arguments: argparse.Namespace) -> int: def _command_doctor(arguments: argparse.Namespace) -> int: - try: - doctor_report = diagnose(arguments.file) - except (ValueError, FileNotFoundError) as error: - print(f"doctor: {error}", file=sys.stderr) - return 2 - print(doctor_report.summary()) - return 0 if doctor_report.conforming else 1 + exit_code = 0 + for file in arguments.file: + try: + doctor_report = diagnose(file) + except (ValueError, FileNotFoundError) as error: + print(f"doctor: {error}", file=sys.stderr) + return 2 + print(doctor_report.summary()) + if not doctor_report.conforming: + exit_code = 1 + return exit_code def main(argv: list[str] | None = None) -> int: diff --git a/tests/test_doctor.py b/tests/test_doctor.py index 23e692b45..74fad1de7 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -84,6 +84,16 @@ def test_cli_doctor_exit_codes( assert cli_main(["doctor", str(bogus)]) == 1 assert "NOT CONFORMING" in capsys.readouterr().out + assert cli_main(["doctor", str(canonical_episode), str(canonical_episode)]) == 0 + both_conforming = capsys.readouterr().out + assert both_conforming.count(str(canonical_episode)) == 2 + assert "NOT CONFORMING" not in both_conforming + + assert cli_main(["doctor", str(canonical_episode), str(bogus)]) == 1 + mixed = capsys.readouterr().out + assert str(canonical_episode) in mixed + assert str(bogus) in mixed + def test_cli_doctor_missing_file_prints_one_line(capsys: pytest.CaptureFixture[str]) -> None: missing = "C:/definitely/not/here.mcap"