use super::*;
use rch_common::{WorkerId, test_guard};
use std::fs::{File, FileTimes};
use std::os::unix::fs::MetadataExt;
fn fixture() -> (PathBuf, TransferPipeline, WorkerConfig) {
let root = tempfile::Builder::new()
.disable_cleanup(true)
.tempdir()
.unwrap()
.keep();
let caller = root.join("caller");
let worker = root.join("worker");
let stage = root.join("stage");
for directory in [&caller, &worker, &stage] {
std::fs::create_dir_all(directory.join("build")).unwrap();
}
let worker_config = WorkerConfig {
id: WorkerId::new("retrieval-space-fixture"),
host: "unreachable.invalid".into(),
user: "worker".into(),
identity_file: "/unused/test-key".into(),
total_slots: 1,
priority: 100,
tags: Vec::new(),
tools: Vec::new(),
};
let config = TransferConfig {
max_transfer_time_ms: Some(900_000),
..Default::default()
};
let mut pipeline = TransferPipeline::new(
stage,
"storage-fixture".into(),
"storage-fixture".into(),
config,
)
.with_retrieval_reference_root(caller);
pipeline.ssh_options.control_master = false;
(root, pipeline, worker_config)
}
fn command(pipeline: &TransferPipeline, worker: &WorkerConfig, root: &Path) -> Command {
let mut command = pipeline.local_artifact_retrieval_for_test(
worker,
&root.join("worker"),
&default_c_cpp_artifact_patterns(),
);
command.env("LC_ALL", "C");
command
}
fn result_command(
pipeline: &TransferPipeline,
worker: &WorkerConfig,
root: &Path,
relative: &Path,
) -> Command {
let planned = pipeline.build_result_dir_retrieve_command(
worker,
root.join("worker").to_str().unwrap(),
relative,
);
let planned = planned.as_std();
let args: Vec<_> = planned.get_args().collect();
let mut command = Command::new(planned.get_program());
command
.args(&args[..args.len() - 2])
.arg(format!("{}/", root.join("worker").join(relative).display()))
.arg(args.last().unwrap())
.env("LC_ALL", "C");
command
}
async fn run(command: &mut Command) -> std::process::Output {
command.stdin(Stdio::null()).kill_on_drop(true);
let output = tokio::time::timeout(Duration::from_secs(30), command.output())
.await
.expect("local rsync storage regression timed out")
.expect("rsync is required for this regression");
assert!(output.status.success(), "{output:?}");
assert!(output.stderr.is_empty(), "{output:?}");
output
}
#[test]
fn artifact_and_declared_result_collectors_use_the_same_integrity_policy() {
let _guard = test_guard!();
let (root, pipeline, worker) = fixture();
let argument = TransferPipeline::artifact_integrity_arg(pipeline.resolved_rsync().flavor);
for built in [
pipeline.build_retrieve_command(
&worker,
root.to_str().unwrap(),
&default_c_cpp_artifact_patterns(),
),
pipeline.build_retrieve_streaming_command(
&worker,
root.to_str().unwrap(),
&default_c_cpp_artifact_patterns(),
),
pipeline.build_result_dir_retrieve_command(
&worker,
root.to_str().unwrap(),
Path::new("reports/gates"),
),
] {
assert!(built.as_std().get_args().any(|actual| actual == argument));
assert!(!built.as_std().get_args().any(|argument| {
matches!(
argument.to_str(),
Some("--inplace" | "--append" | "--size-only")
)
}));
}
}
#[tokio::test]
async fn declared_result_recovery_repairs_equal_metadata_without_touching_caller_outputs() {
let _guard = test_guard!();
let (root, pipeline, worker) = fixture();
let relative = Path::new("reports/gates");
let remote = root.join("worker").join(relative);
let stage = root.join("stage").join(relative);
let caller = root.join("caller").join(relative);
for directory in [&remote, &stage, &caller] {
std::fs::create_dir_all(directory).unwrap();
}
std::fs::write(remote.join("conformance.txt"), b"PASS\n").unwrap();
std::fs::write(stage.join("conformance.txt"), b"FAIL\n").unwrap();
std::fs::write(caller.join("conformance.txt"), b"previous caller report\n").unwrap();
std::fs::write(remote.join("fixture.bin"), [0, 255, 13, 10, 0]).unwrap();
std::fs::write(remote.join("empty-result"), b"").unwrap();
for directory in [&remote, &remote.join("nested")] {
std::fs::create_dir_all(directory).unwrap();
std::fs::write(
directory.join(".rch-clean-overlay-base.tar"),
b"immutable source input",
)
.unwrap();
}
let timestamp = std::fs::metadata(remote.join("conformance.txt"))
.unwrap()
.modified()
.unwrap();
File::options()
.write(true)
.open(stage.join("conformance.txt"))
.unwrap()
.set_times(FileTimes::new().set_modified(timestamp))
.unwrap();
let caller_inode = std::fs::metadata(caller.join("conformance.txt"))
.unwrap()
.ino();
let mut estimate = result_command(&pipeline, &worker, &root, relative);
estimate.arg("--dry-run");
run(&mut estimate).await;
assert_eq!(
std::fs::read(stage.join("conformance.txt")).unwrap(),
b"FAIL\n"
);
assert!(!stage.join("fixture.bin").exists());
run(&mut result_command(&pipeline, &worker, &root, relative)).await;
for name in ["conformance.txt", "fixture.bin", "empty-result"] {
assert_eq!(
std::fs::read(stage.join(name)).unwrap(),
std::fs::read(remote.join(name)).unwrap()
);
}
for directory in [Path::new(""), Path::new("nested")] {
assert!(
!stage
.join(directory)
.join(".rch-clean-overlay-base.tar")
.exists()
);
assert_eq!(
std::fs::read(remote.join(directory).join(".rch-clean-overlay-base.tar")).unwrap(),
b"immutable source input"
);
}
assert_eq!(
std::fs::read(caller.join("conformance.txt")).unwrap(),
b"previous caller report\n"
);
assert_eq!(
std::fs::metadata(caller.join("conformance.txt"))
.unwrap()
.ino(),
caller_inode
);
}
#[tokio::test]
async fn declared_result_integrity_preserves_missing_and_empty_directory_semantics() {
let _guard = test_guard!();
let (root, pipeline, worker) = fixture();
let parent = root.join("stage/reports");
std::fs::create_dir_all(&parent).unwrap();
let missing = Path::new("reports/missing");
let mut command = result_command(&pipeline, &worker, &root, missing);
command.stdin(Stdio::null()).kill_on_drop(true);
let output = tokio::time::timeout(Duration::from_secs(30), command.output())
.await
.expect("missing result-directory probe timed out")
.expect("rsync is required for this regression");
assert!(
!output.status.success(),
"absent required source became success"
);
assert!(!root.join("worker").join(missing).exists());
let empty = Path::new("reports/empty");
std::fs::create_dir_all(root.join("worker").join(empty)).unwrap();
run(&mut result_command(&pipeline, &worker, &root, empty)).await;
assert!(root.join("stage").join(empty).is_dir());
assert_eq!(
std::fs::read_dir(root.join("stage").join(empty))
.unwrap()
.count(),
0
);
}
#[test]
fn openrsync_and_unknown_flavours_force_full_refresh_instead_of_metadata_reuse() {
for flavor in [
RsyncFlavor::Rsync {
major: 2,
minor: 6,
patch: 9,
},
RsyncFlavor::Rsync {
major: 3,
minor: 4,
patch: 1,
},
] {
assert_eq!(
TransferPipeline::artifact_integrity_arg(flavor),
"--checksum"
);
}
for flavor in [
RsyncFlavor::OpenRsync { protocol: Some(29) },
RsyncFlavor::Unknown,
] {
assert_eq!(
TransferPipeline::artifact_integrity_arg(flavor),
"--ignore-times"
);
}
}
#[tokio::test]
async fn integrity_plan_repairs_equal_metadata_and_credits_only_verified_staged_files() {
let _guard = test_guard!();
let (root, pipeline, worker) = fixture();
let worker_root = root.join("worker");
let stage = root.join("stage");
let checksummed =
TransferPipeline::artifact_integrity_arg(pipeline.resolved_rsync().flavor) == "--checksum";
let retained = vec![7_u8; 2 * 1024 * 1024];
let replaced = vec![9_u8; 128 * 1024];
for (name, bytes) in [
("retained.o", retained.as_slice()),
("changed.o", replaced.as_slice()),
] {
std::fs::write(worker_root.join("build").join(name), bytes).unwrap();
}
std::fs::copy(
worker_root.join("build/retained.o"),
stage.join("build/retained.o"),
)
.unwrap();
std::fs::write(stage.join("build/changed.o"), vec![3_u8; replaced.len()]).unwrap();
for name in ["retained.o", "changed.o"] {
let modified = std::fs::metadata(worker_root.join("build").join(name))
.unwrap()
.modified()
.unwrap();
File::options()
.write(true)
.open(stage.join("build").join(name))
.unwrap()
.set_times(FileTimes::new().set_modified(modified))
.unwrap();
}
std::fs::write(worker_root.join("build/report.txt"), b"PASS\n").unwrap();
std::fs::write(
worker_root.join("build/.rch-clean-overlay-base.tar"),
b"immutable input",
)
.unwrap();
let inode = std::fs::metadata(stage.join("build/retained.o"))
.unwrap()
.ino();
let mut dry_run = command(&pipeline, &worker, &root);
dry_run.arg("--dry-run");
let output = run(&mut dry_run).await;
let selected_bytes = (retained.len() + replaced.len() + 5) as u64;
if checksummed {
let plan = retrieval_space::RsyncStoragePlan::parse(&output.stdout).unwrap();
assert_eq!(plan.selected_bytes, selected_bytes);
assert_eq!(plan.replacement_bytes, replaced.len() as u64 + 5);
assert!(plan.additional_bytes(4096).unwrap() < plan.selected_bytes);
}
assert_eq!(
std::fs::read(stage.join("build/changed.o")).unwrap(),
vec![3; replaced.len()]
);
assert!(!stage.join("build/report.txt").exists());
let budget = pipeline
.retrieval_retry_config_inner(|| command(&pipeline, &worker, &root), checksummed)
.await
.unwrap();
assert_eq!(
budget.total_timeout_ms, 900_000,
"allocation credit must not shorten the transfer deadline"
);
run(&mut command(&pipeline, &worker, &root)).await;
assert_eq!(
std::fs::read(stage.join("build/changed.o")).unwrap(),
replaced
);
assert_eq!(
std::fs::read(stage.join("build/retained.o")).unwrap(),
retained
);
if checksummed {
assert_eq!(
std::fs::metadata(stage.join("build/retained.o"))
.unwrap()
.ino(),
inode
);
}
assert_eq!(
std::fs::read(stage.join("build/report.txt")).unwrap(),
b"PASS\n"
);
assert!(!stage.join("build/.rch-clean-overlay-base.tar").exists());
let mut complete = command(&pipeline, &worker, &root);
complete.arg("--dry-run");
let output = run(&mut complete).await;
if checksummed {
let finished = retrieval_space::RsyncStoragePlan::parse(&output.stdout).unwrap();
assert_eq!(finished.selected_bytes, selected_bytes);
assert_eq!(finished.additional_bytes(4096), Some(0));
}
}
#[tokio::test]
async fn insufficient_reserve_refuses_before_collection_without_mutating_staging() {
let _guard = test_guard!();
let (root, mut pipeline, worker) = fixture();
let stage = root.join("stage/build/retained.o");
std::fs::write(&stage, b"retained evidence").unwrap();
std::fs::write(root.join("worker/build/new.o"), b"new output").unwrap();
pipeline.transfer_config.retrieval_min_free_bytes = u64::MAX;
let before = std::fs::metadata(&stage).unwrap();
let checksummed =
TransferPipeline::artifact_integrity_arg(pipeline.resolved_rsync().flavor) == "--checksum";
let result = pipeline
.retrieval_retry_config_inner(|| command(&pipeline, &worker, &root), checksummed)
.await;
assert!(result.is_err());
assert!(!root.join("stage/build/new.o").exists());
assert_eq!(std::fs::read(&stage).unwrap(), b"retained evidence");
assert_eq!(std::fs::metadata(&stage).unwrap().ino(), before.ino());
assert_eq!(
std::fs::read(root.join("worker/build/new.o")).unwrap(),
b"new output"
);
}