texlang_testing/
lib.rs

1/*!
2Texlang unit testing library
3
4This is a crate for writing unit tests for code that uses Texlang.
5It is used extensively in the Texlang standard library,
6    so the unit tests there are good examples of what this crate can do.
7This crate is designed to be used outside of the Texlang project,
8    and work for any Rust code that uses Texlang.
9
10## Basic setup
11
12As is common in all Texlang code,
13    each unit test built with this library works with a specific user-defined Texlang state type.
14This state type is provided by the unit test writer.
15In addition to implementing the [`TexlangState`] trait, this state must also:
16
171. Include the [`TestingComponent`] type as a component.
18   I.e., the state must implement the [`HasComponent<TestingComponent>`](texlang::traits::HasComponent<TestingComponent>) trait.
19
201. Configure the `recoverable_error_hook` method on the [`TexlangState`]
21   trait to invoke [`TestingComponent::recoverable_error_hook`].
22
231. Implement [`Default`].
24
25If the unit test doesn't require anything else from the state,
26    the [`State`] type defined in this library can simply be used.
27This type satisfies all the conditions above.
28
29## Test types
30
31The crate offers a few different types of tests.
32
33### Expansion equality tests
34
35Run using [`run_expansion_equality_test`].
36
37These tests verify that two different TeX snippets expand to the same output.
38For example, an output equality test can verify that
39```tex
40\def\HelloWorld{Hola Mundo}\HelloWorld - \HelloWorld
41```
42and
43```tex
44Hola Mundo - Hola Mundo
45```
46produce the same output.
47
48In this example the second input is just a constant, which is usually how these tests are used.
49We generally use these tests to verify some non-trivial TeX expression
50    (like a `\def` macro definition)
51    evaluates to a specific constant output.
52
53These tests do _not_ verify that the state of the VM is the same in both cases.
54In fact the state is usually different; for example, in the first snippet above the
55    macro `\HelloWorld` will be defined in the state but won't be in the second snippet.
56
57### Failure tests
58
59Run using [`run_fatal_error_test`].
60
61These tests verify that a specific TeX snippet fails to execute.
62
63### Serde tests
64
65Run using [`run_serde_test`].
66
67These tests verify that the Texlang state being used can be
68    successfully serialized and deserialized in the middle of executing a TeX snippet.
69
70A serde test accepts two TeX snippets, `A` and `B`.
71It first runs the VM for the concatenated snippet `AB` and saves the result.
72It then initializes a new VM and performs the following steps:
73
74- Runs the snippet `A`
75- Serializes and deserialized the VM, using a specified format.
76- Runs the snipped `B` in the deserialized VM.
77
78The test verifies that the result from this is the same as the result from the VM for the concatenated snippet `AB`.
79
80## The test suite macro
81
82All of the test types can be run using the run functions described above (e.g., [`run_fatal_error_test`]).
83However the preferred way to write a suite of unit tests is to use the [`test_suite`] macro.
84This macro removes a bunch of boilerplate and makes it easy to add new test cases.
85
86See the macro's documentation for instructions on using it.
87Also the Texlang standard library uses this macro extensively.
88*/
89
90use std::collections::HashMap;
91
92use texlang::prelude as txl;
93use texlang::traits::*;
94use texlang::vm::implement_has_component;
95use texlang::vm::VM;
96use texlang::*;
97
98/// Texlang component that every unit testing state needs to have.
99#[derive(Default)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101pub struct TestingComponent {
102    allow_undefined_command: bool,
103    recover_from_errors: bool,
104    num_recovered_errors: std::cell::RefCell<usize>,
105    tokens: Vec<token::Token>,
106    integer: i32,
107}
108
109impl TestingComponent {
110    fn take_tokens(&mut self) -> Vec<token::Token> {
111        let mut result = Vec::new();
112        std::mem::swap(&mut result, &mut self.tokens);
113        result
114    }
115    /// Recoverable error hook for the testing component.
116    ///
117    /// States used in unit testing must be configured to use this hook.
118    pub fn recoverable_error_hook<S: HasComponent<Self>>(
119        state: &S,
120        recoverable_error: error::TracedTexError,
121    ) -> Result<(), Box<dyn error::TexError>> {
122        let component = state.component();
123        if component.recover_from_errors {
124            let mut num_recovered_errors = component.num_recovered_errors.borrow_mut();
125            *num_recovered_errors += 1;
126            Ok(())
127        } else {
128            Err(recoverable_error.error)
129        }
130    }
131    /// Returns an integer variable command that references an integer stored in the testing component.
132    ///
133    /// If you're writing a unit test that needs an integer variable it's easiest to use this
134    ///     rather than building your own variable.
135    pub fn get_integer<S: HasComponent<TestingComponent>>() -> command::BuiltIn<S> {
136        variable::Command::new_singleton(
137            |state: &S, _: variable::Index| -> &i32 { &state.component().integer },
138            |state: &mut S, _: variable::Index| -> &mut i32 { &mut state.component_mut().integer },
139        )
140        .into()
141    }
142}
143
144/// Simple state type for simple unit tests.
145///
146/// If the primitives under test don't require custom components or
147/// other pieces in the state, it is easier to use this type rather than defining a custom one.
148#[derive(Default)]
149#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
150pub struct State {
151    testing: TestingComponent,
152}
153
154impl TexlangState for State {
155    fn recoverable_error_hook(
156        &self,
157        recoverable_error: error::TracedTexError,
158    ) -> Result<(), Box<dyn error::TexError>> {
159        TestingComponent::recoverable_error_hook(self, recoverable_error)
160    }
161}
162
163implement_has_component![State {
164    testing: TestingComponent,
165}];
166
167/// Option passed to a test runner.
168pub enum TestOption<'a, S> {
169    /// The built-in commands are the result of invoking the provided static function.
170    ///
171    /// Overrides previous `InitialCommands` or `InitialCommandsDyn` options.
172    BuiltInCommands(fn() -> HashMap<&'static str, command::BuiltIn<S>>),
173
174    /// The built-in commands are the result of invoking the provided closure.
175    ///
176    /// Overrides previous `InitialCommands` or `InitialCommandsDyn` options.
177    BuiltInCommandsDyn(Box<dyn Fn() -> HashMap<&'static str, command::BuiltIn<S>> + 'a>),
178
179    /// The provided static function is invoked after the VM is created and before execution starts.
180    /// This can be used to provide more custom VM initialization.
181    ///
182    /// Overrides previous `CustomVMInitialization` or `CustomVMInitializationDyn` options.
183    CustomVMInitialization(fn(&mut VM<S>)),
184
185    /// The provided closure is invoked after the VM is created and before execution starts.
186    /// This can be used to provide more custom VM initialization.
187    ///
188    /// Overrides previous `CustomVMInitialization` or `CustomVMInitializationDyn` options.
189    #[allow(clippy::type_complexity)]
190    CustomVMInitializationDyn(Box<dyn Fn(&mut VM<S>) + 'a>),
191
192    /// Whether undefined commands raise an error.
193    ///
194    /// Overrides previous `AllowUndefinedCommands` options.
195    AllowUndefinedCommands(bool),
196
197    /// Whether to recover from errors.
198    ///
199    /// Overrides previous `RecoverFromErrors` options.
200    RecoverFromErrors(bool),
201}
202
203/// Run an expansion equality test.
204///
205/// The test passes if the two provided input strings expand to the same tokens.
206pub fn run_expansion_equality_test<S, H>(
207    lhs: &str,
208    rhs: &str,
209    expect_recoverable_errors: bool,
210    options: &[TestOption<S>],
211) where
212    S: Default + HasComponent<TestingComponent>,
213    H: texlang::vm::Handlers<S>,
214{
215    let options = ResolvedOptions::new(options);
216
217    let mut vm_1 = initialize_vm(&options);
218    let (output_1, _) = execute_source_code::<S, H>(&mut vm_1, lhs, &options)
219        .map_err(|err| {
220            println!("{err}");
221            err
222        })
223        .unwrap();
224
225    let mut vm_2 = initialize_vm(&options);
226    let (output_2, _) = execute_source_code::<S, H>(&mut vm_2, rhs, &options)
227        .map_err(|err| {
228            println!("{err}");
229            err
230        })
231        .unwrap();
232    compare_output(output_1, &vm_1, output_2, &vm_2);
233
234    let num_recovered_errors = *vm_1.state.component().num_recovered_errors.borrow();
235    match (expect_recoverable_errors, num_recovered_errors) {
236        (true, 0) => {
237            panic!("expected recoverable errors but didn't have any");
238        }
239        (true, _) | (false, 0) => (),
240        (false, i) => {
241            panic!("did not expect recoverable errors but had {i} recoverable errors",);
242        }
243    }
244}
245
246fn compare_output<S>(
247    mut output_1: Vec<token::Token>,
248    vm_1: &vm::VM<S>,
249    mut output_2: Vec<token::Token>,
250    vm_2: &vm::VM<S>,
251) {
252    let trim_space = |v: &mut Vec<token::Token>| {
253        let last = match v.last() {
254            None => return,
255            Some(last) => last,
256        };
257        if last.cat_code() == Some(types::CatCode::Space) {
258            v.pop();
259        }
260    };
261    trim_space(&mut output_1);
262    trim_space(&mut output_2);
263
264    println!("{output_1:?}");
265    println!("{output_2:?}");
266    use ::texlang::token::CommandRef::ControlSequence;
267    use ::texlang::token::Value::CommandRef;
268    let equal = match output_1.len() == output_2.len() {
269        false => {
270            println!(
271                "output lengths do not match: {} != {}",
272                output_1.len(),
273                output_2.len()
274            );
275            false
276        }
277        true => {
278            let mut equal = true;
279            for (token_1, token_2) in output_1.iter().zip(output_2.iter()) {
280                let token_equal = match (&token_1.value(), &token_2.value()) {
281                    (
282                        CommandRef(ControlSequence(cs_name_1)),
283                        CommandRef(ControlSequence(cs_name_2)),
284                    ) => {
285                        let name_1 = vm_1.cs_name_interner().resolve(*cs_name_1).unwrap();
286                        let name_2 = vm_2.cs_name_interner().resolve(*cs_name_2).unwrap();
287                        name_1 == name_2
288                    }
289                    _ => token_1.value() == token_2.value(),
290                };
291                if !token_equal {
292                    equal = false;
293                    break;
294                }
295            }
296            equal
297        }
298    };
299
300    if !equal {
301        println!("Expansion output is different:");
302        println!("------[lhs]------");
303        println!(
304            "'{}'",
305            ::texlang::token::write_tokens(&output_1, vm_1.cs_name_interner())
306        );
307        println!("------[rhs]------");
308        println!(
309            "'{}'",
310            ::texlang::token::write_tokens(&output_2, vm_2.cs_name_interner())
311        );
312        println!("-----------------");
313        panic!("Expansion test failed");
314    }
315}
316
317/// Run a failure test.
318///
319/// The test passes if execution of the provided input fails.
320pub fn run_fatal_error_test<S>(input: &str, options: &[TestOption<S>], check_end_of_input: bool)
321where
322    S: Default + HasComponent<TestingComponent>,
323{
324    let options = ResolvedOptions::new(options);
325
326    let mut vm = initialize_vm(&options);
327    let result = execute_source_code::<S, texlang::vm::DefaultHandlers>(&mut vm, input, &options);
328    match result {
329        Ok((output, _)) => {
330            println!("Expansion succeeded:");
331            println!(
332                "{}",
333                ::texlang::token::write_tokens(&output, vm.cs_name_interner())
334            );
335            panic!("Expansion failure test did not pass: expansion successful");
336        }
337        Err(err) => {
338            if check_end_of_input {
339                assert_eq!(texlang::error::Kind::EndOfInput, err.error.kind());
340            }
341        }
342    }
343}
344
345/// Run a state verification test.
346///
347/// The test passes if after running the input TeX snippet,
348/// the state verifier function runs successfully.
349pub fn run_state_test<S, H>(input: &str, options: &[TestOption<S>], state_verifier: impl Fn(&S))
350where
351    S: Default + HasComponent<TestingComponent>,
352    H: texlang::vm::Handlers<S>,
353{
354    let options = ResolvedOptions::new(options);
355    let mut vm = initialize_vm(&options);
356    execute_source_code::<S, H>(&mut vm, input, &options)
357        .map_err(|err| {
358            println!("{err}");
359            err
360        })
361        .unwrap();
362    state_verifier(&vm.state);
363}
364
365/// Format to use in a serde test.
366pub enum SerdeFormat {
367    Json,
368    MessagePack,
369    BinCode,
370}
371
372#[cfg(not(feature = "serde"))]
373/// Run a serialization/deserialization test
374pub fn run_serde_test<S>(
375    input_1: &str,
376    input_2: &str,
377    options: &[TestOption<S>],
378    format: SerdeFormat,
379) {
380}
381
382#[cfg(feature = "serde")]
383/// Run a serialization/deserialization test
384pub fn run_serde_test<S>(
385    input_1: &str,
386    input_2: &str,
387    options: &[TestOption<S>],
388    format: SerdeFormat,
389) where
390    S: Default + HasComponent<TestingComponent> + serde::Serialize + serde::de::DeserializeOwned,
391{
392    let options = ResolvedOptions::new(options);
393
394    let mut vm_1 = initialize_vm(&options);
395    let (mut output_1_1, _) =
396        execute_source_code::<S, texlang::vm::DefaultHandlers>(&mut vm_1, input_1, &options)
397            .unwrap();
398
399    let mut vm_1 = match format {
400        SerdeFormat::Json => {
401            let serialized = serde_json::to_string_pretty(&vm_1).unwrap();
402            println!("Serialized VM: {serialized}");
403            let mut deserializer = serde_json::Deserializer::from_str(&serialized);
404            vm::VM::deserialize_with_built_in_commands(
405                &mut deserializer,
406                (options.built_in_commands)(),
407            )
408            .unwrap()
409        }
410        SerdeFormat::MessagePack => {
411            let serialized = rmp_serde::to_vec(&vm_1).unwrap();
412            let mut deserializer = rmp_serde::decode::Deserializer::from_read_ref(&serialized);
413            vm::VM::deserialize_with_built_in_commands(
414                &mut deserializer,
415                (options.built_in_commands)(),
416            )
417            .unwrap()
418        }
419        SerdeFormat::BinCode => {
420            let serialized =
421                bincode::serde::encode_to_vec(&vm_1, bincode::config::standard()).unwrap();
422            let deserialized: Box<vm::serde::DeserializedVM<S>> =
423                bincode::serde::decode_from_slice(&serialized, bincode::config::standard())
424                    .unwrap()
425                    .0;
426            vm::serde::finish_deserialization(deserialized, (options.built_in_commands)())
427        }
428    };
429
430    let (mut output_1_2, _) =
431        execute_source_code::<S, texlang::vm::DefaultHandlers>(&mut vm_1, input_2, &options)
432            .unwrap();
433    output_1_1.append(&mut output_1_2);
434
435    let mut vm_2 = initialize_vm(&options);
436    let combined_input = format!["{input_1}{input_2}"];
437    let (output_2, _) = execute_source_code::<S, texlang::vm::DefaultHandlers>(
438        &mut vm_2,
439        &combined_input,
440        &options,
441    )
442    .unwrap();
443
444    compare_output(output_1_1, &vm_1, output_2, &vm_2)
445}
446
447struct ResolvedOptions<'a, S> {
448    built_in_commands: &'a dyn Fn() -> HashMap<&'static str, command::BuiltIn<S>>,
449    custom_vm_initialization: &'a dyn Fn(&mut VM<S>),
450    allow_undefined_commands: bool,
451    recover_from_errors: bool,
452}
453
454impl<'a, S> ResolvedOptions<'a, S> {
455    pub fn new(options: &'a [TestOption<S>]) -> Self {
456        let mut resolved = Self {
457            built_in_commands: &HashMap::new,
458            custom_vm_initialization: &|_| {},
459            allow_undefined_commands: false,
460            recover_from_errors: false,
461        };
462        for option in options {
463            match option {
464                TestOption::BuiltInCommands(f) => resolved.built_in_commands = f,
465                TestOption::BuiltInCommandsDyn(f) => resolved.built_in_commands = f,
466                TestOption::CustomVMInitialization(f) => resolved.custom_vm_initialization = f,
467                TestOption::CustomVMInitializationDyn(f) => resolved.custom_vm_initialization = f,
468                TestOption::AllowUndefinedCommands(b) => resolved.allow_undefined_commands = *b,
469                TestOption::RecoverFromErrors(b) => resolved.recover_from_errors = *b,
470            }
471        }
472        resolved
473    }
474}
475
476fn initialize_vm<S: Default>(options: &ResolvedOptions<S>) -> Box<vm::VM<S>> {
477    let mut vm = Box::new(VM::<S>::new_with_built_in_commands((options
478        .built_in_commands)(
479    )));
480    (options.custom_vm_initialization)(&mut vm);
481    vm
482}
483
484/// Execute source code in a VM with the provided options.
485fn execute_source_code<S, H>(
486    vm: &mut vm::VM<S>,
487    source: &str,
488    options: &ResolvedOptions<S>,
489) -> Result<(Vec<token::Token>, usize), Box<error::TracedTexError>>
490where
491    S: Default + HasComponent<TestingComponent>,
492    H: texlang::vm::Handlers<S>,
493{
494    vm.push_source("testing.tex", source).unwrap();
495    {
496        let component = vm.state.component_mut();
497        component.allow_undefined_command = options.allow_undefined_commands;
498        component.recover_from_errors = options.recover_from_errors;
499        *component.num_recovered_errors.borrow_mut() = 0;
500    }
501    vm.run::<Handlers<H>>()?;
502    Ok({
503        let component = vm.state.component_mut();
504        let tokens = component.take_tokens();
505        let num_recovered_errors = *component.num_recovered_errors.borrow();
506        (tokens, num_recovered_errors)
507    })
508}
509
510struct Handlers<H>(std::marker::PhantomData<H>);
511
512impl<S: HasComponent<TestingComponent>, H: vm::Handlers<S>> vm::Handlers<S> for Handlers<H> {
513    fn character_handler(
514        input: &mut vm::ExecutionInput<S>,
515        token: token::Token,
516        c: char,
517    ) -> txl::Result<()> {
518        input.state_mut().component_mut().tokens.push(token);
519        H::character_handler(input, token, c)
520    }
521
522    fn math_character_handler(
523        input: &mut vm::ExecutionInput<S>,
524        token: token::Token,
525        math_character: types::MathCode,
526    ) -> txl::Result<()> {
527        let s = format!("{math_character:?}");
528        for c in s.chars() {
529            let token = if c.is_ascii_alphabetic() {
530                token::Token::new_letter(c, token.trace_key())
531            } else {
532                token::Token::new_other(c, token.trace_key())
533            };
534            input.state_mut().component_mut().tokens.push(token);
535        }
536        Ok(())
537    }
538
539    fn undefined_command_handler(
540        input: &mut vm::ExecutionInput<S>,
541        token: token::Token,
542    ) -> txl::Result<()> {
543        if input.state().component().allow_undefined_command {
544            input.state_mut().component_mut().tokens.push(token);
545            Ok(())
546        } else {
547            Err(input.fatal_error(error::UndefinedCommandError::new(input.vm(), token)))
548        }
549    }
550
551    fn unexpanded_expansion_command(
552        input: &mut vm::ExecutionInput<S>,
553        token: token::Token,
554    ) -> txl::Result<()> {
555        input.state_mut().component_mut().tokens.push(token);
556        Ok(())
557    }
558}
559
560/// Macro to generate a suite of unit tests
561///
562/// The general use of this macros looks like this:
563/// ```
564/// # use texlang_testing::*;
565/// test_suite![
566///     @state(State),
567///     @options(TestOptions::InitialCommands(built_in_commands)),
568///     expansion_equality_tests(
569///         (case_1, "lhs_1", "rhs_1"),
570///         (case_2, "lhs_2", "rhs_2"),
571///     ),
572///     fatal_error_tests(
573///         (case_3, "input_3"),
574///         (case_4, "input_4"),
575///     ),
576/// ];
577/// ```
578///
579/// The arguments to the macro are:
580///
581/// - `state(State)`: defines which Rust type to use as the VM state in the tests.
582///   This can be omitted, in which case it defaults to the type name `State` in the current scope.
583///
584/// - `options(option_1, option_2, ..., option_n)`: options to pass to the test runner.
585///   This is a list of values of type [TestOption].
586///   The options can be omitted, in which case they default to `options(TestOptions::InitialCommands(built_in_commands))`.
587///   In this case `built_in_commands` is a static function that returns a list of built-in primitives
588///   to initialize the VM with.
589///
590/// - `expansion_equality_tests(cases...)`: a list of expansion equality test cases.
591///   Each case is of the form (case name, left hand side, right hand side).
592///   The data here is fed into the [run_expansion_equality_test] test runner.
593///     
594/// - `failure_tests(cases...)`: a list of failure test cases.
595///   Each case is of the form (case name, input).
596///   The data here is fed into the [run_fatal_error_test] test runner.
597///
598/// Only one `state()` argument may be provided, and if provided it must be in the first position.
599/// Only one `options()` argument may be provided, and if provided it must be in the first position
600///     or after the `state()` argument.
601/// Zero or more of the other arguments may be provided, and in any order.
602#[macro_export]
603macro_rules! test_suite {
604    // expansion_equality_tests
605    (
606        @state($state: ty),
607        @handlers($handlers: ty),
608        @options $options: tt,
609        expansion_equality_tests(
610            $( ($name: ident, $lhs: expr, $rhs: expr $(,)? ), )*
611        ),
612    ) => (
613        $(
614            #[test]
615            fn $name() {
616                let lhs = $lhs;
617                let rhs = $rhs;
618                let options = vec! $options;
619                texlang_testing::run_expansion_equality_test::<$state, $handlers>(&lhs, &rhs, false, &options);
620            }
621        )*
622    );
623    // expansion_equality_tests: fail with a helpful error message if the inner test cases are malformed
624    // (It would be nice to do this for other tests, too.)
625    (
626        @state($state: ty),
627        @handlers($handlers: ty),
628        @options $options: tt,
629        expansion_equality_tests $test_body: tt $(,)?
630    ) => (
631        compile_error!("Invalid test cases for expansion_equality_tests: must be a list of tuples (name, lhs, rhs)");
632    );
633    // serde_tests
634    (
635        @state($state: ty),
636        @handlers($handlers: ty),
637        @options $options: tt,
638        serde_tests(
639            $( ($name: ident, $lhs: expr, $rhs: expr $(,)? ), )*
640        ),
641    ) => (
642        $(
643            mod $name {
644                use super::*;
645                #[cfg_attr(not(feature = "serde"), ignore)]
646                #[test]
647                fn json() {
648                    let lhs = $lhs;
649                    let rhs = $rhs;
650                    let options = vec! $options;
651                    texlang_testing::run_serde_test::<$state>(&lhs, &rhs, &options, texlang_testing::SerdeFormat::Json);
652                }
653                #[cfg_attr(not(feature = "serde"), ignore)]
654                #[test]
655                fn message_pack() {
656                    let lhs = $lhs;
657                    let rhs = $rhs;
658                    let options = vec! $options;
659                    texlang_testing::run_serde_test::<$state>(&lhs, &rhs, &options, texlang_testing::SerdeFormat::MessagePack);
660                }
661                #[cfg_attr(not(feature = "serde"), ignore)]
662                #[test]
663                fn bincode() {
664                    let lhs = $lhs;
665                    let rhs = $rhs;
666                    let options = vec! $options;
667                    texlang_testing::run_serde_test::<$state>(&lhs, &rhs, &options, texlang_testing::SerdeFormat::BinCode);
668                }
669            }
670        )*
671    );
672    // fatal_error_tests
673    (
674        @state($state: ty),
675        @handlers($handlers: ty),
676        @options $options: tt,
677        fatal_error_tests(
678            $( ($name: ident, $input: expr $(,)? ), )*
679        ),
680    ) => (
681        $(
682            #[test]
683            fn $name() {
684                let input = $input;
685                let options = vec! $options;
686                texlang_testing::run_fatal_error_test::<$state>(&input, &options, false);
687            }
688        )*
689    );
690    // end_of_input_error_tests
691    (
692        @state($state: ty),
693        @handlers($handlers: ty),
694        @options $options: tt,
695        end_of_input_error_tests(
696            $( ($name: ident, $input: expr $(,)? ), )*
697        ),
698    ) => (
699        $(
700            #[test]
701            fn $name() {
702                let input = $input;
703                let options = vec! $options;
704                texlang_testing::run_fatal_error_test::<$state>(&input, &options, true);
705            }
706        )*
707    );
708    // recoverable_failure_tests
709    (
710        @state($state: ty),
711        @handlers($handlers: ty),
712        @options $options: tt,
713        recoverable_failure_tests(
714            $( ($name: ident, $lhs: expr, $rhs: expr $(,)? ), )*
715        ),
716    ) => (
717        $(
718            mod $name {
719                use super::*;
720                #[test]
721                fn error_recovery_enabled() {
722                    let lhs = $lhs;
723                    let rhs = $rhs;
724                    let mut options = vec! $options;
725                    options.push(texlang_testing::TestOption::RecoverFromErrors(true));
726                    // TODO: verify a recoverable error was thrown?
727                    // Yes. Also verify the number of errors.
728                    texlang_testing::run_expansion_equality_test::<$state, $handlers>(&lhs, &rhs, true, &options);
729                }
730                #[test]
731                fn error_recovery_disabled() {
732                    let input = $lhs;
733                    let mut options = vec! $options;
734                    options.push(texlang_testing::TestOption::RecoverFromErrors(false));
735                    texlang_testing::run_fatal_error_test::<$state>(&input, &options, false);
736                }
737            }
738        )*
739    );
740    // state_tests
741    (
742        @state($state: ty),
743        @handlers($handlers: ty),
744        @options $options: tt,
745        state_tests(
746            $( ($name: ident, $input: expr, $state_verifier: expr $(,)? ), )*
747        ),
748    ) => (
749        $(
750            #[test]
751            fn $name() {
752                let input = $input;
753                let options = vec! $options;
754                let state_verifier = $state_verifier;
755                texlang_testing::run_state_test::<$state, $handlers>(&input, &options, state_verifier);
756            }
757        )*
758    );
759    // Errors out if the test type (e.g. expansion_equality_tests) is invalid.
760    (
761        @state($state: ty),
762        @handlers($handlers: ty),
763        @options $options: tt,
764        $test_kind: ident $test_cases: tt,
765    ) => (
766        compile_error!("Invalid keyword: test_suite! only accepts the following keywords: `@state, `@handlerss`, `@options`, `expansion_equality_tests`, `failure_tests`, `serde_tests`");
767    );
768    // Creates one test suite for every test type.
769    (
770        @state($state: ty),
771        @handlers($handlers: ty),
772        @options $options: tt,
773        $( $test_kind: ident $test_cases: tt, )+
774    ) => (
775        $(
776            texlang_testing::test_suite![
777                @state($state),
778                @handlers($handlers),
779                @options $options,
780                $test_kind $test_cases,
781            ];
782        )+
783    );
784    // Sets the default state.
785    (
786        /* missing state */
787        $( @handlers($handlers: ty), )?
788        $( @options $options: tt, )?
789        $( $test_kind: ident $test_cases: tt, )+
790    ) => (
791        texlang_testing::test_suite![
792            @state(State),
793            $( @handlers($handlers), )?
794            $( @options $options, )?
795            $( $test_kind $test_cases, )+
796        ];
797    );
798    // Sets the default handlers.
799    (
800        $( @state($state: ty), )?
801        /* missing handlers */
802        $( @options $options: tt, )?
803        $( $test_kind: ident $test_cases: tt, )+
804    ) => (
805        texlang_testing::test_suite![
806            $( @state($state), )?
807            @handlers(texlang::vm::DefaultHandlers),
808            $( @options $options, )?
809            $( $test_kind $test_cases, )+
810        ];
811    );
812    // Sets the default options.
813    (
814        $( @state($state: ty), )?
815        $( @handlers($handlers: ty), )?
816        /* missing options */
817        $( $test_kind: ident $test_cases: tt, )+
818    ) => (
819        texlang_testing::test_suite![
820            $( @state($state), )?
821            $( @handlers($handlers), )?
822            @options (texlang_testing::TestOption::BuiltInCommands(built_in_commands)),
823            $( $test_kind $test_cases, )+
824        ];
825    );
826    // Convert from @option() form to @options form.
827    (
828        $( @state($state: ty), )?
829        $( @handlers($handlers: ty), )?
830        $( @option($option: expr), )+
831        $( $test_kind: ident $test_cases: tt, )+
832    ) => (
833        texlang_testing::test_suite![
834            $( @state($state), )?
835            $( @handlers($handlers), )?
836            @options(
837                $( $option, )*
838            ),
839            $( $test_kind $test_cases, )+
840        ];
841    );
842}