use super::*;
use crate::refactor::cli::Args;
use crate::refactor::rename_engine::RenameEngine;
use std::fs;
fn args(root: &Path, pattern: &str, substitute: &str) -> Args {
Args {
root_dir: root.to_path_buf(),
pattern: Some(pattern.to_string()),
substitute: Some(substitute.to_string()),
patterns_file: None,
assume_yes: true,
verbose: false,
follow_symlinks: false,
backup: false,
files_only: false,
dirs_only: false,
names_only: false,
content_only: false,
max_depth: 0,
exclude_patterns: Vec::new(),
include_patterns: Vec::new(),
format: crate::refactor::cli::OutputFormat::Json,
threads: 1,
progress: crate::refactor::cli::ProgressMode::Never,
ignore_case: false,
use_regex: false,
include_hidden: false,
binary_names: false,
gitignore: false,
}
}
fn tree() -> tempfile::TempDir {
let dir = tempfile::TempDir::new().expect("temp dir");
let root = dir.path();
fs::create_dir_all(root.join("widget_parts")).expect("dir");
fs::write(root.join("widget_parts/widget_core.txt"), "widget and widget\n").expect("file");
fs::write(root.join("untouched.txt"), "nothing here\n").expect("other");
dir
}
#[test]
fn test11072_a_plan_names_every_change_and_writes_nothing() {
let dir = tree();
let engine = RenameEngine::new(args(dir.path(), "widget", "gadget")).expect("engine");
let (plan, _, _) = engine.plan().expect("a plan");
assert_eq!(plan.files_edited, 1);
assert_eq!(plan.replacements, 2, "both occurrences in the one file");
assert_eq!(plan.files_renamed, 1);
assert_eq!(plan.directories_renamed, 1);
assert!(dir.path().join("widget_parts/widget_core.txt").exists());
assert_eq!(
fs::read_to_string(dir.path().join("widget_parts/widget_core.txt")).expect("read"),
"widget and widget\n"
);
}
#[test]
fn test11073_a_plan_is_paginated_without_running_off_the_end() {
let dir = tree();
let engine = RenameEngine::new(args(dir.path(), "widget", "gadget")).expect("engine");
let (plan, _, _) = engine.plan().expect("a plan");
let total = plan.total();
assert!(total >= 3, "content edit, file rename, directory rename");
assert!(plan.page(total, 50).is_empty());
assert!(plan.page(total + 500, 50).is_empty());
let mut walked = Vec::new();
let mut offset = 0;
while !plan.page(offset, 2).is_empty() {
walked.extend(plan.page(offset, 2).iter().cloned());
offset += 2;
}
assert_eq!(walked, plan.changes, "paging loses and repeats nothing");
}
#[test]
fn test11074_the_same_tree_plans_the_same_way_twice() {
let dir = tree();
let engine = RenameEngine::new(args(dir.path(), "widget", "gadget")).expect("engine");
let (first, _, _) = engine.plan().expect("a plan");
let (second, _, _) = engine.plan().expect("a plan");
assert_eq!(first.changes, second.changes);
}
#[test]
fn test11075_applying_a_stale_plan_is_refused_and_writes_nothing() {
let dir = tree();
let engine = RenameEngine::new(args(dir.path(), "widget", "gadget")).expect("engine");
let (reviewed, _, _) = engine.plan().expect("a plan");
fs::write(dir.path().join("widget_extra.txt"), "widget\n").expect("new file");
let refused = engine.apply(&reviewed).expect_err("a stale plan is refused");
let message = refused.to_string();
assert!(
message.contains("changed since"),
"the refusal says why: {message}"
);
assert_eq!(
fs::read_to_string(dir.path().join("widget_parts/widget_core.txt")).expect("read"),
"widget and widget\n"
);
}
#[test]
fn test11076_applying_a_current_plan_carries_it_out() {
let dir = tree();
let engine = RenameEngine::new(args(dir.path(), "widget", "gadget")).expect("engine");
let (reviewed, _, _) = engine.plan().expect("a plan");
engine.apply(&reviewed).expect("a current plan applies");
assert!(dir.path().join("gadget_parts/gadget_core.txt").exists());
assert_eq!(
fs::read_to_string(dir.path().join("gadget_parts/gadget_core.txt")).expect("read"),
"gadget and gadget\n"
);
assert!(!dir.path().join("widget_parts").exists());
}
#[test]
fn test11077_paths_are_reported_as_the_reader_knows_them() {
let dir = tree();
let engine = RenameEngine::new(args(dir.path(), "widget", "gadget")).expect("engine");
let (plan, _, _) = engine.plan().expect("a plan");
for change in &plan.changes {
assert!(
!change.path().starts_with('/'),
"change path is relative: {}",
change.path()
);
}
}
mod barriers {
use super::*;
use crate::refactor::preview::Barrier;
#[test]
fn test11103_a_binary_is_reported_not_silently_skipped() {
let dir = tempfile::TempDir::new().expect("temp dir");
fs::write(dir.path().join("plain.txt"), "widget\n").expect("text");
fs::write(dir.path().join("blob.bin"), [0xff, 0xfe, 0x00, 0x01, 0xc3, 0x28]).expect("blob");
let engine = RenameEngine::new(args(dir.path(), "widget", "gadget")).expect("engine");
let (plan, _, _) = engine.plan().expect("a plan");
assert_eq!(plan.files_edited, 1);
for barrier in &plan.unreadable {
assert_ne!(barrier.barrier, Barrier::PermissionDenied, "{barrier:?}");
}
}
#[cfg(unix)]
#[test]
fn test11104_a_directory_that_cannot_be_read_is_named_and_classified() {
use std::os::unix::fs::PermissionsExt;
if unsafe { libc::geteuid() } == 0 {
return;
}
let dir = tempfile::TempDir::new().expect("temp dir");
fs::write(dir.path().join("visible.txt"), "widget\n").expect("text");
let closed = dir.path().join("closed");
fs::create_dir(&closed).expect("dir");
fs::write(closed.join("hidden.txt"), "widget\n").expect("inner");
fs::set_permissions(&closed, fs::Permissions::from_mode(0o000)).expect("chmod");
let engine = RenameEngine::new(args(dir.path(), "widget", "gadget")).expect("engine");
let planned = engine.plan();
fs::set_permissions(&closed, fs::Permissions::from_mode(0o755)).expect("restore");
let (plan, _, _) = planned.expect("one closed directory does not end the refactor");
assert_eq!(plan.files_edited, 1, "the readable file is still found");
let denied: Vec<_> = plan
.unreadable
.iter()
.filter(|u| u.barrier == Barrier::PermissionDenied)
.collect();
assert!(
!denied.is_empty(),
"the closed directory is reported: {:?}",
plan.unreadable
);
assert!(
denied[0].path.contains("closed"),
"and named: {}",
denied[0].path
);
assert!(denied[0].barrier.is_permission());
}
#[cfg(unix)]
#[test]
fn test11105_a_link_pointing_at_nothing_is_its_own_case() {
let dir = tempfile::TempDir::new().expect("temp dir");
fs::write(dir.path().join("real.txt"), "widget\n").expect("text");
std::os::unix::fs::symlink(dir.path().join("gone.txt"), dir.path().join("dangling"))
.expect("symlink");
let mut with_links = args(dir.path(), "widget", "gadget");
with_links.follow_symlinks = true;
let engine = RenameEngine::new(with_links).expect("engine");
let (plan, _, _) = engine.plan().expect("a dangling link does not end the refactor");
assert_eq!(plan.files_edited, 1);
let broken: Vec<_> = plan
.unreadable
.iter()
.filter(|u| u.barrier == Barrier::BrokenLink)
.collect();
assert!(!broken.is_empty(), "reported: {:?}", plan.unreadable);
assert!(!broken[0].barrier.is_permission());
}
#[test]
fn test11106_every_case_says_what_it_is() {
for barrier in [
Barrier::PermissionDenied,
Barrier::BrokenLink,
Barrier::LinkLoop,
Barrier::Vanished,
Barrier::NotText,
Barrier::Other,
] {
assert!(!barrier.describe().is_empty(), "{barrier:?}");
}
assert!(Barrier::PermissionDenied.is_permission());
for other in [
Barrier::BrokenLink,
Barrier::LinkLoop,
Barrier::Vanished,
Barrier::NotText,
Barrier::Other,
] {
assert!(!other.is_permission(), "{other:?}");
}
}
}