Include the uxntal-test-suite submodule and add the generator crate

This commit is contained in:
Karol Belina 2021-09-30 17:30:19 +02:00
parent 4ae2372c02
commit a242f7fcfb
11 changed files with 265 additions and 32 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "tests/suite"]
path = tests/suite
url = git@github.com:karolbelina/uxntal-test-suite.git

17
Cargo.lock generated
View File

@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "anyhow"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -18,6 +24,16 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "generator"
version = "0.0.0"
dependencies = [
"anyhow",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.27"
@ -41,6 +57,7 @@ name = "ruxnasm"
version = "0.2.0"
dependencies = [
"codespan-reporting",
"generator",
"test-case",
]

View File

@ -11,6 +11,9 @@ keywords = ["assembler", "uxn", "uxntal"]
categories = ["command-line-utilities", "compilers"]
exclude = [".github", ".vscode", "docs"]
[workspace]
members = ["tests/generator"]
[[bin]]
name = "ruxnasm"
required-features = ["bin"]
@ -26,3 +29,4 @@ codespan-reporting = { version = "0.11.1", optional = true }
[dev-dependencies]
test-case = "1.1.0"
generator = { path = "tests/generator" }

View File

@ -0,0 +1,14 @@
[package]
name = "generator"
version = "0.0.0"
authors = ["Karol Belina <karolbelina@gmail.com>"]
edition = "2018"
[lib]
proc-macro = true
[dependencies]
anyhow = "1.0"
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "1.0", features = ["full"] }

View File

@ -0,0 +1,27 @@
mod test;
mod tests;
mod tests_mod;
mod utils;
use proc_macro::TokenStream;
use quote::quote;
use test::Test;
use tests::Tests;
use tests_mod::TestsMod;
#[proc_macro]
pub fn generate_tests(_: TokenStream) -> TokenStream {
let tests = match Tests::discover("tests/suite") {
Ok(tests) => tests,
Err(err) => {
panic!("\n{:?}", err);
}
};
let tests = tests.expand();
(quote! {
#tests
})
.into()
}

View File

@ -0,0 +1,76 @@
use crate::utils::escape_name;
use anyhow::{bail, Result};
use proc_macro2::TokenStream;
use quote::quote;
use std::path::{Component, Path, PathBuf};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Test {
name: String,
dirs: Vec<String>,
path: PathBuf,
}
impl Test {
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let mut components = human_readable_components(path).collect::<Vec<_>>();
let name = components.pop().unwrap();
let relative_path = Path::new("tests/suite").join(path);
if !relative_path.join("input.tal").exists() {
bail!("Test is missing the `input.tal` file");
}
Ok(Self {
name,
dirs: components,
path: relative_path,
})
}
pub fn dirs(&self) -> impl Iterator<Item = String> + '_ {
self.dirs.iter().cloned()
}
pub fn expand(&self) -> TokenStream {
let name = escape_name(&self.name);
let input_path_string = self.path.join("input.tal").display().to_string();
let output_path_string = self.path.join("output.rom").display().to_string();
quote! {
#[test]
fn #name() {
let input_path = ::std::path::Path::new(#input_path_string);
let output_path = ::std::path::Path::new(#output_path_string);
let input = ::std::fs::read(input_path).unwrap();
let expected_output = if output_path.exists() {
Some(::std::fs::read(output_path).unwrap())
} else {
None
};
let actual_output = match assemble(&input) {
Ok((binary, _)) => Some(binary),
Err((errors, _)) => {
println!("{:?}", errors);
None
}
};
assert_eq!(actual_output, expected_output);
}
}
}
}
fn human_readable_components<'a>(path: &'a Path) -> impl Iterator<Item = String> + 'a {
path.components().flat_map(|component| {
if let Component::Normal(dir) = component {
Some(dir.to_string_lossy().into_owned())
} else {
None
}
})
}

View File

@ -0,0 +1,32 @@
use crate::{Test, TestsMod};
use anyhow::{Context as _, Result};
use proc_macro2::TokenStream;
use std::{
fs,
path::{Path, PathBuf},
};
#[derive(Debug)]
pub struct Tests {
tests: Vec<Test>,
}
impl Tests {
pub fn discover(dir: impl AsRef<Path>) -> Result<Self> {
let tests = fs::read_to_string(dir.as_ref().join("index"))
.with_context(|| format!("Couldn't find the test suite's index"))?
.lines()
.map(|line| line.into())
.map(|path: PathBuf| {
Test::load(&path)
.with_context(|| format!("Couldn't load test: {}", path.display(),))
})
.collect::<Result<Vec<_>>>()?;
Ok(Self { tests })
}
pub fn expand(&self) -> TokenStream {
TestsMod::build(&self.tests).expand()
}
}

View File

@ -0,0 +1,59 @@
use crate::utils::escape_name;
use crate::Test;
use proc_macro2::TokenStream;
use quote::quote;
use std::collections::{BTreeMap, BTreeSet};
#[derive(Default, Debug)]
pub struct TestsMod<'a> {
tests: BTreeSet<&'a Test>,
children: BTreeMap<String, Self>,
}
impl<'a> TestsMod<'a> {
pub fn build(tests: &'a [Test]) -> Self {
tests.iter().fold(Self::default(), |mut this, test| {
this.add(test);
this
})
}
fn add(&mut self, test: &'a Test) {
let mut this = self;
for test_dir in test.dirs() {
this = this.children.entry(test_dir).or_default();
}
this.tests.insert(test);
}
pub fn expand(&self) -> TokenStream {
let tests = self.expand_tests();
let children = self.expand_children();
quote! {
#(#tests)*
#(#children)*
}
}
fn expand_tests(&self) -> impl Iterator<Item = TokenStream> + '_ {
self.tests.iter().map(|test| test.expand())
}
fn expand_children(&self) -> impl Iterator<Item = TokenStream> + '_ {
self.children.iter().map(|(name, children)| {
let name = escape_name(name);
let children = children.expand();
quote! {
mod #name {
use super::*;
#children
}
}
})
}
}

View File

@ -0,0 +1,30 @@
use proc_macro2::{Ident, Span};
pub fn escape_name(name: &str) -> Ident {
if name.is_empty() {
return Ident::new("_empty", Span::call_site());
}
let mut last_under = false;
let mut ident: String = name
.to_ascii_lowercase()
.chars()
.filter_map(|c| match c {
c if c.is_alphanumeric() => {
last_under = false;
Some(c.to_ascii_lowercase())
}
_ if !last_under => {
last_under = true;
Some('_')
}
_ => None,
})
.collect();
if !ident.starts_with(|c: char| c == '_' || c.is_ascii_alphabetic()) {
ident = format!("_{}", ident);
}
Ident::new(&ident, Span::call_site())
}

View File

@ -1,33 +1,3 @@
use ruxnasm::{assemble, Error, Error::*};
use test_case::test_case;
use ruxnasm::assemble;
macro_rules! ev {
[] => {{
Vec::<Error>::new()
}};
[$($error: expr),+] => {{
let mut vec = Vec::<Error>::new();
$( vec.push($error); )+
vec
}}
}
#[test_case("(" => ev![NoMatchingClosingParenthesis { span: 0..1 }] ; "test 1")]
#[test_case("()" => ev![] ; "test 2")]
#[test_case("( )" => ev![] ; "test 3")]
#[test_case("( )" => ev![] ; "test 4")]
#[test_case("( ( )" => ev![NoMatchingClosingParenthesis { span: 0..1 }] ; "test 5")]
#[test_case("( ( ) )" => ev![] ; "test 6")]
#[test_case("( () )" => ev![] ; "test 7")]
#[test_case("(())" => ev![] ; "test 8")]
#[test_case(")" => ev![NoMatchingOpeningParenthesis { span: 0..1 }] ; "test 9")]
#[test_case("( ) )" => ev![NoMatchingOpeningParenthesis { span: 4..5 }] ; "test 10")]
#[test_case("( ))" => ev![NoMatchingOpeningParenthesis { span: 3..4 }] ; "test 11")]
#[test_case("() )" => ev![NoMatchingOpeningParenthesis { span: 3..4 }] ; "test 12")]
fn scanner_error_tests(source: &str) -> Vec<Error> {
match assemble(source.as_bytes()) {
Ok(_) => Vec::new(),
Err((errors, _)) => errors,
}
}
generator::generate_tests!();

1
tests/suite Submodule

@ -0,0 +1 @@
Subproject commit 79f3d04e97c5fd79321c64d827386899326a0abf