Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions internal/cmd/test_load.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ func (lt *loadedTest) initializeFirstRunner(gs *state.GlobalState) error {
pwd := lt.source.URL.JoinPath("../")
logger.Debug("Trying to load as a JS test...")
moduleResolver := js.NewModuleResolver(pwd, lt.preInitState, lt.fileSystems)
err := moduleResolver.LoadMainModule(pwd, specifier, lt.source.Data)
err := errext.WithExitCodeIfNone(
moduleResolver.LoadMainModule(pwd, specifier, lt.source.Data),
exitcodes.ScriptException)
if err != nil {
return fmt.Errorf("could not load JS test '%s': %w", testPath, err)
}
Expand Down Expand Up @@ -168,7 +170,9 @@ func (lt *loadedTest) initializeFirstRunner(gs *state.GlobalState) error {
specifier := arc.Filename
pwd := arc.PwdURL
moduleResolver := js.NewModuleResolver(pwd, lt.preInitState, arc.Filesystems)
err := moduleResolver.LoadMainModule(pwd, specifier, arc.Data)
err := errext.WithExitCodeIfNone(
moduleResolver.LoadMainModule(pwd, specifier, arc.Data),
exitcodes.ScriptException)
if err != nil {
return fmt.Errorf("could not load JS test '%s': %w", testPath, err)
}
Expand Down
28 changes: 28 additions & 0 deletions internal/cmd/tests/cmd_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,34 @@ func TestAbortedByScriptSetupErrorWithDependency(t *testing.T) {
assert.Contains(t, stdout, "bogus summary")
}

func TestAbortedByUnknownModules(t *testing.T) {
t.Parallel()
depScript := `
import { something } from "k6/x/somethinghere"
import { another } from "k6/x/anotherone"
`
mainScript := `
import { something } from "k6/x/somethinghere"
import "./a.js"
export default function () { }
`

ts := NewGlobalTestState(t)
ts.Flags.AutoExtensionResolution = false
require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "test.js"), []byte(mainScript), 0o644))
require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "a.js"), []byte(depScript), 0o644))

ts.CmdArgs = []string{"k6", "run", "-v", "--log-output=stdout", "test.js"}
ts.ExpectedExitCode = int(exitcodes.ScriptException)

cmd.ExecuteWithGlobalState(ts.GlobalState)

stdout := ts.Stdout.String()
t.Log(stdout)

assert.Contains(t, stdout, `unknown modules [\"k6/x/anotherone\", \"k6/x/somethinghere\"] were tried to be loaded,`)
}

func runTestWithNoLinger(_ *testing.T, ts *GlobalTestState) {
cmd.ExecuteWithGlobalState(ts.GlobalState)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/js/initcontext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestRequire(t *testing.T) {
t.Run("Nonexistent", func(t *testing.T) {
t.Parallel()
_, err := getSimpleBundle(t, "/script.js", `import "k6/NONEXISTENT";`)
require.ErrorContains(t, err, "GoError: unknown module: k6/NONEXISTENT")
require.ErrorContains(t, err, "GoError: unknown modules [\"k6/NONEXISTENT\"]")
})

t.Run("k6", func(t *testing.T) {
Expand Down
64 changes: 51 additions & 13 deletions js/modules/resolution.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package modules
import (
"fmt"
"net/url"
"slices"
"strconv"
"strings"

"github.com/grafana/sobek"
Expand All @@ -28,15 +30,16 @@ type moduleCacheElement struct {

// ModuleResolver knows how to get base Module that can be initialized
type ModuleResolver struct {
cache map[string]moduleCacheElement
goModules map[string]any
loadCJS FileLoader
compiler *compiler.Compiler
locked bool
reverse map[any]*url.URL // maybe use sobek.ModuleRecord as key
base *url.URL
usage *usage.Usage
logger logrus.FieldLogger
cache map[string]moduleCacheElement
goModules map[string]any
loadCJS FileLoader
compiler *compiler.Compiler
locked bool
reverse map[any]*url.URL // maybe use sobek.ModuleRecord as key
base *url.URL
usage *usage.Usage
logger logrus.FieldLogger
unknownModules []string
}

// NewModuleResolver returns a new module resolution instance that will resolve.
Expand Down Expand Up @@ -66,13 +69,14 @@ func (mr *ModuleResolver) resolveSpecifier(basePWD *url.URL, arg string) (*url.U
return specifier, nil
}

func (mr *ModuleResolver) requireModule(name string) (sobek.ModuleRecord, error) {
func (mr *ModuleResolver) initializeGoModule(name string) (sobek.ModuleRecord, error) {
if mr.locked {
return nil, fmt.Errorf(notPreviouslyResolvedModule, name)
}
mod, ok := mr.goModules[name]
if !ok {
return nil, fmt.Errorf("unknown module: %s", name)
mr.unknownModules = append(mr.unknownModules, name)
return &unknownModule{name: name, requested: make(map[string]struct{})}, nil
}
// we don't want to report extensions and we would have hit cache if this isn't the first time
if !strings.HasPrefix(name, "k6/x/") {
Expand Down Expand Up @@ -150,7 +154,7 @@ func (mr *ModuleResolver) resolve(basePWD *url.URL, arg string) (sobek.ModuleRec
if cached, ok := mr.cache[arg]; ok {
return cached.mod, cached.err
}
mod, err := mr.requireModule(arg)
mod, err := mr.initializeGoModule(arg)
mr.cache[arg] = moduleCacheElement{mod: mod, err: err}
return mod, err
default:
Expand Down Expand Up @@ -229,6 +233,9 @@ func (mr *ModuleResolver) LoadMainModule(pwd *url.URL, specifier string, data []
panic("somehow running source data for " + specifier + " didn't produce a cyclic module record")
}

if len(mr.unknownModules) > 0 {
return newUnknownModulesError(mr.unknownModules)
}
return nil
}

Expand Down Expand Up @@ -271,8 +278,9 @@ func (ms *ModuleSystem) RunSourceData(source *loader.SourceData) (*RunSourceData
}
ci, ok := mod.(sobek.CyclicModuleRecord)
if !ok {
panic("somehow running source data for " + source.URL.String() + " didn't produce a cyclide module record")
panic("somehow running source data for " + source.URL.String() + " didn't produce a cyclic module record")
}

rt := ms.vu.Runtime()
promise := rt.CyclicModuleRecordEvaluate(ci, ms.resolver.sobekModuleResolver)

Expand Down Expand Up @@ -328,3 +336,33 @@ func ExportGloballyModule(rt *sobek.Runtime, modSys *ModuleSystem, moduleName st
}
}
}

// UnknownModulesError is returned when loading a module was not possbile due to one or more of dependencies
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// UnknownModulesError is returned when loading a module was not possbile due to one or more of dependencies
// UnknownModulesError is returned when loading a module was not possible due to one or more of dependencies

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit; ofc non-blocking.

// that couldn't be resolved.
type UnknownModulesError struct {
unknownModules []string
}

func newUnknownModulesError(list []string) UnknownModulesError {
slices.Sort(list)
return UnknownModulesError{unknownModules: list}
}

func (u UnknownModulesError) Error() string {
return fmt.Sprintf("unknown modules [%s] were tried to be loaded, but couldn't - "+
Copy link
Contributor

@codebien codebien Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is still a bit of improvement to be done to have a better DX. A revisited version of original Ankur's proposal.

Suggested change
return fmt.Sprintf("unknown modules [%s] were tried to be loaded, but couldn't - "+
return fmt.Sprintf("unknown module(s): [%s] k6 couldn't resolve the them because they are not built-in k6 modules. If these are extension modules, enable automatic extension resolution or build a custom binary with xk6. See the docs for more details: https://grafana.com/docs/k6/latest/extensions/explore/#use-extensions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we are going overboard on this.

There are more changes to come, and this error is either never going to be seen. Or it will be seen before it either loads a new binary or returns a next error about extension resolutions.

I would prefer all the DX/UX work to be done after we move to the new implementation, which already fix bugs that are reported and people have problems with. I do not think that if someone has disabled auto extension resolution, they would not know about extensions.

"this likely means automatic extension resolution is required or a custom k6 binary with the required extensions",
u.formatList())
}

// List returns the list of unknown modules that lead to the error
func (u UnknownModulesError) List() []string {
return slices.Clone(u.unknownModules)
}

func (u UnknownModulesError) formatList() string {
list := make([]string, len(u.unknownModules))
for i, m := range u.unknownModules {
list[i] = strconv.Quote(m)
}
return strings.Join(list, ", ")
}
47 changes: 47 additions & 0 deletions js/modules/unknown.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package modules

import "github.com/grafana/sobek"

type unknownModule struct {
name string
requested map[string]struct{}
}

func (um *unknownModule) Link() error { return nil }

func (um *unknownModule) Evaluate(_ *sobek.Runtime) *sobek.Promise { panic("this shouldn't be called") }

func (um *unknownModule) InitializeEnvironment() error { return nil }

func (um *unknownModule) Instantiate(_ *sobek.Runtime) (sobek.CyclicModuleInstance, error) {
return &unknownModuleInstance{module: um}, nil
}

func (um *unknownModule) RequestedModules() []string { return nil }

func (um *unknownModule) ResolveExport(name string, _ ...sobek.ResolveSetElement) (*sobek.ResolvedBinding, bool) {
um.requested[name] = struct{}{}
return &sobek.ResolvedBinding{
Module: um,
BindingName: name,
}, false
}

func (um *unknownModule) GetExportedNames(_ func([]string), _ ...sobek.ModuleRecord) bool {
return false
}

type unknownModuleInstance struct {
module *unknownModule
}

func (umi *unknownModuleInstance) GetBindingValue(_ string) sobek.Value {
return nil
}

func (umi *unknownModuleInstance) HasTLA() bool { return false }

func (umi *unknownModuleInstance) ExecuteModule(_ *sobek.Runtime, _, _ func(any) error,
) (sobek.CyclicModuleInstance, error) {
return umi, nil
}
Loading