diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2026-08-18 11:30:54 +0200 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2026-08-18 11:30:54 +0200 |
| commit | 50ba9cfb04e257fafa3fdd566f9f70a1986339e8 (patch) | |
| tree | b4601a46bd2c9b90297e8055bec7d60afc07cdb1 | |
| parent | 22e8f3548a72c78deccedc5a65c1cba029f52e6d (diff) | |
| download | go-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.md | 7 | ||||
| -rw-r--r-- | assert/assert.go | 37 | ||||
| -rw-r--r-- | assert/errequal_test.go | 8 | ||||
| -rw-r--r-- | assert/panic.go | 32 | ||||
| -rw-r--r-- | assert/panic_test.go | 35 | ||||
| -rw-r--r-- | pathrouter/pathrouter_test.go | 8 | ||||
| -rw-r--r-- | testcapture/capture.go | 59 | ||||
| -rw-r--r-- | testcapture/testcapture.go | 43 |
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) |
