@@ -251,6 +251,73 @@ async fn test_sync_context_retry_on_error() {
251
251
assert_eq ! ( server. frame_count( ) , 1 ) ;
252
252
}
253
253
254
+ #[ tokio:: test]
255
+ async fn test_bootstrap_db_downloads_export ( ) {
256
+ let server = MockServer :: start ( ) ;
257
+ let temp_dir = tempdir ( ) . unwrap ( ) ;
258
+ let db_path = temp_dir. path ( ) . join ( "bootstrap.db" ) ;
259
+
260
+ // Seed metadata so SyncContext can be constructed (generation=1)
261
+ gen_metadata_file ( & db_path, 3278479626 , 0 , 0 , 1 ) ;
262
+
263
+ let mut sync_ctx = SyncContext :: new (
264
+ server. connector ( ) ,
265
+ db_path. to_str ( ) . unwrap ( ) . to_string ( ) ,
266
+ server. url ( ) ,
267
+ None ,
268
+ None ,
269
+ )
270
+ . await
271
+ . unwrap ( ) ;
272
+
273
+
274
+ let _ = std:: fs:: remove_file ( & db_path) ;
275
+ let _ = std:: fs:: remove_file ( format ! ( "{}-info" , db_path. to_str( ) . unwrap( ) ) ) ;
276
+
277
+ // Bootstrap should fetch /info and then /export/{generation}
278
+ crate :: sync:: bootstrap_db ( & mut sync_ctx) . await . unwrap ( ) ;
279
+
280
+ assert ! ( std:: path:: Path :: new( & db_path) . exists( ) ) ;
281
+ assert ! ( std:: path:: Path :: new( & format!( "{}-info" , db_path. to_str( ) . unwrap( ) ) ) . exists( ) ) ;
282
+
283
+ assert_eq ! ( sync_ctx. durable_generation( ) , 1 ) ;
284
+ assert_eq ! ( sync_ctx. durable_frame_num( ) , 0 ) ;
285
+
286
+ assert ! ( server. request_count( ) >= 2 ) ;
287
+ }
288
+
289
+ #[ tokio:: test]
290
+ async fn test_bootstrap_db_is_idempotent ( ) {
291
+ let server = MockServer :: start ( ) ;
292
+ let temp_dir = tempdir ( ) . unwrap ( ) ;
293
+ let db_path = temp_dir. path ( ) . join ( "bootstrap2.db" ) ;
294
+
295
+
296
+ gen_metadata_file ( & db_path, 3278479626 , 0 , 0 , 1 ) ;
297
+
298
+ let mut sync_ctx = SyncContext :: new (
299
+ server. connector ( ) ,
300
+ db_path. to_str ( ) . unwrap ( ) . to_string ( ) ,
301
+ server. url ( ) ,
302
+ None ,
303
+ None ,
304
+ )
305
+ . await
306
+ . unwrap ( ) ;
307
+
308
+ let _ = std:: fs:: remove_file ( & db_path) ;
309
+ let _ = std:: fs:: remove_file ( format ! ( "{}-info" , db_path. to_str( ) . unwrap( ) ) ) ;
310
+
311
+
312
+ crate :: sync:: bootstrap_db ( & mut sync_ctx) . await . unwrap ( ) ;
313
+ let first_requests = server. request_count ( ) ;
314
+
315
+ // Second bootstrap should be a no-op (no new network calls)
316
+ crate :: sync:: bootstrap_db ( & mut sync_ctx) . await . unwrap ( ) ;
317
+ let second_requests = server. request_count ( ) ;
318
+ assert_eq ! ( first_requests, second_requests) ;
319
+ }
320
+
254
321
#[ test]
255
322
fn test_hash_verification ( ) {
256
323
let mut metadata = MetadataJson {
@@ -328,12 +395,14 @@ impl Service<http::Uri> for MockConnector {
328
395
}
329
396
}
330
397
398
+ #[ allow( dead_code) ]
331
399
struct MockServer {
332
400
url : String ,
333
401
frame_count : Arc < AtomicU32 > ,
334
402
connector : ConnectorService ,
335
403
return_error : Arc < AtomicBool > ,
336
404
request_count : Arc < AtomicU32 > ,
405
+ export_bytes : Arc < Vec < u8 > > , // bytes returned by /export/{generation}
337
406
}
338
407
339
408
impl MockServer {
@@ -342,6 +411,25 @@ impl MockServer {
342
411
let return_error = Arc :: new ( AtomicBool :: new ( false ) ) ;
343
412
let request_count = Arc :: new ( AtomicU32 :: new ( 0 ) ) ;
344
413
414
+ let export_bytes: Arc < Vec < u8 > > = {
415
+ use crate :: local:: Database ;
416
+ use crate :: database:: OpenFlags ;
417
+ use std:: fs;
418
+ use tempfile:: NamedTempFile ;
419
+
420
+ let tmp = NamedTempFile :: new ( ) . expect ( "temp file for export db" ) ;
421
+ let path = tmp. path ( ) . to_path_buf ( ) ;
422
+ let db = Database :: open ( path. to_str ( ) . unwrap ( ) . to_string ( ) , OpenFlags :: default ( ) )
423
+ . expect ( "open export db" ) ;
424
+ let conn = db. connect ( ) . expect ( "connect export db" ) ;
425
+
426
+ let _ = conn. query ( "CREATE TABLE IF NOT EXISTS t(x INTEGER);" , crate :: params:: Params :: None ) ;
427
+ drop ( conn) ;
428
+ drop ( db) ;
429
+ let bytes = fs:: read ( & path) . expect ( "read export db bytes" ) ;
430
+ Arc :: new ( bytes)
431
+ } ;
432
+
345
433
// Create the mock connector with Some(client_stream)
346
434
let ( tx, mut rx) = tokio:: sync:: mpsc:: channel ( 1 ) ;
347
435
let mock_connector = MockConnector { tx } ;
@@ -353,18 +441,21 @@ impl MockServer {
353
441
connector,
354
442
return_error : return_error. clone ( ) ,
355
443
request_count : request_count. clone ( ) ,
444
+ export_bytes : export_bytes. clone ( ) ,
356
445
} ;
357
446
358
447
// Spawn the server handler
359
448
let frame_count_clone = frame_count. clone ( ) ;
360
449
let return_error_clone = return_error. clone ( ) ;
361
450
let request_count_clone = request_count. clone ( ) ;
451
+ let export_bytes_clone = export_bytes. clone ( ) ;
362
452
363
453
tokio:: spawn ( async move {
364
454
while let Some ( server_stream) = rx. recv ( ) . await {
365
455
let frame_count_clone = frame_count_clone. clone ( ) ;
366
456
let return_error_clone = return_error_clone. clone ( ) ;
367
457
let request_count_clone = request_count_clone. clone ( ) ;
458
+ let export_bytes_clone = export_bytes_clone. clone ( ) ;
368
459
369
460
tokio:: spawn ( async move {
370
461
use hyper:: server:: conn:: Http ;
@@ -377,6 +468,7 @@ impl MockServer {
377
468
let frame_count = frame_count_clone. clone ( ) ;
378
469
let return_error = return_error_clone. clone ( ) ;
379
470
let request_count = request_count_clone. clone ( ) ;
471
+ let export_bytes = export_bytes_clone. clone ( ) ;
380
472
async move {
381
473
request_count. fetch_add ( 1 , Ordering :: SeqCst ) ;
382
474
if return_error. load ( Ordering :: SeqCst ) {
@@ -388,9 +480,9 @@ impl MockServer {
388
480
) ;
389
481
}
390
482
391
- let current_count = frame_count. fetch_add ( 1 , Ordering :: SeqCst ) ;
392
-
393
483
if req. uri ( ) . path ( ) . contains ( "/sync/" ) {
484
+ // Count only sync requests as frames to keep tests stable.
485
+ let current_count = frame_count. fetch_add ( 1 , Ordering :: SeqCst ) ;
394
486
// Return the max_frame_no that has been accepted
395
487
let response = serde_json:: json!( {
396
488
"status" : "ok" ,
@@ -404,6 +496,23 @@ impl MockServer {
404
496
. body ( Body :: from ( response. to_string ( ) ) )
405
497
. unwrap ( ) ,
406
498
)
499
+ } else if req. uri ( ) . path ( ) . eq ( "/info" ) {
500
+ let response = serde_json:: json!( {
501
+ "current_generation" : 1
502
+ } ) ;
503
+ Ok :: < _ , hyper:: Error > (
504
+ http:: Response :: builder ( )
505
+ . status ( 200 )
506
+ . body ( Body :: from ( response. to_string ( ) ) )
507
+ . unwrap ( ) ,
508
+ )
509
+ } else if req. uri ( ) . path ( ) . starts_with ( "/export/" ) {
510
+ Ok :: < _ , hyper:: Error > (
511
+ http:: Response :: builder ( )
512
+ . status ( 200 )
513
+ . body ( Body :: from ( export_bytes. as_ref ( ) . clone ( ) ) )
514
+ . unwrap ( ) ,
515
+ )
407
516
} else {
408
517
Ok ( http:: Response :: builder ( )
409
518
. status ( 404 )
@@ -489,4 +598,4 @@ fn gen_metadata_file(db_path: &Path, hash: u32, version: u32, durable_frame_num:
489
598
. as_bytes ( ) ,
490
599
)
491
600
. unwrap ( ) ;
492
- }
601
+ }
0 commit comments