Files
2026-07-13 12:34:57 +08:00

136 lines
4.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""run_primary_first / clear_markers 单元测试"""
import os
import threading
import time
import pytest
from myboot.utils import worker_sync
from myboot.utils.worker_sync import run_primary_first, clear_markers
@pytest.fixture(autouse=True)
def isolated_marker_dir(tmp_path, monkeypatch):
"""每个测试使用独立的标记目录,避免相互污染。"""
import tempfile
monkeypatch.setattr(tempfile, "gettempdir", lambda: str(tmp_path))
monkeypatch.setenv("MYBOOT_APP_NAME", "testapp")
monkeypatch.delenv("MYBOOT_IS_PRIMARY_WORKER", raising=False)
yield tmp_path
def _marker_dir(tmp_path):
return tmp_path / "myboot_worker_sync_testapp"
def test_single_process_passthrough():
"""无环境变量时默认 primary,直通执行并返回值。"""
assert run_primary_first("m1", lambda: 42) == 42
def test_primary_writes_done(monkeypatch, isolated_marker_dir):
monkeypatch.setenv("MYBOOT_IS_PRIMARY_WORKER", "1")
assert run_primary_first("model", lambda: "ok") == "ok"
done = _marker_dir(isolated_marker_dir) / "model.done"
assert done.exists()
assert done.read_text(encoding="utf-8") # ISO 时间戳非空
def test_secondary_waits_then_runs_secondary_fn(monkeypatch, isolated_marker_dir):
"""secondary 在另一线程先等待,primary 后完成 → secondary 执行 secondary_fn。"""
results = {}
def secondary_thread():
monkeypatch.setenv("MYBOOT_IS_PRIMARY_WORKER", "0")
results["secondary"] = run_primary_first(
"model",
primary_fn=lambda: "heavy",
secondary_fn=lambda: "light",
timeout=5.0,
poll_interval=0.05,
)
monkeypatch.setenv("MYBOOT_IS_PRIMARY_WORKER", "0")
t = threading.Thread(target=secondary_thread)
t.start()
time.sleep(0.2)
# primary 完成
monkeypatch.setenv("MYBOOT_IS_PRIMARY_WORKER", "1")
assert run_primary_first("model", lambda: "heavy") == "heavy"
monkeypatch.setenv("MYBOOT_IS_PRIMARY_WORKER", "0")
t.join(timeout=5)
assert not t.is_alive()
assert results["secondary"] == "light"
def test_secondary_defaults_to_primary_fn(monkeypatch, isolated_marker_dir):
"""secondary_fn 为 None 时 secondary 执行 primary_fn。"""
monkeypatch.setenv("MYBOOT_IS_PRIMARY_WORKER", "1")
run_primary_first("m2", lambda: 1)
monkeypatch.setenv("MYBOOT_IS_PRIMARY_WORKER", "0")
assert run_primary_first("m2", lambda: 1, timeout=1.0, poll_interval=0.05) == 1
def test_primary_failure_writes_failed_and_raises(monkeypatch, isolated_marker_dir):
monkeypatch.setenv("MYBOOT_IS_PRIMARY_WORKER", "1")
def boom():
raise ValueError("download failed")
with pytest.raises(ValueError, match="download failed"):
run_primary_first("model", boom)
failed = _marker_dir(isolated_marker_dir) / "model.failed"
assert failed.exists()
assert "download failed" in failed.read_text(encoding="utf-8")
# secondary 检测到 .failed → RuntimeError 含异常信息
monkeypatch.setenv("MYBOOT_IS_PRIMARY_WORKER", "0")
with pytest.raises(RuntimeError, match="download failed"):
run_primary_first("model", lambda: 1, timeout=2.0, poll_interval=0.05)
def test_secondary_timeout(monkeypatch):
monkeypatch.setenv("MYBOOT_IS_PRIMARY_WORKER", "0")
with pytest.raises(TimeoutError):
run_primary_first("never", lambda: 1, timeout=0.3, poll_interval=0.05)
def test_stale_done_marker_ignored(monkeypatch, isolated_marker_dir):
"""上一轮遗留的旧 .donemtime 过旧)不会让 secondary 跳过等待。"""
d = _marker_dir(isolated_marker_dir)
d.mkdir(parents=True, exist_ok=True)
stale = d / "model.done"
stale.write_text("old-run", encoding="utf-8")
old = worker_sync._MODULE_LOAD_TIME - 100
os.utime(stale, (old, old))
monkeypatch.setenv("MYBOOT_IS_PRIMARY_WORKER", "0")
with pytest.raises(TimeoutError):
run_primary_first("model", lambda: 1, timeout=0.3, poll_interval=0.05)
def test_primary_clears_stale_markers(monkeypatch, isolated_marker_dir):
"""primary 执行前清理旧标记,成功后 .failed 不残留。"""
d = _marker_dir(isolated_marker_dir)
d.mkdir(parents=True, exist_ok=True)
(d / "model.failed").write_text("old failure", encoding="utf-8")
monkeypatch.setenv("MYBOOT_IS_PRIMARY_WORKER", "1")
assert run_primary_first("model", lambda: "ok") == "ok"
assert not (d / "model.failed").exists()
assert (d / "model.done").exists()
def test_clear_markers(monkeypatch, isolated_marker_dir):
monkeypatch.setenv("MYBOOT_IS_PRIMARY_WORKER", "1")
run_primary_first("a", lambda: 1)
run_primary_first("b", lambda: 1)
d = _marker_dir(isolated_marker_dir)
clear_markers("a")
assert not (d / "a.done").exists()
assert (d / "b.done").exists()
clear_markers()
assert not (d / "b.done").exists()