a723e96dcc
保留逐台添加,并通过显式配置一次连接全组;失败时撤销本批新增连接。通过 35 项回归测试、真实 SDK 校准与 1–4 路验证、Ruff 和文档检查。
217 lines
8.4 KiB
Python
217 lines
8.4 KiB
Python
"""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()
|