22"""Pytest plugin"""
33import logging
44import os
5+ import socket
56import subprocess
67import time
8+ import uuid
79from enum import Enum
810from typing import Iterator
911from typing import Optional
2830TEARDOWN_WAIT_SECONDS = 2
2931
3032
33+ def _find_free_port () -> int :
34+ """Find a free port by binding to port 0 and getting the assigned port."""
35+ with socket .socket (socket .AF_INET , socket .SOCK_STREAM ) as s :
36+ s .bind (('' , 0 ))
37+ s .listen (1 )
38+ return s .getsockname ()[1 ]
39+
40+
3141class ExecutionMode (Enum ):
3242 SEQUENTIAL = 1
3343 LEADER = 2
@@ -79,7 +89,11 @@ class _TestContainerManager():
7989 """Manages the setup and teardown of a SingleStoreDB Dev Container"""
8090
8191 def __init__ (self ) -> None :
82- self .container_name = 'singlestoredb-test-container'
92+ # Generate unique container name using UUID and worker ID
93+ worker = os .environ .get ('PYTEST_XDIST_WORKER' , 'master' )
94+ unique_id = uuid .uuid4 ().hex [:8 ]
95+ self .container_name = f'singlestoredb-test-{ worker } -{ unique_id } '
96+
8397 self .dev_image_name = 'ghcr.io/singlestore-labs/singlestoredb-dev'
8498
8599 assert 'SINGLESTORE_LICENSE' in os .environ , 'SINGLESTORE_LICENSE not set'
@@ -91,14 +105,69 @@ def __init__(self) -> None:
91105 'SINGLESTORE_SET_GLOBAL_DEFAULT_PARTITIONS_PER_LEAF' : '1' ,
92106 }
93107
94- self .ports = ['3306' , '8080' , '9000' ]
108+ # Use dynamic port allocation to avoid conflicts
109+ self .mysql_port = _find_free_port ()
110+ self .http_port = _find_free_port ()
111+ self .studio_port = _find_free_port ()
112+ self .ports = [
113+ (self .mysql_port , '3306' ), # External port -> Internal port
114+ (self .http_port , '8080' ),
115+ (self .studio_port , '9000' ),
116+ ]
117+
118+ self .url = f'root:{ self .root_password } @127.0.0.1:{ self .mysql_port } '
119+
120+ def _container_exists (self ) -> bool :
121+ """Check if a container with this name already exists."""
122+ try :
123+ result = subprocess .run (
124+ [
125+ 'docker' , 'ps' , '-a' , '--filter' ,
126+ f'name={ self .container_name } ' ,
127+ '--format' , '{{.Names}}' ,
128+ ],
129+ capture_output = True ,
130+ text = True ,
131+ check = True ,
132+ )
133+ return self .container_name in result .stdout
134+ except subprocess .CalledProcessError :
135+ return False
136+
137+ def _cleanup_existing_container (self ) -> None :
138+ """Stop and remove any existing container with the same name."""
139+ if not self ._container_exists ():
140+ return
95141
96- self .url = f'root:{ self .root_password } @127.0.0.1:3306'
142+ logger .info (f'Found existing container { self .container_name } , cleaning up' )
143+ try :
144+ # Try to stop the container (ignore if it's already stopped)
145+ subprocess .run (
146+ ['docker' , 'stop' , self .container_name ],
147+ capture_output = True ,
148+ check = False ,
149+ )
150+ # Remove the container
151+ subprocess .run (
152+ ['docker' , 'rm' , self .container_name ],
153+ capture_output = True ,
154+ check = True ,
155+ )
156+ logger .debug (f'Cleaned up existing container { self .container_name } ' )
157+ except subprocess .CalledProcessError as e :
158+ logger .warning (f'Failed to cleanup existing container: { e } ' )
159+ # Continue anyway - the unique name should prevent most conflicts
97160
98161 def start (self ) -> None :
162+ # Clean up any existing container with the same name
163+ self ._cleanup_existing_container ()
164+
99165 command = ' ' .join (self ._start_command ())
100166
101- logger .info (f'Starting container { self .container_name } ' )
167+ logger .info (
168+ f'Starting container { self .container_name } on ports { self .mysql_port } , '
169+ f'{ self .http_port } , { self .studio_port } ' ,
170+ )
102171 try :
103172 license = os .environ ['SINGLESTORE_LICENSE' ]
104173 env = {
@@ -108,8 +177,8 @@ def start(self) -> None:
108177 except Exception as e :
109178 logger .exception (e )
110179 raise RuntimeError (
111- 'Failed to start container. '
112- 'Is one already running? ' ,
180+ f 'Failed to start container { self . container_name } . '
181+ f'Command: { command } ' ,
113182 ) from e
114183 logger .debug ('Container started' )
115184
@@ -123,9 +192,9 @@ def _start_command(self) -> Iterator[str]:
123192 else :
124193 yield f'{ key } ={ value } '
125194
126- for port in self .ports :
195+ for external_port , internal_port in self .ports :
127196 yield '-p'
128- yield f'{ port } :{ port } '
197+ yield f'{ external_port } :{ internal_port } '
129198
130199 yield self .dev_image_name
131200
0 commit comments