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:
@@ -124,18 +124,10 @@ Multiple URLs are separated by a blank line, matching Go's text output style.
|
|||||||
## Using the error-path example configs
|
## Using the error-path example configs
|
||||||
|
|
||||||
The `python/configs/` files from `simple.py` work identically with `phases.py`.
|
The `python/configs/` files from `simple.py` work identically with `phases.py`.
|
||||||
Compare the error handling between the two tools:
|
See [`python/configs/usage-phases.md`](../../python/configs/usage-phases.md) for
|
||||||
|
runnable shell commands and expected output for every config file, including a
|
||||||
```sh
|
side-by-side comparison of how `phases.py` and `simple.py` differ on HTTP 4xx
|
||||||
# DNS failure
|
responses.
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ Exit code: `1`
|
|||||||
failure class (DNS, connection-refused, timeout, TLS-cert errors, HTTP 4xx/5xx,
|
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.
|
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.
|
shell commands and expected output for each file.
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|||||||
347
python/configs/usage-phases.md
Normal file
347
python/configs/usage-phases.md
Normal 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
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user