diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2026-06-18 23:23:36 +0200 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2026-06-19 20:16:11 +0200 |
| commit | 71d31d73e7dc3b9cdb3c53b4f343b1928a9e858b (patch) | |
| tree | e1cd6d01837ef6ef07504ba3893c6f5e55f8740b | |
| parent | b95ec7449ebec844221f80ecbf7976f3af878eee (diff) | |
| download | go-gg-71d31d73e7dc3b9cdb3c53b4f343b1928a9e858b.tar.gz | |
add package testcapture, package assert (minimal implementation)
| -rw-r--r-- | .golangci.yaml | 3 | ||||
| -rw-r--r-- | CHANGELOG.md | 4 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | assert/assert.go | 42 | ||||
| -rw-r--r-- | assert/equal.go | 16 | ||||
| -rw-r--r-- | assert/equal_test.go | 31 | ||||
| -rw-r--r-- | assert/errequal.go | 72 | ||||
| -rw-r--r-- | assert/errequal_test.go | 77 | ||||
| -rw-r--r-- | assetembed/assetembed_test.go | 36 | ||||
| -rw-r--r-- | columnar/columnar_test.go | 10 | ||||
| -rw-r--r-- | internal/test/helpers.go | 21 | ||||
| -rw-r--r-- | is/is_test.go | 58 | ||||
| -rw-r--r-- | jsonmatch/diff_test.go | 56 | ||||
| -rw-r--r-- | option/option_test.go | 144 | ||||
| -rw-r--r-- | options/options_test.go | 72 | ||||
| -rw-r--r-- | testcapture/capture.go | 416 | ||||
| -rw-r--r-- | testcapture/capture_test.go | 403 |
17 files changed, 1253 insertions, 210 deletions
diff --git a/.golangci.yaml b/.golangci.yaml index 3a37237..12e9cb8 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -70,6 +70,9 @@ linters: gomoddirectives: toolchain-forbidden: true go-version-pattern: 1\.\d+(\.0)?$ + gosec: + excludes: + - G306 # gosec does not understand how umask works nolintlint: require-specific: true staticcheck: diff --git a/CHANGELOG.md b/CHANGELOG.md index 405af16..38b72cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ SPDX-License-Identifier: Apache-2.0 Changes: -- The minimum Go version was increased from 1.24 to 1.26. +- Add packages assert and testcapture. +- The minimum Go version was increased from 1.24 to 1.26 + because `assert.TestingTB` covers methods added to `testing.TB` in Go 1.26. # v1.9.1 (2026-06-17) @@ -22,7 +22,9 @@ My personal extension of the standard library. ### Addons for testing +- [assert](./assert/): assertions for use in unit tests, plus a testing harness for test assertions like the ones in this package - [jsonmatch](./jsonmatch/): matching of encoded JSON payloads against fixed assertions +- [testcapture](./testcapture/): execute test code in a way that captures error messages and side effects without failing the overall test (e.g. to unit-test test assertions themselves) ## How to contribute 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", + }) +} diff --git a/assetembed/assetembed_test.go b/assetembed/assetembed_test.go index 916d4dc..b5bea37 100644 --- a/assetembed/assetembed_test.go +++ b/assetembed/assetembed_test.go @@ -13,8 +13,8 @@ import ( "testing/fstest" "time" + "go.xyrillian.de/gg/assert" "go.xyrillian.de/gg/assetembed" - . "go.xyrillian.de/gg/internal/test" . "go.xyrillian.de/gg/option" ) @@ -48,15 +48,15 @@ func TestAssetembed(t *testing.T) { return base64str } fooTxtPath := fmt.Sprintf("foo-%s.txt", urlSafe(fooSHA384)) - AssertEqual(t, h.AssetPath("foo.txt"), Some(fooTxtPath)) + assert.Equal(t, h.AssetPath("foo.txt"), Some(fooTxtPath)) barJSPath := fmt.Sprintf("res/assets/bar-%s.js", urlSafe(barSHA384)) - AssertEqual(t, h.AssetPath("res/assets/bar.js"), Some(barJSPath)) - AssertEqual(t, h.AssetPath("res/assets/unknown.css"), None[string]()) + assert.Equal(t, h.AssetPath("res/assets/bar.js"), Some(barJSPath)) + assert.Equal(t, h.AssetPath("res/assets/unknown.css"), None[string]()) // test Handler.SubresourceIntegrity() - AssertEqual(t, h.SubresourceIntegrity("foo.txt"), Some("sha384-"+fooSHA384)) - AssertEqual(t, h.SubresourceIntegrity("res/assets/bar.js"), Some("sha384-"+barSHA384)) - AssertEqual(t, h.SubresourceIntegrity("res/assets/unknown.css"), None[string]()) + assert.Equal(t, h.SubresourceIntegrity("foo.txt"), Some("sha384-"+fooSHA384)) + assert.Equal(t, h.SubresourceIntegrity("res/assets/bar.js"), Some("sha384-"+barSHA384)) + assert.Equal(t, h.SubresourceIntegrity("res/assets/unknown.css"), None[string]()) // test HTTP handler for _, method := range []string{http.MethodHead, http.MethodGet} { @@ -76,26 +76,26 @@ func TestAssetembed(t *testing.T) { } resp := probe("/" + fooTxtPath) - AssertEqual(t, resp.Code, http.StatusOK) - AssertEqual(t, resp.Header().Get("Content-Type"), "text/plain; charset=utf-8") - AssertEqual(t, resp.Header().Get("Content-Length"), strconv.Itoa(len(fooTxt))) - AssertEqual(t, resp.Body.String(), ifGet(fooTxt)) + assert.Equal(t, resp.Code, http.StatusOK) + assert.Equal(t, resp.Header().Get("Content-Type"), "text/plain; charset=utf-8") + assert.Equal(t, resp.Header().Get("Content-Length"), strconv.Itoa(len(fooTxt))) + assert.Equal(t, resp.Body.String(), ifGet(fooTxt)) resp = probe("/" + barJSPath) - AssertEqual(t, resp.Code, http.StatusOK) - AssertEqual(t, resp.Header().Get("Content-Type"), "text/javascript; charset=utf-8") - AssertEqual(t, resp.Header().Get("Content-Length"), strconv.Itoa(len(barJS))) - AssertEqual(t, resp.Body.String(), ifGet(barJS)) + assert.Equal(t, resp.Code, http.StatusOK) + assert.Equal(t, resp.Header().Get("Content-Type"), "text/javascript; charset=utf-8") + assert.Equal(t, resp.Header().Get("Content-Length"), strconv.Itoa(len(barJS))) + assert.Equal(t, resp.Body.String(), ifGet(barJS)) resp = probe("/foo.txt") // without digest! - AssertEqual(t, resp.Code, http.StatusNotFound) + assert.Equal(t, resp.Code, http.StatusNotFound) resp = probe("/res/assets/unknown.css") // entirely unknown file - AssertEqual(t, resp.Code, http.StatusNotFound) + assert.Equal(t, resp.Code, http.StatusNotFound) } req := httptest.NewRequest(http.MethodPut, "/"+fooTxtPath, http.NoBody) resp := httptest.NewRecorder() h.ServeHTTP(resp, req) - AssertEqual(t, resp.Code, http.StatusMethodNotAllowed) + assert.Equal(t, resp.Code, http.StatusMethodNotAllowed) } diff --git a/columnar/columnar_test.go b/columnar/columnar_test.go index 3e050ce..2804ac9 100644 --- a/columnar/columnar_test.go +++ b/columnar/columnar_test.go @@ -8,8 +8,8 @@ import ( "sync" "testing" + "go.xyrillian.de/gg/assert" "go.xyrillian.de/gg/columnar" - . "go.xyrillian.de/gg/internal/test" "go.xyrillian.de/gg/jsonmatch" ) @@ -27,7 +27,7 @@ func testSuccessfulJSONRoundtrip[V any](t *testing.T, list []V, encoded jsonmatc if err != nil { t.Fatal(err) } - AssertEqual(t, []V(unmarshaled), list) + assert.Equal(t, []V(unmarshaled), list) } func TestJSONRoundtripBasic(t *testing.T) { @@ -102,9 +102,9 @@ func TestJSONRoundtripErrors(t *testing.T) { } _, err := json.Marshal(columnar.List[nothingPublic]{{1, 2}}) - AssertEqual(t, err.Error(), `json: error calling MarshalJSON for type columnar.List[go.xyrillian.de/gg/columnar_test.nothingPublic·5]: columnar_test.nothingPublic has no exported fields`) + assert.Equal(t, err.Error(), `json: error calling MarshalJSON for type columnar.List[go.xyrillian.de/gg/columnar_test.nothingPublic·5]: columnar_test.nothingPublic has no exported fields`) err = json.Unmarshal([]byte(`{}`), new(columnar.List[nothingPublic]{})) - AssertEqual(t, err.Error(), `columnar_test.nothingPublic has no exported fields`) + assert.Equal(t, err.Error(), `columnar_test.nothingPublic has no exported fields`) } func TestJSONUnmarshalFromInconsistentLengths(t *testing.T) { @@ -114,7 +114,7 @@ func TestJSONUnmarshalFromInconsistentLengths(t *testing.T) { } err := json.Unmarshal([]byte(`{"Foo":[1,2],"Bar":[3,4,5]}`), new(columnar.List[Record]{})) - AssertEqual(t, err.Error(), `cannot unmarshal from columns with inconsistent lengths [2 3]`) + assert.Equal(t, err.Error(), `cannot unmarshal from columns with inconsistent lengths [2 3]`) } func TestIgnoredFields(t *testing.T) { diff --git a/internal/test/helpers.go b/internal/test/helpers.go deleted file mode 100644 index 4cfca1e..0000000 --- a/internal/test/helpers.go +++ /dev/null @@ -1,21 +0,0 @@ -/******************************************************************************* -* Copyright 2025 Stefan Majewsky <majewsky@gmx.net> -* SPDX-License-Identifier: Apache-2.0 -* Refer to the file "LICENSE" for details. -*******************************************************************************/ - -package test - -import ( - "reflect" - "testing" -) - -func AssertEqual[V any](t *testing.T, 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/is/is_test.go b/is/is_test.go index 854fd48..a0b51cf 100644 --- a/is/is_test.go +++ b/is/is_test.go @@ -7,35 +7,35 @@ import ( "testing" "time" - . "go.xyrillian.de/gg/internal/test" + "go.xyrillian.de/gg/assert" "go.xyrillian.de/gg/is" . "go.xyrillian.de/gg/option" ) func TestComparable(t *testing.T) { - AssertEqual(t, Some("foo").IsSomeAnd(is.EqualTo("foo")), true) - AssertEqual(t, Some("bar").IsSomeAnd(is.EqualTo("foo")), false) + assert.Equal(t, Some("foo").IsSomeAnd(is.EqualTo("foo")), true) + assert.Equal(t, Some("bar").IsSomeAnd(is.EqualTo("foo")), false) - AssertEqual(t, Some("foo").IsSomeAnd(is.DifferentFrom("foo")), false) - AssertEqual(t, Some("bar").IsSomeAnd(is.DifferentFrom("foo")), true) + assert.Equal(t, Some("foo").IsSomeAnd(is.DifferentFrom("foo")), false) + assert.Equal(t, Some("bar").IsSomeAnd(is.DifferentFrom("foo")), true) } func TestOrdered(t *testing.T) { - AssertEqual(t, Some(3).IsSomeAnd(is.Above(4)), false) - AssertEqual(t, Some(4).IsSomeAnd(is.Above(4)), false) - AssertEqual(t, Some(5).IsSomeAnd(is.Above(4)), true) + assert.Equal(t, Some(3).IsSomeAnd(is.Above(4)), false) + assert.Equal(t, Some(4).IsSomeAnd(is.Above(4)), false) + assert.Equal(t, Some(5).IsSomeAnd(is.Above(4)), true) - AssertEqual(t, Some(3).IsSomeAnd(is.Below(4)), true) - AssertEqual(t, Some(4).IsSomeAnd(is.Below(4)), false) - AssertEqual(t, Some(5).IsSomeAnd(is.Below(4)), false) + assert.Equal(t, Some(3).IsSomeAnd(is.Below(4)), true) + assert.Equal(t, Some(4).IsSomeAnd(is.Below(4)), false) + assert.Equal(t, Some(5).IsSomeAnd(is.Below(4)), false) - AssertEqual(t, Some(3).IsSomeAnd(is.NotAbove(4)), true) - AssertEqual(t, Some(4).IsSomeAnd(is.NotAbove(4)), true) - AssertEqual(t, Some(5).IsSomeAnd(is.NotAbove(4)), false) + assert.Equal(t, Some(3).IsSomeAnd(is.NotAbove(4)), true) + assert.Equal(t, Some(4).IsSomeAnd(is.NotAbove(4)), true) + assert.Equal(t, Some(5).IsSomeAnd(is.NotAbove(4)), false) - AssertEqual(t, Some(3).IsSomeAnd(is.NotBelow(4)), false) - AssertEqual(t, Some(4).IsSomeAnd(is.NotBelow(4)), true) - AssertEqual(t, Some(5).IsSomeAnd(is.NotBelow(4)), true) + assert.Equal(t, Some(3).IsSomeAnd(is.NotBelow(4)), false) + assert.Equal(t, Some(4).IsSomeAnd(is.NotBelow(4)), true) + assert.Equal(t, Some(5).IsSomeAnd(is.NotBelow(4)), true) } func TestTime(t *testing.T) { @@ -43,19 +43,19 @@ func TestTime(t *testing.T) { t2 := t1.Add(time.Second) t3 := t2.Add(time.Second) - AssertEqual(t, Some(t1).IsSomeAnd(is.After(t2)), false) - AssertEqual(t, Some(t2).IsSomeAnd(is.After(t2)), false) - AssertEqual(t, Some(t3).IsSomeAnd(is.After(t2)), true) + assert.Equal(t, Some(t1).IsSomeAnd(is.After(t2)), false) + assert.Equal(t, Some(t2).IsSomeAnd(is.After(t2)), false) + assert.Equal(t, Some(t3).IsSomeAnd(is.After(t2)), true) - AssertEqual(t, Some(t1).IsSomeAnd(is.Before(t2)), true) - AssertEqual(t, Some(t2).IsSomeAnd(is.Before(t2)), false) - AssertEqual(t, Some(t3).IsSomeAnd(is.Before(t2)), false) + assert.Equal(t, Some(t1).IsSomeAnd(is.Before(t2)), true) + assert.Equal(t, Some(t2).IsSomeAnd(is.Before(t2)), false) + assert.Equal(t, Some(t3).IsSomeAnd(is.Before(t2)), false) - AssertEqual(t, Some(t1).IsSomeAnd(is.NotAfter(t2)), true) - AssertEqual(t, Some(t2).IsSomeAnd(is.NotAfter(t2)), true) - AssertEqual(t, Some(t3).IsSomeAnd(is.NotAfter(t2)), false) + assert.Equal(t, Some(t1).IsSomeAnd(is.NotAfter(t2)), true) + assert.Equal(t, Some(t2).IsSomeAnd(is.NotAfter(t2)), true) + assert.Equal(t, Some(t3).IsSomeAnd(is.NotAfter(t2)), false) - AssertEqual(t, Some(t1).IsSomeAnd(is.NotBefore(t2)), false) - AssertEqual(t, Some(t2).IsSomeAnd(is.NotBefore(t2)), true) - AssertEqual(t, Some(t3).IsSomeAnd(is.NotBefore(t2)), true) + assert.Equal(t, Some(t1).IsSomeAnd(is.NotBefore(t2)), false) + assert.Equal(t, Some(t2).IsSomeAnd(is.NotBefore(t2)), true) + assert.Equal(t, Some(t3).IsSomeAnd(is.NotBefore(t2)), true) } diff --git a/jsonmatch/diff_test.go b/jsonmatch/diff_test.go index aba2d00..d81c54b 100644 --- a/jsonmatch/diff_test.go +++ b/jsonmatch/diff_test.go @@ -10,7 +10,7 @@ import ( "strings" "testing" - . "go.xyrillian.de/gg/internal/test" + "go.xyrillian.de/gg/assert" "go.xyrillian.de/gg/jsonmatch" . "go.xyrillian.de/gg/option" ) @@ -59,7 +59,7 @@ func TestCanonicalizesActualPayload(t *testing.T) { "qux": jsonmatch.Array{5, nil, 15}, }, } - AssertEqual(t, match.DiffAgainst(message), nil) + assert.Equal(t, match.DiffAgainst(message), nil) // changing the type of `data` to map[string]any and of `data.qux` to []any does not change anything at all; // using the jsonmatch.Object and jsonmatch.Array names on this level is mostly syntactic sugar to communicate intent; @@ -72,7 +72,7 @@ func TestCanonicalizesActualPayload(t *testing.T) { "qux": []any{5, nil, 15}, }, } - AssertEqual(t, match.DiffAgainst(message), nil) + assert.Equal(t, match.DiffAgainst(message), nil) // this is using subtypes that our logic cannot recurse into // (map[opaqueString]any instead of map[string]any and []Option[int] instead of []any); @@ -85,7 +85,7 @@ func TestCanonicalizesActualPayload(t *testing.T) { "qux": []Option[int]{Some(5), None[int](), Some(15)}, }, } - AssertEqual(t, match.DiffAgainst(message), nil) + assert.Equal(t, match.DiffAgainst(message), nil) // this is using a specific struct type instead of a map[string]any, which results in a different serialization // (map[string]any serializes with keys sorted alphabetically, but structs serialize with keys sorted by field declaration order; @@ -101,14 +101,14 @@ func TestCanonicalizesActualPayload(t *testing.T) { Qux: []Option[int]{Some(5), None[int](), Some(15)}, }, } - AssertEqual(t, match.DiffAgainst(message), nil) + assert.Equal(t, match.DiffAgainst(message), nil) // to try and trip up the normalization shown above, this match deliberately contains an unmarshalable object; // jsonmatch should recognize that marshaling and unmarshaling does not work and skip the normalization match = jsonmatch.Object{ "data": unmarshalableObject{}, } - AssertEqual(t, match.DiffAgainst(message), []jsonmatch.Diff{{ + assert.Equal(t, match.DiffAgainst(message), []jsonmatch.Diff{{ Kind: "type mismatch", Pointer: "/data", ExpectedJSON: `<not marshalable to JSON, %#v is jsonmatch_test.unmarshalableObject{}>`, @@ -144,10 +144,10 @@ func TestCapturesFields(t *testing.T) { }, } - AssertEqual(t, match.DiffAgainst(message), nil) - AssertEqual(t, capturedUUID1, uuid1) - AssertEqual(t, capturedUUID2, uuid2) - AssertEqual(t, capturedTag1, "bar") + assert.Equal(t, match.DiffAgainst(message), nil) + assert.Equal(t, capturedUUID1, uuid1) + assert.Equal(t, capturedUUID2, uuid2) + assert.Equal(t, capturedTag1, "bar") // check that CaptureField() complains when unmarshaling JSON messages into incompatible types var ( @@ -166,7 +166,7 @@ func TestCapturesFields(t *testing.T) { }, } - AssertEqual(t, match.DiffAgainst(message), []jsonmatch.Diff{{ + assert.Equal(t, match.DiffAgainst(message), []jsonmatch.Diff{{ Kind: "cannot unmarshal into capture slot (json: cannot unmarshal string into Go value of type int)", Pointer: "/objects/0/id", ExpectedJSON: "<capture slot of type *int>", @@ -198,7 +198,7 @@ func TestCapturesFields(t *testing.T) { }, } - AssertEqual(t, match.DiffAgainst(message), []jsonmatch.Diff{{ + assert.Equal(t, match.DiffAgainst(message), []jsonmatch.Diff{{ Kind: "value mismatch", Pointer: "/objects", ActualJSON: fmt.Sprintf(`[{"id":"%s","tags":["foo"]},{"id":"%s","tags":["bar"]}]`, uuid1, uuid2), @@ -227,7 +227,7 @@ func TestFailsOnValueMismatch(t *testing.T) { }, } - AssertEqual(t, match.DiffAgainst(message), []jsonmatch.Diff{ + assert.Equal(t, match.DiffAgainst(message), []jsonmatch.Diff{ { Kind: "value mismatch", Pointer: "/users/0/name", @@ -315,10 +315,10 @@ func TestFailsOnTypeMismatch(t *testing.T) { objectMatch := jsonmatch.Object{"payload": tc2.Data} if idx1 == idx2 { // if we chose matching JSON and data types, then everything works as intended - AssertEqual(t, objectMatch.DiffAgainst(objectMessage), nil) + assert.Equal(t, objectMatch.DiffAgainst(objectMessage), nil) } else { // otherwise we expect a "type mismatch" error - AssertEqual(t, objectMatch.DiffAgainst(objectMessage), []jsonmatch.Diff{{ + assert.Equal(t, objectMatch.DiffAgainst(objectMessage), []jsonmatch.Diff{{ Kind: "type mismatch", Pointer: "/payload", ActualJSON: tc1.JSON, @@ -329,9 +329,9 @@ func TestFailsOnTypeMismatch(t *testing.T) { // type mismatch inside of an array arrayMatch := jsonmatch.Array{1, tc2.Data} if idx1 == idx2 { - AssertEqual(t, arrayMatch.DiffAgainst(arrayMessage), nil) + assert.Equal(t, arrayMatch.DiffAgainst(arrayMessage), nil) } else { - AssertEqual(t, arrayMatch.DiffAgainst(arrayMessage), []jsonmatch.Diff{{ + assert.Equal(t, arrayMatch.DiffAgainst(arrayMessage), []jsonmatch.Diff{{ Kind: "type mismatch", Pointer: "/1", ActualJSON: tc1.JSON, @@ -342,9 +342,9 @@ func TestFailsOnTypeMismatch(t *testing.T) { // type mismatch for plain scalar if scalarMatch, ok := tc2.Scalar.Unpack(); ok { if idx1 == idx2 { - AssertEqual(t, scalarMatch.DiffAgainst(plainMessage), nil) + assert.Equal(t, scalarMatch.DiffAgainst(plainMessage), nil) } else { - AssertEqual(t, scalarMatch.DiffAgainst(plainMessage), []jsonmatch.Diff{{ + assert.Equal(t, scalarMatch.DiffAgainst(plainMessage), []jsonmatch.Diff{{ Kind: "type mismatch", Pointer: "", ActualJSON: tc1.JSON, @@ -376,13 +376,13 @@ func TestFailsOnUnmarshalError(t *testing.T) { for _, message := range testCases { diffs := match.DiffAgainst(message) - if AssertEqual(t, len(diffs), 1) { + if assert.Equal(t, len(diffs), 1) { diff := diffs[0] - AssertEqual(t, strings.HasPrefix(diff.Kind, "unmarshal error ("), true) - AssertEqual(t, strings.HasSuffix(diff.Kind, ")"), true) - AssertEqual(t, diff.Pointer, "") - AssertEqual(t, diff.ExpectedJSON, `{"data":[23,42]}`) - AssertEqual(t, strings.ReplaceAll(diff.ActualJSON, "\uFFFD", ""), strings.ToValidUTF8(string(message), "")) + assert.Equal(t, strings.HasPrefix(diff.Kind, "unmarshal error ("), true) + assert.Equal(t, strings.HasSuffix(diff.Kind, ")"), true) + assert.Equal(t, diff.Pointer, "") + assert.Equal(t, diff.ExpectedJSON, `{"data":[23,42]}`) + assert.Equal(t, strings.ReplaceAll(diff.ActualJSON, "\uFFFD", ""), strings.ToValidUTF8(string(message), "")) } } } @@ -403,11 +403,11 @@ func TestArrayOnArrayAction(t *testing.T) { }} match := jsonmatch.Array{[]any{1, 2}} - AssertEqual(t, match.DiffAgainst(message), expected) + assert.Equal(t, match.DiffAgainst(message), expected) // this used to fail before the fix in the commit that added this test match = jsonmatch.Array{jsonmatch.Array{1, 2}} - AssertEqual(t, match.DiffAgainst(message), expected) + assert.Equal(t, match.DiffAgainst(message), expected) } func TestDispatchIntoCustomDiffable(t *testing.T) { @@ -426,7 +426,7 @@ func TestDispatchIntoCustomDiffable(t *testing.T) { ExpectedJSON: "2", ActualJSON: "3", }} - AssertEqual(t, match.DiffAgainst(message), expected) + assert.Equal(t, match.DiffAgainst(message), expected) } // jsonWithinJSONString appears in TestDispatchIntoCustomDiffable. diff --git a/option/option_test.go b/option/option_test.go index 646108a..7ba50d9 100644 --- a/option/option_test.go +++ b/option/option_test.go @@ -9,25 +9,25 @@ import ( "slices" "testing" - . "go.xyrillian.de/gg/internal/test" + "go.xyrillian.de/gg/assert" ) func TestZeroValue(t *testing.T) { var zero Option[string] - AssertEqual(t, zero, None[string]()) + assert.Equal(t, zero, None[string]()) } //////////////////////////////////////////////////////////////////////////////// // core API (methods sorted by name) func TestAsPointer(t *testing.T) { - AssertEqual(t, None[int]().AsPointer(), nil) - AssertEqual(t, Some(42).AsPointer(), new(42)) + assert.Equal(t, None[int]().AsPointer(), nil) + assert.Equal(t, Some(42).AsPointer(), new(42)) } func TestAsSlice(t *testing.T) { - AssertEqual(t, None[int]().AsSlice(), []int(nil)) - AssertEqual(t, Some(42).AsSlice(), []int{42}) + assert.Equal(t, None[int]().AsSlice(), []int(nil)) + assert.Equal(t, Some(42).AsSlice(), []int{42}) } func isEven(x int) bool { @@ -36,49 +36,49 @@ func isEven(x int) bool { } func TestFilter(t *testing.T) { - AssertEqual(t, None[int]().Filter(isEven), None[int]()) - AssertEqual(t, Some(41).Filter(isEven), None[int]()) - AssertEqual(t, Some(42).Filter(isEven), Some(42)) + assert.Equal(t, None[int]().Filter(isEven), None[int]()) + assert.Equal(t, Some(41).Filter(isEven), None[int]()) + assert.Equal(t, Some(42).Filter(isEven), Some(42)) } func TestIsNone(t *testing.T) { - AssertEqual(t, None[int]().IsNone(), true) - AssertEqual(t, Some(42).IsNone(), false) + assert.Equal(t, None[int]().IsNone(), true) + assert.Equal(t, Some(42).IsNone(), false) } func TestIsNoneOr(t *testing.T) { - AssertEqual(t, None[int]().IsNoneOr(isEven), true) - AssertEqual(t, Some(41).IsNoneOr(isEven), false) - AssertEqual(t, Some(42).IsNoneOr(isEven), true) + assert.Equal(t, None[int]().IsNoneOr(isEven), true) + assert.Equal(t, Some(41).IsNoneOr(isEven), false) + assert.Equal(t, Some(42).IsNoneOr(isEven), true) } func TestIsSome(t *testing.T) { - AssertEqual(t, None[int]().IsSome(), false) - AssertEqual(t, Some(42).IsSome(), true) + assert.Equal(t, None[int]().IsSome(), false) + assert.Equal(t, Some(42).IsSome(), true) } func TestIsSomeAnd(t *testing.T) { - AssertEqual(t, None[int]().IsSomeAnd(isEven), false) - AssertEqual(t, Some(41).IsSomeAnd(isEven), false) - AssertEqual(t, Some(42).IsSomeAnd(isEven), true) + assert.Equal(t, None[int]().IsSomeAnd(isEven), false) + assert.Equal(t, Some(41).IsSomeAnd(isEven), false) + assert.Equal(t, Some(42).IsSomeAnd(isEven), true) } func TestIsZero(t *testing.T) { - AssertEqual(t, None[int]().IsZero(), true) - AssertEqual(t, Some(42).IsZero(), false) + assert.Equal(t, None[int]().IsZero(), true) + assert.Equal(t, Some(42).IsZero(), false) } func TestIter(t *testing.T) { - AssertEqual(t, slices.Collect(None[int]().Iter()), []int(nil)) - AssertEqual(t, slices.Collect(Some(42).Iter()), []int{42}) + assert.Equal(t, slices.Collect(None[int]().Iter()), []int(nil)) + assert.Equal(t, slices.Collect(Some(42).Iter()), []int{42}) } func TestOr(t *testing.T) { none := None[int]() - AssertEqual(t, none.Or(none), None[int]()) - AssertEqual(t, Some(42).Or(none), Some(42)) - AssertEqual(t, none.Or(Some(43)), Some(43)) - AssertEqual(t, Some(42).Or(Some(43)), Some(42)) + assert.Equal(t, none.Or(none), None[int]()) + assert.Equal(t, Some(42).Or(none), Some(42)) + assert.Equal(t, none.Or(Some(43)), Some(43)) + assert.Equal(t, Some(42).Or(Some(43)), Some(42)) } func TestOrElse(t *testing.T) { @@ -92,29 +92,29 @@ func TestOrElse(t *testing.T) { return Some(43) } - AssertEqual(t, None[int]().OrElse(makeNone), None[int]()) - AssertEqual(t, callCount, 1) - AssertEqual(t, Some(42).OrElse(makeNone), Some(42)) - AssertEqual(t, callCount, 1) - AssertEqual(t, None[int]().OrElse(makeSome), Some(43)) - AssertEqual(t, callCount, 2) - AssertEqual(t, Some(42).OrElse(makeSome), Some(42)) - AssertEqual(t, callCount, 2) + assert.Equal(t, None[int]().OrElse(makeNone), None[int]()) + assert.Equal(t, callCount, 1) + assert.Equal(t, Some(42).OrElse(makeNone), Some(42)) + assert.Equal(t, callCount, 1) + assert.Equal(t, None[int]().OrElse(makeSome), Some(43)) + assert.Equal(t, callCount, 2) + assert.Equal(t, Some(42).OrElse(makeSome), Some(42)) + assert.Equal(t, callCount, 2) } func TestUnpack(t *testing.T) { val, ok := None[int]().Unpack() - AssertEqual(t, val, 0) - AssertEqual(t, ok, false) + assert.Equal(t, val, 0) + assert.Equal(t, ok, false) val, ok = Some(42).Unpack() - AssertEqual(t, val, 42) - AssertEqual(t, ok, true) + assert.Equal(t, val, 42) + assert.Equal(t, ok, true) } func TestUnwrapOr(t *testing.T) { - AssertEqual(t, None[int]().UnwrapOr(5), 5) - AssertEqual(t, Some(42).UnwrapOr(5), 42) + assert.Equal(t, None[int]().UnwrapOr(5), 5) + assert.Equal(t, Some(42).UnwrapOr(5), 42) } func TestUnwrapOrElse(t *testing.T) { @@ -124,18 +124,18 @@ func TestUnwrapOrElse(t *testing.T) { return 5 } - AssertEqual(t, None[int]().UnwrapOrElse(five), 5) - AssertEqual(t, callCount, 1) - AssertEqual(t, Some(42).UnwrapOrElse(five), 42) - AssertEqual(t, callCount, 1) + assert.Equal(t, None[int]().UnwrapOrElse(five), 5) + assert.Equal(t, callCount, 1) + assert.Equal(t, Some(42).UnwrapOrElse(five), 42) + assert.Equal(t, callCount, 1) } func TestXor(t *testing.T) { none := None[int]() - AssertEqual(t, none.Xor(none), None[int]()) - AssertEqual(t, Some(42).Xor(none), Some(42)) - AssertEqual(t, none.Xor(Some(43)), Some(43)) - AssertEqual(t, Some(42).Xor(Some(43)), None[int]()) + assert.Equal(t, none.Xor(none), None[int]()) + assert.Equal(t, Some(42).Xor(none), Some(42)) + assert.Equal(t, none.Xor(Some(43)), Some(43)) + assert.Equal(t, Some(42).Xor(Some(43)), None[int]()) } //////////////////////////////////////////////////////////////////////////////// @@ -145,49 +145,49 @@ func TestFormat(t *testing.T) { none := None[int]() some := Some(42) - AssertEqual(t, fmt.Sprintf("value is %d", none), "value is <none>") - AssertEqual(t, fmt.Sprintf("value is %d", some), "value is 42") - AssertEqual(t, fmt.Sprintf("value is %010d", none), "value is 0000<none>") - AssertEqual(t, fmt.Sprintf("value is %010d", some), "value is 0000000042") + assert.Equal(t, fmt.Sprintf("value is %d", none), "value is <none>") + assert.Equal(t, fmt.Sprintf("value is %d", some), "value is 42") + assert.Equal(t, fmt.Sprintf("value is %010d", none), "value is 0000<none>") + assert.Equal(t, fmt.Sprintf("value is %010d", some), "value is 0000000042") noneList := None[[]int]() someList := Some([]int{4, 2}) - AssertEqual(t, fmt.Sprintf("value is %v", noneList), "value is <none>") - AssertEqual(t, fmt.Sprintf("value is %v", someList), "value is [4 2]") - AssertEqual(t, fmt.Sprintf("value is %#v", noneList), "value is <none>") - AssertEqual(t, fmt.Sprintf("value is %#v", someList), "value is []int{4, 2}") + assert.Equal(t, fmt.Sprintf("value is %v", noneList), "value is <none>") + assert.Equal(t, fmt.Sprintf("value is %v", someList), "value is [4 2]") + assert.Equal(t, fmt.Sprintf("value is %#v", noneList), "value is <none>") + assert.Equal(t, fmt.Sprintf("value is %#v", someList), "value is []int{4, 2}") listOfOptions := []Option[int]{none, some} - AssertEqual(t, fmt.Sprintf("value is %#v", listOfOptions), "value is []option.Option[int]{<none>, 42}") + assert.Equal(t, fmt.Sprintf("value is %#v", listOfOptions), "value is []option.Option[int]{<none>, 42}") } func TestMarshalSQL(t *testing.T) { value, err := None[string]().Value() - AssertEqual(t, err, nil) - AssertEqual(t, value, nil) + assert.Equal(t, err, nil) + assert.Equal(t, value, nil) value, err = Some("hello").Value() - AssertEqual(t, err, nil) - AssertEqual(t, value, "hello") + assert.Equal(t, err, nil) + assert.Equal(t, value, "hello") _, err = Some(struct{}{}).Value() - AssertEqual(t, err.Error(), "unsupported type struct {}, a struct") + assert.Equal(t, err.Error(), "unsupported type struct {}, a struct") } func TestUnmarshalSQL(t *testing.T) { var o1 Option[string] err := o1.Scan(nil) - AssertEqual(t, err, nil) - AssertEqual(t, o1, None[string]()) + assert.Equal(t, err, nil) + assert.Equal(t, o1, None[string]()) var o2 Option[string] err = o2.Scan("hello") - AssertEqual(t, err, nil) - AssertEqual(t, o2, Some("hello")) + assert.Equal(t, err, nil) + assert.Equal(t, o2, Some("hello")) var o3 Option[struct{}] err = o3.Scan("hello") - AssertEqual(t, err.Error(), "unsupported Scan, storing driver.Value type string into type *struct {}") + assert.Equal(t, err.Error(), "unsupported Scan, storing driver.Value type string into type *struct {}") } func TestMarshalAndUnmarshalJSON(t *testing.T) { @@ -208,11 +208,11 @@ func TestMarshalAndUnmarshalJSON(t *testing.T) { S3: Some(3), } buf, err := json.Marshal(original) - AssertEqual(t, err, nil) - AssertEqual(t, string(buf), `{"n1":null,"n2":null,"s1":1,"s2":2,"s3":3}`) + assert.Equal(t, err, nil) + assert.Equal(t, string(buf), `{"n1":null,"n2":null,"s1":1,"s2":2,"s3":3}`) var decoded payload err = json.Unmarshal(buf, &decoded) - AssertEqual(t, err, nil) - AssertEqual(t, decoded, original) + assert.Equal(t, err, nil) + assert.Equal(t, decoded, original) } diff --git a/options/options_test.go b/options/options_test.go index fab378b..89b9e14 100644 --- a/options/options_test.go +++ b/options/options_test.go @@ -7,62 +7,62 @@ import ( "strconv" "testing" - . "go.xyrillian.de/gg/internal/test" + "go.xyrillian.de/gg/assert" . "go.xyrillian.de/gg/option" ) func TestFromPointer(t *testing.T) { - AssertEqual(t, FromPointer[int](nil), None[int]()) - AssertEqual(t, FromPointer(new(int(42))), Some(42)) + assert.Equal(t, FromPointer[int](nil), None[int]()) + assert.Equal(t, FromPointer(new(int(42))), Some(42)) } func TestIsNoneOrZero(t *testing.T) { - AssertEqual(t, IsNoneOrZero(None[int]()), true) - AssertEqual(t, IsNoneOrZero(Some(0)), true) - AssertEqual(t, IsNoneOrZero(Some(1)), false) + assert.Equal(t, IsNoneOrZero(None[int]()), true) + assert.Equal(t, IsNoneOrZero(Some(0)), true) + assert.Equal(t, IsNoneOrZero(Some(1)), false) } func TestMap(t *testing.T) { - AssertEqual(t, Map(None[int](), strconv.Itoa), None[string]()) - AssertEqual(t, Map(Some(42), strconv.Itoa), Some("42")) + assert.Equal(t, Map(None[int](), strconv.Itoa), None[string]()) + assert.Equal(t, Map(Some(42), strconv.Itoa), Some("42")) } func TestMax(t *testing.T) { - AssertEqual(t, Max[int](), None[int]()) - AssertEqual(t, Max(None[int]()), None[int]()) - AssertEqual(t, Max(None[int](), None[int]()), None[int]()) + assert.Equal(t, Max[int](), None[int]()) + assert.Equal(t, Max(None[int]()), None[int]()) + assert.Equal(t, Max(None[int](), None[int]()), None[int]()) - AssertEqual(t, Max(Some(5)), Some(5)) - AssertEqual(t, Max(Some(5), None[int]()), Some(5)) - AssertEqual(t, Max(None[int](), Some(5)), Some(5)) + assert.Equal(t, Max(Some(5)), Some(5)) + assert.Equal(t, Max(Some(5), None[int]()), Some(5)) + assert.Equal(t, Max(None[int](), Some(5)), Some(5)) - AssertEqual(t, Max(Some(5), Some(23)), Some(23)) - AssertEqual(t, Max(None[int](), Some(5), Some(23)), Some(23)) - AssertEqual(t, Max(Some(5), None[int](), Some(23)), Some(23)) - AssertEqual(t, Max(Some(5), Some(23), None[int]()), Some(23)) + assert.Equal(t, Max(Some(5), Some(23)), Some(23)) + assert.Equal(t, Max(None[int](), Some(5), Some(23)), Some(23)) + assert.Equal(t, Max(Some(5), None[int](), Some(23)), Some(23)) + assert.Equal(t, Max(Some(5), Some(23), None[int]()), Some(23)) - AssertEqual(t, Max(Some(23), Some(5)), Some(23)) - AssertEqual(t, Max(None[int](), Some(23), Some(5)), Some(23)) - AssertEqual(t, Max(Some(23), None[int](), Some(5)), Some(23)) - AssertEqual(t, Max(Some(23), Some(5), None[int]()), Some(23)) + assert.Equal(t, Max(Some(23), Some(5)), Some(23)) + assert.Equal(t, Max(None[int](), Some(23), Some(5)), Some(23)) + assert.Equal(t, Max(Some(23), None[int](), Some(5)), Some(23)) + assert.Equal(t, Max(Some(23), Some(5), None[int]()), Some(23)) } func TestMin(t *testing.T) { - AssertEqual(t, Min[int](), None[int]()) - AssertEqual(t, Min(None[int]()), None[int]()) - AssertEqual(t, Min(None[int](), None[int]()), None[int]()) + assert.Equal(t, Min[int](), None[int]()) + assert.Equal(t, Min(None[int]()), None[int]()) + assert.Equal(t, Min(None[int](), None[int]()), None[int]()) - AssertEqual(t, Min(Some(5)), Some(5)) - AssertEqual(t, Min(Some(5), None[int]()), Some(5)) - AssertEqual(t, Min(None[int](), Some(5)), Some(5)) + assert.Equal(t, Min(Some(5)), Some(5)) + assert.Equal(t, Min(Some(5), None[int]()), Some(5)) + assert.Equal(t, Min(None[int](), Some(5)), Some(5)) - AssertEqual(t, Min(Some(5), Some(23)), Some(5)) - AssertEqual(t, Min(None[int](), Some(5), Some(23)), Some(5)) - AssertEqual(t, Min(Some(5), None[int](), Some(23)), Some(5)) - AssertEqual(t, Min(Some(5), Some(23), None[int]()), Some(5)) + assert.Equal(t, Min(Some(5), Some(23)), Some(5)) + assert.Equal(t, Min(None[int](), Some(5), Some(23)), Some(5)) + assert.Equal(t, Min(Some(5), None[int](), Some(23)), Some(5)) + assert.Equal(t, Min(Some(5), Some(23), None[int]()), Some(5)) - AssertEqual(t, Min(Some(23), Some(5)), Some(5)) - AssertEqual(t, Min(None[int](), Some(23), Some(5)), Some(5)) - AssertEqual(t, Min(Some(23), None[int](), Some(5)), Some(5)) - AssertEqual(t, Min(Some(23), Some(5), None[int]()), Some(5)) + assert.Equal(t, Min(Some(23), Some(5)), Some(5)) + assert.Equal(t, Min(None[int](), Some(23), Some(5)), Some(5)) + assert.Equal(t, Min(Some(23), None[int](), Some(5)), Some(5)) + assert.Equal(t, Min(Some(23), Some(5), None[int]()), Some(5)) } diff --git a/testcapture/capture.go b/testcapture/capture.go new file mode 100644 index 0000000..3ac4f52 --- /dev/null +++ b/testcapture/capture.go @@ -0,0 +1,416 @@ +// 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" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "runtime" + "slices" + "sync" + "sync/atomic" + + "go.xyrillian.de/gg/assert" +) + +// Result is returned by func [Capture]. +type Result struct { + // Outcome describes how the test ended. + Outcome Outcome + // Panic contains a payload recovered from a panic(), if Outcome is [OutcomePanicked]. + Panic any + // Messages contains log lines captured from t.Log() calls, or functions calling t.Log(), such as t.Error() and t.Fatal(); + // as well as data captured from t.Output().Write() calls. + Messages []Message + // Attrs contains attributes captured in t.Attr() calls. + Attrs map[string]string + // Artifacts holds the contents of any regular files that were created below t.ArtifactDir(), keyed with the path relative to t.ArtifactDir(). + Artifacts map[string]string +} + +// Outcome is an enum. +// It appears in type [Result]. +type Outcome string + +const ( + // OutcomeFinished describes a [Capture] that ended with the test running to completion. + OutcomeFinished Outcome = "finished" + // OutcomeFailed describes a [Capture] that ended early because of a t.FailNow() call. + OutcomeFailed Outcome = "failed" + // OutcomeSkipped describes a [Capture] that ended early because of a t.SkipNow() call. + OutcomeSkipped Outcome = "skipped" + // OutcomePanicked describes a [Capture] that ended early because of a panic() call. + OutcomePanicked Outcome = "panicked" +) + +// Message is a piece of log output captured by func [Capture]. +// It appears in type [Result]. +// - Each call to t.Log(), t.Logf() or their derived functions results in one Message instance of type [Log]. +// - Writing into t.Output() between two calls to t.Log(), t.Logf() etc. results in a single Message instance of type [Output], even if Write() is called multiple times. +type Message struct { + Message string + Type MessageType +} + +// Log is a shorthand for constructing [Message] objects of type [MessageTypeLog]. +func Log[T interface{ ~string }](message T) Message { + return Message{string(message), MessageTypeLog} +} + +// Output is a shorthand for constructing [Message] objects of type [MessageTypeOutput]. +func Output[T interface{ ~string | ~[]byte }](message T) Message { + return Message{string(message), MessageTypeOutput} +} + +// MessageType is an enum. +// It appears in type [Message]. +type MessageType string + +const ( + // MessageTypeLog describes [Message] instances created by calls to t.Log(), t.Logf(), or functions calling them, such as t.Error() and t.Fatal(). + MessageTypeLog MessageType = "log" + // MessageTypeOutput describes [Message] instances created by calls to t.Output().Write(). + MessageTypeOutput MessageType = "output" +) + +// Capture executes a test function with a stub implementation of [assert.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 { + r := Result{ + Outcome: OutcomeFinished, // can be overridden by Fail() or SkipNow() + } + executeCapture(ctx, name, &r, test) + return r +} + +// capturer is the implementation of [assert.TestingTB] used by func [Capture]. +type capturer struct { + context context.Context + cleanups []func() + name string + result *Result + state struct { + ArtifactDir string + } + + cleanupsMutex sync.Mutex // lock for access to the `cleanups` field + resultMutex sync.RWMutex // lock for access to the `result` field + stateMutex sync.Mutex // lock for access to the `state` field + 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)) { + ctx, cancel := context.WithCancel(ctx) + t := capturer{ + context: ctx, + cleanups: nil, + name: name, + result: r, + } + defer func() { + t.setOutcome(recover()) + cancel() // T.Context() demands that the context be canceled before any cleanup handlers + for _, cleanup := range slices.Backward(t.cleanups) { + cleanup() + } + }() + test(&t) +} + +func (t *capturer) setOutcome(panicPayload any) { + t.resultMutex.Lock() + defer t.resultMutex.Unlock() + if panicPayload == nil { + return + } else if outcome, ok := panicPayload.(Outcome); ok { + t.result.Outcome = outcome + } else { + t.result.Outcome = OutcomePanicked + t.result.Panic = panicPayload + } +} + +func (t *capturer) pushOutput(buf []byte, msgType MessageType) { + t.resultMutex.Lock() + defer t.resultMutex.Unlock() + + // try to merge consecutive t.Output().Write() calls together + if msgType == MessageTypeOutput && len(t.result.Messages) > 0 { + idx := len(t.result.Messages) - 1 + msg := t.result.Messages[idx] + if msg.Type == MessageTypeOutput { + msg.Message = msg.Message + string(buf) + t.result.Messages[idx] = msg + return + } + } + + t.result.Messages = append(t.result.Messages, Message{ + Message: string(buf), + Type: msgType, + }) +} + +var tempdirID atomic.Uint64 + +func pickTempdir() (string, error) { + path := filepath.Join(os.TempDir(), fmt.Sprintf("gg-assert-capture-%d", tempdirID.Add(1))) + return path, os.MkdirAll(path, 0777) +} + +func collectArtifacts(dirPath string) (map[string]string, error) { + dir, err := os.OpenRoot(dirPath) + if err != nil { + return nil, err + } + dirFS := dir.FS() + + result := make(map[string]string) + err = fs.WalkDir(dirFS, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.Type().IsRegular() { + buf, err := fs.ReadFile(dirFS, path) + if err != nil { + return err + } + result[path] = string(buf) + } + return nil + }) + if err != nil { + return nil, err + } + + return result, os.RemoveAll(dirPath) +} + +// ArtifactDir implements the [assert.TestingTB] interface. +func (t *capturer) ArtifactDir() string { + t.stateMutex.Lock() + defer t.stateMutex.Unlock() + if t.state.ArtifactDir == "" { + path, err := pickTempdir() + if err != nil { + t.Fatal("in t.ArtifactDir(): ", err) + } + t.state.ArtifactDir = path + + t.Cleanup(func() { + artifacts, err := collectArtifacts(path) + if err == nil { + t.resultMutex.Lock() + defer t.resultMutex.Unlock() + t.result.Artifacts = artifacts + } else { + t.Error(err) + } + }) + } + return t.state.ArtifactDir +} + +// Attr implements the [assert.TestingTB] interface. +func (t *capturer) Attr(key, value string) { + t.resultMutex.Lock() + defer t.resultMutex.Unlock() + if t.result.Attrs == nil { + t.result.Attrs = make(map[string]string) + } + t.result.Attrs[key] = value +} + +// Chdir implements the [assert.TestingTB] interface. +func (t *capturer) Chdir(dir string) { + t.doChdir(dir) + + // the following is done outside of doChdir() because t.Setenv() also locks t.nonlocalMutex + switch runtime.GOOS { + case "windows", "plan9": + // these platforms do not use the PWD variable + default: + dir, err := os.Getwd() // returns an absolute path even if `dir` is not one + if err != nil { + t.Fatal(err) + } + t.Setenv("PWD", dir) + } +} + +func (t *capturer) doChdir(dir string) { + t.nonlocalMutex.Lock() + defer t.nonlocalMutex.Unlock() + + oldDir, err := os.Open(".") + if err != nil { + t.Fatal(err) + } + err = os.Chdir(dir) + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + err := oldDir.Chdir() + if err != nil { + t.Error("could not reset cwd changed by t.Chdir(): ", err) + } + }) +} + +// Cleanup implements the [assert.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. +func (t *capturer) Context() context.Context { + return t.context +} + +// Error implements the [assert.TestingTB] interface. +func (t *capturer) Error(args ...any) { + t.Log(args...) + t.Fail() +} + +// Errorf implements the [assert.TestingTB] interface. +func (t *capturer) Errorf(format string, args ...any) { + t.Logf(format, args...) + t.Fail() +} + +// Fail implements the [assert.TestingTB] interface. +func (t *capturer) Fail() { + t.resultMutex.Lock() + defer t.resultMutex.Unlock() + t.result.Outcome = OutcomeFailed +} + +// Failed implements the [assert.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. +func (t *capturer) FailNow() { + panic(OutcomeFailed) +} + +// Fatal implements the [assert.TestingTB] interface. +func (t *capturer) Fatal(args ...any) { + t.Log(args...) + t.FailNow() +} + +// Fatalf implements the [assert.TestingTB] interface. +func (t *capturer) Fatalf(format string, args ...any) { + t.Logf(format, args...) + t.FailNow() +} + +// Helper implements the [assert.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. +func (t *capturer) Log(args ...any) { + t.pushOutput(fmt.Append(nil, args...), MessageTypeLog) +} + +// Logf implements the [assert.TestingTB] interface. +func (t *capturer) Logf(format string, args ...any) { + t.pushOutput(fmt.Appendf(nil, format, args...), MessageTypeLog) +} + +// Name implements the [assert.TestingTB] interface. +func (t *capturer) Name() string { + return t.name +} + +// Output implements the [assert.TestingTB] interface. +func (t *capturer) Output() io.Writer { + return outputCapturer{t} +} + +type outputCapturer struct { + t *capturer +} + +// Write implements the [io.Writer] interface. +func (c outputCapturer) Write(buf []byte) (int, error) { + c.t.pushOutput(buf, MessageTypeOutput) + return len(buf), nil +} + +// Setenv implements the [assert.TestingTB] interface. +func (t *capturer) Setenv(key, value string) { + t.nonlocalMutex.Lock() + defer t.nonlocalMutex.Unlock() + + oldValue, hasOldValue := os.LookupEnv(key) + os.Setenv(key, value) + + t.Cleanup(func() { + if hasOldValue { + os.Setenv(key, oldValue) + } else { + os.Unsetenv(key) + } + }) +} + +// Skip implements the [assert.TestingTB] interface. +func (t *capturer) Skip(args ...any) { + t.Log(args...) + t.SkipNow() +} + +// Skipf implements the [assert.TestingTB] interface. +func (t *capturer) Skipf(format string, args ...any) { + t.Logf(format, args...) + t.SkipNow() +} + +// SkipNow implements the [assert.TestingTB] interface. +func (t *capturer) SkipNow() { + panic(OutcomeSkipped) +} + +// Skipped implements the [assert.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. +func (t *capturer) TempDir() string { + path, err := pickTempdir() + if err != nil { + t.Fatal("in t.TempDir(): ", err) + } + t.Cleanup(func() { + err := os.RemoveAll(path) + if err != nil { + t.Error(err) + } + }) + return path +} diff --git a/testcapture/capture_test.go b/testcapture/capture_test.go new file mode 100644 index 0000000..9e53b71 --- /dev/null +++ b/testcapture/capture_test.go @@ -0,0 +1,403 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +package testcapture_test + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "go.xyrillian.de/gg/assert" + "go.xyrillian.de/gg/testcapture" +) + +func TestCaptureArtifactDir(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + // test that writing a regular file works + fooPath := filepath.Join(t.ArtifactDir(), "foo.txt") + err := os.WriteFile(fooPath, []byte("Hello World."), 0666) + if err != nil { + t.Fatal(err) + } + + // test that nesting regular files into directories works + // (and also, implicitly, that multiple calls to ArtifactDir() return the same path) + barPath := filepath.Join(t.ArtifactDir(), "bar/a/b/c/d/data.json") + err = os.MkdirAll(filepath.Dir(barPath), 0777) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile(barPath, []byte(`{"bar":42}`), 0666) + if err != nil { + t.Fatal(err) + } + + // test that empty directories are ignored by the capture + err = os.MkdirAll(filepath.Join(t.ArtifactDir(), "unused"), 0777) + if err != nil { + t.Fatal(err) + } + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFinished, + Artifacts: map[string]string{ + "foo.txt": "Hello World.", + "bar/a/b/c/d/data.json": `{"bar":42}`, + }, + }) +} + +func TestCaptureAttr(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + t.Attr("foo", "bar") + t.Attr("foo", "baz") + t.Attr("hello", "world") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFinished, + Attrs: map[string]string{ + "foo": "baz", + "hello": "world", + }, + }) +} + +func TestCaptureChdir(t *testing.T) { + var cwdInCapture string + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + // ArtifactDir() is used as a target for Chdir()... + dir := t.ArtifactDir() + t.Chdir(dir) + // ...because we have an easy way to check if we are actually in that dir + err := os.WriteFile("foo.txt", []byte("foo"), 0666) + if err != nil { + t.Error(err) + } + + // smuggle the cwd out of the capture for the cleanup test below + cwdInCapture, err = os.Getwd() + if err != nil { + t.Error(err) + } + + // t.Chdir() should have set $PWD to an absolute path + assert.Equal(t, os.Getenv("PWD"), cwdInCapture) + assert.Equal(t, filepath.IsAbs(cwdInCapture), true) + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFinished, + Artifacts: map[string]string{"foo.txt": "foo"}, + }) + + // check that we reset the working directory at the end of the test + cwdAfterCapture, err := os.Getwd() + if err != nil { + t.Error(err) + } + if cwdAfterCapture == cwdInCapture { + t.Error("cwd should have been reset, but still is", cwdInCapture) + } +} + +func TestCaptureCleanup(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + // test that cleanups run in reverse order of registration, and only after the test itself is done + t.Log("starting up") + t.Cleanup(func() { t.Log("first cleanup") }) + t.Cleanup(func() { t.Log("second cleanup") }) + t.Cleanup(func() { t.Log("third cleanup") }) + t.Log("shutting down") + + // test that cleanups run even after a panic + panic("kaboom") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomePanicked, + Messages: []testcapture.Message{ + testcapture.Log("starting up"), + testcapture.Log("shutting down"), + testcapture.Log("third cleanup"), + testcapture.Log("second cleanup"), + testcapture.Log("first cleanup"), + }, + Panic: "kaboom", + }) +} + +func TestCaptureContext(t *testing.T) { + ctx := context.WithValue(t.Context(), "foo", "bar") //nolint:staticcheck // we do not care about type collision risks for this simple test + result := testcapture.Capture(ctx, t.Name(), func(t assert.TestingTB) { + // test that the context is live within the test + err := t.Context().Err() + if err != nil { + t.Error(err) + } + + // test that the context is expired at cleanup time + t.Cleanup(func() { + err := t.Context().Err() + if err == nil { + t.Error("still alive!?") + } + }) + + // test that the context is derived from the one passed to Capture() + value := ctx.Value("foo") + if value != "bar" { + t.Error("did not see the value") + } + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFinished, + }) +} + +func TestCaptureError(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + simpleError := errors.New("bar") + t.Error("foo: ", simpleError) + // check that the test keeps going, and that t.Log() does not reset the Outcome + t.Log("still going") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFailed, + Messages: []testcapture.Message{ + testcapture.Log("foo: bar"), + testcapture.Log("still going"), + }, + }) +} + +func TestCaptureErrorf(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + t.Errorf("foo = %d", 42) + // check that the test keeps going, and that t.Log() does not reset the Outcome + t.Log("still going") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFailed, + Messages: []testcapture.Message{ + testcapture.Log("foo = 42"), + testcapture.Log("still going"), + }, + }) +} + +func TestCaptureFail(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + if !t.Failed() { + t.Log("looking good so far") + } + t.Fail() + if t.Failed() { + t.Log("still going") + } + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFailed, + Messages: []testcapture.Message{ + testcapture.Log("looking good so far"), + testcapture.Log("still going"), + }, + }) +} + +func TestCaptureFailNow(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + if !t.Failed() { + t.Log("looking good so far") + } + t.FailNow() + t.Log("still going") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFailed, + Messages: []testcapture.Message{ + testcapture.Log("looking good so far"), + // "still going" is not logged because FailNow() bails + }, + }) +} + +func TestCaptureFatal(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + t.Log("looking good so far") + t.Fatal("kaboom") + t.Log("still going") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFailed, + Messages: []testcapture.Message{ + testcapture.Log("looking good so far"), + testcapture.Log("kaboom"), + // "still going" is not logged because Fatal() bails + }, + }) +} + +func TestCaptureFatalf(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + t.Log("looking good so far") + t.Fatalf("kaboom %d", 42) + t.Log("still going") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFailed, + Messages: []testcapture.Message{ + testcapture.Log("looking good so far"), + testcapture.Log("kaboom 42"), + // "still going" is not logged because Fatalf() bails + }, + }) +} + +func TestCaptureName(t *testing.T) { + result := testcapture.Capture(t.Context(), "Harold", func(t assert.TestingTB) { + panic(t.Name() + " died") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomePanicked, + Panic: "Harold died", + }) +} + +func TestCaptureOutput(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + t.Log("hello 1") + for range 10 { + fmt.Fprintln(t.Output(), "a") + } + t.Log("hello 2") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFinished, + Messages: []testcapture.Message{ + testcapture.Log("hello 1"), + testcapture.Output(strings.Repeat("a\n", 10)), + testcapture.Log("hello 2"), + }, + }) +} + +func TestCapturePanic(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + t.Error("we are going to blow up") + panic("kaboom") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomePanicked, // Panicked takes precedence over Failed + Messages: []testcapture.Message{ + testcapture.Log("we are going to blow up"), + }, + Panic: "kaboom", + }) +} + +func TestCaptureSetenv(t *testing.T) { + t.Setenv("GG_TEST_SETENV_SCOPE", "outer") + + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + // test t.Setenv() overriding an existing variable + assert.Equal(t, os.Getenv("GG_TEST_SETENV_SCOPE"), "outer") + t.Setenv("GG_TEST_SETENV_SCOPE", "inner") + assert.Equal(t, os.Getenv("GG_TEST_SETENV_SCOPE"), "inner") + + // test t.Setenv() setting a fresh variable + // (this variable should be completely removed after the end of the test) + t.Setenv("GG_TEST_SETENV_PAYLOAD", "42") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFinished, + }) + + assert.Equal(t, os.Getenv("GG_TEST_SETENV_SCOPE"), "outer") + _, ok := os.LookupEnv("GG_TEST_SETENV_PAYLOAD") + assert.Equal(t, ok, false) +} + +func TestCaptureSkip(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + if !t.Skipped() { + t.Skip("this looks uninteresting") + } + t.Log("still going") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeSkipped, + Messages: []testcapture.Message{ + testcapture.Log("this looks uninteresting"), + // "still going" is not logged because Skip() bails + }, + }) +} + +func TestCaptureSkipf(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + if !t.Skipped() { + t.Skipf("pretty sure the answer is %d", 42) + } + t.Log("still going") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeSkipped, + Messages: []testcapture.Message{ + testcapture.Log("pretty sure the answer is 42"), + // "still going" is not logged because Skipf() bails + }, + }) +} + +func TestCaptureSkipNow(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + if !t.Skipped() { + t.Log("this looks uninteresting") + } + t.SkipNow() + t.Log("still going") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeSkipped, + Messages: []testcapture.Message{ + testcapture.Log("this looks uninteresting"), + // "still going" is not logged because Skip() bails + }, + }) +} + +func TestCaptureTempDir(t *testing.T) { + var path string + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + // fill a TempDir with some stuff + path = t.TempDir() + err := os.MkdirAll(filepath.Join(path, "emptydir"), 0777) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(path, "data.json"), []byte(`{"username":"admin"}`), 0666) + if err != nil { + t.Error(err) + } + + // check that each call to TempDir returns a new dir + otherPath := t.TempDir() + if otherPath == path { + t.Error("should have returned a different TempDir") + } + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFinished, + }) + + // check that TempDir was cleaned up + _, err := os.Stat(path) + if err == nil { + t.Error("TempDir was not cleaned up") + } else if !os.IsNotExist(err) { + t.Error(err) + } +} |
