From 835b48f6212e875ebfccd7734f99552670b6a4c6 Mon Sep 17 00:00:00 2001 From: oleiade Date: Wed, 5 Jul 2023 11:11:16 +0200 Subject: [PATCH] Implement `fs` module with open method, File and FileInfo abstractions --- js/modules/k6/experimental/fs/registry.go | 60 +++++++++++++++++++ .../k6/experimental/fs/registry_test.go | 52 ++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 js/modules/k6/experimental/fs/registry.go create mode 100644 js/modules/k6/experimental/fs/registry_test.go diff --git a/js/modules/k6/experimental/fs/registry.go b/js/modules/k6/experimental/fs/registry.go new file mode 100644 index 00000000000..701ae2e4be8 --- /dev/null +++ b/js/modules/k6/experimental/fs/registry.go @@ -0,0 +1,60 @@ +package fs + +import ( + "fmt" + "io" + "path/filepath" + "sync" + + "github.com/spf13/afero" +) + +// registry is a registry of opened files. +type registry struct { + // files holds a safe for concurrent use map of opened files. + // + // Keys are expected to be strings holding the files' path. + // Values are expected to be byte slices holding the files' data. + // + // That way, we can cache the file's content and avoid opening too many + // file descriptor, and re-reading its content every time the file is opened. + // + // Importantly, this also means that if the + // file is modified from outside of k6, the changes will not be reflected in the file's data. + // files map[string][]byte + files sync.Map +} + +// open opens the named file for reading. +// +// If the file was already opened, it returns a pointer to the cached file data. Otherwise, it +// opens the file, reads its content, caches it, and returns a pointer to it. +// +// The file is always opened in read-only mode. The provided file path is cleaned before being +// used. +func (fr *registry) open(filename string, fromFs afero.Fs) ([]byte, error) { + filename = filepath.Clean(filename) + + if f, ok := fr.files.Load(filename); ok { + data, ok := f.([]byte) + if !ok { + panic(fmt.Errorf("registry's file %s is not stored as a byte slice", filename)) + } + + return data, nil + } + + f, err := fromFs.Open(filename) + if err != nil { + return nil, err + } + + data, err := io.ReadAll(f) + if err != nil { + return nil, err + } + + fr.files.Store(filename, data) + + return data, nil +} diff --git a/js/modules/k6/experimental/fs/registry_test.go b/js/modules/k6/experimental/fs/registry_test.go new file mode 100644 index 00000000000..971aff17e0c --- /dev/null +++ b/js/modules/k6/experimental/fs/registry_test.go @@ -0,0 +1,52 @@ +package fs + +import ( + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" +) + +func TestFileRegistryOpen(t *testing.T) { + t.Parallel() + + t.Run("open succeeds", func(t *testing.T) { + t.Parallel() + + registry := ®istry{} + fs := newTestFs(t, func(fs afero.Fs) error { + return afero.WriteFile(fs, "bonjour.txt", []byte("Bonjour, le monde"), 0o644) + }) + + _, gotBeforeOk := registry.files.Load("bonjour.txt") + gotData, gotErr := registry.open("bonjour.txt", fs) + _, gotAfterOk := registry.files.Load("bonjour.txt") + + assert.False(t, gotBeforeOk) + assert.NoError(t, gotErr) + assert.Equal(t, []byte("Bonjour, le monde"), gotData) + assert.True(t, gotAfterOk) + }) + + t.Run("double open succeeds", func(t *testing.T) { + t.Parallel() + + registry := ®istry{} + fs := newTestFs(t, func(fs afero.Fs) error { + return afero.WriteFile(fs, "bonjour.txt", []byte("Bonjour, le monde"), 0o644) + }) + + firstData, firstErr := registry.open("bonjour.txt", fs) + _, gotFirstOk := registry.files.Load("bonjour.txt") + secondData, secondErr := registry.open("bonjour.txt", fs) + _, gotSecondOk := registry.files.Load("bonjour.txt") + + assert.True(t, gotFirstOk) + assert.NoError(t, firstErr) + assert.Equal(t, []byte("Bonjour, le monde"), firstData) + assert.True(t, gotSecondOk) + assert.NoError(t, secondErr) + assert.Equal(t, firstData, secondData) // same pointer + assert.Equal(t, []byte("Bonjour, le monde"), secondData) + }) +}