@@ -166,6 +166,42 @@ def require_cached_collector_image(image):
166166 )
167167
168168
169+ def model_loading_container_user (directory ):
170+ owner = directory .stat ()
171+ return f"--user={ owner .st_uid } :{ owner .st_gid } "
172+
173+
174+ def model_loading_startup_diagnostics (container ):
175+ results = []
176+ commands = (
177+ [
178+ "docker" ,
179+ "inspect" ,
180+ "--format" ,
181+ "{{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}" ,
182+ container ,
183+ ],
184+ ["docker" , "logs" , "--tail" , "20" , container ],
185+ )
186+ for command in commands :
187+ try :
188+ result = subprocess .run ( # noqa: S603 - fixed diagnostics for the exact test-owned container
189+ command , check = False , capture_output = True , text = True , errors = "replace" , timeout = 5
190+ )
191+ results .append (f"{ command [1 ]} exit={ result .returncode } : { (result .stdout + result .stderr )[- 4096 :]} " )
192+ except (OSError , subprocess .SubprocessError ) as error :
193+ results .append (f"{ command [1 ]} unavailable: { type (error ).__name__ } " )
194+ return "Collector startup diagnostics:\n " + "\n " .join (results )
195+
196+
197+ def wait_for_model_loading_collector (container , health_check ):
198+ try :
199+ wait_for (health_check )
200+ except AssertionError as error :
201+ error .add_note (model_loading_startup_diagnostics (container ))
202+ raise
203+
204+
169205def cleanup_model_loading_collector (container , original_error ):
170206 try :
171207 removed = subprocess .run ( # noqa: S603 - exact test-owned container
@@ -206,6 +242,46 @@ def missing(*args):
206242 assert calls == [("docker" , "image" , "inspect" , image )]
207243
208244
245+ def test_model_loading_container_uses_private_fixture_owner (tmp_path ):
246+ tmp_path .chmod (0o700 )
247+ owner = tmp_path .stat ()
248+ assert model_loading_container_user (tmp_path ) == f"--user={ owner .st_uid } :{ owner .st_gid } "
249+ assert owner .st_mode & 0o777 == 0o700
250+
251+
252+ @pytest .mark .parametrize ("diagnostics_fail" , [False , True ])
253+ def test_model_loading_startup_failure_retains_bounded_diagnostics (monkeypatch , diagnostics_fail ):
254+ original_error = AssertionError ("collector not ready" )
255+ calls = []
256+
257+ def failed_wait (_predicate ):
258+ raise original_error
259+
260+ def fake_run (args , ** kwargs ):
261+ calls .append ((args , kwargs ))
262+ if diagnostics_fail :
263+ raise subprocess .TimeoutExpired (args , kwargs ["timeout" ])
264+ raw_output = b"exited exit=1 oom=false" if args [1 ] == "inspect" else b"x" * 8192 + b"permission denied\xff "
265+ output = raw_output .decode ("utf-8" , errors = kwargs .get ("errors" , "strict" ))
266+ return subprocess .CompletedProcess (args , 0 , output , "" )
267+
268+ monkeypatch .setitem (wait_for_model_loading_collector .__globals__ , "wait_for" , failed_wait )
269+ monkeypatch .setattr (subprocess , "run" , fake_run )
270+ with pytest .raises (AssertionError ) as caught :
271+ wait_for_model_loading_collector ("sie-model-loading-test" , lambda : True )
272+ assert caught .value is original_error
273+ (note ,) = caught .value .__notes__
274+ assert len (note ) < 8300
275+ assert "TimeoutExpired" in note if diagnostics_fail else "exited exit=1 oom=false" in note
276+ if not diagnostics_fail :
277+ assert "permission denied" in note
278+ assert "\ufffd " in note
279+ assert [args [1 ] for args , _ in calls ] == ["inspect" , "logs" ]
280+ assert calls [1 ][0 ] == ["docker" , "logs" , "--tail" , "20" , "sie-model-loading-test" ]
281+ assert all (args [- 1 ] == "sie-model-loading-test" and kwargs ["timeout" ] == 5 for args , kwargs in calls )
282+ assert all (kwargs ["errors" ] == "replace" for _ , kwargs in calls )
283+
284+
209285@pytest .mark .parametrize ("removed" , [False , True ])
210286@pytest .mark .parametrize ("original_error" , [None , RuntimeError ("startup or test failure" )])
211287def test_model_loading_cleanup_preserves_failures (monkeypatch , removed , original_error ):
@@ -284,7 +360,7 @@ def test_model_loading_gauge_preserves_prometheus_name_and_remote_unit(tmp_path)
284360 "--cap-drop=ALL" ,
285361 "--security-opt=no-new-privileges" ,
286362 "--read-only" ,
287- "--user=0" ,
363+ model_loading_container_user ( tmp_path ) ,
288364 "-p" ,
289365 "127.0.0.1::4338" ,
290366 "-p" ,
@@ -302,7 +378,7 @@ def get(port, path=""):
302378 with urllib .request .urlopen (f"http://127.0.0.1:{ ports [port ]} { path } " , timeout = 2 ) as response :
303379 return response .read (1_048_577 ).decode ()
304380
305- wait_for ( lambda : get (13133 ))
381+ wait_for_model_loading_collector ( container , lambda : get (13133 ))
306382 request = urllib .request .Request (
307383 f"http://127.0.0.1:{ ports [4338 ]} /v1/metrics" ,
308384 data = model_loading_payload (),
0 commit comments