use std::collections::HashSet;
use std::path::Path;
use crate::compose::types::WatchRule;
use crate::error::{ComposeError, Result};
pub(super) struct SyncPlacement {
pub(super) entry_name: String,
pub(super) dest_dir: String,
}
pub(super) fn plan_sync_placement(root: &Path, changed: &Path, target: &str) -> SyncPlacement {
if root.is_dir() {
let rel = changed.strip_prefix(root).unwrap_or(changed);
let entry_name = rel.to_string_lossy().into_owned();
let dest_dir = target.trim_end_matches('/').to_string();
let dest_dir = if dest_dir.is_empty() {
"/".to_string()
} else {
dest_dir
};
SyncPlacement {
entry_name,
dest_dir,
}
} else {
let target_path = Path::new(target);
let entry_name = target_path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.or_else(|| {
changed
.file_name()
.map(|n| n.to_string_lossy().into_owned())
})
.unwrap_or_default();
let dest_dir = target_path
.parent()
.map(|p| p.to_string_lossy().into_owned())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "/".to_string());
SyncPlacement {
entry_name,
dest_dir,
}
}
}
pub(super) fn is_dispatch_event(kind: ¬ify::EventKind) -> bool {
use notify::EventKind;
matches!(
kind,
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
)
}
pub(super) fn validate_sync_target(rule: &WatchRule) -> Result<()> {
if rule.action.requires_target() && rule.target.is_none() {
return Err(ComposeError::Watch(format!(
"watch rule for '{}' uses a sync action ({}) but has no target",
rule.path,
rule.action.as_token()
)));
}
Ok(())
}
pub(super) fn mkdir_p_argv(dest_dir: &str) -> Vec<String> {
vec![
"mkdir".into(),
"-p".into(),
"--".into(),
dest_dir.to_string(),
]
}
pub(super) fn mark_dir_ensured(
ensured: &mut HashSet<(String, String)>,
container: &str,
dest: &str,
) -> bool {
ensured.insert((container.to_string(), dest.to_string()))
}
#[cfg(test)]
mod tests {
use super::{
is_dispatch_event, mark_dir_ensured, mkdir_p_argv, plan_sync_placement,
validate_sync_target,
};
use crate::compose::types::{WatchAction, WatchRule};
use std::collections::HashSet;
use std::fs;
use tempfile::tempdir;
fn rule(action: WatchAction, target: Option<&str>) -> WatchRule {
WatchRule {
path: "src".into(),
action,
target: target.map(str::to_string),
..Default::default()
}
}
#[test]
fn dispatch_event_filters_access_and_other() {
use notify::event::{AccessKind, CreateKind, ModifyKind, RemoveKind};
use notify::EventKind;
assert!(is_dispatch_event(&EventKind::Create(CreateKind::File)));
assert!(is_dispatch_event(&EventKind::Modify(ModifyKind::Any)));
assert!(is_dispatch_event(&EventKind::Remove(RemoveKind::File)));
assert!(!is_dispatch_event(&EventKind::Access(AccessKind::Open(
notify::event::AccessMode::Read
))));
assert!(!is_dispatch_event(&EventKind::Access(AccessKind::Any)));
assert!(!is_dispatch_event(&EventKind::Other));
assert!(!is_dispatch_event(&EventKind::Any));
}
#[test]
fn validate_sync_target_rejects_targetless_sync() {
assert!(validate_sync_target(&rule(WatchAction::Sync, None)).is_err());
assert!(validate_sync_target(&rule(WatchAction::SyncAndRestart, None)).is_err());
assert!(validate_sync_target(&rule(WatchAction::SyncAndExec, None)).is_err());
}
#[test]
fn validate_sync_target_accepts_target_and_whole_container_actions() {
assert!(validate_sync_target(&rule(WatchAction::Sync, Some("/app"))).is_ok());
assert!(validate_sync_target(&rule(WatchAction::Rebuild, None)).is_ok());
assert!(validate_sync_target(&rule(WatchAction::Restart, None)).is_ok());
}
#[test]
fn mkdir_argv_terminates_options_for_leading_dash_target() {
assert_eq!(mkdir_p_argv("-m0777"), vec!["mkdir", "-p", "--", "-m0777"]);
assert_eq!(mkdir_p_argv("/app"), vec!["mkdir", "-p", "--", "/app"]);
}
#[test]
fn mark_dir_ensured_only_first_time_per_target() {
let mut ensured: HashSet<(String, String)> = HashSet::new();
assert!(mark_dir_ensured(&mut ensured, "c1", "/app"));
assert!(!mark_dir_ensured(&mut ensured, "c1", "/app"));
assert!(mark_dir_ensured(&mut ensured, "c2", "/app"));
assert!(mark_dir_ensured(&mut ensured, "c1", "/other"));
}
#[test]
fn placement_directory_rule_preserves_subpath() {
let dir = tempdir().unwrap();
fs::create_dir(dir.path().join("sub")).unwrap();
let changed = dir.path().join("sub/b.txt");
fs::write(&changed, b"b").unwrap();
let p = plan_sync_placement(dir.path(), &changed, "/app");
assert_eq!(p.entry_name, "sub/b.txt");
assert_eq!(p.dest_dir, "/app");
}
#[test]
fn placement_directory_rule_trailing_slash_target() {
let dir = tempdir().unwrap();
let changed = dir.path().join("a.txt");
fs::write(&changed, b"a").unwrap();
let p = plan_sync_placement(dir.path(), &changed, "/app/");
assert_eq!(p.entry_name, "a.txt");
assert_eq!(p.dest_dir, "/app");
}
#[test]
fn placement_single_file_rule_honours_renaming_target() {
let dir = tempdir().unwrap();
let src = dir.path().join("settings.yml");
fs::write(&src, b"k: v").unwrap();
let p = plan_sync_placement(&src, &src, "/app/config.yml");
assert_eq!(p.entry_name, "config.yml");
assert_eq!(p.dest_dir, "/app");
}
#[test]
fn placement_single_file_rule_same_basename() {
let dir = tempdir().unwrap();
let src = dir.path().join("app.txt");
fs::write(&src, b"x").unwrap();
let p = plan_sync_placement(&src, &src, "/newdir/app.txt");
assert_eq!(p.entry_name, "app.txt");
assert_eq!(p.dest_dir, "/newdir");
}
#[test]
fn placement_single_file_rule_target_at_root() {
let dir = tempdir().unwrap();
let src = dir.path().join("app.txt");
fs::write(&src, b"x").unwrap();
let p = plan_sync_placement(&src, &src, "/app.txt");
assert_eq!(p.entry_name, "app.txt");
assert_eq!(p.dest_dir, "/");
}
}