Work on auto CST parser tests.

This commit is contained in:
Jesse Brault 2025-09-13 18:36:18 -05:00
parent 41693788fc
commit 024baf2064
9 changed files with 197 additions and 46 deletions

12
Cargo.lock generated
View File

@ -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",

View File

@ -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"]

View File

@ -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(())
}

View File

@ -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"

View File

@ -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<Vec<ParserTestFile>> {
let mut files: Vec<ParserTestFile> = vec![];
let mut test_module_names: Vec<String> = 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<TokenStream> = 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::<Vec<_>>();
// 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)
}

View File

@ -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() {

View File

@ -0,0 +1 @@
``

View File

@ -0,0 +1 @@
`${greeting}, ${world}!`

View File

@ -0,0 +1 @@
`Hello, World!`