check / check (push) Waiting to run
snapshot create --prune of a snapshot whose path was an empty directory, as the mount point of a volume that is not mounted is, recorded an empty snapshot and then purged the older snapshots that held the data, with their blobs. The scan now counts the entries it finds in each configured path. Under --prune, a run in which a path of a snapshot it made held nothing skips the prune, keeps every older snapshot, names each such path with its snapshot, and exits non-zero, as #382 does for unreadable files. Only the current run is looked at. Without --prune this check does nothing. A configured path that is a symlink whose target is gone now fails every run, with or without --prune. Model: opus-5-5
139 lines
4.7 KiB
Go
139 lines
4.7 KiB
Go
package cli //nolint:testpackage // shares runEntry and the hermetic config helpers
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// Flags of `snapshot create` used to build argument vectors below.
|
|
const (
|
|
flagPrune = "--prune"
|
|
flagKeepNewerThan = "--keep-newer-than"
|
|
)
|
|
|
|
// TestEntrySnapshotCreateRejectsKeepNewerThanBeforeBackup checks that a
|
|
// --keep-newer-than the post-backup prune could not use stops `snapshot
|
|
// create` before the backup: the command fails and the destination
|
|
// store stays empty. It used to be read only by the prune, after the
|
|
// snapshot was uploaded, and without --prune it was ignored.
|
|
//
|
|
//nolint:paralleltest // replaces os.Args, os.Stdout, os.Stderr and the xdg globals
|
|
func TestEntrySnapshotCreateRejectsKeepNewerThanBeforeBackup(t *testing.T) {
|
|
for _, testCase := range []struct {
|
|
name string
|
|
args []string
|
|
}{
|
|
{
|
|
name: "decimal in a day count",
|
|
args: []string{flagPrune, flagKeepNewerThan, "1.5d"},
|
|
},
|
|
{
|
|
name: "unknown unit",
|
|
args: []string{flagPrune, flagKeepNewerThan, "4x"},
|
|
},
|
|
{
|
|
name: "without --prune",
|
|
args: []string{flagKeepNewerThan, "4w"},
|
|
},
|
|
} {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
configPath, storeDir := writeBackupConfig(t)
|
|
|
|
code, _, _ := runEntry(t, append(
|
|
[]string{flagConfig, configPath, cmdSnapshot, cmdCreate},
|
|
testCase.args...)...)
|
|
|
|
assert.Equal(t, 1, code)
|
|
|
|
stored, err := os.ReadDir(storeDir)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, stored, "nothing may be uploaded")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEntrySnapshotCreateReportsPostBackupPruneFailure makes the
|
|
// post-backup prune fail after a good backup, by planting a manifest
|
|
// that cannot be decoded, and checks that the failure is reported as the
|
|
// prune's and not as a failed snapshot creation.
|
|
//
|
|
//nolint:paralleltest // replaces os.Args, os.Stdout, os.Stderr and the xdg globals
|
|
func TestEntrySnapshotCreateReportsPostBackupPruneFailure(t *testing.T) {
|
|
configPath, storeDir := writeBackupConfig(t)
|
|
|
|
// A remote key is 64 lowercase hex characters. The prune reads every
|
|
// manifest on the store and fails on one it cannot decode.
|
|
plantedDir := filepath.Join(storeDir, "metadata", strings.Repeat("a", 64))
|
|
require.NoError(t, os.MkdirAll(plantedDir, 0o750))
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(plantedDir, "manifest.json.zst"),
|
|
[]byte("not a valid manifest"), 0o600))
|
|
|
|
code, _, stderr := runEntry(t,
|
|
flagConfig, configPath, cmdSnapshot, cmdCreate, flagPrune)
|
|
|
|
assert.Equal(t, 1, code)
|
|
assert.Contains(t, stderr, "Backup completed, but post-backup prune failed")
|
|
assert.NotContains(t, stderr, "Snapshot creation failed")
|
|
|
|
snapshots, err := os.ReadDir(filepath.Join(storeDir, "metadata"))
|
|
require.NoError(t, err)
|
|
assert.Len(t, snapshots, 2,
|
|
"the new snapshot is on the store beside the planted one")
|
|
}
|
|
|
|
// TestEntrySnapshotCreatePruneExitsNonZeroWhenSourceIsEmpty backs up,
|
|
// empties the source directory, as a volume that is not mounted leaves
|
|
// its mount point, and runs `snapshot create --prune`. The command exits
|
|
// non-zero and reports the backup as completed, not as a failed snapshot
|
|
// creation. See https://git.eeqj.de/sneak/vaultik/issues/388.
|
|
//
|
|
//nolint:paralleltest // replaces os.Args, os.Stdout, os.Stderr and the xdg globals
|
|
func TestEntrySnapshotCreatePruneExitsNonZeroWhenSourceIsEmpty(t *testing.T) {
|
|
configPath, storeDir := writeBackupConfig(t)
|
|
|
|
code, _, _ := runEntry(t, flagConfig, configPath, cmdSnapshot, cmdCreate)
|
|
require.Equal(t, 0, code)
|
|
|
|
// writeBackupConfig puts the source directory beside the config file.
|
|
require.NoError(t, os.Remove(
|
|
filepath.Join(filepath.Dir(configPath), "source", "file.txt")))
|
|
|
|
code, _, stderr := runEntry(t,
|
|
flagConfig, configPath, cmdSnapshot, cmdCreate, flagPrune)
|
|
|
|
assert.Equal(t, 1, code)
|
|
assert.Contains(t, stderr,
|
|
"Backup completed, but 1 source path(s) were empty")
|
|
assert.NotContains(t, stderr, "Snapshot creation failed")
|
|
|
|
snapshots, err := os.ReadDir(filepath.Join(storeDir, "metadata"))
|
|
require.NoError(t, err)
|
|
assert.Len(t, snapshots, 2, "the older snapshot is kept")
|
|
}
|
|
|
|
// writeBackupConfig builds a hermetic config with one small file in its
|
|
// source directory and an empty destination store directory. Returns the
|
|
// config path and the store directory.
|
|
func writeBackupConfig(t *testing.T) (string, string) {
|
|
t.Helper()
|
|
|
|
configPath := writeHermeticPruneConfig(t, false)
|
|
|
|
// writeHermeticPruneConfig puts the source and store directories
|
|
// beside the config file.
|
|
dir := filepath.Dir(configPath)
|
|
sourceDir := filepath.Join(dir, "source")
|
|
|
|
require.NoError(t, os.Mkdir(sourceDir, 0o750))
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(sourceDir, "file.txt"), []byte("hello"), 0o600))
|
|
|
|
return configPath, filepath.Join(dir, "store")
|
|
}
|