Skip to content

Commit ede8382

Browse files
szkibaAgnesToulet
andauthored
Fix version command JSON output for output extensions (#5381)
The names of k6 output extensions in the JSON output of the version command were incorrectly placed in the imports property. This commit fixes the output so that the names of k6 output extensions are now correctly placed in the outputs property of their respective extension object. Co-authored-by: Agnès Toulet <[email protected]>
1 parent 8973542 commit ede8382

File tree

2 files changed

+328
-31
lines changed

2 files changed

+328
-31
lines changed

internal/cmd/version.go

Lines changed: 56 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -108,50 +108,75 @@ func versionString() string {
108108
return v
109109
}
110110

111-
type versionCmd struct {
112-
gs *state.GlobalState
113-
isJSON bool
114-
}
111+
// versionDetailsWithExtensions returns the structured details about version including extensions
112+
// returns error if there are unhandled extension types
113+
func versionDetailsWithExtensions(exts []*ext.Extension) (map[string]any, error) {
114+
details := versionDetails()
115115

116-
func (c *versionCmd) run(cmd *cobra.Command, _ []string) error {
117-
if !c.isJSON {
118-
root := cmd.Root()
119-
root.SetArgs([]string{"--version"})
120-
_ = root.Execute()
121-
return nil
116+
if len(exts) == 0 {
117+
return details, nil
122118
}
123119

124-
details := versionDetails()
125-
if exts := ext.GetAll(); len(exts) > 0 {
126-
type extInfo struct {
127-
Module string `json:"module"`
128-
Version string `json:"version"`
129-
Imports []string `json:"imports"`
130-
}
120+
// extInfo represents the JSON structure for an extension in the version details
121+
// modeled after k6 extension registry structure
122+
type extInfo struct {
123+
Module string `json:"module"`
124+
Version string `json:"version"`
125+
Imports []string `json:"imports,omitempty"`
126+
Outputs []string `json:"outputs,omitempty"`
127+
}
131128

132-
ext := make(map[string]extInfo)
133-
for _, e := range exts {
134-
key := e.Path + "@" + e.Version
129+
infoList := make([]*extInfo, 0, len(exts))
130+
infoMap := make(map[string]*extInfo)
135131

136-
if v, ok := ext[key]; ok {
137-
v.Imports = append(v.Imports, e.Name)
138-
ext[key] = v
139-
continue
140-
}
132+
for _, e := range exts {
133+
key := e.Path + "@" + e.Version
141134

142-
ext[key] = extInfo{
135+
info, found := infoMap[key]
136+
if !found {
137+
info = &extInfo{
143138
Module: e.Path,
144139
Version: e.Version,
145-
Imports: []string{e.Name},
146140
}
141+
142+
infoMap[key] = info
143+
infoList = append(infoList, info)
147144
}
148145

149-
list := make([]extInfo, 0, len(ext))
150-
for _, v := range ext {
151-
list = append(list, v)
146+
switch e.Type {
147+
case ext.OutputExtension:
148+
info.Outputs = append(info.Outputs, e.Name)
149+
case ext.JSExtension:
150+
info.Imports = append(info.Imports, e.Name)
151+
case ext.SecretSourceExtension:
152+
// currently, no special handling is needed for secret source extensions
153+
default:
154+
// report unhandled extension type for future proofing
155+
return details, fmt.Errorf("unhandled extension type: %s", e.Type)
152156
}
157+
}
158+
159+
details["extensions"] = infoList
160+
161+
return details, nil
162+
}
163+
164+
type versionCmd struct {
165+
gs *state.GlobalState
166+
isJSON bool
167+
}
168+
169+
func (c *versionCmd) run(cmd *cobra.Command, _ []string) error {
170+
if !c.isJSON {
171+
root := cmd.Root()
172+
root.SetArgs([]string{"--version"})
173+
_ = root.Execute()
174+
return nil
175+
}
153176

154-
details["extensions"] = list
177+
details, err := versionDetailsWithExtensions(ext.GetAll())
178+
if err != nil {
179+
return fmt.Errorf("failed to get version details with extensions: %w", err)
155180
}
156181

157182
if err := json.NewEncoder(c.gs.Stdout).Encode(details); err != nil {

internal/cmd/version_test.go

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import (
66
"testing"
77

88
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
910

11+
"go.k6.io/k6/ext"
1012
"go.k6.io/k6/internal/build"
1113
"go.k6.io/k6/internal/cmd/tests"
1214
)
@@ -82,3 +84,273 @@ func TestVersionJSONSubCommand(t *testing.T) {
8284
assert.Equal(t, runtime.GOARCH, details["go_arch"])
8385
assert.Equal(t, "devel", details[commitKey])
8486
}
87+
88+
func TestVersionDetailsWithExtensions(t *testing.T) {
89+
t.Parallel()
90+
91+
tests := []struct {
92+
name string
93+
exts []*ext.Extension
94+
expected func(t *testing.T, details map[string]any)
95+
expectError bool
96+
}{
97+
{
98+
name: "no extensions",
99+
exts: []*ext.Extension{},
100+
expected: func(t *testing.T, details map[string]any) {
101+
require.NotContains(t, details, "extensions")
102+
require.Contains(t, details, "version")
103+
require.Contains(t, details, "go_version")
104+
require.Contains(t, details, "go_os")
105+
require.Contains(t, details, "go_arch")
106+
},
107+
},
108+
{
109+
name: "single js extension",
110+
exts: []*ext.Extension{
111+
{
112+
Name: "k6/x/test",
113+
Path: "github.com/grafana/xk6-test",
114+
Version: "v0.1.0",
115+
Type: ext.JSExtension,
116+
},
117+
},
118+
expected: func(t *testing.T, details map[string]any) {
119+
require.Contains(t, details, "extensions")
120+
extListRaw, ok := details["extensions"].([]any)
121+
require.True(t, ok, "extensions should be a slice")
122+
require.Len(t, extListRaw, 1)
123+
124+
extMap := extListRaw[0].(map[string]any)
125+
require.Equal(t, "github.com/grafana/xk6-test", extMap["module"])
126+
require.Equal(t, "v0.1.0", extMap["version"])
127+
require.Equal(t, []any{"k6/x/test"}, extMap["imports"])
128+
require.Nil(t, extMap["outputs"])
129+
},
130+
},
131+
{
132+
name: "single output extension",
133+
exts: []*ext.Extension{
134+
{
135+
Name: "prometheus",
136+
Path: "github.com/grafana/xk6-output-prometheus",
137+
Version: "v0.2.0",
138+
Type: ext.OutputExtension,
139+
},
140+
},
141+
expected: func(t *testing.T, details map[string]any) {
142+
require.Contains(t, details, "extensions")
143+
extListRaw, ok := details["extensions"].([]any)
144+
require.True(t, ok, "extensions should be a slice")
145+
require.Len(t, extListRaw, 1)
146+
147+
extMap := extListRaw[0].(map[string]any)
148+
require.Equal(t, "github.com/grafana/xk6-output-prometheus", extMap["module"])
149+
require.Equal(t, "v0.2.0", extMap["version"])
150+
require.Equal(t, []any{"prometheus"}, extMap["outputs"])
151+
require.Nil(t, extMap["imports"])
152+
},
153+
},
154+
{
155+
name: "multiple extensions from same module",
156+
exts: []*ext.Extension{
157+
{
158+
Name: "k6/x/sql",
159+
Path: "github.com/grafana/xk6-sql",
160+
Version: "v0.3.0",
161+
Type: ext.JSExtension,
162+
},
163+
{
164+
Name: "k6/x/sql/driver/mysql",
165+
Path: "github.com/grafana/xk6-sql",
166+
Version: "v0.3.0",
167+
Type: ext.JSExtension,
168+
},
169+
},
170+
expected: func(t *testing.T, details map[string]any) {
171+
require.Contains(t, details, "extensions")
172+
extListRaw, ok := details["extensions"].([]any)
173+
require.True(t, ok, "extensions should be a slice")
174+
require.Len(t, extListRaw, 1, "should consolidate extensions from same module@version")
175+
176+
extMap := extListRaw[0].(map[string]any)
177+
require.Equal(t, "github.com/grafana/xk6-sql", extMap["module"])
178+
require.Equal(t, "v0.3.0", extMap["version"])
179+
imports := extMap["imports"].([]any)
180+
require.Len(t, imports, 2)
181+
require.Contains(t, imports, "k6/x/sql")
182+
require.Contains(t, imports, "k6/x/sql/driver/mysql")
183+
},
184+
},
185+
{
186+
name: "mixed extension types from same module",
187+
exts: []*ext.Extension{
188+
{
189+
Name: "k6/x/dashboard",
190+
Path: "github.com/grafana/xk6-dashboard",
191+
Version: "v0.4.0",
192+
Type: ext.JSExtension,
193+
},
194+
{
195+
Name: "dashboard",
196+
Path: "github.com/grafana/xk6-dashboard",
197+
Version: "v0.4.0",
198+
Type: ext.OutputExtension,
199+
},
200+
},
201+
expected: func(t *testing.T, details map[string]any) {
202+
require.Contains(t, details, "extensions")
203+
extListRaw, ok := details["extensions"].([]any)
204+
require.True(t, ok, "extensions should be a slice")
205+
require.Len(t, extListRaw, 1, "should consolidate extensions from same module@version")
206+
207+
extMap := extListRaw[0].(map[string]any)
208+
require.Equal(t, "github.com/grafana/xk6-dashboard", extMap["module"])
209+
require.Equal(t, "v0.4.0", extMap["version"])
210+
require.Equal(t, []any{"k6/x/dashboard"}, extMap["imports"])
211+
require.Equal(t, []any{"dashboard"}, extMap["outputs"])
212+
},
213+
},
214+
{
215+
name: "multiple different extensions",
216+
exts: []*ext.Extension{
217+
{
218+
Name: "k6/x/test1",
219+
Path: "github.com/example/xk6-test1",
220+
Version: "v1.0.0",
221+
Type: ext.JSExtension,
222+
},
223+
{
224+
Name: "output1",
225+
Path: "github.com/example/xk6-output1",
226+
Version: "v2.0.0",
227+
Type: ext.OutputExtension,
228+
},
229+
{
230+
Name: "k6/x/test2",
231+
Path: "github.com/example/xk6-test2",
232+
Version: "v3.0.0",
233+
Type: ext.JSExtension,
234+
},
235+
},
236+
expected: func(t *testing.T, details map[string]any) {
237+
require.Contains(t, details, "extensions")
238+
extListRaw, ok := details["extensions"].([]any)
239+
require.True(t, ok, "extensions should be a slice")
240+
require.Len(t, extListRaw, 3)
241+
242+
// Verify all extensions are present
243+
modules := make(map[string]bool)
244+
for _, extRaw := range extListRaw {
245+
extMap := extRaw.(map[string]any)
246+
modules[extMap["module"].(string)] = true
247+
}
248+
require.True(t, modules["github.com/example/xk6-test1"])
249+
require.True(t, modules["github.com/example/xk6-output1"])
250+
require.True(t, modules["github.com/example/xk6-test2"])
251+
},
252+
},
253+
{
254+
name: "same module different versions",
255+
exts: []*ext.Extension{
256+
{
257+
Name: "k6/x/test",
258+
Path: "github.com/example/xk6-test",
259+
Version: "v1.0.0",
260+
Type: ext.JSExtension,
261+
},
262+
{
263+
Name: "k6/x/test/v2",
264+
Path: "github.com/example/xk6-test",
265+
Version: "v2.0.0",
266+
Type: ext.JSExtension,
267+
},
268+
},
269+
expected: func(t *testing.T, details map[string]any) {
270+
require.Contains(t, details, "extensions")
271+
extListRaw, ok := details["extensions"].([]any)
272+
require.True(t, ok, "extensions should be a slice")
273+
require.Len(t, extListRaw, 2, "different versions should create separate entries")
274+
275+
// Verify both versions are present
276+
versions := make(map[string]bool)
277+
for _, extRaw := range extListRaw {
278+
extMap := extRaw.(map[string]any)
279+
require.Equal(t, "github.com/example/xk6-test", extMap["module"])
280+
versions[extMap["version"].(string)] = true
281+
}
282+
require.True(t, versions["v1.0.0"])
283+
require.True(t, versions["v2.0.0"])
284+
},
285+
},
286+
{
287+
name: "unhandled extension type",
288+
exts: []*ext.Extension{
289+
{
290+
Name: "unknown",
291+
Path: "github.com/example/xk6-unknown",
292+
Version: "v1.0.0",
293+
Type: 100, // Unknown/unhandled type
294+
},
295+
},
296+
expected: func(_ *testing.T, _ map[string]any) {
297+
// Should not be called when expectError is true
298+
},
299+
expectError: true,
300+
},
301+
{
302+
name: "mixed handled and unhandled extensions",
303+
exts: []*ext.Extension{
304+
{
305+
Name: "k6/x/test",
306+
Path: "github.com/example/xk6-test",
307+
Version: "v1.0.0",
308+
Type: ext.JSExtension,
309+
},
310+
{
311+
Name: "unknown",
312+
Path: "github.com/example/xk6-unknown",
313+
Version: "v1.0.0",
314+
Type: 100, // Unknown/unhandled type
315+
},
316+
{
317+
Name: "output1",
318+
Path: "github.com/example/xk6-output1",
319+
Version: "v2.0.0",
320+
Type: ext.OutputExtension,
321+
},
322+
},
323+
expected: func(_ *testing.T, _ map[string]any) {
324+
// Should not be called when expectError is true
325+
},
326+
expectError: true,
327+
},
328+
}
329+
330+
for _, tt := range tests {
331+
t.Run(tt.name, func(t *testing.T) {
332+
t.Parallel()
333+
334+
details, err := versionDetailsWithExtensions(tt.exts)
335+
336+
if tt.expectError {
337+
require.Error(t, err)
338+
require.Contains(t, err.Error(), "unhandled extension type")
339+
return
340+
}
341+
342+
require.NoError(t, err)
343+
344+
// Convert to map[string]any for easier testing
345+
// (the actual function returns this type but nested structures need conversion)
346+
jsonBytes, jsonErr := json.Marshal(details)
347+
require.NoError(t, jsonErr)
348+
349+
var result map[string]any
350+
jsonErr = json.Unmarshal(jsonBytes, &result)
351+
require.NoError(t, jsonErr)
352+
353+
tt.expected(t, result)
354+
})
355+
}
356+
}

0 commit comments

Comments
 (0)