Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
tests/doc_claims.rs (15.7K)
1 //! CLAUDE.md, checked against the crate it describes.
2 //!
3 //! An integration test on purpose: anything under `src/` would be counted by
4 //! its own scans — a `#[cfg(test)]` block added here to measure which modules
5 //! carry tests promptly made this module one of them, and the line-count
6 //! figures would drift by however long this file happens to be.
7 //!
8 //! Deliberately MIRRORED per crate rather than shared from a helper: each is
9 //! its own git repository that must build standalone, and this needs nothing
10 //! but std.
11
12 /// CLAUDE.md's `(~Nk lines)` figures, checked against the files they describe.
13 ///
14 /// Those numbers exist to set expectations before opening a file — "this is
15 /// the big one" — and they drift in total silence, because a stale number
16 /// reads exactly like a fresh one. A workspace sweep on 2026-09-19 found
17 /// EVERY size figure in every CLAUDE.md stale, all of them undercounts, the
18 /// worst by 49% (cce-designer's app.rs, written ~5.4k at 8046 lines).
19 ///
20 /// Tolerance is 10%: loose enough that ordinary work does not trip it, tight
21 /// enough that a file cannot quietly double. When it fails, write the number
22 /// it reports — that is the whole fix.
23 ///
24 /// Deliberately MIRRORED into each crate that carries such a figure rather
25 /// than shared from a helper: every crate here is its own git repository and
26 /// must build standalone, and this needs nothing but `std`. Same call
27 /// `ramp.rs` makes about its cce-ui parser.
28 pub mod size_claims {
29 use std::path::{Path, PathBuf};
30
31 /// One `(~Nk lines)` claim: the name as CLAUDE.md spells it, the figure,
32 /// and whether the claim also calls it the largest file.
33 fn claims(doc: &str) -> Vec<(String, f64, bool)> {
34 const TAIL: &str = " lines)";
35 let mut out = Vec::new();
36 let mut i = 0;
37 while let Some(p) = doc[i..].find(TAIL) {
38 let end = i + p;
39 i = end + TAIL.len();
40 let Some(open) = doc[..end].rfind('(') else { continue };
41 let inner = &doc[open + 1..end];
42 // "~8k", or "largest file, ~6.9k" — take the last word.
43 let largest = inner.contains("largest file");
44 let word = inner.rsplit([' ', ',']).next().unwrap_or("").trim();
45 let digits = word.trim_start_matches('~');
46 let value = match digits.strip_suffix('k') {
47 Some(k) => k.parse::<f64>().ok().map(|v| v * 1000.0),
48 None => digits.parse::<f64>().ok(),
49 };
50 // The backticked name immediately before the parenthetical.
51 let before = &doc[..open];
52 let name = before.rfind('`').and_then(|e| {
53 before[..e].rfind('`').map(|s| before[s + 1..e].to_string())
54 });
55 if let (Some(v), Some(n)) = (value, name) {
56 if v > 0.0 {
57 out.push((n, v, largest));
58 }
59 }
60 }
61 out
62 }
63
64 /// Every `.rs` file under `src/`, as (path, line count).
65 fn sources(root: &Path) -> Vec<(PathBuf, usize)> {
66 fn walk(dir: &Path, out: &mut Vec<(PathBuf, usize)>) {
67 let Ok(entries) = std::fs::read_dir(dir) else { return };
68 for e in entries.flatten() {
69 let p = e.path();
70 if p.is_dir() {
71 walk(&p, out);
72 } else if p.extension().and_then(|x| x.to_str()) == Some("rs") {
73 if let Ok(s) = std::fs::read_to_string(&p) {
74 out.push((p, s.lines().count()));
75 }
76 }
77 }
78 }
79 let mut v = Vec::new();
80 walk(&root.join("src"), &mut v);
81 v
82 }
83
84 #[test]
85 fn test_claude_md_line_counts_match_the_source() {
86 let root = Path::new(env!("CARGO_MANIFEST_DIR"));
87 let doc = std::fs::read_to_string(root.join("CLAUDE.md"))
88 .expect("CLAUDE.md is missing next to Cargo.toml");
89 let files = sources(root);
90 let claims = claims(&doc);
91 assert!(
92 !claims.is_empty(),
93 "no `(~N lines)` figure found in CLAUDE.md — either the syntax changed \
94 and this scan needs updating, or the figures were removed and so \
95 should this test"
96 );
97
98 let mut bad: Vec<String> = Vec::new();
99 for (name, claimed, largest) in &claims {
100 // A path relative to the crate root, else a unique basename.
101 let hits: Vec<&(PathBuf, usize)> = if root.join(name).is_file() {
102 files.iter().filter(|(p, _)| *p == root.join(name)).collect()
103 } else {
104 files
105 .iter()
106 .filter(|(p, _)| p.file_name().and_then(|x| x.to_str()) == Some(name.as_str()))
107 .collect()
108 };
109 let [(path, actual)] = hits[..] else {
110 bad.push(format!("`{name}`: names {} files under src/, cannot check", hits.len()));
111 continue;
112 };
113 let actual = *actual as f64;
114 let drift = (actual - claimed) / claimed;
115 if drift.abs() > 0.10 {
116 bad.push(format!(
117 "`{name}` is documented as ~{} lines but has {} ({:+.0}%) — write ~{}",
118 round_k(*claimed),
119 actual as usize,
120 drift * 100.0,
121 round_k(actual)
122 ));
123 }
124 if *largest {
125 if let Some((big, n)) = files.iter().max_by_key(|(_, n)| *n) {
126 if big != path {
127 bad.push(format!(
128 "`{name}` is called the largest file, but {} has {n} lines",
129 big.strip_prefix(root).unwrap_or(big).display()
130 ));
131 }
132 }
133 }
134 }
135 assert!(bad.is_empty(), "CLAUDE.md size claims are stale:\n {}", bad.join("\n "));
136 }
137
138 /// "~8k" for 8046, "~3.1k" for 3134, "~950" for 950 — the spelling the
139 /// docs already use, so the failure message can be pasted straight in.
140 fn round_k(n: f64) -> String {
141 if n < 1000.0 {
142 return format!("{}", n.round() as usize);
143 }
144 let k = n / 1000.0;
145 if (k - k.round()).abs() < 0.05 {
146 format!("{}k", k.round() as usize)
147 } else {
148 format!("{k:.1}k")
149 }
150 }
151 }
152
153 /// CLAUDE.md's claims about this crate's OWN tests, checked against the tree.
154 ///
155 /// A test count is the most inviting kind of stale fact: it is concrete, it
156 /// reads as verified, and it is wrong the moment anyone adds a test. The
157 /// 2026-09-19 sweep found cce-window-manager documented at "~143 unit tests"
158 /// with 175, and its list of test-free modules naming `state.rs`, which had
159 /// grown tests — the list was right when written and had quietly inverted.
160 ///
161 /// Four claim shapes are understood, and each is checked only if the doc
162 /// actually makes it:
163 ///
164 /// - `<N> modules carry unit tests` / `<N> modules have them` — how many
165 /// files under src/ contain a `#[cfg(test)]` block.
166 /// - `<N> tests` / `~<N> unit tests` — total `#[test]` count. A `~` figure
167 /// gets a 10% tolerance; a bare one must be exact, because that is what
168 /// writing a bare number claims.
169 /// - `` `x.rs` (the most `` / `` `x.rs` has the most `` — that file has the
170 /// most `#[test]`s of any.
171 /// - `every module has one except `a.rs` … and `b.rs`` — exactly those lack
172 /// tests. Where the doc instead LISTS the modules that have them, the list
173 /// must match the real set exactly.
174 ///
175 /// Deliberately MIRRORED per crate rather than shared: each is its own git
176 /// repository and must build standalone, and this needs nothing but std.
177 pub mod test_claims {
178 use std::path::{Path, PathBuf};
179
180 /// Every `.rs` under src/ that carries a `#[cfg(test)]` block, with how
181 /// many `#[test]` functions it holds.
182 fn test_modules(root: &Path) -> Vec<(PathBuf, usize)> {
183 fn walk(dir: &Path, out: &mut Vec<(PathBuf, usize)>) {
184 let Ok(entries) = std::fs::read_dir(dir) else { return };
185 for e in entries.flatten() {
186 let p = e.path();
187 if p.is_dir() {
188 walk(&p, out);
189 } else if p.extension().and_then(|x| x.to_str()) == Some("rs") {
190 if let Ok(s) = std::fs::read_to_string(&p) {
191 if s.contains("#[cfg(test)]") {
192 out.push((p, s.matches("#[test]").count()));
193 }
194 }
195 }
196 }
197 }
198 let mut v = Vec::new();
199 walk(&root.join("src"), &mut v);
200 v.sort();
201 v
202 }
203
204 /// Every `.rs` under src/, tests or not — the denominator for "every
205 /// module has one except …".
206 fn all_sources(root: &Path) -> Vec<PathBuf> {
207 fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
208 let Ok(entries) = std::fs::read_dir(dir) else { return };
209 for e in entries.flatten() {
210 let p = e.path();
211 if p.is_dir() {
212 walk(&p, out);
213 } else if p.extension().and_then(|x| x.to_str()) == Some("rs") {
214 out.push(p);
215 }
216 }
217 }
218 let mut v = Vec::new();
219 walk(&root.join("src"), &mut v);
220 v.sort();
221 v
222 }
223
224 fn word_to_num(w: &str) -> Option<usize> {
225 const WORDS: [&str; 21] = [
226 "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
227 "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen",
228 "seventeen", "eighteen", "nineteen", "twenty",
229 ];
230 // Docs wrap these in punctuation — "(nine", "(~175", "48 tests:" —
231 // and a token that fails to parse makes the claim silently unchecked,
232 // which is the failure this whole file exists to prevent.
233 let w = w.trim_matches(|c: char| !c.is_ascii_alphanumeric());
234 if let Ok(n) = w.parse::<usize>() {
235 return Some(n);
236 }
237 let lower = w.to_ascii_lowercase();
238 WORDS.iter().position(|x| *x == lower)
239 }
240
241 /// The token right before `phrase`, and whether it was written with `~`.
242 fn number_before(doc: &str, phrase: &str) -> Option<(usize, bool)> {
243 let at = doc.find(phrase)?;
244 let word = doc[..at].split_whitespace().next_back()?;
245 Some((word_to_num(word)?, word.contains('~')))
246 }
247
248 /// Backticked `*.rs` names from `phrase` up to the end of its sentence.
249 fn names_after(doc: &str, phrase: &str) -> Vec<String> {
250 let Some(at) = doc.find(phrase) else { return Vec::new() };
251 let seg = &doc[at..];
252 let end = seg.find(". ").unwrap_or(seg.len());
253 seg[..end]
254 .split('`')
255 .skip(1)
256 .step_by(2)
257 .filter(|t| t.ends_with(".rs"))
258 .map(|t| t.rsplit('/').next().unwrap_or(t).to_string())
259 .collect()
260 }
261
262 fn base(p: &Path) -> String {
263 p.file_name().and_then(|x| x.to_str()).unwrap_or_default().to_string()
264 }
265
266 #[test]
267 fn test_claude_md_test_claims_match_the_source() {
268 let root = Path::new(env!("CARGO_MANIFEST_DIR"));
269 let doc = std::fs::read_to_string(root.join("CLAUDE.md"))
270 .expect("CLAUDE.md is missing next to Cargo.toml");
271 let mods = test_modules(root);
272 let total: usize = mods.iter().map(|(_, n)| n).sum();
273 let mut bad: Vec<String> = Vec::new();
274 let mut checked = 0usize;
275
276 // --- how many modules carry tests
277 for phrase in ["modules carry unit tests", "modules have them", "modules have unit tests"] {
278 if let Some((claimed, _)) = number_before(&doc, phrase) {
279 checked += 1;
280 if claimed != mods.len() {
281 bad.push(format!(
282 "\"{claimed} {phrase}\" — {} modules actually do: {}",
283 mods.len(),
284 mods.iter().map(|(p, _)| base(p)).collect::<Vec<_>>().join(", ")
285 ));
286 }
287 // Where the doc ENUMERATES them, the list must be the real
288 // set. Two or more names means a list; a single one is the
289 // "`browse.rs` has the most" shape, checked separately.
290 let listed = names_after(&doc, phrase);
291 if listed.len() >= 2 {
292 let mut want: Vec<String> = mods.iter().map(|(p, _)| base(p)).collect();
293 let mut got = listed.clone();
294 want.sort();
295 got.sort();
296 got.dedup();
297 if want != got {
298 bad.push(format!(
299 "the listed modules {got:?} are not the ones that have tests {want:?}"
300 ));
301 }
302 }
303 }
304 }
305
306 // --- total test count
307 for phrase in ["unit tests", "tests"] {
308 if let Some((claimed, approx)) = number_before(&doc, phrase) {
309 checked += 1;
310 let off = (total as f64 - claimed as f64) / claimed.max(1) as f64;
311 let stale = if approx { off.abs() > 0.10 } else { total != claimed };
312 if stale {
313 bad.push(format!(
314 "\"{}{claimed} {phrase}\" — the crate has {total}",
315 if approx { "~" } else { "" }
316 ));
317 }
318 break; // "unit tests" wins; don't double-count its "tests" tail
319 }
320 }
321
322 // --- which file has the most
323 for (name, phrase) in [("(the most", "(the most"), ("has the most", "has the most")] {
324 let _ = name;
325 if let Some(at) = doc.find(phrase) {
326 let before = &doc[..at];
327 let claimed = before.rfind('`').and_then(|e| {
328 before[..e].rfind('`').map(|s| before[s + 1..e].to_string())
329 });
330 if let Some(claimed) = claimed.filter(|c| c.ends_with(".rs")) {
331 checked += 1;
332 let claimed = claimed.rsplit('/').next().unwrap_or(&claimed).to_string();
333 if let Some((top, n)) = mods.iter().max_by_key(|(_, n)| *n) {
334 if base(top) != claimed {
335 bad.push(format!(
336 "`{claimed}` is called the one with the most tests, but {} has {n}",
337 base(top)
338 ));
339 }
340 }
341 }
342 }
343 }
344
345 // --- "every module has one except a.rs and b.rs"
346 if doc.contains("every module has one except") {
347 checked += 1;
348 let claimed = names_after(&doc, "every module has one except");
349 let with: Vec<String> = mods.iter().map(|(p, _)| base(p)).collect();
350 let mut without: Vec<String> = all_sources(root)
351 .iter()
352 .map(|p| base(p))
353 .filter(|b| !with.contains(b))
354 .collect();
355 let mut claimed = claimed;
356 without.sort();
357 without.dedup();
358 claimed.sort();
359 claimed.dedup();
360 if claimed != without {
361 bad.push(format!(
362 "the modules without tests are {without:?}, not {claimed:?}"
363 ));
364 }
365 }
366
367 assert!(
368 checked > 0,
369 "CLAUDE.md makes no test-count claim this scan recognizes — either the \
370 wording changed and the scan needs updating, or the claims were removed \
371 and so should this test"
372 );
373 assert!(bad.is_empty(), "CLAUDE.md test claims are stale:\n {}", bad.join("\n "));
374 }
375 }