Skip to content

Commit cd7ac97

Browse files
author
Gemini
committed
Add Zig TDD ledger
1 parent ce7272d commit cd7ac97

6 files changed

Lines changed: 638 additions & 3 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
- No test without proof: every test must call the harness `requireProof` with a non-empty statement + argument (PDD = Proof-Driven Development).
2929
- Use the shared harness in `test/harness.zig` for proof validation and common assertions; import it as `const harness = @import("harness.zig");`.
3030
- For constraint changes, include both the failing graph and a passing counterpart that demonstrates the fix.
31+
- Record red/green transitions in `tdd_ledger.zig`; do not finish a change with any ledger entries still red.
3132

3233
## Commit & Pull Request Guidelines
3334
- Commit messages: imperative, present-tense subject lines (`Add KEM catalog entry`, `Fix nonce discipline check`) with concise detail in the body when needed.

MANIFEST.SHA256

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -847,14 +847,15 @@ fef62683c99ae4bd05790cb5e5307c9b062f94b69ff014a547ace69b9940b6ee .zig-local/z/0
847847
d16bcc32c2766668369593579f602bff13d058ac628d684d845a04f814a7a0a8 .zig-local/z/2000b1773a7d0f28b7e29dd4e9094e70
848848
d559b494bf520a793c0109388e9e97ff8980fc0c40303e68ee6bf0a054dfd2b5 .zig-local/z/53d8da468c12563c8ba398391b5e75e7
849849
142db3c45c7f3ad35704fe0e904fdb14d7efb4fdb29156082824fb7f08132b55 .zig-local/z/facb694da59030723d597fdf5d2a885f
850-
c9c4e77dcfbf0b47932cef4f33e117efcb2d144b458e4ffc8461f460064c4c3c AGENTS.md
850+
74cb6da6c26135d7bf2e973d14e49ce225d3878cb006226fb989e5503274e888 AGENTS.md
851851
4d9f8a6b3b4fb4b9d19daf8b3ee9001e71e238cc5b4e031ac90c5e79426c04a0 Makefile
852852
1bab15ea62034dfc6e58bb7c00b3108636e2a4b6061fa78667b65b8c8ce5d7c3 README.md
853-
7a5ee410c54c18493d5e0b50855f544d21ed5bbcd815530a46c9ab8524dda206 build.zig
853+
990ae8b36e639456a51659046d05fabe8a25f2e9092448a529b5f3662ed0826d build.zig
854854
967e673e794fbacb228c9435945ffb8d01f160da2a39083f4992b350931cc18d constraint-test-generator.zig
855855
3f7554651f7d0b7af01d5d0281b166428d84884303e87e14d5f283a4df112093 docs/QUICKSTART.md
856856
a823bf653352e4d285e3009d7fb889e43fe114c6865309049aa2de48b22afb7c scripts/make_manifest.py
857857
bb0408164c6a9b495b5bfbbae2af866e6549b3a7abae02ba576964c898003a75 scripts/run_tests.sh
858+
79b32c6d6d704a1b952152842af3c1f31bc5f947170c4daccc41a832218a63a1 scripts/tdd_ledger.py
858859
3e351409acb115bcf541a084b95d6a657cee838e96dd980dc434ed383dc6f164 scripts/verify_manifest.py
859860
402ba7d517eed6995f3861dd74ad896200d5ee0c6245390ef13283e13a8b3895 src/catalog/aeads.zig
860861
41b56c2f2064b4ebff20e0a50575af07f9738768b0c0d360ec81e405d5dd48ff src/catalog/kdfs.zig
@@ -886,6 +887,7 @@ a3fe7655ea9e15a921ff1bb2f2325a6c52e0f35e980c6edfabebf8153f0dd15c src/types/assu
886887
ebf9a280e5ef51fc2b70fc19cc40d3550f7be8e14582030388d11701467ea472 src/types/ground.zig
887888
e4f9f2949c066f0219785c75d068680662ea5614e01e031b84641a716981ff14 src/types/mod.zig
888889
c642d8f70fc7f968c998c55f53dc68860b602d6929057cad37598c9689ffaed5 src/types/notions.zig
889-
956c6012d4c738186aa8a56f58319805fe8907566e74d2b4013c4f45b15ee161 test/all_tests.zig
890+
819650821fa148dfbc5c9912bd8646bd6e85a22f06084f11d6704f048a91ebdf tdd_ledger.zig
891+
62e229b73f922062b1161942a742024e0eb7c83f9a618de097e0ecafbbaded6a test/all_tests.zig
890892
aaae2b07fe6273708caa76b6de01414d3bed7e500bc12be0be54579c4a5459db test/constraint_tests.zig
891893
27d1c490ba30187a88322115a87a60261bda3c57d458507bc53a2f45290bdf67 test/harness.zig

build.zig

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ pub fn build(b: *std.Build) void {
4040
.target = target,
4141
.optimize = optimize,
4242
});
43+
tests.root_module.addAnonymousImport("tdd_ledger", .{
44+
.root_source_file = b.path("tdd_ledger.zig"),
45+
.target = target,
46+
.optimize = optimize,
47+
});
48+
tests.root_module.link_libc = true;
49+
tests.root_module.linkSystemLibrary("sqlite3", .{});
4350

4451
const test_step = b.step("test", "Run unit tests");
4552
const run_tests = b.addRunArtifact(tests);

scripts/tdd_ledger.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
#!/usr/bin/env python3
2+
"""Standalone TDD ledger backed by SQLite.
3+
4+
Drop this file into any repo and run `python3 tdd_ledger.py --help`.
5+
The ledger enforces a red->green workflow by recording failing tests first.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import argparse
11+
import os
12+
import sqlite3
13+
import subprocess
14+
import sys
15+
from typing import Iterable, Optional
16+
17+
18+
def parse_args() -> argparse.Namespace:
19+
parser = argparse.ArgumentParser(description="TDD ledger backed by SQLite.")
20+
parser.add_argument(
21+
"--db",
22+
default=os.environ.get("TDD_LEDGER_DB", ".tdd_ledger.sqlite"),
23+
help="Path to sqlite db (default: .tdd_ledger.sqlite or TDD_LEDGER_DB).",
24+
)
25+
sub = parser.add_subparsers(dest="command", required=True)
26+
27+
sub.add_parser("init", help="Initialize the ledger database.")
28+
29+
red = sub.add_parser("red", help="Record a failing test.")
30+
red.add_argument("test", help="Test name or identifier.")
31+
red.add_argument("--note", default=None, help="Optional note.")
32+
red.add_argument("--run", nargs=argparse.REMAINDER, help="Command to run (expects failure).")
33+
34+
green = sub.add_parser("green", help="Record a passing test.")
35+
green.add_argument("test", help="Test name or identifier.")
36+
green.add_argument("--note", default=None, help="Optional note.")
37+
green.add_argument("--run", nargs=argparse.REMAINDER, help="Command to run (expects success).")
38+
39+
sub.add_parser("require-green", help="Exit non-zero if any tests are still red.")
40+
sub.add_parser("status", help="Print status summary.")
41+
42+
return parser.parse_args()
43+
44+
45+
def connect(db_path: str) -> sqlite3.Connection:
46+
conn = sqlite3.connect(db_path)
47+
conn.execute("PRAGMA journal_mode = WAL;")
48+
conn.execute("PRAGMA foreign_keys = ON;")
49+
return conn
50+
51+
52+
def init_db(conn: sqlite3.Connection) -> None:
53+
conn.execute(
54+
"""
55+
CREATE TABLE IF NOT EXISTS ledger (
56+
test_name TEXT PRIMARY KEY,
57+
status TEXT NOT NULL CHECK(status IN ('red','green')),
58+
note TEXT,
59+
last_command TEXT,
60+
last_exit_code INTEGER,
61+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
62+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
63+
);
64+
"""
65+
)
66+
conn.execute(
67+
"""
68+
CREATE TABLE IF NOT EXISTS events (
69+
id INTEGER PRIMARY KEY AUTOINCREMENT,
70+
test_name TEXT NOT NULL,
71+
status TEXT NOT NULL CHECK(status IN ('red','green')),
72+
note TEXT,
73+
command TEXT,
74+
exit_code INTEGER,
75+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
76+
);
77+
"""
78+
)
79+
conn.commit()
80+
81+
82+
def normalize_run_args(run_args: Optional[Iterable[str]]) -> Optional[list[str]]:
83+
if not run_args:
84+
return None
85+
args = list(run_args)
86+
if args and args[0] == "--":
87+
args = args[1:]
88+
if not args:
89+
return None
90+
return args
91+
92+
93+
def run_command(args: list[str]) -> int:
94+
proc = subprocess.run(args, check=False)
95+
return proc.returncode
96+
97+
98+
def upsert_ledger(
99+
conn: sqlite3.Connection,
100+
test: str,
101+
status: str,
102+
note: Optional[str],
103+
command: Optional[str],
104+
exit_code: Optional[int],
105+
) -> None:
106+
conn.execute(
107+
"""
108+
INSERT INTO ledger (test_name, status, note, last_command, last_exit_code)
109+
VALUES (?, ?, ?, ?, ?)
110+
ON CONFLICT(test_name) DO UPDATE SET
111+
status=excluded.status,
112+
note=excluded.note,
113+
last_command=excluded.last_command,
114+
last_exit_code=excluded.last_exit_code,
115+
updated_at=datetime('now');
116+
""",
117+
(test, status, note, command, exit_code),
118+
)
119+
conn.execute(
120+
"""
121+
INSERT INTO events (test_name, status, note, command, exit_code)
122+
VALUES (?, ?, ?, ?, ?);
123+
""",
124+
(test, status, note, command, exit_code),
125+
)
126+
conn.commit()
127+
128+
129+
def cmd_red(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
130+
run_args = normalize_run_args(args.run)
131+
exit_code: Optional[int] = None
132+
cmd_str: Optional[str] = None
133+
if run_args:
134+
cmd_str = " ".join(run_args)
135+
exit_code = run_command(run_args)
136+
if exit_code == 0:
137+
print("expected failing test, but command exited 0", file=sys.stderr)
138+
return 2
139+
upsert_ledger(conn, args.test, "red", args.note, cmd_str, exit_code)
140+
return 0
141+
142+
143+
def cmd_green(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
144+
run_args = normalize_run_args(args.run)
145+
exit_code: Optional[int] = None
146+
cmd_str: Optional[str] = None
147+
if run_args:
148+
cmd_str = " ".join(run_args)
149+
exit_code = run_command(run_args)
150+
if exit_code != 0:
151+
print("expected passing test, but command failed", file=sys.stderr)
152+
return 2
153+
upsert_ledger(conn, args.test, "green", args.note, cmd_str, exit_code)
154+
return 0
155+
156+
157+
def cmd_require_green(conn: sqlite3.Connection) -> int:
158+
rows = conn.execute(
159+
"SELECT test_name FROM ledger WHERE status = 'red' ORDER BY test_name;"
160+
).fetchall()
161+
if rows:
162+
print("red tests remain:", file=sys.stderr)
163+
for (name,) in rows:
164+
print(f"- {name}", file=sys.stderr)
165+
return 1
166+
return 0
167+
168+
169+
def cmd_status(conn: sqlite3.Connection) -> int:
170+
rows = conn.execute(
171+
"SELECT status, COUNT(*) FROM ledger GROUP BY status ORDER BY status;"
172+
).fetchall()
173+
counts = {status: count for status, count in rows}
174+
red = counts.get("red", 0)
175+
green = counts.get("green", 0)
176+
print(f"red={red} green={green}")
177+
return 0
178+
179+
180+
def main() -> int:
181+
args = parse_args()
182+
conn = connect(args.db)
183+
init_db(conn)
184+
185+
if args.command == "init":
186+
return 0
187+
if args.command == "red":
188+
return cmd_red(conn, args)
189+
if args.command == "green":
190+
return cmd_green(conn, args)
191+
if args.command == "require-green":
192+
return cmd_require_green(conn)
193+
if args.command == "status":
194+
return cmd_status(conn)
195+
196+
print("unknown command", file=sys.stderr)
197+
return 2
198+
199+
200+
if __name__ == "__main__":
201+
raise SystemExit(main())

0 commit comments

Comments
 (0)