diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 71ade0b2..ab84768f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -193,7 +193,7 @@ jobs: - name: Test run: | cargo test -p=playdate-build-utils --all-features - cargo test -p=playdate-build --all-features + cargo test -p=playdate-build --all-features -- --nocapture cargo test -p=playdate-device cargo test -p=playdate-tool --all-features diff --git a/Cargo.lock b/Cargo.lock index 5e658237..87f8dd73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -741,7 +741,7 @@ dependencies = [ [[package]] name = "cargo-playdate" -version = "0.4.9" +version = "0.4.10" dependencies = [ "anstyle", "anyhow", @@ -4093,7 +4093,7 @@ dependencies = [ [[package]] name = "playdate-build" -version = "0.2.5" +version = "0.2.6" dependencies = [ "crate-metadata", "dirs", diff --git a/cargo/Cargo.toml b/cargo/Cargo.toml index 4a967bcb..86c03881 100644 --- a/cargo/Cargo.toml +++ b/cargo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-playdate" -version = "0.4.9" +version = "0.4.10" readme = "README.md" description = "Build tool for neat yellow console." keywords = ["playdate", "build", "cargo", "plugin", "cargo-subcommand"] diff --git a/cargo/src/assets/mod.rs b/cargo/src/assets/mod.rs index 42a0645d..d1b8a502 100644 --- a/cargo/src/assets/mod.rs +++ b/cargo/src/assets/mod.rs @@ -214,11 +214,21 @@ pub fn build<'cfg>(config: &'cfg Config) -> CargoResult> { AssetKind::Package => "", AssetKind::Dev => "dev-", }; + + let dep_root = dependency.manifest_path().parent().unwrap(); + config.log() .status("Build", format!("{kind_prefix}assets for {}", dep_pkg_id)); config.log().verbose(|mut log| { - let s = format!("destination: {}", dest.as_relative_to_root(config).display()); - log.status("", s) + let dest = format!("destination: {}", dest.as_relative_to_root(config).display()); + log.status("", dest); + let src = format!("root {}", dep_root.as_relative_to_root(config).display()); + log.status("", src); + if dep_root != &plan.path { + let path = plan.plan.crate_root(); + let src = format!("root (plan) {}", path.as_relative_to_root(config).display()); + log.status("", src); + } }); diff --git a/cargo/src/assets/plan.rs b/cargo/src/assets/plan.rs index e092a183..e2e11f33 100644 --- a/cargo/src/assets/plan.rs +++ b/cargo/src/assets/plan.rs @@ -166,8 +166,8 @@ pub struct CachedPlan<'t, 'cfg> { impl<'t, 'cfg> CachedPlan<'t, 'cfg> { #[must_use = "Cached plan must be used"] fn new(path: PathBuf, plan: AssetsPlan<'t, 'cfg>) -> CargoResult { - let mut serializable = plan.serializable_flatten().collect::>(); - serializable.sort_by_key(|(_, (p, _))| p.to_string_lossy().to_string()); + let mut serializable = plan.iter_flatten().collect::>(); + serializable.sort_by_key(|(_, _, (p, _))| p.to_string_lossy().to_string()); let json = serde_json::to_string(&serializable)?; let difference = if path.try_exists()? { diff --git a/support/build/Cargo.toml b/support/build/Cargo.toml index b252e4a2..df1ca06b 100644 --- a/support/build/Cargo.toml +++ b/support/build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "playdate-build" -version = "0.2.5" +version = "0.2.6" readme = "README.md" description = "Utils that help to build package for Playdate" keywords = ["playdate", "package", "encoding", "manifest", "assets"] diff --git a/support/build/src/assets/mod.rs b/support/build/src/assets/mod.rs index 1bc70e47..7d8b4733 100644 --- a/support/build/src/assets/mod.rs +++ b/support/build/src/assets/mod.rs @@ -12,6 +12,7 @@ use crate::metadata::format::AssetsOptions; pub mod plan; pub mod resolver; +mod tests; use self::plan::*; @@ -121,14 +122,23 @@ pub fn apply_build_plan<'l, 'r, P: AsRef>(plan: BuildPlan<'l, 'r>, AssetsBuildMethod::Link => &link_method, }; - let mut results = HashMap::with_capacity(plan.as_inner().len()); - for entry in plan.into_inner().drain(..) { + let (mut plan, crate_root) = plan.into_parts(); + // let mut results = HashMap::with_capacity(plan.as_inner().len()); + let mut results = HashMap::with_capacity(plan.len()); + // for entry in plan.into_inner().drain(..) { + for entry in plan.drain(..) { let current: Vec<_> = match &entry { - Mapping::AsIs(inc, ..) => vec![method(&inc.source(), &inc.target(), false)], - Mapping::Into(inc, ..) => vec![method(&inc.source(), &inc.target(), true)], + Mapping::AsIs(inc, ..) => { + let source = abs_or_rel_crate_any(inc.source(), crate_root); + vec![method(&source, &inc.target(), false)] + }, + Mapping::Into(inc, ..) => { + let source = abs_or_rel_crate_any(inc.source(), crate_root); + vec![method(&source, &inc.target(), true)] + }, Mapping::ManyInto { sources, target, .. } => { sources.iter() - .map(|inc| (inc.source(), target.join(inc.target()))) + .map(|inc| (abs_or_rel_crate_any(inc.source(), crate_root), target.join(inc.target()))) .map(|(ref source, ref target)| method(source, target, false)) .collect() }, diff --git a/support/build/src/assets/plan.rs b/support/build/src/assets/plan.rs index cd71e403..02d1f6ef 100644 --- a/support/build/src/assets/plan.rs +++ b/support/build/src/assets/plan.rs @@ -1,7 +1,7 @@ use std::hash::Hash; use std::borrow::Cow; use std::str::FromStr; -use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR}; +use std::path::{Path, PathBuf, MAIN_SEPARATOR}; use wax::{Glob, Pattern}; @@ -17,7 +17,7 @@ use super::resolver::*; pub fn build_plan<'l, 'r, 'c: 'l, V>(env: &'c Env, assets: &PlayDateMetadataAssets, options: &AssetsOptions, - crate_root: Option<&Path>) + crate_root: Option<&'c Path>) -> Result, super::Error> where V: Value { @@ -32,14 +32,22 @@ pub fn build_plan<'l, 'r, 'c: 'l, V>(env: &'c Env, let mut include_unresolved = Vec::new(); let mut exclude_exprs = Vec::new(); + const PATH_SEPARATOR: [char; 2] = [MAIN_SEPARATOR, '/']; + let enver = EnvResolver::new(); let crate_root = crate_root.unwrap_or_else(|| env.cargo_manifest_dir()); let link_behavior = options.link_behavior(); let to_relative = |s: &String| -> String { - let p = Path::new(&s); + let p = Path::new(s); if p.is_absolute() || p.has_root() { - p.components().skip(1).collect::().display().to_string() + let trailing_sep = p.components().count() > 1 && s.ends_with(PATH_SEPARATOR); + let mut s = p.components().skip(1).collect::().display().to_string(); + // preserve trailing separator + if trailing_sep && !s.ends_with(PATH_SEPARATOR) { + s.push(MAIN_SEPARATOR); + } + unixish_path_pattern(&s).into_owned() } else { s.to_owned() } @@ -87,16 +95,18 @@ pub fn build_plan<'l, 'r, 'c: 'l, V>(env: &'c Env, let mut mappings = Vec::new(); for (k, v) in map_unresolved.into_iter() { let key = PathBuf::from(k.as_str()); - let value = v.as_str(); - let into_dir = k.as_str().ends_with(MAIN_SEPARATOR_STR); - let source_exists = Path::new(value).try_exists()?; + let value = Cow::Borrowed(v.as_str()); + let into_dir = k.as_str().ends_with(PATH_SEPARATOR); + let source_exists = abs_or_rel_crate_existing(Path::new(value.as_ref()), crate_root)?.is_some(); let mapping = match (source_exists, into_dir) { - (true, true) => Mapping::Into(Match::new(value, key), (k, v)), - (true, false) => Mapping::AsIs(Match::new(value, key), (k, v)), + (true, true) => Mapping::Into(Match::new(value.as_ref(), key), (k, v)), + (true, false) => Mapping::AsIs(Match::new(value.as_ref(), key), (k, v)), (false, _) => { let mut resolved = resolve_includes(value, crate_root, &exclude_exprs, link_behavior)?; + debug!("Possible ManyInto, resolved: {}", resolved.len()); + // filter resolved includes: let _excluded: Vec<_> = resolved.extract_if(|inc| { let path = key.join(inc.target()); @@ -185,7 +195,37 @@ pub fn build_plan<'l, 'r, 'c: 'l, V>(env: &'c Env, // TODO: find source duplicates and warn! - Ok(BuildPlan(mappings)) + Ok(BuildPlan { plan: mappings, + crate_root }) +} + + +/// Make path relative to `crate_root` if it isn't absolute, checking existence. +/// Returns `None` if path doesn't exist. +pub fn abs_or_rel_crate_existing<'t, P1, P2>(p: P1, root: P2) -> std::io::Result>> + where P1: 't + AsRef + Into>, + P2: AsRef { + let p = if p.as_ref().is_absolute() && p.as_ref().try_exists()? { + Some(p.into()) + } else { + let abs = root.as_ref().join(p); + if abs.try_exists()? { + Some(Cow::Owned(abs)) + } else { + None + } + }; + Ok(p) +} + +/// Same as [`abs_or_rel_crate_existing`], but returns given `p` as fallback. +#[inline] +pub fn abs_or_rel_crate_any<'t, P1, P2>(p: P1, root: P2) -> Cow<'t, Path> + where P1: 't + AsRef + Into> + Clone, + P2: AsRef { + abs_or_rel_crate_existing(p.clone(), root).ok() + .flatten() + .unwrap_or(p.into()) } @@ -223,15 +263,26 @@ fn possibly_matching>(path: &Path, expr: P) -> bool { #[derive(Debug, PartialEq, Eq, Hash, serde::Serialize)] -pub struct BuildPlan<'left, 'right>(Vec>); +pub struct BuildPlan<'left, 'right> { + plan: Vec>, + crate_root: &'left Path, +} impl<'left, 'right> BuildPlan<'left, 'right> { - pub fn into_inner(self) -> Vec> { self.0 } - pub fn as_inner(&self) -> &[Mapping<'left, 'right>] { &self.0[..] } + pub fn into_inner(self) -> Vec> { self.plan } + pub fn as_inner(&self) -> &[Mapping<'left, 'right>] { &self.plan[..] } + pub fn into_parts(self) -> (Vec>, &'left Path) { (self.plan, self.crate_root) } + + pub fn crate_root(&self) -> &Path { self.crate_root } + pub fn set_crate_root(&mut self, path: &'left Path) -> &Path { + let old = self.crate_root; + self.crate_root = path; + old + } } impl<'left, 'right> AsRef<[Mapping<'left, 'right>]> for BuildPlan<'left, 'right> { - fn as_ref(&self) -> &[Mapping<'left, 'right>] { &self.0[..] } + fn as_ref(&self) -> &[Mapping<'left, 'right>] { &self.plan[..] } } impl BuildPlan<'_, '_> { @@ -282,15 +333,18 @@ impl BuildPlan<'_, '_> { }) } - pub fn serializable_flatten( + pub fn iter_flatten( &self) - -> impl Iterator))> + '_ { - let pair = |inc: &Match| (inc.target().to_path_buf(), inc.source().to_path_buf()); + -> impl Iterator))> + '_ { + let pair = |inc: &Match| { + (inc.target().to_path_buf(), abs_or_rel_crate_any(inc.source(), self.crate_root).to_path_buf()) + }; self.as_inner() .iter() .flat_map(move |mapping| { let mut rows = Vec::new(); + let kind = mapping.kind(); match mapping { Mapping::AsIs(inc, _) | Mapping::Into(inc, _) => rows.push(pair(inc)), Mapping::ManyInto { sources, target, .. } => { @@ -298,11 +352,11 @@ impl BuildPlan<'_, '_> { .map(|inc| pair(&Match::new(inc.source(), target.join(inc.target()))))); }, }; - rows.into_iter() + rows.into_iter().map(move |(l, r)| (kind, l, r)) }) - .map(|(t, p)| { + .map(|(k, t, p)| { let time = p.metadata().ok().and_then(|m| m.modified().ok()); - (t, (p, time)) + (k, t, (p, time)) }) } } @@ -366,4 +420,33 @@ impl Mapping<'_, '_> { Mapping::ManyInto { sources, .. } => sources.iter().collect(), } } + + pub fn kind(&self) -> MappingKind { + match self { + Mapping::AsIs(..) => MappingKind::AsIs, + Mapping::Into(..) => MappingKind::Into, + Mapping::ManyInto { .. } => MappingKind::ManyInto, + } + } +} + + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)] +pub enum MappingKind { + /// Copy source __to__ target. + AsIs, + /// Copy source __into__ target as-is, preserving related path. + Into, + /// Copy sources __into__ target as-is, preserving matched path. + ManyInto, +} + +impl std::fmt::Display for MappingKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::AsIs => "as-is".fmt(f), + Self::Into => "into".fmt(f), + Self::ManyInto => "many-into".fmt(f), + } + } } diff --git a/support/build/src/assets/resolver.rs b/support/build/src/assets/resolver.rs index c26daac1..82a10d0b 100644 --- a/support/build/src/assets/resolver.rs +++ b/support/build/src/assets/resolver.rs @@ -1,7 +1,7 @@ use std::hash::Hash; use std::borrow::Cow; use std::str; -use std::path::{Path, PathBuf}; +use std::path::{Path, PathBuf, MAIN_SEPARATOR}; use regex::Regex; use wax::{Glob, LinkBehavior, WalkError, WalkEntry}; @@ -17,7 +17,28 @@ pub fn resolve_includes, Excl: AsRef>(expr: S, exclude: &[Excl], links: LinkBehavior) -> Result, Error> { - let glob = Glob::new(expr.as_ref())?; + let expr = unixish_path_pattern(expr.as_ref()); + let crate_root = crate_root.to_string_lossy(); + + // #[cfg(windows)] + // let crate_root = unixish_path_pattern(crate_root.as_ref()); + + let crate_root = Path::new(crate_root.as_ref()); + let glob = Glob::new(expr.as_ref()).map_err(|err| { + // According wax's issue https://github.com/olson-sean-k/wax/issues/34 + // we doesn't support Windows absolute paths and hope to partially relative paths. + if cfg!(windows) { + let expr = PathBuf::from(expr.as_ref()); + if expr.is_absolute() || expr.has_root() { + let issue = "Wax issue https://github.com/olson-sean-k/wax/issues/34"; + Error::Error(format!("{err}, Windows absolute paths are not supported, {issue}")) + } else { + Error::from(err) + } + } else { + Error::from(err) + } + })?; let exclude = exclude.iter().map(AsRef::as_ref).chain(["**/.*/**"]); let walker = glob.walk_with_behavior(crate_root, links) .not(exclude)? @@ -29,13 +50,21 @@ pub fn resolve_includes, Excl: AsRef>(expr: S, // modify target path: let new = if target.is_absolute() && target.starts_with(crate_root) { // make it relative to crate_root: - let len = crate_root.components().count(); - Some(target.components().skip(len).collect()) - // TODO: need test it: - // let a = PathBuf::from(&target.display().to_string()[(crate_root.display().to_string().len() + - // MAIN_SEPARATOR_STR.to_string().len())..]); - // // relative part let b = target.components().skip(len).collect::(); - // assert_eq!(a, b); + if !cfg!(windows) { + let len = crate_root.components().count(); + Some(target.components().skip(len).collect()) + } else { + let target = target.display().to_string(); + target.strip_prefix(&crate_root.display().to_string()) + .map(|s| { + let mut s = Cow::from(s); + while let Some(stripped) = s.strip_prefix([MAIN_SEPARATOR, '/', '\\']) { + s = stripped.to_owned().into() + } + s.into_owned() + }) + .map(PathBuf::from) + } } else if target.is_absolute() { Some(PathBuf::from(target.file_name().expect("target filename"))) } else { @@ -57,6 +86,26 @@ pub fn resolve_includes, Excl: AsRef>(expr: S, } +/// On Windows makes given absolute path to look like POSIX or UNC: +/// `C:/foo/bar/**` or `//./C:/foo/bar/**`. +/// +/// In details: +/// - replace all `\` with `/` +/// - if pattern starts with `:`, escape it as `\`: +/// +/// On unix does nothing. +pub fn unixish_path_pattern(path: &str) -> Cow<'_, str> { + if cfg!(windows) { + path.replace('\\', "/") + .replace(':', "\\:") + .replace("//", "/") + .into() + } else { + path.into() + } +} + + // TODO: use config.env .unwrap_or pub struct EnvResolver(Regex); impl EnvResolver { @@ -162,20 +211,25 @@ impl Eq for Match {} impl Match { pub fn source(&self) -> Cow { match self { - Match::Match(source) => { - let c: Cow = source.path().into(); - c - }, - Match::Pair { source, .. } => source.into(), + Match::Match(source) => Cow::Borrowed(source.path()), + Match::Pair { source, .. } => Cow::Borrowed(source.as_path()), } } pub fn target(&self) -> Cow { + match self { + Match::Match(source) => Cow::Borrowed(Path::new(source.matched().complete())), + Match::Pair { target, .. } => Cow::Borrowed(target.as_path()), + } + } + + pub fn into_parts(self) -> (PathBuf, PathBuf) { match self { Match::Match(source) => { - let c: Cow = Path::new(source.matched().complete()).into(); - c + let target = source.matched().complete().into(); + let source = source.into_path(); + (source, target) }, - Match::Pair { target, .. } => target.into(), + Match::Pair { source, target } => (source, target), } } @@ -321,3 +375,166 @@ impl<'e> serde::Serialize for Expr<'e> { } } } + + +#[cfg(test)] +mod tests { + use super::*; + + + const LINKS: LinkBehavior = LinkBehavior::ReadTarget; + + fn crate_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) } + + + #[test] + fn resolve_includes_one_exact() { + for file in ["Cargo.toml", "src/lib.rs"] { + let resolved = resolve_includes::<_, &str>(file, &crate_root(), &[], LINKS).unwrap(); + assert_eq!(1, resolved.len()); + + let matched = resolved.first().unwrap(); + + assert_eq!(Path::new(file), matched.target()); + assert_eq!( + Path::new(file).canonicalize().unwrap(), + matched.source().canonicalize().unwrap() + ); + } + } + + #[test] + fn resolve_includes_one_glob() { + for (file, expected) in [("Cargo.tom*", "Cargo.toml"), ("**/lib.rs", "src/lib.rs")] { + let resolved = resolve_includes::<_, &str>(file, &crate_root(), &[], LINKS).unwrap(); + assert_eq!(1, resolved.len()); + + let matched = resolved.first().unwrap(); + + assert_eq!(Path::new(expected), matched.target()); + assert_eq!( + Path::new(expected).canonicalize().unwrap(), + matched.source().canonicalize().unwrap() + ); + } + } + + #[test] + fn resolve_includes_many_glob() { + for (file, expected) in [ + ("Cargo.*", &["Cargo.toml"][..]), + ("**/*.rs", &["src/lib.rs", "src/assets/mod.rs"][..]), + ] { + let resolved = resolve_includes::<_, &str>(file, &crate_root(), &[], LINKS).unwrap(); + assert!(!resolved.is_empty()); + + + let mut expected_passed = 0; + + for expected in expected { + expected_passed += 1; + let expected = PathBuf::from(expected); + let matched = resolved.iter() + .find(|matched| matched.target() == expected) + .unwrap(); + + assert_eq!(expected.as_path(), matched.target()); + assert_eq!( + expected.canonicalize().unwrap(), + matched.source().canonicalize().unwrap() + ); + } + + assert_eq!(expected.len(), expected_passed); + } + } + + #[test] + fn resolve_includes_many_glob_exclude() { + let exclude = ["**/lib.*"]; + for (file, expected) in [("Cargo.*", &["Cargo.toml"]), ("**/*.rs", &["src/assets/mod.rs"])] { + let resolved = resolve_includes::<_, &str>(file, &crate_root(), &exclude, LINKS).unwrap(); + assert!(!resolved.is_empty()); + + + let mut expected_passed = 0; + + for expected in expected { + let matched = resolved.iter() + .find(|matched| matched.target() == Path::new(expected)) + .unwrap(); + + assert_eq!(Path::new(expected), matched.target()); + assert_eq!( + Path::new(expected).canonicalize().unwrap(), + matched.source().canonicalize().unwrap() + ); + expected_passed += 1; + } + + assert_eq!(expected.len(), expected_passed); + } + } + + #[test] + #[cfg_attr(windows, should_panic)] + fn resolve_includes_glob_abs_to_local() { + let (file, expected) = (env!("CARGO_MANIFEST_DIR").to_owned() + "/Cargo.*", &["Cargo.toml"]); + + let resolved = resolve_includes::<_, &str>(file, &crate_root(), &[], LINKS).unwrap(); + assert_eq!(expected.len(), resolved.len()); + + let mut expected_passed = 0; + + for expected in expected { + expected_passed += 1; + let matched = resolved.iter() + .find(|matched| matched.target() == Path::new(expected)) + .unwrap(); + + assert_eq!(Path::new(expected), matched.target()); + assert_eq!( + Path::new(expected).canonicalize().unwrap(), + matched.source().canonicalize().unwrap() + ); + } + + assert_eq!(expected.len(), expected_passed); + } + + + #[test] + fn resolver_expr() { + let resolver = EnvResolver::new(); + + let env = { + let mut env = Env::default().unwrap(); + env.vars.insert("FOO".into(), "foo".into()); + env.vars.insert("BAR".into(), "bar".into()); + env + }; + + let exprs = [ + ("${FOO}/file.txt", "foo/file.txt"), + ("${BAR}/file.txt", "bar/file.txt"), + ]; + + for (src, expected) in exprs { + let mut expr = Expr::from(src); + resolver.expr(&mut expr, &env); + + assert_eq!(expected, expr.actual()); + assert_eq!(expected, expr.as_str()); + assert_eq!(src, expr.original()); + } + } + + #[test] + #[should_panic] + fn resolver_missed() { + let resolver = EnvResolver::new(); + let env = Env::default().unwrap(); + let expr = Expr::from("${MISSED}/file.txt"); + resolver.expr(expr, &env); + } +} diff --git a/support/build/src/assets/tests.rs b/support/build/src/assets/tests.rs new file mode 100644 index 00000000..ada9da1e --- /dev/null +++ b/support/build/src/assets/tests.rs @@ -0,0 +1,450 @@ +#![cfg(test)] +use std::collections::HashMap; +use std::collections::HashSet; +use std::path::PathBuf; + +use crate::config::Env; +use crate::assets::resolver::Expr; +use crate::metadata::format::PlayDateMetadataAssets; +use super::*; + +use resolver::unixish_path_pattern; +use resolver::Match; +use toml::Value; + + +mod plan { + use super::*; + use std::env::temp_dir; + + + fn crate_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) } + + + fn prepared_tmp(test_name: &str) -> (PathBuf, PathBuf, [&'static str; 4], Env) { + let temp = temp_dir().join(env!("CARGO_PKG_NAME")) + .join(env!("CARGO_PKG_VERSION")) + .join(test_name); + + let sub = temp.join("dir"); + + if !temp.exists() { + println!("creating temp dir: {temp:?}") + } else { + println!("temp dir: {temp:?}") + } + std::fs::create_dir_all(&temp).unwrap(); + std::fs::create_dir_all(&sub).unwrap(); + + // add temp files + let files = ["foo.txt", "bar.txt", "dir/baz.txt", "dir/boo.txt"]; + for name in files { + std::fs::write(temp.join(name), []).unwrap(); + } + + let env = { + let mut env = Env::default().unwrap(); + env.vars.insert("TMP".into(), temp.to_string_lossy().into_owned()); + env.vars.insert("SUB".into(), sub.to_string_lossy().into_owned()); + env + }; + + (temp, sub, files, env) + } + + + mod list { + use super::*; + + + mod as_is { + use super::*; + + + #[test] + fn local_exact() { + let env = Env::default().unwrap(); + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect(); + + let exprs = tests.iter().map(|s| s.to_string()).collect(); + let assets = PlayDateMetadataAssets::List::(exprs); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + for pair in plan.as_inner() { + assert!(matches!( + pair, + Mapping::AsIs(_, (Expr::Original(left), Expr::Original(right))) + if right == "true" && tests.contains(left.as_str()) + )); + } + } + + + #[test] + fn resolve_local_abs() { + let env = { + let mut env = Env::default().unwrap(); + env.vars.insert( + "SRC_ABS".into(), + concat!(env!("CARGO_MANIFEST_DIR"), "/src").into(), + ); + env + }; + + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + let tests: HashMap<_, _> = { + let man_abs = PathBuf::from("Cargo.toml").canonicalize() + .unwrap() + .to_string_lossy() + .to_string(); + let lib_abs = PathBuf::from("src/lib.rs").canonicalize() + .unwrap() + .to_string_lossy() + .to_string(); + vec![ + ("${CARGO_MANIFEST_DIR}/Cargo.toml", man_abs), + ("${SRC_ABS}/lib.rs", lib_abs), + ].into_iter() + .collect() + }; + + let exprs = tests.keys().map(|s| s.to_string()).collect(); + let assets = PlayDateMetadataAssets::List::(exprs); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + for pair in plan.as_inner() { + assert!(matches!( + pair, + Mapping::AsIs(matched, (Expr::Modified{original, actual}, Expr::Original(right))) + if right == "true" + && tests[original.as_str()] == actual.as_ref() + && matched.source() == Path::new(&tests[original.as_str()]).canonicalize().unwrap() + )); + } + } + + + #[test] + fn resolve_local() { + let env = { + let mut env = Env::default().unwrap(); + env.vars.insert("SRC".into(), "src".into()); + env + }; + + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + let tests: HashMap<_, _> = { vec![("${SRC}/lib.rs", "src/lib.rs"),].into_iter().collect() }; + + let exprs = tests.keys().map(|s| s.to_string()).collect(); + let assets = PlayDateMetadataAssets::List::(exprs); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + for pair in plan.as_inner() { + if let Mapping::AsIs(matched, (Expr::Modified { original, actual }, Expr::Original(right))) = pair + { + assert_eq!("true", right); + assert_eq!(tests[original.as_str()], actual.as_ref()); + assert_eq!( + matched.source().canonicalize().unwrap(), + Path::new(&tests[original.as_str()]).canonicalize().unwrap() + ); + assert_eq!(matched.target(), Path::new(&tests[original.as_str()])); + } else { + panic!("pair is not matching: {pair:#?}"); + } + } + } + + + #[test] + #[cfg_attr(windows, should_panic)] + fn resolve_external() { + let (temp, sub, _files, env) = prepared_tmp("as_is-resolve_external"); + + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + + // tests: + + let tests: HashMap<_, _> = { + vec![ + ("${TMP}/foo.txt", (temp.join("foo.txt"), "foo.txt")), + ("${TMP}/bar.txt", (temp.join("bar.txt"), "bar.txt")), + ("${SUB}/baz.txt", (sub.join("baz.txt"), "baz.txt")), + ("${TMP}/dir/boo.txt", (sub.join("boo.txt"), "boo.txt")), + ].into_iter() + .collect() + }; + + let exprs = tests.keys().map(|s| s.to_string()).collect(); + let assets = PlayDateMetadataAssets::List::(exprs); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + // check targets len + { + let targets = plan.targets().collect::>(); + let expected = tests.values().map(|(_, name)| name).collect::>(); + assert_eq!(expected.len(), targets.len()); + } + + // full check + for pair in plan.as_inner() { + if let Mapping::AsIs(matched, (Expr::Modified { original, actual }, Expr::Original(right))) = pair + { + assert_eq!("true", right); + assert_eq!(tests[original.as_str()].0.to_string_lossy(), actual.as_ref()); + assert_eq!(matched.source(), tests[original.as_str()].0); + assert_eq!(matched.target().to_string_lossy(), tests[original.as_str()].1); + } else { + panic!("pair is not matching: {pair:#?}"); + } + } + } + + + #[test] + #[cfg_attr(windows, should_panic)] + fn resolve_external_many() { + let (_, _, files, env) = prepared_tmp("as_is-resolve_external_many"); + + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + let exprs = ["${TMP}/*.txt", "${SUB}/*.txt"]; + + let assets = + PlayDateMetadataAssets::List::(exprs.iter().map(|s| s.to_string()).collect()); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + // check targets len + { + let targets = plan.targets().collect::>(); + assert_eq!(files.len(), targets.len()); + } + + // full check + for pair in plan.as_inner() { + if let Mapping::AsIs(matched, (Expr::Modified { original, actual }, Expr::Original(right))) = pair + { + assert!(exprs.contains(&original.as_str())); + assert!(Path::new(actual.as_ref()).is_absolute()); + assert_eq!("true", right); + + if let Match::Pair { source, target } = matched { + // target is just filename: + assert_eq!(1, target.components().count()); + assert_eq!(target.file_name(), source.file_name()); + } else { + panic!("pair.matched is not matching: {matched:#?}"); + } + } else { + panic!("pair is not matching: {pair:#?}"); + } + } + } + } + } + + + mod map { + use super::*; + + + mod as_is { + use super::*; + + + #[test] + fn local_exact() { + let env = Env::default().unwrap(); + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect(); + + let exprs = tests.iter() + .map(|s| (s.to_string(), Value::Boolean(true))) + .collect(); + let assets = PlayDateMetadataAssets::Map::(exprs); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + for pair in plan.as_inner() { + if let Mapping::AsIs(matched, (Expr::Original(left), Expr::Original(right))) = pair { + assert_eq!("true", right); + assert!(tests.contains(left.as_str())); + assert_eq!( + left.as_str(), + unixish_path_pattern(matched.target().to_string_lossy().as_ref()) + ); + } else { + panic!("pair is not matching: {pair:#?}"); + } + } + } + + + #[test] + fn local_exact_target() { + let env = Env::default().unwrap(); + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + // left hand of rule: + let targets = ["trg", "/trg", "//trg"]; + // right hand of rule: + let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect(); + // latest because there is no to files into one target, so "into" will be used + + for trg in targets { + let stripped_trg = &trg.replace('/', "").trim().to_owned(); + + let exprs = tests.iter() + .map(|s| (trg.to_string(), Value::String(s.to_string()))) + .collect(); + let assets = PlayDateMetadataAssets::Map::(exprs); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + for pair in plan.as_inner() { + if let Mapping::AsIs( + Match::Pair { source, target }, + (Expr::Original(left), Expr::Original(right)), + ) = pair + { + assert_eq!(left, stripped_trg); + assert!(tests.contains(right.as_str())); + assert_eq!(source, Path::new(right)); + assert_eq!(target, Path::new(stripped_trg)); + } else { + panic!("pair is not matching: {pair:#?}"); + } + } + } + } + } + + + mod one_into { + use super::*; + + + #[test] + fn local_exact() { + let env = Env::default().unwrap(); + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + // left hand of rule: + let targets = ["trg/", "trg//", "/trg/", "//trg/"]; // XXX: " too + let targets_rel = ["trg/", "trg//"]; // non-abs targets + // right hand of rule: + let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect(); + + for trg in targets { + let exprs = tests.iter() + .map(|s| (trg.to_string(), toml::Value::String(s.to_string()))) + .collect(); + let assets = PlayDateMetadataAssets::Map::(exprs); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + for pair in plan.as_inner() { + if let Mapping::Into( + Match::Pair { source, target }, + (Expr::Original(left), Expr::Original(right)), + ) = pair + { + assert_eq!(left, target.to_string_lossy().as_ref()); + assert!(targets_rel.contains(&left.as_str())); + assert!(tests.contains(right.as_str())); + assert_eq!(source, Path::new(right)); + } else { + panic!("pair is not matching: {pair:#?}"); + } + } + } + } + } + + + mod many_into { + use super::*; + + #[test] + #[cfg_attr(windows, should_panic)] + fn resolve_local_target() { + let env = Env::default().unwrap(); + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + // left hand of rule: + let targets = ["/trg/", "//trg/", "/trg", "trg"]; + let targets_rel = ["trg/", "trg"]; // non-abs targets + // right hand of rule: + let tests: HashSet<_> = vec!["Cargo.tom*", "src/lib.*"].into_iter().collect(); + // latest because there is no to files into one target, so "into" will be used + + for trg in targets { + let exprs = tests.iter() + .map(|s| (trg.to_string(), toml::Value::String(s.to_string()))) + .collect(); + let assets = PlayDateMetadataAssets::Map::(exprs); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + for pair in plan.as_inner() { + if let Mapping::ManyInto { sources, + target, + #[cfg(feature = "assets-report")] + excluded, + exprs: (Expr::Original(left), Expr::Original(right)), } = pair + { + assert!(targets_rel.contains(&target.to_string_lossy().as_ref())); + assert_eq!(&target.to_string_lossy(), left); + + assert_eq!(1, sources.len()); + assert!(tests.contains(right.as_str())); + + #[cfg(feature = "assets-report")] + assert_eq!(0, excluded.len()); + } else { + panic!("pair is not matching: {pair:#?}"); + } + } + } + } + } + } +}