|
|
@@ -125,12 +125,13 @@ def _process_report_writer(
|
|
|
original_write_text = reporting._write_text
|
|
|
first_write = True
|
|
|
|
|
|
- def synchronized_write(path: Path, content: str) -> None:
|
|
|
+ def synchronized_write(path: Path, content: str) -> tuple[int, int]:
|
|
|
nonlocal first_write
|
|
|
- original_write_text(path, content)
|
|
|
+ identity = original_write_text(path, content)
|
|
|
if first_write:
|
|
|
first_write = False
|
|
|
first_temp_barrier.wait()
|
|
|
+ return identity
|
|
|
|
|
|
reporting._write_text = synchronized_write
|
|
|
try:
|
|
|
@@ -588,6 +589,27 @@ def test_atomic_write_creates_matching_unique_json_and_markdown_paths(tmp_path:
|
|
|
assert list(tmp_path.glob("*.tmp")) == []
|
|
|
|
|
|
|
|
|
+def test_second_placeholder_collision_cleans_first_and_uses_suffix(
|
|
|
+ tmp_path: Path,
|
|
|
+):
|
|
|
+ reporting = _reporting_module()
|
|
|
+ timestamp = "20260714T011223Z"
|
|
|
+ external_markdown = tmp_path / f"{timestamp}.md"
|
|
|
+ external_markdown.write_text("external markdown")
|
|
|
+
|
|
|
+ json_path, markdown_path = reporting.write_benchmark_report(
|
|
|
+ _marker_report("second-placeholder-collision"),
|
|
|
+ tmp_path,
|
|
|
+ timestamp=timestamp,
|
|
|
+ )
|
|
|
+
|
|
|
+ assert external_markdown.read_text() == "external markdown"
|
|
|
+ assert not (tmp_path / f"{timestamp}.json").exists()
|
|
|
+ assert json_path.name == f"{timestamp}-1.json"
|
|
|
+ assert markdown_path.name == f"{timestamp}-1.md"
|
|
|
+ assert list(tmp_path.glob("*.lock")) == []
|
|
|
+
|
|
|
+
|
|
|
def test_atomic_write_removes_temps_and_published_half_on_replace_failure(
|
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
|
):
|
|
|
@@ -620,19 +642,46 @@ def test_atomic_write_removes_temps_and_published_half_on_replace_failure(
|
|
|
assert list(tmp_path.iterdir()) == []
|
|
|
|
|
|
|
|
|
-def test_reservation_failure_after_lock_creation_removes_own_lock(
|
|
|
+def test_temp_fsync_failure_removes_owned_temp_and_placeholders(
|
|
|
+ tmp_path: Path,
|
|
|
+ monkeypatch: pytest.MonkeyPatch,
|
|
|
+):
|
|
|
+ reporting = _reporting_module()
|
|
|
+
|
|
|
+ def failing_fsync(file_descriptor: int) -> None:
|
|
|
+ del file_descriptor
|
|
|
+ raise OSError("simulated temp fsync failure")
|
|
|
+
|
|
|
+ monkeypatch.setattr(reporting.os, "fsync", failing_fsync)
|
|
|
+
|
|
|
+ with pytest.raises(
|
|
|
+ reporting.BenchmarkReportWriteError,
|
|
|
+ match="failed to write benchmark reports",
|
|
|
+ ):
|
|
|
+ reporting.write_benchmark_report(
|
|
|
+ _marker_report("temp-fsync-failure"),
|
|
|
+ tmp_path,
|
|
|
+ timestamp="20260714T012334Z",
|
|
|
+ )
|
|
|
+
|
|
|
+ assert list(tmp_path.iterdir()) == []
|
|
|
+
|
|
|
+
|
|
|
+def test_second_placeholder_failure_removes_first_placeholder(
|
|
|
tmp_path: Path,
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
):
|
|
|
reporting = _reporting_module()
|
|
|
- original_exists = Path.exists
|
|
|
+ original_open = reporting.os.open
|
|
|
+ timestamp = "20260714T040506Z"
|
|
|
+ markdown_path = tmp_path / f"{timestamp}.md"
|
|
|
|
|
|
- def failing_exists(path: Path) -> bool:
|
|
|
- if path.parent == tmp_path and path.suffix == ".json":
|
|
|
- raise OSError("simulated reservation path check failure")
|
|
|
- return original_exists(path)
|
|
|
+ def failing_open(path, flags, mode=0o777):
|
|
|
+ if Path(path) == markdown_path:
|
|
|
+ raise OSError("simulated second placeholder failure")
|
|
|
+ return original_open(path, flags, mode)
|
|
|
|
|
|
- monkeypatch.setattr(Path, "exists", failing_exists)
|
|
|
+ monkeypatch.setattr(reporting.os, "open", failing_open)
|
|
|
|
|
|
with pytest.raises(
|
|
|
reporting.BenchmarkReportWriteError,
|
|
|
@@ -641,12 +690,108 @@ def test_reservation_failure_after_lock_creation_removes_own_lock(
|
|
|
reporting.write_benchmark_report(
|
|
|
_marker_report("reservation-failure"),
|
|
|
tmp_path,
|
|
|
- timestamp="20260714T040506Z",
|
|
|
+ timestamp=timestamp,
|
|
|
)
|
|
|
|
|
|
assert list(tmp_path.iterdir()) == []
|
|
|
|
|
|
|
|
|
+def test_reservation_uses_final_placeholders_without_sidecar_lock(
|
|
|
+ tmp_path: Path,
|
|
|
+ monkeypatch: pytest.MonkeyPatch,
|
|
|
+):
|
|
|
+ reporting = _reporting_module()
|
|
|
+ timestamp = "20260714T050607Z"
|
|
|
+ first_temp_written = threading.Event()
|
|
|
+ allow_publish = threading.Event()
|
|
|
+ original_write_text = reporting._write_text
|
|
|
+ first_write = True
|
|
|
+
|
|
|
+ def paused_write(path: Path, content: str) -> tuple[int, int]:
|
|
|
+ nonlocal first_write
|
|
|
+ identity = original_write_text(path, content)
|
|
|
+ if first_write:
|
|
|
+ first_write = False
|
|
|
+ first_temp_written.set()
|
|
|
+ assert allow_publish.wait(timeout=10)
|
|
|
+ return identity
|
|
|
+
|
|
|
+ monkeypatch.setattr(reporting, "_write_text", paused_write)
|
|
|
+ with ThreadPoolExecutor(max_workers=1) as executor:
|
|
|
+ future = executor.submit(
|
|
|
+ reporting.write_benchmark_report,
|
|
|
+ _marker_report("placeholder-owner"),
|
|
|
+ tmp_path,
|
|
|
+ timestamp=timestamp,
|
|
|
+ )
|
|
|
+ assert first_temp_written.wait(timeout=10)
|
|
|
+ json_placeholder = tmp_path / f"{timestamp}.json"
|
|
|
+ markdown_placeholder = tmp_path / f"{timestamp}.md"
|
|
|
+ try:
|
|
|
+ assert json_placeholder.exists()
|
|
|
+ assert markdown_placeholder.exists()
|
|
|
+ assert json_placeholder.read_bytes() == b""
|
|
|
+ assert markdown_placeholder.read_bytes() == b""
|
|
|
+ assert list(tmp_path.glob("*.lock")) == []
|
|
|
+ finally:
|
|
|
+ allow_publish.set()
|
|
|
+ json_path, markdown_path = future.result(timeout=10)
|
|
|
+
|
|
|
+ assert json_path == json_placeholder
|
|
|
+ assert markdown_path == markdown_placeholder
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize("competed_suffix", [".json", ".md"])
|
|
|
+def test_external_replacement_of_reserved_final_is_not_overwritten_or_deleted(
|
|
|
+ tmp_path: Path,
|
|
|
+ monkeypatch: pytest.MonkeyPatch,
|
|
|
+ competed_suffix: str,
|
|
|
+):
|
|
|
+ reporting = _reporting_module()
|
|
|
+ timestamp = "20260714T060708Z"
|
|
|
+ first_temp_written = threading.Event()
|
|
|
+ allow_publish = threading.Event()
|
|
|
+ original_write_text = reporting._write_text
|
|
|
+ first_write = True
|
|
|
+
|
|
|
+ def paused_write(path: Path, content: str) -> tuple[int, int]:
|
|
|
+ nonlocal first_write
|
|
|
+ identity = original_write_text(path, content)
|
|
|
+ if first_write:
|
|
|
+ first_write = False
|
|
|
+ first_temp_written.set()
|
|
|
+ assert allow_publish.wait(timeout=10)
|
|
|
+ return identity
|
|
|
+
|
|
|
+ monkeypatch.setattr(reporting, "_write_text", paused_write)
|
|
|
+ competed_path = tmp_path / f"{timestamp}{competed_suffix}"
|
|
|
+ competitor_temp = tmp_path / f"competitor{competed_suffix}"
|
|
|
+ competitor_content = b"external competitor content"
|
|
|
+
|
|
|
+ with ThreadPoolExecutor(max_workers=1) as executor:
|
|
|
+ future = executor.submit(
|
|
|
+ reporting.write_benchmark_report,
|
|
|
+ _marker_report("must-not-overwrite"),
|
|
|
+ tmp_path,
|
|
|
+ timestamp=timestamp,
|
|
|
+ )
|
|
|
+ assert first_temp_written.wait(timeout=10)
|
|
|
+ competitor_temp.write_bytes(competitor_content)
|
|
|
+ reporting.os.replace(competitor_temp, competed_path)
|
|
|
+ allow_publish.set()
|
|
|
+ with pytest.raises(
|
|
|
+ reporting.BenchmarkReportWriteError,
|
|
|
+ match="failed to write benchmark reports",
|
|
|
+ ):
|
|
|
+ future.result(timeout=10)
|
|
|
+
|
|
|
+ assert competed_path.read_bytes() == competitor_content
|
|
|
+ other_suffix = ".md" if competed_suffix == ".json" else ".json"
|
|
|
+ assert not (tmp_path / f"{timestamp}{other_suffix}").exists()
|
|
|
+ assert list(tmp_path.glob("*.lock")) == []
|
|
|
+ assert list(tmp_path.glob("*.tmp")) == []
|
|
|
+
|
|
|
+
|
|
|
def test_concurrent_thread_writers_reserve_distinct_matching_pairs(
|
|
|
tmp_path: Path,
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
@@ -658,11 +803,12 @@ def test_concurrent_thread_writers_reserve_distinct_matching_pairs(
|
|
|
original_write_text = reporting._write_text
|
|
|
thread_state = threading.local()
|
|
|
|
|
|
- def synchronized_write(path: Path, content: str) -> None:
|
|
|
- original_write_text(path, content)
|
|
|
+ def synchronized_write(path: Path, content: str) -> tuple[int, int]:
|
|
|
+ identity = original_write_text(path, content)
|
|
|
if not getattr(thread_state, "first_write_done", False):
|
|
|
thread_state.first_write_done = True
|
|
|
first_temp_barrier.wait()
|
|
|
+ return identity
|
|
|
|
|
|
monkeypatch.setattr(reporting, "_write_text", synchronized_write)
|
|
|
|