diff --git a/Cargo.lock b/Cargo.lock index b91bdb783..b6cfa3ad6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,6 +115,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -391,10 +397,11 @@ checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc" [[package]] name = "cached" -version = "0.49.2" +version = "0.49.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f251fd1e72720ca07bf5d8e310f54a193fd053479a1f6342c6663ee4fa01cf96" +checksum = "8e8e463fceca5674287f32d252fb1d94083758b8709c160efae66d263e5f4eba" dependencies = [ + "ahash", "cached_proc_macro", "cached_proc_macro_types", "hashbrown 0.14.3", @@ -1489,6 +1496,10 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "headers" @@ -3017,6 +3028,7 @@ dependencies = [ "anyhow", "assert_cmd", "bytesize", + "cached", "chrono", "clap", "clap_complete", diff --git a/Cargo.toml b/Cargo.toml index cbea320c9..0f462d4cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ simplelog = "0.12" # commands bytesize = "1" +cached = "0.49.3" clap = { version = "4", features = ["derive", "env", "wrap_help"] } clap_complete = "4" convert_case = "0.6.0" @@ -88,6 +89,7 @@ itertools = "0.12" merge = "0.1" once_cell = "1.19" self_update = { version = "0.39", default-features = false, optional = true, features = ["rustls", "archive-tar", "compression-flate2"] } +toml = "0.8" [dev-dependencies] abscissa_core = { version = "0.7.0", default-features = false, features = ["testing"] } diff --git a/src/commands/backup.rs b/src/commands/backup.rs index d1f4c34f8..78e21cc26 100644 --- a/src/commands/backup.rs +++ b/src/commands/backup.rs @@ -12,7 +12,7 @@ use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::{bail, Context, Result}; use log::{debug, info, warn}; use merge::Merge; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use rustic_core::{ BackupOptions, ConfigOptions, KeyOptions, LocalSourceFilterOptions, LocalSourceSaveOptions, @@ -20,7 +20,7 @@ use rustic_core::{ }; /// `backup` subcommand -#[derive(Clone, Command, Default, Debug, clap::Parser, Deserialize, Merge)] +#[derive(Clone, Command, Default, Debug, clap::Parser, Serialize, Deserialize, Merge)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] // Note: using cli_sources, sources and source within this struct is a hack to support serde(deny_unknown_fields) // for deserializing the backup options from TOML diff --git a/src/commands/copy.rs b/src/commands/copy.rs index 5306a0027..725333f78 100644 --- a/src/commands/copy.rs +++ b/src/commands/copy.rs @@ -10,7 +10,7 @@ use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::{bail, Result}; use log::{error, info}; use merge::Merge; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use rustic_core::{CopySnapshot, Id, KeyOptions}; @@ -31,7 +31,7 @@ pub(crate) struct CopyCmd { } /// Target repository options -#[derive(Default, Clone, Debug, Deserialize, Merge)] +#[derive(Default, Clone, Debug, Serialize, Deserialize, Merge)] pub struct Targets { /// Target repositories #[merge(strategy = merge::vec::overwrite_empty)] diff --git a/src/commands/forget.rs b/src/commands/forget.rs index 6ce30ee0a..395426329 100644 --- a/src/commands/forget.rs +++ b/src/commands/forget.rs @@ -10,7 +10,7 @@ use abscissa_core::{Command, FrameworkError, Runnable}; use anyhow::Result; use merge::Merge; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; use crate::{commands::prune::PruneCmd, filtering::SnapshotFilter}; @@ -63,7 +63,7 @@ impl Override for ForgetCmd { /// Forget options #[serde_as] -#[derive(Clone, Default, Debug, clap::Parser, Deserialize, Merge)] +#[derive(Clone, Default, Debug, clap::Parser, Serialize, Deserialize, Merge)] #[serde(default, rename_all = "kebab-case")] pub struct ForgetOptions { /// Group snapshots by any combination of host,label,paths,tags (default: "host,label,paths") diff --git a/src/commands/show_config.rs b/src/commands/show_config.rs index 10797fe10..84d8ee9a4 100644 --- a/src/commands/show_config.rs +++ b/src/commands/show_config.rs @@ -1,8 +1,10 @@ //! `show-config` subcommand -use crate::{Application, RUSTIC_APP}; +use crate::{status_err, Application, RUSTIC_APP}; -use abscissa_core::{Command, Runnable}; +use abscissa_core::{Command, Runnable, Shutdown}; +use anyhow::Result; +use toml::to_string_pretty; /// `show-config` subcommand #[derive(clap::Parser, Command, Debug)] @@ -10,7 +12,17 @@ pub(crate) struct ShowConfigCmd {} impl Runnable for ShowConfigCmd { fn run(&self) { - let config = RUSTIC_APP.config(); - println!("{config:#?}"); + if let Err(err) = self.inner_run() { + status_err!("{}", err); + RUSTIC_APP.shutdown(Shutdown::Crash); + }; + } +} + +impl ShowConfigCmd { + fn inner_run(&self) -> Result<()> { + let config = to_string_pretty(RUSTIC_APP.config().as_ref())?; + println!("{config}"); + Ok(()) } } diff --git a/src/commands/webdav.rs b/src/commands/webdav.rs index 4e3f548a0..03df8cf5b 100644 --- a/src/commands/webdav.rs +++ b/src/commands/webdav.rs @@ -6,11 +6,11 @@ use abscissa_core::{config::Override, Command, FrameworkError, Runnable, Shutdow use anyhow::{anyhow, Result}; use dav_server::{warp::dav_handler, DavHandler}; use merge::Merge; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use rustic_core::vfs::{FilePolicy, IdenticalSnapshot, Latest, Vfs}; -#[derive(Clone, Command, Default, Debug, clap::Parser, Deserialize, Merge)] +#[derive(Clone, Command, Default, Debug, clap::Parser, Serialize, Deserialize, Merge)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] pub struct WebDavCmd { /// Address to bind the webdav server to. [default: "localhost:8000"] diff --git a/src/config.rs b/src/config.rs index 3a6d3757c..4d0875851 100644 --- a/src/config.rs +++ b/src/config.rs @@ -36,7 +36,7 @@ use crate::{ /// /// # Example // TODO: add example -#[derive(Clone, Default, Debug, Parser, Deserialize, Merge)] +#[derive(Clone, Default, Debug, Parser, Deserialize, Serialize, Merge)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] pub struct RusticConfig { /// Global options @@ -69,7 +69,7 @@ pub struct RusticConfig { pub webdav: WebDavCmd, } -#[derive(Clone, Default, Debug, Parser, Deserialize, Merge)] +#[derive(Clone, Default, Debug, Parser, Serialize, Deserialize, Merge)] #[serde(default, rename_all = "kebab-case")] pub struct AllRepositoryOptions { /// Backend options diff --git a/src/filtering.rs b/src/filtering.rs index fefb7ae1c..5eb42abbe 100644 --- a/src/filtering.rs +++ b/src/filtering.rs @@ -4,8 +4,9 @@ use log::warn; use rustic_core::{repofile::SnapshotFile, StringList}; use std::{error::Error, str::FromStr}; +use cached::proc_macro::cached; use rhai::{serde::to_dynamic, Dynamic, Engine, FnPtr, AST}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; /// A function to filter snapshots @@ -24,6 +25,17 @@ impl FromStr for SnapshotFn { } } +#[cached(key = "String", convert = r#"{ s.to_string() }"#, size = 1)] +fn string_to_fn(s: &str) -> Option { + match SnapshotFn::from_str(s) { + Ok(filter_fn) => Some(filter_fn), + Err(err) => { + warn!("Error evaluating filter-fn {s}: {err}",); + None + } + } +} + impl SnapshotFn { /// Call the function with a [`SnapshotFile`] /// @@ -43,7 +55,7 @@ impl SnapshotFn { } #[serde_as] -#[derive(Clone, Default, Debug, Deserialize, merge::Merge, clap::Parser)] +#[derive(Clone, Default, Debug, Serialize, Deserialize, merge::Merge, clap::Parser)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] pub struct SnapshotFilter { /// Hostname to filter (can be specified multiple times) @@ -71,7 +83,7 @@ pub struct SnapshotFilter { /// Function to filter snapshots #[clap(long, global = true, value_name = "FUNC")] #[serde_as(as = "Option")] - filter_fn: Option, + filter_fn: Option, } impl SnapshotFilter { @@ -87,17 +99,19 @@ impl SnapshotFilter { #[must_use] pub fn matches(&self, snapshot: &SnapshotFile) -> bool { if let Some(filter_fn) = &self.filter_fn { - match filter_fn.call::(snapshot) { - Ok(result) => { - if !result { - return false; + if let Some(func) = string_to_fn(filter_fn) { + match func.call::(snapshot) { + Ok(result) => { + if !result { + return false; + } + } + Err(err) => { + warn!( + "Error evaluating filter-fn for snapshot {}: {err}", + snapshot.id + ); } - } - Err(err) => { - warn!( - "Error evaluating filter-fn for snapshot {}: {err}", - snapshot.id - ); } } } diff --git a/tests/show-config-fixtures/empty.txt b/tests/show-config-fixtures/empty.txt new file mode 100644 index 000000000..460575cbc --- /dev/null +++ b/tests/show-config-fixtures/empty.txt @@ -0,0 +1,80 @@ +[global] +use-profile = [] +dry-run = false +no-progress = false + +[global.env] + +[repository] +no-cache = false +warm-up = false + +[repository.options] + +[repository.options-hot] + +[repository.options-cold] + +[snapshot-filter] +filter-host = [] +filter-label = [] +filter-paths = [] +filter-tags = [] + +[backup] +stdin-filename = "" +with-atime = false +ignore-devid = false +no-scan = false +json = false +quiet = false +init = false +skip-identical-parent = false +force = false +ignore-ctime = false +ignore-inode = false +glob = [] +iglob = [] +glob-file = [] +iglob-file = [] +git-ignore = false +no-require-git = false +custom-ignorefile = [] +exclude-if-present = [] +one-file-system = false +tag = [] +delete-never = false +sources = [] +source = "" + +[copy] +targets = [] + +[forget] +prune = false +filter-host = [] +filter-label = [] +filter-paths = [] +filter-tags = [] +keep-tags = [] +keep-ids = [] +keep-last = 0 +keep-hourly = 0 +keep-daily = 0 +keep-weekly = 0 +keep-monthly = 0 +keep-quarter-yearly = 0 +keep-half-yearly = 0 +keep-yearly = 0 +keep-within = "0s" +keep-within-hourly = "0s" +keep-within-daily = "0s" +keep-within-weekly = "0s" +keep-within-monthly = "0s" +keep-within-quarter-yearly = "0s" +keep-within-half-yearly = "0s" +keep-within-yearly = "0s" + +[webdav] +symlinks = false + diff --git a/tests/show-config.rs b/tests/show-config.rs new file mode 100644 index 000000000..536651322 --- /dev/null +++ b/tests/show-config.rs @@ -0,0 +1,64 @@ +//! Config profile test: runs the application as a subprocess and asserts its +//! output for the `show-config` command + +// #![forbid(unsafe_code)] +// #![warn( +// missing_docs, +// rust_2018_idioms, +// trivial_casts, +// unused_lifetimes, +// unused_qualifications +// )] + +use std::{ + io::{Read, Write}, + path::PathBuf, +}; + +use once_cell::sync::Lazy; + +use abscissa_core::testing::prelude::*; + +use rustic_testing::{files_differ, get_temp_file, TestResult}; + +// Storing this value as a [`Lazy`] static ensures that all instances of +/// the runner acquire a mutex when executing commands and inspecting +/// exit statuses, serializing what would otherwise be multithreaded +/// invocations as `cargo test` executes tests in parallel by default. +pub static LAZY_RUNNER: Lazy = Lazy::new(|| { + let mut runner = CmdRunner::new(env!("CARGO_BIN_EXE_rustic")); + runner.exclusive().capture_stdout(); + runner +}); + +fn cmd_runner() -> CmdRunner { + LAZY_RUNNER.clone() +} + +fn fixtures_dir() -> PathBuf { + ["tests", "show-config-fixtures"].iter().collect() +} + +#[test] +fn show_config_passes() -> TestResult<()> { + let fixture_path = fixtures_dir().join("empty.txt"); + let mut file = get_temp_file()?; + + { + let file = file.as_file_mut(); + let mut runner = cmd_runner(); + let mut cmd = runner.args(["show-config"]).run(); + + let mut output = String::new(); + cmd.stdout().read_to_string(&mut output)?; + file.write_all(output.as_bytes())?; + file.sync_all()?; + cmd.wait()?.expect_success(); + } + + if files_differ(fixture_path, file.path())? { + panic!("generated completions for bash shell differ, breaking change!"); + } + + Ok(()) +}