77import json
88import shutil
99import subprocess
10+ import sys
1011import tempfile
1112import time
1213import urllib .request
1314import uuid
14- from collections .abc import Mapping , Sequence
15+ from collections .abc import Callable , Mapping , Sequence
1516from dataclasses import dataclass
1617from pathlib import Path
1718
@@ -70,11 +71,18 @@ def docker_logs(container: str) -> str:
7071 return result .stdout + result .stderr
7172
7273
73- def wait_for_http (url : str , * , timeout : float ) -> bytes :
74+ def wait_for_http (
75+ url : str ,
76+ * ,
77+ timeout : float ,
78+ assert_live : Callable [[], None ] | None = None ,
79+ ) -> bytes :
7480 """Wait until an HTTP endpoint responds successfully or fail with context."""
7581 deadline = time .monotonic () + timeout
7682 last_error : Exception | None = None
7783 while time .monotonic () < deadline :
84+ if assert_live is not None :
85+ assert_live ()
7886 try :
7987 with urllib .request .urlopen (url , timeout = 5 ) as response :
8088 body : object = response .read ()
@@ -92,7 +100,9 @@ def assert_running(container: str) -> None:
92100 """Assert that Docker still considers a container alive."""
93101 state = docker ("inspect" , "--format" , "{{.State.Running}}" , container )
94102 if state != "true" :
95- raise AssertionError (f"container { container } is not running" )
103+ raise AssertionError (
104+ f"container { container } is not running\n logs:\n { docker_logs (container )} "
105+ )
96106
97107
98108def write_json (path : Path , value : Mapping [str , object ]) -> None :
@@ -242,7 +252,11 @@ def assetlink_sites(document: object) -> list[str]:
242252def assert_initial_addon_behavior (runtime : Runtime ) -> None :
243253 """Verify readiness, HA reachability, DAL serving, and tunnel arguments."""
244254 metrics_url = f"http://127.0.0.1:{ published_port (runtime .addon , 36500 )} /metrics"
245- metrics = wait_for_http (metrics_url , timeout = 120 ).decode ()
255+ metrics = wait_for_http (
256+ metrics_url ,
257+ timeout = 120 ,
258+ assert_live = lambda : assert_running (runtime .addon ),
259+ ).decode ()
246260 if "cloudflared_e2e_up 1" not in metrics :
247261 raise AssertionError (f"unexpected metrics payload: { metrics } " )
248262
@@ -275,6 +289,7 @@ def assert_restart_and_reconfiguration(runtime: Runtime) -> None:
275289 wait_for_http (
276290 f"http://127.0.0.1:{ published_port (runtime .addon , 36500 )} /metrics" ,
277291 timeout = 120 ,
292+ assert_live = lambda : assert_running (runtime .addon ),
278293 )
279294 assert_running (runtime .addon )
280295
@@ -289,6 +304,7 @@ def assert_restart_and_reconfiguration(runtime: Runtime) -> None:
289304 wait_for_http (
290305 f"http://127.0.0.1:{ published_port (runtime .addon , 36500 )} /metrics" ,
291306 timeout = 120 ,
307+ assert_live = lambda : assert_running (runtime .addon ),
292308 )
293309 assert_running (runtime .addon )
294310 if (runtime .addon_data / "digital-asset-links" ).exists ():
@@ -385,11 +401,23 @@ def collect_artifacts(
385401 )
386402
387403
388- def cleanup (runtime : Runtime ) -> None :
404+ def cleanup (settings : Settings , runtime : Runtime ) -> None :
389405 """Remove E2E resources independently and idempotently."""
390406 for container in (runtime .invalid_addon , runtime .addon , runtime .home_assistant ):
391407 docker ("rm" , "--force" , container , check = False )
392408 docker ("network" , "rm" , runtime .network , check = False )
409+ docker (
410+ "run" ,
411+ "--rm" ,
412+ "--volume" ,
413+ f"{ runtime .root } :/cleanup" ,
414+ "--entrypoint" ,
415+ "/bin/rm" ,
416+ settings .addon_image ,
417+ "-rf" ,
418+ "/cleanup" ,
419+ check = False ,
420+ )
393421
394422
395423def parse_settings () -> Settings :
@@ -413,7 +441,10 @@ def parse_settings() -> Settings:
413441
414442def main () -> None :
415443 settings = parse_settings ()
416- with tempfile .TemporaryDirectory (prefix = "hass-cloudflared-e2e-" ) as temporary :
444+ with tempfile .TemporaryDirectory (
445+ prefix = "hass-cloudflared-e2e-" ,
446+ ignore_cleanup_errors = True ,
447+ ) as temporary :
417448 runtime = create_runtime (Path (temporary ))
418449 version : str | None = None
419450 try :
@@ -424,9 +455,23 @@ def main() -> None:
424455 assert_invalid_config_is_rejected (settings , runtime )
425456 assert_running (runtime .home_assistant )
426457 print (f"E2E passed against Home Assistant { version } " )
458+ except Exception :
459+ for label , container in (
460+ ("Home Assistant" , runtime .home_assistant ),
461+ ("add-on" , runtime .addon ),
462+ ("invalid add-on" , runtime .invalid_addon ),
463+ ):
464+ print (
465+ f"\n --- { label } logs ---\n { docker_logs (container )} " ,
466+ file = sys .stderr ,
467+ )
468+ raise
427469 finally :
428- collect_artifacts (settings , runtime , version = version )
429- cleanup (runtime )
470+ try :
471+ collect_artifacts (settings , runtime , version = version )
472+ except (OSError , RuntimeError ) as error :
473+ print (f"Unable to collect all E2E artifacts: { error } " , file = sys .stderr )
474+ cleanup (settings , runtime )
430475
431476
432477if __name__ == "__main__" :
0 commit comments