Add regression tests for CLI error output (#1566)

## Changes
Add regression tests for https://github.com/databricks/cli/issues/1563

We test 2 code paths:
- if there is an error, we can print to stderr
- if there is a valid output, we can print to stdout

We should also consider adding black-box tests that will run the CLI
binary as a black box and inspect its output to stderr/stdout.

## Tests
Unit tests
This commit is contained in:
Gleb Kanterov 2024-07-10 08:38:06 +02:00 committed by GitHub
parent 8f56ca39a2
commit 25737bbb5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 47 additions and 16 deletions

View File

@ -92,9 +92,8 @@ func flagErrorFunc(c *cobra.Command, err error) error {
// Execute adds all child commands to the root command and sets flags appropriately. // Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd. // This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute(cmd *cobra.Command) { func Execute(ctx context.Context, cmd *cobra.Command) error {
// TODO: deferred panic recovery // TODO: deferred panic recovery
ctx := context.Background()
// Run the command // Run the command
cmd, err := cmd.ExecuteContextC(ctx) cmd, err := cmd.ExecuteContextC(ctx)
@ -118,7 +117,5 @@ func Execute(cmd *cobra.Command) {
} }
} }
if err != nil { return err
os.Exit(1)
}
} }

View File

@ -19,6 +19,9 @@ import (
"testing" "testing"
"time" "time"
"github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/libs/flags"
"github.com/databricks/cli/cmd" "github.com/databricks/cli/cmd"
_ "github.com/databricks/cli/cmd/version" _ "github.com/databricks/cli/cmd/version"
"github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/cmdio"
@ -105,7 +108,12 @@ func (t *cobraTestRunner) registerFlagCleanup(c *cobra.Command) {
// Find target command that will be run. Example: if the command run is `databricks fs cp`, // Find target command that will be run. Example: if the command run is `databricks fs cp`,
// target command corresponds to `cp` // target command corresponds to `cp`
targetCmd, _, err := c.Find(t.args) targetCmd, _, err := c.Find(t.args)
require.NoError(t, err) if err != nil && strings.HasPrefix(err.Error(), "unknown command") {
// even if command is unknown, we can proceed
require.NotNil(t, targetCmd)
} else {
require.NoError(t, err)
}
// Force initialization of default flags. // Force initialization of default flags.
// These are initialized by cobra at execution time and would otherwise // These are initialized by cobra at execution time and would otherwise
@ -169,22 +177,28 @@ func (t *cobraTestRunner) RunBackground() {
var stdoutW, stderrW io.WriteCloser var stdoutW, stderrW io.WriteCloser
stdoutR, stdoutW = io.Pipe() stdoutR, stdoutW = io.Pipe()
stderrR, stderrW = io.Pipe() stderrR, stderrW = io.Pipe()
root := cmd.New(t.ctx) ctx := cmdio.NewContext(t.ctx, &cmdio.Logger{
root.SetOut(stdoutW) Mode: flags.ModeAppend,
root.SetErr(stderrW) Reader: bufio.Reader{},
root.SetArgs(t.args) Writer: stderrW,
})
cli := cmd.New(ctx)
cli.SetOut(stdoutW)
cli.SetErr(stderrW)
cli.SetArgs(t.args)
if t.stdinW != nil { if t.stdinW != nil {
root.SetIn(t.stdinR) cli.SetIn(t.stdinR)
} }
// Register cleanup function to restore flags to their original values // Register cleanup function to restore flags to their original values
// once test has been executed. This is needed because flag values reside // once test has been executed. This is needed because flag values reside
// in a global singleton data-structure, and thus subsequent tests might // in a global singleton data-structure, and thus subsequent tests might
// otherwise interfere with each other // otherwise interfere with each other
t.registerFlagCleanup(root) t.registerFlagCleanup(cli)
errch := make(chan error) errch := make(chan error)
ctx, cancel := context.WithCancel(t.ctx) ctx, cancel := context.WithCancel(ctx)
// Tee stdout/stderr to buffers. // Tee stdout/stderr to buffers.
stdoutR = io.TeeReader(stdoutR, &t.stdout) stdoutR = io.TeeReader(stdoutR, &t.stdout)
@ -197,7 +211,7 @@ func (t *cobraTestRunner) RunBackground() {
// Run command in background. // Run command in background.
go func() { go func() {
cmd, err := root.ExecuteContextC(ctx) err := root.Execute(ctx, cli)
if err != nil { if err != nil {
t.Logf("Error running command: %s", err) t.Logf("Error running command: %s", err)
} }
@ -230,7 +244,7 @@ func (t *cobraTestRunner) RunBackground() {
// These commands are globals so we have to clean up to the best of our ability after each run. // These commands are globals so we have to clean up to the best of our ability after each run.
// See https://github.com/spf13/cobra/blob/a6f198b635c4b18fff81930c40d464904e55b161/command.go#L1062-L1066 // See https://github.com/spf13/cobra/blob/a6f198b635c4b18fff81930c40d464904e55b161/command.go#L1062-L1066
//lint:ignore SA1012 cobra sets the context and doesn't clear it //lint:ignore SA1012 cobra sets the context and doesn't clear it
cmd.SetContext(nil) cli.SetContext(nil)
// Make caller aware of error. // Make caller aware of error.
errch <- err errch <- err

View File

@ -0,0 +1,15 @@
package internal
import (
"testing"
assert "github.com/databricks/cli/libs/dyn/dynassert"
)
func TestUnknownCommand(t *testing.T) {
stdout, stderr, err := RequireErrorRun(t, "unknown-command")
assert.Error(t, err, "unknown command", `unknown command "unknown-command" for "databricks"`)
assert.Equal(t, "", stdout.String())
assert.Contains(t, stderr.String(), "unknown command")
}

View File

@ -2,11 +2,16 @@ package main
import ( import (
"context" "context"
"os"
"github.com/databricks/cli/cmd" "github.com/databricks/cli/cmd"
"github.com/databricks/cli/cmd/root" "github.com/databricks/cli/cmd/root"
) )
func main() { func main() {
root.Execute(cmd.New(context.Background())) ctx := context.Background()
err := root.Execute(ctx, cmd.New(ctx))
if err != nil {
os.Exit(1)
}
} }