Skip to content

Commit

Permalink
Merge pull request #3 from xmidt-org/add-fs-interface
Browse files Browse the repository at this point in the history
Add a simple fs interface for easy testing.
  • Loading branch information
schmidtw authored Sep 10, 2023
2 parents ae86551 + b8ca153 commit d5cbf29
Show file tree
Hide file tree
Showing 6 changed files with 969 additions and 0 deletions.
23 changes: 23 additions & 0 deletions internal/fs/fs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

package fs

import "io/fs"

// Look here for interface options to include when we need more functionality:
// https://github.com/hack-pad/hackpadfs/blob/main/fs.go

// FS is the interface for a filesystem used by the program.
type FS interface {
fs.FS

// Mkdir creates a directory with the specified permissions. Should match os.Mkdir().
Mkdir(path string, perm fs.FileMode) error

// ReadFile reads the file and returns the contents. Should match os.ReadFile().
ReadFile(name string) ([]byte, error)

// WriteFile writes the file with the specified permissions. Should match os.WriteFile().
WriteFile(name string, data []byte, perm fs.FileMode) error
}
99 changes: 99 additions & 0 deletions internal/fs/mem/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

package mem

import (
"errors"
"io"
iofs "io/fs"
"time"
)

var (
ErrIsDir = errors.New("is a directory")
)

// File is an in-memory implementation of the iofs.File interface.
type File struct {
Bytes []byte
Perm iofs.FileMode
name string
isDir bool
offset int
closed bool
}

var _ iofs.File = (*File)(nil)

// Implement the iofs.File interface.

func (f *File) Stat() (iofs.FileInfo, error) {
return &fileInfo{
name: f.name,
size: int64(len(f.Bytes)),
mode: f.Perm,
modTime: time.Time{},
isDir: false,
}, nil
}

func (f *File) Read(b []byte) (int, error) {
if f.isDir {
return 0, &iofs.PathError{Op: "read", Path: f.name, Err: ErrIsDir}
}

if f.closed {
return 0, io.ErrClosedPipe
}

if len(f.Bytes) <= f.offset {
return 0, io.EOF
}

n := copy(b, f.Bytes[f.offset:])
f.offset += n
return n, nil
}

func (f *File) Close() error {
f.closed = true
return nil
}

// fileInfo is an in-memory implementation of the iofs.FileInfo interface.
type fileInfo struct {
name string
size int64
mode iofs.FileMode
modTime time.Time
isDir bool
}

var _ iofs.FileInfo = (*fileInfo)(nil)

// Implement the iofs.FileInfo interface.

func (fi *fileInfo) Name() string {
return fi.name
}

func (fi *fileInfo) Size() int64 {
return fi.size
}

func (fi *fileInfo) Mode() iofs.FileMode {
return fi.mode
}

func (fi *fileInfo) ModTime() time.Time {
return fi.modTime
}

func (fi *fileInfo) IsDir() bool {
return fi.isDir
}

func (fi *fileInfo) Sys() interface{} {
return nil
}
146 changes: 146 additions & 0 deletions internal/fs/mem/file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

package mem

import (
"io"
iofs "io/fs"
"testing"

"github.com/stretchr/testify/assert"
)

func TestFile_Stat(t *testing.T) {
tests := []struct {
description string
file File
want iofs.FileInfo
expectedErr error
}{
{
description: "a simple file",
file: File{
Bytes: []byte("hello"),
Perm: iofs.FileMode(0644),
name: "foo",
},
want: &fileInfo{
name: "foo",
size: 5,
mode: iofs.FileMode(0644),
isDir: false,
},
},
}
for _, tc := range tests {
t.Run(tc.description, func(t *testing.T) {
assert := assert.New(t)

got, err := tc.file.Stat()

assert.ErrorIs(err, tc.expectedErr)
assert.Equal(tc.want.Name(), got.Name())
assert.Equal(tc.want.Size(), got.Size())
assert.Equal(tc.want.Mode(), got.Mode())
assert.Equal(tc.want.ModTime(), got.ModTime())
assert.Equal(tc.want.IsDir(), got.IsDir())
assert.Equal(tc.want.Sys(), got.Sys())
})
}
}

func TestFile_Read(t *testing.T) {
tests := []struct {
description string
file File
buf []byte
want int
before func(file *File)
expect string
expectedErr error
}{
{
description: "a simple file read",
file: File{
Bytes: []byte("hello"),
Perm: iofs.FileMode(0644),
name: "foo",
},
buf: make([]byte, 5),
want: 5,
expect: "hello",
}, {
description: "a simple file read that is shorter than the buffer",
file: File{
Bytes: []byte("hello"),
Perm: iofs.FileMode(0644),
name: "foo",
},
buf: make([]byte, 1),
want: 1,
expect: "h",
}, {
description: "read an empty file",
file: File{
Perm: iofs.FileMode(0644),
name: "foo",
},
buf: make([]byte, 5),
want: 0,
expectedErr: io.EOF,
}, {
description: "read a directory",
file: File{
Perm: iofs.FileMode(0755),
name: "foo",
isDir: true,
},
buf: make([]byte, 5),
want: 0,
expectedErr: ErrIsDir,
}, {
description: "read a closed file",
file: File{
Perm: iofs.FileMode(0644),
name: "foo",
},
before: func(file *File) {
file.Close()
},
buf: make([]byte, 5),
want: 0,
expectedErr: io.ErrClosedPipe,
}, {
description: "read a partially read file",
file: File{
Bytes: []byte("hello"),
Perm: iofs.FileMode(0644),
name: "foo",
},
before: func(file *File) {
buf := make([]byte, 1)
_, _ = file.Read(buf)
},
buf: make([]byte, 5),
want: 4,
expect: "ello",
},
}
for _, tc := range tests {
t.Run(tc.description, func(t *testing.T) {
assert := assert.New(t)

file := tc.file
if tc.before != nil {
tc.before(&file)
}

got, err := file.Read(tc.buf)

assert.ErrorIs(err, tc.expectedErr)
assert.Equal(tc.want, got)
assert.Equal(tc.expect, string(tc.buf[:tc.want]))
})
}
}
Loading

0 comments on commit d5cbf29

Please sign in to comment.