summaryrefslogtreecommitdiff
path: root/assert/errequal.go
blob: a297ead672a9d036a703ae0b6e8d80d2b696be53 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
// SPDX-License-Identifier: Apache-2.0

package assert

import (
	"errors"
	"fmt"
	"reflect"
	"regexp"
)

// ErrEqual checks if the actual error matches the expectation.
//   - If expected is nil, the actual error must be nil.
//   - If expected is of type error, the actual error must be exactly equal to it or contain it, as reported by the [errors.Is] function.
//   - If expected is of type string, the actual error must have a message exactly equal to it.
//   - If expected is of type [*regexp.Regexp], the actual error must have a message matching that regexp.
//   - If expected is of any other type, ErrEqual will panic.
func ErrEqual(t TestingTB, actual error, expected any) bool {
	t.Helper()
	err := errEqual(t, actual, expected)
	if err == nil {
		return true
	} else {
		t.Error(err)
		return false
	}
}

// ErrsEqual checks if a list of actual errors matches the expectation.
// Both lists must be of equal length, and each individual entry must match according to [ErrEqual].
func ErrsEqual[A ~[]error, E ~[]V, V any](t TestingTB, actual A, expected E) bool {
	t.Helper()
	ok := true
	for idx := range max(len(actual), len(expected)) {
		var err error
		switch {
		case idx >= len(actual):
			err = errEqual(t, missingError{}, expected[idx])
		case idx >= len(expected):
			err = errEqual(t, actual[idx], missingError{})
		default:
			err = errEqual(t, actual[idx], expected[idx])
		}
		if err != nil {
			t.Errorf("in actual[%d]: %s", idx, err)
			ok = false
		}
	}
	return ok
}

// missingError is used by ErrsEqual() to mark a missing error on one side of the match.
type missingError struct{}

func (missingError) Error() string {
	return "<missing>"
}

func errEqual(t TestingTB, actual error, expected any) error {
	// coerce all types that implement `error` into the interface type `error`,
	// and also coerce all nil values of concrete `error` types into untyped nil
	if expectedErr, ok := expected.(error); ok {
		// convert nil values of concrete error types into a generic nil value
		expectedValue := reflect.ValueOf(expectedErr)
		kind := expectedValue.Kind()
		if (kind == reflect.Pointer || kind == reflect.Interface) && expectedValue.IsNil() {
			expected = nil
		} else {
			expected = expectedErr
		}
	}

	switch expected := expected.(type) {
	case nil:
		if actual == nil {
			return nil
		} else {
			return fmt.Errorf("expected no error, but got %s", formatErrorMessage(actual))
		}
	case error:
		if actual == nil {
			return fmt.Errorf("expected %s, but got no error", formatErrorMessage(expected))
		} else if errors.Is(actual, expected) {
			return nil
		} else {
			return fmt.Errorf("expected %s, but got %q", formatErrorMessage(expected), actual.Error())
		}
	case string:
		if actual == nil {
			return fmt.Errorf("expected %q, but got no error", expected)
		} else if actual.Error() == expected {
			return nil
		} else {
			return fmt.Errorf("expected %q, but got %s", expected, formatErrorMessage(actual))
		}
	case *regexp.Regexp:
		if actual == nil {
			return fmt.Errorf("expected an error matching /%s/, but got no error", expected.String())
		} else if expected.MatchString(actual.Error()) {
			return nil
		} else {
			return fmt.Errorf("expected an error matching /%s/, but got %s", expected.String(), formatErrorMessage(actual))
		}
	default:
		panic(fmt.Sprintf("cannot handle `expected` of type %T", expected))
	}
}

func formatErrorMessage(err error) string {
	if _, ok := err.(missingError); ok { //nolint:errorlint // this error is never wrapped, so errors.Is() is unnecessary
		return err.Error()
	} else {
		return fmt.Sprintf("%q", err.Error())
	}
}