Files
rclone/backend/local/local_internal_diskfull_test.go
T

183 lines
5.9 KiB
Go

//go:build !plan9
// Tests for the FatalIfNoSpace option and isDiskFullError helper.
//
// Kept in a separate file with a !plan9 build tag because syscall.ENOSPC
// is not portable to plan9, mirroring the split in
// fs/fserrors/enospc_error.go vs fs/fserrors/enospc_error_notsupported.go.
package local
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"syscall"
"testing"
"time"
"github.com/rclone/rclone/fs/fserrors"
"github.com/rclone/rclone/fs/object"
"github.com/rclone/rclone/fstest"
"github.com/rclone/rclone/lib/file"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// errReader is an io.Reader that always returns the configured error,
// used to inject a synthetic disk-full failure into Update's io.Copy.
type errReader struct{ err error }
func (r errReader) Read(p []byte) (int, error) { return 0, r.err }
// TestIsDiskFullError covers the helper used by the Update defer.
func TestIsDiskFullError(t *testing.T) {
cases := []struct {
name string
err error
want bool
}{
{"nil", nil, false},
{"unrelated error", errors.New("unrelated error"), false},
{"syscall.ENOSPC direct", syscall.ENOSPC, true},
{"syscall.ENOSPC wrapped", fmt.Errorf("io.Copy: %w", syscall.ENOSPC), true},
{"file.ErrDiskFull direct", file.ErrDiskFull, true},
{"file.ErrDiskFull wrapped", fmt.Errorf("preallocate: %w", file.ErrDiskFull), true},
{"os.PathError wrapping ENOSPC", &os.PathError{Op: "write", Path: "/foo", Err: syscall.ENOSPC}, true},
{"os.SyscallError wrapping ENOSPC", os.NewSyscallError("write", syscall.ENOSPC), true},
{"os.PathError wrapping unrelated", &os.PathError{Op: "write", Path: "/foo", Err: syscall.EPERM}, false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
assert.Equal(t, c.want, isDiskFullError(c.err))
})
}
}
// updateWithReader runs Update with an injected reader error and the given
// FatalIfNoSpace setting, returning the error from Update.
func updateWithReader(t *testing.T, fatalIfNoSpace bool, readerErr error) error {
t.Helper()
r := fstest.NewRunIndividual(t)
f := r.Flocal.(*Fs)
f.opt.FatalIfNoSpace = fatalIfNoSpace
src := object.NewStaticObjectInfo("test.txt", time.Now(), 100, true, nil, f)
o := &Object{
fs: f,
remote: "test.txt",
path: filepath.Join(r.LocalName, "test.txt"),
}
return o.Update(context.Background(), errReader{err: readerErr}, src)
}
// TestUpdateFatalIfNoSpaceOff verifies an ENOSPC during a write is NOT
// wrapped as fatal when the option is off.
func TestUpdateFatalIfNoSpaceOff(t *testing.T) {
err := updateWithReader(t, false, syscall.ENOSPC)
require.Error(t, err)
assert.False(t, fserrors.IsFatalError(err), "ENOSPC must not be fatal when FatalIfNoSpace=false")
}
// TestUpdateFatalIfNoSpaceOn verifies an ENOSPC during a write IS wrapped as
// fatal when the option is on.
func TestUpdateFatalIfNoSpaceOn(t *testing.T) {
err := updateWithReader(t, true, syscall.ENOSPC)
require.Error(t, err)
assert.True(t, fserrors.IsFatalError(err), "ENOSPC must be fatal when FatalIfNoSpace=true")
}
// TestUpdateFatalIfNoSpaceOnButNotDiskFull verifies non-disk-full errors are
// NOT wrapped as fatal even when the option is on.
func TestUpdateFatalIfNoSpaceOnButNotDiskFull(t *testing.T) {
err := updateWithReader(t, true, errors.New("unrelated network error"))
require.Error(t, err)
assert.False(t, fserrors.IsFatalError(err), "non-disk-full errors must not be fatal regardless of option")
}
type diskFullWriterAtCloser struct {
writeErr error
closeErr error
n int
data []byte
offset int64
closed bool
}
func (w *diskFullWriterAtCloser) WriteAt(p []byte, off int64) (int, error) {
w.data = append([]byte(nil), p...)
w.offset = off
return w.n, w.writeErr
}
func (w *diskFullWriterAtCloser) Close() error {
w.closed = true
return w.closeErr
}
func testOpenWriterAtError(t *testing.T, injected error, diskFull bool) {
t.Helper()
for _, enabled := range []bool{false, true} {
t.Run(fmt.Sprintf("enabled=%v", enabled), func(t *testing.T) {
r := fstest.NewRunIndividual(t)
f := r.Flocal.(*Fs)
f.opt.FatalIfNoSpace = enabled
f.opt.NoPreAllocate = true
writer, err := f.OpenWriterAt(context.Background(), "test.txt", 0)
require.NoError(t, err)
wrapped, ok := writer.(*fatalIfNoSpaceWriterAt)
require.True(t, ok)
require.NoError(t, wrapped.WriterAtCloser.Close())
wantN := 2
if injected == nil {
wantN = 4
}
underlying := &diskFullWriterAtCloser{writeErr: injected, closeErr: injected, n: wantN}
wrapped.WriterAtCloser = underlying
n, err := writer.WriteAt([]byte("data"), 17)
assert.Equal(t, wantN, n)
assert.Equal(t, []byte("data"), underlying.data)
assert.Equal(t, int64(17), underlying.offset)
assert.ErrorIs(t, err, injected)
assert.Equal(t, enabled && diskFull, fserrors.IsFatalError(err))
err = writer.Close()
assert.True(t, underlying.closed)
assert.ErrorIs(t, err, injected)
assert.Equal(t, enabled && diskFull, fserrors.IsFatalError(err))
})
}
}
func TestOpenWriterAtFatalIfNoSpace(t *testing.T) {
for _, test := range []struct {
name string
err error
diskFull bool
}{
{"success", nil, false},
{"unrelated", syscall.EPERM, false},
{"ENOSPC", syscall.ENOSPC, true},
{"ErrDiskFull", file.ErrDiskFull, true},
{"wrapped", fmt.Errorf("write: %w", syscall.ENOSPC), true},
} {
t.Run(test.name, func(t *testing.T) { testOpenWriterAtError(t, test.err, test.diskFull) })
}
}
func TestOpenWriterAtSetupError(t *testing.T) {
for _, enabled := range []bool{false, true} {
t.Run(fmt.Sprintf("enabled=%v", enabled), func(t *testing.T) {
r := fstest.NewRunIndividual(t)
f := r.Flocal.(*Fs)
f.opt.FatalIfNoSpace = enabled
require.NoError(t, os.WriteFile(filepath.Join(r.LocalName, "parent"), nil, 0600))
writer, err := f.OpenWriterAt(context.Background(), "parent/child", 0)
require.Error(t, err)
assert.Nil(t, writer)
assert.False(t, fserrors.IsFatalError(err))
})
}
}