all repos

test1 @ 8cb64b3

for testing purposes
6 files changed, 424 insertions(+), 0 deletions(-)
is: add
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2025-12-01 16:49:19 +0200
Change ID: vktopurxlsvnqrwqvuykqlqyvunmurpn
A .github/workflows/ci.yml

@@ -0,0 +1,23 @@

+name: ci +on: + workflow_dispatch: + push: + branches: [main] + paths: ["**.go", "go.mod", "go.sum"] + pull_request: + paths: ["**.go", "go.mod", "go.sum"] + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: setup go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache-dependency-path: go.mod + + - name: test + run: go test -v ./...
A doc.go

@@ -0,0 +1,1 @@

+package x
A go.mod

@@ -0,0 +1,3 @@

+module olexsmir.xyz/x + +go 1.25.3
A is/doc.go

@@ -0,0 +1,13 @@

+// Package is provides minimal assertions for tests. +// +// Example: +// +// func TestX(t *testing.T) { +// var a = "Hello, Gopher!" +// var b = "Hello, Gopher!" +// is.Equal(t, a, b) +// +// var err = errors.New("I'm an error!") +// is.Err(t, err, nil) +// } +package is
A is/is.go

@@ -0,0 +1,86 @@

+package is + +import ( + "bytes" + "errors" + "reflect" + "strings" + "testing" +) + +func Equal[T any](tb testing.TB, expected, got T) { + tb.Helper() + + if !areEqual(expected, got) { + tb.Errorf("expected: %#v, got: %#v", expected, got) + } +} + +func Err(tb testing.TB, got error, expected any) { + tb.Helper() + + if expected != nil && got == nil { + tb.Error("got: <nil>, expected: error") + return + } + + switch e := expected.(type) { + case nil: + if got != nil { + tb.Fatalf("unexpected error: %v", got) + } + + case string: + if !strings.Contains(got.Error(), e) { + tb.Fatalf("expected: %q, got: %q", got.Error(), e) + } + + case error: + if !errors.Is(got, e) { + tb.Fatalf("expected: %T(%v), got: %T(%v)", got, got, e, e) + } + + case reflect.Type: + target := reflect.New(e).Interface() + if !errors.As(got, target) { + tb.Fatalf("expected: %s, got: %T", e, got) + } + + default: + tb.Fatalf("unexpected type: %T", expected) + } +} + +type equaler[T any] interface{ Equal(T) bool } + +func areEqual[T any](a, b T) bool { + if isNil(a) && isNil(b) { + return true + } + + // some types provide .Equal(like time.Time, net.IP) + if eq, ok := any(a).(equaler[T]); ok { + return eq.Equal(b) + } + if aBytes, ok := any(a).([]byte); ok { + bBytes := any(b).([]byte) + return bytes.Equal(aBytes, bBytes) + } + + return reflect.DeepEqual(a, b) +} + +func isNil(v any) bool { + if v == nil { + return true + } + + // non-nil interface can hold a nil value, check the underlying value. + rv := reflect.ValueOf(v) + switch rv.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice, reflect.UnsafePointer: + return rv.IsNil() + default: + return false + } +}
A is/is_test.go

@@ -0,0 +1,298 @@

+package is_test + +import ( + "errors" + "fmt" + "io/fs" + "math/rand/v2" + "reflect" + "testing" + "time" + + "olexsmir.xyz/x/is" +) + +func TestEqual(t *testing.T) { + t.Run("equal", func(t *testing.T) { + now := time.Now() + val := 420 + + tests := map[string]struct{ got, expected any }{ + "int": {69, 69}, + "string": {"hello", "hello"}, + "bool": {true, true}, + "struct": {intType{1}, intType{1}}, + "pointer": {&val, &val}, + "nil": {nil, nil}, + "nil pointer": {(*int)(nil), (*int)(nil)}, + "nil map": {map[string]int(nil), map[string]int(nil)}, + "nil chan": {(chan int)(nil), (chan int)(nil)}, + "nil slice": {[]int(nil), []int(nil)}, + "byte slice": {[]byte("abc"), []byte("abc")}, + "int slice": {[]int{1, 2}, []int{1, 2}}, + "string slice": {[]string{"a", "b"}, []string{"a", "b"}}, + "empty map": {map[string]int{}, map[string]int{}}, + "map": {map[string]int{"a": 1}, map[string]int{"a": 1}}, + "time.Time": {now, now}, + } + for tname, tt := range tests { + t.Run(tname, func(t *testing.T) { + ft := &fakeT{} + is.Equal(ft, tt.expected, tt.got) + if ft.failed { + t.Errorf("failed: %s", ft.msg) + } + }) + } + }) + + t.Run("non-equal", func(t *testing.T) { + now := time.Now() + val1, val2 := 69, 420 + tests := map[string]struct { + expected, got any + expectedMsg string + }{ + "int": {228, 1337, `expected: 228, got: 1337`}, + "string": {"hello", "world", `expected: "hello", got: "world"`}, + "bool": {true, false, "expected: true, got: false"}, + "struct": {intType{1}, intType{2}, `expected: is_test.intType{v:1}, got: is_test.intType{v:2}`}, + "pointer": {&val1, &val2, ``}, + "nil/non-nil": {nil, 2, `expected: <nil>, got: 2`}, + "non-nil/nil": {2, nil, `expected: 2, got: <nil>`}, + "nil slice/empty": {[]int(nil), []int{}, `expected: []int(nil), got: []int{}`}, + "int slice": {[]int{1, 2}, []int{2, 1}, `expected: []int{1, 2}, got: []int{2, 1}`}, + "byte slice": {[]byte("abc"), []byte("def"), `expected: []byte{0x61, 0x62, 0x63}, got: []byte{0x64, 0x65, 0x66}`}, + "chan": {make(chan int), make(chan int), ``}, + "string slice": {[]string{"a", "b"}, []string{"b", "a"}, `expected: []string{"a", "b"}, got: []string{"b", "a"}`}, + "map": {map[string]int{"a": 1}, map[string]int{"a": 2}, `expected: map[string]int{"a":1}, got: map[string]int{"a":2}`}, + "time.Time": {now, now.Add(time.Second), ``}, + } + + for tname, tt := range tests { + t.Run(tname, func(t *testing.T) { + ft := &fakeT{} + is.Equal(ft, tt.expected, tt.got) + shouldHaveFailed(t, ft) + if tt.expectedMsg != "" { + expectMsg(t, ft, tt.expectedMsg) + } + }) + } + }) + + t.Run("implements equaler", func(t *testing.T) { + ft := &fakeT{} + val1, val2 := newRandomy(1), newRandomy(1) + is.Equal(ft, val1, val2) + if ft.failed { + t.Errorf("%#v == %#v: should have passed", val1, val2) + } + }) + + t.Run("implements equaler, not empty", func(t *testing.T) { + ft := &fakeT{} + val1, val2 := newRandomy(1), newRandomy(2) + is.Equal(ft, val1, val2) + shouldHaveFailed(t, ft) + }) + + t.Run("time.Time", func(t *testing.T) { + ft := &fakeT{} + val1 := time.Date(2022, 1, 3, 0, 0, 18, 0, time.UTC) + val2 := time.Date(2022, 1, 3, 5, 0, 18, 0, time.FixedZone("UTC+5", 5*3600)) + is.Equal(ft, val1, val1) + if ft.failed { + t.Errorf("%#v == %#v: should have passed", val1, val2) + } + }) +} + +func TestErr(t *testing.T) { + t.Run("expected nil, got nil", func(t *testing.T) { + ft := &fakeT{} + is.Err(ft, nil, nil) + if ft.failed { + t.Errorf("failed: %s", ft.msg) + } + }) + + t.Run("expected nil, got error", func(t *testing.T) { + ft := &fakeT{} + is.Err(ft, errors.New("an error"), nil) + + shouldBeFatal(t, ft) + shouldHaveFailed(t, ft) + expectMsg(t, ft, "unexpected error: an error") + }) + + t.Run("expected error, got nil", func(t *testing.T) { + ft := &fakeT{} + is.Err(ft, nil, errors.New("err")) + + shouldHaveFailed(t, ft) + expectMsg(t, ft, `got: <nil>, expected: error`) + }) + + t.Run("expected error, got same error", func(t *testing.T) { + ft := &fakeT{} + err := errors.New("an error") + is.Err(ft, err, err) + if ft.failed { + t.Errorf("failed: %s", ft.msg) + } + }) + + t.Run("expected error, got different error", func(t *testing.T) { + ft := &fakeT{} + err1 := errors.New("an error") + err2 := errors.New("second error") + is.Err(ft, err1, err2) + + shouldHaveFailed(t, ft) + shouldBeFatal(t, ft) + expectMsg(t, ft, `expected: *errors.errorString(an error), got: *errors.errorString(second error)`) + }) + + t.Run("expected error, got error of different type", func(t *testing.T) { + ft := &fakeT{} + err1 := errors.New("an error") + err2 := errType("my error") + is.Err(ft, err1, err2) + + shouldHaveFailed(t, ft) + shouldBeFatal(t, ft) + expectMsg(t, ft, `expected: *errors.errorString(an error), got: is_test.errType(my error)`) + }) + + t.Run("expected error, got wrapped error", func(t *testing.T) { + ft := &fakeT{} + err := errors.New("an error") + werr := fmt.Errorf("wrapped: %w", err) + is.Err(ft, werr, err) + if ft.failed { + t.Errorf("failed: %s", ft.msg) + } + }) + + t.Run("got error, string", func(t *testing.T) { + ft := &fakeT{} + is.Err(ft, errors.New("test string"), "test string") + if ft.failed { + t.Errorf("failed: %s", ft.msg) + } + }) + + t.Run("got error, partial string", func(t *testing.T) { + ft := &fakeT{} + is.Err(ft, errors.New("test string"), "string") + if ft.failed { + t.Errorf("failed: %s", ft.msg) + } + }) + + t.Run("got error, not containing string", func(t *testing.T) { + ft := &fakeT{} + is.Err(ft, errors.New("test string"), "asdfasdf") + + shouldHaveFailed(t, ft) + shouldBeFatal(t, ft) + expectMsg(t, ft, `expected: "test string", got: "asdfasdf"`) + }) + + t.Run("got custom type, same types", func(t *testing.T) { + ft := &fakeT{} + err := errType("custom error") + is.Err(ft, err, err) + if ft.failed { + t.Errorf("failed: %s", ft.msg) + } + }) + + t.Run("got custom type, different types", func(t *testing.T) { + ft := &fakeT{} + err := errType("custom error") + is.Err(ft, err, reflect.TypeFor[*fs.PathError]()) + shouldHaveFailed(t, ft) + shouldBeFatal(t, ft) + expectMsg(t, ft, `expected: *fs.PathError, got: is_test.errType`) + }) + + t.Run("unexpected type", func(t *testing.T) { + ft := &fakeT{} + is.Err(ft, errors.New("asdf"), 0) + shouldHaveFailed(t, ft) + shouldBeFatal(t, ft) + expectMsg(t, ft, `unexpected type: int`) + }) +} + +func expectMsg(t *testing.T, ft *fakeT, expectedMsg string) { + t.Helper() + if ft.msg != expectedMsg { + t.Errorf("expected msg: %q; got: %q", expectedMsg, ft.msg) + } +} + +func shouldBeFatal(t *testing.T, ft *fakeT) { + t.Helper() + if !ft.fatal { + t.Error("should be fatal") + } +} + +func shouldHaveFailed(t *testing.T, ft *fakeT) { + t.Helper() + if !ft.failed { + t.Error("should have failed") + } +} + +// helper types --- + +type intType struct{ v int } + +type randomy struct { + val int + rnd float64 +} + +func newRandomy(val int) randomy { return randomy{val, rand.Float64()} } +func (n randomy) Equal(other randomy) bool { return n.val == other.val } + +type errType string + +func (e errType) Error() string { return string(e) } + +// mockT ---- + +type fakeT struct { + testing.TB + failed bool + fatal bool + msg string +} + +func (m *fakeT) Helper() {} + +func (m *fakeT) Log(a ...any) { fmt.Println(a...) } + +func (m *fakeT) Fatal(args ...any) { + m.fatal = true + m.Error(args...) +} + +func (m *fakeT) Fatalf(format string, args ...any) { + m.fatal = true + m.Errorf(format, args...) +} + +func (m *fakeT) Error(args ...any) { + m.failed = true + m.msg = fmt.Sprint(args...) +} + +func (m *fakeT) Errorf(format string, args ...any) { + m.failed = true + m.msg = fmt.Sprintf(format, args...) +}