1use std::collections::HashMap;
12
13use crate::prefix;
14use crate::script;
15use texlang::traits::*;
16use texlang::vm::implement_has_component;
17use texlang::vm::VM;
18use texlang::*;
19
20#[derive(Default)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub struct State {
27 prefix: prefix::Component,
31 script: script::Component,
32 integer: i32,
33}
34
35impl TexlangState for State {}
36
37implement_has_component![
38 State,
39 (prefix::Component, prefix),
40 (script::Component, script),
41];
42
43impl State {
44 pub fn get_integer() -> command::BuiltIn<State> {
45 variable::Command::new_singleton(
46 |state: &State, _: variable::Index| -> &i32 { &state.integer },
47 |state: &mut State, _: variable::Index| -> &mut i32 { &mut state.integer },
48 )
49 .into()
50 }
51}
52
53pub enum TestOption<'a, S> {
55 InitialCommands(fn() -> HashMap<&'static str, command::BuiltIn<S>>),
59
60 InitialCommandsDyn(Box<dyn Fn() -> HashMap<&'static str, command::BuiltIn<S>> + 'a>),
64
65 CustomVMInitialization(fn(&mut VM<S>)),
70
71 #[allow(clippy::type_complexity)]
76 CustomVMInitializationDyn(Box<dyn Fn(&mut VM<S>) + 'a>),
77
78 AllowUndefinedCommands(bool),
82}
83
84pub fn run_expansion_equality_test<S>(lhs: &str, rhs: &str, options: &[TestOption<S>])
88where
89 S: Default + HasComponent<script::Component>,
90{
91 let options = ResolvedOptions::new(options);
92
93 let mut vm_1 = initialize_vm(&options);
94 let output_1 = crate::testing::execute_source_code(&mut vm_1, lhs, &options).unwrap();
95
96 let mut vm_2 = initialize_vm(&options);
97 let output_2 = crate::testing::execute_source_code(&mut vm_2, rhs, &options).unwrap();
98 compare_output(output_1, &vm_1, output_2, &vm_2)
99}
100
101fn compare_output<S>(
102 output_1: Vec<token::Token>,
103 vm_1: &vm::VM<S>,
104 output_2: Vec<token::Token>,
105 vm_2: &vm::VM<S>,
106) {
107 use ::texlang::token::Value::ControlSequence;
108 println!("{output_1:?}");
109 let equal = match output_1.len() == output_2.len() {
110 false => {
111 println!(
112 "output lengths do not match: {} != {}",
113 output_1.len(),
114 output_2.len()
115 );
116 false
117 }
118 true => {
119 let mut equal = true;
120 for (token_1, token_2) in output_1.iter().zip(output_2.iter()) {
121 let token_equal = match (&token_1.value(), &token_2.value()) {
122 (ControlSequence(cs_name_1), ControlSequence(cs_name_2)) => {
123 let name_1 = vm_1.cs_name_interner().resolve(*cs_name_1).unwrap();
124 let name_2 = vm_2.cs_name_interner().resolve(*cs_name_2).unwrap();
125 name_1 == name_2
126 }
127 _ => token_1 == token_2,
128 };
129 if !token_equal {
130 equal = false;
131 break;
132 }
133 }
134 equal
135 }
136 };
137
138 if !equal {
139 println!("Expansion output is different:");
140 println!("------[lhs]------");
141 println!(
142 "'{}'",
143 ::texlang::token::write_tokens(&output_1, vm_1.cs_name_interner())
144 );
145 println!("------[rhs]------");
146 println!(
147 "'{}'",
148 ::texlang::token::write_tokens(&output_2, vm_2.cs_name_interner())
149 );
150 println!("-----------------");
151 panic!("Expansion test failed");
152 }
153}
154
155pub fn run_failure_test<S>(input: &str, options: &[TestOption<S>])
159where
160 S: Default + HasComponent<script::Component>,
161{
162 let options = ResolvedOptions::new(options);
163
164 let mut vm = initialize_vm(&options);
165 let result = execute_source_code(&mut vm, input, &options);
166 if let Ok(output) = result {
167 println!("Expansion succeeded:");
168 println!(
169 "{}",
170 ::texlang::token::write_tokens(&output, vm.cs_name_interner())
171 );
172 panic!("Expansion failure test did not pass: expansion successful");
173 }
174}
175
176pub enum SerdeFormat {
177 Json,
178 MessagePack,
179}
180
181#[cfg(not(feature = "serde"))]
183pub fn run_serde_test<S>(_: &str, _: &str, _: &[TestOption<S>], format: SerdeFormat) {}
184
185#[cfg(feature = "serde")]
187pub fn run_serde_test<S>(
188 input_1: &str,
189 input_2: &str,
190 options: &[TestOption<S>],
191 format: SerdeFormat,
192) where
193 S: Default + HasComponent<script::Component> + serde::Serialize + serde::de::DeserializeOwned,
194{
195 let options = ResolvedOptions::new(options);
196
197 let mut vm_1 = initialize_vm(&options);
198 let mut output_1_1 = crate::testing::execute_source_code(&mut vm_1, input_1, &options).unwrap();
199
200 let mut vm_1 = match format {
201 SerdeFormat::Json => {
202 let serialized = serde_json::to_string_pretty(&vm_1).unwrap();
203 println!("Serialized VM: {serialized}");
204 let mut deserializer = serde_json::Deserializer::from_str(&serialized);
205 vm::VM::deserialize(&mut deserializer, (options.initial_commands)())
206 }
207 SerdeFormat::MessagePack => {
208 let serialized = rmp_serde::to_vec(&vm_1).unwrap();
209 let mut deserializer = rmp_serde::decode::Deserializer::from_read_ref(&serialized);
210 vm::VM::deserialize(&mut deserializer, (options.initial_commands)())
211 }
212 };
213
214 vm_1.push_source("testing2.tex", input_2).unwrap();
215 let mut output_1_2 = script::run(&mut vm_1).unwrap();
216 output_1_1.append(&mut output_1_2);
217
218 let mut vm_2 = initialize_vm(&options);
219 let output_2 =
220 crate::testing::execute_source_code(&mut vm_2, format!["{input_1}{input_2}"], &options)
221 .unwrap();
222
223 compare_output(output_1_1, &vm_1, output_2, &vm_2)
224}
225
226pub struct ResolvedOptions<'a, S> {
227 initial_commands: &'a dyn Fn() -> HashMap<&'static str, command::BuiltIn<S>>,
228 custom_vm_initialization: &'a dyn Fn(&mut VM<S>),
229 allow_undefined_commands: bool,
230}
231
232impl<'a, S> ResolvedOptions<'a, S> {
233 pub fn new(options: &'a [TestOption<S>]) -> Self {
234 let mut resolved = Self {
235 initial_commands: &HashMap::new,
236 custom_vm_initialization: &|_| {},
237 allow_undefined_commands: true,
238 };
239 for option in options {
240 match option {
241 TestOption::InitialCommands(f) => resolved.initial_commands = f,
242 TestOption::InitialCommandsDyn(f) => resolved.initial_commands = f,
243 TestOption::CustomVMInitialization(f) => resolved.custom_vm_initialization = f,
244 TestOption::CustomVMInitializationDyn(f) => resolved.custom_vm_initialization = f,
245 TestOption::AllowUndefinedCommands(b) => resolved.allow_undefined_commands = *b,
246 }
247 }
248 resolved
249 }
250}
251
252pub fn initialize_vm<S: Default>(options: &ResolvedOptions<S>) -> Box<vm::VM<S>> {
253 let mut vm = VM::<S>::new((options.initial_commands)());
254 (options.custom_vm_initialization)(&mut vm);
255 vm
256}
257
258pub fn execute_source_code<S, T: Into<String>>(
260 vm: &mut vm::VM<S>,
261 source: T,
262 options: &ResolvedOptions<S>,
263) -> Result<Vec<token::Token>, Box<error::Error>>
264where
265 S: Default + HasComponent<script::Component>,
266{
267 vm.push_source("testing.tex", source).unwrap();
268 script::set_allow_undefined_command(&mut vm.state, options.allow_undefined_commands);
269 script::run(vm)
270}
271
272#[derive(Default)]
303pub struct InMemoryFileSystem {
304 files: HashMap<std::path::PathBuf, String>,
305}
306
307impl vm::FileSystem for InMemoryFileSystem {
308 fn read_to_string(&self, path: &std::path::Path) -> std::io::Result<String> {
309 match self.files.get(path) {
310 None => Err(std::io::Error::new(
311 std::io::ErrorKind::NotFound,
312 "not found",
313 )),
314 Some(content) => Ok(content.clone()),
315 }
316 }
317 fn write_bytes(&self, _: &std::path::Path, _: &[u8]) -> std::io::Result<()> {
318 unimplemented!()
319 }
320}
321
322impl InMemoryFileSystem {
323 pub fn add_file(&mut self, path: std::path::PathBuf, content: &str) {
325 self.files.insert(path, content.to_string());
326 }
327}
328
329#[macro_export]
372macro_rules! test_suite {
373 ( state($state: ty), options $options: tt, expansion_equality_tests ( $( ($name: ident, $lhs: expr, $rhs: expr $(,)? ) ),* $(,)? ) $(,)? ) => (
374 $(
375 #[test]
376 fn $name() {
377 let lhs = $lhs;
378 let rhs = $rhs;
379 let options = vec! $options;
380 $crate::testing::run_expansion_equality_test::<$state>(&lhs, &rhs, &options);
381 }
382 )*
383 );
384 ( state($state: ty), options $options: tt, expansion_equality_tests $test_body: tt $(,)? ) => (
385 compile_error!("Invalid test cases for expansion_equality_tests: must be a list of tuples (name, lhs, rhs)");
386 );
387 ( state($state: ty), options $options: tt, serde_tests ( $( ($name: ident, $lhs: expr, $rhs: expr $(,)? ) ),* $(,)? ) $(,)? ) => (
388 $(
389 mod $name {
390 use super::*;
391 #[cfg_attr(not(feature = "serde"), ignore)]
392 #[test]
393 fn json() {
394 let lhs = $lhs;
395 let rhs = $rhs;
396 let options = vec! $options;
397 $crate::testing::run_serde_test::<$state>(&lhs, &rhs, &options, $crate::testing::SerdeFormat::Json);
398 }
399 #[cfg_attr(not(feature = "serde"), ignore)]
400 #[test]
401 fn message_pack() {
402 let lhs = $lhs;
403 let rhs = $rhs;
404 let options = vec! $options;
405 $crate::testing::run_serde_test::<$state>(&lhs, &rhs, &options, $crate::testing::SerdeFormat::MessagePack);
406 }
407 }
408 )*
409 );
410 ( state($state: ty), options $options: tt, failure_tests ( $( ($name: ident, $input: expr $(,)? ) ),* $(,)? ) $(,)? ) => (
411 $(
412 #[test]
413 fn $name() {
414 let input = $input;
415 let options = vec! $options;
416 $crate::testing::run_failure_test::<$state>(&input, &options);
417 }
418 )*
419 );
420 ( state($state: ty), options $options: tt, $test_kind: ident $test_cases: tt $(,)? ) => (
421 compile_error!("Invalid keyword: test_suite! only accepts the following keywords: `state, `options`, `expansion_equality_tests`, `failure_tests`, `serde_tests`");
422 );
423 ( state($state: ty), options $options: tt, $( $test_kind: ident $test_cases: tt ),+ $(,)? ) => (
424 $(
425 test_suite![state($state), options $options, $test_kind $test_cases,];
426 )+
427 );
428 ( options $options: tt, $( $test_kind: ident $test_cases: tt ),+ $(,)? ) => (
429 test_suite![state(State), options $options, $( $test_kind $test_cases, )+ ];
430 );
431 ( $( $test_kind: ident $test_cases: tt ),+ $(,)? ) => (
432 test_suite![options (TestOption::InitialCommands(initial_commands)), $( $test_kind $test_cases, )+ ];
433 );
434}
435
436pub use test_suite;