#[cfg(unix)]
use std::{fmt::Write as _, os::unix::fs::PermissionsExt, path::PathBuf};
use std::{fs, path::Path};
use mdtablefix::{rewrite, rewrite_no_wrap};
use rstest::rstest;
use tempfile::tempdir;
const BROKEN: &str = "|A|B|\n|1|2|\n";
const FIXED: &str = "| A | B |\n| 1 | 2 |\n";
#[cfg(unix)]
const CHILD_OPERATION: &str = "MDTABLEFIX_WRITE_FAILURE_OPERATION";
#[cfg(unix)]
const CHILD_TARGET: &str = "MDTABLEFIX_WRITE_FAILURE_TARGET";
fn entry_names(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: &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: &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"
);
}
#[cfg(unix)]
fn large_table() -> String {
let mut text = String::from("|Name|Value|\n|--|--|\n");
for row in 0..200 {
let _ = writeln!(text, "|name-{row}|value-{row}|");
}
text
}
#[cfg(unix)]
fn rewrite_with(operation: &str, path: &Path) -> std::io::Result<()> {
match operation {
"rewrite" => rewrite(path),
"rewrite_no_wrap" => rewrite_no_wrap(path),
other => panic!("unknown rewrite operation {other}"),
}
}
#[rstest]
#[case(rewrite)]
#[case(rewrite_no_wrap)]
fn read_only_target_is_replaced(#[case] rewrite_fn: fn(&Path) -> std::io::Result<()>) {
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);
rewrite_fn(&target).expect("a read-only target must still be replaced");
assert_eq!(fs::read_to_string(&target).expect("read target"), FIXED);
assert_read_only(&target);
assert_eq!(
entry_names(dir.path()),
vec!["sample.md"],
"a successful rewrite must leave no temporary file behind"
);
}
#[cfg(unix)]
#[test]
#[expect(
clippy::disallowed_methods,
reason = "composition root: the parent re-execs this binary, so its environment is the only \
channel in"
)]
fn write_failure_child() {
let Ok(operation) = std::env::var(CHILD_OPERATION) else {
return;
};
let target = PathBuf::from(std::env::var(CHILD_TARGET).expect("the child's target path"));
let error =
rewrite_with(&operation, &target).expect_err("the file-size cap must refuse the write");
assert_eq!(
error.raw_os_error(),
Some(libc::EFBIG),
"the write must be refused for outgrowing the cap: {error:?}"
);
}
#[cfg(unix)]
#[rstest]
#[case("rewrite")]
#[case("rewrite_no_wrap")]
fn write_failure_leaves_the_original_byte_identical(#[case] operation: &str) {
let dir = tempdir().expect("create temporary directory");
let target = dir.path().join("sample.md");
let original = large_table();
fs::write(&target, &original).expect("write fixture");
let output = std::process::Command::new("sh")
.arg("-c")
.arg("trap '' XFSZ; ulimit -f 1; exec \"$1\" --exact write_failure_child --nocapture")
.arg("sh")
.arg(std::env::current_exe().expect("path to this test binary"))
.env(CHILD_OPERATION, operation)
.env(CHILD_TARGET, &target)
.env("LLVM_PROFILE_FILE", "/dev/null")
.output()
.expect("run the rewrite in a child under a file-size limit");
assert!(
String::from_utf8_lossy(&output.stdout).contains("1 passed"),
"the child must run the write-failure test and no other: {output:?}"
);
assert!(
output.status.success(),
"the child must observe a refused write and exit cleanly: {output:?}"
);
assert_eq!(
fs::read_to_string(&target).expect("read target"),
original,
"a failed write must leave the original byte-identical"
);
assert_eq!(
entry_names(dir.path()),
vec!["sample.md"],
"a failed write must leave no temporary file behind"
);
}