Skip to content

Commit 97a2c5f

Browse files
committed
Add /export/ and /info/ to mockServer + add test cases for bootstrapping
1 parent 6e55668 commit 97a2c5f

File tree

1 file changed

+112
-3
lines changed

1 file changed

+112
-3
lines changed

libsql/src/sync/test.rs

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,73 @@ async fn test_sync_context_retry_on_error() {
251251
assert_eq!(server.frame_count(), 1);
252252
}
253253

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+
254321
#[test]
255322
fn test_hash_verification() {
256323
let mut metadata = MetadataJson {
@@ -328,12 +395,14 @@ impl Service<http::Uri> for MockConnector {
328395
}
329396
}
330397

398+
#[allow(dead_code)]
331399
struct MockServer {
332400
url: String,
333401
frame_count: Arc<AtomicU32>,
334402
connector: ConnectorService,
335403
return_error: Arc<AtomicBool>,
336404
request_count: Arc<AtomicU32>,
405+
export_bytes: Arc<Vec<u8>>, // bytes returned by /export/{generation}
337406
}
338407

339408
impl MockServer {
@@ -342,6 +411,25 @@ impl MockServer {
342411
let return_error = Arc::new(AtomicBool::new(false));
343412
let request_count = Arc::new(AtomicU32::new(0));
344413

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+
345433
// Create the mock connector with Some(client_stream)
346434
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
347435
let mock_connector = MockConnector { tx };
@@ -353,18 +441,21 @@ impl MockServer {
353441
connector,
354442
return_error: return_error.clone(),
355443
request_count: request_count.clone(),
444+
export_bytes: export_bytes.clone(),
356445
};
357446

358447
// Spawn the server handler
359448
let frame_count_clone = frame_count.clone();
360449
let return_error_clone = return_error.clone();
361450
let request_count_clone = request_count.clone();
451+
let export_bytes_clone = export_bytes.clone();
362452

363453
tokio::spawn(async move {
364454
while let Some(server_stream) = rx.recv().await {
365455
let frame_count_clone = frame_count_clone.clone();
366456
let return_error_clone = return_error_clone.clone();
367457
let request_count_clone = request_count_clone.clone();
458+
let export_bytes_clone = export_bytes_clone.clone();
368459

369460
tokio::spawn(async move {
370461
use hyper::server::conn::Http;
@@ -377,6 +468,7 @@ impl MockServer {
377468
let frame_count = frame_count_clone.clone();
378469
let return_error = return_error_clone.clone();
379470
let request_count = request_count_clone.clone();
471+
let export_bytes = export_bytes_clone.clone();
380472
async move {
381473
request_count.fetch_add(1, Ordering::SeqCst);
382474
if return_error.load(Ordering::SeqCst) {
@@ -388,9 +480,9 @@ impl MockServer {
388480
);
389481
}
390482

391-
let current_count = frame_count.fetch_add(1, Ordering::SeqCst);
392-
393483
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);
394486
// Return the max_frame_no that has been accepted
395487
let response = serde_json::json!({
396488
"status": "ok",
@@ -404,6 +496,23 @@ impl MockServer {
404496
.body(Body::from(response.to_string()))
405497
.unwrap(),
406498
)
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+
)
407516
} else {
408517
Ok(http::Response::builder()
409518
.status(404)
@@ -489,4 +598,4 @@ fn gen_metadata_file(db_path: &Path, hash: u32, version: u32, durable_frame_num:
489598
.as_bytes(),
490599
)
491600
.unwrap();
492-
}
601+
}

0 commit comments

Comments
 (0)