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,).into()
43 ]);
44 };
45}
46
47#[macro_export]
49macro_rules! multitest {
50 ($tests:expr) => {
51 const _: () = {
52 use $crate::test_macro_support::linkme;
53 #[linkme::distributed_slice($crate::test_macro_support::TESTS)]
54 #[linkme(crate = linkme)]
55 static TEST: Option<fn() -> (&'static str, Vec<$crate::TestCase>)> =
56 Some(|| (module_path!(), $tests));
57 };
58 };
59}
60
61pub struct TestCase(Box<dyn DynRunTest>);
63
64impl TestCase {
65 pub fn new(test: impl 'static + RunTest) -> Self {
67 Self(Box::new(test))
68 }
69}
70
71impl<T: 'static + RunTest> From<T> for TestCase {
72 fn from(test: T) -> Self {
73 Self::new(test)
74 }
75}
76
77struct Test {
79 module: &'static str,
80 test: TestCase,
81 artifact_requirements: TestArtifactRequirements,
82}
83
84impl Test {
85 fn all() -> impl Iterator<Item = Self> {
87 TESTS.iter().flatten().flat_map(|f| {
88 let (module, tests) = f();
89 tests.into_iter().filter_map(move |test| {
90 let mut artifact_requirements = test.0.artifact_requirements()?;
91 artifact_requirements
93 .require(petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY);
94 Some(Self {
95 module,
96 artifact_requirements,
97 test,
98 })
99 })
100 })
101 }
102
103 fn name(&self) -> String {
105 match self.module.split_once("::") {
107 Some((_crate_name, rest)) => format!("{}::{}", rest, self.test.0.leaf_name()),
108 None => self.test.0.leaf_name().to_owned(),
109 }
110 }
111
112 fn run(
113 &self,
114 resolve: fn(&str, TestArtifactRequirements) -> anyhow::Result<TestArtifacts>,
115 ) -> anyhow::Result<()> {
116 let name = self.name();
117 let artifacts = resolve(&name, self.artifact_requirements.clone())
118 .context("failed to resolve artifacts")?;
119 let output_dir = artifacts.get(petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY);
120 let logger = try_init_tracing(output_dir).context("failed to initialize tracing")?;
121
122 let r = catch_unwind(AssertUnwindSafe(|| {
126 self.test.0.run(
127 PetriTestParams {
128 test_name: &name,
129 logger: &logger,
130 },
131 &artifacts,
132 )
133 }));
134 let r = r.unwrap_or_else(|err| {
135 let msg = err
138 .downcast_ref::<&str>()
139 .copied()
140 .or_else(|| err.downcast_ref::<String>().map(|x| x.as_str()));
141
142 let err = if let Some(msg) = msg {
143 anyhow::anyhow!("test panicked: {msg}")
144 } else {
145 anyhow::anyhow!("test panicked (unknown payload type)")
146 };
147 Err(err)
148 });
149 logger.log_test_result(&name, &r);
150 r
151 }
152
153 fn trial(
155 self,
156 resolve: fn(&str, TestArtifactRequirements) -> anyhow::Result<TestArtifacts>,
157 ) -> libtest_mimic::Trial {
158 libtest_mimic::Trial::test(self.name(), move || {
159 self.run(resolve).map_err(|err| format!("{err:#}").into())
160 })
161 }
162}
163
164pub trait RunTest: Send {
168 type Artifacts;
170
171 fn leaf_name(&self) -> &str;
176 fn resolve(&self, resolver: &ArtifactResolver<'_>) -> Option<Self::Artifacts>;
182 fn run(&self, params: PetriTestParams<'_>, artifacts: Self::Artifacts) -> anyhow::Result<()>;
185 fn host_requirements(&self) -> Option<&TestCaseRequirements>;
187}
188
189trait DynRunTest: Send {
190 fn leaf_name(&self) -> &str;
191 fn artifact_requirements(&self) -> Option<TestArtifactRequirements>;
192 fn run(&self, params: PetriTestParams<'_>, artifacts: &TestArtifacts) -> anyhow::Result<()>;
193 fn host_requirements(&self) -> Option<&TestCaseRequirements>;
194}
195
196impl<T: RunTest> DynRunTest for T {
197 fn leaf_name(&self) -> &str {
198 self.leaf_name()
199 }
200
201 fn artifact_requirements(&self) -> Option<TestArtifactRequirements> {
202 let mut requirements = TestArtifactRequirements::new();
203 self.resolve(&ArtifactResolver::collector(&mut requirements))?;
204 Some(requirements)
205 }
206
207 fn run(&self, params: PetriTestParams<'_>, artifacts: &TestArtifacts) -> anyhow::Result<()> {
208 let artifacts = self
209 .resolve(&ArtifactResolver::resolver(artifacts))
210 .context("test should have been skipped")?;
211 self.run(params, artifacts)
212 }
213
214 fn host_requirements(&self) -> Option<&TestCaseRequirements> {
215 self.host_requirements()
216 }
217}
218
219pub struct PetriTestParams<'a> {
221 pub test_name: &'a str,
223 pub logger: &'a PetriLogSource,
225}
226
227pub struct SimpleTest<A, F> {
229 leaf_name: &'static str,
230 resolve: A,
231 run: F,
232 pub host_requirements: Option<TestCaseRequirements>,
234}
235
236impl<A, AR, F, E> SimpleTest<A, F>
237where
238 A: 'static + Send + Fn(&ArtifactResolver<'_>) -> Option<AR>,
239 F: 'static + Send + Fn(PetriTestParams<'_>, AR) -> Result<(), E>,
240 E: Into<anyhow::Error>,
241{
242 pub fn new(
245 leaf_name: &'static str,
246 resolve: A,
247 run: F,
248 host_requirements: Option<TestCaseRequirements>,
249 ) -> Self {
250 SimpleTest {
251 leaf_name,
252 resolve,
253 run,
254 host_requirements,
255 }
256 }
257}
258
259impl<A, AR, F, E> RunTest for SimpleTest<A, F>
260where
261 A: 'static + Send + Fn(&ArtifactResolver<'_>) -> Option<AR>,
262 F: 'static + Send + Fn(PetriTestParams<'_>, AR) -> Result<(), E>,
263 E: Into<anyhow::Error>,
264{
265 type Artifacts = AR;
266
267 fn leaf_name(&self) -> &str {
268 self.leaf_name
269 }
270
271 fn resolve(&self, resolver: &ArtifactResolver<'_>) -> Option<Self::Artifacts> {
272 (self.resolve)(resolver)
273 }
274
275 fn run(&self, params: PetriTestParams<'_>, artifacts: Self::Artifacts) -> anyhow::Result<()> {
276 (self.run)(params, artifacts).map_err(Into::into)
277 }
278
279 fn host_requirements(&self) -> Option<&TestCaseRequirements> {
280 self.host_requirements.as_ref()
281 }
282}
283
284#[derive(clap::Parser)]
285struct Options {
286 #[clap(long)]
288 list_required_artifacts: bool,
289 #[clap(flatten)]
290 inner: libtest_mimic::Arguments,
291}
292
293pub fn test_main(
295 resolve: fn(&str, TestArtifactRequirements) -> anyhow::Result<TestArtifacts>,
296) -> ! {
297 let mut args = <Options as clap::Parser>::parse();
298 if args.list_required_artifacts {
299 for test in Test::all() {
301 println!("{}:", test.name());
302 for artifact in test.artifact_requirements.required_artifacts() {
303 println!("required: {artifact:?}");
304 }
305 for artifact in test.artifact_requirements.optional_artifacts() {
306 println!("optional: {artifact:?}");
307 }
308 println!();
309 }
310 std::process::exit(0);
311 }
312
313 if !matches!(args.inner.test_threads, None | Some(1)) {
318 eprintln!("warning: ignoring value passed to --test-threads, using 1");
319 }
320 args.inner.test_threads = Some(1);
321
322 let host_context = futures::executor::block_on(HostContext::new());
324
325 let trials = Test::all()
326 .map(|test| {
327 let can_run = can_run_test_with_context(test.test.0.host_requirements(), &host_context);
328 test.trial(resolve).with_ignored_flag(!can_run)
329 })
330 .collect();
331
332 libtest_mimic::run(&args.inner, trials).exit();
333}