├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .golangci.yml ├── COPYING ├── README.md ├── assert.go ├── assert_test.go ├── bin ├── .go-1.22.3.pkg ├── .golangci-lint-1.58.2.pkg ├── README.hermit.md ├── activate-hermit ├── go ├── go-main-stubs ├── gofmt ├── golangci-lint ├── hermit └── hermit.hcl ├── go.mod ├── go.sum └── renovate.json5 /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [alecthomas] 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | name: CI 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | - name: Init Hermit 15 | run: ./bin/hermit env -r >> $GITHUB_ENV 16 | - name: Test 17 | run: go test ./... 18 | lint: 19 | name: Lint 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v2 24 | - name: Init Hermit 25 | run: ./bin/hermit env -r >> $GITHUB_ENV 26 | - name: golangci-lint 27 | run: golangci-lint run 28 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: true 3 | skip-dirs: 4 | - _examples 5 | 6 | output: 7 | print-issued-lines: false 8 | 9 | linters: 10 | enable-all: true 11 | disable: 12 | - maligned 13 | - lll 14 | - gocyclo 15 | - gochecknoglobals 16 | - wsl 17 | - whitespace 18 | - godox 19 | - funlen 20 | - gocognit 21 | - gomnd 22 | - goerr113 23 | - godot 24 | - nestif 25 | - testpackage 26 | - nolintlint 27 | - exhaustivestruct 28 | - wrapcheck 29 | - gci 30 | - gofumpt 31 | - gocritic 32 | - nlreturn 33 | - errorlint 34 | - nakedret 35 | - forbidigo 36 | - revive 37 | - cyclop 38 | - ifshort 39 | - paralleltest 40 | - interfacer 41 | - scopelint 42 | - golint 43 | - wastedassign 44 | - forcetypeassert 45 | - gomoddirectives 46 | - thelper 47 | - varnamelen 48 | - depguard 49 | - exhaustruct 50 | - nonamedreturns 51 | - varcheck 52 | - structcheck 53 | - deadcode 54 | - nosnakecase 55 | - mnd 56 | - perfsprint 57 | 58 | linters-settings: 59 | govet: 60 | check-shadowing: true 61 | gocyclo: 62 | min-complexity: 10 63 | dupl: 64 | threshold: 100 65 | goconst: 66 | min-len: 8 67 | min-occurrences: 3 68 | exhaustive: 69 | default-signifies-exhaustive: true 70 | 71 | issues: 72 | max-per-linter: 0 73 | max-same: 0 74 | exclude-use-default: false 75 | exclude: 76 | # Captured by errcheck. 77 | - '^(G104|G204|G307):' 78 | # Very commonly not checked. 79 | - 'Error return value of .(.*\.Help|.*\.MarkFlagRequired|(os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked' 80 | - 'exported method `(.*\.MarshalJSON|.*\.UnmarshalJSON|.*\.EntityURN|.*\.GoString|.*\.Pos)` should have comment or be unexported' 81 | - 'composite literal uses unkeyed fields' 82 | - 'declaration of "err" shadows declaration' 83 | - 'bad syntax for struct tag key' 84 | - 'bad syntax for struct tag pair' 85 | - '^ST1012' 86 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021 Alec Thomas 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A simple assertion library using Go generics 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/alecthomas/assert/v2)](https://pkg.go.dev/github.com/alecthomas/assert/v2) [![CI](https://github.com/alecthomas/assert/actions/workflows/ci.yml/badge.svg)](https://github.com/alecthomas/assert/actions/workflows/ci.yml) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/alecthomas/assert/v2)](https://goreportcard.com/report/github.com/alecthomas/assert/v2) [![Slack chat](https://img.shields.io/static/v1?logo=slack&style=flat&label=slack&color=green&message=gophers)](https://gophers.slack.com/messages/CN9DS8YF3) 5 | 6 | 7 | This library is inspired by testify/require, but with a significantly reduced 8 | API surface based on empirical use of that package. 9 | 10 | It also provides much nicer diff output, eg. 11 | 12 | ``` 13 | === RUN TestFail 14 | assert_test.go:14: Expected values to be equal: 15 | assert.Data{ 16 | - Str: "foo", 17 | + Str: "far", 18 | Num: 10, 19 | } 20 | --- FAIL: TestFail (0.00s) 21 | ``` 22 | 23 | ## API 24 | 25 | Import then use as `assert`: 26 | 27 | ```go 28 | import "github.com/alecthomas/assert/v2" 29 | ``` 30 | 31 | This library has the following API. For all functions, `msgAndArgs` is used to 32 | format error messages using the `fmt` package. 33 | 34 | ```go 35 | // Equal asserts that "expected" and "actual" are equal using google/go-cmp. 36 | // 37 | // If they are not, a diff of the Go representation of the values will be displayed. 38 | func Equal[T comparable](t testing.TB, expected, actual T, msgAndArgs ...interface{}) 39 | 40 | // NotEqual asserts that "expected" is not equal to "actual" using google/go-cmp. 41 | // 42 | // If they are equal the expected value will be displayed. 43 | func NotEqual[T comparable](t testing.TB, expected, actual T, msgAndArgs ...interface{}) 44 | 45 | // Zero asserts that a value is its zero value. 46 | func Zero[T comparable](t testing.TB, value T, msgAndArgs ...interface{}) 47 | 48 | // NotZero asserts that a value is not its zero value. 49 | func NotZero[T comparable](t testing.TB, value T, msgAndArgs ...interface{}) 50 | 51 | // Contains asserts that "haystack" contains "needle". 52 | func Contains(t testing.TB, haystack string, needle string, msgAndArgs ...interface{}) 53 | 54 | // NotContains asserts that "haystack" does not contain "needle". 55 | func NotContains(t testing.TB, haystack string, needle string, msgAndArgs ...interface{}) 56 | 57 | // EqualError asserts that either an error is non-nil and that its message is what is expected, 58 | // or that error is nil if the expected message is empty. 59 | func EqualError(t testing.TB, err error, errString string, msgAndArgs...interface{}) 60 | 61 | // Error asserts that an error is not nil. 62 | func Error(t testing.TB, err error, msgAndArgs ...interface{}) 63 | 64 | // NoError asserts that an error is nil. 65 | func NoError(t testing.TB, err error, msgAndArgs ...interface{}) 66 | 67 | // IsError asserts than any error in "err"'s tree matches "target". 68 | func IsError(t testing.TB, err, target error, msgAndArgs ...interface{}) 69 | 70 | // NotIsError asserts than no error in "err"'s tree matches "target". 71 | func NotIsError(t testing.TB, err, target error, msgAndArgs ...interface{}) 72 | 73 | // Panics asserts that the given function panics. 74 | func Panics(t testing.TB, fn func(), msgAndArgs ...interface{}) 75 | 76 | // NotPanics asserts that the given function does not panic. 77 | func NotPanics(t testing.TB, fn func(), msgAndArgs ...interface{}) 78 | 79 | // Compare two values for equality and return true or false. 80 | func Compare[T any](t testing.TB, x, y T) bool 81 | 82 | // True asserts that an expression is true. 83 | func True(t testing.TB, ok bool, msgAndArgs ...interface{}) 84 | 85 | // False asserts that an expression is false. 86 | func False(t testing.TB, ok bool, msgAndArgs ...interface{}) 87 | ``` 88 | 89 | ## Evaluation process 90 | 91 | Our empirical data of testify usage comes from a monorepo with around 50K lines 92 | of tests. 93 | 94 | These are the usage counts for all testify functions, normalised to the base 95 | (not `Printf()`) non-negative(not `No(t)?`) case for each core function. 96 | 97 | ```text 98 | 2240 Error 99 | 1314 Equal 100 | 219 True 101 | 210 Nil 102 | 167 Empty 103 | 107 Contains 104 | 79 Len 105 | 61 False 106 | 24 EqualValues 107 | 20 EqualError 108 | 17 Zero 109 | 15 Fail 110 | 15 ElementsMatch 111 | 9 Panics 112 | 7 IsType 113 | 6 FileExists 114 | 4 JSONEq 115 | 3 PanicsWithValue 116 | 3 Eventually 117 | ``` 118 | 119 | The decision for each function was: 120 | 121 | ### Keep 122 | 123 | - `Error(t, err)` -> frequently used, keep 124 | - `Equal(t, expected, actual)` -> frequently used, keep but make type safe 125 | - `True(t, expr)` -> frequently used, keep 126 | - `False(t, expr)` -> frequently used, keep 127 | - `Empty(t, thing)` -> `require.Equal(t, len(thing), 0)` 128 | - `Contains(t, haystack string, needle string)` - the only variant used in our codebase, keep as concrete type 129 | - `Zero(t, value)` -> make type safe, keep 130 | - `Panics(t, f)` -> useful, keep 131 | - `EqualError(t, a, b)` -> useful, keep 132 | - `Nil(t, value)` -> frequently used, keep 133 | 134 | ### Not keeping, replace with ... 135 | 136 | - `ElementsMatch(t, a, b)` - use [peterrk/slices](https://github.com/peterrk/slices) or stdlib sort support once it lands. 137 | - `IsType(t, a, b)` -> `require.Equal(t, reflect.TypeOf(a).String(), reflect.TypeOf(b).String())` 138 | - `FileExists()` -> very little use, drop 139 | - `JSONEq()` -> very little use, drop 140 | - `PanicsWithValue()` -> very little use, drop 141 | - `Eventually()` -> very little use, drop 142 | - `Contains(t, haystack []T, needle T)` - very little use, replace with 143 | - `Contains(t, haystack map[K]V, needle K)` - very little use, drop 144 | - `Len(t, v, n)` -> cannot be implemented as a single function with generics`Equal(t, len(v), n)` 145 | - `EqualValues()` - `Equal(t, TYPE(a), TYPE(b))` 146 | - `Fail()` -> `t.Fatal()` 147 | -------------------------------------------------------------------------------- /assert.go: -------------------------------------------------------------------------------- 1 | // Package assert provides type-safe assertions with clean error messages. 2 | package assert 3 | 4 | import ( 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "reflect" 9 | "strconv" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/alecthomas/repr" 14 | "github.com/hexops/gotextdiff" 15 | "github.com/hexops/gotextdiff/myers" 16 | ) 17 | 18 | // A CompareOption modifies how object comparisons behave. 19 | type CompareOption func() []repr.Option 20 | 21 | // Exclude fields of the given type from comparison. 22 | func Exclude[T any]() CompareOption { 23 | return func() []repr.Option { 24 | return []repr.Option{repr.Hide[T]()} 25 | } 26 | } 27 | 28 | // OmitEmpty fields from comparison. 29 | func OmitEmpty() CompareOption { 30 | return func() []repr.Option { 31 | return []repr.Option{repr.OmitEmpty(true)} 32 | } 33 | } 34 | 35 | // IgnoreGoStringer ignores GoStringer implementations when comparing. 36 | func IgnoreGoStringer() CompareOption { 37 | return func() []repr.Option { 38 | return []repr.Option{repr.IgnoreGoStringer()} 39 | } 40 | } 41 | 42 | // Compare two values for equality and return true or false. 43 | func Compare[T any](t testing.TB, x, y T, options ...CompareOption) bool { 44 | return objectsAreEqual(x, y, options...) 45 | } 46 | 47 | func extractCompareOptions(msgAndArgs ...any) ([]any, []CompareOption) { 48 | compareOptions := []CompareOption{} 49 | out := []any{} 50 | for _, arg := range msgAndArgs { 51 | if opt, ok := arg.(CompareOption); ok { 52 | compareOptions = append(compareOptions, opt) 53 | } else { 54 | out = append(out, arg) 55 | } 56 | } 57 | return out, compareOptions 58 | } 59 | 60 | // HasPrefix asserts that the string s starts with prefix. 61 | func HasPrefix(t testing.TB, s, prefix string, msgAndArgs ...any) { 62 | if strings.HasPrefix(s, prefix) { 63 | return 64 | } 65 | t.Helper() 66 | msg := formatMsgAndArgs("Expected string to have prefix:", msgAndArgs...) 67 | t.Fatalf("%s\nPrefix: %q\nString: %q\n", msg, prefix, s) 68 | } 69 | 70 | // HasSuffix asserts that the string s ends with suffix. 71 | func HasSuffix(t testing.TB, s, suffix string, msgAndArgs ...any) { 72 | if strings.HasSuffix(s, suffix) { 73 | return 74 | } 75 | t.Helper() 76 | msg := formatMsgAndArgs("Expected string to have suffix:", msgAndArgs...) 77 | t.Fatalf("%s\nSuffix: %q\nString: %q\n", msg, suffix, s) 78 | } 79 | 80 | // Equal asserts that "expected" and "actual" are equal. 81 | // 82 | // If they are not, a diff of the Go representation of the values will be displayed. 83 | func Equal[T any](t testing.TB, expected, actual T, msgArgsAndCompareOptions ...any) { 84 | msgArgsAndCompareOptions, compareOptions := extractCompareOptions(msgArgsAndCompareOptions...) 85 | if objectsAreEqual(expected, actual, compareOptions...) { 86 | return 87 | } 88 | t.Helper() 89 | msg := formatMsgAndArgs("Expected values to be equal:", msgArgsAndCompareOptions...) 90 | t.Fatalf("%s\n%s", msg, Diff(expected, actual, compareOptions...)) 91 | } 92 | 93 | // NotEqual asserts that "expected" is not equal to "actual". 94 | // 95 | // If they are equal the expected value will be displayed. 96 | func NotEqual[T any](t testing.TB, expected, actual T, msgArgsAndCompareOptions ...any) { 97 | msgArgsAndCompareOptions, compareOptions := extractCompareOptions(msgArgsAndCompareOptions...) 98 | if !objectsAreEqual(expected, actual, compareOptions...) { 99 | return 100 | } 101 | t.Helper() 102 | msg := formatMsgAndArgs("Expected values to not be equal but both were:", msgArgsAndCompareOptions...) 103 | t.Fatalf("%s\n%s", msg, repr.String(expected, repr.Indent(" "))) 104 | } 105 | 106 | // Contains asserts that "haystack" contains "needle". 107 | func Contains(t testing.TB, haystack string, needle string, msgAndArgs ...any) { 108 | if strings.Contains(haystack, needle) { 109 | return 110 | } 111 | t.Helper() 112 | msg := formatMsgAndArgs("Haystack does not contain needle.", msgAndArgs...) 113 | t.Fatalf("%s\nNeedle: %q\nHaystack: %q\n", msg, needle, haystack) 114 | } 115 | 116 | // NotContains asserts that "haystack" does not contain "needle". 117 | func NotContains(t testing.TB, haystack string, needle string, msgAndArgs ...any) { 118 | if !strings.Contains(haystack, needle) { 119 | return 120 | } 121 | t.Helper() 122 | msg := formatMsgAndArgs("Haystack should not contain needle.", msgAndArgs...) 123 | quotedHaystack, quotedNeedle, positions := needlePosition(haystack, needle) 124 | t.Fatalf("%s\nNeedle: %s\nHaystack: %s\n %s\n", msg, quotedNeedle, quotedHaystack, positions) 125 | } 126 | 127 | // SliceContains asserts that "haystack" contains "needle". 128 | func SliceContains[T any](t testing.TB, haystack []T, needle T, msgAndArgs ...interface{}) { 129 | t.Helper() 130 | for _, item := range haystack { 131 | if objectsAreEqual(item, needle) { 132 | return 133 | } 134 | } 135 | 136 | msg := formatMsgAndArgs("Haystack does not contain needle.", msgAndArgs...) 137 | needleRepr := repr.String(needle, repr.Indent(" ")) 138 | haystackRepr := repr.String(haystack, repr.Indent(" ")) 139 | t.Fatalf("%s\nNeedle: %s\nHaystack: %s\n", msg, needleRepr, haystackRepr) 140 | } 141 | 142 | // NotSliceContains asserts that "haystack" does not contain "needle". 143 | func NotSliceContains[T any](t testing.TB, haystack []T, needle T, msgAndArgs ...interface{}) { 144 | t.Helper() 145 | for _, item := range haystack { 146 | if objectsAreEqual(item, needle) { 147 | msg := formatMsgAndArgs("Haystack should not contain needle.", msgAndArgs...) 148 | needleRepr := repr.String(needle, repr.Indent(" ")) 149 | haystackRepr := repr.String(haystack, repr.Indent(" ")) 150 | t.Fatalf("%s\nNeedle: %s\nHaystack: %s\n", msg, needleRepr, haystackRepr) 151 | } 152 | } 153 | } 154 | 155 | // Zero asserts that a value is its zero value. 156 | func Zero[T any](t testing.TB, value T, msgAndArgs ...any) { 157 | var zero T 158 | if objectsAreEqual(value, zero) { 159 | return 160 | } 161 | val := reflect.ValueOf(value) 162 | if (val.Kind() == reflect.Slice || val.Kind() == reflect.Map || val.Kind() == reflect.Array) && val.Len() == 0 { 163 | return 164 | } 165 | t.Helper() 166 | msg := formatMsgAndArgs("Expected a zero value but got:", msgAndArgs...) 167 | t.Fatalf("%s\n%s", msg, repr.String(value, repr.Indent(" "))) 168 | } 169 | 170 | // NotZero asserts that a value is not its zero value. 171 | func NotZero[T any](t testing.TB, value T, msgAndArgs ...any) { 172 | var zero T 173 | if !objectsAreEqual(value, zero) { 174 | val := reflect.ValueOf(value) 175 | if !((val.Kind() == reflect.Slice || val.Kind() == reflect.Map || val.Kind() == reflect.Array) && val.Len() == 0) { 176 | return 177 | } 178 | } 179 | t.Helper() 180 | msg := formatMsgAndArgs("Did not expect the zero value:", msgAndArgs...) 181 | t.Fatalf("%s\n%s", msg, repr.String(value)) 182 | } 183 | 184 | // EqualError asserts that either an error is non-nil and that its message is what is expected, 185 | // or that error is nil if the expected message is empty. 186 | func EqualError(t testing.TB, err error, errString string, msgAndArgs ...any) { 187 | if err == nil && errString == "" { 188 | return 189 | } 190 | t.Helper() 191 | if err == nil { 192 | t.Fatal(formatMsgAndArgs("Expected an error", msgAndArgs...)) 193 | } 194 | if err.Error() != errString { 195 | msg := formatMsgAndArgs("Error message not as expected:", msgAndArgs...) 196 | t.Fatalf("%s\n%s", msg, Diff(errString, err.Error())) 197 | } 198 | } 199 | 200 | // IsError asserts than any error in "err"'s tree matches "target". 201 | func IsError(t testing.TB, err, target error, msgAndArgs ...any) { 202 | if errors.Is(err, target) { 203 | return 204 | } 205 | t.Helper() 206 | t.Fatal(formatMsgAndArgs(fmt.Sprintf("Error tree %+v should contain error %q", err, target), msgAndArgs...)) 207 | } 208 | 209 | // NotIsError asserts than no error in "err"'s tree matches "target". 210 | func NotIsError(t testing.TB, err, target error, msgAndArgs ...any) { 211 | if !errors.Is(err, target) { 212 | return 213 | } 214 | t.Helper() 215 | t.Fatal(formatMsgAndArgs(fmt.Sprintf("Error tree %+v should NOT contain error %q", err, target), msgAndArgs...)) 216 | } 217 | 218 | // Error asserts that an error is not nil. 219 | func Error(t testing.TB, err error, msgAndArgs ...any) { 220 | if err != nil { 221 | return 222 | } 223 | t.Helper() 224 | t.Fatal(formatMsgAndArgs("Expected an error", msgAndArgs...)) 225 | } 226 | 227 | // NoError asserts that an error is nil. 228 | func NoError(t testing.TB, err error, msgAndArgs ...any) { 229 | if err == nil { 230 | return 231 | } 232 | t.Helper() 233 | msg := formatMsgAndArgs("Did not expect an error but got:", msgAndArgs...) 234 | t.Fatalf("%s\n%+v", msg, err) 235 | } 236 | 237 | // True asserts that an expression is true. 238 | func True(t testing.TB, ok bool, msgAndArgs ...any) { 239 | if ok { 240 | return 241 | } 242 | t.Helper() 243 | t.Fatal(formatMsgAndArgs("Expected expression to be true", msgAndArgs...)) 244 | } 245 | 246 | // False asserts that an expression is false. 247 | func False(t testing.TB, ok bool, msgAndArgs ...any) { 248 | if !ok { 249 | return 250 | } 251 | t.Helper() 252 | t.Fatal(formatMsgAndArgs("Expected expression to be false", msgAndArgs...)) 253 | } 254 | 255 | // Panics asserts that the given function panics. 256 | func Panics(t testing.TB, fn func(), msgAndArgs ...any) { 257 | t.Helper() 258 | defer func() { 259 | if recover() == nil { 260 | msg := formatMsgAndArgs("Expected function to panic", msgAndArgs...) 261 | t.Fatal(msg) 262 | } 263 | }() 264 | fn() 265 | } 266 | 267 | // NotPanics asserts that the given function does not panic. 268 | func NotPanics(t testing.TB, fn func(), msgAndArgs ...any) { 269 | t.Helper() 270 | defer func() { 271 | if err := recover(); err != nil { 272 | msg := formatMsgAndArgs("Expected function not to panic", msgAndArgs...) 273 | t.Fatalf("%s\nPanic: %v", msg, err) 274 | } 275 | }() 276 | fn() 277 | } 278 | 279 | // Diff returns a unified diff of the string representation of two values. 280 | func Diff[T any](before, after T, compareOptions ...CompareOption) string { 281 | var lhss, rhss string 282 | // Special case strings so we get nice diffs. 283 | if l, ok := any(before).(string); ok { 284 | lhss = l + "\n" 285 | rhss = any(after).(string) + "\n" 286 | } else { 287 | ropts := expandCompareOptions(compareOptions...) 288 | lhss = repr.String(before, ropts...) + "\n" 289 | rhss = repr.String(after, ropts...) + "\n" 290 | } 291 | edits := myers.ComputeEdits("a.txt", lhss, rhss) 292 | lines := strings.Split(fmt.Sprint(gotextdiff.ToUnified("expected.txt", "actual.txt", lhss, edits)), "\n") 293 | if len(lines) < 3 { 294 | return "" 295 | } 296 | return strings.Join(lines[3:], "\n") 297 | } 298 | 299 | func formatMsgAndArgs(dflt string, msgAndArgs ...any) string { 300 | if len(msgAndArgs) == 0 { 301 | return dflt 302 | } 303 | format, ok := msgAndArgs[0].(string) 304 | if !ok { 305 | panic("message argument to assert function must be a fmt string") 306 | } 307 | return fmt.Sprintf(format, msgAndArgs[1:]...) 308 | } 309 | 310 | func needlePosition(haystack, needle string) (quotedHaystack, quotedNeedle, positions string) { 311 | quotedNeedle = strconv.Quote(needle) 312 | quotedNeedle = quotedNeedle[1 : len(quotedNeedle)-1] 313 | quotedHaystack = strconv.Quote(haystack) 314 | rawPositions := strings.ReplaceAll(quotedHaystack, quotedNeedle, strings.Repeat("^", len(quotedNeedle))) 315 | for _, rn := range rawPositions { 316 | if rn != '^' { 317 | positions += " " 318 | } else { 319 | positions += "^" 320 | } 321 | } 322 | return 323 | } 324 | 325 | func expandCompareOptions(options ...CompareOption) []repr.Option { 326 | ropts := []repr.Option{repr.Indent(" ")} 327 | for _, option := range options { 328 | ropts = append(ropts, option()...) 329 | } 330 | return ropts 331 | } 332 | 333 | func objectsAreEqual(expected, actual any, options ...CompareOption) bool { 334 | if expected == nil || actual == nil { 335 | return expected == actual 336 | } 337 | if exp, eok := expected.([]byte); eok { 338 | if act, aok := actual.([]byte); aok { 339 | return bytes.Equal(exp, act) 340 | } 341 | } 342 | if exp, eok := expected.(string); eok { 343 | if act, aok := actual.(string); aok { 344 | return exp == act 345 | } 346 | } 347 | 348 | ropts := expandCompareOptions(options...) 349 | expectedStr := repr.String(expected, ropts...) 350 | actualStr := repr.String(actual, ropts...) 351 | 352 | return expectedStr == actualStr 353 | } 354 | -------------------------------------------------------------------------------- /assert_test.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | type Data struct { 10 | Str string 11 | Num int64 12 | } 13 | 14 | func TestEqual(t *testing.T) { 15 | assertOk(t, "IdenticalStruct", func(t testing.TB) { 16 | Equal(t, Data{"expected", 1234}, Data{"expected", 1234}) 17 | }) 18 | assertOk(t, "Zero length byte arrays", func(t testing.TB) { 19 | Equal(t, []byte(""), []byte(nil)) 20 | }) 21 | assertOk(t, "Identical byte arrays", func(t testing.TB) { 22 | Equal(t, []byte{4, 2}, []byte{4, 2}) 23 | }) 24 | assertOk(t, "Identical numbers", func(t testing.TB) { 25 | Equal(t, 42, 42) 26 | }) 27 | assertFail(t, "DifferentStruct", func(t testing.TB) { 28 | Equal(t, Data{"expected\ntext", 1234}, Data{"actual\ntext", 1234}) 29 | }) 30 | assertFail(t, "Different bytes arrays", func(t testing.TB) { 31 | Equal(t, []byte{2, 4}, []byte{4, 2}) 32 | }) 33 | assertFail(t, "Different numbers", func(t testing.TB) { 34 | Equal(t, 42, 43) 35 | }) 36 | assertOk(t, "Exclude", func(t testing.TB) { 37 | Equal(t, Data{Str: "expected", Num: 1234}, Data{Str: "expected"}, Exclude[int64]()) 38 | }) 39 | } 40 | 41 | func TestEqualStrings(t *testing.T) { 42 | assertFail(t, "IdenticalStrings", func(t testing.TB) { 43 | Equal(t, "hello\nworld", "goodbye\nworld") 44 | }) 45 | } 46 | 47 | func TestNotEqual(t *testing.T) { 48 | assertOk(t, "DifferentFieldValue", func(t testing.TB) { 49 | NotEqual(t, Data{"expected", 1234}, Data{"expected", 1235}) 50 | }) 51 | assertFail(t, "SameValue", func(t testing.TB) { 52 | NotEqual(t, Data{"expected", 1234}, Data{"expected", 1234}) 53 | }) 54 | assertFail(t, "Exclude", func(t testing.TB) { 55 | NotEqual(t, Data{Str: "expected", Num: 1234}, Data{Str: "expected"}, Exclude[int64]()) 56 | }) 57 | } 58 | 59 | func TestContains(t *testing.T) { 60 | assertOk(t, "Found", func(t testing.TB) { 61 | Contains(t, "a haystack with a needle in it", "needle") 62 | }) 63 | assertFail(t, "NotFound", func(t testing.TB) { 64 | Contains(t, "a haystack with a needle in it", "screw") 65 | }) 66 | } 67 | 68 | func TestNotContains(t *testing.T) { 69 | assertOk(t, "NotFound", func(t testing.TB) { 70 | NotContains(t, "a haystack with a needle in it", "screw") 71 | }) 72 | assertFail(t, "Found", func(t testing.TB) { 73 | NotContains(t, "a haystack with a needle in it", "needle") 74 | }) 75 | } 76 | 77 | func TestSliceContains(t *testing.T) { 78 | assertOk(t, "Found", func(t testing.TB) { 79 | SliceContains(t, []string{"hello", "world"}, "hello") 80 | }) 81 | assertFail(t, "NotFound", func(t testing.TB) { 82 | SliceContains(t, []string{"hello", "world"}, "goodbye") 83 | }) 84 | } 85 | 86 | func TestNotSliceContains(t *testing.T) { 87 | assertOk(t, "NotFound", func(t testing.TB) { 88 | NotSliceContains(t, []string{"hello", "world"}, "goodbye") 89 | }) 90 | assertFail(t, "Found", func(t testing.TB) { 91 | NotSliceContains(t, []string{"hello", "world"}, "hello") 92 | }) 93 | } 94 | 95 | func TestEqualError(t *testing.T) { 96 | assertOk(t, "SameMessage", func(t testing.TB) { 97 | EqualError(t, fmt.Errorf("hello"), "hello") 98 | }) 99 | assertOk(t, "Nil", func(t testing.TB) { 100 | EqualError(t, nil, "") 101 | }) 102 | assertFail(t, "MessageMismatch", func(t testing.TB) { 103 | EqualError(t, fmt.Errorf("hello"), "goodbye") 104 | }) 105 | } 106 | 107 | func TestError(t *testing.T) { 108 | assertOk(t, "Error", func(t testing.TB) { 109 | Error(t, fmt.Errorf("hello")) 110 | }) 111 | assertFail(t, "Nil", func(t testing.TB) { 112 | Error(t, nil) 113 | }) 114 | } 115 | 116 | func TestNoError(t *testing.T) { 117 | assertOk(t, "Nil", func(t testing.TB) { 118 | NoError(t, nil) 119 | }) 120 | assertFail(t, "Error", func(t testing.TB) { 121 | NoError(t, fmt.Errorf("hello")) 122 | }) 123 | } 124 | 125 | func TestZero(t *testing.T) { 126 | assertOk(t, "Struct", func(t testing.TB) { 127 | Zero(t, Data{}) 128 | }) 129 | assertOk(t, "NilSlice", func(t testing.TB) { 130 | var slice []int 131 | Zero(t, slice) 132 | }) 133 | assertFail(t, "NonEmptyStruct", func(t testing.TB) { 134 | Zero(t, Data{Str: "str"}) 135 | }) 136 | assertFail(t, "NonEmptySlice", func(t testing.TB) { 137 | slice := []int{1, 2, 3} 138 | Zero(t, slice) 139 | }) 140 | assertOk(t, "ZeroLenSlice", func(t testing.TB) { 141 | slice := []int{} 142 | Zero(t, slice) 143 | }) 144 | } 145 | 146 | func TestNotZero(t *testing.T) { 147 | assertOk(t, "PopulatedStruct", func(t testing.TB) { 148 | notZero := Data{Str: "hello"} 149 | NotZero(t, notZero) 150 | }) 151 | assertFail(t, "EmptyStruct", func(t testing.TB) { 152 | zero := Data{} 153 | NotZero(t, zero) 154 | }) 155 | assertFail(t, "NilSlice", func(t testing.TB) { 156 | var slice []int 157 | NotZero(t, slice) 158 | }) 159 | assertFail(t, "ZeroLenSlice", func(t testing.TB) { 160 | slice := []int{} 161 | NotZero(t, slice) 162 | }) 163 | assertOk(t, "Slice", func(t testing.TB) { 164 | slice := []int{1, 2, 3} 165 | NotZero(t, slice) 166 | }) 167 | } 168 | 169 | func TestIsError(t *testing.T) { 170 | assertOk(t, "SameError", func(t testing.TB) { 171 | IsError(t, fmt.Errorf("os error: %w", os.ErrClosed), os.ErrClosed) 172 | }) 173 | assertFail(t, "DifferentError", func(t testing.TB) { 174 | IsError(t, fmt.Errorf("not an os error"), os.ErrClosed) 175 | }) 176 | } 177 | 178 | func TestInvalidFormatMsg(t *testing.T) { 179 | Panics(t, func() { 180 | NotZero(t, Data{}, 123) 181 | }) 182 | } 183 | 184 | func TestNotIsError(t *testing.T) { 185 | assertFail(t, "SameError", func(t testing.TB) { 186 | NotIsError(t, fmt.Errorf("os error: %w", os.ErrClosed), os.ErrClosed) 187 | }) 188 | assertOk(t, "DifferentError", func(t testing.TB) { 189 | NotIsError(t, fmt.Errorf("not an os error"), os.ErrClosed) 190 | }) 191 | } 192 | 193 | func TestDiff(t *testing.T) { 194 | Equal(t, "-before\n+after\n", Diff("before", "after")) 195 | } 196 | 197 | func TestHasSuffix(t *testing.T) { 198 | assertOk(t, "Suffix", func(t testing.TB) { 199 | HasSuffix(t, "hello", "lo") 200 | }) 201 | assertFail(t, "NoSuffix", func(t testing.TB) { 202 | HasSuffix(t, "hello", "world") 203 | }) 204 | } 205 | 206 | func TestHasPrefix(t *testing.T) { 207 | assertOk(t, "Prefix", func(t testing.TB) { 208 | HasPrefix(t, "hello", "he") 209 | }) 210 | assertFail(t, "NoPrefix", func(t testing.TB) { 211 | HasPrefix(t, "hello", "world") 212 | }) 213 | } 214 | 215 | type testTester struct { 216 | *testing.T 217 | failed string 218 | } 219 | 220 | func (t *testTester) Fatalf(message string, args ...interface{}) { 221 | t.failed = fmt.Sprintf(message, args...) 222 | } 223 | 224 | func (t *testTester) Fatal(args ...interface{}) { 225 | t.failed = fmt.Sprint(args...) 226 | } 227 | 228 | func assertFail(t *testing.T, name string, fn func(t testing.TB)) { 229 | t.Helper() 230 | t.Run(name, func(t *testing.T) { 231 | t.Helper() 232 | tester := &testTester{T: t} 233 | fn(tester) 234 | if tester.failed == "" { 235 | t.Fatal("Should have failed") 236 | } else { 237 | t.Log(tester.failed) 238 | } 239 | }) 240 | } 241 | 242 | func assertOk(t *testing.T, name string, fn func(t testing.TB)) { 243 | t.Helper() 244 | t.Run(name, func(t *testing.T) { 245 | t.Helper() 246 | tester := &testTester{T: t} 247 | fn(tester) 248 | if tester.failed != "" { 249 | t.Fatal("Should not have failed with:\n", tester.failed) 250 | } 251 | }) 252 | } 253 | -------------------------------------------------------------------------------- /bin/.go-1.22.3.pkg: -------------------------------------------------------------------------------- 1 | hermit -------------------------------------------------------------------------------- /bin/.golangci-lint-1.58.2.pkg: -------------------------------------------------------------------------------- 1 | hermit -------------------------------------------------------------------------------- /bin/README.hermit.md: -------------------------------------------------------------------------------- 1 | # Hermit environment 2 | 3 | This is a [Hermit](https://github.com/cashapp/hermit) bin directory. 4 | 5 | The symlinks in this directory are managed by Hermit and will automatically 6 | download and install Hermit itself as well as packages. These packages are 7 | local to this environment. 8 | -------------------------------------------------------------------------------- /bin/activate-hermit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file must be used with "source bin/activate-hermit" from bash or zsh. 3 | # You cannot run it directly 4 | 5 | if [ "${BASH_SOURCE-}" = "$0" ]; then 6 | echo "You must source this script: \$ source $0" >&2 7 | exit 33 8 | fi 9 | 10 | BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")" 11 | if "${BIN_DIR}/hermit" noop > /dev/null; then 12 | eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")" 13 | 14 | if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then 15 | hash -r 2>/dev/null 16 | fi 17 | 18 | echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated" 19 | fi 20 | -------------------------------------------------------------------------------- /bin/go: -------------------------------------------------------------------------------- 1 | .go-1.22.3.pkg -------------------------------------------------------------------------------- /bin/go-main-stubs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # 4 | # This script generates stub scripts for every Go "main" package in this 5 | # repository. These stubs execute the corresponding main package via "go run". 6 | # 7 | 8 | set -euo pipefail 9 | 10 | usage() { 11 | echo "$(basename $0) [-c] -- create or clean Go main stub scripts" 12 | exit 1 13 | } 14 | 15 | bindir="$(dirname $0)" 16 | root="$(dirname "${bindir}")" 17 | 18 | clean() { 19 | grep -l "^# go-main-stubs stub script" "${bindir}"/* | grep -v bin/go-main-stubs | xargs rm || true 20 | } 21 | 22 | while getopts ":hc" arg; do 23 | case $arg in 24 | c) 25 | echo "Cleaning old stubs" 26 | clean 27 | exit 0 28 | ;; 29 | h | *) 30 | usage 31 | exit 0 32 | ;; 33 | esac 34 | done 35 | 36 | clean 37 | 38 | echo "Creating Go main stubs in ${bindir}" 39 | 40 | echo -n . 41 | 42 | for main in $(cd "${root}" && go list -f '{{if eq "main" .Name}}{{.ImportPath}}{{end}}' ./...); do 43 | stub_script="${bindir}/$(basename $main)" 44 | cat << EOF > ${stub_script} 45 | #!/bin/bash 46 | # go-main-stubs stub script 47 | exec "\$(dirname \$0)/go" run $main "\$@" 48 | EOF 49 | chmod +x "${stub_script}" 50 | echo -n . 51 | done 52 | echo 53 | -------------------------------------------------------------------------------- /bin/gofmt: -------------------------------------------------------------------------------- 1 | .go-1.22.3.pkg -------------------------------------------------------------------------------- /bin/golangci-lint: -------------------------------------------------------------------------------- 1 | .golangci-lint-1.58.2.pkg -------------------------------------------------------------------------------- /bin/hermit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | if [ -z "${HERMIT_STATE_DIR}" ]; then 6 | case "$(uname -s)" in 7 | Darwin) 8 | export HERMIT_STATE_DIR="${HOME}/Library/Caches/hermit" 9 | ;; 10 | Linux) 11 | export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/hermit" 12 | ;; 13 | esac 14 | fi 15 | 16 | export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}" 17 | HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")" 18 | export HERMIT_CHANNEL 19 | export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit} 20 | 21 | if [ ! -x "${HERMIT_EXE}" ]; then 22 | echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2 23 | curl -fsSL "${HERMIT_DIST_URL}/install.sh" | /bin/bash 1>&2 24 | fi 25 | 26 | exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@" 27 | -------------------------------------------------------------------------------- /bin/hermit.hcl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alecthomas/assert/96885dec03fb9107de54aba088c8e366e1d67ce9/bin/hermit.hcl -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alecthomas/assert/v2 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/alecthomas/repr v0.4.0 7 | github.com/hexops/gotextdiff v1.0.3 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 2 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 3 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 4 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 5 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "https://docs.renovatebot.com/renovate-schema.json", 3 | extends: [ 4 | "config:recommended", 5 | ":semanticCommits", 6 | ":semanticCommitTypeAll(chore)", 7 | ":semanticCommitScope(deps)", 8 | "group:allNonMajor", 9 | "schedule:earlyMondays", // Run once a week. 10 | ], 11 | packageRules: [ 12 | { 13 | matchPackageNames: ["golangci-lint"], 14 | matchManagers: ["hermit"], 15 | enabled: false, 16 | }, 17 | ], 18 | } 19 | --------------------------------------------------------------------------------