use super::*;
fn request(path: &std::path::Path, edits: &[(&str, &str)]) -> CborValue {
CborValue::Map(vec![
(
CborValue::Text("path".to_owned()),
CborValue::Text(path.display().to_string()),
),
(
CborValue::Text("edits".to_owned()),
CborValue::Array(
edits
.iter()
.map(|(old_text, new_text)| {
CborValue::Map(vec![
(
CborValue::Text("oldText".to_owned()),
CborValue::Text((*old_text).to_owned()),
),
(
CborValue::Text("newText".to_owned()),
CborValue::Text((*new_text).to_owned()),
),
])
})
.collect(),
),
),
])
}
#[test]
fn replace_is_atomic_when_a_later_target_is_missing() {
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("source.txt");
std::fs::write(&path, "one\ntwo\n").expect("write source");
let mut world = ShellWorld::real();
let error = replace_file(
&request(&path, &[("one", "ONE"), ("missing", "MISSING")]),
&mut world,
)
.expect_err("missing target must fail");
assert_eq!(error.message, "each oldText must match exactly once");
assert_eq!(
std::fs::read_to_string(&path).expect("read source"),
"one\ntwo\n"
);
}
#[test]
fn replace_preserves_bom_and_untouched_mixed_line_endings() {
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("source.txt");
std::fs::write(&path, b"\xef\xbb\xbffirst\r\nsecond\rthird\n").expect("write source");
let mut world = ShellWorld::real();
replace_file(
&request(&path, &[("first\nsecond", "ONE\nTWO")]),
&mut world,
)
.expect("replace normalized target");
assert_eq!(
std::fs::read(&path).expect("read result"),
b"\xef\xbb\xbfONE\r\nTWO\rthird\n"
);
}
#[test]
fn replace_uses_nearby_crlf_for_a_target_without_an_ending() {
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("source.txt");
std::fs::write(&path, b"before\r\ntarget").expect("write source");
let mut world = ShellWorld::real();
replace_file(&request(&path, &[("target", "one\ntwo")]), &mut world).expect("replace target");
assert_eq!(
std::fs::read(&path).expect("read result"),
b"before\r\none\r\ntwo"
);
}
#[test]
fn replace_rejects_cross_entry_overlaps_without_writing() {
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("source.txt");
std::fs::write(&path, "abcdef").expect("write source");
let mut world = ShellWorld::real();
let duplicate = replace_file(&request(&path, &[("a", "A"), ("a", "A")]), &mut world)
.expect_err("duplicate targets overlap");
assert_eq!(duplicate.message, "replacement targets overlap");
assert_eq!(
std::fs::read_to_string(&path).expect("read source"),
"abcdef"
);
let mut world = ShellWorld::real();
let overlapping = replace_file(&request(&path, &[("abc", "A"), ("bcd", "B")]), &mut world)
.expect_err("overlapping targets must fail");
assert_eq!(overlapping.message, "replacement targets overlap");
assert_eq!(
std::fs::read_to_string(&path).expect("read source"),
"abcdef"
);
}
#[test]
fn replace_rejects_repeated_source_target_without_writing() {
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("source.txt");
std::fs::write(&path, b"aba").expect("write source");
let mut world = ShellWorld::real();
let error = replace_file(&request(&path, &[("a", "X")]), &mut world)
.expect_err("repeated target must fail");
assert_eq!(error.message, "each oldText must match exactly once");
assert_eq!(std::fs::read(&path).expect("read source"), b"aba");
}
#[test]
fn replace_rejects_overlapping_ascii_occurrences_without_writing() {
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("source.txt");
std::fs::write(&path, b"aaa").expect("write source");
let mut world = ShellWorld::real();
let error = replace_file(&request(&path, &[("aa", "X")]), &mut world)
.expect_err("overlapping target occurrences must fail");
assert_eq!(error.message, "each oldText must match exactly once");
assert_eq!(std::fs::read(&path).expect("read source"), b"aaa");
}
#[test]
fn replace_rejects_overlapping_multibyte_occurrences_without_writing() {
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("source.txt");
let source = "ééé".as_bytes();
std::fs::write(&path, source).expect("write source");
let mut world = ShellWorld::real();
let error = replace_file(&request(&path, &[("éé", "X")]), &mut world)
.expect_err("overlapping multibyte target occurrences must fail");
assert_eq!(error.message, "each oldText must match exactly once");
assert_eq!(std::fs::read(&path).expect("read source"), source);
}
#[test]
fn replace_rejects_overlap_after_line_ending_normalization_without_writing() {
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("source.txt");
let source = b"\r\n\r\n\r\n";
std::fs::write(&path, source).expect("write source");
let mut world = ShellWorld::real();
let error = replace_file(&request(&path, &[("\n\n", "X")]), &mut world)
.expect_err("normalized overlapping target occurrences must fail");
assert_eq!(error.message, "each oldText must match exactly once");
assert_eq!(std::fs::read(&path).expect("read source"), source);
}
#[test]
fn replace_is_atomic_when_a_later_target_is_ambiguous() {
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("source.txt");
std::fs::write(&path, b"one aaa").expect("write source");
let mut world = ShellWorld::real();
let error = replace_file(&request(&path, &[("one", "ONE"), ("aa", "X")]), &mut world)
.expect_err("later ambiguous target must fail");
assert_eq!(error.message, "each oldText must match exactly once");
assert_eq!(std::fs::read(&path).expect("read source"), b"one aaa");
}
#[test]
fn replace_accepts_unique_multibyte_target() {
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("source.txt");
std::fs::write(&path, "éé").expect("write source");
let mut world = ShellWorld::real();
replace_file(&request(&path, &[("éé", "X")]), &mut world)
.expect("unique multibyte target must replace");
assert_eq!(std::fs::read(&path).expect("read result"), b"X");
}
#[test]
fn replace_noop_has_no_diff_payload() {
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("source.txt");
std::fs::write(&path, "same\n").expect("write source");
let mut world = ShellWorld::real();
let output =
replace_file(&request(&path, &[("same", "same")]), &mut world).expect("no-op replacement");
assert_eq!(output.display.payload, None);
assert_eq!(
output.result,
CborValue::Map(vec![
(
CborValue::Text("edits".to_owned()),
CborValue::Integer(1.into()),
),
(
CborValue::Text("changed".to_owned()),
CborValue::Bool(false)
),
(
CborValue::Text("total_bytes".to_owned()),
CborValue::Integer(5.into()),
),
])
);
}
#[test]
fn replace_rejects_unknown_request_fields() {
let request = CborValue::Map(vec![
(
CborValue::Text("path".to_owned()),
CborValue::Text("x".to_owned()),
),
(
CborValue::Text("edits".to_owned()),
CborValue::Array(Vec::new()),
),
(
CborValue::Text("oldText".to_owned()),
CborValue::Text("legacy".to_owned()),
),
]);
let error = ReplaceRequest::parse(&request).expect_err("legacy field must fail");
assert_eq!(error.message, "request contains an unknown field");
}
#[test]
fn replace_rejects_invalid_utf8_without_writing() {
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("source.txt");
let source = b"before\xffafter";
std::fs::write(&path, source).expect("write source");
let mut world = ShellWorld::real();
let error = replace_file(&request(&path, &[("before", "after")]), &mut world)
.expect_err("invalid UTF-8 must fail");
assert_eq!(error.message, "file is not valid UTF-8");
assert_eq!(std::fs::read(&path).expect("read source"), source);
}
#[test]
fn replace_rejects_oversized_file_without_writing() {
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("source.txt");
let source = vec![b'x'; MAX_SAFE_FILE_READ_BYTES + 1];
std::fs::write(&path, &source).expect("write source");
let mut world = ShellWorld::real();
let error = replace_file(&request(&path, &[("x", "y")]), &mut world)
.expect_err("oversized file must fail");
assert_eq!(error.message, "file could not be read");
assert_eq!(std::fs::read(&path).expect("read source"), source);
}