use super::*;
use crate::test_support::{fixture, remove};
use std::cell::RefCell;
fn directory(name: &str) -> (PathBuf, PathBuf, PathBuf) {
let root: PathBuf = fixture(name);
let live: PathBuf = root.join("index");
let built: PathBuf = root.join("index.cli-git-7");
std::fs::write(&live, b"live").expect("live");
std::fs::write(&built, b"copy").expect("copy");
return (root, live, built);
}
fn names(root: &Path) -> Vec<String> {
let mut found: Vec<String> = std::fs::read_dir(root).expect("directory").map(|entry| { return entry.expect("entry").file_name().to_string_lossy().into_owned(); }).collect();
found.sort();
return found;
}
fn mode_of(path: &Path) -> u32 {
return std::fs::metadata(path).expect("metadata").permissions().mode() & 0o7777;
}
#[test]
fn the_private_copy_has_a_directory_of_its_own_beside_the_live_index() {
let root: PathBuf = fixture("index-reset-publish-copy-directory");
let live: PathBuf = root.join("index");
std::fs::write(&live, b"live").expect("live");
let lock = crate::service_index_lock::try_acquire(live.as_path(), 7).expect("the lock is free");
let first = crate::service_land_follow::PrivateCopy::make(&lock, 7).expect("a copy");
let second = crate::service_land_follow::PrivateCopy::make(&lock, 7).expect("another copy of the same batch");
assert_eq!(first.directory().parent(), Some(root.as_path()));
assert_ne!(first.directory(), second.directory());
assert_eq!(std::fs::read(first.path()).expect("copy"), b"live".to_vec());
drop(first);
drop(second);
drop(lock);
remove(root.as_path());
}
#[test]
fn shared_modes_follow_calc_shared_perm() {
assert_eq!(shared_mode(0o660, 0o600), 0o660);
assert_eq!(shared_mode(0o664, 0o600), 0o664);
assert_eq!(shared_mode(0o660, 0o644), 0o664);
assert_eq!(shared_mode(-0o640, 0o600), 0o640);
assert_eq!(shared_mode(-0o640, 0o666), 0o640);
assert_eq!(shared_mode(0o660, 0o400), 0o440);
assert_eq!(shared_mode(0o660, 0o700), 0o770);
}
#[test]
fn the_mode_is_set_before_any_byte_is_written() {
if crate::umask_child_test_support::ran_in_child(module_path!(), "the_mode_is_set_before_any_byte_is_written", 0o022) {
return;
}
for (shared, expected) in [(-0o640, 0o640), (0o660, 0o664), (0o664, 0o664), (-0o600, 0o600)] {
let (root, _live, built) = directory("publish-order");
let seen: RefCell<Vec<(&str, u32, u64)>> = RefCell::new(Vec::new());
let chmod = |file: &std::fs::File, mode: u32| -> std::io::Result<()> {
seen.borrow_mut().push(("chmod", file.metadata()?.permissions().mode() & 0o7777, file.metadata()?.len()));
return (SYSTEM.chmod)(file, mode);
};
let write = |file: &mut std::fs::File, bytes: &[u8]| -> std::io::Result<usize> {
seen.borrow_mut().push(("write", file.metadata()?.permissions().mode() & 0o7777, file.metadata()?.len()));
return (SYSTEM.write)(file, bytes);
};
let steps: Steps<'_> = Steps { write: &write, chmod: &chmod, ..SYSTEM };
publish_with(&steps, built.as_path(), b"index bytes", shared).expect("published");
let order = seen.into_inner();
assert_eq!(order[0], ("chmod", 0o644, 0), "shared {shared:o}");
assert_eq!(order[1], ("write", expected, 0), "shared {shared:o}");
assert_eq!(mode_of(built.as_path()), expected, "shared {shared:o}");
remove(root.as_path());
}
}
#[test]
fn short_writes_publish_the_whole_file() {
let (root, _live, built) = directory("publish-short");
let bytes: Vec<u8> = (0..1000u32).map(|number| { return (number % 253) as u8; }).collect();
let write = |file: &mut std::fs::File, part: &[u8]| -> std::io::Result<usize> { return (SYSTEM.write)(file, &part[..part.len().min(3)]); };
publish_with(&Steps { write: &write, ..SYSTEM }, built.as_path(), bytes.as_slice(), 0).expect("published");
assert_eq!(std::fs::read(&built).expect("target"), bytes);
remove(root.as_path());
}
#[test]
fn every_failed_step_leaves_the_target_and_no_temporary_file() {
let broken = || -> std::io::Error { return std::io::Error::other("injected"); };
let calls: RefCell<u32> = RefCell::new(0);
let fails_after_a_while = |file: &mut std::fs::File, part: &[u8]| -> std::io::Result<usize> {
*calls.borrow_mut() += 1;
if *calls.borrow() > 2 {
return Err(broken());
}
return (SYSTEM.write)(file, &part[..part.len().min(5)]);
};
let takes_nothing = |_: &mut std::fs::File, _: &[u8]| -> std::io::Result<usize> { return Ok(0); };
let no_chmod = |_: &std::fs::File, _: u32| -> std::io::Result<()> { return Err(broken()); };
let no_sync = |_: &std::fs::File| -> std::io::Result<()> { return Err(broken()); };
let no_rename = |_: &Path, _: &Path| -> std::io::Result<()> { return Err(broken()); };
let no_mode = |_: &std::fs::File| -> std::io::Result<u32> { return Err(broken()); };
let cases: Vec<(&str, Steps<'_>, i32)> = vec![
("a write that fails midway", Steps { write: &fails_after_a_while, ..SYSTEM }, 0),
("a write that takes nothing", Steps { write: &takes_nothing, ..SYSTEM }, 0),
("a mode that cannot be set", Steps { chmod: &no_chmod, ..SYSTEM }, 0o660),
("a flush that fails", Steps { sync: &no_sync, ..SYSTEM }, 0),
("a rename that fails", Steps { rename: &no_rename, ..SYSTEM }, 0),
("a mode that cannot be read", Steps { mode: &no_mode, ..SYSTEM }, 0o660),
];
for (label, steps, shared) in cases {
let (root, _live, built) = directory("publish-failure-steps");
*calls.borrow_mut() = 0;
let before: Vec<String> = names(root.as_path());
assert!(publish_with(&steps, built.as_path(), b"a long enough payload to need several writes", shared).is_err(), "{label}");
assert_eq!(std::fs::read(&built).expect("target"), b"copy".to_vec(), "{label}: the target changed");
assert_eq!(names(root.as_path()), before, "{label}: a file was left");
remove(root.as_path());
}
}
#[test]
fn a_target_that_cannot_be_replaced_leaves_nothing() {
let (root, _live, _built) = directory("publish-target");
let target: PathBuf = root.join("index.cli-git-9");
std::fs::create_dir_all(target.join("occupied")).expect("a directory where the file should go");
assert!(publish(target.as_path(), b"bytes", 0).is_err());
assert!(target.join("occupied").is_dir());
assert!(publish(root.join("missing").join("index.cli-git-9").as_path(), b"bytes", 0).is_err());
assert_eq!(names(root.as_path()), vec!["index".to_string(), "index.cli-git-7".to_string(), "index.cli-git-9".to_string()]);
remove(root.as_path());
}
fn payload(writer: u8, length: usize) -> Vec<u8> {
let mut bytes: Vec<u8> = Vec::with_capacity(length);
bytes.extend_from_slice(&(length as u64).to_be_bytes());
bytes.extend_from_slice(&[writer; 8]);
bytes.resize(length, writer);
return bytes;
}
fn is_whole(bytes: &[u8]) -> bool {
let Some(header) = bytes.get(..16) else {
return false;
};
let length: usize = u64::from_be_bytes(header[..8].try_into().expect("eight bytes")) as usize;
return length == bytes.len() && bytes[8..16] == [bytes[8]; 8] && bytes[16..].iter().all(|byte| { return *byte == bytes[8]; });
}
#[test]
fn concurrent_publications_of_different_payloads_are_always_whole() {
let (root, _live, built) = directory("publish-concurrent");
std::fs::write(&built, payload(0, 4096)).expect("first payload");
let barrier: std::sync::Arc<std::sync::Barrier> = std::sync::Arc::new(std::sync::Barrier::new(4));
let finished: std::sync::Arc<std::sync::atomic::AtomicUsize> = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let reader_started: std::sync::Arc<std::sync::Barrier> = std::sync::Arc::new(std::sync::Barrier::new(2));
let reader = {
let (target, done, started) = (built.clone(), std::sync::Arc::clone(&finished), std::sync::Arc::clone(&reader_started));
std::thread::spawn(move || {
assert!(is_whole(std::fs::read(&target).expect("first read").as_slice()));
started.wait();
let mut reads: u32 = 0;
while done.load(Ordering::SeqCst) < 4 {
assert!(is_whole(std::fs::read(&target).expect("read").as_slice()), "a reader saw a payload that is not whole");
reads += 1;
std::thread::sleep(std::time::Duration::from_millis(1));
}
return reads;
})
};
reader_started.wait();
let writers: Vec<std::thread::JoinHandle<()>> = (1..=4u8)
.map(|writer| {
let (target, gate, done) = (built.clone(), std::sync::Arc::clone(&barrier), std::sync::Arc::clone(&finished));
return std::thread::spawn(move || {
for round in 0..20usize {
gate.wait();
publish(target.as_path(), payload(writer, 8192 + round * 64 + usize::from(writer)).as_slice(), 0).expect("published");
}
done.fetch_add(1, Ordering::SeqCst);
});
})
.collect();
for writer in writers {
writer.join().expect("writer");
}
assert!(reader.join().expect("reader") > 0);
assert!(is_whole(std::fs::read(&built).expect("target").as_slice()));
assert_eq!(names(root.as_path()), vec!["index".to_string(), "index.cli-git-7".to_string()]);
remove(root.as_path());
}
#[test]
fn a_read_inside_each_step_of_a_publication_sees_one_whole_payload() {
let (root, _live, built) = directory("publish-overlap");
let old: Vec<u8> = payload(1, 4096);
let new: Vec<u8> = payload(2, 9000);
std::fs::write(&built, old.as_slice()).expect("first payload");
let during: RefCell<Vec<(&'static str, Vec<u8>)>> = RefCell::new(Vec::new());
let read_on_another_thread = |label: &'static str| {
let target: PathBuf = built.clone();
let bytes: Vec<u8> = std::thread::scope(|scope| { return scope.spawn(|| { return std::fs::read(&target).expect("read"); }).join().expect("reader"); });
during.borrow_mut().push((label, bytes));
};
let write = |file: &mut std::fs::File, part: &[u8]| -> std::io::Result<usize> {
let taken: usize = file.write(&part[..part.len().min(1000)])?;
read_on_another_thread("write");
return Ok(taken);
};
let sync = |file: &std::fs::File| -> std::io::Result<()> {
read_on_another_thread("flush");
return file.sync_all();
};
let rename = |from: &Path, to: &Path| -> std::io::Result<()> {
read_on_another_thread("before rename");
std::fs::rename(from, to)?;
read_on_another_thread("after rename");
return Ok(());
};
publish_with(&Steps { write: &write, sync: &sync, rename: &rename, ..SYSTEM }, built.as_path(), new.as_slice(), 0).expect("published");
let seen = during.into_inner();
assert!(seen.iter().filter(|(label, _)| { return *label == "write"; }).count() > 1, "the reader overlapped several writes: {}", seen.len());
for (label, bytes) in &seen {
let expected: &Vec<u8> = if *label == "after rename" { &new } else { &old };
assert!(bytes == expected, "a reader at {label} saw something other than the whole {} payload", if *label == "after rename" { "new" } else { "old" });
}
assert!(std::fs::read(&built).expect("target") == new);
remove(root.as_path());
}
#[test]
fn the_published_mode_under_a_real_umask_is_gits() {
if crate::umask_child_test_support::ran_in_child(module_path!(), "the_published_mode_under_a_real_umask_is_gits", 0o077) {
return;
}
let (root, _live, built) = directory("publish-mode");
for (shared, expected) in [(0, 0o600), (0o660, 0o660), (0o664, 0o664), (-0o640, 0o640)] {
publish(built.as_path(), b"x", shared).expect("published");
assert_eq!(mode_of(built.as_path()), expected, "shared {shared:o}");
}
remove(root.as_path());
}
#[test]
fn an_interrupted_write_is_tried_again() {
let (root, _live, built) = directory("publish-interrupted");
let tries: RefCell<u32> = RefCell::new(0);
let write = |file: &mut std::fs::File, part: &[u8]| -> std::io::Result<usize> {
*tries.borrow_mut() += 1;
if *tries.borrow() % 2 == 1 {
return Err(std::io::Error::from(std::io::ErrorKind::Interrupted));
}
return (SYSTEM.write)(file, &part[..part.len().min(4)]);
};
publish_with(&Steps { write: &write, ..SYSTEM }, built.as_path(), b"a payload that needs several writes", 0).expect("published");
assert_eq!(std::fs::read(&built).expect("target"), b"a payload that needs several writes".to_vec());
assert!(*tries.borrow() > 4);
remove(root.as_path());
}