105 lines
3.8 KiB
Python
105 lines
3.8 KiB
Python
# -*- coding: utf-8 -*-
|
||||
|
|
"""可用性监控探针测试(issue #61)。
|
|||
|
|
|
|||
|
|
用本地 ThreadingHTTPServer 模拟健康端点:
|
|||
|
|
1. 稳定 200 → 可用率 100% ≥ 99.8%;
|
|||
|
|
2. 间歇 503 → 可用率按失败比例下降,探针统计正确;
|
|||
|
|
3. 阈值判断:间歇失败超 0.2% 时 meets_target=False;
|
|||
|
|
4. 失败明细记录(URL + 原因)。
|
|||
|
|
"""
|
|||
|
|
import http.server
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import threading
|
|||
|
|
import unittest
|
|||
|
|
|
|||
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|||
|
|
from probe_availability import AvailabilityProbe # noqa: E402
|
|||
|
|
|
|||
|
|
|
|||
|
|
class _HealthHandler(http.server.BaseHTTPRequestHandler):
|
|||
|
|
"""可配置成功/失败模式的 /health 处理器。"""
|
|||
|
|
|
|||
|
|
fail_mode = [] # [True/False...] 按请求序号循环
|
|||
|
|
|
|||
|
|
def do_GET(self):
|
|||
|
|
if self.path == "/health":
|
|||
|
|
if self.fail_mode and self.fail_mode[0]:
|
|||
|
|
self.fail_mode[0] = False # 一次性失败
|
|||
|
|
self.send_response(503)
|
|||
|
|
else:
|
|||
|
|
self.send_response(200)
|
|||
|
|
else:
|
|||
|
|
self.send_response(404)
|
|||
|
|
self.end_headers()
|
|||
|
|
|
|||
|
|
def log_message(self, *args): # 静默日志
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
def start_server(fail_once=False):
|
|||
|
|
handler = _HealthHandler
|
|||
|
|
handler.fail_mode = [True] if fail_once else []
|
|||
|
|
srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
|||
|
|
thread = threading.Thread(target=srv.serve_forever, daemon=True)
|
|||
|
|
thread.start()
|
|||
|
|
return srv
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestProbe(unittest.TestCase):
|
|||
|
|
"""探针统计与阈值判断。"""
|
|||
|
|
|
|||
|
|
def test_stable_endpoint_100pct(self):
|
|||
|
|
srv = start_server()
|
|||
|
|
try:
|
|||
|
|
url = f"http://127.0.0.1:{srv.server_port}"
|
|||
|
|
probe = AvailabilityProbe([url], timeout_seconds=2)
|
|||
|
|
report = probe.run(rounds=10, interval=0)
|
|||
|
|
finally:
|
|||
|
|
srv.shutdown()
|
|||
|
|
self.assertEqual(report.total, 10)
|
|||
|
|
self.assertEqual(report.success, 10)
|
|||
|
|
self.assertEqual(report.availability, 1.0)
|
|||
|
|
self.assertTrue(report.meets_target())
|
|||
|
|
|
|||
|
|
def test_single_failure_below_threshold(self):
|
|||
|
|
srv = start_server(fail_once=True)
|
|||
|
|
try:
|
|||
|
|
url = f"http://127.0.0.1:{srv.server_port}"
|
|||
|
|
probe = AvailabilityProbe([url], timeout_seconds=2)
|
|||
|
|
report = probe.run(rounds=500, interval=0) # 1/500 = 0.2%
|
|||
|
|
finally:
|
|||
|
|
srv.shutdown()
|
|||
|
|
self.assertEqual(len(report.failures), 1)
|
|||
|
|
# 1/500 = 0.002 = 99.8% —— 恰好达标(>= target 判定通过)
|
|||
|
|
self.assertGreaterEqual(report.availability, report.target)
|
|||
|
|
self.assertTrue(report.meets_target())
|
|||
|
|
# urllib 对非 200 抛 HTTPError → 记为不可用
|
|||
|
|
self.assertFalse(report.failures[0].ok)
|
|||
|
|
self.assertIn("503", report.failures[0].detail)
|
|||
|
|
|
|||
|
|
def test_high_failure_rate_fails_threshold(self):
|
|||
|
|
# 构造 50% 失败的端点(fail_mode 每次循环交替:用两个处理器状态不可行,
|
|||
|
|
# 改为直接 mock probe_once)
|
|||
|
|
probe = AvailabilityProbe(["http://x"], timeout_seconds=1)
|
|||
|
|
results = [probe.probe_once.__self__.__class__] # noqa - 占位
|
|||
|
|
from unittest import mock
|
|||
|
|
ok_result = mock.MagicMock()
|
|||
|
|
ok_result.ok = True
|
|||
|
|
bad_result = mock.MagicMock()
|
|||
|
|
bad_result.ok = False
|
|||
|
|
bad_result.url = "http://x"
|
|||
|
|
bad_result.detail = "http-503"
|
|||
|
|
bad_result.latency_ms = 1.0
|
|||
|
|
# 交替 200/503 → 6 次探测 3 成功 → 50% 可用率,远低于 99.8%
|
|||
|
|
with mock.patch.object(probe, "probe_once",
|
|||
|
|
side_effect=[ok_result, bad_result] * 3):
|
|||
|
|
report = probe.run(rounds=6, interval=0)
|
|||
|
|
self.assertEqual(report.availability, 0.5)
|
|||
|
|
self.assertFalse(report.meets_target())
|
|||
|
|
self.assertEqual(len(report.failures), 3)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
unittest.main()
|