#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use fren_date::{plan_with_year, ConflictPolicy, ItemKind, PlanOpts, SlugOpts};
use slug_preserve::CaseMode;
use std::fs;
use tempfile::TempDir;
fn touch(path: impl AsRef<std::path::Path>) {
let path = path.as_ref();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap_or_else(|e| panic!("mkdir {}: {}", parent.display(), e));
}
fs::write(path, b"").unwrap_or_else(|e| panic!("write {}: {}", path.display(), e));
}
fn rust_default_opts() -> SlugOpts {
SlugOpts {
separator: '-',
case: CaseMode::Preserve,
split_camel: false,
}
}
#[test]
fn walks_recursively_and_skips_hidden() {
let tmp = TempDir::new().expect("tempdir");
touch(tmp.path().join("Hello World.txt"));
touch(tmp.path().join("subdir").join("Inner File.txt"));
touch(tmp.path().join(".hidden").join("ignored.txt"));
fs::write(tmp.path().join(".hidden_file"), b"").expect("write hidden file");
let plans = plan_with_year(
&[tmp.path()],
&rust_default_opts(),
&PlanOpts {
recursive: true,
..PlanOpts::default()
},
2024,
)
.expect("plan ok");
let originals: Vec<_> = plans
.iter()
.map(|p| {
p.original_path
.strip_prefix(tmp.path())
.unwrap()
.to_string_lossy()
.into_owned()
})
.collect();
assert!(originals.iter().any(|p| p == "Hello World.txt"));
assert!(originals.iter().any(|p| p == "subdir/Inner File.txt"));
assert!(!originals.iter().any(|p| p.starts_with(".hidden")));
}
#[test]
fn bottom_up_sort_files_then_dirs() {
let tmp = TempDir::new().expect("tempdir");
touch(tmp.path().join("My Vacation").join("photo 1.txt"));
touch(tmp.path().join("My Vacation").join("photo 2.txt"));
let plans = plan_with_year(
&[tmp.path()],
&rust_default_opts(),
&PlanOpts {
recursive: true,
..PlanOpts::default()
},
2024,
)
.expect("plan ok");
let mut prev_depth = usize::MAX;
let mut saw_file_at_deepest = false;
for plan in &plans {
if plan.depth == 2 && matches!(plan.kind, ItemKind::File) {
saw_file_at_deepest = true;
}
assert!(
plan.depth <= prev_depth,
"depths must monotonically decrease (deepest first), got {} after {}",
plan.depth,
prev_depth
);
prev_depth = plan.depth;
}
assert!(saw_file_at_deepest, "expected to find a file at depth 2");
}
#[test]
fn no_op_renames_are_filtered() {
let tmp = TempDir::new().expect("tempdir");
touch(tmp.path().join("already-good.txt"));
let plans = plan_with_year(
&[tmp.path()],
&rust_default_opts(),
&PlanOpts {
recursive: true,
..PlanOpts::default()
},
2024,
)
.expect("plan ok");
let originals: Vec<_> = plans
.iter()
.map(|p| {
p.original_path
.file_name()
.unwrap()
.to_string_lossy()
.into_owned()
})
.collect();
assert!(
!originals.iter().any(|n| n == "already-good.txt"),
"no-op should be filtered, got: {originals:?}"
);
}
#[test]
fn within_batch_collision_detected() {
let tmp = TempDir::new().expect("tempdir");
touch(tmp.path().join("Hello World.txt"));
touch(tmp.path().join("Hello World.txt"));
let result = plan_with_year(
&[tmp.path()],
&rust_default_opts(),
&PlanOpts {
recursive: true,
on_conflict: ConflictPolicy::Abort,
..PlanOpts::default()
},
2024,
);
match result {
Err(fren_date::FrenError::WithinBatchCollision { .. }) => {}
other => panic!("expected WithinBatchCollision, got {other:?}"),
}
}
#[test]
fn preexisting_target_aborts() {
let tmp = TempDir::new().expect("tempdir");
touch(tmp.path().join("Hello World.txt"));
touch(tmp.path().join("Hello-World.txt"));
let result = plan_with_year(
&[tmp.path()],
&rust_default_opts(),
&PlanOpts {
recursive: true,
on_conflict: ConflictPolicy::Abort,
..PlanOpts::default()
},
2024,
);
match result {
Err(fren_date::FrenError::TargetExists(_)) => {}
other => panic!("expected TargetExists, got {other:?}"),
}
}
#[test]
fn exclude_paths_are_skipped() {
let tmp = TempDir::new().expect("tempdir");
touch(tmp.path().join("Keep Me.txt"));
touch(tmp.path().join("Skip Me").join("inside.txt"));
let plans = plan_with_year(
&[tmp.path()],
&rust_default_opts(),
&PlanOpts {
recursive: true,
exclude: vec![tmp.path().join("Skip Me")],
..PlanOpts::default()
},
2024,
)
.expect("plan ok");
let originals: Vec<_> = plans
.iter()
.map(|p| p.original_path.to_string_lossy().into_owned())
.collect();
assert!(originals.iter().any(|p| p.contains("Keep Me.txt")));
assert!(
!originals.iter().any(|p| p.contains("Skip Me")),
"excluded path leaked: {originals:?}"
);
}
#[test]
fn extension_lowercased() {
let tmp = TempDir::new().expect("tempdir");
touch(tmp.path().join("My Doc.PDF"));
let plans = plan_with_year(
&[tmp.path()],
&rust_default_opts(),
&PlanOpts {
recursive: true,
..PlanOpts::default()
},
2024,
)
.expect("plan ok");
let new_names: Vec<String> = plans
.iter()
.map(|p| p.new_name.to_string_lossy().into_owned())
.collect();
assert!(
new_names.iter().any(|n| n == "My-Doc.pdf"),
"extension should be lowercased: {new_names:?}"
);
}
#[test]
fn date_detected_in_filename() {
let tmp = TempDir::new().expect("tempdir");
touch(tmp.path().join("photo_20240115.jpg"));
let plans = plan_with_year(
&[tmp.path()],
&rust_default_opts(),
&PlanOpts {
recursive: true,
..PlanOpts::default()
},
2024,
)
.expect("plan ok");
let new_names: Vec<String> = plans
.iter()
.map(|p| p.new_name.to_string_lossy().into_owned())
.collect();
assert!(
new_names.iter().any(|n| n == "photo-2024-01-15.jpg"),
"expected date detection: {new_names:?}"
);
}