diff --git a/src/swift/Swift.py b/src/swift/Swift.py index d259425..ff23a94 100644 --- a/src/swift/Swift.py +++ b/src/swift/Swift.py @@ -1108,7 +1108,19 @@ def run(self, duration: float | None = None, dt: float = 0.05, timeout: float | try: while duration is None or (self.sim_time - start_time) < duration: - self.step(dt) + try: + self.step(dt) + except TimeoutError: + # A disconnect noticed *during* step() (SwiftRoute.py's + # expect_message()/producer racing wait_closed()) raises + # here directly, rather than only ever surfacing via + # the _check_disconnected() poll below -- without this, + # it would propagate straight out of run() as a raw + # traceback instead of the same graceful message/return + # every other disconnect path already gets. + print("\nSwift browser tab closed.") + self.close() + return time.sleep(dt) disconnected_since, expired = self._check_disconnected(disconnected_since, timeout) if expired: diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 629ca06..1d5480d 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -725,6 +725,29 @@ def sleep_raises(s): assert closed == [True] +def test_run_exits_quietly_on_disconnect_during_step(capsys): + # A disconnect noticed *during* step() (SwiftRoute.py's + # expect_message()/producer racing wait_closed()) raises TimeoutError + # directly out of step(), rather than only ever surfacing via + # _check_disconnected()'s poll between steps -- run() must catch this + # the same way it already handles that poll-detected case (print, + # close(), return), not let it propagate as a raw traceback. + env = make_env() + env.headless = True + + def step_raises(dt=0.05, render=True): + raise TimeoutError("Swift browser tab stopped responding") + + env.step = step_raises + closed = [] + env.close = lambda *a, **kw: closed.append(True) + + env.run() # must return normally, not raise + + assert closed == [True] + assert "Swift browser tab closed." in capsys.readouterr().out + + def test_hold_exits_quietly_on_keyboard_interrupt(monkeypatch): env = make_env() env.headless = True