Skip to content

Commit

Permalink
Merge 706a61c into 0a4f80b
Browse files Browse the repository at this point in the history
  • Loading branch information
oleiade authored Oct 10, 2023
2 parents 0a4f80b + 706a61c commit 30d2a2b
Show file tree
Hide file tree
Showing 9 changed files with 439 additions and 12 deletions.
4 changes: 4 additions & 0 deletions examples/experimental/fs/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,7 @@ export default async function () {
// Seek back to the beginning of the file
await file.seek(0, SeekMode.Start);
}

export async function teardown() {
file.close();
}
4 changes: 4 additions & 0 deletions js/modules/k6/experimental/fs/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ const (

// EOFError is emitted when the end of a file has been reached.
EOFError

// BadResourceError indicates the underlying IO resource is either invalid
// or closed, and so the operation could not be performed.
BadResourceError
)

// fsError represents a custom error object emitted by the fs module.
Expand Down
12 changes: 8 additions & 4 deletions js/modules/k6/experimental/fs/errors_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 29 additions & 2 deletions js/modules/k6/experimental/fs/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fs
import (
"io"
"path/filepath"
"sync/atomic"
)

// file is an abstraction for interacting with files.
Expand All @@ -14,12 +15,19 @@ type file struct {

// offset holds the current offset in the file
offset int

// closed indicates whether the file has been closed
closed atomic.Bool
}

// Stat returns a FileInfo describing the named file.
func (f *file) stat() *FileInfo {
func (f *file) stat() (*FileInfo, error) {
if f.closed.Load() {
return nil, newFsError(BadResourceError, "cannot stat closed file")
}

filename := filepath.Base(f.path)
return &FileInfo{Name: filename, Size: len(f.data)}
return &FileInfo{Name: filename, Size: len(f.data)}, nil
}

// FileInfo holds information about a file.
Expand All @@ -38,6 +46,10 @@ type FileInfo struct {
//
// If the end of the file has been reached, it returns EOFError.
func (f *file) Read(into []byte) (n int, err error) {
if f.closed.Load() {
return 0, newFsError(BadResourceError, "cannot read from closed file")
}

start := f.offset
if start == len(f.data) {
return 0, newFsError(EOFError, "EOF")
Expand Down Expand Up @@ -72,6 +84,10 @@ var _ io.Reader = (*file)(nil)
// When using SeekModeStart, the offset must be positive.
// Negative offsets are allowed when using `SeekModeCurrent` or `SeekModeEnd`.
func (f *file) Seek(offset int, whence SeekMode) (int, error) {
if f.closed.Load() {
return 0, newFsError(BadResourceError, "cannot seek in closed file")
}

newOffset := f.offset

switch whence {
Expand Down Expand Up @@ -125,3 +141,14 @@ const (
// the end of the file.
SeekModeEnd
)

// Close closes the file instance, and marks it as closed.
func (f *file) Close() error {
if f.closed.Load() {
return newFsError(BadResourceError, "file already closed")
}

f.closed.Store(true)

return nil
}
110 changes: 110 additions & 0 deletions js/modules/k6/experimental/fs/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,39 @@ package fs
import (
"bytes"
"errors"
"sync/atomic"
"testing"

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

func TestFileImpl(t *testing.T) {
t.Parallel()

t.Run("stat on closed file should fail", func(t *testing.T) {
t.Parallel()

f := &file{
path: "/bonjour.txt",
data: []byte("hello"),
offset: 0,
closed: atomic.Bool{},
}

err := f.Close()
require.NoError(t, err)

gotInfo, gotErr := f.stat()

assert.Nil(t, gotInfo)
assert.Error(t, gotErr)

var fsErr *fsError
assert.ErrorAs(t, gotErr, &fsErr)
assert.Equal(t, BadResourceError, fsErr.kind)
})

t.Run("read", func(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -134,6 +159,29 @@ func TestFileImpl(t *testing.T) {
}
})

t.Run("read from closed file should fail", func(t *testing.T) {
t.Parallel()

f := &file{
path: "/bonjour.txt",
data: []byte("hello"),
offset: 0,
closed: atomic.Bool{},
}

err := f.Close()
require.NoError(t, err)

gotN, gotErr := f.Read(make([]byte, 10))

assert.Equal(t, 0, gotN)
assert.Error(t, gotErr)

var fsErr *fsError
assert.ErrorAs(t, gotErr, &fsErr)
assert.Equal(t, BadResourceError, fsErr.kind)
})

t.Run("seek", func(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -254,4 +302,66 @@ func TestFileImpl(t *testing.T) {
})
}
})

t.Run("seek on closed file should fail", func(t *testing.T) {
t.Parallel()

f := &file{
path: "/bonjour.txt",
data: []byte("hello"),
offset: 0,
closed: atomic.Bool{},
}

err := f.Close()
require.NoError(t, err)

gotN, gotErr := f.Seek(0, SeekModeStart)

assert.Equal(t, 0, gotN)
assert.Error(t, gotErr)

var fsErr *fsError
assert.ErrorAs(t, gotErr, &fsErr)
assert.Equal(t, BadResourceError, fsErr.kind)
})

t.Run("close", func(t *testing.T) {
t.Parallel()

t.Run("closing a file should succeed", func(t *testing.T) {
t.Parallel()

f := &file{
path: "/bonjour.txt",
data: []byte("hello"),
offset: 0,
// defaults to closed == false
}

gotErr := f.Close()

assert.NoError(t, gotErr)
assert.Equal(t, true, f.closed.Load())
})

t.Run("double closing a file should fail", func(t *testing.T) {
t.Parallel()

f := &file{
path: "/bonjour.txt",
data: []byte("hello"),
offset: 0,
closed: atomic.Bool{},
}

err := f.Close()
require.NoError(t, err)
gotErr := f.Close()

var fsErr *fsError
assert.ErrorAs(t, gotErr, &fsErr)
assert.Equal(t, BadResourceError, fsErr.kind)
})
})
}
61 changes: 55 additions & 6 deletions js/modules/k6/experimental/fs/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,16 @@ type File struct {
// Stat returns a promise that will resolve to a [FileInfo] instance describing
// the file.
func (f *File) Stat() *goja.Promise {
promise, resolve, _ := promises.New(f.vu)
promise, resolve, reject := promises.New(f.vu)

go func() {
resolve(f.file.stat())
fileInfo, err := f.file.stat()
if err != nil {
reject(err)
return
}

resolve(fileInfo)
}()

return promise
Expand Down Expand Up @@ -234,10 +240,17 @@ func (f *File) Read(into goja.Value) *goja.Promise {
var fsErr *fsError
isFsErr := errors.As(err, &fsErr)
if isFsErr {
if fsErr.kind == EOFError && n == 0 {
resolve(nil)
} else {
resolve(n)
switch fsErr.kind {
case EOFError:
if n == 0 {
resolve(nil)
} else {
resolve(n)
}
case BadResourceError:
reject(newFsError(BadResourceError, "read() failed; reason: the file has been closed"))
default:
reject(err)
}
} else {
reject(err)
Expand Down Expand Up @@ -298,3 +311,39 @@ func (f *File) Seek(offset goja.Value, whence goja.Value) *goja.Promise {

return promise
}

// Close closes the file and returns a promise that will resolve to null
// once the operation is complete.
//
// Closing a file is a no-op in k6, as we don't have to worry about file
// descriptors. However, we still expose this method to the user to be
// consistent with the existing APIs such as Node's or Deno's.
//
// The promise will resolve to null, regardless of whether the file was
// previously opened or not.
//
// One of the reasons this method is currently is a no-op is that as of
// today (v0.46), k6 does not support opening files in the VU context. As
// a result, the file is always opened in the init context, and thus
// closed when the init context is closed. Any attempt of clever strategies
// attempting to limit long-lived files' content in memory (e.g reference
// counting the VU instances of a file, and releasing the memory once the
// count reaches zero) would thus be premature.
//
// TODO: reevaluate a more sophisticated strategy once we support opening
// files in the VU context.
func (f *File) Close() *goja.Promise {
promise, resolve, reject := promises.New(f.vu)

go func() {
err := f.file.Close()
if err != nil {
reject(err)
return
}

resolve(goja.Null())
}()

return promise
}
Loading

0 comments on commit 30d2a2b

Please sign in to comment.