支持通过 UART 手动逐台添加相机再启动输出
保留已连接相机,添加失败不占用编号,并要求输出停止后才能新增。通过 23 项回归测试、真实 SDK 的 1–4 路逐台添加验证、Ruff 和文档检查。
This commit is contained in:
@@ -55,9 +55,13 @@ def main():
|
||||
open_runtime = partial(FingerRuntime.open, map_cache=str(scratch / "uv-map.npz"))
|
||||
with patch.object(FingerRuntime, "open", side_effect=open_runtime):
|
||||
for count in range(1, 5):
|
||||
bank = FingerSensorBank(sources[:count])
|
||||
bank = FingerSensorBank()
|
||||
try:
|
||||
bank.connect_all()
|
||||
for source in sources[:count]:
|
||||
existing = dict(bank.runtimes)
|
||||
bank.add(source)
|
||||
assert all(bank.runtimes[i] is runtime for i, runtime in existing.items())
|
||||
assert len(bank.read_all()) == len(bank.runtimes)
|
||||
runtimes = list(bank.runtimes.values())
|
||||
for left, right in itertools.combinations(runtimes, 2):
|
||||
assert left.capture is not right.capture
|
||||
@@ -67,7 +71,7 @@ def main():
|
||||
readings = bank.read_all()
|
||||
assert [r.sensor_id for r in readings] == list(range(1, count + 1))
|
||||
assert all(np.isfinite([r.normal, r.shear_x, r.shear_y]).all() for r in readings)
|
||||
print(f"PASS {count} sources: independent captures/processors, 3 complete rounds", flush=True)
|
||||
print(f"PASS {count} sources: incremental add/read, independent states, 3 final rounds", flush=True)
|
||||
finally:
|
||||
bank.close()
|
||||
assert all(runtime.capture is None for runtime in runtimes)
|
||||
|
||||
+219
-139
@@ -90,98 +90,194 @@ class HardwareFreeTest(unittest.TestCase):
|
||||
stderr.__enter__()
|
||||
self.addCleanup(stderr.__exit__, None, None, None)
|
||||
finger = types.ModuleType("orisys.finger")
|
||||
finger.FingerRuntime = types.SimpleNamespace(open=lambda **kwargs: FakeRuntime())
|
||||
finger.FingerRuntime = types.SimpleNamespace(
|
||||
open=lambda **kwargs: FakeRuntime()
|
||||
)
|
||||
capture = types.ModuleType("orisys.capture")
|
||||
capture.is_live_camera = lambda source: isinstance(source, int) or str(source).startswith("/dev/video")
|
||||
capture.is_live_camera = lambda source: (
|
||||
isinstance(source, int) or str(source).startswith("/dev/video")
|
||||
)
|
||||
capture.list_cameras = lambda **kwargs: []
|
||||
package = types.ModuleType("orisys")
|
||||
package.finger, package.capture = finger, capture
|
||||
modules = patch.dict(sys.modules, {
|
||||
"orisys": package, "orisys.finger": finger, "orisys.capture": capture,
|
||||
})
|
||||
modules = patch.dict(
|
||||
sys.modules,
|
||||
{
|
||||
"orisys": package,
|
||||
"orisys.finger": finger,
|
||||
"orisys.capture": capture,
|
||||
},
|
||||
)
|
||||
modules.start()
|
||||
self.addCleanup(modules.stop)
|
||||
|
||||
|
||||
class ConnectionFirstTests(HardwareFreeTest):
|
||||
def test_connects_every_source_before_ready_or_force(self):
|
||||
class ManualAddTests(HardwareFreeTest):
|
||||
def test_service_starts_empty_without_opening_a_camera(self):
|
||||
ser = ByteSerial(b"quit\n")
|
||||
controller = uart.UartFingerController(ser)
|
||||
with patch("orisys.finger.FingerRuntime.open") as open_runtime:
|
||||
controller.serve()
|
||||
open_runtime.assert_not_called()
|
||||
self.assertEqual(ser.output, ["OK ready 0", "OK quit"])
|
||||
|
||||
def test_add_one_through_four_then_start_produces_uniform_output(self):
|
||||
for count in range(1, 5):
|
||||
with self.subTest(count=count):
|
||||
ser = ByteSerial()
|
||||
controller = uart.UartFingerController(ser, sources=list(range(count)))
|
||||
events = []
|
||||
def open_runtime(events=events, **kwargs):
|
||||
events.append(kwargs["video"])
|
||||
return FakeRuntime()
|
||||
commands = "".join(f"add {i * 2}\n" for i in range(count)) + "start\n"
|
||||
ser = ByteSerial(commands.encode())
|
||||
controller = uart.UartFingerController(ser)
|
||||
original_write = ser.write
|
||||
def write(data, events=events, count=count, ser=ser, original_write=original_write):
|
||||
self.assertEqual(events, list(range(count)))
|
||||
|
||||
def write(data, ser=ser, original_write=original_write, count=count):
|
||||
size = original_write(data)
|
||||
if data.startswith(b"force,") and sum(x.startswith("force,") for x in ser.output) == count:
|
||||
if (
|
||||
data.startswith(b"force,")
|
||||
and sum(x.startswith("force,") for x in ser.output) == count
|
||||
):
|
||||
ser.input.extend(b"quit\n")
|
||||
return size
|
||||
|
||||
ser.write = write
|
||||
with patch("orisys.finger.FingerRuntime.open", side_effect=open_runtime):
|
||||
controller.bank.connect_all()
|
||||
controller.serve()
|
||||
self.assertEqual(ser.output[0], f"OK ready {count}")
|
||||
self.assertEqual([x for x in ser.output if x.startswith("force,")], [
|
||||
f"force,{i},1.250000,-2.500000,3.750000" for i in range(1, count + 1)
|
||||
])
|
||||
controller.serve()
|
||||
self.assertEqual(
|
||||
ser.output[: count + 2],
|
||||
["OK ready 0"]
|
||||
+ [f"OK added {i}" for i in range(1, count + 1)]
|
||||
+ ["OK start"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[x for x in ser.output if x.startswith("force,")],
|
||||
[
|
||||
f"force,{i},1.250000,-2.500000,3.750000"
|
||||
for i in range(1, count + 1)
|
||||
],
|
||||
)
|
||||
controller.close_runtime()
|
||||
|
||||
def test_one_failed_connection_rolls_back_every_opened_camera(self):
|
||||
bank = FingerSensorBank([0, 2, 4])
|
||||
opened = []
|
||||
def open_runtime(**kwargs):
|
||||
runtime = FakeRuntime(warmup_error=kwargs["video"] == 2)
|
||||
opened.append(runtime)
|
||||
return runtime
|
||||
with (patch("orisys.finger.FingerRuntime.open", side_effect=open_runtime),
|
||||
self.assertRaisesRegex(RuntimeError, "sensor 2")):
|
||||
bank.connect_all()
|
||||
def test_add_alone_does_not_start_output(self):
|
||||
ser = ByteSerial(b"add 0\nquit\n")
|
||||
uart.UartFingerController(ser).serve()
|
||||
self.assertEqual(ser.output, ["OK ready 0", "OK added 1", "OK quit"])
|
||||
|
||||
def test_start_and_read_require_at_least_one_sensor(self):
|
||||
ser = ByteSerial()
|
||||
controller = uart.UartFingerController(ser)
|
||||
for command in ("start", "read"):
|
||||
controller._dispatch(command)
|
||||
self.assertTrue(ser.output[-1].startswith(f"ERR {command}"))
|
||||
self.assertFalse(controller._streaming)
|
||||
|
||||
def test_add_requires_stop_and_never_interrupts_existing_stream(self):
|
||||
ser = ByteSerial()
|
||||
controller = uart.UartFingerController(ser)
|
||||
controller._dispatch("add 0")
|
||||
controller._dispatch("start")
|
||||
first = controller.bank.runtimes[1]
|
||||
with patch("orisys.finger.FingerRuntime.open") as open_runtime:
|
||||
controller._dispatch("add 2")
|
||||
open_runtime.assert_not_called()
|
||||
self.assertEqual(ser.output[-1], "ERR add stop streaming first")
|
||||
self.assertTrue(controller._streaming)
|
||||
controller._dispatch("stop")
|
||||
controller._dispatch("add 2")
|
||||
self.assertEqual(ser.output[-1], "OK added 2")
|
||||
self.assertIs(controller.bank.runtimes[1], first)
|
||||
self.assertFalse(first.closed)
|
||||
self.assertFalse(controller._streaming)
|
||||
|
||||
def test_failed_add_preserves_other_sensor_and_reuses_available_id(self):
|
||||
bank = FingerSensorBank()
|
||||
self.assertEqual(bank.add(0), 1)
|
||||
first = bank.runtimes[1]
|
||||
failed = FakeRuntime(warmup_error=True)
|
||||
with (
|
||||
patch("orisys.finger.FingerRuntime.open", return_value=failed),
|
||||
self.assertRaises(RuntimeError),
|
||||
):
|
||||
bank.add(2)
|
||||
self.assertTrue(failed.closed)
|
||||
self.assertFalse(first.closed)
|
||||
self.assertEqual(bank.sources, {1: 0})
|
||||
self.assertEqual(bank.add(4), 2)
|
||||
self.assertEqual(bank.sources, {1: 0, 2: 4})
|
||||
|
||||
def test_failed_warmup_without_exception_closes_new_runtime(self):
|
||||
bank = FingerSensorBank()
|
||||
failed = FakeRuntime(warmup_ok=False)
|
||||
with (
|
||||
patch("orisys.finger.FingerRuntime.open", return_value=failed),
|
||||
self.assertRaisesRegex(RuntimeError, "warmup"),
|
||||
):
|
||||
bank.add(0)
|
||||
self.assertTrue(failed.closed)
|
||||
self.assertEqual(bank.runtimes, {})
|
||||
self.assertTrue(all(r.closed for r in opened))
|
||||
self.assertEqual(bank.sources, {})
|
||||
|
||||
def test_no_uart_is_opened_if_camera_preparation_fails(self):
|
||||
with (patch("orisys.finger.FingerRuntime.open", return_value=FakeRuntime(warmup_ok=False)),
|
||||
patch.object(uart, "open_serial") as serial):
|
||||
self.assertEqual(uart.main(["--sources", "0", "2"]), 1)
|
||||
serial.assert_not_called()
|
||||
def test_duplicate_source_and_fifth_camera_are_rejected_before_open(self):
|
||||
bank = FingerSensorBank()
|
||||
bank.add(0)
|
||||
for source in ("0", "/dev/video0", -1):
|
||||
with (
|
||||
self.subTest(source=source),
|
||||
patch("orisys.finger.FingerRuntime.open") as open_runtime,
|
||||
):
|
||||
with self.assertRaises(ValueError):
|
||||
bank.add(source)
|
||||
open_runtime.assert_not_called()
|
||||
for source in (2, 4, 6):
|
||||
bank.add(source)
|
||||
with patch("orisys.finger.FingerRuntime.open") as open_runtime:
|
||||
with self.assertRaisesRegex(ValueError, "4"):
|
||||
bank.add(8)
|
||||
open_runtime.assert_not_called()
|
||||
|
||||
def test_legacy_commands_are_rejected(self):
|
||||
def test_each_sensor_can_use_its_own_config(self):
|
||||
bank = FingerSensorBank(config="shared.json")
|
||||
seen = []
|
||||
|
||||
def open_runtime(**kwargs):
|
||||
seen.append(kwargs["config"])
|
||||
return FakeRuntime()
|
||||
|
||||
with patch("orisys.finger.FingerRuntime.open", side_effect=open_runtime):
|
||||
bank.add(0)
|
||||
bank.add(2, config="right.json")
|
||||
self.assertEqual(seen, ["shared.json", "right.json"])
|
||||
self.assertIsNot(bank.runtimes[1], bank.runtimes[2])
|
||||
self.assertEqual([r.sensor_id for r in bank.read_all()], [1, 2])
|
||||
|
||||
def test_quoted_source_and_config_are_one_argument_each(self):
|
||||
ser = ByteSerial()
|
||||
controller = uart.UartFingerController(ser, sources=[0, 2])
|
||||
for command in ["connect", "connect 2", "connect all", "run", "run all", "search", "exit"]:
|
||||
self.assertTrue(controller._dispatch(command))
|
||||
controller = uart.UartFingerController(ser)
|
||||
with patch.object(controller.bank, "add", return_value=1) as add:
|
||||
controller._dispatch('add "/tmp/test clip.mp4" "my config.json"')
|
||||
add.assert_called_once_with("/tmp/test clip.mp4", config="my config.json")
|
||||
self.assertEqual(ser.output[-1], "OK added 1")
|
||||
|
||||
def test_removed_commands_and_arguments_are_rejected(self):
|
||||
ser = ByteSerial()
|
||||
controller = uart.UartFingerController(ser)
|
||||
for command in ("connect", "run", "search", "exit"):
|
||||
controller._dispatch(command)
|
||||
self.assertTrue(ser.output[-1].startswith("ERR unknown"))
|
||||
|
||||
def test_no_per_sensor_or_all_command_arguments(self):
|
||||
ser = ByteSerial()
|
||||
controller = uart.UartFingerController(ser, sources=[0, 2])
|
||||
for command in ["start 1", "stop all", "read 2", "quit extra"]:
|
||||
self.assertTrue(controller._dispatch(command))
|
||||
for command in (
|
||||
"add",
|
||||
"add 0 a extra",
|
||||
"start 1",
|
||||
"stop all",
|
||||
"read 2",
|
||||
"quit extra",
|
||||
'add "broken',
|
||||
):
|
||||
controller._dispatch(command)
|
||||
self.assertTrue(ser.output[-1].startswith("ERR command"))
|
||||
|
||||
def test_start_stop_and_read_control_entire_connected_bank(self):
|
||||
def test_processing_failure_stops_output_without_partial_round(self):
|
||||
ser = ByteSerial()
|
||||
controller = uart.UartFingerController(ser, sources=[0, 2])
|
||||
controller.bank.connect_all()
|
||||
controller._dispatch("stop")
|
||||
controller._dispatch("read")
|
||||
self.assertEqual(ser.output[-2:], [
|
||||
"force,1,1.250000,-2.500000,3.750000",
|
||||
"force,2,1.250000,-2.500000,3.750000",
|
||||
])
|
||||
self.assertFalse(controller._streaming)
|
||||
controller._dispatch("start")
|
||||
self.assertTrue(controller._streaming)
|
||||
self.assertEqual(ser.output[-1], "OK start")
|
||||
|
||||
def test_read_all_never_publishes_partial_round_on_processing_failure(self):
|
||||
ser = ByteSerial()
|
||||
controller = uart.UartFingerController(ser, sources=[0, 2])
|
||||
controller.bank.connect_all()
|
||||
controller = uart.UartFingerController(ser)
|
||||
controller._dispatch("add 0")
|
||||
controller._dispatch("add 2")
|
||||
controller.bank.runtimes[2].grab_error = True
|
||||
controller._streaming = True
|
||||
controller._dispatch("read")
|
||||
@@ -189,141 +285,125 @@ class ConnectionFirstTests(HardwareFreeTest):
|
||||
self.assertFalse(any(x.startswith("force,") for x in ser.output))
|
||||
self.assertIn("sensor 2", ser.output[-1])
|
||||
|
||||
def test_serve_rejects_incomplete_bank_before_ready(self):
|
||||
ser = ByteSerial()
|
||||
controller = uart.UartFingerController(ser, sources=[0, 2])
|
||||
with self.assertRaises(RuntimeError):
|
||||
controller.serve()
|
||||
self.assertEqual(ser.output, [])
|
||||
|
||||
|
||||
class FramingTests(HardwareFreeTest):
|
||||
def test_partial_line_waits_for_terminator(self):
|
||||
ser = ByteSerial(b"st")
|
||||
controller = uart.UartFingerController(ser, sources=[0])
|
||||
controller = uart.UartFingerController(ser)
|
||||
self.assertIsNone(controller._poll_command_line(blocking=False))
|
||||
ser.input.extend(b"op\n")
|
||||
self.assertEqual(controller._poll_command_line(blocking=False), "stop")
|
||||
|
||||
def test_cr_lf_crlf_commands_are_separate(self):
|
||||
controller = uart.UartFingerController(ByteSerial(b"stop\rstart\nread\r\n"), sources=[0])
|
||||
controller = uart.UartFingerController(ByteSerial(b"stop\rstart\nread\r\n"))
|
||||
self.assertEqual(controller._poll_command_line(blocking=False), "stop")
|
||||
self.assertEqual(controller._poll_command_line(blocking=False), "start")
|
||||
self.assertEqual(controller._poll_command_line(blocking=False), "read")
|
||||
|
||||
def test_overlong_command_is_discarded_until_terminator(self):
|
||||
ser = ByteSerial(b"a" * 1025)
|
||||
controller = uart.UartFingerController(ser, sources=[0])
|
||||
controller = uart.UartFingerController(ser)
|
||||
self.assertIsNone(controller._poll_command_line(blocking=False))
|
||||
ser.input.extend(b"quit\nstop\r")
|
||||
self.assertEqual(controller._poll_command_line(blocking=False), "stop")
|
||||
self.assertTrue(ser.output[-1].startswith("ERR command"))
|
||||
|
||||
def test_read_only_ignores_every_command_and_still_streams(self):
|
||||
ser = ByteSerial(b"stop\nquit\n" + b"a" * 1100 + b"\n")
|
||||
controller = uart.UartFingerController(ser, sources=[0], read_only=True)
|
||||
controller.bank.connect_all()
|
||||
original_write = ser.write
|
||||
def write(data):
|
||||
size = original_write(data)
|
||||
if data.startswith(b"force,"):
|
||||
raise KeyboardInterrupt()
|
||||
return size
|
||||
ser.write = write
|
||||
with self.assertRaises(KeyboardInterrupt):
|
||||
controller.serve()
|
||||
self.assertEqual(ser.output, ["OK ready 1", "force,1,1.250000,-2.500000,3.750000"])
|
||||
|
||||
def test_short_write_is_transport_failure(self):
|
||||
ser = ByteSerial()
|
||||
ser.write = lambda data: len(data) - 1
|
||||
with self.assertRaises(OSError):
|
||||
uart.UartFingerController(ser, sources=[0]).reply("OK ready 1")
|
||||
uart.UartFingerController(ser).reply("OK ready 0")
|
||||
|
||||
def test_error_newlines_do_not_inject_records(self):
|
||||
ser = ByteSerial()
|
||||
uart.UartFingerController(ser, sources=[0]).reply("ERR read bad\nOK quit\r\n")
|
||||
uart.UartFingerController(ser).reply("ERR read bad\nOK quit\r\n")
|
||||
self.assertEqual(ser.output, ["ERR read bad OK quit"])
|
||||
|
||||
|
||||
class BankTests(HardwareFreeTest):
|
||||
def test_invalid_count_config_count_and_duplicate_sources(self):
|
||||
for sources, configs in [([], ["finger"]), (range(5), ["finger"]),
|
||||
([0, 2], ["a", "b", "c"]), ([0, "0"], ["finger"]),
|
||||
([0, "/dev/video0"], ["finger"]), ([-1], ["finger"])]:
|
||||
with self.subTest(sources=sources), self.assertRaises(ValueError):
|
||||
FingerSensorBank(sources, configs=configs)
|
||||
|
||||
def test_configs_and_runtime_states_are_independent(self):
|
||||
bank = FingerSensorBank([0, 2], configs=["left.json", "right.json"])
|
||||
configs = []
|
||||
def open_runtime(**kwargs):
|
||||
configs.append(kwargs["config"])
|
||||
return FakeRuntime()
|
||||
with patch("orisys.finger.FingerRuntime.open", side_effect=open_runtime):
|
||||
bank.connect_all()
|
||||
self.assertEqual(configs, ["left.json", "right.json"])
|
||||
self.assertIsNot(bank.runtimes[1], bank.runtimes[2])
|
||||
readings = bank.read_all()
|
||||
self.assertEqual([r.sensor_id for r in readings], [1, 2])
|
||||
self.assertEqual(readings[0].units, ("raw", "raw", "raw"))
|
||||
|
||||
def test_no_frame_during_warmup_closes_runtime(self):
|
||||
runtime = FakeRuntime(warmup_ok=False)
|
||||
bank = FingerSensorBank([0])
|
||||
with (patch("orisys.finger.FingerRuntime.open", return_value=runtime),
|
||||
self.assertRaisesRegex(RuntimeError, "warmup")):
|
||||
bank.connect_all()
|
||||
self.assertTrue(runtime.closed)
|
||||
self.assertEqual(bank.runtimes, {})
|
||||
|
||||
def test_nonfinite_force_is_not_a_valid_round(self):
|
||||
bank = FingerSensorBank([0])
|
||||
bank.connect_all()
|
||||
bank = FingerSensorBank()
|
||||
bank.add(0)
|
||||
bank.runtimes[1].tactile.read_info = lambda *args: (float("nan"), 0, 0)
|
||||
with self.assertRaisesRegex(RuntimeError, "non-finite"):
|
||||
bank.read_all()
|
||||
|
||||
def test_eof_returns_no_partial_round(self):
|
||||
bank = FingerSensorBank([0, 2])
|
||||
bank.connect_all()
|
||||
bank = FingerSensorBank()
|
||||
bank.add(0)
|
||||
bank.add(2)
|
||||
bank.runtimes[2].frames = 0
|
||||
bank.sources[2] = "ended.mp4"
|
||||
with self.assertRaisesRegex(EOFError, "sensor 2"):
|
||||
bank.read_all()
|
||||
|
||||
def test_all_resources_are_attempted_even_when_close_fails(self):
|
||||
bank = FingerSensorBank([0, 2])
|
||||
bank.connect_all()
|
||||
bank = FingerSensorBank()
|
||||
bank.add(0)
|
||||
bank.add(2)
|
||||
second = bank.runtimes[2]
|
||||
bank.runtimes[1].close = lambda: (_ for _ in ()).throw(RuntimeError("close failed"))
|
||||
bank.runtimes[1].close = lambda: (_ for _ in ()).throw(
|
||||
RuntimeError("close failed")
|
||||
)
|
||||
with self.assertRaisesRegex(RuntimeError, "close failed"):
|
||||
bank.close()
|
||||
self.assertTrue(second.closed)
|
||||
self.assertEqual(bank.runtimes, {})
|
||||
self.assertEqual(bank.sources, {})
|
||||
|
||||
|
||||
class CliTests(HardwareFreeTest):
|
||||
def test_sources_are_required_and_legacy_options_are_removed(self):
|
||||
for args in [[], ["--camera-id", "0"], ["--video", "a.mp4"],
|
||||
["--sources", "0", "--autostart"], ["--sources", "0", "--max-camera-index", "10"],
|
||||
["--sources", "0", "1", "2", "3", "4"], ["--sources", "0", "--timeout", "nan"]]:
|
||||
def test_startup_opens_only_uart_and_waits_for_add(self):
|
||||
ser = ByteSerial(b"quit\n")
|
||||
ser.close = lambda: None
|
||||
with (
|
||||
patch.object(uart, "open_serial", return_value=ser),
|
||||
patch("orisys.finger.FingerRuntime.open") as runtime,
|
||||
):
|
||||
self.assertEqual(uart.main([]), 0)
|
||||
runtime.assert_not_called()
|
||||
self.assertEqual(ser.output, ["OK ready 0", "OK quit"])
|
||||
|
||||
def test_automatic_setup_options_are_removed(self):
|
||||
for args in (
|
||||
["--sources", "0"],
|
||||
["--read-only"],
|
||||
["--autostart"],
|
||||
["--camera-id", "0"],
|
||||
["--timeout", "nan"],
|
||||
):
|
||||
with self.subTest(args=args), patch.object(uart, "open_serial") as serial:
|
||||
with self.assertRaises(SystemExit) as status:
|
||||
uart.main(args)
|
||||
self.assertEqual(status.exception.code, 2)
|
||||
serial.assert_not_called()
|
||||
|
||||
def test_serial_failure_closes_every_connected_runtime(self):
|
||||
def test_transport_failure_closes_cameras_added_by_commands(self):
|
||||
ser = ByteSerial(b"add 0\nadd 2\nstart\n")
|
||||
ser.closed = False
|
||||
ser.close = lambda: setattr(ser, "closed", True)
|
||||
opened = []
|
||||
|
||||
def open_runtime(**kwargs):
|
||||
runtime = FakeRuntime()
|
||||
opened.append(runtime)
|
||||
return runtime
|
||||
with (patch("orisys.finger.FingerRuntime.open", side_effect=open_runtime),
|
||||
patch.object(uart, "open_serial", side_effect=OSError("UART unavailable"))):
|
||||
self.assertEqual(uart.main(["--sources", "0", "2"]), 1)
|
||||
|
||||
original_write = ser.write
|
||||
|
||||
def write(data):
|
||||
if data.startswith(b"force,"):
|
||||
raise OSError("UART disconnected")
|
||||
return original_write(data)
|
||||
|
||||
ser.write = write
|
||||
with (
|
||||
patch.object(uart, "open_serial", return_value=ser),
|
||||
patch("orisys.finger.FingerRuntime.open", side_effect=open_runtime),
|
||||
):
|
||||
self.assertEqual(uart.main([]), 1)
|
||||
self.assertEqual(len(opened), 2)
|
||||
self.assertTrue(ser.closed)
|
||||
self.assertTrue(all(r.closed for r in opened))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user