check / check (push) Failing after 7m18s
All SQLite work in the package serializes on one allocator lock under -race, so its run time follows the total work. Tests that need one backup to a file:// destination now copy a shared backup made once. The sweeper and locality fixtures are smaller, and the list truncation test encodes one manifest instead of 1001, which cut the part of the run that is not parallel from about 11 to 3 seconds. Model: opus-5-5
296 lines
9.7 KiB
Go
296 lines
9.7 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/database"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
"sneak.berlin/go/vaultik/internal/storage"
|
|
"sneak.berlin/go/vaultik/internal/ui"
|
|
"sneak.berlin/go/vaultik/internal/vaultik"
|
|
)
|
|
|
|
// These tests cover https://git.eeqj.de/sneak/vaultik/issues/220: a
|
|
// file:// destination whose directory is missing, such as a USB stick
|
|
// that is not plugged in, cannot be listed. It is not an empty store, so
|
|
// no command may conclude from it that the local snapshots are gone.
|
|
|
|
// newFileDestinationVaultik writes the fault source tree and returns a
|
|
// Vaultik that backs it up to a file:// destination at storeDir.
|
|
func newFileDestinationVaultik(
|
|
ctx context.Context, t *testing.T, storeDir string,
|
|
) (*vaultik.Vaultik, *database.Repositories) {
|
|
t.Helper()
|
|
|
|
tempDir := t.TempDir()
|
|
dataDir := filepath.Join(tempDir, "src")
|
|
writeFaultSourceTree(t, afero.NewOsFs(), dataDir)
|
|
|
|
return openFileDestinationVaultik(ctx, t, storeDir, dataDir,
|
|
filepath.Join(tempDir, "index.sqlite"))
|
|
}
|
|
|
|
// openFileDestinationVaultik returns a Vaultik that backs up dataDir to a
|
|
// file:// destination at storeDir, with its local index at dbPath. The
|
|
// index is closed when the test ends.
|
|
func openFileDestinationVaultik(
|
|
ctx context.Context, t *testing.T, storeDir, dataDir, dbPath string,
|
|
) (*vaultik.Vaultik, *database.Repositories) {
|
|
t.Helper()
|
|
|
|
store, err := storage.NewFileStorer(storeDir)
|
|
require.NoError(t, err)
|
|
|
|
db, err := database.New(ctx, dbPath)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
repos := database.NewRepositories(db)
|
|
cfg := changedFileConfig(dataDir, dbPath)
|
|
|
|
return newBackupVaultik(ctx, cfg, store, repos, db, afero.NewOsFs()), repos
|
|
}
|
|
|
|
// firstBackup is one backup of the snapshot named "first" to a file://
|
|
// destination. dir holds its source tree in src, its store in store and
|
|
// its closed local index in index.sqlite. The first test that needs it
|
|
// makes it, and TestMain removes it. Every test that copies it backs up
|
|
// the same source tree, so no test may change that tree.
|
|
var firstBackup struct {
|
|
once sync.Once
|
|
dir string
|
|
ready bool
|
|
}
|
|
|
|
// TestMain removes firstBackup once every test has run.
|
|
func TestMain(m *testing.M) {
|
|
code := m.Run()
|
|
|
|
_ = os.RemoveAll(firstBackup.dir)
|
|
|
|
os.Exit(code)
|
|
}
|
|
|
|
// makeFirstBackup writes the fault source tree and backs it up as the
|
|
// snapshot named "first", all in a new directory that it records in
|
|
// firstBackup.
|
|
func makeFirstBackup(ctx context.Context, t *testing.T) {
|
|
t.Helper()
|
|
|
|
dir, err := os.MkdirTemp("", "vaultik-first-backup-")
|
|
require.NoError(t, err)
|
|
|
|
firstBackup.dir = dir
|
|
storeDir := filepath.Join(dir, "store")
|
|
dataDir := filepath.Join(dir, "src")
|
|
|
|
require.NoError(t, os.Mkdir(storeDir, 0o750))
|
|
writeFaultSourceTree(t, afero.NewOsFs(), dataDir)
|
|
|
|
v, _ := openFileDestinationVaultik(ctx, t, storeDir, dataDir,
|
|
filepath.Join(dir, "index.sqlite"))
|
|
require.NoError(t, backUp(v, "first"))
|
|
require.NoError(t, v.DB.Close())
|
|
|
|
firstBackup.ready = true
|
|
}
|
|
|
|
// backUpToFileDestination creates the directory storeDir, as a user must
|
|
// before the first backup, and returns a Vaultik whose file://
|
|
// destination there holds a backup of the snapshot named "first". The
|
|
// store and the local index are copies of firstBackup's, which is faster
|
|
// than backing up in every test. Everything the returned Vaultik prints
|
|
// after the backup goes to the returned buffer.
|
|
func backUpToFileDestination(
|
|
ctx context.Context, t *testing.T, storeDir string,
|
|
) (*vaultik.Vaultik, *database.Repositories, *bytes.Buffer) {
|
|
t.Helper()
|
|
|
|
firstBackup.once.Do(func() { makeFirstBackup(ctx, t) })
|
|
require.True(t, firstBackup.ready, "another test failed to make firstBackup")
|
|
|
|
require.NoError(t, os.MkdirAll(storeDir, 0o750))
|
|
require.NoError(t, os.CopyFS(storeDir,
|
|
os.DirFS(filepath.Join(firstBackup.dir, "store"))))
|
|
|
|
index, err := os.ReadFile(filepath.Join(firstBackup.dir, "index.sqlite"))
|
|
require.NoError(t, err)
|
|
|
|
dbPath := filepath.Join(t.TempDir(), "index.sqlite")
|
|
require.NoError(t, os.WriteFile(dbPath, index, 0o600))
|
|
|
|
v, repos := openFileDestinationVaultik(ctx, t, storeDir,
|
|
filepath.Join(firstBackup.dir, "src"), dbPath)
|
|
|
|
out := &bytes.Buffer{}
|
|
v.Stdout = out
|
|
v.UI = ui.NewWithColor(out, false)
|
|
|
|
return v, repos, out
|
|
}
|
|
|
|
// backUpThenUnplug backs up to a file:// destination, then moves the
|
|
// destination directory away, as unplugging the volume it lives on would.
|
|
func backUpThenUnplug(
|
|
ctx context.Context, t *testing.T,
|
|
) (*vaultik.Vaultik, *database.Repositories, *bytes.Buffer) {
|
|
t.Helper()
|
|
|
|
storeDir := filepath.Join(t.TempDir(), "usbstick")
|
|
v, repos, out := backUpToFileDestination(ctx, t, storeDir)
|
|
|
|
require.NoError(t, os.Rename(storeDir, storeDir+"-unplugged"))
|
|
|
|
return v, repos, out
|
|
}
|
|
|
|
// TestBackupFailsWhenDestinationDirectoryMissing checks that a backup to
|
|
// a file:// destination whose directory does not exist fails before it
|
|
// records a snapshot, and does not create the directory.
|
|
func TestBackupFailsWhenDestinationDirectoryMissing(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
volume := filepath.Join(t.TempDir(), "volume")
|
|
v, repos := newFileDestinationVaultik(ctx, t, filepath.Join(volume, "backup"))
|
|
|
|
err := backUp(v, "first")
|
|
require.ErrorIs(t, err, fs.ErrNotExist)
|
|
require.ErrorContains(t, err, "create it, or mount the volume it is on")
|
|
|
|
_, err = os.Stat(volume)
|
|
require.ErrorIs(t, err, fs.ErrNotExist,
|
|
"the backup must not create the destination directory")
|
|
|
|
snapshots, err := repos.Snapshots.ListRecent(ctx, listRecentTestLimit)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, snapshots)
|
|
}
|
|
|
|
// TestListSnapshotsWarnsWhenDestinationMissing checks that snapshot list
|
|
// warns and shows the local index alone, without reporting the local
|
|
// snapshot as missing from the destination.
|
|
func TestListSnapshotsWarnsWhenDestinationMissing(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
v, repos, out := backUpThenUnplug(ctx, t)
|
|
id := localSnapshotID(ctx, t, repos, "first")
|
|
|
|
require.NoError(t, v.ListSnapshots(false))
|
|
|
|
assert.Contains(t, out.String(), "Could not list backup destination store")
|
|
assert.Contains(t, out.String(), "Showing snapshots from the local index only.")
|
|
assert.Contains(t, out.String(), id)
|
|
assert.NotContains(t, out.String(), "not found in backup destination store")
|
|
}
|
|
|
|
// TestRemoveSnapshotWarnsWhenDestinationMissing checks that snapshot
|
|
// remove warns that the metadata could not be removed from the
|
|
// destination, instead of reporting that it was.
|
|
func TestRemoveSnapshotWarnsWhenDestinationMissing(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
v, repos, out := backUpThenUnplug(ctx, t)
|
|
|
|
result, err := v.RemoveSnapshot(localSnapshotID(ctx, t, repos, "first"),
|
|
&vaultik.RemoveOptions{Force: true})
|
|
require.NoError(t, err)
|
|
|
|
assert.False(t, result.RemoteRemoved)
|
|
assert.Contains(t, out.String(),
|
|
"Could not remove snapshot metadata from remote")
|
|
assert.NotContains(t, out.String(),
|
|
"Removed snapshot metadata from remote storage")
|
|
}
|
|
|
|
// TestPruneKeepsLocalRecordsWhenDestinationMissing checks that prune
|
|
// fails on a destination it cannot list and deletes no local snapshot
|
|
// record.
|
|
func TestPruneKeepsLocalRecordsWhenDestinationMissing(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
v, repos, _ := backUpThenUnplug(ctx, t)
|
|
|
|
err := v.Prune(&vaultik.PruneOptions{Force: true})
|
|
require.ErrorIs(t, err, fs.ErrNotExist)
|
|
require.ErrorContains(t, err, "listing remote snapshots")
|
|
|
|
snapshots, err := repos.Snapshots.ListRecent(ctx, listRecentTestLimit)
|
|
require.NoError(t, err)
|
|
assert.Len(t, snapshots, 1, "prune must delete no local snapshot record")
|
|
}
|
|
|
|
// TestPurgeSaysListingFailedOnceWhenDestinationMissing checks that
|
|
// snapshot purge fails on a destination it cannot list, with an error
|
|
// that says "listing remote snapshots" once.
|
|
func TestPurgeSaysListingFailedOnceWhenDestinationMissing(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
v, _, _ := backUpThenUnplug(ctx, t)
|
|
|
|
err := v.PurgeSnapshotsWithOptions(&vaultik.SnapshotPurgeOptions{
|
|
KeepLatest: true,
|
|
Force: true,
|
|
})
|
|
require.ErrorIs(t, err, fs.ErrNotExist)
|
|
assert.Equal(t, 1, strings.Count(err.Error(), "listing remote snapshots"),
|
|
err.Error())
|
|
}
|
|
|
|
// TestNukeRemoteKeepsLocalIndexWhenDestinationMissing checks that remote
|
|
// nuke fails on a destination it cannot list, leaves the local index
|
|
// unchanged, and neither reports the destination empty nor advises
|
|
// running nuke again.
|
|
func TestNukeRemoteKeepsLocalIndexWhenDestinationMissing(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
v, repos, out := backUpThenUnplug(ctx, t)
|
|
|
|
filesBefore, err := repos.Files.ListAll(ctx)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, filesBefore)
|
|
|
|
blobsBefore, err := repos.Blobs.GetAll(ctx)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, blobsBefore)
|
|
|
|
err = v.NukeRemote(true)
|
|
require.ErrorIs(t, err, fs.ErrNotExist)
|
|
require.ErrorContains(t, err, "listing remote snapshots")
|
|
assert.NotContains(t, out.String(), "now empty")
|
|
assert.NotContains(t, out.String(), "vaultik remote nuke --force")
|
|
|
|
snapshots, err := repos.Snapshots.ListRecent(ctx, listRecentTestLimit)
|
|
require.NoError(t, err)
|
|
assert.Len(t, snapshots, 1, "nuke must delete no local snapshot record")
|
|
|
|
files, err := repos.Files.ListAll(ctx)
|
|
require.NoError(t, err)
|
|
assert.Len(t, files, len(filesBefore), "nuke must delete no local file record")
|
|
|
|
blobs, err := repos.Blobs.GetAll(ctx)
|
|
require.NoError(t, err)
|
|
assert.Len(t, blobs, len(blobsBefore), "nuke must delete no local blob record")
|
|
}
|