Skip to content

Commit f747fae

Browse files
committed
test(builder-sidecar): add unit tests for API call logging
22 tests covering _make_log_entry and JSONL file writing: Build: success, failure, no build_id field, handler exception Run-POV: crash, no crash, timeout, exit 77, handler exception, harness not found (exit 127) Run-Test: pass, fail, skipped, handler exception Common: timestamp is start time, required fields, JSON roundtrip, unknown action File: JSONL append, empty file, compact format, multi-line validity NOTE: _make_log_entry is duplicated from the template because FastAPI is not available in the test environment. Signed-off-by: Dongkwan Kim <[email protected]>
1 parent 0f8e5c1 commit f747fae

2 files changed

Lines changed: 261 additions & 0 deletions

File tree

oss_crs/src/templates/oss_crs_builder_server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ def job_worker():
404404
job_results[job_id]["status"] = "running"
405405
ts_start = time.time()
406406
t0 = time.monotonic()
407+
result: dict = {}
407408
try:
408409
handler = _HANDLERS[action]
409410
result = handler(job_id, req_dir, resp_dir)
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
"""Unit tests for builder sidecar API call logging.
2+
3+
NOTE: The _make_log_entry function is duplicated here because the template
4+
file (oss_crs_builder_server.py) imports FastAPI which is not available in
5+
the test environment. If the implementation changes, these tests must be
6+
updated to match. The canonical source is:
7+
oss_crs/src/templates/oss_crs_builder_server.py
8+
"""
9+
10+
import json
11+
from pathlib import Path
12+
13+
14+
# ---------------------------------------------------------------------------
15+
# Duplicated from oss_crs_builder_server.py (see NOTE above)
16+
# ---------------------------------------------------------------------------
17+
18+
_EXIT_CODE_KEYS = {
19+
"build": "build_exit_code",
20+
"run-pov": "pov_exit_code",
21+
"run-test": "test_exit_code",
22+
}
23+
24+
25+
def _make_log_entry(
26+
action: str,
27+
job_id: str,
28+
result: dict,
29+
duration_ms: int,
30+
ts_start: float,
31+
) -> dict:
32+
"""Create a structured log entry for an API call."""
33+
exit_code = result.get(_EXIT_CODE_KEYS.get(action, ""), -1)
34+
entry: dict = {
35+
"ts": ts_start,
36+
"api": action,
37+
"job_id": job_id,
38+
"duration_ms": duration_ms,
39+
"exit_code": exit_code,
40+
}
41+
for key in ("harness", "build_id"):
42+
if key in result:
43+
entry[key] = result[key]
44+
if "error" in result:
45+
entry["error"] = result["error"]
46+
if action == "build":
47+
entry["build_success"] = exit_code == 0
48+
elif action == "run-pov":
49+
entry["crash"] = exit_code > 0 and exit_code != 124
50+
entry["timeout"] = exit_code == 124
51+
elif action == "run-test":
52+
entry["test_passed"] = exit_code == 0
53+
entry["skipped"] = bool(result.get("test_skipped"))
54+
return entry
55+
56+
57+
# ---------------------------------------------------------------------------
58+
# Tests: _make_log_entry
59+
# ---------------------------------------------------------------------------
60+
61+
62+
class TestMakeLogEntryBuild:
63+
"""Tests for build API log entries."""
64+
65+
def test_success(self):
66+
e = _make_log_entry("build", "abc123", {"build_exit_code": 0}, 8920, 1000.0)
67+
assert e["api"] == "build"
68+
assert e["exit_code"] == 0
69+
assert e["build_success"] is True
70+
assert e["ts"] == 1000.0
71+
assert e["duration_ms"] == 8920
72+
73+
def test_failure(self):
74+
e = _make_log_entry("build", "def456", {"build_exit_code": 1}, 5000, 1000.0)
75+
assert e["build_success"] is False
76+
77+
def test_no_build_id_in_entry(self):
78+
"""Build entries use job_id as build_id; no separate build_id field."""
79+
e = _make_log_entry("build", "abc123", {"build_exit_code": 0}, 100, 1000.0)
80+
assert "build_id" not in e
81+
82+
def test_handler_exception(self):
83+
e = _make_log_entry("build", "j1", {"error": "compile crashed"}, 100, 1000.0)
84+
assert e["exit_code"] == -1
85+
assert e["build_success"] is False
86+
assert e["error"] == "compile crashed"
87+
88+
89+
class TestMakeLogEntryRunPov:
90+
"""Tests for run-pov API log entries."""
91+
92+
def test_crash(self):
93+
result = {"pov_exit_code": 1, "harness": "html", "build_id": "base"}
94+
e = _make_log_entry("run-pov", "p1", result, 2340, 1000.0)
95+
assert e["crash"] is True
96+
assert e["timeout"] is False
97+
assert e["harness"] == "html"
98+
assert e["build_id"] == "base"
99+
100+
def test_no_crash(self):
101+
result = {"pov_exit_code": 0, "harness": "fuzz", "build_id": "abc"}
102+
e = _make_log_entry("run-pov", "p2", result, 1500, 1000.0)
103+
assert e["crash"] is False
104+
assert e["timeout"] is False
105+
106+
def test_timeout(self):
107+
result = {"pov_exit_code": 124, "harness": "fuzz", "build_id": "base"}
108+
e = _make_log_entry("run-pov", "p3", result, 30000, 1000.0)
109+
assert e["crash"] is False
110+
assert e["timeout"] is True
111+
assert e["exit_code"] == 124
112+
113+
def test_exit_77_is_crash(self):
114+
"""Exit 77 = libFuzzer security finding, should be classified as crash."""
115+
result = {"pov_exit_code": 77, "harness": "fuzz", "build_id": "base"}
116+
e = _make_log_entry("run-pov", "p4", result, 100, 1000.0)
117+
assert e["crash"] is True
118+
assert e["exit_code"] == 77
119+
120+
def test_handler_exception_not_crash(self):
121+
"""Handler exception (exit_code=-1) must not be classified as crash."""
122+
e = _make_log_entry(
123+
"run-pov", "p5", {"error": "connection refused"}, 100, 1000.0
124+
)
125+
assert e["crash"] is False
126+
assert e["exit_code"] == -1
127+
assert e["error"] == "connection refused"
128+
129+
def test_harness_not_found(self):
130+
"""Exit 127 = harness binary missing, still classified as crash."""
131+
result = {"pov_exit_code": 127, "harness": "missing", "build_id": "base"}
132+
e = _make_log_entry("run-pov", "p6", result, 50, 1000.0)
133+
assert e["crash"] is True
134+
assert e["harness"] == "missing"
135+
136+
137+
class TestMakeLogEntryRunTest:
138+
"""Tests for run-test API log entries."""
139+
140+
def test_pass(self):
141+
result = {"test_exit_code": 0, "test_skipped": False, "build_id": "abc"}
142+
e = _make_log_entry("run-test", "t1", result, 45000, 1000.0)
143+
assert e["test_passed"] is True
144+
assert e["skipped"] is False
145+
assert e["build_id"] == "abc"
146+
147+
def test_fail(self):
148+
result = {"test_exit_code": 1, "test_skipped": False, "build_id": "abc"}
149+
e = _make_log_entry("run-test", "t2", result, 30000, 1000.0)
150+
assert e["test_passed"] is False
151+
assert e["skipped"] is False
152+
153+
def test_skipped(self):
154+
result = {"test_exit_code": 0, "test_skipped": True, "build_id": "abc"}
155+
e = _make_log_entry("run-test", "t3", result, 50, 1000.0)
156+
assert e["test_passed"] is True
157+
assert e["skipped"] is True
158+
159+
def test_handler_exception(self):
160+
e = _make_log_entry("run-test", "t4", {"error": "timeout"}, 100, 1000.0)
161+
assert e["exit_code"] == -1
162+
assert e["test_passed"] is False
163+
assert e["error"] == "timeout"
164+
165+
166+
class TestMakeLogEntryCommon:
167+
"""Tests for common log entry fields."""
168+
169+
def test_timestamp_is_start_time(self):
170+
ts = 1774296986.79
171+
e = _make_log_entry("build", "j1", {"build_exit_code": 0}, 1000, ts)
172+
assert e["ts"] == ts
173+
174+
def test_required_fields_present(self):
175+
for action, result in [
176+
("build", {"build_exit_code": 0}),
177+
("run-pov", {"pov_exit_code": 1, "harness": "h", "build_id": "b"}),
178+
("run-test", {"test_exit_code": 0, "test_skipped": False, "build_id": "b"}),
179+
]:
180+
e = _make_log_entry(action, "j1", result, 100, 1000.0)
181+
assert all(
182+
k in e for k in ("ts", "api", "job_id", "duration_ms", "exit_code")
183+
)
184+
185+
def test_all_entries_json_serializable(self):
186+
entries = [
187+
_make_log_entry("build", "j1", {"build_exit_code": 0}, 100, 1000.0),
188+
_make_log_entry(
189+
"run-pov",
190+
"j2",
191+
{"pov_exit_code": 1, "harness": "h", "build_id": "b"},
192+
100,
193+
1000.0,
194+
),
195+
_make_log_entry(
196+
"run-test",
197+
"j3",
198+
{"test_exit_code": 0, "test_skipped": False, "build_id": "b"},
199+
100,
200+
1000.0,
201+
),
202+
_make_log_entry("run-pov", "j4", {"error": "fail"}, 100, 1000.0),
203+
]
204+
for entry in entries:
205+
line = json.dumps(entry)
206+
assert json.loads(line) == entry
207+
208+
def test_unknown_action(self):
209+
e = _make_log_entry("unknown", "j1", {}, 100, 1000.0)
210+
assert e["exit_code"] == -1
211+
assert "crash" not in e
212+
assert "build_success" not in e
213+
assert "test_passed" not in e
214+
215+
216+
# ---------------------------------------------------------------------------
217+
# Tests: JSONL file writing
218+
# ---------------------------------------------------------------------------
219+
220+
221+
class TestLogApiCallFile:
222+
"""Tests for JSONL file writing mechanics."""
223+
224+
def _write_entry(self, log_file: Path, entry: dict) -> None:
225+
with log_file.open("a") as f:
226+
f.write(json.dumps(entry, separators=(",", ":")) + "\n")
227+
228+
def test_writes_jsonl_lines(self, tmp_path):
229+
log_file = tmp_path / "api-calls.jsonl"
230+
log_file.touch()
231+
self._write_entry(log_file, {"api": "build", "exit_code": 0})
232+
self._write_entry(log_file, {"api": "run-pov", "exit_code": 1})
233+
234+
lines = log_file.read_text().strip().split("\n")
235+
assert len(lines) == 2
236+
assert json.loads(lines[0])["api"] == "build"
237+
assert json.loads(lines[1])["api"] == "run-pov"
238+
239+
def test_empty_file_means_zero_calls(self, tmp_path):
240+
log_file = tmp_path / "api-calls.jsonl"
241+
log_file.touch()
242+
assert log_file.exists()
243+
assert log_file.read_text() == ""
244+
245+
def test_compact_json_no_spaces(self, tmp_path):
246+
log_file = tmp_path / "api-calls.jsonl"
247+
log_file.touch()
248+
self._write_entry(log_file, {"api": "build", "exit_code": 0})
249+
line = log_file.read_text().strip()
250+
assert " " not in line # compact separators
251+
252+
def test_each_line_is_valid_json(self, tmp_path):
253+
log_file = tmp_path / "api-calls.jsonl"
254+
log_file.touch()
255+
for i in range(5):
256+
self._write_entry(log_file, {"api": "run-pov", "n": i})
257+
lines = log_file.read_text().strip().split("\n")
258+
assert len(lines) == 5
259+
for line in lines:
260+
json.loads(line) # should not raise

0 commit comments

Comments
 (0)