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::can_run_test_with_context;
30use crate::tracing::try_init_tracing;
31use anyhow::Context as _;
32use futures::FutureExt as _;
33use pal_async::DefaultDriver;
34use pal_async::DefaultPool;
35use petri_artifacts_core::ArtifactResolver;
36use petri_artifacts_core::RemoteAccess;
37use std::panic::AssertUnwindSafe;
38use std::panic::catch_unwind;
39use test_macro_support::TESTS;
40
41#[macro_export]
44macro_rules! test {
45 ($f:ident, $req:expr) => {
46 $crate::multitest!(vec![
47 $crate::SimpleTest::new_async(stringify!($f), $req, $f).into()
48 ]);
49 };
50}
51
52#[macro_export]
58macro_rules! unstable_test {
59 ($f:ident, $req:expr, $reason:expr) => {
60 $crate::multitest!(vec![
61 $crate::SimpleTest::new_async(stringify!($f), $req, $f)
62 .unstable($reason)
63 .into()
64 ]);
65 };
66}
67
68#[macro_export]
71macro_rules! test_sync {
72 ($f:ident, $req:expr) => {
73 $crate::multitest!(vec![
74 $crate::SimpleTest::new_sync(stringify!($f), $req, $f).into()
75 ]);
76 };
77}
78
79#[macro_export]
85macro_rules! unstable_test_sync {
86 ($f:ident, $req:expr, $reason:expr) => {
87 $crate::multitest!(vec![
88 $crate::SimpleTest::new_sync(stringify!($f), $req, $f)
89 .unstable($reason)
90 .into()
91 ]);
92 };
93}
94
95#[macro_export]
98macro_rules! multitest {
99 ($tests:expr) => {
100 const _: () = {
101 use $crate::test_macro_support::linkme;
102 #[linkme::distributed_slice($crate::test_macro_support::TESTS)]
103 #[linkme(crate = linkme)]
104 static TEST: Option<fn() -> (&'static str, Vec<$crate::TestCase>)> =
105 Some(|| (module_path!(), $tests));
106 };
107 };
108}
109
110pub struct TestCase(Box<dyn DynRunTest>);
112
113impl TestCase {
114 pub fn new(test: impl 'static + RunTest) -> Self {
116 Self(Box::new(test))
117 }
118}
119
120impl<T: 'static + RunTest> From<T> for TestCase {
121 fn from(test: T) -> Self {
122 Self::new(test)
123 }
124}
125
126struct Test {
128 module: &'static str,
129 test: TestCase,
130 artifact_requirements: TestArtifactRequirements,
131}
132
133impl Test {
134 fn all() -> impl Iterator<Item = Self> {
136 TESTS.iter().flatten().flat_map(|f| {
137 let (module, tests) = f();
138 tests.into_iter().filter_map(move |test| {
139 let mut artifact_requirements = test.0.artifact_requirements()?;
140 artifact_requirements.require(
142 petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY,
143 RemoteAccess::LocalOnly,
144 false,
145 );
146 Some(Self {
147 module,
148 artifact_requirements,
149 test,
150 })
151 })
152 })
153 }
154
155 fn name(&self) -> String {
157 match self.module.split_once("::") {
159 Some((_crate_name, rest)) => format!("{}::{}", rest, self.test.0.leaf_name()),
160 None => self.test.0.leaf_name().to_owned(),
161 }
162 }
163
164 fn run(
165 &self,
166 resolve: fn(&str, TestArtifactRequirements) -> anyhow::Result<TestArtifacts>,
167 ) -> anyhow::Result<()> {
168 let name = self.name();
169 let artifacts = resolve(&name, self.artifact_requirements.clone())
170 .context("failed to resolve artifacts")?;
171 let output_dir = artifacts.get(petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY);
172 let logger = try_init_tracing(output_dir, tracing::level_filters::LevelFilter::DEBUG)
173 .context("failed to initialize tracing")?;
174 logger.log_test_start(&name);
177 let mut post_test_hooks = Vec::new();
178
179 #[cfg(windows)]
184 post_test_hooks.push(collect_watson_events_hook(logger.clone()));
185
186 let r = catch_unwind(AssertUnwindSafe(|| {
190 self.test.0.run(
191 PetriTestParams {
192 test_name: &name,
193 logger: &logger,
194 post_test_hooks: &mut post_test_hooks,
195 },
196 &artifacts,
197 )
198 }));
199 let r = r.unwrap_or_else(|err| {
200 let msg = err
203 .downcast_ref::<&str>()
204 .copied()
205 .or_else(|| err.downcast_ref::<String>().map(|x| x.as_str()));
206
207 let err = if let Some(msg) = msg {
208 anyhow::anyhow!("test panicked: {msg}")
209 } else {
210 anyhow::anyhow!("test panicked (unknown payload type)")
211 };
212 Err(err)
213 });
214 logger.log_test_result(&r, self.test.0.unstable().is_some());
215
216 for hook in post_test_hooks {
217 tracing::info!(name = hook.name(), "Running post-test hook");
218 if let Err(e) = hook.run(r.is_ok()) {
219 tracing::error!(
220 error = e.as_ref() as &dyn std::error::Error,
221 "Post-test hook failed"
222 );
223 } else {
224 tracing::info!("Post-test hook completed successfully");
225 }
226 }
227
228 r
229 }
230
231 fn trial(
233 self,
234 resolve: fn(&str, TestArtifactRequirements) -> anyhow::Result<TestArtifacts>,
235 ) -> libtest_mimic::Trial {
236 libtest_mimic::Trial::test(self.name(), move || {
237 let unstable = self.test.0.unstable();
238 match self.run(resolve) {
239 Ok(()) => Ok(()),
240 Err(err) => {
241 let Some(reason) = unstable else {
242 return Err(format!("{err:#}").into());
243 };
244 if std::env::var("PETRI_IGNORE_UNSTABLE_FAILURES")
245 .ok()
246 .is_some_and(|v| !v.is_empty() && v != "0")
247 {
248 tracing::warn!(reason, "ignoring unstable test failure: {err:#}");
249 return Ok(());
250 }
251 Err(format!("unstable test failed (reason: {reason}): {err:#}").into())
252 }
253 }
254 })
255 }
256}
257
258pub trait RunTest: Send {
262 type Artifacts;
264
265 fn leaf_name(&self) -> &str;
270 fn resolve(&self, resolver: ArtifactResolver<'_>) -> Option<Self::Artifacts>;
276 fn run(&self, params: PetriTestParams<'_>, artifacts: Self::Artifacts) -> anyhow::Result<()>;
279 fn host_requirements(&self) -> Option<&TestCaseRequirements>;
281 fn unstable(&self) -> Option<&str>;
283 fn ignored(&self) -> bool;
286}
287
288trait DynRunTest: Send {
289 fn leaf_name(&self) -> &str;
290 fn artifact_requirements(&self) -> Option<TestArtifactRequirements>;
291 fn run(&self, params: PetriTestParams<'_>, artifacts: &TestArtifacts) -> anyhow::Result<()>;
292 fn host_requirements(&self) -> Option<&TestCaseRequirements>;
293 fn unstable(&self) -> Option<&str>;
294 fn ignored(&self) -> bool;
295}
296
297impl<T: RunTest> DynRunTest for T {
298 fn leaf_name(&self) -> &str {
299 self.leaf_name()
300 }
301
302 fn artifact_requirements(&self) -> Option<TestArtifactRequirements> {
303 let mut requirements = TestArtifactRequirements::new();
304 self.resolve(ArtifactResolver::collector(&mut requirements))?;
305 Some(requirements)
306 }
307
308 fn run(&self, params: PetriTestParams<'_>, artifacts: &TestArtifacts) -> anyhow::Result<()> {
309 let artifacts = self
310 .resolve(ArtifactResolver::resolver(artifacts))
311 .context("test should have been skipped")?;
312 self.run(params, artifacts)
313 }
314
315 fn host_requirements(&self) -> Option<&TestCaseRequirements> {
316 self.host_requirements()
317 }
318
319 fn unstable(&self) -> Option<&str> {
320 self.unstable()
321 }
322
323 fn ignored(&self) -> bool {
324 self.ignored()
325 }
326}
327
328pub struct PetriTestParams<'a> {
330 pub test_name: &'a str,
332 pub logger: &'a PetriLogSource,
334 pub post_test_hooks: &'a mut Vec<PetriPostTestHook>,
336}
337
338pub struct PetriPostTestHook {
341 name: String,
343 hook: Box<dyn FnOnce(bool) -> anyhow::Result<()>>,
345}
346
347impl PetriPostTestHook {
348 pub fn new(name: String, hook: impl FnOnce(bool) -> anyhow::Result<()> + 'static) -> Self {
349 Self {
350 name,
351 hook: Box::new(hook),
352 }
353 }
354
355 pub fn name(&self) -> &str {
356 &self.name
357 }
358
359 pub fn run(self, test_passed: bool) -> anyhow::Result<()> {
360 (self.hook)(test_passed)
361 }
362}
363
364#[cfg(windows)]
368fn collect_watson_events_hook(logger: PetriLogSource) -> PetriPostTestHook {
369 let start_time = jiff::Timestamp::now();
370 PetriPostTestHook::new("collect watson events".into(), move |test_passed| {
371 if test_passed {
372 return Ok(());
373 }
374 let events =
375 futures::executor::block_on(crate::vm::hyperv::powershell::watson_events(&start_time));
376 if events.is_empty() {
377 return Ok(());
378 }
379 let log_file = logger.log_file("watson_events")?;
380 for event in events {
381 event.write_to(&log_file);
382 }
383 Ok(())
384 })
385}
386
387pub struct SimpleTest<A, F> {
389 leaf_name: &'static str,
390 resolve: A,
391 run: F,
392 pub host_requirements: Option<TestCaseRequirements>,
394 unstable: Option<&'static str>,
395 ignored: bool,
396 remote_policy: RemoteAccess,
397}
398
399impl<A, AR, F, E> SimpleTest<A, F>
400where
401 A: 'static + Send + Fn(&ArtifactResolver<'_>) -> Option<AR>,
402 F: 'static + Send + AsyncFn(PetriTestParams<'_>, DefaultDriver, AR) -> Result<(), E>,
403 E: Into<anyhow::Error>,
404{
405 pub fn new_async(
415 leaf_name: &'static str,
416 resolve: A,
417 run: F,
418 ) -> SimpleTest<A, impl 'static + Send + Fn(PetriTestParams<'_>, AR) -> Result<(), E>> {
419 SimpleTest::new_sync(leaf_name, resolve, move |params, artifacts| {
420 let mut pool = DefaultPool::named(std::thread::current().name().unwrap_or(leaf_name));
421 let driver = pool.driver();
422 let r = catch_unwind(AssertUnwindSafe(|| {
426 pool.run_until(
427 AssertUnwindSafe(run(params, driver.clone(), artifacts)).catch_unwind(),
428 )
429 }));
430 drop(driver);
433 pool.run();
434 match r.and_then(|r| r) {
435 Ok(r) => r,
436 Err(panic) => std::panic::resume_unwind(panic),
437 }
438 })
439 }
440}
441
442impl<A, AR, F, E> SimpleTest<A, F>
443where
444 A: 'static + Send + Fn(&ArtifactResolver<'_>) -> Option<AR>,
445 F: 'static + Send + Fn(PetriTestParams<'_>, AR) -> Result<(), E>,
446 E: Into<anyhow::Error>,
447{
448 pub fn new_sync(leaf_name: &'static str, resolve: A, run: F) -> Self {
461 SimpleTest {
462 leaf_name,
463 resolve,
464 run,
465 host_requirements: None,
466 unstable: None,
467 ignored: false,
468 remote_policy: RemoteAccess::LocalOnly,
469 }
470 }
471}
472
473impl<A, F> SimpleTest<A, F> {
474 pub fn requirements(mut self, requirements: TestCaseRequirements) -> Self {
476 self.host_requirements = Some(requirements);
477 self
478 }
479
480 pub fn unstable(mut self, reason: &'static str) -> Self {
487 self.unstable = Some(reason);
488 self
489 }
490
491 pub fn ignore(mut self) -> Self {
494 self.ignored = true;
495 self
496 }
497
498 pub fn remote_access(mut self, policy: RemoteAccess) -> Self {
500 self.remote_policy = policy;
501 self
502 }
503}
504
505impl<A, AR, F, E> RunTest for SimpleTest<A, F>
506where
507 A: 'static + Send + Fn(&ArtifactResolver<'_>) -> Option<AR>,
508 F: 'static + Send + Fn(PetriTestParams<'_>, AR) -> Result<(), E>,
509 E: Into<anyhow::Error>,
510{
511 type Artifacts = AR;
512
513 fn leaf_name(&self) -> &str {
514 self.leaf_name
515 }
516
517 fn resolve(&self, mut resolver: ArtifactResolver<'_>) -> Option<Self::Artifacts> {
518 resolver.set_remote_policy(self.remote_policy);
519 (self.resolve)(&resolver)
520 }
521
522 fn run(&self, params: PetriTestParams<'_>, artifacts: Self::Artifacts) -> anyhow::Result<()> {
523 (self.run)(params, artifacts).map_err(Into::into)
524 }
525
526 fn host_requirements(&self) -> Option<&TestCaseRequirements> {
527 self.host_requirements.as_ref()
528 }
529
530 fn unstable(&self) -> Option<&str> {
531 self.unstable
532 }
533
534 fn ignored(&self) -> bool {
535 self.ignored
536 }
537}
538
539#[derive(clap::Parser)]
540struct Options {
541 #[clap(long)]
544 list_required_artifacts: bool,
545 #[clap(long, requires = "list_required_artifacts")]
555 tests_from_stdin: bool,
556 #[clap(flatten)]
557 inner: libtest_mimic::Arguments,
558}
559
560pub fn test_main(
562 resolve: fn(&str, TestArtifactRequirements) -> anyhow::Result<TestArtifacts>,
563) -> ! {
564 let mut args = <Options as clap::Parser>::parse();
565 if args.list_required_artifacts {
566 use std::collections::BTreeSet;
567
568 let mut required_set = BTreeSet::new();
570 let mut optional_set = BTreeSet::new();
571
572 let stdin_tests: Option<BTreeSet<String>> = if args.tests_from_stdin {
574 use std::io::BufRead;
575 let stdin = std::io::stdin();
576 let tests: BTreeSet<String> = stdin
577 .lock()
578 .lines()
579 .map_while(Result::ok)
580 .filter(|line| !line.is_empty())
581 .collect();
582 if tests.is_empty() {
583 eprintln!("warning: no test names provided on stdin");
584 }
585 Some(tests)
586 } else {
587 None
588 };
589
590 for test in Test::all() {
591 let name = test.name();
592
593 let matches = match stdin_tests {
595 Some(ref stdin_tests) => stdin_tests.contains(&name),
596 None => true,
597 };
598
599 if matches {
600 for artifact in test.artifact_requirements.required_artifacts() {
601 required_set.insert(artifact.global_unique_id());
602 }
603 for artifact in test.artifact_requirements.optional_artifacts() {
604 optional_set.insert(artifact.global_unique_id());
605 }
606 }
607 }
608
609 let optional_set: BTreeSet<_> = optional_set.difference(&required_set).cloned().collect();
611
612 let output = petri_artifacts_core::ArtifactListOutput {
613 required: required_set.into_iter().collect(),
614 optional: optional_set.into_iter().collect(),
615 };
616
617 println!(
618 "{}",
619 serde_json::to_string(&output).expect("JSON serialization failed")
620 );
621 std::process::exit(0);
622 }
623
624 if !matches!(args.inner.test_threads, None | Some(1)) {
629 eprintln!("warning: ignoring value passed to --test-threads, using 1");
630 }
631 args.inner.test_threads = Some(1);
632
633 let host_context = futures::executor::block_on(HostContext::new());
635
636 let trials = Test::all()
637 .map(|test| {
638 let can_run = can_run_test_with_context(test.test.0.host_requirements(), &host_context);
639 let ignored = test.test.0.ignored();
640 test.trial(resolve).with_ignored_flag(!can_run || ignored)
641 })
642 .collect();
643
644 libtest_mimic::run(&args.inner, trials).exit();
645}