summaryrefslogtreecommitdiff
path: root/assert
diff options
context:
space:
mode:
Diffstat (limited to 'assert')
-rw-r--r--assert/assert.go42
-rw-r--r--assert/equal.go16
-rw-r--r--assert/equal_test.go31
-rw-r--r--assert/errequal.go72
-rw-r--r--assert/errequal_test.go77
5 files changed, 238 insertions, 0 deletions
diff --git a/assert/assert.go b/assert/assert.go
new file mode 100644
index 0000000..a13c15e
--- /dev/null
+++ b/assert/assert.go
@@ -0,0 +1,42 @@
+// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
+// SPDX-License-Identifier: Apache-2.0
+
+// Package assert contains assertions for use in unit tests.
+// Each assertion in this package returns a bool to indicate whether the check succeeded, and logs a t.Error() when the check does not succeed.
+package assert
+
+import (
+ "context"
+ "io"
+ "testing"
+)
+
+// TestingTB contains all the public functions of [testing.TB] (as of Go 1.26).
+// Functions in this package use this type instead of [testing.TB] because the capture device used by package testcapture cannot implement [testing.TB]: It contains methods that are private to the standard library.
+type TestingTB interface {
+ ArtifactDir() string
+ Attr(key, value string)
+ Chdir(dir string)
+ Cleanup(func())
+ Context() context.Context
+ Error(args ...any)
+ Errorf(format string, args ...any)
+ Fail()
+ Failed() bool
+ FailNow()
+ Fatal(args ...any)
+ Fatalf(format string, args ...any)
+ Helper()
+ Log(args ...any)
+ Logf(format string, args ...any)
+ Name() string
+ Output() io.Writer
+ Setenv(key, value string)
+ Skip(args ...any)
+ Skipf(format string, args ...any)
+ SkipNow()
+ Skipped() bool
+ TempDir() string
+}
+
+var _ TestingTB = testing.TB(nil)
diff --git a/assert/equal.go b/assert/equal.go
new file mode 100644
index 0000000..1222ecf
--- /dev/null
+++ b/assert/equal.go
@@ -0,0 +1,16 @@
+// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
+// SPDX-License-Identifier: Apache-2.0
+
+package assert
+
+import "reflect"
+
+// Equal checks whether both supplied values are equal according to the rules of [reflect.DeepEqual].
+func Equal[V any](t TestingTB, actual, expected V) bool {
+ if reflect.DeepEqual(actual, expected) {
+ return true
+ }
+ t.Helper()
+ t.Errorf("expected %#v, but got %#v", expected, actual)
+ return false
+}
diff --git a/assert/equal_test.go b/assert/equal_test.go
new file mode 100644
index 0000000..c710cea
--- /dev/null
+++ b/assert/equal_test.go
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
+// SPDX-License-Identifier: Apache-2.0
+
+package assert_test
+
+import (
+ "strings"
+ "testing"
+
+ "go.xyrillian.de/gg/assert"
+ "go.xyrillian.de/gg/testcapture"
+)
+
+func expectErrors(t *testing.T, test func(assert.TestingTB), expected string) {
+ t.Helper()
+ r := testcapture.Result{Outcome: testcapture.OutcomeFailed}
+ for line := range strings.SplitSeq(expected, "\n") {
+ line = strings.TrimSpace(line)
+ if line != "" {
+ r.Messages = append(r.Messages, testcapture.Log(line))
+ }
+ }
+ assert.Equal(t, testcapture.Capture(t.Context(), t.Name(), test), r)
+}
+
+func TestEqual(t *testing.T) {
+ assert.Equal(t, true, true)
+ expectErrors(t, func(t assert.TestingTB) {
+ assert.Equal(t, false, true)
+ }, `expected true, but got false`)
+}
diff --git a/assert/errequal.go b/assert/errequal.go
new file mode 100644
index 0000000..4752f26
--- /dev/null
+++ b/assert/errequal.go
@@ -0,0 +1,72 @@
+// 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 {
+ // 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
+ if reflect.ValueOf(expectedErr).IsNil() {
+ expected = nil
+ } else {
+ expected = expectedErr
+ }
+ }
+
+ switch expected := expected.(type) {
+ case nil:
+ if actual == nil {
+ return true
+ } else {
+ t.Errorf("expected no error, but got %q", actual.Error())
+ return false
+ }
+ case error:
+ if actual == nil {
+ t.Errorf("expected %q, but got no error", expected.Error())
+ return false
+ } else if errors.Is(actual, expected) {
+ return true
+ } else {
+ t.Errorf("expected %q, but got %q", expected.Error(), actual.Error())
+ return false
+ }
+ case string:
+ if actual == nil {
+ t.Errorf("expected %q, but got no error", expected)
+ return false
+ } else if actual.Error() == expected {
+ return true
+ } else {
+ t.Errorf("expected %q, but got %q", expected, actual.Error())
+ return false
+ }
+ case *regexp.Regexp:
+ if actual == nil {
+ t.Errorf("expected an error matching /%s/, but got no error", expected.String())
+ return false
+ } else if expected.MatchString(actual.Error()) {
+ return true
+ } else {
+ t.Errorf("expected an error matching /%s/, but got %q", expected.String(), actual.Error())
+ return false
+ }
+ default:
+ panic(fmt.Sprintf("cannot handle `expected` of type %T", expected))
+ }
+}
diff --git a/assert/errequal_test.go b/assert/errequal_test.go
new file mode 100644
index 0000000..5efbdc5
--- /dev/null
+++ b/assert/errequal_test.go
@@ -0,0 +1,77 @@
+// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
+// SPDX-License-Identifier: Apache-2.0
+
+package assert_test
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "regexp"
+ "testing"
+
+ "go.xyrillian.de/gg/assert"
+ "go.xyrillian.de/gg/testcapture"
+)
+
+func TestErrEqual(t *testing.T) {
+ noError := error(nil)
+ fooError := errors.New("foo error")
+ barError := errors.New("bar error")
+ nestedFooError := fmt.Errorf("nested error: %w", fooError)
+
+ // test matching against nil
+ assert.ErrEqual(t, noError, nil)
+ assert.ErrEqual(t, noError, (*os.PathError)(nil))
+ expectErrors(t, func(t assert.TestingTB) {
+ assert.ErrEqual(t, fooError, nil)
+ }, `expected no error, but got "foo error"`)
+ expectErrors(t, func(t assert.TestingTB) {
+ assert.ErrEqual(t, fooError, (*os.PathError)(nil))
+ }, `expected no error, but got "foo error"`)
+
+ // test matching against error
+ assert.ErrEqual(t, fooError, fooError)
+ assert.ErrEqual(t, nestedFooError, fooError)
+ expectErrors(t, func(t assert.TestingTB) {
+ assert.ErrEqual(t, fooError, nestedFooError) // error nesting does not work the other way, we need to see the full `expected`error in`actual`
+ }, `expected "nested error: foo error", but got "foo error"`)
+ expectErrors(t, func(t assert.TestingTB) {
+ assert.ErrEqual(t, barError, fooError) // check with unrelated errors for completeness
+ }, `expected "foo error", but got "bar error"`)
+ expectErrors(t, func(t assert.TestingTB) {
+ assert.ErrEqual(t, nil, fooError)
+ }, `expected "foo error", but got no error`)
+
+ // test matching against string
+ assert.ErrEqual(t, fooError, "foo error")
+ expectErrors(t, func(t assert.TestingTB) {
+ assert.ErrEqual(t, nestedFooError, "foo error") // partial matches do not work
+ }, `expected "foo error", but got "nested error: foo error"`)
+ expectErrors(t, func(t assert.TestingTB) {
+ assert.ErrEqual(t, barError, "foo error") // check with unrelated errors for completeness
+ }, `expected "foo error", but got "bar error"`)
+ expectErrors(t, func(t assert.TestingTB) {
+ assert.ErrEqual(t, nil, "foo error")
+ }, `expected "foo error", but got no error`)
+
+ // test matching against regexp
+ fooRegexp := regexp.MustCompile(`foo`)
+ assert.ErrEqual(t, fooError, fooRegexp) // partial matches allowed here as long as regexp does not use ^ and $
+ assert.ErrEqual(t, nestedFooError, fooRegexp)
+ expectErrors(t, func(t assert.TestingTB) {
+ assert.ErrEqual(t, barError, fooRegexp) // check with unrelated errors for completeness
+ }, `expected an error matching /foo/, but got "bar error"`)
+ expectErrors(t, func(t assert.TestingTB) {
+ assert.ErrEqual(t, nil, fooRegexp)
+ }, `expected an error matching /foo/, but got no error`)
+
+ // test matching against unexpected type
+ result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) {
+ assert.ErrEqual(t, errors.New("42"), 42)
+ })
+ assert.Equal(t, result, testcapture.Result{
+ Outcome: testcapture.OutcomePanicked,
+ Panic: "cannot handle `expected` of type int",
+ })
+}