rustleaf_macros_internal/
lib.rs1use proc_macro::TokenStream;
2use quote::quote;
3use std::fs;
4use std::path::Path;
5use syn::{parse_macro_input, LitStr};
6
7#[derive(Debug, Clone)]
8enum TestType {
9 Normal,
10 Panic,
11 Ignore,
12}
13
14#[proc_macro_attribute]
16pub fn rustleaf_tests(args: TokenStream, _input: TokenStream) -> TokenStream {
17 let test_dir = parse_macro_input!(args as LitStr);
18 let test_dir_path = test_dir.value();
19
20 let test_files = match discover_rustleaf_files(&test_dir_path) {
22 Ok(files) => files,
23 Err(e) => panic!("Failed to read test directory '{test_dir_path}': {e}"),
24 };
25
26 let test_functions =
28 test_files
29 .iter()
30 .map(|(test_name, include_path, test_type, full_path)| {
31 let test_fn_name = syn::Ident::new(test_name, proc_macro2::Span::call_site());
32
33 let is_panic_test = matches!(test_type, TestType::Panic);
34 let test_body = quote! {
35 let md_content = include_str!(#include_path);
37
38 let extract_rustleaf_code_block = |md_content: &str| -> Option<String> {
39 let lines: Vec<&str> = md_content.lines().collect();
40 let mut in_rustleaf_block = false;
41 let mut code_lines = Vec::new();
42
43 for line in lines {
44 if line.trim() == "```rustleaf" {
45 in_rustleaf_block = true;
46 continue;
47 }
48 if line.trim() == "```" && in_rustleaf_block {
49 break;
50 }
51 if in_rustleaf_block {
52 code_lines.push(line);
53 }
54 }
55
56 if code_lines.is_empty() {
57 None
58 } else {
59 Some(code_lines.join("\n"))
60 }
61 };
62
63 let update_markdown_with_results = |_original_md: &str, source: &str, circle: &str,
64 output_section: &str, execution_output: &str, lex_output: &str,
65 parse_output: &str, eval_output: &str, assertion_count: u32| -> String {
66 let output_display = if output_section == "None" {
68 format!("# Output\n{}", output_section)
69 } else {
70 format!("# Output\n```\n{}\n```", output_section)
71 };
72
73 format!(
74 "# Program\nStatus: {}\nAssertions: {}\n\n```rustleaf\n{}\n```\n\n{}\n\n# Result\n```rust\n{}\n```\n\n# Lex\n```rust\n{}\n```\n\n# Parse\n```rust\n{}\n```\n\n# Eval\n```rust\n{}\n```",
75 circle, assertion_count, source, output_display, execution_output, lex_output, parse_output, eval_output
76 )
77 };
78
79 let source = extract_rustleaf_code_block(md_content)
80 .expect("Failed to find rustleaf code block in markdown file");
81
82 println!("DEBUG: Starting test for file: {}", #full_path);
83 println!("DEBUG: Extracted source code: {}", source);
84
85 rustleaf::core::start_print_capture();
87 rustleaf::core::start_assertion_count();
88
89 println!("DEBUG: Starting lexing phase");
91 let tokens_result = rustleaf::lexer::Lexer::tokenize(&source);
92 let lex_output = match &tokens_result {
93 Ok(tokens) => {
94 let formatted_tokens: Vec<String> = tokens.iter().enumerate()
95 .map(|(i, token)| format!("{}: {:?}", i, token))
96 .collect();
97 format!("Ok(\n [\n {}\n ],\n)", formatted_tokens.join(",\n "))
98 }
99 Err(e) => format!("Err({:#?})", e)
100 };
101 println!("DEBUG: Lexing completed");
102
103 println!("DEBUG: Starting parsing phase");
105 let parse_result = match &tokens_result {
106 Ok(_) => rustleaf::parser::Parser::parse_str(&source),
107 Err(e) => Err(anyhow::Error::msg(format!("Lex error: {}", e))),
108 };
109 let parse_output = format!("{:#?}", parse_result);
110 println!("DEBUG: Parsing completed");
111
112 println!("DEBUG: Starting compilation phase");
114 let eval_output = match &parse_result {
115 Ok(ast) => {
116 let eval_result = rustleaf::eval::Compiler::compile(ast.clone());
117 format!("{:#?}", eval_result)
118 }
119 Err(_) => "Skipped due to parse error".to_string(),
120 };
121 println!("DEBUG: Compilation completed");
122
123 println!("DEBUG: Starting evaluation phase");
125 let (output_section, execution_output, eval_success, final_result, assertion_count) = match parse_result {
126 Ok(ast) => {
127 println!("DEBUG: AST parsing successful, starting evaluation setup");
128 let test_file_dir = std::path::Path::new(#full_path).parent().map(|p| p.to_path_buf());
132 println!("DEBUG: Test file directory: {:?}", test_file_dir);
133
134 println!("DEBUG: About to call evaluate_with_dir - this is where the stack overflow likely occurs");
135 let result = rustleaf::eval::evaluate_with_dir(ast, test_file_dir);
136 println!("DEBUG: evaluate_with_dir returned successfully");
137 let captured_output = rustleaf::core::get_captured_prints();
139 let assertion_count = rustleaf::core::get_assertion_count();
140 let execution_output = format!("{:#?}", result).replace("\\n", "\n");
141 println!("DEBUG: Captured {} prints, {} assertions", captured_output.len(), assertion_count);
142
143 let output_section = if captured_output.is_empty() {
144 "None".to_string()
145 } else {
146 captured_output.join("\n")
147 };
148
149 let eval_success = result.is_ok();
150 (output_section, execution_output, eval_success, result, assertion_count)
151 }
152 Err(parse_error) => {
153 println!("DEBUG: Parse error occurred: {}", parse_error);
154 let captured_output = rustleaf::core::get_captured_prints();
156 let assertion_count = rustleaf::core::get_assertion_count();
157
158 let output_section = if captured_output.is_empty() {
159 "None".to_string()
160 } else {
161 captured_output.join("\n")
162 };
163
164 let error_msg = format!("Parse error: {}", parse_error);
165 let error_result = Err(anyhow::Error::msg(error_msg.clone()));
166 (output_section, "Skipped due to parse error".to_string(), false, error_result, assertion_count)
167 }
168 };
169 println!("DEBUG: Evaluation completed");
170
171 println!("DEBUG: Determining test result (eval_success: {}, assertion_count: {})", eval_success, assertion_count);
174 let circle = if #is_panic_test {
175 if eval_success { "🔴" } else { "🟢" }
176 } else {
177 if eval_success {
178 if assertion_count == 0 { "🟡" } else { "🟢" }
180 } else {
181 "🔴"
182 }
183 };
184 println!("DEBUG: Test result circle: {}", circle);
185
186 println!("DEBUG: Updating markdown file with results");
188 let updated_md_content = update_markdown_with_results(
189 md_content, &source, &circle, &output_section, &execution_output,
190 &lex_output, &parse_output, &eval_output, assertion_count
191 );
192 std::fs::write(#full_path, updated_md_content).unwrap();
193 println!("DEBUG: Markdown file updated successfully");
194
195 println!("DEBUG: Test completed, returning final result");
197 final_result.unwrap();
198 };
199
200 let test_body = test_body;
202
203 match test_type {
204 TestType::Ignore => quote! {
205 #[test]
206 #[ignore]
207 fn #test_fn_name() {
208 #test_body
209 }
210 },
211 TestType::Panic => quote! {
212 #[test]
213 #[should_panic]
214 fn #test_fn_name() {
215 #test_body
216 }
217 },
218 _ => quote! {
219 #[test]
220 fn #test_fn_name() {
221 #test_body
222 }
223 },
224 }
225 });
226
227 let expanded = quote! {
228 #(#test_functions)*
229 };
230
231 TokenStream::from(expanded)
232}
233
234fn discover_rustleaf_files(
235 test_dir: &str,
236) -> Result<Vec<(String, String, TestType, String)>, std::io::Error> {
237 let mut test_files = Vec::new();
238 let test_path = Path::new(test_dir);
239
240 for entry in fs::read_dir(test_path)? {
241 let entry = entry?;
242 let path = entry.path();
243
244 if path.is_file() {
245 if let Some(extension) = path.extension() {
246 if extension == "md" {
247 let file_path = path.to_string_lossy().to_string();
248
249 let test_name = generate_test_name(&file_path);
251
252 let filename = path.file_name().unwrap().to_string_lossy();
254 let test_type = if filename.ends_with("_panic.md") {
255 TestType::Panic
256 } else if filename.ends_with("_ignore.md") {
257 TestType::Ignore
258 } else {
259 TestType::Normal
260 };
261
262 let test_dir_name = Path::new(test_dir)
265 .file_name()
266 .unwrap_or_else(|| std::ffi::OsStr::new(""))
267 .to_string_lossy();
268 let include_path = format!("{test_dir_name}/{filename}");
269
270 test_files.push((test_name, include_path, test_type, file_path));
271 }
272 }
273 }
274 }
275
276 test_files.sort_by(|a, b| a.0.cmp(&b.0)); Ok(test_files)
278}
279
280fn generate_test_name(file_path: &str) -> String {
281 let relative_path = if let Some(stripped) = file_path.strip_prefix("./tests/") {
283 stripped } else if let Some(stripped) = file_path.strip_prefix("tests/") {
285 stripped } else {
287 file_path
288 };
289
290 let without_extension = if let Some(stem) = Path::new(relative_path).file_stem() {
292 let parent = Path::new(relative_path).parent().unwrap_or(Path::new(""));
293 if parent.as_os_str().is_empty() {
294 stem.to_string_lossy().to_string()
295 } else {
296 format!("{}/{}", parent.to_string_lossy(), stem.to_string_lossy())
297 }
298 } else {
299 relative_path.to_string()
300 };
301
302 without_extension.replace(['/', '\\'], "_")
304}