Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
396 changes: 396 additions & 0 deletions cache_more_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,396 @@
package wtcache

import (
"os"
"testing"
"time"

"github.com/spf13/afero"
)

func (f *failingFs) Remove(name string) error {
if f.failRemove {
return errRemoteUnavailable
}
return f.Fs.Remove(name)
}

func (f *failingFs) RemoveAll(path string) error {
if f.failRemoveAll {
return errRemoteUnavailable
}
return f.Fs.RemoveAll(path)
}

func (f *failingFs) Rename(oldname, newname string) error {
if f.failRename {
return errRemoteUnavailable
}
return f.Fs.Rename(oldname, newname)
}

func (f *failingFs) Mkdir(name string, perm os.FileMode) error {
if f.failMkdir {
return errRemoteUnavailable
}
return f.Fs.Mkdir(name, perm)
}

func (f *failingFs) MkdirAll(path string, perm os.FileMode) error {
if f.failMkdir {
return errRemoteUnavailable
}
return f.Fs.MkdirAll(path, perm)
}

func (f *failingFs) Chmod(name string, mode os.FileMode) error {
if f.failChmod {
return errRemoteUnavailable
}
return f.Fs.Chmod(name, mode)
}

func (f *failingFs) Chown(name string, uid, gid int) error {
if f.failChown {
return errRemoteUnavailable
}
return f.Fs.Chown(name, uid, gid)
}

func (f *failingFs) Chtimes(name string, atime, mtime time.Time) error {
if f.failChtimes {
return errRemoteUnavailable
}
return f.Fs.Chtimes(name, atime, mtime)
}

func TestFsName(t *testing.T) {
f, _, _ := newTestFs(t, time.Minute)
if got := f.Name(); got != "WriteThroughCacheFs" {
t.Fatalf("expected fixed Name(), got %q", got)
}
}

func TestMkdirAndMkdirAllApplyToRemoteAndMirrorToMem(t *testing.T) {
f, remote, _ := newTestFs(t, time.Minute)

if err := f.Mkdir("onedir", 0o755); err != nil {
t.Fatalf("Mkdir: %v", err)
}
if err := f.MkdirAll("nested/deep/dir", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}

for _, path := range []string{"onedir", "nested/deep/dir"} {
if _, err := remote.Stat(path); err != nil {
t.Fatalf("expected %q to exist on remote: %v", path, err)
}
if _, err := f.mem.Stat(path); err != nil {
t.Fatalf("expected %q mirrored into mem: %v", path, err)
}
}
}

func TestMkdirPropagatesRemoteError(t *testing.T) {
remote := &failingFs{Fs: afero.NewMemMapFs(), failMkdir: true}
f := New(remote, time.Minute, WithSweepInterval(time.Hour))
t.Cleanup(func() { _ = f.Close() })

if err := f.Mkdir("dir", 0o755); err == nil {
t.Fatalf("expected Mkdir to propagate remote error")
}
if err := f.MkdirAll("a/b", 0o755); err == nil {
t.Fatalf("expected MkdirAll to propagate remote error")
}
}

func TestOpenFileReadOnlyDelegatesToOpen(t *testing.T) {
f, _, _ := newTestFs(t, time.Minute)
mustWrite(t, f, "hello.txt", "content")

file, err := f.OpenFile("hello.txt", os.O_RDONLY, 0)
if err != nil {
t.Fatalf("OpenFile(O_RDONLY): %v", err)
}
data, err := afero.ReadAll(file)
file.Close()
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
if string(data) != "content" {
t.Fatalf("expected %q, got %q", "content", data)
}
}

func TestOpenFileWritePreloadsRemoteContentWhenNotFreshAndNotTruncating(t *testing.T) {
f, remote, _ := newTestFs(t, time.Minute)

if err := afero.WriteFile(remote, "hello.txt", []byte("hello"), 0o644); err != nil {
t.Fatalf("seed remote: %v", err)
}
// Not cached yet (never opened/written through f), so this OpenFile
// must preload the existing remote content before appending.
file, err := f.OpenFile("hello.txt", os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
t.Fatalf("OpenFile(O_APPEND): %v", err)
}
if _, err := file.Write([]byte(" world")); err != nil {
t.Fatalf("Write: %v", err)
}
if err := file.Close(); err != nil {
t.Fatalf("Close: %v", err)
}

if got := mustRead(t, f, "hello.txt"); got != "hello world" {
t.Fatalf("expected preloaded+appended content %q, got %q", "hello world", got)
}
}

func TestOpenFileWriteWithTruncateSkipsPreload(t *testing.T) {
f, remote, _ := newTestFs(t, time.Minute)

if err := afero.WriteFile(remote, "hello.txt", []byte("hello"), 0o644); err != nil {
t.Fatalf("seed remote: %v", err)
}
file, err := f.OpenFile("hello.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
if err != nil {
t.Fatalf("OpenFile(O_TRUNC): %v", err)
}
if _, err := file.Write([]byte("new")); err != nil {
t.Fatalf("Write: %v", err)
}
if err := file.Close(); err != nil {
t.Fatalf("Close: %v", err)
}

if got := mustRead(t, f, "hello.txt"); got != "new" {
t.Fatalf("expected truncated content %q, got %q", "new", got)
}
}

func TestOpenNonexistentClearsCacheAndReturnsError(t *testing.T) {
f, _, _ := newTestFs(t, time.Minute)

if _, err := f.Open("missing.txt"); err == nil {
t.Fatalf("expected error opening nonexistent file")
}
if f.isFresh("missing.txt") {
t.Fatalf("expected no cache entry for a failed open")
}
if _, err := f.mem.Stat("missing.txt"); err == nil {
t.Fatalf("expected no mem entry for a failed open")
}
}

func TestOpenDirectoryDelegatesToRemote(t *testing.T) {
f, _, _ := newTestFs(t, time.Minute)

if err := f.MkdirAll("adir", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}

file, err := f.Open("adir")
if err != nil {
t.Fatalf("Open(dir): %v", err)
}
defer file.Close()

info, err := file.Stat()
if err != nil {
t.Fatalf("Stat: %v", err)
}
if !info.IsDir() {
t.Fatalf("expected directory FileInfo")
}
}

func TestRemoveAllRemovesNestedCacheEntriesUnderPrefix(t *testing.T) {
f, remote, _ := newTestFs(t, time.Minute)

mustWrite(t, f, "dir/a.txt", "a")
mustWrite(t, f, "dir/b.txt", "b")

if err := f.RemoveAll("dir"); err != nil {
t.Fatalf("RemoveAll: %v", err)
}

for _, path := range []string{"dir/a.txt", "dir/b.txt"} {
if f.isFresh(path) {
t.Fatalf("expected %q cache entry cleared by RemoveAll", path)
}
if _, err := f.mem.Stat(path); err == nil {
t.Fatalf("expected %q removed from mem", path)
}
}
if _, err := remote.Stat("dir"); err == nil {
t.Fatalf("expected dir removed from remote")
}
}

func TestStatServesFromCacheWhenFreshElseRemote(t *testing.T) {
f, remote, clock := newTestFs(t, time.Minute)

mustWrite(t, f, "hello.txt", "content")

info, err := f.Stat("hello.txt")
if err != nil {
t.Fatalf("Stat (fresh): %v", err)
}
if info.Size() != int64(len("content")) {
t.Fatalf("expected cached size %d, got %d", len("content"), info.Size())
}

*clock = clock.Add(2 * time.Minute)
if err := afero.WriteFile(remote, "hello.txt", []byte("longer content"), 0o644); err != nil {
t.Fatalf("out-of-band remote write: %v", err)
}

info, err = f.Stat("hello.txt")
if err != nil {
t.Fatalf("Stat (expired): %v", err)
}
if info.Size() != int64(len("longer content")) {
t.Fatalf("expected remote size %d, got %d", len("longer content"), info.Size())
}
}

func TestChmodChownChtimesMirrorToFreshCacheEntry(t *testing.T) {
f, remote, _ := newTestFs(t, time.Minute)
mustWrite(t, f, "hello.txt", "content")

if err := f.Chmod("hello.txt", 0o600); err != nil {
t.Fatalf("Chmod: %v", err)
}
remoteInfo, err := remote.Stat("hello.txt")
if err != nil {
t.Fatalf("remote Stat: %v", err)
}
memInfo, err := f.mem.Stat("hello.txt")
if err != nil {
t.Fatalf("mem Stat: %v", err)
}
if remoteInfo.Mode().Perm() != 0o600 || memInfo.Mode().Perm() != 0o600 {
t.Fatalf("expected mode 0600 on both layers, got remote=%v mem=%v", remoteInfo.Mode(), memInfo.Mode())
}

mtime := time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC)
if err := f.Chtimes("hello.txt", mtime, mtime); err != nil {
t.Fatalf("Chtimes: %v", err)
}
remoteInfo, _ = remote.Stat("hello.txt")
memInfo, _ = f.mem.Stat("hello.txt")
if !remoteInfo.ModTime().Equal(mtime) || !memInfo.ModTime().Equal(mtime) {
t.Fatalf("expected mtime %v on both layers, got remote=%v mem=%v", mtime, remoteInfo.ModTime(), memInfo.ModTime())
}

if err := f.Chown("hello.txt", 1000, 1000); err != nil {
t.Fatalf("Chown: %v", err)
}
}

func TestChmodChownChtimesPropagateRemoteError(t *testing.T) {
remote := &failingFs{
Fs: afero.NewMemMapFs(),
failChmod: true,
failChown: true,
failChtimes: true,
}
f := New(remote, time.Minute, WithSweepInterval(time.Hour))
t.Cleanup(func() { _ = f.Close() })

if err := f.Chmod("hello.txt", 0o600); err == nil {
t.Fatalf("expected Chmod to propagate remote error")
}
if err := f.Chown("hello.txt", 1, 1); err == nil {
t.Fatalf("expected Chown to propagate remote error")
}
if err := f.Chtimes("hello.txt", time.Now(), time.Now()); err == nil {
t.Fatalf("expected Chtimes to propagate remote error")
}
}

func TestRemovePropagatesRemoteError(t *testing.T) {
remote := &failingFs{Fs: afero.NewMemMapFs(), failRemove: true}
f := New(remote, time.Minute, WithSweepInterval(time.Hour))
t.Cleanup(func() { _ = f.Close() })

mustWrite(t, f, "hello.txt", "content")
if err := f.Remove("hello.txt"); err == nil {
t.Fatalf("expected Remove to propagate remote error")
}
}

func TestRemoveAllPropagatesRemoteError(t *testing.T) {
remote := &failingFs{Fs: afero.NewMemMapFs(), failRemoveAll: true}
f := New(remote, time.Minute, WithSweepInterval(time.Hour))
t.Cleanup(func() { _ = f.Close() })

mustWrite(t, f, "dir/a.txt", "a")
if err := f.RemoveAll("dir"); err == nil {
t.Fatalf("expected RemoveAll to propagate remote error")
}
}

func TestRenamePropagatesRemoteError(t *testing.T) {
remote := &failingFs{Fs: afero.NewMemMapFs(), failRename: true}
f := New(remote, time.Minute, WithSweepInterval(time.Hour))
t.Cleanup(func() { _ = f.Close() })

mustWrite(t, f, "old.txt", "content")
if err := f.Rename("old.txt", "new.txt"); err == nil {
t.Fatalf("expected Rename to propagate remote error")
}
}

func TestRenamePrefixMovesNestedDirectoryEntries(t *testing.T) {
f, remote, _ := newTestFs(t, time.Minute)

mustWrite(t, f, "olddir/a.txt", "a")
mustWrite(t, f, "olddir/b.txt", "b")

if err := f.Rename("olddir", "newdir"); err != nil {
t.Fatalf("Rename: %v", err)
}

for _, old := range []string{"olddir/a.txt", "olddir/b.txt"} {
if f.isFresh(old) {
t.Fatalf("expected old path %q no longer cached", old)
}
}

// Mutate remote out-of-band under the new name to prove the renamed
// entries are still served from cache.
if err := afero.WriteFile(remote, "newdir/a.txt", []byte("changed"), 0o644); err != nil {
t.Fatalf("out-of-band remote write: %v", err)
}
if got := mustRead(t, f, "newdir/a.txt"); got != "a" {
t.Fatalf("expected renamed cache entry to serve %q, got %q", "a", got)
}
}

func TestCloseIsIdempotent(t *testing.T) {
f, _, _ := newTestFs(t, time.Minute)

if err := f.Close(); err != nil {
t.Fatalf("first Close: %v", err)
}
if err := f.Close(); err != nil {
t.Fatalf("second Close: %v", err)
}
}

func TestNewDefaultSweepIntervalFloorsAtOneSecond(t *testing.T) {
f := New(afero.NewMemMapFs(), 100*time.Millisecond)
defer f.Close()
if f.interval != time.Second {
t.Fatalf("expected default interval floored to 1s, got %v", f.interval)
}
}

func TestNewDefaultSweepIntervalIsQuarterOfTTL(t *testing.T) {
f := New(afero.NewMemMapFs(), 8*time.Second)
defer f.Close()
if f.interval != 2*time.Second {
t.Fatalf("expected default interval ttl/4=2s, got %v", f.interval)
}
}
Loading
Loading