Files
http-latency-prober/docs/summaries/2026-07-02-11-14-hxprobe-file-input.md
Jan Novak f487a4b1bd docs: hxprobe plans, summaries, explanations, usage, and changelog
Plans (docs/plans/):
- 2026-07-01-23-47-py-hxprobe-httpx.md — initial httpx probe design
- 2026-07-02-09-32 through 14-05 — standalone project, toolchain, usage
  doc + Makefile, file input (-f), simplification pass, run-summary footer

Summaries (docs/summaries/): one per completed feature, recording what
was actually built, deviations from the plan, and verification steps

Explanations (docs/explanations/): two deep-dives written during review —
hxprobe concurrency model and worst-exit-code + render-loop analysis

Usage (docs/usage/hxprobe.md): overview with pointer to hxprobe/USAGE.md
for the full runnable reference

Walkthrough (docs/py-latprobe-walkthrough.md): narrative tour of the
latprobe Python package for interview / code-review context

CHANGELOG.md: entries for all hxprobe features (toolchain, usage doc,
file input, simplification, run-summary footer)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 13:22:59 +02:00

4.4 KiB
Raw Permalink Blame History

Summary: hxprobe reads target URLs from a file

Plan: docs/plans/2026-07-02-11-14-hxprobe-file-input.md

What was built

  • hxprobe/hxprobe/cli.py: new -f/--file PATH flag, mutually exclusive with positional url args. urls positional changed nargs="+"nargs="*". New _load_urls() helper (same format as simple.py's load_sites(): one URL per line, # comments, blank lines skipped, first token per line) — reimplemented locally rather than imported, keeping hxprobe's "no imports outside its own directory" rule intact. Validation added right after parser.parse_args(), inside the same try/except _ArgExit block: both-given and neither-given are usage errors via parser.error(); missing/unreadable/empty file are usage errors via direct stderr.write() + return EXIT_USAGE (mirroring the existing --timeout invalid-value handling style already in the file).
  • hxprobe/tests/test_cli.py: new TestCLIFileInput class, 5 tests — reads URLs from a temp file successfully, missing file, empty file, both-sources error, neither-given error.
  • hxprobe/configs/*.txt: 7 new fixtures mirroring python/configs/'s exact set. Each header comment states an "Expected exit code" that was verified by actually running the fixture through hxprobe -f ... during implementation (not assumed from the simple.py originals, whose blanket 0/1 exit scheme is fundamentally different from hxprobe's per-failure-class 06 worst-code-wins scheme).
  • hxprobe/USAGE.md: new "Reading URLs from a file (-f)" section, placed after "Multiple URLs", with real captured output for the successful case, the mutually-exclusive error case, and one failure fixture (dns-failure.txt), plus a table listing all 7 fixtures and their expected exit codes.

Key design decisions

  • Manual post-parse validation instead of argparse's add_mutually_exclusive_group — a variadic positional (nargs="*") doesn't mix cleanly with argparse's built-in mutually-exclusive-group machinery. Manual checks after parser.parse_args() (still inside the same try/except _ArgExit, using parser.error()) give the same usage-error behavior with full control over the message text.
  • File I/O errors don't go through parser.error() — they use the same direct stderr.write() + return EXIT_USAGE pattern already established for --timeout parsing failures, since they're discovered after parsing succeeds, not during it.
  • Every fixture's exit code was verified live, not assumed. Two required real judgment calls the simple.py originals didn't need: http-errors.txt and mixed.txt both needed --fail added to their demo command (hxprobe treats 4xx as success without it, unlike simple.py which always raises on HTTPError) to actually demonstrate a failure — without --fail both would silently show exit 0.

Deviations from the plan

None of substance. The plan anticipated needing to verify exit codes live rather than assume them; that anticipation paid off exactly as expected for http-errors.txt/mixed.txt (needed --fail added) and timeout.txt (needed --timeout 2s added to keep the demo fast, and a note added about this sandbox occasionally short-circuiting one of the two timeout targets to an immediate "connection refused" — observed directly: in this session's test run, 10.255.255.1 genuinely timed out; in an earlier, unrelated test earlier in the session it instead got refused immediately. The fixture keeps both targets so at least one demonstrates the real timeout path regardless).

Verification

  • all-ok.txt → exit 0, dns-failure.txt → 2, connection-refused.txt → 3, tls-errors.txt → 5, timeout.txt (with --timeout 2s) → 4, http-errors.txt (with --fail) → 6, mixed.txt (with --fail) → 6 — all confirmed via real $? checks (not through a grep pipe, which masks the real exit code — caught and corrected this during testing)
  • uv run pytest tests/test_cli.py -m "not integration" — 16/16 pass (11 pre-existing + 5 new)
  • uv run ruff check . / uv run ruff format --check . — clean
  • Both-sources-given and missing-file error messages verified against the doc's pasted output, including the usage: block that's actually printed (an early draft of the doc omitted it — caught on review)