Skip to content

Commit

Permalink
Use k6 module system to load js customization
Browse files Browse the repository at this point in the history
This is mostly needed as soon compiler.Compile will be gone. And even if
not supporting ESM syntax will require actually having a full module
system.

The change is quite big and there are some questionable things around
getting the current working directory.
  • Loading branch information
mstoykov committed Jul 1, 2024
1 parent c126554 commit 973bb7d
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 59 deletions.
105 changes: 66 additions & 39 deletions dashboard/customize.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@
package dashboard

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/url"
"path/filepath"
"reflect"
"strings"

"github.com/grafana/sobek"
"github.com/sirupsen/logrus"
"go.k6.io/k6/js/common"
"go.k6.io/k6/js/compiler"
"go.k6.io/k6/js/modules"
"go.k6.io/k6/lib"
"go.k6.io/k6/lib/fsext"
)
Expand Down Expand Up @@ -83,20 +87,67 @@ func exists(fs fsext.Fs, filename string) bool {

type configLoader struct {
runtime *sobek.Runtime
compiler *compiler.Compiler
modSys *modules.ModuleSystem
defaultConfig *sobek.Object
proc *process
cwd *url.URL
}

type vu struct {
initEnv *common.InitEnvironment
rt *sobek.Runtime
}

func (v *vu) InitEnv() *common.InitEnvironment {
return v.initEnv
}

func (v *vu) Runtime() *sobek.Runtime {
return v.rt
}

func (v *vu) Events() common.Events {
panic("Events() isn't implemented in dashboard VU")
}

func (v *vu) Context() context.Context {
panic("Context() isn't implemented in dashboard VU")
}

func (v *vu) RegisterCallback() func(func() error) {
panic("RegisterCallback() isn't implemented in dashboard VU")
}

func (v *vu) State() *lib.State {
panic("State() isn't implemented in dashboard VU")
}

func newConfigLoader(defaultConfig json.RawMessage, proc *process) (*configLoader, error) {
comp := compiler.New(proc.logger)

comp.Options.CompatibilityMode = lib.CompatibilityModeExtended
comp.Options.Strict = true
mr := modules.NewModuleResolver(nil, func(specifier *url.URL, _ string) ([]byte, error) {
file, err := proc.fs.Open(specifier.Path)
if err != nil {
return nil, err
}

con := newConfigConsole(proc.logger)

return io.ReadAll(file)
}, comp)
runtime := sobek.New()
cwdURL, err := url.Parse("file:///" + proc.wd)
if err != nil {
return nil, err
}
ms := modules.NewModuleSystem(mr, &vu{
rt: runtime,
initEnv: &common.InitEnvironment{
CWD: cwdURL,
},
})

con := newConfigConsole(proc.logger)

runtime.SetFieldNameMapper(sobek.UncapFieldNameMapper())

Expand All @@ -111,26 +162,24 @@ func newConfigLoader(defaultConfig json.RawMessage, proc *process) (*configLoade

loader := &configLoader{
runtime: runtime,
compiler: comp,
defaultConfig: def,
proc: proc,
modSys: ms,
cwd: cwdURL,
}

return loader, nil
}

func (loader *configLoader) load(filename string) (json.RawMessage, error) {
file, err := loader.proc.fs.Open(filename)
if err != nil {
return nil, err
if !strings.HasPrefix(filename, "./") &&
!strings.HasPrefix(filename, "/") &&
!strings.HasPrefix(filename, "../") &&
!strings.HasPrefix(filename, "file://") {
filename = "./" + filename
}

src, err := io.ReadAll(file)
if err != nil {
return nil, err
}

val, err := loader.eval(src, filename)
val, err := loader.eval(filename)
if err != nil {
return nil, err
}
Expand All @@ -144,40 +193,18 @@ func isObject(val sobek.Value) bool {
return val != nil && val.ExportType() != nil && val.ExportType().Kind() == reflect.Map
}

func (loader *configLoader) eval(src []byte, filename string) (*sobek.Object, error) {
prog, _, err := loader.compiler.Compile(string(src), filename, false)
if err != nil {
return nil, err
}

exports := loader.runtime.NewObject()
module := loader.runtime.NewObject()

if err = module.Set("exports", exports); err != nil {
return nil, err
}

val, err := loader.runtime.RunProgram(prog)
func (loader *configLoader) eval(filename string) (*sobek.Object, error) {
exports, err := loader.modSys.Require(loader.cwd, filename)
fmt.Println(exports, err)
if err != nil {
return nil, err
}

call, isCallable := sobek.AssertFunction(val)
if !isCallable {
return nil, fmt.Errorf("%w, file: %s", errNotFunction, filename)
}

_, err = call(exports, module, exports)
if err != nil {
return nil, err
}

def := exports.Get("default")
if def == nil {
return nil, fmt.Errorf("%w, file: %s", errNoExport, filename)
}

if call, isCallable = sobek.AssertFunction(def); isCallable {
if call, isCallable := sobek.AssertFunction(def); isCallable {
def, err = call(exports, loader.defaultConfig)
if err != nil {
return nil, err
Expand Down
37 changes: 29 additions & 8 deletions dashboard/customize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ package dashboard

import (
_ "embed"
"fmt"
"os"
"testing"

"github.com/grafana/sobek"
"github.com/sirupsen/logrus"
logtest "github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"go.k6.io/k6/lib/fsext"
)

func Test_loadConfigJSON(t *testing.T) {
Expand Down Expand Up @@ -52,9 +56,13 @@ func Test_customize_env_found(t *testing.T) {

th := helper(t).osFs()

conf, err := customize(testconfig, th.proc)
var err error
th.proc.wd, err = os.Getwd()

Check failure on line 60 in dashboard/customize_test.go

View workflow job for this annotation

GitHub Actions / Lint

use of `os.Getwd` forbidden because "Using anything except Signal and SyscallError from the os package is forbidden" (forbidigo)

Check failure on line 60 in dashboard/customize_test.go

View workflow job for this annotation

GitHub Actions / Lint

use of `os.Getwd` forbidden because "Using anything except Signal and SyscallError from the os package is forbidden" (forbidigo)
assert.NoError(t, err)

conf, err := customize(testconfig, th.proc)
assert.NoError(t, err)
fmt.Println(string(conf))

assert.True(t, gjson.GetBytes(conf, `tabs.#(id="custom")`).Exists())

Expand All @@ -71,6 +79,10 @@ func TestConfigInReadme(t *testing.T) {

th := helper(t).osFs()

var err error
th.proc.wd, err = os.Getwd()

Check failure on line 83 in dashboard/customize_test.go

View workflow job for this annotation

GitHub Actions / Lint

use of `os.Getwd` forbidden because "Using anything except Signal and SyscallError from the os package is forbidden" (forbidigo)

Check failure on line 83 in dashboard/customize_test.go

View workflow job for this annotation

GitHub Actions / Lint

use of `os.Getwd` forbidden because "Using anything except Signal and SyscallError from the os package is forbidden" (forbidigo)
assert.NoError(t, err)

conf, err := loadConfigJS("../.dashboard.js", testconfig, th.proc)

assert.NoError(t, err)
Expand Down Expand Up @@ -164,31 +176,40 @@ func Test_loadConfigJS_error(t *testing.T) {
func Test_configLoader_eval_error(t *testing.T) {
t.Parallel()

th := helper(t).osFs()
evalHelper := func(src string) (*sobek.Object, error) {
t.Helper()

loader, err := newConfigLoader(testconfig, th.proc)
th := helper(t)
th.proc.fs = fsext.NewMemMapFs()
th.proc.wd = "/some/path/"

assert.NoError(t, err)
loader, err := newConfigLoader(testconfig, th.proc)
require.NoError(t, err)

err = fsext.WriteFile(th.proc.fs, "/some/path/morestuff/test.js", []byte(src), 0o6)
require.NoError(t, err)
return loader.eval("./morestuff/test.js")
}

obj, err := loader.eval([]byte("invalid script"), "")
obj, err := evalHelper("invalid script")

assert.Error(t, err)
assert.Nil(t, obj)

// no default export
obj, err = loader.eval([]byte("let answer = 42"), "")
obj, err = evalHelper("let answer = 42")

assert.Error(t, err)
assert.Nil(t, obj)

// no return value from export function
obj, err = loader.eval([]byte("export default function() {}"), "")
obj, err = evalHelper("export default function() {}")

assert.Error(t, err)
assert.Nil(t, obj)

// error in default export function
obj, err = loader.eval([]byte("export default function() {throw Error()}"), "")
obj, err = evalHelper("export default function() {throw Error()}")

assert.Error(t, err)
assert.Nil(t, obj)
Expand Down
8 changes: 8 additions & 0 deletions dashboard/extension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func TestNewExtension(t *testing.T) {
params.ConfigArgument = "port=1&host=localhost"
params.OutputType = "dashboard"
params.FS = fsext.NewMemMapFs()
params.ScriptPath = &url.URL{}

ext, err := New(params)

Expand All @@ -53,6 +54,7 @@ func testReadSSE(t *testing.T, nlines int) []string {
params.Logger = logrus.StandardLogger()
params.ConfigArgument = "period=10ms&port=0"
params.FS = fsext.NewMemMapFs()
params.ScriptPath = &url.URL{}

ext, err := New(params)

Expand Down Expand Up @@ -140,6 +142,7 @@ func TestExtension_no_http(t *testing.T) {
params.Logger = logrus.StandardLogger()
params.ConfigArgument = "port=-1"
params.FS = fsext.NewMemMapFs()
params.ScriptPath = &url.URL{}

ext, err := New(params)

Expand Down Expand Up @@ -167,6 +170,7 @@ func TestExtension_random_port(t *testing.T) {
params.Logger = logrus.StandardLogger()
params.ConfigArgument = "port=0"
params.FS = fsext.NewMemMapFs()
params.ScriptPath = &url.URL{}

ext, err := New(params)

Expand Down Expand Up @@ -198,6 +202,7 @@ func TestExtension_error_used_port(t *testing.T) {
params.Logger = logrus.StandardLogger()
params.ConfigArgument = "port=0"
params.FS = fsext.NewMemMapFs()
params.ScriptPath = &url.URL{}

ext, err := New(params)

Expand Down Expand Up @@ -228,6 +233,7 @@ func TestExtension_open(t *testing.T) {
params.Logger = logrus.StandardLogger()
params.ConfigArgument = "port=0&open"
params.FS = fsext.NewMemMapFs()
params.ScriptPath = &url.URL{}

ext, err := New(params)

Expand Down Expand Up @@ -255,6 +261,7 @@ func TestExtension_report(t *testing.T) {
params.Logger = logrus.StandardLogger()
params.ConfigArgument = "period=10ms&port=-1&report=" + file.Name() + ".gz"
params.FS = osFS
params.ScriptPath = &url.URL{}

ext, err := New(params)

Expand Down Expand Up @@ -299,6 +306,7 @@ func TestExtension_skip_report(t *testing.T) {
params.Logger = logrus.StandardLogger()
params.ConfigArgument = "period=10ms&port=-1&report=" + file.Name() + ".gz"
params.FS = osFS
params.ScriptPath = &url.URL{}

ext, err := New(params)

Expand Down
8 changes: 8 additions & 0 deletions dashboard/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ import (
"github.com/sirupsen/logrus"
"go.k6.io/k6/cmd/state"
"go.k6.io/k6/lib/fsext"
"go.k6.io/k6/loader"
"go.k6.io/k6/output"
)

type process struct {
logger logrus.FieldLogger
fs fsext.Fs
env map[string]string
wd string
}

func (proc *process) fromParams(params output.Params) *process {
proc.fs = params.FS
proc.logger = params.Logger
proc.env = params.Environment
proc.wd = loader.Dir(params.ScriptPath).Path

return proc
}
Expand All @@ -29,6 +32,11 @@ func (proc *process) fromGlobalState(gs *state.GlobalState) *process {
proc.fs = gs.FS
proc.logger = gs.Logger
proc.env = gs.Env
var err error
proc.wd, err = gs.Getwd()
if err != nil {
panic(err)
}

return proc
}
9 changes: 6 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@ require (
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/google/pprof v0.0.0-20230728192033-2ba5b33183c6 // indirect
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mstoykov/atlas v0.0.0-20220811071828-388f114305dd // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.33.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
Expand All @@ -44,8 +47,8 @@ require (
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect
Expand Down
Loading

0 comments on commit 973bb7d

Please sign in to comment.