1#[doc(hidden)]
7pub mod test_macro_support {
8 use super::TestCase;
9 pub use linkme;
10
11 #[linkme::distributed_slice]
12 pub static TESTS: [fn() -> (&'static str, Vec<TestCase>)];
13}
14
15use crate::PetriLogSource;
16use crate::TestArtifactRequirements;
17use crate::TestArtifacts;
18use crate::tracing::try_init_tracing;
19use anyhow::Context as _;
20use petri_artifacts_core::ArtifactResolver;
21use std::panic::AssertUnwindSafe;
22use std::panic::catch_unwind;
23use std::path::Path;
24use test_macro_support::TESTS;
25
26#[macro_export]
28macro_rules! test {
29 ($f:ident, $req:expr) => {
30 $crate::multitest!(vec![
31 $crate::SimpleTest::new(stringify!($f), $req, $f).into()
32 ]);
33 };
34}
35
36#[macro_export]
38macro_rules! multitest {
39 ($tests:expr) => {
40 const _: () = {
41 use $crate::test_macro_support::linkme;
42 #[linkme::distributed_slice($crate::test_macro_support::TESTS)]
43 #[linkme(crate = linkme)]
44 static TEST: fn() -> (&'static str, Vec<$crate::TestCase>) =
45 || (module_path!(), $tests);
46 };
47 };
48}
49
50pub struct TestCase(Box<dyn DynRunTest>);
52
53impl TestCase {
54 pub fn new(test: impl 'static + RunTest) -> Self {
56 Self(Box::new(test))
57 }
58}
59
60impl<T: 'static + RunTest> From<T> for TestCase {
61 fn from(test: T) -> Self {
62 Self::new(test)
63 }
64}
65
66struct Test {
68 module: &'static str,
69 test: TestCase,
70 requirements: TestArtifactRequirements,
71}
72
73impl Test {
74 fn all() -> impl Iterator<Item = Self> {
76 TESTS.iter().flat_map(|f| {
77 let (module, tests) = f();
78 tests.into_iter().filter_map(move |test| {
79 let mut requirements = test.0.requirements()?;
80 requirements.require(petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY);
82 Some(Self {
83 module,
84 requirements,
85 test,
86 })
87 })
88 })
89 }
90
91 fn name(&self) -> String {
93 match self.module.split_once("::") {
95 Some((_crate_name, rest)) => format!("{}::{}", rest, self.test.0.leaf_name()),
96 None => self.test.0.leaf_name().to_owned(),
97 }
98 }
99
100 fn run(
101 &self,
102 resolve: fn(&str, TestArtifactRequirements) -> anyhow::Result<TestArtifacts>,
103 ) -> anyhow::Result<()> {
104 let name = self.name();
105 let artifacts =
106 resolve(&name, self.requirements.clone()).context("failed to resolve artifacts")?;
107 let output_dir = artifacts.get(petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY);
108 let logger = try_init_tracing(output_dir).context("failed to initialize tracing")?;
109
110 let r = catch_unwind(AssertUnwindSafe(|| {
114 self.test.0.run(
115 PetriTestParams {
116 test_name: &name,
117 logger: &logger,
118 output_dir,
119 },
120 &artifacts,
121 )
122 }));
123 let r = r.unwrap_or_else(|err| {
124 let msg = err
127 .downcast_ref::<&str>()
128 .copied()
129 .or_else(|| err.downcast_ref::<String>().map(|x| x.as_str()));
130
131 let err = if let Some(msg) = msg {
132 anyhow::anyhow!("test panicked: {msg}")
133 } else {
134 anyhow::anyhow!("test panicked (unknown payload type)")
135 };
136 Err(err)
137 });
138 let result_path = match &r {
139 Ok(()) => {
140 tracing::info!("test passed");
141 "petri.passed"
142 }
143 Err(err) => {
144 tracing::error!(
145 error = err.as_ref() as &dyn std::error::Error,
146 "test failed"
147 );
148 "petri.failed"
149 }
150 };
151 fs_err::write(output_dir.join(result_path), &name).unwrap();
154 r
155 }
156
157 fn trial(
159 self,
160 resolve: fn(&str, TestArtifactRequirements) -> anyhow::Result<TestArtifacts>,
161 ) -> libtest_mimic::Trial {
162 libtest_mimic::Trial::test(self.name(), move || {
163 self.run(resolve).map_err(|err| format!("{err:#}").into())
164 })
165 }
166}
167
168pub trait RunTest: Send {
172 type Artifacts;
174
175 fn leaf_name(&self) -> &str;
180 fn resolve(&self, resolver: &ArtifactResolver<'_>) -> Option<Self::Artifacts>;
186 fn run(&self, params: PetriTestParams<'_>, artifacts: Self::Artifacts) -> anyhow::Result<()>;
189}
190
191trait DynRunTest: Send {
192 fn leaf_name(&self) -> &str;
193 fn requirements(&self) -> Option<TestArtifactRequirements>;
194 fn run(&self, params: PetriTestParams<'_>, artifacts: &TestArtifacts) -> anyhow::Result<()>;
195}
196
197impl<T: RunTest> DynRunTest for T {
198 fn leaf_name(&self) -> &str {
199 self.leaf_name()
200 }
201
202 fn requirements(&self) -> Option<TestArtifactRequirements> {
203 let mut requirements = TestArtifactRequirements::new();
204 self.resolve(&ArtifactResolver::collector(&mut requirements))?;
205 Some(requirements)
206 }
207
208 fn run(&self, params: PetriTestParams<'_>, artifacts: &TestArtifacts) -> anyhow::Result<()> {
209 let artifacts = self
210 .resolve(&ArtifactResolver::resolver(artifacts))
211 .context("test should have been skipped")?;
212 self.run(params, artifacts)
213 }
214}
215
216#[non_exhaustive]
218pub struct PetriTestParams<'a> {
219 pub test_name: &'a str,
221 pub logger: &'a PetriLogSource,
223 pub output_dir: &'a Path,
225}
226
227pub struct SimpleTest<A, F> {
229 leaf_name: &'static str,
230 resolve: A,
231 run: F,
232}
233
234impl<A, AR, F, E> SimpleTest<A, F>
235where
236 A: 'static + Send + Fn(&ArtifactResolver<'_>) -> Option<AR>,
237 F: 'static + Send + Fn(PetriTestParams<'_>, AR) -> Result<(), E>,
238 E: Into<anyhow::Error>,
239{
240 pub fn new(leaf_name: &'static str, resolve: A, run: F) -> Self {
243 SimpleTest {
244 leaf_name,
245 resolve,
246 run,
247 }
248 }
249}
250
251impl<A, AR, F, E> RunTest for SimpleTest<A, F>
252where
253 A: 'static + Send + Fn(&ArtifactResolver<'_>) -> Option<AR>,
254 F: 'static + Send + Fn(PetriTestParams<'_>, AR) -> Result<(), E>,
255 E: Into<anyhow::Error>,
256{
257 type Artifacts = AR;
258
259 fn leaf_name(&self) -> &str {
260 self.leaf_name
261 }
262
263 fn resolve(&self, resolver: &ArtifactResolver<'_>) -> Option<Self::Artifacts> {
264 (self.resolve)(resolver)
265 }
266
267 fn run(&self, params: PetriTestParams<'_>, artifacts: Self::Artifacts) -> anyhow::Result<()> {
268 (self.run)(params, artifacts).map_err(Into::into)
269 }
270}
271
272#[derive(clap::Parser)]
273struct Options {
274 #[clap(long)]
276 list_required_artifacts: bool,
277 #[clap(flatten)]
278 inner: libtest_mimic::Arguments,
279}
280
281pub fn test_main(
283 resolve: fn(&str, TestArtifactRequirements) -> anyhow::Result<TestArtifacts>,
284) -> ! {
285 let mut args = <Options as clap::Parser>::parse();
286 if args.list_required_artifacts {
287 for test in Test::all() {
289 println!("{}:", test.name());
290 for artifact in test.requirements.required_artifacts() {
291 println!("required: {artifact:?}");
292 }
293 for artifact in test.requirements.optional_artifacts() {
294 println!("optional: {artifact:?}");
295 }
296 println!();
297 }
298 std::process::exit(0);
299 }
300
301 if !matches!(args.inner.test_threads, None | Some(1)) {
306 eprintln!("warning: ignoring value passed to --test-threads, using 1");
307 }
308 args.inner.test_threads = Some(1);
309
310 let trials = Test::all().map(|test| test.trial(resolve)).collect();
311 libtest_mimic::run(&args.inner, trials).exit()
312}