├── .github ├── FUNDING.yml └── workflows │ ├── example.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── assertions.go ├── assertions_error.go ├── assertions_test.go ├── cli_utils.go ├── cli_utils_test.go ├── cmd └── check-cov │ └── main.go ├── each.go ├── each_test.go ├── fixtures └── coverage │ ├── cov.txt │ ├── foo.go │ └── foo_test.go ├── go.mod ├── go.sum ├── go.work ├── go.work.sum ├── got.go ├── got_test.go ├── lib ├── benchmark │ ├── .golangci.yml │ ├── bench_test.go │ ├── go.mod │ ├── go.sum │ ├── myers │ │ └── diff.go │ └── readme.md ├── diff │ ├── README.md │ ├── ast.go │ ├── format.go │ ├── format_test.go │ └── token.go ├── example │ ├── .golangci.yml │ ├── 01_simple_assertion_test.go │ ├── 02_advanced_test.go │ ├── 03_setup_test.go │ ├── 04_suite_test.go │ ├── 05_mocking_test.go │ ├── 06_customize_assertion_output_test.go │ ├── README.md │ ├── example.go │ ├── go.mod │ └── go.sum ├── got-vscode-extension │ ├── .eslintrc.json │ ├── .gitignore │ ├── .vscode │ │ ├── extensions.json │ │ ├── launch.json │ │ ├── settings.json │ │ └── tasks.json │ ├── .vscodeignore │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── snippets.json │ ├── snippets │ │ └── main.go │ ├── src │ │ ├── extension.ts │ │ └── test │ │ │ ├── runTest.ts │ │ │ └── suite │ │ │ └── index.ts │ ├── tsconfig.json │ └── vsc-extension-quickstart.md ├── lcs │ ├── lcs.go │ ├── lcs_test.go │ ├── sequence.go │ ├── sequence_test.go │ └── utils.go ├── mock │ ├── mock.go │ ├── mock_test.go │ ├── spy.go │ └── stub.go └── utils │ ├── utils.go │ └── utils_test.go ├── setup_test.go ├── snapshots.go ├── snapshots_test.go ├── utils.go ├── utils_private_test.go ├── utils_req.go ├── utils_serve.go └── utils_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ysmood] 2 | -------------------------------------------------------------------------------- /.github/workflows/example.yml: -------------------------------------------------------------------------------- 1 | name: Example 2 | 3 | on: [push] 4 | 5 | env: 6 | GODEBUG: tracebackancestors=1000 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | defaults: 13 | run: 14 | working-directory: lib/example 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: actions/setup-go@v5 20 | with: 21 | go-version: 1.22 22 | 23 | - name: lint 24 | run: go run github.com/ysmood/golangci-lint@latest -v 1.64.5 25 | 26 | - name: test 27 | run: | 28 | go test -race -coverprofile=coverage.out ./... 29 | go run github.com/ysmood/got/cmd/check-cov@latest 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | GODEBUG: tracebackancestors=1000 7 | 8 | jobs: 9 | test: 10 | runs-on: ${{ matrix.os }} 11 | 12 | strategy: 13 | matrix: 14 | os: [macos-latest, windows-latest, ubuntu-latest] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: actions/setup-go@v5 20 | with: 21 | go-version: 1.22 22 | 23 | - name: lint 24 | if: matrix.os == 'ubuntu-latest' 25 | run: go run github.com/ysmood/golangci-lint@latest -v 1.64.5 26 | 27 | - name: test 28 | env: 29 | TERM: xterm-256color 30 | run: go test -coverprofile="coverage.out" . ./lib/diff ./lib/lcs ./lib/mock ./lib/utils 31 | 32 | - name: coverage 33 | if: matrix.os == 'ubuntu-latest' 34 | run: go run ./cmd/check-cov 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.out 2 | *.test 3 | tmp/ 4 | .got/ -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: v1.64 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2020 Yad Smood 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | An enjoyable golang test framework. 4 | 5 | ## Features 6 | 7 | - Pretty output using [gop](https://github.com/ysmood/gop) and [diff](lib/diff) 8 | - Fluent API design that takes the full advantage of IDE 9 | - Handy assertion helpers 10 | - Handy utils for testing 11 | - Value snapshot assertion 12 | - Customizable assertion error output 13 | 14 | ## Guides 15 | 16 | Read the [example project](lib/example) to get started. 17 | 18 | Got uses itself as the test framework, so the source code itself is the best doc. 19 | 20 | Install the [vscode extension](https://marketplace.visualstudio.com/items?itemName=ysmood.got-vscode-extension) for snippets like: `gp`, `gt`, and `gsetup`. 21 | 22 | To ensure test coverage of your project, you can run the command below: 23 | 24 | ```shell 25 | go test -race -coverprofile=coverage.out ./... 26 | go run github.com/ysmood/got/cmd/check-cov@latest 27 | ``` 28 | 29 | By default the [check-cov](cmd/check-cov) requires 100% coverage, run it with the `-h` flag to see the help doc. 30 | 31 | ## API reference 32 | 33 | [Link](https://pkg.go.dev/github.com/ysmood/got) 34 | -------------------------------------------------------------------------------- /assertions.go: -------------------------------------------------------------------------------- 1 | package got 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "reflect" 8 | "regexp" 9 | "runtime" 10 | "strings" 11 | "sync/atomic" 12 | 13 | "github.com/ysmood/got/lib/utils" 14 | ) 15 | 16 | // Assertions helpers 17 | type Assertions struct { 18 | Testable 19 | 20 | ErrorHandler AssertionError 21 | 22 | must bool 23 | 24 | desc []string 25 | } 26 | 27 | // Desc returns a clone with the format description. The description will be printed before the error message. 28 | func (as Assertions) Desc(format string, args ...interface{}) Assertions { 29 | n := as 30 | n.desc = append(n.desc, fmt.Sprintf(format, args...)) 31 | return n 32 | } 33 | 34 | // Must returns a clone with the FailNow enabled. It will exit the current goroutine if the assertion fails. 35 | func (as Assertions) Must() Assertions { 36 | n := as 37 | n.must = true 38 | return n 39 | } 40 | 41 | // Eq asserts that x equals y when converted to the same type, such as compare float 1.0 and integer 1 . 42 | // For strict value and type comparison use Assertions.Equal . 43 | // For how comparison works, see [utils.SmartCompare] . 44 | func (as Assertions) Eq(x, y interface{}) { 45 | as.Helper() 46 | if utils.SmartCompare(x, y) == 0 { 47 | return 48 | } 49 | as.err(AssertionEq, x, y) 50 | } 51 | 52 | // Neq asserts that x not equals y even when converted to the same type. 53 | // For how comparison works, see [utils.SmartCompare] . 54 | func (as Assertions) Neq(x, y interface{}) { 55 | as.Helper() 56 | if utils.SmartCompare(x, y) != 0 { 57 | return 58 | } 59 | 60 | _, xNil := utils.IsNil(x) 61 | _, yNil := utils.IsNil(y) 62 | 63 | if !xNil && !yNil && reflect.TypeOf(x).Kind() == reflect.TypeOf(y).Kind() { 64 | as.err(AssertionNeqSame, x, y) 65 | return 66 | } 67 | as.err(AssertionNeq, x, y) 68 | } 69 | 70 | // Equal asserts that x equals y. 71 | // For loose type comparison use Assertions.Eq, such as compare float 1.0 and integer 1 . 72 | func (as Assertions) Equal(x, y interface{}) { 73 | as.Helper() 74 | if utils.Compare(x, y) == 0 { 75 | return 76 | } 77 | as.err(AssertionEq, x, y) 78 | } 79 | 80 | // Gt asserts that x is greater than y. 81 | // For how comparison works, see [utils.SmartCompare] . 82 | func (as Assertions) Gt(x, y interface{}) { 83 | as.Helper() 84 | if utils.SmartCompare(x, y) > 0 { 85 | return 86 | } 87 | as.err(AssertionGt, x, y) 88 | } 89 | 90 | // Gte asserts that x is greater than or equal to y. 91 | // For how comparison works, see [utils.SmartCompare] . 92 | func (as Assertions) Gte(x, y interface{}) { 93 | as.Helper() 94 | if utils.SmartCompare(x, y) >= 0 { 95 | return 96 | } 97 | as.err(AssertionGte, x, y) 98 | } 99 | 100 | // Lt asserts that x is less than y. 101 | // For how comparison works, see [utils.SmartCompare] . 102 | func (as Assertions) Lt(x, y interface{}) { 103 | as.Helper() 104 | if utils.SmartCompare(x, y) < 0 { 105 | return 106 | } 107 | as.err(AssertionLt, x, y) 108 | } 109 | 110 | // Lte asserts that x is less than or equal to b. 111 | // For how comparison works, see [utils.SmartCompare] . 112 | func (as Assertions) Lte(x, y interface{}) { 113 | as.Helper() 114 | if utils.SmartCompare(x, y) <= 0 { 115 | return 116 | } 117 | as.err(AssertionLte, x, y) 118 | } 119 | 120 | // InDelta asserts that x and y are within the delta of each other. 121 | // For how comparison works, see [utils.SmartCompare] . 122 | func (as Assertions) InDelta(x, y interface{}, delta float64) { 123 | as.Helper() 124 | if math.Abs(utils.SmartCompare(x, y)) <= delta { 125 | return 126 | } 127 | as.err(AssertionInDelta, x, y, delta) 128 | } 129 | 130 | // True asserts that x is true. 131 | func (as Assertions) True(x bool) { 132 | as.Helper() 133 | if x { 134 | return 135 | } 136 | as.err(AssertionTrue) 137 | } 138 | 139 | // False asserts that x is false. 140 | func (as Assertions) False(x bool) { 141 | as.Helper() 142 | if !x { 143 | return 144 | } 145 | as.err(AssertionFalse) 146 | } 147 | 148 | // Nil asserts that the last item in args is nilable and nil 149 | func (as Assertions) Nil(args ...interface{}) { 150 | as.Helper() 151 | if len(args) == 0 { 152 | as.err(AssertionNoArgs) 153 | return 154 | } 155 | last := args[len(args)-1] 156 | if _, yes := utils.IsNil(last); yes { 157 | return 158 | } 159 | as.err(AssertionNil, last, args) 160 | } 161 | 162 | // NotNil asserts that the last item in args is nilable and not nil 163 | func (as Assertions) NotNil(args ...interface{}) { 164 | as.Helper() 165 | if len(args) == 0 { 166 | as.err(AssertionNoArgs) 167 | return 168 | } 169 | last := args[len(args)-1] 170 | 171 | if last == nil { 172 | as.err(AssertionNotNil, last, args) 173 | return 174 | } 175 | 176 | nilable, yes := utils.IsNil(last) 177 | if !nilable { 178 | as.err(AssertionNotNilable, last, args) 179 | return 180 | } 181 | 182 | if yes { 183 | as.err(AssertionNotNilableNil, last, args) 184 | } 185 | } 186 | 187 | // Zero asserts x is zero value for its type. 188 | func (as Assertions) Zero(x interface{}) { 189 | as.Helper() 190 | if reflect.DeepEqual(x, reflect.Zero(reflect.TypeOf(x)).Interface()) { 191 | return 192 | } 193 | as.err(AssertionZero, x) 194 | } 195 | 196 | // NotZero asserts that x is not zero value for its type. 197 | func (as Assertions) NotZero(x interface{}) { 198 | as.Helper() 199 | if reflect.DeepEqual(x, reflect.Zero(reflect.TypeOf(x)).Interface()) { 200 | as.err(AssertionNotZero, x) 201 | } 202 | } 203 | 204 | // Regex asserts that str matches the regex pattern 205 | func (as Assertions) Regex(pattern, str string) { 206 | as.Helper() 207 | if regexp.MustCompile(pattern).MatchString(str) { 208 | return 209 | } 210 | as.err(AssertionRegex, pattern, str) 211 | } 212 | 213 | // Has asserts that container has item. 214 | // The container can be a string, []byte, slice, array, or map. 215 | // For how comparison works, see [utils.SmartCompare] . 216 | func (as Assertions) Has(container, item interface{}) { 217 | as.Helper() 218 | 219 | if c, ok := container.(string); ok && hasStr(c, item) { 220 | return 221 | } else if c, ok := container.([]byte); ok && hasStr(string(c), item) { 222 | return 223 | } 224 | 225 | cv := reflect.Indirect(reflect.ValueOf(container)) 226 | switch cv.Kind() { 227 | case reflect.Slice, reflect.Array: 228 | for i := 0; i < cv.Len(); i++ { 229 | if utils.SmartCompare(cv.Index(i).Interface(), item) == 0 { 230 | return 231 | } 232 | } 233 | case reflect.Map: 234 | keys := cv.MapKeys() 235 | for _, k := range keys { 236 | if utils.SmartCompare(cv.MapIndex(k).Interface(), item) == 0 { 237 | return 238 | } 239 | } 240 | } 241 | 242 | as.err(AssertionHas, container, item) 243 | } 244 | 245 | // Len asserts that the length of list equals l 246 | func (as Assertions) Len(list interface{}, l int) { 247 | as.Helper() 248 | actual := reflect.ValueOf(list).Len() 249 | if actual == l { 250 | return 251 | } 252 | as.err(AssertionLen, actual, l, list) 253 | } 254 | 255 | // Err asserts that the last item in args is error 256 | func (as Assertions) Err(args ...interface{}) { 257 | as.Helper() 258 | if len(args) == 0 { 259 | as.err(AssertionNoArgs) 260 | return 261 | } 262 | last := args[len(args)-1] 263 | if err, _ := last.(error); err != nil { 264 | return 265 | } 266 | as.err(AssertionErr, last, args) 267 | } 268 | 269 | // E is a shortcut for Must().Nil(args...) 270 | func (as Assertions) E(args ...interface{}) { 271 | as.Helper() 272 | as.Must().Nil(args...) 273 | } 274 | 275 | // Panic executes fn and asserts that fn panics 276 | func (as Assertions) Panic(fn func()) (val interface{}) { 277 | as.Helper() 278 | 279 | defer func() { 280 | as.Helper() 281 | 282 | val = recover() 283 | if val == nil { 284 | as.err(AssertionPanic, fn) 285 | } 286 | }() 287 | 288 | fn() 289 | 290 | return 291 | } 292 | 293 | // Is asserts that x is kind of y, it uses reflect.Kind to compare. 294 | // If x and y are both error type, it will use errors.Is to compare. 295 | func (as Assertions) Is(x, y interface{}) { 296 | as.Helper() 297 | 298 | if x == nil && y == nil { 299 | return 300 | } 301 | 302 | if ae, ok := x.(error); ok { 303 | if be, ok := y.(error); ok { 304 | if errors.Is(ae, be) { 305 | return 306 | } 307 | 308 | as.err(AssertionIsInChain, x, y) 309 | return 310 | } 311 | } 312 | 313 | at := reflect.TypeOf(x) 314 | bt := reflect.TypeOf(y) 315 | if x != nil && y != nil && at.Kind() == bt.Kind() { 316 | return 317 | } 318 | as.err(AssertionIsKind, x, y) 319 | } 320 | 321 | // Count asserts that the returned function will be called n times 322 | func (as Assertions) Count(n int) func() { 323 | as.Helper() 324 | count := int64(0) 325 | 326 | as.Cleanup(func() { 327 | c := int(atomic.LoadInt64(&count)) 328 | if c != n { 329 | as.Helper() 330 | as.err(AssertionCount, n, c) 331 | } 332 | }) 333 | 334 | return func() { 335 | atomic.AddInt64(&count, 1) 336 | } 337 | } 338 | 339 | func (as Assertions) err(t AssertionErrType, details ...interface{}) { 340 | as.Helper() 341 | 342 | if len(as.desc) > 0 { 343 | for _, d := range as.desc { 344 | as.Logf("%s", d) 345 | } 346 | } 347 | 348 | // TODO: we should take advantage of the Helper function 349 | _, f, l, _ := runtime.Caller(2) 350 | c := &AssertionCtx{ 351 | Type: t, 352 | Details: details, 353 | File: f, 354 | Line: l, 355 | } 356 | 357 | as.Logf("%s", as.ErrorHandler.Report(c)) 358 | 359 | if as.must { 360 | as.FailNow() 361 | return 362 | } 363 | 364 | as.Fail() 365 | } 366 | 367 | func hasStr(c string, item interface{}) bool { 368 | if it, ok := item.(string); ok { 369 | if strings.Contains(c, it) { 370 | return true 371 | } 372 | } else if it, ok := item.([]byte); ok { 373 | if strings.Contains(c, string(it)) { 374 | return true 375 | } 376 | } else if it, ok := item.(rune); ok { 377 | if strings.ContainsRune(c, it) { 378 | return true 379 | } 380 | } 381 | return false 382 | } 383 | -------------------------------------------------------------------------------- /assertions_error.go: -------------------------------------------------------------------------------- 1 | package got 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | "github.com/ysmood/gop" 9 | "github.com/ysmood/got/lib/diff" 10 | ) 11 | 12 | // AssertionErrType enum 13 | type AssertionErrType int 14 | 15 | const ( 16 | // AssertionEq type 17 | AssertionEq AssertionErrType = iota 18 | // AssertionNeqSame type 19 | AssertionNeqSame 20 | // AssertionNeq type 21 | AssertionNeq 22 | // AssertionGt type 23 | AssertionGt 24 | // AssertionGte type 25 | AssertionGte 26 | // AssertionLt type 27 | AssertionLt 28 | // AssertionLte type 29 | AssertionLte 30 | // AssertionInDelta type 31 | AssertionInDelta 32 | // AssertionTrue type 33 | AssertionTrue 34 | // AssertionFalse type 35 | AssertionFalse 36 | // AssertionNil type 37 | AssertionNil 38 | // AssertionNoArgs type 39 | AssertionNoArgs 40 | // AssertionNotNil type 41 | AssertionNotNil 42 | // AssertionNotNilable type 43 | AssertionNotNilable 44 | // AssertionNotNilableNil type 45 | AssertionNotNilableNil 46 | // AssertionZero type 47 | AssertionZero 48 | // AssertionNotZero type 49 | AssertionNotZero 50 | // AssertionRegex type 51 | AssertionRegex 52 | // AssertionHas type 53 | AssertionHas 54 | // AssertionLen type 55 | AssertionLen 56 | // AssertionErr type 57 | AssertionErr 58 | // AssertionPanic type 59 | AssertionPanic 60 | // AssertionIsInChain type 61 | AssertionIsInChain 62 | // AssertionIsKind type 63 | AssertionIsKind 64 | // AssertionCount type 65 | AssertionCount 66 | // AssertionSnapshot type 67 | AssertionSnapshot 68 | ) 69 | 70 | // AssertionCtx holds the context of an assertion 71 | type AssertionCtx struct { 72 | Type AssertionErrType 73 | Details []interface{} 74 | File string 75 | Line int 76 | } 77 | 78 | // AssertionError handler 79 | type AssertionError interface { 80 | Report(*AssertionCtx) string 81 | } 82 | 83 | var _ AssertionError = AssertionErrorReport(nil) 84 | 85 | // AssertionErrorReport is used to convert a func to AssertionError 86 | type AssertionErrorReport func(*AssertionCtx) string 87 | 88 | // Report interface 89 | func (ae AssertionErrorReport) Report(ac *AssertionCtx) string { 90 | return ae(ac) 91 | } 92 | 93 | type defaultAssertionError struct { 94 | fns map[AssertionErrType]func(details ...interface{}) string 95 | } 96 | 97 | // NewDefaultAssertionError handler 98 | func NewDefaultAssertionError(theme gop.Theme, diffTheme diff.Theme) AssertionError { 99 | f := func(v interface{}) string { 100 | return gop.Format(gop.Tokenize(v), theme) 101 | } 102 | 103 | k := func(s string) string { 104 | return " " + gop.Stylize("⦗"+s+"⦘", theme(gop.Error)) + " " 105 | } 106 | 107 | fns := map[AssertionErrType]func(details ...interface{}) string{ 108 | AssertionEq: func(details ...interface{}) string { 109 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 110 | defer cancel() 111 | 112 | x := f(details[0]) 113 | y := f(details[1]) 114 | 115 | if diffTheme == nil { 116 | return j(x, k("not =="), y) 117 | } 118 | 119 | if hasNewline(x, y) { 120 | df := diff.Format(diff.Tokenize(ctx, gop.StripANSI(x), gop.StripANSI(y)), diffTheme) 121 | return j(x, k("not =="), y, df) 122 | } 123 | 124 | dx, dy := diff.TokenizeLine(ctx, gop.StripANSI(x), gop.StripANSI(y)) 125 | return diff.Format(dx, diffTheme) + k("not ==") + diff.Format(dy, diffTheme) 126 | }, 127 | AssertionNeqSame: func(details ...interface{}) string { 128 | x := f(details[0]) 129 | y := f(details[1]) 130 | return j(x, k("=="), y) 131 | }, 132 | AssertionNeq: func(details ...interface{}) string { 133 | x := f(details[0]) 134 | y := f(details[1]) 135 | return j(x, k("=="), y, k("when converted to the same type")) 136 | }, 137 | AssertionGt: func(details ...interface{}) string { 138 | x := f(details[0]) 139 | y := f(details[1]) 140 | return j(x, k("not >"), y) 141 | }, 142 | AssertionGte: func(details ...interface{}) string { 143 | x := f(details[0]) 144 | y := f(details[1]) 145 | return j(x, k("not ≥"), y) 146 | }, 147 | AssertionLt: func(details ...interface{}) string { 148 | x := f(details[0]) 149 | y := f(details[1]) 150 | return j(x, k("not <"), y) 151 | }, 152 | AssertionLte: func(details ...interface{}) string { 153 | x := f(details[0]) 154 | y := f(details[1]) 155 | return j(x, k("not ≤"), y) 156 | }, 157 | AssertionInDelta: func(details ...interface{}) string { 158 | x := f(details[0]) 159 | y := f(details[1]) 160 | delta := f(details[2]) 161 | return j(k("delta between"), x, k("and"), y, k("not ≤"), delta) 162 | }, 163 | AssertionTrue: func(_ ...interface{}) string { 164 | return k("should be") + f(true) 165 | }, 166 | AssertionFalse: func(_ ...interface{}) string { 167 | return k("should be") + f(false) 168 | }, 169 | AssertionNil: func(details ...interface{}) string { 170 | last := f(details[0]) 171 | return j(k("last argument"), last, k("should be"), f(nil)) 172 | }, 173 | AssertionNoArgs: func(_ ...interface{}) string { 174 | return k("no arguments received") 175 | }, 176 | AssertionNotNil: func(_ ...interface{}) string { 177 | return k("last argument shouldn't be") + f(nil) 178 | }, 179 | AssertionNotNilable: func(details ...interface{}) string { 180 | last := f(details[0]) 181 | return j(k("last argument"), last, k("is not nilable")) 182 | }, 183 | AssertionNotNilableNil: func(details ...interface{}) string { 184 | last := f(details[0]) 185 | return j(k("last argument"), last, k("shouldn't be"), f(nil)) 186 | }, 187 | AssertionZero: func(details ...interface{}) string { 188 | x := f(details[0]) 189 | return j(x, k("should be zero value for its type")) 190 | }, 191 | AssertionNotZero: func(details ...interface{}) string { 192 | x := f(details[0]) 193 | return j(x, k("shouldn't be zero value for its type")) 194 | }, 195 | AssertionRegex: func(details ...interface{}) string { 196 | pattern := f(details[0]) 197 | str := f(details[1]) 198 | return j(pattern, k("should match"), str) 199 | }, 200 | AssertionHas: func(details ...interface{}) string { 201 | container := f(details[0]) 202 | str := f(details[1]) 203 | return j(container, k("should has"), str) 204 | }, 205 | AssertionLen: func(details ...interface{}) string { 206 | actual := f(details[0]) 207 | l := f(details[1]) 208 | return k("expect len") + actual + k("to be") + l 209 | }, 210 | AssertionErr: func(details ...interface{}) string { 211 | last := f(details[0]) 212 | return j(k("last value"), last, k("should be ")) 213 | }, 214 | AssertionPanic: func(_ ...interface{}) string { 215 | return k("should panic") 216 | }, 217 | AssertionIsInChain: func(details ...interface{}) string { 218 | x := f(details[0]) 219 | y := f(details[1]) 220 | return j(x, k("should in chain of"), y) 221 | }, 222 | AssertionIsKind: func(details ...interface{}) string { 223 | x := f(details[0]) 224 | y := f(details[1]) 225 | return j(x, k("should be kind of"), y) 226 | }, 227 | AssertionCount: func(details ...interface{}) string { 228 | n := f(details[0]) 229 | count := f(details[1]) 230 | return k("should count") + n + k("times, but got") + count 231 | }, 232 | } 233 | 234 | return &defaultAssertionError{fns: fns} 235 | } 236 | 237 | // Report interface 238 | func (ae *defaultAssertionError) Report(ac *AssertionCtx) string { 239 | return ae.fns[ac.Type](ac.Details...) 240 | } 241 | 242 | func j(args ...string) string { 243 | if hasNewline(args...) { 244 | for i := 0; i < len(args); i++ { 245 | args[i] = strings.Trim(args[i], " ") 246 | } 247 | return "\n" + strings.Join(args, "\n\n") 248 | } 249 | return strings.Join(args, "") 250 | } 251 | 252 | func hasNewline(args ...string) bool { 253 | for _, arg := range args { 254 | if strings.Contains(arg, "\n") { 255 | return true 256 | } 257 | } 258 | return false 259 | } 260 | -------------------------------------------------------------------------------- /assertions_test.go: -------------------------------------------------------------------------------- 1 | package got_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/ysmood/gop" 12 | "github.com/ysmood/got" 13 | ) 14 | 15 | func TestAssertion(t *testing.T) { 16 | as := setup(t) 17 | 18 | as.Eq(1, 1) 19 | as.Eq(1.0, 1) 20 | as.Eq([]int{1, 3}, []int{1, 3}) 21 | as.Eq(map[int]int{1: 2, 3: 4}, map[int]int{3: 4, 1: 2}) 22 | as.Eq(nil, nil) 23 | as.Eq(map[int]int(nil), nil) 24 | fn := func() {} 25 | as.Eq(map[int]interface{}{1: fn, 2: nil}, map[int]interface{}{2: nil, 1: fn}) 26 | as.Eq((*int)(nil), nil) 27 | 28 | as.Neq(1.1, 1) 29 | as.Neq([]int{1, 2}, []int{2, 1}) 30 | as.Neq("true", true) 31 | as.Neq(errors.New("a"), errors.New("b")) 32 | 33 | as.Equal(1, 1) 34 | arr := []int{1, 2} 35 | as.Equal(arr, arr) 36 | as.Equal(fn, fn) 37 | 38 | as.Lt(time.Millisecond, time.Second) 39 | as.Lte(1, 1) 40 | 41 | as.Gt(2, 1.5) 42 | as.Gte(2, 2.0) 43 | 44 | now := time.Now() 45 | as.Eq(now, now) 46 | as.Lt(now, now.Add(time.Second)) 47 | as.Gt(now.Add(time.Second), now) 48 | 49 | as.InDelta(1.1, 1.2, 0.2) 50 | 51 | as.True(true) 52 | as.False(false) 53 | 54 | as.Nil(nil) 55 | as.Nil((*int)(nil)) 56 | as.Nil(os.Stat("go.mod")) 57 | as.NotNil([]int{}) 58 | 59 | as.Zero("") 60 | as.Zero(0) 61 | as.Zero(time.Time{}) 62 | as.NotZero(1) 63 | as.NotZero("ok") 64 | as.NotZero(time.Now()) 65 | 66 | as.Regex(`\d\d`, "10") 67 | as.Has(`test`, 'e') 68 | as.Has(`test`, "es") 69 | as.Has(`test`, []byte("es")) 70 | as.Has([]byte(`test`), "es") 71 | as.Has([]byte(`test`), []byte("es")) 72 | as.Has([]int{1, 2, 3}, 2) 73 | as.Has([3]int{1, 2, 3}, 2) 74 | as.Has(map[int]int{1: 4, 2: 5, 3: 6}, 5) 75 | 76 | as.Len([]int{1, 2}, 2) 77 | 78 | as.Err(1, 2, errors.New("err")) 79 | as.Panic(func() { panic(1) }) 80 | 81 | as.Is(1, 2) 82 | err := errors.New("err") 83 | as.Is(err, err) 84 | as.Is(fmt.Errorf("%w", err), err) 85 | as.Is(nil, nil) 86 | 87 | as.Must().Eq(1, 1) 88 | 89 | count := as.Count(2) 90 | count() 91 | count() 92 | } 93 | 94 | func TestAssertionErr(t *testing.T) { 95 | m := &mock{t: t} 96 | as := got.New(m) 97 | as.Assertions.ErrorHandler = got.NewDefaultAssertionError(gop.ThemeNone, nil) 98 | 99 | type data struct { 100 | A int 101 | S string 102 | } 103 | 104 | as.Desc("not %s", "equal").Eq(1, 2.0) 105 | m.check("not equal\n1 ⦗not ==⦘ 2.0") 106 | 107 | as.Desc("test").Desc("not %s", "equal").Eq(1, 2.0) 108 | m.check("test\nnot equal\n1 ⦗not ==⦘ 2.0") 109 | 110 | as.Eq(data{1, "a"}, data{1, "b"}) 111 | m.check(` 112 | got_test.data{ 113 | A: 1, 114 | S: "a", 115 | } 116 | 117 | ⦗not ==⦘ 118 | 119 | got_test.data{ 120 | A: 1, 121 | S: "b", 122 | }`) 123 | 124 | as.Eq(true, "a&") 125 | m.check(`true ⦗not ==⦘ "a&"`) 126 | 127 | as.Eq(nil, "ok") 128 | m.check(`nil ⦗not ==⦘ "ok"`) 129 | 130 | as.Eq(1, nil) 131 | m.check(`1 ⦗not ==⦘ nil`) 132 | 133 | as.Equal(1, 1.0) 134 | m.check("1 ⦗not ==⦘ 1.0") 135 | as.Equal([]int{1}, []int{2}) 136 | m.check(` 137 | []int{ 138 | 1, 139 | } 140 | 141 | ⦗not ==⦘ 142 | 143 | []int{ 144 | 2, 145 | }`) 146 | 147 | as.Neq(1, 1) 148 | m.check("1 ⦗==⦘ 1") 149 | as.Neq(1.0, 1) 150 | m.check("1.0 ⦗==⦘ 1 ⦗when converted to the same type⦘ ") 151 | 152 | as.Lt(1, 1) 153 | m.check("1 ⦗not <⦘ 1") 154 | as.Lte(2, 1) 155 | 156 | m.check("2 ⦗not ≤⦘ 1") 157 | as.Gt(1, 1) 158 | m.check("1 ⦗not >⦘ 1") 159 | as.Gte(1, 2) 160 | m.check("1 ⦗not ≥⦘ 2") 161 | 162 | as.InDelta(10, 20, 3) 163 | m.check(" ⦗delta between⦘ 10 ⦗and⦘ 20 ⦗not ≤⦘ 3.0") 164 | 165 | as.True(false) 166 | m.check(" ⦗should be⦘ true") 167 | as.False(true) 168 | m.check(" ⦗should be⦘ false") 169 | 170 | as.Nil(1) 171 | m.check(" ⦗last argument⦘ 1 ⦗should be⦘ nil") 172 | as.Nil() 173 | m.check(" ⦗no arguments received⦘ ") 174 | as.NotNil(nil) 175 | m.check(" ⦗last argument shouldn't be⦘ nil") 176 | as.NotNil((*int)(nil)) 177 | m.check(" ⦗last argument⦘ (*int)(nil) ⦗shouldn't be⦘ nil") 178 | as.NotNil() 179 | m.check(" ⦗no arguments received⦘ ") 180 | as.NotNil(1) 181 | m.check(" ⦗last argument⦘ 1 ⦗is not nilable⦘ ") 182 | 183 | as.Zero(1) 184 | m.check("1 ⦗should be zero value for its type⦘ ") 185 | as.NotZero(0) 186 | m.check("0 ⦗shouldn't be zero value for its type⦘ ") 187 | 188 | as.Regex(`\d\d`, "aaa") 189 | m.check(`"\\d\\d" ⦗should match⦘ "aaa"`) 190 | as.Has(`test`, "x") 191 | m.check(`"test" ⦗should has⦘ "x"`) 192 | 193 | as.Len([]int{1, 2}, 3) 194 | m.check(" ⦗expect len⦘ 2 ⦗to be⦘ 3") 195 | 196 | as.Err(nil) 197 | m.check(" ⦗last value⦘ nil ⦗should be ⦘ ") 198 | as.Panic(func() {}) 199 | m.check(" ⦗should panic⦘ ") 200 | as.Err() 201 | m.check(" ⦗no arguments received⦘ ") 202 | as.Err(1) 203 | m.check(" ⦗last value⦘ 1 ⦗should be ⦘ ") 204 | 205 | func() { 206 | defer func() { 207 | _ = recover() 208 | }() 209 | as.E(1, errors.New("E")) 210 | }() 211 | m.check(` 212 | ⦗last argument⦘ 213 | 214 | &errors.errorString{ 215 | s: "E", 216 | } 217 | 218 | ⦗should be⦘ 219 | 220 | nil`) 221 | 222 | as.Is(1, 2.2) 223 | m.check("1 ⦗should be kind of⦘ 2.2") 224 | as.Is(errors.New("a"), errors.New("b")) 225 | m.check(` 226 | &errors.errorString{ 227 | s: "a", 228 | } 229 | 230 | ⦗should in chain of⦘ 231 | 232 | &errors.errorString{ 233 | s: "b", 234 | }`) 235 | as.Is(nil, errors.New("a")) 236 | m.check(` 237 | nil 238 | 239 | ⦗should be kind of⦘ 240 | 241 | &errors.errorString{ 242 | s: "a", 243 | }`) 244 | as.Is(errors.New("a"), nil) 245 | m.check(` 246 | &errors.errorString{ 247 | s: "a", 248 | } 249 | 250 | ⦗should be kind of⦘ 251 | 252 | nil`) 253 | 254 | { 255 | count := as.Count(2) 256 | count() 257 | m.cleanup() 258 | m.check(` ⦗should count⦘ 2 ⦗times, but got⦘ 1`) 259 | 260 | count = as.Count(1) 261 | wg := sync.WaitGroup{} 262 | wg.Add(2) 263 | go func() { 264 | count() 265 | wg.Done() 266 | }() 267 | go func() { 268 | count() 269 | wg.Done() 270 | }() 271 | wg.Wait() 272 | m.cleanup() 273 | m.check(` ⦗should count⦘ 1 ⦗times, but got⦘ 2`) 274 | } 275 | } 276 | 277 | func TestAssertionColor(t *testing.T) { 278 | m := &mock{t: t} 279 | 280 | g := got.New(m) 281 | g.Eq([]int{1, 2}, []int{1, 3}) 282 | m.checkWithStyle(true, ` 283 | <36>[]int<39>{ 284 | <32>1<39>, 285 | <32>2<39>, 286 | } 287 | 288 | <31><4>⦗not ==⦘<24><39> 289 | 290 | <36>[]int<39>{ 291 | <32>1<39>, 292 | <32>3<39>, 293 | } 294 | 295 | <45><30>@@ diff chunk @@<39><49> 296 | 2 2 1, 297 | <31>3 -<39> <31>2<39>, 298 | <32> 3 +<39> <32>3<39>, 299 | 4 4 } 300 | 301 | `) 302 | 303 | g.Eq("abc", "axc") 304 | m.checkWithStyle(true, `"a<31>b<39>c" <31><4>⦗not ==⦘<24><39> "a<32>x<39>c"`) 305 | 306 | g.Eq(3, "a") 307 | m.checkWithStyle(true, `<31>3<39> <31><4>⦗not ==⦘<24><39> <32>"a"<39>`) 308 | } 309 | 310 | func TestCustomAssertionError(t *testing.T) { 311 | m := &mock{t: t} 312 | 313 | g := got.New(m) 314 | g.ErrorHandler = got.AssertionErrorReport(func(c *got.AssertionCtx) string { 315 | if c.Type == got.AssertionEq { 316 | return "custom eq" 317 | } 318 | return "" 319 | }) 320 | g.Eq(1, 2) 321 | m.check("custom eq") 322 | } 323 | -------------------------------------------------------------------------------- /cli_utils.go: -------------------------------------------------------------------------------- 1 | package got 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // EnsureCoverage via report file generated from, for example: 13 | // 14 | // go test -coverprofile=coverage.out 15 | // 16 | // Return error if any file's coverage is less than min, min is a percentage value. 17 | func EnsureCoverage(path string, min float64) error { 18 | tmp, _ := os.CreateTemp("", "") 19 | report := tmp.Name() 20 | defer func() { _ = os.Remove(report) }() 21 | _ = tmp.Close() 22 | _, err := exec.Command("go", "tool", "cover", "-html", path, "-o", report).CombinedOutput() 23 | if err != nil { 24 | return err 25 | } 26 | 27 | list := parseReport(report) 28 | rejected := []string{} 29 | for _, c := range list { 30 | if c.coverage < min { 31 | rejected = append(rejected, fmt.Sprintf(" %s (%0.1f%%)", c.path, c.coverage)) 32 | } 33 | } 34 | 35 | if len(rejected) > 0 { 36 | return fmt.Errorf( 37 | "Test coverage for these files should be greater than %.2f%%:\n%s", 38 | min, 39 | strings.Join(rejected, "\n"), 40 | ) 41 | } 42 | return nil 43 | } 44 | 45 | type cov struct { 46 | path string 47 | coverage float64 48 | } 49 | 50 | var regCov = regexp.MustCompile(``) 51 | 52 | func parseReport(path string) []cov { 53 | out, _ := os.ReadFile(path) 54 | 55 | ms := regCov.FindAllStringSubmatch(string(out), -1) 56 | 57 | list := []cov{} 58 | for _, m := range ms { 59 | c, _ := strconv.ParseFloat(m[2], 32) 60 | list = append(list, cov{m[1], c}) 61 | } 62 | return list 63 | } 64 | -------------------------------------------------------------------------------- /cli_utils_test.go: -------------------------------------------------------------------------------- 1 | package got_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ysmood/got" 7 | ) 8 | 9 | func TestEnsureCoverage(t *testing.T) { 10 | g := setup(t) 11 | g.Nil(got.EnsureCoverage("fixtures/coverage/cov.txt", 100)) 12 | 13 | g.Err(got.EnsureCoverage("fixtures/coverage/cov.txt", 120)) 14 | g.Err(got.EnsureCoverage("not-exists", 100)) 15 | } 16 | -------------------------------------------------------------------------------- /cmd/check-cov/main.go: -------------------------------------------------------------------------------- 1 | // Package main ... 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/ysmood/got" 10 | ) 11 | 12 | var covFile = flag.String("cov-file", "coverage.out", "the path of the coverage report") 13 | var minCover = flag.Float64("min", 100, "min coverage rate or exit code with 1") 14 | 15 | func main() { 16 | if !flag.Parsed() { 17 | flag.Parse() 18 | } 19 | 20 | err := got.EnsureCoverage(*covFile, *minCover) 21 | if err != nil { 22 | fmt.Println(err) 23 | os.Exit(1) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /each.go: -------------------------------------------------------------------------------- 1 | package got 2 | 3 | import ( 4 | "reflect" 5 | "runtime/debug" 6 | ) 7 | 8 | // Only run tests with it 9 | type Only struct{} 10 | 11 | // Skip the current test 12 | type Skip struct{} 13 | 14 | // Each runs each exported method Fn on type Ctx as a subtest of t. 15 | // The iteratee can be a struct Ctx or: 16 | // 17 | // iteratee(t Testable) (ctx Ctx) 18 | // 19 | // Each Fn will be called like: 20 | // 21 | // ctx.Fn() 22 | // 23 | // If iteratee is Ctx, its G field will be set to New(t) for each test. 24 | // Any Fn that has the same name with the embedded one will be ignored. 25 | func Each(t Testable, iteratee interface{}) (count int) { 26 | t.Helper() 27 | 28 | itVal := normalizeIteratee(t, iteratee) 29 | 30 | ctxType := itVal.Type().Out(0) 31 | 32 | methods := filterMethods(ctxType) 33 | 34 | runVal := reflect.ValueOf(t).MethodByName("Run") 35 | cbType := runVal.Type().In(1) 36 | 37 | for _, m := range methods { 38 | // because the callback is in another goroutine, we create closures for each loop 39 | method := m 40 | 41 | runVal.Call([]reflect.Value{ 42 | reflect.ValueOf(method.Name), 43 | reflect.MakeFunc(cbType, func(args []reflect.Value) []reflect.Value { 44 | t := args[0].Interface().(Testable) 45 | doSkip(t, method) 46 | count++ 47 | res := itVal.Call(args) 48 | return callMethod(t, method, res[0]) 49 | }), 50 | }) 51 | } 52 | return 53 | } 54 | 55 | func normalizeIteratee(t Testable, iteratee interface{}) reflect.Value { 56 | t.Helper() 57 | 58 | if iteratee == nil { 59 | t.Logf("iteratee shouldn't be nil") 60 | t.FailNow() 61 | } 62 | 63 | itVal := reflect.ValueOf(iteratee) 64 | itType := itVal.Type() 65 | fail := true 66 | 67 | switch itType.Kind() { 68 | case reflect.Func: 69 | if itType.NumIn() != 1 || itType.NumOut() != 1 { 70 | break 71 | } 72 | try(func() { 73 | _ = reflect.New(itType.In(0).Elem()).Interface().(Testable) 74 | fail = false 75 | }) 76 | 77 | case reflect.Struct: 78 | fnType := reflect.FuncOf([]reflect.Type{reflect.TypeOf(t)}, []reflect.Type{itType}, false) 79 | structVal := itVal 80 | itVal = reflect.MakeFunc(fnType, func(args []reflect.Value) []reflect.Value { 81 | sub := args[0].Interface().(Testable) 82 | as := reflect.ValueOf(New(sub)) 83 | 84 | c := reflect.New(itType).Elem() 85 | c.Set(structVal) 86 | try(func() { c.FieldByName("G").Set(as) }) 87 | 88 | return []reflect.Value{c} 89 | }) 90 | fail = false 91 | } 92 | 93 | if fail { 94 | t.Logf("iteratee <%v> should be a struct or ", itType) 95 | t.FailNow() 96 | } 97 | return itVal 98 | } 99 | 100 | func callMethod(t Testable, method reflect.Method, receiver reflect.Value) []reflect.Value { 101 | args := make([]reflect.Value, method.Type.NumIn()) 102 | args[0] = receiver 103 | 104 | for i := 1; i < len(args); i++ { 105 | args[i] = reflect.New(method.Type.In(i)).Elem() 106 | } 107 | 108 | defer func() { 109 | if err := recover(); err != nil { 110 | t.Logf("[panic] %v\n%s", err, debug.Stack()) 111 | t.Fail() 112 | } 113 | }() 114 | 115 | method.Func.Call(args) 116 | 117 | return []reflect.Value{} 118 | } 119 | 120 | func filterMethods(typ reflect.Type) []reflect.Method { 121 | embedded := map[string]struct{}{} 122 | for i := 0; i < typ.NumField(); i++ { 123 | field := typ.Field(i) 124 | if field.Anonymous { 125 | for j := 0; j < field.Type.NumMethod(); j++ { 126 | embedded[field.Type.Method(j).Name] = struct{}{} 127 | } 128 | } 129 | } 130 | 131 | methods := []reflect.Method{} 132 | onlyList := []reflect.Method{} 133 | for i := 0; i < typ.NumMethod(); i++ { 134 | method := typ.Method(i) 135 | if _, has := embedded[method.Name]; has { 136 | continue 137 | } 138 | 139 | if method.Type.NumIn() > 1 && method.Type.In(1) == reflect.TypeOf(Only{}) { 140 | onlyList = append(onlyList, method) 141 | } 142 | 143 | methods = append(methods, method) 144 | } 145 | 146 | if len(onlyList) > 0 { 147 | return onlyList 148 | } 149 | 150 | return methods 151 | } 152 | 153 | func doSkip(t Testable, method reflect.Method) { 154 | if method.Type.NumIn() > 1 && method.Type.In(1) == reflect.TypeOf(Skip{}) { 155 | t.SkipNow() 156 | } 157 | } 158 | 159 | func try(fn func()) { 160 | defer func() { 161 | _ = recover() 162 | }() 163 | fn() 164 | } 165 | -------------------------------------------------------------------------------- /each_test.go: -------------------------------------------------------------------------------- 1 | package got_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ysmood/got" 7 | ) 8 | 9 | func TestEach(t *testing.T) { 10 | count := got.Each(t, StructVal{val: 1}) 11 | got.New(t).Eq(count, 2) 12 | } 13 | 14 | type StructVal struct { 15 | got.G 16 | val int 17 | } 18 | 19 | func (c StructVal) Normal() { 20 | c.Eq(c.val, 1) 21 | } 22 | 23 | func (c StructVal) ExtraInOut(int) int { 24 | c.Eq(c.val, 1) 25 | return 0 26 | } 27 | 28 | func (c StructVal) TestSkip(got.Skip) { 29 | } 30 | 31 | func TestEachEmbedded(t *testing.T) { 32 | got.Each(t, Container{}) 33 | } 34 | 35 | type Container struct { 36 | Embedded 37 | } 38 | 39 | func (c Container) A() { c.Fail() } 40 | func (c Container) B() {} 41 | 42 | type Embedded struct { 43 | *testing.T 44 | } 45 | 46 | func (c Embedded) A() {} 47 | func (c Embedded) C() { c.Fail() } 48 | 49 | func TestEachWithOnly(t *testing.T) { 50 | got.Each(t, Only{}) 51 | } 52 | 53 | type Only struct { 54 | *testing.T 55 | } 56 | 57 | func (c Only) A(got.Only) {} 58 | func (c Only) B() { panic("") } 59 | 60 | func TestEachErr(t *testing.T) { 61 | as := got.New(t) 62 | m := &mock{t: t} 63 | 64 | as.Panic(func() { 65 | got.Each(m, nil) 66 | }) 67 | m.check("iteratee shouldn't be nil") 68 | 69 | as.Panic(func() { 70 | got.Each(m, 1) 71 | }) 72 | m.check("iteratee should be a struct or ") 73 | 74 | it := func() Err { return Err{} } 75 | as.Panic(func() { 76 | got.Each(m, it) 77 | }) 78 | m.check("iteratee should be a struct or ") 79 | } 80 | 81 | type Err struct { 82 | } 83 | 84 | func (s Err) A(int) {} 85 | 86 | func TestPanicAsFailure(t *testing.T) { 87 | as := got.New(t) 88 | 89 | m := &mock{t: t} 90 | it := func(_ *mock) PanicAsFailure { return PanicAsFailure{} } 91 | as.Eq(got.Each(m, it), 2) 92 | as.True(m.failed) 93 | as.Has(m.msg, "[panic] err") 94 | } 95 | 96 | type PanicAsFailure struct { 97 | } 98 | 99 | func (p PanicAsFailure) A() { 100 | panic("err") 101 | } 102 | 103 | func (p PanicAsFailure) B() { 104 | } 105 | -------------------------------------------------------------------------------- /fixtures/coverage/cov.txt: -------------------------------------------------------------------------------- 1 | mode: atomic 2 | github.com/ysmood/got/fixtures/coverage/foo.go:4.16,6.2 1 1 3 | -------------------------------------------------------------------------------- /fixtures/coverage/foo.go: -------------------------------------------------------------------------------- 1 | // Package coverage ... 2 | package coverage 3 | 4 | // Foo ... 5 | func Foo() int { 6 | return 1 7 | } 8 | -------------------------------------------------------------------------------- /fixtures/coverage/foo_test.go: -------------------------------------------------------------------------------- 1 | package coverage 2 | 3 | import "testing" 4 | 5 | func TestFoo(_ *testing.T) { 6 | Foo() 7 | } 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ysmood/got 2 | 3 | go 1.21 4 | 5 | require github.com/ysmood/gop v0.2.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= 2 | github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= 3 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.22 2 | 3 | use ( 4 | . 5 | ./lib/benchmark 6 | ./lib/example 7 | ) 8 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 2 | github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= 3 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 4 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 5 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 6 | -------------------------------------------------------------------------------- /got.go: -------------------------------------------------------------------------------- 1 | // Package got is an enjoyable golang test framework. 2 | package got 3 | 4 | import ( 5 | "flag" 6 | "os" 7 | "reflect" 8 | "regexp" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/ysmood/gop" 13 | "github.com/ysmood/got/lib/diff" 14 | ) 15 | 16 | // Testable interface. Usually, you use *testing.T as it. 17 | type Testable interface { 18 | Name() string // same as testing.common.Name 19 | Skipped() bool // same as testing.common.Skipped 20 | Failed() bool // same as testing.common.Failed 21 | Cleanup(func()) // same as testing.common.Cleanup 22 | FailNow() // same as testing.common.FailNow 23 | Fail() // same as testing.common.Fail 24 | Helper() // same as testing.common.Helper 25 | Logf(format string, args ...interface{}) // same as testing.common.Logf 26 | SkipNow() // same as testing.common.Skip 27 | } 28 | 29 | // G is the helper context, it provides some handy helpers for testing 30 | type G struct { 31 | Testable 32 | Assertions 33 | Utils 34 | 35 | snapshots *sync.Map 36 | } 37 | 38 | // Setup returns a helper to init G instance 39 | func Setup(init func(g G)) func(t Testable) G { 40 | return func(t Testable) G { 41 | g := New(t) 42 | if init != nil { 43 | init(g) 44 | } 45 | return g 46 | } 47 | } 48 | 49 | // T is the shortcut for New 50 | func T(t Testable) G { 51 | return New(t) 52 | } 53 | 54 | // New G instance 55 | func New(t Testable) G { 56 | eh := NewDefaultAssertionError(gop.ThemeDefault, diff.ThemeDefault) 57 | 58 | g := G{ 59 | t, 60 | Assertions{Testable: t, ErrorHandler: eh}, 61 | Utils{t}, 62 | &sync.Map{}, 63 | } 64 | 65 | g.loadSnapshots() 66 | 67 | return g 68 | } 69 | 70 | // DefaultFlags will set the "go test" flag if not yet presented. 71 | // It must be executed in the init() function. 72 | // Such as the timeout: 73 | // 74 | // DefaultFlags("timeout=10s") 75 | func DefaultFlags(flags ...string) { 76 | // remove default timeout from "go test" 77 | filtered := []string{} 78 | for _, arg := range os.Args { 79 | if arg != "-test.timeout=10m0s" { 80 | filtered = append(filtered, arg) 81 | } 82 | } 83 | os.Args = filtered 84 | 85 | list := map[string]struct{}{} 86 | reg := regexp.MustCompile(`^-test\.(\w+)`) 87 | for _, arg := range os.Args { 88 | ms := reg.FindStringSubmatch(arg) 89 | if ms != nil { 90 | list[ms[1]] = struct{}{} 91 | } 92 | } 93 | 94 | for _, flag := range flags { 95 | if _, has := list[strings.Split(flag, "=")[0]]; !has { 96 | os.Args = append(os.Args, "-test."+flag) 97 | } 98 | } 99 | } 100 | 101 | // Parallel config of "go test -parallel" 102 | func Parallel() (n int) { 103 | flag.Parse() 104 | flag.Visit(func(f *flag.Flag) { 105 | if f.Name == "test.parallel" { 106 | v := reflect.ValueOf(f.Value).Elem().Convert(reflect.TypeOf(n)) 107 | n = v.Interface().(int) 108 | } 109 | }) 110 | return 111 | } 112 | -------------------------------------------------------------------------------- /got_test.go: -------------------------------------------------------------------------------- 1 | package got_test 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSetup(t *testing.T) { 8 | g := setup(t) 9 | g.Eq(1, 1) 10 | } 11 | -------------------------------------------------------------------------------- /lib/benchmark/.golangci.yml: -------------------------------------------------------------------------------- 1 | 2 | run: 3 | skip-dirs-use-default: false 4 | 5 | linters: 6 | enable: 7 | - gofmt 8 | - revive 9 | - gocyclo 10 | - misspell 11 | - bodyclose 12 | 13 | gocyclo: 14 | min-complexity: 15 15 | 16 | issues: 17 | exclude-use-default: false 18 | 19 | -------------------------------------------------------------------------------- /lib/benchmark/bench_test.go: -------------------------------------------------------------------------------- 1 | package benchmark 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/sergi/go-diff/diffmatchpatch" 10 | "github.com/ysmood/got/lib/benchmark/myers" 11 | "github.com/ysmood/got/lib/lcs" 12 | ) 13 | 14 | var x = randStr(100) 15 | var y = randStr(100) 16 | 17 | func BenchmarkRandomYad(b *testing.B) { 18 | c := context.Background() 19 | 20 | xs := lcs.NewChars(x) 21 | ys := lcs.NewChars(y) 22 | 23 | b.StartTimer() 24 | 25 | for i := 0; i < b.N; i++ { 26 | xs.YadLCS(c, ys) 27 | } 28 | } 29 | 30 | func BenchmarkRandomGoogle(b *testing.B) { 31 | dmp := diffmatchpatch.New() 32 | 33 | xs, ys := []rune(x), []rune(y) 34 | 35 | b.StartTimer() 36 | 37 | for i := 0; i < b.N; i++ { 38 | dmp.DiffMainRunes(xs, ys, false) 39 | } 40 | } 41 | 42 | func BenchmarkRandomMyers(b *testing.B) { 43 | xs, ys := split(x), split(y) 44 | 45 | for i := 0; i < b.N; i++ { 46 | _ = myers.Diff(xs, ys) 47 | } 48 | } 49 | 50 | func randStr(n int) string { 51 | b := make([]byte, n) 52 | _, _ = rand.Read(b) 53 | return string(b) 54 | } 55 | 56 | func split(text string) []string { 57 | return strings.Split(text, "") 58 | } 59 | -------------------------------------------------------------------------------- /lib/benchmark/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ysmood/got/lib/benchmark 2 | 3 | go 1.18 4 | 5 | require github.com/sergi/go-diff v1.2.0 6 | -------------------------------------------------------------------------------- /lib/benchmark/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 5 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 6 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= 10 | github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 13 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 16 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 17 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 18 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 19 | -------------------------------------------------------------------------------- /lib/benchmark/myers/diff.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package myers implements the Myers diff algorithm. 6 | package myers 7 | 8 | // Sources: 9 | // https://blog.jcoglan.com/2017/02/17/the-myers-diff-algorithm-part-3/ 10 | // https://www.codeproject.com/Articles/42279/%2FArticles%2F42279%2FInvestigating-Myers-diff-algorithm-Part-1-of-2 11 | 12 | // OpKind ... 13 | type OpKind int 14 | 15 | const ( 16 | // Delete ... 17 | Delete OpKind = iota 18 | // Insert ... 19 | Insert 20 | ) 21 | 22 | // Diff ... 23 | func Diff(before, after []string) []*Operation { 24 | return operations(before, after) 25 | } 26 | 27 | // Operation ... 28 | type Operation struct { 29 | Kind OpKind 30 | Content []string // content from b 31 | I1, I2 int // indices of the line in a 32 | J1 int // indices of the line in b, J2 implied by len(Content) 33 | } 34 | 35 | // operations returns the list of operations to convert a into b, consolidating 36 | // operations for multiple lines and not including equal lines. 37 | func operations(a, b []string) []*Operation { 38 | if len(a) == 0 && len(b) == 0 { 39 | return nil 40 | } 41 | 42 | trace, offset := shortestEditSequence(a, b) 43 | snakes := backtrack(trace, len(a), len(b), offset) 44 | 45 | M, N := len(a), len(b) 46 | 47 | var i int 48 | solution := make([]*Operation, len(a)+len(b)) 49 | 50 | add := func(op *Operation, i2, j2 int) { 51 | if op == nil { 52 | return 53 | } 54 | op.I2 = i2 55 | if op.Kind == Insert { 56 | op.Content = b[op.J1:j2] 57 | } 58 | solution[i] = op 59 | i++ 60 | } 61 | x, y := 0, 0 62 | for _, snake := range snakes { 63 | if len(snake) < 2 { 64 | continue 65 | } 66 | var op *Operation 67 | // delete (horizontal) 68 | for snake[0]-snake[1] > x-y { 69 | if op == nil { 70 | op = &Operation{ 71 | Kind: Delete, 72 | I1: x, 73 | J1: y, 74 | } 75 | } 76 | x++ 77 | if x == M { 78 | break 79 | } 80 | } 81 | add(op, x, y) 82 | op = nil 83 | // insert (vertical) 84 | for snake[0]-snake[1] < x-y { 85 | if op == nil { 86 | op = &Operation{ 87 | Kind: Insert, 88 | I1: x, 89 | J1: y, 90 | } 91 | } 92 | y++ 93 | } 94 | add(op, x, y) 95 | op = nil 96 | // equal (diagonal) 97 | for x < snake[0] { 98 | x++ 99 | y++ 100 | } 101 | if x >= M && y >= N { 102 | break 103 | } 104 | } 105 | return solution[:i] 106 | } 107 | 108 | // backtrack uses the trace for the edit sequence computation and returns the 109 | // "snakes" that make up the solution. A "snake" is a single deletion or 110 | // insertion followed by zero or diagonals. 111 | func backtrack(trace [][]int, x, y, offset int) [][]int { 112 | snakes := make([][]int, len(trace)) 113 | d := len(trace) - 1 114 | for ; x > 0 && y > 0 && d > 0; d-- { 115 | V := trace[d] 116 | if len(V) == 0 { 117 | continue 118 | } 119 | snakes[d] = []int{x, y} 120 | 121 | k := x - y 122 | 123 | var prev int 124 | if k == -d || (k != d && V[k-1+offset] < V[k+1+offset]) { 125 | prev = k + 1 126 | } else { 127 | prev = k - 1 128 | } 129 | 130 | x = V[prev+offset] 131 | y = x - prev 132 | } 133 | if x < 0 || y < 0 { 134 | return snakes 135 | } 136 | snakes[d] = []int{x, y} 137 | return snakes 138 | } 139 | 140 | // shortestEditSequence returns the shortest edit sequence that converts a into b. 141 | func shortestEditSequence(a, b []string) ([][]int, int) { 142 | M, N := len(a), len(b) 143 | V := make([]int, 2*(N+M)+1) 144 | offset := N + M 145 | trace := make([][]int, N+M+1) 146 | 147 | // Iterate through the maximum possible length of the SES (N+M). 148 | for d := 0; d <= N+M; d++ { 149 | copyV := make([]int, len(V)) 150 | // k lines are represented by the equation y = x - k. We move in 151 | // increments of 2 because end points for even d are on even k lines. 152 | for k := -d; k <= d; k += 2 { 153 | // At each point, we either go down or to the right. We go down if 154 | // k == -d, and we go to the right if k == d. We also prioritize 155 | // the maximum x value, because we prefer deletions to insertions. 156 | var x int 157 | if k == -d || (k != d && V[k-1+offset] < V[k+1+offset]) { 158 | x = V[k+1+offset] // down 159 | } else { 160 | x = V[k-1+offset] + 1 // right 161 | } 162 | 163 | y := x - k 164 | 165 | // Diagonal moves while we have equal contents. 166 | for x < M && y < N && a[x] == b[y] { 167 | x++ 168 | y++ 169 | } 170 | 171 | V[k+offset] = x 172 | 173 | // Return if we've exceeded the maximum values. 174 | if x == M && y == N { 175 | // Makes sure to save the state of the array before returning. 176 | copy(copyV, V) 177 | trace[d] = copyV 178 | return trace, offset 179 | } 180 | } 181 | 182 | // Save the state of the array. 183 | copy(copyV, V) 184 | trace[d] = copyV 185 | } 186 | return nil, 0 187 | } 188 | -------------------------------------------------------------------------------- /lib/benchmark/readme.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | ```txt 4 | $ go test -benchmem -bench . 5 | goos: darwin 6 | goarch: amd64 7 | pkg: github.com/ysmood/got/lib/benchmark 8 | cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz 9 | BenchmarkRandomYad-8 52597 20401 ns/op 14931 B/op 213 allocs/op 10 | BenchmarkRandomGoogle-8 18609 63802 ns/op 44848 B/op 906 allocs/op 11 | BenchmarkRandomMyers-8 15781 75848 ns/op 411412 B/op 360 allocs/op 12 | PASS 13 | ok github.com/ysmood/got/lib/benchmark 3.886s 14 | ``` 15 | 16 | YadLCS is faster and uses less memory than [Google Myer's algorithm](https://github.com/sergi/go-diff/blob/849d7ebc9716f43ec1295e9bc00e5c8cffef3d9f/diffmatchpatch/diff.go#L5-L7) when the item histogram is large, it's common in line based diff in large text context. 17 | -------------------------------------------------------------------------------- /lib/diff/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | A simple lib to diff two string with pretty output. It also provide some low-level API to customize the output. 4 | -------------------------------------------------------------------------------- /lib/diff/ast.go: -------------------------------------------------------------------------------- 1 | // Package diff ... 2 | package diff 3 | 4 | // TokenLine of tokens 5 | type TokenLine struct { 6 | Type Type 7 | Tokens []*Token 8 | } 9 | 10 | // ParseTokenLines of tokens 11 | func ParseTokenLines(ts []*Token) []*TokenLine { 12 | list := []*TokenLine{} 13 | var l *TokenLine 14 | for _, t := range ts { 15 | switch t.Type { 16 | case SameSymbol, AddSymbol, DelSymbol: 17 | l = &TokenLine{} 18 | list = append(list, l) 19 | l.Type = t.Type 20 | } 21 | l.Tokens = append(l.Tokens, t) 22 | } 23 | return list 24 | } 25 | 26 | // SpreadTokenLines to tokens 27 | func SpreadTokenLines(lines []*TokenLine) []*Token { 28 | out := []*Token{} 29 | for _, l := range lines { 30 | out = append(out, l.Tokens...) 31 | } 32 | return out 33 | } 34 | -------------------------------------------------------------------------------- /lib/diff/format.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/ysmood/gop" 8 | ) 9 | 10 | // Theme for diff 11 | type Theme func(t Type) []gop.Style 12 | 13 | // ThemeDefault colors for Sprint 14 | var ThemeDefault = func(t Type) []gop.Style { 15 | switch t { 16 | case AddSymbol: 17 | return []gop.Style{gop.Green} 18 | case DelSymbol: 19 | return []gop.Style{gop.Red} 20 | case AddWords: 21 | return []gop.Style{gop.Green} 22 | case DelWords: 23 | return []gop.Style{gop.Red} 24 | case ChunkStart: 25 | return []gop.Style{gop.Black, gop.BgMagenta} 26 | } 27 | return []gop.Style{gop.None} 28 | } 29 | 30 | // ThemeNone colors for Sprint 31 | var ThemeNone = func(_ Type) []gop.Style { 32 | return []gop.Style{gop.None} 33 | } 34 | 35 | // Diff x and y into a human readable string. 36 | func Diff(x, y string) string { 37 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 38 | defer cancel() 39 | return Format(Tokenize(ctx, x, y), ThemeDefault) 40 | } 41 | 42 | // Tokenize x and y into diff tokens with diff words and narrow chunks. 43 | func Tokenize(ctx context.Context, x, y string) []*Token { 44 | ts := TokenizeText(ctx, x, y) 45 | lines := ParseTokenLines(ts) 46 | lines = Narrow(1, lines) 47 | Words(ctx, lines) 48 | return SpreadTokenLines(lines) 49 | } 50 | 51 | // Format tokens into a human readable string 52 | func Format(ts []*Token, theme Theme) string { 53 | out := "" 54 | 55 | for _, t := range ts { 56 | s := t.Literal 57 | out += gop.Stylize(s, theme(t.Type)) 58 | } 59 | 60 | return out 61 | } 62 | 63 | // Narrow the context around each diff section to n lines. 64 | func Narrow(n int, lines []*TokenLine) []*TokenLine { 65 | if n < 0 { 66 | n = 0 67 | } 68 | 69 | keep := map[int]bool{} 70 | for i, l := range lines { 71 | switch l.Type { 72 | case AddSymbol, DelSymbol: 73 | for j := max(i-n, 0); j <= i+n && j < len(lines); j++ { 74 | keep[j] = true 75 | } 76 | } 77 | } 78 | 79 | out := []*TokenLine{} 80 | for i, l := range lines { 81 | if !keep[i] { 82 | continue 83 | } 84 | 85 | if _, has := keep[i-1]; !has { 86 | ts := []*Token{{ChunkStart, "@@ diff chunk @@"}, {Newline, "\n"}} 87 | out = append(out, &TokenLine{ChunkStart, ts}) 88 | } 89 | 90 | out = append(out, l) 91 | 92 | if _, has := keep[i+1]; !has { 93 | ts := []*Token{{ChunkEnd, ""}, {Newline, "\n"}} 94 | out = append(out, &TokenLine{ChunkEnd, ts}) 95 | } 96 | } 97 | 98 | return out 99 | } 100 | 101 | // Words diff 102 | func Words(ctx context.Context, lines []*TokenLine) { 103 | delLines := []*TokenLine{} 104 | addLines := []*TokenLine{} 105 | 106 | df := func() { 107 | if len(delLines) == 0 || len(delLines) != len(addLines) { 108 | return 109 | } 110 | 111 | for i := 0; i < len(delLines); i++ { 112 | d := delLines[i] 113 | a := addLines[i] 114 | 115 | dts, ats := TokenizeLine(ctx, d.Tokens[2].Literal, a.Tokens[2].Literal) 116 | d.Tokens = append(d.Tokens[0:2], append(dts, d.Tokens[3:]...)...) 117 | a.Tokens = append(a.Tokens[0:2], append(ats, a.Tokens[3:]...)...) 118 | } 119 | 120 | delLines = []*TokenLine{} 121 | addLines = []*TokenLine{} 122 | } 123 | 124 | for _, l := range lines { 125 | switch l.Type { 126 | case DelSymbol: 127 | delLines = append(delLines, l) 128 | case AddSymbol: 129 | addLines = append(addLines, l) 130 | default: 131 | df() 132 | } 133 | } 134 | 135 | df() 136 | } 137 | 138 | func max(x, y int) int { 139 | if x < y { 140 | return y 141 | } 142 | return x 143 | } 144 | -------------------------------------------------------------------------------- /lib/diff/format_test.go: -------------------------------------------------------------------------------- 1 | package diff_test 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/ysmood/gop" 9 | "github.com/ysmood/got" 10 | "github.com/ysmood/got/lib/diff" 11 | "github.com/ysmood/got/lib/lcs" 12 | ) 13 | 14 | var setup = got.Setup(func(g got.G) { 15 | g.ErrorHandler = got.NewDefaultAssertionError(nil, nil) 16 | }) 17 | 18 | func split(s string) []string { return strings.Split(s, "") } 19 | 20 | func TestDiff(t *testing.T) { 21 | g := setup(t) 22 | 23 | out := gop.StripANSI(diff.Diff("abc", "axc")) 24 | 25 | g.Eq(out, `@@ diff chunk @@ 26 | 1 - abc 27 | 1 + axc 28 | 29 | `) 30 | } 31 | 32 | func TestFormat(t *testing.T) { 33 | g := setup(t) 34 | ts := diff.TokenizeText( 35 | g.Context(), 36 | strings.ReplaceAll("a b c d f g h h j q z", " ", "\n"), 37 | strings.ReplaceAll("a b c d e f g i j k r x y z", " ", "\n"), 38 | ) 39 | 40 | df := diff.Format(ts, diff.ThemeNone) 41 | 42 | g.Eq(df, ""+ 43 | "01 01 a\n"+ 44 | "02 02 b\n"+ 45 | "03 03 c\n"+ 46 | "04 04 d\n"+ 47 | " 05 + e\n"+ 48 | "05 06 f\n"+ 49 | "06 07 g\n"+ 50 | "07 - h\n"+ 51 | "08 - h\n"+ 52 | " 08 + i\n"+ 53 | "09 09 j\n"+ 54 | "10 - q\n"+ 55 | " 10 + k\n"+ 56 | " 11 + r\n"+ 57 | " 12 + x\n"+ 58 | " 13 + y\n"+ 59 | "11 14 z\n"+ 60 | "") 61 | } 62 | 63 | func TestDisconnectedChunks(t *testing.T) { 64 | g := setup(t) 65 | ts := diff.TokenizeText( 66 | g.Context(), 67 | strings.ReplaceAll("a b c d f g h i j k l m n", " ", "\n"), 68 | strings.ReplaceAll("x b c d f g h i x k l m n", " ", "\n"), 69 | ) 70 | 71 | lines := diff.ParseTokenLines(ts) 72 | lines = diff.Narrow(1, lines) 73 | ts = diff.SpreadTokenLines(lines) 74 | 75 | df := diff.Format(ts, diff.ThemeNone) 76 | 77 | g.Eq(df, ""+ 78 | "@@ diff chunk @@\n"+ 79 | "01 - a\n"+ 80 | " 01 + x\n"+ 81 | "02 02 b\n"+ 82 | "\n"+ 83 | "@@ diff chunk @@\n"+ 84 | "08 08 i\n"+ 85 | "09 - j\n"+ 86 | " 09 + x\n"+ 87 | "10 10 k\n"+ 88 | "\n"+ 89 | "") 90 | } 91 | 92 | func TestChunks0(t *testing.T) { 93 | g := setup(t) 94 | ts := diff.TokenizeText( 95 | g.Context(), 96 | strings.ReplaceAll("a b c", " ", "\n"), 97 | strings.ReplaceAll("a x c", " ", "\n"), 98 | ) 99 | 100 | lines := diff.ParseTokenLines(ts) 101 | lines = diff.Narrow(-1, lines) 102 | ts = diff.SpreadTokenLines(lines) 103 | 104 | df := diff.Format(ts, diff.ThemeNone) 105 | 106 | g.Eq(df, ""+ 107 | "@@ diff chunk @@\n"+ 108 | "2 - b\n"+ 109 | " 2 + x\n"+ 110 | "\n"+ 111 | "") 112 | } 113 | 114 | func TestNoDifference(t *testing.T) { 115 | g := setup(t) 116 | ts := diff.TokenizeText(g.Context(), "a", "b") 117 | 118 | df := diff.Format(ts, diff.ThemeNone) 119 | 120 | g.Eq(df, ""+ 121 | "1 - a\n"+ 122 | " 1 + b\n"+ 123 | "") 124 | } 125 | 126 | func TestTwoLines(t *testing.T) { 127 | g := setup(t) 128 | 129 | format := func(ts []*diff.Token) string { 130 | out := "" 131 | for _, t := range ts { 132 | txt := strings.TrimSpace(strings.ReplaceAll(t.Literal, "", " ")) 133 | switch t.Type { 134 | case diff.DelWords: 135 | out += "-" + txt 136 | case diff.AddWords: 137 | out += "+" + txt 138 | default: 139 | out += "=" + txt 140 | } 141 | } 142 | return out 143 | } 144 | 145 | check := func(x, y, ex, ey string) { 146 | t.Helper() 147 | 148 | tx, ty := diff.TokenizeLine(g.Context(), 149 | strings.ReplaceAll(x, " ", ""), 150 | strings.ReplaceAll(y, " ", "")) 151 | dx, dy := format(tx), format(ty) 152 | 153 | if dx != ex || dy != ey { 154 | t.Error("\n", dx, "\n", dy, "\n!=\n", ex, "\n", ey) 155 | } 156 | } 157 | 158 | check( 159 | " a b c d f g h i j k l m n", 160 | " x x b c d f g h i x k l m n", 161 | "-a=b c d f g h i-j=k l m n", 162 | "+x x=b c d f g h i+x=k l m n", 163 | ) 164 | 165 | check( 166 | " 4 9 0 4 5 0 8 8 5 3", 167 | " 4 9 0 5 4 3 7 5 2", 168 | "=4 9 0 4 5-0 8 8 5 3", 169 | "=4 9 0+5=4+3 7=5+2", 170 | ) 171 | 172 | check( 173 | " 4 9 0 4 5 0 8", 174 | " 4 9 0 5 4 3 7", 175 | "=4 9 0 4-5 0 8", 176 | "=4 9 0+5=4+3 7", 177 | ) 178 | } 179 | 180 | func TestColor(t *testing.T) { 181 | g := setup(t) 182 | 183 | out := diff.Diff("abc", "axc") 184 | 185 | g.Eq(gop.VisualizeANSI(out), `<45><30>@@ diff chunk @@<39><49> 186 | <31>1 -<39> a<31>b<39>c 187 | <32> 1 +<39> a<32>x<39>c 188 | 189 | `) 190 | } 191 | 192 | func TestCustomSplit(t *testing.T) { 193 | g := setup(t) 194 | 195 | ctx := context.WithValue(g.Context(), lcs.SplitKey, split) 196 | 197 | g.Eq(diff.TokenizeLine(ctx, "abc", "abc")) 198 | } 199 | -------------------------------------------------------------------------------- /lib/diff/token.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/ysmood/got/lib/lcs" 9 | ) 10 | 11 | // Type of token 12 | type Type int 13 | 14 | const ( 15 | // Newline type 16 | Newline Type = iota 17 | // Space type 18 | Space 19 | 20 | // ChunkStart type 21 | ChunkStart 22 | // ChunkEnd type 23 | ChunkEnd 24 | 25 | // SameSymbol type 26 | SameSymbol 27 | // SameLine type 28 | SameLine 29 | 30 | // AddSymbol type 31 | AddSymbol 32 | // AddLine type 33 | AddLine 34 | 35 | // DelSymbol typ 36 | DelSymbol 37 | // DelLine type 38 | DelLine 39 | 40 | // SameWords type 41 | SameWords 42 | // AddWords type 43 | AddWords 44 | // DelWords type 45 | DelWords 46 | 47 | // EmptyLine type 48 | EmptyLine 49 | ) 50 | 51 | // Token presents a symbol in diff layout 52 | type Token struct { 53 | Type Type 54 | Literal string 55 | } 56 | 57 | // TokenizeText text block a and b into diff tokens. 58 | func TokenizeText(ctx context.Context, x, y string) []*Token { 59 | xls := lcs.NewLines(x) // x lines 60 | yls := lcs.NewLines(y) // y lines 61 | 62 | // TODO: We should use index to check equality, remove the usage of xs.Sub 63 | s := xls.Sub(xls.YadLCS(ctx, yls)) 64 | 65 | ts := []*Token{} 66 | 67 | xNum, yNum, sNum := numFormat(xls, yls) 68 | 69 | for i, j, k := 0, 0, 0; i < len(xls) || j < len(yls); { 70 | if i < len(xls) && (k == len(s) || neq(xls[i], s[k])) { 71 | ts = append(ts, 72 | &Token{DelSymbol, fmt.Sprintf(xNum, i+1) + "-"}, 73 | &Token{Space, " "}, 74 | &Token{DelLine, xls[i].String()}, 75 | &Token{Newline, "\n"}) 76 | i++ 77 | } else if j < len(yls) && (k == len(s) || neq(yls[j], s[k])) { 78 | ts = append(ts, 79 | &Token{AddSymbol, fmt.Sprintf(yNum, j+1) + "+"}, 80 | &Token{Space, " "}, 81 | &Token{AddLine, yls[j].String()}, 82 | &Token{Newline, "\n"}) 83 | j++ 84 | } else { 85 | ts = append(ts, 86 | &Token{SameSymbol, fmt.Sprintf(sNum, i+1, j+1) + " "}, 87 | &Token{Space, " "}, 88 | &Token{SameLine, s[k].String() + "\n"}) 89 | i, j, k = i+1, j+1, k+1 90 | } 91 | } 92 | 93 | return ts 94 | } 95 | 96 | // TokenizeLine two different lines 97 | func TokenizeLine(ctx context.Context, x, y string) ([]*Token, []*Token) { 98 | split := lcs.Split 99 | val := ctx.Value(lcs.SplitKey) 100 | if val != nil { 101 | split = val.(func(string) []string) 102 | } 103 | 104 | xs := lcs.NewWords(split(x)) 105 | ys := lcs.NewWords(split(y)) 106 | 107 | // TODO: We should use index to check equality, remove the usage of xs.Sub 108 | s := xs.Sub(xs.YadLCS(ctx, ys)) 109 | 110 | xTokens := []*Token{} 111 | yTokens := []*Token{} 112 | 113 | merge := func(ts []*Token) []*Token { 114 | last := len(ts) - 1 115 | if last > 0 && ts[last].Type == ts[last-1].Type { 116 | ts[last-1].Literal += ts[last].Literal 117 | ts = ts[:last] 118 | } 119 | return ts 120 | } 121 | 122 | for i, j, k := 0, 0, 0; i < len(xs) || j < len(ys); { 123 | if i < len(xs) && (k == len(s) || neq(xs[i], s[k])) { 124 | xTokens = append(xTokens, &Token{DelWords, xs[i].String()}) 125 | i++ 126 | } else if j < len(ys) && (k == len(s) || neq(ys[j], s[k])) { 127 | yTokens = append(yTokens, &Token{AddWords, ys[j].String()}) 128 | j++ 129 | } else { 130 | xTokens = append(xTokens, &Token{SameWords, s[k].String()}) 131 | yTokens = append(yTokens, &Token{SameWords, s[k].String()}) 132 | i, j, k = i+1, j+1, k+1 133 | } 134 | 135 | xTokens = merge(xTokens) 136 | yTokens = merge(yTokens) 137 | } 138 | 139 | return xTokens, yTokens 140 | } 141 | 142 | func numFormat(x, y lcs.Sequence) (string, string, string) { 143 | xl := len(fmt.Sprintf("%d", len(x))) 144 | yl := len(fmt.Sprintf("%d", len(y))) 145 | 146 | return fmt.Sprintf("%%0%dd "+strings.Repeat(" ", yl+1), xl), 147 | fmt.Sprintf(strings.Repeat(" ", xl)+" %%0%dd ", yl), 148 | fmt.Sprintf("%%0%dd %%0%dd ", xl, yl) 149 | } 150 | 151 | func neq(x, y lcs.Comparable) bool { 152 | return x.String() != y.String() 153 | } 154 | -------------------------------------------------------------------------------- /lib/example/.golangci.yml: -------------------------------------------------------------------------------- 1 | 2 | run: 3 | skip-dirs-use-default: false 4 | 5 | linters: 6 | enable: 7 | - gofmt 8 | - revive 9 | - gocyclo 10 | - misspell 11 | - bodyclose 12 | 13 | gocyclo: 14 | min-complexity: 15 15 | 16 | issues: 17 | exclude-use-default: false 18 | 19 | -------------------------------------------------------------------------------- /lib/example/01_simple_assertion_test.go: -------------------------------------------------------------------------------- 1 | package example_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ysmood/got" 7 | "github.com/ysmood/got/lib/example" 8 | ) 9 | 10 | // Use got as an light weight assertion lib in standard Go test function. 11 | func TestAssertion(t *testing.T) { 12 | // Run "go doc got.Assertions" to list available assertion methods. 13 | got.T(t).Eq(example.Sum("1", "1"), "2") 14 | } 15 | -------------------------------------------------------------------------------- /lib/example/02_advanced_test.go: -------------------------------------------------------------------------------- 1 | package example_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/ysmood/got" 8 | "github.com/ysmood/got/lib/example" 9 | ) 10 | 11 | func TestChainMethods(t *testing.T) { 12 | g := setup(t) 13 | 14 | g.Desc("1 must equal 1").Must().Eq(example.Sum("1", "2"), "3") 15 | } 16 | 17 | func TestUtils(t *testing.T) { 18 | g := setup(t) 19 | 20 | // Run "go doc got.Utils" to list available helpers 21 | s := g.Serve() 22 | s.Mux.HandleFunc("/", example.ServeSum) 23 | 24 | val := g.Req("", s.URL("?a=1&b=2")).Bytes().String() 25 | g.Eq(val, "3") 26 | } 27 | 28 | func TestTableDriven(t *testing.T) { 29 | testCases := []struct{ desc, a, b, expected string }{{ 30 | "first", 31 | "1", "2", "3", 32 | }, { 33 | "second", 34 | "2", "3", "5", 35 | }} 36 | 37 | for _, c := range testCases { 38 | t.Run(c.desc, func(t *testing.T) { 39 | g := setup(t) 40 | g.Eq(example.Sum(c.a, c.b), c.expected) 41 | }) 42 | } 43 | } 44 | 45 | func TestSnapshot(t *testing.T) { 46 | g := setup(t) 47 | 48 | g.Snapshot("snapshot the map value", map[int]string{1: "1", 2: "2"}) 49 | } 50 | 51 | func TestWaitGroup(t *testing.T) { 52 | g := got.T(t) 53 | 54 | check := func() { 55 | time.Sleep(time.Millisecond * 30) 56 | 57 | g.Eq(1, 1) 58 | } 59 | 60 | // This check won't be executed because the test will end before the goroutine starts. 61 | go check() 62 | 63 | // This check will be executed because the test will wait for the goroutine to finish. 64 | g.Go(check) 65 | } 66 | -------------------------------------------------------------------------------- /lib/example/03_setup_test.go: -------------------------------------------------------------------------------- 1 | package example_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/ysmood/got" 8 | "github.com/ysmood/gotrace" 9 | ) 10 | 11 | func init() { 12 | // Set default timeout for the entire "go test" 13 | got.DefaultFlags("timeout=10s") 14 | } 15 | 16 | // G is your custom test context. 17 | type G struct { 18 | got.G 19 | 20 | // You can add your own fields, usually data you want init before each test. 21 | time string 22 | } 23 | 24 | // setup is a helper function to setup your test context G. 25 | var setup = func(t *testing.T) G { 26 | g := got.T(t) 27 | 28 | // The function passed to it will be surely executed after the test 29 | g.Cleanup(func() {}) 30 | 31 | // Concurrently run each test 32 | g.Parallel() 33 | 34 | // Make sure there's no goroutine leak for each test 35 | gotrace.CheckLeak(g, 0) 36 | 37 | // Timeout for each test 38 | g.PanicAfter(time.Second) 39 | 40 | return G{g, time.Now().Format(time.DateTime)} 41 | } 42 | 43 | func TestSetup(t *testing.T) { 44 | g := setup(t) 45 | 46 | // Here we use the custom field we have defined in G 47 | g.Gt(g.time, "2023-01-02 15:04:05") 48 | } 49 | -------------------------------------------------------------------------------- /lib/example/04_suite_test.go: -------------------------------------------------------------------------------- 1 | package example_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/ysmood/got" 8 | "github.com/ysmood/got/lib/example" 9 | ) 10 | 11 | func TestSuite(t *testing.T) { 12 | // Execute each exported methods of SumSuite. 13 | // Each exported methods on SumSuite is a test case. 14 | got.Each(t, SumSuite{}) 15 | } 16 | 17 | type SumSuite struct { 18 | got.G 19 | } 20 | 21 | func (g SumSuite) Sum() { 22 | g.Eq(example.Sum("1", "1"), "2") 23 | } 24 | 25 | func TestSumAdvancedSuite(t *testing.T) { 26 | // The got.Each can also accept a function to init the g for each test case. 27 | got.Each(t, func(t *testing.T) SumAdvancedSuite { 28 | g := got.New(t) 29 | 30 | // Concurrently run each test 31 | g.Parallel() 32 | 33 | // Timeout for each test 34 | g.PanicAfter(time.Second) 35 | 36 | return SumAdvancedSuite{g, "1", "2"} 37 | }) 38 | } 39 | 40 | type SumAdvancedSuite struct { 41 | got.G 42 | 43 | a, b string 44 | } 45 | 46 | func (g SumAdvancedSuite) Sum() { 47 | g.Eq(example.Sum(g.a, g.b), "3") 48 | } 49 | -------------------------------------------------------------------------------- /lib/example/05_mocking_test.go: -------------------------------------------------------------------------------- 1 | package example_test 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | "net/url" 7 | "testing" 8 | 9 | "github.com/ysmood/got/lib/example" 10 | "github.com/ysmood/got/lib/mock" 11 | ) 12 | 13 | // Mocking the http.ResponseWriter interface 14 | type mockResponseWriter struct { 15 | mock.Mock 16 | } 17 | 18 | func (m *mockResponseWriter) Write(b []byte) (int, error) { 19 | // Proxy the input and output of mockResponseWriter.Write method 20 | return mock.Proxy(m, m.Write)(b) 21 | } 22 | 23 | func (m *mockResponseWriter) Header() http.Header { 24 | return mock.Proxy(m, m.Header)() 25 | } 26 | 27 | func (m *mockResponseWriter) WriteHeader(code int) { 28 | mock.Proxy(m, m.WriteHeader)(code) 29 | } 30 | 31 | func TestMocking(t *testing.T) { 32 | g := setup(t) 33 | 34 | m := &mockResponseWriter{} 35 | 36 | // Stub the mockResponseWriter.Write method with ours 37 | mock.Stub(m, m.Write, func(b []byte) (int, error) { 38 | // Here want to ensure the input is "3" 39 | g.Eq(string(b), "3") 40 | return 0, nil 41 | }) 42 | 43 | u, _ := url.Parse("?a=1&b=2") 44 | example.ServeSum(m, &http.Request{URL: u}) 45 | 46 | // When the input is "3" let the mockResponseWriter.Write return (1, nil) 47 | mock.On(m, m.Write).When([]byte("3")).Return(1, nil) 48 | 49 | example.ServeSum(m, &http.Request{URL: u}) 50 | 51 | // We can use the Calls helper to get all the input and output history of a method. 52 | // Check the lib/example/.got/snapshots/TestMocking/calls.gop file for the details. 53 | g.Snapshot("calls", m.Calls(m.Write)) 54 | g.Desc("the Write should be called twice").Len(m.Calls(m.Write), 2) 55 | } 56 | 57 | // mock the rand.Source 58 | type mockSource struct { 59 | mock.Mock 60 | } 61 | 62 | func (m *mockSource) Int63() int64 { 63 | return mock.Proxy(m, m.Int63)() 64 | } 65 | 66 | func (m *mockSource) Seed(seed int64) { 67 | mock.Proxy(m, m.Seed)(seed) 68 | } 69 | 70 | func TestFallback(t *testing.T) { 71 | g := setup(t) 72 | 73 | m := &mockSource{} 74 | 75 | // Sometimes if there are a lot of method, and you want to stub only one of them, 76 | // you can use the Fallback method to fallback all non-stubbed methods to the struct you have passed to it. 77 | m.Fallback(rand.NewSource(1)) 78 | 79 | // Here the Rand method will always return the same value. 80 | g.Eq(example.Rand(m), "5577006791947779410") 81 | } 82 | -------------------------------------------------------------------------------- /lib/example/06_customize_assertion_output_test.go: -------------------------------------------------------------------------------- 1 | package example_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/ysmood/gop" 8 | "github.com/ysmood/got" 9 | "github.com/ysmood/got/lib/diff" 10 | ) 11 | 12 | // An example to only override the default error output of got.Assertions.Eq 13 | func TestCustomizeAssertionOutput(t *testing.T) { 14 | g := got.New(t) 15 | 16 | dh := got.NewDefaultAssertionError(gop.ThemeDefault, diff.ThemeDefault) 17 | h := got.AssertionErrorReport(func(c *got.AssertionCtx) string { 18 | if c.Type == got.AssertionEq { 19 | return fmt.Sprintf("%v != %v", c.Details[0], c.Details[1]) 20 | } 21 | return dh.Report(c) 22 | }) 23 | g.Assertions.ErrorHandler = h 24 | 25 | g.Eq(1, 1) 26 | } 27 | -------------------------------------------------------------------------------- /lib/example/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This is an example project to demonstrate how to use `got` for testing. 4 | 5 | Run `go test` for testing. 6 | 7 | For CI setup example check the [Example Github Action](../../.github/workflows/example.yml). 8 | -------------------------------------------------------------------------------- /lib/example/example.go: -------------------------------------------------------------------------------- 1 | // Package example ... 2 | package example 3 | 4 | import ( 5 | "fmt" 6 | "math/rand" 7 | "net/http" 8 | "strconv" 9 | ) 10 | 11 | // Sum a and b as number 12 | func Sum(a, b string) string { 13 | an, _ := strconv.ParseInt(a, 10, 32) 14 | bn, _ := strconv.ParseInt(b, 10, 32) 15 | return fmt.Sprintf("%d", an+bn) 16 | } 17 | 18 | // ServeSum http handler function 19 | func ServeSum(w http.ResponseWriter, r *http.Request) { 20 | s := Sum(r.URL.Query().Get("a"), r.URL.Query().Get("b")) 21 | _, _ = w.Write([]byte(s)) 22 | } 23 | 24 | // Rand generate a random int as string 25 | func Rand(src rand.Source) string { 26 | return fmt.Sprintf("%d", rand.New(src).Int()) 27 | } 28 | -------------------------------------------------------------------------------- /lib/example/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ysmood/got/lib/example 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/ysmood/gop v0.2.0 7 | github.com/ysmood/gotrace v0.6.0 8 | ) 9 | -------------------------------------------------------------------------------- /lib/example/go.sum: -------------------------------------------------------------------------------- 1 | github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= 2 | github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= 3 | github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= 4 | -------------------------------------------------------------------------------- /lib/got-vscode-extension/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /lib/got-vscode-extension/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /lib/got-vscode-extension/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /lib/got-vscode-extension/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/test/**/*.js" 30 | ], 31 | "preLaunchTask": "${defaultBuildTask}" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /lib/got-vscode-extension/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /lib/got-vscode-extension/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /lib/got-vscode-extension/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | vsc-extension-quickstart.md 9 | **/tsconfig.json 10 | **/.eslintrc.json 11 | **/*.map 12 | **/*.ts 13 | -------------------------------------------------------------------------------- /lib/got-vscode-extension/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2020 Yad Smood 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/got-vscode-extension/README.md: -------------------------------------------------------------------------------- 1 | # got-vscode-extension README 2 | 3 | ## Features 4 | 5 | - Some useful snippets 6 | -------------------------------------------------------------------------------- /lib/got-vscode-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "got-vscode-extension", 3 | "displayName": "got-vscode-extension", 4 | "description": "got vscode extension", 5 | "version": "0.1.4", 6 | "publisher": "ysmood", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/ysmood/got.git" 10 | }, 11 | "engines": { 12 | "vscode": "^1.65.2" 13 | }, 14 | "categories": [ 15 | "Snippets" 16 | ], 17 | "activationEvents": [ 18 | "onLanguage:go" 19 | ], 20 | "main": "./out/extension.js", 21 | "contributes": { 22 | "snippets": [ 23 | { 24 | "language": "go", 25 | "path": "./snippets.json" 26 | } 27 | ], 28 | "commands": [ 29 | { 30 | "command": "got-vscode-extension.testCurrent", 31 | "title": "got: test current focused case" 32 | } 33 | ] 34 | }, 35 | "scripts": { 36 | "vscode:prepublish": "npm run compile", 37 | "compile": "tsc -p ./", 38 | "lint": "eslint . --ext .ts,.tsx", 39 | "watch": "tsc -watch -p ./", 40 | "test": "node ./out/test/runTest.js" 41 | }, 42 | "devDependencies": { 43 | "@types/node": "^16.0.0", 44 | "@types/vscode": "^1.65.0", 45 | "@vscode/test-electron": "^2.1.3", 46 | "@typescript-eslint/eslint-plugin": "^4.16.0", 47 | "@typescript-eslint/parser": "^4.16.0", 48 | "eslint": "^7.21.0", 49 | "typescript": "^4.6.2" 50 | } 51 | } -------------------------------------------------------------------------------- /lib/got-vscode-extension/snippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "gop print": { 3 | "prefix": "gp", 4 | "body": [ 5 | "gop.P($0)" 6 | ] 7 | }, 8 | "got setup": { 9 | "prefix": "gsetup", 10 | "body": [ 11 | "package ${0:example}_test", 12 | "", 13 | "import (", 14 | "\t\"time\"", 15 | "", 16 | "\t\"github.com/ysmood/got\"", 17 | "\t\"github.com/ysmood/gotrace\"", 18 | ")", 19 | "", 20 | "func init() {", 21 | "\t// Set default timeout for the entire \"go test\"", 22 | "\tgot.DefaultFlags(\"timeout=10s\")", 23 | "}", 24 | "", 25 | "var setup = got.Setup(func(g got.G) {", 26 | "\t// The function passed to it will be surely executed after the test", 27 | "\tg.Cleanup(func() {})", 28 | "", 29 | "\t// Concurrently run each test", 30 | "\tg.Parallel()", 31 | "", 32 | "\t// Make sure there's no goroutine leak for each test", 33 | "\tgotrace.CheckLeak(g, 0)", 34 | "", 35 | "\t// Timeout for each test", 36 | "\tg.PanicAfter(time.Second)", 37 | "})", 38 | "" 39 | ] 40 | }, 41 | "got test function": { 42 | "prefix": "gt", 43 | "body": [ 44 | "", 45 | "func Test$1(t *testing.T) {", 46 | "\tg := got.T(t)", 47 | "", 48 | "\t${0:g.Eq(1, 1)}", 49 | "}", 50 | "" 51 | ] 52 | }, 53 | "got test function with setup": { 54 | "prefix": "gts", 55 | "body": [ 56 | "", 57 | "func Test$1(t *testing.T) {", 58 | "\tg := setup(t)", 59 | "", 60 | "\t${0:g.Eq(1, 1)}", 61 | "}", 62 | "" 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/got-vscode-extension/snippets/main.go: -------------------------------------------------------------------------------- 1 | // Package main ... 2 | package main 3 | 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | "log" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | type snippet struct { 13 | Prefix string `json:"prefix,omitempty"` 14 | Body []string `json:"body,omitempty"` 15 | Description string `json:"description,omitempty"` 16 | } 17 | 18 | type snippets map[string]snippet 19 | 20 | func main() { 21 | b, err := os.ReadFile("lib/example/03_setup_test.go") 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | 26 | setup := strings.Replace(string(b), "example_test", "${0:example}_test", -1) 27 | 28 | s := snippets{ 29 | "gop print": { 30 | Prefix: "gp", 31 | Body: []string{"gop.P($0)"}, 32 | }, 33 | "got test function": { 34 | Prefix: "gt", 35 | Body: strings.Split(` 36 | func Test$1(t *testing.T) { 37 | g := got.T(t) 38 | 39 | ${0:g.Eq(1, 1)} 40 | } 41 | `, "\n"), 42 | }, 43 | "got test function with setup": { 44 | Prefix: "gts", 45 | Body: strings.Split(` 46 | func Test$1(t *testing.T) { 47 | g := setup(t) 48 | 49 | ${0:g.Eq(1, 1)} 50 | } 51 | `, "\n"), 52 | }, 53 | "got setup": { 54 | Prefix: "gsetup", 55 | Body: strings.Split(string(setup), "\n"), 56 | }, 57 | } 58 | 59 | buf := bytes.NewBuffer(nil) 60 | enc := json.NewEncoder(buf) 61 | enc.SetEscapeHTML(false) 62 | enc.SetIndent("", " ") 63 | err = enc.Encode(s) 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | 68 | err = os.WriteFile("lib/got-vscode-extension/snippets.json", buf.Bytes(), 0764) 69 | if err != nil { 70 | log.Fatal(err) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/got-vscode-extension/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export function activate(context: vscode.ExtensionContext) { 4 | let disposable = vscode.commands.registerCommand('got-vscode-extension.testCurrent', () => { 5 | vscode.window.showInformationMessage('todo'); 6 | }); 7 | 8 | context.subscriptions.push(disposable); 9 | 10 | console.log('got-vscode-extension loaded'); 11 | } 12 | 13 | export function deactivate() {} 14 | -------------------------------------------------------------------------------- /lib/got-vscode-extension/src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to the extension test runner script 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath, launchArgs: ['../../'] }); 17 | } catch (err) { 18 | console.error(err); 19 | console.error('Failed to run tests'); 20 | process.exit(1); 21 | } 22 | } 23 | 24 | main(); -------------------------------------------------------------------------------- /lib/got-vscode-extension/src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export function run(): Promise { 3 | return new Promise(() => {}); 4 | } -------------------------------------------------------------------------------- /lib/got-vscode-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | ".vscode-test" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /lib/got-vscode-extension/vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Get up and running straight away 13 | 14 | * Press `F5` to open a new window with your extension loaded. 15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 16 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 17 | * Find output from your extension in the debug console. 18 | 19 | ## Make changes 20 | 21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | 25 | ## Explore the API 26 | 27 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 28 | 29 | ## Run tests 30 | 31 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 32 | * Press `F5` to run the tests in a new window with your extension loaded. 33 | * See the output of the test result in the debug console. 34 | * Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. 35 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 36 | * You can create folders inside the `test` folder to structure your tests any way you want. 37 | 38 | ## Go further 39 | 40 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 41 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VSCode extension marketplace. 42 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 43 | -------------------------------------------------------------------------------- /lib/lcs/lcs.go: -------------------------------------------------------------------------------- 1 | // Package lcs ... 2 | package lcs 3 | 4 | import ( 5 | "context" 6 | ) 7 | 8 | // Indices is the index list of items in xs that forms the LCS between xs and ys. 9 | type Indices []int 10 | 11 | // YadLCS returns the x index of each Comparable that are in the YadLCS between x and y. 12 | // The complexity is O(M * log(L)), M is the number of char matches between x and y, L is the length of LCS. 13 | // The worst memory complexity is O(M), but usually it's much less. 14 | // 15 | // The advantage of this algorithm is it's easy to understand and implement. It converts the LCS 16 | // problem into problems that are familiar to us, such as LIS, binary-search, object-recycle, etc., which give us 17 | // more room to do the optimization for each streamline. 18 | func (xs Sequence) YadLCS(ctx context.Context, ys Sequence) Indices { 19 | o := xs.Occurrence(ys) 20 | r := result{list: make([]*node, 0, min(len(xs), len(ys)))} 21 | rest := len(ys) 22 | 23 | for _, xi := range o { 24 | if ctx.Err() != nil { 25 | break 26 | } 27 | 28 | from := len(r.list) 29 | for _, i := range xi { 30 | from = r.add(from, i, rest) 31 | } 32 | 33 | rest-- 34 | } 35 | 36 | return r.lcs() 37 | } 38 | 39 | type node struct { 40 | x int 41 | p *node 42 | 43 | c int // pointer count for node recycle 44 | } 45 | 46 | func (n *node) link(x int, m *node) { 47 | if m != nil { 48 | m.c++ 49 | } 50 | 51 | n.p = m 52 | n.x = x 53 | } 54 | 55 | type result struct { 56 | list []*node 57 | 58 | // reuse node to reduce memory allocation 59 | recycle []*node 60 | } 61 | 62 | func (r *result) new(x int, n *node) *node { 63 | var m *node 64 | 65 | // reuse node if possible 66 | l := len(r.recycle) 67 | if l > 0 { 68 | m = r.recycle[l-1] 69 | r.recycle = r.recycle[:l-1] 70 | } else { 71 | m = &node{} 72 | } 73 | 74 | m.link(x, n) 75 | 76 | return m 77 | } 78 | 79 | func (r *result) replace(i, x int, n *node) { 80 | // recycle nodes 81 | if m := r.list[i]; m.c == 0 { 82 | for p := m.p; p != nil && p != n; p = p.p { 83 | p.c-- 84 | if p.c == 0 { 85 | r.recycle = append(r.recycle, p) 86 | } else { 87 | break 88 | } 89 | } 90 | 91 | m.link(x, n) 92 | return 93 | } 94 | 95 | r.list[i] = r.new(x, n) 96 | } 97 | 98 | func (r *result) add(from, x, rest int) int { 99 | l := len(r.list) 100 | 101 | next, n := r.find(from, x) 102 | if n != nil { 103 | if l-next < rest { // only when we have enough rest xs 104 | if next == l { 105 | r.list = append(r.list, r.new(x, n)) 106 | } else if x < r.list[next].x { 107 | r.replace(next, x, n) 108 | } 109 | return next 110 | } 111 | } 112 | 113 | if l == 0 { 114 | r.list = append(r.list, r.new(x, n)) 115 | return 1 116 | } 117 | 118 | if l-1 < rest && x < r.list[0].x { 119 | r.replace(0, x, nil) 120 | } 121 | 122 | return 0 123 | } 124 | 125 | // binary search to find the largest r.list[i].x that is smaller than x 126 | func (r *result) find(from, x int) (int, *node) { 127 | var found *node 128 | for i, j := 0, from; i < j; { 129 | h := (i + j) >> 1 130 | n := r.list[h] 131 | if n.x < x { 132 | from = h 133 | found = n 134 | i = h + 1 135 | } else { 136 | j = h 137 | } 138 | } 139 | return from + 1, found 140 | } 141 | 142 | func (r *result) lcs() Indices { 143 | l := len(r.list) 144 | 145 | idx := make(Indices, l) 146 | 147 | if l == 0 { 148 | return idx 149 | } 150 | 151 | for p := r.list[l-1]; p != nil; p = p.p { 152 | l-- 153 | idx[l] = p.x 154 | } 155 | 156 | return idx 157 | } 158 | -------------------------------------------------------------------------------- /lib/lcs/lcs_test.go: -------------------------------------------------------------------------------- 1 | package lcs_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "runtime/debug" 8 | "testing" 9 | "time" 10 | 11 | "github.com/ysmood/got" 12 | "github.com/ysmood/got/lib/lcs" 13 | ) 14 | 15 | var setup = got.Setup(func(g got.G) { 16 | g.ErrorHandler = got.NewDefaultAssertionError(nil, nil) 17 | }) 18 | 19 | func TestLCS(t *testing.T) { 20 | g := got.T(t) 21 | 22 | check := func(i int, x, y string) { 23 | t.Helper() 24 | 25 | e := func(msg ...interface{}) { 26 | t.Helper() 27 | t.Log(i, x, y) 28 | t.Error(msg...) 29 | t.FailNow() 30 | } 31 | 32 | defer func() { 33 | err := recover() 34 | if err != nil { 35 | debug.PrintStack() 36 | e(err) 37 | } 38 | }() 39 | 40 | xs, ys := lcs.NewChars(x), lcs.NewChars(y) 41 | 42 | s := xs.Sub(xs.YadLCS(context.Background(), ys)) 43 | out := s.String() 44 | expected := lcs.StandardLCS(xs, ys).String() 45 | 46 | if !s.IsSubsequenceOf(xs) { 47 | e(s.String(), "is not subsequence of", x) 48 | } 49 | 50 | if !s.IsSubsequenceOf(ys) { 51 | e(s.String(), "is not subsequence of", y) 52 | } 53 | 54 | if len(out) != len(expected) { 55 | e("length of", out, "doesn't equal length of", expected) 56 | } 57 | } 58 | 59 | randStr := func() string { 60 | const c = 8 61 | b := make([]byte, c) 62 | for i := 0; i < c; i++ { 63 | b[i] = byte('a' + g.RandInt(0, c)) 64 | } 65 | return string(b) 66 | } 67 | 68 | check(0, "", "") 69 | check(0, "", "a") 70 | 71 | for i := 1; i < 1000; i++ { 72 | check(i, randStr(), randStr()) 73 | } 74 | } 75 | 76 | func TestLCSLongContentSmallChange(t *testing.T) { 77 | eq := func(x, y, expected string) { 78 | t.Helper() 79 | 80 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 81 | defer cancel() 82 | xs, ys := lcs.NewChars(x), lcs.NewChars(y) 83 | lcs := xs.YadLCS(ctx, ys) 84 | out := xs.Sub(lcs).String() 85 | 86 | if out != expected { 87 | t.Error(out, "!=", expected) 88 | } 89 | } 90 | 91 | x := bytes.Repeat([]byte("x"), 100000) 92 | y := bytes.Repeat([]byte("y"), 100000) 93 | eq(string(x), string(y), "") 94 | 95 | x[len(x)/2] = byte('a') 96 | y[len(y)/2] = byte('a') 97 | eq(string(x), string(y), "a") 98 | 99 | x[len(x)/2] = byte('y') 100 | y[len(y)/2] = byte('x') 101 | eq(string(x), string(y), "xy") 102 | } 103 | 104 | func TestContext(t *testing.T) { 105 | g := got.T(t) 106 | 107 | c := g.Context() 108 | c.Cancel() 109 | l := lcs.NewChars("abc").YadLCS(c, lcs.NewChars("abc")) 110 | g.Len(l, 0) 111 | } 112 | 113 | func TestLongRandom(_ *testing.T) { 114 | size := 10000 115 | x := randStr(size) 116 | y := randStr(size) 117 | 118 | c := context.Background() 119 | 120 | xs := lcs.NewChars(x) 121 | ys := lcs.NewChars(y) 122 | xs.YadLCS(c, ys) 123 | } 124 | 125 | func randStr(n int) string { 126 | b := make([]byte, n) 127 | _, _ = rand.Read(b) 128 | return string(b) 129 | } 130 | -------------------------------------------------------------------------------- /lib/lcs/sequence.go: -------------------------------------------------------------------------------- 1 | package lcs 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "regexp" 7 | ) 8 | 9 | // Sequence list 10 | type Sequence []Comparable 11 | 12 | // Sub from p, it will automatically decompress the compressed p. 13 | func (xs Sequence) Sub(idx Indices) Sequence { 14 | s := make(Sequence, len(idx)) 15 | for i, ix := range idx { 16 | s[i] = xs[ix] 17 | } 18 | return s 19 | } 20 | 21 | // IsSubsequenceOf returns true if x is a subsequence of y 22 | func (xs Sequence) IsSubsequenceOf(ys Sequence) bool { 23 | for i, j := 0, 0; i < len(xs); i++ { 24 | for { 25 | if j >= len(ys) { 26 | return false 27 | } 28 | if eq(xs[i], ys[j]) { 29 | j++ 30 | break 31 | } 32 | j++ 33 | } 34 | } 35 | 36 | return true 37 | } 38 | 39 | // Histogram of each Comparable 40 | func (xs Sequence) Histogram() map[string][]int { 41 | h := map[string][]int{} 42 | for i := len(xs) - 1; i >= 0; i-- { 43 | s := xs[i].String() 44 | h[s] = append(h[s], i) 45 | } 46 | return h 47 | } 48 | 49 | // Occurrence returns the position of each element of y in x. 50 | func (xs Sequence) Occurrence(y Sequence) [][]int { 51 | o := make([][]int, len(y)) 52 | h := xs.Histogram() 53 | 54 | for i, c := range y { 55 | if indexes, has := h[c.String()]; has { 56 | o[i] = indexes 57 | } 58 | } 59 | 60 | return o 61 | } 62 | 63 | // Comparable interface 64 | type Comparable interface { 65 | // String for comparison, such as the hash 66 | String() string 67 | } 68 | 69 | // Element of a line, a word, or a character 70 | type Element string 71 | 72 | // String returns the full content 73 | func (e Element) String() string { 74 | return string(e) 75 | } 76 | 77 | // NewChars from string 78 | func NewChars(s string) Sequence { 79 | cs := Sequence{} 80 | for _, r := range s { 81 | cs = append(cs, Element(r)) 82 | } 83 | return cs 84 | } 85 | 86 | // NewWords from string list 87 | func NewWords(words []string) Sequence { 88 | cs := make(Sequence, len(words)) 89 | for i, word := range words { 90 | cs[i] = Element(word) 91 | } 92 | return cs 93 | } 94 | 95 | // NewLines from string. It will split the s via newlines. 96 | func NewLines(s string) Sequence { 97 | sc := bufio.NewScanner(bytes.NewBufferString(s)) 98 | cs := Sequence{} 99 | for i := 0; sc.Scan(); i++ { 100 | cs = append(cs, Element(sc.Text())) 101 | } 102 | 103 | if len(s) > 0 && s[len(s)-1] == '\n' { 104 | cs = append(cs, Element("")) 105 | } 106 | 107 | return cs 108 | } 109 | 110 | // RegWord to match a word 111 | var regWord = regexp.MustCompile(`(?s)` + // enable . to match newline 112 | `[[:alpha:]]{1,12}` + // match alphabets, length limit is 12 113 | `|[[:digit:]]{1,3}` + // match digits, length limit is 3 114 | `|.` + // match others as single-char words 115 | ``) 116 | 117 | // RegRune to match a rune 118 | var regRune = regexp.MustCompile(`(?s).`) 119 | 120 | type contextSplitKey struct{} 121 | 122 | // SplitKey for context 123 | var SplitKey = contextSplitKey{} 124 | 125 | // Split a line into words 126 | func Split(s string) []string { 127 | var reg *regexp.Regexp 128 | if len(s) <= 100 { 129 | reg = regRune 130 | } else { 131 | reg = regWord 132 | } 133 | 134 | return reg.FindAllString(s, -1) 135 | } 136 | -------------------------------------------------------------------------------- /lib/lcs/sequence_test.go: -------------------------------------------------------------------------------- 1 | package lcs_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/ysmood/got" 8 | "github.com/ysmood/got/lib/lcs" 9 | ) 10 | 11 | func TestSplit(t *testing.T) { 12 | g := got.T(t) 13 | 14 | g.Len(lcs.Split(""), 0) 15 | 16 | check := func(in string, expect ...string) { 17 | g.Helper() 18 | out := lcs.Split(strings.Repeat("\t", 100) + in)[100:] 19 | g.Eq(out, expect) 20 | } 21 | 22 | check("find a place to eat 热干面", 23 | "find", " ", "a", " ", "place", " ", "to", " ", "eat", " ", "热", "干", "面") 24 | 25 | check(" as.Equal(arr, arr) test", 26 | " ", "as", ".", "Equal", "(", "arr", ",", " ", "arr", ")", " ", "test") 27 | 28 | check("English-Words紧接着中文", 29 | "English", "-", "Words", "紧", "接", "着", "中", "文") 30 | 31 | check("123456test12345", 32 | "123", "456", "test", "123", "45") 33 | 34 | check("WordVeryVeryVeryVeryVeryVeryVerylong", 35 | "WordVeryVery", "VeryVeryVery", "VeryVerylong") 36 | } 37 | 38 | func TestIsSubsequenceOf(t *testing.T) { 39 | g := got.T(t) 40 | 41 | y := lcs.NewChars("abc") 42 | 43 | g.True(lcs.NewChars("ab").IsSubsequenceOf(y)) 44 | g.True(lcs.NewChars("ac").IsSubsequenceOf(y)) 45 | g.True(lcs.NewChars("bc").IsSubsequenceOf(y)) 46 | g.False(lcs.NewChars("cb").IsSubsequenceOf(y)) 47 | g.False(lcs.NewChars("ba").IsSubsequenceOf(y)) 48 | g.False(lcs.NewChars("ca").IsSubsequenceOf(y)) 49 | } 50 | 51 | func TestNew(t *testing.T) { 52 | g := setup(t) 53 | g.Len(lcs.NewLines("a"), 1) 54 | g.Len(lcs.NewLines("a\n"), 2) 55 | g.Len(lcs.NewLines("a\n\n"), 3) 56 | g.Len(lcs.NewLines("\na"), 2) 57 | g.Eq(lcs.NewLines("\nabc\nabc").String(), "\nabc\nabc") 58 | 59 | g.Len(lcs.NewWords([]string{"a", "b"}), 2) 60 | } 61 | -------------------------------------------------------------------------------- /lib/lcs/utils.go: -------------------------------------------------------------------------------- 1 | package lcs 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func min(x, y int) int { 8 | if x < y { 9 | return x 10 | } 11 | return y 12 | } 13 | 14 | func eq(x, y Comparable) bool { 15 | return x.String() == y.String() 16 | } 17 | 18 | // String interface 19 | func (xs Sequence) String() string { 20 | if len(xs) == 0 { 21 | return "" 22 | } 23 | 24 | l := 0 25 | for _, el := range xs { 26 | l += len(el.String()) 27 | } 28 | if l == len(xs) { 29 | out := "" 30 | for _, c := range xs { 31 | out += c.String() 32 | } 33 | return out 34 | } 35 | 36 | out := []string{} 37 | for _, c := range xs { 38 | out = append(out, c.String()) 39 | } 40 | return strings.Join(out, "\n") 41 | } 42 | 43 | // StandardLCS implementation for testing purpose only, because it's very inefficient. 44 | // https://en.wikipedia.org/wiki/Longest_common_subsequence_problem#LCS_function_defined. 45 | func StandardLCS(xs, ys Sequence) Sequence { 46 | last := func(s Sequence) Comparable { 47 | return s[len(s)-1] 48 | } 49 | noLast := func(s Sequence) Sequence { 50 | return s[:len(s)-1] 51 | } 52 | 53 | if len(xs)*len(ys) == 0 { 54 | return Sequence{} 55 | } else if last(xs).String() == last(ys).String() { 56 | return append(StandardLCS(noLast(xs), noLast(ys)), last(xs)) 57 | } 58 | 59 | left, right := StandardLCS(xs, noLast(ys)), StandardLCS(noLast(xs), ys) 60 | if len(left) > len(right) { 61 | return left 62 | } 63 | return right 64 | } 65 | -------------------------------------------------------------------------------- /lib/mock/mock.go: -------------------------------------------------------------------------------- 1 | // Package mock provides a simple way to stub struct methods. 2 | package mock 3 | 4 | import ( 5 | "reflect" 6 | "regexp" 7 | "runtime" 8 | "strings" 9 | "sync" 10 | ) 11 | 12 | // Fallbackable interface 13 | type Fallbackable interface { 14 | Fallback(any) 15 | } 16 | 17 | // Mock helper for interface stubbing 18 | type Mock struct { 19 | lock sync.Mutex 20 | 21 | fallback reflect.Value 22 | 23 | stubs map[string]interface{} 24 | 25 | calls map[string][]Call 26 | } 27 | 28 | // Fallback the methods that are not stubbed to fb. 29 | func (m *Mock) Fallback(fb interface{}) { 30 | m.lock.Lock() 31 | defer m.lock.Unlock() 32 | 33 | m.fallback = reflect.ValueOf(fb) 34 | } 35 | 36 | // Stop the stub 37 | func (m *Mock) Stop(method any) { 38 | panicIfNotFunc(method) 39 | 40 | m.lock.Lock() 41 | defer m.lock.Unlock() 42 | 43 | delete(m.stubs, fnName(method)) 44 | } 45 | 46 | // Proxy the input and output of method on mock for later stub. 47 | func Proxy[M any](mock Fallbackable, method M) M { 48 | panicIfNotFunc(method) 49 | 50 | m := toMock(mock) 51 | 52 | m.lock.Lock() 53 | defer m.lock.Unlock() 54 | 55 | name := fnName(method) 56 | 57 | if fn, has := m.stubs[name]; has { 58 | return fn.(M) 59 | } 60 | 61 | if !m.fallback.IsValid() { 62 | panic("you should specify the mock.Mock.Fallback") 63 | } 64 | 65 | methodVal := m.fallback.MethodByName(name) 66 | if !methodVal.IsValid() { 67 | panic(m.fallback.Type().String() + " doesn't have method: " + name) 68 | } 69 | 70 | return m.spy(name, m.fallback.MethodByName(name).Interface()).(M) 71 | } 72 | 73 | func toMock(mock Fallbackable) *Mock { 74 | if m, ok := mock.(*Mock); ok { 75 | return m 76 | } 77 | 78 | return reflect.Indirect(reflect.ValueOf(mock)).FieldByName("Mock").Addr().Interface().(*Mock) 79 | } 80 | 81 | func fnName(fn interface{}) string { 82 | fv := reflect.ValueOf(fn) 83 | 84 | fi := runtime.FuncForPC(fv.Pointer()) 85 | 86 | name := regexp.MustCompile(`^.+\.`).ReplaceAllString(fi.Name(), "") 87 | 88 | // remove the "-fm" suffix for struct methods 89 | name = strings.TrimSuffix(name, "-fm") 90 | 91 | return name 92 | } 93 | 94 | func panicIfNotFunc(fn any) { 95 | if reflect.TypeOf(fn).Kind() != reflect.Func { 96 | panic("the input should be a function") 97 | } 98 | } 99 | 100 | func toReturnValues(t reflect.Type, res []reflect.Value) []reflect.Value { 101 | out := []reflect.Value{} 102 | for i := 0; i < t.NumOut(); i++ { 103 | v := reflect.New(t.Out(i)).Elem() 104 | if res[i].IsValid() { 105 | v.Set(res[i]) 106 | } 107 | out = append(out, v) 108 | } 109 | 110 | return out 111 | } 112 | -------------------------------------------------------------------------------- /lib/mock/mock_test.go: -------------------------------------------------------------------------------- 1 | package mock_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/ysmood/got" 8 | "github.com/ysmood/got/lib/mock" 9 | ) 10 | 11 | type mockBuffer struct { 12 | mock.Mock 13 | } 14 | 15 | func (t *mockBuffer) Write(p []byte) (n int, err error) { 16 | return mock.Proxy(t, t.Write)(p) 17 | } 18 | 19 | func (t *mockBuffer) Len() int { 20 | return mock.Proxy(t, t.Len)() 21 | } 22 | 23 | func (t *mockBuffer) NonExists() int { 24 | return mock.Proxy(t, t.NonExists)() 25 | } 26 | 27 | func TestMock(t *testing.T) { 28 | g := got.T(t) 29 | 30 | b := bytes.NewBuffer(nil) 31 | 32 | m := &mockBuffer{} 33 | m.Fallback(b) 34 | mock.Stub(m, m.Write, func(p []byte) (int, error) { 35 | return b.Write(append(p, []byte(" ")...)) 36 | }) 37 | n, err := m.Write([]byte("test")) 38 | g.Nil(err) 39 | g.Eq(n, 6) 40 | 41 | g.Eq(m.Len(), 6) 42 | 43 | val := g.Panic(func() { 44 | m := mockBuffer{} 45 | m.Len() 46 | }) 47 | g.Eq(val, "you should specify the mock.Mock.Fallback") 48 | 49 | val = g.Panic(func() { 50 | m := mockBuffer{} 51 | m.Fallback(b) 52 | m.NonExists() 53 | }) 54 | g.Eq(val, `*bytes.Buffer doesn't have method: NonExists`) 55 | 56 | g.Eq(g.Panic(func() { 57 | m.Stop("") 58 | }), "the input should be a function") 59 | } 60 | 61 | func TestMockUtils(t *testing.T) { 62 | g := got.T(t) 63 | 64 | b := bytes.NewBuffer(nil) 65 | 66 | m := &mockBuffer{} 67 | m.Fallback(b) 68 | 69 | { 70 | when := mock.On(m, m.Write).When([]byte{}) 71 | when.Return(2, nil).Times(2) 72 | 73 | n, err := m.Write([]byte{}) 74 | g.Nil(err) 75 | g.Eq(n, 2) 76 | 77 | n, err = m.Write([]byte{}) 78 | g.Nil(err) 79 | g.Eq(n, 2) 80 | 81 | n, err = m.Write([]byte{}) 82 | g.Nil(err) 83 | g.Eq(n, 0) 84 | 85 | g.Eq(when.Count(), 2) 86 | g.Len(m.Calls(m.Write), 3) 87 | g.Snapshot("calls", m.Calls(m.Write)) 88 | } 89 | 90 | { 91 | mock.On(m, m.Write).When(mock.Any).Return(2, nil) 92 | n, err := m.Write([]byte{}) 93 | g.Nil(err) 94 | g.Eq(n, 2) 95 | } 96 | 97 | { 98 | mock.On(m, m.Write).When([]byte{}).Return(2, nil).Once() 99 | 100 | n, err := m.Write([]byte{}) 101 | g.Nil(err) 102 | g.Eq(n, 2) 103 | 104 | n, err = m.Write([]byte{}) 105 | g.Nil(err) 106 | g.Eq(n, 0) 107 | } 108 | 109 | { 110 | mock.On(m, m.Write).When(true).Return(2, nil) 111 | v := g.Panic(func() { 112 | _, _ = m.Write(nil) 113 | }) 114 | g.Eq(v, "No mock.StubOn.When matches: []interface {}{[]uint8(nil)}") 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/mock/spy.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import "reflect" 4 | 5 | // Call record the input and output of a method call 6 | type Call struct { 7 | Input []any 8 | Return []any 9 | } 10 | 11 | // Calls returns all the calls of method 12 | func (m *Mock) Calls(method any) []Call { 13 | panicIfNotFunc(method) 14 | 15 | m.lock.Lock() 16 | defer m.lock.Unlock() 17 | 18 | return m.calls[fnName(method)] 19 | } 20 | 21 | // Record all the input and output of a method 22 | func (m *Mock) spy(name string, fn any) any { 23 | v := reflect.ValueOf(fn) 24 | t := v.Type() 25 | 26 | if m.calls == nil { 27 | m.calls = map[string][]Call{} 28 | } 29 | 30 | return reflect.MakeFunc(t, func(args []reflect.Value) []reflect.Value { 31 | ret := v.Call(args) 32 | 33 | m.lock.Lock() 34 | m.calls[name] = append(m.calls[name], Call{ 35 | valToInterface(args), 36 | valToInterface(ret), 37 | }) 38 | m.lock.Unlock() 39 | 40 | return ret 41 | }).Interface() 42 | } 43 | 44 | func valToInterface(list []reflect.Value) []any { 45 | ret := make([]any, len(list)) 46 | for i, v := range list { 47 | ret[i] = v.Interface() 48 | } 49 | return ret 50 | } 51 | -------------------------------------------------------------------------------- /lib/mock/stub.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sync" 7 | 8 | "github.com/ysmood/got/lib/utils" 9 | ) 10 | 11 | // Stub the method with stub 12 | func Stub[M any](mock Fallbackable, method M, stub M) { 13 | panicIfNotFunc(method) 14 | 15 | m := toMock(mock) 16 | 17 | m.lock.Lock() 18 | defer m.lock.Unlock() 19 | 20 | if m.stubs == nil { 21 | m.stubs = map[string]interface{}{} 22 | } 23 | 24 | name := fnName(method) 25 | 26 | m.stubs[name] = m.spy(name, stub) 27 | } 28 | 29 | // StubOn utils 30 | type StubOn struct { 31 | when []*StubWhen 32 | } 33 | 34 | // StubWhen utils 35 | type StubWhen struct { 36 | lock *sync.Mutex 37 | on *StubOn 38 | in []interface{} 39 | ret *StubReturn 40 | count int // how many times this stub has been matched 41 | } 42 | 43 | // StubReturn utils 44 | type StubReturn struct { 45 | on *StubOn 46 | out []reflect.Value 47 | times *StubTimes 48 | } 49 | 50 | // StubTimes utils 51 | type StubTimes struct { 52 | count int 53 | } 54 | 55 | // On helper to stub methods to conditionally return values. 56 | func On[M any](mock Fallbackable, method M) *StubOn { 57 | panicIfNotFunc(method) 58 | 59 | m := toMock(mock) 60 | 61 | s := &StubOn{ 62 | when: []*StubWhen{}, 63 | } 64 | 65 | eq := func(in, arg []interface{}) bool { 66 | for i := 0; i < len(in); i++ { 67 | if in[i] != Any && utils.Compare(in[i], arg[i]) != 0 { 68 | return false 69 | } 70 | } 71 | return true 72 | } 73 | 74 | t := reflect.TypeOf(method) 75 | 76 | fn := reflect.MakeFunc(t, func(args []reflect.Value) []reflect.Value { 77 | argsIt := utils.ToInterfaces(args) 78 | for _, when := range s.when { 79 | if eq(when.in, argsIt) { 80 | when.lock.Lock() 81 | 82 | when.ret.times.count-- 83 | if when.ret.times.count == 0 { 84 | m.Stop(method) 85 | } 86 | 87 | when.count++ 88 | 89 | when.lock.Unlock() 90 | 91 | return toReturnValues(t, when.ret.out) 92 | } 93 | } 94 | panic(fmt.Sprintf("No mock.StubOn.When matches: %#v", argsIt)) 95 | }) 96 | 97 | Stub(m, method, fn.Interface().(M)) 98 | 99 | return s 100 | } 101 | 102 | // Any input 103 | var Any = struct{}{} 104 | 105 | // When input args of stubbed method matches in 106 | func (s *StubOn) When(in ...interface{}) *StubWhen { 107 | w := &StubWhen{lock: &sync.Mutex{}, on: s, in: in} 108 | s.when = append(s.when, w) 109 | return w 110 | } 111 | 112 | // Return the out as the return values of stubbed method 113 | func (s *StubWhen) Return(out ...interface{}) *StubReturn { 114 | r := &StubReturn{on: s.on, out: utils.ToValues(out)} 115 | r.Times(0) 116 | s.ret = r 117 | return r 118 | } 119 | 120 | // Count returns how many times this condition has been matched 121 | func (s *StubWhen) Count() int { 122 | s.lock.Lock() 123 | defer s.lock.Unlock() 124 | return s.count 125 | } 126 | 127 | // Times specifies how how many stubs before stop, if n <= 0 it will never stop. 128 | func (s *StubReturn) Times(n int) *StubOn { 129 | t := &StubTimes{count: n} 130 | s.times = t 131 | return s.on 132 | } 133 | 134 | // Once specifies stubs only once before stop 135 | func (s *StubReturn) Once() *StubOn { 136 | return s.Times(1) 137 | } 138 | -------------------------------------------------------------------------------- /lib/utils/utils.go: -------------------------------------------------------------------------------- 1 | // Package utils ... 2 | package utils 3 | 4 | import ( 5 | "reflect" 6 | "strings" 7 | "time" 8 | 9 | "github.com/ysmood/gop" 10 | ) 11 | 12 | var float64Type = reflect.TypeOf(0.0) 13 | 14 | // SmartCompare returns the float value of x minus y. 15 | // If x and y are numerical types, the result will be the subtraction between them, such as x is int(1), y is float64(1.2), 16 | // the result will be -0.2 . time.Time is also a numerical value. 17 | // If x or y are not numerical types, both of them will be converted to string format of its value type, the result will be 18 | // the strings.Compare result between them, such as x is int(1), y is "a", the result will be 1 . 19 | func SmartCompare(x, y interface{}) float64 { 20 | _, xNil := IsNil(x) 21 | _, yNil := IsNil(y) 22 | if xNil && yNil { 23 | return 0 24 | } 25 | 26 | if reflect.DeepEqual(x, y) { 27 | return 0 28 | } 29 | 30 | if x != nil && y != nil { 31 | xVal := reflect.Indirect(reflect.ValueOf(x)) 32 | yVal := reflect.Indirect(reflect.ValueOf(y)) 33 | 34 | if xVal.Type().ConvertibleTo(float64Type) && yVal.Type().ConvertibleTo(float64Type) { 35 | return xVal.Convert(float64Type).Float() - yVal.Convert(float64Type).Float() 36 | } 37 | 38 | if xt, ok := xVal.Interface().(time.Time); ok { 39 | if yt, ok := yVal.Interface().(time.Time); ok { 40 | return float64(xt.Sub(yt)) 41 | } 42 | } 43 | } 44 | 45 | return Compare(x, y) 46 | } 47 | 48 | // Compare returns the float value of x minus y 49 | func Compare(x, y interface{}) float64 { 50 | return float64(strings.Compare(gop.Plain(x), gop.Plain(y))) 51 | } 52 | 53 | // ToInterfaces convertor 54 | func ToInterfaces(vs []reflect.Value) []interface{} { 55 | out := []interface{}{} 56 | for _, v := range vs { 57 | out = append(out, v.Interface()) 58 | } 59 | return out 60 | } 61 | 62 | // ToValues convertor 63 | func ToValues(vs []interface{}) []reflect.Value { 64 | out := []reflect.Value{} 65 | for _, v := range vs { 66 | out = append(out, reflect.ValueOf(v)) 67 | } 68 | return out 69 | } 70 | 71 | // IsNil returns true, true if the value is nilable and is nil. 72 | func IsNil(x interface{}) (bool, bool) { 73 | if x == nil { 74 | return true, true 75 | } 76 | 77 | val := reflect.ValueOf(x) 78 | k := val.Kind() 79 | nilable := k == reflect.Chan || 80 | k == reflect.Func || 81 | k == reflect.Interface || 82 | k == reflect.Map || 83 | k == reflect.Ptr || 84 | k == reflect.Slice 85 | 86 | if nilable { 87 | return true, val.IsNil() 88 | } 89 | 90 | return false, false 91 | } 92 | -------------------------------------------------------------------------------- /lib/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/ysmood/got/lib/utils" 9 | ) 10 | 11 | func TestSmartCompare(t *testing.T) { 12 | now := time.Now() 13 | 14 | circular := map[int]interface{}{} 15 | circular[0] = circular 16 | 17 | fn := func() {} 18 | fn2 := func() {} 19 | ch := make(chan int, 1) 20 | ch2 := make(chan int, 1) 21 | 22 | testCases := []struct { 23 | x interface{} 24 | y interface{} 25 | s interface{} 26 | }{ 27 | {1, 1, 0.0}, 28 | {1, 3.0, -2.0}, 29 | {1, "a", 1.0}, 30 | {"b", "a", 1.0}, 31 | {1, nil, -1.0}, 32 | {fn, fn, 0.0}, 33 | {fn, fn2, -1.0}, 34 | {ch, ch, 0.0}, 35 | {ch, ch2, -1.0}, 36 | {now.Add(time.Second), now, float64(time.Second)}, 37 | {circular, circular, 0.0}, 38 | {circular, 0, 1.0}, 39 | {map[int]interface{}{1: 1.0}, map[int]interface{}{1: 1}, 1.0}, 40 | {[]byte(nil), nil, 0.0}, 41 | } 42 | for i, c := range testCases { 43 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 44 | s := utils.SmartCompare(c.x, c.y) 45 | if s != c.s { 46 | t.Fail() 47 | t.Log("expect s to be", c.s, "but got", s) 48 | } 49 | }) 50 | } 51 | } 52 | 53 | func TestCompare(t *testing.T) { 54 | if utils.Compare(1, 1.0) == 0 { 55 | t.Fail() 56 | } 57 | } 58 | 59 | func TestOthers(t *testing.T) { 60 | vs := utils.ToValues([]interface{}{1}) 61 | 62 | if utils.ToInterfaces(vs)[0] != 1 { 63 | t.Error("fail") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /setup_test.go: -------------------------------------------------------------------------------- 1 | package got_test 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/ysmood/gop" 9 | "github.com/ysmood/got" 10 | ) 11 | 12 | var setup = got.Setup(func(g got.G) { 13 | g.Parallel() 14 | }) 15 | 16 | var _ got.Testable = &mock{} 17 | 18 | type mock struct { 19 | sync.Mutex 20 | t *testing.T 21 | failed bool 22 | skipped bool 23 | msg string 24 | cleanupList []func() 25 | recover bool 26 | name string 27 | } 28 | 29 | func (m *mock) Name() string { 30 | if m.name == "" { 31 | return "mock" 32 | } 33 | return m.name 34 | } 35 | func (m *mock) Skipped() bool { return m.skipped } 36 | func (m *mock) Failed() bool { return m.failed } 37 | func (m *mock) Helper() {} 38 | func (m *mock) Cleanup(f func()) { m.cleanupList = append([]func(){f}, m.cleanupList...) } 39 | func (m *mock) SkipNow() {} 40 | func (m *mock) Fail() { m.failed = true } 41 | 42 | func (m *mock) FailNow() { 43 | m.Lock() 44 | defer m.Unlock() 45 | 46 | m.failed = true 47 | if !m.recover { 48 | panic("fail now") 49 | } 50 | m.recover = false 51 | } 52 | 53 | func (m *mock) Logf(format string, args ...interface{}) { 54 | m.Lock() 55 | defer m.Unlock() 56 | 57 | if m.msg != "" { 58 | m.msg += "\n" 59 | } 60 | 61 | m.msg += fmt.Sprintf(format, args...) 62 | } 63 | 64 | func (m *mock) Run(_ string, fn func(*mock)) { 65 | fn(m) 66 | } 67 | 68 | func (m *mock) cleanup() { 69 | for _, f := range m.cleanupList { 70 | f() 71 | } 72 | m.cleanupList = nil 73 | } 74 | 75 | func (m *mock) check(expected string) { 76 | m.t.Helper() 77 | m.checkWithStyle(false, expected) 78 | } 79 | 80 | func (m *mock) reset() { 81 | m.Lock() 82 | defer m.Unlock() 83 | 84 | m.failed = false 85 | m.msg = "" 86 | } 87 | 88 | func (m *mock) checkWithStyle(visualizeStyle bool, expected string) { 89 | m.Lock() 90 | defer m.Unlock() 91 | 92 | m.t.Helper() 93 | 94 | if !m.failed { 95 | m.t.Error("should fail") 96 | } 97 | 98 | msg := "" 99 | if visualizeStyle { 100 | msg = gop.VisualizeANSI(m.msg) 101 | } else { 102 | msg = gop.StripANSI(m.msg) 103 | } 104 | 105 | if msg != expected { 106 | m.t.Errorf("\n\n[[[msg]]]\n\n%s\n\n[[[doesn't equal]]]\n\n%s\n\n", msg, expected) 107 | } 108 | 109 | m.failed = false 110 | m.msg = "" 111 | } 112 | -------------------------------------------------------------------------------- /snapshots.go: -------------------------------------------------------------------------------- 1 | package got 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/ysmood/got/lib/utils" 10 | ) 11 | 12 | const snapshotJSONExt = ".json" 13 | 14 | type snapshot struct { 15 | value any 16 | used bool 17 | } 18 | 19 | func (g G) snapshotsDir() string { 20 | return filepath.Join(".got", "snapshots", escapeFileName(g.Name())) 21 | } 22 | 23 | func (g G) loadSnapshots() { 24 | paths, err := filepath.Glob(filepath.Join(g.snapshotsDir(), "*"+snapshotJSONExt)) 25 | g.E(err) 26 | 27 | for _, path := range paths { 28 | g.snapshots.Store(path, snapshot{g.JSON(g.Read(path)), false}) 29 | } 30 | 31 | g.Cleanup(func() { 32 | if g.Failed() { 33 | return 34 | } 35 | 36 | g.snapshots.Range(func(path, data interface{}) bool { 37 | s := data.(snapshot) 38 | if !s.used { 39 | g.E(os.Remove(path.(string))) 40 | } 41 | return true 42 | }) 43 | }) 44 | } 45 | 46 | // Snapshot asserts that x equals the snapshot with the specified name, name should be unique under the same test case. 47 | // It will create a new snapshot file if the name is not found. 48 | // The snapshot file will be saved to ".got/snapshots/{TEST_NAME}". 49 | // To update the snapshot, just change the name of the snapshot or remove the corresponding snapshot file. 50 | // It will auto-remove the unused snapshot files after the test. 51 | // The snapshot files should be version controlled. 52 | // The format of the snapshot file is json. 53 | func (g G) Snapshot(name string, x interface{}) { 54 | g.Helper() 55 | 56 | path := filepath.Join(g.snapshotsDir(), escapeFileName(name)+snapshotJSONExt) 57 | 58 | if data, ok := g.snapshots.Load(path); ok { 59 | s := data.(snapshot) 60 | xVal := g.JSON(g.ToJSON(x).Bytes()) 61 | if utils.SmartCompare(xVal, s.value) == 0 { 62 | g.snapshots.Store(path, snapshot{x, true}) 63 | } else { 64 | g.Assertions.err(AssertionEq, xVal, s.value) 65 | } 66 | return 67 | } 68 | 69 | g.snapshots.Store(path, snapshot{x, true}) 70 | 71 | g.Cleanup(func() { 72 | g.E(os.MkdirAll(g.snapshotsDir(), 0755)) 73 | g.E(os.WriteFile(path, g.ToJSON(x).Bytes(), 0644)) 74 | }) 75 | } 76 | 77 | func escapeFileName(fileName string) string { 78 | // Define the invalid characters for both Windows and Unix 79 | invalidChars := `< > : " / \ | ? *` 80 | 81 | // Replace the invalid characters with an underscore 82 | regex := "[" + regexp.QuoteMeta(invalidChars) + "]" 83 | escapedFileName := regexp.MustCompile(regex).ReplaceAllString(fileName, "_") 84 | 85 | // Remove any leading or trailing spaces or dots 86 | escapedFileName = strings.Trim(escapedFileName, " .") 87 | 88 | // Remove consecutive dots 89 | escapedFileName = regexp.MustCompile(`\.{2,}`).ReplaceAllString(escapedFileName, ".") 90 | 91 | return escapedFileName 92 | } 93 | -------------------------------------------------------------------------------- /snapshots_test.go: -------------------------------------------------------------------------------- 1 | package got_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/ysmood/gop" 9 | "github.com/ysmood/got" 10 | ) 11 | 12 | func TestSnapshots(t *testing.T) { 13 | g := got.T(t) 14 | 15 | type C struct { 16 | Val int 17 | } 18 | 19 | g.Snapshot("a", "ok") 20 | g.Snapshot("b", 1) 21 | g.Snapshot("b", 1) 22 | g.Snapshot("c", C{10}) 23 | 24 | g.Run("sub", func(g got.G) { 25 | g.Snapshot("d", "ok") 26 | }) 27 | 28 | m := &mock{t: t, name: t.Name()} 29 | gm := got.New(m) 30 | gm.Snapshot("a", "ok") 31 | gm.Snapshot("a", "no") 32 | m.check(`"no" ⦗not ==⦘ "ok"`) 33 | 34 | gm.Snapshot("a", map[int]int{1: 2}) 35 | g.Has(m.msg, "diff chunk") 36 | m.reset() 37 | 38 | gm.ErrorHandler = got.NewDefaultAssertionError(gop.ThemeNone, nil) 39 | gm.Snapshot("a", "no") 40 | m.checkWithStyle(true, `"no" ⦗not ==⦘ "ok"`) 41 | } 42 | 43 | func TestSnapshotsCreate(t *testing.T) { 44 | path := filepath.FromSlash(".got/snapshots/TestSnapshotsCreate/a.json") 45 | err := os.RemoveAll(path) 46 | if err != nil { 47 | panic(err) 48 | } 49 | 50 | g := got.T(t) 51 | 52 | g.Cleanup(func() { 53 | g.True(g.PathExists(path)) 54 | }) 55 | 56 | g.Snapshot("a", "ok") 57 | } 58 | 59 | func TestSnapshotsNotUsed(t *testing.T) { 60 | path := filepath.FromSlash(".got/snapshots/TestSnapshotsNotUsed/a.json") 61 | 62 | g := got.T(t) 63 | g.WriteFile(path, []byte(`1`)) 64 | 65 | m := &mock{t: t, name: t.Name()} 66 | got.New(m) 67 | m.cleanup() 68 | 69 | g.False(g.PathExists(path)) 70 | } 71 | 72 | func TestSnapshotsNotUsedWhenFailure(t *testing.T) { 73 | path := filepath.FromSlash(".got/snapshots/TestSnapshotsNotUsedWhenFailure/a.json") 74 | 75 | g := got.T(t) 76 | g.WriteFile(path, []byte(`1`)) 77 | 78 | m := &mock{t: t, name: t.Name()} 79 | gm := got.New(m) 80 | gm.Fail() 81 | m.cleanup() 82 | 83 | g.True(g.PathExists(path)) 84 | } 85 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package got 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "encoding/hex" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "io/fs" 12 | "math/big" 13 | "os" 14 | "path/filepath" 15 | "reflect" 16 | "sync" 17 | "text/template" 18 | "time" 19 | ) 20 | 21 | // Context helper 22 | type Context struct { 23 | context.Context 24 | Cancel func() 25 | } 26 | 27 | // Utils for commonly used methods 28 | type Utils struct { 29 | Testable 30 | } 31 | 32 | // Fatal is the same as [testing.common.Fatal] 33 | func (ut Utils) Fatal(args ...interface{}) { 34 | ut.Helper() 35 | ut.Log(args...) 36 | ut.FailNow() 37 | } 38 | 39 | // Fatalf is the same as [testing.common.Fatalf] 40 | func (ut Utils) Fatalf(format string, args ...interface{}) { 41 | ut.Helper() 42 | ut.Logf(format, args...) 43 | ut.FailNow() 44 | } 45 | 46 | // Log is the same as [testing.common.Log] 47 | func (ut Utils) Log(args ...interface{}) { 48 | ut.Helper() 49 | ut.Logf("%s", fmt.Sprintln(args...)) 50 | } 51 | 52 | // Error is the same as [testing.common.Error] 53 | func (ut Utils) Error(args ...interface{}) { 54 | ut.Helper() 55 | ut.Log(args...) 56 | ut.Fail() 57 | } 58 | 59 | // Errorf is the same as [testing.common.Errorf] 60 | func (ut Utils) Errorf(format string, args ...interface{}) { 61 | ut.Helper() 62 | ut.Logf(format, args...) 63 | ut.Fail() 64 | } 65 | 66 | // Skipf is the same as [testing.common.Skipf] 67 | func (ut Utils) Skipf(format string, args ...interface{}) { 68 | ut.Helper() 69 | ut.Logf(format, args...) 70 | ut.SkipNow() 71 | } 72 | 73 | // Skip is the same as [testing.common.Skip] 74 | func (ut Utils) Skip(args ...interface{}) { 75 | ut.Helper() 76 | ut.Log(args...) 77 | ut.SkipNow() 78 | } 79 | 80 | // Go runs f in a goroutine and wait for it to finish before the test ends. 81 | func (ut Utils) Go(f func()) { 82 | wait := make(chan struct{}) 83 | ut.Cleanup(func() { <-wait }) 84 | 85 | go func() { 86 | f() 87 | 88 | wait <- struct{}{} 89 | }() 90 | } 91 | 92 | // Run f as a sub-test 93 | func (ut Utils) Run(name string, f func(t G)) bool { 94 | runVal := reflect.ValueOf(ut.Testable).MethodByName("Run") 95 | return runVal.Call([]reflect.Value{ 96 | reflect.ValueOf(name), 97 | reflect.MakeFunc(runVal.Type().In(1), func(args []reflect.Value) []reflect.Value { 98 | f(New(args[0].Interface().(Testable))) 99 | return nil 100 | }), 101 | })[0].Interface().(bool) 102 | } 103 | 104 | // Parallel is the same as [testing.T.Parallel] 105 | func (ut Utils) Parallel() Utils { 106 | reflect.ValueOf(ut.Testable).MethodByName("Parallel").Call(nil) 107 | return ut 108 | } 109 | 110 | // DoAfter d duration if the test is still running 111 | func (ut Utils) DoAfter(d time.Duration, do func()) (cancel func()) { 112 | ctx := ut.Context() 113 | go func() { 114 | ut.Helper() 115 | tmr := time.NewTimer(d) 116 | defer tmr.Stop() 117 | select { 118 | case <-ctx.Done(): 119 | case <-tmr.C: 120 | do() 121 | } 122 | }() 123 | return ctx.Cancel 124 | } 125 | 126 | // PanicAfter d duration if the test is still running 127 | func (ut Utils) PanicAfter(d time.Duration) (cancel func()) { 128 | return ut.DoAfter(d, func() { 129 | ut.Helper() 130 | panicWithTrace(fmt.Sprintf("%s timeout after %v", ut.Name(), d)) 131 | }) 132 | } 133 | 134 | // Context that will be canceled after the test 135 | func (ut Utils) Context() Context { 136 | ctx, cancel := context.WithCancel(context.Background()) 137 | ut.Cleanup(cancel) 138 | return Context{ctx, cancel} 139 | } 140 | 141 | // Timeout context that will be canceled after the test 142 | func (ut Utils) Timeout(d time.Duration) Context { 143 | ctx, cancel := context.WithTimeout(context.Background(), d) 144 | ut.Cleanup(cancel) 145 | return Context{ctx, cancel} 146 | } 147 | 148 | // RandStr generates a random string with the specified length 149 | func (ut Utils) RandStr(l int) string { 150 | ut.Helper() 151 | b := ut.RandBytes((l + 1) / 2) 152 | return hex.EncodeToString(b)[:l] 153 | } 154 | 155 | // RandInt generates a random integer within [min, max) 156 | func (ut Utils) RandInt(min, max int) int { 157 | ut.Helper() 158 | n, err := rand.Int(rand.Reader, big.NewInt(int64(max-min))) 159 | ut.err(err) 160 | return int(n.Int64()) + min 161 | } 162 | 163 | // RandBytes generates a random byte array with the specified length 164 | func (ut Utils) RandBytes(l int) []byte { 165 | ut.Helper() 166 | b := make([]byte, l) 167 | _, err := rand.Read(b) 168 | ut.err(err) 169 | return b 170 | } 171 | 172 | // Render template. It will use [Utils.Read] to read the value as the template string. 173 | func (ut Utils) Render(value interface{}, data interface{}) *bytes.Buffer { 174 | ut.Helper() 175 | out := bytes.NewBuffer(nil) 176 | t := template.New("") 177 | t, err := t.Parse(ut.Read(value).String()) 178 | ut.err(err) 179 | ut.err(t.Execute(out, data)) 180 | return out 181 | } 182 | 183 | // WriteFile at path with content, it uses [Utils.Open] to open the file. 184 | func (ut Utils) WriteFile(path string, content interface{}) { 185 | f := ut.Open(true, path) 186 | defer func() { ut.err(f.Close()) }() 187 | ut.Write(content)(f) 188 | } 189 | 190 | // PathExists checks if path exists 191 | func (ut Utils) PathExists(path string) bool { 192 | _, err := os.Stat(path) 193 | return err == nil 194 | } 195 | 196 | // Chdir is like [os.Chdir] but will restore the dir after test. 197 | func (ut Utils) Chdir(dir string) { 198 | ut.Helper() 199 | cwd, err := os.Getwd() 200 | ut.err(err) 201 | ut.err(os.Chdir(dir)) 202 | ut.Cleanup(func() { _ = os.Chdir(cwd) }) 203 | } 204 | 205 | // Setenv is like [os.Setenv] but will restore the env after test. 206 | func (ut Utils) Setenv(key, value string) { 207 | ut.Helper() 208 | old := os.Getenv(key) 209 | ut.err(os.Setenv(key, value)) 210 | ut.Cleanup(func() { _ = os.Setenv(key, old) }) 211 | } 212 | 213 | // MkdirAll is like [os.MkdirAll] but will remove the dir after test and fail the test if error. 214 | // The default perm is 0755. 215 | func (ut Utils) MkdirAll(perm fs.FileMode, path string) { 216 | if perm == 0 { 217 | perm = 0755 218 | } 219 | 220 | dir := filepath.Dir(path) 221 | 222 | if !ut.PathExists(dir) { 223 | ut.MkdirAll(perm, dir) 224 | } 225 | 226 | if ut.PathExists(path) { 227 | return 228 | } 229 | 230 | ut.err(os.Mkdir(path, perm)) 231 | ut.Cleanup(func() { _ = os.RemoveAll(path) }) 232 | } 233 | 234 | // Open a file. Override it if create is true. Directories will be auto-created. 235 | // If the directory and file doesn't exist, it will be removed after the test. 236 | func (ut Utils) Open(create bool, path string) (f *os.File) { 237 | ut.Helper() 238 | 239 | var err error 240 | if create { 241 | ut.MkdirAll(0, filepath.Dir(path)) 242 | f, err = os.Create(path) 243 | if err == nil { 244 | ut.Cleanup(func() { _ = os.Remove(path) }) 245 | } 246 | } else { 247 | f, err = os.Open(path) 248 | } 249 | ut.err(err) 250 | 251 | return f 252 | } 253 | 254 | // Read all from value. If the value is string and it's a file path, 255 | // the file content will be read, or the string will be returned. 256 | // If the value is [io.Reader], the reader will be read. If the value is []byte, the value will be returned. 257 | // Others will be converted to string and returned. 258 | func (ut Utils) Read(value interface{}) *bytes.Buffer { 259 | ut.Helper() 260 | 261 | var r io.Reader 262 | 263 | switch v := value.(type) { 264 | case string: 265 | if !ut.PathExists(v) { 266 | return bytes.NewBufferString(v) 267 | } 268 | f := ut.Open(false, v) 269 | defer func() { ut.err(f.Close()) }() 270 | r = f 271 | case io.Reader: 272 | r = v 273 | case []byte: 274 | return bytes.NewBuffer(v) 275 | default: 276 | return bytes.NewBufferString(fmt.Sprintf("%v", v)) 277 | } 278 | 279 | b := bytes.NewBuffer(nil) 280 | _, err := io.Copy(b, r) 281 | ut.err(err) 282 | return b 283 | } 284 | 285 | // JSON from string, []byte, or io.Reader 286 | func (ut Utils) JSON(src interface{}) (v interface{}) { 287 | ut.Helper() 288 | 289 | var b []byte 290 | switch obj := src.(type) { 291 | case []byte: 292 | b = obj 293 | case string: 294 | b = []byte(obj) 295 | case io.Reader: 296 | var err error 297 | b, err = io.ReadAll(obj) 298 | ut.err(err) 299 | } 300 | ut.err(json.Unmarshal(b, &v)) 301 | return 302 | } 303 | 304 | // ToJSON convert obj to JSON bytes 305 | func (ut Utils) ToJSON(obj interface{}) *bytes.Buffer { 306 | ut.Helper() 307 | 308 | buf := bytes.NewBuffer(nil) 309 | 310 | enc := json.NewEncoder(buf) 311 | enc.SetEscapeHTML(false) 312 | enc.SetIndent("", " ") 313 | 314 | err := enc.Encode(obj) 315 | ut.err(err) 316 | 317 | buf.Truncate(buf.Len() - 1) // Remove the trailing newline 318 | 319 | return buf 320 | } 321 | 322 | // ToJSONString convert obj to JSON string 323 | func (ut Utils) ToJSONString(obj interface{}) string { 324 | ut.Helper() 325 | return ut.ToJSON(obj).String() 326 | } 327 | 328 | // Write obj to the writer. Encode obj to []byte and cache it for writer. 329 | // If obj is not []byte, string, or [io.Reader], it will be encoded as JSON. 330 | func (ut Utils) Write(obj interface{}) (writer func(io.Writer)) { 331 | lock := sync.Mutex{} 332 | var cache []byte 333 | return func(w io.Writer) { 334 | lock.Lock() 335 | defer lock.Unlock() 336 | 337 | ut.Helper() 338 | 339 | if cache != nil { 340 | _, err := w.Write(cache) 341 | ut.err(err) 342 | return 343 | } 344 | 345 | buf := bytes.NewBuffer(nil) 346 | w = io.MultiWriter(buf, w) 347 | 348 | var err error 349 | switch v := obj.(type) { 350 | case []byte: 351 | _, err = w.Write(v) 352 | case string: 353 | _, err = w.Write([]byte(v)) 354 | case io.Reader: 355 | _, err = io.Copy(w, v) 356 | default: 357 | err = json.NewEncoder(w).Encode(v) 358 | } 359 | ut.err(err) 360 | cache = buf.Bytes() 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /utils_private_test.go: -------------------------------------------------------------------------------- 1 | package got 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestPanicAfter(t *testing.T) { 9 | ut := New(t) 10 | 11 | ut.Panic(func() { 12 | panicWithTrace(1) 13 | }) 14 | 15 | wait := make(chan struct{}) 16 | 17 | old := panicWithTrace 18 | panicWithTrace = func(v interface{}) { 19 | ut.Eq(v, "TestPanicAfter timeout after 1ns") 20 | close(wait) 21 | } 22 | defer func() { panicWithTrace = old }() 23 | 24 | ut.PanicAfter(1) 25 | time.Sleep(time.Millisecond) 26 | <-wait 27 | } 28 | -------------------------------------------------------------------------------- /utils_req.go: -------------------------------------------------------------------------------- 1 | package got 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "io" 8 | "mime" 9 | "net/http" 10 | "path/filepath" 11 | ) 12 | 13 | // ReqMIME option type, it should be like ".json", "test.json", "a/b/c.jpg", etc 14 | type ReqMIME string 15 | 16 | // Req is a helper method to send http request. It will handle errors automatically, so you don't need to check errors. 17 | // The method is the http method, default value is "GET". 18 | // If an option is [http.Header], it will be used as the request header. 19 | // If an option is [ReqMIME], it will be used to set the Content-Type header. 20 | // If an option is [context.Context], it will be used as the request context. 21 | // Other option type will be treat as request body, it will be encoded by [Utils.Write]. 22 | // Some request examples: 23 | // 24 | // Req("GET", "http://example.com") 25 | // Req("GET", "http://example.com", context.TODO()) 26 | // Req("POST", "http://example.com", map[string]any{"a": 1}) 27 | // Req("POST", "http://example.com", http.Header{"Host": "example.com"}, ReqMIME(".json"), map[string]any{"a": 1}) 28 | func (ut Utils) Req(method, url string, options ...interface{}) *ResHelper { 29 | ut.Helper() 30 | 31 | header := http.Header{} 32 | var host string 33 | var contentType string 34 | var body io.Reader 35 | ctx := context.Background() 36 | 37 | for _, item := range options { 38 | switch val := item.(type) { 39 | case http.Header: 40 | host = val.Get("Host") 41 | val.Del("Host") 42 | header = val 43 | case ReqMIME: 44 | contentType = mime.TypeByExtension(filepath.Ext(string(val))) 45 | case context.Context: 46 | ctx = val 47 | default: 48 | buf := bytes.NewBuffer(nil) 49 | ut.Write(val)(buf) 50 | body = buf 51 | } 52 | } 53 | 54 | req, err := http.NewRequestWithContext(ctx, method, url, body) 55 | if err != nil { 56 | return &ResHelper{ut, nil, err} 57 | } 58 | 59 | if header != nil { 60 | req.Header = header 61 | } 62 | 63 | req.Host = host 64 | req.Header.Set("Content-Type", contentType) 65 | 66 | res, err := http.DefaultClient.Do(req) 67 | return &ResHelper{ut, res, err} 68 | } 69 | 70 | // ResHelper of the request 71 | type ResHelper struct { 72 | ut Utils 73 | *http.Response 74 | err error 75 | } 76 | 77 | // Bytes parses body as [*bytes.Buffer] and returns the result 78 | func (res *ResHelper) Bytes() *bytes.Buffer { 79 | res.ut.Helper() 80 | res.ut.err(res.err) 81 | return res.ut.Read(res.Body) 82 | } 83 | 84 | // String parses body as string and returns the result 85 | func (res *ResHelper) String() string { 86 | res.ut.Helper() 87 | return res.Bytes().String() 88 | } 89 | 90 | // JSON parses body as json and returns the result 91 | func (res *ResHelper) JSON() (v interface{}) { 92 | res.ut.Helper() 93 | res.ut.err(res.err) 94 | return res.ut.JSON(res.Body) 95 | } 96 | 97 | // Unmarshal body to v as json, it's like [json.Unmarshal]. 98 | func (res *ResHelper) Unmarshal(v interface{}) { 99 | res.ut.Helper() 100 | res.ut.err(json.Unmarshal(res.Bytes().Bytes(), v)) 101 | } 102 | 103 | // Err of request protocol 104 | func (res *ResHelper) Err() error { 105 | return res.err 106 | } 107 | 108 | func (ut Utils) err(err error) { 109 | ut.Helper() 110 | 111 | if err != nil { 112 | ut.Fatal(err) 113 | } 114 | } 115 | 116 | // there no way to stop a blocking test from outside 117 | var panicWithTrace = func(v interface{}) { 118 | panic(v) 119 | } 120 | -------------------------------------------------------------------------------- /utils_serve.go: -------------------------------------------------------------------------------- 1 | package got 2 | 3 | import ( 4 | "mime" 5 | "net" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | // Serve http on a random port. The server will be auto-closed after the test. 14 | func (ut Utils) Serve() *Router { 15 | ut.Helper() 16 | return ut.ServeWith("tcp4", "127.0.0.1:0") 17 | } 18 | 19 | // ServeWith specified network and address 20 | func (ut Utils) ServeWith(network, address string) *Router { 21 | ut.Helper() 22 | 23 | mux := http.NewServeMux() 24 | srv := &http.Server{Handler: mux} 25 | 26 | l, err := net.Listen(network, address) 27 | ut.err(err) 28 | 29 | ut.Cleanup(func() { _ = srv.Close() }) 30 | 31 | go func() { _ = srv.Serve(l) }() 32 | 33 | u, err := url.Parse("http://" + l.Addr().String()) 34 | ut.err(err) 35 | 36 | return &Router{ut, u, srv, mux} 37 | } 38 | 39 | // Router of a http server 40 | type Router struct { 41 | ut Utils 42 | HostURL *url.URL 43 | Server *http.Server 44 | Mux *http.ServeMux 45 | } 46 | 47 | // URL will prefix the path with the server's host 48 | func (rt *Router) URL(path ...string) string { 49 | p := strings.Join(path, "") 50 | if !strings.HasPrefix(p, "/") { 51 | p = "/" + p 52 | } 53 | 54 | return rt.HostURL.String() + p 55 | } 56 | 57 | // Route on the pattern. Check the doc of [http.ServeMux] for the syntax of pattern. 58 | // It will use [Utils.HandleHTTP] to handle each request. 59 | func (rt *Router) Route(pattern, file string, value ...interface{}) *Router { 60 | h := rt.ut.HandleHTTP(file, value...) 61 | 62 | rt.Mux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { 63 | h(w, r) 64 | }) 65 | 66 | return rt 67 | } 68 | 69 | // HandleHTTP handles a request. If file exists serve the file content. The file will be used to set the Content-Type header. 70 | // If the file doesn't exist, the value will be encoded by [Utils.Write] and used as the response body. 71 | func (ut Utils) HandleHTTP(file string, value ...interface{}) func(http.ResponseWriter, *http.Request) { 72 | var obj interface{} 73 | if len(value) > 1 { 74 | obj = value 75 | } else if len(value) == 1 { 76 | obj = value[0] 77 | } 78 | 79 | write := ut.Write(obj) 80 | 81 | return func(w http.ResponseWriter, r *http.Request) { 82 | if _, err := os.Stat(file); err == nil { 83 | http.ServeFile(w, r, file) 84 | return 85 | } 86 | 87 | if obj == nil { 88 | return 89 | } 90 | 91 | w.Header().Add("Content-Type", mime.TypeByExtension(filepath.Ext(file))) 92 | 93 | write(w) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package got_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "sync" 10 | "testing" 11 | "time" 12 | 13 | "github.com/ysmood/got" 14 | ) 15 | 16 | func init() { 17 | got.DefaultFlags("parallel=3") 18 | } 19 | 20 | func TestHelper(t *testing.T) { 21 | ut := got.T(t) 22 | 23 | ctx := ut.Context() 24 | ctx.Cancel() 25 | <-ctx.Done() 26 | <-ut.Timeout(0).Done() 27 | 28 | ut.Len(ut.RandStr(10), 10) 29 | ut.Lt(ut.RandInt(0, 1), 1) 30 | ut.Gt(ut.RandInt(-2, -1), -3) 31 | 32 | f := ut.Open(true, "tmp/test.txt") 33 | ut.Nil(os.Stat("tmp/test.txt")) 34 | ut.Write(1)(f) 35 | ut.Nil(f.Close()) 36 | f = ut.Open(false, "tmp/test.txt") 37 | ut.Eq(ut.JSON(f), 1) 38 | 39 | ut.Setenv(ut.RandStr(8), ut.RandStr(8)) 40 | p := fmt.Sprintf("tmp/%s/b/c", ut.RandStr(8)) 41 | ut.MkdirAll(0, p) 42 | ut.MkdirAll(0, "tmp") 43 | ut.PathExists(p) 44 | 45 | s := ut.RandStr(16) 46 | ut.WriteFile("tmp/test.txt", s) 47 | ut.Eq(ut.Read("tmp/test.txt").String(), s) 48 | ut.Eq(ut.Read(123).String(), "123") 49 | ut.Eq(ut.Read([]byte("ok")).String(), "ok") 50 | ut.Eq(ut.Render("{{.}}", 10).String(), "10") 51 | 52 | ut.Eq(ut.JSON([]byte("1")), 1) 53 | ut.Eq(ut.JSON("true"), true) 54 | 55 | ut.Eq(ut.ToJSONString(10), "10") 56 | ut.Eq(ut.ToJSONString("<>"), `"<>"`) 57 | 58 | buf := bytes.NewBuffer(nil) 59 | ut.Write([]byte("ok"))(buf) 60 | ut.Eq(buf.String(), "ok") 61 | 62 | ut.Run("subtest", func(t got.G) { 63 | t.Eq(1, 1) 64 | }) 65 | 66 | ut.Eq(got.Parallel(), 3) 67 | 68 | { 69 | s := ut.Serve() 70 | s.Route("/", ".txt") 71 | s.Route("/file", "go.mod") 72 | s.Route("/a", ".html", "ok") 73 | s.Route("/b", ".json", "ok", 1) 74 | f, err := os.Open("go.mod") 75 | ut.E(err) 76 | s.Route("/c", ".html", f) 77 | s.Mux.HandleFunc("/d", func(_ http.ResponseWriter, r *http.Request) { 78 | ut.Eq(ut.Read(r.Body).String(), "1\n") 79 | }) 80 | s.Mux.HandleFunc("/f", func(_ http.ResponseWriter, r *http.Request) { 81 | ut.Has(r.Header.Get("Content-Type"), "application/json") 82 | ut.Eq(r.Header.Get("Test-Header"), "ok") 83 | ut.Eq(r.Host, "test.com") 84 | }) 85 | s.Mux.HandleFunc("/timeout", func(_ http.ResponseWriter, r *http.Request) { 86 | <-r.Context().Done() 87 | }) 88 | 89 | ut.Eq(ut.Req("", s.URL()).String(), "") 90 | ut.Has(ut.Req("", s.URL("/file")).String(), "ysmood/got") 91 | ut.Eq(ut.Req("", s.URL("/a")).String(), "ok") 92 | ut.Eq(ut.Req("", s.URL("a")).String(), "ok") 93 | 94 | ut.Has(ut.Req("", s.URL("/c")).String(), "ysmood/got") 95 | ut.Req(http.MethodPost, s.URL("/d"), 1) 96 | ut.Req(http.MethodPost, s.URL("/f"), http.Header{"Test-Header": {"ok"}, "Host": {"test.com"}}, got.ReqMIME(".json"), 1) 97 | ut.Has(ut.Req("", s.URL("/timeout"), ut.Timeout(100*time.Millisecond)).Err().Error(), "context deadline exceeded") 98 | ut.Has(ut.Req("", string(rune(0x7f))).Err().Error(), `invalid control character in URL`) 99 | 100 | res := ut.Req("", s.URL("/b")) 101 | ut.Eq(res.JSON(), []interface{}{"ok", float64(1)}) 102 | ut.Has(res.Header.Get("Content-Type"), "application/json") 103 | 104 | res = ut.Req("", s.URL("/b")) 105 | var v []interface{} 106 | res.Unmarshal(&v) 107 | ut.Eq(v, []interface{}{"ok", 1.0}) 108 | } 109 | 110 | ut.DoAfter(time.Hour, func() {}) 111 | 112 | m := &mock{t: t} 113 | mut := got.New(m) 114 | 115 | m.msg = "" 116 | mut.Log("a", 1) 117 | ut.Eq(m.msg, "a 1\n") 118 | 119 | m.msg = "" 120 | ut.Panic(func() { 121 | buf := bytes.NewBufferString("a") 122 | mut.JSON(buf) 123 | }) 124 | ut.Eq(m.msg, "invalid character 'a' looking for beginning of value\n") 125 | 126 | m.msg = "" 127 | ut.Panic(func() { 128 | mut.Fatal("test skip") 129 | }) 130 | ut.Eq(m.msg, "test skip\n") 131 | 132 | m.msg = "" 133 | ut.Panic(func() { 134 | mut.Fatalf("test skip") 135 | }) 136 | ut.Eq(m.msg, "test skip") 137 | 138 | m.msg = "" 139 | mut.Error("test skip") 140 | ut.Eq(m.msg, "test skip\n") 141 | 142 | m.msg = "" 143 | mut.Errorf("test skip") 144 | ut.Eq(m.msg, "test skip") 145 | 146 | m.msg = "" 147 | mut.Skip("test skip") 148 | ut.Eq(m.msg, "test skip\n") 149 | 150 | m.msg = "" 151 | mut.Skipf("test skip") 152 | ut.Eq(m.msg, "test skip") 153 | } 154 | 155 | func TestServe(t *testing.T) { 156 | ut := setup(t) 157 | 158 | key := ut.RandStr(8) 159 | s := ut.Serve().Route("/", "", key) 160 | count := 30 161 | 162 | wg := sync.WaitGroup{} 163 | wg.Add(count) 164 | 165 | request := func() { 166 | req, err := http.NewRequest(http.MethodGet, s.URL(), nil) 167 | ut.E(err) 168 | 169 | res, err := http.DefaultClient.Do(req) 170 | ut.E(err) 171 | 172 | b, err := io.ReadAll(res.Body) 173 | ut.E(err) 174 | 175 | ut.Eq(string(b), key) 176 | wg.Done() 177 | } 178 | 179 | for i := 0; i < count; i++ { 180 | go request() 181 | } 182 | 183 | wg.Wait() 184 | } 185 | 186 | func TestPathExists(t *testing.T) { 187 | g := got.T(t) 188 | 189 | g.False(g.PathExists("not-exists")) 190 | g.False(g.PathExists("*!")) 191 | g.True(g.PathExists("lib")) 192 | } 193 | 194 | func TestChdir(t *testing.T) { 195 | g := got.T(t) 196 | 197 | g.Chdir("lib") 198 | 199 | g.PathExists("diff") 200 | } 201 | 202 | func TestGo(t *testing.T) { 203 | g := got.T(t) 204 | 205 | c := g.Count(1) 206 | 207 | g.Go(func() { 208 | time.Sleep(time.Millisecond * 30) 209 | c() 210 | }) 211 | } 212 | --------------------------------------------------------------------------------