Skip to main content

windjammer_runtime/
doc_test.rs

1//! Doc test extraction and execution utilities
2//!
3//! Provides runtime support for extracting and running tests from documentation.
4
5use std::collections::HashMap;
6
7/// Represents a doc test extracted from documentation
8#[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/// Registry for doc tests
40#[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    /// Register a doc test
53    pub fn register(&mut self, module: &str, test: DocTest) {
54        self.tests.entry(module.to_string()).or_default().push(test);
55    }
56
57    /// Get all tests for a module
58    pub fn get_tests(&self, module: &str) -> Option<&Vec<DocTest>> {
59        self.tests.get(module)
60    }
61
62    /// Get all modules with doc tests
63    pub fn modules(&self) -> Vec<&String> {
64        self.tests.keys().collect()
65    }
66
67    /// Total number of doc tests
68    pub fn total_tests(&self) -> usize {
69        self.tests.values().map(|v| v.len()).sum()
70    }
71}
72
73/// Extract doc tests from a doc comment
74///
75/// Looks for ` ```test ` or ` ``` ` code blocks and extracts them as tests.
76///
77/// # Example
78/// ```
79/// use windjammer_runtime::doc_test::extract_doc_tests;
80///
81/// let doc = r#"
82/// /// This function adds two numbers.
83/// ///
84/// /// # Example
85/// /// ```
86/// /// let result = add(2, 3);
87/// /// assert_eq!(result, 5);
88/// /// ```
89/// "#;
90///
91/// let tests = extract_doc_tests("my_module", doc);
92/// assert_eq!(tests.len(), 1);
93/// ```
94pub 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                // End of code block
108                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                // Start of code block
130                in_code_block = true;
131                start_line = line_num;
132
133                // Check for attributes
134                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}