This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-pack / tests / chaos.rs
5.8 kB 175 lines
1use std::path::Path; 2use std::process::{Command, Stdio}; 3use std::time::{Duration, Instant}; 4 5use knot_git::Layout; 6use knot_types::{Oid, RefName, RepoDid}; 7 8mod common; 9use common::{must, pack_objects, receive_request}; 10 11const DID: &str = "did:plc:squid"; 12const BLOB_BYTES: usize = 32 * 1024 * 1024; 13 14fn incompressible(len: usize) -> Vec<u8> { 15 let mut state = 0x2545_f491_4f6c_dd1du64; 16 (0..len) 17 .map(|_| { 18 state ^= state << 13; 19 state ^= state >> 7; 20 state ^= state << 17; 21 (state & 0xff) as u8 22 }) 23 .collect() 24} 25 26struct Seed { 27 c1: String, 28 c2: String, 29 request: std::path::PathBuf, 30} 31 32fn build_seed(scratch: &Path) -> Seed { 33 let work = scratch.join("work"); 34 std::fs::create_dir_all(&work).unwrap(); 35 must(&work, &["init", "-q", "-b", "main"]); 36 std::fs::write(work.join("base.txt"), "baseline\n").unwrap(); 37 must(&work, &["add", "-A"]); 38 must(&work, &["commit", "-q", "-m", "c1"]); 39 let c1 = must(&work, &["rev-parse", "HEAD"]); 40 std::fs::write(work.join("big.bin"), incompressible(BLOB_BYTES)).unwrap(); 41 must(&work, &["add", "-A"]); 42 must(&work, &["commit", "-q", "-m", "c2"]); 43 let c2 = must(&work, &["rev-parse", "HEAD"]); 44 45 let oids: Vec<String> = must(&work, &["rev-list", "--objects", &c2, "--not", &c1]) 46 .lines() 47 .map(|line| line.split_whitespace().next().unwrap().to_string()) 48 .collect(); 49 let pack = pack_objects(&work, &oids); 50 let request = scratch.join("c2.request"); 51 std::fs::write( 52 &request, 53 receive_request("refs/heads/main", &c1, &c2, &pack), 54 ) 55 .unwrap(); 56 Seed { c1, c2, request } 57} 58 59fn fresh_repo(scratch: &Path, trial: usize, seed: &Seed) -> std::path::PathBuf { 60 let scan = scratch.join(format!("scan-{trial}")); 61 let layout = Layout::new(&scan); 62 let did = RepoDid::new(DID).unwrap(); 63 layout.create(&did).unwrap(); 64 let bare = layout.repo_path(&did).unwrap(); 65 must( 66 scratch.join("work").as_path(), 67 &[ 68 "push", 69 "-q", 70 bare.to_str().unwrap(), 71 &format!("{}:refs/heads/main", seed.c1), 72 ], 73 ); 74 scan 75} 76 77fn spawn_worker(scan: &Path, request: &Path) -> std::process::Child { 78 Command::new(std::env::current_exe().unwrap()) 79 .args(["--exact", "chaos_receive_worker", "--nocapture"]) 80 .env("KNOT_CHAOS_ROLE", "worker") 81 .env("KNOT_CHAOS_SCAN", scan) 82 .env("KNOT_CHAOS_REQUEST", request) 83 .stdout(Stdio::null()) 84 .stderr(Stdio::null()) 85 .spawn() 86 .expect("spawn chaos worker") 87} 88 89fn fsck_clean(bare: &Path) -> Result<(), String> { 90 knot_fixtures::fsck(bare) 91} 92 93fn main_tip(scan: &Path) -> Option<Oid> { 94 let layout = Layout::new(scan); 95 let repo = layout 96 .open(&RepoDid::new(DID).unwrap()) 97 .expect("repo must reopen cleanly after a kill"); 98 repo.find_ref(&RefName::new("refs/heads/main").unwrap()) 99 .expect("references must be readable after a kill") 100} 101 102#[test] 103fn chaos_receive_worker() { 104 if std::env::var("KNOT_CHAOS_ROLE").as_deref() != Ok("worker") { 105 return; 106 } 107 let scan = std::env::var("KNOT_CHAOS_SCAN").unwrap(); 108 let request = std::env::var("KNOT_CHAOS_REQUEST").unwrap(); 109 let layout = Layout::new(&scan); 110 let repo = layout.open(&RepoDid::new(DID).unwrap()).unwrap(); 111 let body = std::fs::read(&request).unwrap(); 112 let _ = knot_pack::receive_pack(&repo, &body); 113} 114 115#[test] 116fn kill9_during_receive_pack_leaves_a_consistent_repo() { 117 let scratch = tempfile::tempdir().unwrap(); 118 let seed = build_seed(scratch.path()); 119 let c1 = Oid::from_hex(&seed.c1).unwrap(); 120 let c2 = Oid::from_hex(&seed.c2).unwrap(); 121 let did = RepoDid::new(DID).unwrap(); 122 123 let warm_scan = fresh_repo(scratch.path(), 9000, &seed); 124 let started = Instant::now(); 125 let mut warm = spawn_worker(&warm_scan, &seed.request); 126 warm.wait().unwrap(); 127 let full = started.elapsed(); 128 assert_eq!( 129 main_tip(&warm_scan), 130 Some(c2), 131 "uninterrupted receive must fast-forward main to new tip" 132 ); 133 134 let fractions = [0.30, 0.45, 0.55, 0.62, 0.70, 0.78, 0.85, 0.92, 1.05, 1.25]; 135 let delays: Vec<Duration> = std::iter::once(Duration::from_millis(2)) 136 .chain(std::iter::once(Duration::from_millis(5))) 137 .chain(fractions.iter().map(|fraction| full.mul_f64(*fraction))) 138 .chain(std::iter::once(full.mul_f64(2.0))) 139 .chain(std::iter::once(full.mul_f64(2.0))) 140 .collect(); 141 142 let outcomes: Vec<Oid> = delays 143 .iter() 144 .enumerate() 145 .map(|(trial, delay)| { 146 let scan = fresh_repo(scratch.path(), trial, &seed); 147 let mut child = spawn_worker(&scan, &seed.request); 148 std::thread::sleep(*delay); 149 let _ = child.kill(); 150 child.wait().unwrap(); 151 152 let bare = Layout::new(&scan).repo_path(&did).unwrap(); 153 fsck_clean(&bare).unwrap_or_else(|errors| { 154 panic!("trial {trial}: killed receive left a corrupt repo:\n{errors}") 155 }); 156 let tip = main_tip(&scan).unwrap_or_else(|| { 157 panic!("trial {trial}: main vanished after a kill, acknowledged ref was lost") 158 }); 159 assert!( 160 tip == c1 || tip == c2, 161 "trial {trial}: main must hold either acknowledged baseline or completed tip, never a torn value, got {tip}" 162 ); 163 tip 164 }) 165 .collect(); 166 167 assert!( 168 outcomes.contains(&c1), 169 "no trial was interrupted before ref update; chaos window never opened" 170 ); 171 assert!( 172 outcomes.contains(&c2), 173 "no trial ran to completion; receive never finished under chosen delays" 174 ); 175}