git.lucas.co / cce-compositor
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 }