rustic/tests/hooks.rs
aawsome cb3903b9dc
feat(config): Add hooks (#1218)
This PR adds `run-before`, `run-after`, `run-failure` and `run-finally`
hooks for:
- all commands in the `[global.hooks]` config profile section
- commands accessing the repository in the `[repository.hooks]` config
profile section
- the `backup` command specifically in the `[backup.hooks]` config
profile section
- specific backup sources in the `[backup.snapshots.hooks]` section

Note: This PR includes only calling the given commands. If there is the
wish for supplying information to the commands (env variables or
parameter substitution), this should be covered by a separate feature
request/PR.

closes #902

---------

Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com>
Co-authored-by: simonsan <14062932+simonsan@users.noreply.github.com>
2024-10-05 01:58:55 +00:00

315 lines
9.1 KiB
Rust

//! Hooks test: runs the application as a subprocess and asserts its
//! interaction with different files due to hooks
// #![forbid(unsafe_code)]
// #![warn(
// missing_docs,
// rust_2018_idioms,
// trivial_casts,
// unused_lifetimes,
// unused_qualifications
// )]
use std::path::PathBuf;
use abscissa_core::fs::remove_file;
use assert_cmd::Command;
use predicates::prelude::predicate;
use rstest::{fixture, rstest};
use tempfile::{tempdir, TempDir};
use rustic_testing::TestResult;
#[fixture]
fn hook_fixture_dir() -> PathBuf {
["tests", "hooks-fixtures"].iter().collect()
}
#[fixture]
fn generated_dir() -> PathBuf {
["tests", "generated"].iter().collect()
}
#[fixture]
fn toml_fixture_dir() -> PathBuf {
hook_fixture_dir()
}
#[fixture]
fn log_fixture_dir() -> PathBuf {
hook_fixture_dir().join("log")
}
pub fn rustic_runner(temp_dir: &TempDir) -> TestResult<Command> {
let password = "test";
let repo_dir = temp_dir.path().join("repo");
let mut runner = Command::new(env!("CARGO_BIN_EXE_rustic"));
runner
.arg("-r")
.arg(repo_dir)
.arg("--password")
.arg(password)
.arg("--no-progress");
Ok(runner)
}
#[allow(dead_code)]
enum BackupAction {
WithBackup,
WithoutBackup,
}
// Load a template from a file and replace a placeholder with a value
// and write the result to a new file
fn load_template_replace_and_write(
template_path: &PathBuf,
placeholder: &str,
value: &str,
output_path: &PathBuf,
) -> TestResult<()> {
let template = std::fs::read_to_string(template_path)?;
let replaced = template.replace(placeholder, value);
std::fs::write(output_path, replaced)?;
Ok(())
}
fn setup(with_backup: BackupAction) -> TestResult<TempDir> {
let temp_dir = tempdir()?;
rustic_runner(&temp_dir)?
.args(["init"])
.assert()
.success()
.stderr(predicate::str::contains("successfully created."))
.stderr(predicate::str::contains("successfully added."));
match with_backup {
BackupAction::WithBackup => {
rustic_runner(&temp_dir)?
// We need this so output on stderr is not being taken as an error
.arg("--log-level=error")
.args(["backup", "src/"])
.assert()
.success();
}
BackupAction::WithoutBackup => {}
}
Ok(temp_dir)
}
#[derive(Debug, PartialEq, Clone, Copy)]
enum RunnerStatus {
Success,
Failure,
}
fn run_hook_comparison(
temp_dir: TempDir,
hooks_config: PathBuf,
args: &[&str],
snapshot_name: &str,
log_live_path: PathBuf,
status: RunnerStatus,
) -> TestResult<()> {
{
let runner = rustic_runner(&temp_dir)?
// We need this so output on stderr is not being taken as an error
.arg("--log-level=error")
.args(["-P", hooks_config.to_str().unwrap()])
.args(args)
.assert();
match status {
RunnerStatus::Success => runner.success(),
RunnerStatus::Failure => runner.failure(),
};
}
let log_live = std::fs::read_to_string(&log_live_path)?;
remove_file(log_live_path)?;
insta::assert_ron_snapshot!(snapshot_name, log_live);
Ok(())
}
#[rstest]
fn test_empty_hooks_do_nothing_passes(toml_fixture_dir: PathBuf) -> TestResult<()> {
let hooks_config = toml_fixture_dir.join("empty_hooks_success");
let temp_dir = setup(BackupAction::WithoutBackup)?;
{
rustic_runner(&temp_dir)?
.args(["-P", hooks_config.to_str().unwrap()])
.arg("repoinfo")
.assert()
.success()
.stdout(predicate::str::contains("Total Size"));
}
Ok(())
}
macro_rules! generate_test_hook_function {
($name:ident, $fixture:expr, $args:expr, $status:expr) => {
#[rstest]
fn $name(toml_fixture_dir: PathBuf, generated_dir: PathBuf) -> TestResult<()> {
let hooks_config_path = toml_fixture_dir.join($fixture);
let args = $args;
let file_name = format!("{}.log", $fixture);
let log_live_path = generated_dir.join(&file_name);
run_hook_comparison(
setup(BackupAction::WithoutBackup)?,
hooks_config_path,
args,
$fixture,
log_live_path,
$status,
)?;
Ok(())
}
};
}
// Scenario: Global hooks pass in order
generate_test_hook_function!(
test_global_hooks_order_passes,
"global_hooks_success",
&["repoinfo"],
RunnerStatus::Success
);
// Scenario: Repository hooks pass in order
generate_test_hook_function!(
test_repository_hooks_order_passes,
"repository_hooks_success",
&["check"],
RunnerStatus::Success
);
// Scenario: Backup hooks pass in order
generate_test_hook_function!(
test_backup_hooks_order_passes,
"backup_hooks_success",
&["backup", "src/"],
RunnerStatus::Success
);
// Scenario: Full hooks pass in order
generate_test_hook_function!(
test_full_hooks_order_passes,
"full_hooks_success",
&["backup", "src/"],
RunnerStatus::Success
);
// Scenario: Check do not run backup hooks
generate_test_hook_function!(
test_check_do_not_run_backup_hooks_passes,
"check_not_backup_hooks_success",
&["check"],
RunnerStatus::Success
);
// Scenario: Failure in before backup hook does not run backup
generate_test_hook_function!(
test_backup_hooks_with_failure_passes,
"backup_hooks_failure",
&["backup", "src/"],
RunnerStatus::Failure
);
// Scenario: Failure in after backup hook does run repo and global
// hooks failed and finally
generate_test_hook_function!(
test_full_hooks_with_failure_before_backup_passes,
"full_hooks_before_backup_failure",
&["backup", "src/"],
RunnerStatus::Failure
);
// Scenario: Failure in before repo hook does run repo and global
// hooks failed and finally
generate_test_hook_function!(
test_full_hooks_with_failure_before_repo_passes,
"full_hooks_before_repo_failure",
&["backup", "src/"],
RunnerStatus::Failure
);
#[rstest]
#[case(vec!["backup", "src/"], "backup", BackupAction::WithoutBackup)]
#[case(vec!["cat", "tree", "latest"], "cat", BackupAction::WithBackup)]
#[case(vec!["config"], "config", BackupAction::WithoutBackup)]
#[case(vec!["completions", "bash"], "completions", BackupAction::WithoutBackup)]
#[case(vec!["check"], "check", BackupAction::WithBackup)]
// #[case(vec!["copy"], "copy", BackupAction::WithBackup)]
// #[case(vec!["diff"], "diff", BackupAction::WithBackup)]
// TODO: Does it work? Also: Docs command requires a TTY
// #[case(vec!["docs", "--help"], "docs", BackupAction::WithoutBackup)]
#[case(vec!["dump", "latest:src/lib.rs"], "dump", BackupAction::WithBackup)]
#[case(vec!["find"], "find", BackupAction::WithoutBackup)]
#[case(vec!["forget"], "forget", BackupAction::WithoutBackup)]
// TODO: Needs special handling
// #[case(vec!["init", "--dry-run"], "init", BackupAction::WithoutBackup)]
// TODO: Needs user input
// #[case(vec!["key", "add"], "key", BackupAction::WithoutBackup)]
#[case(vec!["list", "indexpacks"], "list", BackupAction::WithBackup)]
#[case(vec!["ls", "latest"], "ls", BackupAction::WithBackup)]
#[case(vec!["merge"], "merge", BackupAction::WithoutBackup)]
#[case(vec!["snapshots"], "snapshots", BackupAction::WithBackup)]
#[case(vec!["show-config"], "show-config", BackupAction::WithoutBackup)]
// TODO: Github API errors with `NetworkError: api request failed with status: 403`
// #[case(vec!["self-update"], "self-update", BackupAction::WithoutBackup)]
#[case(vec!["prune"], "prune", BackupAction::WithBackup)]
#[case(vec!["repoinfo"], "repoinfo", BackupAction::WithBackup)]
#[case(vec!["repair", "index"], "repair", BackupAction::WithBackup)]
#[case(vec!["restore", "latest", "tests/generated/test-restore", "--dry-run"], "restore", BackupAction::WithBackup)]
#[case(vec!["tag"], "tag", BackupAction::WithBackup)]
// TODO: Requires user input
// #[case(vec!["webdav"], "webdav", BackupAction::WithBackup)]
fn test_hooks_access_for_all_commands_passes(
#[case] command_args: Vec<&str>,
#[case] command_name: &str,
#[case] backup_action: BackupAction,
toml_fixture_dir: PathBuf,
generated_dir: PathBuf,
) -> TestResult<()> {
let file_name = format!("{command_name}_hooks_access_success");
let mut output_config_file_name = file_name.clone();
output_config_file_name.push_str(".toml");
let hooks_config_path = generated_dir.join(&file_name);
let output_config_path = generated_dir.join(output_config_file_name);
load_template_replace_and_write(
&toml_fixture_dir.join("commands_hooks_access_success.tpl"),
"${{filename}}",
&file_name,
&output_config_path,
)?;
let log_live_path = generated_dir.join(format!("{file_name}.log"));
let setup = setup(backup_action)?;
run_hook_comparison(
setup,
hooks_config_path.clone(),
command_args.as_slice(),
&file_name,
log_live_path.clone(),
RunnerStatus::Success,
)?;
remove_file(output_config_path)?;
Ok(())
}