From 25f908b6b2d587e78bc156b53de40b91f7157458 Mon Sep 17 00:00:00 2001 From: aawsome <37850842+aawsome@users.noreply.github.com> Date: Mon, 30 Dec 2024 09:28:25 +0100 Subject: [PATCH] refactor(tests): Use command-specific test modules (#394) --- crates/core/tests/integration.rs | 432 +----------------- crates/core/tests/integration/backup.rs | 245 ++++++++++ crates/core/tests/integration/find.rs | 61 +++ crates/core/tests/integration/ls.rs | 52 +++ crates/core/tests/integration/prune.rs | 71 +++ crates/core/tests/integration/restore.rs | 1 + crates/core/tests/integration/vfs.rs | 60 +++ .../tests/snapshots/integration__vfs-nix.snap | 78 ++++ .../snapshots/integration__vfs-windows.snap | 45 ++ 9 files changed, 634 insertions(+), 411 deletions(-) create mode 100644 crates/core/tests/integration/backup.rs create mode 100644 crates/core/tests/integration/find.rs create mode 100644 crates/core/tests/integration/ls.rs create mode 100644 crates/core/tests/integration/prune.rs create mode 100644 crates/core/tests/integration/restore.rs create mode 100644 crates/core/tests/integration/vfs.rs create mode 100644 crates/core/tests/snapshots/integration__vfs-nix.snap create mode 100644 crates/core/tests/snapshots/integration__vfs-windows.snap diff --git a/crates/core/tests/integration.rs b/crates/core/tests/integration.rs index 2356c47a..d75c482d 100644 --- a/crates/core/tests/integration.rs +++ b/crates/core/tests/integration.rs @@ -23,41 +23,37 @@ //! The tests that use the fixtures are defined as functions with the `#[rstest]` attribute. //! The fixtures are passed as arguments to the test functions. +mod integration { + mod backup; + mod find; + mod ls; + mod prune; + mod restore; + mod vfs; + use super::*; +} + +use std::{env, fs::File, path::Path, sync::Arc}; + use anyhow::Result; -use bytes::Bytes; use flate2::read::GzDecoder; -use globset::Glob; use insta::{ assert_ron_snapshot, internals::{Content, ContentPath}, Settings, }; -use pretty_assertions::assert_eq; -use rstest::{fixture, rstest}; -use rustic_core::{ - repofile::{Metadata, Node, PackId, SnapshotFile}, - BackupOptions, CheckOptions, CommandInput, ConfigOptions, FindMatches, FindNode, FullIndex, - IndexedFull, IndexedStatus, KeyOptions, LimitOption, LsOptions, NoProgressBars, OpenStatus, - ParentOptions, PathList, PruneOptions, Repository, RepositoryBackends, RepositoryOptions, - RusticResult, SnapshotGroupCriterion, SnapshotOptions, StringList, -}; +use rstest::fixture; use serde::Serialize; - -use rustic_testing::backend::in_memory_backend::InMemoryBackend; - -use std::{collections::BTreeMap, ffi::OsStr}; -use std::{ - env, - fs::File, - path::{Path, PathBuf}, - str::FromStr, - sync::Arc, - time::Duration, -}; -// uncomment for logging output -// use simplelog::{Config, SimpleLogger}; use tar::Archive; use tempfile::{tempdir, TempDir}; +// uncomment for logging output +// use simplelog::{Config, SimpleLogger}; + +use rustic_core::{ + CommandInput, ConfigOptions, FullIndex, IndexedFull, IndexedStatus, KeyOptions, NoProgressBars, + OpenStatus, PathList, Repository, RepositoryBackends, RepositoryOptions, +}; +use rustic_testing::backend::in_memory_backend::InMemoryBackend; type RepoOpen = Repository; @@ -189,392 +185,6 @@ fn repo_with_commands() -> Result<()> { Ok(()) } -#[rstest] -fn test_backup_with_tar_gz_passes( - tar_gz_testdata: Result, - set_up_repo: Result, - insta_snapshotfile_redaction: Settings, - insta_node_redaction: Settings, -) -> Result<()> { - // uncomment for logging output - // SimpleLogger::init(log::LevelFilter::Debug, Config::default())?; - - // Fixtures - let (source, repo) = (tar_gz_testdata?, set_up_repo?.to_indexed_ids()?); - - let paths = &source.path_list(); - - // we use as_path to not depend on the actual tempdir - let opts = BackupOptions::default().as_path(PathBuf::from_str("test")?); - - // first backup - let first_snapshot = repo.backup(&opts, paths, SnapshotFile::default())?; - - // We can also bind to scope ( https://docs.rs/insta/latest/insta/struct.Settings.html#method.bind_to_scope ) - // But I think that can get messy with a lot of tests, also checking which settings are currently applied - // will be probably harder - insta_snapshotfile_redaction.bind(|| { - assert_with_win("backup-tar-summary-first", &first_snapshot); - }); - - assert_eq!(first_snapshot.parent, None); - - // tree of first backup - // re-read index - let repo = repo.to_indexed_ids()?; - let tree = repo.node_from_path(first_snapshot.tree, Path::new("test/0/tests"))?; - let tree: rustic_core::repofile::Tree = repo.get_tree(&tree.subtree.expect("Sub tree"))?; - - insta_node_redaction.bind(|| { - assert_with_win("backup-tar-tree", tree); - }); - - // get all snapshots and check them - let all_snapshots = repo.get_all_snapshots()?; - assert_eq!(vec![first_snapshot.clone()], all_snapshots); - // save list of pack files - let packs1: Vec = repo.list()?.collect(); - - // re-read index - let repo = repo.to_indexed_ids()?; - // second backup - let second_snapshot = repo.backup(&opts, paths, SnapshotFile::default())?; - - insta_snapshotfile_redaction.bind(|| { - assert_with_win("backup-tar-summary-second", &second_snapshot); - }); - - assert_eq!(second_snapshot.parent, Some(first_snapshot.id)); - assert_eq!(first_snapshot.tree, second_snapshot.tree); - - // pack files should be unchanged - let packs2: Vec<_> = repo.list()?.collect(); - assert_eq!(packs1, packs2); - - // re-read index - let repo = repo.to_indexed_ids()?; - // third backup with tags and explicitly given parent - let snap = SnapshotOptions::default() - .tags([StringList::from_str("a,b")?]) - .to_snapshot()?; - let opts = opts.parent_opts(ParentOptions::default().parent(second_snapshot.id.to_string())); - let third_snapshot = repo.backup(&opts, paths, snap)?; - - insta_snapshotfile_redaction.bind(|| { - assert_with_win("backup-tar-summary-third", &third_snapshot); - }); - assert_eq!(third_snapshot.parent, Some(second_snapshot.id)); - assert_eq!(third_snapshot.tree, second_snapshot.tree); - - // get all snapshots and check them - let mut all_snapshots = repo.get_all_snapshots()?; - all_snapshots.sort_unstable(); - assert_eq!( - vec![first_snapshot, second_snapshot, third_snapshot], - all_snapshots - ); - - // pack files should be unchanged - let packs2: Vec<_> = repo.list()?.collect(); - assert_eq!(packs1, packs2); - let packs3: Vec<_> = repo.list()?.collect(); - assert_eq!(packs1, packs3); - - // Check if snapshots can be retrieved - let mut ids: Vec<_> = all_snapshots.iter().map(|sn| sn.id.to_string()).collect(); - let snaps = repo.get_snapshots(&ids)?; - assert_eq!(snaps, all_snapshots); - - // reverse order - all_snapshots.reverse(); - ids.reverse(); - let snaps = repo.update_snapshots(snaps, &ids)?; - assert_eq!(snaps, all_snapshots); - - // get snapshot group - let group_by = SnapshotGroupCriterion::new().tags(true); - let mut groups = repo.get_snapshot_group(&[], group_by, |_| true)?; - - // sort groups to get unique result - groups.iter_mut().for_each(|(_, snaps)| snaps.sort()); - groups.sort_by_key(|(group, _)| group.tags.clone()); - - insta_snapshotfile_redaction.bind(|| { - assert_with_win("backup-tar-groups", &groups); - }); - - // filter snapshots by tag - let filter = |snap: &SnapshotFile| snap.tags.contains("a"); - let snaps = repo.get_matching_snapshots(filter)?; - insta_snapshotfile_redaction.bind(|| { - assert_with_win("backup-tar-matching-snaps", &snaps); - }); - - Ok(()) -} - -#[rstest] -fn test_backup_dry_run_with_tar_gz_passes( - tar_gz_testdata: Result, - set_up_repo: Result, - insta_snapshotfile_redaction: Settings, - insta_node_redaction: Settings, -) -> Result<()> { - // Fixtures - let (source, repo) = (tar_gz_testdata?, set_up_repo?.to_indexed_ids()?); - - let paths = &source.path_list(); - - // we use as_path to not depend on the actual tempdir - let opts = BackupOptions::default() - .as_path(PathBuf::from_str("test")?) - .dry_run(true); - - // dry-run backup - let snap_dry_run = repo.backup(&opts, paths, SnapshotFile::default())?; - - insta_snapshotfile_redaction.bind(|| { - assert_with_win("dryrun-tar-summary-first", &snap_dry_run); - }); - - // check that repo is still empty - let snaps = repo.get_all_snapshots()?; - assert_eq!(snaps.len(), 0); - assert_eq!(repo.list::()?.count(), 0); - assert_eq!(repo.list::()?.count(), 0); - - // first real backup - let opts = opts.dry_run(false); - let first_snapshot = repo.backup(&opts, paths, SnapshotFile::default())?; - assert_eq!(snap_dry_run.tree, first_snapshot.tree); - let packs: Vec<_> = repo.list::()?.collect(); - - // tree of first backup - // re-read index - let repo = repo.to_indexed_ids()?; - let tree = repo.node_from_path(first_snapshot.tree, Path::new("test/0/tests"))?; - let tree = repo.get_tree(&tree.subtree.expect("Sub tree"))?; - - insta_node_redaction.bind(|| { - assert_with_win("dryrun-tar-tree", tree); - }); - - // re-read index - let repo = repo.to_indexed_ids()?; - // second dry-run backup - let opts = opts.dry_run(true); - let snap_dry_run = repo.backup(&opts, paths, SnapshotFile::default())?; - - insta_snapshotfile_redaction.bind(|| { - assert_with_win("dryrun-tar-summary-second", &snap_dry_run); - }); - - // check that no data has been added - let snaps = repo.get_all_snapshots()?; - assert_eq!(snaps, vec![first_snapshot]); - let packs_dry_run: Vec = repo.list()?.collect(); - assert_eq!(packs_dry_run, packs); - - // re-read index - let repo = repo.to_indexed_ids()?; - // second real backup - let opts = opts.dry_run(false); - let second_snapshot = repo.backup(&opts, paths, SnapshotFile::default())?; - assert_eq!(snap_dry_run.tree, second_snapshot.tree); - Ok(()) -} - -#[rstest] -fn test_backup_stdin_command( - set_up_repo: Result, - insta_snapshotfile_redaction: Settings, -) -> Result<()> { - // Fixtures - let repo = set_up_repo?.to_indexed_ids()?; - let paths = PathList::from_string("-")?; - - let cmd: CommandInput = "echo test".parse()?; - let opts = BackupOptions::default() - .stdin_filename("test") - .stdin_command(cmd); - // backup data from cmd - let snapshot = repo.backup(&opts, &paths, SnapshotFile::default())?; - insta_snapshotfile_redaction.bind(|| { - assert_with_win("stdin-command-summary", &snapshot); - }); - - // re-read index - let repo = repo.to_indexed()?; - - // check content - let node = repo.node_from_snapshot_path("latest:test", |_| true)?; - let mut content = Vec::new(); - repo.dump(&node, &mut content)?; - assert_eq!(content, b"test\n"); - Ok(()) -} - -#[rstest] -fn test_ls_and_read( - tar_gz_testdata: Result, - set_up_repo: Result, - insta_node_redaction: Settings, -) -> Result<()> { - // Fixtures - let (source, repo) = (tar_gz_testdata?, set_up_repo?.to_indexed_ids()?); - let paths = &source.path_list(); - - // we use as_path to not depend on the actual tempdir - let opts = BackupOptions::default().as_path(PathBuf::from_str("test")?); - // backup test-data - let snapshot = repo.backup(&opts, paths, SnapshotFile::default())?; - - // test non-existing entries - let mut node = Node::new_node( - OsStr::new(""), - rustic_core::repofile::NodeType::Dir, - Metadata::default(), - ); - node.subtree = Some(snapshot.tree); - - // re-read index - let repo = repo.to_indexed_ids()?; - - let entries: BTreeMap<_, _> = repo - .ls(&node, &LsOptions::default())? - .collect::>()?; - - insta_node_redaction.bind(|| { - assert_with_win("ls", &entries); - }); - - // test reading a file from the repository - let repo = repo.to_indexed()?; - let path: PathBuf = ["test", "0", "tests", "testfile"].iter().collect(); - let node = entries.get(&path).unwrap(); - let file = repo.open_file(node)?; - - let data = repo.read_file_at(&file, 0, 21)?; // read full content - assert_eq!(Bytes::from("This is a test file.\n"), &data); - assert_eq!(data, repo.read_file_at(&file, 0, 4096)?); // read beyond file end - assert_eq!(Bytes::new(), repo.read_file_at(&file, 25, 1)?); // offset beyond file end - assert_eq!(Bytes::from("test"), repo.read_file_at(&file, 10, 4)?); // read partial content - - // test reading an empty file from the repository - let path: PathBuf = ["test", "0", "tests", "empty-file"].iter().collect(); - let node = entries.get(&path).unwrap(); - let file = repo.open_file(node)?; - assert_eq!(Bytes::new(), repo.read_file_at(&file, 0, 0)?); // empty files - Ok(()) -} - -#[rstest] -fn test_find(tar_gz_testdata: Result, set_up_repo: Result) -> Result<()> { - // Fixtures - let (source, repo) = (tar_gz_testdata?, set_up_repo?.to_indexed_ids()?); - let paths = &source.path_list(); - - // we use as_path to not depend on the actual tempdir - let opts = BackupOptions::default().as_path(PathBuf::from_str("test")?); - // backup test-data - let snapshot = repo.backup(&opts, paths, SnapshotFile::default())?; - - // re-read index - let repo = repo.to_indexed_ids()?; - - // test non-existing path - let not_found = repo.find_nodes_from_path(vec![snapshot.tree], Path::new("not_existing"))?; - assert_with_win("find-nodes-not-found", not_found); - // test non-existing match - let glob = Glob::new("not_existing")?.compile_matcher(); - let not_found = - repo.find_matching_nodes(vec![snapshot.tree], &|path, _| glob.is_match(path))?; - assert_with_win("find-matching-nodes-not-found", not_found); - - // test existing path - let FindNode { matches, .. } = - repo.find_nodes_from_path(vec![snapshot.tree], Path::new("test/0/tests/testfile"))?; - assert_with_win("find-nodes-existing", matches); - // test existing match - let glob = Glob::new("testfile")?.compile_matcher(); - let match_func = |path: &Path, _: &Node| { - glob.is_match(path) || path.file_name().is_some_and(|f| glob.is_match(f)) - }; - let FindMatches { paths, matches, .. } = - repo.find_matching_nodes(vec![snapshot.tree], &match_func)?; - assert_with_win("find-matching-existing", (paths, matches)); - // test existing match - let glob = Glob::new("testfile*")?.compile_matcher(); - let match_func = |path: &Path, _: &Node| { - glob.is_match(path) || path.file_name().is_some_and(|f| glob.is_match(f)) - }; - let FindMatches { paths, matches, .. } = - repo.find_matching_nodes(vec![snapshot.tree], &match_func)?; - assert_with_win("find-matching-wildcard-existing", (paths, matches)); - Ok(()) -} - -#[rstest] -fn test_prune( - tar_gz_testdata: Result, - set_up_repo: Result, - #[values(true, false)] instant_delete: bool, - #[values( - LimitOption::Percentage(0), - LimitOption::Percentage(50), - LimitOption::Unlimited - )] - max_unused: LimitOption, -) -> Result<()> { - // Fixtures - let (source, repo) = (tar_gz_testdata?, set_up_repo?.to_indexed_ids()?); - - let opts = BackupOptions::default(); - - // first backup - let paths = PathList::from_iter(Some(source.0.path().join("0/0/9"))); - let snapshot1 = repo.backup(&opts, &paths, SnapshotFile::default())?; - - // re-read index - let repo = repo.to_indexed_ids()?; - // second backup - let paths = PathList::from_iter(Some(source.0.path().join("0/0/9/2"))); - let _ = repo.backup(&opts, &paths, SnapshotFile::default())?; - - // re-read index - let repo = repo.to_indexed_ids()?; - // third backup - let paths = PathList::from_iter(Some(source.0.path().join("0/0/9/3"))); - let _ = repo.backup(&opts, &paths, SnapshotFile::default())?; - - // drop index - let repo = repo.drop_index(); - repo.delete_snapshots(&[snapshot1.id])?; - - // get prune plan - let prune_opts = PruneOptions::default() - .instant_delete(instant_delete) - .max_unused(max_unused) - .keep_delete(Duration::ZERO); - let plan = repo.prune_plan(&prune_opts)?; - // TODO: Snapshot-test the plan (currently doesn't impl Serialize) - // assert_ron_snapshot!("prune", plan); - repo.prune(&prune_opts, plan)?; - - // run check - let check_opts = CheckOptions::default().read_data(true); - repo.check(check_opts)?; - - if !instant_delete { - // re-run if we only marked pack files. As keep-delete = 0, they should be removed here - let plan = repo.prune_plan(&prune_opts)?; - repo.prune(&prune_opts, plan)?; - repo.check(check_opts)?; - } - - Ok(()) -} - /// Verifies that users can create wrappers around repositories /// without resorting to generics. The rationale is that such /// types can be used to dynamically open, store, and cache repos. diff --git a/crates/core/tests/integration/backup.rs b/crates/core/tests/integration/backup.rs new file mode 100644 index 00000000..3614e8c4 --- /dev/null +++ b/crates/core/tests/integration/backup.rs @@ -0,0 +1,245 @@ +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; + +use anyhow::Result; +use insta::Settings; +use pretty_assertions::assert_eq; +use rstest::rstest; + +use rustic_core::{ + repofile::{PackId, SnapshotFile}, + BackupOptions, CommandInput, ParentOptions, PathList, SnapshotGroupCriterion, SnapshotOptions, + StringList, +}; + +use super::{ + assert_with_win, insta_node_redaction, insta_snapshotfile_redaction, set_up_repo, + tar_gz_testdata, RepoOpen, TestSource, +}; + +#[rstest] +fn test_backup_with_tar_gz_passes( + tar_gz_testdata: Result, + set_up_repo: Result, + insta_snapshotfile_redaction: Settings, + insta_node_redaction: Settings, +) -> Result<()> { + // uncomment for logging output + // SimpleLogger::init(log::LevelFilter::Debug, Config::default())?; + + // Fixtures + let (source, repo) = (tar_gz_testdata?, set_up_repo?.to_indexed_ids()?); + + let paths = &source.path_list(); + + // we use as_path to not depend on the actual tempdir + let opts = BackupOptions::default().as_path(PathBuf::from_str("test")?); + + // first backup + let first_snapshot = repo.backup(&opts, paths, SnapshotFile::default())?; + + // We can also bind to scope ( https://docs.rs/insta/latest/insta/struct.Settings.html#method.bind_to_scope ) + // But I think that can get messy with a lot of tests, also checking which settings are currently applied + // will be probably harder + insta_snapshotfile_redaction.bind(|| { + assert_with_win("backup-tar-summary-first", &first_snapshot); + }); + + assert_eq!(first_snapshot.parent, None); + + // tree of first backup + // re-read index + let repo = repo.to_indexed_ids()?; + let tree = repo.node_from_path(first_snapshot.tree, Path::new("test/0/tests"))?; + let tree: rustic_core::repofile::Tree = repo.get_tree(&tree.subtree.expect("Sub tree"))?; + + insta_node_redaction.bind(|| { + assert_with_win("backup-tar-tree", tree); + }); + + // get all snapshots and check them + let all_snapshots = repo.get_all_snapshots()?; + assert_eq!(vec![first_snapshot.clone()], all_snapshots); + // save list of pack files + let packs1: Vec = repo.list()?.collect(); + + // re-read index + let repo = repo.to_indexed_ids()?; + // second backup + let second_snapshot = repo.backup(&opts, paths, SnapshotFile::default())?; + + insta_snapshotfile_redaction.bind(|| { + assert_with_win("backup-tar-summary-second", &second_snapshot); + }); + + assert_eq!(second_snapshot.parent, Some(first_snapshot.id)); + assert_eq!(first_snapshot.tree, second_snapshot.tree); + + // pack files should be unchanged + let packs2: Vec<_> = repo.list()?.collect(); + assert_eq!(packs1, packs2); + + // re-read index + let repo = repo.to_indexed_ids()?; + // third backup with tags and explicitly given parent + let snap = SnapshotOptions::default() + .tags([StringList::from_str("a,b")?]) + .to_snapshot()?; + let opts = opts.parent_opts(ParentOptions::default().parent(second_snapshot.id.to_string())); + let third_snapshot = repo.backup(&opts, paths, snap)?; + + insta_snapshotfile_redaction.bind(|| { + assert_with_win("backup-tar-summary-third", &third_snapshot); + }); + assert_eq!(third_snapshot.parent, Some(second_snapshot.id)); + assert_eq!(third_snapshot.tree, second_snapshot.tree); + + // get all snapshots and check them + let mut all_snapshots = repo.get_all_snapshots()?; + all_snapshots.sort_unstable(); + assert_eq!( + vec![first_snapshot, second_snapshot, third_snapshot], + all_snapshots + ); + + // pack files should be unchanged + let packs2: Vec<_> = repo.list()?.collect(); + assert_eq!(packs1, packs2); + let packs3: Vec<_> = repo.list()?.collect(); + assert_eq!(packs1, packs3); + + // Check if snapshots can be retrieved + let mut ids: Vec<_> = all_snapshots.iter().map(|sn| sn.id.to_string()).collect(); + let snaps = repo.get_snapshots(&ids)?; + assert_eq!(snaps, all_snapshots); + + // reverse order + all_snapshots.reverse(); + ids.reverse(); + let snaps = repo.update_snapshots(snaps, &ids)?; + assert_eq!(snaps, all_snapshots); + + // get snapshot group + let group_by = SnapshotGroupCriterion::new().tags(true); + let mut groups = repo.get_snapshot_group(&[], group_by, |_| true)?; + + // sort groups to get unique result + groups.iter_mut().for_each(|(_, snaps)| snaps.sort()); + groups.sort_by_key(|(group, _)| group.tags.clone()); + + insta_snapshotfile_redaction.bind(|| { + assert_with_win("backup-tar-groups", &groups); + }); + + // filter snapshots by tag + let filter = |snap: &SnapshotFile| snap.tags.contains("a"); + let snaps = repo.get_matching_snapshots(filter)?; + insta_snapshotfile_redaction.bind(|| { + assert_with_win("backup-tar-matching-snaps", &snaps); + }); + + Ok(()) +} + +#[rstest] +fn test_backup_dry_run_with_tar_gz_passes( + tar_gz_testdata: Result, + set_up_repo: Result, + insta_snapshotfile_redaction: Settings, + insta_node_redaction: Settings, +) -> Result<()> { + // Fixtures + let (source, repo) = (tar_gz_testdata?, set_up_repo?.to_indexed_ids()?); + + let paths = &source.path_list(); + + // we use as_path to not depend on the actual tempdir + let opts = BackupOptions::default() + .as_path(PathBuf::from_str("test")?) + .dry_run(true); + + // dry-run backup + let snap_dry_run = repo.backup(&opts, paths, SnapshotFile::default())?; + + insta_snapshotfile_redaction.bind(|| { + assert_with_win("dryrun-tar-summary-first", &snap_dry_run); + }); + + // check that repo is still empty + let snaps = repo.get_all_snapshots()?; + assert_eq!(snaps.len(), 0); + assert_eq!(repo.list::()?.count(), 0); + assert_eq!(repo.list::()?.count(), 0); + + // first real backup + let opts = opts.dry_run(false); + let first_snapshot = repo.backup(&opts, paths, SnapshotFile::default())?; + assert_eq!(snap_dry_run.tree, first_snapshot.tree); + let packs: Vec<_> = repo.list::()?.collect(); + + // tree of first backup + // re-read index + let repo = repo.to_indexed_ids()?; + let tree = repo.node_from_path(first_snapshot.tree, Path::new("test/0/tests"))?; + let tree = repo.get_tree(&tree.subtree.expect("Sub tree"))?; + + insta_node_redaction.bind(|| { + assert_with_win("dryrun-tar-tree", tree); + }); + + // re-read index + let repo = repo.to_indexed_ids()?; + // second dry-run backup + let opts = opts.dry_run(true); + let snap_dry_run = repo.backup(&opts, paths, SnapshotFile::default())?; + + insta_snapshotfile_redaction.bind(|| { + assert_with_win("dryrun-tar-summary-second", &snap_dry_run); + }); + + // check that no data has been added + let snaps = repo.get_all_snapshots()?; + assert_eq!(snaps, vec![first_snapshot]); + let packs_dry_run: Vec = repo.list()?.collect(); + assert_eq!(packs_dry_run, packs); + + // re-read index + let repo = repo.to_indexed_ids()?; + // second real backup + let opts = opts.dry_run(false); + let second_snapshot = repo.backup(&opts, paths, SnapshotFile::default())?; + assert_eq!(snap_dry_run.tree, second_snapshot.tree); + Ok(()) +} + +#[rstest] +fn test_backup_stdin_command( + set_up_repo: Result, + insta_snapshotfile_redaction: Settings, +) -> Result<()> { + // Fixtures + let repo = set_up_repo?.to_indexed_ids()?; + let paths = PathList::from_string("-")?; + + let cmd: CommandInput = "echo test".parse()?; + let opts = BackupOptions::default() + .stdin_filename("test") + .stdin_command(cmd); + // backup data from cmd + let snapshot = repo.backup(&opts, &paths, SnapshotFile::default())?; + insta_snapshotfile_redaction.bind(|| { + assert_with_win("stdin-command-summary", &snapshot); + }); + + // re-read index + let repo = repo.to_indexed()?; + + // check content + let node = repo.node_from_snapshot_path("latest:test", |_| true)?; + let mut content = Vec::new(); + repo.dump(&node, &mut content)?; + assert_eq!(content, b"test\n"); + Ok(()) +} diff --git a/crates/core/tests/integration/find.rs b/crates/core/tests/integration/find.rs new file mode 100644 index 00000000..a647282b --- /dev/null +++ b/crates/core/tests/integration/find.rs @@ -0,0 +1,61 @@ +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; + +use anyhow::Result; +use globset::Glob; +use rstest::rstest; + +use rustic_core::{ + repofile::{Node, SnapshotFile}, + BackupOptions, FindMatches, FindNode, +}; + +use super::{assert_with_win, set_up_repo, tar_gz_testdata, RepoOpen, TestSource}; + +#[rstest] +fn test_find(tar_gz_testdata: Result, set_up_repo: Result) -> Result<()> { + // Fixtures + let (source, repo) = (tar_gz_testdata?, set_up_repo?.to_indexed_ids()?); + let paths = &source.path_list(); + + // we use as_path to not depend on the actual tempdir + let opts = BackupOptions::default().as_path(PathBuf::from_str("test")?); + // backup test-data + let snapshot = repo.backup(&opts, paths, SnapshotFile::default())?; + + // re-read index + let repo = repo.to_indexed_ids()?; + + // test non-existing path + let not_found = repo.find_nodes_from_path(vec![snapshot.tree], Path::new("not_existing"))?; + assert_with_win("find-nodes-not-found", not_found); + // test non-existing match + let glob = Glob::new("not_existing")?.compile_matcher(); + let not_found = + repo.find_matching_nodes(vec![snapshot.tree], &|path, _| glob.is_match(path))?; + assert_with_win("find-matching-nodes-not-found", not_found); + + // test existing path + let FindNode { matches, .. } = + repo.find_nodes_from_path(vec![snapshot.tree], Path::new("test/0/tests/testfile"))?; + assert_with_win("find-nodes-existing", matches); + // test existing match + let glob = Glob::new("testfile")?.compile_matcher(); + let match_func = |path: &Path, _: &Node| { + glob.is_match(path) || path.file_name().is_some_and(|f| glob.is_match(f)) + }; + let FindMatches { paths, matches, .. } = + repo.find_matching_nodes(vec![snapshot.tree], &match_func)?; + assert_with_win("find-matching-existing", (paths, matches)); + // test existing match + let glob = Glob::new("testfile*")?.compile_matcher(); + let match_func = |path: &Path, _: &Node| { + glob.is_match(path) || path.file_name().is_some_and(|f| glob.is_match(f)) + }; + let FindMatches { paths, matches, .. } = + repo.find_matching_nodes(vec![snapshot.tree], &match_func)?; + assert_with_win("find-matching-wildcard-existing", (paths, matches)); + Ok(()) +} diff --git a/crates/core/tests/integration/ls.rs b/crates/core/tests/integration/ls.rs new file mode 100644 index 00000000..184ef5ff --- /dev/null +++ b/crates/core/tests/integration/ls.rs @@ -0,0 +1,52 @@ +use std::{collections::BTreeMap, ffi::OsStr}; +use std::{path::PathBuf, str::FromStr}; + +use anyhow::Result; +use insta::Settings; +use rstest::rstest; + +use rustic_core::{ + repofile::{Metadata, Node, SnapshotFile}, + BackupOptions, LsOptions, RusticResult, +}; + +use super::{ + assert_with_win, insta_node_redaction, set_up_repo, tar_gz_testdata, RepoOpen, TestSource, +}; + +#[rstest] +fn test_ls( + tar_gz_testdata: Result, + set_up_repo: Result, + insta_node_redaction: Settings, +) -> Result<()> { + // Fixtures + let (source, repo) = (tar_gz_testdata?, set_up_repo?.to_indexed_ids()?); + let paths = &source.path_list(); + + // we use as_path to not depend on the actual tempdir + let opts = BackupOptions::default().as_path(PathBuf::from_str("test")?); + // backup test-data + let snapshot = repo.backup(&opts, paths, SnapshotFile::default())?; + + // test non-existing entries + let mut node = Node::new_node( + OsStr::new(""), + rustic_core::repofile::NodeType::Dir, + Metadata::default(), + ); + node.subtree = Some(snapshot.tree); + + // re-read index + let repo = repo.to_indexed_ids()?; + + let entries: BTreeMap<_, _> = repo + .ls(&node, &LsOptions::default())? + .collect::>()?; + + insta_node_redaction.bind(|| { + assert_with_win("ls", &entries); + }); + + Ok(()) +} diff --git a/crates/core/tests/integration/prune.rs b/crates/core/tests/integration/prune.rs new file mode 100644 index 00000000..75b3034a --- /dev/null +++ b/crates/core/tests/integration/prune.rs @@ -0,0 +1,71 @@ +use std::time::Duration; + +use anyhow::Result; +use rstest::rstest; + +use rustic_core::{ + repofile::SnapshotFile, BackupOptions, CheckOptions, LimitOption, PathList, PruneOptions, +}; + +use super::{set_up_repo, tar_gz_testdata, RepoOpen, TestSource}; + +#[rstest] +fn test_prune( + tar_gz_testdata: Result, + set_up_repo: Result, + #[values(true, false)] instant_delete: bool, + #[values( + LimitOption::Percentage(0), + LimitOption::Percentage(50), + LimitOption::Unlimited + )] + max_unused: LimitOption, +) -> Result<()> { + // Fixtures + let (source, repo) = (tar_gz_testdata?, set_up_repo?.to_indexed_ids()?); + + let opts = BackupOptions::default(); + + // first backup + let paths = PathList::from_iter(Some(source.0.path().join("0/0/9"))); + let snapshot1 = repo.backup(&opts, &paths, SnapshotFile::default())?; + + // re-read index + let repo = repo.to_indexed_ids()?; + // second backup + let paths = PathList::from_iter(Some(source.0.path().join("0/0/9/2"))); + let _ = repo.backup(&opts, &paths, SnapshotFile::default())?; + + // re-read index + let repo = repo.to_indexed_ids()?; + // third backup + let paths = PathList::from_iter(Some(source.0.path().join("0/0/9/3"))); + let _ = repo.backup(&opts, &paths, SnapshotFile::default())?; + + // drop index + let repo = repo.drop_index(); + repo.delete_snapshots(&[snapshot1.id])?; + + // get prune plan + let prune_opts = PruneOptions::default() + .instant_delete(instant_delete) + .max_unused(max_unused) + .keep_delete(Duration::ZERO); + let plan = repo.prune_plan(&prune_opts)?; + // TODO: Snapshot-test the plan (currently doesn't impl Serialize) + // assert_ron_snapshot!("prune", plan); + repo.prune(&prune_opts, plan)?; + + // run check + let check_opts = CheckOptions::default().read_data(true); + repo.check(check_opts)?; + + if !instant_delete { + // re-run if we only marked pack files. As keep-delete = 0, they should be removed here + let plan = repo.prune_plan(&prune_opts)?; + repo.prune(&prune_opts, plan)?; + repo.check(check_opts)?; + } + + Ok(()) +} diff --git a/crates/core/tests/integration/restore.rs b/crates/core/tests/integration/restore.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/core/tests/integration/restore.rs @@ -0,0 +1 @@ + diff --git a/crates/core/tests/integration/vfs.rs b/crates/core/tests/integration/vfs.rs new file mode 100644 index 00000000..9d7584c3 --- /dev/null +++ b/crates/core/tests/integration/vfs.rs @@ -0,0 +1,60 @@ +use std::{path::PathBuf, str::FromStr}; + +use anyhow::Result; +use bytes::Bytes; +use insta::Settings; +use pretty_assertions::assert_eq; +use rstest::rstest; + +use rustic_core::{repofile::SnapshotFile, vfs::Vfs, BackupOptions}; + +use super::{ + assert_with_win, insta_node_redaction, set_up_repo, tar_gz_testdata, RepoOpen, TestSource, +}; + +#[rstest] +fn test_vfs( + tar_gz_testdata: Result, + set_up_repo: Result, + insta_node_redaction: Settings, +) -> Result<()> { + // Fixtures + let (source, repo) = (tar_gz_testdata?, set_up_repo?.to_indexed_ids()?); + let paths = &source.path_list(); + + // we use as_path to not depend on the actual tempdir + let opts = BackupOptions::default().as_path(PathBuf::from_str("test")?); + // backup test-data + let snapshot = repo.backup(&opts, paths, SnapshotFile::default())?; + + // re-read index + let repo = repo.to_indexed()?; + // create Vfs + let node = repo.node_from_snapshot_and_path(&snapshot, "")?; + let vfs = Vfs::from_dir_node(&node); + + // test reading a directory using vfs + let path: PathBuf = ["test", "0", "tests"].iter().collect(); + let entries = vfs.dir_entries_from_path(&repo, &path)?; + insta_node_redaction.bind(|| { + assert_with_win("vfs", &entries); + }); + + // test reading a file from the repository + let path: PathBuf = ["test", "0", "tests", "testfile"].iter().collect(); + let node = vfs.node_from_path(&repo, &path)?; + let file = repo.open_file(&node)?; + + let data = repo.read_file_at(&file, 0, 21)?; // read full content + assert_eq!(Bytes::from("This is a test file.\n"), &data); + assert_eq!(data, repo.read_file_at(&file, 0, 4096)?); // read beyond file end + assert_eq!(Bytes::new(), repo.read_file_at(&file, 25, 1)?); // offset beyond file end + assert_eq!(Bytes::from("test"), repo.read_file_at(&file, 10, 4)?); // read partial content + + // test reading an empty file from the repository + let path: PathBuf = ["test", "0", "tests", "empty-file"].iter().collect(); + let node = vfs.node_from_path(&repo, &path)?; + let file = repo.open_file(&node)?; + assert_eq!(Bytes::new(), repo.read_file_at(&file, 0, 0)?); // empty files + Ok(()) +} diff --git a/crates/core/tests/snapshots/integration__vfs-nix.snap b/crates/core/tests/snapshots/integration__vfs-nix.snap new file mode 100644 index 00000000..1abce237 --- /dev/null +++ b/crates/core/tests/snapshots/integration__vfs-nix.snap @@ -0,0 +1,78 @@ +--- +source: crates/core/tests/integration.rs +expression: snap +--- +[ + { + "name": "empty-file", + "type": "file", + "mode": "[some]", + "mtime": "[some]", + "atime": "[some]", + "ctime": "[some]", + "uid": "[uid]", + "gid": "[gid]", + "user": "[user]", + "group": "[group]", + "inode": "[inode]", + "device_id": "[device_id]", + "links": 1, + "content": Some([]), + }, + { + "name": "testfile", + "type": "file", + "mode": "[some]", + "mtime": "[some]", + "atime": "[some]", + "ctime": "[some]", + "uid": "[uid]", + "gid": "[gid]", + "user": "[user]", + "group": "[group]", + "inode": "[inode]", + "device_id": "[device_id]", + "size": 21, + "links": 2, + "content": Some([ + Id("649b8b471e7d7bc175eec758a7006ac693c434c8297c07db15286788c837154a"), + ]), + }, + { + "name": "testfile-hardlink", + "type": "file", + "mode": "[some]", + "mtime": "[some]", + "atime": "[some]", + "ctime": "[some]", + "uid": "[uid]", + "gid": "[gid]", + "user": "[user]", + "group": "[group]", + "inode": "[inode]", + "device_id": "[device_id]", + "size": 21, + "links": 2, + "content": Some([ + Id("649b8b471e7d7bc175eec758a7006ac693c434c8297c07db15286788c837154a"), + ]), + }, + { + "name": "testfile-symlink", + "type": "symlink", + "linktarget": "testfile", + "mode": "[some]", + "mtime": "[some]", + "atime": "[some]", + "ctime": "[some]", + "uid": "[uid]", + "gid": "[gid]", + "user": "[user]", + "group": "[group]", + "inode": "[inode]", + "device_id": "[device_id]", + "size": 8, + "links": 1, + "content": None, + }, +] diff --git a/crates/core/tests/snapshots/integration__vfs-windows.snap b/crates/core/tests/snapshots/integration__vfs-windows.snap new file mode 100644 index 00000000..774dd2a4 --- /dev/null +++ b/crates/core/tests/snapshots/integration__vfs-windows.snap @@ -0,0 +1,45 @@ +--- +source: crates/core/tests/integration.rs +expression: snap +--- +[ + { + "name": "empty-file", + "type": "file", + "mtime": "[some]", + "atime": "[some]", + "ctime": "[some]", + "content": Some([]), + }, + { + "name": "testfile", + "type": "file", + "mtime": "[some]", + "atime": "[some]", + "ctime": "[some]", + "size": 21, + "content": Some([ + Id("649b8b471e7d7bc175eec758a7006ac693c434c8297c07db15286788c837154a"), + ]), + }, + { + "name": "testfile-hardlink", + "type": "file", + "mtime": "[some]", + "atime": "[some]", + "ctime": "[some]", + "size": 21, + "content": Some([ + Id("649b8b471e7d7bc175eec758a7006ac693c434c8297c07db15286788c837154a"), + ]), + }, + { + "name": "testfile-symlink", + "type": "symlink", + "linktarget": "testfile", + "mtime": "[some]", + "atime": "[some]", + "ctime": "[some]", + "content": None, + }, +]