Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[fs] Add file.read and file.seek methods to the fs module (2/3) #3309

Merged
merged 12 commits into from
Nov 6, 2023
38 changes: 35 additions & 3 deletions examples/experimental/fs/fs.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,52 @@
import { open } from "k6/experimental/fs";
import { open, SeekMode } from "k6/experimental/fs";

export const options = {
vus: 100,
iterations: 1000,
};

// As k6 does not support asynchronous code in the init context, yet, we need to
// use a top-level async function to be able to use the `await` keyword.
// k6 doesn't support async in the init context. We use a top-level async function for `await`.
//
// Each Virtual User gets its own `file` copy.
// So, operations like `seek` or `read` won't impact other VUs.
let file;
(async function () {
file = await open("bonjour.txt");
})();

export default async function () {
// About information about the file
const fileinfo = await file.stat();
if (fileinfo.name != "bonjour.txt") {
throw new Error("Unexpected file name");
}

const buffer = new Uint8Array(4);

let totalBytesRead = 0;
while (true) {
// Read into the buffer
const bytesRead = await file.read(buffer);
if (bytesRead == null) {
// EOF
break;
}

// Do something useful with the content of the buffer

totalBytesRead += bytesRead;

// If bytesRead is less than the buffer size, we've read the whole file
if (bytesRead < buffer.byteLength) {
break;
}
}

// Check that we read the expected number of bytes
if (totalBytesRead != fileinfo.size) {
oleiade marked this conversation as resolved.
Show resolved Hide resolved
throw new Error("Unexpected number of bytes read");
}

// Seek back to the beginning of the file
await file.seek(0, SeekMode.Start);
}
3 changes: 3 additions & 0 deletions js/modules/k6/experimental/fs/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ const (

// TypeError is emitted when an incorrect type has been used.
TypeError

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

// 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.

114 changes: 114 additions & 0 deletions js/modules/k6/experimental/fs/file.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package fs

import (
"io"
"path/filepath"
"sync/atomic"

"go.k6.io/k6/lib"
)

// file is an abstraction for interacting with files.
Expand All @@ -10,6 +14,13 @@ type file struct {

// data holds a pointer to the file's data
data []byte

// offset holds the current offset in the file
//
// TODO: using an atomic here does not guarantee ordering of reads and seeks, and leaves
// the behavior not strictly defined. This is something we might want to address in the future, and
// is tracked as part of #3433.
offset atomic.Int64
}

// Stat returns a FileInfo describing the named file.
Expand All @@ -26,3 +37,106 @@ type FileInfo struct {
// Size holds the size of the file in bytes.
Size int `json:"size"`
}

// Read reads up to len(into) bytes into the provided byte slice.
//
// It returns the number of bytes read (0 <= n <= len(into)) and any error
// encountered.
//
// If the end of the file has been reached, it returns EOFError.
func (f *file) Read(into []byte) (n int, err error) {
currentOffset := f.offset.Load()
fileSize := f.size()

// Check if we have reached the end of the file
if currentOffset == fileSize {
return 0, newFsError(EOFError, "EOF")
}

// Calculate the effective new offset
targetOffset := currentOffset + int64(len(into))
newOffset := lib.Min(targetOffset, fileSize)

// Read the data into the provided slice, and update
// the offset accordingly
n = copy(into, f.data[currentOffset:newOffset])
f.offset.Store(newOffset)

// If we've reached or surpassed the end, set the error to EOF
if targetOffset > fileSize {
err = newFsError(EOFError, "EOF")
}

return n, err
}

// Ensure that `file` implements the io.Reader interface.
var _ io.Reader = (*file)(nil)

// Seek sets the offset for the next operation on the file, under the mode given by `whence`.
//
// `offset` indicates the number of bytes to move the offset. Based on
// the `whence` parameter, the offset is set relative to the start,
// current offset or end of the file.
//
// When using SeekModeStart, the offset must be positive.
// Negative offsets are allowed when using `SeekModeCurrent` or `SeekModeEnd`.
func (f *file) Seek(offset int64, whence SeekMode) (int64, error) {
startingOffset := f.offset.Load()

newOffset := startingOffset
switch whence {
case SeekModeStart:
if offset < 0 {
return 0, newFsError(TypeError, "offset cannot be negative when using SeekModeStart")
}

newOffset = offset
case SeekModeCurrent:
newOffset += offset
case SeekModeEnd:
if offset > 0 {
return 0, newFsError(TypeError, "offset cannot be positive when using SeekModeEnd")
}

newOffset = f.size() + offset
default:
return 0, newFsError(TypeError, "invalid seek mode")
}

if newOffset < 0 {
return 0, newFsError(TypeError, "seeking before start of file")
}

if newOffset > f.size() {
return 0, newFsError(TypeError, "seeking beyond end of file")
}

// Update the file instance's offset to the new selected position
f.offset.Store(newOffset)

return newOffset, nil
}

var _ io.Seeker = (*file)(nil)

// SeekMode is used to specify the seek mode when seeking in a file.
type SeekMode = int

const (
// SeekModeStart sets the offset relative to the start of the file.
SeekModeStart SeekMode = iota + 1

// SeekModeCurrent seeks relative to the current offset.
SeekModeCurrent

// SeekModeEnd seeks relative to the end of the file.
//
// When using this mode the seek operation will move backwards from
// the end of the file.
SeekModeEnd
)
oleiade marked this conversation as resolved.
Show resolved Hide resolved

func (f *file) size() int64 {
return int64(len(f.data))
}
Loading
Loading