1#![forbid(unsafe_code)]
10
11use std::collections::HashMap;
12use std::path::PathBuf;
13use std::sync::Arc;
14
15#[doc(hidden)]
17pub use paste;
18use std::cell::RefCell;
19use std::ffi::OsStr;
20use std::marker::PhantomData;
21use std::path::Path;
22
23#[derive(Debug, Clone)]
25pub enum ArtifactSource {
26 Local(PathBuf),
28 Remote {
30 url: String,
32 },
33}
34
35#[derive(Debug, Copy, Clone, PartialEq, Eq)]
37pub enum RemoteAccess {
38 Allow,
40 LocalOnly,
42}
43
44pub trait ArtifactId: 'static {
50 #[doc(hidden)]
52 const GLOBAL_UNIQUE_ID: &'static str;
53
54 #[doc(hidden)]
56 const SUPPORTS_BLOB_DISK: bool;
57
58 #[doc(hidden)]
61 fn i_know_what_im_doing_with_this_manual_impl_instead_of_using_the_declare_artifacts_macro();
62}
63
64#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
67pub struct ArtifactHandle<A: ArtifactId>(PhantomData<A>);
68
69impl<A: ArtifactId + std::fmt::Debug> std::fmt::Debug for ArtifactHandle<A> {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 std::fmt::Debug::fmt(&self.erase(), f)
72 }
73}
74
75pub struct ResolvedArtifact<A = ()>(Option<PathBuf>, PhantomData<A>);
77
78impl<A> Clone for ResolvedArtifact<A> {
79 fn clone(&self) -> Self {
80 Self(self.0.clone(), self.1)
81 }
82}
83
84impl<A> std::fmt::Debug for ResolvedArtifact<A> {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 f.debug_tuple("ResolvedArtifact").field(&self.0).finish()
87 }
88}
89
90impl<A> ResolvedArtifact<A> {
91 pub fn erase(self) -> ResolvedArtifact {
93 ResolvedArtifact(self.0, PhantomData)
94 }
95
96 #[track_caller]
98 pub fn get(&self) -> &Path {
99 self.0
100 .as_ref()
101 .expect("cannot get path in requirements phase")
102 }
103}
104
105impl<A> From<ResolvedArtifact<A>> for PathBuf {
106 #[track_caller]
107 fn from(ra: ResolvedArtifact<A>) -> PathBuf {
108 ra.0.expect("cannot get path in requirements phase")
109 }
110}
111
112impl<A> AsRef<Path> for ResolvedArtifact<A> {
113 #[track_caller]
114 fn as_ref(&self) -> &Path {
115 self.get()
116 }
117}
118
119impl<A> AsRef<OsStr> for ResolvedArtifact<A> {
120 #[track_caller]
121 fn as_ref(&self) -> &OsStr {
122 self.get().as_ref()
123 }
124}
125
126#[derive(Clone, Debug)]
128pub struct ResolvedOptionalArtifact<A = ()>(OptionalArtifactState, PhantomData<A>);
129
130#[derive(Clone, Debug)]
131enum OptionalArtifactState {
132 Collecting,
133 Missing,
134 Present(PathBuf),
135}
136
137impl<A> ResolvedOptionalArtifact<A> {
138 pub fn erase(self) -> ResolvedOptionalArtifact {
140 ResolvedOptionalArtifact(self.0, PhantomData)
141 }
142
143 #[track_caller]
145 pub fn get(&self) -> Option<&Path> {
146 match self.0 {
147 OptionalArtifactState::Collecting => panic!("cannot get path in requirements phase"),
148 OptionalArtifactState::Missing => None,
149 OptionalArtifactState::Present(ref path) => Some(path),
150 }
151 }
152}
153
154pub struct ResolvedArtifactSource<A = ()>(Option<ArtifactSource>, PhantomData<A>);
156
157impl<A> Clone for ResolvedArtifactSource<A> {
158 fn clone(&self) -> Self {
159 Self(self.0.clone(), self.1)
160 }
161}
162
163impl<A> std::fmt::Debug for ResolvedArtifactSource<A> {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 f.debug_tuple("ResolvedArtifactSource")
166 .field(&self.0)
167 .finish()
168 }
169}
170
171impl<A> ResolvedArtifactSource<A> {
172 pub fn erase(self) -> ResolvedArtifactSource {
174 ResolvedArtifactSource(self.0, PhantomData)
175 }
176
177 #[track_caller]
179 pub fn get(&self) -> &ArtifactSource {
180 self.0
181 .as_ref()
182 .expect("cannot get source in requirements phase")
183 }
184}
185
186pub struct ArtifactResolver<'a> {
189 inner: ArtifactResolverInner<'a>,
190 remote_policy: RemoteAccess,
191}
192
193impl<'a> ArtifactResolver<'a> {
194 pub fn set_remote_policy(&mut self, test_policy: RemoteAccess) {
200 self.remote_policy = if matches!(test_policy, RemoteAccess::LocalOnly)
201 || matches!(
202 std::env::var("PETRI_REMOTE_ARTIFACTS").as_deref(),
203 Ok("0") | Ok("false")
204 ) {
205 RemoteAccess::LocalOnly
206 } else {
207 RemoteAccess::Allow
208 };
209 }
210
211 pub fn collector(requirements: &'a mut TestArtifactRequirements) -> Self {
214 ArtifactResolver {
215 inner: ArtifactResolverInner::Collecting(RefCell::new(requirements)),
216 remote_policy: RemoteAccess::LocalOnly,
217 }
218 }
219
220 pub fn resolver(artifacts: &'a TestArtifacts) -> Self {
222 ArtifactResolver {
223 inner: ArtifactResolverInner::Resolving(artifacts),
224 remote_policy: RemoteAccess::LocalOnly,
225 }
226 }
227
228 fn effective_remote<A: ArtifactId>(&self, per_call: RemoteAccess) -> RemoteAccess {
231 if matches!(self.remote_policy, RemoteAccess::LocalOnly) || !A::SUPPORTS_BLOB_DISK {
232 RemoteAccess::LocalOnly
233 } else {
234 per_call
235 }
236 }
237
238 pub fn require<A: ArtifactId>(&self, handle: ArtifactHandle<A>) -> ResolvedArtifact<A> {
240 let source = self.require_source(handle, RemoteAccess::LocalOnly);
241 ResolvedArtifact(
242 source.0.map(|s| match s {
243 ArtifactSource::Local(p) => p,
244 ArtifactSource::Remote { url } => panic!(
245 "artifact required via require() resolved to remote source `{url}`; \
246 use require_source(..., RemoteAccess::Allow) or download the artifact locally"
247 ),
248 }),
249 PhantomData,
250 )
251 }
252
253 pub fn try_require<A: ArtifactId>(
255 &self,
256 handle: ArtifactHandle<A>,
257 ) -> ResolvedOptionalArtifact<A> {
258 match &self.inner {
259 ArtifactResolverInner::Collecting(requirements) => {
260 requirements
261 .borrow_mut()
262 .require(handle.erase(), RemoteAccess::LocalOnly, true);
263 ResolvedOptionalArtifact(OptionalArtifactState::Collecting, PhantomData)
264 }
265 ArtifactResolverInner::Resolving(artifacts) => ResolvedOptionalArtifact(
266 artifacts
267 .try_get(handle)
268 .map_or(OptionalArtifactState::Missing, |p| {
269 OptionalArtifactState::Present(p.to_owned())
270 }),
271 PhantomData,
272 ),
273 }
274 }
275
276 pub fn require_source<A: ArtifactId>(
283 &self,
284 handle: ArtifactHandle<A>,
285 remote: RemoteAccess,
286 ) -> ResolvedArtifactSource<A> {
287 let effective = self.effective_remote::<A>(remote);
288 match &self.inner {
289 ArtifactResolverInner::Collecting(requirements) => {
290 requirements
291 .borrow_mut()
292 .require(handle.erase(), effective, false);
293 ResolvedArtifactSource(None, PhantomData)
294 }
295 ArtifactResolverInner::Resolving(artifacts) => {
296 ResolvedArtifactSource(Some(artifacts.get_source(handle).clone()), PhantomData)
297 }
298 }
299 }
300}
301
302enum ArtifactResolverInner<'a> {
303 Collecting(RefCell<&'a mut TestArtifactRequirements>),
304 Resolving(&'a TestArtifacts),
305}
306
307#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
310pub struct ErasedArtifactHandle {
311 artifact_id_str: &'static str,
312}
313
314impl ErasedArtifactHandle {
315 pub fn global_unique_id(&self) -> String {
317 self.artifact_id_str.to_string()
318 }
319}
320
321impl std::fmt::Debug for ErasedArtifactHandle {
322 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323 write!(
328 f,
329 "{}",
330 self.artifact_id_str
331 .strip_suffix("__ty")
332 .unwrap_or(self.artifact_id_str)
333 )
334 }
335}
336
337impl<A: ArtifactId> PartialEq<ErasedArtifactHandle> for ArtifactHandle<A> {
338 fn eq(&self, other: &ErasedArtifactHandle) -> bool {
339 &self.erase() == other
340 }
341}
342
343impl<A: ArtifactId> PartialEq<ArtifactHandle<A>> for ErasedArtifactHandle {
344 fn eq(&self, other: &ArtifactHandle<A>) -> bool {
345 self == &other.erase()
346 }
347}
348
349impl<A: ArtifactId> ArtifactHandle<A> {
350 pub const fn new() -> Self {
353 Self(PhantomData)
354 }
355}
356
357pub trait AsArtifactHandle {
360 fn erase(&self) -> ErasedArtifactHandle;
362}
363
364impl AsArtifactHandle for ErasedArtifactHandle {
365 fn erase(&self) -> ErasedArtifactHandle {
366 *self
367 }
368}
369
370impl<A: ArtifactId> AsArtifactHandle for ArtifactHandle<A> {
371 fn erase(&self) -> ErasedArtifactHandle {
372 ErasedArtifactHandle {
373 artifact_id_str: A::GLOBAL_UNIQUE_ID,
374 }
375 }
376}
377
378#[macro_export]
380macro_rules! declare_artifacts {
381 (
382 $(
383 $(#[$doc:meta])*
384 $name:ident
385 ),*
386 $(,)?
387 ) => {
388 $crate::declare_artifacts_inner!(
389 $(
390 $(#[$doc])*
391 $name(false),
392 )*
393 );
394 };
395}
396
397#[macro_export]
399macro_rules! declare_blob_artifacts {
400 (
401 $(
402 $(#[$doc:meta])*
403 $name:ident
404 ),*
405 $(,)?
406 ) => {
407 $crate::declare_artifacts_inner!(
408 $(
409 $(#[$doc])*
410 $name(true),
411 )*
412 );
413 };
414}
415
416#[macro_export]
419macro_rules! declare_artifacts_inner {
420 (
421 $(
422 $(#[$doc:meta])*
423 $name:ident($supports_blob_disk:literal)
424 ),*
425 $(,)?
426 ) => {
427 $(
428 $crate::paste::paste! {
429 $(#[$doc])*
430 #[expect(non_camel_case_types)]
431 pub const $name: $crate::ArtifactHandle<$name> = $crate::ArtifactHandle::new();
432
433 #[doc = concat!("Type-tag for [`", stringify!($name), "`]")]
434 #[expect(non_camel_case_types)]
435 #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
436 pub enum $name {}
437
438 #[expect(non_snake_case)]
439 mod [< $name __ty >] {
440 impl $crate::ArtifactId for super::$name {
441 const GLOBAL_UNIQUE_ID: &'static str = module_path!();
442 const SUPPORTS_BLOB_DISK: bool = $supports_blob_disk;
443 fn i_know_what_im_doing_with_this_manual_impl_instead_of_using_the_declare_artifacts_macro() {}
444 }
445 }
446 }
447 )*
448 };
449}
450
451pub trait ResolveTestArtifact {
457 fn resolve(&self, id: ErasedArtifactHandle) -> anyhow::Result<PathBuf>;
462
463 fn resolve_source(&self, id: ErasedArtifactHandle) -> anyhow::Result<ArtifactSource> {
470 self.resolve(id).map(ArtifactSource::Local)
471 }
472}
473
474impl<T: ResolveTestArtifact + ?Sized> ResolveTestArtifact for &T {
475 fn resolve(&self, id: ErasedArtifactHandle) -> anyhow::Result<PathBuf> {
476 (**self).resolve(id)
477 }
478
479 fn resolve_source(&self, id: ErasedArtifactHandle) -> anyhow::Result<ArtifactSource> {
480 (**self).resolve_source(id)
481 }
482}
483
484#[derive(Debug, Copy, Clone)]
486struct ArtifactRequirement {
487 optional: bool,
488 remote: RemoteAccess,
489}
490
491#[derive(Clone)]
493pub struct TestArtifactRequirements {
494 artifacts: Vec<(ErasedArtifactHandle, ArtifactRequirement)>,
495}
496
497impl TestArtifactRequirements {
498 pub fn new() -> Self {
500 TestArtifactRequirements {
501 artifacts: Vec::new(),
502 }
503 }
504
505 pub fn require(
507 &mut self,
508 dependency: impl AsArtifactHandle,
509 remote: RemoteAccess,
510 optional: bool,
511 ) -> &mut Self {
512 self.artifacts
513 .push((dependency.erase(), ArtifactRequirement { optional, remote }));
514 self
515 }
516
517 pub fn required_artifacts(&self) -> impl Iterator<Item = ErasedArtifactHandle> + '_ {
519 self.artifacts
520 .iter()
521 .filter_map(|&(a, req)| (!req.optional).then_some(a))
522 }
523
524 pub fn optional_artifacts(&self) -> impl Iterator<Item = ErasedArtifactHandle> + '_ {
526 self.artifacts
527 .iter()
528 .filter_map(|&(a, req)| req.optional.then_some(a))
529 }
530
531 pub fn resolve(&self, resolver: impl ResolveTestArtifact) -> anyhow::Result<TestArtifacts> {
538 let mut failed = String::new();
539 let mut resolved = HashMap::new();
540
541 let mut merged: HashMap<ErasedArtifactHandle, ArtifactRequirement> = HashMap::new();
545 for &(a, req) in &self.artifacts {
546 merged
547 .entry(a)
548 .and_modify(|existing| {
549 existing.optional = existing.optional && req.optional;
551 if matches!(req.remote, RemoteAccess::LocalOnly) {
553 existing.remote = RemoteAccess::LocalOnly;
554 }
555 })
556 .or_insert(req);
557 }
558
559 for (a, req) in merged {
560 let use_source = matches!(req.remote, RemoteAccess::Allow);
561
562 let result = if use_source {
563 resolver.resolve_source(a)
564 } else {
565 resolver.resolve(a).map(ArtifactSource::Local)
566 };
567
568 match result {
569 Ok(source) => {
570 resolved.insert(a, source);
571 }
572 Err(_) if req.optional => {}
573 Err(e) => failed.push_str(&format!("{:?} - {:#}\n", a, e)),
574 }
575 }
576
577 if !failed.is_empty() {
578 anyhow::bail!("Artifact resolution failed:\n{}", failed);
579 }
580
581 Ok(TestArtifacts {
582 artifacts: Arc::new(resolved),
583 })
584 }
585}
586
587#[derive(Clone)]
590pub struct TestArtifacts {
591 artifacts: Arc<HashMap<ErasedArtifactHandle, ArtifactSource>>,
592}
593
594impl TestArtifacts {
595 #[track_caller]
600 pub fn try_get(&self, artifact: impl AsArtifactHandle) -> Option<&Path> {
601 self.artifacts.get(&artifact.erase()).map(|source| {
602 match source {
603 ArtifactSource::Local(p) => p.as_path(),
604 ArtifactSource::Remote { .. } => panic!(
605 "Artifact {:?} is only available remotely; use require_source() or download it locally",
606 artifact.erase()
607 ),
608 }
609 })
610 }
611
612 #[track_caller]
616 pub fn get(&self, artifact: impl AsArtifactHandle) -> &Path {
617 self.try_get(artifact.erase())
618 .unwrap_or_else(|| panic!("Artifact not initially required: {:?}", artifact.erase()))
619 }
620
621 #[track_caller]
623 pub fn try_get_source(&self, artifact: impl AsArtifactHandle) -> Option<&ArtifactSource> {
624 self.artifacts.get(&artifact.erase())
625 }
626
627 #[track_caller]
629 pub fn get_source(&self, artifact: impl AsArtifactHandle) -> &ArtifactSource {
630 self.try_get_source(artifact.erase())
631 .unwrap_or_else(|| panic!("Artifact not initially required: {:?}", artifact.erase()))
632 }
633}
634
635#[derive(serde::Serialize, serde::Deserialize)]
637pub struct ArtifactListOutput {
638 pub required: Vec<String>,
640 pub optional: Vec<String>,
642}