221 lines
6.8 KiB
Rust
221 lines
6.8 KiB
Rust
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<String, (String, ExpTyp)>,
|
|
parser: HashMap<String, (String, ExpTyp)>,
|
|
}
|
|
|
|
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<HashMap<String, (String, ExpTyp)>> {
|
|
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<CollectedFiles> {
|
|
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<usize> {
|
|
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<usize> {
|
|
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<usize> {
|
|
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
|
|
}
|