Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,14 @@ python3 -m venv .venv
- [插件开发](docs/development/plugin-development.md)
- [更新日志](CHANGELOG.md)

WaveBench 使用 MIT 许可证。感谢 Linux DO 社区提供交流和支持。
WaveBench 使用 MIT 许可证。

## 特别感谢

<img src="https://cdn3.ldstatic.com/original/3X/9/7/97ed5d6d97f4c7f3dc0670d097bf457527c375f5.png" alt="linuxDoLogo" width="150" />

感谢 [Linux DO 社区](https://linux.do/)提供交流和支持。

<img src="https://www.krill-code.com/brand/logo-horizontal.png" alt="Krill" width="150" />

感谢 [Krill AI](https://www.krill-code.com/) 对本项目的赞助。
7 changes: 4 additions & 3 deletions src/wavebench/services/run_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
57 changes: 55 additions & 2 deletions tests/test_analysis_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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')
Expand Down
Loading