use std::fs;
#[cfg(unix)]
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use assert_cmd::Command;
#[cfg(unix)]
use predicates::str::contains;
use tempfile::tempdir;
#[cfg(unix)]
#[path = "in_place_atomic/failure.rs"]
mod failure;
const BROKEN: &str = "|A|B|\n|1|2|\n";
const FIXED: &str = "| A | B |\n| 1 | 2 |\n";
fn in_place(path: &std::path::Path) -> assert_cmd::assert::Assert { in_place_all(&[path]) }
fn in_place_all(paths: &[&std::path::Path]) -> assert_cmd::assert::Assert {
let mut command =
Command::cargo_bin("mdtablefix").expect("failed to create cargo command for mdtablefix");
command.arg("--in-place");
for path in paths {
command.arg(path);
}
command.assert()
}
fn entry_names(path: &std::path::Path) -> Vec<String> {
let mut names: Vec<String> = fs::read_dir(path)
.expect("read directory")
.map(|entry| {
entry
.expect("read directory entry")
.file_name()
.to_string_lossy()
.into_owned()
})
.collect();
names.sort();
names
}
fn set_read_only(path: &std::path::Path) {
let mut permissions = fs::metadata(path).expect("read metadata").permissions();
#[cfg(unix)]
permissions.set_mode(0o444);
#[cfg(not(unix))]
permissions.set_readonly(true);
fs::set_permissions(path, permissions).expect("make the target read-only");
}
fn assert_read_only(path: &std::path::Path) {
let permissions = fs::metadata(path).expect("read metadata").permissions();
assert!(
permissions.readonly(),
"the read-only attribute must survive the swap"
);
#[cfg(unix)]
assert_eq!(
permissions.mode() & 0o777,
0o444,
"the read-only mode must survive the swap"
);
}
#[test]
fn in_place_rewrites_and_leaves_no_temporary_file() {
let dir = tempdir().expect("create temporary directory");
let target = dir.path().join("sample.md");
fs::write(&target, BROKEN).expect("write fixture");
in_place(&target).success().stdout("").stderr("");
assert_eq!(fs::read_to_string(&target).expect("read target"), FIXED);
assert_eq!(
entry_names(dir.path()),
vec!["sample.md"],
"a successful rewrite must leave no temporary file behind"
);
}
#[cfg(unix)]
#[test]
fn in_place_preserves_file_mode() {
let dir = tempdir().expect("create temporary directory");
let target = dir.path().join("sample.md");
fs::write(&target, BROKEN).expect("write fixture");
fs::set_permissions(&target, fs::Permissions::from_mode(0o640)).expect("set mode");
in_place(&target).success();
let mode = fs::metadata(&target)
.expect("read metadata")
.permissions()
.mode()
& 0o777;
assert_eq!(mode, 0o640, "the swap must preserve the original mode");
}
#[test]
fn in_place_replaces_read_only_file() {
let dir = tempdir().expect("create temporary directory");
let target = dir.path().join("sample.md");
fs::write(&target, BROKEN).expect("write fixture");
set_read_only(&target);
in_place(&target).success().stderr("");
assert_eq!(fs::read_to_string(&target).expect("read target"), FIXED);
assert_read_only(&target);
assert_eq!(
entry_names(dir.path()),
vec!["sample.md"],
"replacing a read-only target must leave no temporary file behind"
);
}
#[cfg(unix)]
#[test]
fn in_place_retries_past_a_stale_temporary_file() {
let dir = tempdir().expect("create temporary directory");
let target = dir.path().join("sample.md");
fs::write(&target, BROKEN).expect("write fixture");
let status = std::process::Command::new("sh")
.arg("-c")
.arg("touch sample.md.mdtablefix-$$-0.tmp; exec \"$1\" --in-place sample.md")
.arg("sh")
.arg(env!("CARGO_BIN_EXE_mdtablefix"))
.current_dir(dir.path())
.status()
.expect("run mdtablefix through sh");
assert!(
status.success(),
"a stale temporary name must not fail the run"
);
assert_eq!(fs::read_to_string(&target).expect("read target"), FIXED);
let names = entry_names(dir.path());
assert_eq!(
names.len(),
2,
"only the target and the stale file may remain: {names:?}"
);
assert_eq!(names[0], "sample.md", "unexpected entries: {names:?}");
assert!(
names[1].starts_with("sample.md.mdtablefix-") && names[1].ends_with("-0.tmp"),
"the stale file must survive untouched: {names:?}"
);
}
#[cfg(unix)]
#[test]
fn in_place_declines_symlinked_target() {
let dir = tempdir().expect("create temporary directory");
let real = dir.path().join("real.md");
let link = dir.path().join("link.md");
fs::write(&real, BROKEN).expect("write fixture");
std::os::unix::fs::symlink("real.md", &link).expect("create symlink");
in_place(&link).code(2).stderr(contains("symlink"));
assert_eq!(
fs::read_to_string(&real).expect("read real file"),
BROKEN,
"declining a symlink must leave its target untouched"
);
assert!(
fs::symlink_metadata(&link)
.expect("read link metadata")
.file_type()
.is_symlink(),
"the symlink itself must survive"
);
assert_eq!(entry_names(dir.path()), vec!["link.md", "real.md"]);
}
#[cfg(unix)]
#[test]
fn in_place_leaves_a_clean_file_untouched() {
let dir = tempdir().expect("create temporary directory");
let target = dir.path().join("clean.md");
fs::write(&target, FIXED).expect("write fixture");
let before = fs::metadata(&target).expect("read metadata before");
in_place(&target).success();
let after = fs::metadata(&target).expect("read metadata after");
assert_eq!(
before.ino(),
after.ino(),
"a clean file must not be replaced through a temporary"
);
assert_eq!(
before.mtime(),
after.mtime(),
"a clean file's modification time must not move"
);
assert_eq!(
before.mtime_nsec(),
after.mtime_nsec(),
"a clean file's modification time must not move"
);
assert_eq!(fs::read_to_string(&target).expect("read fixture"), FIXED);
assert_eq!(entry_names(dir.path()), vec!["clean.md"]);
}
#[cfg(unix)]
#[test]
fn in_place_accepts_a_symlink_to_a_clean_file() {
let dir = tempdir().expect("create temporary directory");
let real = dir.path().join("real.md");
let link = dir.path().join("link.md");
fs::write(&real, FIXED).expect("write fixture");
std::os::unix::fs::symlink("real.md", &link).expect("create symlink");
in_place(&link).success();
assert_eq!(fs::read_to_string(&real).expect("read real file"), FIXED);
assert!(
fs::symlink_metadata(&link)
.expect("read link metadata")
.file_type()
.is_symlink(),
"the symlink itself must survive"
);
assert_eq!(entry_names(dir.path()), vec!["link.md", "real.md"]);
}