use heroforge_core::sync::quic::{QuicClient, QuicServer};
use heroforge_core::Repository;
use std::path::Path;
use std::time::Instant;
use tokio::runtime::Runtime;
const NUM_FILES: usize = 1000;
const NUM_MODIFIED: usize = 4;
fn main() {
println!("╔════════════════════════════════════════════════════════════╗");
println!("║ QUIC Incremental Sync Test - heroforge ║");
println!("╚════════════════════════════════════════════════════════════╝\n");
let test_dir = Path::new("/tmp/quic_incremental_test");
if test_dir.exists() {
std::fs::remove_dir_all(test_dir).expect("Failed to clean up");
}
std::fs::create_dir_all(test_dir).expect("Failed to create test dir");
let source_path = test_dir.join("source.forge");
let dest_path = test_dir.join("dest.forge");
println!(
"=== Step 1: Creating source repository with {} files ===\n",
NUM_FILES
);
let start = Instant::now();
let source_repo = Repository::init(&source_path).expect("Failed to init source repo");
let init_hash = source_repo
.commit_builder()
.message("initial empty check-in")
.author("test-user")
.initial()
.execute()
.expect("Failed to create initial checkin");
println!(" Repository initialized: {}", source_path.display());
println!(" Initial checkin: {}", &init_hash[..16]);
let mut files_data: Vec<(String, Vec<u8>)> = Vec::with_capacity(NUM_FILES);
for i in 0..NUM_FILES {
let name = format!("src/file_{:04}.txt", i);
let content = format!(
"File number {}\nCreated for incremental sync test\nRandom data: {}\nMore content line 4\nLine 5\n",
i,
i * 17 + 42
);
files_data.push((name, content.into_bytes()));
}
files_data.push((
"README.md".to_string(),
b"# Incremental Sync Test\n\nThis repo has 1000 test files.\n".to_vec(),
));
let files: Vec<(&str, &[u8])> = files_data
.iter()
.map(|(n, c)| (n.as_str(), c.as_slice()))
.collect();
let commit_hash = source_repo
.commit_builder()
.message("Add 1000 test files")
.author("test-user")
.parent(&init_hash)
.branch("trunk")
.files(&files)
.execute()
.expect("Failed to commit files");
let create_time = start.elapsed();
println!(
" Created {} files in commit: {}",
NUM_FILES,
&commit_hash[..16]
);
println!(" Time: {:.2}s\n", create_time.as_secs_f64());
let source_files = source_repo
.files()
.at_commit(&commit_hash)
.list()
.expect("Failed to list files");
println!(" Source file count: {}", source_files.len());
assert_eq!(
source_files.len(),
NUM_FILES + 1,
"Expected {} files",
NUM_FILES + 1
);
let source_artifacts: i64 = source_repo
.database()
.connection()
.query_row("SELECT COUNT(*) FROM blob", [], |row| row.get(0))
.unwrap();
println!(" Source artifact count: {}\n", source_artifacts);
println!("=== Step 2: Initial sync via QUIC ===\n");
let dest_repo = Repository::init(&dest_path).expect("Failed to init dest repo");
let project_code = source_repo
.project_code()
.expect("Failed to get project code");
dest_repo
.database()
.connection()
.execute(
"UPDATE config SET value = ?1 WHERE name = 'project-code'",
[&project_code],
)
.expect("Failed to set project code");
println!(" Destination initialized: {}", dest_path.display());
println!(" Project code: {}\n", &project_code[..16]);
drop(source_repo);
drop(dest_repo);
let (initial_sent, initial_received) = run_quic_sync(&source_path, &dest_path, "Initial sync");
let dest_repo = Repository::open(&dest_path).expect("Failed to open dest");
let dest_files = dest_repo
.files()
.at_commit(&commit_hash)
.list()
.expect("Failed to list dest files");
println!(" Destination file count: {}", dest_files.len());
assert_eq!(
dest_files.len(),
NUM_FILES + 1,
"Destination should have all files"
);
for i in [0, 100, 500, 999] {
let name = format!("src/file_{:04}.txt", i);
let content = dest_repo
.files()
.at_commit(&commit_hash)
.read_string(&name)
.expect("Failed to read file");
let expected_start = format!("File number {}", i);
assert!(
content.starts_with(&expected_start),
"Content mismatch for {}",
name
);
}
println!(" Content verification: PASSED\n");
let dest_artifacts: i64 = dest_repo
.database()
.connection()
.query_row("SELECT COUNT(*) FROM blob", [], |row| row.get(0))
.unwrap();
println!(" Destination artifact count: {}", dest_artifacts);
println!(" Initial sync sent {} artifacts\n", initial_sent);
drop(dest_repo);
println!(
"=== Step 3: Modifying {} files in source ===\n",
NUM_MODIFIED
);
let source_repo = Repository::open_rw(&source_path).expect("Failed to open source");
let mut modified_files_data: Vec<(String, Vec<u8>)> = Vec::with_capacity(NUM_FILES + 1);
let modified_indices = [0, 250, 500, 750];
for i in 0..NUM_FILES {
let name = format!("src/file_{:04}.txt", i);
let content = if modified_indices.contains(&i) {
format!(
"MODIFIED - File number {}\nThis file was updated!\nNew content here.\nTimestamp: {}\n",
i,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
)
} else {
format!(
"File number {}\nCreated for incremental sync test\nRandom data: {}\nMore content line 4\nLine 5\n",
i,
i * 17 + 42
)
};
modified_files_data.push((name, content.into_bytes()));
}
modified_files_data.push((
"README.md".to_string(),
b"# Incremental Sync Test\n\nThis repo has 1000 test files.\n".to_vec(),
));
let files: Vec<(&str, &[u8])> = modified_files_data
.iter()
.map(|(n, c)| (n.as_str(), c.as_slice()))
.collect();
let modified_hash = source_repo
.commit_builder()
.message("Modify 4 files")
.author("test-user")
.parent(&commit_hash)
.branch("trunk")
.files(&files)
.execute()
.expect("Failed to commit modifications");
println!(
" Modified files: {:?}",
modified_indices
.iter()
.map(|i| format!("file_{:04}.txt", i))
.collect::<Vec<_>>()
);
println!(" New commit: {}\n", &modified_hash[..16]);
let source_artifacts_after: i64 = source_repo
.database()
.connection()
.query_row("SELECT COUNT(*) FROM blob", [], |row| row.get(0))
.unwrap();
let new_artifacts = source_artifacts_after - source_artifacts;
println!(
" New artifacts in source: {} (4 modified files + 1 manifest)\n",
new_artifacts
);
drop(source_repo);
println!("=== Step 4: Incremental sync (only modified files) ===\n");
let (incr_sent, incr_received) = run_quic_sync(&source_path, &dest_path, "Incremental sync");
println!("\n Incremental sync sent {} artifacts", incr_sent);
println!(" Expected: ~5 (4 modified files + 1 manifest)");
if incr_sent <= 10 {
println!(" Result: PASSED - Only modified files were transferred!\n");
} else {
println!(" Result: WARNING - More artifacts than expected were transferred\n");
}
let dest_repo = Repository::open(&dest_path).expect("Failed to open dest");
for i in modified_indices {
let name = format!("src/file_{:04}.txt", i);
let content = dest_repo
.files()
.at_commit(&modified_hash)
.read_string(&name)
.expect("Failed to read file");
assert!(
content.starts_with("MODIFIED"),
"File {} should be modified",
name
);
}
println!(" Modified content verification: PASSED");
let unmodified_indices = [1, 100, 999];
for i in unmodified_indices {
let name = format!("src/file_{:04}.txt", i);
let content = dest_repo
.files()
.at_commit(&modified_hash)
.read_string(&name)
.expect("Failed to read file");
let expected_start = format!("File number {}", i);
assert!(
content.starts_with(&expected_start),
"File {} should be unmodified",
name
);
}
println!(" Unmodified content verification: PASSED\n");
let dest_artifacts_final: i64 = dest_repo
.database()
.connection()
.query_row("SELECT COUNT(*) FROM blob", [], |row| row.get(0))
.unwrap();
println!("╔════════════════════════════════════════════════════════════╗");
println!("║ SUMMARY ║");
println!("╚════════════════════════════════════════════════════════════╝\n");
println!(" Source repository: {}", source_path.display());
println!(" Destination repository: {}", dest_path.display());
println!(" Total files: {}", NUM_FILES + 1);
println!(" Modified files: {}", NUM_MODIFIED);
println!();
println!(" Initial sync:");
println!(" - Artifacts sent: {}", initial_sent);
println!(" - Artifacts received: {}", initial_received);
println!();
println!(" Incremental sync:");
println!(" - Artifacts sent: {}", incr_sent);
println!(" - Artifacts received: {}", incr_received);
println!();
println!(" Final artifact counts:");
println!(" - Source: {}", source_artifacts_after);
println!(" - Destination: {}", dest_artifacts_final);
println!();
if incr_sent <= 10 && dest_artifacts_final == source_artifacts_after {
println!(" TEST PASSED: Incremental sync correctly transferred only modified files!");
} else {
println!(" TEST FAILED: Unexpected sync behavior");
std::process::exit(1);
}
println!("\n To explore the repositories:");
println!(" heroforge ui {}", source_path.display());
println!(" heroforge ui {}", dest_path.display());
}
fn run_quic_sync(source_path: &Path, dest_path: &Path, description: &str) -> (usize, usize) {
println!(" Starting {}...", description);
let source_path = source_path.to_path_buf();
let dest_path = dest_path.to_path_buf();
let rt = Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
let server = QuicServer::bind("127.0.0.1:0").expect("Failed to bind server");
let addr = server.local_addr().expect("Failed to get addr").to_string();
let source_path_clone = source_path.clone();
let server_handle = std::thread::spawn(move || {
let rt = Runtime::new().expect("runtime");
rt.block_on(async {
let repo = Repository::open(&source_path_clone).expect("open source");
server.handle_sync(&repo, &source_path_clone).await
})
});
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let dest_path_clone = dest_path.clone();
let client_result = {
let repo = Repository::open_rw(&dest_path_clone).expect("open dest");
QuicClient::sync(&repo, &dest_path_clone, &addr).await
};
let client_stats = client_result.expect("Client sync failed");
let server_stats = server_handle
.join()
.expect("Server panicked")
.expect("Server sync failed");
(server_stats.artifacts_sent, client_stats.artifacts_received)
});
println!(" {} complete.", description);
result
}