1use 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#[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 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 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#[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
167pub enum TestOption<'a, S> {
169 BuiltInCommands(fn() -> HashMap<&'static str, command::BuiltIn<S>>),
173
174 BuiltInCommandsDyn(Box<dyn Fn() -> HashMap<&'static str, command::BuiltIn<S>> + 'a>),
178
179 CustomVMInitialization(fn(&mut VM<S>)),
184
185 #[allow(clippy::type_complexity)]
190 CustomVMInitializationDyn(Box<dyn Fn(&mut VM<S>) + 'a>),
191
192 AllowUndefinedCommands(bool),
196
197 RecoverFromErrors(bool),
201}
202
203pub 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
317pub 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
345pub 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
365pub enum SerdeFormat {
367 Json,
368 MessagePack,
369 BinCode,
370}
371
372#[cfg(not(feature = "serde"))]
373pub 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")]
383pub 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
484fn 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_export]
603macro_rules! test_suite {
604 (
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 (
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 (
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 (
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 (
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 (
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 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 (
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 (
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 (
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 (
786 $( @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 (
800 $( @state($state: ty), )?
801 $( @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 (
814 $( @state($state: ty), )?
815 $( @handlers($handlers: ty), )?
816 $( $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 (
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}