use std::fs::rename;
use std::fs::{remove_file, OpenOptions};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use assert_fs::prelude::*;
use assert_fs::TempDir;
use dir_assert::assert_paths;
use itertools::Itertools;
use pretty_assertions::assert_eq;
use rayon::prelude::ParallelIterator;
use rstest::rstest;
use tracing_test::traced_test;
use conserve::counters::Counter;
use conserve::monitor::test::TestMonitor;
use conserve::transport::open_local_transport;
use conserve::{
backup, restore, Apath, Archive, BackupOptions, BandId, BandSelectionPolicy, BlockHash,
EntryTrait, Exclude, RestoreOptions, ValidateOptions,
};
#[derive(Debug, Clone)]
enum TreeChanges {
None,
AlterFile,
RenameFile,
}
impl TreeChanges {
fn apply(&self, dir: &TempDir) {
match self {
TreeChanges::None => {}
TreeChanges::AlterFile => {
dir.child("file").write_str("changed").unwrap();
}
TreeChanges::RenameFile => {
rename(dir.child("file"), dir.child("file2")).unwrap();
}
}
}
}
#[rstest]
#[traced_test]
#[test]
fn backup_after_damage(
#[values(DamageAction::Delete, DamageAction::Truncate)] action: DamageAction,
#[values(
DamageLocation::BandHead(0),
DamageLocation::BandTail(0),
DamageLocation::Block(0)
)]
location: DamageLocation,
#[values(TreeChanges::None, TreeChanges::AlterFile, TreeChanges::RenameFile)]
changes: TreeChanges,
) {
let archive_dir = TempDir::new().unwrap();
let source_dir = TempDir::new().unwrap();
let archive = Archive::create_path(archive_dir.path()).expect("create archive");
source_dir
.child("file")
.write_str("content in first backup")
.unwrap();
let backup_options = BackupOptions::default();
backup(
&archive,
source_dir.path(),
&backup_options,
TestMonitor::arc(),
)
.expect("initial backup");
drop(archive);
action.damage(&location.to_path(&archive_dir));
let archive =
Archive::open(conserve::transport::open_local_transport(archive_dir.path()).unwrap())
.expect("open archive");
changes.apply(&source_dir);
let backup_stats = backup(
&archive,
source_dir.path(),
&backup_options,
TestMonitor::arc(),
)
.expect("write second backup after damage");
dbg!(&backup_stats);
match changes {
TreeChanges::None => match location {
DamageLocation::Block(_) => {
assert_eq!(backup_stats.replaced_damaged_blocks, 1);
assert_eq!(backup_stats.written_blocks, 1);
}
_ => {
assert_eq!(backup_stats.replaced_damaged_blocks, 0);
assert_eq!(backup_stats.written_blocks, 0);
}
},
TreeChanges::RenameFile => match location {
DamageLocation::Block(_) => {
assert_eq!(backup_stats.written_blocks, 1);
assert_eq!(backup_stats.replaced_damaged_blocks, 0);
}
_ => {
assert_eq!(backup_stats.deduplicated_blocks, 1);
}
},
TreeChanges::AlterFile => {
assert_eq!(backup_stats.written_blocks, 1);
assert_eq!(backup_stats.deduplicated_blocks, 0);
assert_eq!(backup_stats.replaced_damaged_blocks, 0);
}
}
{
let restore_dir = TempDir::new().unwrap();
let monitor = TestMonitor::arc();
restore(
&archive,
restore_dir.path(),
&RestoreOptions::default(),
monitor.clone(),
)
.expect("restore second backup");
monitor.assert_counter(Counter::Files, 1);
monitor.assert_no_errors();
assert_paths!(source_dir.path(), restore_dir.path());
}
let versions = archive.list_band_ids().expect("list versions");
assert_eq!(versions, [BandId::zero(), BandId::new(&[1])]);
let apaths = archive
.iter_entries(
BandSelectionPolicy::Latest,
Apath::root(),
Exclude::nothing(),
TestMonitor::arc(),
)
.expect("iter entries")
.map(|e| e.apath().to_string())
.collect_vec();
if matches!(changes, TreeChanges::RenameFile) {
assert_eq!(apaths, ["/", "/file2"]);
} else {
assert_eq!(apaths, ["/", "/file"]);
}
archive
.validate(&ValidateOptions::default(), Arc::new(TestMonitor::new()))
.expect("validate");
}
#[derive(Debug, Clone)]
pub enum DamageAction {
Truncate,
Delete,
}
impl DamageAction {
pub fn damage(&self, path: &Path) {
assert!(path.exists(), "Path to be damaged does not exist: {path:?}");
match self {
DamageAction::Truncate => {
OpenOptions::new()
.write(true)
.truncate(true)
.open(path)
.expect("truncate file");
}
DamageAction::Delete => {
remove_file(path).expect("delete file");
}
}
}
}
#[derive(Debug, Clone)]
pub enum DamageLocation {
BandHead(u32),
BandTail(u32),
Block(usize),
}
impl DamageLocation {
pub fn to_path(&self, archive_dir: &Path) -> PathBuf {
match self {
DamageLocation::BandHead(band_id) => archive_dir
.join(BandId::from(*band_id).to_string())
.join("BANDHEAD"),
DamageLocation::BandTail(band_id) => archive_dir
.join(BandId::from(*band_id).to_string())
.join("BANDTAIL"),
DamageLocation::Block(block_index) => {
let archive =
Archive::open(open_local_transport(archive_dir).expect("open transport"))
.expect("open archive");
let block_dir = archive.block_dir();
let block_hash = block_dir
.blocks(TestMonitor::arc())
.expect("list blocks")
.collect::<Vec<BlockHash>>()
.into_iter()
.sorted()
.nth(*block_index)
.expect("Archive has an nth block");
archive_dir
.join("d")
.join(conserve::blockdir::block_relpath(&block_hash))
}
}
}
}