From 794ed592c9f14ce8399478a32bc8660677833f59 Mon Sep 17 00:00:00 2001 From: Joshua Potter Date: Sun, 9 Jan 2022 05:28:38 -0500 Subject: [PATCH] Add tests to `path` and `copy` modules. --- .homesync.yml | 1 + Cargo.toml | 4 ++ src/copy.rs | 194 ++++++++++++++++++++++++++++++++++++++++++++++++-- src/path.rs | 110 ++++++++++++++++++++-------- 4 files changed, 275 insertions(+), 34 deletions(-) create mode 100644 .homesync.yml diff --git a/.homesync.yml b/.homesync.yml new file mode 100644 index 0000000..5dd01c1 --- /dev/null +++ b/.homesync.yml @@ -0,0 +1 @@ +Hello, world! \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index b299e72..8415125 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,7 @@ serde_yaml = "0.8" simplelog = { version = "^0.11.1", features = ["paris"] } url = { version = "2.2.2", features = ["serde"] } yaml-rust = "0.4.4" + +[dev-dependencies] +serial_test = "0.5.1" +tempfile = "3.3.0" diff --git a/src/copy.rs b/src/copy.rs index 1e5e022..174f434 100644 --- a/src/copy.rs +++ b/src/copy.rs @@ -179,12 +179,20 @@ pub fn stage(pc: &PathConfig) -> Result<()> { fn get_workdir(pc: &PathConfig) -> Result { let workdir = path::resolve(&pc.config.repos.local)?; - match Repository::open(workdir.resolved()) { - Ok(_) => Ok(workdir), - Err(_) => Err(io::Error::new( + if let Ok(repo) = Repository::open(workdir.resolved()) { + if repo.workdir().is_some() { + Ok(workdir) + } else { + Err(io::Error::new( + io::ErrorKind::NotFound, + "Local repository is --bare.", + ))? + } + } else { + Err(io::Error::new( io::ErrorKind::NotFound, "Local repository not found.", - ))?, + ))? } } @@ -227,3 +235,181 @@ fn get_package_lookup(pc: &PathConfig) -> HashMap> { } seen } + +// ======================================== +// Tests +// ======================================== + +#[cfg(test)] +mod tests { + use super::*; + use crate::{config, path}; + use git2::Repository; + use std::{env, fs::File, io::Write}; + use tempfile::TempDir; + + // Tests must be serial since we are updating our environment variables. + use serial_test::serial; + + // Wrap functionality around this method to ensure the temporary directory + // does not go out of scope before we are ready. + fn build_home(func: T) { + let temp_dir = TempDir::new().unwrap(); + + let mut home_dir = temp_dir.path().to_path_buf(); + home_dir.push("home/owner"); + fs::create_dir_all(&home_dir).unwrap(); + + let mut homesync_yml = home_dir.to_path_buf(); + homesync_yml.push(".homesync.yml"); + File::create(&homesync_yml).unwrap(); + path::resolve(&homesync_yml).unwrap(); + + let mut config_homesync_yml = home_dir.to_path_buf(); + config_homesync_yml.push(".config/homesync"); + fs::create_dir_all(&config_homesync_yml).unwrap(); + config_homesync_yml.push("homesync.yml"); + File::create(&config_homesync_yml).unwrap(); + + env::set_var("HOME", &home_dir); + env::set_var("XDG_CONFIG_HOME", ""); + + let template = path::resolve(Path::new("examples/template.yaml")).unwrap(); + let config = config::load(&vec![template]).unwrap(); + + func(&config, &home_dir); + } + + fn build_repo(pc: &PathConfig) -> PathBuf { + let repo_dir = path::expand(&pc.config.repos.local).unwrap(); + Repository::init(&repo_dir).unwrap(); + + let mut path = repo_dir.to_path_buf(); + path.push("b"); + fs::create_dir(&path).unwrap(); + path.pop(); + path.push("a"); + File::create(&path).unwrap(); + path.pop(); + path.push("b/c"); + File::create(&path).unwrap(); + + repo_dir + } + + #[test] + #[serial] + fn walk_repo() { + build_home(|pc, _home_dir| { + let repo_dir = build_repo(pc); + let walked = super::walk_repo(&repo_dir).unwrap(); + let mut walked: Vec = walked + .iter() + .map(|w| w.unresolved().to_path_buf()) + .collect(); + walked.sort(); + assert_eq!(walked, vec![PathBuf::from("a"), PathBuf::from("b/c")]); + }); + } + + #[test] + #[serial] + fn package_lookup() { + build_home(|pc, _home_dir| { + let lookup = super::get_package_lookup(pc); + assert_eq!(lookup.len(), 4); + assert_eq!( + lookup + .iter() + .filter(|(_, v)| v.is_some()) + .collect::>() + .len(), + 2 + ); + assert_eq!( + lookup + .iter() + .filter(|(_, v)| v.is_none()) + .collect::>() + .len(), + 2 + ); + }); + } + + #[test] + #[serial] + fn apply_all() { + build_home(|pc, home_dir| { + let repo_dir = build_repo(pc); + let targets = [".homesync.yml", ".config/homesync/homesync.yml"]; + + for target in &targets { + let mut repo_path = repo_dir.to_path_buf(); + repo_path.push(&format!("$HOME/{}", target)); + fs::create_dir_all(repo_path.parent().unwrap()).unwrap(); + let mut file = File::create(&repo_path).unwrap(); + file.write_all(b"Hello, world!").unwrap(); + } + + super::apply_all(pc).expect("Could not apply packages"); + + for target in &targets { + let mut home_path = home_dir.to_path_buf(); + home_path.push(target); + let contents = fs::read_to_string(&home_path).unwrap(); + assert_eq!(contents, "Hello, world!"); + } + }); + } + + #[test] + #[serial] + fn apply_one() { + build_home(|pc, home_dir| { + let repo_dir = build_repo(pc); + let targets = [".homesync.yml", ".config/homesync/homesync.yml"]; + + for target in &targets { + let mut repo_path = repo_dir.to_path_buf(); + repo_path.push(&format!("$HOME/{}", target)); + fs::create_dir_all(repo_path.parent().unwrap()).unwrap(); + let mut file = File::create(&repo_path).unwrap(); + file.write_all(b"Hello, world!").unwrap(); + } + + super::apply_one(pc, "homesync").expect("Could not apply `homesync`"); + + for target in &targets { + let mut home_path = home_dir.to_path_buf(); + home_path.push(target); + let contents = fs::read_to_string(&home_path).unwrap(); + assert_eq!(contents, "Hello, world!"); + } + }); + } + + #[test] + #[serial] + fn stage() { + build_home(|pc, _home_dir| { + let repo_dir = build_repo(pc); + super::stage(pc).expect("Could not stage files."); + // Copied over the files in $HOME that exist, and deleted files that + // were previously defined but not referenced in the config. + let walked = super::walk_repo(&repo_dir).unwrap(); + let mut walked: Vec = walked + .iter() + .map(|w| w.unresolved().to_path_buf()) + .collect(); + walked.sort(); + assert_eq!( + walked, + vec![ + PathBuf::from("$HOME/.config/homesync/homesync.yml"), + PathBuf::from("$HOME/.homesync.yml"), + ] + ); + }); + } +} diff --git a/src/path.rs b/src/path.rs index 28fbc0e..dccdb83 100644 --- a/src/path.rs +++ b/src/path.rs @@ -8,7 +8,7 @@ use std::{ env::VarError, error, ffi::OsString, - fmt, fs, + fmt, hash::{Hash, Hasher}, io, path::{Component, Path, PathBuf}, @@ -195,34 +195,6 @@ impl<'de> Deserialize<'de> for ResPathBuf { } } -// ======================================== -// Validation -// ======================================== - -pub fn validate_is_file(path: &Path) -> Result<()> { - let metadata = fs::metadata(path)?; - if !metadata.is_file() { - // TODO(jrpotter): Use `IsADirectory` when stable. - Err(io::Error::new( - io::ErrorKind::Other, - format!("'{}' is not a file.", path.display()), - ))?; - } - Ok(()) -} - -pub fn validate_is_dir(path: &Path) -> Result<()> { - let metadata = fs::metadata(path)?; - if !metadata.is_dir() { - // TODO(jrpotter): Use `NotADirectory` when stable. - Err(io::Error::new( - io::ErrorKind::Other, - format!("'{}' is not a directory.", path.display()), - ))?; - } - Ok(()) -} - // ======================================== // Resolution // ======================================== @@ -236,7 +208,7 @@ pub fn expand(path: &Path) -> Result { match comp { Component::Prefix(_) => Err(io::Error::new( io::ErrorKind::InvalidInput, - "We do not currently support Windows.", + "We do not support Windows.", ))?, Component::RootDir => { expanded.clear(); @@ -292,3 +264,81 @@ pub fn soft_resolve(path: &Path) -> Result> { Err(Error::VarError(_)) => Ok(None), } } + +// ======================================== +// Tests +// ======================================== + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + use tempfile::NamedTempFile; + + #[test] + fn respath_absolute() { + let abs = Path::new("/home/jrpotter/example"); + let rel = Path::new("home/jrpotter/example"); + assert!(ResPathBuf::new(abs, rel).is_ok()); + assert!(ResPathBuf::new(rel, rel).is_err()); + } + + #[test] + fn respath_equality() { + let path = Path::new("/home/jrpotter/example"); + let res1 = ResPathBuf::new(path, Path::new("rel1")).unwrap(); + let res2 = ResPathBuf::new(path, Path::new("rel2")).unwrap(); + assert_eq!(res1, res2); + } + + #[test] + fn respath_hash() { + let mut set = HashSet::new(); + let path = Path::new("/home/jrpotter/example"); + let res1 = ResPathBuf::new(path, Path::new("rel1")).unwrap(); + let res2 = ResPathBuf::new(path, Path::new("rel2")).unwrap(); + set.insert(res1); + set.insert(res2); + assert_eq!(set.len(), 1); + } + + #[test] + fn expand_root_dir() { + let current = env::current_dir().unwrap(); + let expanded = expand(Path::new("")).unwrap(); + assert_eq!(current, expanded); + let expanded = expand(Path::new("/")).unwrap(); + assert_eq!(Path::new("/"), expanded); + } + + #[test] + fn expand_component() { + env::set_var("EXAMPLE", "example"); + let expanded = expand(Path::new("/a/b/$EXAMPLE/c")).unwrap(); + assert_eq!(Path::new("/a/b/example/c"), expanded); + let expanded = expand(Path::new("/a/b/pre$EXAMPLE/c")).unwrap(); + assert_eq!(Path::new("/a/b/pre$EXAMPLE/c"), expanded); + } + + #[test] + fn resolve() { + let path: PathBuf; + { + let temp = NamedTempFile::new().unwrap(); + path = temp.path().to_path_buf(); + assert!(super::resolve(&path).is_ok()); + } + assert!(super::resolve(&path).is_err()); + } + + #[test] + fn soft_resolve() { + let path: PathBuf; + { + let temp = NamedTempFile::new().unwrap(); + path = temp.path().to_path_buf(); + assert!(super::soft_resolve(&path).unwrap().is_some()); + } + assert!(super::soft_resolve(&path).unwrap().is_none()); + } +}