use std::{collections::HashMap, ffi::OsStr, io::Write, os::unix::ffi::OsStrExt, path::{Path, PathBuf}, process::ExitCode}; use camino::Utf8PathBuf; use clap::Parser; use mclangc; #[macro_use] mod logger; /// Testing program for mclangc, taken inspiration from porth, which was made by tsoding :3 #[derive(Debug, clap::Parser)] #[command(version, about, long_about = None)] struct CliArgs { /// Path to the test folder #[arg(long, short, default_value="./tests")] path: Utf8PathBuf, #[clap(subcommand)] cmd: CliCmd } #[derive(Debug, clap::Subcommand)] pub enum CliCmd { /// Run the tests Run, /// Run the tests and set the output as the expected output Compile } struct CollectedFiles { tokeniser: HashMap, parser: HashMap, } enum ExpTyp { Text((PathBuf, String)), Path(PathBuf), } impl ExpTyp { pub fn path(&self) -> &Path { match self { Self::Text((p, _)) => p, Self::Path(p) => p, } } } fn collect_files_for_single_type(path: &Path) -> anyhow::Result> { let mut files = HashMap::new(); for file in path.read_dir()? { let file = file?; if file.file_type()?.is_file() { if file.path().extension() != Some(OsStr::from_bytes(b"mcl")) { continue; } let src = std::fs::read_to_string(file.path())?; let exp_p = file.path().with_extension("exp"); let name = file.path().with_extension("").file_name().unwrap().to_string_lossy().to_string(); if exp_p.exists() { let exp = std::fs::read_to_string(&exp_p)?; files.insert(name, (src, ExpTyp::Text((exp_p, exp)))); } else { files.insert(name, (src, ExpTyp::Path(exp_p))); } } } Ok(files) } fn collect_all_files(path: &Path) -> anyhow::Result { let path = path.to_path_buf(); let mut tkn = path.clone(); tkn.push("tokeniser"); let mut parser = path.clone(); parser.push("parser"); Ok(CollectedFiles { tokeniser: collect_files_for_single_type(&tkn)?, parser: collect_files_for_single_type(&parser)?, }) } fn test_tokeniser(cf: &CollectedFiles, compile: bool) -> anyhow::Result { let mut err_count = 0; for (name, (src, expected)) in &cf.tokeniser { let tokens = match mclangc::tokeniser::tokenise(src, &format!("tokeniser/{name}.mcl")) { Ok(v) => v, Err(e) => { crate::error!("Test tokeniser/{name} had an error: {e}"); err_count += 1; continue; } }; if compile { let path = expected.path(); if path.exists() { crate::info!("Test tokeniser/{name} already has a *.exp file, overwriting"); } else { crate::info!("Test tokeniser/{name} doesnt a *.exp file, creating"); } let mut fp = std::fs::File::options() .write(true) .truncate(true) .create(true) .open(path)?; write!(fp, "{tokens:#?}")?; } else { let ExpTyp::Text((_, exp)) = expected else { crate::warn!("Test tokeniser/{name} doesnt have a *.exp file, please make it by running 'test compile'"); continue; }; if format!("{tokens:#?}") == *exp { crate::info!("Test tokeniser/{name}: OK"); } else { crate::error!("Test tokeniser/{name}: FAIL"); crate::debug!("Expected: {exp}"); crate::debug!("Got: {tokens:#?}"); err_count += 1; } } } Ok(err_count) } fn test_parser(cf: &CollectedFiles, compile: bool) -> anyhow::Result { let mut err_count = 0; for (name, (src, expected)) in &cf.parser { let tokens = match mclangc::tokeniser::tokenise(src, &format!("parser/{name}.mcl")) { Ok(v) => v, Err(e) => { crate::error!("Test parser/{name} had an error: {e}"); err_count += 1; continue; } }; let ast = match mclangc::parser::parse_program(tokens) { Ok(v) => v, Err(e) => { crate::error!("Test parser/{name} had an error: {e}"); err_count += 1; continue; } }; if compile { let path = expected.path(); if path.exists() { crate::info!("Test parser/{name} already has a *.exp file, overwriting"); } else { crate::info!("Test parser/{name} doesnt a *.exp file, creating"); } let mut fp = std::fs::File::options() .write(true) .truncate(true) .create(true) .open(path)?; write!(fp, "{ast:#?}")?; } else { let ExpTyp::Text((_, exp)) = expected else { crate::warn!("Test parser/{name} doesnt have a *.exp file, please make it by running 'test compile'"); continue; }; if format!("{ast:#?}") == *exp { crate::info!("Test parser/{name}: OK"); } else { crate::error!("Test parser/{name}: FAIL"); crate::debug!("Expected: {exp}"); crate::debug!("Got: {ast:#?}"); err_count += 1; } } } Ok(err_count) } fn test(cf: &CollectedFiles, compile: bool) -> anyhow::Result { let mut err_count = test_tokeniser(&cf, compile)?; err_count += test_parser(&cf, compile)?; Ok(err_count) } fn main() -> ExitCode { let cli = CliArgs::parse(); let cf = match collect_all_files(cli.path.as_std_path()) { Ok(v) => v, Err(e) => { crate::error!("Failed to read directory '{}', do you have permission to read it?: {e}", cli.path); return ExitCode::FAILURE; } }; let ec = match cli.cmd { CliCmd::Run => { match test(&cf, false) { Ok(v) => v, Err(e) => { crate::error!("Had an error: {e}"); return ExitCode::FAILURE; } } } CliCmd::Compile => { match test(&cf, true) { Ok(v) => v, Err(e) => { crate::error!("Had an error: {e}"); return ExitCode::FAILURE; } } } }; if ec > 0 { crate::error!("Testing FAILED, had {ec} errors"); return ExitCode::FAILURE; } else { crate::info!("Testing SUCCEEDED, had 0 errors"); } ExitCode::SUCCESS }