Skip to content

Commit

Permalink
testing: add tiny framework for testing rustic's compat with latest r…
Browse files Browse the repository at this point in the history
…estic (#1303)

This is the first PR to lay some foundations to test rustic's
compatibility against a restic repository. It should essentially show,
how to start out with it and give some fixtures to play around with.

TODO:

- [X] add CI workflow + test that uses restic (latest) via
https://github.com/AnimMouse/setup-restic, to create a new repo and run
rustic against it
- [X] investigate `AnimMouse/setup-restic` failure and fix
- [X] forked `AnimMouse/setup-restic` to `rustic-rs/setup-restic` to run
on `@main` and apply faster fixes to our CI
- [X] used `AnimMouse/setup-restic` as a foundation for
`rustic-rs/setup-rustic`, still WIP
- [x] update to 'release' feature in test when #1307 is merged

---------

Signed-off-by: simonsan <[email protected]>
  • Loading branch information
simonsan authored Nov 19, 2024
1 parent b0bd5b6 commit 47426db
Show file tree
Hide file tree
Showing 10 changed files with 309 additions and 0 deletions.
82 changes: 82 additions & 0 deletions .github/workflows/compat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Compatibility

on:
pull_request:
paths-ignore:
- "**/*.md"
push:
branches:
- main
- "renovate/**"
paths-ignore:
- "**/*.md"
schedule:
- cron: "0 0 * * 0"
merge_group:
types: [checks_requested]

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
test:
name: Test
runs-on: ${{ matrix.job.os }}
strategy:
matrix:
rust: [stable]
feature: [release]
job:
- os: macos-latest
- os: ubuntu-latest
- os: windows-latest
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
if: github.event_name != 'pull_request'
with:
fetch-depth: 0

- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
if: github.event_name == 'pull_request'
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0

- name: Setup Restic
uses: rustic-rs/setup-restic@main

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1482605bfc5719782e1267fd0c0cc350fe7646b8 # v1
with:
toolchain: stable

- name: Create fixtures
shell: bash
run: |
restic init
restic backup src
mv src/lib.rs lib.rs
restic backup src
mv lib.rs src/lib.rs
env:
RESTIC_REPOSITORY: ./tests/repository-fixtures/repo
RESTIC_PASSWORD: restic

- uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2

- name: Run Cargo Test
run: cargo test -r --test repositories --features ${{ matrix.feature }} -- test_restic_latest_repo_with_rustic_passes --exact --show-output --ignored

result:
name: Result (Compat)
runs-on: ubuntu-latest
needs:
- test
steps:
- name: Mark the job as successful
run: exit 0
if: success()
- name: Mark the job as unsuccessful
run: exit 1
if: "!success()"
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,15 @@ abscissa_core = { version = "0.8.1", default-features = false, features = ["test
assert_cmd = "2.0.16"
cfg-if = "1.0.0"
dircmp = "0.2"
flate2 = "1.0.34"
insta = { version = "1.40.0", features = ["ron"] }
predicates = "3.1.2"
pretty_assertions = "1.4"
quickcheck = "1"
quickcheck_macros = "1"
rstest = "0.23"
rustic_testing = "0.3.0"
tar = "0.4.42"
tempfile = "3.13"
toml = "0.8"

Expand Down
174 changes: 174 additions & 0 deletions tests/repositories.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
use anyhow::Result;
use assert_cmd::Command;
use flate2::read::GzDecoder;
use rstest::{fixture, rstest};
use rustic_testing::TestResult;
use std::{fs::File, path::Path};
use tar::Archive;
use tempfile::{tempdir, TempDir};

#[derive(Debug)]
struct TestSource(TempDir);

impl TestSource {
pub fn new(tmp: TempDir) -> Self {
Self(tmp)
}

pub fn into_path(self) -> TempDir {
self.0
}
}

fn open_and_unpack(open_path: &'static str, unpack_dir: &TempDir) -> Result<()> {
let path = Path::new(open_path).canonicalize()?;
let tar_gz = File::open(path)?;
let tar = GzDecoder::new(tar_gz);
let mut archive = Archive::new(tar);
archive.set_preserve_permissions(true);
archive.set_preserve_mtime(true);
archive.unpack(unpack_dir)?;
Ok(())
}

#[fixture]
fn rustic_repo() -> Result<TestSource> {
let dir = tempdir()?;
let path = "tests/repository-fixtures/rustic-repo.tar.gz";
open_and_unpack(path, &dir)?;
Ok(TestSource::new(dir))
}

#[fixture]
fn restic_repo() -> Result<TestSource> {
let dir = tempdir()?;
let path = "tests/repository-fixtures/restic-repo.tar.gz";
open_and_unpack(path, &dir)?;
Ok(TestSource::new(dir))
}

#[fixture]
fn rustic_copy_repo() -> Result<TestSource> {
let dir = tempdir()?;
let path = "tests/repository-fixtures/rustic-copy-repo.tar.gz";
open_and_unpack(path, &dir)?;

Ok(TestSource::new(dir))
}

#[fixture]
fn src_snapshot() -> Result<TestSource> {
let dir = tempdir()?;
let path = "tests/repository-fixtures/src-snapshot.tar.gz";
open_and_unpack(path, &dir)?;
Ok(TestSource::new(dir))
}

pub fn rustic_runner(temp_dir: &Path, password: &'static str) -> TestResult<Command> {
let repo_dir = temp_dir.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)
}

#[rstest]
fn test_rustic_repo_passes(rustic_repo: Result<TestSource>) -> TestResult<()> {
let rustic_repo = rustic_repo?;
let repo_password = "rustic";
let rustic_repo_path = rustic_repo.into_path();
let rustic_repo_path = rustic_repo_path.path();

{
let mut runner = rustic_runner(rustic_repo_path, repo_password)?;
runner.args(["check", "--read-data"]).assert().success();
}

{
let mut runner = rustic_runner(rustic_repo_path, repo_password)?;
runner
.arg("snapshots")
.assert()
.success()
.stdout(predicates::str::contains("2 snapshot(s)"));
}

{
let mut runner = rustic_runner(rustic_repo_path, repo_password)?;
runner
.arg("diff")
.arg("31d477a2")
.arg("86371783")
.assert()
.success()
.stdout(predicates::str::contains("1 removed"));
}

Ok(())
}

#[rstest]
fn test_restic_repo_with_rustic_passes(restic_repo: Result<TestSource>) -> TestResult<()> {
let restic_repo = restic_repo?;
let repo_password = "restic";
let restic_repo_path = restic_repo.into_path();
let restic_repo_path = restic_repo_path.path();

{
let mut runner = rustic_runner(restic_repo_path, repo_password)?;
runner.args(["check", "--read-data"]).assert().success();
}

{
let mut runner = rustic_runner(restic_repo_path, repo_password)?;
runner
.arg("snapshots")
.assert()
.success()
.stdout(predicates::str::contains("2 snapshot(s)"));
}

{
let mut runner = rustic_runner(restic_repo_path, repo_password)?;
runner
.arg("diff")
.arg("9305509c")
.arg("af05ecb6")
.assert()
.success()
.stdout(predicates::str::contains("1 removed"));
}

Ok(())
}

#[rstest]
#[ignore = "requires live fixture, run manually in CI"]
fn test_restic_latest_repo_with_rustic_passes() -> TestResult<()> {
let path = "tests/repository-fixtures/";
let repo_password = "restic";
let restic_repo_path = Path::new(path).canonicalize()?;
let restic_repo_path = restic_repo_path.as_path();

{
let mut runner = rustic_runner(restic_repo_path, repo_password)?;
runner.args(["check", "--read-data"]).assert().success();
}

{
let mut runner = rustic_runner(restic_repo_path, repo_password)?;
runner
.arg("snapshots")
.assert()
.success()
.stdout(predicates::str::contains("2 snapshot(s)"));
}

Ok(())
}
3 changes: 3 additions & 0 deletions tests/repository-fixtures/COPY.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[repository]
repository = "${{REPOSITORY}}"
password = "${{PASSWORD}}"
47 changes: 47 additions & 0 deletions tests/repository-fixtures/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Repository Fixtures

This directory contains fixtures for testing the `rustic` and `restic`
repositories.

The `rustic` repository is used to test the `rustic` binary. The `restic`
repository is a repository created `restic`. The latter is used to ensure that
`rustic` can read and write to a repository created by `restic`. The
`rustic-copy-repo` repository is used to test the copying of snapshots between
repositories.

## Accessing the Repositories

The `rustic` repository is located at `./rustic-repo`. The `restic` repository
is located at `./restic-repo`. There is an empty repository located at
`./rustic-copy-repo` that can be used to test the copying of snapshots between
repositories.

## Repository Layout

The `rustic` repository contains the following snapshots:

```console
| ID | Time | Host | Label | Tags | Paths | Files | Dirs | Size |
|----------|---------------------|---------|-------|------|-------|-------|------|-----------|
| 31d477a2 | 2024-10-08 08:11:00 | TowerPC | | | src | 51 | 7 | 240.5 kiB |
| 86371783 | 2024-10-08 08:13:12 | TowerPC | | | src | 50 | 7 | 238.6 kiB |
```

The `restic` repository contains the following snapshots:

```console
ID Time Host Tags Paths
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
9305509c 2024-10-08 08:14:50 TowerPC src
af05ecb6 2024-10-08 08:15:05 TowerPC src
```

The difference between the two snapshots is that the `lib.rs` file in the `src`
directory was removed between the two snapshots.

The `rustic-copy-repo` repository is empty and contains no snapshots.

### Passwords

The `rustic` repository is encrypted with the password `rustic`. The `restic`
repository is encrypted with the password `restic`.
Binary file added tests/repository-fixtures/restic-repo.tar.gz
Binary file not shown.
Binary file added tests/repository-fixtures/rustic-copy-repo.tar.gz
Binary file not shown.
Binary file added tests/repository-fixtures/rustic-repo.tar.gz
Binary file not shown.
Binary file added tests/repository-fixtures/src-snapshot.tar.gz
Binary file not shown.

0 comments on commit 47426db

Please sign in to comment.