diff --git a/Cargo.lock b/Cargo.lock index 819984b..618145a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "cst-test-generator" +version = "0.1.0" +dependencies = [ + "convert_case", + "prettyplease", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "deimos" version = "0.1.0" @@ -176,6 +187,7 @@ dependencies = [ "ast-generator", "clap", "codespan-reporting", + "cst-test-generator", "indoc", "log", "pest", diff --git a/Cargo.toml b/Cargo.toml index 0b277e7..fa9786a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,8 @@ indoc = "2.0.6" [build-dependencies] ast-generator = { path = "ast-generator" } +cst-test-generator = { path = "cst-test-generator" } [workspace] resolver = "3" -members = ["ast-generator"] +members = ["ast-generator", "cst-test-generator"] diff --git a/build.rs b/build.rs index d321772..bea202c 100644 --- a/build.rs +++ b/build.rs @@ -1,9 +1,18 @@ +use cst_test_generator::generate_test_files; +use std::env; +use std::fs; +use std::path::Path; + fn main() -> std::io::Result<()> { println!("cargo:rerun-if-changed=src/parser/deimos.pest"); - // let out_dir = env::var("OUT_DIR").unwrap(); - // let out_dir_path = Path::new(&out_dir); - // let testing_txt_path = out_dir_path.join("testing.rs"); - // let output = test_dump(); - // write(&testing_txt_path, output)?; + let out_dir = env::var("OUT_DIR").unwrap(); + let out_dir_path = Path::new(&out_dir); + let parser_tests_dir = out_dir_path.join("src").join("parser").join("tests"); + fs::create_dir_all(&parser_tests_dir)?; + let files = generate_test_files(Path::new("src/parser/tests"))?; + for parser_test_file in &files { + let file_path = parser_tests_dir.join(&parser_test_file.file_name); + fs::write(file_path, &parser_test_file.contents)?; + } Ok(()) } diff --git a/cst-test-generator/Cargo.toml b/cst-test-generator/Cargo.toml new file mode 100644 index 0000000..364f731 --- /dev/null +++ b/cst-test-generator/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "cst-test-generator" +version = "0.1.0" +edition = "2024" + +[dependencies] +convert_case = "0.8.0" +prettyplease = "0.2.37" +proc-macro2 = "1.0.101" +quote = "1.0.40" +syn = "2.0.106" diff --git a/cst-test-generator/src/lib.rs b/cst-test-generator/src/lib.rs new file mode 100644 index 0000000..d83eac6 --- /dev/null +++ b/cst-test-generator/src/lib.rs @@ -0,0 +1,115 @@ +use convert_case::{Case, Casing}; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use std::path::Path; +use std::{fs, io}; +use syn::File; + +pub struct ParserTestFile { + pub file_name: String, + pub contents: String, +} + +pub fn generate_test_files(tests_dir: &Path) -> io::Result> { + let mut files: Vec = vec![]; + let mut test_module_names: Vec = vec![]; + + // generate test file for each sub dir + for sub_dir in fs::read_dir(tests_dir)? { + let sub_dir = sub_dir?; + let sub_dir_path = sub_dir.path(); + if sub_dir_path.is_dir() { + let sub_dir_file_name = sub_dir.file_name(); + let sub_dir_string = sub_dir_file_name.to_string_lossy(); + test_module_names.push(sub_dir_string.to_string()); + + let sub_dir_pascal = sub_dir_string.to_case(Case::Pascal); + let rule_ident = format_ident!("{}", sub_dir_pascal); + + let mut tests: Vec = vec![]; + for test_file in fs::read_dir(sub_dir_path)? { + let test_file = test_file?; + let test_file_name = test_file.file_name(); + let test_ident = format_ident!("{}", test_file_name.to_string_lossy()); + + let src_input = fs::read_to_string(test_file.path())?; + + let test = quote! { + #[test] + fn #test_ident() { + parses_to(Rule::#rule_ident, #src_input) + } + }; + + tests.push(test); + } + + let tests_mod_ident = format_ident!("{}_tests", sub_dir.file_name().to_string_lossy()); + let test_file_contents = quote! { + #[cfg(test)] + mod #tests_mod_ident { + use crate::parser::Rule; + use crate::parser::tests::parses_to; + + #(#tests)* + } + }; + let parsed: File = syn::parse2(test_file_contents).unwrap(); + let contents = prettyplease::unparse(&parsed); + + let file_name = sub_dir.file_name().to_string_lossy().to_string() + ".rs"; + files.push(ParserTestFile { + file_name, + contents, + }) + } else { + println!("Warning: not a directory: {:?}", sub_dir_path); + } + } + + let test_mod_statements = test_module_names + .iter() + .map(|name| format_ident!("{}", name)) + .map(|module_ident| quote! { mod #module_ident; }) + .collect::>(); + + // generate main mod.rs file + let main_mod = quote! { + use crate::parser::DeimosParser; + use crate::parser::Rule; + use pest_derive::Parser; + + #(#test_mod_statements)* + + pub(crate) fn parses_to(rule: Rule, input: &str) { + let parse_result = DeimosParser::parse(rule, input); + if let Err(e) = parse_result { + panic!("Parsing failed.\n{}", e); + } else { + let mut pairs = parse_result.unwrap(); + if input.trim() != pairs.as_str().trim() { + panic!( + "Parsing did not consume entire input. Consumed only:\n{}", + pairs.as_str().trim() + ); + } + let first = pairs.next().unwrap(); + if rule != pair.as_rule() { + panic!( + "Expected {} but found {:?}.", + stringify!(rule), + pair.as_rule() + ); + } + } + } + }; + let mod_file_parsed: File = syn::parse2(main_mod).unwrap(); + let mod_file_contents = prettyplease::unparse(&mod_file_parsed); + files.push(ParserTestFile { + file_name: String::from("mod.rs"), + contents: mod_file_contents, + }); + + Ok(files) +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 78b7239..2350421 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -107,46 +107,46 @@ mod deimos_parser_tests { ) } - #[test] - fn if_else_statement() { - parses_to( - Rule::IfElseStatement, - indoc! {" - if (foo == 42) { - bar() - } else { - baz() - }"}, - ) - } - - #[test] - fn if_else_if_statement() { - parses_to( - Rule::IfElseStatement, - indoc! {" - if (foo == 42) { - bar() - } else if (foo == 16) { - baz() - }"}, - ) - } - - #[test] - fn if_else_if_else_statement() { - parses_to( - Rule::IfElseStatement, - indoc! {" - if (foo == 42) { - foo() - } else if (foo == 16) { - baz() - } else { - fizz() - }"}, - ) - } + // #[test] + // fn if_else_statement() { + // parses_to( + // Rule::IfElseStatement, + // indoc! {" + // if (foo == 42) { + // bar() + // } else { + // baz() + // }"}, + // ) + // } + // + // #[test] + // fn if_else_if_statement() { + // parses_to( + // Rule::IfElseStatement, + // indoc! {" + // if (foo == 42) { + // bar() + // } else if (foo == 16) { + // baz() + // }"}, + // ) + // } + // + // #[test] + // fn if_else_if_else_statement() { + // parses_to( + // Rule::IfElseStatement, + // indoc! {" + // if (foo == 42) { + // foo() + // } else if (foo == 16) { + // baz() + // } else { + // fizz() + // }"}, + // ) + // } #[test] fn while_statement() { diff --git a/src/parser/tests/backtick_string/empty b/src/parser/tests/backtick_string/empty new file mode 100644 index 0000000..935e6d8 --- /dev/null +++ b/src/parser/tests/backtick_string/empty @@ -0,0 +1 @@ +`` \ No newline at end of file diff --git a/src/parser/tests/backtick_string/expressions b/src/parser/tests/backtick_string/expressions new file mode 100644 index 0000000..3bace26 --- /dev/null +++ b/src/parser/tests/backtick_string/expressions @@ -0,0 +1 @@ +`${greeting}, ${world}!` \ No newline at end of file diff --git a/src/parser/tests/backtick_string/no_expressions b/src/parser/tests/backtick_string/no_expressions new file mode 100644 index 0000000..12ffd82 --- /dev/null +++ b/src/parser/tests/backtick_string/no_expressions @@ -0,0 +1 @@ +`Hello, World!` \ No newline at end of file