import logging from queue import Empty import time import config from control_runtime import ( ControlWorker, load_model, validate_device_settings, validate_target, ) class FakeHardware: def __init__(self, *, connect_ok=True): self.connect_ok = connect_ok self.connected = False self.disconnect_count = 0 self.motor_commands = [] def connect(self): self.connected = self.connect_ok return self.connect_ok def disconnect(self): self.connected = False self.disconnect_count += 1 def get_flow(self, _address): return 20.0 def get_pressure(self, address): if address == config.PRESSURE_BEFORE_ADDR: return 200.0 return 100.0 def set_motor_position(self, position, channel=None): self.motor_commands.append((float(position), channel)) return True class OverpressureHardware(FakeHardware): def get_pressure(self, address): if address == config.PRESSURE_BEFORE_ADDR: return config.MAX_PRESSURE_KPA + 1.0 return 100.0 def wait_for_event(worker, predicate, timeout=2.0): deadline = time.monotonic() + timeout while time.monotonic() < deadline: try: event = worker.events.get(timeout=0.1) except Empty: continue if predicate(event): return event raise AssertionError("等待后台线程事件超时") def make_logger(): logger = logging.getLogger(f"test-control-runtime-{time.monotonic_ns()}") logger.addHandler(logging.NullHandler()) return logger def test_validate_ui_values(): settings = validate_device_settings(" 192.168.1.88 ", "502", "1") assert settings.host == "192.168.1.88" assert settings.port == 502 assert settings.slave_id == 1 assert validate_target("50.5") == 50.5 def test_builtin_model_is_valve_model_004(): info = load_model() assert info.is_builtin is True assert info.path.name == "valve_model_004.json" assert info.point_count == 80 assert info.model.motor_open == config.MOTOR_OPEN_POSITION assert info.model.motor_closed == config.MOTOR_CLOSED_POSITION def test_worker_connect_control_and_safe_disconnect(): hardware = FakeHardware() worker = ControlWorker(make_logger(), hardware_factory=lambda _settings: hardware) worker.start() wait_for_event( worker, lambda event: event.get("kind") == "state" and event.get("state") == "DISCONNECTED", ) settings = validate_device_settings("192.168.1.88", 502, 1) worker.request_connect(settings) wait_for_event( worker, lambda event: event.get("kind") == "state" and event.get("state") == "MONITORING", ) assert hardware.connected is True assert hardware.motor_commands[0] == ( config.MOTOR_OPEN_POSITION, config.MOTOR_OUTPUT_ADDR, ) worker.request_target(50.0) wait_for_event( worker, lambda event: event.get("kind") == "target_applied", ) telemetry = wait_for_event( worker, lambda event: event.get("kind") == "telemetry" and event.get("target_flow_slm") == 50.0, ) assert telemetry["measured_flow_slm"] == 20.0 assert 0.0 <= telemetry["opening_pct"] <= 100.0 assert telemetry["pid_mode"] in {"FAR", "NEAR"} worker.request_disconnect() wait_for_event( worker, lambda event: event.get("kind") == "state" and event.get("state") == "DISCONNECTED", ) assert hardware.connected is False assert hardware.motor_commands[-1] == ( config.MOTOR_OPEN_POSITION, config.MOTOR_OUTPUT_ADDR, ) assert hardware.disconnect_count == 1 worker.request_shutdown() wait_for_event(worker, lambda event: event.get("kind") == "shutdown_complete") worker.thread.join(timeout=1.0) assert not worker.is_alive() def test_worker_connection_failure_reports_fault_without_hardware_access(): hardware = FakeHardware(connect_ok=False) worker = ControlWorker(make_logger(), hardware_factory=lambda _settings: hardware) worker.start() wait_for_event(worker, lambda event: event.get("state") == "DISCONNECTED") worker.request_connect(validate_device_settings("192.168.1.88", 502, 1)) fault = wait_for_event(worker, lambda event: event.get("kind") == "fault") assert "无法连接 PLC" in fault["message"] assert hardware.motor_commands == [] assert hardware.disconnect_count == 1 worker.request_shutdown() wait_for_event(worker, lambda event: event.get("kind") == "shutdown_complete") worker.thread.join(timeout=1.0) assert not worker.is_alive() def test_monitoring_stage_overpressure_triggers_safe_disconnect(): hardware = OverpressureHardware() worker = ControlWorker(make_logger(), hardware_factory=lambda _settings: hardware) worker.start() wait_for_event(worker, lambda event: event.get("state") == "DISCONNECTED") worker.request_connect(validate_device_settings("192.168.1.88", 502, 1)) wait_for_event(worker, lambda event: event.get("state") == "MONITORING") fault = wait_for_event(worker, lambda event: event.get("kind") == "fault") assert "超过上限" in fault["message"] assert hardware.connected is False assert hardware.motor_commands[-1][0] == config.MOTOR_OPEN_POSITION worker.request_shutdown() wait_for_event(worker, lambda event: event.get("kind") == "shutdown_complete") worker.thread.join(timeout=1.0) assert not worker.is_alive()