Files
http-latency-prober/docs/plans/2026-07-02-12-05-hxprobe-simplification.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

175 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# hxprobe simplification plan
## Context
The `hxprobe` Python package works and is well-tested, but it carries several
pieces of **speculative scaffolding** — generality added for things that "might
happen later" — plus some **redundant state** and **duplicated logic**. The
user's explicit directive: keep the code simple, with no unnecessary
abstractions or scaffolding for hypothetical future features.
This plan removes that overhead without changing any observable behavior. All
existing tests in `tests/test_cli.py` and `tests/test_probe.py` must continue
to pass unchanged (they are the behavioral contract). Net effect: ~4050 fewer
lines, fewer moving parts, no new abstractions.
The trigger was a question about the `_Parser._print_message`/`print_help`/
`print_usage` overrides, whose `file=` parameter is accepted but never used —
the classic "kept for a future that never came" smell. Investigation showed the
whole `_Parser` subclass re-implements behavior the standard library already
provides.
---
## Changes
### 1. Delete the `_Parser` subclass — use stdlib stream redirection
**File:** `hxprobe/hxprobe/cli.py` (lines 6094, and the `run()` wiring ~375455)
The `_Parser` class + `_ArgExit` exception (~32 lines) exist only to (a) route
argparse's help/usage/error output to the injected `stdout`/`stderr` streams and
(b) raise instead of calling `sys.exit()`. The standard library already does
both:
- `argparse.ArgumentParser` writes help to `sys.stdout` and usage/errors to
`sys.stderr` by default, and already raises `SystemExit` (not a hard exit) —
so it is already testable.
- `contextlib.redirect_stdout(stdout)` / `redirect_stderr(stderr)` patch the
streams argparse writes to.
**Do:**
- Remove `class _ArgExit`, `class _Parser`, and all four overrides
(`_print_message`, `print_help`, `print_usage`, `error`, `exit`).
- In `run()`, build a plain `argparse.ArgumentParser(prog="hxprobe", ...)` (drop
the `out=`/`err=` kwargs).
- Wrap the parse + the two manual validations in redirection and catch
`SystemExit`:
```python
import contextlib
...
try:
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
ns = parser.parse_args(args)
if ns.urls and ns.file:
parser.error("cannot combine positional url arguments with -f/--file")
if not ns.urls and not ns.file:
parser.error("no URLs given (pass as arguments or with -f/--file)")
except SystemExit as exc:
return EXIT_OK if not exc.code else EXIT_USAGE
```
**Behavior parity (verified against tests):**
- `-h` → argparse prints help to `stdout`, raises `SystemExit(0)` → returns
`EXIT_OK`. (`test_help_shows_hxprobe_prog_name`, `test_help_lists_protocol_flags`)
- `parser.error(...)` → prints `prog: error: msg` + usage to `stderr`, raises
`SystemExit(2)` → mapped to `EXIT_USAGE`. (`test_*_is_usage_error`)
- Note: stdlib `error()` exits with code 2; we normalise any non-zero parse exit
to `EXIT_USAGE` (1) at the single catch site, replacing the per-method code
baked into the old override.
Only the parse/validate block needs redirection; the rest of `run()` keeps
writing directly to the passed `stdout`/`stderr`.
### 2. Trim `_TimingStream.get_extra_info` to the branch that's actually used
**File:** `hxprobe/hxprobe/probe.py` (lines 262275)
`server_addr` and `client_addr` are only ever queried by `httpx/_main.py` (the
`httpx` CLI command), never by the request path hxprobe drives — verified by
grepping the installed `httpcore`/`httpx`. The only branch httpcore's sync
connection path calls is `ssl_object` (and `is_readable`, which we intentionally
leave unhandled → `None`).
**Do:** reduce the method to:
```python
def get_extra_info(self, info: str):
if info == "ssl_object" and isinstance(self._sock, ssl.SSLSocket):
return self._sock
return None
```
### 3. Drop the `_load_urls` "future annotations" scaffolding
**File:** `hxprobe/hxprobe/cli.py` (lines 358369)
The `line.split()[0]` + docstring ("forward-compatible with future
'url key=value' annotations") is scaffolding for a feature that doesn't exist.
Use the stripped line directly and simplify the docstring to describe what it
actually does (one URL per line, `#` comments and blank lines skipped).
`test_reads_urls_from_file` (comments + blanks) still passes.
### 4. Remove redundant `_set` flags in `_Trace`
**File:** `hxprobe/hxprobe/probe.py` (lines 126191)
`_dns_set`, `_connect_set`, `_tls_set` duplicate information already carried by
`Phase.present` on the corresponding `self.dns` / `self.connect` / `self.tls`.
The initial `Phase()` has `present=False`, so the first-hop-wins guard is
identical.
**Do:** delete the three boolean fields; replace each guard, e.g.
`if not self._dns_set:``if not self.dns.present:` (same for connect/tls).
Keeps the redirect first-hop-wins semantics documented at probe.py:114123.
### 5. De-duplicate the failure counting logic
**File:** `hxprobe/hxprobe/cli.py`
The identical "dedupe failures into ordered (phase, message, count)" loop appears
twice: `_print_failure_summary` (lines 242258) and `_build_json_entry`
(lines 314325). Extract one helper next to the other rendering helpers:
```python
def _summarize_failures(failed: list[Result]) -> list[tuple[str, str, int]]:
counts: dict[tuple[str, str], int] = {}
order: list[tuple[str, str]] = []
for r in failed:
key = (r.fail_phase, str(r.err))
if key not in counts:
order.append(key)
counts[key] = 0
counts[key] += 1
return [(ph, msg, counts[(ph, msg)]) for ph, msg in order]
```
Rewrite both call sites to consume it (text side keeps the `1 ×` vs `N ×`
formatting; JSON side maps to `{"phase", "count", "message"}`).
---
## Explicitly NOT changing (considered, kept)
- **The custom httpcore backend** (`_TimingBackend`/`_TimingStream`/
`_TimingTransport`) — this *is* the tool's reason to exist (splitting DNS/TCP,
timing TLS). Not scaffolding.
- **`ThreadPoolExecutor` concurrency** — backs the shipped `-c/--concurrency`
and multi-URL/`-f` features. Real, not speculative.
- **Explicit per-phase dataclass fields** in `probe.py`/`aggregate.py` — a loop
would be shorter but less readable; explicit is clearer and matches the text/
JSON renderers. Leave as-is.
- **`CertInfo.sans`** — not shown in the text block but *is* emitted in JSON
verbose output; it's a real feature, not dead.
---
## Verification
1. **Unit/CLI tests (primary contract):**
```
cd hxprobe && .venv/bin/python -m pytest tests/test_cli.py tests/test_probe.py -q
```
All must pass with no edits to the test files. These already cover: `-h`
help→stdout + exit 0, the four usage errors→stderr + exit 1, DNS/connect/
timeout failures, `--fail`, `--json`, redirects, `--no-http2`,
`--no-follow-redirects`, `-f` file input, and verbose/cert/TLS detail.
2. **Lint:** `cd hxprobe && .venv/bin/ruff check hxprobe/`
3. **Manual smoke (help + error routing, since #1 rewrites that path):**
```
.venv/bin/python -m hxprobe -h # help on stdout, exit 0
.venv/bin/python -m hxprobe # "no URLs given" on stderr, exit 1
.venv/bin/python -m hxprobe https://example.com -v # phases + verbose block
```
4. **Per-CLAUDE.md project conventions:** after implementing, add a
`docs/summaries/<yyyy-mm-dd-hh-mm>-hxprobe-simplification.md` summary and a
`CHANGELOG.md` entry. (No `docs/usage/` change — behavior is unchanged.)