"""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()