This repository has no description
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}