use super::{
AUDIT_FILE, AUDIT_HISTORY_BYTES, AuditOutcome, AuditRecord, ROTATE_AT_BYTES, RotationStep, append_line, audit_line,
move_aside_between,
};
use crate::command_test_support::{fixture, remove};
use crate::commit_timing::TIMING_FILE;
use crate::rule_linked_worktree::GuardedCommand;
use std::path::{Path, PathBuf};
fn kept_generations() -> u64 {
return AUDIT_HISTORY_BYTES / ROTATE_AT_BYTES;
}
fn full_file(lines: &[String]) -> Vec<u8> {
let mut bytes: Vec<u8> = Vec::new();
for line in lines {
bytes.extend_from_slice(line.as_bytes());
bytes.push(b'\n');
}
let room: usize = ROTATE_AT_BYTES as usize - bytes.len();
bytes.extend(std::iter::repeat_n(b'x', room - 1));
bytes.push(b'\n');
return bytes;
}
fn marker(name: &str) -> String {
return format!("{{\"marker\":\"{name}\"}}");
}
fn write_generation(target: &Path, number: u64, lines: &[String]) {
let mut name: std::ffi::OsString = target.as_os_str().to_os_string();
name.push(format!(".{number}"));
std::fs::write(PathBuf::from(name), full_file(lines)).expect("a generation");
}
fn saved_conflict_line(root: &Path, copies: &[String]) -> String {
let place: (PathBuf, &'static str) = (root.join(".git"), "main");
let arguments = crate::command_test_support::os_arguments(&["checkout", "--theirs", "--", "a.rs"]);
let line: String = audit_line(&AuditRecord {
time_millis: 1_700_000_000_000,
process: 1,
parent: 1,
form: GuardedCommand::CheckoutPaths,
outcome: AuditOutcome::SavedConflict,
place: &place,
git_dir: place.0.as_path(),
directory: root,
arguments: arguments.as_slice(),
dirty: None,
copies,
});
return line.trim_end().to_string();
}
fn existing_copy(root: &Path, name: &str) -> String {
let place: PathBuf = root.join(name);
std::fs::write(&place, b"the only content of a replaced file\n").expect("a saved copy");
return format!("a.rs => {}", place.display());
}
fn rotate_once(target: &Path, name: &str) {
std::fs::write(target, full_file(&[marker(name)])).expect("a full live file");
append_line(target, b"{\"live\":1}\n").expect("the append that rotates");
}
fn generations(target: &Path) -> Vec<u64> {
let directory: &Path = target.parent().expect("a directory");
let prefix: String = format!("{}.", target.file_name().expect("a name").to_string_lossy());
let mut numbers: Vec<u64> = std::fs::read_dir(directory)
.expect("directory")
.filter_map(|entry| {
let name: String = entry.expect("entry").file_name().to_string_lossy().into_owned();
return name.strip_prefix(prefix.as_str()).and_then(|rest| { return rest.parse::<u64>().ok(); });
})
.collect();
numbers.sort_unstable();
return numbers;
}
fn first_line_of(target: &Path, number: u64) -> String {
let mut name: std::ffi::OsString = target.as_os_str().to_os_string();
name.push(format!(".{number}"));
let text: String = String::from_utf8_lossy(&std::fs::read(PathBuf::from(name)).expect("a generation")).into_owned();
return text.lines().next().unwrap_or_default().to_string();
}
fn private_names(root: &Path) -> Vec<String> {
return std::fs::read_dir(root)
.expect("directory")
.map(|entry| { return entry.expect("entry").file_name().to_string_lossy().into_owned(); })
.filter(|name| { return name.contains(".moving-"); })
.collect();
}
#[test]
fn each_rotation_of_the_audit_file_keeps_its_own_generation() {
let root: PathBuf = fixture("generations-each");
let target: PathBuf = root.join(AUDIT_FILE);
for name in ["first", "second", "third"] {
rotate_once(target.as_path(), name);
}
assert_eq!(generations(target.as_path()), vec![1, 2, 3]);
assert_eq!(first_line_of(target.as_path(), 1), marker("first"));
assert_eq!(first_line_of(target.as_path(), 3), marker("third"));
assert_eq!(std::fs::read(&target).expect("live file"), b"{\"live\":1}\n", "the live file holds only what came after");
remove(root.as_path());
}
#[test]
fn over_the_bound_the_oldest_generation_that_names_no_copy_goes() {
let root: PathBuf = fixture("generations-bound");
let target: PathBuf = root.join(AUDIT_FILE);
let extra: u64 = 3;
for number in 1..=kept_generations() + extra - 1 {
write_generation(target.as_path(), number, &[marker(&format!("g{number}"))]);
}
rotate_once(target.as_path(), "newest");
let newest: u64 = kept_generations() + extra;
let expected: Vec<u64> = (extra + 1..=newest).collect();
assert_eq!(generations(target.as_path()), expected, "the oldest {extra} went and {} stay", kept_generations());
assert_eq!(first_line_of(target.as_path(), newest), marker("newest"));
remove(root.as_path());
}
#[test]
fn a_generation_that_names_a_copy_on_disk_is_never_removed() {
let root: PathBuf = fixture("generations-pinned");
let target: PathBuf = root.join(AUDIT_FILE);
let extra: u64 = 3;
let copy: String = existing_copy(root.as_path(), "copy-a");
write_generation(target.as_path(), 1, &[marker("g1"), saved_conflict_line(root.as_path(), &[copy])]);
for number in 2..=kept_generations() + extra - 1 {
write_generation(target.as_path(), number, &[marker(&format!("g{number}"))]);
}
rotate_once(target.as_path(), "newest");
let newest: u64 = kept_generations() + extra;
let mut expected: Vec<u64> = vec![1];
expected.extend(extra + 1..=newest);
assert_eq!(generations(target.as_path()), expected, "generation 1 stays; the bound is spent on the others");
remove(root.as_path());
}
#[test]
fn a_generation_that_names_a_deleted_copy_goes_like_any_other() {
let root: PathBuf = fixture("generations-deleted");
let target: PathBuf = root.join(AUDIT_FILE);
let extra: u64 = 3;
let copy: String = existing_copy(root.as_path(), "copy-b");
write_generation(target.as_path(), 1, &[marker("g1"), saved_conflict_line(root.as_path(), &[copy])]);
std::fs::remove_file(root.join("copy-b")).expect("the copy is deleted");
for number in 2..=kept_generations() + extra - 1 {
write_generation(target.as_path(), number, &[marker(&format!("g{number}"))]);
}
rotate_once(target.as_path(), "newest");
let newest: u64 = kept_generations() + extra;
assert_eq!(generations(target.as_path()), (extra + 1..=newest).collect::<Vec<u64>>());
remove(root.as_path());
}
#[test]
fn when_only_pinned_generations_exceed_the_bound_none_is_removed() {
let root: PathBuf = fixture("generations-all-pinned");
let target: PathBuf = root.join(AUDIT_FILE);
let extra: u64 = 3;
for number in 1..=kept_generations() + extra - 1 {
let copy: String = existing_copy(root.as_path(), &format!("copy-{number}"));
write_generation(target.as_path(), number, &[marker(&format!("g{number}")), saved_conflict_line(root.as_path(), &[copy])]);
}
rotate_once(target.as_path(), "newest");
let newest: u64 = kept_generations() + extra;
assert_eq!(generations(target.as_path()), (1..=newest).collect::<Vec<u64>>(), "all stay, over the bound");
remove(root.as_path());
}
#[test]
fn lines_that_name_nothing_on_disk_do_not_pin_a_generation() {
let root: PathBuf = fixture("generations-unpinned-lines");
let target: PathBuf = root.join(AUDIT_FILE);
let extra: u64 = 3;
let pruned: String = format!(
"{{\"schemaVersion\":1,\"form\":\"PruneDiscarded\",\"outcome\":\"pruned\",\"path\":{},\"copies\":[\"a.rs\"]}}",
crate::json_record::quote(&root.to_string_lossy())
);
write_generation(
target.as_path(),
1,
&[marker("g1"), saved_conflict_line(root.as_path(), &[String::from("a.rs => none")]), pruned],
);
for number in 2..=kept_generations() + extra - 1 {
write_generation(target.as_path(), number, &[marker(&format!("g{number}"))]);
}
rotate_once(target.as_path(), "newest");
let newest: u64 = kept_generations() + extra;
assert_eq!(generations(target.as_path()), (extra + 1..=newest).collect::<Vec<u64>>());
remove(root.as_path());
}
#[test]
fn a_file_left_by_a_killed_rotation_is_counted_and_never_removed() {
let root: PathBuf = fixture("generations-killed");
let target: PathBuf = root.join(AUDIT_FILE);
let extra: u64 = 2;
std::fs::write(&target, full_file(&[marker("killed")])).expect("a full live file");
let seen_full: std::fs::File = std::fs::OpenOptions::new().read(true).append(true).open(&target).expect("open");
let killed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = move_aside_between(target.as_path(), &seen_full, &mut |step| {
if step == RotationStep::AfterMove {
panic!("killed after the move");
}
});
}));
assert!(killed.is_err(), "the stand-in kill happened");
assert_eq!(private_names(root.as_path()).len(), 1, "the file waits at its private name");
for number in 1..=kept_generations() + extra - 1 {
write_generation(target.as_path(), number, &[marker(&format!("g{number}"))]);
}
rotate_once(target.as_path(), "newest");
let stranded: Vec<String> = private_names(root.as_path());
assert_eq!(stranded.len(), 1, "the file taken by the killed writer is still there");
let text: String = String::from_utf8_lossy(&std::fs::read(root.join(&stranded[0])).expect("kept file")).into_owned();
assert_eq!(text.lines().next(), Some(marker("killed").as_str()), "with its lines");
let numbered: Vec<u64> = generations(target.as_path());
let newest: u64 = kept_generations() + extra;
assert_eq!(numbered.last(), Some(&newest));
assert!(numbered.len() < usize::try_from(newest).expect("small"), "its bytes took part of the bound, so older generations went: {numbered:?}");
remove(root.as_path());
}
#[test]
fn a_second_name_left_by_a_killed_rotation_is_removed_and_the_generation_kept() {
let root: PathBuf = fixture("generations-two-names");
let target: PathBuf = root.join(AUDIT_FILE);
write_generation(target.as_path(), 1, &[marker("g1")]);
std::fs::hard_link(root.join(format!("{AUDIT_FILE}.1")), root.join(format!("{AUDIT_FILE}.moving-4242-0"))).expect("second name");
rotate_once(target.as_path(), "newest");
assert_eq!(private_names(root.as_path()), Vec::<String>::new(), "the extra name is gone");
assert_eq!(generations(target.as_path()), vec![1, 2]);
assert_eq!(first_line_of(target.as_path(), 1), marker("g1"), "the generation keeps every line");
remove(root.as_path());
}
#[test]
fn a_number_taken_by_another_writer_is_never_replaced() {
let root: PathBuf = fixture("generations-number-race");
let target: PathBuf = root.join(AUDIT_FILE);
std::fs::write(&target, full_file(&[marker("ours")])).expect("a full live file");
let seen_full: std::fs::File = std::fs::OpenOptions::new().read(true).append(true).open(&target).expect("open");
move_aside_between(target.as_path(), &seen_full, &mut |step| {
if step == RotationStep::AfterMove {
write_generation(target.as_path(), 1, &[marker("theirs")]);
}
})
.expect("moved");
assert_eq!(generations(target.as_path()), vec![1, 2]);
assert_eq!(first_line_of(target.as_path(), 1), marker("theirs"));
assert_eq!(first_line_of(target.as_path(), 2), marker("ours"));
remove(root.as_path());
}
#[test]
fn an_audit_rotation_that_fails_does_not_drop_the_record() {
use std::os::unix::fs::PermissionsExt;
let root: PathBuf = fixture("generations-fails");
let target: PathBuf = root.join(AUDIT_FILE);
std::fs::write(&target, full_file(&[marker("full")])).expect("a full live file");
std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o500)).expect("a directory that takes no new name");
let appended = append_line(target.as_path(), b"{\"kept\":1}\n");
std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o700)).expect("writable again");
appended.expect("the record is appended whatever the rotation did");
let text: Vec<u8> = std::fs::read(&target).expect("target");
assert!(text.ends_with(b"{\"kept\":1}\n"));
assert!(text.starts_with(&full_file(&[marker("full")])), "the history is still in the file at the name");
remove(root.as_path());
}
#[test]
fn a_timing_log_keeps_one_generation() {
for file in [TIMING_FILE, crate::add_timing::ADD_TIMING_FILE, crate::push_trigger_log::TRIGGER_FILE] {
let root: PathBuf = fixture(&format!("generations-timing-{file}"));
let target: PathBuf = root.join(file);
for name in ["first", "second", "third"] {
rotate_once(target.as_path(), name);
}
assert_eq!(generations(target.as_path()), vec![1], "{file} keeps one generation");
assert_eq!(first_line_of(target.as_path(), 1), marker("third"), "{file}: the newest rotation is the one kept");
remove(root.as_path());
}
}
#[test]
fn a_line_that_may_name_a_copy_keeps_its_generation() {
let root: PathBuf = fixture("generations-doubtful");
let target: PathBuf = root.join(AUDIT_FILE);
let extra: u64 = 3;
let torn: String = String::from("{\"schemaVersion\":1,\"outcome\":\"saved-conflict\",\"copies\":[\"a.rs => /where/it/we");
let unshowable: String = saved_conflict_line(root.as_path(), &[String::from("a.rs => /bytes/that/are/\u{FFFD}/in/the/path")]);
write_generation(target.as_path(), 1, &[marker("g1"), torn]);
write_generation(target.as_path(), 2, &[marker("g2"), unshowable]);
for number in 3..=kept_generations() + extra - 1 {
write_generation(target.as_path(), number, &[marker(&format!("g{number}"))]);
}
rotate_once(target.as_path(), "newest");
let newest: u64 = kept_generations() + extra;
let numbered: Vec<u64> = generations(target.as_path());
assert!(numbered.starts_with(&[1, 2]), "both doubtful generations stay: {numbered:?}");
assert_eq!(numbered.last(), Some(&newest));
remove(root.as_path());
}