Run the disk-full restore test again (closes #207)
check / check (pull_request) Successful in 3m43s

TestRestoreReportsDiskFull was skipped pending
#163, which is closed. The skip
and its "skipped until" wording are removed.

Restore is unchanged. With the skip removed the test failed because
restore succeeded: its simulated full disk capped only Create, but
restore now opens each file with OpenFile, so nothing was capped. It now
caps OpenFile, and only for files under the restore target: restore also
writes the decrypted metadata database under $TMPDIR through the same
filesystem, and capping that would fail the restore before any file
reached the target.

Judgement call: the test was corrected, not restore; both assertions are
unchanged.

Model: opus-5-5
This commit is contained in:
2026-10-01 17:59:40 +00:00
parent d886a9026f
commit 48a386e3ab
+18 -21
View File
@@ -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 := &quotaFS{Fs: osFS, remaining: &budget}
quota := &quotaFS{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 &quotaFile{File: f, remaining: q.remaining}, nil