From 86993921e6455710e1111cc4ec2f9ffb19ce7dbd Mon Sep 17 00:00:00 2001 From: Scaxlibur <51772892+Scaxlibur@users.noreply.github.com> Date: Wed, 9 Sep 2026 12:01:41 +0800 Subject: [PATCH 1/3] docs: update README sponsor links --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ead3ec69..cc8604c2 100644 --- a/README.md +++ b/README.md @@ -111,4 +111,14 @@ python3 -m venv .venv - [插件开发](docs/development/plugin-development.md) - [更新日志](CHANGELOG.md) -WaveBench 使用 MIT 许可证。感谢 Linux DO 社区提供交流和支持。 +WaveBench 使用 MIT 许可证。 + +## 特别感谢 + +linuxDoLogo + +感谢 [Linux DO 社区](https://linux.do/)提供交流和支持。 + +Krill + +感谢 [Krill中转站](https://www.krill-code.com/) 对本项目的赞助。 \ No newline at end of file From 80e9399480bd66b0abbcea94cbe72ab900a68919 Mon Sep 17 00:00:00 2001 From: Scaxlibur <51772892+Scaxlibur@users.noreply.github.com> Date: Wed, 9 Sep 2026 12:09:27 +0800 Subject: [PATCH 2/3] docs: refine sponsor wording --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc8604c2..a4b64353 100644 --- a/README.md +++ b/README.md @@ -121,4 +121,4 @@ WaveBench 使用 MIT 许可证。 Krill -感谢 [Krill中转站](https://www.krill-code.com/) 对本项目的赞助。 \ No newline at end of file +感谢 [Krill AI](https://www.krill-code.com/) 对本项目的赞助。 \ No newline at end of file From 7adf802b9fd49bd838ffc1ae064371b09310fc4b Mon Sep 17 00:00:00 2001 From: Scaxlibur <51772892+Scaxlibur@users.noreply.github.com> Date: Wed, 9 Sep 2026 12:27:57 +0800 Subject: [PATCH 3/3] fix: retry atomic analysis file replacement on Windows --- src/wavebench/services/run_pipeline.py | 7 ++-- tests/test_analysis_resources.py | 57 +++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/wavebench/services/run_pipeline.py b/src/wavebench/services/run_pipeline.py index de805092..656ee920 100644 --- a/src/wavebench/services/run_pipeline.py +++ b/src/wavebench/services/run_pipeline.py @@ -37,6 +37,7 @@ from wavebench.services.run_analysis import evaluate_expect from wavebench.services.run_artifacts import RunStepRecord from wavebench.services.run_plan import RunPlan, RunStep +from wavebench.services.platform_io import _replace_file ANALYSIS_PIPELINE_SCHEMA = "wavebench.analysis_pipeline.v1" @@ -765,7 +766,7 @@ def blocks(): file.write(encoded) file.flush() os.fsync(file.fileno()) - os.replace(temporary, path) + _replace_file(temporary, path) if budget: budget.committed_file(path.stat().st_size) finally: @@ -796,7 +797,7 @@ def blocks(): budget.pending_file(file.tell()) file.flush() os.fsync(file.fileno()) - os.replace(temporary, path) + _replace_file(temporary, path) if budget: budget.committed_file(path.stat().st_size) finally: @@ -812,7 +813,7 @@ def _atomic_write_bytes(path: Path, data: bytes, *, budget: AnalysisBudget | Non file.write(data) file.flush() os.fsync(file.fileno()) - os.replace(temporary, path) + _replace_file(temporary, path) if budget: budget.committed_file(len(data)) finally: diff --git a/tests/test_analysis_resources.py b/tests/test_analysis_resources.py index 9457461d..b2b9b929 100644 --- a/tests/test_analysis_resources.py +++ b/tests/test_analysis_resources.py @@ -3,6 +3,7 @@ from hashlib import sha256 import io from unittest.mock import patch +from types import SimpleNamespace import numpy as np import pytest @@ -15,7 +16,7 @@ from wavebench.errors import ConfigError, DataError, ExecutionIntentError, error_envelope from wavebench.services.analysis_service import run_analysis, check_analysis from wavebench.services.execution_intent import build_execution_intent, verify_execution_intent -from wavebench.services.run_pipeline import _atomic_write_csv, _export_signal +from wavebench.services.run_pipeline import _atomic_write_csv, _atomic_write_npy, _atomic_write_bytes, _export_signal from wavebench.report.analysis import read_curve, write_analysis_report from test_analysis_service import analysis_input as analysis_input from test_psd_pipeline import PARAMS, plan_for, EXPORT @@ -147,12 +148,64 @@ def test_write_limit_and_io_failure_cleanup(tmp_path): _atomic_write_csv(path, ["x", "y"], np.ones((100, 2)), budget=AnalysisBudget(replace(AnalysisLimits(), max_output_bytes=10))) assert path.read_text() == "keep original" - with patch("wavebench.services.run_pipeline.os.replace", side_effect=OSError("disk full")): + with patch("wavebench.services.run_pipeline._replace_file", side_effect=OSError("disk full")): with pytest.raises(OSError): _atomic_write_csv(path, ["x", "y"], np.ones((2, 2))) assert list(tmp_path.iterdir()) == [path] +@pytest.mark.parametrize("kind", ["bytes", "npy", "csv"]) +@pytest.mark.parametrize("persistent", [False, True]) +def test_analysis_windows_replace_contention(tmp_path, monkeypatch, kind, persistent): + from unittest.mock import Mock + from wavebench.services import platform_io + + path = tmp_path / "result" + path.write_bytes(b"keep original") + real_replace = platform_io.os.replace + attempts = [] + + def move(source, target, flags): + attempts.append((source, target)) + assert path.read_bytes() == b"keep original" + if persistent or len(attempts) == 1: + return 0 + real_replace(source, target) + return 1 + + kernel32 = SimpleNamespace(MoveFileExW=Mock(side_effect=move)) + monkeypatch.setattr(platform_io, "os", SimpleNamespace(name="nt")) + monkeypatch.setattr(platform_io.ctypes, "WinDLL", lambda *a, **kw: kernel32, raising=False) + monkeypatch.setattr(platform_io.ctypes, "get_last_error", lambda: 32, raising=False) + sleep = Mock() + monkeypatch.setattr(platform_io.time, "sleep", sleep) + budget = AnalysisBudget(AnalysisLimits()) + + def write(): + if kind == "bytes": + _atomic_write_bytes(path, b"new contents", budget=budget) + elif kind == "npy": + _atomic_write_npy(path, np.ones((2, 2)), budget=budget) + else: + _atomic_write_csv(path, ["x", "y"], np.ones((2, 2)), budget=budget) + + if persistent: + with pytest.raises(OSError, match="MoveFileExW failed"): + write() + assert path.read_bytes() == b"keep original" + assert budget.output_files == budget.output_bytes == 0 + assert len(attempts) == 3 + else: + write() + assert path.read_bytes() != b"keep original" + assert budget.output_files == 1 + assert budget.output_bytes == path.stat().st_size + assert len(attempts) == 2 + assert sleep.call_count == len(attempts) - 1 + assert len(set(attempts)) == 1 + assert list(tmp_path.iterdir()) == [path] + + def test_runtime_fft_rejected_before_call_and_previous_export_retained(tmp_path, analysis_input): capture, recipe = analysis_input recipe.write_text('schema="wavebench.analysis_recipe.v1"\noperations=[{op="export",name="before",formats=["npy"]},{op="fft"},{op="export",name="after",formats=["npy"]}]\n[resources]\nmax_fft_length=64\n')