From 85738db4824018d13d8e23cea5ff38ec53025f4a Mon Sep 17 00:00:00 2001 From: oleiade Date: Fri, 14 Jul 2023 14:34:46 +0200 Subject: [PATCH] Add an fs.openSync temporary helper --- js/modules/k6/experimental/fs/module.go | 38 ++++- js/modules/k6/experimental/fs/module_test.go | 153 +++++++++++++++++++ 2 files changed, 190 insertions(+), 1 deletion(-) diff --git a/js/modules/k6/experimental/fs/module.go b/js/modules/k6/experimental/fs/module.go index 97d1937c2477..e47316beb962 100644 --- a/js/modules/k6/experimental/fs/module.go +++ b/js/modules/k6/experimental/fs/module.go @@ -50,7 +50,8 @@ func (rm *RootModule) NewModuleInstance(vu modules.VU) modules.Instance { func (mi *ModuleInstance) Exports() modules.Exports { return modules.Exports{ Named: map[string]any{ - "open": mi.Open, + "open": mi.Open, + "openSync": mi.OpenSync, }, } } @@ -90,6 +91,41 @@ func (mi *ModuleInstance) Open(path goja.Value) *goja.Promise { return promise } +// OpenSync opens a file and returns a [File] instance. +// +// This method is synchronous and should only be used in the init context. +// +// This method is intended as a temporary workaround until we have proper +// support for asynchronous functions execution in the k6 init context. +// +// TODO @oleiade: remove this method once we have proper support for +// asynchronous functions execution in the k6 init context. +func (mi *ModuleInstance) OpenSync(path goja.Value) (goja.Value, error) { + rt := mi.vu.Runtime() + + // Files can only be opened in the init context. + if mi.vu.State() != nil { + common.Throw(rt, newFsError(ForbiddenError, "open() failed; reason: opening a file in the VU context is forbidden")) + } + + if common.IsNullish(path) { + common.Throw(rt, newFsError(TypeError, "open() failed; reason: path cannot be null or undefined")) + } + + // Obtain the underlying path string from the JS value. + pathStr := path.String() + if pathStr == "" { + common.Throw(rt, newFsError(TypeError, "open() failed; reason: path cannot be empty")) + } + + file, err := mi.openImpl(pathStr) + if err != nil { + common.Throw(rt, err) + } + + return rt.ToValue(file), nil +} + func (mi *ModuleInstance) openImpl(path string) (*File, error) { initEnv := mi.vu.InitEnv() diff --git a/js/modules/k6/experimental/fs/module_test.go b/js/modules/k6/experimental/fs/module_test.go index 1a41fe2e220c..abdca0ebe42d 100644 --- a/js/modules/k6/experimental/fs/module_test.go +++ b/js/modules/k6/experimental/fs/module_test.go @@ -173,6 +173,159 @@ func TestOpen(t *testing.T) { }) } +func TestOpenSync(t *testing.T) { + t.Parallel() + + t.Run("opening existing file synchronously should succeed", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + openPath string + wantPath string + }{ + { + name: "open absolute path", + openPath: fsext.FilePathSeparator + "bonjour.txt", + wantPath: fsext.FilePathSeparator + "bonjour.txt", + }, + { + name: "open relative path", + openPath: filepath.Join(".", fsext.FilePathSeparator, "bonjour.txt"), + wantPath: fsext.FilePathSeparator + "bonjour.txt", + }, + { + name: "open path with ..", + openPath: fsext.FilePathSeparator + "dir" + fsext.FilePathSeparator + ".." + fsext.FilePathSeparator + "bonjour.txt", + wantPath: fsext.FilePathSeparator + "bonjour.txt", + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + runtime, err := newConfiguredRuntime(t) + require.NoError(t, err) + + fs := newTestFs(t, func(fs afero.Fs) error { + fileErr := afero.WriteFile(fs, tt.wantPath, []byte("Bonjour, le monde"), 0o644) + if fileErr != nil { + return fileErr + } + + return fs.Mkdir(fsext.FilePathSeparator+"dir", 0o644) + }) + runtime.VU.InitEnvField.FileSystems["file"] = fs + runtime.VU.InitEnvField.CWD = &url.URL{Scheme: "file", Path: fsext.FilePathSeparator} + + _, err = runtime.VU.RuntimeField.RunString(fmt.Sprintf(` + try { + const file = fs.openSync(%q) + + if (file.path !== %q) { + throw 'unexpected file path ' + file.path + '; expected %q'; + } + } catch (err) { + throw "unexpected error: " + err + } + + `, tt.openPath, tt.wantPath, tt.wantPath)) + + assert.NoError(t, err) + }) + } + }) + + t.Run("opening file synchronously in VU context should fail", func(t *testing.T) { + t.Parallel() + + runtime, err := newConfiguredRuntime(t) + require.NoError(t, err) + + runtime.MoveToVUContext(&lib.State{ + Tags: lib.NewVUStateTags(metrics.NewRegistry().RootTagSet().With("tag-vu", "mytag")), + }) + + _, err = runtime.VU.RuntimeField.RunString(` + fs.openSync('bonjour.txt') + `) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "ForbiddenError") + }) + + t.Run("calling open without providing a path should fail", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + openPath string + }{ + { + name: "open empty path should fail", + openPath: "", + }, + { + name: "open null path should fail", + openPath: "null", + }, + { + name: "open undefined path should fail", + openPath: "undefined", + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + runtime, err := newConfiguredRuntime(t) + require.NoError(t, err) + + _, gotErr := runtime.VU.RuntimeField.RunString(fmt.Sprintf(`fs.openSync(%q)`, tt.openPath)) + + assert.Error(t, gotErr) + }) + } + }) + + t.Run("opening directory synchronously should fail", func(t *testing.T) { + t.Parallel() + + runtime, err := newConfiguredRuntime(t) + require.NoError(t, err) + + testDirPath := fsext.FilePathSeparator + "dir" + fs := newTestFs(t, func(fs afero.Fs) error { + return fs.Mkdir(testDirPath, 0o644) + }) + + runtime.VU.InitEnvField.FileSystems["file"] = fs + + _, err = runtime.VU.RuntimeField.RunString(fmt.Sprintf(`fs.openSync(%q)`, testDirPath)) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "InvalidResourceError") + }) + + t.Run("opening non existing file should fail", func(t *testing.T) { + t.Parallel() + + runtime, err := newConfiguredRuntime(t) + require.NoError(t, err) + + _, err = runtime.VU.RuntimeField.RunString(`fs.openSync('doesnotexist.txt')`) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "NotFoundError") + }) +} + func TestFile(t *testing.T) { t.Parallel()