This repository has no description
0

Configure Feed

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

core / knot2 / crates / knot-server / tests / invariants.rs
8.2 kB 247 lines
1use std::collections::{BTreeMap, BTreeSet}; 2use std::path::{Path, PathBuf}; 3 4use walkdir::WalkDir; 5 6fn knot_root() -> PathBuf { 7 Path::new(env!("CARGO_MANIFEST_DIR")) 8 .ancestors() 9 .nth(2) 10 .expect("knot root is two levels above server crate") 11 .to_path_buf() 12} 13 14fn workspace_root() -> PathBuf { 15 knot_root() 16 .parent() 17 .expect("workspace root is the parent of the knot root") 18 .to_path_buf() 19} 20 21fn crate_src_files() -> impl Iterator<Item = PathBuf> { 22 WalkDir::new(knot_root().join("crates")) 23 .into_iter() 24 .filter_map(Result::ok) 25 .filter(|entry| entry.file_type().is_file()) 26 .map(|entry| entry.into_path()) 27 .filter(|path| path.extension().is_some_and(|ext| ext == "rs")) 28 .filter(|path| { 29 path.components() 30 .any(|component| component.as_os_str() == "src") 31 }) 32} 33 34#[test] 35fn no_subprocess_spawning_in_src() { 36 let offenders: Vec<String> = crate_src_files() 37 .filter(|path| { 38 std::fs::read_to_string(path) 39 .map(|text| text.contains("process::Command")) 40 .unwrap_or(false) 41 }) 42 .map(|path| path.display().to_string()) 43 .collect(); 44 assert!( 45 offenders.is_empty(), 46 "design pillar: no subprocesses for git or anything else, but these src files spawn one: {offenders:?}" 47 ); 48} 49 50fn lock_field<'a>(block: &'a str, key: &str) -> Option<&'a str> { 51 block.lines().find_map(|line| { 52 line.strip_prefix(key)? 53 .strip_prefix(" = \"")? 54 .strip_suffix('"') 55 }) 56} 57 58type PackageKey<'a> = (&'a str, &'a str); 59type LockGraph<'a> = BTreeMap<PackageKey<'a>, Vec<DepRef<'a>>>; 60 61fn lock_graph(lock: &str) -> LockGraph<'_> { 62 lock.split("[[package]]") 63 .skip(1) 64 .filter_map(|block| { 65 let name = lock_field(block, "name")?; 66 let version = lock_field(block, "version")?; 67 Some(((name, version), lock_dependencies(block))) 68 }) 69 .collect() 70} 71 72fn lock_dependencies(block: &str) -> Vec<DepRef<'_>> { 73 block 74 .split_once("dependencies = [") 75 .and_then(|(_, rest)| rest.split(']').next()) 76 .into_iter() 77 .flat_map(str::lines) 78 .filter_map(|line| line.trim().strip_prefix('"')) 79 .filter_map(|entry| entry.split('"').next()) 80 .map(|entry| { 81 entry 82 .split_once(' ') 83 .map_or(DepRef::Name(entry), |(name, rest)| { 84 DepRef::Exact(name, rest.split(' ').next().unwrap_or(rest)) 85 }) 86 }) 87 .collect() 88} 89 90#[derive(Debug, Clone, Copy)] 91enum DepRef<'a> { 92 Name(&'a str), 93 Exact(&'a str, &'a str), 94} 95 96impl<'a> DepRef<'a> { 97 fn name(&self) -> &'a str { 98 match self { 99 DepRef::Name(name) | DepRef::Exact(name, _) => name, 100 } 101 } 102} 103 104fn reachable_from<'a>( 105 graph: &LockGraph<'a>, 106 by_name: &BTreeMap<&'a str, Vec<PackageKey<'a>>>, 107 package: PackageKey<'a>, 108 seen: &mut BTreeSet<PackageKey<'a>>, 109) { 110 if seen.insert(package) { 111 graph.get(&package).into_iter().flatten().for_each(|dep| { 112 let (name, version) = match dep { 113 DepRef::Name(name) => (*name, None), 114 DepRef::Exact(name, version) => (*name, Some(*version)), 115 }; 116 by_name 117 .get(name) 118 .into_iter() 119 .flatten() 120 .filter(|(_, held)| version.is_none_or(|version| *held == version)) 121 .for_each(|key| reachable_from(graph, by_name, *key, seen)); 122 }); 123 } 124} 125 126#[test] 127fn no_durable_state_or_native_git_crates() { 128 let lock = std::fs::read_to_string(workspace_root().join("Cargo.lock")) 129 .expect("workspace Cargo.lock is readable"); 130 let graph = lock_graph(&lock); 131 let by_name: BTreeMap<&str, Vec<PackageKey<'_>>> = 132 graph.keys().fold(BTreeMap::new(), |mut names, key| { 133 names.entry(key.0).or_default().push(*key); 134 names 135 }); 136 let server = by_name 137 .get("knot-server") 138 .and_then(|keys| keys.first()) 139 .copied() 140 .expect("the lockfile parse must find knot-server"); 141 assert!( 142 graph.get(&server).is_some_and(|deps| !deps.is_empty()), 143 "the lockfile parse must find knot-server's dependency list" 144 ); 145 146 let mut reachable = BTreeSet::new(); 147 reachable_from(&graph, &by_name, server, &mut reachable); 148 assert!( 149 reachable.iter().any(|(name, _)| *name == "gix"), 150 "the reachability walk must reach the git engine, so an empty walk is a broken parse" 151 ); 152 153 let banned = [ 154 "rusqlite", 155 "sqlx", 156 "sled", 157 "fjall", 158 "redb", 159 "git2", 160 "libgit2-sys", 161 ]; 162 let present: Vec<&str> = banned 163 .into_iter() 164 .filter(|name| reachable.iter().any(|(held, _)| held == name)) 165 .collect(); 166 assert!( 167 present.is_empty(), 168 "design pillar: no durable state but git and all git work through gix, but the server's dependency graph includes: {present:?}" 169 ); 170} 171 172fn dependents_of<'a>(graph: &LockGraph<'a>, package: &str) -> Vec<&'a str> { 173 graph 174 .iter() 175 .filter(|((name, _), _)| *name != package) 176 .filter(|(_, deps)| deps.iter().any(|dep| dep.name() == package)) 177 .map(|((name, _), _)| *name) 178 .collect() 179} 180 181#[test] 182fn nothing_depends_on_the_offline_migration_tool() { 183 let lock = std::fs::read_to_string(workspace_root().join("Cargo.lock")) 184 .expect("workspace Cargo.lock is readable"); 185 let graph = lock_graph(&lock); 186 assert!( 187 !dependents_of(&graph, "knot-types").is_empty(), 188 "the shared newtypes have dependents, so an empty answer here is a broken parse" 189 ); 190 191 let dependents = dependents_of(&graph, "knot-migrate"); 192 assert!( 193 dependents.is_empty(), 194 "knot-migrate is an offline one-shot tool whose rusqlite dependency must never reach the server, but it is depended on by: {dependents:?}" 195 ); 196} 197 198#[test] 199fn the_shared_limit_defaults_match_the_config_defaults() { 200 use confique::{Config, Layer}; 201 use knot_xrpc::{Budgets, ByteLimits, ReadBudget}; 202 203 fn ms(budget: ReadBudget) -> u64 { 204 match budget { 205 ReadBudget::Within(within) => within.as_millis() as u64, 206 ReadBudget::Unbounded => u64::MAX, 207 } 208 } 209 210 let xrpc = <knot_config::XrpcConfig as Config>::Layer::default_values(); 211 let server = <knot_config::ServerConfig as Config>::Layer::default_values(); 212 let bytes = ByteLimits::default(); 213 let budgets = Budgets::default(); 214 let push_ms = budgets.languages_push.get().as_millis() as u64; 215 216 let configured = [ 217 ("body", xrpc.max_body_bytes), 218 ("patch", xrpc.max_patch_bytes), 219 ("patch_decompressed", xrpc.max_patch_decompressed_bytes), 220 ("response", xrpc.max_response_bytes), 221 ("archive", xrpc.max_archive_bytes), 222 ("fork_pack", xrpc.fork_max_pack_bytes), 223 ("pack", server.ssh_max_pack_bytes), 224 ("tree_last_commit", xrpc.tree_last_commit_budget_ms), 225 ("blob_last_commit", xrpc.blob_last_commit_budget_ms), 226 ("languages", xrpc.languages_budget_ms), 227 ("languages_push", xrpc.languages_push_budget_ms), 228 ] 229 .map(|(name, value)| (name, value.expect("every limit has a config default"))); 230 let shared = [ 231 ("body", bytes.body.get() as u64), 232 ("patch", bytes.patch.get() as u64), 233 ("patch_decompressed", bytes.patch_decompressed.get()), 234 ("response", bytes.response.get() as u64), 235 ("archive", bytes.archive.get()), 236 ("fork_pack", bytes.fork_pack.get()), 237 ("pack", bytes.pack.get() as u64), 238 ("tree_last_commit", ms(budgets.tree_last_commit.get())), 239 ("blob_last_commit", ms(budgets.blob_last_commit.get())), 240 ("languages", ms(budgets.languages.get())), 241 ("languages_push", push_ms), 242 ]; 243 assert_eq!( 244 configured, shared, 245 "the config defaults and the in-code defaults must match. update ByteLimits::default and Budgets::default alongside the config defaults" 246 ); 247}