git.lucas.co / cce-files
file manager
git clone https://git.lucas.co/cce-files.git

tests/doc_claims.rs (9.9K)

  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 claims about this crate's OWN tests, checked against the tree.
 13 ///
 14 /// A test count is the most inviting kind of stale fact: it is concrete, it
 15 /// reads as verified, and it is wrong the moment anyone adds a test. The
 16 /// 2026-09-19 sweep found cce-window-manager documented at "~143 unit tests"
 17 /// with 175, and its list of test-free modules naming `state.rs`, which had
 18 /// grown tests — the list was right when written and had quietly inverted.
 19 ///
 20 /// Four claim shapes are understood, and each is checked only if the doc
 21 /// actually makes it:
 22 ///
 23 /// - `<N> modules carry unit tests` / `<N> modules have them` — how many
 24 ///   files under src/ contain a `#[cfg(test)]` block.
 25 /// - `<N> tests` / `~<N> unit tests` — total `#[test]` count. A `~` figure
 26 ///   gets a 10% tolerance; a bare one must be exact, because that is what
 27 ///   writing a bare number claims.
 28 /// - ``  `x.rs` (the most  `` / `` `x.rs` has the most `` — that file has the
 29 ///   most `#[test]`s of any.
 30 /// - `every module has one except `a.rs` … and `b.rs`` — exactly those lack
 31 ///   tests. Where the doc instead LISTS the modules that have them, the list
 32 ///   must match the real set exactly.
 33 ///
 34 /// Deliberately MIRRORED per crate rather than shared: each is its own git
 35 /// repository and must build standalone, and this needs nothing but std.
 36 pub mod test_claims {
 37     use std::path::{Path, PathBuf};
 38 
 39     /// Every `.rs` under src/ that carries a `#[cfg(test)]` block, with how
 40     /// many `#[test]` functions it holds.
 41     fn test_modules(root: &Path) -> Vec<(PathBuf, usize)> {
 42         fn walk(dir: &Path, out: &mut Vec<(PathBuf, usize)>) {
 43             let Ok(entries) = std::fs::read_dir(dir) else { return };
 44             for e in entries.flatten() {
 45                 let p = e.path();
 46                 if p.is_dir() {
 47                     walk(&p, out);
 48                 } else if p.extension().and_then(|x| x.to_str()) == Some("rs") {
 49                     if let Ok(s) = std::fs::read_to_string(&p) {
 50                         if s.contains("#[cfg(test)]") {
 51                             out.push((p, s.matches("#[test]").count()));
 52                         }
 53                     }
 54                 }
 55             }
 56         }
 57         let mut v = Vec::new();
 58         walk(&root.join("src"), &mut v);
 59         v.sort();
 60         v
 61     }
 62 
 63     /// Every `.rs` under src/, tests or not — the denominator for "every
 64     /// module has one except …".
 65     fn all_sources(root: &Path) -> Vec<PathBuf> {
 66         fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
 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                     out.push(p);
 74                 }
 75             }
 76         }
 77         let mut v = Vec::new();
 78         walk(&root.join("src"), &mut v);
 79         v.sort();
 80         v
 81     }
 82 
 83     fn word_to_num(w: &str) -> Option<usize> {
 84         const WORDS: [&str; 21] = [
 85             "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
 86             "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen",
 87             "seventeen", "eighteen", "nineteen", "twenty",
 88         ];
 89         // Docs wrap these in punctuation — "(nine", "(~175", "48 tests:" —
 90         // and a token that fails to parse makes the claim silently unchecked,
 91         // which is the failure this whole file exists to prevent.
 92         let w = w.trim_matches(|c: char| !c.is_ascii_alphanumeric());
 93         if let Ok(n) = w.parse::<usize>() {
 94             return Some(n);
 95         }
 96         let lower = w.to_ascii_lowercase();
 97         WORDS.iter().position(|x| *x == lower)
 98     }
 99 
100     /// The token right before `phrase`, and whether it was written with `~`.
101     fn number_before(doc: &str, phrase: &str) -> Option<(usize, bool)> {
102         let at = doc.find(phrase)?;
103         let word = doc[..at].split_whitespace().next_back()?;
104         Some((word_to_num(word)?, word.contains('~')))
105     }
106 
107     /// Backticked `*.rs` names from `phrase` up to the end of its sentence.
108     fn names_after(doc: &str, phrase: &str) -> Vec<String> {
109         let Some(at) = doc.find(phrase) else { return Vec::new() };
110         let seg = &doc[at..];
111         let end = seg.find(". ").unwrap_or(seg.len());
112         seg[..end]
113             .split('`')
114             .skip(1)
115             .step_by(2)
116             .filter(|t| t.ends_with(".rs"))
117             .map(|t| t.rsplit('/').next().unwrap_or(t).to_string())
118             .collect()
119     }
120 
121     fn base(p: &Path) -> String {
122         p.file_name().and_then(|x| x.to_str()).unwrap_or_default().to_string()
123     }
124 
125     #[test]
126     fn test_claude_md_test_claims_match_the_source() {
127         let root = Path::new(env!("CARGO_MANIFEST_DIR"));
128         let doc = std::fs::read_to_string(root.join("CLAUDE.md"))
129             .expect("CLAUDE.md is missing next to Cargo.toml");
130         let mods = test_modules(root);
131         let total: usize = mods.iter().map(|(_, n)| n).sum();
132         let mut bad: Vec<String> = Vec::new();
133         let mut checked = 0usize;
134 
135         // --- how many modules carry tests
136         for phrase in ["modules carry unit tests", "modules have them", "modules have unit tests"] {
137             if let Some((claimed, _)) = number_before(&doc, phrase) {
138                 checked += 1;
139                 if claimed != mods.len() {
140                     bad.push(format!(
141                         "\"{claimed} {phrase}\" — {} modules actually do: {}",
142                         mods.len(),
143                         mods.iter().map(|(p, _)| base(p)).collect::<Vec<_>>().join(", ")
144                     ));
145                 }
146                 // Where the doc ENUMERATES them, the list must be the real
147                 // set. Two or more names means a list; a single one is the
148                 // "`browse.rs` has the most" shape, checked separately.
149                 let listed = names_after(&doc, phrase);
150                 if listed.len() >= 2 {
151                     let mut want: Vec<String> = mods.iter().map(|(p, _)| base(p)).collect();
152                     let mut got = listed.clone();
153                     want.sort();
154                     got.sort();
155                     got.dedup();
156                     if want != got {
157                         bad.push(format!(
158                             "the listed modules {got:?} are not the ones that have tests {want:?}"
159                         ));
160                     }
161                 }
162             }
163         }
164 
165         // --- total test count
166         for phrase in ["unit tests", "tests"] {
167             if let Some((claimed, approx)) = number_before(&doc, phrase) {
168                 checked += 1;
169                 let off = (total as f64 - claimed as f64) / claimed.max(1) as f64;
170                 let stale = if approx { off.abs() > 0.10 } else { total != claimed };
171                 if stale {
172                     bad.push(format!(
173                         "\"{}{claimed} {phrase}\" — the crate has {total}",
174                         if approx { "~" } else { "" }
175                     ));
176                 }
177                 break; // "unit tests" wins; don't double-count its "tests" tail
178             }
179         }
180 
181         // --- which file has the most
182         for (name, phrase) in [("(the most", "(the most"), ("has the most", "has the most")] {
183             let _ = name;
184             if let Some(at) = doc.find(phrase) {
185                 let before = &doc[..at];
186                 let claimed = before.rfind('`').and_then(|e| {
187                     before[..e].rfind('`').map(|s| before[s + 1..e].to_string())
188                 });
189                 if let Some(claimed) = claimed.filter(|c| c.ends_with(".rs")) {
190                     checked += 1;
191                     let claimed = claimed.rsplit('/').next().unwrap_or(&claimed).to_string();
192                     if let Some((top, n)) = mods.iter().max_by_key(|(_, n)| *n) {
193                         if base(top) != claimed {
194                             bad.push(format!(
195                                 "`{claimed}` is called the one with the most tests, but {} has {n}",
196                                 base(top)
197                             ));
198                         }
199                     }
200                 }
201             }
202         }
203 
204         // --- "every module has one except a.rs and b.rs"
205         if doc.contains("every module has one except") {
206             checked += 1;
207             let claimed = names_after(&doc, "every module has one except");
208             let with: Vec<String> = mods.iter().map(|(p, _)| base(p)).collect();
209             let mut without: Vec<String> = all_sources(root)
210                 .iter()
211                 .map(|p| base(p))
212                 .filter(|b| !with.contains(b))
213                 .collect();
214             let mut claimed = claimed;
215             without.sort();
216             without.dedup();
217             claimed.sort();
218             claimed.dedup();
219             if claimed != without {
220                 bad.push(format!(
221                     "the modules without tests are {without:?}, not {claimed:?}"
222                 ));
223             }
224         }
225 
226         assert!(
227             checked > 0,
228             "CLAUDE.md makes no test-count claim this scan recognizes — either the \
229              wording changed and the scan needs updating, or the claims were removed \
230              and so should this test"
231         );
232         assert!(bad.is_empty(), "CLAUDE.md test claims are stale:\n  {}", bad.join("\n  "));
233     }
234 }