Here we go again...

This commit is contained in:
2025-08-28 21:25:10 +04:00
parent 25d4762a3c
commit 42335c1178
62 changed files with 4579 additions and 4460 deletions

5
pkg/utils/convert.go Normal file
View File

@@ -0,0 +1,5 @@
package utils
func ToPtr[T any](value T) *T {
return &value
}

155
pkg/utils/convert_test.go Normal file
View File

@@ -0,0 +1,155 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestToPtr(t *testing.T) {
t.Run("ToPtr with int", func(t *testing.T) {
value := 42
ptr := ToPtr(value)
require.NotNil(t, ptr)
assert.Equal(t, value, *ptr)
assert.NotSame(t, &value, ptr, "Should return a new pointer")
})
t.Run("ToPtr with string", func(t *testing.T) {
value := "test string"
ptr := ToPtr(value)
require.NotNil(t, ptr)
assert.Equal(t, value, *ptr)
})
t.Run("ToPtr with bool", func(t *testing.T) {
value := true
ptr := ToPtr(value)
require.NotNil(t, ptr)
assert.Equal(t, value, *ptr)
})
t.Run("ToPtr with float64", func(t *testing.T) {
value := 3.14159
ptr := ToPtr(value)
require.NotNil(t, ptr)
assert.InEpsilon(t, value, *ptr, 0.0001)
})
t.Run("ToPtr with struct", func(t *testing.T) {
type TestStruct struct {
Field1 string
Field2 int
}
value := TestStruct{Field1: "test", Field2: 123}
ptr := ToPtr(value)
require.NotNil(t, ptr)
assert.Equal(t, value, *ptr)
assert.Equal(t, "test", ptr.Field1)
assert.Equal(t, 123, ptr.Field2)
})
t.Run("ToPtr with slice", func(t *testing.T) {
value := []int{1, 2, 3}
ptr := ToPtr(value)
require.NotNil(t, ptr)
assert.Equal(t, value, *ptr)
assert.Len(t, *ptr, 3)
})
t.Run("ToPtr with map", func(t *testing.T) {
value := map[string]int{"one": 1, "two": 2}
ptr := ToPtr(value)
require.NotNil(t, ptr)
assert.Equal(t, value, *ptr)
assert.Len(t, *ptr, 2)
})
t.Run("ToPtr with nil interface", func(t *testing.T) {
var value any = nil
ptr := ToPtr(value)
require.NotNil(t, ptr)
assert.Nil(t, *ptr)
})
t.Run("ToPtr with pointer", func(t *testing.T) {
originalValue := 42
originalPtr := &originalValue
ptr := ToPtr(originalPtr)
require.NotNil(t, ptr)
assert.Equal(t, originalPtr, *ptr)
assert.NotSame(t, originalPtr, ptr, "Should return a pointer to pointer")
})
t.Run("ToPtr with uint", func(t *testing.T) {
value := uint(100)
ptr := ToPtr(value)
require.NotNil(t, ptr)
assert.Equal(t, value, *ptr)
})
t.Run("ToPtr modification safety", func(t *testing.T) {
value := 10
ptr := ToPtr(value)
*ptr = 20
assert.Equal(t, 10, value, "Original value should not be modified")
assert.Equal(t, 20, *ptr, "Pointer value should be modified")
})
t.Run("ToPtr with byte array", func(t *testing.T) {
value := [3]byte{1, 2, 3}
ptr := ToPtr(value)
require.NotNil(t, ptr)
assert.Equal(t, value, *ptr)
})
t.Run("ToPtr with rune", func(t *testing.T) {
value := 'A'
ptr := ToPtr(value)
require.NotNil(t, ptr)
assert.Equal(t, value, *ptr)
assert.Equal(t, int32(65), *ptr)
})
t.Run("ToPtr with empty string", func(t *testing.T) {
value := ""
ptr := ToPtr(value)
require.NotNil(t, ptr)
assert.Equal(t, value, *ptr)
assert.Empty(t, *ptr)
})
t.Run("ToPtr with zero values", func(t *testing.T) {
// Test with various zero values
intZero := 0
intPtr := ToPtr(intZero)
require.NotNil(t, intPtr)
assert.Equal(t, 0, *intPtr)
boolZero := false
boolPtr := ToPtr(boolZero)
require.NotNil(t, boolPtr)
assert.False(t, *boolPtr)
floatZero := 0.0
floatPtr := ToPtr(floatZero)
require.NotNil(t, floatPtr)
assert.Equal(t, 0.0, *floatPtr) //nolint:testifylint
})
}

105
pkg/utils/error.go Normal file
View File

@@ -0,0 +1,105 @@
package utils
import (
"errors"
"fmt"
"reflect"
)
// ErrorHandler represents a function that handles a specific error type
type ErrorHandler func(error) error
// ErrorMatcher holds the error type/value and its handler
type ErrorMatcher struct {
ErrorType any // Can be error value (sentinel) or error type
Handler ErrorHandler
IsSentinel bool // true for sentinel errors, false for custom types
}
// HandleError processes an error against a list of matchers and executes the appropriate handler.
// It returns (true, handlerResult) if a matching handler is found and executed,
// or (false, nil) if no matcher matches the error.
// If err is nil, returns (true, nil).
//
// Example:
//
// handled, result := HandleError(err,
// OnSentinelError(io.EOF, func(e error) error {
// return nil // EOF is expected, ignore it
// }),
// OnCustomError(func(e *CustomError) error {
// return fmt.Errorf("custom error: %w", e)
// }),
// )
func HandleError(err error, matchers ...ErrorMatcher) (bool, error) {
if err == nil {
return true, nil
}
for _, matcher := range matchers {
if matcher.IsSentinel {
// Handle sentinel errors with errors.Is
if sentinelErr, ok := matcher.ErrorType.(error); ok {
if errors.Is(err, sentinelErr) {
return true, matcher.Handler(err)
}
}
} else {
// Handle custom error types with errors.As
errorType := reflect.TypeOf(matcher.ErrorType)
errorValue := reflect.New(errorType).Interface()
if errors.As(err, errorValue) {
return true, matcher.Handler(err)
}
}
}
return false, nil // No matcher found
}
// HandleErrorOrDie processes an error against a list of matchers and executes the appropriate handler.
// If a matching handler is found, it returns the handler's result.
// If no matcher matches the error, it panics with a descriptive message.
// This function is useful when all expected error types must be handled explicitly.
//
// Example:
//
// result := HandleErrorOrDie(err,
// OnSentinelError(context.Canceled, func(e error) error {
// return fmt.Errorf("operation canceled")
// }),
// OnCustomError(func(e *ValidationError) error {
// return fmt.Errorf("validation failed: %w", e)
// }),
// ) // Panics if err doesn't match any handler
func HandleErrorOrDie(err error, matchers ...ErrorMatcher) error {
ok, err := HandleError(err, matchers...)
if !ok {
panic(fmt.Sprintf("Unhandled error of type %T: %v", err, err))
}
return err
}
func OnSentinelError(sentinelErr error, handler ErrorHandler) ErrorMatcher {
return ErrorMatcher{
ErrorType: sentinelErr,
Handler: handler,
IsSentinel: true,
}
}
func OnCustomError[T error](handler func(T) error) ErrorMatcher {
var zero T
return ErrorMatcher{
ErrorType: zero,
Handler: func(err error) error {
var typedErr T
if errors.As(err, &typedErr) {
return handler(typedErr)
}
return nil
},
IsSentinel: false,
}
}

386
pkg/utils/error_test.go Normal file
View File

@@ -0,0 +1,386 @@
package utils
import (
"context"
"errors"
"fmt"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Custom error types for testing
type CustomError struct {
Code int
Message string
}
func (e *CustomError) Error() string {
return fmt.Sprintf("custom error %d: %s", e.Code, e.Message)
}
type ValidationError struct {
Field string
Value string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed for field %s with value %s", e.Field, e.Value)
}
// Sentinel errors for testing
var (
ErrSentinel1 = errors.New("sentinel error 1")
ErrSentinel2 = errors.New("sentinel error 2")
)
func TestHandleError(t *testing.T) {
t.Run("HandleError with nil error", func(t *testing.T) {
handled, result := HandleError(nil)
assert.True(t, handled)
assert.NoError(t, result)
})
t.Run("HandleError with sentinel error match", func(t *testing.T) {
err := io.EOF
handled, result := HandleError(err,
OnSentinelError(io.EOF, func(e error) error {
return errors.New("handled EOF")
}),
)
assert.True(t, handled)
assert.EqualError(t, result, "handled EOF")
})
t.Run("HandleError with wrapped sentinel error", func(t *testing.T) {
wrappedErr := fmt.Errorf("wrapped: %w", io.EOF)
handled, result := HandleError(wrappedErr,
OnSentinelError(io.EOF, func(e error) error {
return errors.New("handled wrapped EOF")
}),
)
assert.True(t, handled)
assert.EqualError(t, result, "handled wrapped EOF")
})
t.Run("HandleError with custom error type match", func(t *testing.T) {
err := &CustomError{Code: 404, Message: "not found"}
handled, result := HandleError(err,
OnCustomError(func(e *CustomError) error {
return fmt.Errorf("handled custom error with code %d", e.Code)
}),
)
assert.True(t, handled)
assert.EqualError(t, result, "handled custom error with code 404")
})
t.Run("HandleError with wrapped custom error", func(t *testing.T) {
customErr := &CustomError{Code: 500, Message: "internal error"}
wrappedErr := fmt.Errorf("wrapped: %w", customErr)
handled, result := HandleError(wrappedErr,
OnCustomError(func(e *CustomError) error {
return fmt.Errorf("handled wrapped custom error: %s", e.Message)
}),
)
assert.True(t, handled)
assert.EqualError(t, result, "handled wrapped custom error: internal error")
})
t.Run("HandleError with no matching handler", func(t *testing.T) {
err := errors.New("unhandled error")
handled, result := HandleError(err,
OnSentinelError(io.EOF, func(e error) error {
return nil
}),
OnCustomError(func(e *CustomError) error {
return nil
}),
)
assert.False(t, handled)
assert.NoError(t, result)
})
t.Run("HandleError with multiple matchers first match wins", func(t *testing.T) {
err := io.EOF
handled, result := HandleError(err,
OnSentinelError(io.EOF, func(e error) error {
return errors.New("first handler")
}),
OnSentinelError(io.EOF, func(e error) error {
return errors.New("second handler")
}),
)
assert.True(t, handled)
assert.EqualError(t, result, "first handler")
})
t.Run("HandleError with handler returning nil", func(t *testing.T) {
err := io.EOF
handled, result := HandleError(err,
OnSentinelError(io.EOF, func(e error) error {
return nil
}),
)
assert.True(t, handled)
assert.NoError(t, result)
})
t.Run("HandleError with multiple error types", func(t *testing.T) {
customErr := &CustomError{Code: 400, Message: "bad request"}
validationErr := &ValidationError{Field: "email", Value: "invalid"}
// Test CustomError handling
handled1, result1 := HandleError(customErr,
OnCustomError(func(e *CustomError) error {
return fmt.Errorf("custom: %d", e.Code)
}),
OnCustomError(func(e *ValidationError) error {
return fmt.Errorf("validation: %s", e.Field)
}),
)
assert.True(t, handled1)
require.EqualError(t, result1, "custom: 400")
// Test ValidationError handling
handled2, result2 := HandleError(validationErr,
OnCustomError(func(e *CustomError) error {
return fmt.Errorf("custom: %d", e.Code)
}),
OnCustomError(func(e *ValidationError) error {
return fmt.Errorf("validation: %s", e.Field)
}),
)
assert.True(t, handled2)
assert.EqualError(t, result2, "validation: email")
})
t.Run("HandleError with context errors", func(t *testing.T) {
// Test context.Canceled
handled1, result1 := HandleError(context.Canceled,
OnSentinelError(context.Canceled, func(e error) error {
return errors.New("operation canceled")
}),
)
assert.True(t, handled1)
require.EqualError(t, result1, "operation canceled")
// Test context.DeadlineExceeded
handled2, result2 := HandleError(context.DeadlineExceeded,
OnSentinelError(context.DeadlineExceeded, func(e error) error {
return errors.New("deadline exceeded")
}),
)
assert.True(t, handled2)
assert.EqualError(t, result2, "deadline exceeded")
})
t.Run("HandleError preserves original error in handler", func(t *testing.T) {
originalErr := &CustomError{Code: 403, Message: "forbidden"}
var capturedErr error
handled, _ := HandleError(originalErr,
OnCustomError(func(e *CustomError) error {
capturedErr = e
return nil
}),
)
assert.True(t, handled)
assert.Equal(t, originalErr, capturedErr)
})
}
func TestHandleErrorOrDie(t *testing.T) {
t.Run("HandleErrorOrDie with nil error", func(t *testing.T) {
result := HandleErrorOrDie(nil)
assert.NoError(t, result)
})
t.Run("HandleErrorOrDie with matched error", func(t *testing.T) {
err := io.EOF
result := HandleErrorOrDie(err,
OnSentinelError(io.EOF, func(e error) error {
return errors.New("handled EOF in die")
}),
)
assert.EqualError(t, result, "handled EOF in die")
})
t.Run("HandleErrorOrDie panics on unmatched error", func(t *testing.T) {
err := errors.New("unmatched error")
assert.Panics(t, func() {
HandleErrorOrDie(err,
OnSentinelError(io.EOF, func(e error) error {
return nil
}),
)
})
})
t.Run("HandleErrorOrDie with custom error panic", func(t *testing.T) {
customErr := &CustomError{Code: 500, Message: "server error"}
assert.Panics(t, func() {
HandleErrorOrDie(customErr,
OnCustomError(func(e *ValidationError) error {
return nil
}),
)
})
})
t.Run("HandleErrorOrDie with multiple matchers", func(t *testing.T) {
validationErr := &ValidationError{Field: "username", Value: ""}
result := HandleErrorOrDie(validationErr,
OnSentinelError(io.EOF, func(e error) error {
return errors.New("EOF handler")
}),
OnCustomError(func(e *CustomError) error {
return errors.New("custom handler")
}),
OnCustomError(func(e *ValidationError) error {
return fmt.Errorf("validation handler: field=%s", e.Field)
}),
)
assert.EqualError(t, result, "validation handler: field=username")
})
}
func TestOnSentinelError(t *testing.T) {
t.Run("OnSentinelError creates proper matcher", func(t *testing.T) {
handler := func(e error) error { return e }
matcher := OnSentinelError(io.EOF, handler)
assert.Equal(t, io.EOF, matcher.ErrorType)
assert.True(t, matcher.IsSentinel)
assert.NotNil(t, matcher.Handler)
})
t.Run("OnSentinelError with custom sentinel", func(t *testing.T) {
customSentinel := errors.New("custom sentinel")
callCount := 0
matcher := OnSentinelError(customSentinel, func(e error) error {
callCount++
return errors.New("handled custom sentinel")
})
// Test that it matches the sentinel
handled, result := HandleError(customSentinel, matcher)
assert.True(t, handled)
require.EqualError(t, result, "handled custom sentinel")
assert.Equal(t, 1, callCount)
// Test that it matches wrapped sentinel
wrappedErr := fmt.Errorf("wrapped: %w", customSentinel)
handled, result = HandleError(wrappedErr, matcher)
assert.True(t, handled)
require.EqualError(t, result, "handled custom sentinel")
assert.Equal(t, 2, callCount)
})
}
func TestOnCustomError(t *testing.T) {
t.Run("OnCustomError creates proper matcher", func(t *testing.T) {
matcher := OnCustomError(func(e *CustomError) error {
return fmt.Errorf("handled: %d", e.Code)
})
assert.False(t, matcher.IsSentinel)
assert.NotNil(t, matcher.Handler)
// Test the handler works
err := &CustomError{Code: 200, Message: "ok"}
result := matcher.Handler(err)
assert.EqualError(t, result, "handled: 200")
})
t.Run("OnCustomError with different error types", func(t *testing.T) {
// Create matchers for different types
customMatcher := OnCustomError(func(e *CustomError) error {
return fmt.Errorf("custom error: code=%d", e.Code)
})
validationMatcher := OnCustomError(func(e *ValidationError) error {
return fmt.Errorf("validation error: field=%s", e.Field)
})
// Test with CustomError
customErr := &CustomError{Code: 404, Message: "not found"}
handled, result := HandleError(customErr, customMatcher, validationMatcher)
assert.True(t, handled)
require.EqualError(t, result, "custom error: code=404")
// Test with ValidationError
validationErr := &ValidationError{Field: "age", Value: "-1"}
handled, result = HandleError(validationErr, customMatcher, validationMatcher)
assert.True(t, handled)
assert.EqualError(t, result, "validation error: field=age")
})
t.Run("OnCustomError handler receives correct type", func(t *testing.T) {
var receivedErr *CustomError
matcher := OnCustomError(func(e *CustomError) error {
receivedErr = e
return nil
})
originalErr := &CustomError{Code: 301, Message: "redirect"}
handled, _ := HandleError(originalErr, matcher)
assert.True(t, handled)
require.NotNil(t, receivedErr)
assert.Equal(t, 301, receivedErr.Code)
assert.Equal(t, "redirect", receivedErr.Message)
})
}
func TestErrorMatcherEdgeCases(t *testing.T) {
t.Run("Invalid sentinel error type in matcher", func(t *testing.T) {
// Create a matcher with invalid ErrorType for sentinel
matcher := ErrorMatcher{
ErrorType: "not an error", // Invalid type
Handler: func(e error) error { return e },
IsSentinel: true,
}
err := errors.New("test error")
handled, result := HandleError(err, matcher)
assert.False(t, handled)
assert.NoError(t, result)
})
t.Run("Handler that panics", func(t *testing.T) {
matcher := OnSentinelError(io.EOF, func(e error) error {
panic("handler panic")
})
assert.Panics(t, func() {
HandleError(io.EOF, matcher)
})
})
t.Run("Complex error chain", func(t *testing.T) {
// Create a complex error chain
baseErr := &CustomError{Code: 500, Message: "base"}
wrapped1 := fmt.Errorf("layer1: %w", baseErr)
wrapped2 := fmt.Errorf("layer2: %w", wrapped1)
wrapped3 := fmt.Errorf("layer3: %w", wrapped2)
handled, result := HandleError(wrapped3,
OnCustomError(func(e *CustomError) error {
return fmt.Errorf("found custom error at code %d", e.Code)
}),
)
assert.True(t, handled)
assert.EqualError(t, result, "found custom error at code 500")
})
}

17
pkg/utils/print.go Normal file
View File

@@ -0,0 +1,17 @@
package utils
import (
"fmt"
"os"
"github.com/jedib0t/go-pretty/v6/text"
)
func PrintErr(color text.Color, format string, a ...any) {
fmt.Fprintln(os.Stderr, color.Sprintf(format, a...))
}
func PrintErrAndExit(color text.Color, exitCode int, format string, a ...any) {
PrintErr(color, format, a...)
os.Exit(exitCode)
}

250
pkg/utils/print_test.go Normal file
View File

@@ -0,0 +1,250 @@
package utils
import (
"bytes"
"context"
"io"
"os"
"os/exec"
"strings"
"testing"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPrintErr(t *testing.T) {
t.Run("PrintErr writes to stderr with color", func(t *testing.T) {
// Capture stderr
oldStderr := os.Stderr
reader, writer, _ := os.Pipe()
os.Stderr = writer
// Call PrintErr
PrintErr(text.FgRed, "Error: %s", "test error")
// Restore stderr and read output
writer.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
io.Copy(&buf, reader)
output := buf.String()
// The output should contain the message (color codes are included)
assert.Contains(t, output, "test error")
assert.Contains(t, output, "Error:")
assert.True(t, strings.HasSuffix(output, "\n"))
})
t.Run("PrintErr with multiple format arguments", func(t *testing.T) {
oldStderr := os.Stderr
reader, writer, _ := os.Pipe()
os.Stderr = writer
PrintErr(text.FgYellow, "Warning: %s at line %d", "issue", 42)
writer.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
io.Copy(&buf, reader)
output := buf.String()
assert.Contains(t, output, "Warning: issue at line 42")
})
t.Run("PrintErr with no format arguments", func(t *testing.T) {
oldStderr := os.Stderr
reader, writer, _ := os.Pipe()
os.Stderr = writer
PrintErr(text.FgGreen, "Simple message")
writer.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
io.Copy(&buf, reader)
output := buf.String()
assert.Contains(t, output, "Simple message")
assert.True(t, strings.HasSuffix(output, "\n"))
})
t.Run("PrintErr with different colors", func(t *testing.T) {
colors := []text.Color{
text.FgRed,
text.FgGreen,
text.FgYellow,
text.FgBlue,
text.FgMagenta,
text.FgCyan,
}
for _, color := range colors {
oldStderr := os.Stderr
reader, writer, _ := os.Pipe()
os.Stderr = writer
PrintErr(color, "Message with color")
writer.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
io.Copy(&buf, reader)
output := buf.String()
assert.Contains(t, output, "Message with color")
}
})
t.Run("PrintErr with empty string", func(t *testing.T) {
oldStderr := os.Stderr
reader, writer, _ := os.Pipe()
os.Stderr = writer
PrintErr(text.FgRed, "")
writer.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
io.Copy(&buf, reader)
output := buf.String()
assert.Equal(t, "\n", strings.TrimPrefix(output, "\x1b[31m\x1b[0m")) // Just newline after color codes
})
t.Run("PrintErr with special characters", func(t *testing.T) {
oldStderr := os.Stderr
reader, writer, _ := os.Pipe()
os.Stderr = writer
PrintErr(text.FgRed, "Special chars: %s", "!@#$%^&*()")
writer.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
io.Copy(&buf, reader)
output := buf.String()
assert.Contains(t, output, "Special chars: !@#$%^&*()")
})
t.Run("PrintErr with percent sign in message", func(t *testing.T) {
oldStderr := os.Stderr
reader, writer, _ := os.Pipe()
os.Stderr = writer
PrintErr(text.FgRed, "Progress: 100%% complete")
writer.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
io.Copy(&buf, reader)
output := buf.String()
assert.Contains(t, output, "Progress: 100% complete")
})
}
func TestPrintErrAndExit(t *testing.T) {
if os.Getenv("BE_CRASHER") == "1" {
// This is the subprocess that will actually call PrintErrAndExit
exitCode := 1
if code := os.Getenv("EXIT_CODE"); code != "" {
switch code {
case "0":
exitCode = 0
case "1":
exitCode = 1
case "2":
exitCode = 2
}
}
PrintErrAndExit(text.FgRed, exitCode, "Error: %s", "fatal error")
return
}
t.Run("PrintErrAndExit calls os.Exit with correct code", func(t *testing.T) {
testCases := []struct {
name string
exitCode int
}{
{"Exit with code 0", 0},
{"Exit with code 1", 1},
{"Exit with code 2", 2},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ctx := context.Background()
cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=TestPrintErrAndExit")
cmd.Env = append(os.Environ(),
"BE_CRASHER=1",
"EXIT_CODE="+string(rune('0'+testCase.exitCode)))
var stderr bytes.Buffer
cmd.Stderr = &stderr
err := cmd.Run()
if testCase.exitCode == 0 {
require.NoError(t, err)
} else {
require.Error(t, err)
if exitErr, ok := err.(*exec.ExitError); ok {
assert.Equal(t, testCase.exitCode, exitErr.ExitCode())
}
}
// Check that error message was printed to stderr
assert.Contains(t, stderr.String(), "Error: fatal error")
})
}
})
t.Run("PrintErrAndExit prints before exiting", func(t *testing.T) {
ctx := context.Background()
cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=TestPrintErrAndExit")
cmd.Env = append(os.Environ(), "BE_CRASHER=1", "EXIT_CODE=1")
var stderr bytes.Buffer
cmd.Stderr = &stderr
cmd.Run() // Ignore error since we expect non-zero exit
output := stderr.String()
assert.Contains(t, output, "Error: fatal error")
assert.True(t, strings.HasSuffix(output, "\n"))
})
}
// Benchmarks for performance testing
func BenchmarkPrintErr(b *testing.B) {
// Redirect stderr to /dev/null for benchmarking
oldStderr := os.Stderr
devNull, _ := os.Open(os.DevNull)
os.Stderr = devNull
defer func() {
os.Stderr = oldStderr
devNull.Close()
}()
b.Run("Simple message", func(b *testing.B) {
for range b.N {
PrintErr(text.FgRed, "Error message")
}
})
b.Run("Formatted message", func(b *testing.B) {
for range b.N {
PrintErr(text.FgRed, "Error: %s at line %d", "issue", 42)
}
})
b.Run("Different colors", func(b *testing.B) {
colors := []text.Color{text.FgRed, text.FgGreen, text.FgYellow}
for idx := range b.N {
PrintErr(colors[idx%len(colors)], "Message %d", idx)
}
})
}