支持传感器独立校准文件与批量配置启动
保留逐台添加,并通过显式配置一次连接全组;失败时撤销本批新增连接。通过 35 项回归测试、真实 SDK 校准与 1–4 路验证、Ruff 和文档检查。
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user