diff --git a/js/bundle.go b/js/bundle.go
index 69a8f8ec800..7c50a064ecc 100644
--- a/js/bundle.go
+++ b/js/bundle.go
@@ -302,9 +302,10 @@ func (b *Bundle) instantiate(logger logrus.FieldLogger, rt *goja.Runtime, init *
// 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,
+ SharedObjects: init.sharedObjects,
+ Logger: logger,
+ FileSystems: init.filesystems,
+ CWD: init.pwd,
}
ctx := common.WithInitEnv(context.Background(), initenv)
*init.ctxPtr = common.WithRuntime(ctx, rt)
diff --git a/js/common/initenv.go b/js/common/initenv.go
index be0a1b116ef..b5ba4037bad 100644
--- a/js/common/initenv.go
+++ b/js/common/initenv.go
@@ -23,6 +23,7 @@ package common
import (
"net/url"
"path/filepath"
+ "sync"
"github.com/sirupsen/logrus"
"github.com/spf13/afero"
@@ -37,6 +38,7 @@ type InitEnvironment struct {
// 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
+ SharedObjects *SharedObjects
}
// GetAbsFilePath should be used to access the FileSystems, since afero has a
@@ -60,3 +62,34 @@ func (ie *InitEnvironment) GetAbsFilePath(filename string) string {
}
return filename
}
+
+// SharedObjects is a collection of general store for objects to be shared. It is mostly a wrapper
+// around map[string]interface with a lock and stuff.
+// The reason behind not just using sync.Map is that it still needs a lock when we want to only call
+// the function constructor if there is no such key at which point you already need a lock so ...
+type SharedObjects struct {
+ data map[string]interface{}
+ l sync.Mutex
+}
+
+// NewSharedObjects returns a new SharedObjects ready to use
+func NewSharedObjects() *SharedObjects {
+ return &SharedObjects{
+ data: make(map[string]interface{}),
+ }
+}
+
+// GetOrCreateShare returns a shared value with the given name or sets it's value whatever
+// createCallback returns and returns it.
+func (so *SharedObjects) GetOrCreateShare(name string, createCallback func() interface{}) interface{} {
+ so.l.Lock()
+ defer so.l.Unlock()
+
+ value, ok := so.data[name]
+ if !ok {
+ value = createCallback()
+ so.data[name] = value
+ }
+
+ return value
+}
diff --git a/js/console_test.go b/js/console_test.go
index 92b06002809..a6e4421666d 100644
--- a/js/console_test.go
+++ b/js/console_test.go
@@ -76,6 +76,7 @@ func getSimpleRunner(tb testing.TB, filename, data string, opts ...interface{})
var (
fs = afero.NewMemMapFs()
rtOpts = lib.RuntimeOptions{CompatibilityMode: null.NewString("base", true)}
+ logger = testutils.NewLogger(tb)
)
for _, o := range opts {
switch opt := o.(type) {
@@ -83,10 +84,12 @@ func getSimpleRunner(tb testing.TB, filename, data string, opts ...interface{})
fs = opt
case lib.RuntimeOptions:
rtOpts = opt
+ case *logrus.Logger:
+ logger = opt
}
}
return New(
- testutils.NewLogger(tb),
+ logger,
&loader.SourceData{
URL: &url.URL{Path: filename, Scheme: "file"},
Data: []byte(data),
diff --git a/js/initcontext.go b/js/initcontext.go
index c8f9a1a1163..661f142a6c3 100644
--- a/js/initcontext.go
+++ b/js/initcontext.go
@@ -69,6 +69,8 @@ type InitContext struct {
compatibilityMode lib.CompatibilityMode
logger logrus.FieldLogger
+
+ sharedObjects *common.SharedObjects
}
// NewInitContext creates a new initcontext with the provided arguments
@@ -85,6 +87,7 @@ func NewInitContext(
programs: make(map[string]programWithSource),
compatibilityMode: compatMode,
logger: logger,
+ sharedObjects: common.NewSharedObjects(),
}
}
@@ -110,6 +113,7 @@ func newBoundInitContext(base *InitContext, ctxPtr *context.Context, rt *goja.Ru
programs: programs,
compatibilityMode: base.compatibilityMode,
logger: base.logger,
+ sharedObjects: base.sharedObjects,
}
}
@@ -162,7 +166,6 @@ func (i *InitContext) requireFile(name string) (goja.Value, error) {
if pgm.pgm == nil {
// Load the sources; the loader takes care of remote loading, etc.
- // TODO: don't use the Global logger
data, err := loader.Load(i.logger, i.filesystems, fileURL, name)
if err != nil {
return goja.Undefined(), err
diff --git a/js/modules.go b/js/modules.go
index 28d780b0940..ff6c5d0d691 100644
--- a/js/modules.go
+++ b/js/modules.go
@@ -25,6 +25,7 @@ import (
_ "github.com/loadimpact/k6/js/modules/k6"
_ "github.com/loadimpact/k6/js/modules/k6/crypto"
_ "github.com/loadimpact/k6/js/modules/k6/crypto/x509"
+ _ "github.com/loadimpact/k6/js/modules/k6/data"
_ "github.com/loadimpact/k6/js/modules/k6/encoding"
_ "github.com/loadimpact/k6/js/modules/k6/grpc"
_ "github.com/loadimpact/k6/js/modules/k6/http"
diff --git a/js/modules/k6/data/data.go b/js/modules/k6/data/data.go
new file mode 100644
index 00000000000..2e43ab2bf45
--- /dev/null
+++ b/js/modules/k6/data/data.go
@@ -0,0 +1,95 @@
+/*
+ *
+ * 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 data
+
+import (
+ "context"
+
+ "github.com/dop251/goja"
+ "github.com/loadimpact/k6/js/common"
+ "github.com/loadimpact/k6/js/internal/modules"
+ "github.com/loadimpact/k6/lib"
+ "github.com/pkg/errors"
+)
+
+type data struct{}
+
+func init() {
+ modules.Register("k6/data", new(data))
+}
+
+const sharedArrayNamePrefix = "k6/data/SharedArray."
+
+// XSharedArray is a constructor returning a shareable read-only array
+// indentified by the name and having their contents be whatever the call returns
+func (d *data) XSharedArray(ctx context.Context, name string, call goja.Callable) (goja.Value, error) {
+ if lib.GetState(ctx) != nil {
+ return nil, errors.New("new SharedArray must be called in the init context")
+ }
+
+ initEnv := common.GetInitEnv(ctx)
+ if initEnv == nil {
+ return nil, errors.New("missing init environment")
+ }
+ if len(name) == 0 {
+ return nil, errors.New("empty name provided to SharedArray's constructor")
+ }
+
+ name = sharedArrayNamePrefix + name
+ value := initEnv.SharedObjects.GetOrCreateShare(name, func() interface{} {
+ return getShareArrayFromCall(common.GetRuntime(ctx), call)
+ })
+ array, ok := value.(sharedArray)
+ if !ok { // TODO more info in the error?
+ return nil, errors.New("wrong type of shared object")
+ }
+
+ return array.wrap(&ctx, common.GetRuntime(ctx)), nil
+}
+
+func getShareArrayFromCall(rt *goja.Runtime, call goja.Callable) sharedArray {
+ gojaValue, err := call(goja.Undefined())
+ if err != nil {
+ common.Throw(rt, err)
+ }
+ obj := gojaValue.ToObject(rt)
+ if obj.ClassName() != "Array" {
+ common.Throw(rt, errors.New("only arrays can be made into SharedArray")) // TODO better error
+ }
+ arr := make([]string, obj.Get("length").ToInteger())
+
+ // We specifically use JSON.stringify here as we need to use JSON.parse on the way out
+ // it also has the benefit of needing only one loop and being more JS then using golang's json
+ cal, err := rt.RunString(`(function(input, output) {
+ for (var i = 0; i < input.length; i++) {
+ output[i] = JSON.stringify(input[i])
+ }
+ })`)
+ if err != nil {
+ common.Throw(rt, err)
+ }
+ newCall, _ := goja.AssertFunction(cal)
+ _, err = newCall(goja.Undefined(), gojaValue, rt.ToValue(arr))
+ if err != nil {
+ common.Throw(rt, err)
+ }
+ return sharedArray{arr: arr}
+}
diff --git a/js/modules/k6/data/share.go b/js/modules/k6/data/share.go
new file mode 100644
index 00000000000..de0b25bd732
--- /dev/null
+++ b/js/modules/k6/data/share.go
@@ -0,0 +1,135 @@
+/*
+ *
+ * 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 data
+
+import (
+ "context"
+
+ "github.com/dop251/goja"
+ "github.com/loadimpact/k6/js/common"
+)
+
+// TODO fix it not working really well with setupData or just make it more broken
+// TODO fix it working with console.log
+type sharedArray struct {
+ arr []string
+}
+
+func (s sharedArray) wrap(ctxPtr *context.Context, rt *goja.Runtime) goja.Value {
+ cal, err := rt.RunString(arrayWrapperCode)
+ if err != nil {
+ common.Throw(rt, err)
+ }
+ call, _ := goja.AssertFunction(cal)
+ wrapped, err := call(goja.Undefined(), rt.ToValue(common.Bind(rt, s, ctxPtr)))
+ if err != nil {
+ common.Throw(rt, err)
+ }
+
+ return wrapped
+}
+
+func (s sharedArray) Get(index int) (interface{}, error) {
+ if index < 0 || index >= len(s.arr) {
+ return goja.Undefined(), nil
+ }
+
+ // we specifically use JSON.parse to get the json to an object inside as otherwise we won't be
+ // able to freeze it as goja doesn't let us unless it is a pure goja object and this is the
+ // easiest way to get one.
+ return s.arr[index], nil
+}
+
+func (s sharedArray) Length() int {
+ return len(s.arr)
+}
+
+/* This implementation is commented as with it - it is harder to deepFreeze it with this implementation.
+type sharedArrayIterator struct {
+ a *sharedArray
+ index int
+}
+
+func (sai *sharedArrayIterator) Next() (interface{}, error) {
+ if sai.index == len(sai.a.arr)-1 {
+ return map[string]bool{"done": true}, nil
+ }
+ sai.index++
+ var tmp interface{}
+ if err := json.Unmarshal(sai.a.arr[sai.index], &tmp); err != nil {
+ return goja.Undefined(), err
+ }
+ return map[string]interface{}{"value": tmp}, nil
+}
+
+func (s sharedArray) Iterator() *sharedArrayIterator {
+ return &sharedArrayIterator{a: &s, index: -1}
+}
+*/
+
+const arrayWrapperCode = `(function(val) {
+ function deepFreeze(o) {
+ Object.freeze(o);
+ if (o === undefined) {
+ return o;
+ }
+
+ Object.getOwnPropertyNames(o).forEach(function (prop) {
+ if (o[prop] !== null
+ && (typeof o[prop] === "object" || typeof o[prop] === "function")
+ && !Object.isFrozen(o[prop])) {
+ deepFreeze(o[prop]);
+ }
+ });
+
+ return o;
+ };
+
+ var arrayHandler = {
+ get: function(target, property, receiver) {
+ switch (property){
+ case "length":
+ return target.length();
+ case Symbol.iterator:
+ return function(){
+ var index = 0;
+ return {
+ "next": function() {
+ if (index >= target.length()) {
+ return {done: true}
+ }
+ var result = {value: deepFreeze(JSON.parse(target.get(index)))};
+ index++;
+ return result;
+ }
+ }
+ }
+ }
+ var i = parseInt(property);
+ if (isNaN(i)) {
+ return undefined;
+ }
+
+ return deepFreeze(JSON.parse(target.get(i)));
+ }
+ };
+ return new Proxy(val, arrayHandler);
+})`
diff --git a/js/share_test.go b/js/share_test.go
new file mode 100644
index 00000000000..760f88dd1a1
--- /dev/null
+++ b/js/share_test.go
@@ -0,0 +1,182 @@
+/*
+ *
+ * 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 js
+
+import (
+ "context"
+ "io/ioutil"
+ "testing"
+
+ "github.com/loadimpact/k6/lib"
+ "github.com/loadimpact/k6/lib/testutils"
+ "github.com/loadimpact/k6/stats"
+ "github.com/sirupsen/logrus"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestInitContextNewSharedArray(t *testing.T) {
+ data := `'use strict';
+var SharedArray = require("k6/data").SharedArray;
+function generateArray() {
+ console.log("once");
+ var n = 50;
+ var arr = new Array(n);
+ for (var i = 0 ; i