use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream}; use quote::{format_ident, quote, TokenStreamExt}; use std::env; use std::fs; use std::path::{Path, PathBuf}; struct ExpectedErrors(Vec); impl quote::ToTokens for ExpectedErrors { fn to_tokens(&self, tokens: &mut TokenStream) { let mut inner = TokenStream::new(); for err in self.0.iter() { inner.append(Literal::string(err)); inner.append(Punct::new(',', Spacing::Alone)); } tokens.append(Ident::new("vec", Span::call_site())); tokens.append(Punct::new('!', Spacing::Joint)); tokens.append(Group::new(Delimiter::Parenthesis, inner)) } } fn generate_test_for_file(path: PathBuf) -> String { let contents = fs::read_to_string(&path) .expect("Unable to read input") .replace("\r\n", "\n"); let display_path = path.display().to_string(); // Start iterating over lines and processing directives.... let mut disabled = quote! {}; let mut assertions = Vec::new(); let mut lines = contents.lines(); while let Some(line) = lines.next() { let line = match line.strip_prefix("//") { Some(line) => line, None => continue, }; let line = line.trim(); if let Some(line) = line.strip_prefix("@ignore") { let reason = line.trim(); assert_ne!( reason, "", "You need to provide at least some description for ignoring in {display_path}" ); disabled = quote! { #[ignore = #reason] }; } else if line == "@concrete:" { let mut concrete = String::new(); while let Some(line) = lines.next() { let line = match line.strip_prefix("// | ") { Some(line) => line, None => break, }; concrete.push_str(line); concrete.push_str("\n"); } assertions.push(quote! { crate::assert_concrete(source.clone(), #concrete, #display_path); }); } else if line == "@compiles-to:" { let mut compiled = String::new(); while let Some(line) = lines.next() { let line = match line.strip_prefix("// | ") { Some(line) => line, None => break, }; compiled.push_str(line); compiled.push_str("\n"); } assertions.push(quote! { crate::assert_compiles_to(_module.clone(), #compiled, #display_path); }); } else if let Some(line) = line.strip_prefix("@type:") { let (pos, expected) = line .trim() .split_once(' ') .expect("Mal-formed type expectation"); let pos: usize = pos .trim() .parse() .expect(&format!("Unable to parse position '{pos}'")); let expected = expected.trim(); assertions.push(quote! { crate::assert_type_at(_module.clone(), #pos, #expected, #display_path); }); } else if let Some(line) = line.strip_prefix("@type-error:") { let (pos, expected) = line .trim() .split_once(' ') .expect("Mal-formed type-error expectation"); let pos: usize = pos .trim() .parse() .expect(&format!("Unable to parse position '{pos}'")); let expected = expected.trim(); assertions.push(quote! { crate::assert_type_error_at(_module.clone(), &_errors, #pos, #expected, #display_path); }); } else if line == "@no-errors" { assertions.push(quote! { crate::assert_no_errors(_module.clone(), &_errors); }); } else if let Some(line) = line.strip_prefix("@eval:") { let expected = line.trim(); assertions.push(quote! { crate::assert_eval_ok(&program, _module.clone(), #expected); }); } else if let Some(line) = line.strip_prefix("@check-error:") { let expected = line.trim(); assertions.push(quote! { crate::assert_check_error(_module.clone(), &_errors, #expected); }); } else if line == "@expect-errors:" { let mut errors = Vec::new(); while let Some(line) = lines.next() { let line = match line.strip_prefix("// | ") { Some(line) => line, None => break, }; errors.push(line.to_string()); } let errors = ExpectedErrors(errors); assertions.push(quote! { crate::assert_errors(_module.clone(), &_errors, #errors); }); } else if line.starts_with("@") { panic!("Test file {display_path} has unknown directive: {line}"); } } let name = format_ident!("{}", path.file_stem().unwrap().to_string_lossy()); let test_method = quote! { #disabled fn #name() { let source : std::rc::Rc = #contents.into(); let mut program = crate::test_runtime(#display_path, source.clone()); let (_errors, _module) = program.load_module("__test__").unwrap(); #(#assertions)* } }; let syntax_tree = syn::parse2(test_method).unwrap(); prettyplease::unparse(&syntax_tree) } fn process_directory(output: &mut String, path: T) where T: AsRef, { let fine_ext: std::ffi::OsString = "fine".into(); let path = path.as_ref(); for entry in std::fs::read_dir(path).expect("Unable to read directory") { match entry { Ok(dirent) => { let file_type = dirent.file_type().unwrap(); if file_type.is_dir() { let file_name = dirent.file_name(); let file_name = file_name.to_string_lossy().to_owned(); output.push_str(&format!("mod {file_name} {{\n")); process_directory(output, dirent.path()); output.push_str("}\n\n"); } else if file_type.is_file() { if dirent.path().extension() == Some(&fine_ext) { output.push_str(&format!("// {}\n", dirent.path().display())); output.push_str("#[test]\n"); output.push_str(&generate_test_for_file(dirent.path())); output.push_str("\n\n"); } } else { eprintln!("Skipping symlink: {}", path.display()); } } Err(e) => eprintln!("Unable to read directory entry: {:?}", e), } } } fn main() { println!("cargo:rerun-if-changed=./tests"); let mut test_source = String::new(); process_directory(&mut test_source, "./tests"); let out_dir = env::var_os("OUT_DIR").unwrap(); let dest_path = Path::new(&out_dir).join("generated_tests.rs"); fs::write(dest_path, test_source).unwrap(); }