Skip to content
Merged
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
62 changes: 48 additions & 14 deletions tests/test_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import threading
import time
from collections.abc import Callable

import pytest

Expand Down Expand Up @@ -158,32 +159,48 @@ def test_remote_check_disabled() -> None:


def test_run_loop_stops_cleanly() -> None:
# Use a healthy manager: with backoff enabled an unhealthy one would grow
# the interval and could starve the ">= 3 checks" assertion below.
"""循环反复检查,而设置停止事件后线程真的结束了。

不按墙钟断言:"睡 0.3 秒后至少 3 次检查"考的是**这台机器够快**,而 CI 上带
coverage 插桩的 macOS runner 把 ``run_loop`` 的一轮从标称 50ms 拉到 120ms,
于是等到 2 次就断言失败(main 上真实发生过)。这里改成:次数用带截止时间的
等待,"停下来"则用 join 证明——那才是测试名字里的 stops cleanly。
"""
# 用健康的 manager:不健康会触发退避,间隔指数增长,把"若干次检查"饿死。
hc = HealthChecker(_TM(alive=True, ports="list"), _hc())
seen: list[HealthStatus] = []
before_threads = set(threading.enumerate())
stop = hc.run_loop(interval=0.05, callback=lambda st: seen.append(st))
assert isinstance(stop, threading.Event)
time.sleep(0.3)
monitor = next((t for t in threading.enumerate() if t not in before_threads), None)
assert monitor is not None, "run_loop 没有起后台线程"

_wait_for_values(seen, 3)

checks_at_stop = len(seen)
stop.set()
time.sleep(0.15)
assert len(seen) >= 3, len(seen)
monitor.join(timeout=5)
assert not monitor.is_alive(), "设置停止事件后循环没有退出"
# 已经进入 check() 的那一轮仍会回调一次(实现只保证在 wait 之前查一次事件),
# 再多就说明停止没生效。线程已 join,计数此后不会再变,所以这是确定性的。
late = len(seen) - checks_at_stop
assert late <= 1, f"停止后又回调了 {late} 次"
assert all(isinstance(st, HealthStatus) for st in seen)
n = len(seen)
time.sleep(0.15)
assert len(seen) == n, (len(seen), n)


def test_run_loop_callback_error_swallowed() -> None:
"""回调抛出的异常被记下来,而不是把监控线程弄死。"""
hc = HealthChecker(_TM(alive=True, ports="dict"), _hc())

def bad_cb(_st: HealthStatus) -> None:
raise ValueError("cb boom")

stop = hc.run_loop(interval=0.02, callback=bad_cb)
time.sleep(0.15)
_wait_for(
lambda: isinstance(hc.last_callback_error, ValueError),
lambda: f"回调异常没被记录:{hc.last_callback_error!r}",
)
stop.set()
assert isinstance(hc.last_callback_error, ValueError)


def test_health_status_str() -> None:
Expand Down Expand Up @@ -281,12 +298,29 @@ def test_backoff_interval_formula() -> None:
assert HealthChecker._backoff_interval(60.0, 10, 300.0) == 300.0


def _wait_for_values(values: list, n: int, timeout: float = 3.0) -> None:
"""Busy-wait until ``values`` has at least ``n`` entries."""
def _wait_for(
predicate: Callable[[], bool],
describe: Callable[[], str],
timeout: float = 5.0,
) -> None:
"""Wait for *predicate* on a deadline, instead of napping a fixed time.

固定秒数的 ``sleep`` 断言的是"这台机器够快",而不是被测行为。``describe``
是个函数:失败信息要在**等过之后**才取,否则它记的是等待前那一刻的状态。
"""
deadline = time.time() + timeout
while len(values) < n and time.time() < deadline:
while not predicate() and time.time() < deadline:
time.sleep(0.01)
assert len(values) >= n, f"captured only {len(values)} values, need {n}"
assert predicate(), describe()


def _wait_for_values(values: list, n: int, timeout: float = 5.0) -> None:
"""Busy-wait until ``values`` has at least ``n`` entries."""
_wait_for(
lambda: len(values) >= n,
lambda: f"captured only {len(values)} values, need {n}",
timeout,
)


def _fake_wait_recorder(
Expand Down
Loading