Skip to content

Commit 3160bde

Browse files
committed
Add deps command to return dependancies of a given script/archive
This does include dependancies that might be needed for building a new binary with auto extension resolution, but also all imports. Similar to auto extension resolution, this does take into account only imports not `require` calls. Closes #5166
1 parent b274196 commit 3160bde

File tree

5 files changed

+326
-45
lines changed

5 files changed

+326
-45
lines changed

internal/cmd/deps.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"slices"
8+
9+
"github.com/spf13/cobra"
10+
11+
"go.k6.io/k6/cmd/state"
12+
"go.k6.io/k6/ext"
13+
"go.k6.io/k6/internal/build"
14+
)
15+
16+
func getCmdDeps(gs *state.GlobalState) *cobra.Command {
17+
depsCmd := &cobra.Command{
18+
Use: "deps",
19+
Short: "Resolve dependencies of a test",
20+
Long: `Resolve dependencies of a test including automatic extenstion resolution.` +
21+
`And outputs all dependencies for the test and whether custom build is required.`,
22+
Args: cobra.ExactArgs(1),
23+
RunE: func(cmd *cobra.Command, args []string) error {
24+
test, err := loadLocalTestWithoutRunner(gs, cmd, args)
25+
if err != nil {
26+
var unsatisfiedErr binaryIsNotSatisfyingDependenciesError
27+
if !errors.As(err, &unsatisfiedErr) {
28+
return err
29+
}
30+
}
31+
32+
deps := test.Dependencies()
33+
depsMap := map[string]string{}
34+
for name, constraint := range deps {
35+
if constraint == nil {
36+
depsMap[name] = "*"
37+
continue
38+
}
39+
depsMap[name] = constraint.String()
40+
}
41+
imports := test.Imports()
42+
slices.Sort(imports)
43+
44+
result := struct {
45+
Dependencies map[string]string `json:"dependencies"`
46+
Imports []string `json:"imports"`
47+
CustomBuildRequired bool `json:"customBuildRequired"`
48+
}{
49+
Dependencies: depsMap,
50+
Imports: imports,
51+
CustomBuildRequired: isCustomBuildRequired(deps, build.Version, ext.GetAll()),
52+
}
53+
54+
buf := &bytes.Buffer{}
55+
enc := json.NewEncoder(buf)
56+
enc.SetEscapeHTML(false)
57+
enc.SetIndent("", " ")
58+
if err := enc.Encode(result); err != nil {
59+
return err
60+
}
61+
62+
printToStdout(gs, buf.String())
63+
return nil
64+
},
65+
}
66+
67+
depsCmd.Flags().SortFlags = false
68+
depsCmd.Flags().AddFlagSet(runtimeOptionFlagSet(false))
69+
70+
return depsCmd
71+
}

internal/cmd/deps_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"slices"
6+
"strings"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
11+
"go.k6.io/k6/internal/cmd/tests"
12+
"go.k6.io/k6/internal/lib/testutils"
13+
)
14+
15+
func TestGetCmdDeps(t *testing.T) {
16+
t.Parallel()
17+
testCases := []struct {
18+
name string
19+
files map[string][]byte
20+
expectedDeps map[string]string
21+
expectCustomBuild bool
22+
expectedImports []string
23+
}{
24+
{
25+
name: "single external dependency",
26+
files: map[string][]byte{
27+
"/main.js": []byte(`import http from "k6/http";
28+
import foo from "k6/x/foo";
29+
30+
export default function () {
31+
http.get("https://example.com");
32+
foo();
33+
}
34+
`),
35+
},
36+
expectedDeps: map[string]string{"k6/x/foo": "*"},
37+
expectCustomBuild: true,
38+
expectedImports: []string{"/main.js", "k6/http", "k6/x/foo"},
39+
},
40+
{
41+
name: "no external dependency",
42+
files: map[string][]byte{
43+
"/main.js": []byte(`import http from "k6/http";
44+
45+
export default function () {
46+
http.get("https://example.com");
47+
}
48+
`),
49+
},
50+
expectedDeps: map[string]string{},
51+
expectCustomBuild: false,
52+
expectedImports: []string{"/main.js", "k6/http"},
53+
},
54+
{
55+
name: "nested local imports",
56+
files: map[string][]byte{
57+
"/main.js": []byte(`import helper from "./lib/helper.js";
58+
59+
export default function () {
60+
helper();
61+
}
62+
`),
63+
"/lib/helper.js": []byte(`import nested from "../shared/nested.js";
64+
import ext from "k6/x/bar";
65+
66+
export default function () {
67+
nested();
68+
ext();
69+
}
70+
`),
71+
"/shared/nested.js": []byte(`export default function () {
72+
return "nested";
73+
}
74+
`),
75+
},
76+
expectedDeps: map[string]string{"k6/x/bar": "*"},
77+
expectCustomBuild: true,
78+
expectedImports: []string{"/lib/helper.js", "/main.js", "/shared/nested.js", "k6/x/bar"},
79+
},
80+
{
81+
name: "use directive across files",
82+
files: map[string][]byte{
83+
"/main.js": []byte(`import directive from "./modules/with-directive.js";
84+
85+
export default function () {
86+
directive();
87+
}
88+
`),
89+
"/modules/with-directive.js": []byte(`"use k6 with k6/x/alpha >= 1.2.3";
90+
import beta from "k6/x/beta";
91+
import util from "./util.js";
92+
93+
export default function () {
94+
util();
95+
beta();
96+
}
97+
`),
98+
"/modules/util.js": []byte(`export default function () {
99+
return "util";
100+
}
101+
`),
102+
},
103+
expectedDeps: map[string]string{"k6/x/alpha": ">=1.2.3", "k6/x/beta": "*"},
104+
expectCustomBuild: true,
105+
expectedImports: []string{"/main.js", "/modules/util.js", "/modules/with-directive.js", "k6/x/beta"},
106+
},
107+
}
108+
109+
for _, tc := range testCases {
110+
t.Run(tc.name, func(t *testing.T) {
111+
t.Parallel()
112+
ts := tests.NewGlobalTestState(t)
113+
ts.FS = testutils.MakeMemMapFs(t, tc.files)
114+
115+
cmd := getCmdDeps(ts.GlobalState)
116+
cmd.SetArgs([]string{"/main.js"})
117+
require.NoError(t, cmd.Execute())
118+
119+
var output struct {
120+
Dependencies map[string]string `json:"dependencies"`
121+
Imports []string `json:"imports"`
122+
CustomBuildRequired bool `json:"customBuildRequired"`
123+
}
124+
require.NoError(t, json.Unmarshal(ts.Stdout.Bytes(), &output))
125+
126+
require.Equal(t, tc.expectedDeps, output.Dependencies)
127+
128+
require.Equal(t, tc.expectCustomBuild, output.CustomBuildRequired)
129+
130+
expectedImports := slices.Clone(tc.expectedImports)
131+
for i, expectedImport := range tc.expectedImports {
132+
if !strings.HasPrefix(expectedImport, "k6") {
133+
expectedImports[i] = "file://" + expectedImport
134+
}
135+
}
136+
137+
require.EqualValues(t, expectedImports, output.Imports)
138+
})
139+
}
140+
}

internal/cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func newRootCommand(gs *state.GlobalState) *rootCommand {
7878
rootCmd.SetIn(gs.Stdin)
7979

8080
subCommands := []func(*state.GlobalState) *cobra.Command{
81-
getCmdArchive, getCmdCloud, getCmdNewScript, getCmdInspect,
81+
getCmdArchive, getCmdCloud, getCmdNewScript, getCmdInspect, getCmdDeps,
8282
getCmdLogin, getCmdPause, getCmdResume, getCmdScale, getCmdRun,
8383
getCmdStats, getCmdStatus, getCmdVersion,
8484
}

internal/cmd/runtime_options_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ func testRuntimeOptionsCase(t *testing.T, tc runtimeOptionsTestCase) {
7575
},
7676
}
7777

78-
require.NoError(t, test.initializeFirstRunner(ts.GlobalState))
78+
require.NoError(t, test.prepareFirstRunner(ts.GlobalState))
79+
require.NoError(t, test.continueInitialization(ts.GlobalState))
7980

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

99100
archTest := getRunnerErr(lib.RuntimeOptions{})
100-
require.NoError(t, archTest.initializeFirstRunner(ts.GlobalState))
101+
require.NoError(t, archTest.prepareFirstRunner(ts.GlobalState))
102+
require.NoError(t, archTest.continueInitialization(ts.GlobalState))
101103

102104
for key, val := range tc.expRTOpts.Env {
103105
archTest = getRunnerErr(lib.RuntimeOptions{Env: map[string]string{key: "almost " + val}})
104-
require.NoError(t, archTest.initializeFirstRunner(ts.GlobalState))
106+
require.NoError(t, archTest.prepareFirstRunner(ts.GlobalState))
107+
require.NoError(t, archTest.continueInitialization(ts.GlobalState))
105108
assert.Equal(t, "almost "+val, archTest.initRunner.MakeArchive().Env[key])
106109
}
107110
}

0 commit comments

Comments
 (0)