Skip to content

Commit

Permalink
Introduces --json flag for k6 version sub-command (#4093)
Browse files Browse the repository at this point in the history
* Introduces --json flag for k6 version sub-command

* Align terms with registry

* Moving version related logic to cmd

* Ensure that extension version match before grouping

* Simplify JSON details marshaling
  • Loading branch information
olegbespalov authored Dec 16, 2024
1 parent 13658b4 commit ecabc88
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 55 deletions.
3 changes: 1 addition & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"go.k6.io/k6/cmd/state"
"go.k6.io/k6/errext"
"go.k6.io/k6/errext/exitcodes"
"go.k6.io/k6/lib/consts"
"go.k6.io/k6/log"
)

Expand Down Expand Up @@ -79,7 +78,7 @@ func (c *rootCommand) persistentPreRunE(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
c.globalState.Logger.Debugf("k6 version: v%s", consts.FullVersion())
c.globalState.Logger.Debugf("k6 version: v%s", fullVersion())
return nil
}

Expand Down
156 changes: 147 additions & 9 deletions cmd/version.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package cmd

import (
"encoding/json"
"fmt"
"runtime"
"runtime/debug"
"strings"

"github.com/spf13/cobra"
Expand All @@ -10,8 +13,89 @@ import (
"go.k6.io/k6/lib/consts"
)

const (
commitKey = "commit"
commitDirtyKey = "commit_dirty"
)

// fullVersion returns the maximally full version and build information for
// the currently running k6 executable.
func fullVersion() string {
details := versionDetails()

goVersionArch := fmt.Sprintf("%s, %s/%s", details["go_version"], details["go_os"], details["go_arch"])

k6version := fmt.Sprintf("%s", details["version"])
// for the fallback case when the version is not in the expected format
// cobra adds a "v" prefix to the version
k6version = strings.TrimLeft(k6version, "v")

commit, ok := details[commitKey].(string)
if !ok || commit == "" {
return fmt.Sprintf("%s (%s)", k6version, goVersionArch)
}

isDirty, ok := details[commitDirtyKey].(bool)
if ok && isDirty {
commit += "-dirty"
}

return fmt.Sprintf("%s (commit/%s, %s)", k6version, commit, goVersionArch)
}

// versionDetails returns the structured details about version
func versionDetails() map[string]interface{} {
v := consts.Version
if !strings.HasPrefix(v, "v") {
v = "v" + v
}

details := map[string]interface{}{
"version": v,
"go_version": runtime.Version(),
"go_os": runtime.GOOS,
"go_arch": runtime.GOARCH,
}

buildInfo, ok := debug.ReadBuildInfo()
if !ok {
return details
}

var (
commit string
dirty bool
)
for _, s := range buildInfo.Settings {
switch s.Key {
case "vcs.revision":
commitLen := 10
if len(s.Value) < commitLen {
commitLen = len(s.Value)
}
commit = s.Value[:commitLen]
case "vcs.modified":
if s.Value == "true" {
dirty = true
}
default:
}
}

if commit == "" {
return details
}

details[commitKey] = commit
if dirty {
details[commitDirtyKey] = true
}

return details
}

func versionString() string {
v := consts.FullVersion()
v := fullVersion()

if exts := ext.GetAll(); len(exts) > 0 {
extsDesc := make([]string, 0, len(exts))
Expand All @@ -24,16 +108,70 @@ func versionString() string {
return v
}

func getCmdVersion(_ *state.GlobalState) *cobra.Command {
// versionCmd represents the version command.
return &cobra.Command{
type versionCmd struct {
gs *state.GlobalState
isJSON bool
}

func (c *versionCmd) run(cmd *cobra.Command, _ []string) error {
if !c.isJSON {
root := cmd.Root()
root.SetArgs([]string{"--version"})
_ = root.Execute()
return nil
}

details := versionDetails()
if exts := ext.GetAll(); len(exts) > 0 {
type extInfo struct {
Module string `json:"module"`
Version string `json:"version"`
Imports []string `json:"imports"`
}

ext := make(map[string]extInfo)
for _, e := range exts {
key := e.Path + "@" + e.Version

if v, ok := ext[key]; ok {
v.Imports = append(v.Imports, e.Name)
ext[key] = v
continue
}

ext[key] = extInfo{
Module: e.Path,
Version: e.Version,
Imports: []string{e.Name},
}
}

list := make([]extInfo, 0, len(ext))
for _, v := range ext {
list = append(list, v)
}

details["extensions"] = list
}

if err := json.NewEncoder(c.gs.Stdout).Encode(details); err != nil {
return fmt.Errorf("failed to encode/output version details: %w", err)
}

return nil
}

func getCmdVersion(gs *state.GlobalState) *cobra.Command {
versionCmd := &versionCmd{gs: gs}

cmd := &cobra.Command{
Use: "version",
Short: "Show application version",
Long: `Show the application version and exit.`,
Run: func(cmd *cobra.Command, _ []string) {
root := cmd.Root()
root.SetArgs([]string{"--version"})
_ = root.Execute()
},
RunE: versionCmd.run,
}

cmd.Flags().BoolVar(&versionCmd.isJSON, "json", false, "if set, output version information will be in JSON format")

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

import (
"encoding/json"
"runtime"
"testing"

"github.com/stretchr/testify/assert"
"go.k6.io/k6/cmd/tests"
"go.k6.io/k6/lib/consts"
)

func TestVersionFlag(t *testing.T) {
t.Parallel()

ts := tests.NewGlobalTestState(t)
ts.ExpectedExitCode = 0
ts.CmdArgs = []string{"k6", "--version"}

ExecuteWithGlobalState(ts.GlobalState)

stdout := ts.Stdout.String()
t.Log(stdout)
assert.NotEmpty(t, stdout)

// Check that the version/format string is correct
assert.Contains(t, stdout, "k6 v")
assert.Contains(t, stdout, consts.Version)
assert.Contains(t, stdout, runtime.Version())
assert.Contains(t, stdout, runtime.GOOS)
assert.Contains(t, stdout, runtime.GOARCH)
}

func TestVersionSubCommand(t *testing.T) {
t.Parallel()

ts := tests.NewGlobalTestState(t)
ts.ExpectedExitCode = 0
ts.CmdArgs = []string{"k6", "version"}

ExecuteWithGlobalState(ts.GlobalState)

stdout := ts.Stdout.String()
t.Log(stdout)
assert.NotEmpty(t, stdout)

// Check that the version/format string is correct
assert.Contains(t, stdout, "k6 v")
assert.Contains(t, stdout, consts.Version)
assert.Contains(t, stdout, runtime.Version())
assert.Contains(t, stdout, runtime.GOOS)
assert.Contains(t, stdout, runtime.GOARCH)
}

func TestVersionJSONSubCommand(t *testing.T) {
t.Parallel()

ts := tests.NewGlobalTestState(t)
ts.ExpectedExitCode = 0
ts.CmdArgs = []string{"k6", "version", "--json"}

ExecuteWithGlobalState(ts.GlobalState)

stdout := ts.Stdout.String()
t.Log(stdout)
assert.NotEmpty(t, stdout)

// try to unmarshal the JSON output
var details map[string]interface{}
err := json.Unmarshal([]byte(stdout), &details)
assert.NoError(t, err)

// Check that details are correct
assert.Contains(t, details, "version")
assert.Contains(t, details, "go_version")
assert.Contains(t, details, "go_os")
assert.Contains(t, details, "go_arch")
assert.Equal(t, "v"+consts.Version, details["version"])
assert.Equal(t, runtime.Version(), details["go_version"])
assert.Equal(t, runtime.GOOS, details["go_os"])
assert.Equal(t, runtime.GOARCH, details["go_arch"])
}
44 changes: 0 additions & 44 deletions lib/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,12 @@
package consts

import (
"fmt"
"runtime"
"runtime/debug"
"strings"
)

// Version contains the current semantic version of k6.
const Version = "0.55.0"

// FullVersion returns the maximally full version and build information for
// the currently running k6 executable.
func FullVersion() string {
goVersionArch := fmt.Sprintf("%s, %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH)

buildInfo, ok := debug.ReadBuildInfo()
if !ok {
return fmt.Sprintf("%s (%s)", Version, goVersionArch)
}

var (
commit string
dirty bool
)
for _, s := range buildInfo.Settings {
switch s.Key {
case "vcs.revision":
commitLen := 10
if len(s.Value) < commitLen {
commitLen = len(s.Value)
}
commit = s.Value[:commitLen]
case "vcs.modified":
if s.Value == "true" {
dirty = true
}
default:
}
}

if commit == "" {
return fmt.Sprintf("%s (%s)", Version, goVersionArch)
}

if dirty {
commit += "-dirty"
}

return fmt.Sprintf("%s (commit/%s, %s)", Version, commit, goVersionArch)
}

// Banner returns the ASCII-art banner with the k6 logo
func Banner() string {
banner := strings.Join([]string{
Expand Down

0 comments on commit ecabc88

Please sign in to comment.