diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..c96a255 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,28 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Go + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v -rapid.checks=50000 ./... diff --git a/README.md b/README.md index 403748b..365c88c 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,17 @@ Features: - profound testing (we use property based tsting to ensure simularity with real implementation Non features: -- make a varaity of different backends (like NetFS, s2, etc.). I try to keep package as clean from dependencies as possible +- make a varaity of different backends (like NetFS, google cloud, s3, etc.). I try to keep package as clean from dependencies as possible - simulating of concurrent effect of filesystem (e.g. concurrent ReadDir with file removing) TODO: - [ ] Make count in test to see how much function envocation we have - [ ] Document what fileMode are supported - [ ] O_APPEND +- [ ] Fallocate? +- [ ] copy paste docs from orig functions +- [ ] subdirs in tests +- [ ] CI with test and fmtcheck +- [ ] Use more stdlib errors (how to test this?) +- [ ] Test relative paths +- [ ] Test wrapped error? diff --git a/go.mod b/go.mod index 2bf1059..7c5cab7 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/myxo/gofs -go 1.22.0 +go 1.21 require ( github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index 0af4125..4906a8a 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/gofs.go b/gofs.go index ea81c11..1f3c653 100644 --- a/gofs.go +++ b/gofs.go @@ -25,6 +25,7 @@ type FS interface { Rename(oldpath, newpath string) error Truncate(name string, size int64) error WriteFile(name string, data []byte, perm os.FileMode) error + Stat(name string) (os.FileInfo, error) } type File struct { @@ -32,6 +33,10 @@ type File struct { osFile *os.File } +var _ io.ReadCloser = &File{} +var _ io.WriteCloser = &File{} +var _ io.ReaderAt = &File{} + func NewFromOs(fp *os.File) *File { return &File{osFile: fp} } @@ -165,86 +170,98 @@ func (f *File) WriteString(s string) (n int, err error) { } func (f *File) WriteTo(w io.Writer) (n int64, err error) { panic("todo") } +func (f *File) IsFake() bool { + return f.osFile == nil +} + //func (f *File) SetDeadline(t time.Time) error{ panic("todo") } //func (f *File) SetReadDeadline(t time.Time) error{ panic("todo") } //func (f *File) SetWriteDeadline(t time.Time) error{ panic("todo") } -type OsFs struct{} +type osFs struct{} -var _ FS = &OsFs{} +var _ FS = &osFs{} -func (OsFs) Create(name string) (*File, error) { +func OsFs() FS { + return &osFs{} +} + +func (osFs) Create(name string) (*File, error) { fp, err := os.Create(name) return NewFromOs(fp), err } -func (OsFs) CreateTemp(dir, pattern string) (*File, error) { +func (osFs) CreateTemp(dir, pattern string) (*File, error) { fp, err := os.CreateTemp(dir, pattern) return NewFromOs(fp), err } -func (OsFs) Open(name string) (*File, error) { +func (osFs) Open(name string) (*File, error) { fp, err := os.Open(name) return NewFromOs(fp), err } -func (OsFs) OpenFile(name string, flag int, perm os.FileMode) (*File, error) { +func (osFs) OpenFile(name string, flag int, perm os.FileMode) (*File, error) { fp, err := os.OpenFile(name, flag, perm) return NewFromOs(fp), err } -func (OsFs) Chdir(dir string) error { +func (osFs) Chdir(dir string) error { return os.Chdir(dir) } -func (OsFs) Chmod(name string, mode os.FileMode) error { +func (osFs) Chmod(name string, mode os.FileMode) error { return os.Chmod(name, mode) } -func (OsFs) Chown(name string, uid, gid int) error { +func (osFs) Chown(name string, uid, gid int) error { return os.Chown(name, uid, gid) } -func (OsFs) Mkdir(name string, perm os.FileMode) error { +func (osFs) Mkdir(name string, perm os.FileMode) error { return os.Mkdir(name, perm) } -func (OsFs) MkdirAll(path string, perm os.FileMode) error { +func (osFs) MkdirAll(path string, perm os.FileMode) error { return os.MkdirAll(path, perm) } -func (OsFs) MkdirTemp(dir, pattern string) (string, error) { +func (osFs) MkdirTemp(dir, pattern string) (string, error) { return os.MkdirTemp(dir, pattern) } -func (OsFs) ReadFile(name string) ([]byte, error) { +func (osFs) ReadFile(name string) ([]byte, error) { return os.ReadFile(name) } -func (OsFs) Readlink(name string) (string, error) { +func (osFs) Readlink(name string) (string, error) { return os.Readlink(name) } -func (OsFs) Remove(name string) error { +func (osFs) Remove(name string) error { return os.Remove(name) } -func (OsFs) RemoveAll(path string) error { +func (osFs) RemoveAll(path string) error { return os.RemoveAll(path) } -func (OsFs) Rename(oldpath, newpath string) error { +func (osFs) Rename(oldpath, newpath string) error { return os.Rename(oldpath, newpath) } -func (OsFs) Truncate(name string, size int64) error { +func (osFs) Truncate(name string, size int64) error { return os.Truncate(name, size) } -func (OsFs) WriteFile(name string, data []byte, perm os.FileMode) error { +func (osFs) WriteFile(name string, data []byte, perm os.FileMode) error { return os.WriteFile(name, data, perm) } -func (OsFs) ReadDir(name string) ([]os.DirEntry, error) { +func (osFs) ReadDir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) } + +func (osFs) Stat(name string) (os.FileInfo, error) { + return os.Stat(name) +} diff --git a/memory.go b/memory.go index 056ab92..31af93c 100644 --- a/memory.go +++ b/memory.go @@ -11,6 +11,17 @@ import ( "unsafe" ) +func MakeError(op string, path string, text string) error { + return fmt.Errorf("%s %s: %s", op, path, text) +} + +func MakeWrappedError(op string, path string, err error, text string) error { + if err == nil || err == io.EOF { + return err + } + return fmt.Errorf("%s %s: %w %s", op, path, err, text) +} + var filePool = sync.Pool{New: func() any { return &mockData{ buff: make([]byte, 0, 32*1024), @@ -25,7 +36,8 @@ type mockData struct { buff []byte realName string // this is synced with inodes map isDirectory bool - dirContent []*mockData // if isDirectory is true + fs *FakeFS // TODO: move to FakeFile? + parent *mockData perm os.FileMode dyrtyPages []interval // well... it's not exactly pages... } @@ -41,7 +53,6 @@ func (m *mockData) reset() { m.buff = m.buff[:0] m.perm = 0 m.isDirectory = false - m.dirContent = nil } func (m *mockData) Size() int64 { @@ -64,7 +75,6 @@ type FakeFile struct { name string flag int cursor int64 - fs FS readDirSlice []os.DirEntry // non empty only on directory iteration with ReadDir function readDirSlice2 []os.FileInfo // non empty only on directory iteration with ReadDir function } @@ -73,7 +83,7 @@ func (f *FakeFile) Chdir() error { panic("todo") } func (f *FakeFile) Chmod(mode os.FileMode) error { if f.data == nil { - return fmt.Errorf("file already closed") + return os.ErrInvalid } f.data.perm = mode & fs.ModePerm return nil @@ -83,7 +93,7 @@ func (f *FakeFile) Chown(uid, gid int) error { panic("todo") } func (f *FakeFile) Close() error { if f.data == nil { - return fmt.Errorf("file already closed") + return os.ErrInvalid } if err := f.Sync(); err != nil { // TODO: should I release memory here? @@ -103,12 +113,12 @@ func (f *FakeFile) Name() string { func (f *FakeFile) Read(b []byte) (n int, err error) { n, err = f.pread(b, f.cursor) f.cursor += int64(n) - return n, err + return n, MakeWrappedError("Read", f.name, err, "") } func (f *FakeFile) ReadAt(b []byte, off int64) (n int, err error) { if off < 0 { - return 0, fmt.Errorf("negative offset") + return 0, MakeError("ReadAt", f.name, "negative offset") } // Mimic weird implementation of ReadAt from stdlib for len(b) > 0 { @@ -121,12 +131,12 @@ func (f *FakeFile) ReadAt(b []byte, off int64) (n int, err error) { b = b[m:] off += int64(m) } - return n, err + return n, MakeWrappedError("ReadAt", f.name, err, "") } func (f *FakeFile) pread(b []byte, off int64) (n int, err error) { if f.data == nil { - return 0, fmt.Errorf("file already closed") + return 0, os.ErrInvalid } if off < 0 { return 0, fmt.Errorf("negative offset") @@ -135,7 +145,7 @@ func (f *FakeFile) pread(b []byte, off int64) (n int, err error) { return 0, nil } if !hasReadPerm(f.flag) { - return 0, fmt.Errorf("file %q open wiithout write permission", f.name) + return 0, fmt.Errorf("%w file open without write permission", os.ErrPermission) } if off > int64(len(f.data.buff)) { return 0, io.ErrUnexpectedEOF @@ -149,14 +159,16 @@ func (f *FakeFile) pread(b []byte, off int64) (n int, err error) { func (f *FakeFile) ReadDir(n int) ([]os.DirEntry, error) { if f.data == nil { - return nil, fmt.Errorf("file already closed") + return nil, os.ErrInvalid } if !f.data.isDirectory { - return nil, fmt.Errorf("file %q not a directory", f.name) + return nil, MakeError("ReadDir", f.name, "not a directory") } if f.readDirSlice == nil { - for i := range f.data.dirContent { - f.readDirSlice = append(f.readDirSlice, NewInfoDataFromNode(f.data.dirContent[i], f.data.dirContent[i].realName)) + content, err := f.data.fs.getDirContent(f.name) + _ = err // TODO + for i := range content { + f.readDirSlice = append(f.readDirSlice, NewInfoDataFromNode(content[i], content[i].realName)) } } if n > 0 { @@ -174,14 +186,16 @@ func (f *FakeFile) ReadDir(n int) ([]os.DirEntry, error) { func (f *FakeFile) Readdir(n int) ([]os.FileInfo, error) { if f.data == nil { - return nil, fmt.Errorf("file already closed") + return nil, os.ErrInvalid } if !f.data.isDirectory { - return nil, fmt.Errorf("file %q not a directory", f.data.realName) + return nil, MakeError("ReadDir", f.name, "not a directory") } if f.readDirSlice2 == nil { - for i := range f.data.dirContent { - f.readDirSlice2 = append(f.readDirSlice2, NewInfoDataFromNode(f.data.dirContent[i], f.data.dirContent[i].realName)) + content, err := f.data.fs.getDirContent(f.name) + _ = err // TODO + for i := range content { + f.readDirSlice2 = append(f.readDirSlice2, NewInfoDataFromNode(content[i], content[i].realName)) } } if n > 0 { @@ -231,7 +245,7 @@ type fileWithoutReadFrom struct { func (f *FakeFile) Seek(offset int64, whence int) (ret int64, err error) { if f.data == nil { - return 0, fmt.Errorf("file already closed") + return 0, os.ErrInvalid } newOffset := int64(0) start := int64(0) @@ -245,7 +259,7 @@ func (f *FakeFile) Seek(offset int64, whence int) (ret int64, err error) { } newOffset = start + offset if newOffset < 0 { - return 0, fmt.Errorf("seek offset is negative") + return 0, MakeError("Seek", f.name, "seek offset is negative") } f.cursor = newOffset return newOffset, nil @@ -253,7 +267,7 @@ func (f *FakeFile) Seek(offset int64, whence int) (ret int64, err error) { func (f *FakeFile) Stat() (os.FileInfo, error) { if f.data == nil { - return nil, fmt.Errorf("file already closed") + return nil, os.ErrInvalid } // TODO: check read persmissions? info := NewInfoDataFromNode(f.data, f.name) @@ -262,7 +276,7 @@ func (f *FakeFile) Stat() (os.FileInfo, error) { func (f *FakeFile) Sync() error { if f.data == nil { - return fmt.Errorf("file already closed") + return os.ErrInvalid } clear(f.data.dyrtyPages) return nil @@ -270,13 +284,13 @@ func (f *FakeFile) Sync() error { func (f *FakeFile) Truncate(size int64) error { if f.data == nil { - return fmt.Errorf("file already closed") + return os.ErrInvalid } if size < 0 { - return fmt.Errorf("negative truncate size") + return MakeError("Truncate", f.name, "negative truncate size") } if !hasWritePerm(f.flag) { - return fmt.Errorf("file open without write permission") + return MakeWrappedError("Truncate", f.name, os.ErrPermission, "file open without write permission") } f.data.buff = resizeSlice(f.data.buff, int(size)) clear(f.data.buff[len(f.data.buff):cap(f.data.buff)]) @@ -285,7 +299,7 @@ func (f *FakeFile) Truncate(size int64) error { func (f *FakeFile) Write(b []byte) (n int, err error) { if f.data == nil { - return 0, fmt.Errorf("file already closed") + return 0, os.ErrInvalid } writePos := f.cursor if isAppend(f.flag) { @@ -294,7 +308,7 @@ func (f *FakeFile) Write(b []byte) (n int, err error) { n, err = f.pwrite(b, writePos) // what with cursor with append flag? It doesn't matter? f.cursor = writePos + int64(n) - return n, err + return n, MakeWrappedError("Write", f.name, err, "") } func (f *FakeFile) WriteAt(b []byte, off int64) (n int, err error) { @@ -316,19 +330,19 @@ func (f *FakeFile) WriteAt(b []byte, off int64) (n int, err error) { b = b[m:] off += int64(m) } - return n, err + return n, MakeWrappedError("WriteAt", f.name, err, "") } func (f *FakeFile) pwrite(b []byte, off int64) (n int, err error) { if f.data == nil { - return 0, fmt.Errorf("file already closed") + return 0, os.ErrInvalid } if off < 0 { return 0, fmt.Errorf("negative offset") } if !isReadWrite(f.flag) && !isWriteOnly(f.flag) { - return 0, fmt.Errorf("file %q open wiithout write permission", f.name) + return 0, fmt.Errorf("%w file open wiithout write permission", os.ErrPermission) } if len(b) == 0 { @@ -369,7 +383,6 @@ func NewInfoDataFromNode(inode *mockData, name string) *infoData { info.size = inode.Size() info.mode = inode.perm info.isDir = inode.isDirectory - // info.modTime = // TODO return &info } @@ -386,7 +399,8 @@ func (m *infoData) Mode() os.FileMode { } func (m *infoData) ModTime() time.Time { - panic("todo") + // TODO: make possibility to change modTime for test (e.g. via special function) + return m.modTime } func (m *infoData) IsDir() bool { diff --git a/memory_fs.go b/memory_fs.go index ae04bea..604b448 100644 --- a/memory_fs.go +++ b/memory_fs.go @@ -3,12 +3,16 @@ package gofs import ( "fmt" "io" + "io/fs" + "math/rand" "os" "path/filepath" + "sort" ) type FakeFS struct { - inodes map[string]*mockData + inodes map[string]*mockData + workDir string } var _ FS = &FakeFS{} @@ -17,12 +21,14 @@ const rootDir = "/" func NewMemoryFs() *FakeFS { fs := &FakeFS{ - inodes: map[string]*mockData{}, + inodes: map[string]*mockData{}, + workDir: rootDir, } fs.inodes[rootDir] = &mockData{ realName: rootDir, isDirectory: true, perm: 0666, + fs: fs, } return fs } @@ -51,37 +57,41 @@ func checkOpenPerm(flag int, inode *mockData) error { func (f *FakeFS) OpenFile(name string, flag int, perm os.FileMode) (*File, error) { dirPath := filepath.Dir(name) + if dirPath == "" || dirPath == "." { + dirPath = f.workDir + } dir, dirExist := f.inodes[dirPath] if !dirExist || !dir.isDirectory { - return nil, fmt.Errorf("dir not exist") + return nil, MakeError("OpenFile", name, "dir not exist") } inode, ok := f.inodes[name] if !ok { if !isCreate(flag) { - return nil, fmt.Errorf("no such file %q", name) + return nil, MakeWrappedError("OpenFile", name, os.ErrNotExist, "") } // TODO: check directory perms inode = filePool.Get().(*mockData) inode.reset() inode.realName = name inode.perm = perm + inode.fs = f + inode.parent = dir if !isCreate(flag) { // read and write allowed with any perm if you just created the file if err := checkOpenPerm(flag, inode); err != nil { - return nil, err + return nil, MakeWrappedError("OpenFile", name, err, "") } } f.inodes[name] = inode - dir.dirContent = append(dir.dirContent, inode) } else { if err := checkOpenPerm(flag, inode); err != nil { - return nil, err + return nil, MakeWrappedError("OpenFile", name, err, "") } if isCreate(flag) && isExclusive(flag) { - return nil, fmt.Errorf("file already exist") + return nil, MakeWrappedError("OpenFile", name, os.ErrExist, "with create flag") } if isTruncate(flag) { if !inode.hasWritePerm() { - return nil, fmt.Errorf("file does not have write perm") + return nil, MakeWrappedError("OpenFile", name, os.ErrPermission, "file does not have write perm") } clear(inode.buff) inode.buff = inode.buff[:0] @@ -93,37 +103,79 @@ func (f *FakeFS) OpenFile(name string, flag int, perm os.FileMode) (*File, error name: name, data: inode, flag: flag, - fs: f, }, }, nil } -func (f *FakeFS) Chdir(dir string) error { panic("TODO") } -func (f *FakeFS) Chmod(name string, mode os.FileMode) error { panic("TODO") } -func (f *FakeFS) Chown(name string, uid, gid int) error { panic("TODO") } +func (f *FakeFS) Chdir(dir string) error { panic("TODO") } + +func (f *FakeFS) Chmod(name string, mode os.FileMode) error { + inode, ok := f.inodes[name] + if !ok { + return MakeWrappedError("Chmod", name, os.ErrNotExist, "") + } + inode.perm = mode & fs.ModePerm + return nil +} + +func (f *FakeFS) Chown(name string, uid, gid int) error { panic("TODO") } func (f *FakeFS) Mkdir(name string, perm os.FileMode) error { parentPath := filepath.Dir(name) parent, parentExist := f.inodes[parentPath] if !parentExist || !parent.isDirectory { - return fmt.Errorf("parent %q not exist", parentPath) + return MakeError("Mkdir", name, "parent path does not exist") } if _, exist := f.inodes[name]; exist { - return fmt.Errorf("already exist") + return MakeWrappedError("Mkdir", name, os.ErrExist, "") } inode := &mockData{ realName: name, isDirectory: true, perm: perm, + fs: f, + parent: parent, } f.inodes[name] = inode - parent.dirContent = append(parent.dirContent, inode) return nil } -func (f *FakeFS) MkdirAll(path string, perm os.FileMode) error { panic("TODO") } +func (f *FakeFS) MkdirAll(path string, perm os.FileMode) error { + parentPath := filepath.Dir(path) + if parentPath == "." { + // TODO: check if we catch this if in test + parentPath = f.workDir + } + parent, parentExist := f.inodes[parentPath] + if !parentExist { + if err := f.MkdirAll(parentPath, perm); err != nil { + return err + } + parent, parentExist = f.inodes[parentPath] + if !parentExist { + panic("internal error: cannot create parent directory") + } + } else if !parent.isDirectory { + return MakeError("MkdirAll", path, "parent path exist, but is't not a directory") + } + + if _, exist := f.inodes[path]; exist { + return nil + } + + inode := &mockData{ + realName: path, + isDirectory: true, + perm: perm, + fs: f, + parent: parent, + } + f.inodes[path] = inode + return nil +} + func (f *FakeFS) MkdirTemp(dir, pattern string) (string, error) { panic("TODO") } func (f *FakeFS) ReadFile(name string) ([]byte, error) { @@ -137,41 +189,85 @@ func (f *FakeFS) ReadFile(name string) ([]byte, error) { } func (f *FakeFS) ReadDir(name string) ([]os.DirEntry, error) { - panic("todo") + fp, err := f.Open(name) + if err != nil { + return nil, err + } + defer fp.Close() + + dirs, err := fp.ReadDir(-1) + sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() }) + return dirs, err } func (f *FakeFS) Readlink(name string) (string, error) { panic("TODO") } -func (f *FakeFS) Remove(name string) error { panic("TODO") } -func (f *FakeFS) RemoveAll(path string) error { panic("TODO") } + +func (f *FakeFS) Remove(name string) error { + return f.remove(name, false) +} + +func (f *FakeFS) RemoveAll(path string) error { + return f.remove(path, true) +} + +func (f *FakeFS) remove(name string, all bool) error { + inode, ok := f.inodes[name] + if !ok { + if all { + return nil + } + return MakeWrappedError("Remove", name, os.ErrNotExist, "") + } + if inode.isDirectory { + content, err := f.getDirContent(name) + _ = err // TODO + if all { + for _, dinode := range content { + if err := f.remove(dinode.realName, true); err != nil { + return err + } + } + } else { + if len(content) != 0 { + return MakeError("Remove", name, "directory is not empty") + } + } + } + delete(f.inodes, name) + return nil + +} func (f *FakeFS) Rename(oldpath, newpath string) error { inode, ok := f.inodes[oldpath] if !ok { - return fmt.Errorf("file not exist (%s)", oldpath) + return MakeWrappedError("Rename", oldpath, os.ErrNotExist, "") } - dir := filepath.Dir(newpath) - dirNode, ok := f.inodes[dir] + + targetDir := filepath.Dir(newpath) + targetDirNode, ok := f.inodes[targetDir] if !ok { - return fmt.Errorf("directory on new path does not exis (%s)", newpath) + return MakeWrappedError("Rename", newpath, os.ErrNotExist, "directory on new path does not exist") } - if !dirNode.isDirectory { - return fmt.Errorf("%s is not a directory", dir) + if !targetDirNode.isDirectory { + return MakeError("Rename", newpath, "target directory is not an directory") } delete(f.inodes, oldpath) f.inodes[newpath] = inode inode.realName = newpath + inode.parent = targetDirNode return nil } func (f *FakeFS) Truncate(name string, size int64) error { inode, ok := f.inodes[name] if !ok { - return fmt.Errorf("file not exist") + return MakeWrappedError("Truncate", name, os.ErrNotExist, "") } if !inode.hasWritePerm() { - return fmt.Errorf("file does not have write perm") + return MakeWrappedError("Truncate", name, os.ErrPermission, "file does not have write perm") } // TODO: check write permission inode.buff = resizeSlice(inode.buff, int(size)) @@ -201,3 +297,56 @@ func (f *FakeFS) Release() { } clear(f.inodes) } + +func (f *FakeFS) Stat(name string) (os.FileInfo, error) { + inode, ok := f.inodes[name] + if !ok { + return nil, os.ErrNotExist + } + // TODO: check read persmissions? + info := NewInfoDataFromNode(inode, inode.realName) + return info, nil +} + +// This function will probably changed by v1.0 +func (f *FakeFS) CorruptFile(path string, offset int64) error { + fp, ok := f.inodes[path] + if !ok { + return os.ErrNotExist + } + if offset < 0 || offset >= fp.Size() { + return fmt.Errorf("offset is out of file") + } + fp.buff[offset]++ + return nil +} + +// This function will probably changed by v1.0 +func (f *FakeFS) CorruptDirtyPages(seedRand *rand.Rand) { + for _, data := range f.inodes { + for _, dirtyInterval := range data.dyrtyPages { + flipByte := seedRand.Int63n(dirtyInterval.to-dirtyInterval.from) + dirtyInterval.from + if flipByte < int64(len(data.buff)) { // TODO: do I need this if? + data.buff[flipByte]++ + } + } + } +} + +func (f *FakeFS) getDirContent(path string) ([]*mockData, error) { + if path == "." { + path = f.workDir + } + + // TODO: check if have + var res []*mockData + for _, node := range f.inodes { + if node.parent == nil { + continue + } + if node.parent.realName == path { + res = append(res, node) + } + } + return res, nil +} diff --git a/memory_test.go b/memory_test.go index a08487b..ff01281 100644 --- a/memory_test.go +++ b/memory_test.go @@ -15,6 +15,19 @@ import ( "pgregory.net/rapid" ) +type TestingT interface { + Fatalf(format string, a ...any) + Helper() +} + +func NoError(t TestingT, err error) { + t.Helper() + if err != nil { + t.Fatalf("Unexpected error: %+v", err) + } +} + + func checkSyncError(t *rapid.T, errOs error, errFake error) { t.Helper() if (errOs != nil) != (errFake != nil) { @@ -35,36 +48,31 @@ func listOsFiles(dirPath string) { func TestFS(t *testing.T) { dir := t.TempDir() - possibleFilenames := []string{"/a/test.file.1", "/a/test.file.2", "/b/test.file.1", "/b/test.file.2"} - possibleDirs := []string{"/a", "/b"} + possibleFilenames := []string{"/foo/a/test.file.1", "/foo/a/test.file.2", "/foo/b/test.file.1", "/foo/b/test.file.2"} + possibleDirs := []string{".", "/foo", "/foo/a", "/foo/b"} rapid.Check(t, func(t *rapid.T) { fs := NewMemoryFs() var osFiles []*File var fakeFiles []*File - fileCount := 0 defer func() { for i := range osFiles { osFiles[i].Close() } - os.RemoveAll(filepath.Join(dir, "a")) - os.RemoveAll(filepath.Join(dir, "b")) + _ = os.RemoveAll(filepath.Join(dir, "foo")) fs.Release() }() - err := fs.Mkdir("/a", 0777) - require.NoError(t, err) - err = os.MkdirAll(filepath.Join(dir, "a"), 0777) - require.NoError(t, err) + NoError(t, os.MkdirAll(filepath.Join(dir, "foo/a"), 0777)) + NoError(t, fs.MkdirAll("/foo/a", 0777)) createFiles := func() { fpOs, err := os.Create(filepath.Join(dir, possibleFilenames[0])) - require.NoError(t, err) + NoError(t, err) fpFake, err := fs.Create(filepath.Join("/", possibleFilenames[0])) - require.NoError(t, err) + NoError(t, err) osFiles = append(osFiles, NewFromOs(fpOs)) fakeFiles = append(fakeFiles, fpFake) - fileCount++ } createFiles() @@ -146,20 +154,7 @@ func TestFS(t *testing.T) { diOs, errOs := fpOs.ReadDir(n) diFake, errFake := fpFake.ReadDir(n) checkSyncError(t, errOs, errFake) - slices.SortFunc(diOs, func(a os.DirEntry, b os.DirEntry) int { return cmp.Compare(a.Name(), b.Name()) }) - slices.SortFunc(diFake, func(a os.DirEntry, b os.DirEntry) int { return cmp.Compare(a.Name(), b.Name()) }) - require.Equal(t, len(diOs), len(diFake)) - for i := range diOs { - require.Equal(t, diOs[i].Name(), diFake[i].Name()) - require.Equal(t, diOs[i].IsDir(), diFake[i].IsDir()) - infoOs, errOs := diOs[i].Info() - infoFake, errFake := diFake[i].Info() - checkSyncError(t, errOs, errFake) - if errOs != nil { - require.Equal(t, infoOs.Size(), infoFake.Size()) - require.Equal(t, infoOs.Mode(), infoFake.Mode()) - } - } + CompareDirEntries(t, diOs, diFake) }, "ReadFrom": func(t *rapid.T) { fpOs, fpFake := getFiles() @@ -212,11 +207,7 @@ func TestFS(t *testing.T) { fiFake, errFake := fpFake.Stat() checkSyncError(t, errOs, errFake) if fiOs != nil { - require.Equal(t, fiOs.Name(), fiFake.Name()) - require.Equal(t, fiOs.IsDir(), fiFake.IsDir()) - require.Equal(t, fiOs.Size(), fiFake.Size()) - // we do not compare mode, since it depends on parent fs directory, so hard to check in property test - // We do not compare time, since it's hard to mock, and not really relevant + CompareFileInfo(t, fiOs, fiFake) } }, "Sync": func(t *rapid.T) { @@ -284,39 +275,50 @@ func TestFS(t *testing.T) { // "FS_Chdir": func(t *rapid.T) {}, "FS_Chmod": func(t *rapid.T) { p := rapid.SampledFrom(possibleFilenames).Draw(t, "file to create") - fpOs, errOs := os.Open(filepath.Join(dir, p)) - fpFake, errFake := fs.Open(filepath.Join("/", p)) + possibleModes := []os.FileMode{0666, 0222, 0444} // rw, w-only, r-only + mode := rapid.SampledFrom(possibleModes).Draw(t, "file mode") + errOs := os.Chmod(filepath.Join(dir, p), mode) + errFake := fs.Chmod(filepath.Join("/", p), mode) checkSyncError(t, errOs, errFake) - if errOs == nil { - possibleModes := []os.FileMode{0666, 0222, 0444} // rw, w-only, r-only - mode := rapid.SampledFrom(possibleModes).Draw(t, "file mode") - errOs := fpOs.Chmod(mode) - errFake := fpFake.Chmod(mode) - checkSyncError(t, errOs, errFake) - } }, // "FS_Chown": func(t *rapid.T) {}, "FS_Mkdir": func(t *rapid.T) { - p := rapid.SampledFrom(possibleDirs).Draw(t, "dir to create") + p := rapid.SampledFrom(possibleDirs).Draw(t, "dir") errOs := os.Mkdir(filepath.Join(dir, p), 0777) errFake := fs.Mkdir(filepath.Join("/", p), 0777) checkSyncError(t, errOs, errFake) }, - // "FS_MkdirAll": func(t *rapid.T) {}, + "FS_MkdirAll": func(t *rapid.T) { + p := rapid.SampledFrom(possibleDirs).Draw(t, "dir") + errOs := os.MkdirAll(filepath.Join(dir, p), 0777) + errFake := fs.MkdirAll(filepath.Join("/", p), 0777) + checkSyncError(t, errOs, errFake) + }, // "FS_MkdirTemp": func(t *rapid.T) {}, "FS_ReadFile": func(t *rapid.T) { - p := rapid.SampledFrom(possibleFilenames).Draw(t, "file to read") + p := rapid.SampledFrom(possibleFilenames).Draw(t, "file") contOs, errOs := os.ReadFile(filepath.Join(dir, p)) contFake, errFake := fs.ReadFile(filepath.Join("/", p)) checkSyncError(t, errOs, errFake) require.Equal(t, contOs, contFake) }, - // "FS_Readlink": func(t *rapid.T) {}, - // "FS_Remove": func(t *rapid.T) {}, - // "FS_RemoveAll": func(t *rapid.T) {}, + "FS_Remove": func(t *rapid.T) { + // TODO: remove also dirs and subdirs + p := rapid.SampledFrom(possibleFilenames).Draw(t, "path") + errOs := os.Remove(filepath.Join(dir, p)) + errFake := fs.Remove(filepath.Join("/", p)) + checkSyncError(t, errOs, errFake) + }, + "FS_RemoveAll": func(t *rapid.T) { + // TODO: remove also dirs and subdirs + p := rapid.SampledFrom(possibleFilenames).Draw(t, "path") + errOs := os.RemoveAll(filepath.Join(dir, p)) + errFake := fs.RemoveAll(filepath.Join("/", p)) + checkSyncError(t, errOs, errFake) + }, "FS_Rename": func(t *rapid.T) { - oldname := rapid.SampledFrom(possibleFilenames).Draw(t, "file to write") - newname := rapid.SampledFrom(possibleFilenames).Draw(t, "file to write") + oldname := rapid.SampledFrom(possibleFilenames).Draw(t, "file") + newname := rapid.SampledFrom(possibleFilenames).Draw(t, "file") oldOsPath := filepath.Join(dir, oldname) newOsPath := filepath.Join(dir, newname) @@ -328,14 +330,14 @@ func TestFS(t *testing.T) { checkSyncError(t, errOs, errFake) }, "FS_Truncate": func(t *rapid.T) { - p := rapid.SampledFrom(possibleFilenames).Draw(t, "file to write") + p := rapid.SampledFrom(possibleFilenames).Draw(t, "file") n := rapid.Int64Range(0, 1024).Draw(t, "truncate size") errOs := os.Truncate(filepath.Join(dir, p), n) errFake := fs.Truncate(filepath.Join("/", p), n) checkSyncError(t, errOs, errFake) }, "FS_WriteFile": func(t *rapid.T) { - p := rapid.SampledFrom(possibleFilenames).Draw(t, "file to write") + p := rapid.SampledFrom(possibleFilenames).Draw(t, "file") n := rapid.IntRange(0, 1024).Draw(t, "write size") buff := make([]byte, n) rand.Read(buff) @@ -343,95 +345,48 @@ func TestFS(t *testing.T) { errFake := fs.WriteFile(filepath.Join("/", p), buff, 0777) checkSyncError(t, errOs, errFake) }, + "FS_Stat": func(t *rapid.T) { + p := rapid.SampledFrom(possibleFilenames).Draw(t, "file") + fiOs, errOs := os.Stat(filepath.Join(dir, p)) + fiFake, errFake := fs.Stat(filepath.Join("/", p)) + checkSyncError(t, errOs, errFake) + if fiOs != nil { + CompareFileInfo(t, fiOs, fiFake) + } + }, + "FS_ReadDir": func(t *rapid.T) { + p := rapid.SampledFrom(possibleDirs).Draw(t, "dir") + diOs, errOs := os.ReadDir(filepath.Join(dir, p)) + diFake, errFake := fs.ReadDir(filepath.Join("/", p)) + checkSyncError(t, errOs, errFake) + CompareDirEntries(t, diOs, diFake) + }, }) }) } -func TestTmp(t *testing.T) { - dir := t.TempDir() - fs := NewMemoryFs() - var osFiles []*File - var fakeFiles []*File - fileCount := 0 +func CompareFileInfo(t *rapid.T, fiOs os.FileInfo, fiFake os.FileInfo) { + require.Equal(t, fiOs.Name(), fiFake.Name()) + require.Equal(t, fiOs.IsDir(), fiFake.IsDir()) + require.Equal(t, fiOs.Size(), fiFake.Size()) + // we do not compare mode, since it depends on parent fs directory, so hard to check in property test + // We do not compare time, since it's hard to mock, and not really relevant +} - defer func() { - for i := range osFiles { - osFiles[i].Close() +func CompareDirEntries(t *rapid.T, diOs []os.DirEntry, diFake []os.DirEntry) { + slices.SortFunc(diOs, func(a os.DirEntry, b os.DirEntry) int { return cmp.Compare(a.Name(), b.Name()) }) + slices.SortFunc(diFake, func(a os.DirEntry, b os.DirEntry) int { return cmp.Compare(a.Name(), b.Name()) }) + require.Equal(t, len(diOs), len(diFake)) + for i := range diOs { + require.Equal(t, diOs[i].Name(), diFake[i].Name()) + require.Equal(t, diOs[i].IsDir(), diFake[i].IsDir()) + infoOs, errOs := diOs[i].Info() + infoFake, errFake := diFake[i].Info() + checkSyncError(t, errOs, errFake) + if errOs != nil { + require.Equal(t, infoOs.Size(), infoFake.Size()) + require.Equal(t, infoOs.Mode(), infoFake.Mode()) } - os.RemoveAll(filepath.Join(dir, "a")) - os.RemoveAll(filepath.Join(dir, "b")) - fs.Release() - }() - err := fs.Mkdir("/a", 0777) - require.NoError(t, err) - err = os.MkdirAll(filepath.Join(dir, "a"), 0777) - require.NoError(t, err) - createFiles := func() { - fpOs, err := os.Create(filepath.Join(dir, "/a/file.test")) - require.NoError(t, err) - fpFake, err := fs.Create(filepath.Join("/", "/a/file.test")) - require.NoError(t, err) - - osFiles = append(osFiles, NewFromOs(fpOs)) - fakeFiles = append(fakeFiles, fpFake) - fileCount++ - } - createFiles() - - p := "/a/file.test" - fpOs, errOs := os.Open(filepath.Join(dir, p)) - fpFake, errFake := fs.Open(filepath.Join("/", p)) - fmt.Printf("oserr:%q, fakeErr:%q\n", errOs, errFake) - - fmt.Println("chmod") - mode := os.FileMode(0222) - errOs = fpOs.Chmod(mode) - errFake = fpFake.Chmod(mode) - fmt.Printf("oserr:%q, fakeErr:%q\n", errOs, errFake) - - fmt.Println("writeat") - offset := int64(0) - n := 1 - buff := make([]byte, n) - rand.Read(buff) - nOs, errOs := fpOs.WriteAt(buff, offset) - nFake, errFake := fpFake.WriteAt(buff, offset) - fmt.Printf("oserr:%q, fakeErr:%q\n", errOs, errFake) - - fmt.Println("FS_create") - fpOs, errOs = os.Create(filepath.Join(dir, p)) - fpFake, errFake = fs.Create(filepath.Join("/", p)) - fmt.Printf("oserr:%q, fakeErr:%q\n", errOs, errFake) - - n = 1 - buff = make([]byte, n) - rand.Read(buff) - nOs, errOs = fpOs.Write(nil) - nFake, errFake = fpFake.Write(nil) - fmt.Printf("oserr:%q, fakeErr:%q\n", errOs, errFake) - if nOs != nFake { - t.Fatalf("os impl return %d, we %d", nOs, nFake) } } -func TestTmp2(t *testing.T) { - dir := t.TempDir() - //dir := "/Users/myxo/tmp" - path := filepath.Join(dir, "test2") - fp, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0444) - fmt.Println("open", err) - buff := []byte("hello") - _, err = fp.Read(buff) - fmt.Println("read", err) - _, err = fp.WriteAt(buff, 0) - fmt.Println("write", err) - n, err := fp.ReadAt(buff, 0) - fmt.Println("read", n, err, buff[:n]) - fmt.Println("close") - fp.Close() - fp, err = os.OpenFile(path, os.O_RDONLY, 0222) - fmt.Println("open", err) - n, err = fp.ReadAt(buff, 0) - fmt.Println("read", n, err, buff[:n]) - -} diff --git a/util.go b/util.go index acde4ac..62ef8ed 100644 --- a/util.go +++ b/util.go @@ -6,9 +6,6 @@ func resizeSlice(s []byte, size int) []byte { if size < 0 { panic("negative size") } - if size > 1024*1024 { - panic(size) - } if cap(s) < size { s = append(s[:cap(s)], make([]byte, size-cap(s))...) } else { diff --git a/util_test.go b/util_test.go index 12300ae..d738395 100644 --- a/util_test.go +++ b/util_test.go @@ -19,3 +19,4 @@ func TestResize(t *testing.T) { } }) } +