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 petri_artifacts_core::ArtifactResolver;
33use std::panic::AssertUnwindSafe;
34use std::panic::catch_unwind;
35use test_macro_support::TESTS;
36
37#[macro_export]
39macro_rules! test {
40 ($f:ident, $req:expr) => {
41 $crate::multitest!(vec![
42 $crate::SimpleTest::new(stringify!($f), $req, $f, None, false).into()
43 ]);
44 };
45}
46
47#[macro_export]
49macro_rules! unstable_test {
50 ($f:ident, $req:expr) => {
51 $crate::multitest!(vec![
52 $crate::SimpleTest::new(stringify!($f), $req, $f, None, true).into()
53 ]);
54 };
55}
56
57#[macro_export]
59macro_rules! multitest {
60 ($tests:expr) => {
61 const _: () = {
62 use $crate::test_macro_support::linkme;
63 #[linkme::distributed_slice($crate::test_macro_support::TESTS)]
64 #[linkme(crate = linkme)]
65 static TEST: Option<fn() -> (&'static str, Vec<$crate::TestCase>)> =
66 Some(|| (module_path!(), $tests));
67 };
68 };
69}
70
71pub struct TestCase(Box<dyn DynRunTest>);
73
74impl TestCase {
75 pub fn new(test: impl 'static + RunTest) -> Self {
77 Self(Box::new(test))
78 }
79}
80
81impl<T: 'static + RunTest> From<T> for TestCase {
82 fn from(test: T) -> Self {
83 Self::new(test)
84 }
85}
86
87struct Test {
89 module: &'static str,
90 test: TestCase,
91 artifact_requirements: TestArtifactRequirements,
92}
93
94impl Test {
95 fn all() -> impl Iterator<Item = Self> {
97 TESTS.iter().flatten().flat_map(|f| {
98 let (module, tests) = f();
99 tests.into_iter().filter_map(move |test| {
100 let mut artifact_requirements = test.0.artifact_requirements()?;
101 artifact_requirements
103 .require(petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY);
104 Some(Self {
105 module,
106 artifact_requirements,
107 test,
108 })
109 })
110 })
111 }
112
113 fn name(&self) -> String {
115 match self.module.split_once("::") {
117 Some((_crate_name, rest)) => format!("{}::{}", rest, self.test.0.leaf_name()),
118 None => self.test.0.leaf_name().to_owned(),
119 }
120 }
121
122 fn run(
123 &self,
124 resolve: fn(&str, TestArtifactRequirements) -> anyhow::Result<TestArtifacts>,
125 ) -> anyhow::Result<()> {
126 let name = self.name();
127 let artifacts = resolve(&name, self.artifact_requirements.clone())
128 .context("failed to resolve artifacts")?;
129 let output_dir = artifacts.get(petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY);
130 let logger = try_init_tracing(output_dir, tracing::level_filters::LevelFilter::DEBUG)
131 .context("failed to initialize tracing")?;
132 let mut post_test_hooks = Vec::new();
133
134 let r = catch_unwind(AssertUnwindSafe(|| {
138 self.test.0.run(
139 PetriTestParams {
140 test_name: &name,
141 logger: &logger,
142 post_test_hooks: &mut post_test_hooks,
143 },
144 &artifacts,
145 )
146 }));
147 let r = r.unwrap_or_else(|err| {
148 let msg = err
151 .downcast_ref::<&str>()
152 .copied()
153 .or_else(|| err.downcast_ref::<String>().map(|x| x.as_str()));
154
155 let err = if let Some(msg) = msg {
156 anyhow::anyhow!("test panicked: {msg}")
157 } else {
158 anyhow::anyhow!("test panicked (unknown payload type)")
159 };
160 Err(err)
161 });
162 logger.log_test_result(&name, &r, self.test.0.unstable());
163
164 for hook in post_test_hooks {
165 tracing::info!(name = hook.name(), "Running post-test hook");
166 if let Err(e) = hook.run(r.is_ok()) {
167 tracing::error!(
168 error = e.as_ref() as &dyn std::error::Error,
169 "Post-test hook failed"
170 );
171 } else {
172 tracing::info!("Post-test hook completed successfully");
173 }
174 }
175
176 r
177 }
178
179 fn trial(
181 self,
182 resolve: fn(&str, TestArtifactRequirements) -> anyhow::Result<TestArtifacts>,
183 ) -> libtest_mimic::Trial {
184 libtest_mimic::Trial::test(self.name(), move || match self.run(resolve) {
185 Ok(()) => Ok(()),
186 Err(err)
187 if self.test.0.unstable()
188 && std::env::var("PETRI_REPORT_UNSTABLE_FAIL")
189 .ok()
190 .is_none_or(|v| v.is_empty() || v == "0") =>
191 {
192 tracing::warn!("ignoring unstable test failure: {err:#}");
193 Ok(())
194 }
195 Err(err) => Err(format!("{err:#}").into()),
196 })
197 }
198}
199
200pub trait RunTest: Send {
204 type Artifacts;
206
207 fn leaf_name(&self) -> &str;
212 fn resolve(&self, resolver: &ArtifactResolver<'_>) -> Option<Self::Artifacts>;
218 fn run(&self, params: PetriTestParams<'_>, artifacts: Self::Artifacts) -> anyhow::Result<()>;
221 fn host_requirements(&self) -> Option<&TestCaseRequirements>;
223 fn unstable(&self) -> bool;
225}
226
227trait DynRunTest: Send {
228 fn leaf_name(&self) -> &str;
229 fn artifact_requirements(&self) -> Option<TestArtifactRequirements>;
230 fn run(&self, params: PetriTestParams<'_>, artifacts: &TestArtifacts) -> anyhow::Result<()>;
231 fn host_requirements(&self) -> Option<&TestCaseRequirements>;
232 fn unstable(&self) -> bool;
233}
234
235impl<T: RunTest> DynRunTest for T {
236 fn leaf_name(&self) -> &str {
237 self.leaf_name()
238 }
239
240 fn artifact_requirements(&self) -> Option<TestArtifactRequirements> {
241 let mut requirements = TestArtifactRequirements::new();
242 self.resolve(&ArtifactResolver::collector(&mut requirements))?;
243 Some(requirements)
244 }
245
246 fn run(&self, params: PetriTestParams<'_>, artifacts: &TestArtifacts) -> anyhow::Result<()> {
247 let artifacts = self
248 .resolve(&ArtifactResolver::resolver(artifacts))
249 .context("test should have been skipped")?;
250 self.run(params, artifacts)
251 }
252
253 fn host_requirements(&self) -> Option<&TestCaseRequirements> {
254 self.host_requirements()
255 }
256
257 fn unstable(&self) -> bool {
258 self.unstable()
259 }
260}
261
262pub struct PetriTestParams<'a> {
264 pub test_name: &'a str,
266 pub logger: &'a PetriLogSource,
268 pub post_test_hooks: &'a mut Vec<PetriPostTestHook>,
270}
271
272pub struct PetriPostTestHook {
275 name: String,
277 hook: Box<dyn FnOnce(bool) -> anyhow::Result<()>>,
279}
280
281impl PetriPostTestHook {
282 pub fn new(name: String, hook: impl FnOnce(bool) -> anyhow::Result<()> + 'static) -> Self {
283 Self {
284 name,
285 hook: Box::new(hook),
286 }
287 }
288
289 pub fn name(&self) -> &str {
290 &self.name
291 }
292
293 pub fn run(self, test_passed: bool) -> anyhow::Result<()> {
294 (self.hook)(test_passed)
295 }
296}
297
298pub struct SimpleTest<A, F> {
300 leaf_name: &'static str,
301 resolve: A,
302 run: F,
303 pub host_requirements: Option<TestCaseRequirements>,
305 unstable: bool,
306}
307
308impl<A, AR, F, E> SimpleTest<A, F>
309where
310 A: 'static + Send + Fn(&ArtifactResolver<'_>) -> Option<AR>,
311 F: 'static + Send + Fn(PetriTestParams<'_>, AR) -> Result<(), E>,
312 E: Into<anyhow::Error>,
313{
314 pub fn new(
317 leaf_name: &'static str,
318 resolve: A,
319 run: F,
320 host_requirements: Option<TestCaseRequirements>,
321 unstable: bool,
322 ) -> Self {
323 SimpleTest {
324 leaf_name,
325 resolve,
326 run,
327 host_requirements,
328 unstable,
329 }
330 }
331}
332
333impl<A, AR, F, E> RunTest for SimpleTest<A, F>
334where
335 A: 'static + Send + Fn(&ArtifactResolver<'_>) -> Option<AR>,
336 F: 'static + Send + Fn(PetriTestParams<'_>, AR) -> Result<(), E>,
337 E: Into<anyhow::Error>,
338{
339 type Artifacts = AR;
340
341 fn leaf_name(&self) -> &str {
342 self.leaf_name
343 }
344
345 fn resolve(&self, resolver: &ArtifactResolver<'_>) -> Option<Self::Artifacts> {
346 (self.resolve)(resolver)
347 }
348
349 fn run(&self, params: PetriTestParams<'_>, artifacts: Self::Artifacts) -> anyhow::Result<()> {
350 (self.run)(params, artifacts).map_err(Into::into)
351 }
352
353 fn host_requirements(&self) -> Option<&TestCaseRequirements> {
354 self.host_requirements.as_ref()
355 }
356
357 fn unstable(&self) -> bool {
358 self.unstable
359 }
360}
361
362#[derive(clap::Parser)]
363struct Options {
364 #[clap(long)]
366 list_required_artifacts: bool,
367 #[clap(flatten)]
368 inner: libtest_mimic::Arguments,
369}
370
371pub fn test_main(
373 resolve: fn(&str, TestArtifactRequirements) -> anyhow::Result<TestArtifacts>,
374) -> ! {
375 let mut args = <Options as clap::Parser>::parse();
376 if args.list_required_artifacts {
377 for test in Test::all() {
379 println!("{}:", test.name());
380 for artifact in test.artifact_requirements.required_artifacts() {
381 println!("required: {artifact:?}");
382 }
383 for artifact in test.artifact_requirements.optional_artifacts() {
384 println!("optional: {artifact:?}");
385 }
386 println!();
387 }
388 std::process::exit(0);
389 }
390
391 if !matches!(args.inner.test_threads, None | Some(1)) {
396 eprintln!("warning: ignoring value passed to --test-threads, using 1");
397 }
398 args.inner.test_threads = Some(1);
399
400 let host_context = futures::executor::block_on(HostContext::new());
402
403 let trials = Test::all()
404 .map(|test| {
405 let can_run = can_run_test_with_context(test.test.0.host_requirements(), &host_context);
406 test.trial(resolve).with_ignored_flag(!can_run)
407 })
408 .collect();
409
410 libtest_mimic::run(&args.inner, trials).exit();
411}