1- use std:: path:: Path ;
2- use std:: sync:: atomic:: { AtomicU16 , AtomicU64 , Ordering } ;
1+ use std:: path:: { Path , PathBuf } ;
2+ use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
3+ use std:: sync:: OnceLock ;
34use std:: time:: Duration ;
45
5- static PORT_COUNTER : AtomicU16 = AtomicU16 :: new ( 0 ) ;
66static ID_COUNTER : AtomicU64 = AtomicU64 :: new ( 0 ) ;
7+ static TEST_IMAGE_TAG : OnceLock < String > = OnceLock :: new ( ) ;
8+
9+ fn build_test_image ( ) -> String {
10+ let manifest_dir = PathBuf :: from ( env ! ( "CARGO_MANIFEST_DIR" ) ) ;
11+ let repo_root = manifest_dir
12+ . parent ( )
13+ . expect ( "CARGO_MANIFEST_DIR should have a parent" ) ;
14+ let tag = "libsql-server:test" . to_string ( ) ;
15+
16+ // Check if image already exists
17+ let check_output = std:: process:: Command :: new ( "docker" )
18+ . args ( [ "images" , "-q" , & tag] )
19+ . output ( )
20+ . expect ( "failed to run docker images" ) ;
21+
22+ if !check_output. stdout . is_empty ( ) {
23+ return tag;
24+ }
25+
26+ let output = std:: process:: Command :: new ( "docker" )
27+ . arg ( "build" )
28+ . arg ( "-t" )
29+ . arg ( & tag)
30+ . arg ( "-f" )
31+ . arg ( repo_root. join ( "Dockerfile" ) )
32+ . arg ( repo_root)
33+ . output ( )
34+ . expect ( "failed to run docker build" ) ;
35+
36+ if !output. status . success ( ) {
37+ panic ! (
38+ "docker build failed: {}" ,
39+ String :: from_utf8_lossy( & output. stderr)
40+ ) ;
41+ }
42+
43+ tag
44+ }
745
8- fn next_port ( ) -> u16 {
9- let counter = PORT_COUNTER . fetch_add ( 1 , Ordering :: SeqCst ) ;
10- 20000 + ( counter % 10000 )
46+ fn get_test_image ( ) -> & ' static str {
47+ TEST_IMAGE_TAG . get_or_init ( || build_test_image ( ) )
48+ }
49+
50+ async fn docker_host_port ( container_name : & str , container_port : u16 ) -> anyhow:: Result < u16 > {
51+ let output = tokio:: process:: Command :: new ( "docker" )
52+ . args ( [ "port" , container_name, & format ! ( "{}" , container_port) ] )
53+ . output ( )
54+ . await ?;
55+ if !output. status . success ( ) {
56+ anyhow:: bail!(
57+ "docker port failed: {}" ,
58+ String :: from_utf8_lossy( & output. stderr)
59+ ) ;
60+ }
61+ let line = String :: from_utf8_lossy ( & output. stdout ) ;
62+ // Format: "0.0.0.0:49153"
63+ let port = line
64+ . trim ( )
65+ . split ( ':' )
66+ . last ( )
67+ . ok_or_else ( || anyhow:: anyhow!( "unexpected docker port output: {}" , line) ) ?
68+ . parse :: < u16 > ( )
69+ . map_err ( |e| anyhow:: anyhow!( "failed to parse port from '{}': {}" , line, e) ) ?;
70+ Ok ( port)
1171}
1272
1373fn unique_id ( ) -> String {
@@ -31,8 +91,6 @@ pub struct MinioFixture {
3191impl MinioFixture {
3292 pub async fn start ( ) -> anyhow:: Result < Self > {
3393 let uid = unique_id ( ) ;
34- let api_port = next_port ( ) ;
35- let console_port = next_port ( ) ;
3694 let container_name = format ! ( "minio-test-{}" , uid) ;
3795 let network_name = format ! ( "sqld-net-{}" , uid) ;
3896
@@ -48,7 +106,7 @@ impl MinioFixture {
48106 ) ;
49107 }
50108
51- // Start minio container
109+ // Start minio container with random host ports
52110 let run_output = tokio:: process:: Command :: new ( "docker" )
53111 . args ( [
54112 "run" ,
@@ -58,9 +116,9 @@ impl MinioFixture {
58116 "--network" ,
59117 & network_name,
60118 "-p" ,
61- & format ! ( "{} :9000", api_port ) ,
119+ " :9000",
62120 "-p" ,
63- & format ! ( "{} :9001", console_port ) ,
121+ " :9001",
64122 "-e" ,
65123 "MINIO_ROOT_USER=minioadmin" ,
66124 "-e" ,
@@ -85,6 +143,10 @@ impl MinioFixture {
85143 ) ;
86144 }
87145
146+ // Discover dynamically assigned host ports
147+ let api_port = docker_host_port ( & container_name, 9000 ) . await ?;
148+ let console_port = docker_host_port ( & container_name, 9001 ) . await ?;
149+
88150 // Wait for minio to be ready
89151 let client = reqwest:: Client :: new ( ) ;
90152 let deadline = tokio:: time:: Instant :: now ( ) + Duration :: from_secs ( 30 ) ;
@@ -146,10 +208,6 @@ impl MinioFixture {
146208 } )
147209 }
148210
149- pub fn endpoint ( & self ) -> String {
150- format ! ( "http://127.0.0.1:{}" , self . api_port)
151- }
152-
153211 pub fn internal_endpoint ( & self ) -> String {
154212 format ! ( "http://{}:9000" , self . container_name)
155213 }
@@ -168,7 +226,7 @@ impl MinioFixture {
168226 Ok ( ( ) )
169227 }
170228
171- pub async fn restart ( & self ) -> anyhow:: Result < ( ) > {
229+ pub async fn restart ( & mut self ) -> anyhow:: Result < ( ) > {
172230 let output = tokio:: process:: Command :: new ( "docker" )
173231 . args ( [ "start" , & self . container_name ] )
174232 . output ( )
@@ -179,6 +237,9 @@ impl MinioFixture {
179237 String :: from_utf8_lossy( & output. stderr)
180238 ) ;
181239 }
240+ // Re-discover host ports after restart
241+ self . api_port = docker_host_port ( & self . container_name , 9000 ) . await ?;
242+ self . console_port = docker_host_port ( & self . container_name , 9001 ) . await ?;
182243 // Wait for minio to be ready
183244 let client = reqwest:: Client :: new ( ) ;
184245 let deadline = tokio:: time:: Instant :: now ( ) + Duration :: from_secs ( 30 ) ;
@@ -208,18 +269,19 @@ impl MinioFixture {
208269 }
209270}
210271
211- pub struct SqldFixture < ' a > {
212- minio : & ' a MinioFixture ,
272+ pub struct SqldFixture {
273+ network_name : String ,
274+ internal_endpoint : String ,
213275 http_port : u16 ,
214276 pub container_name : String ,
215277}
216278
217- impl < ' a > SqldFixture < ' a > {
218- pub fn new ( minio : & ' a MinioFixture ) -> Self {
219- let http_port = next_port ( ) ;
279+ impl SqldFixture {
280+ pub fn new ( minio : & MinioFixture ) -> Self {
220281 Self {
221- minio,
222- http_port,
282+ network_name : minio. network_name . clone ( ) ,
283+ internal_endpoint : minio. internal_endpoint ( ) ,
284+ http_port : 0 ,
223285 container_name : format ! ( "sqld-test-{}" , unique_id( ) ) ,
224286 }
225287 }
@@ -232,7 +294,6 @@ impl<'a> SqldFixture<'a> {
232294 . await ;
233295
234296 let data_dir_str = data_dir. to_str ( ) . unwrap ( ) ;
235- let network_name = & self . minio . network_name ;
236297
237298 let run_output = tokio:: process:: Command :: new ( "docker" )
238299 . args ( [
@@ -241,11 +302,11 @@ impl<'a> SqldFixture<'a> {
241302 "--name" ,
242303 & self . container_name ,
243304 "--network" ,
244- network_name,
305+ & self . network_name ,
245306 "-p" ,
246- & format ! ( "{} :8080", self . http_port ) ,
307+ " :8080",
247308 "-e" ,
248- & format ! ( "LIBSQL_BOTTOMLESS_ENDPOINT={}" , self . minio . internal_endpoint( ) ) ,
309+ & format ! ( "LIBSQL_BOTTOMLESS_ENDPOINT={}" , self . internal_endpoint) ,
249310 "-e" ,
250311 "LIBSQL_BOTTOMLESS_BUCKET=bottomless" ,
251312 "-e" ,
@@ -258,9 +319,15 @@ impl<'a> SqldFixture<'a> {
258319 "SQLD_ENABLE_BOTTOMLESS_REPLICATION=true" ,
259320 "-e" ,
260321 "SQLD_DB_PATH=/var/lib/sqld" ,
322+ "-e" ,
323+ "LIBSQL_BOTTOMLESS_S3_READ_TIMEOUT_SECS=5" ,
324+ "-e" ,
325+ "LIBSQL_BOTTOMLESS_S3_CONNECT_TIMEOUT_SECS=5" ,
326+ "-e" ,
327+ "LIBSQL_BOTTOMLESS_S3_OPERATION_ATTEMPT_TIMEOUT_SECS=10" ,
261328 "-v" ,
262329 & format ! ( "{}:/var/lib/sqld" , data_dir_str) ,
263- "ghcr.io/tursodatabase/libsql-server:latest" ,
330+ get_test_image ( ) ,
264331 ] )
265332 . output ( )
266333 . await ?;
@@ -272,6 +339,8 @@ impl<'a> SqldFixture<'a> {
272339 ) ;
273340 }
274341
342+ self . http_port = docker_host_port ( & self . container_name , 8080 ) . await ?;
343+
275344 Ok ( ( ) )
276345 }
277346
@@ -314,6 +383,7 @@ impl<'a> SqldFixture<'a> {
314383 String :: from_utf8_lossy( & output. stderr)
315384 ) ;
316385 }
386+ self . http_port = docker_host_port ( & self . container_name , 8080 ) . await ?;
317387 Ok ( ( ) )
318388 }
319389
0 commit comments