diff --git a/clients/python/src/ate_env/backend/__init__.py b/clients/python/src/ate_env/backend/__init__.py index 0dbebec..a23860b 100644 --- a/clients/python/src/ate_env/backend/__init__.py +++ b/clients/python/src/ate_env/backend/__init__.py @@ -17,4 +17,3 @@ from .substrate import SubstrateBackendDriver __all__ = ["BackendDriver", "MockBackendDriver", "SubstrateBackendDriver"] - diff --git a/clients/python/src/ate_env/fleet.py b/clients/python/src/ate_env/fleet.py index bad0ed8..0fdca4f 100644 --- a/clients/python/src/ate_env/fleet.py +++ b/clients/python/src/ate_env/fleet.py @@ -233,6 +233,7 @@ def _acquire_one( backend=self.backend, runtime=runtime, data_plane=data_plane, + default_timeout_s=self.config.step_timeout_s, ) def _build_runtime( diff --git a/clients/python/src/ate_env/handle.py b/clients/python/src/ate_env/handle.py index 0199b49..4b33681 100644 --- a/clients/python/src/ate_env/handle.py +++ b/clients/python/src/ate_env/handle.py @@ -40,13 +40,14 @@ class SandboxHandle: backend: BackendDriver = field(repr=False) runtime: RuntimeGuestHook = field(repr=False) data_plane: DataPlaneEndpoint | None = None + default_timeout_s: float = 120.0 def exec( self, command: str | list[str], cwd: str = "", env: dict[str, str] | None = None, - timeout_s: float = 120.0, + timeout_s: float | None = None, check: bool = True, ) -> ExecResult: """Execute command inside the sandbox. @@ -57,10 +58,11 @@ def exec( of ``check``. Returns ExecResult. """ - res = self.runtime.exec(command, cwd=cwd, env=env, timeout_s=timeout_s) + to_s = self.default_timeout_s if timeout_s is None else timeout_s + res = self.runtime.exec(command, cwd=cwd, env=env, timeout_s=to_s) if check and res.timed_out: raise CommandTimeoutError( - f"Command '{command}' timed out after {timeout_s:g}s:\n" + f"Command '{command}' timed out after {to_s:g}s:\n" f"STDOUT: {res.stdout}\nSTDERR: {res.stderr}", result=res, ) @@ -77,14 +79,15 @@ async def exec_async( command: str | list[str], cwd: str = "", env: dict[str, str] | None = None, - timeout_s: float = 120.0, + timeout_s: float | None = None, check: bool = True, ) -> ExecResult: """Asynchronously execute command inside the sandbox.""" - res = await self.runtime.exec_async(command, cwd=cwd, env=env, timeout_s=timeout_s) + to_s = self.default_timeout_s if timeout_s is None else timeout_s + res = await self.runtime.exec_async(command, cwd=cwd, env=env, timeout_s=to_s) if check and res.timed_out: raise CommandTimeoutError( - f"Command '{command}' timed out after {timeout_s:g}s:\n" + f"Command '{command}' timed out after {to_s:g}s:\n" f"STDOUT: {res.stdout}\nSTDERR: {res.stderr}", result=res, ) diff --git a/clients/python/src/ate_env/loop_runner.py b/clients/python/src/ate_env/loop_runner.py index 9eb54e3..efac4bb 100644 --- a/clients/python/src/ate_env/loop_runner.py +++ b/clients/python/src/ate_env/loop_runner.py @@ -73,7 +73,7 @@ async def _shutdown() -> None: pass self._loop.call_soon_threadsafe(self._loop.stop) self._thread.join(timeout=2.0) - if not self._loop.is_closed(): + if not self._thread.is_alive() and not self._loop.is_closed(): self._loop.close() def __del__(self) -> None: diff --git a/clients/python/tests/test_exec_semantics.py b/clients/python/tests/test_exec_semantics.py index db116ea..4f81afa 100644 --- a/clients/python/tests/test_exec_semantics.py +++ b/clients/python/tests/test_exec_semantics.py @@ -118,3 +118,10 @@ def test_mock_runtime_file_round_trip(name): def test_read_missing_file_raises_file_not_found(): with pytest.raises(FileNotFoundError): MockRuntimeHook().read_file_bytes("/missing.txt") + + +def test_fleet_step_timeout_applies_to_handle(): + fleet = SandboxFleet(FleetConfig(backend="mock", step_timeout_s=42.0)) + h = fleet.acquire("t1") + assert h.default_timeout_s == 42.0 + h.release()