diff --git a/js/bundle.go b/js/bundle.go index a179dd107e0..69a8f8ec800 100644 --- a/js/bundle.go +++ b/js/bundle.go @@ -299,7 +299,15 @@ func (b *Bundle) instantiate(logger logrus.FieldLogger, rt *goja.Runtime, init * rt.Set("__VU", vuID) rt.Set("console", common.Bind(rt, newConsole(logger), init.ctxPtr)) - *init.ctxPtr = common.WithRuntime(context.Background(), rt) + // TODO: get rid of the unused ctxPtr, use a real external context (so we + // can interrupt), build the common.InitEnvironment earlier and reuse it + initenv := &common.InitEnvironment{ + Logger: logger, + FileSystems: init.filesystems, + CWD: init.pwd, + } + ctx := common.WithInitEnv(context.Background(), initenv) + *init.ctxPtr = common.WithRuntime(ctx, rt) unbindInit := common.BindToGlobal(rt, common.Bind(rt, init, init.ctxPtr)) if _, err := rt.RunProgram(b.Program); err != nil { return err diff --git a/js/common/context.go b/js/common/context.go index 2ffa168b1bb..9f992525ab8 100644 --- a/js/common/context.go +++ b/js/common/context.go @@ -30,12 +30,15 @@ type ctxKey int const ( ctxKeyRuntime ctxKey = iota + ctxKeyInitEnv ) +// WithRuntime attaches the given goja runtime to the context. func WithRuntime(ctx context.Context, rt *goja.Runtime) context.Context { return context.WithValue(ctx, ctxKeyRuntime, rt) } +// GetRuntime retrieves the attached goja runtime from the given context. func GetRuntime(ctx context.Context) *goja.Runtime { v := ctx.Value(ctxKeyRuntime) if v == nil { @@ -43,3 +46,17 @@ func GetRuntime(ctx context.Context) *goja.Runtime { } return v.(*goja.Runtime) } + +// WithInitEnv attaches the given init environment to the context. +func WithInitEnv(ctx context.Context, initEnv *InitEnvironment) context.Context { + return context.WithValue(ctx, ctxKeyInitEnv, initEnv) +} + +// GetInitEnv retrieves the attached init environment struct from the given context. +func GetInitEnv(ctx context.Context) *InitEnvironment { + v := ctx.Value(ctxKeyInitEnv) + if v == nil { + return nil + } + return v.(*InitEnvironment) +} diff --git a/js/common/context_test.go b/js/common/context_test.go index 6966b339f47..e8aeaf01215 100644 --- a/js/common/context_test.go +++ b/js/common/context_test.go @@ -36,3 +36,9 @@ func TestContextRuntime(t *testing.T) { func TestContextRuntimeNil(t *testing.T) { assert.Nil(t, GetRuntime(context.Background())) } + +func TestContextInitEnv(t *testing.T) { + ie := &InitEnvironment{} + assert.Nil(t, GetInitEnv(context.Background())) + assert.Equal(t, ie, GetInitEnv(WithInitEnv(context.Background(), ie))) +} diff --git a/js/common/initenv.go b/js/common/initenv.go new file mode 100644 index 00000000000..be0a1b116ef --- /dev/null +++ b/js/common/initenv.go @@ -0,0 +1,62 @@ +/* + * + * k6 - a next-generation load testing tool + * Copyright (C) 2020 Load Impact + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +package common + +import ( + "net/url" + "path/filepath" + + "github.com/sirupsen/logrus" + "github.com/spf13/afero" +) + +// InitEnvironment contains properties that can be accessed by Go code executed +// in the k6 init context. It can be accessed by calling common.GetInitEnv(). +type InitEnvironment struct { + Logger logrus.FieldLogger + FileSystems map[string]afero.Fs + CWD *url.URL + // TODO: add RuntimeOptions and other properties, goja sources, etc. + // ideally, we should leave this as the only data structure necessary for + // executing the init context for all JS modules +} + +// GetAbsFilePath should be used to access the FileSystems, since afero has a +// bug when opening files with relative paths - it caches them from the FS root, +// not the current working directory... So, if necessary, this method will +// transform any relative paths into absolute ones, using the CWD. +// +// TODO: refactor? It was copied from +// https://github.com/loadimpact/k6/blob/c51095ad7304bdd1e82cdb33c91abc331533b886/js/initcontext.go#L211-L222 +func (ie *InitEnvironment) GetAbsFilePath(filename string) string { + // Here IsAbs should be enough but unfortunately it doesn't handle absolute paths starting from + // the current drive on windows like `\users\noname\...`. Also it makes it more easy to test and + // will probably be need for archive execution under windows if always consider '/...' as an + // absolute path. + if filename[0] != '/' && filename[0] != '\\' && !filepath.IsAbs(filename) { + filename = filepath.Join(ie.CWD.Path, filename) + } + filename = filepath.Clean(filename) + if filename[0:1] != afero.FilePathSeparator { + filename = afero.FilePathSeparator + filename + } + return filename +} diff --git a/js/initcontext.go b/js/initcontext.go index 93241630519..c8f9a1a1163 100644 --- a/js/initcontext.go +++ b/js/initcontext.go @@ -49,6 +49,8 @@ const openCantBeUsedOutsideInitContextMsg = `The "open()" function is only avail `(i.e. the global scope), see https://k6.io/docs/using-k6/test-life-cycle for more information` // InitContext provides APIs for use in the init context. +// +// TODO: refactor most/all of this state away, use common.InitEnvironment instead type InitContext struct { // Bound runtime; used to instantiate objects. runtime *goja.Runtime diff --git a/js/modules/k6/grpc/client.go b/js/modules/k6/grpc/client.go index d338481555c..5d4600faa35 100644 --- a/js/modules/k6/grpc/client.go +++ b/js/modules/k6/grpc/client.go @@ -25,6 +25,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net" "strconv" "strings" @@ -116,17 +117,26 @@ func (c *Client) Load(ctxPtr *context.Context, importPaths []string, filenames . return nil, errors.New("load must be called in the init context") } - f, err := protoparse.ResolveFilenames(importPaths, filenames...) - if err != nil { - return nil, err + initEnv := common.GetInitEnv(*ctxPtr) + if initEnv == nil { + return nil, errors.New("missing init environment") + } + + // If no import paths are specified, use the current working directory + if len(importPaths) == 0 { + importPaths = append(importPaths, initEnv.CWD.Path) } parser := protoparse.Parser{ ImportPaths: importPaths, - InferImportPaths: len(importPaths) == 0, + InferImportPaths: false, + Accessor: protoparse.FileAccessor(func(filename string) (io.ReadCloser, error) { + absFilePath := initEnv.GetAbsFilePath(filename) + return initEnv.FileSystems["file"].Open(absFilePath) + }), } - fds, err := parser.ParseFiles(f...) + fds, err := parser.ParseFiles(filenames...) if err != nil { return nil, err } @@ -144,7 +154,11 @@ func (c *Client) Load(ctxPtr *context.Context, importPaths []string, filenames . } var rtn []MethodInfo - c.mds = make(map[string]protoreflect.MethodDescriptor) + if c.mds == nil { + // This allows us to call load() multiple times, without overwriting the + // previously loaded definitions. + c.mds = make(map[string]protoreflect.MethodDescriptor) + } files.RangeFiles(func(fd protoreflect.FileDescriptor) bool { sds := fd.Services() diff --git a/js/modules/k6/grpc/client_test.go b/js/modules/k6/grpc/client_test.go index bf23679629c..d484f1c257c 100644 --- a/js/modules/k6/grpc/client_test.go +++ b/js/modules/k6/grpc/client_test.go @@ -23,12 +23,17 @@ package grpc import ( "bytes" "context" + "net/url" + "os" + "runtime" "strings" "testing" "github.com/dop251/goja" "github.com/sirupsen/logrus" + "github.com/spf13/afero" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" @@ -39,11 +44,14 @@ import ( "github.com/loadimpact/k6/js/common" "github.com/loadimpact/k6/lib" + "github.com/loadimpact/k6/lib/fsext" "github.com/loadimpact/k6/lib/metrics" "github.com/loadimpact/k6/lib/testutils/httpmultibin" "github.com/loadimpact/k6/stats" ) +const isWindows = runtime.GOOS == "windows" + func assertMetricEmitted(t *testing.T, metric *stats.Metric, sampleContainers []stats.SampleContainer, url string) { seenMetric := false @@ -87,7 +95,24 @@ func TestClient(t *testing.T) { }, } + cwd, err := os.Getwd() + require.NoError(t, err) + + fs := afero.NewOsFs() + if isWindows { + fs = fsext.NewTrimFilePathSeparatorFs(fs) + } + + initEnv := &common.InitEnvironment{ + Logger: logrus.New(), + CWD: &url.URL{Path: cwd}, + FileSystems: map[string]afero.Fs{ + "file": fs, + }, + } + ctx := common.WithRuntime(context.Background(), rt) + ctx = common.WithInitEnv(ctx, initEnv) rt.Set("grpc", common.Bind(rt, New(), &ctx)) diff --git a/js/modules/k6/grpc/grpc.go b/js/modules/k6/grpc/grpc.go index 829e2e50e6a..5864e96ecea 100644 --- a/js/modules/k6/grpc/grpc.go +++ b/js/modules/k6/grpc/grpc.go @@ -27,7 +27,7 @@ import ( ) func init() { - modules.Register("k6/protocols/grpc", New()) + modules.Register("k6/net/grpc", New()) } // GRPC represents the gRPC protocol module for k6 diff --git a/js/modules/k6/http/request_test.go b/js/modules/k6/http/request_test.go index be14ce74304..3e20727ff1d 100644 --- a/js/modules/k6/http/request_test.go +++ b/js/modules/k6/http/request_test.go @@ -542,11 +542,11 @@ func TestRequestAndBatch(t *testing.T) { } t.Run("ocsp_stapled_good", func(t *testing.T) { _, err := common.RunString(rt, ` - var res = http.request("GET", "https://www.microsoft.com/"); + var res = http.request("GET", "https://www.microsoft.com/en-us/"); if (res.ocsp.status != http.OCSP_STATUS_GOOD) { throw new Error("wrong ocsp stapled response status: " + res.ocsp.status); } `) assert.NoError(t, err) - assertRequestMetricsEmitted(t, stats.GetBufferedSamples(samples), "GET", "https://www.microsoft.com/", "", 200, "") + assertRequestMetricsEmitted(t, stats.GetBufferedSamples(samples), "GET", "https://www.microsoft.com/en-us/", "", 200, "") }) }) t.Run("Invalid", func(t *testing.T) { diff --git a/js/runner_test.go b/js/runner_test.go index fc9e9aa4984..c88c4aa652c 100644 --- a/js/runner_test.go +++ b/js/runner_test.go @@ -527,6 +527,7 @@ func TestVURunContext(t *testing.T) { fnCalled = true assert.Equal(t, vu.Runtime, common.GetRuntime(*vu.Context), "incorrect runtime in context") + assert.Nil(t, common.GetInitEnv(*vu.Context)) // shouldn't get this in the vu context state := lib.GetState(*vu.Context) if assert.NotNil(t, state) { diff --git a/samples/grpc.js b/samples/grpc.js index 072159f631b..4690c647af5 100644 --- a/samples/grpc.js +++ b/samples/grpc.js @@ -1,4 +1,4 @@ -import grpc from 'k6/protocols/grpc'; +import grpc from 'k6/net/grpc'; import { check } from "k6"; let client = grpc.newClient();