1use std::collections::BTreeMap;
2use std::hash::Hash;
3use std::borrow::Cow;
4use std::str::FromStr;
5use std::path::{Path, PathBuf, MAIN_SEPARATOR};
6
7use wax::{Glob, Pattern};
8
9use crate::config::Env;
10use crate::metadata::format::{AssetsOptions, AssetsRules, RuleValue};
11
12use super::resolver::*;
13
14
15pub fn build_plan<'l, 'r, S>(env: &Env,
17 assets: &AssetsRules<S>,
18 options: &AssetsOptions,
19 crate_root: Option<Cow<'_, Path>>)
20 -> Result<BuildPlan<'l, 'r>, super::Error>
21 where S: Eq + Hash + ToString + AsRef<str>
22{
23 let mut map_unresolved = Vec::new();
31 let mut include_unresolved = Vec::new();
32 let mut exclude_exprs = Vec::new();
33
34 const PATH_SEPARATOR: [char; 2] = [MAIN_SEPARATOR, '/'];
35
36 let enver = EnvResolver::with_cache();
37 let crate_root = crate_root.unwrap_or_else(|| env.cargo_manifest_dir().into());
38 let link_behavior = options.link_behavior();
39
40 let to_relative = |s: &S| -> String {
41 let s = s.to_string();
42 let p = Path::new(&s);
43 if p.is_absolute() || p.has_root() {
44 let trailing_sep = p.components().count() > 1 && s.ends_with(PATH_SEPARATOR);
45 let mut s = p.components().skip(1).collect::<PathBuf>().display().to_string();
46 if trailing_sep && !s.ends_with(PATH_SEPARATOR) {
48 s.push(MAIN_SEPARATOR);
49 }
50 sanitize_path_pattern(&s).into_owned()
51 } else {
52 s.to_owned()
53 }
54 };
55
56
57 let assets_requirements = assets.env_required();
58 let compile_target_agnostic = !assets_requirements.contains(&"TARGET");
59 log::debug!("assets required env vars: {assets_requirements:?}");
60 log::debug!("compile-target-agnostic: {compile_target_agnostic}");
61
62
63 match assets {
64 AssetsRules::List(vec) => {
65 include_unresolved.extend(
66 vec.iter()
67 .map(to_relative)
68 .map(Expr::from)
69 .map(|e| enver.expr(e, env)),
70 )
71 },
72 AssetsRules::Map(map) => {
73 for (k, v) in map {
74 let k = to_relative(k);
75 match v {
76 RuleValue::Boolean(v) => {
77 match v {
78 true => include_unresolved.push(enver.expr(Expr::from(k), env)),
79 false => exclude_exprs.push(enver.expr(Expr::from(k), env)),
80 }
81 },
82 RuleValue::String(from) => {
83 map_unresolved.push((enver.expr(Expr::from(k), env), enver.expr(Expr::from(from), env)))
84 },
85 }
86 }
87 },
88 }
89
90
91 let exclude_globs: Vec<_> =
94 exclude_exprs.iter()
95 .filter_map(|expr| {
96 Glob::from_str(expr.as_str()).map_err(|err| error!("invalid filter expression: {err}"))
97 .ok()
98 })
99 .collect();
100
101
102 let mut mappings = Vec::new();
104 for (k, v) in map_unresolved.into_iter() {
105 let key = PathBuf::from(k.as_str());
106 let value = Cow::Borrowed(v.as_str());
107 let into_dir = k.as_str().ends_with(PATH_SEPARATOR);
108 let source_exists = abs_if_existing(Path::new(value.as_ref()), &crate_root)?.is_some();
109
110 let mapping = match (source_exists, into_dir) {
111 (true, true) => Mapping::Into(Match::new(value.as_ref(), key), (k, v)),
112 (true, false) => Mapping::AsIs(Match::new(value.as_ref(), key), (k, v)),
113 (false, _) => {
114 let mut resolved = resolve_includes(value, &crate_root, &exclude_exprs, link_behavior)?;
115
116 debug!("Possible ManyInto, resolved: {}", resolved.len());
117
118 let _excluded: Vec<_> = resolved.extract_if(.., |inc| {
120 let path = key.join(inc.target());
121 glob_matches_any(&path, &exclude_globs)
122 })
123 .collect();
124
125
126 Mapping::ManyInto { sources: resolved,
127 target: (&k).into(),
128 exprs: (k, v),
129 #[cfg(feature = "assets-report")]
130 excluded: _excluded }
131 },
132 };
133
134 mappings.push(mapping);
135 }
136
137
138 for mapping in mappings.iter_mut() {
140 let possible = match &mapping {
141 Mapping::AsIs(inc, ..) => inc.source().is_dir() && possibly_matching_any(&inc.target(), &exclude_exprs),
142 Mapping::Into(inc, ..) => inc.source().is_dir() && possibly_matching_any(&inc.target(), &exclude_exprs),
143 Mapping::ManyInto { .. } => false,
144 };
145
146 if possible {
147 let (source_root, target, exprs) = match &mapping {
148 Mapping::AsIs(inc, expr) => {
152 (inc.source(), inc.target(), expr)
155 },
156 Mapping::Into(inc, expr) => {
157 let source = inc.source();
160 let target = inc.target();
161 let target_base = target.join(source.file_name().expect("source filename"));
162 (source, Cow::from(target_base), expr)
163 },
164 Mapping::ManyInto { .. } => unreachable!(),
165 };
166
167 let mut resolved = resolve_includes("**/*", &source_root, &exclude_exprs, link_behavior)?;
169
170 let is_not_empty = |inc: &Match| !inc.target().as_os_str().is_empty();
172 let excluded: Vec<_> = resolved.extract_if(.., |inc| {
173 let target = target.join(inc.target());
174 !is_not_empty(inc) ||
175 glob_matches_any(&inc.source(), &exclude_globs) ||
176 glob_matches_any(&target, &exclude_globs)
177 })
178 .collect();
179
180 if excluded.is_empty() {
182 continue;
183 }
184
185 *mapping = Mapping::ManyInto { sources: resolved,
186 target: target.into(),
187 exprs: exprs.to_owned(),
188 #[cfg(feature = "assets-report")]
189 excluded };
190 }
191 }
192
193
194 for k in include_unresolved {
195 let resolved = resolve_includes(&k, &crate_root, &exclude_exprs, link_behavior)?;
196 mappings.extend(resolved.into_iter()
197 .map(|inc| Mapping::AsIs(inc, (k.clone(), "true".into()))));
198 }
199
200
201 mappings.dedup_by(|a, b| a.eq_ignore_expr(b));
203
204 let vars = enver.into_cache().unwrap_or_default();
209
210 Ok(BuildPlan { plan: mappings,
211 crate_root: crate_root.to_path_buf(),
212 compile_target_agnostic,
213 vars })
214}
215
216
217pub fn abs_if_existing<'t, P1, P2>(path: P1, root: P2) -> std::io::Result<Option<Cow<'t, Path>>>
222 where P1: 't + AsRef<Path> + Into<Cow<'t, Path>>,
223 P2: AsRef<Path> {
224 let p = if path.as_ref().is_absolute() && path.as_ref().try_exists()? {
225 Some(path.into())
226 } else {
227 let abs = root.as_ref().join(path);
228 if abs.try_exists()? {
229 Some(Cow::Owned(abs))
230 } else {
231 None
232 }
233 };
234 Ok(p)
235}
236
237#[inline]
239pub fn abs_if_existing_any<'t, P1, P2>(path: P1, root: P2) -> Cow<'t, Path>
240 where P1: 't + AsRef<Path> + Into<Cow<'t, Path>> + Clone,
241 P2: AsRef<Path> {
242 abs_if_existing(path.clone(), root).ok()
243 .flatten()
244 .unwrap_or(path.into())
245}
246
247
248fn glob_matches_any<'a, I: IntoIterator<Item = &'a Glob<'a>>>(path: &Path, exprs: I) -> bool {
249 exprs.into_iter().any(|glob| glob.is_match(path))
250}
251
252
253fn possibly_matching_any<P: Into<PathBuf>, I: IntoIterator<Item = P>>(path: &Path, exprs: I) -> bool {
260 exprs.into_iter().any(|expr| possibly_matching(path, expr))
261}
262
263
264fn possibly_matching<P: Into<PathBuf>>(path: &Path, expr: P) -> bool {
267 let len = path.components().count();
270 let filter: PathBuf = expr.into()
271 .components()
272 .enumerate() .filter(|(i, _)| *i < len)
274 .map(|(_, p)| p)
275 .collect();
276
277 let glob = Glob::new(filter.as_os_str().to_str().unwrap()).unwrap();
278 glob.is_match(path)
279}
280
281
282#[derive(Debug, PartialEq, Eq, Hash)]
284#[cfg_attr(feature = "serde", derive(serde::Serialize))]
285pub struct BuildPlan<'left, 'right> {
286 plan: Vec<Mapping<'left, 'right>>,
288 crate_root: PathBuf,
290
291 compile_target_agnostic: bool,
293 vars: BTreeMap<String, String>,
295}
296
297impl<'left, 'right> BuildPlan<'left, 'right> {
298 pub fn into_inner(self) -> Vec<Mapping<'left, 'right>> { self.plan }
299 pub fn as_inner(&self) -> &[Mapping<'left, 'right>] { &self.plan[..] }
300 pub fn into_parts(self) -> (Vec<Mapping<'left, 'right>>, PathBuf) { (self.plan, self.crate_root) }
301
302 pub fn crate_root(&self) -> &Path { &self.crate_root }
303 pub fn set_crate_root<T: Into<PathBuf>>(&mut self, path: T) -> PathBuf {
304 let old = std::mem::replace(&mut self.crate_root, path.into());
305 old
306 }
307
308 pub fn compile_target_agnostic(&self) -> bool { self.compile_target_agnostic }
309 pub fn used_env_vars(&self) -> &BTreeMap<String, String> { &self.vars }
310}
311
312impl<'left, 'right> AsRef<[Mapping<'left, 'right>]> for BuildPlan<'left, 'right> {
313 fn as_ref(&self) -> &[Mapping<'left, 'right>] { &self.plan[..] }
314}
315
316
317impl std::fmt::Display for BuildPlan<'_, '_> {
318 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319 let align = |f: &mut std::fmt::Formatter<'_>| -> std::fmt::Result {
320 if !matches!(f.align(), Some(std::fmt::Alignment::Right)) {
321 return Ok(());
322 }
323
324 let c = f.fill();
325 if let Some(width) = f.width() {
326 for _ in 0..width {
327 write!(f, "{c}")?;
328 }
329 Ok(())
330 } else {
331 write!(f, "{c}")
332 }
333 };
334
335
336 let print = |f: &mut std::fmt::Formatter<'_>,
337 inc: &Match,
338 (left, right): &(Expr, Expr),
339 br: bool|
340 -> std::fmt::Result {
341 let target = inc.target();
342 let source = inc.source();
343 let left = left.original();
344 let right = right.original();
345 align(f)?;
346 write!(f, "{target:#?} <- {source:#?} ({left} = {right})")?;
347 if br { writeln!(f) } else { Ok(()) }
348 };
349
350 let items = self.as_inner();
351 let len = items.len();
352 for (i, item) in items.into_iter().enumerate() {
353 let last = i == len - 1;
354 match item {
355 Mapping::AsIs(inc, exprs) => print(f, inc, exprs, !last)?,
356 Mapping::Into(inc, exprs) => print(f, inc, exprs, !last)?,
357 Mapping::ManyInto { sources,
358 target,
359 exprs,
360 .. } => {
361 let len = sources.len();
362 for (i_in, inc) in sources.iter().enumerate() {
363 let last = last && i_in == len - 1;
364 let m = Match::new(inc.source(), target.join(inc.target()));
365 print(f, &m, exprs, !last)?;
366 }
367 },
368 }
369 }
370
371 Ok(())
372 }
373}
374
375impl BuildPlan<'_, '_> {
376 pub fn targets(&self) -> impl Iterator<Item = Cow<'_, Path>> {
377 self.as_inner().iter().flat_map(|mapping| {
378 match mapping {
379 Mapping::AsIs(inc, ..) => vec![inc.target()].into_iter(),
380 Mapping::Into(inc, ..) => vec![inc.target()].into_iter(),
381 Mapping::ManyInto { sources, target, .. } => {
382 sources.iter()
383 .map(|inc| Cow::from(target.join(inc.target())))
384 .collect::<Vec<_>>()
385 .into_iter()
386 },
387 }
388 })
389 }
390
391 pub fn iter_flatten(&self) -> impl Iterator<Item = (MappingKind, PathBuf, PathBuf)> + '_ {
392 let pair = |inc: &Match| {
393 (inc.target().to_path_buf(), abs_if_existing_any(inc.source(), &self.crate_root).to_path_buf())
394 };
395
396 self.as_inner().iter().flat_map(move |mapping| {
397 let mut rows = Vec::new();
398 let kind = mapping.kind();
399 match mapping {
400 Mapping::AsIs(inc, _) | Mapping::Into(inc, _) => rows.push(pair(inc)),
401 Mapping::ManyInto { sources, target, .. } => {
402 rows.extend(sources.iter().map(|inc| {
403 pair(&Match::new(inc.source(), target.join(inc.target())))
404 }));
405 },
406 };
407 rows.into_iter().map(move |(l, r)| (kind, l, r))
408 })
409 }
410
411 pub fn iter_flatten_meta(
412 &self)
413 -> impl Iterator<Item = (MappingKind, PathBuf, (PathBuf, Option<std::time::SystemTime>))> + '_ {
414 self.iter_flatten().map(|(k, t, p)| {
415 let time = p.metadata().ok().and_then(|m| m.modified().ok());
416 (k, t, (p, time))
417 })
418 }
419}
420
421
422#[derive(Debug, PartialEq, Eq, Hash)]
423#[cfg_attr(feature = "serde", derive(serde::Serialize))]
424pub enum Mapping<'left, 'right>
425 where Self: 'left + 'right {
426 AsIs(Match, (Expr<'left>, Expr<'right>)), Into(Match, (Expr<'left>, Expr<'right>)),
432 ManyInto {
434 sources: Vec<Match>,
435
436 target: PathBuf,
438
439 #[cfg(feature = "assets-report")]
440 excluded: Vec<Match>, exprs: (Expr<'left>, Expr<'right>),
443 },
444}
445
446impl Mapping<'_, '_> {
447 pub fn eq_ignore_expr(&self, other: &Self) -> bool {
449 match (self, other) {
450 (Mapping::AsIs(a, _), Mapping::AsIs(b, _)) | (Mapping::Into(a, _), Mapping::Into(b, _)) => a.eq(b),
451 (Mapping::AsIs(a, _), Mapping::Into(b, _)) | (Mapping::Into(b, _), Mapping::AsIs(a, _)) => a.eq(b),
452
453 (Mapping::AsIs(..), Mapping::ManyInto { .. }) => false,
454 (Mapping::Into(..), Mapping::ManyInto { .. }) => false,
455 (Mapping::ManyInto { .. }, Mapping::AsIs(..)) => false,
456 (Mapping::ManyInto { .. }, Mapping::Into(..)) => false,
457
458 (
459 Mapping::ManyInto { sources: sa,
460 target: ta,
461 .. },
462 Mapping::ManyInto { sources: sb,
463 target: tb,
464 .. },
465 ) => sa.eq(sb) && ta.eq(tb),
466 }
467 }
468
469 pub fn exprs(&self) -> (&Expr<'_>, &Expr<'_>) {
470 match self {
471 Mapping::AsIs(_, (left, right)) | Mapping::Into(_, (left, right)) => (left, right),
472 Mapping::ManyInto { exprs: (left, right), .. } => (left, right),
473 }
474 }
475
476 pub fn sources(&self) -> Vec<&Match> {
477 match self {
478 Mapping::AsIs(source, ..) | Mapping::Into(source, ..) => vec![source],
479 Mapping::ManyInto { sources, .. } => sources.iter().collect(),
480 }
481 }
482
483 pub fn kind(&self) -> MappingKind {
484 match self {
485 Mapping::AsIs(..) => MappingKind::AsIs,
486 Mapping::Into(..) => MappingKind::Into,
487 Mapping::ManyInto { .. } => MappingKind::ManyInto,
488 }
489 }
490
491
492 pub fn pretty_print_compact(&self) -> String {
493 let (k, l, r) = match self {
494 Mapping::AsIs(_, (l, r)) => ('=', l, r),
495 Mapping::Into(_, (l, r)) => ('I', l, r),
496 Mapping::ManyInto { exprs: (l, r), .. } => ('M', l, r),
497 };
498 format!("{{{k}:{:?}={:?}}}", l.original(), r.original())
499 }
500}
501
502
503#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
504#[cfg_attr(feature = "serde", derive(serde::Serialize))]
505pub enum MappingKind {
506 AsIs,
508 Into,
510 ManyInto,
512}
513
514impl std::fmt::Display for MappingKind {
515 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516 match self {
517 Self::AsIs => "as-is".fmt(f),
518 Self::Into => "into".fmt(f),
519 Self::ManyInto => "many-into".fmt(f),
520 }
521 }
522}
523
524
525pub trait EnvRequired<'t> {
526 type Item;
527 type Output: IntoIterator<Item = Self::Item>;
528
529 fn env_required(&'t self) -> Self::Output;
530}
531
532
533impl<'t> EnvRequired<'t> for Mapping<'_, '_> where Self: 't {
534 type Item = &'t str;
535 type Output = Vec<Self::Item>;
536
537 fn env_required(&'t self) -> Self::Output {
538 let resolver = EnvResolver::new();
539
540 let keys = match self {
541 Mapping::AsIs(_, (l, r)) => [l.original(), r.original()],
542 Mapping::Into(_, (l, r)) => [l.original(), r.original()],
543 Mapping::ManyInto { exprs: (l, r), .. } => [l.original(), r.original()],
544 };
545
546 keys.into_iter()
547 .flat_map(|s| resolver.matches(s))
548 .map(|m| m.as_str())
549 .collect()
550 }
551}
552
553
554impl<'s, S: 's + Eq + Hash + AsRef<str>> EnvRequired<'s> for AssetsRules<S> {
555 type Item = &'s str;
556 type Output = Vec<Self::Item>;
557
558 fn env_required(&'s self) -> Self::Output {
559 let resolver = EnvResolver::new();
560 self.env_required_with(&resolver).collect()
561 }
562}
563
564
565impl<S: Eq + Hash + AsRef<str>> AssetsRules<S> {
566 pub fn all_keys(&self) -> impl Iterator<Item = &str> {
567 let mut a = None;
568 let mut b = None;
569 match self {
570 AssetsRules::List(list) => {
571 a = Some(list.into_iter().map(AsRef::as_ref));
572 },
573 AssetsRules::Map(map) => {
574 let keys = map.keys().into_iter().map(AsRef::as_ref);
575 let values = map.values().into_iter().filter_map(|v| {
576 match v {
577 RuleValue::String(s) => Some(s.as_str()),
578 RuleValue::Boolean(_) => None,
579 }
580 });
581 b = Some(keys.chain(values));
582 },
583 };
584
585 a.into_iter().flatten().chain(b.into_iter().flatten())
586 }
587
588 pub fn env_required_with<'t: 'r, 'r>(&'t self,
589 resolver: &'r EnvResolver)
590 -> impl Iterator<Item = &'t str> + 'r {
591 self.all_keys()
592 .flat_map(|s| resolver.matches(s))
593 .map(|m| m.as_str())
594 }
595}
596
597
598#[cfg(test)]
599mod tests {
600 use std::collections::HashMap;
601 use std::collections::HashSet;
602 use std::path::{PathBuf, Path};
603
604 use crate::config::Env;
605 use crate::assets::resolver::Expr;
606 use crate::metadata::format::RuleValue;
607 use crate::metadata::format::AssetsRules;
608 use super::*;
609
610
611 fn crate_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) }
612
613
614 mod abs_if_existing {
615 use std::borrow::Cow;
616
617 use super::*;
618 use super::abs_if_existing;
619
620
621 #[test]
622 fn local() {
623 let roots = [
624 Cow::from(Path::new(env!("CARGO_MANIFEST_DIR"))),
625 crate_root().into(),
626 ];
627 let paths = ["Cargo.toml", "src/lib.rs"];
628
629 for root in roots {
630 for test in paths {
631 let path = Path::new(test);
632
633 let crated = abs_if_existing(path, &root).unwrap();
634 assert!(crated.is_some(), "{crated:?} should be exist (src: {path:?})");
635
636 let crated = crated.unwrap();
637 let expected = root.join(path);
638 assert_eq!(expected, crated);
639 }
640 }
641 }
642
643 #[test]
644 fn external_rel() {
645 let roots = [
646 Cow::from(Path::new(env!("CARGO_MANIFEST_DIR"))),
647 crate_root().into(),
648 ];
649 let paths = ["../utils/Cargo.toml", "./../utils/src/lib.rs"];
650
651 for root in roots {
652 for test in paths {
653 let path = Path::new(test);
654
655 let crated = abs_if_existing(path, &root).unwrap();
656 assert!(crated.is_some(), "{crated:?} should be exist (src: {path:?})");
657
658 let crated = crated.unwrap();
659 let expected = root.join(path);
660 assert_eq!(expected, crated);
661 }
662 }
663 }
664
665 #[test]
666 fn external_abs() {
667 let roots = [
668 Cow::from(Path::new(env!("CARGO_MANIFEST_DIR"))),
669 crate_root().into(),
670 ];
671 let paths = ["utils", "utils/Cargo.toml"];
672
673 for root in roots {
674 for test in paths {
675 let path = root.parent().unwrap().join(test);
676
677 let crated = abs_if_existing(&path, &root).unwrap();
678 assert!(crated.is_some(), "{crated:?} should be exist (src: {path:?})");
679
680 let crated = crated.unwrap();
681 let expected = path.as_path();
682 assert_eq!(expected, crated);
683 }
684 }
685 }
686 }
687
688
689 mod plan {
690 use super::*;
691 use std::env::temp_dir;
692
693
694 fn prepared_tmp(test_name: &str) -> (PathBuf, PathBuf, [&'static str; 4], Env) {
695 let temp = temp_dir().join(env!("CARGO_PKG_NAME"))
696 .join(env!("CARGO_PKG_VERSION"))
697 .join(test_name);
698
699 let sub = temp.join("dir");
700
701 if !temp.exists() {
702 println!("creating temp dir: {temp:?}")
703 } else {
704 println!("temp dir: {temp:?}")
705 }
706 std::fs::create_dir_all(&temp).unwrap();
707 std::fs::create_dir_all(&sub).unwrap();
708
709 let files = ["foo.txt", "bar.txt", "dir/baz.txt", "dir/boo.txt"];
711 for name in files {
712 std::fs::write(temp.join(name), []).unwrap();
713 }
714
715 let env = {
716 let mut env = Env::try_default().unwrap();
717 env.vars.insert("TMP".into(), temp.to_string_lossy().into_owned());
718 env.vars.insert("SUB".into(), sub.to_string_lossy().into_owned());
719 env
720 };
721
722 (temp, sub, files, env)
723 }
724
725
726 mod list {
727 use super::*;
728
729
730 mod as_is {
731 use super::*;
732
733
734 #[test]
735 fn local_exact() {
736 let env = Env::try_default().unwrap();
737 let opts = AssetsOptions::default();
738
739 let root = crate_root();
740 let root = Some(Cow::Borrowed(root.as_path()));
741
742 let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect();
743
744 let exprs = tests.iter().map(|s| s.to_string()).collect();
745 let assets = AssetsRules::List(exprs);
746
747 let plan = build_plan(&env, &assets, &opts, root).unwrap();
748
749 for pair in plan.as_inner() {
750 assert!(matches!(
751 pair,
752 Mapping::AsIs(_, (Expr::Original(left), Expr::Original(right)))
753 if right == "true" && tests.contains(left.as_str())
754 ));
755 }
756 }
757
758
759 #[test]
760 fn resolve_local_abs() {
761 let env = {
762 let mut env = Env::try_default().unwrap();
763 env.vars.insert(
764 "SRC_ABS".into(),
765 concat!(env!("CARGO_MANIFEST_DIR"), "/src").into(),
766 );
767 env
768 };
769
770 let opts = AssetsOptions::default();
771
772 let root = crate_root();
773 let root = Some(root.as_path().into());
774
775 let tests: HashMap<_, _> = {
776 let man_abs = PathBuf::from("Cargo.toml").canonicalize()
777 .unwrap()
778 .to_string_lossy()
779 .to_string();
780 let lib_abs = PathBuf::from("src/lib.rs").canonicalize()
781 .unwrap()
782 .to_string_lossy()
783 .to_string();
784 vec![
785 ("${CARGO_MANIFEST_DIR}/Cargo.toml", man_abs),
786 ("${SRC_ABS}/lib.rs", lib_abs),
787 ].into_iter()
788 .collect()
789 };
790
791 let exprs = tests.keys().map(|s| s.to_string()).collect();
792 let assets = AssetsRules::List(exprs);
793
794 let plan = build_plan(&env, &assets, &opts, root).unwrap();
795
796 for pair in plan.as_inner() {
797 assert!(matches!(
798 pair,
799 Mapping::AsIs(matched, (Expr::Modified{original, actual}, Expr::Original(right)))
800 if right == "true"
801 && tests[original.as_str()] == actual.as_ref()
802 && matched.source() == Path::new(&tests[original.as_str()]).canonicalize().unwrap()
803 ));
804 }
805 }
806
807
808 #[test]
809 fn resolve_local() {
810 let env = {
811 let mut env = Env::try_default().unwrap();
812 env.vars.insert("SRC".into(), "src".into());
813 env
814 };
815
816 let opts = AssetsOptions::default();
817
818 let root = crate_root();
819 let root = Some(root.as_path().into());
820
821 let tests: HashMap<_, _> = { vec![("${SRC}/lib.rs", "src/lib.rs"),].into_iter().collect() };
822
823 let exprs = tests.keys().map(|s| s.to_string()).collect();
824 let assets = AssetsRules::List(exprs);
825
826 let plan = build_plan(&env, &assets, &opts, root).unwrap();
827
828 for pair in plan.as_inner() {
829 if let Mapping::AsIs(matched, (Expr::Modified { original, actual }, Expr::Original(right))) =
830 pair
831 {
832 assert_eq!("true", right);
833 assert_eq!(tests[original.as_str()], actual.as_ref());
834 assert_eq!(
835 matched.source().canonicalize().unwrap(),
836 Path::new(&tests[original.as_str()]).canonicalize().unwrap()
837 );
838 assert_eq!(matched.target(), Path::new(&tests[original.as_str()]));
839 } else {
840 panic!("pair is not matching: {pair:#?}");
841 }
842 }
843 }
844
845
846 #[test]
847 #[cfg_attr(windows, should_panic)]
848 fn resolve_exact_external_abs() {
849 let (temp, sub, _files, env) = prepared_tmp("as_is-resolve_external");
850
851 let opts = AssetsOptions::default();
852
853 let root = crate_root();
854 let root = Some(root.as_path().into());
855
856
857 let tests: HashMap<_, _> = {
860 vec![
861 ("${TMP}/foo.txt", (temp.join("foo.txt"), "foo.txt")),
862 ("${TMP}/bar.txt", (temp.join("bar.txt"), "bar.txt")),
863 ("${SUB}/baz.txt", (sub.join("baz.txt"), "baz.txt")),
864 ("${TMP}/dir/boo.txt", (sub.join("boo.txt"), "boo.txt")),
865 ].into_iter()
866 .collect()
867 };
868
869 let exprs = tests.keys().map(|s| s.to_string()).collect();
870 let assets = AssetsRules::List(exprs);
871
872 let plan = build_plan(&env, &assets, &opts, root).unwrap();
873
874 {
876 let targets = plan.targets().collect::<Vec<_>>();
877 let expected = tests.values().map(|(_, name)| name).collect::<Vec<_>>();
878 assert_eq!(expected.len(), targets.len());
879 }
880
881 for pair in plan.as_inner() {
883 if let Mapping::AsIs(matched, (Expr::Modified { original, actual }, Expr::Original(right))) =
884 pair
885 {
886 assert_eq!("true", right);
887 assert_eq!(tests[original.as_str()].0.to_string_lossy(), actual.as_ref());
888 assert_eq!(matched.source(), tests[original.as_str()].0);
889 assert_eq!(matched.target().to_string_lossy(), tests[original.as_str()].1);
890 } else {
891 panic!("pair is not matching: {pair:#?}");
892 }
893 }
894 }
895
896
897 #[test]
898 #[cfg_attr(windows, should_panic)]
899 fn resolve_glob_external_many() {
900 let (_, _, files, env) = prepared_tmp("as_is-resolve_external_many");
901
902 let opts = AssetsOptions::default();
903
904 let root = crate_root();
905 let root = Some(root.as_path().into());
906
907 let exprs = ["${TMP}/*.txt", "${SUB}/*.txt"];
908
909 let assets = AssetsRules::List(exprs.iter().map(|s| s.to_string()).collect());
910
911 let plan = build_plan(&env, &assets, &opts, root).unwrap();
912
913 {
915 let targets = plan.targets().collect::<Vec<_>>();
916 assert_eq!(files.len(), targets.len());
917 }
918
919 for pair in plan.as_inner() {
921 if let Mapping::AsIs(matched, (Expr::Modified { original, actual }, Expr::Original(right))) =
922 pair
923 {
924 assert!(exprs.contains(&original.as_str()));
925 assert!(Path::new(actual.as_ref()).is_absolute());
926 assert_eq!("true", right);
927
928 if let Match::Pair { source, target } = matched {
929 assert_eq!(1, target.components().count());
931 assert_eq!(target.file_name(), source.file_name());
932 } else {
933 panic!("pair.matched is not matching: {matched:#?}");
934 }
935 } else {
936 panic!("pair is not matching: {pair:#?}");
937 }
938 }
939 }
940 }
941 }
942
943
944 mod map {
945 use super::*;
946
947
948 mod as_is {
949 use super::*;
950
951
952 #[test]
953 fn local_exact() {
954 let env = Env::try_default().unwrap();
955 let opts = AssetsOptions::default();
956
957 let root = crate_root();
958 let root = Some(root.as_path().into());
959
960 let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect();
961
962 let exprs = tests.iter()
963 .map(|s| (s.to_string(), RuleValue::Boolean(true)))
964 .collect();
965
966 let assets = AssetsRules::Map(exprs);
967
968 let plan = build_plan(&env, &assets, &opts, root).unwrap();
969
970 for pair in plan.as_inner() {
971 if let Mapping::AsIs(matched, (Expr::Original(left), Expr::Original(right))) = pair {
972 assert_eq!("true", right);
973 assert!(tests.contains(left.as_str()));
974 assert_eq!(
975 left.as_str(),
976 sanitize_path_pattern(matched.target().to_string_lossy().as_ref())
977 );
978 } else {
979 panic!("pair is not matching: {pair:#?}");
980 }
981 }
982 }
983
984
985 #[test]
986 fn local_exact_target() {
987 let env = Env::try_default().unwrap();
988 let opts = AssetsOptions::default();
989
990 let root = crate_root();
991 let root = Some(root.as_path().into());
992
993 let targets = ["trg", "/trg", "//trg"];
995 let tests = vec!["Cargo.toml", "src/lib.rs"];
997 for trg in targets {
1000 let stripped_trg = &trg.replace('/', "").trim().to_owned();
1001
1002 let exprs = tests.iter()
1003 .map(|s| (trg.to_string(), RuleValue::String(s.to_string())))
1004 .collect();
1005
1006 let assets = AssetsRules::Map(exprs);
1007
1008 let plan = build_plan(&env, &assets, &opts, root.clone()).unwrap();
1009
1010 for pair in plan.as_inner() {
1011 if let Mapping::AsIs(
1012 Match::Pair { source, target },
1013 (Expr::Original(left), Expr::Original(right)),
1014 ) = pair
1015 {
1016 assert_eq!(left, stripped_trg);
1017 assert!(tests.contains(&right.as_str()));
1018 assert_eq!(source, Path::new(right));
1019 assert_eq!(target, Path::new(stripped_trg));
1020 } else {
1021 panic!("pair is not matching: {pair:#?}");
1022 }
1023 }
1024 }
1025 }
1026
1027 #[test]
1028 fn local_exact_exclude() {
1029 let env = Env::try_default().unwrap();
1030 let opts = AssetsOptions::default();
1031
1032 let root = crate_root();
1033 let root = Some(root.as_path().into());
1034
1035 let tests = vec!["Cargo.toml", "src/lib.rs"];
1037
1038 let exprs = tests.iter()
1039 .map(|s| (*s, RuleValue::Boolean(true)))
1040 .chain([("*.toml", RuleValue::Boolean(false))])
1041 .collect();
1042
1043 let assets = AssetsRules::Map(exprs);
1044
1045 let plan = build_plan(&env, &assets, &opts, root.clone()).unwrap();
1046
1047 let pairs = plan.as_inner();
1048 assert_eq!(1, pairs.len());
1049
1050 let pair = pairs.first().unwrap();
1051
1052 if let Mapping::AsIs(_, (Expr::Original(left), _)) = pair {
1053 assert_eq!(tests[1], left);
1054 } else {
1055 panic!("pair is not matching: {pair:#?}");
1056 }
1057 }
1058 }
1059
1060
1061 mod one_into {
1062 use super::*;
1063
1064
1065 #[test]
1066 fn local_exact_target() {
1067 let env = Env::try_default().unwrap();
1068 let opts = AssetsOptions::default();
1069
1070 let root = crate_root();
1071 let root = Some(root.as_path().into());
1072
1073 let targets = ["trg/", "trg//", "/trg/", "//trg/"];
1075 let targets_rel = ["trg/", "trg//"]; let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect();
1078
1079 for trg in targets {
1080 let exprs = tests.iter()
1081 .map(|s| (trg.to_string(), RuleValue::String(s.to_string())))
1082 .collect();
1083
1084 let assets = AssetsRules::Map(exprs);
1085
1086 let plan = build_plan(&env, &assets, &opts, root.clone()).unwrap();
1087
1088 for pair in plan.as_inner() {
1089 if let Mapping::Into(
1090 Match::Pair { source, target },
1091 (Expr::Original(left), Expr::Original(right)),
1092 ) = pair
1093 {
1094 assert_eq!(left, target.to_string_lossy().as_ref());
1095 assert!(targets_rel.contains(&left.as_str()));
1096 assert!(tests.contains(right.as_str()));
1097 assert_eq!(source, Path::new(right));
1098 } else {
1099 panic!("pair is not matching: {pair:#?}");
1100 }
1101 }
1102 }
1103 }
1104 }
1105
1106
1107 mod many_into {
1108 use super::*;
1109
1110 #[test]
1111 #[cfg_attr(windows, should_panic)]
1112 fn glob_local_target() {
1113 let env = Env::try_default().unwrap();
1114 let opts = AssetsOptions::default();
1115
1116 let root = crate_root();
1117 let root = Some(root.as_path().into());
1118
1119 let targets = ["/trg/", "//trg/", "/trg", "trg"];
1121 let targets_rel = ["trg/", "trg"]; let tests = vec!["Cargo.tom*", "src/lib.*"];
1124 for trg in targets {
1127 let exprs = tests.iter()
1128 .map(|s| (trg.to_string(), RuleValue::String(s.to_string())))
1129 .collect();
1130
1131 let assets = AssetsRules::Map(exprs);
1132
1133 let plan = build_plan(&env, &assets, &opts, root.clone()).unwrap();
1134
1135 for pair in plan.as_inner() {
1136 if let Mapping::ManyInto { sources,
1137 target,
1138 #[cfg(feature = "assets-report")]
1139 excluded,
1140 exprs: (Expr::Original(left), Expr::Original(right)), } = pair
1141 {
1142 assert!(targets_rel.contains(&target.to_string_lossy().as_ref()));
1143 assert_eq!(&target.to_string_lossy(), left);
1144
1145 assert_eq!(1, sources.len());
1146 assert!(tests.contains(&right.as_str()));
1147
1148 #[cfg(feature = "assets-report")]
1149 assert_eq!(0, excluded.len());
1150 } else {
1151 panic!("pair is not matching: {pair:#?}");
1152 }
1153 }
1154 }
1155 }
1156
1157
1158 #[test]
1159 #[cfg_attr(windows, should_panic)]
1160 fn glob_local_exclude() {
1161 let env = Env::try_default().unwrap();
1162 let opts = AssetsOptions::default();
1163
1164 let root = crate_root();
1165 let root = Some(root.as_path().into());
1166
1167 let exclude = "src/*";
1169 let trg = "/trg";
1171 let tests: HashMap<_, _> = vec![("Cargo.tom*", 1), ("src/lib.*", 0)].into_iter().collect();
1173 let exprs = tests.iter()
1175 .enumerate()
1176 .map(|(i, (s, _))| (trg.to_string() + &"/".repeat(i), RuleValue::String(s.to_string())))
1177 .chain([(exclude.to_string(), RuleValue::Boolean(false))])
1178 .collect();
1179
1180 let assets = AssetsRules::Map(exprs);
1181 let plan = build_plan(&env, &assets, &opts, root.clone()).unwrap();
1182
1183 let pairs = plan.as_inner();
1184 assert_eq!(2, pairs.len());
1185
1186 for pair in pairs.iter() {
1187 if let Mapping::ManyInto { sources,
1188 target,
1189 exprs: (Expr::Original(left), Expr::Original(right)),
1190 .. } = pair
1191 {
1192 assert_eq!(&target.to_string_lossy(), left);
1193
1194 let sources_expected = tests[right.as_str()];
1195 assert_eq!(sources_expected, sources.len());
1196 } else {
1197 panic!("pair is not matching: {pair:#?}");
1198 }
1199 }
1200 }
1201 }
1202 }
1203 }
1204}