windjammer_runtime/
doc_test.rs1use std::collections::HashMap;
6
7#[derive(Debug, Clone)]
9pub struct DocTest {
10 pub name: String,
11 pub code: String,
12 pub line: usize,
13 pub should_panic: bool,
14 pub ignore: bool,
15}
16
17impl DocTest {
18 pub fn new(name: String, code: String, line: usize) -> Self {
19 Self {
20 name,
21 code,
22 line,
23 should_panic: false,
24 ignore: false,
25 }
26 }
27
28 pub fn with_should_panic(mut self) -> Self {
29 self.should_panic = true;
30 self
31 }
32
33 pub fn with_ignore(mut self) -> Self {
34 self.ignore = true;
35 self
36 }
37}
38
39#[derive(Debug, Default)]
41pub struct DocTestRegistry {
42 tests: HashMap<String, Vec<DocTest>>,
43}
44
45impl DocTestRegistry {
46 pub fn new() -> Self {
47 Self {
48 tests: HashMap::new(),
49 }
50 }
51
52 pub fn register(&mut self, module: &str, test: DocTest) {
54 self.tests.entry(module.to_string()).or_default().push(test);
55 }
56
57 pub fn get_tests(&self, module: &str) -> Option<&Vec<DocTest>> {
59 self.tests.get(module)
60 }
61
62 pub fn modules(&self) -> Vec<&String> {
64 self.tests.keys().collect()
65 }
66
67 pub fn total_tests(&self) -> usize {
69 self.tests.values().map(|v| v.len()).sum()
70 }
71}
72
73pub fn extract_doc_tests(module: &str, doc_comment: &str) -> Vec<DocTest> {
95 let mut tests = Vec::new();
96 let mut in_code_block = false;
97 let mut current_code = String::new();
98 let mut start_line = 0;
99 let mut should_panic = false;
100 let mut ignore = false;
101
102 for (line_num, line) in doc_comment.lines().enumerate() {
103 let trimmed = line.trim_start_matches("///").trim();
104
105 if trimmed.starts_with("```") {
106 if in_code_block {
107 if !current_code.is_empty() {
109 let mut test = DocTest::new(
110 format!("{}_doctest_{}", module, tests.len()),
111 current_code.clone(),
112 start_line,
113 );
114
115 if should_panic {
116 test = test.with_should_panic();
117 }
118 if ignore {
119 test = test.with_ignore();
120 }
121
122 tests.push(test);
123 }
124 in_code_block = false;
125 current_code.clear();
126 should_panic = false;
127 ignore = false;
128 } else {
129 in_code_block = true;
131 start_line = line_num;
132
133 let block_type = trimmed.trim_start_matches("```");
135 should_panic = block_type.contains("should_panic");
136 ignore = block_type.contains("ignore");
137 }
138 } else if in_code_block {
139 current_code.push_str(trimmed);
140 current_code.push('\n');
141 }
142 }
143
144 tests
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn test_doc_test_creation() {
153 let test = DocTest::new(
154 "test_add".to_string(),
155 "assert_eq!(1 + 1, 2);".to_string(),
156 10,
157 );
158
159 assert_eq!(test.name, "test_add");
160 assert_eq!(test.code, "assert_eq!(1 + 1, 2);");
161 assert_eq!(test.line, 10);
162 assert!(!test.should_panic);
163 assert!(!test.ignore);
164 }
165
166 #[test]
167 fn test_doc_test_with_attributes() {
168 let test = DocTest::new("test".to_string(), "code".to_string(), 0)
169 .with_should_panic()
170 .with_ignore();
171
172 assert!(test.should_panic);
173 assert!(test.ignore);
174 }
175
176 #[test]
177 fn test_registry() {
178 let mut registry = DocTestRegistry::new();
179
180 let test1 = DocTest::new("test1".to_string(), "code1".to_string(), 0);
181 let test2 = DocTest::new("test2".to_string(), "code2".to_string(), 5);
182
183 registry.register("module_a", test1);
184 registry.register("module_a", test2.clone());
185 registry.register("module_b", test2);
186
187 assert_eq!(registry.total_tests(), 3);
188 assert_eq!(registry.modules().len(), 2);
189 assert_eq!(registry.get_tests("module_a").unwrap().len(), 2);
190 assert_eq!(registry.get_tests("module_b").unwrap().len(), 1);
191 }
192
193 #[test]
194 fn test_extract_simple_doc_test() {
195 let doc = r#"
196/// This function adds two numbers.
197///
198/// # Example
199/// ```
200/// let result = add(2, 3);
201/// assert_eq!(result, 5);
202/// ```
203"#;
204
205 let tests = extract_doc_tests("test_module", doc);
206 assert_eq!(tests.len(), 1);
207 assert!(tests[0].code.contains("let result = add(2, 3);"));
208 assert!(tests[0].code.contains("assert_eq!(result, 5);"));
209 }
210
211 #[test]
212 fn test_extract_multiple_doc_tests() {
213 let doc = r#"
214/// Function with multiple examples
215///
216/// # Example 1
217/// ```
218/// assert_eq!(1 + 1, 2);
219/// ```
220///
221/// # Example 2
222/// ```
223/// assert_eq!(2 + 2, 4);
224/// ```
225"#;
226
227 let tests = extract_doc_tests("test_module", doc);
228 assert_eq!(tests.len(), 2);
229 }
230
231 #[test]
232 fn test_extract_should_panic() {
233 let doc = r#"
234/// This should panic
235///
236/// ```should_panic
237/// panic!("expected");
238/// ```
239"#;
240
241 let tests = extract_doc_tests("test_module", doc);
242 assert_eq!(tests.len(), 1);
243 assert!(tests[0].should_panic);
244 }
245
246 #[test]
247 fn test_extract_ignore() {
248 let doc = r#"
249/// This is ignored
250///
251/// ```ignore
252/// expensive_test();
253/// ```
254"#;
255
256 let tests = extract_doc_tests("test_module", doc);
257 assert_eq!(tests.len(), 1);
258 assert!(tests[0].ignore);
259 }
260
261 #[test]
262 fn test_extract_no_tests() {
263 let doc = r#"
264/// Just documentation, no code blocks
265"#;
266
267 let tests = extract_doc_tests("test_module", doc);
268 assert_eq!(tests.len(), 0);
269 }
270}