diff --git a/internal/vaultik/fault_injection_test.go b/internal/vaultik/fault_injection_test.go index dd02574..055fcfd 100644 --- a/internal/vaultik/fault_injection_test.go +++ b/internal/vaultik/fault_injection_test.go @@ -680,18 +680,10 @@ func faultScannerFactory( // Scenario 5: the restore target runs out of space mid-file. Restore // must fail with an out-of-space error, and must not leave a truncated -// file at the target path presenting as a complete restore. Restore -// today writes each file straight to its final path and does not remove -// it when a write fails, so the truncated file survives; deleting it is -// tracked by https://git.eeqj.de/sneak/vaultik/issues/163. Skipped until -// that lands, so the destination assertion below is recorded rather than -// dropped. +// file at the target path presenting as a complete restore. // //nolint:paralleltest // installs the global logger via log.Initialize func TestRestoreReportsDiskFull(t *testing.T) { - t.Skip("blocked on https://git.eeqj.de/sneak/vaultik/issues/163: " + - "a disk-full write leaves a truncated file at the target path " + - "instead of removing it") log.Initialize(log.Config{}) osFS := afero.NewOsFs() @@ -717,10 +709,10 @@ func TestRestoreReportsDiskFull(t *testing.T) { id := fullFaultBackup(ctx, t, osFS, inner, cfg, repos, dataDir, dbPath, "diskfull") require.NoError(t, db.Close()) - // Restore onto a filesystem that allows only a few bytes of file - // content: enough to create files, far too little to hold them. + // Restore onto a target that allows only a few bytes of file content: + // enough to create files, far too little to hold them. budget := int64(8) - quota := "aFS{Fs: osFS, remaining: &budget} + quota := "aFS{Fs: osFS, dir: restoreDir, remaining: &budget} v := newReaderVaultik(ctx, cfg, inner, nil, quota) err = v.Restore(&vaultik.RestoreOptions{SnapshotID: id, TargetDir: restoreDir}) @@ -754,21 +746,26 @@ func assertRestoredTree( // budget is exhausted, mirroring a real ENOSPC. var errNoSpace = errors.New("no space left on device") -// quotaFS is an afero.Fs whose files may write only a fixed total number -// of content bytes before failing, simulating a full restore target. It -// wraps the interface so every method except Create delegates to the -// real filesystem; only file writes are capped. +// quotaFS is an afero.Fs on which files opened under dir may write only +// a fixed total number of content bytes before failing, simulating a full +// restore target. Every other method, and every file outside dir, goes +// straight to the real filesystem: restore also writes the decrypted +// metadata database under $TMPDIR through this filesystem, and capping +// that would fail the restore before it wrote anything to the target. type quotaFS struct { afero.Fs + dir string remaining *int64 } -//nolint:ireturn // afero.Fs.Create's signature requires returning afero.File. -func (q *quotaFS) Create(name string) (afero.File, error) { - f, err := q.Fs.Create(name) - if err != nil { - return nil, err +//nolint:ireturn // afero.Fs.OpenFile's signature requires returning afero.File. +func (q *quotaFS) OpenFile( + name string, flag int, perm os.FileMode, +) (afero.File, error) { + f, err := q.Fs.OpenFile(name, flag, perm) + if err != nil || !strings.HasPrefix(name, q.dir) { + return f, err } return "aFile{File: f, remaining: q.remaining}, nil