Skip to content

Commit 9fc0f0a

Browse files
abhaypaswanclaude
andcommitted
Make every problem standalone, and verify every documented example
Two gaps, both found by actually trying the things the docs claim. Problems were not standalone. The demo ran anywhere, but the tests depended on a root conftest.py, so a problem copied out of the repository could not be tested -- which contradicts the README's claim that each one is self-contained. Each problem now carries its own conftest.py putting its src on sys.path, and the root one is gone. That works for every invocation: from the repo root, from inside the problem, by file, by node id, and from a copy with no repo files present at all. A new CI job proves it by copying each problem to a temp directory and running it there. Documentation was unverified. scripts/check_examples.py now runs every command in every bash fence and checks that every unlabelled block following one really is what the command prints. Timestamps, epochs, temp paths and generated ids are masked so genuinely variable output still gets checked structurally rather than exempted wholesale. It found four things on its first run: - tic-tac-toe --human tracebacked with EOFError on piped stdin instead of ending the game. HumanPlayer now raises PlayerQuit on EOF or Ctrl-C and main exits cleanly; two tests cover it. - payment-gateway's README dropped a transaction reference when pasting. - text-editor's README had hand-realigned columns, so no line matched. - logging-framework's README dropped a context field, abbreviated a path, and elided mid-line. I had originally exempted that problem from output checking because of its timestamps, which is exactly how the drift survived -- masking instead of exempting caught all three. 883 tests, 55 documented commands, 17 problems standalone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 3dca36f commit 9fc0f0a

29 files changed

Lines changed: 768 additions & 59 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ jobs:
3535
- name: Check the generated catalog, README table and patterns doc are current
3636
run: python scripts/build_catalog.py --check
3737

38-
demos:
39-
# Every problem advertises a runnable demo. This makes sure they all still
40-
# run, which the unit tests do not cover.
38+
standalone:
39+
# Each problem claims to be self-contained. This proves it: every problem
40+
# is copied out of the repository, away from the root pyproject.toml, and
41+
# its tests and demo are run with nothing else present.
4142
runs-on: ubuntu-latest
4243
steps:
4344
- uses: actions/checkout@v4
@@ -46,22 +47,56 @@ jobs:
4647
with:
4748
python-version: "3.12"
4849

49-
- name: Run every problem's demo
50+
- run: pip install pytest
51+
52+
- name: Run every problem in isolation
5053
run: |
51-
set -e
54+
set -u
5255
failed=0
56+
workdir=$(mktemp -d)
57+
5358
for problem in problems/*/; do
5459
slug=$(basename "$problem")
55-
printf '%-22s' "$slug"
56-
if (cd "$problem" && timeout 120 python src/main.py > /dev/null 2>&1); then
57-
echo "ok"
60+
cp -R "$problem" "$workdir/$slug"
61+
find "$workdir/$slug" -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null || true
62+
printf '%-24s' "$slug"
63+
64+
if (cd "$workdir/$slug" && timeout 300 python -m pytest -q > /dev/null 2>&1); then
65+
tests=ok
66+
else
67+
tests=TESTS-FAILED
68+
failed=1
69+
fi
70+
71+
if (cd "$workdir/$slug" && timeout 300 python src/main.py > /dev/null 2>&1 < /dev/null); then
72+
demo=ok
5873
else
59-
echo "FAILED"
74+
demo=DEMO-FAILED
6075
failed=1
6176
fi
77+
78+
echo "tests:$tests demo:$demo"
6279
done
80+
6381
exit $failed
6482
83+
examples:
84+
# Every command in the documentation is executed, and every output block
85+
# the docs show is checked against what the command really prints.
86+
runs-on: ubuntu-latest
87+
steps:
88+
- uses: actions/checkout@v4
89+
90+
- uses: actions/setup-python@v5
91+
with:
92+
python-version: "3.12"
93+
cache: pip
94+
95+
- run: pip install -r requirements-dev.txt
96+
97+
- name: Check every documented example
98+
run: python scripts/check_examples.py
99+
65100
lint:
66101
runs-on: ubuntu-latest
67102
steps:

‎CONTRIBUTING.md‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,14 @@ problems/your-problem/
3939
└── test_your_problem.py
4040
```
4141

42-
**The package name must be the slug with underscores.** Every problem's `src/`
43-
goes on `sys.path` at once (see the root `conftest.py`), so a shared package
44-
name like `model` would mean the second problem to define it silently gets the
45-
first one's module.
42+
**The package name must be the slug with underscores.** Each problem's
43+
`conftest.py` puts its own `src/` on `sys.path`, and when the whole suite runs
44+
they are all there at once — so a shared package name like `model` would mean
45+
the second problem to define it silently gets the first one's module.
46+
47+
That `conftest.py` is also what makes a problem **standalone**: copy the
48+
directory out of the repository and `python3 -m pytest` still works inside it.
49+
CI proves this for every problem on every push, so do not remove it.
4650

4751
### What each piece needs
4852

@@ -91,9 +95,17 @@ python3 -m pytest # all green
9195
python3 scripts/validate.py # every problem complete and consistent
9296
python3 scripts/build_catalog.py # refresh catalog.json, the README table, docs/patterns.md
9397
ruff check . && ruff format .
98+
python3 scripts/check_examples.py # every documented command runs, every shown output is real
9499
```
95100

96-
CI runs all of these, plus every problem's demo, on Python 3.10 through 3.13.
101+
CI runs all of these on Python 3.10 through 3.13, plus every problem in
102+
isolation outside the repository.
103+
104+
**If your README shows command output, paste what the command actually
105+
prints.** `check_examples.py` runs every ```bash block and checks every
106+
unlabelled block after one against the real output, so a hand-tidied paste
107+
fails the build. Timestamps, temporary paths and epoch values are masked, and
108+
a trailing `...` on a line means "the rest was trimmed".
97109

98110
Never hand-edit `catalog.json`, the problem table in `README.md`, or
99111
`docs/patterns.md` — all three are generated from the `meta.json` files. The one

‎README.md‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ Or one problem's:
113113
python3 -m pytest problems/chess -v
114114
```
115115

116+
Each problem is genuinely self-contained — copy one out of the repository and
117+
its tests and demo still run, with nothing else present:
118+
119+
```bash
120+
cd problems/chess && python3 -m pytest
121+
```
122+
116123
## How this repository is put together
117124

118125
Each problem lives in `problems/<slug>/` and is self-contained — you can read
@@ -131,6 +138,7 @@ genuinely uses, and how long it should take. Those files validate against
131138
```bash
132139
python3 scripts/validate.py # check every problem is complete and consistent
133140
python3 scripts/build_catalog.py # regenerate catalog.json, the table above, and docs/patterns.md
141+
python3 scripts/check_examples.py # run every command in these docs and verify its output
134142
```
135143

136144
`catalog.json`, the problem table above, and `docs/patterns.md` are all
@@ -169,7 +177,10 @@ the design is meant to handle.
169177
4. Run `python3 scripts/build_catalog.py` to refresh the catalog
170178
5. Open a pull request
171179

172-
CI runs the tests, the validator, and a check that the catalog is up to date.
180+
CI runs four jobs: the test suite on Python 3.10–3.13 plus the validator and
181+
the generated-file check; every problem in isolation, copied out of the
182+
repository; every command in the documentation, with its shown output verified;
183+
and lint.
173184

174185
<p align="right">(<a href="#readme-top">back to top</a>)</p>
175186

‎conftest.py‎

Lines changed: 0 additions & 29 deletions
This file was deleted.

‎problems/atm/conftest.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Put this problem's `src` on sys.path, so its tests run from anywhere.
2+
3+
pytest loads the nearest conftest.py before importing the test modules beneath
4+
it, which is what makes this work whether you run:
5+
6+
pytest # from this directory
7+
pytest problems/atm # from the repo root
8+
pytest tests/test_atm.py # a single file
9+
10+
It also means the directory is genuinely self-contained: copy it out of the
11+
repository and the tests still run, with no repo-level setup to remember.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import sys
17+
from pathlib import Path
18+
19+
SRC = Path(__file__).resolve().parent / "src"
20+
21+
if str(SRC) not in sys.path:
22+
sys.path.insert(0, str(SRC))

‎problems/chat-room/conftest.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Put this problem's `src` on sys.path, so its tests run from anywhere.
2+
3+
pytest loads the nearest conftest.py before importing the test modules beneath
4+
it, which is what makes this work whether you run:
5+
6+
pytest # from this directory
7+
pytest problems/chat-room # from the repo root
8+
pytest tests/test_chat_room.py # a single file
9+
10+
It also means the directory is genuinely self-contained: copy it out of the
11+
repository and the tests still run, with no repo-level setup to remember.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import sys
17+
from pathlib import Path
18+
19+
SRC = Path(__file__).resolve().parent / "src"
20+
21+
if str(SRC) not in sys.path:
22+
sys.path.insert(0, str(SRC))

‎problems/chess/conftest.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Put this problem's `src` on sys.path, so its tests run from anywhere.
2+
3+
pytest loads the nearest conftest.py before importing the test modules beneath
4+
it, which is what makes this work whether you run:
5+
6+
pytest # from this directory
7+
pytest problems/chess # from the repo root
8+
pytest tests/test_chess.py # a single file
9+
10+
It also means the directory is genuinely self-contained: copy it out of the
11+
repository and the tests still run, with no repo-level setup to remember.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import sys
17+
from pathlib import Path
18+
19+
SRC = Path(__file__).resolve().parent / "src"
20+
21+
if str(SRC) not in sys.path:
22+
sys.path.insert(0, str(SRC))
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Put this problem's `src` on sys.path, so its tests run from anywhere.
2+
3+
pytest loads the nearest conftest.py before importing the test modules beneath
4+
it, which is what makes this work whether you run:
5+
6+
pytest # from this directory
7+
pytest problems/elevator-system # from the repo root
8+
pytest tests/test_elevator_system.py # a single file
9+
10+
It also means the directory is genuinely self-contained: copy it out of the
11+
repository and the tests still run, with no repo-level setup to remember.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import sys
17+
from pathlib import Path
18+
19+
SRC = Path(__file__).resolve().parent / "src"
20+
21+
if str(SRC) not in sys.path:
22+
sys.path.insert(0, str(SRC))

‎problems/file-system/conftest.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Put this problem's `src` on sys.path, so its tests run from anywhere.
2+
3+
pytest loads the nearest conftest.py before importing the test modules beneath
4+
it, which is what makes this work whether you run:
5+
6+
pytest # from this directory
7+
pytest problems/file-system # from the repo root
8+
pytest tests/test_file_system.py # a single file
9+
10+
It also means the directory is genuinely self-contained: copy it out of the
11+
repository and the tests still run, with no repo-level setup to remember.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import sys
17+
from pathlib import Path
18+
19+
SRC = Path(__file__).resolve().parent / "src"
20+
21+
if str(SRC) not in sys.path:
22+
sys.path.insert(0, str(SRC))

‎problems/logging-framework/README.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,13 +207,14 @@ cd problems/logging-framework && python3 src/main.py
207207
```
208208

209209
```
210-
2026-09-19 05:35:39 [INFO] app: GET /orders request_id=req-8814 user=alice
211-
2026-09-19 05:35:39 [ERROR] app: Upstream timeout request_id=req-8814 status=504 upstream=payments
210+
2026-09-19 06:27:43 [INFO] app: GET /orders request_id=req-8814 user=alice
211+
2026-09-19 06:27:43 [ERROR] app: Upstream timeout request_id=req-8814 status=504 upstream=payments user=alice
212212
213213
7 records passed through the chain.
214-
2 of them reached errors.jsonl:
214+
2 of them reached /tmp/lld-logging-a1b2c3/errors.jsonl:
215215
216-
{"level": "ERROR", "logger": "app", "message": "Upstream timeout", "status": 504, ...}
216+
{"level": "ERROR", "logger": "app", "message": "Upstream timeout", "request_id": "req-8814", "status": 504, "timestamp": 1789799263.605953, "upstream": "payments", "user": "alice"}
217+
{"level": "ERROR", "logger": "db", "message": "Connection pool exhausted", "size": 20, "timestamp": 1789799263.6060832, "waiting": 7}
217218
```
218219

219220
## Tests

0 commit comments

Comments
 (0)