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 }