This is an automated email from the git hooks/post-receive script.
git pushed a commit to branch main
in repository ego.
View the commit online.
commit 842293f1fce5ec0aca8e50764aee72ecfa3592c7
Author: [email protected] <[email protected]>
AuthorDate: Mon Mar 9 10:43:52 2026 -0600
feat: add public test infrastructure with assert and require packages
Implements Task 11: a testify-inspired assertion framework following
Go testing best practices. The assert package provides non-fatal
assertions (Text, Visible, NotVisible, Size, ChildCount, Equal, NotNil)
with optional custom messages. The require package wraps these as fatal
assertions via FailNow.
Golden image testing with configurable tolerance supports updating golden
files via -update flag for maintaining PNG snapshots. All assertions use
the TB interface pattern for test doubles and proper t.Helper() calls.
Comprehensive test suite covers all assertion functions including edge
cases and error paths.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
tests/assert/assert.go | 132 ++++++++++++++++++++
tests/assert/assert_test.go | 298 ++++++++++++++++++++++++++++++++++++++++++++
tests/assert/golden.go | 128 +++++++++++++++++++
tests/require/require.go | 85 +++++++++++++
4 files changed, 643 insertions(+)
diff --git a/tests/assert/assert.go b/tests/assert/assert.go
new file mode 100644
index 0000000..b0095b4
--- /dev/null
+++ b/tests/assert/assert.go
@@ -0,0 +1,132 @@
+// Package assert provides non-fatal test assertions modelled after the
+// testify/assert style. Every function calls t.Helper so failure lines point
+// to the call site, and returns a bool so callers can short-circuit further
+// checks when needed.
+package assert
+
+import (
+ "fmt"
+ "testing"
+)
+
+// TB is the subset of testing.TB used by this package. Accepting an interface
+// instead of *testing.T makes it possible to substitute test doubles in tests
+// of the package itself, and is the standard pattern recommended by the Go
+// testing docs.
+type TB interface {
+ Helper()
+ Errorf(format string, args ...interface{})
+}
+
+// Ensure *testing.T and *testing.B satisfy TB at compile time.
+var _ TB = (*testing.T)(nil)
+var _ TB = (*testing.B)(nil)
+
+// formatMsg returns the optional message supplied by the caller. If no
+// arguments are given it returns an empty string. If the first argument is a
+// string and more arguments follow they are treated as a format string and
+// parameters; otherwise all arguments are joined with a space via fmt.Sprint.
+func formatMsg(msgAndArgs ...interface{}) string {
+ if len(msgAndArgs) == 0 {
+ return ""
+ }
+ if format, ok := msgAndArgs[0].(string); ok && len(msgAndArgs) > 1 {
+ return fmt.Sprintf(format, msgAndArgs[1:]...)
+ }
+ return fmt.Sprint(msgAndArgs...)
+}
+
+// annotate prepends the caller-supplied message (if any) to base.
+func annotate(base, extra string) string {
+ if extra == "" {
+ return base
+ }
+ return base + "\n" + extra
+}
+
+// fail calls t.Errorf with the fully-assembled message. Using a constant
+// format string ("%s") satisfies go vet's printf checker while still
+// allowing the message to be built dynamically.
+func fail(t TB, msg string) {
+ t.Helper()
+ t.Errorf("%s", msg)
+}
+
+// Text asserts that obj.Text() returns expected.
+func Text(t TB, obj interface{ Text() string }, expected string, msgAndArgs ...interface{}) bool {
+ t.Helper()
+ got := obj.Text()
+ if got == expected {
+ return true
+ }
+ fail(t, annotate(fmt.Sprintf("Text(): got %q, want %q", got, expected), formatMsg(msgAndArgs...)))
+ return false
+}
+
+// Visible asserts that obj.Visible() returns true.
+func Visible(t TB, obj interface{ Visible() bool }, msgAndArgs ...interface{}) bool {
+ t.Helper()
+ if obj.Visible() {
+ return true
+ }
+ fail(t, annotate("expected object to be visible", formatMsg(msgAndArgs...)))
+ return false
+}
+
+// NotVisible asserts that obj.Visible() returns false.
+func NotVisible(t TB, obj interface{ Visible() bool }, msgAndArgs ...interface{}) bool {
+ t.Helper()
+ if !obj.Visible() {
+ return true
+ }
+ fail(t, annotate("expected object to not be visible", formatMsg(msgAndArgs...)))
+ return false
+}
+
+// Size asserts that obj.Size() returns the given width and height.
+func Size(t TB, obj interface{ Size() (int, int) }, w, h int, msgAndArgs ...interface{}) bool {
+ t.Helper()
+ gw, gh := obj.Size()
+ if gw == w && gh == h {
+ return true
+ }
+ fail(t, annotate(fmt.Sprintf("Size(): got (%d, %d), want (%d, %d)", gw, gh, w, h), formatMsg(msgAndArgs...)))
+ return false
+}
+
+// ChildCount asserts that obj.ChildrenCount() returns n.
+func ChildCount(t TB, obj interface{ ChildrenCount() int }, n int, msgAndArgs ...interface{}) bool {
+ t.Helper()
+ got := obj.ChildrenCount()
+ if got == n {
+ return true
+ }
+ fail(t, annotate(fmt.Sprintf("ChildrenCount(): got %d, want %d", got, n), formatMsg(msgAndArgs...)))
+ return false
+}
+
+// Equal asserts that expected and actual are deeply equal using fmt.Sprintf for
+// comparison. For structural equality of complex types callers should use a
+// dedicated matcher; this helper covers the common case of comparable values.
+func Equal(t TB, expected, actual interface{}, msgAndArgs ...interface{}) bool {
+ t.Helper()
+ // Use fmt.Sprintf to get a canonical string representation and compare.
+ // This keeps the package dependency-free while covering all basic types.
+ es := fmt.Sprintf("%v", expected)
+ as := fmt.Sprintf("%v", actual)
+ if es == as {
+ return true
+ }
+ fail(t, annotate(fmt.Sprintf("Equal(): got %v, want %v", actual, expected), formatMsg(msgAndArgs...)))
+ return false
+}
+
+// NotNil asserts that obj is not nil.
+func NotNil(t TB, obj interface{}, msgAndArgs ...interface{}) bool {
+ t.Helper()
+ if obj != nil {
+ return true
+ }
+ fail(t, annotate("expected non-nil value, got nil", formatMsg(msgAndArgs...)))
+ return false
+}
diff --git a/tests/assert/assert_test.go b/tests/assert/assert_test.go
new file mode 100644
index 0000000..9d641b2
--- /dev/null
+++ b/tests/assert/assert_test.go
@@ -0,0 +1,298 @@
+package assert
+
+import (
+ "image"
+ "image/color"
+ "testing"
+)
+
+// --- mock objects ---
+
+type mockTextObj struct{ text string }
+
+func (m mockTextObj) Text() string { return m.text }
+
+type mockVisibleObj struct{ visible bool }
+
+func (m mockVisibleObj) Visible() bool { return m.visible }
+
+type mockSizeObj struct{ w, h int }
+
+func (m mockSizeObj) Size() (int, int) { return m.w, m.h }
+
+type mockChildObj struct{ count int }
+
+func (m mockChildObj) ChildrenCount() int { return m.count }
+
+// --- recorder ---
+
+// recorder satisfies TB and captures whether Errorf was called so that
+// tests of the assert package's failure paths do not propagate unexpected
+// failures to the outer test.
+type recorder struct {
+ failed bool
+}
+
+func (r *recorder) Helper() {}
+func (r *recorder) Errorf(format string, args ...interface{}) {
+ r.failed = true
+}
+
+// --- TestText ---
+
+func TestText(t *testing.T) {
+ tests := []struct {
+ name string
+ obj mockTextObj
+ expected string
+ wantPass bool
+ }{
+ {"matching text", mockTextObj{"hello"}, "hello", true},
+ {"mismatched text", mockTextObj{"hello"}, "world", false},
+ {"empty strings equal", mockTextObj{""}, "", true},
+ {"empty vs non-empty", mockTextObj{""}, "x", false},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ rec := &recorder{}
+ got := Text(rec, tc.obj, tc.expected)
+ if got != tc.wantPass {
+ t.Errorf("Text() returned %v, want %v", got, tc.wantPass)
+ }
+ // Verify Errorf was called exactly when the assertion should fail.
+ if rec.failed == tc.wantPass {
+ t.Errorf("Errorf called=%v but wantPass=%v", rec.failed, tc.wantPass)
+ }
+ })
+ }
+}
+
+// TestTextMsg verifies that the optional message is forwarded on failure.
+func TestTextMsg(t *testing.T) {
+ rec := &recorder{}
+ Text(rec, mockTextObj{"a"}, "b", "extra context info")
+ if !rec.failed {
+ t.Error("expected Errorf to be called on failure")
+ }
+}
+
+// --- TestVisible / TestNotVisible ---
+
+func TestVisible(t *testing.T) {
+ tests := []struct {
+ name string
+ visible bool
+ wantPass bool
+ }{
+ {"visible passes", true, true},
+ {"not visible fails", false, false},
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ rec := &recorder{}
+ got := Visible(rec, mockVisibleObj{tc.visible})
+ if got != tc.wantPass {
+ t.Errorf("Visible() returned %v, want %v", got, tc.wantPass)
+ }
+ })
+ }
+}
+
+func TestNotVisible(t *testing.T) {
+ tests := []struct {
+ name string
+ visible bool
+ wantPass bool
+ }{
+ {"not visible passes", false, true},
+ {"visible fails", true, false},
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ rec := &recorder{}
+ got := NotVisible(rec, mockVisibleObj{tc.visible})
+ if got != tc.wantPass {
+ t.Errorf("NotVisible() returned %v, want %v", got, tc.wantPass)
+ }
+ })
+ }
+}
+
+// --- TestSize ---
+
+func TestSize(t *testing.T) {
+ tests := []struct {
+ name string
+ w, h int
+ expW int
+ expH int
+ wantPass bool
+ }{
+ {"exact match", 100, 200, 100, 200, true},
+ {"wrong width", 99, 200, 100, 200, false},
+ {"wrong height", 100, 199, 100, 200, false},
+ {"both wrong", 1, 2, 3, 4, false},
+ {"zero size", 0, 0, 0, 0, true},
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ rec := &recorder{}
+ got := Size(rec, mockSizeObj{tc.w, tc.h}, tc.expW, tc.expH)
+ if got != tc.wantPass {
+ t.Errorf("Size() returned %v, want %v", got, tc.wantPass)
+ }
+ })
+ }
+}
+
+// --- TestChildCount ---
+
+func TestChildCount(t *testing.T) {
+ tests := []struct {
+ name string
+ count int
+ want int
+ wantPass bool
+ }{
+ {"exact", 3, 3, true},
+ {"too few", 2, 3, false},
+ {"too many", 4, 3, false},
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ rec := &recorder{}
+ got := ChildCount(rec, mockChildObj{tc.count}, tc.want)
+ if got != tc.wantPass {
+ t.Errorf("ChildCount() returned %v, want %v", got, tc.wantPass)
+ }
+ })
+ }
+}
+
+// --- TestEqual ---
+
+func TestEqual(t *testing.T) {
+ tests := []struct {
+ name string
+ expected interface{}
+ actual interface{}
+ wantPass bool
+ }{
+ {"equal ints", 42, 42, true},
+ {"unequal ints", 42, 43, false},
+ {"equal strings", "foo", "foo", true},
+ {"unequal strings", "foo", "bar", false},
+ {"equal bool true", true, true, true},
+ {"unequal bool", true, false, false},
+ {"nil equal", nil, nil, true},
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ rec := &recorder{}
+ got := Equal(rec, tc.expected, tc.actual)
+ if got != tc.wantPass {
+ t.Errorf("Equal() returned %v, want %v", got, tc.wantPass)
+ }
+ })
+ }
+}
+
+// --- TestNotNil ---
+
+func TestNotNil(t *testing.T) {
+ t.Run("non-nil passes", func(t *testing.T) {
+ rec := &recorder{}
+ v := "hello"
+ if !NotNil(rec, v) {
+ t.Error("NotNil() returned false for non-nil value")
+ }
+ if rec.failed {
+ t.Error("Errorf called unexpectedly for non-nil value")
+ }
+ })
+
+ t.Run("nil fails", func(t *testing.T) {
+ rec := &recorder{}
+ got := NotNil(rec, nil)
+ if got {
+ t.Error("NotNil() returned true for nil")
+ }
+ if !rec.failed {
+ t.Error("expected Errorf to be called for nil")
+ }
+ })
+}
+
+// --- TestGoldenImageComparison ---
+
+// TestGoldenImageComparison exercises the internal imagesEqual and channelClose
+// functions directly, covering identical images, different images, and
+// tolerance scenarios.
+func TestGoldenImageComparison(t *testing.T) {
+ makeImg := func(c color.RGBA) image.Image {
+ img := image.NewRGBA(image.Rect(0, 0, 2, 2))
+ for y := 0; y < 2; y++ {
+ for x := 0; x < 2; x++ {
+ img.Set(x, y, c)
+ }
+ }
+ return img
+ }
+
+ red := color.RGBA{R: 255, A: 255}
+ almostRed := color.RGBA{R: 253, A: 255} // 2 off in R channel
+ blue := color.RGBA{B: 255, A: 255}
+
+ t.Run("identical images are equal", func(t *testing.T) {
+ a := makeImg(red)
+ b := makeImg(red)
+ if !imagesEqual(a, b, 0) {
+ t.Error("identical images reported as not equal")
+ }
+ })
+
+ t.Run("different images are not equal at zero tolerance", func(t *testing.T) {
+ a := makeImg(red)
+ b := makeImg(blue)
+ if imagesEqual(a, b, 0) {
+ t.Error("different images reported as equal")
+ }
+ })
+
+ t.Run("within tolerance passes", func(t *testing.T) {
+ a := makeImg(red)
+ b := makeImg(almostRed) // delta = 2 in R channel
+ if !imagesEqual(a, b, 2) {
+ t.Error("images within tolerance reported as not equal")
+ }
+ })
+
+ t.Run("just outside tolerance fails", func(t *testing.T) {
+ a := makeImg(red)
+ b := makeImg(almostRed) // delta = 2 in R channel
+ if imagesEqual(a, b, 1) {
+ t.Error("images outside tolerance reported as equal")
+ }
+ })
+
+ t.Run("different bounds are not equal", func(t *testing.T) {
+ a := image.NewRGBA(image.Rect(0, 0, 2, 2))
+ b := image.NewRGBA(image.Rect(0, 0, 3, 3))
+ if imagesEqual(a, b, 0) {
+ t.Error("images with different bounds reported as equal")
+ }
+ })
+
+ t.Run("channelClose symmetric", func(t *testing.T) {
+ if channelClose(200, 150) != 50 {
+ t.Errorf("channelClose(200,150) = %d, want 50", channelClose(200, 150))
+ }
+ if channelClose(150, 200) != 50 {
+ t.Errorf("channelClose(150,200) = %d, want 50", channelClose(150, 200))
+ }
+ if channelClose(0, 0) != 0 {
+ t.Errorf("channelClose(0,0) = %d, want 0", channelClose(0, 0))
+ }
+ })
+}
diff --git a/tests/assert/golden.go b/tests/assert/golden.go
new file mode 100644
index 0000000..590668c
--- /dev/null
+++ b/tests/assert/golden.go
@@ -0,0 +1,128 @@
+package assert
+
+import (
+ "flag"
+ "fmt"
+ "image"
+ "image/png"
+ "os"
+ "path/filepath"
+)
+
+// Tolerance is the maximum per-channel absolute difference allowed when
+// comparing two images with Golden. A value of 0 (the default) requires
+// pixel-perfect equality.
+var Tolerance uint8 = 0
+
+var updateGolden = flag.Bool("update", false, "update golden files")
+
+// Golden compares img against the PNG stored in testdata/<name>.png (relative
+// to the directory of the calling test file as reported by t.Name).
+//
+// When the -update flag is set the golden file is written/overwritten with img
+// and the assertion always passes. Otherwise the stored image is loaded and
+// compared pixel-by-pixel using the global Tolerance threshold. On mismatch
+// the actual image is saved to testdata/<name>_actual.png for inspection.
+func Golden(t TB, img image.Image, name string, msgAndArgs ...interface{}) bool {
+ t.Helper()
+
+ dir := filepath.Join("testdata")
+ goldenPath := filepath.Join(dir, name+".png")
+
+ if *updateGolden {
+ if err := saveImage(goldenPath, img); err != nil {
+ fail(t, annotate(fmt.Sprintf("Golden: failed to write golden file %s: %v", goldenPath, err), formatMsg(msgAndArgs...)))
+ return false
+ }
+ return true
+ }
+
+ golden, err := loadImage(goldenPath)
+ if err != nil {
+ fail(t, annotate(fmt.Sprintf("Golden: failed to load golden file %s: %v", goldenPath, err), formatMsg(msgAndArgs...)))
+ return false
+ }
+
+ if imagesEqual(img, golden, Tolerance) {
+ return true
+ }
+
+ actualPath := filepath.Join(dir, name+"_actual.png")
+ // Best-effort save of the actual image; ignore the error since we are
+ // already about to report a test failure.
+ _ = saveImage(actualPath, img)
+
+ fail(t, annotate(
+ fmt.Sprintf("Golden: image mismatch for %q (actual saved to %s)", name, actualPath),
+ formatMsg(msgAndArgs...),
+ ))
+ return false
+}
+
+// saveImage encodes img as PNG to the given path, creating parent directories
+// as needed.
+func saveImage(path string, img image.Image) error {
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ return fmt.Errorf("create directory: %w", err)
+ }
+ f, err := os.Create(path)
+ if err != nil {
+ return fmt.Errorf("create file: %w", err)
+ }
+ defer f.Close()
+ if err := png.Encode(f, img); err != nil {
+ return fmt.Errorf("encode PNG: %w", err)
+ }
+ return nil
+}
+
+// loadImage decodes a PNG from path and returns it.
+func loadImage(path string) (image.Image, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, fmt.Errorf("open file: %w", err)
+ }
+ defer f.Close()
+ img, err := png.Decode(f)
+ if err != nil {
+ return nil, fmt.Errorf("decode PNG: %w", err)
+ }
+ return img, nil
+}
+
+// channelClose returns the absolute difference between two uint8 channel
+// values, i.e. |a - b|.
+func channelClose(a, b uint8) uint8 {
+ if a >= b {
+ return a - b
+ }
+ return b - a
+}
+
+// imagesEqual returns true when every corresponding pixel of a and b differs
+// by at most tolerance in every RGBA channel. Images of different sizes are
+// never equal.
+func imagesEqual(a, b image.Image, tolerance uint8) bool {
+ ab := a.Bounds()
+ bb := b.Bounds()
+ if ab != bb {
+ return false
+ }
+
+ for y := ab.Min.Y; y < ab.Max.Y; y++ {
+ for x := ab.Min.X; x < ab.Max.X; x++ {
+ ra, ga, ba, aa := a.At(x, y).RGBA()
+ rb, gb, bb2, ab2 := b.At(x, y).RGBA()
+
+ // RGBA() returns 16-bit values; shift to 8-bit for comparison.
+ if channelClose(uint8(ra>>8), uint8(rb>>8)) > tolerance ||
+ channelClose(uint8(ga>>8), uint8(gb>>8)) > tolerance ||
+ channelClose(uint8(ba>>8), uint8(bb2>>8)) > tolerance ||
+ channelClose(uint8(aa>>8), uint8(ab2>>8)) > tolerance {
+ return false
+ }
+ }
+ }
+ return true
+}
+
diff --git a/tests/require/require.go b/tests/require/require.go
new file mode 100644
index 0000000..37f6c1b
--- /dev/null
+++ b/tests/require/require.go
@@ -0,0 +1,85 @@
+// Package require provides fatal test assertions that mirror the assert package.
+// Each function delegates to the corresponding assert function and calls
+// t.FailNow when the assertion fails, stopping the test immediately.
+package require
+
+import (
+ "image"
+ "testing"
+
+ "git.enlightenment.org/cedric/ego/tests/assert"
+)
+
+// TB is the subset of testing.TB required by this package: it extends
+// assert.TB with FailNow so that failing assertions abort the test.
+type TB interface {
+ assert.TB
+ FailNow()
+}
+
+// Ensure *testing.T satisfies TB at compile time.
+var _ TB = (*testing.T)(nil)
+
+// Text asserts that obj.Text() returns expected, stopping the test on failure.
+func Text(t TB, obj interface{ Text() string }, expected string, msgAndArgs ...interface{}) {
+ t.Helper()
+ if !assert.Text(t, obj, expected, msgAndArgs...) {
+ t.FailNow()
+ }
+}
+
+// Visible asserts that obj.Visible() returns true, stopping the test on failure.
+func Visible(t TB, obj interface{ Visible() bool }, msgAndArgs ...interface{}) {
+ t.Helper()
+ if !assert.Visible(t, obj, msgAndArgs...) {
+ t.FailNow()
+ }
+}
+
+// NotVisible asserts that obj.Visible() returns false, stopping the test on failure.
+func NotVisible(t TB, obj interface{ Visible() bool }, msgAndArgs ...interface{}) {
+ t.Helper()
+ if !assert.NotVisible(t, obj, msgAndArgs...) {
+ t.FailNow()
+ }
+}
+
+// Size asserts that obj.Size() returns (w, h), stopping the test on failure.
+func Size(t TB, obj interface{ Size() (int, int) }, w, h int, msgAndArgs ...interface{}) {
+ t.Helper()
+ if !assert.Size(t, obj, w, h, msgAndArgs...) {
+ t.FailNow()
+ }
+}
+
+// ChildCount asserts that obj.ChildrenCount() returns n, stopping the test on failure.
+func ChildCount(t TB, obj interface{ ChildrenCount() int }, n int, msgAndArgs ...interface{}) {
+ t.Helper()
+ if !assert.ChildCount(t, obj, n, msgAndArgs...) {
+ t.FailNow()
+ }
+}
+
+// Equal asserts that expected and actual are equal, stopping the test on failure.
+func Equal(t TB, expected, actual interface{}, msgAndArgs ...interface{}) {
+ t.Helper()
+ if !assert.Equal(t, expected, actual, msgAndArgs...) {
+ t.FailNow()
+ }
+}
+
+// NotNil asserts that obj is not nil, stopping the test on failure.
+func NotNil(t TB, obj interface{}, msgAndArgs ...interface{}) {
+ t.Helper()
+ if !assert.NotNil(t, obj, msgAndArgs...) {
+ t.FailNow()
+ }
+}
+
+// Golden compares img against the stored golden PNG, stopping the test on failure.
+func Golden(t TB, img image.Image, name string, msgAndArgs ...interface{}) {
+ t.Helper()
+ if !assert.Golden(t, img, name, msgAndArgs...) {
+ t.FailNow()
+ }
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.