mod common;
fn count_bak_files_for(dir: &std::path::Path, filename: &str) -> usize {
let prefix = format!("{filename}.bak.");
std::fs::read_dir(dir)
.expect("read dir")
.filter_map(Result::ok)
.filter(|e| {
e.file_name()
.to_str()
.is_some_and(|n| n.starts_with(&prefix))
})
.count()
}
#[test]
fn batch_retention_caps_write_backups_within_single_manifest() {
let dir = tempfile::tempdir().expect("tempdir");
let manifest = common::manifest(&[
serde_json::json!({"op": "write", "target": "f.txt", "content": "v0\n"}),
serde_json::json!({"op": "write", "target": "f.txt", "content": "v1\n"}),
serde_json::json!({"op": "write", "target": "f.txt", "content": "v2\n"}),
serde_json::json!({"op": "write", "target": "f.txt", "content": "v3\n"}),
]);
let output = common::atomwrite()
.env_remove("ATOMWRITE_BACKUP")
.args([
"--workspace",
dir.path().to_str().unwrap(),
"batch",
"--keep-backup",
"--retention",
"2",
])
.write_stdin(manifest)
.output()
.expect("run");
assert!(
output.status.success(),
"batch --retention 2 --keep-backup deve suceder: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
std::fs::read_to_string(dir.path().join("f.txt")).unwrap(),
"v3\n"
);
let bak_count = count_bak_files_for(dir.path(), "f.txt");
assert!(
bak_count <= 2,
"batch --retention 2 deve limitar backups acumulados de f.txt a no maximo 2, obteve {bak_count}"
);
assert!(
bak_count >= 1,
"pelo menos 1 backup deveria ter sido criado (3 das 4 escritas encontram o alvo existente)"
);
}
#[test]
fn batch_explicit_backup_flag_forces_backup_for_op_without_own_flag() {
let dir = tempfile::tempdir().expect("tempdir");
common::create_test_file(dir.path(), "f.txt", "bye\n");
let manifest = common::manifest(&[serde_json::json!({
"op": "delete",
"target": "f.txt",
})]);
let output = common::atomwrite()
.env_remove("ATOMWRITE_BACKUP")
.args([
"--workspace",
dir.path().to_str().unwrap(),
"batch",
"--backup",
])
.write_stdin(manifest)
.output()
.expect("run");
assert!(
output.status.success(),
"batch --backup com op sem campo backup deve suceder: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(!dir.path().join("f.txt").exists());
assert_eq!(
count_bak_files_for(dir.path(), "f.txt"),
1,
"batch --backup explicito deve forcar backup mesmo com op.backup ausente/false"
);
}
#[test]
fn batch_no_backup_overrides_op_level_backup_true() {
let dir = tempfile::tempdir().expect("tempdir");
common::create_test_file(dir.path(), "f.txt", "bye\n");
let manifest = common::manifest(&[serde_json::json!({
"op": "delete",
"target": "f.txt",
"backup": true,
})]);
let output = common::atomwrite()
.env_remove("ATOMWRITE_BACKUP")
.args([
"--workspace",
dir.path().to_str().unwrap(),
"batch",
"--no-backup",
])
.write_stdin(manifest)
.output()
.expect("run");
assert!(
output.status.success(),
"batch --no-backup com op.backup=true deve suceder: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(!dir.path().join("f.txt").exists());
assert_eq!(
count_bak_files_for(dir.path(), "f.txt"),
0,
"batch --no-backup deve vencer op.backup=true (regressao)"
);
}
#[test]
fn batch_transaction_pre_backup_honors_retention() {
let dir = tempfile::tempdir().expect("tempdir");
common::create_test_file(dir.path(), "f.txt", "seed\n");
for i in 0..3 {
let manifest = common::manifest(&[serde_json::json!({
"op": "write",
"target": "f.txt",
"content": format!("v{i}\n"),
})]);
let output = common::atomwrite()
.env_remove("ATOMWRITE_BACKUP")
.args([
"--workspace",
dir.path().to_str().unwrap(),
"batch",
"--transaction",
"--retention",
"1",
])
.write_stdin(manifest)
.output()
.expect("run");
assert!(
output.status.success(),
"batch --transaction --retention 1 iteracao {i} deve suceder: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let bak_count = count_bak_files_for(dir.path(), "f.txt");
assert!(
bak_count <= 1,
"batch --transaction --retention 1 deve limitar o pre-backup transacional acumulado a no maximo 1, obteve {bak_count}"
);
}
#[test]
fn batch_delete_op_backup_honors_retention() {
let dir = tempfile::tempdir().expect("tempdir");
let mut ops = Vec::new();
for i in 0..4 {
ops.push(serde_json::json!({
"op": "write",
"target": "f.txt",
"content": format!("v{i}\n"),
}));
ops.push(serde_json::json!({
"op": "delete",
"target": "f.txt",
"backup": true,
}));
}
let manifest = common::manifest(&ops);
let output = common::atomwrite()
.env_remove("ATOMWRITE_BACKUP")
.args([
"--workspace",
dir.path().to_str().unwrap(),
"batch",
"--retention",
"2",
])
.write_stdin(manifest)
.output()
.expect("run");
assert!(
output.status.success(),
"ciclos write+delete com --retention 2 devem suceder: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(!dir.path().join("f.txt").exists());
let bak_count = count_bak_files_for(dir.path(), "f.txt");
assert!(
bak_count <= 2,
"op delete com backup deve honrar --retention 2 (nao o hardcode antigo de 5), obteve {bak_count}"
);
assert!(
bak_count >= 1,
"pelo menos 1 backup de delete deveria existir"
);
}
#[test]
fn batch_backup_and_no_backup_together_is_rejected() {
let dir = tempfile::tempdir().expect("tempdir");
let output = common::atomwrite()
.args([
"--workspace",
dir.path().to_str().unwrap(),
"batch",
"--backup",
"--no-backup",
])
.write_stdin("")
.output()
.expect("run");
assert_eq!(
output.status.code(),
Some(2),
"batch --backup --no-backup deve ser rejeitado pelo clap com exit 2: {:?}",
String::from_utf8_lossy(&output.stderr)
);
}