import types import time import pytest from target_simulator.simulation.simulation_controller import SimulationController from unittest.mock import Mock class DummyVar: def __init__(self, value=None): self._v = value def get(self): return self._v def set(self, v): self._v = v class DummyComm: def __init__(self, use_json=False, send_results=None): self.is_open = True self._use_json_protocol = use_json # send_results can be a list or a callable self._send_calls = [] self._send_results = send_results def send_commands(self, cmds): self._send_calls.append(cmds) if callable(self._send_results): return self._send_results(cmds) if isinstance(self._send_results, list): if len(self._send_results) >= len(self._send_calls): return self._send_results[len(self._send_calls) - 1] return self._send_results[-1] return True def send_scenario(self, scenario): return True def router(self): return None class DummyCommManager: def __init__(self, target_comm=None): self.target_communicator = target_comm class DummyHub: def __init__(self, active_sequence=None): # active_sequence: iterable of booleans returned by has_active_real_targets self._seq = list(active_sequence) if active_sequence is not None else [] def has_active_real_targets(self): if self._seq: return self._seq.pop(0) return False def reset(self): pass def get_ownship_state(self): return {} def set_simulation_origin(self, origin): # No-op for tests return class DummyScenario: def __init__(self): class T: _total_duration_s = 5.0 self.targets = [T()] self.name = "dummy" def get_all_targets(self): return self.targets def reset_simulation(self): pass class DummyMainView: def __init__(self): self.is_simulation_running = DummyVar(False) self.update_time = DummyVar(1.0) self.time_multiplier = 1.0 self.scenario = DummyScenario() self.ppi_widget = types.SimpleNamespace( clear_trails=lambda: None, clear_previews=lambda: None ) self.sim_slider_var = DummyVar(0.0) self.simulation_engine = None self.current_archive = None self._start_in_progress_main = False # Minimal simulation_controls used by finalize callbacks self.simulation_controls = types.SimpleNamespace( sim_slider_var=DummyVar(0.0), show_notice=lambda *a, **k: None, update_targets_table=lambda *a, **k: None, update_ownship_display=lambda *a, **k: None, sim_scenario_combobox=types.SimpleNamespace(set=lambda v: None), ) def _update_simulation_progress_display(self): pass def _update_button_states(self): pass def _refresh_analysis_list(self): pass def show_status_message(self, text, timeout_ms=None): # No-op for tests return def after(self, delay_ms, func, *args, **kwargs): # Execute callbacks synchronously to simplify testing try: return func(*args, **kwargs) except TypeError: # Some callers pass a lambda; try calling without args return func() def clear_status_message(self): return def test_reset_returns_false_when_not_connected(): hub = DummyHub() cm = DummyCommManager(None) controller = SimulationController(cm, hub, config_manager=None, logger=Mock()) mv = DummyMainView() assert controller.reset_radar_state(mv) is False def test_legacy_reset_success(monkeypatch): # Legacy communicator: _use_json_protocol False comm = DummyComm(use_json=False, send_results=[True]) cm = DummyCommManager(comm) hub = DummyHub(active_sequence=[False]) controller = SimulationController(cm, hub, config_manager=None, logger=Mock()) mv = DummyMainView() assert controller.reset_radar_state(mv) is True def test_json_reset_fallback_to_per_target(monkeypatch): # JSON communicator that fails simple reset but succeeds per-target results = [ False, True, ] # first send (json reset) -> False, second (per-target) -> True def send_results(cmds): # pop result accordingly return results.pop(0) comm = DummyComm(use_json=True, send_results=send_results) cm = DummyCommManager(comm) # hub returns False (cleared) after per-target hub = DummyHub(active_sequence=[True, False]) # Patch command_builder.build_json_reset_ids to return two payloads import target_simulator.simulation.simulation_controller as scmod monkeypatch.setattr( scmod.command_builder, "build_json_reset_ids", lambda: ['{"id":1}'] ) controller = SimulationController(cm, hub, config_manager=None, logger=Mock()) mv = DummyMainView() assert controller.reset_radar_state(mv) is True def test_start_simulation_creates_engine_and_sets_running(monkeypatch): # Patch SimulationEngine used in controller to a dummy engine class DummyEngine: def __init__(self, communicator=None, simulation_hub=None): self.started = False def set_time_multiplier(self, v): pass def set_update_interval(self, v): pass def load_scenario(self, s): pass def start(self): self.started = True def stop(self): self.started = False def is_running(self): return self.started monkeypatch.setattr( "target_simulator.simulation.simulation_controller.SimulationEngine", DummyEngine, ) comm = DummyComm(use_json=False, send_results=[True]) cm = DummyCommManager(comm) hub = DummyHub() controller = SimulationController(cm, hub, config_manager=None, logger=Mock()) mv = DummyMainView() # Run background start synchronously in tests to avoid threading timing issues class _ImmediateThread: def __init__(self, target=None, name=None, daemon=None): self._target = target def start(self): if self._target: self._target() monkeypatch.setattr( "target_simulator.simulation.simulation_controller.threading.Thread", _ImmediateThread, ) # start simulation (runs inline via patched Thread.start) controller.start_simulation(mv) # Wait up to 1s for background start to finish in test environment import time as _time deadline = _time.time() + 1.0 while _time.time() < deadline and ( mv.simulation_engine is None or not mv.is_simulation_running.get() ): _time.sleep(0.01) # engine should be created and main_view.simulation_engine set assert mv.simulation_engine is not None