package cmdutil import ( "bytes" "errors" "io" "os" "os/exec" "testing" "github.com/hashicorp/go-multierror" "github.com/pulumi/pulumi/sdk/v3/go/common/testing/iotest" "github.com/pulumi/pulumi/sdk/v3/go/common/util/result" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRunFunc_Bail(t *testing.T) { t.Parallel() // Verifies that a use of RunFunc that returns BailError // will cause the program to exit with a non-zero exit code // without printing an error message. // // Unfortunately, we can't test this directly, // because the `os.Exit` call in RunResultFunc. // // Instead, we'll re-run the test binary, // and have it run TestFakeCommand. // We'll verify the output of that binary instead. exe, err := os.Executable() require.NoError(t, err) cmd := exec.Command(exe, "-test.run=^TestFakeCommand$") cmd.Env = append(os.Environ(), "TEST_FAKE=1") // Write output to the buffer and to the test logger. var buff bytes.Buffer output := io.MultiWriter(&buff, iotest.LogWriter(t)) cmd.Stdout = output cmd.Stderr = output err = cmd.Run() exitErr := new(exec.ExitError) require.ErrorAs(t, err, &exitErr) assert.NotZero(t, exitErr.ExitCode()) assert.Empty(t, buff.String()) } //nolint:paralleltest // not a real test func TestFakeCommand(t *testing.T) { if os.Getenv("TEST_FAKE") != "1" { // This is not a real test. // It's a fake test that we'll run as a subprocess // to verify that the RunFunc function works correctly. // See TestRunFunc_Bail for more details. return } cmd := &cobra.Command{ Run: RunFunc(func(cmd *cobra.Command, args []string) error { return result.BailErrorf("bail") }), } err := cmd.Execute() // Unreachable: RunFunc should have called os.Exit. assert.Fail(t, "unreachable", "RunFunc should have called os.Exit: %v", err) } func TestErrorMessage(t *testing.T) { t.Parallel() tests := []struct { desc string give error want string }{ { desc: "simple error", give: errors.New("great sadness"), want: "great sadness", }, { desc: "hashi multi error", give: multierror.Append( errors.New("foo"), errors.New("bar"), errors.New("baz"), ), want: "3 errors occurred:" + "\n 1) foo" + "\n 2) bar" + "\n 3) baz", }, { desc: "std errors.Join", give: errors.Join( errors.New("foo"), errors.New("bar"), errors.New("baz"), ), want: "3 errors occurred:" + "\n 1) foo" + "\n 2) bar" + "\n 3) baz", }, { desc: "empty multi error", // This is technically invalid, // but we guard against it, // so let's test it too. give: &invalidEmptyMultiError{}, want: "invalid empty multi error", }, { desc: "single wrapped error", give: &multierror.Error{ Errors: []error{ errors.New("great sadness"), }, }, want: "great sadness", }, { desc: "multi error inside single wrapped error", give: &multierror.Error{ Errors: []error{ errors.Join( errors.New("foo"), errors.New("bar"), errors.New("baz"), ), }, }, want: "3 errors occurred:" + "\n 1) foo" + "\n 2) bar" + "\n 3) baz", }, } for _, tt := range tests { tt := tt t.Run(tt.desc, func(t *testing.T) { t.Parallel() got := errorMessage(tt.give) assert.Equal(t, tt.want, got) }) } } // invalidEmptyMultiError is an invalid error type // that implements Unwrap() []error, but returns an empty slice. // This is invalid per the contract for that method. type invalidEmptyMultiError struct{} func (*invalidEmptyMultiError) Error() string { return "invalid empty multi error" } func (*invalidEmptyMultiError) Unwrap() []error { return []error{} // invalid }