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>
4.4 KiB
4.4 KiB
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 PATHflag, mutually exclusive with positionalurlargs.urlspositional changednargs="+"→nargs="*". New_load_urls()helper (same format assimple.py'sload_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 afterparser.parse_args(), inside the sametry/except _ArgExitblock: both-given and neither-given are usage errors viaparser.error(); missing/unreadable/empty file are usage errors via directstderr.write()+return EXIT_USAGE(mirroring the existing--timeoutinvalid-value handling style already in the file).hxprobe/tests/test_cli.py: newTestCLIFileInputclass, 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 mirroringpython/configs/'s exact set. Each header comment states an "Expected exit code" that was verified by actually running the fixture throughhxprobe -f ...during implementation (not assumed from thesimple.pyoriginals, whose blanket 0/1 exit scheme is fundamentally different from hxprobe's per-failure-class 0–6 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'sadd_mutually_exclusive_group— a variadic positional (nargs="*") doesn't mix cleanly with argparse's built-in mutually-exclusive-group machinery. Manual checks afterparser.parse_args()(still inside the sametry/except _ArgExit, usingparser.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 directstderr.write()+return EXIT_USAGEpattern already established for--timeoutparsing 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.pyoriginals didn't need:http-errors.txtandmixed.txtboth needed--failadded to their demo command (hxprobe treats 4xx as success without it, unlikesimple.pywhich always raises onHTTPError) to actually demonstrate a failure — without--failboth 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 agreppipe, 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)