支持传感器独立校准文件与批量配置启动

保留逐台添加,并通过显式配置一次连接全组;失败时撤销本批新增连接。通过 35 项回归测试、真实 SDK 校准与 1–4 路验证、Ruff 和文档检查。
This commit is contained in:
ykcheungab
2026-10-14 16:11:15 +08:00
parent 133a98e418
commit a723e96dcc
7 changed files with 460 additions and 24 deletions
+216
View File
@@ -0,0 +1,216 @@
"""Calibration binding and transactional setup regression tests."""
import json
import sys
import uuid
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "serial"))
import example_uart as uart
from finger_api import FingerSensorBank
from test_uart import ByteSerial, FakeRuntime, HardwareFreeTest
class SetupTests(HardwareFreeTest):
def setUp(self):
super().setUp()
self.folder = (
Path(__file__).resolve().parents[1]
/ ".scratch/uart-calibration-setup/tests"
/ uuid.uuid4().hex
)
self.folder.mkdir(parents=True)
self.calibration = self.folder / "sensor calibration.json"
self.calibration.write_text("{}", encoding="utf-8")
def setup_file(self, sensors):
path = self.folder / "sensors.json"
path.write_text(json.dumps({"sensors": sensors}), encoding="utf-8")
return path
def test_explicit_calibration_is_bound_to_correct_runtime(self):
bank = FingerSensorBank()
with patch(
"orisys.finger.FingerRuntime.open", return_value=FakeRuntime()
) as factory:
bank.add(0, cal_path=str(self.calibration))
self.assertEqual(
factory.call_args.kwargs["cal_path"], str(self.calibration.resolve())
)
self.assertEqual(bank.calibration_paths[1], str(self.calibration.resolve()))
def test_missing_and_empty_calibration_do_not_fall_back(self):
for calibration in ("", str(self.folder / "missing.json")):
with (
self.subTest(calibration=calibration),
patch("orisys.finger.FingerRuntime.open") as factory,
):
with self.assertRaises(ValueError):
FingerSensorBank().add(0, cal_path=calibration)
factory.assert_not_called()
def test_uart_add_accepts_calibration_and_optional_config(self):
controller = uart.UartFingerController(ByteSerial())
command = f'add 0 custom.json --calibration "{self.calibration}"'
with patch(
"orisys.finger.FingerRuntime.open", return_value=FakeRuntime()
) as factory:
controller._dispatch(command)
self.assertEqual(controller.ser.output, ["OK added 1"])
self.assertEqual(factory.call_args.kwargs["config"], "custom.json")
self.assertEqual(
factory.call_args.kwargs["cal_path"], str(self.calibration.resolve())
)
def test_setup_paths_are_relative_to_setup_file(self):
clip = self.folder / "recording.avi"
clip.touch()
config = self.folder / "sensor.json"
config.write_text("{}", encoding="utf-8")
setup = self.setup_file(
[
{
"source": "recording.avi",
"config": "sensor.json",
"calibration": self.calibration.name,
},
{"source": 2},
]
)
seen = []
def open_runtime(**kwargs):
seen.append(kwargs)
return FakeRuntime()
bank = FingerSensorBank()
with patch("orisys.finger.FingerRuntime.open", side_effect=open_runtime):
self.assertEqual(bank.add_setup(setup), (1, 2))
self.assertEqual(seen[0]["video"], str(clip.resolve()))
self.assertEqual(seen[0]["config"], str(config.resolve()))
self.assertEqual(seen[0]["cal_path"], str(self.calibration.resolve()))
self.assertNotIn("cal_path", seen[1])
def test_distinct_calibration_files_are_selected_in_source_order(self):
second = self.folder / "second.json"
second.write_text("{}", encoding="utf-8")
setup = self.setup_file(
[
{"source": 0, "calibration": self.calibration.name},
{"source": 2, "calibration": second.name},
]
)
seen = []
def open_runtime(**kwargs):
seen.append(kwargs["cal_path"])
return FakeRuntime()
with patch("orisys.finger.FingerRuntime.open", side_effect=open_runtime):
FingerSensorBank().add_setup(setup)
self.assertEqual(seen, [str(self.calibration.resolve()), str(second.resolve())])
def test_batch_failure_rolls_back_only_new_sensors(self):
bank = FingerSensorBank()
bank.add(0)
original = bank.runtimes[1]
opened = []
def open_runtime(**kwargs):
runtime = FakeRuntime(warmup_error=kwargs["video"] == 4)
opened.append(runtime)
return runtime
setup = self.setup_file([{"source": 2}, {"source": 4}])
with (
patch("orisys.finger.FingerRuntime.open", side_effect=open_runtime),
self.assertRaises(RuntimeError),
):
bank.add_setup(setup)
self.assertEqual(bank.sources, {1: 0})
self.assertEqual(bank.calibration_paths, {1: None})
self.assertIs(bank.runtimes[1], original)
self.assertFalse(original.closed)
self.assertTrue(all(runtime.closed for runtime in opened))
self.assertEqual(bank.add(6), 2)
def test_setup_rejects_typos_bad_types_duplicates_and_excess_count_before_open(
self,
):
for sensors in (
[],
[{"source": True}],
[{"source": 0, "calibration_file": "oops"}],
[{"source": 0}, {"source": "/dev/video0"}],
[{"source": i} for i in range(5)],
[{"source": 0, "calibration": 42}],
[{"source": 0, "config": ""}],
):
with (
self.subTest(sensors=sensors),
patch("orisys.finger.FingerRuntime.open") as factory,
):
with self.assertRaises(ValueError):
FingerSensorBank().add_setup(self.setup_file(sensors))
factory.assert_not_called()
def test_uart_load_can_extend_manual_setup_but_requires_stop(self):
setup = self.setup_file([{"source": 2}, {"source": 4}, {"source": 6}])
controller = uart.UartFingerController(ByteSerial())
controller._dispatch("add 0")
controller._dispatch("start")
controller._dispatch(f'load "{setup}"')
self.assertEqual(controller.ser.output[-1], "ERR load stop streaming first")
controller._dispatch("stop")
controller._dispatch(f'load "{setup}"')
self.assertEqual(controller.ser.output[-1], "OK loaded 2 3 4")
self.assertFalse(controller._streaming)
def test_one_cli_command_loads_four_sensors_and_starts_output(self):
setup = self.setup_file(
[{"source": i * 2, "calibration": self.calibration.name} for i in range(4)]
)
ser = ByteSerial()
ser.closed = False
ser.close = lambda: setattr(ser, "closed", True)
original_write = ser.write
def write(data):
size = original_write(data)
if (
data.startswith(b"force,")
and sum(line.startswith("force,") for line in ser.output) == 4
):
ser.input.extend(b"quit\n")
return size
ser.write = write
with patch.object(uart, "open_serial", return_value=ser):
self.assertEqual(uart.main(["--setup", str(setup), "--start"]), 0)
self.assertEqual(ser.output[0], "OK ready 4")
self.assertEqual(sum(line.startswith("force,") for line in ser.output), 4)
self.assertTrue(ser.closed)
def test_start_option_requires_setup(self):
with (
patch.object(uart, "open_serial") as serial,
self.assertRaises(SystemExit) as status,
):
uart.main(["--start"])
self.assertEqual(status.exception.code, 2)
serial.assert_not_called()
def test_setup_without_start_waits_for_uart_commands(self):
setup = self.setup_file([{"source": 0}, {"source": 2}])
ser = ByteSerial(b"quit\n")
ser.close = lambda: None
with patch.object(uart, "open_serial", return_value=ser):
self.assertEqual(uart.main(["--setup", str(setup)]), 0)
self.assertEqual(ser.output, ["OK ready 2", "OK quit"])
def test_bad_startup_setup_never_opens_uart(self):
setup = self.setup_file([{"source": 0, "calibration": "missing.json"}])
with patch.object(uart, "open_serial") as serial:
self.assertEqual(uart.main(["--setup", str(setup)]), 1)
serial.assert_not_called()