@@ -134,9 +134,10 @@ def get_output(self) -> str:
134134
135135
136136class DockerTarget (Target ):
137- def __init__ (self , container ):
137+ def __init__ (self , container , network = None ):
138138 super ().__init__ ()
139139 self .container = container
140+ self .network = network
140141 self ._client = pypi_docker .from_env (timeout = DOCKER_CLIENT_TIMEOUT )
141142
142143 def __getattr__ (self , name ):
@@ -248,13 +249,36 @@ def download(self, remote_path: str, local_path: str) -> None:
248249 def restart (self ) -> None :
249250 self .container .restart ()
250251
251- def get_ip (self ):
252- self .container .reload ()
253- return self .container .attrs ["NetworkSettings" ]["Networks" ]["bridge" ]["IPAddress" ]
252+ def _network_attr (self , key , network = None ):
253+ """Return a NetworkSettings attribute for the given Docker network.
254254
255- def get_gateway (self ):
255+ If *network* is ``None`` and the target was created with a dedicated
256+ network, that network is used. Otherwise the value from the first
257+ attached network that has a non-empty value for *key* is returned.
258+ """
259+ if network is None and self .network is not None :
260+ network = self .network .name
256261 self .container .reload ()
257- return self .container .attrs ["NetworkSettings" ]["Networks" ]["bridge" ]["Gateway" ]
262+ networks = self .container .attrs ["NetworkSettings" ]["Networks" ]
263+ if network is not None :
264+ if network not in networks :
265+ raise RuntimeError (f"Container { self .container .short_id } is not attached to network '{ network } '" )
266+ return networks [network ][key ]
267+ value = next (
268+ (v .get (key ) for v in networks .values () if v .get (key , "" ) != "" ),
269+ None ,
270+ )
271+ if value is None :
272+ raise RuntimeError (f"Container { self .container .short_id } has no { key } on any network" )
273+ return value
274+
275+ def get_ip (self , network = None ):
276+ """Return the container IP on the given Docker network."""
277+ return self ._network_attr ("IPAddress" , network )
278+
279+ def get_gateway (self , network = None ):
280+ """Return the gateway IP on the given Docker network."""
281+ return self ._network_attr ("Gateway" , network )
258282
259283 def ssh (self , username = "score" , password = "score" , port = 2222 ):
260284 return Ssh (target_ip = self .get_ip (), port = port , username = username , password = password )
@@ -322,25 +346,40 @@ def target_init(request, _docker_configuration):
322346
323347 docker_image = request .config .getoption ("docker_image" )
324348 client = pypi_docker .from_env (timeout = DOCKER_CLIENT_TIMEOUT )
349+
325350 known_keys = {"command" , "init" , "environment" , "volumes" , "shm_size" , "detach" , "auto_remove" }
326351 reserved_overrides = {k for k in ("detach" , "auto_remove" ) if k in _docker_configuration }
327352 if reserved_overrides :
328353 logger .warning (f"docker_configuration contains reserved keys { reserved_overrides } which will be ignored" )
329354 extra_kwargs = {k : v for k , v in _docker_configuration .items () if k not in known_keys }
330- container = client .containers .run (
331- docker_image ,
332- _docker_configuration ["command" ],
333- detach = True ,
334- auto_remove = False ,
335- init = _docker_configuration ["init" ],
336- environment = _docker_configuration ["environment" ],
337- volumes = _docker_configuration ["volumes" ],
338- shm_size = _docker_configuration ["shm_size" ],
339- ** extra_kwargs ,
355+
356+ # Create a per-container bridge network so that get_ip() / get_gateway()
357+ # return addresses unique to this container.
358+ network = client .networks .create (
359+ f"score_itf_{ os .urandom (8 ).hex ()} " ,
360+ driver = "bridge" ,
340361 )
362+
363+ try :
364+ container = client .containers .run (
365+ docker_image ,
366+ _docker_configuration ["command" ],
367+ detach = True ,
368+ auto_remove = False ,
369+ init = _docker_configuration ["init" ],
370+ environment = _docker_configuration ["environment" ],
371+ volumes = _docker_configuration ["volumes" ],
372+ shm_size = _docker_configuration ["shm_size" ],
373+ network = network .name ,
374+ ** extra_kwargs ,
375+ )
376+ except Exception :
377+ network .remove ()
378+ raise
379+
341380 target = None
342381 try :
343- target = DockerTarget (container )
382+ target = DockerTarget (container , network = network )
344383 yield target
345384 finally :
346385 try :
@@ -352,7 +391,13 @@ def target_init(request, _docker_configuration):
352391 except Exception :
353392 logger .warning ("Coverage extraction failed" , exc_info = True )
354393 try :
355- container .stop (timeout = 1 )
394+ try :
395+ container .stop (timeout = 1 )
396+ finally :
397+ # Ensure restart() doesn't accidentally delete the container mid-test.
398+ container .remove (force = True )
356399 finally :
357- # Ensure restart() doesn't accidentally delete the container mid-test.
358- container .remove (force = True )
400+ try :
401+ network .remove ()
402+ except Exception :
403+ logger .warning (f"Failed to remove network { network .name } " , exc_info = True )
0 commit comments