1212
1313REPO_ROOT = Path (__file__ ).resolve ().parent .parent .parent
1414EXAMPLES_DIR = REPO_ROOT / 'examples'
15+ DAPR_PORT_BIND_FAILURE_MARKERS = (
16+ 'bind: address already in use' ,
17+ 'failed to start internal gRPC server: could not listen on any endpoint' ,
18+ )
1519
1620
1721def pytest_configure (config : pytest .Config ) -> None :
@@ -38,7 +42,18 @@ def _terminate(proc: subprocess.Popen[str]) -> None:
3842 terminate_process_group (proc , force = True )
3943 proc .wait ()
4044
41- def run (self , args : str , * , timeout : int = 30 , until : list [str ] | None = None ) -> str :
45+ @staticmethod
46+ def _is_dapr_port_bind_failure (output : str ) -> bool :
47+ return all (marker in output for marker in DAPR_PORT_BIND_FAILURE_MARKERS )
48+
49+ def run (
50+ self ,
51+ args : str ,
52+ * ,
53+ timeout : int = 30 ,
54+ until : list [str ] | None = None ,
55+ port_bind_retries : int = 1 ,
56+ ) -> str :
4257 """Run a foreground command, block until it finishes, and return output.
4358
4459 Use this for short-lived processes (e.g. a publisher that exits on its
@@ -49,7 +64,19 @@ def run(self, args: str, *, timeout: int = 30, until: list[str] | None = None) -
4964 timeout: Maximum seconds to wait before killing the process.
5065 until: If provided, the process is terminated as soon as every
5166 string in this list has appeared in the accumulated output.
67+ port_bind_retries: Retry count for Dapr sidecar startup failures
68+ caused by a transient random-port collision.
5269 """
70+ attempts = max (1 , port_bind_retries + 1 )
71+ for attempt in range (attempts ):
72+ output = self ._run_once (args , timeout = timeout , until = until )
73+ if attempt < attempts - 1 and self ._is_dapr_port_bind_failure (output ):
74+ continue
75+ return output
76+
77+ return output
78+
79+ def _run_once (self , args : str , * , timeout : int , until : list [str ] | None ) -> str :
5380 proc = subprocess .Popen (
5481 args = ('dapr' , 'run' , * shlex .split (args )),
5582 cwd = self ._cwd ,
0 commit comments