use std::collections::HashMap;
use crate::prefix;
use crate::script;
use texlang::traits::*;
use texlang::vm::implement_has_component;
use texlang::vm::VM;
use texlang::*;
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct State {
prefix: prefix::Component,
script: script::Component,
integer: i32,
}
impl TexlangState for State {}
implement_has_component![
State,
(prefix::Component, prefix),
(script::Component, script),
];
impl State {
pub fn get_integer() -> command::BuiltIn<State> {
variable::Command::new_singleton(
|state: &State, _: variable::Index| -> &i32 { &state.integer },
|state: &mut State, _: variable::Index| -> &mut i32 { &mut state.integer },
)
.into()
}
}
pub enum TestOption<'a, S> {
InitialCommands(fn() -> HashMap<&'static str, command::BuiltIn<S>>),
InitialCommandsDyn(Box<dyn Fn() -> HashMap<&'static str, command::BuiltIn<S>> + 'a>),
CustomVMInitialization(fn(&mut VM<S>)),
#[allow(clippy::type_complexity)]
CustomVMInitializationDyn(Box<dyn Fn(&mut VM<S>) + 'a>),
AllowUndefinedCommands(bool),
}
pub fn run_expansion_equality_test<S>(lhs: &str, rhs: &str, options: &[TestOption<S>])
where
S: Default + HasComponent<script::Component>,
{
let options = ResolvedOptions::new(options);
let mut vm_1 = initialize_vm(&options);
let output_1 = crate::testing::execute_source_code(&mut vm_1, lhs, &options).unwrap();
let mut vm_2 = initialize_vm(&options);
let output_2 = crate::testing::execute_source_code(&mut vm_2, rhs, &options).unwrap();
compare_output(output_1, &vm_1, output_2, &vm_2)
}
fn compare_output<S>(
output_1: Vec<token::Token>,
vm_1: &vm::VM<S>,
output_2: Vec<token::Token>,
vm_2: &vm::VM<S>,
) {
use ::texlang::token::Value::ControlSequence;
println!("{output_1:?}");
let equal = match output_1.len() == output_2.len() {
false => {
println!(
"output lengths do not match: {} != {}",
output_1.len(),
output_2.len()
);
false
}
true => {
let mut equal = true;
for (token_1, token_2) in output_1.iter().zip(output_2.iter()) {
let token_equal = match (&token_1.value(), &token_2.value()) {
(ControlSequence(cs_name_1), ControlSequence(cs_name_2)) => {
let name_1 = vm_1.cs_name_interner().resolve(*cs_name_1).unwrap();
let name_2 = vm_2.cs_name_interner().resolve(*cs_name_2).unwrap();
name_1 == name_2
}
_ => token_1 == token_2,
};
if !token_equal {
equal = false;
break;
}
}
equal
}
};
if !equal {
println!("Expansion output is different:");
println!("------[lhs]------");
println!(
"'{}'",
::texlang::token::write_tokens(&output_1, vm_1.cs_name_interner())
);
println!("------[rhs]------");
println!(
"'{}'",
::texlang::token::write_tokens(&output_2, vm_2.cs_name_interner())
);
println!("-----------------");
panic!("Expansion test failed");
}
}
pub fn run_failure_test<S>(input: &str, options: &[TestOption<S>])
where
S: Default + HasComponent<script::Component>,
{
let options = ResolvedOptions::new(options);
let mut vm = initialize_vm(&options);
let result = execute_source_code(&mut vm, input, &options);
if let Ok(output) = result {
println!("Expansion succeeded:");
println!(
"{}",
::texlang::token::write_tokens(&output, vm.cs_name_interner())
);
panic!("Expansion failure test did not pass: expansion successful");
}
}
pub enum SerdeFormat {
Json,
MessagePack,
}
#[cfg(not(feature = "serde"))]
pub fn run_serde_test<S>(_: &str, _: &str, _: &[TestOption<S>], format: SerdeFormat) {}
#[cfg(feature = "serde")]
pub fn run_serde_test<S>(
input_1: &str,
input_2: &str,
options: &[TestOption<S>],
format: SerdeFormat,
) where
S: Default + HasComponent<script::Component> + serde::Serialize + serde::de::DeserializeOwned,
{
let options = ResolvedOptions::new(options);
let mut vm_1 = initialize_vm(&options);
let mut output_1_1 = crate::testing::execute_source_code(&mut vm_1, input_1, &options).unwrap();
let mut vm_1 = match format {
SerdeFormat::Json => {
let serialized = serde_json::to_string_pretty(&vm_1).unwrap();
println!("Serialized VM: {serialized}");
let mut deserializer = serde_json::Deserializer::from_str(&serialized);
vm::VM::deserialize(&mut deserializer, (options.initial_commands)())
}
SerdeFormat::MessagePack => {
let serialized = rmp_serde::to_vec(&vm_1).unwrap();
let mut deserializer = rmp_serde::decode::Deserializer::from_read_ref(&serialized);
vm::VM::deserialize(&mut deserializer, (options.initial_commands)())
}
};
vm_1.push_source("testing2.tex", input_2).unwrap();
let mut output_1_2 = script::run(&mut vm_1).unwrap();
output_1_1.append(&mut output_1_2);
let mut vm_2 = initialize_vm(&options);
let output_2 =
crate::testing::execute_source_code(&mut vm_2, format!["{input_1}{input_2}"], &options)
.unwrap();
compare_output(output_1_1, &vm_1, output_2, &vm_2)
}
pub struct ResolvedOptions<'a, S> {
initial_commands: &'a dyn Fn() -> HashMap<&'static str, command::BuiltIn<S>>,
custom_vm_initialization: &'a dyn Fn(&mut VM<S>),
allow_undefined_commands: bool,
}
impl<'a, S> ResolvedOptions<'a, S> {
pub fn new(options: &'a [TestOption<S>]) -> Self {
let mut resolved = Self {
initial_commands: &HashMap::new,
custom_vm_initialization: &|_| {},
allow_undefined_commands: true,
};
for option in options {
match option {
TestOption::InitialCommands(f) => resolved.initial_commands = f,
TestOption::InitialCommandsDyn(f) => resolved.initial_commands = f,
TestOption::CustomVMInitialization(f) => resolved.custom_vm_initialization = f,
TestOption::CustomVMInitializationDyn(f) => resolved.custom_vm_initialization = f,
TestOption::AllowUndefinedCommands(b) => resolved.allow_undefined_commands = *b,
}
}
resolved
}
}
pub fn initialize_vm<S: Default>(options: &ResolvedOptions<S>) -> Box<vm::VM<S>> {
let mut vm = VM::<S>::new((options.initial_commands)());
(options.custom_vm_initialization)(&mut vm);
vm
}
pub fn execute_source_code<S, T: Into<String>>(
vm: &mut vm::VM<S>,
source: T,
options: &ResolvedOptions<S>,
) -> Result<Vec<token::Token>, Box<error::Error>>
where
S: Default + HasComponent<script::Component>,
{
vm.push_source("testing.tex", source).unwrap();
script::set_allow_undefined_command(&mut vm.state, options.allow_undefined_commands);
script::run(vm)
}
#[derive(Default)]
pub struct InMemoryFileSystem {
files: HashMap<std::path::PathBuf, String>,
}
impl vm::FileSystem for InMemoryFileSystem {
fn read_to_string(&self, path: &std::path::Path) -> std::io::Result<String> {
match self.files.get(path) {
None => Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"not found",
)),
Some(content) => Ok(content.clone()),
}
}
fn write_bytes(&self, _: &std::path::Path, _: &[u8]) -> std::io::Result<()> {
unimplemented!()
}
}
impl InMemoryFileSystem {
pub fn add_file(&mut self, path: std::path::PathBuf, content: &str) {
self.files.insert(path, content.to_string());
}
}
#[macro_export]
macro_rules! test_suite {
( state($state: ty), options $options: tt, expansion_equality_tests ( $( ($name: ident, $lhs: expr, $rhs: expr $(,)? ) ),* $(,)? ) $(,)? ) => (
$(
#[test]
fn $name() {
let lhs = $lhs;
let rhs = $rhs;
let options = vec! $options;
$crate::testing::run_expansion_equality_test::<$state>(&lhs, &rhs, &options);
}
)*
);
( state($state: ty), options $options: tt, expansion_equality_tests $test_body: tt $(,)? ) => (
compile_error!("Invalid test cases for expansion_equality_tests: must be a list of tuples (name, lhs, rhs)");
);
( state($state: ty), options $options: tt, serde_tests ( $( ($name: ident, $lhs: expr, $rhs: expr $(,)? ) ),* $(,)? ) $(,)? ) => (
$(
mod $name {
use super::*;
#[cfg_attr(not(feature = "serde"), ignore)]
#[test]
fn json() {
let lhs = $lhs;
let rhs = $rhs;
let options = vec! $options;
$crate::testing::run_serde_test::<$state>(&lhs, &rhs, &options, $crate::testing::SerdeFormat::Json);
}
#[cfg_attr(not(feature = "serde"), ignore)]
#[test]
fn message_pack() {
let lhs = $lhs;
let rhs = $rhs;
let options = vec! $options;
$crate::testing::run_serde_test::<$state>(&lhs, &rhs, &options, $crate::testing::SerdeFormat::MessagePack);
}
}
)*
);
( state($state: ty), options $options: tt, failure_tests ( $( ($name: ident, $input: expr $(,)? ) ),* $(,)? ) $(,)? ) => (
$(
#[test]
fn $name() {
let input = $input;
let options = vec! $options;
$crate::testing::run_failure_test::<$state>(&input, &options);
}
)*
);
( state($state: ty), options $options: tt, $test_kind: ident $test_cases: tt $(,)? ) => (
compile_error!("Invalid keyword: test_suite! only accepts the following keywords: `state, `options`, `expansion_equality_tests`, `failure_tests`, `serde_tests`");
);
( state($state: ty), options $options: tt, $( $test_kind: ident $test_cases: tt ),+ $(,)? ) => (
$(
test_suite![state($state), options $options, $test_kind $test_cases,];
)+
);
( options $options: tt, $( $test_kind: ident $test_cases: tt ),+ $(,)? ) => (
test_suite![state(State), options $options, $( $test_kind $test_cases, )+ ];
);
( $( $test_kind: ident $test_cases: tt ),+ $(,)? ) => (
test_suite![options (TestOption::InitialCommands(initial_commands)), $( $test_kind $test_cases, )+ ];
);
}
pub use test_suite;