test: add cmdtest package (#15251)
## Description This PR introduces the `cmdtest` package, offering a lightweight wrapper around cobra commands to simplify testing CLI utilities. I backfilled tests for the `version` command, which was an example of a very simple test setup; and for the `export` command, which was more involved due to the server and client context requirements. I did notice that there are some existing tests for some utilities, but the `cmdtest` package follows a simple pattern that has been easy to use successfully in [the relayer](https://github.com/cosmos/relayer/blob/main/internal/relayertest/system.go) and in other projects outside the Cosmos ecosystem. While filling in these tests, I started removing uses of `cmd.Print`, as that is the root cause of issues like #8498, #7964, #15167, and possibly others. Internal to cobra, the print family of methods write to `cmd.OutOrStderr()` -- meaning that if the authors call `cmd.SetOutput()` before executing the command, the output will be written to stdout as expected; otherwise it will go to stderr. I don't understand why that would be the default behavior, but it is probably too late to change from cobra's side. Instead of `cmd.Print`, we prefer to `fmt.Fprint(cmd.OutOrStdout())` or `fmt.Fprint(cmd.ErrOrStderr())` as appropriate, giving an unambiguous destination for output. And the new tests collect those outputs in plain `bytes.Buffer` values so that we can assert their content appropriately. In the longer term, I would like to deprecate and eventually remove the `testutil` package's `ApplyMockIO` method and its `BufferWriter` and `BufferReader` types, as they are unnecessary indirection when a simpler solution exists. But that can wait until `cmdtest` has propagated through the codebase more. --- ### Author Checklist *All items are required. Please add a note to the item if the item is not applicable and please add links to any relevant follow up issues.* I have... - [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#pr-targeting)) - [ ] ~~provided a link to the relevant issue or specification~~ - [x] reviewed "Files changed" and left comments if necessary - [ ] confirmed all CI checks have passed ### Reviewers Checklist *All items are required. Please add a note if the item is not applicable and please add your handle next to the items reviewed if you only reviewed selected items.* I have... - [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] confirmed all author checklist items have been addressed - [ ] confirmed that this PR does not change production code
This commit is contained in:
+4
-2
@@ -2,6 +2,7 @@ package version
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cometbft/cometbft/libs/cli"
|
||||
@@ -16,11 +17,12 @@ func NewVersionCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the application binary version information",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
verInfo := NewInfo()
|
||||
|
||||
if long, _ := cmd.Flags().GetBool(flagLong); !long {
|
||||
cmd.Println(verInfo.Version)
|
||||
fmt.Fprintln(cmd.OutOrStdout(), verInfo.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -42,7 +44,7 @@ func NewVersionCommand() *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.Println(string(bz))
|
||||
fmt.Fprintln(cmd.OutOrStdout(), string(bz))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cometbft/cometbft/libs/cli"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/testutil"
|
||||
"github.com/cosmos/cosmos-sdk/testutil/cmdtest"
|
||||
"github.com/cosmos/cosmos-sdk/version"
|
||||
)
|
||||
|
||||
@@ -40,6 +42,103 @@ go version go1.14 linux/amd64`
|
||||
require.Equal(t, want, info.String())
|
||||
}
|
||||
|
||||
func TestCLI(t *testing.T) {
|
||||
setVersionPackageVars(t)
|
||||
|
||||
sys := cmdtest.NewSystem()
|
||||
sys.AddCommands(version.NewVersionCommand())
|
||||
|
||||
t.Run("no flags", func(t *testing.T) {
|
||||
res := sys.MustRun(t, "version")
|
||||
|
||||
// Only prints the version, with a newline, to stdout.
|
||||
require.Equal(t, testVersion+"\n", res.Stdout.String())
|
||||
require.Empty(t, res.Stderr.String())
|
||||
})
|
||||
|
||||
t.Run("--long flag", func(t *testing.T) {
|
||||
res := sys.MustRun(t, "version", "--long")
|
||||
|
||||
out := res.Stdout.String()
|
||||
lines := strings.Split(out, "\n")
|
||||
require.Contains(t, lines, "name: testchain-app")
|
||||
require.Contains(t, lines, "server_name: testchaind")
|
||||
require.Contains(t, lines, `version: "3.14"`)
|
||||
require.Contains(t, lines, "commit: abc123")
|
||||
require.Contains(t, lines, "build_tags: mybuildtag")
|
||||
|
||||
require.Empty(t, res.Stderr.String())
|
||||
})
|
||||
|
||||
t.Run("--output=json flag", func(t *testing.T) {
|
||||
res := sys.MustRun(t, "version", "--output=json")
|
||||
|
||||
var info version.Info
|
||||
require.NoError(t, json.Unmarshal(res.Stdout.Bytes(), &info))
|
||||
|
||||
// Assert against a couple fields that are difficult to predict in test
|
||||
// without copying and pasting code.
|
||||
require.NotEmpty(t, info.GoVersion)
|
||||
|
||||
// The SDK version appears to not be set during this test, so we'll ignore it here.
|
||||
|
||||
// Now clear out the non-empty fields, so we can compare against a fixed value.
|
||||
info.GoVersion = ""
|
||||
|
||||
want := version.Info{
|
||||
Name: testName,
|
||||
AppName: testAppName,
|
||||
Version: testVersion,
|
||||
GitCommit: testCommit,
|
||||
BuildTags: testBuildTags,
|
||||
}
|
||||
require.Equal(t, want, info)
|
||||
|
||||
require.Empty(t, res.Stderr.String())
|
||||
})
|
||||
|
||||
t.Run("positional args rejected", func(t *testing.T) {
|
||||
res := sys.Run("version", "foo")
|
||||
require.Error(t, res.Err)
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
testName = "testchain-app"
|
||||
testAppName = "testchaind"
|
||||
testVersion = "3.14"
|
||||
testCommit = "abc123"
|
||||
testBuildTags = "mybuildtag"
|
||||
)
|
||||
|
||||
// setVersionPackageVars temporarily overrides the package variables in the version package
|
||||
// so that we can assert meaningful output.
|
||||
func setVersionPackageVars(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
var (
|
||||
origName = version.Name
|
||||
origAppName = version.AppName
|
||||
origVersion = version.Version
|
||||
origCommit = version.Commit
|
||||
origBuildTags = version.BuildTags
|
||||
)
|
||||
|
||||
t.Cleanup(func() {
|
||||
version.Name = origName
|
||||
version.AppName = origAppName
|
||||
version.Version = origVersion
|
||||
version.Commit = origCommit
|
||||
version.BuildTags = origBuildTags
|
||||
})
|
||||
|
||||
version.Name = testName
|
||||
version.AppName = testAppName
|
||||
version.Version = testVersion
|
||||
version.Commit = testCommit
|
||||
version.BuildTags = testBuildTags
|
||||
}
|
||||
|
||||
func Test_runVersionCmd(t *testing.T) {
|
||||
cmd := version.NewVersionCommand()
|
||||
_, mockOut := testutil.ApplyMockIO(cmd)
|
||||
|
||||
Reference in New Issue
Block a user