aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Majewsky <majewsky@gmx.net>2026-08-18 11:30:54 +0200
committerStefan Majewsky <majewsky@gmx.net>2026-08-18 11:30:54 +0200
commit50ba9cfb04e257fafa3fdd566f9f70a1986339e8 (patch)
treeb4601a46bd2c9b90297e8055bec7d60afc07cdb1
parent22e8f3548a72c78deccedc5a65c1cba029f52e6d (diff)
downloadgo-gg-50ba9cfb04e257fafa3fdd566f9f70a1986339e8.tar.gz
assert: add Panics, PanicsWith
I want to use this for test coverage in microprom. This turned out to be a bigger change than expected because testcapture has a dependency on assert, which I had to invert to be able to use testcapture.Capture() in assert.
-rw-r--r--CHANGELOG.md7
-rw-r--r--assert/assert.go37
-rw-r--r--assert/errequal_test.go8
-rw-r--r--assert/panic.go32
-rw-r--r--assert/panic_test.go35
-rw-r--r--pathrouter/pathrouter_test.go8
-rw-r--r--testcapture/capture.go59
-rw-r--r--testcapture/testcapture.go43
8 files changed, 152 insertions, 77 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4033682..d48fd23 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,13 @@ SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
SPDX-License-Identifier: Apache-2.0
-->
+# v1.14.0 (TBD)
+
+Changes:
+
+- Add package microprom.
+- Add `assert.Panics()` and `assert.PanicsWith()`.
+
# v1.13.3 (2026-08-05)
Changes:
diff --git a/assert/assert.go b/assert/assert.go
index a13c15e..fe725f6 100644
--- a/assert/assert.go
+++ b/assert/assert.go
@@ -5,38 +5,9 @@
// 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"
-)
+import "go.xyrillian.de/gg/testcapture"
// 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)
+// Functions in this package use this type instead of [testing.TB] to allow
+// mocks of [testing.TB] to be substituted in tests for this package.
+type TestingTB = testcapture.TestingTB
diff --git a/assert/errequal_test.go b/assert/errequal_test.go
index b97342a..2cefeab 100644
--- a/assert/errequal_test.go
+++ b/assert/errequal_test.go
@@ -11,7 +11,6 @@ import (
"testing"
"go.xyrillian.de/gg/assert"
- "go.xyrillian.de/gg/testcapture"
)
func TestErrEqual(t *testing.T) {
@@ -67,13 +66,10 @@ func TestErrEqual(t *testing.T) {
}, `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) {
+ result := assert.PanicsWith[string](t, func() {
assert.ErrEqual(t, errors.New("42"), 42)
})
- assert.Equal(t, result, testcapture.Result{
- Outcome: testcapture.OutcomePanicked,
- Panic: "cannot handle `expected` of type int",
- })
+ assert.Equal(t, result, "cannot handle `expected` of type int")
// an earlier version had a bug because this call caused reflect.Value.IsNil() to be called on a value of kind Struct
expectErrors(t, func(t assert.TestingTB) {
diff --git a/assert/panic.go b/assert/panic.go
new file mode 100644
index 0000000..69828a4
--- /dev/null
+++ b/assert/panic.go
@@ -0,0 +1,32 @@
+// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
+// SPDX-License-Identifier: Apache-2.0
+
+package assert
+
+import (
+ "go.xyrillian.de/gg/testcapture"
+)
+
+// Panics runs the provided action and fails the test if it does not panic.
+// On success, the error value passed to the call of panic is returned.
+func Panics(t TestingTB, action func()) any {
+ return PanicsWith[any](t, action)
+}
+
+// PanicsWith is like Panics(), but also checks if the recovered error value
+// is of type T, failing the test if the type assertion fails.
+func PanicsWith[T any](t TestingTB, action func()) T {
+ t.Helper()
+ result := testcapture.Capture(t.Context(), t.Name(), func(_ TestingTB) {
+ action()
+ })
+ if result.Outcome != testcapture.OutcomePanicked {
+ t.Fatal("did not panic")
+ }
+ value, ok := result.Panic.(T)
+ if !ok {
+ var zero T
+ t.Fatalf("panicked with incorrect type: expected %T, but got %#v", zero, result.Panic)
+ }
+ return value
+}
diff --git a/assert/panic_test.go b/assert/panic_test.go
new file mode 100644
index 0000000..135ce18
--- /dev/null
+++ b/assert/panic_test.go
@@ -0,0 +1,35 @@
+// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
+// SPDX-License-Identifier: Apache-2.0
+
+package assert_test
+
+import (
+ "testing"
+
+ "go.xyrillian.de/gg/assert"
+ "go.xyrillian.de/gg/testcapture"
+)
+
+func TestPanics(t *testing.T) {
+ //only testing error cases here, coverage of the happy path is provided by
+ //usage of assert.Panics() in actual tests of other packages
+ tc := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) {
+ assert.Panics(t, func() {
+ // no panic
+ })
+ })
+ assert.Equal(t, tc.Outcome, testcapture.OutcomeFailed)
+ assert.Equal(t, tc.Messages, []testcapture.Message{
+ testcapture.Log("did not panic"),
+ })
+
+ tc = testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) {
+ assert.PanicsWith[string](t, func() {
+ panic(42)
+ })
+ })
+ assert.Equal(t, tc.Outcome, testcapture.OutcomeFailed)
+ assert.Equal(t, tc.Messages, []testcapture.Message{
+ testcapture.Log("panicked with incorrect type: expected string, but got 42"),
+ })
+}
diff --git a/pathrouter/pathrouter_test.go b/pathrouter/pathrouter_test.go
index 3e91ce9..54108ac 100644
--- a/pathrouter/pathrouter_test.go
+++ b/pathrouter/pathrouter_test.go
@@ -14,7 +14,6 @@ import (
"go.xyrillian.de/gg/assert"
pr "go.xyrillian.de/gg/pathrouter"
- "go.xyrillian.de/gg/testcapture"
)
func TestRouting(t *testing.T) {
@@ -157,11 +156,8 @@ func TestRouting(t *testing.T) {
func TestPanics(t *testing.T) {
check := func(expected string, action func()) {
t.Helper()
- result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { action() })
- assert.Equal(t, result, testcapture.Result{
- Outcome: testcapture.OutcomePanicked,
- Panic: expected,
- })
+ actual := assert.PanicsWith[string](t, action)
+ assert.Equal(t, actual, expected)
}
check(`matcher within CatchAllVariable() may not accept unlimited path lengths`, func() {
diff --git a/testcapture/capture.go b/testcapture/capture.go
index 3ac4f52..fd98c27 100644
--- a/testcapture/capture.go
+++ b/testcapture/capture.go
@@ -1,9 +1,6 @@
// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
// SPDX-License-Identifier: Apache-2.0
-// Package testcapture contains [Capture], a function that executes test code in a way that captures error messages and side effects without failing the overall test.
-//
-// The main intended use case is testing test assertions where calls to e.g. t.Error() are an expected part of a successful test run.
package testcapture
import (
@@ -17,8 +14,6 @@ import (
"slices"
"sync"
"sync/atomic"
-
- "go.xyrillian.de/gg/assert"
)
// Result is returned by func [Capture].
@@ -81,11 +76,11 @@ const (
MessageTypeOutput MessageType = "output"
)
-// Capture executes a test function with a stub implementation of [assert.TestingTB] that captures all calls to it.
+// Capture executes a test function with a stub implementation of [TestingTB] that captures all calls to it.
// It is intended for unit-testing test assertions.
//
// The name argument is what will be reported in t.Name() within the test.
-func Capture(ctx context.Context, name string, test func(assert.TestingTB)) Result {
+func Capture(ctx context.Context, name string, test func(TestingTB)) Result {
r := Result{
Outcome: OutcomeFinished, // can be overridden by Fail() or SkipNow()
}
@@ -93,7 +88,7 @@ func Capture(ctx context.Context, name string, test func(assert.TestingTB)) Resu
return r
}
-// capturer is the implementation of [assert.TestingTB] used by func [Capture].
+// capturer is the implementation of [TestingTB] used by func [Capture].
type capturer struct {
context context.Context
cleanups []func()
@@ -109,7 +104,7 @@ type capturer struct {
nonlocalMutex sync.Mutex // lock for non-local effects like Setenv() or filesystem operations
}
-func executeCapture(ctx context.Context, name string, r *Result, test func(assert.TestingTB)) {
+func executeCapture(ctx context.Context, name string, r *Result, test func(TestingTB)) {
ctx, cancel := context.WithCancel(ctx)
t := capturer{
context: ctx,
@@ -196,7 +191,7 @@ func collectArtifacts(dirPath string) (map[string]string, error) {
return result, os.RemoveAll(dirPath)
}
-// ArtifactDir implements the [assert.TestingTB] interface.
+// ArtifactDir implements the [TestingTB] interface.
func (t *capturer) ArtifactDir() string {
t.stateMutex.Lock()
defer t.stateMutex.Unlock()
@@ -221,7 +216,7 @@ func (t *capturer) ArtifactDir() string {
return t.state.ArtifactDir
}
-// Attr implements the [assert.TestingTB] interface.
+// Attr implements the [TestingTB] interface.
func (t *capturer) Attr(key, value string) {
t.resultMutex.Lock()
defer t.resultMutex.Unlock()
@@ -231,7 +226,7 @@ func (t *capturer) Attr(key, value string) {
t.result.Attrs[key] = value
}
-// Chdir implements the [assert.TestingTB] interface.
+// Chdir implements the [TestingTB] interface.
func (t *capturer) Chdir(dir string) {
t.doChdir(dir)
@@ -269,82 +264,82 @@ func (t *capturer) doChdir(dir string) {
})
}
-// Cleanup implements the [assert.TestingTB] interface.
+// Cleanup implements the [TestingTB] interface.
func (t *capturer) Cleanup(action func()) {
t.cleanupsMutex.Lock()
defer t.cleanupsMutex.Unlock()
t.cleanups = append(t.cleanups, action)
}
-// Context implements the [assert.TestingTB] interface.
+// Context implements the [TestingTB] interface.
func (t *capturer) Context() context.Context {
return t.context
}
-// Error implements the [assert.TestingTB] interface.
+// Error implements the [TestingTB] interface.
func (t *capturer) Error(args ...any) {
t.Log(args...)
t.Fail()
}
-// Errorf implements the [assert.TestingTB] interface.
+// Errorf implements the [TestingTB] interface.
func (t *capturer) Errorf(format string, args ...any) {
t.Logf(format, args...)
t.Fail()
}
-// Fail implements the [assert.TestingTB] interface.
+// Fail implements the [TestingTB] interface.
func (t *capturer) Fail() {
t.resultMutex.Lock()
defer t.resultMutex.Unlock()
t.result.Outcome = OutcomeFailed
}
-// Failed implements the [assert.TestingTB] interface.
+// Failed implements the [TestingTB] interface.
func (t *capturer) Failed() bool {
t.resultMutex.RLock()
defer t.resultMutex.RUnlock()
return t.result.Outcome == OutcomeFailed
}
-// FailNow implements the [assert.TestingTB] interface.
+// FailNow implements the [TestingTB] interface.
func (t *capturer) FailNow() {
panic(OutcomeFailed)
}
-// Fatal implements the [assert.TestingTB] interface.
+// Fatal implements the [TestingTB] interface.
func (t *capturer) Fatal(args ...any) {
t.Log(args...)
t.FailNow()
}
-// Fatalf implements the [assert.TestingTB] interface.
+// Fatalf implements the [TestingTB] interface.
func (t *capturer) Fatalf(format string, args ...any) {
t.Logf(format, args...)
t.FailNow()
}
-// Helper implements the [assert.TestingTB] interface.
+// Helper implements the [TestingTB] interface.
func (t *capturer) Helper() {
// no-op because we do not collect file and line information at the moment
}
-// Log implements the [assert.TestingTB] interface.
+// Log implements the [TestingTB] interface.
func (t *capturer) Log(args ...any) {
t.pushOutput(fmt.Append(nil, args...), MessageTypeLog)
}
-// Logf implements the [assert.TestingTB] interface.
+// Logf implements the [TestingTB] interface.
func (t *capturer) Logf(format string, args ...any) {
t.pushOutput(fmt.Appendf(nil, format, args...), MessageTypeLog)
}
-// Name implements the [assert.TestingTB] interface.
+// Name implements the [TestingTB] interface.
func (t *capturer) Name() string {
return t.name
}
-// Output implements the [assert.TestingTB] interface.
+// Output implements the [TestingTB] interface.
func (t *capturer) Output() io.Writer {
return outputCapturer{t}
}
@@ -359,7 +354,7 @@ func (c outputCapturer) Write(buf []byte) (int, error) {
return len(buf), nil
}
-// Setenv implements the [assert.TestingTB] interface.
+// Setenv implements the [TestingTB] interface.
func (t *capturer) Setenv(key, value string) {
t.nonlocalMutex.Lock()
defer t.nonlocalMutex.Unlock()
@@ -376,31 +371,31 @@ func (t *capturer) Setenv(key, value string) {
})
}
-// Skip implements the [assert.TestingTB] interface.
+// Skip implements the [TestingTB] interface.
func (t *capturer) Skip(args ...any) {
t.Log(args...)
t.SkipNow()
}
-// Skipf implements the [assert.TestingTB] interface.
+// Skipf implements the [TestingTB] interface.
func (t *capturer) Skipf(format string, args ...any) {
t.Logf(format, args...)
t.SkipNow()
}
-// SkipNow implements the [assert.TestingTB] interface.
+// SkipNow implements the [TestingTB] interface.
func (t *capturer) SkipNow() {
panic(OutcomeSkipped)
}
-// Skipped implements the [assert.TestingTB] interface.
+// Skipped implements the [TestingTB] interface.
func (t *capturer) Skipped() bool {
t.resultMutex.RLock()
defer t.resultMutex.RUnlock()
return t.result.Outcome == OutcomeSkipped
}
-// TempDir implements the [assert.TestingTB] interface.
+// TempDir implements the [TestingTB] interface.
func (t *capturer) TempDir() string {
path, err := pickTempdir()
if err != nil {
diff --git a/testcapture/testcapture.go b/testcapture/testcapture.go
new file mode 100644
index 0000000..b0c0fac
--- /dev/null
+++ b/testcapture/testcapture.go
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
+// SPDX-License-Identifier: Apache-2.0
+
+// Package testcapture contains [Capture], a function that executes test code in a way that captures error messages and side effects without failing the overall test.
+//
+// The main intended use case is testing test assertions where calls to e.g. t.Error() are an expected part of a successful test run.
+package testcapture
+
+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)