test_benchmark_reporting.py 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162
  1. import importlib
  2. import importlib.util
  3. import json
  4. import multiprocessing
  5. import threading
  6. from concurrent.futures import ThreadPoolExecutor
  7. from datetime import datetime, timezone
  8. from pathlib import Path
  9. import pytest
  10. from pydantic import ValidationError
  11. from agent_lab.application.benchmark import (
  12. BenchmarkCaseId,
  13. BenchmarkConfig,
  14. BenchmarkModelCallTiming,
  15. BenchmarkMode,
  16. BenchmarkRunResult,
  17. )
  18. def _reporting_module():
  19. spec = importlib.util.find_spec(
  20. "agent_lab.infrastructure.benchmark_reporting"
  21. )
  22. assert spec is not None, "benchmark_reporting module must be implemented"
  23. return importlib.import_module(
  24. "agent_lab.infrastructure.benchmark_reporting"
  25. )
  26. def _config(**overrides: object) -> BenchmarkConfig:
  27. payload = {
  28. "schema_version": 1,
  29. "base_url": "https://provider.example/v1",
  30. "model": "benchmark-model",
  31. "runs_per_case": 1,
  32. "cases": [BenchmarkCaseId.ORDINARY_CHAT],
  33. "modes": [BenchmarkMode.DUAL_AGENT],
  34. }
  35. payload.update(overrides)
  36. return BenchmarkConfig.model_validate(payload)
  37. def _run(
  38. *,
  39. case_id: BenchmarkCaseId = BenchmarkCaseId.ORDINARY_CHAT,
  40. mode: BenchmarkMode = BenchmarkMode.DUAL_AGENT,
  41. iteration: int = 1,
  42. status: str = "passed",
  43. initial_provider_ttft_ms: int | None = 10,
  44. visible_ttft_ms: int | None = 20,
  45. turn_wall_time_ms: int | None = 30,
  46. prompt_tokens: int | None = 2,
  47. completion_tokens: int | None = 3,
  48. total_tokens: int | None = 5,
  49. cached_tokens: int | None = 1,
  50. model_call_count: int | None = 1,
  51. fallback_count: int | None = 0,
  52. tool_count: int | None = 0,
  53. tool_latencies_ms: list[int | None] | None = None,
  54. semantic_failures: list[str] | None = None,
  55. error: str | None = None,
  56. model_calls: list[BenchmarkModelCallTiming] | None = None,
  57. parallel_events_overlapped: bool | None = None,
  58. ) -> BenchmarkRunResult:
  59. return BenchmarkRunResult(
  60. case_id=case_id,
  61. mode=mode,
  62. iteration=iteration,
  63. status=status,
  64. initial_provider_ttft_ms=initial_provider_ttft_ms,
  65. visible_ttft_ms=visible_ttft_ms,
  66. turn_wall_time_ms=turn_wall_time_ms,
  67. prompt_tokens=prompt_tokens,
  68. completion_tokens=completion_tokens,
  69. total_tokens=total_tokens,
  70. cached_tokens=cached_tokens,
  71. model_call_count=model_call_count,
  72. fallback_count=fallback_count,
  73. tool_count=tool_count,
  74. event_names=[],
  75. batch_event_names=[],
  76. tool_event_names=[],
  77. event_sources=[],
  78. tool_statuses=[],
  79. tool_latencies_ms=tool_latencies_ms or [],
  80. semantic_failures=semantic_failures or [],
  81. error=error,
  82. model_calls=model_calls or [],
  83. parallel_events_overlapped=parallel_events_overlapped,
  84. )
  85. def _model_call(
  86. call_index: int,
  87. elapsed_ms: int | None,
  88. ) -> BenchmarkModelCallTiming:
  89. return BenchmarkModelCallTiming(
  90. call_index=call_index,
  91. call_kind="chat_completion",
  92. first_item_kind="raw_chunk",
  93. provider_ttft_ms=1,
  94. visible_ttft_ms=2,
  95. elapsed_ms=elapsed_ms,
  96. usage=None,
  97. )
  98. def _marker_report(marker: str):
  99. reporting = _reporting_module()
  100. return reporting.build_benchmark_report(
  101. _config(model=marker),
  102. [_run()],
  103. mock=True,
  104. )
  105. def _process_report_writer(
  106. output_dir: str,
  107. marker: str,
  108. timestamp: str,
  109. first_temp_barrier,
  110. result_queue,
  111. ) -> None:
  112. reporting = _reporting_module()
  113. original_write_text = reporting._write_text
  114. first_write = True
  115. def synchronized_write(path: Path, content: str) -> None:
  116. nonlocal first_write
  117. original_write_text(path, content)
  118. if first_write:
  119. first_write = False
  120. first_temp_barrier.wait()
  121. reporting._write_text = synchronized_write
  122. try:
  123. json_path, markdown_path = reporting.write_benchmark_report(
  124. _marker_report(marker),
  125. output_dir,
  126. timestamp=timestamp,
  127. )
  128. except BaseException as exc:
  129. result_queue.put(("error", marker, repr(exc)))
  130. return
  131. result_queue.put(
  132. ("ok", marker, json_path.parent.name, markdown_path.parent.name)
  133. )
  134. def _assert_concurrent_report_pairs(
  135. output_dir: Path,
  136. markers: list[str],
  137. ) -> None:
  138. published_dirs = sorted(
  139. path
  140. for path in output_dir.iterdir()
  141. if not path.name.startswith(".")
  142. )
  143. assert len(published_dirs) == len(markers)
  144. assert all(path.is_symlink() for path in published_dirs)
  145. actual_markers = set()
  146. targets = set()
  147. for published_dir in published_dirs:
  148. target = published_dir.readlink()
  149. assert not target.is_absolute()
  150. targets.add(target)
  151. json_path = published_dir / "report.json"
  152. markdown_path = published_dir / "report.md"
  153. marker = json.loads(json_path.read_text())["target"]["model"]
  154. markdown = markdown_path.read_text()
  155. actual_markers.add(marker)
  156. assert f"- Model: `{marker}`" in markdown
  157. assert actual_markers == set(markers)
  158. assert len(targets) == len(markers)
  159. _assert_no_publication_artifacts(output_dir)
  160. def _assert_no_publication_artifacts(output_dir: Path) -> None:
  161. entries = list(output_dir.iterdir())
  162. leftovers = [
  163. path.name
  164. for path in entries
  165. if path.name.endswith((".tmp", ".lock"))
  166. or "placeholder" in path.name
  167. or path.suffix in {".json", ".md"}
  168. ]
  169. assert leftovers == []
  170. published_dirs = [
  171. path
  172. for path in entries
  173. if path.is_symlink()
  174. and path.readlink().name.startswith(".")
  175. and path.readlink().name.endswith(".bundle")
  176. ]
  177. published_targets = {path.readlink() for path in published_dirs}
  178. private_bundles = {
  179. Path(path.name)
  180. for path in entries
  181. if path.name.startswith(".") and path.is_dir()
  182. }
  183. assert private_bundles == published_targets
  184. for published_dir in published_dirs:
  185. assert (published_dir / "report.json").stat().st_size > 0
  186. assert (published_dir / "report.md").stat().st_size > 0
  187. @pytest.mark.parametrize(
  188. ("values", "quantile", "expected"),
  189. [
  190. ([], 0.50, None),
  191. ([7], 0.50, 7.0),
  192. ([7], 0.95, 7.0),
  193. ([1, 3, 5, 7], 0.50, 4.0),
  194. ([1, 3, 5, 7], 0.95, 6.7),
  195. ],
  196. )
  197. def test_percentile_uses_linear_interpolation(
  198. values: list[int], quantile: float, expected: float | None
  199. ):
  200. reporting = _reporting_module()
  201. assert reporting.percentile(values, quantile) == pytest.approx(expected)
  202. def test_report_models_are_strict_and_include_required_summary_fields():
  203. reporting = _reporting_module()
  204. generated_at = datetime(2026, 7, 14, 1, 2, 3, tzinfo=timezone.utc)
  205. report = reporting.build_benchmark_report(
  206. _config(),
  207. [_run()],
  208. mock=True,
  209. generated_at=generated_at,
  210. )
  211. assert isinstance(report, reporting.BenchmarkReport)
  212. assert report.schema_version == 1
  213. assert report.generated_at == generated_at
  214. assert report.target.model_dump() == {
  215. "base_url": "https://provider.example/v1",
  216. "model": "benchmark-model",
  217. "mock": True,
  218. }
  219. assert report.config.model_dump(mode="json") == {
  220. "runs_per_case": 1,
  221. "cases": ["ordinary_chat"],
  222. "modes": ["dual_agent"],
  223. }
  224. assert report.overall.model_dump() == {
  225. "attempted": 1,
  226. "passed": 1,
  227. "failed": 0,
  228. "warnings": report.overall.warnings,
  229. "system_errors": [],
  230. }
  231. with pytest.raises(ValidationError):
  232. reporting.BenchmarkReport.model_validate(
  233. report.model_dump() | {"schema_version": "1"}
  234. )
  235. with pytest.raises(ValidationError):
  236. reporting.BenchmarkAggregate.model_validate(
  237. report.aggregates[0].model_dump() | {"unexpected": True}
  238. )
  239. def test_aggregate_excludes_failed_and_null_samples_from_metrics():
  240. reporting = _reporting_module()
  241. runs = [
  242. _run(
  243. iteration=1,
  244. initial_provider_ttft_ms=10,
  245. visible_ttft_ms=20,
  246. turn_wall_time_ms=30,
  247. prompt_tokens=2,
  248. completion_tokens=3,
  249. total_tokens=5,
  250. cached_tokens=1,
  251. model_call_count=1,
  252. fallback_count=0,
  253. tool_count=2,
  254. ),
  255. _run(
  256. iteration=2,
  257. initial_provider_ttft_ms=None,
  258. visible_ttft_ms=40,
  259. turn_wall_time_ms=None,
  260. prompt_tokens=None,
  261. completion_tokens=7,
  262. total_tokens=7,
  263. cached_tokens=None,
  264. model_call_count=3,
  265. fallback_count=1,
  266. tool_count=None,
  267. ),
  268. _run(
  269. iteration=3,
  270. status="failed",
  271. initial_provider_ttft_ms=999,
  272. visible_ttft_ms=999,
  273. turn_wall_time_ms=999,
  274. prompt_tokens=999,
  275. completion_tokens=999,
  276. total_tokens=999,
  277. cached_tokens=999,
  278. model_call_count=999,
  279. fallback_count=999,
  280. tool_count=999,
  281. error="failed",
  282. ),
  283. ]
  284. aggregate = reporting.build_benchmark_report(
  285. _config(runs_per_case=3), runs, mock=True
  286. ).aggregates[0]
  287. assert (aggregate.attempted, aggregate.passed, aggregate.failed) == (3, 2, 1)
  288. assert aggregate.initial_provider_ttft_ms.model_dump() == {
  289. "count": 1,
  290. "p50": 10.0,
  291. "p95": 10.0,
  292. }
  293. assert aggregate.visible_ttft_ms.model_dump() == {
  294. "count": 2,
  295. "p50": 30.0,
  296. "p95": 39.0,
  297. }
  298. assert aggregate.turn_wall_time_ms.count == 1
  299. assert aggregate.prompt_tokens.model_dump() == {
  300. "count": 1,
  301. "total": 2,
  302. "mean": 2.0,
  303. }
  304. assert aggregate.completion_tokens.model_dump() == {
  305. "count": 2,
  306. "total": 10,
  307. "mean": 5.0,
  308. }
  309. assert aggregate.total_tokens.total == 12
  310. assert aggregate.cached_tokens.total == 1
  311. assert aggregate.model_call_count.model_dump() == {
  312. "count": 2,
  313. "total": 4,
  314. "mean": 2.0,
  315. }
  316. assert aggregate.fallback_count.total == 1
  317. assert aggregate.tool_count.model_dump() == {
  318. "count": 1,
  319. "total": 2,
  320. "mean": 2.0,
  321. }
  322. def test_aggregate_flattens_passed_model_and_tool_latency_samples():
  323. reporting = _reporting_module()
  324. runs = [
  325. _run(
  326. iteration=1,
  327. model_calls=[
  328. _model_call(1, 10),
  329. _model_call(2, None),
  330. _model_call(3, 30),
  331. ],
  332. tool_latencies_ms=[5, None, 15],
  333. ),
  334. _run(
  335. iteration=2,
  336. model_calls=[_model_call(1, 20)],
  337. tool_latencies_ms=[25],
  338. ),
  339. _run(
  340. iteration=3,
  341. status="failed",
  342. model_calls=[_model_call(1, 999)],
  343. tool_latencies_ms=[999],
  344. ),
  345. ]
  346. report = reporting.build_benchmark_report(
  347. _config(runs_per_case=3), runs, mock=True
  348. )
  349. aggregate = report.aggregates[0]
  350. markdown = reporting.render_markdown(report)
  351. assert aggregate.model_elapsed_ms.model_dump() == {
  352. "count": 3,
  353. "p50": 20.0,
  354. "p95": 29.0,
  355. }
  356. assert aggregate.tool_latency_ms.model_dump() == {
  357. "count": 3,
  358. "p50": 15.0,
  359. "p95": 24.0,
  360. }
  361. assert "Model elapsed p50/p95 ms" in markdown
  362. assert "Tool latency p50/p95 ms" in markdown
  363. assert "20.0/29.0" in markdown
  364. assert "15.0/24.0" in markdown
  365. def test_zero_latency_samples_emit_low_confidence_warnings():
  366. reporting = _reporting_module()
  367. aggregate = reporting.build_benchmark_report(
  368. _config(),
  369. [
  370. _run(
  371. initial_provider_ttft_ms=None,
  372. visible_ttft_ms=None,
  373. turn_wall_time_ms=None,
  374. model_calls=[],
  375. tool_latencies_ms=[],
  376. )
  377. ],
  378. mock=True,
  379. ).aggregates[0]
  380. assert {
  381. warning.split(" p95 low confidence:", 1)[0]
  382. for warning in aggregate.warnings
  383. } == {
  384. "ordinary_chat/dual_agent initial_provider_ttft_ms",
  385. "ordinary_chat/dual_agent visible_ttft_ms",
  386. "ordinary_chat/dual_agent turn_wall_time_ms",
  387. "ordinary_chat/dual_agent model_elapsed_ms",
  388. "ordinary_chat/dual_agent tool_latency_ms",
  389. }
  390. assert all("0 samples" in warning for warning in aggregate.warnings)
  391. def test_aggregates_are_stably_sorted_and_warn_for_low_confidence_p95():
  392. reporting = _reporting_module()
  393. runs = [
  394. _run(
  395. case_id=BenchmarkCaseId.WEB_SEARCH_TWO_ANSWERS,
  396. mode=BenchmarkMode.DUAL_AGENT,
  397. ),
  398. _run(
  399. case_id=BenchmarkCaseId.ORDINARY_CHAT,
  400. mode=BenchmarkMode.CHAT_AGENT_TOOLS,
  401. ),
  402. _run(
  403. case_id=BenchmarkCaseId.ORDINARY_CHAT,
  404. mode=BenchmarkMode.DUAL_AGENT,
  405. ),
  406. ]
  407. report = reporting.build_benchmark_report(
  408. _config(
  409. cases=[
  410. BenchmarkCaseId.WEB_SEARCH_TWO_ANSWERS,
  411. BenchmarkCaseId.ORDINARY_CHAT,
  412. ],
  413. modes=[
  414. BenchmarkMode.CHAT_AGENT_TOOLS,
  415. BenchmarkMode.DUAL_AGENT,
  416. ],
  417. ),
  418. runs,
  419. mock=True,
  420. )
  421. assert [(item.case_id.value, item.mode.value) for item in report.aggregates] == [
  422. ("ordinary_chat", "chat_agent_tools"),
  423. ("ordinary_chat", "dual_agent"),
  424. ("web_search_two_answers", "dual_agent"),
  425. ]
  426. assert all(
  427. any("p95 low confidence" in warning for warning in item.warnings)
  428. for item in report.aggregates
  429. )
  430. assert report.overall.warnings == [
  431. warning
  432. for aggregate in report.aggregates
  433. for warning in aggregate.warnings
  434. ]
  435. def test_errors_are_redacted_before_truncation_in_json_and_markdown():
  436. reporting = _reporting_module()
  437. real_key = "sk-live-secret-value"
  438. leaked_error = (
  439. f"Authorization: Bearer {real_key}; "
  440. f"API-Key={real_key}; "
  441. f"https://example.test/?api_key={real_key}&token={real_key}; "
  442. f'{{"access_token":"{real_key}","key":"{real_key}"}}; '
  443. + "x" * 3000
  444. )
  445. report = reporting.build_benchmark_report(
  446. _config(),
  447. [
  448. _run(
  449. status="failed",
  450. semantic_failures=[f"token={real_key}"],
  451. error=leaked_error,
  452. )
  453. ],
  454. mock=False,
  455. secrets=[real_key],
  456. )
  457. payload = report.model_dump(mode="json")
  458. json_text = reporting.report_to_json(report)
  459. markdown = reporting.render_markdown(report)
  460. assert len(report.runs[0].error) == 2048
  461. assert real_key not in json.dumps(payload)
  462. assert real_key not in json_text
  463. assert real_key not in markdown
  464. assert "[REDACTED]" in json_text
  465. assert "[REDACTED]" in markdown
  466. def test_error_truncation_is_utf8_safe_and_bounded_after_redaction():
  467. reporting = _reporting_module()
  468. real_key = "sk-byte-bound-secret"
  469. error = f"Bearer {real_key} " + "密" * 1000
  470. sanitized = reporting.sanitize_error(error, secrets=[real_key])
  471. redacted = reporting.redact_text(error, secrets=[real_key])
  472. expected = redacted.encode("utf-8")[:2048].decode("utf-8", errors="ignore")
  473. assert sanitized == expected
  474. assert real_key not in sanitized
  475. assert len(sanitized.encode("utf-8")) <= 2048
  476. assert "\ufffd" not in sanitized
  477. def test_configured_secret_is_redacted_from_all_report_string_surfaces():
  478. reporting = _reporting_module()
  479. real_key = "sk-everywhere-secret"
  480. run = _run().model_copy(
  481. update={
  482. "event_names": [real_key],
  483. "event_sources": [f"Bearer {real_key}"],
  484. "tool_statuses": [f"token={real_key}"],
  485. }
  486. )
  487. report = reporting.build_benchmark_report(
  488. _config(
  489. base_url=f"https://provider.example/{real_key}",
  490. model=f"model-{real_key}",
  491. ),
  492. [run],
  493. mock=False,
  494. secrets=[real_key],
  495. )
  496. rendered = reporting.report_to_json(report) + reporting.render_markdown(report)
  497. assert real_key not in rendered
  498. assert "[REDACTED]" in rendered
  499. def test_system_errors_are_typed_redacted_counted_and_rendered():
  500. reporting = _reporting_module()
  501. real_key = "sk-runner-system-secret"
  502. report = reporting.build_benchmark_report(
  503. _config(),
  504. [],
  505. mock=False,
  506. secrets=[real_key],
  507. system_errors=[f"Authorization: Bearer {real_key}"],
  508. )
  509. payload = json.loads(reporting.report_to_json(report))
  510. markdown = reporting.render_markdown(report)
  511. assert report.overall.model_dump() == {
  512. "attempted": 1,
  513. "passed": 0,
  514. "failed": 1,
  515. "warnings": [],
  516. "system_errors": ["Authorization: [REDACTED] [REDACTED]"],
  517. }
  518. assert payload["overall"]["system_errors"] == report.overall.system_errors
  519. assert "## System Errors" in markdown
  520. assert "Authorization: [REDACTED] [REDACTED]" in markdown
  521. assert real_key not in reporting.report_to_json(report)
  522. assert real_key not in markdown
  523. def test_markdown_is_rendered_from_the_same_json_facts():
  524. reporting = _reporting_module()
  525. report = reporting.build_benchmark_report(
  526. _config(runs_per_case=2),
  527. [
  528. _run(iteration=1, visible_ttft_ms=10, total_tokens=5),
  529. _run(iteration=2, visible_ttft_ms=30, total_tokens=9),
  530. ],
  531. mock=True,
  532. generated_at=datetime(2026, 7, 14, tzinfo=timezone.utc),
  533. )
  534. payload = json.loads(reporting.report_to_json(report))
  535. markdown = reporting.render_markdown(report)
  536. aggregate = payload["aggregates"][0]
  537. assert str(aggregate["visible_ttft_ms"]["p50"]) in markdown
  538. assert str(aggregate["visible_ttft_ms"]["p95"]) in markdown
  539. assert str(aggregate["total_tokens"]["total"]) in markdown
  540. assert str(aggregate["total_tokens"]["mean"]) in markdown
  541. assert payload == report.model_dump(mode="json")
  542. def test_markdown_preserves_runtime_error_and_semantic_failures_together():
  543. reporting = _reporting_module()
  544. report = reporting.build_benchmark_report(
  545. _config(),
  546. [
  547. _run(
  548. status="failed",
  549. error="runtime exploded",
  550. semantic_failures=[
  551. "expected web_search event",
  552. "expected two visible answers",
  553. ],
  554. )
  555. ],
  556. mock=True,
  557. )
  558. markdown = reporting.render_markdown(report)
  559. assert "runtime exploded" in markdown
  560. assert "expected web_search event" in markdown
  561. assert "expected two visible answers" in markdown
  562. def test_markdown_runs_table_shows_parallel_event_overlap():
  563. reporting = _reporting_module()
  564. report = reporting.build_benchmark_report(
  565. _config(cases=[BenchmarkCaseId.PARALLEL_VOLUME_SCHEDULE]),
  566. [
  567. _run(
  568. case_id=BenchmarkCaseId.PARALLEL_VOLUME_SCHEDULE,
  569. parallel_events_overlapped=True,
  570. )
  571. ],
  572. mock=True,
  573. )
  574. payload = json.loads(reporting.report_to_json(report))
  575. markdown = reporting.render_markdown(report)
  576. assert payload["runs"][0]["parallel_events_overlapped"] is True
  577. assert "| Parallel overlap |" in markdown
  578. assert "| parallel_volume_schedule | dual_agent | 1 | passed | true | - |" in (
  579. markdown
  580. )
  581. def test_atomic_write_publishes_complete_bundle_through_relative_symlink(
  582. tmp_path: Path,
  583. ):
  584. reporting = _reporting_module()
  585. report = _marker_report("atomic-bundle")
  586. timestamp = "20260714T010203Z"
  587. competitor = tmp_path / timestamp
  588. competitor.mkdir()
  589. (competitor / "sentinel").write_text("competitor")
  590. json_path, markdown_path = reporting.write_benchmark_report(
  591. report,
  592. tmp_path,
  593. timestamp=timestamp,
  594. )
  595. published_dir = tmp_path / f"{timestamp}-1"
  596. assert json_path == published_dir / "report.json"
  597. assert markdown_path == published_dir / "report.md"
  598. assert published_dir.is_symlink()
  599. bundle_target = published_dir.readlink()
  600. assert not bundle_target.is_absolute()
  601. assert bundle_target.name.startswith(f".{timestamp}.")
  602. assert bundle_target.name.endswith(".bundle")
  603. assert competitor.joinpath("sentinel").read_text() == "competitor"
  604. assert json.loads(json_path.read_text()) == report.model_dump(mode="json")
  605. assert markdown_path.read_text() == reporting.render_markdown(report)
  606. _assert_no_publication_artifacts(tmp_path)
  607. @pytest.mark.parametrize("collision_kind", ["create", "replace"])
  608. def test_symlink_collision_retries_without_removing_or_replacing_competitor(
  609. tmp_path: Path,
  610. monkeypatch: pytest.MonkeyPatch,
  611. collision_kind: str,
  612. ):
  613. reporting = _reporting_module()
  614. timestamp = "20260714T011223Z"
  615. public_path = tmp_path / timestamp
  616. real_symlink = reporting.os.symlink
  617. real_replace = reporting.os.replace
  618. symlink_calls = 0
  619. if collision_kind == "replace":
  620. old_target = tmp_path / "old-competitor"
  621. old_target.mkdir()
  622. real_symlink(old_target.name, public_path)
  623. def collide_before_symlink(source: str, destination: Path) -> None:
  624. nonlocal symlink_calls
  625. symlink_calls += 1
  626. destination = Path(destination)
  627. if symlink_calls == 1:
  628. if collision_kind == "create":
  629. destination.mkdir()
  630. (destination / "sentinel").write_text("competitor")
  631. else:
  632. new_target = tmp_path / "new-competitor"
  633. new_target.mkdir()
  634. replacement = tmp_path / "competitor-replacement"
  635. real_symlink(new_target.name, replacement)
  636. real_replace(replacement, destination)
  637. real_symlink(source, destination)
  638. monkeypatch.setattr(reporting.os, "symlink", collide_before_symlink)
  639. json_path, markdown_path = reporting.write_benchmark_report(
  640. _marker_report(f"symlink-{collision_kind}-collision"),
  641. tmp_path,
  642. timestamp=timestamp,
  643. )
  644. assert symlink_calls == 2
  645. assert json_path.parent == tmp_path / f"{timestamp}-1"
  646. assert markdown_path.parent == json_path.parent
  647. if collision_kind == "create":
  648. assert public_path.joinpath("sentinel").read_text() == "competitor"
  649. else:
  650. assert public_path.readlink() == Path("new-competitor")
  651. assert json.loads(json_path.read_text())["target"]["model"] == (
  652. f"symlink-{collision_kind}-collision"
  653. )
  654. _assert_no_publication_artifacts(tmp_path)
  655. def test_bundle_is_complete_and_matching_before_single_publication(
  656. tmp_path: Path,
  657. monkeypatch: pytest.MonkeyPatch,
  658. ):
  659. reporting = _reporting_module()
  660. real_symlink = reporting.os.symlink
  661. publication_snapshots: list[tuple[str, str]] = []
  662. def inspect_complete_bundle(source: str, destination: Path) -> None:
  663. destination = Path(destination)
  664. bundle = destination.parent / source
  665. json_text = bundle.joinpath("report.json").read_text()
  666. markdown_text = bundle.joinpath("report.md").read_text()
  667. marker = json.loads(json_text)["target"]["model"]
  668. assert f"- Model: `{marker}`" in markdown_text
  669. publication_snapshots.append((json_text, markdown_text))
  670. real_symlink(source, destination)
  671. monkeypatch.setattr(reporting.os, "symlink", inspect_complete_bundle)
  672. json_path, markdown_path = reporting.write_benchmark_report(
  673. _marker_report("complete-before-publish"),
  674. tmp_path,
  675. timestamp="20260714T011224Z",
  676. )
  677. assert len(publication_snapshots) == 1
  678. assert json_path.parent == markdown_path.parent
  679. assert json_path.parent.is_symlink()
  680. assert json_path.read_text() == publication_snapshots[0][0]
  681. assert markdown_path.read_text() == publication_snapshots[0][1]
  682. _assert_no_publication_artifacts(tmp_path)
  683. def test_interrupt_after_symlink_creation_keeps_resolvable_complete_bundle(
  684. tmp_path: Path,
  685. monkeypatch: pytest.MonkeyPatch,
  686. ):
  687. reporting = _reporting_module()
  688. real_symlink = reporting.os.symlink
  689. timestamp = "20260714T011224Z-interrupted"
  690. def publish_then_interrupt(source: str, destination: Path) -> None:
  691. real_symlink(source, destination)
  692. raise KeyboardInterrupt
  693. monkeypatch.setattr(reporting.os, "symlink", publish_then_interrupt)
  694. with pytest.raises(KeyboardInterrupt):
  695. reporting.write_benchmark_report(
  696. _marker_report("interrupt-after-symlink"),
  697. tmp_path,
  698. timestamp=timestamp,
  699. )
  700. published_dir = tmp_path / timestamp
  701. assert published_dir.is_symlink()
  702. assert published_dir.resolve(strict=True).is_dir()
  703. assert (
  704. json.loads(published_dir.joinpath("report.json").read_text())["target"][
  705. "model"
  706. ]
  707. == "interrupt-after-symlink"
  708. )
  709. assert "- Model: `interrupt-after-symlink`" in (
  710. published_dir.joinpath("report.md").read_text()
  711. )
  712. _assert_no_publication_artifacts(tmp_path)
  713. @pytest.mark.parametrize(
  714. "raw_timestamp",
  715. [
  716. pytest.param("<absolute>", id="absolute-path"),
  717. pytest.param("../outside-report", id="parent-traversal"),
  718. pytest.param("segment/report", id="forward-slash"),
  719. pytest.param(r"segment\report", id="backslash"),
  720. pytest.param(".", id="dot-segment"),
  721. pytest.param("..", id="double-dot-segment"),
  722. pytest.param("", id="empty"),
  723. pytest.param(" ", id="space"),
  724. pytest.param("\t", id="tab"),
  725. pytest.param("2026\n0714", id="control-newline"),
  726. pytest.param("2026\x00714", id="control-nul"),
  727. pytest.param("-20260714", id="non-alnum-prefix-hyphen"),
  728. pytest.param("_20260714", id="non-alnum-prefix-underscore"),
  729. pytest.param("2026\u00a00714", id="unicode-whitespace"),
  730. pytest.param("2026\uff0f0714", id="unicode-fullwidth-slash"),
  731. pytest.param("2026\u22150714", id="unicode-division-slash"),
  732. pytest.param("20260714", id="unicode-fullwidth-digits"),
  733. pytest.param("2026\u202ejson", id="unicode-bidi-control"),
  734. pytest.param("a" * 129, id="too-long"),
  735. ],
  736. )
  737. def test_invalid_explicit_timestamp_is_rejected_before_any_filesystem_write(
  738. tmp_path: Path,
  739. raw_timestamp: str,
  740. ):
  741. reporting = _reporting_module()
  742. output_dir = tmp_path / "reports"
  743. timestamp = (
  744. str(tmp_path / "outside-absolute")
  745. if raw_timestamp == "<absolute>"
  746. else raw_timestamp
  747. )
  748. entries_before = sorted(
  749. path.relative_to(tmp_path) for path in tmp_path.rglob("*")
  750. )
  751. with pytest.raises(
  752. reporting.BenchmarkReportWriteError,
  753. match="invalid benchmark report timestamp",
  754. ):
  755. reporting.write_benchmark_report(
  756. _marker_report("invalid-timestamp"),
  757. output_dir,
  758. timestamp=timestamp,
  759. )
  760. entries_after = sorted(
  761. path.relative_to(tmp_path) for path in tmp_path.rglob("*")
  762. )
  763. assert entries_after == entries_before
  764. assert not output_dir.exists()
  765. def test_report_files_and_bundle_directory_are_fsynced_before_publication(
  766. tmp_path: Path,
  767. monkeypatch: pytest.MonkeyPatch,
  768. ):
  769. reporting = _reporting_module()
  770. real_fsync = reporting.os.fsync
  771. real_symlink = reporting.os.symlink
  772. fsynced_objects: list[tuple[int, int]] = []
  773. symlink_calls = 0
  774. def record_fsync(file_descriptor: int) -> None:
  775. file_stat = reporting.os.fstat(file_descriptor)
  776. fsynced_objects.append((file_stat.st_dev, file_stat.st_ino))
  777. real_fsync(file_descriptor)
  778. def inspect_before_symlink(source: str, destination: Path) -> None:
  779. nonlocal symlink_calls
  780. symlink_calls += 1
  781. destination = Path(destination)
  782. bundle_dir = destination.parent / source
  783. expected_before_publication = {
  784. (
  785. bundle_dir.joinpath(name).stat().st_dev,
  786. bundle_dir.joinpath(name).stat().st_ino,
  787. )
  788. for name in ("report.json", "report.md")
  789. }
  790. bundle_stat = bundle_dir.stat()
  791. expected_before_publication.add((bundle_stat.st_dev, bundle_stat.st_ino))
  792. assert set(fsynced_objects) == expected_before_publication
  793. assert len(fsynced_objects) == 3
  794. real_symlink(source, destination)
  795. monkeypatch.setattr(reporting.os, "fsync", record_fsync)
  796. monkeypatch.setattr(reporting.os, "symlink", inspect_before_symlink)
  797. reporting.write_benchmark_report(
  798. _marker_report("fsync-before-publish"),
  799. tmp_path,
  800. timestamp="20260714T011225Z",
  801. )
  802. assert symlink_calls == 1
  803. output_stat = tmp_path.stat()
  804. assert (output_stat.st_dev, output_stat.st_ino) in fsynced_objects
  805. assert len(fsynced_objects) == 4
  806. assert len(set(fsynced_objects)) == 4
  807. _assert_no_publication_artifacts(tmp_path)
  808. def test_file_write_failure_removes_only_unpublished_private_bundle(
  809. tmp_path: Path,
  810. monkeypatch: pytest.MonkeyPatch,
  811. ):
  812. reporting = _reporting_module()
  813. original_write_text = reporting._write_text
  814. write_calls = 0
  815. def fail_second_file(path: Path, content: str) -> None:
  816. nonlocal write_calls
  817. write_calls += 1
  818. if write_calls == 2:
  819. raise OSError("simulated report file failure")
  820. original_write_text(path, content)
  821. monkeypatch.setattr(reporting, "_write_text", fail_second_file)
  822. with pytest.raises(
  823. reporting.BenchmarkReportWriteError,
  824. match="failed to write benchmark reports",
  825. ):
  826. reporting.write_benchmark_report(
  827. _marker_report("file-write-failure"),
  828. tmp_path,
  829. timestamp="20260714T011226Z",
  830. )
  831. assert list(tmp_path.iterdir()) == []
  832. def test_bundle_fsync_failure_removes_unpublished_private_bundle(
  833. tmp_path: Path,
  834. monkeypatch: pytest.MonkeyPatch,
  835. ):
  836. reporting = _reporting_module()
  837. original_fsync_directory = reporting._fsync_directory
  838. def fail_bundle_fsync(path: Path) -> None:
  839. if path.name.endswith(".bundle"):
  840. raise OSError("simulated bundle fsync failure")
  841. original_fsync_directory(path)
  842. monkeypatch.setattr(reporting, "_fsync_directory", fail_bundle_fsync)
  843. with pytest.raises(
  844. reporting.BenchmarkReportWriteError,
  845. match="failed to write benchmark reports",
  846. ):
  847. reporting.write_benchmark_report(
  848. _marker_report("bundle-fsync-failure"),
  849. tmp_path,
  850. timestamp="20260714T011227Z",
  851. )
  852. assert list(tmp_path.iterdir()) == []
  853. def test_symlink_failure_after_publication_start_keeps_complete_private_bundle(
  854. tmp_path: Path,
  855. monkeypatch: pytest.MonkeyPatch,
  856. ):
  857. reporting = _reporting_module()
  858. def fail_symlink(source: str, destination: Path) -> None:
  859. del source, destination
  860. raise OSError("simulated symlink failure")
  861. monkeypatch.setattr(reporting.os, "symlink", fail_symlink)
  862. with pytest.raises(
  863. reporting.BenchmarkReportWriteError,
  864. match="failed to write benchmark reports",
  865. ):
  866. reporting.write_benchmark_report(
  867. _marker_report("symlink-failure"),
  868. tmp_path,
  869. timestamp="20260714T011228Z",
  870. )
  871. entries = list(tmp_path.iterdir())
  872. assert len(entries) == 1
  873. bundle_dir = entries[0]
  874. assert bundle_dir.name.startswith(".20260714T011228Z.")
  875. assert bundle_dir.name.endswith(".bundle")
  876. assert json.loads(bundle_dir.joinpath("report.json").read_text())["target"][
  877. "model"
  878. ] == "symlink-failure"
  879. assert "- Model: `symlink-failure`" in bundle_dir.joinpath(
  880. "report.md"
  881. ).read_text()
  882. def test_symlink_interrupt_after_publication_start_keeps_private_bundle(
  883. tmp_path: Path,
  884. monkeypatch: pytest.MonkeyPatch,
  885. ):
  886. reporting = _reporting_module()
  887. def interrupt_symlink(source: str, destination: Path) -> None:
  888. del source, destination
  889. raise KeyboardInterrupt
  890. monkeypatch.setattr(reporting.os, "symlink", interrupt_symlink)
  891. with pytest.raises(KeyboardInterrupt):
  892. reporting.write_benchmark_report(
  893. _marker_report("symlink-interrupt"),
  894. tmp_path,
  895. timestamp="20260714T011229Z",
  896. )
  897. entries = list(tmp_path.iterdir())
  898. assert len(entries) == 1
  899. bundle_dir = entries[0]
  900. assert bundle_dir.name.startswith(".20260714T011229Z.")
  901. assert bundle_dir.name.endswith(".bundle")
  902. assert json.loads(bundle_dir.joinpath("report.json").read_text())["target"][
  903. "model"
  904. ] == "symlink-interrupt"
  905. assert "- Model: `symlink-interrupt`" in bundle_dir.joinpath(
  906. "report.md"
  907. ).read_text()
  908. def test_post_publication_directory_fsync_failure_keeps_public_bundle(
  909. tmp_path: Path,
  910. monkeypatch: pytest.MonkeyPatch,
  911. ):
  912. reporting = _reporting_module()
  913. original_fsync_directory = reporting._fsync_directory
  914. fsync_calls = 0
  915. def fail_output_directory_fsync(path: Path) -> None:
  916. nonlocal fsync_calls
  917. fsync_calls += 1
  918. if fsync_calls == 2:
  919. raise OSError("simulated output directory fsync failure")
  920. original_fsync_directory(path)
  921. monkeypatch.setattr(
  922. reporting,
  923. "_fsync_directory",
  924. fail_output_directory_fsync,
  925. )
  926. with pytest.raises(
  927. reporting.BenchmarkReportWriteError,
  928. match="failed to write benchmark reports",
  929. ):
  930. reporting.write_benchmark_report(
  931. _marker_report("published-before-fsync-failure"),
  932. tmp_path,
  933. timestamp="20260714T011230Z",
  934. )
  935. published_dir = tmp_path / "20260714T011230Z"
  936. assert published_dir.is_symlink()
  937. assert (
  938. json.loads(published_dir.joinpath("report.json").read_text())["target"]["model"]
  939. == "published-before-fsync-failure"
  940. )
  941. assert "- Model: `published-before-fsync-failure`" in (
  942. published_dir.joinpath("report.md").read_text()
  943. )
  944. _assert_no_publication_artifacts(tmp_path)
  945. @pytest.mark.parametrize("stress_round", range(3))
  946. def test_concurrent_thread_writers_publish_distinct_matching_bundles(
  947. tmp_path: Path,
  948. monkeypatch: pytest.MonkeyPatch,
  949. stress_round: int,
  950. ):
  951. reporting = _reporting_module()
  952. writer_count = 8
  953. markers = [
  954. f"thread-{stress_round}-writer-{index}"
  955. for index in range(writer_count)
  956. ]
  957. first_file_barrier = threading.Barrier(writer_count, timeout=15)
  958. original_write_text = reporting._write_text
  959. thread_state = threading.local()
  960. def synchronized_write(path: Path, content: str) -> None:
  961. original_write_text(path, content)
  962. if not getattr(thread_state, "first_write_done", False):
  963. thread_state.first_write_done = True
  964. first_file_barrier.wait()
  965. monkeypatch.setattr(reporting, "_write_text", synchronized_write)
  966. with ThreadPoolExecutor(max_workers=writer_count) as executor:
  967. results = list(
  968. executor.map(
  969. lambda marker: reporting.write_benchmark_report(
  970. _marker_report(marker),
  971. tmp_path,
  972. timestamp=f"20260714T02030{stress_round}Z",
  973. ),
  974. markers,
  975. )
  976. )
  977. assert len({json_path.parent for json_path, _ in results}) == writer_count
  978. assert all(
  979. json_path.parent == markdown_path.parent
  980. for json_path, markdown_path in results
  981. )
  982. _assert_concurrent_report_pairs(tmp_path, markers)
  983. @pytest.mark.parametrize("stress_round", range(2))
  984. def test_concurrent_process_writers_publish_distinct_matching_bundles(
  985. tmp_path: Path,
  986. stress_round: int,
  987. ):
  988. context = multiprocessing.get_context("spawn")
  989. writer_count = 4
  990. markers = [
  991. f"process-{stress_round}-writer-{index}"
  992. for index in range(writer_count)
  993. ]
  994. first_file_barrier = context.Barrier(writer_count, timeout=20)
  995. result_queue = context.Queue()
  996. processes = [
  997. context.Process(
  998. target=_process_report_writer,
  999. args=(
  1000. str(tmp_path),
  1001. marker,
  1002. f"20260714T03040{stress_round}Z",
  1003. first_file_barrier,
  1004. result_queue,
  1005. ),
  1006. )
  1007. for marker in markers
  1008. ]
  1009. for process in processes:
  1010. process.start()
  1011. results = [result_queue.get(timeout=30) for _ in processes]
  1012. for process in processes:
  1013. process.join(timeout=30)
  1014. assert all(process.exitcode == 0 for process in processes)
  1015. assert all(result[0] == "ok" for result in results), results
  1016. assert all(result[2] == result[3] for result in results)
  1017. assert len({result[2] for result in results}) == writer_count
  1018. _assert_concurrent_report_pairs(tmp_path, markers)