76일차 자료 추가 (답 포함): 실습 15개 · 의도 · 생활 속 예 · 실행 결과, LAB-A · 정답 워크플로우와 25단계 실행 기록, 교안 개요 30장 · 4단계 정리, 바이브 코딩 tretry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mcchaeandClaude Opus 5.5 committed 2026-10-29 23:18:55 +09:00
1 parent 3f36605043
commit bbf17e4696
141 files changed
+34072

No files matched your search

+60
View File
@@ -0,0 +1,60 @@
import json
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import pytest
class FakeClock:
"""sleep() only moves the clock, so a test never waits and the waits can be checked."""
def __init__(self):
self.now = 0.0
self.slept: list[float] = []
def sleep(self, seconds: float):
self.slept.append(seconds)
self.now += seconds
def __call__(self):
return self.now
@pytest.fixture
def clock():
return FakeClock()
@pytest.fixture
def server():
"""A local server whose answers come from a script: path -> list of (status, body, headers), one per call."""
script: dict[str, list] = {}
seen: list[dict] = []
class Handler(BaseHTTPRequestHandler):
def answer(self):
length = int(self.headers.get("Content-Length") or 0)
seen.append({"path": self.path, "method": self.command, "headers": dict(self.headers),
"body": self.rfile.read(length).decode() if length else ""})
queue = script.get(self.path) or [(200, {"ok": True}, {})]
status, body, headers = queue.pop(0) if len(queue) > 1 else queue[0]
if status == "sleep":
import time
time.sleep(body)
status, body, headers = 200, {"late": True}, {}
data = json.dumps(body).encode()
self.send_response(status)
for key, value in headers.items():
self.send_header(key, value)
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
do_GET = do_POST = answer
def log_message(self, *args):
pass
httpd = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
threading.Thread(target=httpd.serve_forever, daemon=True).start()
yield f"http://127.0.0.1:{httpd.server_port}", script, seen
httpd.shutdown()
+29
View File
@@ -0,0 +1,29 @@
"""R11: the screen ranks targets by failed tries and lists the tries of the chosen one."""
from textual.widgets import DataTable
from tretry import log
from tretry.app import LogApp
from tretry.retry import Attempt, Result
def make_log(tmp_path):
path = tmp_path / "log.jsonl"
log.write(path, "http", "POST http://x/tickets", Result(False, attempts=[Attempt(1, 0, 3, False, "timeout"), Attempt(2, 3.5, 3, False, "timeout")],
stopped="tries", seconds=6.5), now=100)
log.write(path, "command", "curl ok", Result(True, attempts=[Attempt(1, 0, 0.1, True)], stopped="success", seconds=0.1), now=101)
return path
async def test_tables(tmp_path):
app = LogApp(make_log(tmp_path))
async with app.run_test() as pilot:
await pilot.pause()
targets = app.query_one("#targets", DataTable)
assert targets.row_count == 2 and targets.get_row_at(0)[0] == "POST http://x/tickets" # most failures first
assert app.query_one("#tries", DataTable).row_count == 2
await pilot.press("down")
await pilot.pause()
assert app.query_one("#tries", DataTable).row_count == 1
await pilot.press("f")
await pilot.pause()
assert app.query_one("#tries", DataTable).row_count == 0
+91
View File
@@ -0,0 +1,91 @@
"""R1 · R3 · R4 · R5 · R7 · R8 · R9 · R10 through the command line (real commands, local fake server, temp log)."""
import json
import sys
from tretry import cli, log
PY = sys.executable
def run(capsys, *argv):
code = cli.main(list(argv))
out = capsys.readouterr()
return code, out.out, out.err
def test_command_fails_then_fallback(tmp_path, capsys):
logfile = tmp_path / "log.jsonl"
code, out, err = run(capsys, "--tries", "2", "--backoff", "0", "--log", str(logfile), "--fallback", "echo cached",
"--", PY, "-c", "import sys; sys.exit(3)")
assert code == 0 and out.strip() == "cached" and "by the fallback" in err
rows = log.read(logfile)
assert [r["fallback"] for r in rows] == [False, False, True] and rows[0]["error"].startswith("exit 3")
def test_command_timeout(tmp_path, capsys):
code, _, err = run(capsys, "--tries", "1", "--timeout", "0.3", "--log", str(tmp_path / "l"), "--",
PY, "-c", "import time; time.sleep(5)")
assert code == 1 and "timeout 0.3s" in err
def test_http_5xx_retried_4xx_not(server, tmp_path, capsys):
base, script, seen = server
script["/flaky"] = [(500, {"error": "x"}, {}), (201, {"ticket_id": "T-1"}, {})]
script["/bad"] = [(400, {"error": "title is required"}, {})]
logfile = str(tmp_path / "log.jsonl")
code, out, _ = run(capsys, "http", "POST", base + "/flaky", "--body", "{}", "--backoff", "0", "--log", logfile)
assert code == 0 and "T-1" in out and len(seen) == 2
code, _, err = run(capsys, "http", "POST", base + "/bad", "--body", "{}", "--backoff", "0", "--log", logfile,
"--fallback", "echo cached")
assert code == 3 and len(seen) == 3 and "not-retryable" in err # one try, no fallback
def test_http_same_idempotency_key_every_try(server, tmp_path, capsys):
base, script, seen = server
script["/tickets"] = [(500, {}, {}), (500, {}, {}), (201, {"ticket_id": "T-1"}, {})]
code, _, _ = run(capsys, "http", "POST", base + "/tickets", "--body", '{"title": "t"}', "--idem-key", "auto",
"--body-key", "key", "--backoff", "0", "--log", str(tmp_path / "l"))
keys = {s["headers"]["Idempotency-Key"] for s in seen}
bodies = {json.loads(s["body"])["key"] for s in seen}
assert code == 0 and len(seen) == 3 and len(keys) == 1 and keys == bodies # R5
def test_http_timeout_is_retried(server, tmp_path, capsys):
base, script, seen = server
script["/slow"] = [("sleep", 1.0, {}), (200, {"ok": True}, {})]
code, _, err = run(capsys, "http", "GET", base + "/slow", "--timeout", "0.3", "--backoff", "0", "--log", str(tmp_path / "l"))
assert code == 0 and "timeout" in err and len(seen) == 2
def test_log_never_keeps_a_key(server, tmp_path, capsys):
base, script, _ = server
script["/x?api_key=secret123"] = [(503, {}, {})]
logfile = tmp_path / "log.jsonl"
run(capsys, "http", "GET", base + "/x?api_key=secret123", "--tries", "1", "--header", "Authorization: Bearer sk-abcdefghijklmnop",
"--log", str(logfile))
text = logfile.read_text()
assert "secret123" not in text and "sk-abcdefghijklmnop" not in text and "***" in text # R7
def test_json_output_and_exit_code(tmp_path, capsys):
code, out, _ = run(capsys, "--tries", "2", "--backoff", "0", "--json", "--log", str(tmp_path / "l"), "--", PY, "-c", "raise SystemExit(1)")
data = json.loads(out)
assert code == 1 and data["exit_code"] == 1 and data["stopped"] == "tries" and len(data["attempts"]) == 2 # R8
def test_stats_and_breaker(tmp_path, capsys):
logfile = str(tmp_path / "log.jsonl")
fail = ["--backoff", "0", "--log", logfile, "--", PY, "-c", "raise SystemExit(1)"]
run(capsys, "--tries", "3", *fail)
run(capsys, "--tries", "2", *fail) # 5 failed tries in a row → breaker opens
code, out, err = run(capsys, "--tries", "3", "--fallback", "echo cached", *fail)
assert code == 0 and out.strip() == "cached" and "breaker open" in err # R10: no try, straight to fallback
code, out, _ = run(capsys, "stats", "--log", logfile, "--json")
row = json.loads(out)[0]
assert row["runs"] == 3 and row["failed_attempts"] == 5 and row["fallbacks"] == 1 and row["success_rate"] == 0.333 # R9
def test_usage_errors(capsys):
assert run(capsys, "echo", "hi")[0] == 2 # no --
assert run(capsys, "http", "POST", "http://x", "--body", "{bad")[0] == 2
assert run(capsys, "--tries", "0", "--", "true")[0] == 2
+67
View File
@@ -0,0 +1,67 @@
"""R1 · R2 · R3 · R6: the engine with fake attempts and a fake clock (no waiting)."""
from tretry.retry import Outcome, Policy, run_with_retry
def failing(times, retryable=True):
calls = []
def attempt(timeout):
calls.append(timeout)
if len(calls) <= times:
return Outcome(False, retryable, f"boom {len(calls)}")
return Outcome(True, output="done")
return attempt, calls
def test_success_after_two_failures_waits_with_backoff(clock):
attempt, calls = failing(2)
result = run_with_retry(attempt, Policy(tries=4, backoff=0.5), sleep=clock.sleep, clock=clock)
assert result.ok and result.output == "done" and len(calls) == 3
assert clock.slept == [0.5, 1.0] # R1: 0.5 → 1.0 (doubles)
def test_wait_has_a_cap_and_jitter_only_shortens():
policy = Policy(tries=10, backoff=1, max_wait=4, jitter=0.5)
assert [policy.wait(n, rand=lambda: 0.0) for n in range(1, 6)] == [1, 2, 4, 4, 4] # R1: cap
assert policy.wait(3, rand=lambda: 1.0) == 2.0 # R2: up to 50% shorter
def test_all_fail_then_fallback(clock):
attempt, _ = failing(9)
result = run_with_retry(attempt, Policy(tries=3), fallback=lambda t: Outcome(True, output="cached"),
sleep=clock.sleep, clock=clock)
assert result.ok and result.used_fallback and result.output == "cached" # R3
assert [a.fallback for a in result.attempts] == [False, False, False, True]
def test_not_retryable_stops_at_once_and_skips_fallback(clock):
attempt, calls = failing(9, retryable=False)
result = run_with_retry(attempt, Policy(tries=5), fallback=lambda t: Outcome(True, output="cached"),
sleep=clock.sleep, clock=clock)
assert not result.ok and len(calls) == 1 and result.stopped == "not-retryable" and not result.used_fallback
def test_deadline_stops_before_a_wait_that_cannot_fit(clock):
def slow(timeout):
clock.now += 3 # every try takes 3 s
return Outcome(False, True, "timeout")
result = run_with_retry(slow, Policy(tries=5, backoff=1, deadline=7), sleep=clock.sleep, clock=clock)
assert result.stopped == "deadline" and len(result.attempts) == 2 # R6: 3 + 1 + 3 = 7 → no third try
assert result.seconds <= 7
def test_deadline_shortens_the_last_timeout(clock):
seen = []
def attempt(timeout):
seen.append(timeout)
clock.now += timeout
return Outcome(False, True, "timeout")
run_with_retry(attempt, Policy(tries=3, backoff=0.5, deadline=5), per_try_timeout=3, sleep=clock.sleep, clock=clock)
assert seen == [3, 1.5] # 3 + 0.5 wait → 1.5 s left for try 2
def test_retry_after_is_respected_up_to_the_cap(clock):
answers = [Outcome(False, True, "HTTP 429", status=429, retry_after=3), Outcome(True, output="ok")]
result = run_with_retry(lambda t: answers.pop(0), Policy(tries=3, backoff=0.5, max_wait=8), sleep=clock.sleep, clock=clock)
assert result.ok and clock.slept == [3] # R4: the server's 3 s, not our 0.5 s