1#[doc(hidden)]
7pub mod test_macro_support {
8 #![expect(unsafe_code)]
10
11 use super::TestCase;
12 pub use linkme;
13
14 #[linkme::distributed_slice]
15 pub static TESTS: [Option<fn() -> (&'static str, Vec<TestCase>)>];
16
17 #[linkme::distributed_slice(TESTS)]
21 static WORKAROUND: Option<fn() -> (&'static str, Vec<TestCase>)> = None;
22}
23
24use crate::PetriLogSource;
25use crate::TestArtifactRequirements;
26use crate::TestArtifacts;
27use crate::requirements::HostContext;
28use crate::requirements::TestCaseRequirements;
29use crate::requirements::TestRequirement;
30use crate::requirements::can_run_test_with_context;
31use crate::tracing::try_init_tracing;
32use anyhow::Context as _;
33use futures::FutureExt as _;
34use pal_async::DefaultDriver;
35use pal_async::DefaultPool;
36use petri_artifacts_core::ArtifactResolver;
37use petri_artifacts_core::RemoteAccess;
38use std::collections::BTreeMap;
39use std::panic::AssertUnwindSafe;
40use std::panic::catch_unwind;
41use test_macro_support::TESTS;
42
43#[macro_export]
46macro_rules! test {
47 ($f:ident, $req:expr) => {
48 $crate::multitest!(vec![
49 $crate::SimpleTest::new_async(stringify!($f), $req, $f).into()
50 ]);
51 };
52}
53
54#[macro_export]
60macro_rules! unstable_test {
61 ($f:ident, $req:expr, $reason:expr) => {
62 $crate::multitest!(vec![
63 $crate::SimpleTest::new_async(stringify!($f), $req, $f)
64 .unstable($reason)
65 .into()
66 ]);
67 };
68}
69
70#[macro_export]
73macro_rules! test_sync {
74 ($f:ident, $req:expr) => {
75 $crate::multitest!(vec![
76 $crate::SimpleTest::new_sync(stringify!($f), $req, $f).into()
77 ]);
78 };
79}
80
81#[macro_export]
87macro_rules! unstable_test_sync {
88 ($f:ident, $req:expr, $reason:expr) => {
89 $crate::multitest!(vec![
90 $crate::SimpleTest::new_sync(stringify!($f), $req, $f)
91 .unstable($reason)
92 .into()
93 ]);
94 };
95}
96
97#[macro_export]
100macro_rules! multitest {
101 ($tests:expr) => {
102 const _: () = {
103 use $crate::test_macro_support::linkme;
104 #[linkme::distributed_slice($crate::test_macro_support::TESTS)]
105 #[linkme(crate = linkme)]
106 static TEST: Option<fn() -> (&'static str, Vec<$crate::TestCase>)> =
107 Some(|| (module_path!(), $tests));
108 };
109 };
110}
111
112pub struct TestCase(Box<dyn DynRunTest>);
114
115impl TestCase {
116 pub fn new(test: impl 'static + RunTest) -> Self {
118 Self(Box::new(test))
119 }
120}
121
122impl<T: 'static + RunTest> From<T> for TestCase {
123 fn from(test: T) -> Self {
124 Self::new(test)
125 }
126}
127
128struct Test {
130 module: &'static str,
131 test: TestCase,
132 artifact_requirements: TestArtifactRequirements,
133}
134
135impl Test {
136 fn all() -> impl Iterator<Item = Self> {
138 TESTS.iter().flatten().flat_map(|f| {
139 let (module, tests) = f();
140 tests.into_iter().filter_map(move |test| {
141 let mut artifact_requirements = test.0.artifact_requirements()?;
142 artifact_requirements.require(
144 petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY,
145 RemoteAccess::LocalOnly,
146 false,
147 None,
148 );
149 Some(Self {
150 module,
151 artifact_requirements,
152 test,
153 })
154 })
155 })
156 }
157
158 fn name(&self) -> String {
160 match self.module.split_once("::") {
162 Some((_crate_name, rest)) => format!("{}::{}", rest, self.test.0.leaf_name()),
163 None => self.test.0.leaf_name().to_owned(),
164 }
165 }
166
167 fn run(
168 &self,
169 resolve: fn(&str, TestArtifactRequirements) -> anyhow::Result<TestArtifacts>,
170 unstable: bool,
171 ) -> anyhow::Result<()> {
172 let name = self.name();
173 let artifacts = resolve(&name, self.artifact_requirements.clone())
174 .context("failed to resolve artifacts")?;
175 let output_dir = artifacts.get(petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY);
176 let is_nested_host = self
177 .artifact_requirements
178 .nested_artifacts()
179 .next()
180 .is_some();
181 let logger = try_init_tracing(
182 output_dir,
183 tracing::level_filters::LevelFilter::DEBUG,
184 is_nested_host.then(|| "l1_".into()),
185 )
186 .context("failed to initialize tracing")?;
187 logger.log_test_start(&name);
190 let mut post_test_hooks = Vec::new();
191
192 #[cfg(windows)]
197 post_test_hooks.push(collect_watson_events_hook(logger.clone()));
198
199 let r = catch_unwind(AssertUnwindSafe(|| {
203 self.test.0.run(
204 PetriTestParams {
205 test_name: &name,
206 log_source: &logger,
207 post_test_hooks: &mut post_test_hooks,
208 },
209 &artifacts,
210 )
211 }));
212 let r = r.unwrap_or_else(|err| {
213 let msg = err
216 .downcast_ref::<&str>()
217 .copied()
218 .or_else(|| err.downcast_ref::<String>().map(|x| x.as_str()));
219
220 let err = if let Some(msg) = msg {
221 anyhow::anyhow!("test panicked: {msg}")
222 } else {
223 anyhow::anyhow!("test panicked (unknown payload type)")
224 };
225 Err(err)
226 });
227 logger.log_test_result(&r, unstable);
228
229 for hook in post_test_hooks {
230 tracing::info!(name = hook.name(), "Running post-test hook");
231 if let Err(e) = hook.run(r.is_ok()) {
232 tracing::error!(
233 error = e.as_ref() as &dyn std::error::Error,
234 "Post-test hook failed"
235 );
236 } else {
237 tracing::info!("Post-test hook completed successfully");
238 }
239 }
240
241 r
242 }
243
244 fn trial(
246 self,
247 resolve: fn(&str, TestArtifactRequirements) -> anyhow::Result<TestArtifacts>,
248 host_context: &HostContext,
249 ) -> libtest_mimic::Trial {
250 let unstable = self.test.0.unstable(host_context).map(str::to_owned);
251 libtest_mimic::Trial::test(self.name(), move || {
252 let unstable = unstable.as_deref();
253 match self.run(resolve, unstable.is_some()) {
254 Ok(()) => Ok(()),
255 Err(err) => {
256 let Some(reason) = unstable else {
257 return Err(format!("{err:#}").into());
258 };
259 if std::env::var(petri_artifacts_core::env::PETRI_IGNORE_UNSTABLE_FAILURES)
260 .ok()
261 .is_some_and(|v| !v.is_empty() && v != "0")
262 {
263 tracing::warn!(reason, "ignoring unstable test failure: {err:#}");
264 return Ok(());
265 }
266 Err(format!("unstable test failed (reason: {reason}): {err:#}").into())
267 }
268 }
269 })
270 }
271}
272
273pub trait RunTest: Send {
277 type Artifacts;
279
280 fn leaf_name(&self) -> &str;
285 fn resolve(&self, resolver: ArtifactResolver<'_>) -> Option<Self::Artifacts>;
291 fn run(&self, params: PetriTestParams<'_>, artifacts: Self::Artifacts) -> anyhow::Result<()>;
294 fn host_requirements(&self) -> Option<&TestCaseRequirements>;
296 fn unstable(&self, host_context: &HostContext) -> Option<&str>;
298 fn ignored(&self) -> bool;
301}
302
303trait DynRunTest: Send {
304 fn leaf_name(&self) -> &str;
305 fn artifact_requirements(&self) -> Option<TestArtifactRequirements>;
306 fn run(&self, params: PetriTestParams<'_>, artifacts: &TestArtifacts) -> anyhow::Result<()>;
307 fn host_requirements(&self) -> Option<&TestCaseRequirements>;
308 fn unstable(&self, host_context: &HostContext) -> Option<&str>;
309 fn ignored(&self) -> bool;
310}
311
312impl<T: RunTest> DynRunTest for T {
313 fn leaf_name(&self) -> &str {
314 self.leaf_name()
315 }
316
317 fn artifact_requirements(&self) -> Option<TestArtifactRequirements> {
318 let mut requirements = TestArtifactRequirements::new();
319 self.resolve(ArtifactResolver::collector(&mut requirements))?;
320 Some(requirements)
321 }
322
323 fn run(&self, params: PetriTestParams<'_>, artifacts: &TestArtifacts) -> anyhow::Result<()> {
324 let artifacts = self
325 .resolve(ArtifactResolver::resolver(artifacts))
326 .context("test should have been skipped")?;
327 self.run(params, artifacts)
328 }
329
330 fn host_requirements(&self) -> Option<&TestCaseRequirements> {
331 self.host_requirements()
332 }
333
334 fn unstable(&self, host_context: &HostContext) -> Option<&str> {
335 self.unstable(host_context)
336 }
337
338 fn ignored(&self) -> bool {
339 self.ignored()
340 }
341}
342
343pub struct PetriTestParams<'a> {
345 pub test_name: &'a str,
347 pub log_source: &'a PetriLogSource,
349 pub post_test_hooks: &'a mut Vec<PetriPostTestHook>,
351}
352
353pub struct PetriPostTestHook {
356 name: String,
358 hook: Box<dyn FnOnce(bool) -> anyhow::Result<()>>,
360}
361
362impl PetriPostTestHook {
363 pub fn new(name: String, hook: impl FnOnce(bool) -> anyhow::Result<()> + 'static) -> Self {
364 Self {
365 name,
366 hook: Box::new(hook),
367 }
368 }
369
370 pub fn name(&self) -> &str {
371 &self.name
372 }
373
374 pub fn run(self, test_passed: bool) -> anyhow::Result<()> {
375 (self.hook)(test_passed)
376 }
377}
378
379#[cfg(windows)]
383fn collect_watson_events_hook(logger: PetriLogSource) -> PetriPostTestHook {
384 let start_time = jiff::Timestamp::now();
385 PetriPostTestHook::new("collect watson events".into(), move |test_passed| {
386 if test_passed {
387 return Ok(());
388 }
389 let events =
390 futures::executor::block_on(crate::vm::hyperv::powershell::watson_events(&start_time));
391 if events.is_empty() {
392 return Ok(());
393 }
394 let log_file = logger.log_file("watson_events")?;
395 for event in events {
396 event.write_to(&log_file);
397 }
398 Ok(())
399 })
400}
401
402pub struct SimpleTest<A, F> {
404 leaf_name: &'static str,
405 resolve: A,
406 run: F,
407 pub host_requirements: Option<TestCaseRequirements>,
409 unstable: Option<(&'static str, TestRequirement)>,
410 ignored: bool,
411 remote_policy: RemoteAccess,
412}
413
414impl<A, AR, F, E> SimpleTest<A, F>
415where
416 A: 'static + Send + Fn(&ArtifactResolver<'_>) -> Option<AR>,
417 F: 'static + Send + AsyncFn(PetriTestParams<'_>, DefaultDriver, AR) -> Result<(), E>,
418 E: Into<anyhow::Error>,
419{
420 pub fn new_async(
430 leaf_name: &'static str,
431 resolve: A,
432 run: F,
433 ) -> SimpleTest<A, impl 'static + Send + Fn(PetriTestParams<'_>, AR) -> Result<(), E>> {
434 SimpleTest::new_sync(leaf_name, resolve, move |params, artifacts| {
435 let mut pool = DefaultPool::named(std::thread::current().name().unwrap_or(leaf_name));
436 let driver = pool.driver();
437 let r = catch_unwind(AssertUnwindSafe(|| {
441 pool.run_until(
442 AssertUnwindSafe(run(params, driver.clone(), artifacts)).catch_unwind(),
443 )
444 }));
445 drop(driver);
448 pool.run();
449 match r.flatten() {
450 Ok(r) => r,
451 Err(panic) => std::panic::resume_unwind(panic),
452 }
453 })
454 }
455}
456
457impl<A, AR, F, E> SimpleTest<A, F>
458where
459 A: 'static + Send + Fn(&ArtifactResolver<'_>) -> Option<AR>,
460 F: 'static + Send + Fn(PetriTestParams<'_>, AR) -> Result<(), E>,
461 E: Into<anyhow::Error>,
462{
463 pub fn new_sync(leaf_name: &'static str, resolve: A, run: F) -> Self {
476 SimpleTest {
477 leaf_name,
478 resolve,
479 run,
480 host_requirements: None,
481 unstable: None,
482 ignored: false,
483 remote_policy: RemoteAccess::LocalOnly,
484 }
485 }
486}
487
488impl<A, F> SimpleTest<A, F> {
489 pub fn requirements(mut self, requirements: TestCaseRequirements) -> Self {
491 self.host_requirements = Some(requirements);
492 self
493 }
494
495 pub fn unstable(self, reason: &'static str) -> Self {
502 self.unstable_on(reason, TestRequirement::Any)
503 }
504
505 pub fn unstable_on(mut self, reason: &'static str, requirement: TestRequirement) -> Self {
510 self.unstable = Some((reason, requirement));
511 self
512 }
513
514 pub fn ignore(mut self) -> Self {
517 self.ignored = true;
518 self
519 }
520
521 pub fn remote_access(mut self, policy: RemoteAccess) -> Self {
523 self.remote_policy = policy;
524 self
525 }
526}
527
528impl<A, AR, F, E> RunTest for SimpleTest<A, F>
529where
530 A: 'static + Send + Fn(&ArtifactResolver<'_>) -> Option<AR>,
531 F: 'static + Send + Fn(PetriTestParams<'_>, AR) -> Result<(), E>,
532 E: Into<anyhow::Error>,
533{
534 type Artifacts = AR;
535
536 fn leaf_name(&self) -> &str {
537 self.leaf_name
538 }
539
540 fn resolve(&self, mut resolver: ArtifactResolver<'_>) -> Option<Self::Artifacts> {
541 resolver.set_remote_policy(self.remote_policy);
542 (self.resolve)(&resolver)
543 }
544
545 fn run(&self, params: PetriTestParams<'_>, artifacts: Self::Artifacts) -> anyhow::Result<()> {
546 (self.run)(params, artifacts).map_err(Into::into)
547 }
548
549 fn host_requirements(&self) -> Option<&TestCaseRequirements> {
550 self.host_requirements.as_ref()
551 }
552
553 fn unstable(&self, host_context: &HostContext) -> Option<&str> {
554 self.unstable
555 .as_ref()
556 .filter(|(_, requirement)| requirement.is_satisfied(host_context))
557 .map(|(reason, _)| *reason)
558 }
559
560 fn ignored(&self) -> bool {
561 self.ignored
562 }
563}
564
565#[derive(clap::Parser)]
566struct Options {
567 #[clap(long)]
570 list_required_artifacts: bool,
571 #[clap(long, requires = "list_required_artifacts")]
581 tests_from_stdin: bool,
582 #[clap(flatten)]
583 inner: libtest_mimic::Arguments,
584}
585
586pub fn test_main(
588 resolve: fn(&str, TestArtifactRequirements) -> anyhow::Result<TestArtifacts>,
589) -> ! {
590 let mut args = <Options as clap::Parser>::parse();
591 if args.list_required_artifacts {
592 use std::collections::BTreeSet;
593
594 let mut required_set = BTreeSet::new();
596 let mut optional_set = BTreeSet::new();
597 let mut nested_set: BTreeMap<String, Vec<String>> = BTreeMap::new();
598
599 let stdin_tests: Option<BTreeSet<String>> = if args.tests_from_stdin {
601 use std::io::BufRead;
602 let stdin = std::io::stdin();
603 let tests: BTreeSet<String> = stdin
604 .lock()
605 .lines()
606 .map_while(Result::ok)
607 .filter(|line| !line.is_empty())
608 .collect();
609 if tests.is_empty() {
610 eprintln!("warning: no test names provided on stdin");
611 }
612 Some(tests)
613 } else {
614 None
615 };
616
617 for test in Test::all() {
618 let name = test.name();
619
620 let matches = match stdin_tests {
622 Some(ref stdin_tests) => stdin_tests.contains(&name),
623 None => true,
624 };
625
626 if matches {
627 for artifact in test.artifact_requirements.required_artifacts() {
628 required_set.insert(artifact.global_unique_id().to_string());
629 }
630 for artifact in test.artifact_requirements.optional_artifacts() {
631 optional_set.insert(artifact.global_unique_id().to_string());
632 }
633 for (artifact, test) in test.artifact_requirements.nested_artifacts() {
634 nested_set
635 .entry(artifact.global_unique_id().to_string())
636 .or_default()
637 .push(test.to_string());
638 }
639 }
640 }
641
642 let optional_set: BTreeSet<_> = optional_set.difference(&required_set).cloned().collect();
644
645 let output = petri_artifacts_core::ArtifactListOutput {
646 required: required_set.into_iter().collect(),
647 optional: optional_set.into_iter().collect(),
648 nested: nested_set.into_iter().collect(),
649 };
650
651 println!(
652 "{}",
653 serde_json::to_string(&output).expect("JSON serialization failed")
654 );
655 std::process::exit(0);
656 }
657
658 if !matches!(args.inner.test_threads, None | Some(1)) {
663 eprintln!("warning: ignoring value passed to --test-threads, using 1");
664 }
665 args.inner.test_threads = Some(1);
666
667 let host_context = futures::executor::block_on(HostContext::new());
669
670 let trials = Test::all()
671 .map(|test| {
672 let can_run = can_run_test_with_context(test.test.0.host_requirements(), &host_context);
673 let ignored = test.test.0.ignored();
674 test.trial(resolve, &host_context)
675 .with_ignored_flag(!can_run || ignored)
676 })
677 .collect();
678
679 libtest_mimic::run(&args.inner, trials).exit();
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685 use crate::requirements::ExecutionEnvironment;
686 use crate::requirements::OpenVmmHypervisor;
687 use crate::requirements::Vendor;
688 use test_with_tracing::test;
689
690 fn host_context(hypervisor: Option<OpenVmmHypervisor>) -> HostContext {
691 HostContext {
692 vm_host_info: None,
693 vendor: Vendor::Intel,
694 execution_environment: ExecutionEnvironment::Baremetal,
695 vpci_supported: false,
696 openvmm_hypervisor: hypervisor,
697 }
698 }
699
700 #[test]
701 fn instability_matches_the_host_without_restricting_eligibility() {
702 let test = SimpleTest::new_sync(
703 "conditional_unstable",
704 |_| Some(()),
705 |_, ()| Ok::<_, anyhow::Error>(()),
706 )
707 .unstable_on(
708 "MSHV pause hang",
709 TestRequirement::OpenVmmHypervisor(OpenVmmHypervisor::Mshv),
710 );
711 let test = TestCase::new(test);
712 for hypervisor in [
713 Some(OpenVmmHypervisor::Mshv),
714 Some(OpenVmmHypervisor::Kvm),
715 Some(OpenVmmHypervisor::Whp),
716 None,
717 ] {
718 let context = host_context(hypervisor);
719 assert!(can_run_test_with_context(
720 test.0.host_requirements(),
721 &context
722 ));
723 assert_eq!(
724 test.0.unstable(&context),
725 (hypervisor == Some(OpenVmmHypervisor::Mshv)).then_some("MSHV pause hang")
726 );
727 }
728 }
729
730 #[test]
731 fn instability_can_combine_backend_and_host_requirements() {
732 let test = SimpleTest::new_sync(
733 "conditional_unstable",
734 |_| Some(()),
735 |_, ()| Ok::<_, anyhow::Error>(()),
736 )
737 .unstable_on(
738 "Intel nested MSHV only",
739 TestRequirement::OpenVmmHypervisor(OpenVmmHypervisor::Mshv)
740 .and(TestRequirement::Vendor(Vendor::Intel))
741 .and(TestRequirement::ExecutionEnvironment(
742 ExecutionEnvironment::Nested,
743 )),
744 );
745 let mut context = host_context(Some(OpenVmmHypervisor::Mshv));
746 assert_eq!(RunTest::unstable(&test, &context), None);
747 context.execution_environment = ExecutionEnvironment::Nested;
748 assert_eq!(
749 RunTest::unstable(&test, &context),
750 Some("Intel nested MSHV only")
751 );
752 context.vendor = Vendor::Amd;
753 assert_eq!(RunTest::unstable(&test, &context), None);
754 }
755
756 #[test]
757 fn unconditional_and_stable_tests_keep_their_behavior() {
758 let stable =
759 SimpleTest::new_sync("stable", |_| Some(()), |_, ()| Ok::<_, anyhow::Error>(()));
760 let unstable = SimpleTest::new_sync(
761 "unconditional_unstable",
762 |_| Some(()),
763 |_, ()| Ok::<_, anyhow::Error>(()),
764 )
765 .unstable_on("conditional", TestRequirement::Any.not())
766 .unstable("all hosts");
767 for hypervisor in [
768 None,
769 Some(OpenVmmHypervisor::Mshv),
770 Some(OpenVmmHypervisor::Kvm),
771 ] {
772 let context = host_context(hypervisor);
773 assert_eq!(RunTest::unstable(&stable, &context), None);
774 assert_eq!(RunTest::unstable(&unstable, &context), Some("all hosts"));
775 }
776 }
777
778 #[test]
779 fn conditional_instability_controls_failure_reporting() {
780 let ignore_unstable =
781 std::env::var(petri_artifacts_core::env::PETRI_IGNORE_UNSTABLE_FAILURES)
782 .ok()
783 .is_some_and(|v| !v.is_empty() && v != "0");
784 for hypervisor in [OpenVmmHypervisor::Mshv, OpenVmmHypervisor::Kvm] {
785 let test = Test {
786 module: "petri",
787 test: SimpleTest::new_sync(
788 "reported_failure",
789 |_| Some(()),
790 |_, ()| Ok::<_, anyhow::Error>(()),
791 )
792 .unstable_on(
793 "MSHV only",
794 TestRequirement::OpenVmmHypervisor(OpenVmmHypervisor::Mshv),
795 )
796 .into(),
797 artifact_requirements: TestArtifactRequirements::new(),
798 };
799 let trial = test.trial(
800 |_, _| anyhow::bail!("artifact resolution failed"),
801 &host_context(Some(hypervisor)),
802 );
803 let result = libtest_mimic::run(
804 &libtest_mimic::Arguments {
805 test_threads: Some(1),
806 quiet: true,
807 ..Default::default()
808 },
809 vec![trial],
810 );
811 assert_eq!(result.num_ignored, 0);
812 assert_eq!(
813 result.has_failed(),
814 !(hypervisor == OpenVmmHypervisor::Mshv && ignore_unstable),
815 );
816 }
817 }
818}