diff --git a/exec.go b/exec.go index 3965877..40cf49a 100644 --- a/exec.go +++ b/exec.go @@ -7,6 +7,8 @@ import ( "time" ) +type ExitError = exec.ExitError + // Exec represents an command executer. type Exec struct { Signal os.Signal diff --git a/exec_test.go b/exec_test.go index 6d0f6a2..39355c4 100644 --- a/exec_test.go +++ b/exec_test.go @@ -3,6 +3,7 @@ package exec import ( "bytes" "context" + "errors" "fmt" "os" "os/exec" @@ -284,3 +285,123 @@ func gentests(withSleepTest bool) []testcase { } return tests } + +func TestExitError(t *testing.T) { + t.Run("TypeIdentity", func(t *testing.T) { + // Test that exec.ExitError is identical to os/exec.ExitError + var execExitError *ExitError + var osExecExitError *exec.ExitError + + // Type assertion should work both ways + _ = (*exec.ExitError)(execExitError) + _ = (*ExitError)(osExecExitError) + }) + + t.Run("ActualErrorCase", func(t *testing.T) { + // Create a command that will fail with non-zero exit code + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = Command("cmd", "/c", "exit 1") + } else { + cmd = Command("sh", "-c", "exit 1") + } + + err := cmd.Run() + if err == nil { + t.Fatal("Expected command to fail with exit code 1") + } + + // Test that the error can be type asserted to exec.ExitError (our alias) + var exitError *ExitError + if !errors.As(err, &exitError) { + t.Fatalf("Expected error to be *exec.ExitError, got %T", err) + } + + // Test that the error can also be type asserted to os/exec.ExitError + var osExitError *exec.ExitError + if !errors.As(err, &osExitError) { + t.Fatalf("Expected error to be *os/exec.ExitError, got %T", err) + } + + // Verify ExitCode method works correctly + if exitError.ExitCode() != 1 { + t.Errorf("Expected exit code 1, got %d", exitError.ExitCode()) + } + + if osExitError.ExitCode() != 1 { + t.Errorf("Expected exit code 1, got %d", osExitError.ExitCode()) + } + + // Verify they are the same underlying object + if exitError != osExitError { + t.Error("Expected both type assertions to return the same object") + } + }) + + t.Run("ErrorMessageAndMethods", func(t *testing.T) { + // Test with a different exit code + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = Command("cmd", "/c", "exit 42") + } else { + cmd = Command("sh", "-c", "exit 42") + } + + err := cmd.Run() + if err == nil { + t.Fatal("Expected command to fail with exit code 42") + } + + var exitError *ExitError + if !errors.As(err, &exitError) { + t.Fatalf("Expected error to be *exec.ExitError, got %T", err) + } + + // Test ExitCode method + if exitError.ExitCode() != 42 { + t.Errorf("Expected exit code 42, got %d", exitError.ExitCode()) + } + + // Test Error method (inherited from os/exec.ExitError) + errorMsg := exitError.Error() + if errorMsg == "" { + t.Error("Expected non-empty error message") + } + + // Test that ProcessState is accessible + if exitError.ProcessState == nil { + t.Error("Expected ProcessState to be non-nil") + } + + // Test ProcessState.ExitCode() method + if exitError.ExitCode() != 42 { + t.Errorf("Expected ProcessState.ExitCode() to return 42, got %d", exitError.ExitCode()) + } + }) + + t.Run("CommandContextWithError", func(t *testing.T) { + // Test with CommandContext function + ctx := context.Background() + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = CommandContext(ctx, "cmd", "/c", "exit 5") + } else { + cmd = CommandContext(ctx, "sh", "-c", "exit 5") + } + + err := cmd.Run() + if err == nil { + t.Fatal("Expected command to fail with exit code 5") + } + + // Verify the alias works with CommandContext as well + var exitError *ExitError + if !errors.As(err, &exitError) { + t.Fatalf("Expected error to be *exec.ExitError, got %T", err) + } + + if exitError.ExitCode() != 5 { + t.Errorf("Expected exit code 5, got %d", exitError.ExitCode()) + } + }) +}