fix: recover calibration after smart-cam restart
This commit is contained in:
1 parent
33d87ac9d9
commit
714a2e1db2
3 files changed
+227
-10
No files matched your search
@@ -0,0 +1,111 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import redirect_stderr
|
||||
import io
|
||||
import subprocess
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import scan_calibrator as core
|
||||
|
||||
|
||||
def completed(returncode: int, stdout: str = "", stderr: str = ""):
|
||||
return subprocess.CompletedProcess(["docker"], returncode, stdout, stderr)
|
||||
|
||||
|
||||
class ApiRecoveryTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
core._HELPER_READY = False
|
||||
|
||||
@mock.patch("scan_calibrator.wait_for_scan_http", return_value=True)
|
||||
@mock.patch("scan_calibrator.subprocess.run")
|
||||
def test_transient_curl_failure_waits_and_retries(self, run_mock, wait_mock):
|
||||
core._HELPER_READY = True
|
||||
run_mock.side_effect = [
|
||||
completed(52, stderr="curl: (52) Empty reply from server"),
|
||||
completed(0, stdout='{"ptz": {"pan": 1, "tilt": 2, "zoom": 3}}'),
|
||||
]
|
||||
|
||||
payload = core.api("GET", "/api/v1/ptz", timeout=5)
|
||||
|
||||
self.assertEqual(payload["ptz"]["zoom"], 3)
|
||||
self.assertEqual(run_mock.call_count, 2)
|
||||
wait_mock.assert_called_once_with()
|
||||
self.assertFalse(core._HELPER_READY)
|
||||
|
||||
@mock.patch("scan_calibrator.wait_for_scan_http", return_value=True)
|
||||
@mock.patch("scan_calibrator.subprocess.run")
|
||||
def test_connection_refused_is_retried(self, run_mock, wait_mock):
|
||||
run_mock.side_effect = [
|
||||
completed(7, stderr="curl: (7) Failed to connect to 127.0.0.1"),
|
||||
completed(0, stdout='{"scan": {"state": "idle"}}'),
|
||||
]
|
||||
|
||||
payload = core.api("GET", "/api/v1/scan/status", timeout=5)
|
||||
|
||||
self.assertEqual(payload["scan"]["state"], "idle")
|
||||
wait_mock.assert_called_once_with()
|
||||
|
||||
@mock.patch("scan_calibrator.wait_for_scan_http")
|
||||
@mock.patch("scan_calibrator.subprocess.run")
|
||||
def test_non_transient_http_error_is_not_retried(self, run_mock, wait_mock):
|
||||
run_mock.return_value = completed(
|
||||
22, stderr="curl: (22) The requested URL returned error: 404"
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(SystemExit, "404"):
|
||||
core.api("GET", "/missing", timeout=5)
|
||||
|
||||
self.assertEqual(run_mock.call_count, 1)
|
||||
wait_mock.assert_not_called()
|
||||
|
||||
@mock.patch("scan_calibrator.time.sleep")
|
||||
@mock.patch("scan_calibrator.scan_http_ready", side_effect=[False, True])
|
||||
@mock.patch("scan_calibrator.time.monotonic", side_effect=[0.0, 1.0, 2.0])
|
||||
def test_health_wait_polls_until_service_recovers(
|
||||
self, _monotonic_mock, ready_mock, sleep_mock
|
||||
):
|
||||
self.assertTrue(core.wait_for_scan_http(timeout=10.0))
|
||||
self.assertEqual(ready_mock.call_count, 2)
|
||||
sleep_mock.assert_called_once_with(core.API_RECOVERY_POLL_INTERVAL)
|
||||
|
||||
@mock.patch("scan_calibrator.run")
|
||||
@mock.patch("scan_calibrator.subprocess.run")
|
||||
def test_overlap_helper_compiles_only_once_per_session(
|
||||
self, subprocess_run_mock, checked_run_mock
|
||||
):
|
||||
subprocess_run_mock.return_value = completed(0)
|
||||
|
||||
core.ensure_helper()
|
||||
core.ensure_helper()
|
||||
|
||||
self.assertEqual(checked_run_mock.call_count, 2)
|
||||
subprocess_run_mock.assert_called_once_with(
|
||||
["docker", "exec", "smart-cam", "test", "-x", core.HELPER_CONTAINER_BIN],
|
||||
text=True, capture_output=True, timeout=10,
|
||||
)
|
||||
|
||||
@mock.patch("scan_calibrator.time.sleep")
|
||||
@mock.patch("scan_calibrator.measure_images")
|
||||
@mock.patch("scan_calibrator.snapshot", return_value="candidate.jpg")
|
||||
@mock.patch("scan_calibrator.move")
|
||||
def test_restore_failure_does_not_mask_measurement_failure(
|
||||
self, move_mock, _snapshot_mock, measure_mock, _sleep_mock
|
||||
):
|
||||
origin = {"pan": 100, "tilt": 200, "zoom": 300}
|
||||
move_mock.side_effect = [dict(origin), SystemExit("恢复起点失败")]
|
||||
measure_mock.side_effect = SystemExit("原始测量失败")
|
||||
stderr = io.StringIO()
|
||||
|
||||
with redirect_stderr(stderr):
|
||||
with self.assertRaisesRegex(SystemExit, "原始测量失败"):
|
||||
core.measure_delta(
|
||||
"pan", 50, 1, 0.0, baseline="baseline.jpg", origin=origin
|
||||
)
|
||||
|
||||
self.assertIn("恢复起点失败", stderr.getvalue())
|
||||
self.assertEqual(move_mock.call_count, 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user