Skip to content
15 changes: 7 additions & 8 deletions internal/cmd/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,6 @@ func (c *cmdCloud) preRun(cmd *cobra.Command, _ []string) error {
//
//nolint:funlen,gocognit,cyclop
func (c *cmdCloud) run(cmd *cobra.Command, args []string) error {
printBanner(c.gs)

progressBar := pb.New(
pb.WithConstLeft("Init"),
pb.WithConstProgress(0, "Loading test script..."),
)
printBar(c.gs, progressBar)

test, err := loadAndConfigureLocalTest(c.gs, cmd, args, getPartialConfig)
if err != nil {
return err
Expand All @@ -111,6 +103,13 @@ func (c *cmdCloud) run(cmd *cobra.Command, args []string) error {
// TODO: validate for usage of execution segment
// TODO: validate for externally controlled executor (i.e. executors that aren't distributable)
// TODO: move those validations to a separate function and reuse validateConfig()?
printBanner(c.gs)

progressBar := pb.New(
pb.WithConstLeft("Init"),
pb.WithConstProgress(0, "Loading test script..."),
)
printBar(c.gs, progressBar)

modifyAndPrintBar(c.gs, progressBar, pb.WithConstProgress(0, "Building the archive..."))
arc := testRunState.Runner.MakeArchive()
Expand Down
68 changes: 67 additions & 1 deletion internal/cmd/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/grafana/k6deps"
"github.com/grafana/k6provider"
"github.com/grafana/sobek"
"github.com/grafana/sobek/ast"
"github.com/grafana/sobek/parser"
"github.com/spf13/cobra"

"go.k6.io/k6/cloudapi"
Expand Down Expand Up @@ -213,7 +216,6 @@ func (b *customBinary) run(gs *state.GlobalState) error {
// isCustomBuildRequired checks if there is at least one dependency that are not satisfied by the binary
// considering the version of k6 and any built-in extension
func isCustomBuildRequired(deps map[string]*semver.Constraints, k6Version string, exts []*ext.Extension) bool {
// return early if there are no dependencies
if len(deps) == 0 {
return false
}
Expand Down Expand Up @@ -391,3 +393,67 @@ func extractToken(gs *state.GlobalState) (string, error) {

return config.Token.String, nil
}

func processUseDirectives(name string, text []byte) (map[string]string, error) {
deps := make(map[string]string)
// TODO In theory this will be super easy to parse even with comments and special cases such as #! at the beginning
// of the file, but using sobek.Parse is likely *a lot* more sure to work correctly.
m, err := sobek.Parse(name, string(text), parser.IsModule, parser.WithDisableSourceMaps)
if err != nil {
return nil, err
}
updateDep := func(dep, constraint string) error {
// TODO: We could actually do constraint comparison here and get the more specific one
oldConstraint, ok := deps[dep]
if !ok || oldConstraint == "" { // either nothing or it didn't have constraint
deps[dep] = constraint
return nil
}
if constraint == oldConstraint || constraint == "" {
return nil
}
return fmt.Errorf("already have constraint for %q, when parsing %q in %q", dep, constraint, name)
}

directives := findDirectives(m.Body)
for _, directive := range directives {
// normalize spaces
directive = strings.ReplaceAll(directive, " ", " ")
if !strings.HasPrefix(directive, "use k6") {
continue
}
directive = strings.TrimSpace(strings.TrimPrefix(directive, "use k6"))
if !strings.HasPrefix(directive, "with k6/x/") {
err := updateDep("k6", directive)
if err != nil {
return nil, err
}
continue
}
directive = strings.TrimSpace(strings.TrimPrefix(directive, "with "))
dep, constraint, _ := strings.Cut(directive, " ")
err := updateDep(dep, constraint)
if err != nil {
return nil, err
}
}

return deps, nil
}

// TODO(@mstoykov): in a future RP make this more generic
func findDirectives(list []ast.Statement) []string {
var result []string
for _, st := range list {
if st, ok := st.(*ast.ExpressionStatement); ok {
if e, ok := st.Expression.(*ast.StringLiteral); ok {
result = append(result, e.Value.String())
} else {
break
}
} else {
break
}
}
return result
}
106 changes: 106 additions & 0 deletions internal/cmd/launcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,3 +559,109 @@ func TestGetProviderConfig(t *testing.T) {
})
}
}

func TestProcessUseDirectives(t *testing.T) {
t.Parallel()
tests := map[string]struct {
input string
expectedOutput map[string]string
expectedError string
}{
"nothing": {
input: "export default function() {}",
expectedOutput: map[string]string{},
},
"nothing really": {
input: `"use k6"`,
expectedOutput: map[string]string{
"k6": "",
},
},
"k6 pinning": {
input: `"use k6 > 1.4.0"`,
expectedOutput: map[string]string{
"k6": "> 1.4.0",
},
},
"a extension": {
input: `"use k6 with k6/x/sql"`,
expectedOutput: map[string]string{
"k6/x/sql": "",
},
},
"an extension with constraint": {
input: `"use k6 with k6/x/sql > 1.4.0"`,
expectedOutput: map[string]string{
"k6/x/sql": "> 1.4.0",
},
},
"complex": {
input: `
// something here
"use k6 with k6/x/A"
function a (){
"use k6 with k6/x/B"
let s = JSON.stringify( "use k6 with k6/x/C")
"use k6 with k6/x/D"

return s
}

export const b = "use k6 with k6/x/E"
"use k6 with k6/x/F"

// Here for esbuild and k6 warnings
a()
export default function(){}
`,
expectedOutput: map[string]string{
"k6/x/A": "",
},
},

"repeat": {
input: `
"use k6 with k6/x/A"
"use k6 with k6/x/A"
`,
expectedOutput: map[string]string{
"k6/x/A": "",
},
},
"repeat with constraint first": {
input: `
"use k6 with k6/x/A > 1.4.0"
"use k6 with k6/x/A"
`,
expectedOutput: map[string]string{
"k6/x/A": "> 1.4.0",
},
},
"constraint difference": {
input: `
"use k6 > 1.4.0"
"use k6 = 1.2.3"
`,
expectedError: `already have constraint for "k6", when parsing "= 1.2.3" in "name.js"`,
},
"constraint difference for extensions": {
input: `
"use k6 with k6/x/A > 1.4.0"
"use k6 with k6/x/A = 1.2.3"
`,
expectedError: `already have constraint for "k6/x/A", when parsing "= 1.2.3" in "name.js"`,
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()

m, err := processUseDirectives("name.js", []byte(test.input))
assert.EqualValues(t, test.expectedOutput, m)
if len(test.expectedError) > 0 {
assert.ErrorContains(t, err, test.expectedError)
}
})
}
}
31 changes: 29 additions & 2 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ type rootCommand struct {

// newRootCommand creates a root command with a default launcher
func newRootCommand(gs *state.GlobalState) *rootCommand {
return newRootWithLauncher(gs, newLauncher(gs))
if gs.Env["K6_OLD_RESOLUTION"] == "true" {
return newRootWithLauncher(gs, newLauncher(gs))
}
return newRootWithLauncher(gs, nil)
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see the reason for passing a nil launcher here. I would expect to pass it and not used it if not needed. Maybe some explanation would help.

}

// newRootWithLauncher creates a root command with a launcher.
Expand Down Expand Up @@ -108,7 +111,7 @@ func (c *rootCommand) persistentPreRunE(cmd *cobra.Command, args []string) error
c.globalState.Logger.Debugf("k6 version: v%s", fullVersion())

// If automatic extension resolution is not enabled, continue with the regular k6 execution path
if !c.globalState.Flags.AutoExtensionResolution {
if !c.globalState.Flags.AutoExtensionResolution || c.launcher == nil {
c.globalState.Logger.Debug("Automatic extension resolution is disabled.")
return nil
}
Expand Down Expand Up @@ -152,6 +155,30 @@ func (c *rootCommand) execute() {
exitCode = int(ecerr.ExitCode())
}

var differentBinaryError binaryIsNotSatisfyingDependenciesError

if errors.As(err, &differentBinaryError) {
deps := differentBinaryError.deps
c.globalState.Logger.
WithField("deps", deps).
Info("Automatic extension resolution is enabled. The current k6 binary doesn't satisfy all dependencies," +
" it's required to provision a custom binary.")
provisioner := newK6BuildProvisioner(c.globalState)
var customBinary commandExecutor
customBinary, err = provisioner.provision(constraintsMapToProvisionDependency(deps))
if err != nil {
c.globalState.Logger.
WithError(err).
Error("Failed to provision a k6 binary with required dependencies." +
" Please, make sure to report this issue by opening a bug report.")
} else {
err = customBinary.run(c.globalState)
if err == nil {
return
}
}
}

errText, fields := errext.Format(err)
c.globalState.Logger.WithFields(fields).Error(errText)
if c.loggerIsRemote {
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ func (c *cmdRun) run(cmd *cobra.Command, args []string) (err error) {
logger.WithError(err).Debug("Everything has finished, exiting k6 with an error!")
}
}()
printBanner(c.gs)

globalCtx, globalCancel := context.WithCancel(c.gs.Ctx)
defer globalCancel()
Expand Down Expand Up @@ -106,6 +105,7 @@ func (c *cmdRun) run(cmd *cobra.Command, args []string) (err error) {
if err != nil {
return err
}
printBanner(c.gs)
if test.keyLogger != nil {
defer func() {
if klErr := test.keyLogger.Close(); klErr != nil {
Expand Down
Loading
Loading