Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: add cmdtest package #15251

Merged
merged 5 commits into from
Mar 6, 2023
Merged

test: add cmdtest package #15251

merged 5 commits into from
Mar 6, 2023

Conversation

mark-rushakoff
Copy link
Member

@mark-rushakoff mark-rushakoff commented Mar 2, 2023

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 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...

  • included the correct type prefix in the PR title
  • targeted the correct branch (see PR Targeting)
  • provided a link to the relevant issue or specification
  • 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 in the PR title
  • confirmed all author checklist items have been addressed
  • confirmed that this PR does not change production code

And add first example tests to the version package.

This is a pattern I've used repeatedly and successfully in the past. It
offers a simple way to validate that a command emits expected output to
stdout and stderr, and the command's returned error.

I saw that there was already some functionality around "MockIO" in the
testutil package, but that sits at an awkward position where you still
have to manage your cobra.Command separately. Moreover, the BufferReader
and BufferWriter interfaces it uses are unnecessary -- virtually every
test around the CLI will suffice with holding all output in memory, and
there is no reason to complicate stdin substitution with anything more
complicated than a simple io.Reader.

One piece that neither the previous nor the new approach can detect, is
use of (*cobra.Command).Println. This has been the source of previous
issues where output was expected to go to stdout but instead was sent to
stderr. Per the docs on Println, "Println is a convenience method to
Println to the defined output, fallback to Stderr if not set."

Our tests do call (*cobra.Command).SetOutput, so application code using
cmd.Println will work as intended, but this does not force third parties
to call SetOutput correctly. We may be able to set up a PersistentPreRun
function that checks if (*cobra.Command).OutOrStderr is in fact
os.Stderr, and if so, warn or panic.

On the other hand, if we are careful to always print to
cmd.OutOrStdout() or cmd.ErrOrStderr() appropriately, then hopefully
third party applications would follow suit and more consistently print
to the intended stream.
This is an example of a slightly more specialized use of
(*cmdtest.System). In this set of tests, we have a type ExportSystem
that wraps a cmdtest.System and always passes an associated context in
its Run and MustRun methods. The export CLI does not accept any
information on stdin, so no RunWithInput methods are exposed.
@mark-rushakoff mark-rushakoff requested a review from a team as a code owner March 2, 2023 18:44
@github-prbot github-prbot requested review from a team and kocubinski and removed request for a team March 2, 2023 18:44
@@ -78,5 +78,14 @@ type (

// AppExporter is a function that dumps all app state to
// JSON-serializable structure and returns the current validator set.
AppExporter func(log.Logger, dbm.DB, io.Writer, int64, bool, []string, AppOptions, []string) (ExportedApp, error)
AppExporter func(
logger log.Logger,
Copy link
Member Author

Choose a reason for hiding this comment

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

I only added names for the arguments in this call, to make it slightly easier to understand what an implementation of AppExporter should expect.

if err != nil {
return err
}
defer f.Close()

Check failure

Code scanning / gosec

Deferring unsafe method "Close" on type "io.WriteCloser"

Deferring unsafe method "Close" on type "*os.File"
This is an exceptional case where ctx doesn't belong as the first
argument in a method.
)

// System is a system under test.
type System struct {
Copy link
Member

@julienrbrt julienrbrt Mar 2, 2023

Choose a reason for hiding this comment

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

This is cool; however, I find the term System a bit ambiguous.
Maybe it's my perception, but if I think of system the two first things that come in my mind are operating system and file system.
I don't have a better term 😅, but it is not immediately clear that it is for testing cobra commands.

@mark-rushakoff
Copy link
Member Author

mark-rushakoff commented Mar 3, 2023 via email

Copy link
Member

@tac0turtle tac0turtle left a comment

Choose a reason for hiding this comment

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

this api looks good to me, it brings consistency to our cli tests as well. Would like to hear from others as well

Copy link
Member

@julienrbrt julienrbrt left a comment

Choose a reason for hiding this comment

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

ACK. I cannot think of a better name, either.
Maybe adding a godoc to define what a System represents could be beneficial.

@@ -0,0 +1,120 @@
// Package cmdtest contains a framework for testing cobra Commands within Go unit tests.
package cmdtest
Copy link
Member

Choose a reason for hiding this comment

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

nit, maybe this should be under testutil/cli like the other cli testutils.

@mark-rushakoff mark-rushakoff added the A:automerge Automatically merge PR once all prerequisites pass. label Mar 6, 2023
@mergify mergify bot merged commit e7097b1 into main Mar 6, 2023
@mergify mergify bot deleted the mr/cmdtest branch March 6, 2023 19:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A:automerge Automatically merge PR once all prerequisites pass.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants