6 files changed,
424 insertions(+),
0 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2025-12-01 16:49:19 +0200
Authored at:
2025-12-01 16:49:08 +0200
Change ID:
vktopurxlsvnqrwqvuykqlqyvunmurpn
jump to
| A | .github/workflows/ci.yml |
| A | doc.go |
| A | go.mod |
| A | is/doc.go |
| A | is/is.go |
| A | is/is_test.go |
A
.github/workflows/ci.yml
··· 1 +name: ci 2 +on: 3 + workflow_dispatch: 4 + push: 5 + branches: [main] 6 + paths: ["**.go", "go.mod", "go.sum"] 7 + pull_request: 8 + paths: ["**.go", "go.mod", "go.sum"] 9 + 10 +jobs: 11 + tests: 12 + runs-on: ubuntu-latest 13 + steps: 14 + - uses: actions/checkout@v5 15 + 16 + - name: setup go 17 + uses: actions/setup-go@v5 18 + with: 19 + go-version-file: go.mod 20 + cache-dependency-path: go.mod 21 + 22 + - name: test 23 + run: go test -v ./...
A
is/doc.go
··· 1 +// Package is provides minimal assertions for tests. 2 +// 3 +// Example: 4 +// 5 +// func TestX(t *testing.T) { 6 +// var a = "Hello, Gopher!" 7 +// var b = "Hello, Gopher!" 8 +// is.Equal(t, a, b) 9 +// 10 +// var err = errors.New("I'm an error!") 11 +// is.Err(t, err, nil) 12 +// } 13 +package is
A
is/is.go
··· 1 +package is 2 + 3 +import ( 4 + "bytes" 5 + "errors" 6 + "reflect" 7 + "strings" 8 + "testing" 9 +) 10 + 11 +func Equal[T any](tb testing.TB, expected, got T) { 12 + tb.Helper() 13 + 14 + if !areEqual(expected, got) { 15 + tb.Errorf("expected: %#v, got: %#v", expected, got) 16 + } 17 +} 18 + 19 +func Err(tb testing.TB, got error, expected any) { 20 + tb.Helper() 21 + 22 + if expected != nil && got == nil { 23 + tb.Error("got: <nil>, expected: error") 24 + return 25 + } 26 + 27 + switch e := expected.(type) { 28 + case nil: 29 + if got != nil { 30 + tb.Fatalf("unexpected error: %v", got) 31 + } 32 + 33 + case string: 34 + if !strings.Contains(got.Error(), e) { 35 + tb.Fatalf("expected: %q, got: %q", got.Error(), e) 36 + } 37 + 38 + case error: 39 + if !errors.Is(got, e) { 40 + tb.Fatalf("expected: %T(%v), got: %T(%v)", got, got, e, e) 41 + } 42 + 43 + case reflect.Type: 44 + target := reflect.New(e).Interface() 45 + if !errors.As(got, target) { 46 + tb.Fatalf("expected: %s, got: %T", e, got) 47 + } 48 + 49 + default: 50 + tb.Fatalf("unexpected type: %T", expected) 51 + } 52 +} 53 + 54 +type equaler[T any] interface{ Equal(T) bool } 55 + 56 +func areEqual[T any](a, b T) bool { 57 + if isNil(a) && isNil(b) { 58 + return true 59 + } 60 + 61 + // some types provide .Equal(like time.Time, net.IP) 62 + if eq, ok := any(a).(equaler[T]); ok { 63 + return eq.Equal(b) 64 + } 65 + if aBytes, ok := any(a).([]byte); ok { 66 + bBytes := any(b).([]byte) 67 + return bytes.Equal(aBytes, bBytes) 68 + } 69 + 70 + return reflect.DeepEqual(a, b) 71 +} 72 + 73 +func isNil(v any) bool { 74 + if v == nil { 75 + return true 76 + } 77 + 78 + // non-nil interface can hold a nil value, check the underlying value. 79 + rv := reflect.ValueOf(v) 80 + switch rv.Kind() { 81 + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice, reflect.UnsafePointer: 82 + return rv.IsNil() 83 + default: 84 + return false 85 + } 86 +}
A
is/is_test.go
··· 1 +package is_test 2 + 3 +import ( 4 + "errors" 5 + "fmt" 6 + "io/fs" 7 + "math/rand/v2" 8 + "reflect" 9 + "testing" 10 + "time" 11 + 12 + "olexsmir.xyz/x/is" 13 +) 14 + 15 +func TestEqual(t *testing.T) { 16 + t.Run("equal", func(t *testing.T) { 17 + now := time.Now() 18 + val := 420 19 + 20 + tests := map[string]struct{ got, expected any }{ 21 + "int": {69, 69}, 22 + "string": {"hello", "hello"}, 23 + "bool": {true, true}, 24 + "struct": {intType{1}, intType{1}}, 25 + "pointer": {&val, &val}, 26 + "nil": {nil, nil}, 27 + "nil pointer": {(*int)(nil), (*int)(nil)}, 28 + "nil map": {map[string]int(nil), map[string]int(nil)}, 29 + "nil chan": {(chan int)(nil), (chan int)(nil)}, 30 + "nil slice": {[]int(nil), []int(nil)}, 31 + "byte slice": {[]byte("abc"), []byte("abc")}, 32 + "int slice": {[]int{1, 2}, []int{1, 2}}, 33 + "string slice": {[]string{"a", "b"}, []string{"a", "b"}}, 34 + "empty map": {map[string]int{}, map[string]int{}}, 35 + "map": {map[string]int{"a": 1}, map[string]int{"a": 1}}, 36 + "time.Time": {now, now}, 37 + } 38 + for tname, tt := range tests { 39 + t.Run(tname, func(t *testing.T) { 40 + ft := &fakeT{} 41 + is.Equal(ft, tt.expected, tt.got) 42 + if ft.failed { 43 + t.Errorf("failed: %s", ft.msg) 44 + } 45 + }) 46 + } 47 + }) 48 + 49 + t.Run("non-equal", func(t *testing.T) { 50 + now := time.Now() 51 + val1, val2 := 69, 420 52 + tests := map[string]struct { 53 + expected, got any 54 + expectedMsg string 55 + }{ 56 + "int": {228, 1337, `expected: 228, got: 1337`}, 57 + "string": {"hello", "world", `expected: "hello", got: "world"`}, 58 + "bool": {true, false, "expected: true, got: false"}, 59 + "struct": {intType{1}, intType{2}, `expected: is_test.intType{v:1}, got: is_test.intType{v:2}`}, 60 + "pointer": {&val1, &val2, ``}, 61 + "nil/non-nil": {nil, 2, `expected: <nil>, got: 2`}, 62 + "non-nil/nil": {2, nil, `expected: 2, got: <nil>`}, 63 + "nil slice/empty": {[]int(nil), []int{}, `expected: []int(nil), got: []int{}`}, 64 + "int slice": {[]int{1, 2}, []int{2, 1}, `expected: []int{1, 2}, got: []int{2, 1}`}, 65 + "byte slice": {[]byte("abc"), []byte("def"), `expected: []byte{0x61, 0x62, 0x63}, got: []byte{0x64, 0x65, 0x66}`}, 66 + "chan": {make(chan int), make(chan int), ``}, 67 + "string slice": {[]string{"a", "b"}, []string{"b", "a"}, `expected: []string{"a", "b"}, got: []string{"b", "a"}`}, 68 + "map": {map[string]int{"a": 1}, map[string]int{"a": 2}, `expected: map[string]int{"a":1}, got: map[string]int{"a":2}`}, 69 + "time.Time": {now, now.Add(time.Second), ``}, 70 + } 71 + 72 + for tname, tt := range tests { 73 + t.Run(tname, func(t *testing.T) { 74 + ft := &fakeT{} 75 + is.Equal(ft, tt.expected, tt.got) 76 + shouldHaveFailed(t, ft) 77 + if tt.expectedMsg != "" { 78 + expectMsg(t, ft, tt.expectedMsg) 79 + } 80 + }) 81 + } 82 + }) 83 + 84 + t.Run("implements equaler", func(t *testing.T) { 85 + ft := &fakeT{} 86 + val1, val2 := newRandomy(1), newRandomy(1) 87 + is.Equal(ft, val1, val2) 88 + if ft.failed { 89 + t.Errorf("%#v == %#v: should have passed", val1, val2) 90 + } 91 + }) 92 + 93 + t.Run("implements equaler, not empty", func(t *testing.T) { 94 + ft := &fakeT{} 95 + val1, val2 := newRandomy(1), newRandomy(2) 96 + is.Equal(ft, val1, val2) 97 + shouldHaveFailed(t, ft) 98 + }) 99 + 100 + t.Run("time.Time", func(t *testing.T) { 101 + ft := &fakeT{} 102 + val1 := time.Date(2022, 1, 3, 0, 0, 18, 0, time.UTC) 103 + val2 := time.Date(2022, 1, 3, 5, 0, 18, 0, time.FixedZone("UTC+5", 5*3600)) 104 + is.Equal(ft, val1, val1) 105 + if ft.failed { 106 + t.Errorf("%#v == %#v: should have passed", val1, val2) 107 + } 108 + }) 109 +} 110 + 111 +func TestErr(t *testing.T) { 112 + t.Run("expected nil, got nil", func(t *testing.T) { 113 + ft := &fakeT{} 114 + is.Err(ft, nil, nil) 115 + if ft.failed { 116 + t.Errorf("failed: %s", ft.msg) 117 + } 118 + }) 119 + 120 + t.Run("expected nil, got error", func(t *testing.T) { 121 + ft := &fakeT{} 122 + is.Err(ft, errors.New("an error"), nil) 123 + 124 + shouldBeFatal(t, ft) 125 + shouldHaveFailed(t, ft) 126 + expectMsg(t, ft, "unexpected error: an error") 127 + }) 128 + 129 + t.Run("expected error, got nil", func(t *testing.T) { 130 + ft := &fakeT{} 131 + is.Err(ft, nil, errors.New("err")) 132 + 133 + shouldHaveFailed(t, ft) 134 + expectMsg(t, ft, `got: <nil>, expected: error`) 135 + }) 136 + 137 + t.Run("expected error, got same error", func(t *testing.T) { 138 + ft := &fakeT{} 139 + err := errors.New("an error") 140 + is.Err(ft, err, err) 141 + if ft.failed { 142 + t.Errorf("failed: %s", ft.msg) 143 + } 144 + }) 145 + 146 + t.Run("expected error, got different error", func(t *testing.T) { 147 + ft := &fakeT{} 148 + err1 := errors.New("an error") 149 + err2 := errors.New("second error") 150 + is.Err(ft, err1, err2) 151 + 152 + shouldHaveFailed(t, ft) 153 + shouldBeFatal(t, ft) 154 + expectMsg(t, ft, `expected: *errors.errorString(an error), got: *errors.errorString(second error)`) 155 + }) 156 + 157 + t.Run("expected error, got error of different type", func(t *testing.T) { 158 + ft := &fakeT{} 159 + err1 := errors.New("an error") 160 + err2 := errType("my error") 161 + is.Err(ft, err1, err2) 162 + 163 + shouldHaveFailed(t, ft) 164 + shouldBeFatal(t, ft) 165 + expectMsg(t, ft, `expected: *errors.errorString(an error), got: is_test.errType(my error)`) 166 + }) 167 + 168 + t.Run("expected error, got wrapped error", func(t *testing.T) { 169 + ft := &fakeT{} 170 + err := errors.New("an error") 171 + werr := fmt.Errorf("wrapped: %w", err) 172 + is.Err(ft, werr, err) 173 + if ft.failed { 174 + t.Errorf("failed: %s", ft.msg) 175 + } 176 + }) 177 + 178 + t.Run("got error, string", func(t *testing.T) { 179 + ft := &fakeT{} 180 + is.Err(ft, errors.New("test string"), "test string") 181 + if ft.failed { 182 + t.Errorf("failed: %s", ft.msg) 183 + } 184 + }) 185 + 186 + t.Run("got error, partial string", func(t *testing.T) { 187 + ft := &fakeT{} 188 + is.Err(ft, errors.New("test string"), "string") 189 + if ft.failed { 190 + t.Errorf("failed: %s", ft.msg) 191 + } 192 + }) 193 + 194 + t.Run("got error, not containing string", func(t *testing.T) { 195 + ft := &fakeT{} 196 + is.Err(ft, errors.New("test string"), "asdfasdf") 197 + 198 + shouldHaveFailed(t, ft) 199 + shouldBeFatal(t, ft) 200 + expectMsg(t, ft, `expected: "test string", got: "asdfasdf"`) 201 + }) 202 + 203 + t.Run("got custom type, same types", func(t *testing.T) { 204 + ft := &fakeT{} 205 + err := errType("custom error") 206 + is.Err(ft, err, err) 207 + if ft.failed { 208 + t.Errorf("failed: %s", ft.msg) 209 + } 210 + }) 211 + 212 + t.Run("got custom type, different types", func(t *testing.T) { 213 + ft := &fakeT{} 214 + err := errType("custom error") 215 + is.Err(ft, err, reflect.TypeFor[*fs.PathError]()) 216 + shouldHaveFailed(t, ft) 217 + shouldBeFatal(t, ft) 218 + expectMsg(t, ft, `expected: *fs.PathError, got: is_test.errType`) 219 + }) 220 + 221 + t.Run("unexpected type", func(t *testing.T) { 222 + ft := &fakeT{} 223 + is.Err(ft, errors.New("asdf"), 0) 224 + shouldHaveFailed(t, ft) 225 + shouldBeFatal(t, ft) 226 + expectMsg(t, ft, `unexpected type: int`) 227 + }) 228 +} 229 + 230 +func expectMsg(t *testing.T, ft *fakeT, expectedMsg string) { 231 + t.Helper() 232 + if ft.msg != expectedMsg { 233 + t.Errorf("expected msg: %q; got: %q", expectedMsg, ft.msg) 234 + } 235 +} 236 + 237 +func shouldBeFatal(t *testing.T, ft *fakeT) { 238 + t.Helper() 239 + if !ft.fatal { 240 + t.Error("should be fatal") 241 + } 242 +} 243 + 244 +func shouldHaveFailed(t *testing.T, ft *fakeT) { 245 + t.Helper() 246 + if !ft.failed { 247 + t.Error("should have failed") 248 + } 249 +} 250 + 251 +// helper types --- 252 + 253 +type intType struct{ v int } 254 + 255 +type randomy struct { 256 + val int 257 + rnd float64 258 +} 259 + 260 +func newRandomy(val int) randomy { return randomy{val, rand.Float64()} } 261 +func (n randomy) Equal(other randomy) bool { return n.val == other.val } 262 + 263 +type errType string 264 + 265 +func (e errType) Error() string { return string(e) } 266 + 267 +// mockT ---- 268 + 269 +type fakeT struct { 270 + testing.TB 271 + failed bool 272 + fatal bool 273 + msg string 274 +} 275 + 276 +func (m *fakeT) Helper() {} 277 + 278 +func (m *fakeT) Log(a ...any) { fmt.Println(a...) } 279 + 280 +func (m *fakeT) Fatal(args ...any) { 281 + m.fatal = true 282 + m.Error(args...) 283 +} 284 + 285 +func (m *fakeT) Fatalf(format string, args ...any) { 286 + m.fatal = true 287 + m.Errorf(format, args...) 288 +} 289 + 290 +func (m *fakeT) Error(args ...any) { 291 + m.failed = true 292 + m.msg = fmt.Sprint(args...) 293 +} 294 + 295 +func (m *fakeT) Errorf(format string, args ...any) { 296 + m.failed = true 297 + m.msg = fmt.Sprintf(format, args...) 298 +}