Skip to main content

petri/
test.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Infrastructure for defining tests.
5
6#[doc(hidden)]
7pub mod test_macro_support {
8    // UNSAFETY: Needed for linkme.
9    #![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    // Always have at least one entry to work around linker bugs.
18    //
19    // See <https://github.com/llvm/llvm-project/issues/65855>.
20    #[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/// Defines a single test named after the async run function `$f`, using `$req`
44/// to resolve its artifacts. See [`SimpleTest::new_async`].
45#[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/// Defines a single unstable test named after the async run function `$f`,
55/// using `$req` to resolve its artifacts.
56///
57/// `$reason` documents why the test is unstable and is logged when an unstable
58/// failure is ignored.
59#[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/// Defines a single test named after the synchronous run function `$f`, using
71/// `$req` to resolve its artifacts. See [`SimpleTest::new_sync`].
72#[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/// Defines a single unstable test named after the synchronous run function
82/// `$f`, using `$req` to resolve its artifacts.
83///
84/// `$reason` documents why the test is unstable and is logged when an unstable
85/// failure is ignored.
86#[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/// Defines a set of tests from an expression evaluating to a
98/// [`Vec<TestCase>`](TestCase).
99#[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
112/// A single test case.
113pub struct TestCase(Box<dyn DynRunTest>);
114
115impl TestCase {
116    /// Creates a new test case from a value that implements [`RunTest`].
117    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
128/// A single test, with module name.
129struct Test {
130    module: &'static str,
131    test: TestCase,
132    artifact_requirements: TestArtifactRequirements,
133}
134
135impl Test {
136    /// Returns all the tests defined in this crate.
137    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                // All tests require the log directory.
143                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    /// Returns the name of the test.
159    fn name(&self) -> String {
160        // Strip the crate name from the module path, for consistency with libtest.
161        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        // Record the test's identity up front, so that a test which is killed
188        // or crashes before reporting a result is still identifiable.
189        logger.log_test_start(&name);
190        let mut post_test_hooks = Vec::new();
191
192        // A process that is faulted or killed writes nothing to its own logs,
193        // so the host's error reporting events are the only record that it
194        // crashed and the only source of the Watson report ID needed to find
195        // the dump.
196        #[cfg(windows)]
197        post_test_hooks.push(collect_watson_events_hook(logger.clone()));
198
199        // Catch test panics in order to cleanly log the panic result. Without
200        // this, `libtest_mimic` will report the panic to stdout and fail the
201        // test, but the details won't end up in our per-test JSON log.
202        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            // The error from `catch_unwind` is almost always either a
214            // `&str` or a `String`, since that's what `panic!` produces.
215            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    /// Returns a libtest-mimic trial to run the test.
245    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
273/// A test that can be run.
274///
275/// Register it to be run with [`test!`] or [`multitest!`].
276pub trait RunTest: Send {
277    /// The type of artifacts required by the test.
278    type Artifacts;
279
280    /// The leaf name of the test.
281    ///
282    /// To produce the full test name, this will be prefixed with the module
283    /// name where the test is defined.
284    fn leaf_name(&self) -> &str;
285    /// Returns the artifacts required by the test.
286    ///
287    /// Returns `None` if this test makes no sense for this host environment
288    /// (e.g., an x86_64 test on an aarch64 host) and should be left out of the
289    /// test list.
290    fn resolve(&self, resolver: ArtifactResolver<'_>) -> Option<Self::Artifacts>;
291    /// Runs the test, which has been assigned `name`, with the given
292    /// `artifacts`.
293    fn run(&self, params: PetriTestParams<'_>, artifacts: Self::Artifacts) -> anyhow::Result<()>;
294    /// Returns the host requirements of the current test, if any.
295    fn host_requirements(&self) -> Option<&TestCaseRequirements>;
296    /// Returns the reason this test is unstable on this host, or `None` if stable.
297    fn unstable(&self, host_context: &HostContext) -> Option<&str>;
298    /// Whether this test is ignored (skipped by default, like a libtest
299    /// `#[ignore]` test).
300    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
343/// Parameters passed to a [`RunTest`] when it is run.
344pub struct PetriTestParams<'a> {
345    /// The name of the running test.
346    pub test_name: &'a str,
347    /// The logger for the test.
348    pub log_source: &'a PetriLogSource,
349    /// Any hooks that want to run after the test completes.
350    pub post_test_hooks: &'a mut Vec<PetriPostTestHook>,
351}
352
353/// A post-test hook to be run after the test completes, regardless of if it
354/// succeeds or fails.
355pub struct PetriPostTestHook {
356    /// The name of the hook.
357    name: String,
358    /// The hook function.
359    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/// Returns a hook that, if the test failed, writes the Windows Error Reporting
380/// and Azure Watson events from the test's execution window to
381/// `watson_events.log`.
382#[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
402/// A test defined by an artifact resolver function and a run function.
403pub struct SimpleTest<A, F> {
404    leaf_name: &'static str,
405    resolve: A,
406    run: F,
407    /// Optional test requirements
408    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    /// Returns a new test with the given `leaf_name`, `resolve`, and `run`
421    /// functions, whose `run` function is async, running it on a task
422    /// pool owned by petri.
423    ///
424    /// The test defaults to stable, not ignored, with no host requirements and
425    /// a [`RemoteAccess::LocalOnly`] policy. Use the builder methods
426    /// ([`requirements`](Self::requirements), [`unstable`](Self::unstable),
427    /// [`ignore`](Self::ignore), [`remote_access`](Self::remote_access)) to
428    /// override these.
429    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            // The inner catch keeps a panicking test body from unwinding through
438            // the pool; the outer one catches panics raised by spawned tasks,
439            // such as the VM's timeout watchdog.
440            let r = catch_unwind(AssertUnwindSafe(|| {
441                pool.run_until(
442                    AssertUnwindSafe(run(params, driver.clone(), artifacts)).catch_unwind(),
443                )
444            }));
445            // Let the diagnostic tasks the VM spawns as it is dropped finish
446            // before the failure is reported.
447            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    /// Returns a new test with the given `leaf_name`, `resolve`, and `run`
464    /// functions.
465    ///
466    /// Use [`new_async`](Self::new_async) instead if the test constructs a
467    /// [`PetriVm`](crate::PetriVm); its diagnostics on failure rely on petri
468    /// owning the task pool.
469    ///
470    /// The test defaults to stable, not ignored, with no host requirements and
471    /// a [`RemoteAccess::LocalOnly`] policy. Use the builder methods
472    /// ([`requirements`](Self::requirements), [`unstable`](Self::unstable),
473    /// [`ignore`](Self::ignore), [`remote_access`](Self::remote_access)) to
474    /// override these.
475    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    /// Sets the host requirements that must be satisfied for this test to run.
490    pub fn requirements(mut self, requirements: TestCaseRequirements) -> Self {
491        self.host_requirements = Some(requirements);
492        self
493    }
494
495    /// Marks this test as unstable. When `PETRI_IGNORE_UNSTABLE_FAILURES` is
496    /// set (as it is in CI), a failure of this test is logged and ignored
497    /// rather than failing the run; otherwise it fails like any other test.
498    ///
499    /// `reason` documents why the test is unstable and is logged when an
500    /// unstable failure is ignored.
501    pub fn unstable(self, reason: &'static str) -> Self {
502        self.unstable_on(reason, TestRequirement::Any)
503    }
504
505    /// Marks this test as unstable only on hosts that satisfy `requirement`.
506    ///
507    /// On other hosts the test still runs, but failures remain gating.
508    /// This does not change the test's host eligibility requirements.
509    pub fn unstable_on(mut self, reason: &'static str, requirement: TestRequirement) -> Self {
510        self.unstable = Some((reason, requirement));
511        self
512    }
513
514    /// Marks this test as ignored: it is skipped by default and only runs when
515    /// explicitly requested (like a libtest `#[ignore]` test).
516    pub fn ignore(mut self) -> Self {
517        self.ignored = true;
518        self
519    }
520
521    /// Sets the remote-access policy used when resolving artifacts.
522    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    /// Lists the required artifacts for all tests in JSON format.
568    /// Use --tests-from-stdin to query artifacts for specific tests.
569    #[clap(long)]
570    list_required_artifacts: bool,
571    /// When used with --list-required-artifacts, read exact test names from
572    /// stdin (one per line) to query artifacts for specific tests only.
573    ///
574    /// Even though users can use nextest's filter logic to run a subset of
575    /// tests, due to nextest's architecture of running one test per binary we
576    /// cannot accept a nextest filter here directly. Instead, vmm-tests-run
577    /// must first call nextest with the desired filter to determine the exact
578    /// test names to pass via stdin, then ask petri what artifacts are required
579    /// for those tests.
580    #[clap(long, requires = "list_required_artifacts")]
581    tests_from_stdin: bool,
582    #[clap(flatten)]
583    inner: libtest_mimic::Arguments,
584}
585
586/// Entry point for test binaries.
587pub 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        // Collect all artifacts from tests (all tests, or those specified via stdin)
595        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        // If reading test names from stdin, collect them into a set for exact matching
600        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            // If reading from stdin, do exact matching; otherwise include all tests
621            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        // Remove from optional any artifacts that are required
643        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    // Always just use one thread to avoid interleaving logs and to avoid using
659    // too many resources. These tests are usually run under nextest, which will
660    // run them in parallel in separate processes with appropriate concurrency
661    // limits.
662    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    // Create the host context once to avoid repeated expensive queries
668    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}