Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
71 changes: 71 additions & 0 deletions internal/cmd/deps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package cmd

import (
"bytes"
"encoding/json"
"errors"
"slices"

"github.com/spf13/cobra"

"go.k6.io/k6/cmd/state"
"go.k6.io/k6/ext"
"go.k6.io/k6/internal/build"
)

func getCmdDeps(gs *state.GlobalState) *cobra.Command {
depsCmd := &cobra.Command{
Use: "deps",
Short: "Resolve dependencies of a test",
Long: `Resolve dependencies of a test including automatic extenstion resolution.` +
`And outputs all dependencies for the test and whether a custom build is required.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
test, err := loadLocalTestWithoutRunner(gs, cmd, args)
if err != nil {
var unsatisfiedErr binaryIsNotSatisfyingDependenciesError
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the expected behaviour when the error is a binaryIsNotSatisfyingDependenciesError? Looks like we're just ignoring it. Maybe we should log a debug log if we can ignore it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here the idea is that if this is the error - this means to run this we need a new binary with som extesnions.

But as we only care about the dependancies and that is already what we've got, here we just check that if this isn't the error, as in that case we need to return it.

if !errors.As(err, &unsatisfiedErr) {
return err
}
}

deps := test.Dependencies()
depsMap := map[string]string{}
for name, constraint := range deps {
if constraint == nil {
depsMap[name] = "*"
continue
}
depsMap[name] = constraint.String()
}
imports := test.Imports()
slices.Sort(imports)

result := struct {
BuildDependancies map[string]string `json:"buildDependancies"`
Imports []string `json:"imports"`
CustomBuildRequired bool `json:"customBuildRequired"`
}{
BuildDependancies: depsMap,
Imports: imports,
CustomBuildRequired: isCustomBuildRequired(deps, build.Version, ext.GetAll()),
}

buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if err := enc.Encode(result); err != nil {
return err
}

printToStdout(gs, buf.String())
return nil
},
}

depsCmd.Flags().SortFlags = false
depsCmd.Flags().AddFlagSet(runtimeOptionFlagSet(false))

return depsCmd
}
140 changes: 140 additions & 0 deletions internal/cmd/deps_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package cmd

import (
"encoding/json"
"slices"
"strings"
"testing"

"github.com/stretchr/testify/require"

"go.k6.io/k6/internal/cmd/tests"
"go.k6.io/k6/internal/lib/testutils"
)

func TestGetCmdDeps(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
files map[string][]byte
expectedDeps map[string]string
expectCustomBuild bool
expectedImports []string
}{
{
name: "single external dependency",
files: map[string][]byte{
"/main.js": []byte(`import http from "k6/http";
import foo from "k6/x/foo";

export default function () {
http.get("https://example.com");
foo();
}
`),
},
expectedDeps: map[string]string{"k6/x/foo": "*"},
expectCustomBuild: true,
expectedImports: []string{"/main.js", "k6/http", "k6/x/foo"},
},
{
name: "no external dependency",
files: map[string][]byte{
"/main.js": []byte(`import http from "k6/http";

export default function () {
http.get("https://example.com");
}
`),
},
expectedDeps: map[string]string{},
expectCustomBuild: false,
expectedImports: []string{"/main.js", "k6/http"},
},
{
name: "nested local imports",
files: map[string][]byte{
"/main.js": []byte(`import helper from "./lib/helper.js";

export default function () {
helper();
}
`),
"/lib/helper.js": []byte(`import nested from "../shared/nested.js";
import ext from "k6/x/bar";

export default function () {
nested();
ext();
}
`),
"/shared/nested.js": []byte(`export default function () {
return "nested";
}
`),
},
expectedDeps: map[string]string{"k6/x/bar": "*"},
expectCustomBuild: true,
expectedImports: []string{"/lib/helper.js", "/main.js", "/shared/nested.js", "k6/x/bar"},
},
{
name: "use directive across files",
files: map[string][]byte{
"/main.js": []byte(`import directive from "./modules/with-directive.js";

export default function () {
directive();
}
`),
"/modules/with-directive.js": []byte(`"use k6 with k6/x/alpha >= 1.2.3";
import beta from "k6/x/beta";
import util from "./util.js";

export default function () {
util();
beta();
}
`),
"/modules/util.js": []byte(`export default function () {
return "util";
}
`),
},
expectedDeps: map[string]string{"k6/x/alpha": ">=1.2.3", "k6/x/beta": "*"},
expectCustomBuild: true,
expectedImports: []string{"/main.js", "/modules/util.js", "/modules/with-directive.js", "k6/x/beta"},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ts := tests.NewGlobalTestState(t)
ts.FS = testutils.MakeMemMapFs(t, tc.files)

cmd := getCmdDeps(ts.GlobalState)
cmd.SetArgs([]string{"/main.js"})
require.NoError(t, cmd.Execute())

var output struct {
BuildDependancies map[string]string `json:"buildDependancies"`
Imports []string `json:"imports"`
CustomBuildRequired bool `json:"customBuildRequired"`
}
require.NoError(t, json.Unmarshal(ts.Stdout.Bytes(), &output))

require.Equal(t, tc.expectedDeps, output.BuildDependancies)

require.Equal(t, tc.expectCustomBuild, output.CustomBuildRequired)

expectedImports := slices.Clone(tc.expectedImports)
for i, expectedImport := range tc.expectedImports {
if !strings.HasPrefix(expectedImport, "k6") {
expectedImports[i] = "file://" + expectedImport
}
}

require.EqualValues(t, expectedImports, output.Imports)
})
}
}
2 changes: 1 addition & 1 deletion internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func newRootCommand(gs *state.GlobalState) *rootCommand {
rootCmd.SetIn(gs.Stdin)

subCommands := []func(*state.GlobalState) *cobra.Command{
getCmdArchive, getCmdCloud, getCmdNewScript, getCmdInspect,
getCmdArchive, getCmdCloud, getCmdNewScript, getCmdInspect, getCmdDeps,
getCmdLogin, getCmdPause, getCmdResume, getCmdScale, getCmdRun,
getCmdStats, getCmdStatus, getCmdVersion,
}
Expand Down
9 changes: 6 additions & 3 deletions internal/cmd/runtime_options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ func testRuntimeOptionsCase(t *testing.T, tc runtimeOptionsTestCase) {
},
}

require.NoError(t, test.initializeFirstRunner(ts.GlobalState))
require.NoError(t, test.prepareFirstRunner(ts.GlobalState))
require.NoError(t, test.continueInitialization(ts.GlobalState))

archive := test.initRunner.MakeArchive()
archiveBuf := &bytes.Buffer{}
Expand All @@ -97,11 +98,13 @@ func testRuntimeOptionsCase(t *testing.T, tc runtimeOptionsTestCase) {
}

archTest := getRunnerErr(lib.RuntimeOptions{})
require.NoError(t, archTest.initializeFirstRunner(ts.GlobalState))
require.NoError(t, archTest.prepareFirstRunner(ts.GlobalState))
require.NoError(t, archTest.continueInitialization(ts.GlobalState))

for key, val := range tc.expRTOpts.Env {
archTest = getRunnerErr(lib.RuntimeOptions{Env: map[string]string{key: "almost " + val}})
require.NoError(t, archTest.initializeFirstRunner(ts.GlobalState))
require.NoError(t, archTest.prepareFirstRunner(ts.GlobalState))
require.NoError(t, archTest.continueInitialization(ts.GlobalState))
assert.Equal(t, "almost "+val, archTest.initRunner.MakeArchive().Env[key])
}
}
Expand Down
Loading
Loading