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)]
57 fn i_know_what_im_doing_with_this_manual_impl_instead_of_using_the_declare_artifacts_macro();
58}
59
60#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
63pub struct ArtifactHandle<A: ArtifactId>(PhantomData<A>);
64
65impl<A: ArtifactId + std::fmt::Debug> std::fmt::Debug for ArtifactHandle<A> {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 std::fmt::Debug::fmt(&self.erase(), f)
68 }
69}
70
71pub struct ResolvedArtifact<A = ()>(Option<PathBuf>, PhantomData<A>);
73
74impl<A> Clone for ResolvedArtifact<A> {
75 fn clone(&self) -> Self {
76 Self(self.0.clone(), self.1)
77 }
78}
79
80impl<A> std::fmt::Debug for ResolvedArtifact<A> {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.debug_tuple("ResolvedArtifact").field(&self.0).finish()
83 }
84}
85
86impl<A> ResolvedArtifact<A> {
87 pub fn erase(self) -> ResolvedArtifact {
89 ResolvedArtifact(self.0, PhantomData)
90 }
91
92 #[track_caller]
94 pub fn get(&self) -> &Path {
95 self.0
96 .as_ref()
97 .expect("cannot get path in requirements phase")
98 }
99}
100
101impl<A> From<ResolvedArtifact<A>> for PathBuf {
102 #[track_caller]
103 fn from(ra: ResolvedArtifact<A>) -> PathBuf {
104 ra.0.expect("cannot get path in requirements phase")
105 }
106}
107
108impl<A> AsRef<Path> for ResolvedArtifact<A> {
109 #[track_caller]
110 fn as_ref(&self) -> &Path {
111 self.get()
112 }
113}
114
115impl<A> AsRef<OsStr> for ResolvedArtifact<A> {
116 #[track_caller]
117 fn as_ref(&self) -> &OsStr {
118 self.get().as_ref()
119 }
120}
121
122#[derive(Clone, Debug)]
124pub struct ResolvedOptionalArtifact<A = ()>(OptionalArtifactState, PhantomData<A>);
125
126#[derive(Clone, Debug)]
127enum OptionalArtifactState {
128 Collecting,
129 Missing,
130 Present(PathBuf),
131}
132
133impl<A> ResolvedOptionalArtifact<A> {
134 pub fn erase(self) -> ResolvedOptionalArtifact {
136 ResolvedOptionalArtifact(self.0, PhantomData)
137 }
138
139 #[track_caller]
141 pub fn get(&self) -> Option<&Path> {
142 match self.0 {
143 OptionalArtifactState::Collecting => panic!("cannot get path in requirements phase"),
144 OptionalArtifactState::Missing => None,
145 OptionalArtifactState::Present(ref path) => Some(path),
146 }
147 }
148}
149
150pub struct ResolvedArtifactSource<A = ()>(Option<ArtifactSource>, PhantomData<A>);
152
153impl<A> Clone for ResolvedArtifactSource<A> {
154 fn clone(&self) -> Self {
155 Self(self.0.clone(), self.1)
156 }
157}
158
159impl<A> std::fmt::Debug for ResolvedArtifactSource<A> {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 f.debug_tuple("ResolvedArtifactSource")
162 .field(&self.0)
163 .finish()
164 }
165}
166
167impl<A> ResolvedArtifactSource<A> {
168 pub fn erase(self) -> ResolvedArtifactSource {
170 ResolvedArtifactSource(self.0, PhantomData)
171 }
172
173 #[track_caller]
175 pub fn get(&self) -> &ArtifactSource {
176 self.0
177 .as_ref()
178 .expect("cannot get source in requirements phase")
179 }
180}
181
182pub struct ArtifactResolver<'a> {
185 inner: ArtifactResolverInner<'a>,
186 remote_policy: RemoteAccess,
187}
188
189impl<'a> ArtifactResolver<'a> {
190 fn default_remote_policy() -> RemoteAccess {
196 match std::env::var("PETRI_REMOTE_ARTIFACTS").as_deref() {
197 Ok("0") | Ok("false") => RemoteAccess::LocalOnly,
198 _ => RemoteAccess::Allow,
199 }
200 }
201
202 pub fn collector(requirements: &'a mut TestArtifactRequirements) -> Self {
205 ArtifactResolver {
206 inner: ArtifactResolverInner::Collecting(RefCell::new(requirements)),
207 remote_policy: Self::default_remote_policy(),
208 }
209 }
210
211 pub fn resolver(artifacts: &'a TestArtifacts) -> Self {
213 ArtifactResolver {
214 inner: ArtifactResolverInner::Resolving(artifacts),
215 remote_policy: Self::default_remote_policy(),
216 }
217 }
218
219 fn effective_remote(&self, per_call: RemoteAccess) -> RemoteAccess {
222 if matches!(self.remote_policy, RemoteAccess::LocalOnly) {
223 RemoteAccess::LocalOnly
224 } else {
225 per_call
226 }
227 }
228
229 pub fn require<A: ArtifactId>(&self, handle: ArtifactHandle<A>) -> ResolvedArtifact<A> {
231 let source = self.require_source(handle, RemoteAccess::LocalOnly);
232 ResolvedArtifact(
233 source.0.map(|s| match s {
234 ArtifactSource::Local(p) => p,
235 ArtifactSource::Remote { url } => panic!(
236 "artifact required via require() resolved to remote source `{url}`; \
237 use require_source(..., RemoteAccess::Allow) or download the artifact locally"
238 ),
239 }),
240 PhantomData,
241 )
242 }
243
244 pub fn try_require<A: ArtifactId>(
246 &self,
247 handle: ArtifactHandle<A>,
248 ) -> ResolvedOptionalArtifact<A> {
249 match &self.inner {
250 ArtifactResolverInner::Collecting(requirements) => {
251 requirements.borrow_mut().try_require(handle.erase());
252 ResolvedOptionalArtifact(OptionalArtifactState::Collecting, PhantomData)
253 }
254 ArtifactResolverInner::Resolving(artifacts) => ResolvedOptionalArtifact(
255 artifacts
256 .try_get(handle)
257 .map_or(OptionalArtifactState::Missing, |p| {
258 OptionalArtifactState::Present(p.to_owned())
259 }),
260 PhantomData,
261 ),
262 }
263 }
264
265 pub fn require_source<A: ArtifactId>(
272 &self,
273 handle: ArtifactHandle<A>,
274 remote: RemoteAccess,
275 ) -> ResolvedArtifactSource<A> {
276 let effective = self.effective_remote(remote);
277 match &self.inner {
278 ArtifactResolverInner::Collecting(requirements) => {
279 requirements
280 .borrow_mut()
281 .require_source(handle.erase(), effective);
282 ResolvedArtifactSource(None, PhantomData)
283 }
284 ArtifactResolverInner::Resolving(artifacts) => {
285 ResolvedArtifactSource(Some(artifacts.get_source(handle).clone()), PhantomData)
286 }
287 }
288 }
289}
290
291enum ArtifactResolverInner<'a> {
292 Collecting(RefCell<&'a mut TestArtifactRequirements>),
293 Resolving(&'a TestArtifacts),
294}
295
296#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
299pub struct ErasedArtifactHandle {
300 artifact_id_str: &'static str,
301}
302
303impl std::fmt::Debug for ErasedArtifactHandle {
304 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305 write!(
310 f,
311 "{}",
312 self.artifact_id_str
313 .strip_suffix("__ty")
314 .unwrap_or(self.artifact_id_str)
315 )
316 }
317}
318
319impl<A: ArtifactId> PartialEq<ErasedArtifactHandle> for ArtifactHandle<A> {
320 fn eq(&self, other: &ErasedArtifactHandle) -> bool {
321 &self.erase() == other
322 }
323}
324
325impl<A: ArtifactId> PartialEq<ArtifactHandle<A>> for ErasedArtifactHandle {
326 fn eq(&self, other: &ArtifactHandle<A>) -> bool {
327 self == &other.erase()
328 }
329}
330
331impl<A: ArtifactId> ArtifactHandle<A> {
332 pub const fn new() -> Self {
335 Self(PhantomData)
336 }
337}
338
339pub trait AsArtifactHandle {
342 fn erase(&self) -> ErasedArtifactHandle;
344}
345
346impl AsArtifactHandle for ErasedArtifactHandle {
347 fn erase(&self) -> ErasedArtifactHandle {
348 *self
349 }
350}
351
352impl<A: ArtifactId> AsArtifactHandle for ArtifactHandle<A> {
353 fn erase(&self) -> ErasedArtifactHandle {
354 ErasedArtifactHandle {
355 artifact_id_str: A::GLOBAL_UNIQUE_ID,
356 }
357 }
358}
359
360#[macro_export]
362macro_rules! declare_artifacts {
363 (
364 $(
365 $(#[$doc:meta])*
366 $name:ident
367 ),*
368 $(,)?
369 ) => {
370 $(
371 $crate::paste::paste! {
372 $(#[$doc])*
373 #[expect(non_camel_case_types)]
374 pub const $name: $crate::ArtifactHandle<$name> = $crate::ArtifactHandle::new();
375
376 #[doc = concat!("Type-tag for [`", stringify!($name), "`]")]
377 #[expect(non_camel_case_types)]
378 #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
379 pub enum $name {}
380
381 #[expect(non_snake_case)]
382 mod [< $name __ty >] {
383 impl $crate::ArtifactId for super::$name {
384 const GLOBAL_UNIQUE_ID: &'static str = module_path!();
385 fn i_know_what_im_doing_with_this_manual_impl_instead_of_using_the_declare_artifacts_macro() {}
386 }
387 }
388 }
389 )*
390 };
391}
392
393pub trait ResolveTestArtifact {
399 fn resolve(&self, id: ErasedArtifactHandle) -> anyhow::Result<PathBuf>;
404
405 fn resolve_source(&self, id: ErasedArtifactHandle) -> anyhow::Result<ArtifactSource> {
412 self.resolve(id).map(ArtifactSource::Local)
413 }
414}
415
416impl<T: ResolveTestArtifact + ?Sized> ResolveTestArtifact for &T {
417 fn resolve(&self, id: ErasedArtifactHandle) -> anyhow::Result<PathBuf> {
418 (**self).resolve(id)
419 }
420
421 fn resolve_source(&self, id: ErasedArtifactHandle) -> anyhow::Result<ArtifactSource> {
422 (**self).resolve_source(id)
423 }
424}
425
426#[derive(Debug, Copy, Clone)]
428struct ArtifactRequirement {
429 optional: bool,
430 remote: RemoteAccess,
431}
432
433#[derive(Clone)]
435pub struct TestArtifactRequirements {
436 artifacts: Vec<(ErasedArtifactHandle, ArtifactRequirement)>,
437}
438
439impl TestArtifactRequirements {
440 pub fn new() -> Self {
442 TestArtifactRequirements {
443 artifacts: Vec::new(),
444 }
445 }
446
447 pub fn require(&mut self, dependency: impl AsArtifactHandle) -> &mut Self {
449 self.require_source(dependency, RemoteAccess::LocalOnly)
450 }
451
452 pub fn try_require(&mut self, dependency: impl AsArtifactHandle) -> &mut Self {
454 self.artifacts.push((
455 dependency.erase(),
456 ArtifactRequirement {
457 optional: true,
458 remote: RemoteAccess::LocalOnly,
459 },
460 ));
461 self
462 }
463
464 pub fn require_source(
466 &mut self,
467 dependency: impl AsArtifactHandle,
468 remote: RemoteAccess,
469 ) -> &mut Self {
470 self.artifacts.push((
471 dependency.erase(),
472 ArtifactRequirement {
473 optional: false,
474 remote,
475 },
476 ));
477 self
478 }
479
480 pub fn required_artifacts(&self) -> impl Iterator<Item = ErasedArtifactHandle> + '_ {
482 self.artifacts
483 .iter()
484 .filter_map(|&(a, req)| (!req.optional).then_some(a))
485 }
486
487 pub fn optional_artifacts(&self) -> impl Iterator<Item = ErasedArtifactHandle> + '_ {
489 self.artifacts
490 .iter()
491 .filter_map(|&(a, req)| req.optional.then_some(a))
492 }
493
494 pub fn resolve(&self, resolver: impl ResolveTestArtifact) -> anyhow::Result<TestArtifacts> {
501 let mut failed = String::new();
502 let mut resolved = HashMap::new();
503
504 let mut merged: HashMap<ErasedArtifactHandle, ArtifactRequirement> = HashMap::new();
508 for &(a, req) in &self.artifacts {
509 merged
510 .entry(a)
511 .and_modify(|existing| {
512 existing.optional = existing.optional && req.optional;
514 if matches!(req.remote, RemoteAccess::LocalOnly) {
516 existing.remote = RemoteAccess::LocalOnly;
517 }
518 })
519 .or_insert(req);
520 }
521
522 for (a, req) in merged {
523 let use_source = matches!(req.remote, RemoteAccess::Allow);
524
525 let result = if use_source {
526 resolver.resolve_source(a)
527 } else {
528 resolver.resolve(a).map(ArtifactSource::Local)
529 };
530
531 match result {
532 Ok(source) => {
533 resolved.insert(a, source);
534 }
535 Err(_) if req.optional => {}
536 Err(e) => failed.push_str(&format!("{:?} - {:#}\n", a, e)),
537 }
538 }
539
540 if !failed.is_empty() {
541 anyhow::bail!("Artifact resolution failed:\n{}", failed);
542 }
543
544 Ok(TestArtifacts {
545 artifacts: Arc::new(resolved),
546 })
547 }
548}
549
550#[derive(Clone)]
553pub struct TestArtifacts {
554 artifacts: Arc<HashMap<ErasedArtifactHandle, ArtifactSource>>,
555}
556
557impl TestArtifacts {
558 #[track_caller]
563 pub fn try_get(&self, artifact: impl AsArtifactHandle) -> Option<&Path> {
564 self.artifacts.get(&artifact.erase()).map(|source| {
565 match source {
566 ArtifactSource::Local(p) => p.as_path(),
567 ArtifactSource::Remote { .. } => panic!(
568 "Artifact {:?} is only available remotely; use require_source() or download it locally",
569 artifact.erase()
570 ),
571 }
572 })
573 }
574
575 #[track_caller]
579 pub fn get(&self, artifact: impl AsArtifactHandle) -> &Path {
580 self.try_get(artifact.erase())
581 .unwrap_or_else(|| panic!("Artifact not initially required: {:?}", artifact.erase()))
582 }
583
584 #[track_caller]
586 pub fn try_get_source(&self, artifact: impl AsArtifactHandle) -> Option<&ArtifactSource> {
587 self.artifacts.get(&artifact.erase())
588 }
589
590 #[track_caller]
592 pub fn get_source(&self, artifact: impl AsArtifactHandle) -> &ArtifactSource {
593 self.try_get_source(artifact.erase())
594 .unwrap_or_else(|| panic!("Artifact not initially required: {:?}", artifact.erase()))
595 }
596}