Skip to content

Commit

Permalink
Add a file.close method
Browse files Browse the repository at this point in the history
The method 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.
  • Loading branch information
oleiade committed Oct 10, 2023
1 parent 1076af7 commit 706a61c
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 4 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();
}
51 changes: 47 additions & 4 deletions js/modules/k6/experimental/fs/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,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 @@ -304,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
}
117 changes: 117 additions & 0 deletions js/modules/k6/experimental/fs/module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,35 @@ func TestFile(t *testing.T) {
assert.NoError(t, err)
})

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

runtime, err := newConfiguredRuntime(t)
require.NoError(t, err)

testFilePath := fsext.FilePathSeparator + "bonjour.txt"
fs := newTestFs(t, func(fs afero.Fs) error {
return afero.WriteFile(fs, testFilePath, []byte("Bonjour, le monde"), 0o644)
})
runtime.VU.InitEnvField.FileSystems["file"] = fs

_, err = runtime.RunOnEventLoop(wrapInAsyncLambda(fmt.Sprintf(`
const file = await fs.open(%q)
await file.close()
try {
const info = await file.stat()
throw 'unexpected promise resolution with result: ' + info;
} catch (err) {
if (err.name !== 'BadResourceError') {
throw 'unexpected error: ' + err
}
}
`, testFilePath)))

assert.NoError(t, err)
})

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

Expand Down Expand Up @@ -366,6 +395,35 @@ func TestFile(t *testing.T) {
assert.NoError(t, err)
})

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

runtime, err := newConfiguredRuntime(t)
require.NoError(t, err)

testFilePath := fsext.FilePathSeparator + "bonjour.txt"
fs := newTestFs(t, func(fs afero.Fs) error {
return afero.WriteFile(fs, testFilePath, []byte("hello"), 0o644)
})
runtime.VU.InitEnvField.FileSystems["file"] = fs

_, err = runtime.RunOnEventLoop(wrapInAsyncLambda(fmt.Sprintf(`
const file = await fs.open(%q)
await file.close()
try {
const bytesRead = await file.read(new Uint8Array(3))
throw 'unexpected promise resolution with result: ' + bytesRead;
} catch (err) {
if (err.name !== 'BadResourceError') {
throw 'unexpected error: ' + err
}
}
`, testFilePath)))

assert.NoError(t, err)
})

t.Run("seek with invalid arguments should fail", func(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -446,6 +504,65 @@ func TestFile(t *testing.T) {

assert.NoError(t, err)
})

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

runtime, err := newConfiguredRuntime(t)
require.NoError(t, err)

testFilePath := fsext.FilePathSeparator + "bonjour.txt"
fs := newTestFs(t, func(fs afero.Fs) error {
return afero.WriteFile(fs, testFilePath, []byte("hello"), 0o644)
})
runtime.VU.InitEnvField.FileSystems["file"] = fs

_, err = runtime.RunOnEventLoop(wrapInAsyncLambda(fmt.Sprintf(`
const file = await fs.open(%q)
await file.close()
try {
const newOffset = await file.seek(0, fs.SeekMode.Start)
throw 'unexpected promise resolution with result: ' + newOffset;
} catch (err) {
if (err.name !== 'BadResourceError') {
throw 'unexpected error: ' + err
}
}
`, testFilePath)))

assert.NoError(t, err)
})

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

runtime, err := newConfiguredRuntime(t)
require.NoError(t, err)

testFilePath := fsext.FilePathSeparator + "bonjour.txt"
fs := newTestFs(t, func(fs afero.Fs) error {
return afero.WriteFile(fs, testFilePath, []byte("hello"), 0o644)
})
runtime.VU.InitEnvField.FileSystems["file"] = fs

_, err = runtime.RunOnEventLoop(wrapInAsyncLambda(fmt.Sprintf(`
const file = await fs.open(%q)
await file.close()
try {
await file.close()
throw 'unexpected promise resolution';
} catch (err) {
if (err.name !== 'BadResourceError') {
throw 'unexpected error: ' + err
}
}
`, testFilePath)))

assert.NoError(t, err)
})
}

func TestOpenImpl(t *testing.T) {
Expand Down

0 comments on commit 706a61c

Please sign in to comment.