1+ # noqa: type
2+
13import multiprocessing
2- import time
4+ import os
35import socket
46import sys
7+ import time
58import traceback
6- import os
9+ from contextlib import suppress
10+ from multiprocessing .context import ForkServerContext , SpawnContext
11+ from typing import Optional , Type , Union # Add Type and Union here
12+
713import pytest
8- from typing import Optional # Add this import at the top
9- from rcs .envs .creators import SimEnvCreator
10- from rcs .envs .utils import (
11- default_mujoco_cameraset_cfg ,
12- default_sim_gripper_cfg ,
13- default_sim_robot_cfg ,
14- )
1514from rcs .envs .base import ControlMode , RelativeTo
16- from rcs .rpc .server import RcsServer
15+ from rcs .envs .creators import SimEnvCreator
16+ from rcs .envs .utils import default_sim_gripper_cfg , default_sim_robot_cfg
1717from rcs .rpc .client import RcsClient
18+ from rcs .rpc .server import RcsServer
1819
1920HOST = "127.0.0.1"
2021
22+
2123def get_free_port () -> int :
2224 with socket .socket (socket .AF_INET , socket .SOCK_STREAM ) as s :
2325 s .bind ((HOST , 0 ))
2426 return s .getsockname ()[1 ]
2527
28+
2629def wait_for_port (
2730 host : str ,
2831 port : int ,
2932 timeout : float ,
3033 server_proc : Optional [multiprocessing .Process ] = None ,
31- err_q : Optional [multiprocessing .Queue ] = None
34+ err_q : Optional [multiprocessing .Queue ] = None ,
3235) -> None :
3336 start = time .time ()
3437 last_exc = None
@@ -44,21 +47,17 @@ def wait_for_port(
4447 if server_proc is not None and not server_proc .is_alive ():
4548 server_err = None
4649 if err_q is not None :
47- try :
50+ with suppress ( Exception ) :
4851 server_err = err_q .get_nowait ()
49- except Exception :
50- pass
5152 msg = f"Server process exited early (exitcode={ server_proc .exitcode } )."
5253 if server_err :
5354 msg += f"\n Server traceback:\n { server_err } "
5455 raise RuntimeError (msg )
5556 time .sleep (0.2 )
5657 server_err = None
5758 if err_q is not None :
58- try :
59+ with suppress ( Exception ) :
5960 server_err = err_q .get_nowait ()
60- except Exception :
61- pass
6261 msg = f"Timed out waiting for { host } :{ port } to open."
6362 if last_exc :
6463 msg += f" Last socket error: { last_exc } "
@@ -68,6 +67,7 @@ def wait_for_port(
6867 msg += f"\n Server traceback:\n { server_err } "
6968 raise TimeoutError (msg )
7069
70+
7171def run_server (host : str , port : int , err_q : multiprocessing .Queue ) -> None :
7272 try :
7373 env = SimEnvCreator ()(
@@ -76,7 +76,7 @@ def run_server(host: str, port: int, err_q: multiprocessing.Queue) -> None:
7676 robot_cfg = default_sim_robot_cfg (),
7777 gripper_cfg = default_sim_gripper_cfg (),
7878 # Disabled to avoid rendering problem in python subprocess.
79- #cameras=default_mujoco_cameraset_cfg(),
79+ # cameras=default_mujoco_cameraset_cfg(),
8080 max_relative_movement = 0.1 ,
8181 relative_to = RelativeTo .LAST_STEP ,
8282 )
@@ -90,20 +90,22 @@ def run_server(host: str, port: int, err_q: multiprocessing.Queue) -> None:
9090 time .sleep (1 )
9191 except Exception :
9292 tb = "" .join (traceback .format_exception (* sys .exc_info ()))
93- try :
93+ with suppress ( Exception ) :
9494 err_q .put (tb )
95- except Exception :
96- pass
9795 sys .exit (1 )
9896
99- def _mp_context () -> multiprocessing .context .BaseContext :
97+
98+ def _mp_context () -> Union [SpawnContext , ForkServerContext ]:
10099 # Prefer spawn to avoid fork-related issues with GL/MuJoCo/threaded libs
101100 methods = multiprocessing .get_all_start_methods ()
102101 if "spawn" in methods :
103102 return multiprocessing .get_context ("spawn" )
104103 if "forkserver" in methods :
105104 return multiprocessing .get_context ("forkserver" )
106- return multiprocessing .get_context (methods [0 ])
105+
106+ msg = "No suitable multiprocessing context found."
107+ raise RuntimeError (msg )
108+
107109
108110def _external_server_from_env () -> tuple [str , int ] | None :
109111 # Set RCS_TEST_HOST and RCS_TEST_PORT to reuse an already running server.
@@ -119,6 +121,7 @@ def _external_server_from_env() -> tuple[str, int] | None:
119121 return HOST , 50055
120122 return None
121123
124+
122125def test_run_server_starts_and_stops ():
123126 # Skip if reusing an external server
124127 ext = _external_server_from_env ()
@@ -130,17 +133,25 @@ def test_run_server_starts_and_stops():
130133 server_proc = ctx .Process (target = run_server , args = (HOST , port , err_q ))
131134 server_proc .start ()
132135 try :
133- wait_for_port (HOST , port , timeout = 120.0 , server_proc = server_proc , err_q = err_q )
136+ wait_for_port (HOST , port , timeout = 120.0 , server_proc = server_proc , err_q = err_q ) # type: ignore
134137 assert server_proc .is_alive (), "Server process did not start as expected."
135138 finally :
136139 if server_proc .is_alive ():
137140 server_proc .terminate ()
138141 server_proc .join (timeout = 5 )
139142 assert not server_proc .is_alive (), "Server process did not terminate as expected."
140143
144+
141145class TestRcsClientServer :
146+ client : RcsClient
147+ host : str = HOST
148+ port : int = 0
149+ server_proc = None
150+ err_q : Optional [multiprocessing .Queue ] = None
151+
152+
142153 @classmethod
143- def setup_class (cls ):
154+ def setup_class (cls : Type [ "TestRcsClientServer" ] ):
144155 ext = _external_server_from_env ()
145156 if ext :
146157 cls .host , cls .port = ext
@@ -156,11 +167,11 @@ def setup_class(cls):
156167 cls .server_proc = ctx .Process (target = run_server , args = (cls .host , cls .port , cls .err_q ))
157168 cls .server_proc .start ()
158169 # Wait until the server is actually listening or fail early if it crashed
159- wait_for_port (cls .host , cls .port , timeout = 180.0 , server_proc = cls .server_proc , err_q = cls .err_q )
170+ wait_for_port (cls .host , cls .port , timeout = 180.0 , server_proc = cls .server_proc , err_q = cls .err_q ) # type: ignore
160171 cls .client = RcsClient (host = cls .host , port = cls .port )
161172
162173 @classmethod
163- def teardown_class (cls ):
174+ def teardown_class (cls : Type [ "TestRcsClientServer" ] ):
164175 try :
165176 if getattr (cls , "client" , None ):
166177 cls .client .close ()
@@ -188,8 +199,14 @@ def test_unwrapped(self):
188199 _ = self .client .unwrapped
189200
190201 def test_close (self ):
191- self .client .close ()
202+ if self .client is not None :
203+ self .client .close ()
192204 # Reconnect for further tests
193- wait_for_port (self .__class__ .host , self .__class__ .port , timeout = 15.0 ,
194- server_proc = self .__class__ .server_proc , err_q = self .__class__ .err_q )
205+ wait_for_port (
206+ self .__class__ .host ,
207+ self .__class__ .port ,
208+ timeout = 15.0 ,
209+ server_proc = self .__class__ .server_proc , # type: ignore
210+ err_q = self .__class__ .err_q ,
211+ )
195212 self .__class__ .client = RcsClient (host = self .__class__ .host , port = self .__class__ .port )
0 commit comments