docs: add usage-phases.md beside usage-simple.md in python/configs/

- Rename python/configs/usage.md → usage-simple.md so both scripts have a
  parallel runnable reference file in the same directory
- Add python/configs/usage-phases.md: runnable sh commands + real expected
  output for phases.py against every error-path config, including a note on
  the key difference from simple.py (HTTP 4xx exits 0 in phases.py vs 1 in
  simple.py because phases.py completes at the network level)
- Update doc cross-links in docs/usage/py-simple.md and py-phases.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 12:11:53 +02:00
parent f609d8124f
commit 16fff2f964
4 changed files with 352 additions and 13 deletions

View File

@@ -124,18 +124,10 @@ Multiple URLs are separated by a blank line, matching Go's text output style.
## Using the error-path example configs
The `python/configs/` files from `simple.py` work identically with `phases.py`.
Compare the error handling between the two tools:
```sh
# DNS failure
python phases.py configs/dns-failure.txt
# Connection refused — note DNS phase is present, TCP is partial
python phases.py configs/connection-refused.txt
# TLS errors — all three phases up to TLS are present
python phases.py configs/tls-errors.txt
```
See [`python/configs/usage-phases.md`](../../python/configs/usage-phases.md) for
runnable shell commands and expected output for every config file, including a
side-by-side comparison of how `phases.py` and `simple.py` differ on HTTP 4xx
responses.
## Limitations

View File

@@ -64,7 +64,7 @@ Exit code: `1`
failure class (DNS, connection-refused, timeout, TLS-cert errors, HTTP 4xx/5xx,
and a mixed scenario). Each file includes a comment explaining what it exercises.
See [`python/configs/usage.md`](../../python/configs/usage.md) for the runnable
See [`python/configs/usage-simple.md`](../../python/configs/usage-simple.md) for the runnable
shell commands and expected output for each file.
## Limitations

View File

@@ -0,0 +1,347 @@
# `phases.py` — Example Config Files
Each `.txt` file in this directory targets a distinct error path so you can
observe `phases.py`'s per-phase behaviour for every failure class. Run them
from the `python/` directory.
A key difference from `simple.py`: on failure, `phases.py` shows the phases
that *did* complete before the error — giving you partial timing up to the
point of failure.
---
## Direct URL (no config file)
`phases.py` also accepts bare URLs as positional arguments:
```sh
python phases.py https://example.com
```
Expected output:
```
https://example.com (200)
DNS lookup : 14.19 ms
TCP connect : 9.60 ms
TLS handshake : 16.44 ms
Server (TTFB) : 70.29 ms
Transfer : 0.13 ms
─────────────────────────────
Total : 118.14 ms
```
For `http://` URLs the TLS row is omitted:
```sh
python phases.py http://example.com
```
```
http://example.com (200)
DNS lookup : 13.61 ms
TCP connect : 10.56 ms
Server (TTFB) : 14.51 ms
Transfer : 0.08 ms
─────────────────────────────
Total : 38.91 ms
```
---
## `all-ok.txt` — all sites respond successfully
```sh
python phases.py configs/all-ok.txt
echo "exit: $?"
```
Expected output (latencies vary; multiple results separated by a blank line):
```
https://example.com (200)
DNS lookup : 3.82 ms
TCP connect : 10.04 ms
TLS handshake : 17.12 ms
Server (TTFB) : 28.59 ms
Transfer : 0.09 ms
─────────────────────────────
Total : 66.16 ms
https://www.google.com (200)
DNS lookup : 14.31 ms
TCP connect : 8.47 ms
TLS handshake : 24.61 ms
Server (TTFB) : 95.24 ms
Transfer : 33.96 ms
─────────────────────────────
Total : 182.67 ms
https://www.iana.org (200)
DNS lookup : 23.19 ms
TCP connect : 9.77 ms
TLS handshake : 14.51 ms
Server (TTFB) : 68.53 ms
Transfer : 0.20 ms
─────────────────────────────
Total : 125.93 ms
exit: 0
```
---
## `dns-failure.txt` — DNS resolution fails
DNS fails immediately — no TCP or TLS phases run. Only `Total` is shown
alongside the error (DNS lookup itself is not shown since it never completed).
```sh
python phases.py configs/dns-failure.txt
echo "exit: $?"
```
Expected output:
```
https://this-host-does-not-exist.invalid (FAILED)
─────────────────────────────
Total : 2.67 ms
✗ dns: [Errno 8] nodename nor servname provided, or not known
http://no.such.host.invalid (FAILED)
─────────────────────────────
Total : 0.65 ms
✗ dns: [Errno 8] nodename nor servname provided, or not known
exit: 1
```
---
## `connection-refused.txt` — TCP connection refused
DNS succeeds (loopback resolves instantly), TCP connect fails immediately.
Both DNS and TCP phases are shown as partial timing.
```sh
python phases.py configs/connection-refused.txt
echo "exit: $?"
```
Expected output:
```
http://127.0.0.1:9999 (FAILED)
DNS lookup : 1.03 ms
TCP connect : 0.82 ms
─────────────────────────────
Total : 1.88 ms
✗ connect: [Errno 61] Connection refused
http://127.0.0.1:19999 (FAILED)
DNS lookup : 0.01 ms
TCP connect : 0.40 ms
─────────────────────────────
Total : 0.41 ms
✗ connect: [Errno 61] Connection refused
exit: 1
```
---
## `timeout.txt` — connect hangs until timeout
DNS resolves, TCP SYN is sent but never gets a reply. `phases.py` waits the
full **10-second** timeout per host before printing FAIL.
> ⚠️ This run takes approximately **20 seconds** to complete.
```sh
python phases.py configs/timeout.txt
echo "exit: $?"
```
Expected output (after ~20 s):
```
http://10.255.255.1/ (FAILED)
DNS lookup : x.xx ms
TCP connect : 10000.xx ms
─────────────────────────────
Total : 10000.xx ms
✗ timeout: timed out
http://192.0.2.1/ (FAILED)
DNS lookup : x.xx ms
TCP connect : 10000.xx ms
─────────────────────────────
Total : 10000.xx ms
✗ timeout: timed out
exit: 1
```
---
## `tls-errors.txt` — TLS certificate errors
DNS and TCP complete; TLS handshake is attempted and fails. All three phases
are shown with real timings even though the request fails.
> **Note:** badssl.com occasionally resets the connection mid-handshake. The
> error may read `[Errno 54] Connection reset by peer` instead of
> `CERTIFICATE_VERIFY_FAILED` — both are classified as `✗ tls`.
```sh
python phases.py configs/tls-errors.txt
echo "exit: $?"
```
Expected output:
```
https://expired.badssl.com/ (FAILED)
DNS lookup : 15.39 ms
TCP connect : 124.98 ms
TLS handshake : 326.52 ms
─────────────────────────────
Total : 479.50 ms
✗ tls: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired …
https://self-signed.badssl.com/ (FAILED)
DNS lookup : 2.06 ms
TCP connect : 127.74 ms
TLS handshake : 963.72 ms
─────────────────────────────
Total : 1103.21 ms
✗ tls: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate …
https://incomplete-chain.badssl.com/ (FAILED)
DNS lookup : 2.11 ms
TCP connect : 2128.40 ms
TLS handshake : 381.42 ms
─────────────────────────────
Total : 2532.73 ms
✗ tls: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer …
exit: 1
```
---
## `http-errors.txt` — HTTP 4xx / 5xx status codes
Unlike `simple.py` (which treats 4xx as FAIL), `phases.py` completes all
phases and shows the raw HTTP status code. The request succeeded at the network
level — you get full phase breakdown plus the 404.
```sh
python phases.py configs/http-errors.txt
echo "exit: $?"
```
Expected output:
```
https://www.google.com/this-page-does-not-exist-at-all-1234567890 (404)
DNS lookup : 4.12 ms
TCP connect : 10.56 ms
TLS handshake : 25.03 ms
Server (TTFB) : 124.26 ms
Transfer : 0.19 ms
─────────────────────────────
Total : 172.60 ms
https://github.com/this-repo-does-not-exist-abcxyz123/no-way (404)
DNS lookup : 16.56 ms
TCP connect : 22.86 ms
TLS handshake : 24.47 ms
Server (TTFB) : 275.52 ms
Transfer : 90.08 ms
─────────────────────────────
Total : 441.17 ms
https://www.iana.org/this-page-does-not-exist-either (404)
DNS lookup : 2.48 ms
TCP connect : 10.03 ms
TLS handshake : 16.10 ms
Server (TTFB) : 75.14 ms
Transfer : 0.46 ms
─────────────────────────────
Total : 115.90 ms
exit: 0
```
> **Compare with `simple.py`:** the same URLs return `FAIL (HTTP Error 404: Not Found)`
> in `simple.py` (because `urlopen` raises for 4xx) but `(404)` with a full
> phase breakdown in `phases.py` (because the request completed at the network level).
> Exit code is `0` here vs `1` in `simple.py`.
---
## `mixed.txt` — one of each (no timeout)
```sh
python phases.py configs/mixed.txt
echo "exit: $?"
```
Expected output:
```
https://example.com (200)
DNS lookup : 4.88 ms
TCP connect : 11.79 ms
TLS handshake : 17.17 ms
Server (TTFB) : 67.79 ms
Transfer : 0.09 ms
─────────────────────────────
Total : 108.72 ms
https://no.such.host.invalid (FAILED)
─────────────────────────────
Total : 1.46 ms
✗ dns: [Errno 8] nodename nor servname provided, or not known
http://127.0.0.1:9999 (FAILED)
DNS lookup : 0.01 ms
TCP connect : 0.82 ms
─────────────────────────────
Total : 0.85 ms
✗ connect: [Errno 61] Connection refused
https://self-signed.badssl.com/ (FAILED)
DNS lookup : 1.54 ms
TCP connect : 1129.98 ms
TLS handshake : 296.93 ms
─────────────────────────────
Total : 1438.84 ms
✗ tls: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate …
https://github.com/this-repo-does-not-exist-abcxyz123/no-way (404)
DNS lookup : 13.48 ms
TCP connect : 22.98 ms
TLS handshake : 24.58 ms
Server (TTFB) : 324.54 ms
Transfer : 95.05 ms
─────────────────────────────
Total : 495.53 ms
exit: 1
```
Note how the GitHub 404 shows all five phases (network succeeded) while the
DNS, TCP, and TLS failures each show only the phases that ran.
---
## Run all configs
```sh
# Quick sweep — skips the slow timeout config
for f in configs/*.txt; do
[[ "$f" == *timeout* ]] && continue
echo
echo "=== $f ==="
python phases.py "$f"
echo "exit: $?"
done
```
```sh
# Full sweep — includes timeout (~20 s extra)
for f in configs/*.txt; do
echo
echo "=== $f ==="
python phases.py "$f"
echo "exit: $?"
done
```