all repos

x @ 8cb64b3

go extra()
6 files changed, 424 insertions(+), 0 deletions(-)
is: add
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
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 doc.go
···
        
        1
        +package x

      
A go.mod
···
        
        1
        +module olexsmir.xyz/x

      
        
        2
        +

      
        
        3
        +go 1.25.3

      
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
        +}