pax_global_header00006660000000000000000000000064146324536440014525gustar00rootroot0000000000000052 comment=d68f04bc9499ab392ee360a552d20824143b9459 gotest.tools-3.5.1/000077500000000000000000000000001463245364400141775ustar00rootroot00000000000000gotest.tools-3.5.1/.circleci/000077500000000000000000000000001463245364400160325ustar00rootroot00000000000000gotest.tools-3.5.1/.circleci/config.yml000066400000000000000000000031741463245364400200270ustar00rootroot00000000000000version: 2.1 orbs: go: gotest/tools@0.0.14 workflows: ci: jobs: - lint - go/test: name: test-golang-1.18 executor: name: go/golang tag: 1.18-alpine post-steps: &xgenerics - run: name: "test x/generics" working_directory: ./x/generics command: gotestsum -ftestname - go/test: name: test-golang-1.19 executor: name: go/golang tag: 1.19-alpine post-steps: *xgenerics - go/test: name: test-golang-1.20 executor: name: go/golang tag: 1.20-alpine - go/test: name: test-windows executor: windows pre-steps: - run: | git config --global core.autocrlf false git config --global core.symlinks true - run: | choco upgrade golang echo 'export PATH="$PATH:/c/Program Files/Go/bin"' > $BASH_ENV - run: go version executors: windows: machine: image: windows-server-2019-vs2019:stable resource_class: windows.medium shell: bash.exe jobs: lint: executor: name: go/golang tag: 1.20-alpine steps: - checkout - go/install-golangci-lint: prefix: v1.51.1 version: 1.51.1 - go/install: {package: git} - run: name: Lint command: golangci-lint run -v --concurrency 2 - run: name: Lint x/generics working_directory: ./x/generics command: golangci-lint run -v --concurrency 2 gotest.tools-3.5.1/.codecov.yml000066400000000000000000000001651463245364400164240ustar00rootroot00000000000000coverage: status: project: default: threshold: 2 patch: default: threshold: 20 gotest.tools-3.5.1/.github/000077500000000000000000000000001463245364400155375ustar00rootroot00000000000000gotest.tools-3.5.1/.github/workflows/000077500000000000000000000000001463245364400175745ustar00rootroot00000000000000gotest.tools-3.5.1/.github/workflows/sync-main.yaml000066400000000000000000000005101463245364400223520ustar00rootroot00000000000000name: Merge main into master on: push: branches: [main] jobs: sync: name: Merge main branch runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: {fetch-depth: 0} - name: merge run: | git checkout master git merge --ff-only main git push origin master gotest.tools-3.5.1/.gitignore000066400000000000000000000000541463245364400161660ustar00rootroot00000000000000vendor/ .dobi/ Gopkg.lock .depsources dist/ gotest.tools-3.5.1/.golangci.yml000066400000000000000000000022441463245364400165650ustar00rootroot00000000000000linters-settings: goconst: min-len: 5 min-occurrences: 10 lll: line-length: 100 maintidx: under: 35 issues: exclude-use-default: false exclude-rules: - text: 'result .* is always' linters: [unparam] - text: 'always receives' linters: [unparam] - path: _test\.go linters: [errcheck, staticcheck, lll, maintidx] - path: internal/difflib/difflib\.go text: . - text: 'return value of .*Close` is not checked' linters: [errcheck] - text: 'SA1019' linters: [staticcheck] - path: internal/ text: 'ST1000' linters: [stylecheck] - path: 'example_test\.go' linters: [bodyclose] linters: disable-all: true enable: - bodyclose - depguard - dogsled - errcheck - errorlint - exportloopref - gocognit - goconst - gofmt - goimports - gosimple - govet - ineffassign - lll - maintidx - misspell - nakedret - nestif - nilerr - nilnil - nolintlint - prealloc - revive - staticcheck - stylecheck - typecheck - unconvert - unparam - unused - wastedassign - whitespace gotest.tools-3.5.1/CONTRIBUTING.md000066400000000000000000000006441463245364400164340ustar00rootroot00000000000000# Contributing to gotest.tools Thank you for your interest in contributing to the project! Below are some suggestions which may make the process easier. ## Pull requests Pull requests for new features should generally be preceded by an issue explaining the feature and why it is necessary. Pull requests for bug fixes are always appreciated. They should almost always include a test which fails without the bug fix. gotest.tools-3.5.1/LICENSE000066400000000000000000000010611463245364400152020ustar00rootroot00000000000000Copyright 2018 gotest.tools authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. gotest.tools-3.5.1/README.md000066400000000000000000000040111463245364400154520ustar00rootroot00000000000000# gotest.tools A collection of packages to augment `testing` and support common patterns. [![GoDoc](https://godoc.org/gotest.tools?status.svg)](https://pkg.go.dev/gotest.tools/v3/?tab=subdirectories) [![CircleCI](https://circleci.com/gh/gotestyourself/gotest.tools/tree/main.svg?style=shield)](https://circleci.com/gh/gotestyourself/gotest.tools/tree/main) [![Go Reportcard](https://goreportcard.com/badge/gotest.tools)](https://goreportcard.com/report/gotest.tools) ## Usage With Go modules enabled (go1.11+) ``` $ go get gotest.tools/v3 ``` ``` import "gotest.tools/v3/assert" ``` To use `gotest.tools` with an older version of Go that does not understand Go module paths pin to version `v2.3.0`. ## Packages * [assert](http://pkg.go.dev/gotest.tools/v3/assert) - compare values and fail the test when a comparison fails * [env](http://pkg.go.dev/gotest.tools/v3/env) - test code which uses environment variables * [fs](http://pkg.go.dev/gotest.tools/v3/fs) - create temporary files and compare a filesystem tree to an expected value * [golden](http://pkg.go.dev/gotest.tools/v3/golden) - compare large multi-line strings against values frozen in golden files * [icmd](http://pkg.go.dev/gotest.tools/v3/icmd) - execute binaries and test the output * [poll](http://pkg.go.dev/gotest.tools/v3/poll) - test asynchronous code by polling until a desired state is reached * [skip](http://pkg.go.dev/gotest.tools/v3/skip) - skip a test and print the source code of the condition used to skip the test ## Related * [gotest.tools/gotestsum](https://github.com/gotestyourself/gotestsum) - go test runner with custom output * [go testing patterns](https://github.com/gotestyourself/gotest.tools/wiki/Go-Testing-Patterns) - zero-dependency patterns for organizing test cases * [test doubles and patching](https://github.com/gotestyourself/gotest.tools/wiki/Test-Doubles-And-Patching) - zero-dependency test doubles (fakes, spies, stubs, and mocks) and monkey patching patterns ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md). gotest.tools-3.5.1/assert/000077500000000000000000000000001463245364400155005ustar00rootroot00000000000000gotest.tools-3.5.1/assert/assert.go000066400000000000000000000266411463245364400173410ustar00rootroot00000000000000/* Package assert provides assertions for comparing expected values to actual values in tests. When an assertion fails a helpful error message is printed. # Example usage All the assertions in this package use [testing.T.Helper] to mark themselves as test helpers. This allows the testing package to print the filename and line number of the file function that failed. assert.NilError(t, err) // filename_test.go:212: assertion failed: error is not nil: file not found If any assertion is called from a helper function, make sure to call t.Helper from the helper function so that the filename and line number remain correct. The examples below show assert used with some common types and the failure messages it produces. The filename and line number portion of the failure message is omitted from these examples for brevity. // booleans assert.Assert(t, ok) // assertion failed: ok is false assert.Assert(t, !missing) // assertion failed: missing is true // primitives assert.Equal(t, count, 1) // assertion failed: 0 (count int) != 1 (int) assert.Equal(t, msg, "the message") // assertion failed: my message (msg string) != the message (string) assert.Assert(t, total != 10) // use Assert for NotEqual // assertion failed: total is 10 assert.Assert(t, count > 20, "count=%v", count) // assertion failed: count is <= 20: count=1 // errors assert.NilError(t, closer.Close()) // assertion failed: error is not nil: close /file: errno 11 assert.Error(t, err, "the exact error message") // assertion failed: expected error "the exact error message", got "oops" assert.ErrorContains(t, err, "includes this") // assertion failed: expected error to contain "includes this", got "oops" assert.ErrorIs(t, err, os.ErrNotExist) // assertion failed: error is "oops", not "file does not exist" (os.ErrNotExist) // complex types assert.DeepEqual(t, result, myStruct{Name: "title"}) // assertion failed: ... (diff of the two structs) assert.Assert(t, is.Len(items, 3)) // assertion failed: expected [] (length 0) to have length 3 assert.Assert(t, len(sequence) != 0) // use Assert for NotEmpty // assertion failed: len(sequence) is 0 assert.Assert(t, is.Contains(mapping, "key")) // assertion failed: map[other:1] does not contain key // pointers and interface assert.Assert(t, ref == nil) // assertion failed: ref is not nil assert.Assert(t, ref != nil) // use Assert for NotNil // assertion failed: ref is nil # Assert and Check [Assert] and [Check] are very similar, they both accept a [cmp.Comparison], and fail the test when the comparison fails. The one difference is that Assert uses [testing.T.FailNow] to fail the test, which will end the test execution immediately. Check uses [testing.T.Fail] to fail the test, which allows it to return the result of the comparison, then proceed with the rest of the test case. Like [testing.T.FailNow], [Assert] must be called from the goroutine running the test, not from other goroutines created during the test. [Check] is safe to use from any goroutine. # Comparisons Package [gotest.tools/v3/assert/cmp] provides many common comparisons. Additional comparisons can be written to compare values in other ways. See the example Assert (CustomComparison). # Automated migration from testify gty-migrate-from-testify is a command which translates Go source code from testify assertions to the assertions provided by this package. See http://pkg.go.dev/gotest.tools/v3/assert/cmd/gty-migrate-from-testify. */ package assert // import "gotest.tools/v3/assert" import ( gocmp "github.com/google/go-cmp/cmp" "gotest.tools/v3/assert/cmp" "gotest.tools/v3/internal/assert" ) // BoolOrComparison can be a bool, [cmp.Comparison], or error. See [Assert] for // details about how this type is used. type BoolOrComparison interface{} // TestingT is the subset of [testing.T] (see also [testing.TB]) used by the assert package. type TestingT interface { FailNow() Fail() Log(args ...interface{}) } type helperT interface { Helper() } // Assert performs a comparison. If the comparison fails, the test is marked as // failed, a failure message is logged, and execution is stopped immediately. // // The comparison argument may be one of three types: // // bool // True is success. False is a failure. The failure message will contain // the literal source code of the expression. // // cmp.Comparison // Uses cmp.Result.Success() to check for success or failure. // The comparison is responsible for producing a helpful failure message. // http://pkg.go.dev/gotest.tools/v3/assert/cmp provides many common comparisons. // // error // A nil value is considered success, and a non-nil error is a failure. // The return value of error.Error is used as the failure message. // // Extra details can be added to the failure message using msgAndArgs. msgAndArgs // may be either a single string, or a format string and args that will be // passed to [fmt.Sprintf]. // // Assert uses [testing.TB.FailNow] to fail the test. Like t.FailNow, Assert must be called // from the goroutine running the test function, not from other // goroutines created during the test. Use [Check] from other goroutines. func Assert(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) { if ht, ok := t.(helperT); ok { ht.Helper() } if !assert.Eval(t, assert.ArgsFromComparisonCall, comparison, msgAndArgs...) { t.FailNow() } } // Check performs a comparison. If the comparison fails the test is marked as // failed, a failure message is printed, and Check returns false. If the comparison // is successful Check returns true. Check may be called from any goroutine. // // See [Assert] for details about the comparison arg and failure messages. func Check(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) bool { if ht, ok := t.(helperT); ok { ht.Helper() } if !assert.Eval(t, assert.ArgsFromComparisonCall, comparison, msgAndArgs...) { t.Fail() return false } return true } // NilError fails the test immediately if err is not nil, and includes err.Error // in the failure message. // // NilError uses [testing.TB.FailNow] to fail the test. Like t.FailNow, NilError must be // called from the goroutine running the test function, not from other // goroutines created during the test. Use [Check] from other goroutines. func NilError(t TestingT, err error, msgAndArgs ...interface{}) { if ht, ok := t.(helperT); ok { ht.Helper() } if !assert.Eval(t, assert.ArgsAfterT, err, msgAndArgs...) { t.FailNow() } } // Equal uses the == operator to assert two values are equal and fails the test // if they are not equal. // // If the comparison fails Equal will use the variable names and types of // x and y as part of the failure message to identify the actual and expected // values. // // assert.Equal(t, actual, expected) // // main_test.go:41: assertion failed: 1 (actual int) != 21 (expected int32) // // If either x or y are a multi-line string the failure message will include a // unified diff of the two values. If the values only differ by whitespace // the unified diff will be augmented by replacing whitespace characters with // visible characters to identify the whitespace difference. // // Equal uses [testing.T.FailNow] to fail the test. Like t.FailNow, Equal must be // called from the goroutine running the test function, not from other // goroutines created during the test. Use [Check] with [cmp.Equal] from other // goroutines. func Equal(t TestingT, x, y interface{}, msgAndArgs ...interface{}) { if ht, ok := t.(helperT); ok { ht.Helper() } if !assert.Eval(t, assert.ArgsAfterT, cmp.Equal(x, y), msgAndArgs...) { t.FailNow() } } // DeepEqual uses [github.com/google/go-cmp/cmp] // to assert two values are equal and fails the test if they are not equal. // // Package [gotest.tools/v3/assert/opt] provides some additional // commonly used Options. // // DeepEqual uses [testing.T.FailNow] to fail the test. Like t.FailNow, DeepEqual must be // called from the goroutine running the test function, not from other // goroutines created during the test. Use [Check] with [cmp.DeepEqual] from other // goroutines. func DeepEqual(t TestingT, x, y interface{}, opts ...gocmp.Option) { if ht, ok := t.(helperT); ok { ht.Helper() } if !assert.Eval(t, assert.ArgsAfterT, cmp.DeepEqual(x, y, opts...)) { t.FailNow() } } // Error fails the test if err is nil, or if err.Error is not equal to expected. // Both err.Error and expected will be included in the failure message. // Error performs an exact match of the error text. Use [ErrorContains] if only // part of the error message is relevant. Use [ErrorType] or [ErrorIs] to compare // errors by type. // // Error uses [testing.T.FailNow] to fail the test. Like t.FailNow, Error must be // called from the goroutine running the test function, not from other // goroutines created during the test. Use [Check] with [cmp.Error] from other // goroutines. func Error(t TestingT, err error, expected string, msgAndArgs ...interface{}) { if ht, ok := t.(helperT); ok { ht.Helper() } if !assert.Eval(t, assert.ArgsAfterT, cmp.Error(err, expected), msgAndArgs...) { t.FailNow() } } // ErrorContains fails the test if err is nil, or if err.Error does not // contain the expected substring. Both err.Error and the expected substring // will be included in the failure message. // // ErrorContains uses [testing.T.FailNow] to fail the test. Like t.FailNow, ErrorContains // must be called from the goroutine running the test function, not from other // goroutines created during the test. Use [Check] with [cmp.ErrorContains] from other // goroutines. func ErrorContains(t TestingT, err error, substring string, msgAndArgs ...interface{}) { if ht, ok := t.(helperT); ok { ht.Helper() } if !assert.Eval(t, assert.ArgsAfterT, cmp.ErrorContains(err, substring), msgAndArgs...) { t.FailNow() } } // ErrorType fails the test if err is nil, or err is not the expected type. // New code should use ErrorIs instead. // // Expected can be one of: // // func(error) bool // The function should return true if the error is the expected type. // // struct{} or *struct{} // A struct or a pointer to a struct. The assertion fails if the error is // not of the same type. // // *interface{} // A pointer to an interface type. The assertion fails if err does not // implement the interface. // // reflect.Type // The assertion fails if err does not implement the reflect.Type. // // ErrorType uses [testing.T.FailNow] to fail the test. Like t.FailNow, ErrorType // must be called from the goroutine running the test function, not from other // goroutines created during the test. Use [Check] with [cmp.ErrorType] from other // goroutines. // // Deprecated: Use [ErrorIs] func ErrorType(t TestingT, err error, expected interface{}, msgAndArgs ...interface{}) { if ht, ok := t.(helperT); ok { ht.Helper() } if !assert.Eval(t, assert.ArgsAfterT, cmp.ErrorType(err, expected), msgAndArgs...) { t.FailNow() } } // ErrorIs fails the test if err is nil, or the error does not match expected // when compared using errors.Is. See [errors.Is] for // accepted arguments. // // ErrorIs uses [testing.T.FailNow] to fail the test. Like t.FailNow, ErrorIs // must be called from the goroutine running the test function, not from other // goroutines created during the test. Use [Check] with [cmp.ErrorIs] from other // goroutines. func ErrorIs(t TestingT, err error, expected error, msgAndArgs ...interface{}) { if ht, ok := t.(helperT); ok { ht.Helper() } if !assert.Eval(t, assert.ArgsAfterT, cmp.ErrorIs(err, expected), msgAndArgs...) { t.FailNow() } } gotest.tools-3.5.1/assert/assert_ext_test.go000066400000000000000000000102171463245364400212500ustar00rootroot00000000000000package assert_test import ( "go/ast" "go/parser" "go/token" "os" "runtime" "strings" "testing" "gotest.tools/v3/assert" "gotest.tools/v3/internal/source" ) func TestEqual_WithGoldenUpdate(t *testing.T) { t.Run("assert failed with -update=false", func(t *testing.T) { ft := &fakeTestingT{} actual := `not this value` assert.Equal(ft, actual, expectedOne) assert.Assert(t, ft.failNowed) }) t.Run("var is updated when -update=true", func(t *testing.T) { patchUpdate(t) t.Cleanup(func() { resetVariable(t, "expectedOne", "") }) actual := `this is the actual value that we are testing ` assert.Equal(t, actual, expectedOne) raw, err := os.ReadFile(fileName(t)) assert.NilError(t, err) expected := "var expectedOne = `this is the\nactual value\nthat we are testing\n`" assert.Assert(t, strings.Contains(string(raw), expected), "actual=%v", string(raw)) }) t.Run("const is updated when -update=true", func(t *testing.T) { patchUpdate(t) t.Cleanup(func() { resetVariable(t, "expectedTwo", "") }) actual := `this is the new expected value ` assert.Equal(t, actual, expectedTwo) raw, err := os.ReadFile(fileName(t)) assert.NilError(t, err) expected := "const expectedTwo = `this is the new\nexpected value\n`" assert.Assert(t, strings.Contains(string(raw), expected), "actual=%v", string(raw)) }) t.Run("var inside function is updated when -update=true", func(t *testing.T) { patchUpdate(t) t.Cleanup(func() { resetVariable(t, "expectedInsideFunc", "") }) actual := `this is the new expected value for var inside function ` expectedInsideFunc := `` assert.Equal(t, actual, expectedInsideFunc) raw, err := os.ReadFile(fileName(t)) assert.NilError(t, err) expected := "expectedInsideFunc := `this is the new\nexpected value\nfor var inside function\n`" assert.Assert(t, strings.Contains(string(raw), expected), "actual=%v", string(raw)) }) t.Run("const inside function is updated when -update=true", func(t *testing.T) { patchUpdate(t) t.Cleanup(func() { resetVariable(t, "expectedConstInsideFunc", "") }) actual := `this is the new expected value for const inside function ` const expectedConstInsideFunc = `` assert.Equal(t, actual, expectedConstInsideFunc) raw, err := os.ReadFile(fileName(t)) assert.NilError(t, err) expected := "const expectedConstInsideFunc = `this is the new\nexpected value\nfor const inside function\n`" assert.Assert(t, strings.Contains(string(raw), expected), "actual=%v", string(raw)) }) } // expectedOne is updated by running the tests with -update var expectedOne = `` // expectedTwo is updated by running the tests with -update const expectedTwo = `` func patchUpdate(t *testing.T) { source.Update = true t.Cleanup(func() { source.Update = false }) } func fileName(t *testing.T) string { t.Helper() _, filename, _, ok := runtime.Caller(1) assert.Assert(t, ok, "failed to get call stack") return filename } func resetVariable(t *testing.T, varName string, value string) { t.Helper() _, filename, _, ok := runtime.Caller(1) assert.Assert(t, ok, "failed to get call stack") fileset := token.NewFileSet() astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors|parser.ParseComments) assert.NilError(t, err) var ident *ast.Ident ast.Inspect(astFile, func(n ast.Node) bool { switch v := n.(type) { case *ast.AssignStmt: if len(v.Lhs) == 1 { if id, ok := v.Lhs[0].(*ast.Ident); ok { if id.Name == varName { ident = id return false } } } case *ast.ValueSpec: for _, id := range v.Names { if id.Name == varName { ident = id return false } } } return true }) assert.Assert(t, ident != nil, "failed to get ident for %s", varName) err = source.UpdateVariable(filename, fileset, astFile, ident, value) assert.NilError(t, err, "failed to reset file") } type fakeTestingT struct { failNowed bool failed bool msgs []string } func (f *fakeTestingT) FailNow() { f.failNowed = true } func (f *fakeTestingT) Fail() { f.failed = true } func (f *fakeTestingT) Log(args ...interface{}) { f.msgs = append(f.msgs, args[0].(string)) } func (f *fakeTestingT) Helper() {} gotest.tools-3.5.1/assert/assert_test.go000066400000000000000000000271231463245364400203740ustar00rootroot00000000000000package assert import ( "fmt" "os" "testing" gocmp "github.com/google/go-cmp/cmp" "gotest.tools/v3/assert/cmp" ) type fakeTestingT struct { failNowed bool failed bool msgs []string } func (f *fakeTestingT) FailNow() { f.failNowed = true } func (f *fakeTestingT) Fail() { f.failed = true } func (f *fakeTestingT) Log(args ...interface{}) { f.msgs = append(f.msgs, fmt.Sprint(args...)) } func (f *fakeTestingT) Helper() {} func TestAssert_WithBinaryExpression_Failures(t *testing.T) { t.Run("equal", func(t *testing.T) { fakeT := &fakeTestingT{} Assert(fakeT, 1 == 6) expectFailNowed(t, fakeT, "assertion failed: 1 is not 6") }) t.Run("not equal", func(t *testing.T) { fakeT := &fakeTestingT{} a := 1 Assert(fakeT, a != 1) expectFailNowed(t, fakeT, "assertion failed: a is 1") }) t.Run("greater than", func(t *testing.T) { fakeT := &fakeTestingT{} Assert(fakeT, 1 > 5) expectFailNowed(t, fakeT, "assertion failed: 1 is <= 5") }) t.Run("less than", func(t *testing.T) { fakeT := &fakeTestingT{} Assert(fakeT, 5 < 1) expectFailNowed(t, fakeT, "assertion failed: 5 is >= 1") }) t.Run("greater than or equal", func(t *testing.T) { fakeT := &fakeTestingT{} Assert(fakeT, 1 >= 5) expectFailNowed(t, fakeT, "assertion failed: 1 is less than 5") }) t.Run("less than or equal", func(t *testing.T) { fakeT := &fakeTestingT{} Assert(fakeT, 6 <= 2) expectFailNowed(t, fakeT, "assertion failed: 6 is greater than 2") }) } func TestAssertWithBoolIdent(t *testing.T) { fakeT := &fakeTestingT{} var ok bool Assert(fakeT, ok) expectFailNowed(t, fakeT, "assertion failed: ok is false") } func TestAssertWithBoolFailureNotEqual(t *testing.T) { fakeT := &fakeTestingT{} var err error Assert(fakeT, err != nil) expectFailNowed(t, fakeT, "assertion failed: err is nil") } func TestAssertWithBoolFailureNotTrue(t *testing.T) { fakeT := &fakeTestingT{} badNews := true Assert(fakeT, !badNews) expectFailNowed(t, fakeT, "assertion failed: badNews is true") } func TestAssertWithBoolFailureAndExtraMessage(t *testing.T) { fakeT := &fakeTestingT{} Assert(fakeT, 1 > 5, "sometimes things fail") expectFailNowed(t, fakeT, "assertion failed: 1 is <= 5: sometimes things fail") } func TestAssertWithBoolSuccess(t *testing.T) { fakeT := &fakeTestingT{} Assert(fakeT, 1 < 5) expectSuccess(t, fakeT) } func TestAssertWithBoolMultiLineFailure(t *testing.T) { fakeT := &fakeTestingT{} Assert(fakeT, func() bool { for range []int{1, 2, 3, 4} { } return false }()) expectFailNowed(t, fakeT, `assertion failed: expression is false: func() bool { for range []int{1, 2, 3, 4} { } return false }()`) } type exampleComparison struct { success bool message string } func (c exampleComparison) Compare() (bool, string) { return c.success, c.message } func TestAssertWithComparisonSuccess(t *testing.T) { fakeT := &fakeTestingT{} cmp := exampleComparison{success: true} Assert(fakeT, cmp.Compare) expectSuccess(t, fakeT) } func TestAssertWithComparisonFailure(t *testing.T) { fakeT := &fakeTestingT{} cmp := exampleComparison{message: "oops, not good"} Assert(fakeT, cmp.Compare) expectFailNowed(t, fakeT, "assertion failed: oops, not good") } func TestAssertWithComparisonAndExtraMessage(t *testing.T) { fakeT := &fakeTestingT{} cmp := exampleComparison{message: "oops, not good"} Assert(fakeT, cmp.Compare, "extra stuff %v", true) expectFailNowed(t, fakeT, "assertion failed: oops, not good: extra stuff true") } type customError struct { field bool } func (e *customError) Error() string { // access a field of the receiver to simulate the behaviour of most // implementations, and test handling of non-nil typed errors. e.field = true return "custom error" } func TestNilError(t *testing.T) { t.Run("nil interface", func(t *testing.T) { fakeT := &fakeTestingT{} var err error NilError(fakeT, err) expectSuccess(t, fakeT) }) t.Run("nil literal", func(t *testing.T) { fakeT := &fakeTestingT{} NilError(fakeT, nil) expectSuccess(t, fakeT) }) t.Run("interface with non-nil type", func(t *testing.T) { fakeT := &fakeTestingT{} var customErr *customError NilError(fakeT, customErr) expected := "assertion failed: error is not nil: error has type *assert.customError" expectFailNowed(t, fakeT, expected) }) t.Run("non-nil error", func(t *testing.T) { fakeT := &fakeTestingT{} NilError(fakeT, fmt.Errorf("this is the error")) expectFailNowed(t, fakeT, "assertion failed: error is not nil: this is the error") }) t.Run("non-nil error with struct type", func(t *testing.T) { fakeT := &fakeTestingT{} err := structError{} NilError(fakeT, err) expectFailNowed(t, fakeT, "assertion failed: error is not nil: this is a struct") }) t.Run("non-nil error with map type", func(t *testing.T) { fakeT := &fakeTestingT{} var err mapError NilError(fakeT, err) expectFailNowed(t, fakeT, "assertion failed: error is not nil: ") }) } type structError struct{} func (structError) Error() string { return "this is a struct" } type mapError map[int]string func (m mapError) Error() string { return m[0] } func TestCheckFailure(t *testing.T) { fakeT := &fakeTestingT{} if Check(fakeT, 1 == 2) { t.Error("expected check to return false on failure") } expectFailed(t, fakeT, "assertion failed: 1 is not 2") } func TestCheckSuccess(t *testing.T) { fakeT := &fakeTestingT{} if !Check(fakeT, true) { t.Error("expected check to return true on success") } expectSuccess(t, fakeT) } func TestCheckEqualFailure(t *testing.T) { fakeT := &fakeTestingT{} actual, expected := 5, 9 Check(fakeT, cmp.Equal(actual, expected)) expectFailed(t, fakeT, "assertion failed: 5 (actual int) != 9 (expected int)") } func TestCheck_MultipleFunctionsOnTheSameLine(t *testing.T) { fakeT := &fakeTestingT{} f := func(b bool) {} f(Check(fakeT, false)) // TODO: update the expected when there is a more correct fix expectFailed(t, fakeT, "assertion failed: but assert failed to find the expression to print") } func TestEqualSuccess(t *testing.T) { fakeT := &fakeTestingT{} Equal(fakeT, 1, 1) expectSuccess(t, fakeT) Equal(fakeT, "abcd", "abcd") expectSuccess(t, fakeT) } func TestEqualFailure(t *testing.T) { fakeT := &fakeTestingT{} actual, expected := 1, 3 Equal(fakeT, actual, expected) expectFailNowed(t, fakeT, "assertion failed: 1 (actual int) != 3 (expected int)") } func TestEqualFailureTypes(t *testing.T) { fakeT := &fakeTestingT{} Equal(fakeT, 3, uint(3)) expectFailNowed(t, fakeT, `assertion failed: 3 (int) != 3 (uint)`) } func TestEqualFailureWithSelectorArgument(t *testing.T) { fakeT := &fakeTestingT{} type tc struct { expected string } var testcase = tc{expected: "foo"} Equal(fakeT, "ok", testcase.expected) expectFailNowed(t, fakeT, "assertion failed: ok (string) != foo (testcase.expected string)") } func TestEqualFailureWithIndexExpr(t *testing.T) { fakeT := &fakeTestingT{} expected := map[string]string{"foo": "bar"} Equal(fakeT, "ok", expected["foo"]) expectFailNowed(t, fakeT, `assertion failed: ok (string) != bar (expected["foo"] string)`) } func TestEqualFailureWithCallExprArgument(t *testing.T) { fakeT := &fakeTestingT{} ce := customError{} Equal(fakeT, "", ce.Error()) expectFailNowed(t, fakeT, "assertion failed: (string) != custom error (string)") } func TestAssertFailureWithOfflineComparison(t *testing.T) { fakeT := &fakeTestingT{} a := 1 b := 2 // store comparison in a variable, so ast lookup can't find it comparison := cmp.Equal(a, b) Assert(fakeT, comparison) // expected value wont have variable names expectFailNowed(t, fakeT, "assertion failed: 1 (int) != 2 (int)") } type testingT interface { Errorf(msg string, args ...interface{}) Fatalf(msg string, args ...interface{}) } func expectFailNowed(t testingT, fakeT *fakeTestingT, expected string) { if ht, ok := t.(helperT); ok { ht.Helper() } if fakeT.failed { t.Errorf("should not have failed, got messages %s", fakeT.msgs) } if !fakeT.failNowed { t.Fatalf("should have failNowed with message %s", expected) } if fakeT.msgs[0] != expected { t.Fatalf("should have failure message %q, got %q", expected, fakeT.msgs[0]) } } func expectFailed(t testingT, fakeT *fakeTestingT, expected string) { if ht, ok := t.(helperT); ok { ht.Helper() } if fakeT.failNowed { t.Errorf("should not have failNowed, got messages %s", fakeT.msgs) } if !fakeT.failed { t.Fatalf("should have failed with message %s", expected) } if fakeT.msgs[0] != expected { t.Fatalf("should have failure message %q, got %q", expected, fakeT.msgs[0]) } } func expectSuccess(t testingT, fakeT *fakeTestingT) { if ht, ok := t.(helperT); ok { ht.Helper() } if fakeT.failNowed { t.Errorf("should not have failNowed, got messages %s", fakeT.msgs) } if fakeT.failed { t.Errorf("should not have failed, got messages %s", fakeT.msgs) } } type stub struct { a string b int } func TestDeepEqualSuccess(t *testing.T) { actual := stub{"ok", 1} expected := stub{"ok", 1} fakeT := &fakeTestingT{} DeepEqual(fakeT, actual, expected, gocmp.AllowUnexported(stub{})) expectSuccess(t, fakeT) } func TestDeepEqualFailure(t *testing.T) { actual := stub{"ok", 1} expected := stub{"ok", 2} fakeT := &fakeTestingT{} DeepEqual(fakeT, actual, expected, gocmp.AllowUnexported(stub{})) if !fakeT.failNowed { t.Fatal("should have failNowed") } } func TestErrorFailure(t *testing.T) { t.Run("nil error", func(t *testing.T) { fakeT := &fakeTestingT{} var err error Error(fakeT, err, "this error") expectFailNowed(t, fakeT, "assertion failed: expected an error, got nil") }) t.Run("different error", func(t *testing.T) { fakeT := &fakeTestingT{} err := fmt.Errorf("the actual error") Error(fakeT, err, "this error") expected := `assertion failed: expected error "this error", got "the actual error"` expectFailNowed(t, fakeT, expected) }) } func TestErrorContainsFailure(t *testing.T) { t.Run("nil error", func(t *testing.T) { fakeT := &fakeTestingT{} var err error ErrorContains(fakeT, err, "this error") expectFailNowed(t, fakeT, "assertion failed: expected an error, got nil") }) t.Run("different error", func(t *testing.T) { fakeT := &fakeTestingT{} err := fmt.Errorf("the actual error") ErrorContains(fakeT, err, "this error") expected := `assertion failed: expected error to contain "this error", got "the actual error"` expectFailNowed(t, fakeT, expected) }) } func TestErrorTypeFailure(t *testing.T) { t.Run("nil error", func(t *testing.T) { fakeT := &fakeTestingT{} var err error ErrorType(fakeT, err, os.IsNotExist) expectFailNowed(t, fakeT, "assertion failed: error is nil, not os.IsNotExist") }) t.Run("different error", func(t *testing.T) { fakeT := &fakeTestingT{} err := fmt.Errorf("the actual error") ErrorType(fakeT, err, os.IsNotExist) expected := `assertion failed: error is the actual error (*errors.errorString), not os.IsNotExist` expectFailNowed(t, fakeT, expected) }) } func TestErrorIs(t *testing.T) { t.Run("nil error", func(t *testing.T) { fakeT := &fakeTestingT{} var err error ErrorIs(fakeT, err, os.ErrNotExist) expected := `assertion failed: error is nil, not "file does not exist" (os.ErrNotExist)` expectFailNowed(t, fakeT, expected) }) t.Run("different error", func(t *testing.T) { fakeT := &fakeTestingT{} err := fmt.Errorf("the actual error") ErrorIs(fakeT, err, os.ErrNotExist) expected := `assertion failed: error is "the actual error", not "file does not exist" (os.ErrNotExist)` expectFailNowed(t, fakeT, expected) }) t.Run("same error", func(t *testing.T) { fakeT := &fakeTestingT{} err := fmt.Errorf("some wrapping: %w", os.ErrNotExist) ErrorIs(fakeT, err, os.ErrNotExist) expectSuccess(t, fakeT) }) } gotest.tools-3.5.1/assert/cmd/000077500000000000000000000000001463245364400162435ustar00rootroot00000000000000gotest.tools-3.5.1/assert/cmd/gty-migrate-from-testify/000077500000000000000000000000001463245364400231225ustar00rootroot00000000000000gotest.tools-3.5.1/assert/cmd/gty-migrate-from-testify/call.go000066400000000000000000000125621463245364400243720ustar00rootroot00000000000000package main import ( "bytes" "fmt" "go/ast" "go/format" "go/token" ) // call wraps a testify/assert ast.CallExpr and exposes properties of the // expression to facilitate migrating the expression to a gotest.tools/v3/assert type call struct { fileset *token.FileSet expr *ast.CallExpr xIdent *ast.Ident selExpr *ast.SelectorExpr // assert is Assert (if the testify package was require), or Check (if the // testify package was assert). assert string } func (c call) String() string { buf := new(bytes.Buffer) _ = format.Node(buf, token.NewFileSet(), c.expr) return buf.String() } func (c call) StringWithFileInfo() string { if c.fileset.File(c.expr.Pos()) == nil { return fmt.Sprintf("%s at unknown file", c) } return fmt.Sprintf("%s at %s:%d", c, relativePath(c.fileset.File(c.expr.Pos()).Name()), c.fileset.Position(c.expr.Pos()).Line) } // testingT returns the first argument of the call, which is assumed to be a // *ast.Ident of *testing.T or a compatible interface. func (c call) testingT() ast.Expr { if len(c.expr.Args) == 0 { return nil } return c.expr.Args[0] } // extraArgs returns the arguments of the expression starting from index func (c call) extraArgs(index int) []ast.Expr { if len(c.expr.Args) <= index { return nil } return c.expr.Args[index:] } // args returns a range of arguments from the expression func (c call) args(from, to int) []ast.Expr { return c.expr.Args[from:to] } // arg returns a single argument from the expression func (c call) arg(index int) ast.Expr { return c.expr.Args[index] } // newCallFromCallExpr returns a new call build from a ast.CallExpr. Returns false // if the call expression does not have the expected ast nodes. func newCallFromCallExpr(callExpr *ast.CallExpr, migration migration) (call, bool) { c := call{} selector, ok := callExpr.Fun.(*ast.SelectorExpr) if !ok { return c, false } ident, ok := selector.X.(*ast.Ident) if !ok { return c, false } return call{ fileset: migration.fileset, xIdent: ident, selExpr: selector, expr: callExpr, }, true } // newTestifyCallFromNode returns a call that wraps a valid testify assertion. // Returns false if the call expression is not a testify assertion. func newTestifyCallFromNode(callExpr *ast.CallExpr, migration migration) (call, bool) { tcall, ok := newCallFromCallExpr(callExpr, migration) if !ok { return tcall, false } testifyNewAssignStmt := testifyAssertionsAssignment(tcall, migration) switch { case testifyNewAssignStmt != nil: return updateCallForTestifyNew(tcall, testifyNewAssignStmt, migration) case isTestifyPkgCall(tcall, migration): tcall.assert = migration.importNames.funcNameFromTestifyName(tcall.xIdent.Name) return tcall, true } return tcall, false } // isTestifyPkgCall returns true if the call is a testify package-level assertion // (as apposed to an assertion method on the Assertions type) // // TODO: check if the xIdent.Obj.Decl is an import declaration instead of // assuming that a name matching the import name is always an import. Some code // may shadow import names, which could lead to incorrect results. func isTestifyPkgCall(tcall call, migration migration) bool { return migration.importNames.matchesTestify(tcall.xIdent) } // testifyAssertionsAssignment returns an ast.AssignStmt if the call is a testify // call from an Assertions object returned from assert.New(t) (not a package level // assert). Otherwise returns nil. func testifyAssertionsAssignment(tcall call, migration migration) *ast.AssignStmt { if tcall.xIdent.Obj == nil { return nil } assignStmt, ok := tcall.xIdent.Obj.Decl.(*ast.AssignStmt) if !ok { return nil } if isAssignmentFromAssertNew(assignStmt, migration) { return assignStmt } return nil } func updateCallForTestifyNew( tcall call, testifyNewAssignStmt *ast.AssignStmt, migration migration, ) (call, bool) { testifyNewCallExpr := callExprFromAssignment(testifyNewAssignStmt) if testifyNewCallExpr == nil { return tcall, false } testifyNewCall, ok := newCallFromCallExpr(testifyNewCallExpr, migration) if !ok { return tcall, false } tcall.assert = migration.importNames.funcNameFromTestifyName(testifyNewCall.xIdent.Name) tcall.expr = addMissingTestingTArgToCallExpr(tcall.expr, testifyNewCall.testingT()) return tcall, true } // addMissingTestingTArgToCallExpr adds a testingT arg as the first arg of the // ast.CallExpr and returns a copy of the ast.CallExpr func addMissingTestingTArgToCallExpr(callExpr *ast.CallExpr, testingT ast.Expr) *ast.CallExpr { return &ast.CallExpr{ Fun: callExpr.Fun, Args: append([]ast.Expr{removePos(testingT)}, callExpr.Args...), } } func removePos(node ast.Expr) ast.Expr { switch typed := node.(type) { case *ast.Ident: return &ast.Ident{Name: typed.Name} } return node } // TODO: use pkgInfo and walkForType instead? func isAssignmentFromAssertNew(assign *ast.AssignStmt, migration migration) bool { callExpr := callExprFromAssignment(assign) if callExpr == nil { return false } tcall, ok := newCallFromCallExpr(callExpr, migration) if !ok { return false } if !migration.importNames.matchesTestify(tcall.xIdent) { return false } if len(tcall.expr.Args) != 1 { return false } return tcall.selExpr.Sel.Name == "New" } func callExprFromAssignment(assign *ast.AssignStmt) *ast.CallExpr { if len(assign.Rhs) != 1 { return nil } callExpr, ok := assign.Rhs[0].(*ast.CallExpr) if !ok { return nil } return callExpr } gotest.tools-3.5.1/assert/cmd/gty-migrate-from-testify/call_test.go000066400000000000000000000012741463245364400254270ustar00rootroot00000000000000package main import ( "go/ast" "go/token" "testing" "gotest.tools/v3/assert" ) func TestCall_String(t *testing.T) { c := &call{ expr: &ast.CallExpr{Fun: ast.NewIdent("myFunc")}, } assert.Equal(t, c.String(), "myFunc()") } func TestCall_StringWithFileInfo(t *testing.T) { c := &call{ fileset: token.NewFileSet(), expr: &ast.CallExpr{ Fun: &ast.Ident{ Name: "myFunc", NamePos: 17, }}, } t.Run("unknown file", func(t *testing.T) { assert.Equal(t, c.StringWithFileInfo(), "myFunc() at unknown file") }) t.Run("at position", func(t *testing.T) { c.fileset.AddFile("source.go", 10, 100) assert.Equal(t, c.StringWithFileInfo(), "myFunc() at source.go:1") }) } gotest.tools-3.5.1/assert/cmd/gty-migrate-from-testify/doc.go000066400000000000000000000007561463245364400242260ustar00rootroot00000000000000/* Command gty-migrate-from-testify migrates packages from testify/assert and testify/require to [gotest.tools/v3/assert]. $ go get gotest.tools/v3/assert/cmd/gty-migrate-from-testify Usage: gty-migrate-from-testify [OPTIONS] PACKAGE [PACKAGE...] See --help for full usage. To run on all packages (including external test packages) use: go list \ -f '{{.ImportPath}} {{if .XTestGoFiles}}{{"\n"}}{{.ImportPath}}_test{{end}}' \ ./... | xargs gty-migrate-from-testify */ package main gotest.tools-3.5.1/assert/cmd/gty-migrate-from-testify/flags.go000066400000000000000000000005631463245364400245510ustar00rootroot00000000000000package main import ( "encoding/csv" "strings" ) type stringSliceValue []string func (s stringSliceValue) String() string { return strings.Join(s, ", ") } func (s *stringSliceValue) Set(raw string) error { if raw == "" { return nil } v, err := csv.NewReader(strings.NewReader(raw)).Read() if err != nil { return err } *s = append(*s, v...) return nil } gotest.tools-3.5.1/assert/cmd/gty-migrate-from-testify/main.go000066400000000000000000000142221463245364400243760ustar00rootroot00000000000000package main import ( "bytes" "errors" "flag" "fmt" "go/ast" "go/format" "go/token" "log" "os" "path" "path/filepath" "strings" "golang.org/x/tools/go/packages" "golang.org/x/tools/imports" ) type options struct { pkgs []string dryRun bool debug bool cmpImportName string showLoaderErrors bool buildFlags []string localImportPath string } func main() { name := os.Args[0] flags, opts := setupFlags(name) handleExitError(name, flags.Parse(os.Args[1:])) setupLogging(opts) opts.pkgs = flags.Args() handleExitError(name, run(*opts)) } func setupLogging(opts *options) { log.SetFlags(0) enableDebug = opts.debug } var enableDebug = false func debugf(msg string, args ...interface{}) { if enableDebug { log.Printf("DEBUG: "+msg, args...) } } func setupFlags(name string) (*flag.FlagSet, *options) { opts := options{} flags := flag.NewFlagSet(name, flag.ContinueOnError) flags.BoolVar(&opts.dryRun, "dry-run", false, "don't write changes to file") flags.BoolVar(&opts.debug, "debug", false, "enable debug logging") flags.StringVar(&opts.cmpImportName, "cmp-pkg-import-alias", "is", "import alias to use for the assert/cmp package") flags.BoolVar(&opts.showLoaderErrors, "print-loader-errors", false, "print errors from loading source") flags.Var((*stringSliceValue)(&opts.buildFlags), "build-flags", "build flags to pass to Go when loading source files") flags.StringVar(&opts.localImportPath, "local-import-path", "", "value to pass to 'goimports -local' flag for sorting local imports") flags.Usage = func() { fmt.Fprintf(os.Stderr, `Usage: %s [OPTIONS] PACKAGE [PACKAGE...] Migrate calls from testify/{assert|require} to gotest.tools/v3/assert. `, name) flags.PrintDefaults() } return flags, &opts } func handleExitError(name string, err error) { switch { case err == nil: return case errors.Is(err, flag.ErrHelp): os.Exit(0) default: log.Println(name + ": Error: " + err.Error()) os.Exit(3) } } func run(opts options) error { imports.LocalPrefix = opts.localImportPath fset := token.NewFileSet() pkgs, err := loadPackages(opts, fset) if err != nil { return fmt.Errorf("failed to load program: %w", err) } debugf("package count: %d", len(pkgs)) for _, pkg := range pkgs { debugf("file count for package %v: %d", pkg.PkgPath, len(pkg.Syntax)) for _, astFile := range pkg.Syntax { absFilename := fset.File(astFile.Pos()).Name() filename := relativePath(absFilename) importNames := newImportNames(astFile.Imports, opts) if !importNames.hasTestifyImports() { debugf("skipping file %s, no imports", filename) continue } debugf("migrating %s with imports: %#v", filename, importNames) m := migration{ file: astFile, fileset: fset, importNames: importNames, pkgInfo: pkg.TypesInfo, } migrateFile(m) if opts.dryRun { continue } raw, err := formatFile(m) if err != nil { return fmt.Errorf("failed to format %s: %w", filename, err) } if err := os.WriteFile(absFilename, raw, 0); err != nil { return fmt.Errorf("failed to write file %s: %w", filename, err) } } } return nil } var loadMode = packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedDeps | packages.NeedImports | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedTypesSizes | packages.NeedSyntax func loadPackages(opts options, fset *token.FileSet) ([]*packages.Package, error) { conf := &packages.Config{ Mode: loadMode, Fset: fset, Tests: true, Logf: debugf, BuildFlags: opts.buildFlags, } pkgs, err := packages.Load(conf, opts.pkgs...) if err != nil { return nil, err } if opts.showLoaderErrors { packages.PrintErrors(pkgs) } return pkgs, nil } func relativePath(p string) string { cwd, err := os.Getwd() if err != nil { return p } rel, err := filepath.Rel(cwd, p) if err != nil { return p } return rel } type importNames struct { testifyAssert string testifyRequire string assert string cmp string } func (p importNames) hasTestifyImports() bool { return p.testifyAssert != "" || p.testifyRequire != "" } func (p importNames) matchesTestify(ident *ast.Ident) bool { return ident.Name == p.testifyAssert || ident.Name == p.testifyRequire } func (p importNames) funcNameFromTestifyName(name string) string { switch name { case p.testifyAssert: return funcNameCheck case p.testifyRequire: return funcNameAssert default: panic("unexpected testify import name " + name) } } func newImportNames(imports []*ast.ImportSpec, opt options) importNames { defaultAssertAlias := path.Base(pkgAssert) importNames := importNames{ assert: defaultAssertAlias, cmp: path.Base(pkgCmp), } for _, spec := range imports { switch strings.Trim(spec.Path.Value, `"`) { case pkgTestifyAssert, pkgGopkgTestifyAssert: importNames.testifyAssert = identOrDefault(spec.Name, "assert") case pkgTestifyRequire, pkgGopkgTestifyRequire: importNames.testifyRequire = identOrDefault(spec.Name, "require") default: pkgPath := strings.Trim(spec.Path.Value, `"`) switch { // v3/assert is already imported and has an alias case pkgPath == pkgAssert: if spec.Name != nil && spec.Name.Name != "" { importNames.assert = spec.Name.Name } continue // some other package is imported as assert case importedAs(spec, path.Base(pkgAssert)) && importNames.assert == defaultAssertAlias: importNames.assert = "gtyassert" } } } if opt.cmpImportName != "" { importNames.cmp = opt.cmpImportName } return importNames } func importedAs(spec *ast.ImportSpec, pkg string) bool { if path.Base(strings.Trim(spec.Path.Value, `"`)) == pkg { return true } return spec.Name != nil && spec.Name.Name == pkg } func identOrDefault(ident *ast.Ident, def string) string { if ident != nil { return ident.Name } return def } func formatFile(migration migration) ([]byte, error) { buf := new(bytes.Buffer) err := format.Node(buf, migration.fileset, migration.file) if err != nil { return nil, err } filename := migration.fileset.File(migration.file.Pos()).Name() return imports.Process(filename, buf.Bytes(), nil) } gotest.tools-3.5.1/assert/cmd/gty-migrate-from-testify/main_test.go000066400000000000000000000023101463245364400254300ustar00rootroot00000000000000package main import ( "os" "testing" "github.com/google/go-cmp/cmp" "gotest.tools/v3/assert" "gotest.tools/v3/env" "gotest.tools/v3/fs" "gotest.tools/v3/golden" ) func TestRun(t *testing.T) { setupLogging(&options{}) dir := fs.NewDir(t, "test-run", fs.WithDir("src/example.com/example", fs.FromDir("testdata/full"))) defer dir.Remove() defer env.Patch(t, "GO111MODULE", "off")() defer env.Patch(t, "GOPATH", dir.Path())() err := run(options{ pkgs: []string{"example.com/example"}, showLoaderErrors: true, }) assert.NilError(t, err) raw, err := os.ReadFile(dir.Join("src/example.com/example/some_test.go")) assert.NilError(t, err) golden.Assert(t, string(raw), "full-expected/some_test.go") } func TestSetupFlags(t *testing.T) { flags, opts := setupFlags("testing") assert.Assert(t, flags.Usage != nil) err := flags.Parse([]string{ "--dry-run", "--debug", "--cmp-pkg-import-alias=foo", "--print-loader-errors", }) assert.NilError(t, err) expected := &options{ dryRun: true, debug: true, cmpImportName: "foo", showLoaderErrors: true, } assert.DeepEqual(t, opts, expected, cmpOptions) } var cmpOptions = cmp.AllowUnexported(options{}) gotest.tools-3.5.1/assert/cmd/gty-migrate-from-testify/migrate.go000066400000000000000000000242401463245364400251030ustar00rootroot00000000000000package main import ( "go/ast" "go/token" "go/types" "log" "path" "golang.org/x/tools/go/ast/astutil" ) const ( pkgTestifyAssert = "github.com/stretchr/testify/assert" pkgGopkgTestifyAssert = "gopkg.in/stretchr/testify.v1/assert" pkgTestifyRequire = "github.com/stretchr/testify/require" pkgGopkgTestifyRequire = "gopkg.in/stretchr/testify.v1/require" pkgAssert = "gotest.tools/v3/assert" pkgCmp = "gotest.tools/v3/assert/cmp" ) const ( funcNameAssert = "Assert" funcNameCheck = "Check" ) var allTestifyPks = []string{ pkgTestifyAssert, pkgTestifyRequire, pkgGopkgTestifyAssert, pkgGopkgTestifyRequire, } type migration struct { file *ast.File fileset *token.FileSet importNames importNames pkgInfo *types.Info } func migrateFile(migration migration) { astutil.Apply(migration.file, nil, replaceCalls(migration)) updateImports(migration) } func updateImports(migration migration) { for _, remove := range allTestifyPks { astutil.DeleteImport(migration.fileset, migration.file, remove) } var alias string if migration.importNames.assert != path.Base(pkgAssert) { alias = migration.importNames.assert } astutil.AddNamedImport(migration.fileset, migration.file, alias, pkgAssert) if migration.importNames.cmp != path.Base(pkgCmp) { alias = migration.importNames.cmp } astutil.AddNamedImport(migration.fileset, migration.file, alias, pkgCmp) } type emptyNode struct{} func (n emptyNode) Pos() token.Pos { return 0 } func (n emptyNode) End() token.Pos { return 0 } var removeNode = emptyNode{} func replaceCalls(migration migration) func(cursor *astutil.Cursor) bool { return func(cursor *astutil.Cursor) bool { var newNode ast.Node switch typed := cursor.Node().(type) { case *ast.SelectorExpr: newNode = getReplacementTestingT(typed, migration.importNames) case *ast.CallExpr: newNode = getReplacementAssertion(typed, migration) case *ast.AssignStmt: newNode = getReplacementAssignment(typed, migration) } switch newNode { case nil: case removeNode: cursor.Delete() default: cursor.Replace(newNode) } return true } } func getReplacementTestingT(selector *ast.SelectorExpr, names importNames) ast.Node { xIdent, ok := selector.X.(*ast.Ident) if !ok { return nil } if selector.Sel.Name != "TestingT" || !names.matchesTestify(xIdent) { return nil } return &ast.SelectorExpr{ X: &ast.Ident{Name: names.assert, NamePos: xIdent.NamePos}, Sel: selector.Sel, } } func getReplacementAssertion(callExpr *ast.CallExpr, migration migration) ast.Node { tcall, ok := newTestifyCallFromNode(callExpr, migration) if !ok { return nil } if len(tcall.expr.Args) < 2 { return convertTestifySingleArgCall(tcall) } return convertTestifyAssertion(tcall, migration) } func getReplacementAssignment(assign *ast.AssignStmt, migration migration) ast.Node { if isAssignmentFromAssertNew(assign, migration) { return removeNode } return nil } func convertTestifySingleArgCall(tcall call) ast.Node { switch tcall.selExpr.Sel.Name { case "TestingT": // handled as SelectorExpr return nil case "New": // handled by getReplacementAssignment return nil default: log.Printf("%s: skipping unknown selector", tcall.StringWithFileInfo()) return nil } } func convertTestifyAssertion(tcall call, migration migration) ast.Node { imports := migration.importNames switch tcall.selExpr.Sel.Name { case "NoError", "NoErrorf": return convertNoError(tcall, imports) case "True", "Truef": return convertTrue(tcall, imports) case "False", "Falsef": return convertFalse(tcall, imports) case "Equal", "Equalf", "Exactly", "Exactlyf", "EqualValues", "EqualValuesf": return convertEqual(tcall, migration) case "Contains", "Containsf": return convertTwoArgComparison(tcall, imports, "Contains") case "Len", "Lenf": return convertTwoArgComparison(tcall, imports, "Len") case "Panics", "Panicsf": return convertOneArgComparison(tcall, imports, "Panics") case "EqualError", "EqualErrorf": return convertEqualError(tcall, imports) case "Error", "Errorf": return convertError(tcall, imports) case "ErrorContains", "ErrorContainsf": return convertErrorContains(tcall, imports) case "Empty", "Emptyf": return convertEmpty(tcall, imports) case "Nil", "Nilf": return convertNil(tcall, migration) case "NotNil", "NotNilf": return convertNegativeComparison(tcall, imports, &ast.Ident{Name: "nil"}, 2) case "NotEqual", "NotEqualf": return convertNegativeComparison(tcall, imports, tcall.arg(2), 3) case "Fail", "Failf": return convertFail(tcall, "Error") case "FailNow", "FailNowf": return convertFail(tcall, "Fatal") case "NotEmpty", "NotEmptyf": return convertNotEmpty(tcall, imports) case "NotZero", "NotZerof": zero := &ast.BasicLit{Kind: token.INT, Value: "0"} return convertNegativeComparison(tcall, imports, zero, 2) } log.Printf("%s: skipping unsupported assertion", tcall.StringWithFileInfo()) return nil } func newCallExpr(x, sel string, args []ast.Expr) *ast.CallExpr { return &ast.CallExpr{ Fun: &ast.SelectorExpr{ X: &ast.Ident{Name: x}, Sel: &ast.Ident{Name: sel}, }, Args: args, } } func newCallExprArgs(t ast.Expr, cmp ast.Expr, extra ...ast.Expr) []ast.Expr { return append(append([]ast.Expr{t}, cmp), extra...) } func newCallExprWithPosition(tcall call, imports importNames, args []ast.Expr) *ast.CallExpr { return &ast.CallExpr{ Fun: &ast.SelectorExpr{ X: &ast.Ident{ Name: imports.assert, NamePos: tcall.xIdent.NamePos, }, Sel: &ast.Ident{Name: tcall.assert}, }, Args: args, } } func convertNoError(tcall call, imports importNames) ast.Node { // use assert.NilError() for require.NoError() if tcall.assert == funcNameAssert { return newCallExprWithoutComparison(tcall, imports, "NilError") } // use assert.Check() for assert.NoError() return newCallExprWithoutComparison(tcall, imports, "Check") } func convertEqualError(tcall call, imports importNames) ast.Node { if tcall.assert == funcNameAssert { return newCallExprWithoutComparison(tcall, imports, "Error") } return convertTwoArgComparison(tcall, imports, "Error") } func newCallExprWithoutComparison(tcall call, imports importNames, name string) ast.Node { return &ast.CallExpr{ Fun: &ast.SelectorExpr{ X: &ast.Ident{ Name: imports.assert, NamePos: tcall.xIdent.NamePos, }, Sel: &ast.Ident{Name: name}, }, Args: tcall.expr.Args, } } func convertOneArgComparison(tcall call, imports importNames, cmpName string) ast.Node { return newCallExprWithPosition(tcall, imports, newCallExprArgs( tcall.testingT(), newCallExpr(imports.cmp, cmpName, []ast.Expr{tcall.arg(1)}), tcall.extraArgs(2)...)) } func convertTrue(tcall call, imports importNames) ast.Node { return newCallExprWithPosition(tcall, imports, tcall.expr.Args) } func convertFalse(tcall call, imports importNames) ast.Node { return newCallExprWithPosition(tcall, imports, newCallExprArgs( tcall.testingT(), &ast.UnaryExpr{Op: token.NOT, X: tcall.arg(1)}, tcall.extraArgs(2)...)) } func convertEqual(tcall call, migration migration) ast.Node { imports := migration.importNames hasExtraArgs := len(tcall.extraArgs(3)) > 0 cmpEqual := convertTwoArgComparison(tcall, imports, "Equal") if tcall.assert == funcNameAssert { cmpEqual = newCallExprWithoutComparison(tcall, imports, "Equal") } cmpDeepEqual := convertTwoArgComparison(tcall, imports, "DeepEqual") if tcall.assert == funcNameAssert && !hasExtraArgs { cmpDeepEqual = newCallExprWithoutComparison(tcall, imports, "DeepEqual") } gotype := walkForType(migration.pkgInfo, tcall.arg(1)) if isUnknownType(gotype) { gotype = walkForType(migration.pkgInfo, tcall.arg(2)) } if isUnknownType(gotype) { return cmpDeepEqual } switch gotype.Underlying().(type) { case *types.Basic: return cmpEqual default: return cmpDeepEqual } } func convertTwoArgComparison(tcall call, imports importNames, cmpName string) ast.Node { return newCallExprWithPosition(tcall, imports, newCallExprArgs( tcall.testingT(), newCallExpr(imports.cmp, cmpName, tcall.args(1, 3)), tcall.extraArgs(3)...)) } func convertError(tcall call, imports importNames) ast.Node { cmpArgs := []ast.Expr{ tcall.arg(1), &ast.BasicLit{Kind: token.STRING, Value: `""`}} return newCallExprWithPosition(tcall, imports, newCallExprArgs( tcall.testingT(), newCallExpr(imports.cmp, "ErrorContains", cmpArgs), tcall.extraArgs(2)...)) } func convertErrorContains(tcall call, imports importNames) ast.Node { return &ast.CallExpr{ Fun: &ast.SelectorExpr{ X: &ast.Ident{ Name: imports.assert, NamePos: tcall.xIdent.NamePos, }, Sel: &ast.Ident{Name: "ErrorContains"}, }, Args: tcall.expr.Args, } } func convertEmpty(tcall call, imports importNames) ast.Node { cmpArgs := []ast.Expr{ tcall.arg(1), &ast.BasicLit{Kind: token.INT, Value: "0"}, } return newCallExprWithPosition(tcall, imports, newCallExprArgs( tcall.testingT(), newCallExpr(imports.cmp, "Len", cmpArgs), tcall.extraArgs(2)...)) } func convertNil(tcall call, migration migration) ast.Node { gotype := walkForType(migration.pkgInfo, tcall.arg(1)) if gotype != nil && gotype.String() == "error" { return convertNoError(tcall, migration.importNames) } return convertOneArgComparison(tcall, migration.importNames, "Nil") } func convertNegativeComparison( tcall call, imports importNames, right ast.Expr, extra int, ) ast.Node { return newCallExprWithPosition(tcall, imports, newCallExprArgs( tcall.testingT(), &ast.BinaryExpr{X: tcall.arg(1), Op: token.NEQ, Y: right}, tcall.extraArgs(extra)...)) } func convertFail(tcall call, selector string) ast.Node { extraArgs := tcall.extraArgs(1) if len(extraArgs) > 1 { selector += "f" } return &ast.CallExpr{ Fun: &ast.SelectorExpr{ X: tcall.testingT(), Sel: &ast.Ident{Name: selector}, }, Args: extraArgs, } } func convertNotEmpty(tcall call, imports importNames) ast.Node { lenExpr := &ast.CallExpr{ Fun: &ast.Ident{Name: "len"}, Args: tcall.args(1, 2), } zeroExpr := &ast.BasicLit{Kind: token.INT, Value: "0"} return newCallExprWithPosition(tcall, imports, newCallExprArgs( tcall.testingT(), &ast.BinaryExpr{X: lenExpr, Op: token.NEQ, Y: zeroExpr}, tcall.extraArgs(2)...)) } gotest.tools-3.5.1/assert/cmd/gty-migrate-from-testify/migrate_test.go000066400000000000000000000163041463245364400261440ustar00rootroot00000000000000package main import ( "go/token" "testing" "golang.org/x/tools/go/packages" "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" "gotest.tools/v3/env" "gotest.tools/v3/fs" "gotest.tools/v3/icmd" ) func TestMigrateFileReplacesTestingT(t *testing.T) { source := ` package foo import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSomething(t *testing.T) { a := assert.TestingT(t) b := require.TestingT(t) c := require.TestingT(t) if a == b {} _ = c } func do(t require.TestingT) {} ` migration := newMigrationFromSource(t, source) migrateFile(migration) expected := `package foo import ( "testing" "gotest.tools/v3/assert" ) func TestSomething(t *testing.T) { a := assert.TestingT(t) b := assert.TestingT(t) c := assert.TestingT(t) if a == b { } _ = c } func do(t assert.TestingT) {} ` actual, err := formatFile(migration) assert.NilError(t, err) assert.Assert(t, cmp.Equal(expected, string(actual))) } func newMigrationFromSource(t *testing.T, source string) migration { t.Helper() goMod := `module example.com/foo require github.com/stretchr/testify v1.7.1 ` dir := fs.NewDir(t, t.Name(), fs.WithFile("foo.go", source), fs.WithFile("go.mod", goMod)) fileset := token.NewFileSet() env.ChangeWorkingDir(t, dir.Path()) icmd.RunCommand("go", "mod", "tidy").Assert(t, icmd.Success) opts := options{pkgs: []string{"./..."}} pkgs, err := loadPackages(opts, fileset) assert.NilError(t, err) packages.PrintErrors(pkgs) pkg := pkgs[0] assert.Assert(t, !pkg.IllTyped) return migration{ file: pkg.Syntax[0], fileset: fileset, importNames: newImportNames(pkg.Syntax[0].Imports, opts), pkgInfo: pkg.TypesInfo, } } func TestMigrateFileWithNamedCmpPackage(t *testing.T) { source := ` package foo import ( "testing" "github.com/stretchr/testify/assert" ) func TestSomething(t *testing.T) { assert.Equal(t, "a", "b") } ` migration := newMigrationFromSource(t, source) migration.importNames.cmp = "is" migrateFile(migration) expected := `package foo import ( "testing" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestSomething(t *testing.T) { assert.Check(t, is.Equal("a", "b")) } ` actual, err := formatFile(migration) assert.NilError(t, err) assert.Assert(t, cmp.Equal(expected, string(actual))) } func TestMigrateFileWithCommentsOnAssert(t *testing.T) { source := ` package foo import ( "testing" "github.com/stretchr/testify/assert" ) func TestSomething(t *testing.T) { // This is going to fail assert.Equal(t, "a", "b") } ` migration := newMigrationFromSource(t, source) migrateFile(migration) expected := `package foo import ( "testing" "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" ) func TestSomething(t *testing.T) { // This is going to fail assert.Check(t, cmp.Equal("a", "b")) } ` actual, err := formatFile(migration) assert.NilError(t, err) assert.Assert(t, cmp.Equal(expected, string(actual))) } func TestMigrateFileConvertNilToNilError(t *testing.T) { source := ` package foo import ( "testing" "github.com/stretchr/testify/require" "github.com/stretchr/testify/assert" ) func TestSomething(t *testing.T) { var err error assert.Nil(t, err) require.Nil(t, err) } ` migration := newMigrationFromSource(t, source) migrateFile(migration) expected := `package foo import ( "testing" "gotest.tools/v3/assert" ) func TestSomething(t *testing.T) { var err error assert.Check(t, err) assert.NilError(t, err) } ` actual, err := formatFile(migration) assert.NilError(t, err) assert.Assert(t, cmp.Equal(expected, string(actual))) } func TestMigrateFileConvertAssertNew(t *testing.T) { source := ` package foo import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSomething(t *testing.T) { is := assert.New(t) is.Equal("one", "two") is.NotEqual("one", "two") assert := require.New(t) assert.Equal("one", "two") assert.NotEqual("one", "two") } func TestOtherName(z *testing.T) { is := require.New(z) is.Equal("one", "two") } ` migration := newMigrationFromSource(t, source) migrateFile(migration) expected := `package foo import ( "testing" "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" ) func TestSomething(t *testing.T) { assert.Check(t, cmp.Equal("one", "two")) assert.Check(t, "one" != "two") assert.Equal(t, "one", "two") assert.Assert(t, "one" != "two") } func TestOtherName(z *testing.T) { assert.Equal(z, "one", "two") } ` actual, err := formatFile(migration) assert.NilError(t, err) assert.Assert(t, cmp.Equal(expected, string(actual))) } func TestMigrateFileWithExtraArgs(t *testing.T) { source := ` package foo import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSomething(t *testing.T) { var err error assert.Error(t, err, "this is a comment") require.ErrorContains(t, err, "this in the error") assert.Empty(t, nil, "more comment") require.Equal(t, []string{}, []string{}, "because") } ` migration := newMigrationFromSource(t, source) migration.importNames.cmp = "is" migrateFile(migration) expected := `package foo import ( "testing" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestSomething(t *testing.T) { var err error assert.Check(t, is.ErrorContains(err, ""), "this is a comment") assert.ErrorContains(t, err, "this in the error") assert.Check(t, is.Len(nil, 0), "more comment") assert.Assert(t, is.DeepEqual([]string{}, []string{}), "because") } ` actual, err := formatFile(migration) assert.NilError(t, err) assert.Assert(t, cmp.Equal(expected, string(actual))) } func TestMigrate_AssertAlreadyImported(t *testing.T) { source := ` package foo import ( "testing" "github.com/stretchr/testify/require" "gotest.tools/v3/assert" ) func TestSomething(t *testing.T) { var err error assert.Error(t, err, "this is the error") require.Equal(t, []string{}, []string{}, "because") } ` migration := newMigrationFromSource(t, source) migration.importNames.cmp = "is" migrateFile(migration) expected := `package foo import ( "testing" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestSomething(t *testing.T) { var err error assert.Error(t, err, "this is the error") assert.Assert(t, is.DeepEqual([]string{}, []string{}), "because") } ` actual, err := formatFile(migration) assert.NilError(t, err) assert.Assert(t, cmp.Equal(expected, string(actual))) } func TestMigrate_AssertAlreadyImportedWithAlias(t *testing.T) { source := ` package foo import ( "testing" "github.com/stretchr/testify/require" gtya "gotest.tools/v3/assert" ) func TestSomething(t *testing.T) { var err error gtya.Error(t, err, "this is the error") require.Equal(t, []string{}, []string{}, "because") } ` migration := newMigrationFromSource(t, source) migration.importNames.cmp = "is" migrateFile(migration) expected := `package foo import ( "testing" gtya "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestSomething(t *testing.T) { var err error gtya.Error(t, err, "this is the error") gtya.Assert(t, is.DeepEqual([]string{}, []string{}), "because") } ` actual, err := formatFile(migration) assert.NilError(t, err) assert.Assert(t, cmp.Equal(expected, string(actual))) } gotest.tools-3.5.1/assert/cmd/gty-migrate-from-testify/testdata/000077500000000000000000000000001463245364400247335ustar00rootroot00000000000000gotest.tools-3.5.1/assert/cmd/gty-migrate-from-testify/testdata/full-expected/000077500000000000000000000000001463245364400274745ustar00rootroot00000000000000gotest.tools-3.5.1/assert/cmd/gty-migrate-from-testify/testdata/full-expected/some_test.go000066400000000000000000000057121463245364400320320ustar00rootroot00000000000000package foo import ( "fmt" "testing" "github.com/go-check/check" "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" ) type mystruct struct { a int expected int } func TestFirstThing(t *testing.T) { rt := assert.TestingT(t) assert.Check(t, cmp.Equal("foo", "bar")) assert.Check(t, cmp.Equal(1, 2)) assert.Check(t, false) assert.Check(t, !true) assert.NilError(rt, nil) assert.Check(t, cmp.DeepEqual(map[string]bool{"a": true}, nil)) assert.Check(t, cmp.DeepEqual([]int{1}, nil)) assert.Equal(rt, "a", "B") } func TestSecondThing(t *testing.T) { var foo mystruct assert.DeepEqual(t, foo, mystruct{}) assert.DeepEqual(t, mystruct{}, mystruct{}) assert.Check(t, nil, "foo %d", 3) assert.NilError(t, nil, "foo %d", 3) assert.Check(t, cmp.ErrorContains(fmt.Errorf("foo"), "")) assert.Assert(t, 77 != 0) } func TestOthers(t *testing.T) { assert.Check(t, cmp.Contains([]string{}, "foo")) assert.Assert(t, cmp.Len([]int{}, 3)) assert.Check(t, cmp.Panics(func() { panic("foo") })) assert.Error(t, fmt.Errorf("bad days"), "good days") assert.Check(t, nil != nil) t.Error("why") t.Fatal("why not") assert.Assert(t, len([]bool{}) != 0) // Unsupported asseert assert.NotContains(t, []bool{}, true) } func TestAssertNew(t *testing.T) { assert.Check(t, cmp.Equal("a", "b")) } type unit struct { c *testing.T } func thing(t *testing.T) unit { return unit{c: t} } func TestStoredTestingT(t *testing.T) { u := thing(t) assert.Check(u.c, cmp.Equal("A", "b")) u = unit{c: t} assert.Check(u.c, cmp.Equal("A", "b")) } func TestNotNamedT(c *testing.T) { assert.Check(c, cmp.Equal("A", "b")) } func TestEqualsWithComplexTypes(t *testing.T) { expected := []int{1, 2, 3} assert.Check(t, cmp.DeepEqual(expected, nil)) expectedM := map[int]bool{} assert.Check(t, cmp.DeepEqual(expectedM, nil)) expectedI := 123 assert.Check(t, cmp.Equal(expectedI, 0)) assert.Check(t, cmp.Equal(doInt(), 3)) // TODO: struct field } func doInt() int { return 1 } func TestEqualWithPrimitiveTypes(t *testing.T) { s := "foo" ptrString := &s assert.Check(t, cmp.Equal(*ptrString, "foo")) assert.Check(t, cmp.Equal(doInt(), doInt())) x := doInt() y := doInt() assert.Check(t, cmp.Equal(x, y)) tc := mystruct{a: 3, expected: 5} assert.Check(t, cmp.Equal(tc.a, tc.expected)) } func TestTableTest(t *testing.T) { var testcases = []struct { opts []string actual string expected string expectedOpts []string }{ { opts: []string{"a", "b"}, actual: "foo", expected: "else", }, } for _, testcase := range testcases { assert.Check(t, cmp.Equal(testcase.actual, testcase.expected)) assert.Check(t, cmp.DeepEqual(testcase.opts, testcase.expectedOpts)) } } func TestWithChecker(c *check.C) { var err error assert.Check(c, err) } func HelperWithAssertTestingT(t assert.TestingT) { var err error assert.Check(t, err, "with assert.TestingT") } func BenchmarkSomething(b *testing.B) { var err error assert.Check(b, err) } gotest.tools-3.5.1/assert/cmd/gty-migrate-from-testify/testdata/full/000077500000000000000000000000001463245364400256755ustar00rootroot00000000000000gotest.tools-3.5.1/assert/cmd/gty-migrate-from-testify/testdata/full/some_test.go000066400000000000000000000053421463245364400302320ustar00rootroot00000000000000package foo import ( "fmt" "testing" "github.com/go-check/check" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type mystruct struct { a int expected int } func TestFirstThing(t *testing.T) { rt := require.TestingT(t) assert.Equal(t, "foo", "bar") assert.Equal(t, 1, 2) assert.True(t, false) assert.False(t, true) require.NoError(rt, nil) assert.Equal(t, map[string]bool{"a": true}, nil) assert.Equal(t, []int{1}, nil) require.Equal(rt, "a", "B") } func TestSecondThing(t *testing.T) { var foo mystruct require.Equal(t, foo, mystruct{}) require.Equal(t, mystruct{}, mystruct{}) assert.NoError(t, nil, "foo %d", 3) require.NoError(t, nil, "foo %d", 3) assert.Error(t, fmt.Errorf("foo")) require.NotZero(t, 77) } func TestOthers(t *testing.T) { assert.Contains(t, []string{}, "foo") require.Len(t, []int{}, 3) assert.Panics(t, func() { panic("foo") }) require.EqualError(t, fmt.Errorf("bad days"), "good days") assert.NotNil(t, nil) assert.Fail(t, "why") assert.FailNow(t, "why not") require.NotEmpty(t, []bool{}) // Unsupported asseert assert.NotContains(t, []bool{}, true) } func TestAssertNew(t *testing.T) { a := assert.New(t) a.Equal("a", "b") } type unit struct { c *testing.T } func thing(t *testing.T) unit { return unit{c: t} } func TestStoredTestingT(t *testing.T) { u := thing(t) assert.Equal(u.c, "A", "b") u = unit{c: t} assert.Equal(u.c, "A", "b") } func TestNotNamedT(c *testing.T) { assert.Equal(c, "A", "b") } func TestEqualsWithComplexTypes(t *testing.T) { expected := []int{1, 2, 3} assert.Equal(t, expected, nil) expectedM := map[int]bool{} assert.Equal(t, expectedM, nil) expectedI := 123 assert.Equal(t, expectedI, 0) assert.Equal(t, doInt(), 3) // TODO: struct field } func doInt() int { return 1 } func TestEqualWithPrimitiveTypes(t *testing.T) { s := "foo" ptrString := &s assert.Equal(t, *ptrString, "foo") assert.Equal(t, doInt(), doInt()) x := doInt() y := doInt() assert.Equal(t, x, y) tc := mystruct{a: 3, expected: 5} assert.Equal(t, tc.a, tc.expected) } func TestTableTest(t *testing.T) { var testcases = []struct { opts []string actual string expected string expectedOpts []string }{ { opts: []string{"a", "b"}, actual: "foo", expected: "else", }, } for _, testcase := range testcases { assert.Equal(t, testcase.actual, testcase.expected) assert.Equal(t, testcase.opts, testcase.expectedOpts) } } func TestWithChecker(c *check.C) { var err error assert.NoError(c, err) } func HelperWithAssertTestingT(t assert.TestingT) { var err error assert.NoError(t, err, "with assert.TestingT") } func BenchmarkSomething(b *testing.B) { var err error assert.NoError(b, err) } gotest.tools-3.5.1/assert/cmd/gty-migrate-from-testify/walktype.go000066400000000000000000000011441463245364400253110ustar00rootroot00000000000000package main import ( "go/ast" "go/types" ) // walkForType walks the AST tree and returns the type of the expression func walkForType(pkgInfo *types.Info, node ast.Node) types.Type { var result types.Type visit := func(node ast.Node) bool { if expr, ok := node.(ast.Expr); ok { if typeAndValue, ok := pkgInfo.Types[expr]; ok { result = typeAndValue.Type return false } } return true } ast.Inspect(node, visit) return result } func isUnknownType(typ types.Type) bool { if typ == nil { return true } basic, ok := typ.(*types.Basic) return ok && basic.Kind() == types.Invalid } gotest.tools-3.5.1/assert/cmp/000077500000000000000000000000001463245364400162575ustar00rootroot00000000000000gotest.tools-3.5.1/assert/cmp/compare.go000066400000000000000000000267361463245364400202520ustar00rootroot00000000000000/*Package cmp provides Comparisons for Assert and Check*/ package cmp // import "gotest.tools/v3/assert/cmp" import ( "errors" "fmt" "reflect" "regexp" "strings" "github.com/google/go-cmp/cmp" "gotest.tools/v3/internal/format" ) // Comparison is a function which compares values and returns [ResultSuccess] if // the actual value matches the expected value. If the values do not match the // [Result] will contain a message about why it failed. type Comparison func() Result // DeepEqual compares two values using [github.com/google/go-cmp/cmp] // and succeeds if the values are equal. // // The comparison can be customized using comparison Options. // Package [gotest.tools/v3/assert/opt] provides some additional // commonly used Options. func DeepEqual(x, y interface{}, opts ...cmp.Option) Comparison { return func() (result Result) { defer func() { if panicmsg, handled := handleCmpPanic(recover()); handled { result = ResultFailure(panicmsg) } }() diff := cmp.Diff(x, y, opts...) if diff == "" { return ResultSuccess } return multiLineDiffResult(diff, x, y) } } func handleCmpPanic(r interface{}) (string, bool) { if r == nil { return "", false } panicmsg, ok := r.(string) if !ok { panic(r) } switch { case strings.HasPrefix(panicmsg, "cannot handle unexported field"): return panicmsg, true } panic(r) } func toResult(success bool, msg string) Result { if success { return ResultSuccess } return ResultFailure(msg) } // RegexOrPattern may be either a [*regexp.Regexp] or a string that is a valid // regexp pattern. type RegexOrPattern interface{} // Regexp succeeds if value v matches regular expression re. // // Example: // // assert.Assert(t, cmp.Regexp("^[0-9a-f]{32}$", str)) // r := regexp.MustCompile("^[0-9a-f]{32}$") // assert.Assert(t, cmp.Regexp(r, str)) func Regexp(re RegexOrPattern, v string) Comparison { match := func(re *regexp.Regexp) Result { return toResult( re.MatchString(v), fmt.Sprintf("value %q does not match regexp %q", v, re.String())) } return func() Result { switch regex := re.(type) { case *regexp.Regexp: return match(regex) case string: re, err := regexp.Compile(regex) if err != nil { return ResultFailure(err.Error()) } return match(re) default: return ResultFailure(fmt.Sprintf("invalid type %T for regex pattern", regex)) } } } // Equal succeeds if x == y. See [gotest.tools/v3/assert.Equal] for full documentation. func Equal(x, y interface{}) Comparison { return func() Result { switch { case x == y: return ResultSuccess case isMultiLineStringCompare(x, y): diff := format.UnifiedDiff(format.DiffConfig{A: x.(string), B: y.(string)}) return multiLineDiffResult(diff, x, y) } return ResultFailureTemplate(` {{- printf "%v" .Data.x}} ( {{- with callArg 0 }}{{ formatNode . }} {{end -}} {{- printf "%T" .Data.x -}} ) != {{ printf "%v" .Data.y}} ( {{- with callArg 1 }}{{ formatNode . }} {{end -}} {{- printf "%T" .Data.y -}} )`, map[string]interface{}{"x": x, "y": y}) } } func isMultiLineStringCompare(x, y interface{}) bool { strX, ok := x.(string) if !ok { return false } strY, ok := y.(string) if !ok { return false } return strings.Contains(strX, "\n") || strings.Contains(strY, "\n") } func multiLineDiffResult(diff string, x, y interface{}) Result { return ResultFailureTemplate(` --- {{ with callArg 0 }}{{ formatNode . }}{{else}}←{{end}} +++ {{ with callArg 1 }}{{ formatNode . }}{{else}}→{{end}} {{ .Data.diff }}`, map[string]interface{}{"diff": diff, "x": x, "y": y}) } // Len succeeds if the sequence has the expected length. func Len(seq interface{}, expected int) Comparison { return func() (result Result) { defer func() { if e := recover(); e != nil { result = ResultFailure(fmt.Sprintf("type %T does not have a length", seq)) } }() value := reflect.ValueOf(seq) length := value.Len() if length == expected { return ResultSuccess } msg := fmt.Sprintf("expected %s (length %d) to have length %d", seq, length, expected) return ResultFailure(msg) } } // Contains succeeds if item is in collection. Collection may be a string, map, // slice, or array. // // If collection is a string, item must also be a string, and is compared using // [strings.Contains]. // If collection is a Map, contains will succeed if item is a key in the map. // If collection is a slice or array, item is compared to each item in the // sequence using [reflect.DeepEqual]. func Contains(collection interface{}, item interface{}) Comparison { return func() Result { colValue := reflect.ValueOf(collection) if !colValue.IsValid() { return ResultFailure("nil does not contain items") } msg := fmt.Sprintf("%v does not contain %v", collection, item) itemValue := reflect.ValueOf(item) switch colValue.Type().Kind() { case reflect.String: if itemValue.Type().Kind() != reflect.String { return ResultFailure("string may only contain strings") } return toResult( strings.Contains(colValue.String(), itemValue.String()), fmt.Sprintf("string %q does not contain %q", collection, item)) case reflect.Map: if itemValue.Type() != colValue.Type().Key() { return ResultFailure(fmt.Sprintf( "%v can not contain a %v key", colValue.Type(), itemValue.Type())) } return toResult(colValue.MapIndex(itemValue).IsValid(), msg) case reflect.Slice, reflect.Array: for i := 0; i < colValue.Len(); i++ { if reflect.DeepEqual(colValue.Index(i).Interface(), item) { return ResultSuccess } } return ResultFailure(msg) default: return ResultFailure(fmt.Sprintf("type %T does not contain items", collection)) } } } // Panics succeeds if f() panics. func Panics(f func()) Comparison { return func() (result Result) { defer func() { if err := recover(); err != nil { result = ResultSuccess } }() f() return ResultFailure("did not panic") } } // Error succeeds if err is a non-nil error, and the error message equals the // expected message. func Error(err error, message string) Comparison { return func() Result { switch { case err == nil: return ResultFailure("expected an error, got nil") case err.Error() != message: return ResultFailure(fmt.Sprintf( "expected error %q, got %s", message, formatErrorMessage(err))) } return ResultSuccess } } // ErrorContains succeeds if err is a non-nil error, and the error message contains // the expected substring. func ErrorContains(err error, substring string) Comparison { return func() Result { switch { case err == nil: return ResultFailure("expected an error, got nil") case !strings.Contains(err.Error(), substring): return ResultFailure(fmt.Sprintf( "expected error to contain %q, got %s", substring, formatErrorMessage(err))) } return ResultSuccess } } type causer interface { Cause() error } func formatErrorMessage(err error) string { //nolint:errorlint // unwrapping is not appropriate here if _, ok := err.(causer); ok { return fmt.Sprintf("%q\n%+v", err, err) } // This error was not wrapped with github.com/pkg/errors return fmt.Sprintf("%q", err) } // Nil succeeds if obj is a nil interface, pointer, or function. // // Use [gotest.tools/v3/assert.NilError] for comparing errors. Use Len(obj, 0) for comparing slices, // maps, and channels. func Nil(obj interface{}) Comparison { msgFunc := func(value reflect.Value) string { return fmt.Sprintf("%v (type %s) is not nil", reflect.Indirect(value), value.Type()) } return isNil(obj, msgFunc) } func isNil(obj interface{}, msgFunc func(reflect.Value) string) Comparison { return func() Result { if obj == nil { return ResultSuccess } value := reflect.ValueOf(obj) kind := value.Type().Kind() if kind >= reflect.Chan && kind <= reflect.Slice { if value.IsNil() { return ResultSuccess } return ResultFailure(msgFunc(value)) } return ResultFailure(fmt.Sprintf("%v (type %s) can not be nil", value, value.Type())) } } // ErrorType succeeds if err is not nil and is of the expected type. // // Expected can be one of: // // func(error) bool // // Function should return true if the error is the expected type. // // type struct{}, type &struct{} // // A struct or a pointer to a struct. // Fails if the error is not of the same type as expected. // // type &interface{} // // A pointer to an interface type. // Fails if err does not implement the interface. // // reflect.Type // // Fails if err does not implement the [reflect.Type]. // // Deprecated: Use [ErrorIs] func ErrorType(err error, expected interface{}) Comparison { return func() Result { switch expectedType := expected.(type) { case func(error) bool: return cmpErrorTypeFunc(err, expectedType) case reflect.Type: if expectedType.Kind() == reflect.Interface { return cmpErrorTypeImplementsType(err, expectedType) } return cmpErrorTypeEqualType(err, expectedType) case nil: return ResultFailure("invalid type for expected: nil") } expectedType := reflect.TypeOf(expected) switch { case expectedType.Kind() == reflect.Struct, isPtrToStruct(expectedType): return cmpErrorTypeEqualType(err, expectedType) case isPtrToInterface(expectedType): return cmpErrorTypeImplementsType(err, expectedType.Elem()) } return ResultFailure(fmt.Sprintf("invalid type for expected: %T", expected)) } } func cmpErrorTypeFunc(err error, f func(error) bool) Result { if f(err) { return ResultSuccess } actual := "nil" if err != nil { actual = fmt.Sprintf("%s (%T)", err, err) } return ResultFailureTemplate(`error is {{ .Data.actual }} {{- with callArg 1 }}, not {{ formatNode . }}{{end -}}`, map[string]interface{}{"actual": actual}) } func cmpErrorTypeEqualType(err error, expectedType reflect.Type) Result { if err == nil { return ResultFailure(fmt.Sprintf("error is nil, not %s", expectedType)) } errValue := reflect.ValueOf(err) if errValue.Type() == expectedType { return ResultSuccess } return ResultFailure(fmt.Sprintf("error is %s (%T), not %s", err, err, expectedType)) } func cmpErrorTypeImplementsType(err error, expectedType reflect.Type) Result { if err == nil { return ResultFailure(fmt.Sprintf("error is nil, not %s", expectedType)) } errValue := reflect.ValueOf(err) if errValue.Type().Implements(expectedType) { return ResultSuccess } return ResultFailure(fmt.Sprintf("error is %s (%T), not %s", err, err, expectedType)) } func isPtrToInterface(typ reflect.Type) bool { return typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Interface } func isPtrToStruct(typ reflect.Type) bool { return typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Struct } var ( stdlibErrorNewType = reflect.TypeOf(errors.New("")) stdlibFmtErrorType = reflect.TypeOf(fmt.Errorf("%w", fmt.Errorf(""))) ) // ErrorIs succeeds if errors.Is(actual, expected) returns true. See // [errors.Is] for accepted argument values. func ErrorIs(actual error, expected error) Comparison { return func() Result { if errors.Is(actual, expected) { return ResultSuccess } // The type of stdlib errors is excluded because the type is not relevant // in those cases. The type is only important when it is a user defined // custom error type. return ResultFailureTemplate(`error is {{- if not .Data.a }} nil,{{ else }} {{- printf " \"%v\"" .Data.a }} {{- if notStdlibErrorType .Data.a }} ({{ printf "%T" .Data.a }}){{ end }}, {{- end }} not {{ printf "\"%v\"" .Data.x }} ( {{- with callArg 1 }}{{ formatNode . }}{{ end }} {{- if notStdlibErrorType .Data.x }}{{ printf " %T" .Data.x }}{{ end }})`, map[string]interface{}{"a": actual, "x": expected}) } } gotest.tools-3.5.1/assert/cmp/compare_test.go000066400000000000000000000367511463245364400213070ustar00rootroot00000000000000package cmp import ( "errors" "fmt" "go/ast" "io" "os" "reflect" "regexp" "strings" "testing" "github.com/google/go-cmp/cmp" ) func TestDeepEqual(t *testing.T) { t.Run("failure", func(t *testing.T) { result := DeepEqual([]string{"a", "b"}, []string{"b", "a"})() if result.Success() { t.Errorf("expected failure") } args := []ast.Expr{&ast.Ident{Name: "result"}, &ast.Ident{Name: "exp"}} message := result.(templatedResult).FailureMessage(args) expected := "\n--- result\n+++ exp\n" if !strings.HasPrefix(message, expected) { t.Errorf("expected prefix \n%q\ngot\n%q\n", expected, message) } }) t.Run("success", func(t *testing.T) { actual := DeepEqual([]string{"a"}, []string{"a"})() assertSuccess(t, actual) }) } type Stub struct { unx int } func TestDeepEqualWithUnexported(t *testing.T) { result := DeepEqual(Stub{}, Stub{unx: 1})() assertFailureHasPrefix(t, result, `cannot handle unexported field at {cmp.Stub}.unx:`) } func TestRegexp(t *testing.T) { var testcases = []struct { name string regex interface{} value string match bool expErr string }{ { name: "pattern string match", regex: "^[0-9]+$", value: "12123423456", match: true, }, { name: "simple pattern string no match", regex: "bob", value: "Probably", expErr: `value "Probably" does not match regexp "bob"`, }, { name: "pattern string no match", regex: "^1", value: "2123423456", expErr: `value "2123423456" does not match regexp "^1"`, }, { name: "regexp match", regex: regexp.MustCompile("^d[0-9a-f]{8}$"), value: "d1632beef", match: true, }, { name: "invalid regexp", regex: "^1(", value: "2", expErr: "error parsing regexp: missing closing ): `^1(`", }, { name: "invalid type", regex: struct{}{}, value: "some string", expErr: "invalid type struct {} for regex pattern", }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { res := Regexp(tc.regex, tc.value)() if tc.match { assertSuccess(t, res) } else { assertFailure(t, res, tc.expErr) } }) } } func TestLen(t *testing.T) { var testcases = []struct { seq interface{} length int expectedSuccess bool expectedMessage string }{ { seq: []string{"A", "b", "c"}, length: 3, expectedSuccess: true, }, { seq: []string{"A", "b", "c"}, length: 2, expectedMessage: "expected [A b c] (length 3) to have length 2", }, { seq: map[string]int{"a": 1, "b": 2}, length: 2, expectedSuccess: true, }, { seq: [3]string{"a", "b", "c"}, length: 3, expectedSuccess: true, }, { seq: "abcd", length: 4, expectedSuccess: true, }, { seq: "abcd", length: 3, expectedMessage: "expected abcd (length 4) to have length 3", }, } for _, testcase := range testcases { t.Run(fmt.Sprintf("%v len=%d", testcase.seq, testcase.length), func(t *testing.T) { result := Len(testcase.seq, testcase.length)() if testcase.expectedSuccess { assertSuccess(t, result) } else { assertFailure(t, result, testcase.expectedMessage) } }) } } func TestPanics(t *testing.T) { panicker := func() { panic("AHHHHHHHHHHH") } result := Panics(panicker)() assertSuccess(t, result) result = Panics(func() {})() assertFailure(t, result, "did not panic") } type innerstub struct { num int } type stub struct { stub innerstub num int } func TestDeepEqualEquivalenceToReflectDeepEqual(t *testing.T) { var testcases = []struct { left interface{} right interface{} }{ {nil, nil}, {7, 7}, {false, false}, {stub{innerstub{1}, 2}, stub{innerstub{1}, 2}}, {[]int{1, 2, 3}, []int{1, 2, 3}}, {[]byte(nil), []byte(nil)}, {nil, []byte(nil)}, {1, uint64(1)}, {7, "7"}, } for _, testcase := range testcases { expected := reflect.DeepEqual(testcase.left, testcase.right) res := DeepEqual(testcase.left, testcase.right, cmpStub)() if res.Success() != expected { msg := res.(StringResult).FailureMessage() t.Errorf("deepEqual(%v, %v) did not return %v (message %s)", testcase.left, testcase.right, expected, msg) } } } var cmpStub = cmp.AllowUnexported(stub{}, innerstub{}) func TestContains(t *testing.T) { var testcases = []struct { seq interface{} item interface{} expected bool expectedMsg string }{ { seq: error(nil), item: 0, expectedMsg: "nil does not contain items", }, { seq: "abcdef", item: "cde", expected: true, }, { seq: "abcdef", item: "foo", expectedMsg: `string "abcdef" does not contain "foo"`, }, { seq: "abcdef", item: 3, expectedMsg: `string may only contain strings`, }, { seq: map[rune]int{'a': 1, 'b': 2}, item: 'b', expected: true, }, { seq: map[rune]int{'a': 1}, item: 'c', expectedMsg: "map[97:1] does not contain 99", }, { seq: map[int]int{'a': 1, 'b': 2}, item: 'b', expectedMsg: "map[int]int can not contain a int32 key", }, { seq: []interface{}{"a", 1, 'a', 1.0, true}, item: 'a', expected: true, }, { seq: []interface{}{"a", 1, 'a', 1.0, true}, item: 3, expectedMsg: "[a 1 97 1 true] does not contain 3", }, { seq: [3]byte{99, 10, 100}, item: byte(99), expected: true, }, { seq: [3]byte{99, 10, 100}, item: byte(98), expectedMsg: "[99 10 100] does not contain 98", }, } for _, testcase := range testcases { name := fmt.Sprintf("%v in %v", testcase.item, testcase.seq) t.Run(name, func(t *testing.T) { result := Contains(testcase.seq, testcase.item)() if testcase.expected { assertSuccess(t, result) } else { assertFailure(t, result, testcase.expectedMsg) } }) } } func TestEqualMultiLine(t *testing.T) { result := `abcd 1234 aaaa bbbb` exp := `abcd 1111 aaaa bbbb` expected := ` --- result +++ exp @@ -1,4 +1,4 @@ abcd -1234 +1111 aaaa bbbb ` args := []ast.Expr{&ast.Ident{Name: "result"}, &ast.Ident{Name: "exp"}} res := Equal(result, exp)() assertFailureTemplate(t, res, args, expected) } func TestEqual_PointersNotEqual(t *testing.T) { x := 123 y := 123 res := Equal(&x, &y)() args := []ast.Expr{&ast.Ident{Name: "x"}, &ast.Ident{Name: "y"}} expected := fmt.Sprintf("%p (x *int) != %p (y *int)", &x, &y) assertFailureTemplate(t, res, args, expected) } // errorWithCause mimics the error formatting of github.com/pkg/errors type errorWithCause struct { msg string cause error } func (e errorWithCause) Error() string { return fmt.Sprintf("%v with cause: %v", e.msg, e.cause) } func (e errorWithCause) Cause() error { return e.cause } func (e errorWithCause) Format(s fmt.State, verb rune) { switch verb { case 'v': if s.Flag('+') { fmt.Fprintf(s, "%+v", e.Cause()) fmt.Fprint(s, "\nstack trace") return } fallthrough case 's': io.WriteString(s, e.Error()) case 'q': fmt.Fprintf(s, "%q", e.Error()) } } func TestError(t *testing.T) { result := Error(nil, "the error message")() assertFailure(t, result, "expected an error, got nil") // A Wrapped error also includes the stack result = Error(errorWithCause{cause: errors.New("other"), msg: "wrapped"}, "the error message")() assertFailureHasPrefix(t, result, `expected error "the error message", got "wrapped with cause: other" other stack trace`) msg := "the message" result = Error(errors.New(msg), msg)() assertSuccess(t, result) } func TestErrorContains(t *testing.T) { result := ErrorContains(nil, "the error message")() assertFailure(t, result, "expected an error, got nil") result = ErrorContains(errors.New("other"), "the error")() assertFailureHasPrefix(t, result, `expected error to contain "the error", got "other"`) msg := "the full message" result = ErrorContains(errors.New(msg), "full")() assertSuccess(t, result) } func TestNil(t *testing.T) { result := Nil(nil)() assertSuccess(t, result) var s *string result = Nil(s)() assertSuccess(t, result) var closer io.Closer result = Nil(closer)() assertSuccess(t, result) result = Nil("wrong")() assertFailure(t, result, "wrong (type string) can not be nil") notnil := "notnil" result = Nil(¬nil)() assertFailure(t, result, "notnil (type *string) is not nil") result = Nil([]string{"a"})() assertFailure(t, result, "[a] (type []string) is not nil") } type testingT interface { Errorf(msg string, args ...interface{}) } type helperT interface { Helper() } func assertSuccess(t testingT, res Result) { if ht, ok := t.(helperT); ok { ht.Helper() } if !res.Success() { msg := res.(StringResult).FailureMessage() t.Errorf("expected success, but got failure with message %q", msg) } } func assertFailure(t testingT, res Result, expected string) { if ht, ok := t.(helperT); ok { ht.Helper() } if res.Success() { t.Errorf("expected failure") } message := res.(StringResult).FailureMessage() if message != expected { t.Errorf("expected \n%q\ngot\n%q\n", expected, message) } } func assertFailureHasPrefix(t testingT, res Result, prefix string) { if ht, ok := t.(helperT); ok { ht.Helper() } if res.Success() { t.Errorf("expected failure") } message := res.(StringResult).FailureMessage() if !strings.HasPrefix(message, prefix) { t.Errorf("expected \n%v\nto start with\n%v\n", message, prefix) } } func assertFailureTemplate(t testingT, res Result, args []ast.Expr, expected string) { if ht, ok := t.(helperT); ok { ht.Helper() } if res.Success() { t.Errorf("expected failure") } message := res.(templatedResult).FailureMessage(args) if message != expected { t.Errorf("expected \n%q\ngot\n%q\n", expected, message) } } type stubError struct{} func (s stubError) Error() string { return "stub error" } func isErrorOfTypeStub(err error) bool { return reflect.TypeOf(err) == reflect.TypeOf(stubError{}) } type notStubError struct{} func (s notStubError) Error() string { return "not stub error" } func isErrorOfTypeNotStub(err error) bool { return reflect.TypeOf(err) == reflect.TypeOf(notStubError{}) } type specialStubIface interface { Special() } type stubPtrError struct{} func (s *stubPtrError) Error() string { return "stub ptr error" } func TestErrorTypeWithNil(t *testing.T) { var testcases = []struct { name string expType interface{} expected string }{ { name: "with struct", expType: stubError{}, expected: "error is nil, not cmp.stubError", }, { name: "with pointer to struct", expType: &stubPtrError{}, expected: "error is nil, not *cmp.stubPtrError", }, { name: "with interface", expType: (*specialStubIface)(nil), expected: "error is nil, not cmp.specialStubIface", }, { name: "with reflect.Type", expType: reflect.TypeOf(stubError{}), expected: "error is nil, not cmp.stubError", }, } for _, testcase := range testcases { t.Run(testcase.name, func(t *testing.T) { result := ErrorType(nil, testcase.expType)() assertFailure(t, result, testcase.expected) }) } } func TestErrorTypeSuccess(t *testing.T) { var testcases = []struct { name string expType interface{} err error }{ { name: "with function", expType: isErrorOfTypeStub, err: stubError{}, }, { name: "with struct", expType: stubError{}, err: stubError{}, }, { name: "with pointer to struct", expType: &stubPtrError{}, err: &stubPtrError{}, }, { name: "with interface", expType: (*error)(nil), err: stubError{}, }, { name: "with reflect.Type struct", expType: reflect.TypeOf(stubError{}), err: stubError{}, }, { name: "with reflect.Type interface", expType: reflect.TypeOf((*error)(nil)).Elem(), err: stubError{}, }, } for _, testcase := range testcases { t.Run(testcase.name, func(t *testing.T) { result := ErrorType(testcase.err, testcase.expType)() assertSuccess(t, result) }) } } func TestErrorTypeFailure(t *testing.T) { var testcases = []struct { name string expType interface{} expected string }{ { name: "with struct", expType: notStubError{}, expected: "error is stub error (cmp.stubError), not cmp.notStubError", }, { name: "with pointer to struct", expType: &stubPtrError{}, expected: "error is stub error (cmp.stubError), not *cmp.stubPtrError", }, { name: "with interface", expType: (*specialStubIface)(nil), expected: "error is stub error (cmp.stubError), not cmp.specialStubIface", }, { name: "with reflect.Type struct", expType: reflect.TypeOf(notStubError{}), expected: "error is stub error (cmp.stubError), not cmp.notStubError", }, { name: "with reflect.Type interface", expType: reflect.TypeOf((*specialStubIface)(nil)).Elem(), expected: "error is stub error (cmp.stubError), not cmp.specialStubIface", }, } for _, testcase := range testcases { t.Run(testcase.name, func(t *testing.T) { result := ErrorType(stubError{}, testcase.expType)() assertFailure(t, result, testcase.expected) }) } } func TestErrorTypeInvalid(t *testing.T) { result := ErrorType(stubError{}, nil)() assertFailure(t, result, "invalid type for expected: nil") result = ErrorType(stubError{}, "my type!")() assertFailure(t, result, "invalid type for expected: string") } func TestErrorTypeWithFunc(t *testing.T) { result := ErrorType(nil, isErrorOfTypeStub)() assertFailureTemplate(t, result, []ast.Expr{nil, &ast.Ident{Name: "isErrorOfTypeStub"}}, "error is nil, not isErrorOfTypeStub") result = ErrorType(stubError{}, isErrorOfTypeNotStub)() assertFailureTemplate(t, result, []ast.Expr{nil, &ast.Ident{Name: "isErrorOfTypeNotStub"}}, "error is stub error (cmp.stubError), not isErrorOfTypeNotStub") } func TestErrorIs(t *testing.T) { t.Run("equal", func(t *testing.T) { result := ErrorIs(stubError{}, stubError{})() assertSuccess(t, result) }) t.Run("actual is nil, not stdlib error", func(t *testing.T) { result := ErrorIs(nil, stubError{})() args := []ast.Expr{ &ast.Ident{Name: "err"}, &ast.SelectorExpr{ X: &ast.Ident{Name: "mypkg"}, Sel: &ast.Ident{Name: "StubError"}, }, } expected := `error is nil, not "stub error" (mypkg.StubError cmp.stubError)` assertFailureTemplate(t, result, args, expected) }) t.Run("not equal, not stdlib error", func(t *testing.T) { result := ErrorIs(notStubError{}, stubError{})() args := []ast.Expr{ &ast.Ident{Name: "err"}, &ast.SelectorExpr{ X: &ast.Ident{Name: "mypkg"}, Sel: &ast.Ident{Name: "StubError"}, }, } expected := `error is "not stub error" (cmp.notStubError), not "stub error" (mypkg.StubError cmp.stubError)` assertFailureTemplate(t, result, args, expected) }) t.Run("actual is nil, stdlib error", func(t *testing.T) { result := ErrorIs(nil, os.ErrClosed)() args := []ast.Expr{ &ast.Ident{Name: "err"}, &ast.SelectorExpr{ X: &ast.Ident{Name: "os"}, Sel: &ast.Ident{Name: "ErrClosed"}, }, } expected := `error is nil, not "file already closed" (os.ErrClosed)` assertFailureTemplate(t, result, args, expected) }) t.Run("not equal, stdlib error", func(t *testing.T) { result := ErrorIs(fmt.Errorf("foo"), os.ErrClosed)() args := []ast.Expr{ &ast.Ident{Name: "err"}, &ast.SelectorExpr{ X: &ast.Ident{Name: "os"}, Sel: &ast.Ident{Name: "ErrClosed"}, }, } expected := `error is "foo", not "file already closed" (os.ErrClosed)` assertFailureTemplate(t, result, args, expected) }) } gotest.tools-3.5.1/assert/cmp/result.go000066400000000000000000000056341463245364400201340ustar00rootroot00000000000000package cmp import ( "bytes" "fmt" "go/ast" "reflect" "text/template" "gotest.tools/v3/internal/source" ) // A Result of a [Comparison]. type Result interface { Success() bool } // StringResult is an implementation of [Result] that reports the error message // string verbatim and does not provide any templating or formatting of the // message. type StringResult struct { success bool message string } // Success returns true if the comparison was successful. func (r StringResult) Success() bool { return r.success } // FailureMessage returns the message used to provide additional information // about the failure. func (r StringResult) FailureMessage() string { return r.message } // ResultSuccess is a constant which is returned by a [Comparison] to // indicate success. var ResultSuccess = StringResult{success: true} // ResultFailure returns a failed [Result] with a failure message. func ResultFailure(message string) StringResult { return StringResult{message: message} } // ResultFromError returns [ResultSuccess] if err is nil. Otherwise [ResultFailure] // is returned with the error message as the failure message. func ResultFromError(err error) Result { if err == nil { return ResultSuccess } return ResultFailure(err.Error()) } type templatedResult struct { template string data map[string]interface{} } func (r templatedResult) Success() bool { return false } func (r templatedResult) FailureMessage(args []ast.Expr) string { msg, err := renderMessage(r, args) if err != nil { return fmt.Sprintf("failed to render failure message: %s", err) } return msg } func (r templatedResult) UpdatedExpected(stackIndex int) error { // TODO: would be nice to have structured data instead of a map return source.UpdateExpectedValue(stackIndex+1, r.data["x"], r.data["y"]) } // ResultFailureTemplate returns a [Result] with a template string and data which // can be used to format a failure message. The template may access data from .Data, // the comparison args with the callArg function, and the formatNode function may // be used to format the call args. func ResultFailureTemplate(template string, data map[string]interface{}) Result { return templatedResult{template: template, data: data} } func renderMessage(result templatedResult, args []ast.Expr) (string, error) { tmpl := template.New("failure").Funcs(template.FuncMap{ "formatNode": source.FormatNode, "callArg": func(index int) ast.Expr { if index >= len(args) { return nil } return args[index] }, // TODO: any way to include this from ErrorIS instead of here? "notStdlibErrorType": func(typ interface{}) bool { r := reflect.TypeOf(typ) return r != stdlibFmtErrorType && r != stdlibErrorNewType }, }) var err error tmpl, err = tmpl.Parse(result.template) if err != nil { return "", err } buf := new(bytes.Buffer) err = tmpl.Execute(buf, map[string]interface{}{ "Data": result.data, }) return buf.String(), err } gotest.tools-3.5.1/assert/example_test.go000066400000000000000000000010311463245364400205140ustar00rootroot00000000000000package assert_test import ( "fmt" "regexp" "testing" "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" ) var t = &testing.T{} func ExampleAssert_customComparison() { regexPattern := func(value string, pattern string) cmp.Comparison { return func() cmp.Result { re := regexp.MustCompile(pattern) if re.MatchString(value) { return cmp.ResultSuccess } return cmp.ResultFailure( fmt.Sprintf("%q did not match pattern %q", value, pattern)) } } assert.Assert(t, regexPattern("12345.34", `\d+.\d\d`)) } gotest.tools-3.5.1/assert/opt/000077500000000000000000000000001463245364400163025ustar00rootroot00000000000000gotest.tools-3.5.1/assert/opt/opt.go000066400000000000000000000072271463245364400174430ustar00rootroot00000000000000/*Package opt provides common go-cmp.Options for use with assert.DeepEqual. */ package opt // import "gotest.tools/v3/assert/opt" import ( "fmt" "reflect" "strings" "time" gocmp "github.com/google/go-cmp/cmp" ) // DurationWithThreshold returns a [gocmp.Comparer] for comparing [time.Duration]. The // Comparer returns true if the difference between the two Duration values is // within the threshold and neither value is zero. func DurationWithThreshold(threshold time.Duration) gocmp.Option { return gocmp.Comparer(cmpDuration(threshold)) } func cmpDuration(threshold time.Duration) func(x, y time.Duration) bool { return func(x, y time.Duration) bool { if x == 0 || y == 0 { return false } delta := x - y return delta <= threshold && delta >= -threshold } } // TimeWithThreshold returns a [gocmp.Comparer] for comparing [time.Time]. The // Comparer returns true if the difference between the two Time values is // within the threshold and neither value is zero. func TimeWithThreshold(threshold time.Duration) gocmp.Option { return gocmp.Comparer(cmpTime(threshold)) } func cmpTime(threshold time.Duration) func(x, y time.Time) bool { return func(x, y time.Time) bool { if x.IsZero() || y.IsZero() { return false } delta := x.Sub(y) return delta <= threshold && delta >= -threshold } } // PathString is a [gocmp.FilterPath] filter that returns true when path.String() // matches any of the specs. // // The path spec is a dot separated string where each segment is a field name. // Slices, Arrays, and Maps are always matched against every element in the // sequence. [gocmp.Indirect], [gocmp.Transform], and [gocmp.TypeAssertion] are always // ignored. // // Note: this path filter is not type safe. Incorrect paths will be silently // ignored. Consider using a type safe path filter for more complex paths. func PathString(specs ...string) func(path gocmp.Path) bool { return func(path gocmp.Path) bool { for _, spec := range specs { if path.String() == spec { return true } } return false } } // PathDebug is a [gocmp.FilterPath] filter that always returns false. It prints // each path it receives. It can be used to debug path matching problems. func PathDebug(path gocmp.Path) bool { fmt.Printf("PATH string=%s gostring=%s\n", path, path.GoString()) for _, step := range path { fmt.Printf(" STEP %s\ttype=%s\t%s\n", formatStepType(step), step.Type(), stepTypeFields(step)) } return false } func formatStepType(step gocmp.PathStep) string { return strings.Title(strings.TrimPrefix(reflect.TypeOf(step).String(), "*cmp.")) } func stepTypeFields(step gocmp.PathStep) string { switch typed := step.(type) { case gocmp.StructField: return fmt.Sprintf("name=%s", typed.Name()) case gocmp.MapIndex: return fmt.Sprintf("key=%s", typed.Key().Interface()) case gocmp.Transform: return fmt.Sprintf("name=%s", typed.Name()) case gocmp.SliceIndex: return fmt.Sprintf("name=%d", typed.Key()) } return "" } // PathField is a [gocmp.FilterPath] filter that matches a struct field by name. // PathField will match every instance of the field in a recursive or nested // structure. func PathField(structType interface{}, field string) func(gocmp.Path) bool { typ := reflect.TypeOf(structType) if typ.Kind() != reflect.Struct { panic(fmt.Sprintf("type %s is not a struct", typ)) } if _, ok := typ.FieldByName(field); !ok { panic(fmt.Sprintf("type %s does not have field %s", typ, field)) } return func(path gocmp.Path) bool { return path.Index(-2).Type() == typ && isStructField(path.Index(-1), field) } } func isStructField(step gocmp.PathStep, name string) bool { field, ok := step.(gocmp.StructField) return ok && field.Name() == name } gotest.tools-3.5.1/assert/opt/opt_test.go000066400000000000000000000133511463245364400204750ustar00rootroot00000000000000package opt import ( "sort" "testing" "time" gocmp "github.com/google/go-cmp/cmp" "gotest.tools/v3/assert" ) func TestDurationWithThreshold(t *testing.T) { var testcases = []struct { name string x, y, threshold time.Duration expected bool }{ { name: "delta is threshold", threshold: time.Second, x: 3 * time.Second, y: 2 * time.Second, expected: true, }, { name: "delta is negative threshold", threshold: time.Second, x: 2 * time.Second, y: 3 * time.Second, expected: true, }, { name: "delta within threshold", threshold: time.Second, x: 300 * time.Millisecond, y: 100 * time.Millisecond, expected: true, }, { name: "delta within negative threshold", threshold: time.Second, x: 100 * time.Millisecond, y: 300 * time.Millisecond, expected: true, }, { name: "delta outside threshold", threshold: time.Second, x: 5 * time.Second, y: 300 * time.Millisecond, }, { name: "delta outside negative threshold", threshold: time.Second, x: 300 * time.Millisecond, y: 5 * time.Second, }, { name: "x is 0", threshold: time.Second, y: 5 * time.Millisecond, }, { name: "y is 0", threshold: time.Second, x: 5 * time.Millisecond, }, } for _, testcase := range testcases { t.Run(testcase.name, func(t *testing.T) { actual := cmpDuration(testcase.threshold)(testcase.x, testcase.y) assert.Equal(t, actual, testcase.expected) }) } } func TestTimeWithThreshold(t *testing.T) { var now = time.Now() var testcases = []struct { name string x, y time.Time threshold time.Duration expected bool }{ { name: "delta is threshold", threshold: time.Minute, x: now, y: now.Add(time.Minute), expected: true, }, { name: "delta is negative threshold", threshold: time.Minute, x: now, y: now.Add(-time.Minute), expected: true, }, { name: "delta within threshold", threshold: time.Hour, x: now, y: now.Add(time.Minute), expected: true, }, { name: "delta within negative threshold", threshold: time.Hour, x: now, y: now.Add(-time.Minute), expected: true, }, { name: "delta outside threshold", threshold: time.Second, x: now, y: now.Add(time.Minute), }, { name: "delta outside negative threshold", threshold: time.Second, x: now, y: now.Add(-time.Minute), }, { name: "x is 0", threshold: time.Second, y: now, }, { name: "y is 0", threshold: time.Second, x: now, }, } for _, testcase := range testcases { t.Run(testcase.name, func(t *testing.T) { actual := cmpTime(testcase.threshold)(testcase.x, testcase.y) assert.Equal(t, actual, testcase.expected) }) } } type node struct { Value nodeValue Labels map[string]node Children []node Ref *node } type nodeValue struct { Value int } type pathRecorder struct { filter func(p gocmp.Path) bool matches []string } func (p *pathRecorder) record(path gocmp.Path) bool { if p.filter(path) { p.matches = append(p.matches, path.GoString()) } return false } func matchPaths(fixture interface{}, filter func(gocmp.Path) bool) []string { rec := &pathRecorder{filter: filter} gocmp.Equal(fixture, fixture, gocmp.FilterPath(rec.record, gocmp.Ignore())) sort.Strings(rec.matches) return rec.matches } func TestPathStringFromStruct(t *testing.T) { fixture := node{ Ref: &node{ Children: []node{ {}, { Labels: map[string]node{ "first": {Value: nodeValue{Value: 3}}, }, }, }, }, } spec := "Ref.Children.Labels.Value" matches := matchPaths(fixture, PathString(spec)) expected := []string{ `{opt.node}.Ref.Children[1].Labels["first"].Value`, `{opt.node}.Ref.Children[1].Labels["first"].Value`, } assert.DeepEqual(t, matches, expected) } func TestPathStringFromSlice(t *testing.T) { fixture := []node{ { Ref: &node{ Children: []node{ {}, { Labels: map[string]node{ "first": {}, "second": { Ref: &node{Value: nodeValue{Value: 3}}, }, }, }, }, }, }, } spec := "Ref.Children.Labels.Ref.Value" matches := matchPaths(fixture, PathString(spec)) expected := []string{ `{[]opt.node}[0].Ref.Children[1].Labels["second"].Ref.Value`, `{[]opt.node}[0].Ref.Children[1].Labels["second"].Ref.Value`, `{[]opt.node}[0].Ref.Children[1].Labels["second"].Ref.Value`, `{[]opt.node}[0].Ref.Children[1].Labels["second"].Ref.Value`, } assert.DeepEqual(t, matches, expected) } func TestPathField(t *testing.T) { fixture := node{ Value: nodeValue{Value: 3}, Children: []node{ {}, {Value: nodeValue{Value: 2}}, {Ref: &node{Value: nodeValue{Value: 9}}}, }, } filter := PathField(nodeValue{}, "Value") matches := matchPaths(fixture, filter) expected := []string{ "{opt.node}.Children[0].Value.Value", "{opt.node}.Children[0].Value.Value", "{opt.node}.Children[1].Value.Value", "{opt.node}.Children[1].Value.Value", "{opt.node}.Children[2].Ref.Value.Value", "{opt.node}.Children[2].Ref.Value.Value", "{opt.node}.Children[2].Value.Value", "{opt.node}.Children[2].Value.Value", "{opt.node}.Value.Value", } assert.DeepEqual(t, matches, expected) } func TestPathDebug(t *testing.T) { fixture := node{ Value: nodeValue{Value: 3}, Children: []node{ {Ref: &node{Value: nodeValue{Value: 9}}}, }, Labels: map[string]node{ "label1": {}, }, } gocmp.Equal(fixture, fixture, gocmp.FilterPath(PathDebug, gocmp.Ignore())) } gotest.tools-3.5.1/env/000077500000000000000000000000001463245364400147675ustar00rootroot00000000000000gotest.tools-3.5.1/env/env.go000066400000000000000000000060701463245364400161110ustar00rootroot00000000000000/* Package env provides functions to test code that read environment variables or the current working directory. */ package env // import "gotest.tools/v3/env" import ( "os" "strings" "gotest.tools/v3/assert" "gotest.tools/v3/internal/cleanup" ) type helperT interface { Helper() } // Patch changes the value of an environment variable, and returns a // function which will reset the the value of that variable back to the // previous state. // // When used with Go 1.14+ the unpatch function will be called automatically // when the test ends, unless the TEST_NOCLEANUP env var is set to true. // // Deprecated: use t.SetEnv func Patch(t assert.TestingT, key, value string) func() { if ht, ok := t.(helperT); ok { ht.Helper() } oldValue, envVarExists := os.LookupEnv(key) assert.NilError(t, os.Setenv(key, value)) clean := func() { if ht, ok := t.(helperT); ok { ht.Helper() } if !envVarExists { assert.NilError(t, os.Unsetenv(key)) return } assert.NilError(t, os.Setenv(key, oldValue)) } cleanup.Cleanup(t, clean) return clean } // PatchAll sets the environment to env, and returns a function which will // reset the environment back to the previous state. // // When used with Go 1.14+ the unpatch function will be called automatically // when the test ends, unless the TEST_NOCLEANUP env var is set to true. func PatchAll(t assert.TestingT, env map[string]string) func() { if ht, ok := t.(helperT); ok { ht.Helper() } oldEnv := os.Environ() os.Clearenv() for key, value := range env { assert.NilError(t, os.Setenv(key, value), "setenv %s=%s", key, value) } clean := func() { if ht, ok := t.(helperT); ok { ht.Helper() } os.Clearenv() for key, oldVal := range ToMap(oldEnv) { assert.NilError(t, os.Setenv(key, oldVal), "setenv %s=%s", key, oldVal) } } cleanup.Cleanup(t, clean) return clean } // ToMap takes a list of strings in the format returned by [os.Environ] and // returns a mapping of keys to values. func ToMap(env []string) map[string]string { result := map[string]string{} for _, raw := range env { key, value := getParts(raw) result[key] = value } return result } func getParts(raw string) (string, string) { if raw == "" { return "", "" } // Environment variables on windows can begin with = // http://blogs.msdn.com/b/oldnewthing/archive/2010/05/06/10008132.aspx parts := strings.SplitN(raw[1:], "=", 2) key := raw[:1] + parts[0] if len(parts) == 1 { return key, "" } return key, parts[1] } // ChangeWorkingDir to the directory, and return a function which restores the // previous working directory. // // When used with Go 1.14+ the previous working directory will be restored // automatically when the test ends, unless the TEST_NOCLEANUP env var is set to // true. func ChangeWorkingDir(t assert.TestingT, dir string) func() { if ht, ok := t.(helperT); ok { ht.Helper() } cwd, err := os.Getwd() assert.NilError(t, err) assert.NilError(t, os.Chdir(dir)) clean := func() { if ht, ok := t.(helperT); ok { ht.Helper() } assert.NilError(t, os.Chdir(cwd)) } cleanup.Cleanup(t, clean) return clean } gotest.tools-3.5.1/env/env_test.go000066400000000000000000000071501463245364400171500ustar00rootroot00000000000000package env import ( "os" "runtime" "sort" "testing" "gotest.tools/v3/assert" "gotest.tools/v3/fs" "gotest.tools/v3/internal/source" "gotest.tools/v3/skip" ) func TestPatchFromUnset(t *testing.T) { key, value := "FOO_IS_UNSET", "VALUE" revert := Patch(t, key, value) assert.Assert(t, value == os.Getenv(key)) revert() _, isSet := os.LookupEnv(key) assert.Assert(t, !isSet) } func TestPatch(t *testing.T) { skip.If(t, os.Getenv("PATH") == "") oldVal := os.Getenv("PATH") key, value := "PATH", "NEWVALUE" revert := Patch(t, key, value) assert.Assert(t, value == os.Getenv(key)) revert() assert.Assert(t, oldVal == os.Getenv(key)) } func TestPatch_IntegrationWithCleanup(t *testing.T) { skip.If(t, source.GoVersionLessThan(1, 14)) key := "totally_unique_env_var_key" t.Run("cleanup in subtest", func(t *testing.T) { Patch(t, key, "the-new-value") assert.Equal(t, os.Getenv(key), "the-new-value") }) t.Run("env var is unset", func(t *testing.T) { v, ok := os.LookupEnv(key) assert.Assert(t, !ok, "expected env var to be unset, got %v", v) }) } func TestPatchAll(t *testing.T) { oldEnv := os.Environ() newEnv := map[string]string{ "FIRST": "STARS", "THEN": "MOON", } revert := PatchAll(t, newEnv) actual := os.Environ() sort.Strings(actual) assert.DeepEqual(t, []string{"FIRST=STARS", "THEN=MOON"}, actual) revert() assert.DeepEqual(t, sorted(oldEnv), sorted(os.Environ())) } func TestPatchAllWindows(t *testing.T) { skip.If(t, runtime.GOOS != "windows") oldEnv := os.Environ() newEnv := map[string]string{ "FIRST": "STARS", "THEN": "MOON", "=FINAL": "SUN", "=BAR": "", } revert := PatchAll(t, newEnv) actual := os.Environ() sort.Strings(actual) assert.DeepEqual(t, []string{"=BAR=", "=FINAL=SUN", "FIRST=STARS", "THEN=MOON"}, actual) revert() assert.DeepEqual(t, sorted(oldEnv), sorted(os.Environ())) } func sorted(source []string) []string { sort.Strings(source) return source } func TestPatchAll_IntegrationWithCleanup(t *testing.T) { skip.If(t, source.GoVersionLessThan(1, 14)) key := "totally_unique_env_var_key" t.Run("cleanup in subtest", func(t *testing.T) { PatchAll(t, map[string]string{key: "the-new-value"}) assert.Equal(t, os.Getenv(key), "the-new-value") }) t.Run("env var is unset", func(t *testing.T) { v, ok := os.LookupEnv(key) assert.Assert(t, !ok, "expected env var to be unset, got %v", v) }) } func TestToMap(t *testing.T) { source := []string{ "key=value", "novaluekey", "=foo=bar", "z=singlecharkey", "b", "", } actual := ToMap(source) expected := map[string]string{ "key": "value", "novaluekey": "", "=foo": "bar", "z": "singlecharkey", "b": "", "": "", } assert.DeepEqual(t, expected, actual) } func TestChangeWorkingDir(t *testing.T) { tmpDir := fs.NewDir(t, t.Name()) defer tmpDir.Remove() origWorkDir := pwd(t) reset := ChangeWorkingDir(t, tmpDir.Path()) t.Run("changed to dir", func(t *testing.T) { assert.Equal(t, pwd(t), tmpDir.Path()) }) t.Run("reset dir", func(t *testing.T) { reset() assert.Equal(t, pwd(t), origWorkDir) }) } func TestChangeWorkingDir_IntegrationWithCleanup(t *testing.T) { skip.If(t, source.GoVersionLessThan(1, 14)) tmpDir := fs.NewDir(t, t.Name()) defer tmpDir.Remove() origWorkDir := pwd(t) t.Run("cleanup in subtest", func(t *testing.T) { ChangeWorkingDir(t, tmpDir.Path()) assert.Equal(t, pwd(t), tmpDir.Path()) }) t.Run("working dir is reset", func(t *testing.T) { assert.Equal(t, pwd(t), origWorkDir) }) } func pwd(t *testing.T) string { t.Helper() dir, err := os.Getwd() assert.NilError(t, err) return dir } gotest.tools-3.5.1/env/example_test.go000066400000000000000000000005151463245364400200110ustar00rootroot00000000000000package env import "testing" var t = &testing.T{} // Patch an environment variable and defer to return to the previous state func ExamplePatch() { defer Patch(t, "PATH", "/custom/path")() } // Patch all environment variables func ExamplePatchAll() { defer PatchAll(t, map[string]string{ "ONE": "FOO", "TWO": "BAR", })() } gotest.tools-3.5.1/fs/000077500000000000000000000000001463245364400146075ustar00rootroot00000000000000gotest.tools-3.5.1/fs/example_test.go000066400000000000000000000031101463245364400176230ustar00rootroot00000000000000package fs_test import ( "os" "testing" "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" "gotest.tools/v3/fs" "gotest.tools/v3/golden" ) var t = &testing.T{} // Create a temporary directory which contains a single file func ExampleNewDir() { dir := fs.NewDir(t, "test-name", fs.WithFile("file1", "content\n")) defer dir.Remove() files, err := os.ReadDir(dir.Path()) assert.NilError(t, err) assert.Assert(t, cmp.Len(files, 0)) } // Create a new file with some content func ExampleNewFile() { file := fs.NewFile(t, "test-name", fs.WithContent("content\n"), fs.AsUser(0, 0)) defer file.Remove() content, err := os.ReadFile(file.Path()) assert.NilError(t, err) assert.Equal(t, "content\n", content) } // Create a directory and subdirectory with files func ExampleWithDir() { dir := fs.NewDir(t, "test-name", fs.WithDir("subdir", fs.WithMode(os.FileMode(0700)), fs.WithFile("file1", "content\n")), ) defer dir.Remove() } // Test that a directory contains the expected files, and all the files have the // expected properties. func ExampleEqual() { path := operationWhichCreatesFiles() expected := fs.Expected(t, fs.WithFile("one", "", fs.WithBytes(golden.Get(t, "one.golden")), fs.WithMode(0600)), fs.WithDir("data", fs.WithFile("config", "", fs.MatchAnyFileContent))) assert.Assert(t, fs.Equal(path, expected)) } func operationWhichCreatesFiles() string { return "example-path" } // Add a file to an existing directory func ExampleApply() { dir := fs.NewDir(t, "test-name") defer dir.Remove() fs.Apply(t, dir, fs.WithFile("file1", "content\n")) } gotest.tools-3.5.1/fs/file.go000066400000000000000000000064121463245364400160600ustar00rootroot00000000000000/* Package fs provides tools for creating temporary files, and testing the contents and structure of a directory. */ package fs // import "gotest.tools/v3/fs" import ( "os" "path/filepath" "runtime" "strings" "gotest.tools/v3/assert" "gotest.tools/v3/internal/cleanup" ) // Path objects return their filesystem path. Path may be implemented by a // real filesystem object (such as File and Dir) or by a type which updates // entries in a Manifest. type Path interface { Path() string Remove() } var ( _ Path = &Dir{} _ Path = &File{} ) // File is a temporary file on the filesystem type File struct { path string } type helperT interface { Helper() } // NewFile creates a new file in a temporary directory using prefix as part of // the filename. The PathOps are applied to the before returning the File. // // When used with Go 1.14+ the file will be automatically removed when the test // ends, unless the TEST_NOCLEANUP env var is set to true. func NewFile(t assert.TestingT, prefix string, ops ...PathOp) *File { if ht, ok := t.(helperT); ok { ht.Helper() } tempfile, err := os.CreateTemp("", cleanPrefix(prefix)+"-") assert.NilError(t, err) file := &File{path: tempfile.Name()} cleanup.Cleanup(t, file.Remove) assert.NilError(t, tempfile.Close()) assert.NilError(t, applyPathOps(file, ops)) return file } func cleanPrefix(prefix string) string { // windows requires both / and \ are replaced if runtime.GOOS == "windows" { prefix = strings.Replace(prefix, string(os.PathSeparator), "-", -1) } return strings.Replace(prefix, "/", "-", -1) } // Path returns the full path to the file func (f *File) Path() string { return f.path } // Remove the file func (f *File) Remove() { _ = os.Remove(f.path) } // Dir is a temporary directory type Dir struct { path string } // NewDir returns a new temporary directory using prefix as part of the directory // name. The PathOps are applied before returning the Dir. // // When used with Go 1.14+ the directory will be automatically removed when the test // ends, unless the TEST_NOCLEANUP env var is set to true. func NewDir(t assert.TestingT, prefix string, ops ...PathOp) *Dir { if ht, ok := t.(helperT); ok { ht.Helper() } path, err := os.MkdirTemp("", cleanPrefix(prefix)+"-") assert.NilError(t, err) dir := &Dir{path: path} cleanup.Cleanup(t, dir.Remove) assert.NilError(t, applyPathOps(dir, ops)) return dir } // Path returns the full path to the directory func (d *Dir) Path() string { return d.path } // Remove the directory func (d *Dir) Remove() { _ = os.RemoveAll(d.path) } // Join returns a new path with this directory as the base of the path func (d *Dir) Join(parts ...string) string { return filepath.Join(append([]string{d.Path()}, parts...)...) } // DirFromPath returns a Dir for a path that already exists. No directory is created. // Unlike NewDir the directory will not be removed automatically when the test exits, // it is the callers responsibly to remove the directory. // DirFromPath can be used with Apply to modify an existing directory. // // If the path does not already exist, use NewDir instead. func DirFromPath(t assert.TestingT, path string, ops ...PathOp) *Dir { if ht, ok := t.(helperT); ok { ht.Helper() } dir := &Dir{path: path} assert.NilError(t, applyPathOps(dir, ops)) return dir } gotest.tools-3.5.1/fs/file_test.go000066400000000000000000000051561463245364400171230ustar00rootroot00000000000000package fs_test import ( "errors" "os" "path/filepath" "testing" "gotest.tools/v3/assert" "gotest.tools/v3/fs" "gotest.tools/v3/internal/source" "gotest.tools/v3/skip" ) func TestNewDirWithOpsAndManifestEqual(t *testing.T) { var userOps []fs.PathOp if os.Geteuid() == 0 { userOps = append(userOps, fs.AsUser(1001, 1002)) } ops := []fs.PathOp{ fs.WithFile("file1", "contenta", fs.WithMode(0400)), fs.WithFile("file2", "", fs.WithBytes([]byte{0, 1, 2})), fs.WithFile("file5", "", userOps...), fs.WithSymlink("link1", "file1"), fs.WithDir("sub", fs.WithFiles(map[string]string{ "file3": "contentb", "file4": "contentc", }), fs.WithMode(0705), ), } dir := fs.NewDir(t, "test-all", ops...) defer dir.Remove() manifestOps := append( ops[:3], fs.WithSymlink("link1", dir.Join("file1")), ops[4], ) assert.Assert(t, fs.Equal(dir.Path(), fs.Expected(t, manifestOps...))) } func TestNewFile(t *testing.T) { t.Run("with test name", func(t *testing.T) { tmpFile := fs.NewFile(t, t.Name()) _, err := os.Stat(tmpFile.Path()) assert.NilError(t, err) tmpFile.Remove() _, err = os.Stat(tmpFile.Path()) assert.ErrorIs(t, err, os.ErrNotExist) }) t.Run(`with \ in name`, func(t *testing.T) { tmpFile := fs.NewFile(t, `foo\thing`) _, err := os.Stat(tmpFile.Path()) assert.NilError(t, err) tmpFile.Remove() _, err = os.Stat(tmpFile.Path()) assert.ErrorIs(t, err, os.ErrNotExist) }) } func TestNewFile_IntegrationWithCleanup(t *testing.T) { skip.If(t, source.GoVersionLessThan(1, 14)) var tmpFile *fs.File t.Run("cleanup in subtest", func(t *testing.T) { tmpFile = fs.NewFile(t, t.Name()) _, err := os.Stat(tmpFile.Path()) assert.NilError(t, err) }) t.Run("file has been removed", func(t *testing.T) { _, err := os.Stat(tmpFile.Path()) assert.ErrorIs(t, err, os.ErrNotExist) }) } func TestNewDir_IntegrationWithCleanup(t *testing.T) { skip.If(t, source.GoVersionLessThan(1, 14)) var tmpFile *fs.Dir t.Run("cleanup in subtest", func(t *testing.T) { tmpFile = fs.NewDir(t, t.Name()) _, err := os.Stat(tmpFile.Path()) assert.NilError(t, err) }) t.Run("dir has been removed", func(t *testing.T) { _, err := os.Stat(tmpFile.Path()) assert.ErrorIs(t, err, os.ErrNotExist) }) } func TestDirFromPath(t *testing.T) { tmpdir := t.TempDir() dir := fs.DirFromPath(t, tmpdir, fs.WithFile("newfile", "")) _, err := os.Stat(dir.Join("newfile")) assert.NilError(t, err) assert.Equal(t, dir.Path(), tmpdir) assert.Equal(t, dir.Join("newfile"), filepath.Join(tmpdir, "newfile")) dir.Remove() _, err = os.Stat(tmpdir) assert.Assert(t, errors.Is(err, os.ErrNotExist)) } gotest.tools-3.5.1/fs/manifest.go000066400000000000000000000055431463245364400167530ustar00rootroot00000000000000package fs import ( "fmt" "io" "os" "path/filepath" "gotest.tools/v3/assert" ) // Manifest stores the expected structure and properties of files and directories // in a filesystem. type Manifest struct { root *directory } type resource struct { mode os.FileMode uid uint32 gid uint32 } type file struct { resource content io.ReadCloser ignoreCariageReturn bool compareContentFunc func(b []byte) CompareResult } func (f *file) Type() string { return "file" } type symlink struct { resource target string } func (f *symlink) Type() string { return "symlink" } type directory struct { resource items map[string]dirEntry filepathGlobs map[string]*filePath } func (f *directory) Type() string { return "directory" } type dirEntry interface { Type() string } // ManifestFromDir creates a [Manifest] by reading the directory at path. The // manifest stores the structure and properties of files in the directory. // ManifestFromDir can be used with [Equal] to compare two directories. func ManifestFromDir(t assert.TestingT, path string) Manifest { if ht, ok := t.(helperT); ok { ht.Helper() } manifest, err := manifestFromDir(path) assert.NilError(t, err) return manifest } func manifestFromDir(path string) (Manifest, error) { info, err := os.Stat(path) switch { case err != nil: return Manifest{}, err case !info.IsDir(): return Manifest{}, fmt.Errorf("path %s must be a directory", path) } directory, err := newDirectory(path, info) return Manifest{root: directory}, err } func newDirectory(path string, info os.FileInfo) (*directory, error) { items := make(map[string]dirEntry) children, err := os.ReadDir(path) if err != nil { return nil, err } for _, child := range children { fullPath := filepath.Join(path, child.Name()) items[child.Name()], err = getTypedResource(fullPath, child) if err != nil { return nil, err } } return &directory{ resource: newResourceFromInfo(info), items: items, filepathGlobs: make(map[string]*filePath), }, nil } func getTypedResource(path string, entry os.DirEntry) (dirEntry, error) { info, err := entry.Info() if err != nil { return nil, err } switch { case info.IsDir(): return newDirectory(path, info) case info.Mode()&os.ModeSymlink != 0: return newSymlink(path, info) // TODO: devices, pipes? default: return newFile(path, info) } } func newSymlink(path string, info os.FileInfo) (*symlink, error) { target, err := os.Readlink(path) if err != nil { return nil, err } return &symlink{ resource: newResourceFromInfo(info), target: target, }, err } func newFile(path string, info os.FileInfo) (*file, error) { // TODO: defer file opening to reduce number of open FDs? readCloser, err := os.Open(path) if err != nil { return nil, err } return &file{ resource: newResourceFromInfo(info), content: readCloser, }, err } gotest.tools-3.5.1/fs/manifest_test.go000066400000000000000000000052101463245364400200010ustar00rootroot00000000000000package fs import ( "bytes" "io" "os" "runtime" "strings" "testing" "github.com/google/go-cmp/cmp" "gotest.tools/v3/assert" ) func TestManifestFromDir(t *testing.T) { var defaultFileMode os.FileMode = 0644 var subDirMode = 0755 | os.ModeDir var jFileMode os.FileMode = 0600 if runtime.GOOS == "windows" { defaultFileMode = 0666 subDirMode = 0777 | os.ModeDir jFileMode = 0666 } var userOps []PathOp var expectedUserResource = newResource(defaultFileMode) if os.Geteuid() == 0 { userOps = append(userOps, AsUser(1001, 1002)) expectedUserResource = resource{mode: defaultFileMode, uid: 1001, gid: 1002} } srcDir := NewDir(t, t.Name(), WithFile("j", "content j", WithMode(0600)), WithDir("s", WithFile("k", "content k")), WithSymlink("f", "j"), WithFile("x", "content x", userOps...)) defer srcDir.Remove() expected := Manifest{ root: &directory{ resource: newResource(defaultRootDirMode), items: map[string]dirEntry{ "j": &file{ resource: newResource(jFileMode), content: readCloser("content j"), }, "s": &directory{ resource: newResource(subDirMode), items: map[string]dirEntry{ "k": &file{ resource: newResource(defaultFileMode), content: readCloser("content k"), }, }, filepathGlobs: map[string]*filePath{}, }, "f": &symlink{ resource: newResource(defaultSymlinkMode), target: srcDir.Join("j"), }, "x": &file{ resource: expectedUserResource, content: readCloser("content x"), }, }, filepathGlobs: map[string]*filePath{}, }, } actual := ManifestFromDir(t, srcDir.Path()) assert.DeepEqual(t, actual, expected, cmpManifest) actual.root.items["j"].(*file).content.Close() actual.root.items["x"].(*file).content.Close() actual.root.items["s"].(*directory).items["k"].(*file).content.Close() } func TestSymlinks(t *testing.T) { rootDirectory := NewDir(t, "root", WithFile("foo.txt", "foo"), WithSymlink("foo.link", "foo.txt")) defer rootDirectory.Remove() expected := Expected(t, WithFile("foo.txt", "foo"), WithSymlink("foo.link", rootDirectory.Join("foo.txt"))) assert.Assert(t, Equal(rootDirectory.Path(), expected)) } var cmpManifest = cmp.Options{ cmp.AllowUnexported(Manifest{}, resource{}, file{}, symlink{}, directory{}), cmp.Comparer(func(x, y io.ReadCloser) bool { if x == nil || y == nil { return x == y } xContent, err := io.ReadAll(x) if err != nil { return false } yContent, err := io.ReadAll(y) if err != nil { return false } return bytes.Equal(xContent, yContent) }), } func readCloser(s string) io.ReadCloser { return io.NopCloser(strings.NewReader(s)) } gotest.tools-3.5.1/fs/manifest_unix.go000066400000000000000000000011641463245364400200110ustar00rootroot00000000000000//go:build !windows // +build !windows package fs import ( "os" "runtime" "syscall" ) const defaultRootDirMode = os.ModeDir | 0700 var defaultSymlinkMode = os.ModeSymlink | 0777 func init() { switch runtime.GOOS { case "darwin": defaultSymlinkMode = os.ModeSymlink | 0755 } } func newResourceFromInfo(info os.FileInfo) resource { statT := info.Sys().(*syscall.Stat_t) return resource{ mode: info.Mode(), uid: statT.Uid, gid: statT.Gid, } } func (p *filePath) SetMode(mode os.FileMode) { p.file.mode = mode } func (p *directoryPath) SetMode(mode os.FileMode) { p.directory.mode = mode | os.ModeDir } gotest.tools-3.5.1/fs/manifest_windows.go000066400000000000000000000007111463245364400205150ustar00rootroot00000000000000package fs import "os" const ( defaultRootDirMode = os.ModeDir | 0777 defaultSymlinkMode = os.ModeSymlink | 0666 ) func newResourceFromInfo(info os.FileInfo) resource { return resource{mode: info.Mode()} } func (p *filePath) SetMode(mode os.FileMode) { bits := mode & 0600 p.file.mode = bits + bits/010 + bits/0100 } // TODO: is mode ignored on windows? func (p *directoryPath) SetMode(mode os.FileMode) { p.directory.mode = defaultRootDirMode } gotest.tools-3.5.1/fs/ops.go000066400000000000000000000163001463245364400157370ustar00rootroot00000000000000package fs import ( "bytes" "fmt" "io" "os" "path/filepath" "strings" "time" "gotest.tools/v3/assert" ) const defaultFileMode = 0644 // PathOp is a function which accepts a [Path] and performs an operation on that // path. When called with real filesystem objects ([File] or [Dir]) a PathOp modifies // the filesystem at the path. When used with a [Manifest] object a PathOp updates // the manifest to expect a value. type PathOp func(path Path) error type manifestResource interface { SetMode(mode os.FileMode) SetUID(uid uint32) SetGID(gid uint32) } type manifestFile interface { manifestResource SetContent(content io.ReadCloser) } type manifestDirectory interface { manifestResource AddSymlink(path, target string) error AddFile(path string, ops ...PathOp) error AddDirectory(path string, ops ...PathOp) error } // WithContent writes content to a file at [Path] func WithContent(content string) PathOp { return func(path Path) error { if m, ok := path.(manifestFile); ok { m.SetContent(io.NopCloser(strings.NewReader(content))) return nil } return os.WriteFile(path.Path(), []byte(content), defaultFileMode) } } // WithBytes write bytes to a file at [Path] func WithBytes(raw []byte) PathOp { return func(path Path) error { if m, ok := path.(manifestFile); ok { m.SetContent(io.NopCloser(bytes.NewReader(raw))) return nil } return os.WriteFile(path.Path(), raw, defaultFileMode) } } // WithReaderContent copies the reader contents to the file at [Path] func WithReaderContent(r io.Reader) PathOp { return func(path Path) error { if m, ok := path.(manifestFile); ok { m.SetContent(io.NopCloser(r)) return nil } f, err := os.OpenFile(path.Path(), os.O_WRONLY, defaultFileMode) if err != nil { return err } defer f.Close() _, err = io.Copy(f, r) return err } } // AsUser changes ownership of the file system object at [Path] func AsUser(uid, gid int) PathOp { return func(path Path) error { if m, ok := path.(manifestResource); ok { m.SetUID(uint32(uid)) m.SetGID(uint32(gid)) return nil } return os.Chown(path.Path(), uid, gid) } } // WithFile creates a file in the directory at path with content func WithFile(filename, content string, ops ...PathOp) PathOp { return func(path Path) error { if m, ok := path.(manifestDirectory); ok { ops = append([]PathOp{WithContent(content), WithMode(defaultFileMode)}, ops...) return m.AddFile(filename, ops...) } fullpath := filepath.Join(path.Path(), filepath.FromSlash(filename)) if err := createFile(fullpath, content); err != nil { return err } return applyPathOps(&File{path: fullpath}, ops) } } func createFile(fullpath string, content string) error { return os.WriteFile(fullpath, []byte(content), defaultFileMode) } // WithFiles creates all the files in the directory at path with their content func WithFiles(files map[string]string) PathOp { return func(path Path) error { if m, ok := path.(manifestDirectory); ok { for filename, content := range files { // TODO: remove duplication with WithFile if err := m.AddFile(filename, WithContent(content), WithMode(defaultFileMode)); err != nil { return err } } return nil } for filename, content := range files { fullpath := filepath.Join(path.Path(), filepath.FromSlash(filename)) if err := createFile(fullpath, content); err != nil { return err } } return nil } } // FromDir copies the directory tree from the source path into the new [Dir] func FromDir(source string) PathOp { return func(path Path) error { if _, ok := path.(manifestDirectory); ok { return fmt.Errorf("use manifest.FromDir") } return copyDirectory(source, path.Path()) } } // WithDir creates a subdirectory in the directory at path. Additional [PathOp] // can be used to modify the subdirectory func WithDir(name string, ops ...PathOp) PathOp { const defaultMode = 0755 return func(path Path) error { if m, ok := path.(manifestDirectory); ok { ops = append([]PathOp{WithMode(defaultMode)}, ops...) return m.AddDirectory(name, ops...) } fullpath := filepath.Join(path.Path(), filepath.FromSlash(name)) err := os.MkdirAll(fullpath, defaultMode) if err != nil { return err } return applyPathOps(&Dir{path: fullpath}, ops) } } // Apply the PathOps to the [File] func Apply(t assert.TestingT, path Path, ops ...PathOp) { if ht, ok := t.(helperT); ok { ht.Helper() } assert.NilError(t, applyPathOps(path, ops)) } func applyPathOps(path Path, ops []PathOp) error { for _, op := range ops { if err := op(path); err != nil { return err } } return nil } // WithMode sets the file mode on the directory or file at [Path] func WithMode(mode os.FileMode) PathOp { return func(path Path) error { if m, ok := path.(manifestResource); ok { m.SetMode(mode) return nil } return os.Chmod(path.Path(), mode) } } func copyDirectory(source, dest string) error { entries, err := os.ReadDir(source) if err != nil { return err } for _, entry := range entries { sourcePath := filepath.Join(source, entry.Name()) destPath := filepath.Join(dest, entry.Name()) err = copyEntry(entry, destPath, sourcePath) if err != nil { return err } } return nil } func copyEntry(entry os.DirEntry, destPath string, sourcePath string) error { if entry.IsDir() { if err := os.Mkdir(destPath, 0755); err != nil { return err } return copyDirectory(sourcePath, destPath) } info, err := entry.Info() if err != nil { return err } if info.Mode()&os.ModeSymlink != 0 { return copySymLink(sourcePath, destPath) } return copyFile(sourcePath, destPath) } func copySymLink(source, dest string) error { link, err := os.Readlink(source) if err != nil { return err } return os.Symlink(link, dest) } func copyFile(source, dest string) error { content, err := os.ReadFile(source) if err != nil { return err } return os.WriteFile(dest, content, 0644) } // WithSymlink creates a symlink in the directory which links to target. // Target must be a path relative to the directory. // // Note: the argument order is the inverse of [os.Symlink] to be consistent with // the other functions in this package. func WithSymlink(path, target string) PathOp { return func(root Path) error { if v, ok := root.(manifestDirectory); ok { return v.AddSymlink(path, target) } return os.Symlink(filepath.Join(root.Path(), target), filepath.Join(root.Path(), path)) } } // WithHardlink creates a link in the directory which links to target. // Target must be a path relative to the directory. // // Note: the argument order is the inverse of [os.Link] to be consistent with // the other functions in this package. func WithHardlink(path, target string) PathOp { return func(root Path) error { if _, ok := root.(manifestDirectory); ok { return fmt.Errorf("WithHardlink not implemented for manifests") } return os.Link(filepath.Join(root.Path(), target), filepath.Join(root.Path(), path)) } } // WithTimestamps sets the access and modification times of the file system object // at path. func WithTimestamps(atime, mtime time.Time) PathOp { return func(root Path) error { if _, ok := root.(manifestDirectory); ok { return fmt.Errorf("WithTimestamp not implemented for manifests") } return os.Chtimes(root.Path(), atime, mtime) } } gotest.tools-3.5.1/fs/ops_test.go000066400000000000000000000047661463245364400170130ustar00rootroot00000000000000package fs_test import ( "os" "path/filepath" "runtime" "strings" "testing" "time" "gotest.tools/v3/assert" "gotest.tools/v3/fs" ) func TestFromDir(t *testing.T) { dir := fs.NewDir(t, "test-from-dir", fs.FromDir("testdata/copy-test")) defer dir.Remove() expected := fs.Expected(t, fs.WithFile("1", "1\n"), fs.WithDir("a", fs.WithFile("1", "1\n"), fs.WithFile("2", "2\n"), fs.WithDir("b", fs.WithFile("1", "1\n")))) assert.Assert(t, fs.Equal(dir.Path(), expected)) } func TestFromDirSymlink(t *testing.T) { dir := fs.NewDir(t, "test-from-dir", fs.FromDir("testdata/copy-test-with-symlink")) defer dir.Remove() currentdir, err := os.Getwd() assert.NilError(t, err) link2 := filepath.FromSlash("../2") link3 := "/some/inexistent/link" if runtime.GOOS == "windows" { link3 = filepath.Join(filepath.VolumeName(currentdir), link3) } expected := fs.Expected(t, fs.WithFile("1", "1\n"), fs.WithDir("a", fs.WithFile("1", "1\n"), fs.WithFile("2", "2\n"), fs.WithDir("b", fs.WithFile("1", "1\n"), fs.WithSymlink("2", link2), fs.WithSymlink("3", link3), fs.WithSymlink("4", "5"), ))) assert.Assert(t, fs.Equal(dir.Path(), expected)) } func TestWithTimestamps(t *testing.T) { stamp := time.Date(2011, 11, 11, 5, 55, 55, 0, time.UTC) tmpFile := fs.NewFile(t, t.Name(), fs.WithTimestamps(stamp, stamp)) defer tmpFile.Remove() stat, err := os.Stat(tmpFile.Path()) assert.NilError(t, err) assert.DeepEqual(t, stat.ModTime(), stamp) } func TestApply(t *testing.T) { t.Run("with file", func(t *testing.T) { tmpFile := fs.NewFile(t, "test-update-file", fs.WithContent("contenta")) defer tmpFile.Remove() fs.Apply(t, tmpFile, fs.WithContent("contentb")) content, err := os.ReadFile(tmpFile.Path()) assert.NilError(t, err) assert.Equal(t, string(content), "contentb") }) t.Run("with dir", func(t *testing.T) { tmpDir := fs.NewDir(t, "test-update-dir") defer tmpDir.Remove() fs.Apply(t, tmpDir, fs.WithFile("file1", "contenta")) fs.Apply(t, tmpDir, fs.WithFile("file2", "contentb")) expected := fs.Expected(t, fs.WithFile("file1", "contenta"), fs.WithFile("file2", "contentb")) assert.Assert(t, fs.Equal(tmpDir.Path(), expected)) }) } func TestWithReaderContent(t *testing.T) { content := "this is a test" dir := fs.NewDir(t, t.Name(), fs.WithFile("1", "", fs.WithReaderContent(strings.NewReader(content))), ) defer dir.Remove() expected := fs.Expected(t, fs.WithFile("1", content)) assert.Assert(t, fs.Equal(dir.Path(), expected)) } gotest.tools-3.5.1/fs/path.go000066400000000000000000000111431463245364400160720ustar00rootroot00000000000000package fs import ( "bytes" "io" "os" "gotest.tools/v3/assert" ) // resourcePath is an adaptor for resources so they can be used as a Path // with PathOps. type resourcePath struct{} func (p *resourcePath) Path() string { return "manifest: not a filesystem path" } func (p *resourcePath) Remove() {} type filePath struct { resourcePath file *file } func (p *filePath) SetContent(content io.ReadCloser) { p.file.content = content } func (p *filePath) SetUID(uid uint32) { p.file.uid = uid } func (p *filePath) SetGID(gid uint32) { p.file.gid = gid } type directoryPath struct { resourcePath directory *directory } func (p *directoryPath) SetUID(uid uint32) { p.directory.uid = uid } func (p *directoryPath) SetGID(gid uint32) { p.directory.gid = gid } func (p *directoryPath) AddSymlink(path, target string) error { p.directory.items[path] = &symlink{ resource: newResource(defaultSymlinkMode), target: target, } return nil } func (p *directoryPath) AddFile(path string, ops ...PathOp) error { newFile := &file{resource: newResource(0)} p.directory.items[path] = newFile exp := &filePath{file: newFile} return applyPathOps(exp, ops) } func (p *directoryPath) AddGlobFiles(glob string, ops ...PathOp) error { newFile := &file{resource: newResource(0)} newFilePath := &filePath{file: newFile} p.directory.filepathGlobs[glob] = newFilePath return applyPathOps(newFilePath, ops) } func (p *directoryPath) AddDirectory(path string, ops ...PathOp) error { newDir := newDirectoryWithDefaults() p.directory.items[path] = newDir exp := &directoryPath{directory: newDir} return applyPathOps(exp, ops) } // Expected returns a [Manifest] with a directory structured created by ops. The // [PathOp] operations are applied to the manifest as expectations of the // filesystem structure and properties. func Expected(t assert.TestingT, ops ...PathOp) Manifest { if ht, ok := t.(helperT); ok { ht.Helper() } newDir := newDirectoryWithDefaults() e := &directoryPath{directory: newDir} assert.NilError(t, applyPathOps(e, ops)) return Manifest{root: newDir} } func newDirectoryWithDefaults() *directory { return &directory{ resource: newResource(defaultRootDirMode), items: make(map[string]dirEntry), filepathGlobs: make(map[string]*filePath), } } func newResource(mode os.FileMode) resource { return resource{ mode: mode, uid: currentUID(), gid: currentGID(), } } func currentUID() uint32 { return normalizeID(os.Getuid()) } func currentGID() uint32 { return normalizeID(os.Getgid()) } func normalizeID(id int) uint32 { // ids will be -1 on windows if id < 0 { return 0 } return uint32(id) } var anyFileContent = io.NopCloser(bytes.NewReader(nil)) // MatchAnyFileContent is a [PathOp] that updates a [Manifest] so that the file // at path may contain any content. func MatchAnyFileContent(path Path) error { if m, ok := path.(*filePath); ok { m.SetContent(anyFileContent) } return nil } // MatchContentIgnoreCarriageReturn is a [PathOp] that ignores cariage return // discrepancies. func MatchContentIgnoreCarriageReturn(path Path) error { if m, ok := path.(*filePath); ok { m.file.ignoreCariageReturn = true } return nil } const anyFile = "*" // MatchExtraFiles is a [PathOp] that updates a [Manifest] to allow a directory // to contain unspecified files. func MatchExtraFiles(path Path) error { if m, ok := path.(*directoryPath); ok { return m.AddFile(anyFile) } return nil } // CompareResult is the result of comparison. // // See [gotest.tools/v3/assert/cmp.StringResult] for a convenient implementation of // this interface. type CompareResult interface { Success() bool FailureMessage() string } // MatchFileContent is a [PathOp] that updates a [Manifest] to use the provided // function to determine if a file's content matches the expectation. func MatchFileContent(f func([]byte) CompareResult) PathOp { return func(path Path) error { if m, ok := path.(*filePath); ok { m.file.compareContentFunc = f } return nil } } // MatchFilesWithGlob is a [PathOp] that updates a [Manifest] to match files using // glob pattern, and check them using the ops. func MatchFilesWithGlob(glob string, ops ...PathOp) PathOp { return func(path Path) error { if m, ok := path.(*directoryPath); ok { return m.AddGlobFiles(glob, ops...) } return nil } } // anyFileMode is represented by uint32_max const anyFileMode os.FileMode = 4294967295 // MatchAnyFileMode is a [PathOp] that updates a [Manifest] so that the resource at path // will match any file mode. func MatchAnyFileMode(path Path) error { if m, ok := path.(manifestResource); ok { m.SetMode(anyFileMode) } return nil } gotest.tools-3.5.1/fs/report.go000066400000000000000000000152211463245364400164520ustar00rootroot00000000000000package fs import ( "bytes" "fmt" "io" "os" "path/filepath" "runtime" "sort" "strings" "gotest.tools/v3/assert/cmp" "gotest.tools/v3/internal/format" ) // Equal compares a directory to the expected structured described by a manifest // and returns success if they match. If they do not match the failure message // will contain all the differences between the directory structure and the // expected structure defined by the [Manifest]. // // Equal is a [cmp.Comparison] which can be used with [gotest.tools/v3/assert.Assert]. func Equal(path string, expected Manifest) cmp.Comparison { return func() cmp.Result { actual, err := manifestFromDir(path) if err != nil { return cmp.ResultFromError(err) } failures := eqDirectory(string(os.PathSeparator), expected.root, actual.root) if len(failures) == 0 { return cmp.ResultSuccess } msg := fmt.Sprintf("directory %s does not match expected:\n", path) return cmp.ResultFailure(msg + formatFailures(failures)) } } type failure struct { path string problems []problem } type problem string func notEqual(property string, x, y interface{}) problem { return problem(fmt.Sprintf("%s: expected %s got %s", property, x, y)) } func errProblem(reason string, err error) problem { return problem(fmt.Sprintf("%s: %s", reason, err)) } func existenceProblem(filename, reason string, args ...interface{}) problem { return problem(filename + ": " + fmt.Sprintf(reason, args...)) } func eqResource(x, y resource) []problem { var p []problem if x.uid != y.uid { p = append(p, notEqual("uid", x.uid, y.uid)) } if x.gid != y.gid { p = append(p, notEqual("gid", x.gid, y.gid)) } if x.mode != anyFileMode && x.mode != y.mode { p = append(p, notEqual("mode", x.mode, y.mode)) } return p } func removeCarriageReturn(in []byte) []byte { return bytes.Replace(in, []byte("\r\n"), []byte("\n"), -1) } func eqFile(x, y *file) []problem { p := eqResource(x.resource, y.resource) switch { case x.content == nil: p = append(p, existenceProblem("content", "expected content is nil")) return p case x.content == anyFileContent: return p case y.content == nil: p = append(p, existenceProblem("content", "actual content is nil")) return p } xContent, xErr := io.ReadAll(x.content) defer x.content.Close() yContent, yErr := io.ReadAll(y.content) defer y.content.Close() if xErr != nil { p = append(p, errProblem("failed to read expected content", xErr)) } if yErr != nil { p = append(p, errProblem("failed to read actual content", xErr)) } if xErr != nil || yErr != nil { return p } if x.compareContentFunc != nil { r := x.compareContentFunc(yContent) if !r.Success() { p = append(p, existenceProblem("content", r.FailureMessage())) } return p } if x.ignoreCariageReturn || y.ignoreCariageReturn { xContent = removeCarriageReturn(xContent) yContent = removeCarriageReturn(yContent) } if !bytes.Equal(xContent, yContent) { p = append(p, diffContent(xContent, yContent)) } return p } func diffContent(x, y []byte) problem { diff := format.UnifiedDiff(format.DiffConfig{ A: string(x), B: string(y), From: "expected", To: "actual", }) // Remove the trailing newline in the diff. A trailing newline is always // added to a problem by formatFailures. diff = strings.TrimSuffix(diff, "\n") return problem("content:\n" + indent(diff, " ")) } func indent(s, prefix string) string { buf := new(bytes.Buffer) lines := strings.SplitAfter(s, "\n") for _, line := range lines { buf.WriteString(prefix + line) } return buf.String() } func eqSymlink(x, y *symlink) []problem { p := eqResource(x.resource, y.resource) xTarget := x.target yTarget := y.target if runtime.GOOS == "windows" { xTarget = strings.ToLower(xTarget) yTarget = strings.ToLower(yTarget) } if xTarget != yTarget { p = append(p, notEqual("target", x.target, y.target)) } return p } func eqDirectory(path string, x, y *directory) []failure { p := eqResource(x.resource, y.resource) var f []failure matchedFiles := make(map[string]bool) for _, name := range sortedKeys(x.items) { if name == anyFile { continue } matchedFiles[name] = true xEntry := x.items[name] yEntry, ok := y.items[name] if !ok { p = append(p, existenceProblem(name, "expected %s to exist", xEntry.Type())) continue } if xEntry.Type() != yEntry.Type() { p = append(p, notEqual(name, xEntry.Type(), yEntry.Type())) continue } f = append(f, eqEntry(filepath.Join(path, name), xEntry, yEntry)...) } if len(x.filepathGlobs) != 0 { for _, name := range sortedKeys(y.items) { m := matchGlob(name, y.items[name], x.filepathGlobs) matchedFiles[name] = m.match f = append(f, m.failures...) } } if _, ok := x.items[anyFile]; ok { return maybeAppendFailure(f, path, p) } for _, name := range sortedKeys(y.items) { if !matchedFiles[name] { p = append(p, existenceProblem(name, "unexpected %s", y.items[name].Type())) } } return maybeAppendFailure(f, path, p) } func maybeAppendFailure(failures []failure, path string, problems []problem) []failure { if len(problems) > 0 { return append(failures, failure{path: path, problems: problems}) } return failures } func sortedKeys(items map[string]dirEntry) []string { keys := make([]string, 0, len(items)) for key := range items { keys = append(keys, key) } sort.Strings(keys) return keys } // eqEntry assumes x and y to be the same type func eqEntry(path string, x, y dirEntry) []failure { resp := func(problems []problem) []failure { if len(problems) == 0 { return nil } return []failure{{path: path, problems: problems}} } switch typed := x.(type) { case *file: return resp(eqFile(typed, y.(*file))) case *symlink: return resp(eqSymlink(typed, y.(*symlink))) case *directory: return eqDirectory(path, typed, y.(*directory)) } return nil } type globMatch struct { match bool failures []failure } func matchGlob(name string, yEntry dirEntry, globs map[string]*filePath) globMatch { m := globMatch{} for glob, expectedFile := range globs { ok, err := filepath.Match(glob, name) if err != nil { p := errProblem("failed to match glob pattern", err) f := failure{path: name, problems: []problem{p}} m.failures = append(m.failures, f) } if ok { m.match = true m.failures = eqEntry(name, expectedFile.file, yEntry) return m } } return m } func formatFailures(failures []failure) string { sort.Slice(failures, func(i, j int) bool { return failures[i].path < failures[j].path }) buf := new(bytes.Buffer) for _, failure := range failures { buf.WriteString(failure.path + "\n") for _, problem := range failure.problems { buf.WriteString(" " + string(problem) + "\n") } } return buf.String() } gotest.tools-3.5.1/fs/report_test.go000066400000000000000000000166661463245364400175270ustar00rootroot00000000000000package fs import ( "fmt" "path/filepath" "runtime" "testing" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/skip" ) func TestEqualMissingRoot(t *testing.T) { result := Equal("/bogus/path/does/not/exist", Expected(t))() assert.Assert(t, !result.Success()) expected := "stat /bogus/path/does/not/exist: no such file or directory" if runtime.GOOS == "windows" { expected = "CreateFile /bogus/path/does/not/exist" } assert.Assert(t, is.Contains(result.(cmpFailure).FailureMessage(), expected)) } func TestEqualModeMismatch(t *testing.T) { dir := NewDir(t, t.Name(), WithMode(0500)) defer dir.Remove() result := Equal(dir.Path(), Expected(t))() assert.Assert(t, !result.Success()) expected := fmtExpected(`directory %s does not match expected: / mode: expected drwx------ got dr-x------ `, dir.Path()) if runtime.GOOS == "windows" { expected = fmtExpected(`directory %s does not match expected: \ mode: expected drwxrwxrwx got dr-xr-xr-x `, dir.Path()) } assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) } func TestEqualRootIsAFile(t *testing.T) { file := NewFile(t, t.Name()) defer file.Remove() result := Equal(file.Path(), Expected(t))() assert.Assert(t, !result.Success()) expected := fmt.Sprintf("path %s must be a directory", file.Path()) assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) } func TestEqualSuccess(t *testing.T) { dir := NewDir(t, t.Name(), WithMode(0700)) defer dir.Remove() assert.Assert(t, Equal(dir.Path(), Expected(t))) } func TestEqualDirectoryHasWithExtraFiles(t *testing.T) { dir := NewDir(t, t.Name(), WithFile("extra1", "content")) defer dir.Remove() manifest := Expected(t, WithFile("file1", "content")) result := Equal(dir.Path(), manifest)() assert.Assert(t, !result.Success()) expected := fmtExpected(`directory %s does not match expected: / file1: expected file to exist extra1: unexpected file `, dir.Path()) assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) } func fmtExpected(format string, args ...interface{}) string { return filepath.FromSlash(fmt.Sprintf(format, args...)) } func TestEqualWithMatchAnyFileContent(t *testing.T) { dir := NewDir(t, t.Name(), WithFile("data", "this is some data")) defer dir.Remove() expected := Expected(t, WithFile("data", "different content", MatchAnyFileContent)) assert.Assert(t, Equal(dir.Path(), expected)) } func TestEqualWithFileContent(t *testing.T) { dir := NewDir(t, "assert-test-root", WithFile("file1", "line1\nline2\nline3")) defer dir.Remove() manifest := Expected(t, WithFile("file1", "line2\nline3")) result := Equal(dir.Path(), manifest)() expected := fmtExpected(`directory %s does not match expected: /file1 content: --- expected +++ actual @@ -1,2 +1,3 @@ +line1 line2 line3 `, dir.Path()) assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) } func TestEqualWithMatchContentIgnoreCarriageReturn(t *testing.T) { dir := NewDir(t, t.Name(), WithFile("file1", "line1\r\nline2")) defer dir.Remove() manifest := Expected(t, WithFile("file1", "line1\nline2", MatchContentIgnoreCarriageReturn)) result := Equal(dir.Path(), manifest)() assert.Assert(t, result.Success()) } func TestEqualDirectoryWithMatchExtraFiles(t *testing.T) { file1 := WithFile("file1", "same in both") dir := NewDir(t, t.Name(), file1, WithFile("extra", "some content")) defer dir.Remove() expected := Expected(t, file1, MatchExtraFiles) assert.Assert(t, Equal(dir.Path(), expected)) } func TestEqualManyFailures(t *testing.T) { dir := NewDir(t, t.Name(), WithFile("file1", "same in both"), WithFile("extra", "some content"), WithSymlink("sym1", "extra")) defer dir.Remove() manifest := Expected(t, WithDir("subdir", WithFile("somefile", "")), WithFile("file1", "not the\nsame in both")) result := Equal(dir.Path(), manifest)() assert.Assert(t, !result.Success()) expected := fmtExpected(`directory %s does not match expected: / subdir: expected directory to exist extra: unexpected file sym1: unexpected symlink /file1 content: --- expected +++ actual @@ -1,2 +1 @@ -not the same in both `, dir.Path()) assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) } type cmpFailure interface { FailureMessage() string } func TestMatchAnyFileMode(t *testing.T) { dir := NewDir(t, t.Name(), WithFile("data", "content", WithMode(0777))) defer dir.Remove() expected := Expected(t, WithFile("data", "content", MatchAnyFileMode)) assert.Assert(t, Equal(dir.Path(), expected)) } func TestMatchFileContent(t *testing.T) { dir := NewDir(t, t.Name(), WithFile("data", "content")) defer dir.Remove() t.Run("content matches", func(t *testing.T) { matcher := func(b []byte) CompareResult { return is.ResultSuccess } manifest := Expected(t, WithFile("data", "different", MatchFileContent(matcher))) assert.Assert(t, Equal(dir.Path(), manifest)) }) t.Run("content does not match", func(t *testing.T) { matcher := func(b []byte) CompareResult { return is.ResultFailure("data content differs from expected") } manifest := Expected(t, WithFile("data", "content", MatchFileContent(matcher))) result := Equal(dir.Path(), manifest)() assert.Assert(t, !result.Success()) expected := fmtExpected(`directory %s does not match expected: /data content: data content differs from expected `, dir.Path()) assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) }) } func TestMatchExtraFilesGlob(t *testing.T) { dir := NewDir(t, t.Name(), WithFile("t.go", "data"), WithFile("a.go", "data"), WithFile("conf.yml", "content", WithMode(0600))) defer dir.Remove() t.Run("matching globs", func(t *testing.T) { manifest := Expected(t, MatchFilesWithGlob("*.go", MatchAnyFileMode, MatchAnyFileContent), MatchFilesWithGlob("*.yml", MatchAnyFileMode, MatchAnyFileContent)) assert.Assert(t, Equal(dir.Path(), manifest)) }) t.Run("matching globs with wrong mode", func(t *testing.T) { skip.If(t, runtime.GOOS == "windows", "expect mode does not match on windows") manifest := Expected(t, MatchFilesWithGlob("*.go", MatchAnyFileMode, MatchAnyFileContent), MatchFilesWithGlob("*.yml", MatchAnyFileContent, WithMode(0700))) result := Equal(dir.Path(), manifest)() assert.Assert(t, !result.Success()) expected := fmtExpected(`directory %s does not match expected: conf.yml mode: expected -rwx------ got -rw------- `, dir.Path()) assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) }) t.Run("matching partial glob", func(t *testing.T) { manifest := Expected(t, MatchFilesWithGlob("*.go", MatchAnyFileMode, MatchAnyFileContent)) result := Equal(dir.Path(), manifest)() assert.Assert(t, !result.Success()) expected := fmtExpected(`directory %s does not match expected: / conf.yml: unexpected file `, dir.Path()) assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) }) t.Run("invalid glob", func(t *testing.T) { manifest := Expected(t, MatchFilesWithGlob("[-x]")) result := Equal(dir.Path(), manifest)() assert.Assert(t, !result.Success()) expected := fmtExpected(`directory %s does not match expected: / a.go: unexpected file conf.yml: unexpected file t.go: unexpected file a.go failed to match glob pattern: syntax error in pattern conf.yml failed to match glob pattern: syntax error in pattern t.go failed to match glob pattern: syntax error in pattern `, dir.Path()) assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) }) } gotest.tools-3.5.1/fs/testdata/000077500000000000000000000000001463245364400164205ustar00rootroot00000000000000gotest.tools-3.5.1/fs/testdata/copy-test-with-symlink/000077500000000000000000000000001463245364400230045ustar00rootroot00000000000000gotest.tools-3.5.1/fs/testdata/copy-test-with-symlink/1000066400000000000000000000000021463245364400230570ustar00rootroot000000000000001 gotest.tools-3.5.1/fs/testdata/copy-test-with-symlink/a/000077500000000000000000000000001463245364400232245ustar00rootroot00000000000000gotest.tools-3.5.1/fs/testdata/copy-test-with-symlink/a/1000066400000000000000000000000021463245364400232770ustar00rootroot000000000000001 gotest.tools-3.5.1/fs/testdata/copy-test-with-symlink/a/2000066400000000000000000000000021463245364400233000ustar00rootroot000000000000002 gotest.tools-3.5.1/fs/testdata/copy-test-with-symlink/a/b/000077500000000000000000000000001463245364400234455ustar00rootroot00000000000000gotest.tools-3.5.1/fs/testdata/copy-test-with-symlink/a/b/1000066400000000000000000000000021463245364400235200ustar00rootroot000000000000001 gotest.tools-3.5.1/fs/testdata/copy-test-with-symlink/a/b/2000077700000000000000000000000001463245364400240232../2ustar00rootroot00000000000000gotest.tools-3.5.1/fs/testdata/copy-test-with-symlink/a/b/3000077700000000000000000000000001463245364400276212/some/inexistent/linkustar00rootroot00000000000000gotest.tools-3.5.1/fs/testdata/copy-test-with-symlink/a/b/40000777000000000000000000000000014632453644002361525ustar00rootroot00000000000000gotest.tools-3.5.1/fs/testdata/copy-test/000077500000000000000000000000001463245364400203475ustar00rootroot00000000000000gotest.tools-3.5.1/fs/testdata/copy-test/1000066400000000000000000000000021463245364400204220ustar00rootroot000000000000001 gotest.tools-3.5.1/fs/testdata/copy-test/a/000077500000000000000000000000001463245364400205675ustar00rootroot00000000000000gotest.tools-3.5.1/fs/testdata/copy-test/a/1000066400000000000000000000000021463245364400206420ustar00rootroot000000000000001 gotest.tools-3.5.1/fs/testdata/copy-test/a/2000066400000000000000000000000021463245364400206430ustar00rootroot000000000000002 gotest.tools-3.5.1/fs/testdata/copy-test/a/b/000077500000000000000000000000001463245364400210105ustar00rootroot00000000000000gotest.tools-3.5.1/fs/testdata/copy-test/a/b/1000066400000000000000000000000021463245364400210630ustar00rootroot000000000000001 gotest.tools-3.5.1/go.mod000066400000000000000000000003001463245364400152760ustar00rootroot00000000000000module gotest.tools/v3 go 1.17 require ( github.com/google/go-cmp v0.5.9 golang.org/x/tools v0.2.0 ) require ( golang.org/x/mod v0.6.0 // indirect golang.org/x/sys v0.1.0 // indirect ) gotest.tools-3.5.1/go.sum000066400000000000000000000063511463245364400153370ustar00rootroot00000000000000github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gotest.tools-3.5.1/golden/000077500000000000000000000000001463245364400154475ustar00rootroot00000000000000gotest.tools-3.5.1/golden/example_test.go000066400000000000000000000005621463245364400204730ustar00rootroot00000000000000package golden_test import ( "testing" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) var t = &testing.T{} func ExampleAssert() { golden.Assert(t, "foo", "foo-content.golden") } func ExampleString() { assert.Assert(t, golden.String("foo", "foo-content.golden")) } func ExampleAssertBytes() { golden.AssertBytes(t, []byte("foo"), "foo-content.golden") } gotest.tools-3.5.1/golden/golden.go000066400000000000000000000132261463245364400172520ustar00rootroot00000000000000/* Package golden provides tools for comparing large mutli-line strings. Golden files are files in the ./testdata/ subdirectory of the package under test. Golden files can be automatically updated to match new values by running `go test pkgname -update`. To ensure the update is correct compare the diff of the old expected value to the new expected value. */ package golden import ( "bytes" "flag" "fmt" "os" "path/filepath" "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" "gotest.tools/v3/internal/format" "gotest.tools/v3/internal/source" ) func init() { flag.BoolVar(&source.Update, "test.update-golden", false, "deprecated flag") } type helperT interface { Helper() } // NormalizeCRLFToLF enables end-of-line normalization for actual values passed // to Assert and String, as well as the values saved to golden files with // -update. // // Defaults to true. If you use the core.autocrlf=true git setting on windows // you will need to set this to false. // // The value may be set to false by setting GOTESTTOOLS_GOLDEN_NormalizeCRLFToLF=false // in the environment before running tests. // // The default value may change in a future major release. // // This does not affect the contents of the golden files themselves. And depending on the // git settings on your system (or in github action platform default like windows), the // golden files may contain CRLF line endings. You can avoid this by setting the // .gitattributes file in your repo to use LF line endings for all files, or just the golden // files, by adding the following line to your .gitattributes file: // // * text=auto eol=lf var NormalizeCRLFToLF = os.Getenv("GOTESTTOOLS_GOLDEN_NormalizeCRLFToLF") != "false" // FlagUpdate returns true when the -update flag has been set. func FlagUpdate() bool { return source.IsUpdate() } // Open opens the file in ./testdata func Open(t assert.TestingT, filename string) *os.File { if ht, ok := t.(helperT); ok { ht.Helper() } f, err := os.Open(Path(filename)) assert.NilError(t, err) return f } // Get returns the contents of the file in ./testdata func Get(t assert.TestingT, filename string) []byte { if ht, ok := t.(helperT); ok { ht.Helper() } expected, err := os.ReadFile(Path(filename)) assert.NilError(t, err) return expected } // Path returns the full path to a file in ./testdata func Path(filename string) string { if filepath.IsAbs(filename) { return filename } return filepath.Join("testdata", filename) } func removeCarriageReturn(in []byte) []byte { if !NormalizeCRLFToLF { return in } return bytes.Replace(in, []byte("\r\n"), []byte("\n"), -1) } // Assert compares actual to the expected value in the golden file. // // Running `go test pkgname -update` will write the value of actual // to the golden file. // // This is equivalent to assert.Assert(t, String(actual, filename)) func Assert(t assert.TestingT, actual string, filename string, msgAndArgs ...interface{}) { if ht, ok := t.(helperT); ok { ht.Helper() } assert.Assert(t, String(actual, filename), msgAndArgs...) } // String compares actual to the contents of filename and returns success // if the strings are equal. // // Running `go test pkgname -update` will write the value of actual // to the golden file. // // Any \r\n substrings in actual are converted to a single \n character // before comparing it to the expected string. When updating the golden file the // normalized version will be written to the file. This allows Windows to use // the same golden files as other operating systems. func String(actual string, filename string) cmp.Comparison { return func() cmp.Result { actualBytes := removeCarriageReturn([]byte(actual)) result, expected := compare(actualBytes, filename) if result != nil { return result } diff := format.UnifiedDiff(format.DiffConfig{ A: string(expected), B: string(actualBytes), From: "expected", To: "actual", }) return cmp.ResultFailure("\n" + diff + failurePostamble(filename)) } } func failurePostamble(filename string) string { return fmt.Sprintf(` You can run 'go test . -update' to automatically update %s to the new expected value.' `, Path(filename)) } // AssertBytes compares actual to the expected value in the golden. // // Running `go test pkgname -update` will write the value of actual // to the golden file. // // This is equivalent to assert.Assert(t, Bytes(actual, filename)) func AssertBytes( t assert.TestingT, actual []byte, filename string, msgAndArgs ...interface{}, ) { if ht, ok := t.(helperT); ok { ht.Helper() } assert.Assert(t, Bytes(actual, filename), msgAndArgs...) } // Bytes compares actual to the contents of filename and returns success // if the bytes are equal. // // Running `go test pkgname -update` will write the value of actual // to the golden file. func Bytes(actual []byte, filename string) cmp.Comparison { return func() cmp.Result { result, expected := compare(actual, filename) if result != nil { return result } msg := fmt.Sprintf("%v (actual) != %v (expected)", actual, expected) return cmp.ResultFailure(msg + failurePostamble(filename)) } } func compare(actual []byte, filename string) (cmp.Result, []byte) { if err := update(filename, actual); err != nil { return cmp.ResultFromError(err), nil } expected, err := os.ReadFile(Path(filename)) if err != nil { return cmp.ResultFromError(err), nil } if bytes.Equal(expected, actual) { return cmp.ResultSuccess, nil } return nil, expected } func update(filename string, actual []byte) error { if !source.IsUpdate() { return nil } if dir := filepath.Dir(Path(filename)); dir != "." { if err := os.MkdirAll(dir, 0755); err != nil { return err } } return os.WriteFile(Path(filename), actual, 0644) } gotest.tools-3.5.1/golden/golden_test.go000066400000000000000000000151331463245364400203100ustar00rootroot00000000000000package golden import ( "os" "path/filepath" "testing" "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" "gotest.tools/v3/fs" "gotest.tools/v3/internal/source" ) type fakeT struct { Failed bool } func (t *fakeT) Log(...interface{}) { } func (t *fakeT) FailNow() { t.Failed = true } func (t *fakeT) Fail() { t.Failed = true } func (t *fakeT) Helper() {} func TestGoldenOpenInvalidFile(t *testing.T) { fakeT := new(fakeT) Open(fakeT, "/invalid/path") assert.Assert(t, fakeT.Failed) } func TestGoldenOpenAbsolutePath(t *testing.T) { file := fs.NewFile(t, "abs-test", fs.WithContent("content\n")) defer file.Remove() fakeT := new(fakeT) f := Open(fakeT, file.Path()) assert.Assert(t, !fakeT.Failed) f.Close() } func TestGoldenOpen(t *testing.T) { filename, clean := setupGoldenFile(t, "") defer clean() fakeT := new(fakeT) f := Open(fakeT, filename) assert.Assert(t, !fakeT.Failed) f.Close() } func TestGoldenGetInvalidFile(t *testing.T) { fakeT := new(fakeT) Get(fakeT, "/invalid/path") assert.Assert(t, fakeT.Failed) } func TestGoldenGetAbsolutePath(t *testing.T) { file := fs.NewFile(t, "abs-test", fs.WithContent("content\n")) defer file.Remove() fakeT := new(fakeT) Get(fakeT, file.Path()) assert.Assert(t, !fakeT.Failed) } func TestGoldenGet(t *testing.T) { expected := "content\nline1\nline2" filename, clean := setupGoldenFile(t, expected) defer clean() fakeT := new(fakeT) actual := Get(fakeT, filename) assert.Assert(t, !fakeT.Failed) assert.Assert(t, cmp.DeepEqual(actual, []byte(expected))) } func TestGoldenAssertInvalidContent(t *testing.T) { filename, clean := setupGoldenFile(t, "content") defer clean() fakeT := new(fakeT) Assert(fakeT, "foo", filename) assert.Assert(t, fakeT.Failed) } func TestGoldenAssertInvalidContentUpdate(t *testing.T) { setUpdateFlag(t) filename, clean := setupGoldenFile(t, "content") defer clean() fakeT := new(fakeT) Assert(fakeT, "foo", filename) assert.Assert(t, !fakeT.Failed) } func TestGoldenAssertAbsolutePath(t *testing.T) { file := fs.NewFile(t, "abs-test", fs.WithContent("foo")) defer file.Remove() fakeT := new(fakeT) Assert(fakeT, "foo", file.Path()) assert.Assert(t, !fakeT.Failed) } func TestGoldenAssertInDir(t *testing.T) { filename, clean := setupGoldenFileWithDir(t, "testdatasubdir", "foo") defer clean() fakeT := new(fakeT) Assert(fakeT, "foo", filepath.Join("testdatasubdir", filename)) assert.Assert(t, !fakeT.Failed) _, err := os.Stat("testdatasubdir") assert.Assert(t, os.IsNotExist(err), "testdatasubdir should not exist outside of testdata") } func TestGoldenAssertInDir_UpdateGolden(t *testing.T) { filename, clean := setupGoldenFileWithDir(t, "testdatasubdir", "foo") defer clean() setUpdateFlag(t) fakeT := new(fakeT) Assert(fakeT, "foo", filepath.Join("testdatasubdir", filename)) assert.Assert(t, !fakeT.Failed) _, err := os.Stat("testdatasubdir") assert.Assert(t, os.IsNotExist(err), "testdatasubdir should not exist outside of testdata") } func TestGoldenAssert(t *testing.T) { filename, clean := setupGoldenFile(t, "foo") defer clean() fakeT := new(fakeT) Assert(fakeT, "foo", filename) assert.Assert(t, !fakeT.Failed) } func TestAssert_WithCarriageReturnInActual(t *testing.T) { filename, clean := setupGoldenFile(t, "a\rfoo\nbar\n") defer clean() fakeT := new(fakeT) Assert(fakeT, "a\rfoo\r\nbar\r\n", filename) assert.Assert(t, !fakeT.Failed) } func TestAssert_WithCarriageReturnInActual_UpdateGolden(t *testing.T) { filename, clean := setupGoldenFile(t, "") defer clean() unsetUpdateFlag := setUpdateFlag(t) fakeT := new(fakeT) Assert(fakeT, "a\rfoo\r\nbar\r\n", filename) assert.Assert(t, !fakeT.Failed) unsetUpdateFlag() actual := Get(fakeT, filename) assert.Equal(t, string(actual), "a\rfoo\nbar\n") Assert(t, "a\rfoo\r\nbar\r\n", filename, "matches with carriage returns") Assert(t, "a\rfoo\nbar\n", filename, "matches without carriage returns") } func TestGoldenAssertBytes(t *testing.T) { filename, clean := setupGoldenFile(t, "foo") defer clean() fakeT := new(fakeT) AssertBytes(fakeT, []byte("foo"), filename) assert.Assert(t, !fakeT.Failed) } func setUpdateFlag(t *testing.T) func() { orig := source.Update source.Update = true undo := func() { source.Update = orig } t.Cleanup(undo) return undo } func setupGoldenFileWithDir(t *testing.T, dirname, content string) (string, func()) { dirpath := filepath.Join("testdata", dirname) _ = os.MkdirAll(filepath.Join("testdata", dirname), 0755) f, err := os.CreateTemp(dirpath, t.Name()+"-") assert.NilError(t, err, "fail to create test golden file") defer f.Close() _, err = f.Write([]byte(content)) assert.NilError(t, err) return filepath.Base(f.Name()), func() { assert.NilError(t, os.Remove(f.Name())) assert.NilError(t, os.Remove(dirpath)) } } func setupGoldenFile(t *testing.T, content string) (string, func()) { _ = os.Mkdir("testdata", 0755) f, err := os.CreateTemp("testdata", t.Name()+"-") assert.NilError(t, err, "fail to create test golden file") defer f.Close() _, err = f.Write([]byte(content)) assert.NilError(t, err) return filepath.Base(f.Name()), func() { assert.NilError(t, os.Remove(f.Name())) } } func TestStringFailure(t *testing.T) { filename, clean := setupGoldenFile(t, "this is\nthe text") defer clean() result := String("this is\nnot the text", filename)() assert.Assert(t, !result.Success()) assert.Equal(t, result.(failure).FailureMessage(), ` --- expected +++ actual @@ -1,2 +1,2 @@ this is -the text +not the text `+failurePostamble(filename)) } type failure interface { FailureMessage() string } func TestBytesFailure(t *testing.T) { filename, clean := setupGoldenFile(t, "5556") defer clean() result := Bytes([]byte("5555"), filename)() assert.Assert(t, !result.Success()) assert.Equal(t, result.(failure).FailureMessage(), `[53 53 53 53] (actual) != [53 53 53 54] (expected)`+failurePostamble(filename)) } func TestFlagUpdate(t *testing.T) { assert.Assert(t, !FlagUpdate()) setUpdateFlag(t) assert.Assert(t, FlagUpdate()) } func TestUpdate_CreatesPathsAndFile(t *testing.T) { setUpdateFlag(t) dir := fs.NewDir(t, t.Name()) t.Run("creates the file", func(t *testing.T) { filename := dir.Join("filename") err := update(filename, nil) assert.NilError(t, err) _, err = os.Stat(filename) assert.NilError(t, err) }) t.Run("creates directories", func(t *testing.T) { filename := dir.Join("one/two/filename") err := update(filename, nil) assert.NilError(t, err) _, err = os.Stat(filename) assert.NilError(t, err) t.Run("no error when directory exists", func(t *testing.T) { err = update(filename, nil) assert.NilError(t, err) }) }) } gotest.tools-3.5.1/icmd/000077500000000000000000000000001463245364400151135ustar00rootroot00000000000000gotest.tools-3.5.1/icmd/command.go000066400000000000000000000157321463245364400170700ustar00rootroot00000000000000/*Package icmd executes binaries and provides convenient assertions for testing the results. */ package icmd // import "gotest.tools/v3/icmd" import ( "bytes" "fmt" "io" "os" "os/exec" "strings" "sync" "time" "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" ) type helperT interface { Helper() } // None is a token to inform Result.Assert that the output should be empty const None = "[NOTHING]" type lockedBuffer struct { m sync.RWMutex buf bytes.Buffer } func (buf *lockedBuffer) Write(b []byte) (int, error) { buf.m.Lock() defer buf.m.Unlock() return buf.buf.Write(b) } func (buf *lockedBuffer) String() string { buf.m.RLock() defer buf.m.RUnlock() return buf.buf.String() } // Result stores the result of running a command type Result struct { Cmd *exec.Cmd ExitCode int Error error // Timeout is true if the command was killed because it ran for too long Timeout bool outBuffer *lockedBuffer errBuffer *lockedBuffer } // Assert compares the Result against the Expected struct, and fails the test if // any of the expectations are not met. // // This function is equivalent to assert.Assert(t, result.Equal(exp)). func (r *Result) Assert(t assert.TestingT, exp Expected) *Result { if ht, ok := t.(helperT); ok { ht.Helper() } assert.Assert(t, r.Equal(exp)) return r } // Equal compares the result to Expected. If the result doesn't match expected // returns a formatted failure message with the command, stdout, stderr, exit code, // and any failed expectations. func (r *Result) Equal(exp Expected) cmp.Comparison { return func() cmp.Result { return cmp.ResultFromError(r.match(exp)) } } // Compare the result to Expected and return an error if they do not match. func (r *Result) Compare(exp Expected) error { return r.match(exp) } func (r *Result) match(exp Expected) error { errors := []string{} add := func(format string, args ...interface{}) { errors = append(errors, fmt.Sprintf(format, args...)) } if exp.ExitCode != r.ExitCode { add("ExitCode was %d expected %d", r.ExitCode, exp.ExitCode) } if exp.Timeout != r.Timeout { if exp.Timeout { add("Expected command to timeout") } else { add("Expected command to finish, but it hit the timeout") } } if !matchOutput(exp.Out, r.Stdout()) { add("Expected stdout to contain %q", exp.Out) } if !matchOutput(exp.Err, r.Stderr()) { add("Expected stderr to contain %q", exp.Err) } switch { // If a non-zero exit code is expected there is going to be an error. // Don't require an error message as well as an exit code because the // error message is going to be "exit status which is not useful case exp.Error == "" && exp.ExitCode != 0: case exp.Error == "" && r.Error != nil: add("Expected no error") case exp.Error != "" && r.Error == nil: add("Expected error to contain %q, but there was no error", exp.Error) case exp.Error != "" && !strings.Contains(r.Error.Error(), exp.Error): add("Expected error to contain %q", exp.Error) } if len(errors) == 0 { return nil } return fmt.Errorf("%s\nFailures:\n%s", r, strings.Join(errors, "\n")) } func matchOutput(expected string, actual string) bool { switch expected { case None: return actual == "" default: return strings.Contains(actual, expected) } } func (r *Result) String() string { var timeout string if r.Timeout { timeout = " (timeout)" } var errString string if r.Error != nil { errString = "\nError: " + r.Error.Error() } return fmt.Sprintf(` Command: %s ExitCode: %d%s%s Stdout: %v Stderr: %v `, strings.Join(r.Cmd.Args, " "), r.ExitCode, timeout, errString, r.Stdout(), r.Stderr()) } // Expected is the expected output from a Command. This struct is compared to a // Result struct by Result.Assert(). type Expected struct { ExitCode int Timeout bool Error string Out string Err string } // Success is the default expected result. A Success result is one with a 0 // ExitCode. var Success = Expected{} // Stdout returns the stdout of the process as a string func (r *Result) Stdout() string { return r.outBuffer.String() } // Stderr returns the stderr of the process as a string func (r *Result) Stderr() string { return r.errBuffer.String() } // Combined returns the stdout and stderr combined into a single string func (r *Result) Combined() string { return r.outBuffer.String() + r.errBuffer.String() } func (r *Result) setExitError(err error) { if err == nil { return } r.Error = err r.ExitCode = processExitCode(err) } // Cmd contains the arguments and options for a process to run as part of a test // suite. type Cmd struct { Command []string Timeout time.Duration Stdin io.Reader Stdout io.Writer Stderr io.Writer Dir string Env []string ExtraFiles []*os.File } // Command create a simple Cmd with the specified command and arguments func Command(command string, args ...string) Cmd { return Cmd{Command: append([]string{command}, args...)} } // RunCmd runs a command and returns a Result func RunCmd(cmd Cmd, cmdOperators ...CmdOp) *Result { result := StartCmd(cmd, cmdOperators...) if result.Error != nil { return result } return WaitOnCmd(cmd.Timeout, result) } // RunCommand runs a command with default options, and returns a result func RunCommand(command string, args ...string) *Result { return RunCmd(Command(command, args...)) } // StartCmd starts a command, but doesn't wait for it to finish func StartCmd(cmd Cmd, cmdOperators ...CmdOp) *Result { for _, op := range cmdOperators { op(&cmd) } result := buildCmd(cmd) if result.Error != nil { return result } result.setExitError(result.Cmd.Start()) return result } // TODO: support exec.CommandContext func buildCmd(cmd Cmd) *Result { var execCmd *exec.Cmd switch len(cmd.Command) { case 1: execCmd = exec.Command(cmd.Command[0]) default: execCmd = exec.Command(cmd.Command[0], cmd.Command[1:]...) } outBuffer := new(lockedBuffer) errBuffer := new(lockedBuffer) execCmd.Stdin = cmd.Stdin execCmd.Dir = cmd.Dir execCmd.Env = cmd.Env if cmd.Stdout != nil { execCmd.Stdout = io.MultiWriter(outBuffer, cmd.Stdout) } else { execCmd.Stdout = outBuffer } if cmd.Stderr != nil { execCmd.Stderr = io.MultiWriter(errBuffer, cmd.Stderr) } else { execCmd.Stderr = errBuffer } execCmd.ExtraFiles = cmd.ExtraFiles return &Result{ Cmd: execCmd, outBuffer: outBuffer, errBuffer: errBuffer, } } // WaitOnCmd waits for a command to complete. If timeout is non-nil then // only wait until the timeout. func WaitOnCmd(timeout time.Duration, result *Result) *Result { if timeout == time.Duration(0) { result.setExitError(result.Cmd.Wait()) return result } done := make(chan error, 1) // Wait for command to exit in a goroutine go func() { done <- result.Cmd.Wait() }() select { case <-time.After(timeout): killErr := result.Cmd.Process.Kill() if killErr != nil { fmt.Printf("failed to kill (pid=%d): %v\n", result.Cmd.Process.Pid, killErr) } result.Timeout = true case err := <-done: result.setExitError(err) } return result } gotest.tools-3.5.1/icmd/command_test.go000066400000000000000000000100011463245364400201070ustar00rootroot00000000000000package icmd import ( "bytes" "errors" "os" "os/exec" "path/filepath" "runtime" "testing" "time" "gotest.tools/v3/assert" "gotest.tools/v3/fs" "gotest.tools/v3/internal/maint" ) var ( bindir = fs.NewDir(maint.T, "icmd-dir") binname = bindir.Join("bin-stub") + pathext() stubpath = filepath.FromSlash("./internal/stub") ) func pathext() string { if runtime.GOOS == "windows" { return ".exe" } return "" } func TestMain(m *testing.M) { exitcode := m.Run() bindir.Remove() os.Exit(exitcode) } func buildStub(t assert.TestingT) { if _, err := os.Stat(binname); err == nil { return } result := RunCommand("go", "build", "-o", binname, stubpath) result.Assert(t, Success) } func TestRunCommandSuccess(t *testing.T) { buildStub(t) result := RunCommand(binname) result.Assert(t, Success) } func TestRunCommandWithCombined(t *testing.T) { buildStub(t) result := RunCommand(binname, "-warn") result.Assert(t, Expected{}) assert.Equal(t, result.Combined(), "this is stdout\nthis is stderr\n") assert.Equal(t, result.Stdout(), "this is stdout\n") assert.Equal(t, result.Stderr(), "this is stderr\n") } func TestRunCommandWithTimeoutFinished(t *testing.T) { buildStub(t) result := RunCmd(Cmd{ Command: []string{binname, "-sleep=1ms"}, Timeout: 2 * time.Second, }) result.Assert(t, Expected{Out: "this is stdout"}) } func TestRunCommandWithTimeoutKilled(t *testing.T) { buildStub(t) command := []string{binname, "-sleep=200ms"} result := RunCmd(Cmd{Command: command, Timeout: 30 * time.Millisecond}) result.Assert(t, Expected{Timeout: true, Out: None, Err: None}) } func TestRunCommandWithErrors(t *testing.T) { buildStub(t) result := RunCommand("doesnotexists") expected := `exec: "doesnotexists": executable file not found` result.Assert(t, Expected{Out: None, Err: None, ExitCode: 127, Error: expected}) } func TestRunCommandWithStdoutNoStderr(t *testing.T) { buildStub(t) result := RunCommand(binname) result.Assert(t, Expected{Out: "this is stdout\n", Err: None}) } func TestRunCommandWithExitCode(t *testing.T) { buildStub(t) result := RunCommand(binname, "-fail=99") result.Assert(t, Expected{ ExitCode: 99, Error: "exit status 99", }) } func TestResult_Match_NotMatched(t *testing.T) { result := &Result{ Cmd: exec.Command("binary", "arg1"), ExitCode: 99, Error: errors.New("exit code 99"), outBuffer: newLockedBuffer("the output"), errBuffer: newLockedBuffer("the stderr"), Timeout: true, } exp := Expected{ ExitCode: 101, Out: "Something else", Err: None, } err := result.match(exp) assert.ErrorContains(t, err, "Failures") assert.Equal(t, err.Error(), expectedMatch) } var expectedMatch = ` Command: binary arg1 ExitCode: 99 (timeout) Error: exit code 99 Stdout: the output Stderr: the stderr Failures: ExitCode was 99 expected 101 Expected command to finish, but it hit the timeout Expected stdout to contain "Something else" Expected stderr to contain "[NOTHING]"` func newLockedBuffer(s string) *lockedBuffer { return &lockedBuffer{buf: *bytes.NewBufferString(s)} } func TestResult_Match_NotMatchedNoError(t *testing.T) { result := &Result{ Cmd: exec.Command("binary", "arg1"), outBuffer: newLockedBuffer("the output"), errBuffer: newLockedBuffer("the stderr"), } exp := Expected{ ExitCode: 101, Out: "Something else", Err: None, } err := result.match(exp) assert.ErrorContains(t, err, "Failures") assert.Equal(t, err.Error(), expectedResultMatchNoMatch) } var expectedResultMatchNoMatch = ` Command: binary arg1 ExitCode: 0 Stdout: the output Stderr: the stderr Failures: ExitCode was 0 expected 101 Expected stdout to contain "Something else" Expected stderr to contain "[NOTHING]"` func TestResult_Match_Match(t *testing.T) { result := &Result{ Cmd: exec.Command("binary", "arg1"), outBuffer: newLockedBuffer("the output"), errBuffer: newLockedBuffer("the stderr"), } exp := Expected{ Out: "the output", Err: "the stderr", } err := result.match(exp) assert.NilError(t, err) } gotest.tools-3.5.1/icmd/example_test.go000066400000000000000000000006341463245364400201370ustar00rootroot00000000000000package icmd_test import ( "testing" "gotest.tools/v3/icmd" ) var t = &testing.T{} func ExampleRunCommand() { result := icmd.RunCommand("bash", "-c", "echo all good") result.Assert(t, icmd.Success) } func ExampleRunCmd() { result := icmd.RunCmd(icmd.Command("cat", "/does/not/exist")) result.Assert(t, icmd.Expected{ ExitCode: 1, Err: "cat: /does/not/exist: No such file or directory", }) } gotest.tools-3.5.1/icmd/exitcode.go000066400000000000000000000005031463245364400172440ustar00rootroot00000000000000package icmd import ( "errors" "os/exec" ) func processExitCode(err error) int { if err == nil { return 0 } var exitErr *exec.ExitError if errors.As(err, &exitErr) { if exitErr.ProcessState == nil { return 0 } if code := exitErr.ProcessState.ExitCode(); code != -1 { return code } } return 127 } gotest.tools-3.5.1/icmd/internal/000077500000000000000000000000001463245364400167275ustar00rootroot00000000000000gotest.tools-3.5.1/icmd/internal/stub/000077500000000000000000000000001463245364400177045ustar00rootroot00000000000000gotest.tools-3.5.1/icmd/internal/stub/main.go000066400000000000000000000006501463245364400211600ustar00rootroot00000000000000// Package main produces a test-binary used in tests. package main import ( "flag" "fmt" "os" "time" ) func main() { sleep := flag.Duration("sleep", 0, "Sleep") warn := flag.Bool("warn", false, "Warn") fail := flag.Int("fail", 0, "Fail with code") flag.Parse() if *sleep != 0 { time.Sleep(*sleep) } fmt.Println("this is stdout") if *warn { fmt.Fprintln(os.Stderr, "this is stderr") } os.Exit(*fail) } gotest.tools-3.5.1/icmd/ops.go000066400000000000000000000023361463245364400162470ustar00rootroot00000000000000package icmd import ( "io" "os" "time" ) // CmdOp is an operation which modified a Cmd structure used to execute commands type CmdOp func(*Cmd) // WithTimeout sets the timeout duration of the command func WithTimeout(timeout time.Duration) CmdOp { return func(c *Cmd) { c.Timeout = timeout } } // WithEnv sets the environment variable of the command. // Each arguments are in the form of KEY=VALUE func WithEnv(env ...string) CmdOp { return func(c *Cmd) { c.Env = env } } // Dir sets the working directory of the command func Dir(path string) CmdOp { return func(c *Cmd) { c.Dir = path } } // WithStdin sets the standard input of the command to the specified reader func WithStdin(r io.Reader) CmdOp { return func(c *Cmd) { c.Stdin = r } } // WithStdout sets the standard output of the command to the specified writer func WithStdout(w io.Writer) CmdOp { return func(c *Cmd) { c.Stdout = w } } // WithStderr sets the standard error of the command to the specified writer func WithStderr(w io.Writer) CmdOp { return func(c *Cmd) { c.Stderr = w } } // WithExtraFile adds a file descriptor to the command func WithExtraFile(f *os.File) CmdOp { return func(c *Cmd) { c.ExtraFiles = append(c.ExtraFiles, f) } } gotest.tools-3.5.1/internal/000077500000000000000000000000001463245364400160135ustar00rootroot00000000000000gotest.tools-3.5.1/internal/assert/000077500000000000000000000000001463245364400173145ustar00rootroot00000000000000gotest.tools-3.5.1/internal/assert/assert.go000066400000000000000000000071071463245364400211510ustar00rootroot00000000000000// Package assert provides internal utilties for assertions. package assert import ( "fmt" "go/ast" "go/token" "reflect" "gotest.tools/v3/assert/cmp" "gotest.tools/v3/internal/format" "gotest.tools/v3/internal/source" ) // LogT is the subset of testing.T used by the assert package. type LogT interface { Log(args ...interface{}) } type helperT interface { Helper() } const failureMessage = "assertion failed: " // Eval the comparison and print a failure messages if the comparison has failed. func Eval( t LogT, argSelector argSelector, comparison interface{}, msgAndArgs ...interface{}, ) bool { if ht, ok := t.(helperT); ok { ht.Helper() } var success bool switch check := comparison.(type) { case bool: if check { return true } logFailureFromBool(t, msgAndArgs...) // Undocumented legacy comparison without Result type case func() (success bool, message string): success = runCompareFunc(t, check, msgAndArgs...) case nil: return true case error: msg := failureMsgFromError(check) t.Log(format.WithCustomMessage(failureMessage+msg, msgAndArgs...)) case cmp.Comparison: success = RunComparison(t, argSelector, check, msgAndArgs...) case func() cmp.Result: success = RunComparison(t, argSelector, check, msgAndArgs...) default: t.Log(fmt.Sprintf("invalid Comparison: %v (%T)", check, check)) } return success } func runCompareFunc( t LogT, f func() (success bool, message string), msgAndArgs ...interface{}, ) bool { if ht, ok := t.(helperT); ok { ht.Helper() } if success, message := f(); !success { t.Log(format.WithCustomMessage(failureMessage+message, msgAndArgs...)) return false } return true } func logFailureFromBool(t LogT, msgAndArgs ...interface{}) { if ht, ok := t.(helperT); ok { ht.Helper() } const stackIndex = 3 // Assert()/Check(), assert(), logFailureFromBool() args, err := source.CallExprArgs(stackIndex) if err != nil { t.Log(err.Error()) return } const comparisonArgIndex = 1 // Assert(t, comparison) if len(args) <= comparisonArgIndex { t.Log(failureMessage + "but assert failed to find the expression to print") return } msg, err := boolFailureMessage(args[comparisonArgIndex]) if err != nil { t.Log(err.Error()) msg = "expression is false" } t.Log(format.WithCustomMessage(failureMessage+msg, msgAndArgs...)) } func failureMsgFromError(err error) string { // Handle errors with non-nil types v := reflect.ValueOf(err) if v.Kind() == reflect.Ptr && v.IsNil() { return fmt.Sprintf("error is not nil: error has type %T", err) } return "error is not nil: " + err.Error() } func boolFailureMessage(expr ast.Expr) (string, error) { if binaryExpr, ok := expr.(*ast.BinaryExpr); ok { x, err := source.FormatNode(binaryExpr.X) if err != nil { return "", err } y, err := source.FormatNode(binaryExpr.Y) if err != nil { return "", err } switch binaryExpr.Op { case token.NEQ: return x + " is " + y, nil case token.EQL: return x + " is not " + y, nil case token.GTR: return x + " is <= " + y, nil case token.LSS: return x + " is >= " + y, nil case token.GEQ: return x + " is less than " + y, nil case token.LEQ: return x + " is greater than " + y, nil } } if unaryExpr, ok := expr.(*ast.UnaryExpr); ok && unaryExpr.Op == token.NOT { x, err := source.FormatNode(unaryExpr.X) if err != nil { return "", err } return x + " is true", nil } if ident, ok := expr.(*ast.Ident); ok { return ident.Name + " is false", nil } formatted, err := source.FormatNode(expr) if err != nil { return "", err } return "expression is false: " + formatted, nil } gotest.tools-3.5.1/internal/assert/result.go000066400000000000000000000073171463245364400211710ustar00rootroot00000000000000package assert import ( "errors" "fmt" "go/ast" "gotest.tools/v3/assert/cmp" "gotest.tools/v3/internal/format" "gotest.tools/v3/internal/source" ) // RunComparison and return Comparison.Success. If the comparison fails a messages // will be printed using t.Log. func RunComparison( t LogT, argSelector argSelector, f cmp.Comparison, msgAndArgs ...interface{}, ) bool { if ht, ok := t.(helperT); ok { ht.Helper() } result := f() if result.Success() { return true } if source.IsUpdate() { if updater, ok := result.(updateExpected); ok { const stackIndex = 3 // Assert/Check, assert, RunComparison err := updater.UpdatedExpected(stackIndex) switch { case err == nil: return true case errors.Is(err, source.ErrNotFound): // do nothing, fallthrough to regular failure message default: t.Log("failed to update source", err) return false } } } var message string switch typed := result.(type) { case resultWithComparisonArgs: const stackIndex = 3 // Assert/Check, assert, RunComparison args, err := source.CallExprArgs(stackIndex) if err != nil { t.Log(err.Error()) } message = typed.FailureMessage(filterPrintableExpr(argSelector(args))) case resultBasic: message = typed.FailureMessage() default: message = fmt.Sprintf("comparison returned invalid Result type: %T", result) } t.Log(format.WithCustomMessage(failureMessage+message, msgAndArgs...)) return false } type resultWithComparisonArgs interface { FailureMessage(args []ast.Expr) string } type resultBasic interface { FailureMessage() string } type updateExpected interface { UpdatedExpected(stackIndex int) error } // filterPrintableExpr filters the ast.Expr slice to only include Expr that are // easy to read when printed and contain relevant information to an assertion. // // Ident and SelectorExpr are included because they print nicely and the variable // names may provide additional context to their values. // BasicLit and CompositeLit are excluded because their source is equivalent to // their value, which is already available. // Other types are ignored for now, but could be added if they are relevant. func filterPrintableExpr(args []ast.Expr) []ast.Expr { result := make([]ast.Expr, len(args)) for i, arg := range args { if isShortPrintableExpr(arg) { result[i] = arg continue } if starExpr, ok := arg.(*ast.StarExpr); ok { result[i] = starExpr.X continue } } return result } func isShortPrintableExpr(expr ast.Expr) bool { switch expr.(type) { case *ast.Ident, *ast.SelectorExpr, *ast.IndexExpr, *ast.SliceExpr: return true case *ast.BinaryExpr, *ast.UnaryExpr: return true default: // CallExpr, ParenExpr, TypeAssertExpr, KeyValueExpr, StarExpr return false } } type argSelector func([]ast.Expr) []ast.Expr // ArgsAfterT selects args starting at position 1. Used when the caller has a // testing.T as the first argument, and the args to select should follow it. func ArgsAfterT(args []ast.Expr) []ast.Expr { if len(args) < 1 { return nil } return args[1:] } // ArgsFromComparisonCall selects args from the CallExpression at position 1. // Used when the caller has a testing.T as the first argument, and the args to // select are passed to the cmp.Comparison at position 1. func ArgsFromComparisonCall(args []ast.Expr) []ast.Expr { if len(args) <= 1 { return nil } if callExpr, ok := args[1].(*ast.CallExpr); ok { return callExpr.Args } return nil } // ArgsAtZeroIndex selects args from the CallExpression at position 1. // Used when the caller accepts a single cmp.Comparison argument. func ArgsAtZeroIndex(args []ast.Expr) []ast.Expr { if len(args) == 0 { return nil } if callExpr, ok := args[0].(*ast.CallExpr); ok { return callExpr.Args } return nil } gotest.tools-3.5.1/internal/cleanup/000077500000000000000000000000001463245364400174425ustar00rootroot00000000000000gotest.tools-3.5.1/internal/cleanup/cleanup.go000066400000000000000000000016021463245364400214170ustar00rootroot00000000000000/* Package cleanup handles migration to and support for the Go 1.14+ testing.TB.Cleanup() function. */ package cleanup import ( "os" "strings" ) type cleanupT interface { Cleanup(f func()) } // implemented by gotest.tools/x/subtest.TestContext type addCleanupT interface { AddCleanup(f func()) } type logT interface { Log(...interface{}) } type helperT interface { Helper() } var noCleanup = strings.ToLower(os.Getenv("TEST_NOCLEANUP")) == "true" // Cleanup registers f as a cleanup function on t if any mechanisms are available. // // Skips registering f if TEST_NOCLEANUP is set to true. func Cleanup(t logT, f func()) { if ht, ok := t.(helperT); ok { ht.Helper() } if noCleanup { t.Log("skipping cleanup because TEST_NOCLEANUP was enabled.") return } if ct, ok := t.(cleanupT); ok { ct.Cleanup(f) return } if tc, ok := t.(addCleanupT); ok { tc.AddCleanup(f) } } gotest.tools-3.5.1/internal/difflib/000077500000000000000000000000001463245364400174125ustar00rootroot00000000000000gotest.tools-3.5.1/internal/difflib/LICENSE000066400000000000000000000026451463245364400204260ustar00rootroot00000000000000Copyright (c) 2013, Patrick Mezard All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. The names of its contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. gotest.tools-3.5.1/internal/difflib/difflib.go000066400000000000000000000317241463245364400213470ustar00rootroot00000000000000/*Package difflib is a partial port of Python difflib module. Original source: https://github.com/pmezard/go-difflib This file is trimmed to only the parts used by this repository. */ package difflib // import "gotest.tools/v3/internal/difflib" func min(a, b int) int { if a < b { return a } return b } func max(a, b int) int { if a > b { return a } return b } // Match stores line numbers of size of match type Match struct { A int B int Size int } // OpCode identifies the type of diff type OpCode struct { Tag byte I1 int I2 int J1 int J2 int } // SequenceMatcher compares sequence of strings. The basic // algorithm predates, and is a little fancier than, an algorithm // published in the late 1980's by Ratcliff and Obershelp under the // hyperbolic name "gestalt pattern matching". The basic idea is to find // the longest contiguous matching subsequence that contains no "junk" // elements (R-O doesn't address junk). The same idea is then applied // recursively to the pieces of the sequences to the left and to the right // of the matching subsequence. This does not yield minimal edit // sequences, but does tend to yield matches that "look right" to people. // // SequenceMatcher tries to compute a "human-friendly diff" between two // sequences. Unlike e.g. UNIX(tm) diff, the fundamental notion is the // longest *contiguous* & junk-free matching subsequence. That's what // catches peoples' eyes. The Windows(tm) windiff has another interesting // notion, pairing up elements that appear uniquely in each sequence. // That, and the method here, appear to yield more intuitive difference // reports than does diff. This method appears to be the least vulnerable // to synching up on blocks of "junk lines", though (like blank lines in // ordinary text files, or maybe "

" lines in HTML files). That may be // because this is the only method of the 3 that has a *concept* of // "junk" . // // Timing: Basic R-O is cubic time worst case and quadratic time expected // case. SequenceMatcher is quadratic time for the worst case and has // expected-case behavior dependent in a complicated way on how many // elements the sequences have in common; best case time is linear. type SequenceMatcher struct { a []string b []string b2j map[string][]int IsJunk func(string) bool autoJunk bool bJunk map[string]struct{} matchingBlocks []Match fullBCount map[string]int bPopular map[string]struct{} opCodes []OpCode } // NewMatcher returns a new SequenceMatcher func NewMatcher(a, b []string) *SequenceMatcher { m := SequenceMatcher{autoJunk: true} m.SetSeqs(a, b) return &m } // SetSeqs sets two sequences to be compared. func (m *SequenceMatcher) SetSeqs(a, b []string) { m.SetSeq1(a) m.SetSeq2(b) } // SetSeq1 sets the first sequence to be compared. The second sequence to be compared is // not changed. // // SequenceMatcher computes and caches detailed information about the second // sequence, so if you want to compare one sequence S against many sequences, // use .SetSeq2(s) once and call .SetSeq1(x) repeatedly for each of the other // sequences. // // See also SetSeqs() and SetSeq2(). func (m *SequenceMatcher) SetSeq1(a []string) { if &a == &m.a { return } m.a = a m.matchingBlocks = nil m.opCodes = nil } // SetSeq2 sets the second sequence to be compared. The first sequence to be compared is // not changed. func (m *SequenceMatcher) SetSeq2(b []string) { if &b == &m.b { return } m.b = b m.matchingBlocks = nil m.opCodes = nil m.fullBCount = nil m.chainB() } func (m *SequenceMatcher) chainB() { // Populate line -> index mapping b2j := map[string][]int{} for i, s := range m.b { indices := b2j[s] indices = append(indices, i) b2j[s] = indices } // Purge junk elements m.bJunk = map[string]struct{}{} if m.IsJunk != nil { junk := m.bJunk for s := range b2j { if m.IsJunk(s) { junk[s] = struct{}{} } } for s := range junk { delete(b2j, s) } } // Purge remaining popular elements popular := map[string]struct{}{} n := len(m.b) if m.autoJunk && n >= 200 { ntest := n/100 + 1 for s, indices := range b2j { if len(indices) > ntest { popular[s] = struct{}{} } } for s := range popular { delete(b2j, s) } } m.bPopular = popular m.b2j = b2j } func (m *SequenceMatcher) isBJunk(s string) bool { _, ok := m.bJunk[s] return ok } // Find longest matching block in a[alo:ahi] and b[blo:bhi]. // // If IsJunk is not defined: // // Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where // alo <= i <= i+k <= ahi // blo <= j <= j+k <= bhi // and for all (i',j',k') meeting those conditions, // k >= k' // i <= i' // and if i == i', j <= j' // // In other words, of all maximal matching blocks, return one that // starts earliest in a, and of all those maximal matching blocks that // start earliest in a, return the one that starts earliest in b. // // If IsJunk is defined, first the longest matching block is // determined as above, but with the additional restriction that no // junk element appears in the block. Then that block is extended as // far as possible by matching (only) junk elements on both sides. So // the resulting block never matches on junk except as identical junk // happens to be adjacent to an "interesting" match. // // If no blocks match, return (alo, blo, 0). func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match { // CAUTION: stripping common prefix or suffix would be incorrect. // E.g., // ab // acab // Longest matching block is "ab", but if common prefix is // stripped, it's "a" (tied with "b"). UNIX(tm) diff does so // strip, so ends up claiming that ab is changed to acab by // inserting "ca" in the middle. That's minimal but unintuitive: // "it's obvious" that someone inserted "ac" at the front. // Windiff ends up at the same place as diff, but by pairing up // the unique 'b's and then matching the first two 'a's. besti, bestj, bestsize := alo, blo, 0 // find longest junk-free match // during an iteration of the loop, j2len[j] = length of longest // junk-free match ending with a[i-1] and b[j] j2len := map[int]int{} for i := alo; i != ahi; i++ { // look at all instances of a[i] in b; note that because // b2j has no junk keys, the loop is skipped if a[i] is junk newj2len := map[int]int{} for _, j := range m.b2j[m.a[i]] { // a[i] matches b[j] if j < blo { continue } if j >= bhi { break } k := j2len[j-1] + 1 newj2len[j] = k if k > bestsize { besti, bestj, bestsize = i-k+1, j-k+1, k } } j2len = newj2len } // Extend the best by non-junk elements on each end. In particular, // "popular" non-junk elements aren't in b2j, which greatly speeds // the inner loop above, but also means "the best" match so far // doesn't contain any junk *or* popular non-junk elements. for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) && m.a[besti-1] == m.b[bestj-1] { besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 } for besti+bestsize < ahi && bestj+bestsize < bhi && !m.isBJunk(m.b[bestj+bestsize]) && m.a[besti+bestsize] == m.b[bestj+bestsize] { bestsize += 1 } // Now that we have a wholly interesting match (albeit possibly // empty!), we may as well suck up the matching junk on each // side of it too. Can't think of a good reason not to, and it // saves post-processing the (possibly considerable) expense of // figuring out what to do with it. In the case of an empty // interesting match, this is clearly the right thing to do, // because no other kind of match is possible in the regions. for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) && m.a[besti-1] == m.b[bestj-1] { besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 } for besti+bestsize < ahi && bestj+bestsize < bhi && m.isBJunk(m.b[bestj+bestsize]) && m.a[besti+bestsize] == m.b[bestj+bestsize] { bestsize += 1 } return Match{A: besti, B: bestj, Size: bestsize} } // GetMatchingBlocks returns a list of triples describing matching subsequences. // // Each triple is of the form (i, j, n), and means that // a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in // i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are // adjacent triples in the list, and the second is not the last triple in the // list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe // adjacent equal blocks. // // The last triple is a dummy, (len(a), len(b), 0), and is the only // triple with n==0. func (m *SequenceMatcher) GetMatchingBlocks() []Match { if m.matchingBlocks != nil { return m.matchingBlocks } var matchBlocks func(alo, ahi, blo, bhi int, matched []Match) []Match matchBlocks = func(alo, ahi, blo, bhi int, matched []Match) []Match { match := m.findLongestMatch(alo, ahi, blo, bhi) i, j, k := match.A, match.B, match.Size if match.Size > 0 { if alo < i && blo < j { matched = matchBlocks(alo, i, blo, j, matched) } matched = append(matched, match) if i+k < ahi && j+k < bhi { matched = matchBlocks(i+k, ahi, j+k, bhi, matched) } } return matched } matched := matchBlocks(0, len(m.a), 0, len(m.b), nil) // It's possible that we have adjacent equal blocks in the // matching_blocks list now. nonAdjacent := []Match{} i1, j1, k1 := 0, 0, 0 for _, b := range matched { // Is this block adjacent to i1, j1, k1? i2, j2, k2 := b.A, b.B, b.Size if i1+k1 == i2 && j1+k1 == j2 { // Yes, so collapse them -- this just increases the length of // the first block by the length of the second, and the first // block so lengthened remains the block to compare against. k1 += k2 } else { // Not adjacent. Remember the first block (k1==0 means it's // the dummy we started with), and make the second block the // new block to compare against. if k1 > 0 { nonAdjacent = append(nonAdjacent, Match{i1, j1, k1}) } i1, j1, k1 = i2, j2, k2 } } if k1 > 0 { nonAdjacent = append(nonAdjacent, Match{i1, j1, k1}) } nonAdjacent = append(nonAdjacent, Match{len(m.a), len(m.b), 0}) m.matchingBlocks = nonAdjacent return m.matchingBlocks } // GetOpCodes returns a list of 5-tuples describing how to turn a into b. // // Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple // has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the // tuple preceding it, and likewise for j1 == the previous j2. // // The tags are characters, with these meanings: // // 'r' (replace): a[i1:i2] should be replaced by b[j1:j2] // // 'd' (delete): a[i1:i2] should be deleted, j1==j2 in this case. // // 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case. // // 'e' (equal): a[i1:i2] == b[j1:j2] func (m *SequenceMatcher) GetOpCodes() []OpCode { if m.opCodes != nil { return m.opCodes } i, j := 0, 0 matching := m.GetMatchingBlocks() opCodes := make([]OpCode, 0, len(matching)) for _, m := range matching { // invariant: we've pumped out correct diffs to change // a[:i] into b[:j], and the next matching block is // a[ai:ai+size] == b[bj:bj+size]. So we need to pump // out a diff to change a[i:ai] into b[j:bj], pump out // the matching block, and move (i,j) beyond the match ai, bj, size := m.A, m.B, m.Size tag := byte(0) if i < ai && j < bj { tag = 'r' } else if i < ai { tag = 'd' } else if j < bj { tag = 'i' } if tag > 0 { opCodes = append(opCodes, OpCode{tag, i, ai, j, bj}) } i, j = ai+size, bj+size // the list of matching blocks is terminated by a // sentinel with size 0 if size > 0 { opCodes = append(opCodes, OpCode{'e', ai, i, bj, j}) } } m.opCodes = opCodes return m.opCodes } // GetGroupedOpCodes isolates change clusters by eliminating ranges with no changes. // // Return a generator of groups with up to n lines of context. // Each group is in the same format as returned by GetOpCodes(). func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode { if n < 0 { n = 3 } codes := m.GetOpCodes() if len(codes) == 0 { codes = []OpCode{{'e', 0, 1, 0, 1}} } // Fixup leading and trailing groups if they show no changes. if codes[0].Tag == 'e' { c := codes[0] i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 codes[0] = OpCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2} } if codes[len(codes)-1].Tag == 'e' { c := codes[len(codes)-1] i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 codes[len(codes)-1] = OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)} } nn := n + n groups := [][]OpCode{} group := []OpCode{} for _, c := range codes { i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 // End the current group and start a new one whenever // there is a large range with no changes. if c.Tag == 'e' && i2-i1 > nn { group = append(group, OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)}) groups = append(groups, group) group = []OpCode{} i1, j1 = max(i1, i2-n), max(j1, j2-n) } group = append(group, OpCode{c.Tag, i1, i2, j1, j2}) } if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') { groups = append(groups, group) } return groups } gotest.tools-3.5.1/internal/format/000077500000000000000000000000001463245364400173035ustar00rootroot00000000000000gotest.tools-3.5.1/internal/format/diff.go000066400000000000000000000074731463245364400205550ustar00rootroot00000000000000// Package format provides utilities for formatting diffs and messages. package format import ( "bytes" "fmt" "strings" "unicode" "gotest.tools/v3/internal/difflib" ) const ( contextLines = 2 ) // DiffConfig for a unified diff type DiffConfig struct { A string B string From string To string } // UnifiedDiff is a modified version of difflib.WriteUnifiedDiff with better // support for showing the whitespace differences. func UnifiedDiff(conf DiffConfig) string { a := strings.SplitAfter(conf.A, "\n") b := strings.SplitAfter(conf.B, "\n") groups := difflib.NewMatcher(a, b).GetGroupedOpCodes(contextLines) if len(groups) == 0 { return "" } buf := new(bytes.Buffer) writeFormat := func(format string, args ...interface{}) { buf.WriteString(fmt.Sprintf(format, args...)) } writeLine := func(prefix string, s string) { buf.WriteString(prefix + s) } if hasWhitespaceDiffLines(groups, a, b) { writeLine = visibleWhitespaceLine(writeLine) } formatHeader(writeFormat, conf) for _, group := range groups { formatRangeLine(writeFormat, group) for _, opCode := range group { in, out := a[opCode.I1:opCode.I2], b[opCode.J1:opCode.J2] switch opCode.Tag { case 'e': formatLines(writeLine, " ", in) case 'r': formatLines(writeLine, "-", in) formatLines(writeLine, "+", out) case 'd': formatLines(writeLine, "-", in) case 'i': formatLines(writeLine, "+", out) } } } return buf.String() } // hasWhitespaceDiffLines returns true if any diff groups is only different // because of whitespace characters. func hasWhitespaceDiffLines(groups [][]difflib.OpCode, a, b []string) bool { for _, group := range groups { in, out := new(bytes.Buffer), new(bytes.Buffer) for _, opCode := range group { if opCode.Tag == 'e' { continue } for _, line := range a[opCode.I1:opCode.I2] { in.WriteString(line) } for _, line := range b[opCode.J1:opCode.J2] { out.WriteString(line) } } if removeWhitespace(in.String()) == removeWhitespace(out.String()) { return true } } return false } func removeWhitespace(s string) string { var result []rune for _, r := range s { if !unicode.IsSpace(r) { result = append(result, r) } } return string(result) } func visibleWhitespaceLine(ws func(string, string)) func(string, string) { mapToVisibleSpace := func(r rune) rune { switch r { case '\n': case ' ': return '·' case '\t': return '▷' case '\v': return '▽' case '\r': return '↵' case '\f': return '↓' default: if unicode.IsSpace(r) { return '�' } } return r } return func(prefix, s string) { ws(prefix, strings.Map(mapToVisibleSpace, s)) } } func formatHeader(wf func(string, ...interface{}), conf DiffConfig) { if conf.From != "" || conf.To != "" { wf("--- %s\n", conf.From) wf("+++ %s\n", conf.To) } } func formatRangeLine(wf func(string, ...interface{}), group []difflib.OpCode) { first, last := group[0], group[len(group)-1] range1 := formatRangeUnified(first.I1, last.I2) range2 := formatRangeUnified(first.J1, last.J2) wf("@@ -%s +%s @@\n", range1, range2) } // Convert range to the "ed" format func formatRangeUnified(start, stop int) string { // Per the diff spec at http://www.unix.org/single_unix_specification/ beginning := start + 1 // lines start numbering with one length := stop - start if length == 1 { return fmt.Sprintf("%d", beginning) } if length == 0 { beginning-- // empty ranges begin at line just before the range } return fmt.Sprintf("%d,%d", beginning, length) } func formatLines(writeLine func(string, string), prefix string, lines []string) { for _, line := range lines { writeLine(prefix, line) } // Add a newline if the last line is missing one so that the diff displays // properly. if !strings.HasSuffix(lines[len(lines)-1], "\n") { writeLine("", "\n") } } gotest.tools-3.5.1/internal/format/diff_test.go000066400000000000000000000030341463245364400216010ustar00rootroot00000000000000package format_test import ( "testing" "gotest.tools/v3/assert" "gotest.tools/v3/golden" "gotest.tools/v3/internal/format" ) func TestUnifiedDiff(t *testing.T) { var testcases = []struct { name string a string b string expected string from string to string }{ { name: "empty diff", a: "a\nb\nc", b: "a\nb\nc", from: "from", to: "to", }, { name: "one diff with header", a: "a\nxyz\nc", b: "a\nb\nc", from: "from", to: "to", expected: "one-diff-with-header.golden", }, { name: "many diffs", a: "a123\nxyz\nc\nbaba\nz\nt\nj2j2\nok\nok\ndone\n", b: "a123\nxyz\nc\nabab\nz\nt\nj2j2\nok\nok\n", expected: "many-diff.golden", }, { name: "no trailing newline", a: "a123\nxyz\nc\nbaba\nz\nt\nj2j2\nok\nok\ndone\n", b: "a123\nxyz\nc\nabab\nz\nt\nj2j2\nok\nok", expected: "many-diff-no-trailing-newline.golden", }, { name: "whitespace diff", a: " something\n something\n \v\r\n", b: " something\n\tsomething\n \n", expected: "whitespace-diff.golden", }, } for _, testcase := range testcases { t.Run(testcase.name, func(t *testing.T) { diff := format.UnifiedDiff(format.DiffConfig{ A: testcase.a, B: testcase.b, From: testcase.from, To: testcase.to, }) if testcase.expected != "" { assert.Assert(t, golden.String(diff, testcase.expected)) return } assert.Equal(t, diff, "") }) } } gotest.tools-3.5.1/internal/format/format.go000066400000000000000000000012671463245364400211300ustar00rootroot00000000000000package format // import "gotest.tools/v3/internal/format" import "fmt" // Message accepts a msgAndArgs varargs and formats it using fmt.Sprintf func Message(msgAndArgs ...interface{}) string { switch len(msgAndArgs) { case 0: return "" case 1: return fmt.Sprintf("%v", msgAndArgs[0]) default: return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) } } // WithCustomMessage accepts one or two messages and formats them appropriately func WithCustomMessage(source string, msgAndArgs ...interface{}) string { custom := Message(msgAndArgs...) switch { case custom == "": return source case source == "": return custom } return fmt.Sprintf("%s: %s", source, custom) } gotest.tools-3.5.1/internal/format/format_test.go000066400000000000000000000022311463245364400221570ustar00rootroot00000000000000package format_test import ( "testing" "gotest.tools/v3/assert" "gotest.tools/v3/internal/format" ) func TestMessage(t *testing.T) { var testcases = []struct { doc string args []interface{} expected string }{ { doc: "none", }, { doc: "single string", args: args("foo"), expected: "foo", }, { doc: "single non-string", args: args(123), expected: "123", }, { doc: "format string and args", args: args("%s %v", "a", 3), expected: "a 3", }, } for _, tc := range testcases { t.Run(tc.doc, func(t *testing.T) { assert.Equal(t, format.Message(tc.args...), tc.expected) }) } } func args(a ...interface{}) []interface{} { return a } func TestWithCustomMessage(t *testing.T) { t.Run("only custom", func(t *testing.T) { msg := format.WithCustomMessage("", "extra") assert.Equal(t, msg, "extra") }) t.Run("only source", func(t *testing.T) { msg := format.WithCustomMessage("source") assert.Equal(t, msg, "source") }) t.Run("source and custom", func(t *testing.T) { msg := format.WithCustomMessage("source", "extra") assert.Equal(t, msg, "source: extra") }) } gotest.tools-3.5.1/internal/format/testdata/000077500000000000000000000000001463245364400211145ustar00rootroot00000000000000gotest.tools-3.5.1/internal/format/testdata/many-diff-no-trailing-newline.golden000066400000000000000000000001051463245364400300340ustar00rootroot00000000000000@@ -2,10 +2,8 @@ xyz c -baba +abab z t j2j2 ok -ok -done - +ok gotest.tools-3.5.1/internal/format/testdata/many-diff.golden000066400000000000000000000001121463245364400241520ustar00rootroot00000000000000@@ -2,5 +2,5 @@ xyz c -baba +abab z t @@ -8,4 +8,3 @@ ok ok -done gotest.tools-3.5.1/internal/format/testdata/one-diff-with-header.golden000066400000000000000000000000561463245364400261750ustar00rootroot00000000000000--- from +++ to @@ -1,3 +1,3 @@ a -xyz +b c gotest.tools-3.5.1/internal/format/testdata/whitespace-diff.golden000066400000000000000000000001341463245364400253460ustar00rootroot00000000000000@@ -1,4 +1,4 @@ ··something -······something -····▽↵ +▷something +·· gotest.tools-3.5.1/internal/maint/000077500000000000000000000000001463245364400171235ustar00rootroot00000000000000gotest.tools-3.5.1/internal/maint/maint.go000066400000000000000000000012361463245364400205640ustar00rootroot00000000000000// Package maint implements assert.TestingT for uses outside of test cases, // for example, in a TestMain. package maint // import "gotest.tools/v3/internal/maint" import ( "fmt" "os" ) // T provides an implementation of assert.TestingT which uses os.Exit, and // fmt.Println. This implementation can be used outside of test cases to provide // assert.TestingT, for example in a TestMain. var T = t{} type t struct{} // FailNow exits with a non-zero code func (t t) FailNow() { os.Exit(1) } // Fail exits with a non-zero code func (t t) Fail() { os.Exit(2) } // Log args by printing them to stdout func (t t) Log(args ...interface{}) { fmt.Println(args...) } gotest.tools-3.5.1/internal/source/000077500000000000000000000000001463245364400173135ustar00rootroot00000000000000gotest.tools-3.5.1/internal/source/defers.go000066400000000000000000000022241463245364400211120ustar00rootroot00000000000000package source import ( "fmt" "go/ast" "go/token" ) func scanToDeferLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node { var matchedNode ast.Node ast.Inspect(node, func(node ast.Node) bool { switch { case node == nil || matchedNode != nil: return false case fileset.Position(node.End()).Line == lineNum: if funcLit, ok := node.(*ast.FuncLit); ok { matchedNode = funcLit return false } } return true }) debug("defer line node: %s", debugFormatNode{matchedNode}) return matchedNode } func guessDefer(node ast.Node) (ast.Node, error) { defers := collectDefers(node) switch len(defers) { case 0: return nil, fmt.Errorf("failed to find expression in defer") case 1: return defers[0].Call, nil default: return nil, fmt.Errorf( "ambiguous call expression: multiple (%d) defers in call block", len(defers)) } } func collectDefers(node ast.Node) []*ast.DeferStmt { var defers []*ast.DeferStmt ast.Inspect(node, func(node ast.Node) bool { if d, ok := node.(*ast.DeferStmt); ok { defers = append(defers, d) debug("defer: %s", debugFormatNode{d}) return false } return true }) return defers } gotest.tools-3.5.1/internal/source/source.go000066400000000000000000000071571463245364400211540ustar00rootroot00000000000000// Package source provides utilities for handling source-code. package source // import "gotest.tools/v3/internal/source" import ( "bytes" "errors" "fmt" "go/ast" "go/format" "go/parser" "go/token" "os" "runtime" ) // FormattedCallExprArg returns the argument from an ast.CallExpr at the // index in the call stack. The argument is formatted using FormatNode. func FormattedCallExprArg(stackIndex int, argPos int) (string, error) { args, err := CallExprArgs(stackIndex + 1) if err != nil { return "", err } if argPos >= len(args) { return "", errors.New("failed to find expression") } return FormatNode(args[argPos]) } // CallExprArgs returns the ast.Expr slice for the args of an ast.CallExpr at // the index in the call stack. func CallExprArgs(stackIndex int) ([]ast.Expr, error) { _, filename, line, ok := runtime.Caller(stackIndex + 1) if !ok { return nil, errors.New("failed to get call stack") } debug("call stack position: %s:%d", filename, line) fileset := token.NewFileSet() astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors) if err != nil { return nil, fmt.Errorf("failed to parse source file %s: %w", filename, err) } expr, err := getCallExprArgs(fileset, astFile, line) if err != nil { return nil, fmt.Errorf("call from %s:%d: %w", filename, line, err) } return expr, nil } func getNodeAtLine(fileset *token.FileSet, astFile ast.Node, lineNum int) (ast.Node, error) { if node := scanToLine(fileset, astFile, lineNum); node != nil { return node, nil } if node := scanToDeferLine(fileset, astFile, lineNum); node != nil { node, err := guessDefer(node) if err != nil || node != nil { return node, err } } return nil, nil } func scanToLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node { var matchedNode ast.Node ast.Inspect(node, func(node ast.Node) bool { switch { case node == nil || matchedNode != nil: return false case fileset.Position(node.Pos()).Line == lineNum: matchedNode = node return false } return true }) return matchedNode } func getCallExprArgs(fileset *token.FileSet, astFile ast.Node, line int) ([]ast.Expr, error) { node, err := getNodeAtLine(fileset, astFile, line) switch { case err != nil: return nil, err case node == nil: return nil, fmt.Errorf("failed to find an expression") } debug("found node: %s", debugFormatNode{node}) visitor := &callExprVisitor{} ast.Walk(visitor, node) if visitor.expr == nil { return nil, errors.New("failed to find call expression") } debug("callExpr: %s", debugFormatNode{visitor.expr}) return visitor.expr.Args, nil } type callExprVisitor struct { expr *ast.CallExpr } func (v *callExprVisitor) Visit(node ast.Node) ast.Visitor { if v.expr != nil || node == nil { return nil } debug("visit: %s", debugFormatNode{node}) switch typed := node.(type) { case *ast.CallExpr: v.expr = typed return nil case *ast.DeferStmt: ast.Walk(v, typed.Call.Fun) return nil } return v } // FormatNode using go/format.Node and return the result as a string func FormatNode(node ast.Node) (string, error) { buf := new(bytes.Buffer) err := format.Node(buf, token.NewFileSet(), node) return buf.String(), err } var debugEnabled = os.Getenv("GOTESTTOOLS_DEBUG") != "" func debug(format string, args ...interface{}) { if debugEnabled { fmt.Fprintf(os.Stderr, "DEBUG: "+format+"\n", args...) } } type debugFormatNode struct { ast.Node } func (n debugFormatNode) String() string { if n.Node == nil { return "none" } out, err := FormatNode(n.Node) if err != nil { return fmt.Sprintf("failed to format %s: %s", n.Node, err) } return fmt.Sprintf("(%T) %s", n.Node, out) } gotest.tools-3.5.1/internal/source/source_test.go000066400000000000000000000035141463245364400222040ustar00rootroot00000000000000package source_test // using a separate package for test to avoid circular imports with the assert // package import ( "fmt" "runtime" "strings" "testing" "gotest.tools/v3/assert" "gotest.tools/v3/internal/source" "gotest.tools/v3/skip" ) func TestFormattedCallExprArg_SingleLine(t *testing.T) { msg, err := shim("not", "this", "this text") assert.NilError(t, err) assert.Equal(t, `"this text"`, msg) } func TestFormattedCallExprArg_MultiLine(t *testing.T) { msg, err := shim( "first", "second", "this text", ) assert.NilError(t, err) assert.Equal(t, `"this text"`, msg) } func TestFormattedCallExprArg_IfStatement(t *testing.T) { if msg, err := shim( "first", "second", "this text", ); true { assert.NilError(t, err) assert.Equal(t, `"this text"`, msg) } } func shim(_, _, _ string) (string, error) { return source.FormattedCallExprArg(1, 2) } func TestFormattedCallExprArg_InDefer(t *testing.T) { skip.If(t, isGoVersion18) cap := &capture{} func() { defer cap.shim("first", "second") }() assert.NilError(t, cap.err) assert.Equal(t, cap.value, `"second"`) } func isGoVersion18() bool { return strings.HasPrefix(runtime.Version(), "go1.8.") } type capture struct { value string err error } func (c *capture) shim(_, _ string) { c.value, c.err = source.FormattedCallExprArg(1, 1) } func TestFormattedCallExprArg_InAnonymousDefer(t *testing.T) { cap := &capture{} func() { fmt.Println() defer fmt.Println() defer func() { cap.shim("first", "second") }() }() assert.NilError(t, cap.err) assert.Equal(t, cap.value, `"second"`) } func TestFormattedCallExprArg_InDeferMultipleDefers(t *testing.T) { skip.If(t, isGoVersion18) cap := &capture{} func() { fmt.Println() defer fmt.Println() defer cap.shim("first", "second") }() assert.ErrorContains(t, cap.err, "ambiguous call expression") } gotest.tools-3.5.1/internal/source/update.go000066400000000000000000000103061463245364400211240ustar00rootroot00000000000000package source import ( "bytes" "errors" "flag" "fmt" "go/ast" "go/format" "go/parser" "go/token" "os" "runtime" "strings" ) // IsUpdate is returns true if the -update flag is set. It indicates the user // running the tests would like to update any golden values. func IsUpdate() bool { if Update { return true } return flag.Lookup("update").Value.(flag.Getter).Get().(bool) } // Update is a shim for testing, and for compatibility with the old -update-golden // flag. var Update bool func init() { if f := flag.Lookup("update"); f != nil { getter, ok := f.Value.(flag.Getter) msg := "some other package defined an incompatible -update flag, expected a flag.Bool" if !ok { panic(msg) } if _, ok := getter.Get().(bool); !ok { panic(msg) } return } flag.Bool("update", false, "update golden values") } // ErrNotFound indicates that UpdateExpectedValue failed to find the // variable to update, likely because it is not a package level variable. var ErrNotFound = fmt.Errorf("failed to find variable for update of golden value") // UpdateExpectedValue looks for a package-level variable with a name that // starts with expected in the arguments to the caller. If the variable is // found, the value of the variable will be updated to value of the other // argument to the caller. func UpdateExpectedValue(stackIndex int, x, y interface{}) error { _, filename, line, ok := runtime.Caller(stackIndex + 1) if !ok { return errors.New("failed to get call stack") } debug("call stack position: %s:%d", filename, line) fileset := token.NewFileSet() astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors|parser.ParseComments) if err != nil { return fmt.Errorf("failed to parse source file %s: %w", filename, err) } expr, err := getCallExprArgs(fileset, astFile, line) if err != nil { return fmt.Errorf("call from %s:%d: %w", filename, line, err) } if len(expr) < 3 { debug("not enough arguments %d: %v", len(expr), debugFormatNode{Node: &ast.CallExpr{Args: expr}}) return ErrNotFound } argIndex, ident := getIdentForExpectedValueArg(expr) if argIndex < 0 || ident == nil { debug("no arguments started with the word 'expected': %v", debugFormatNode{Node: &ast.CallExpr{Args: expr}}) return ErrNotFound } value := x if argIndex == 1 { value = y } strValue, ok := value.(string) if !ok { debug("value must be type string, got %T", value) return ErrNotFound } return UpdateVariable(filename, fileset, astFile, ident, strValue) } // UpdateVariable writes to filename the contents of astFile with the value of // the variable updated to value. func UpdateVariable( filename string, fileset *token.FileSet, astFile *ast.File, ident *ast.Ident, value string, ) error { obj := ident.Obj if obj == nil { return ErrNotFound } if obj.Kind != ast.Con && obj.Kind != ast.Var { debug("can only update var and const, found %v", obj.Kind) return ErrNotFound } switch decl := obj.Decl.(type) { case *ast.ValueSpec: if len(decl.Names) != 1 { debug("more than one name in ast.ValueSpec") return ErrNotFound } decl.Values[0] = &ast.BasicLit{ Kind: token.STRING, Value: "`" + value + "`", } case *ast.AssignStmt: if len(decl.Lhs) != 1 { debug("more than one name in ast.AssignStmt") return ErrNotFound } decl.Rhs[0] = &ast.BasicLit{ Kind: token.STRING, Value: "`" + value + "`", } default: debug("can only update *ast.ValueSpec, found %T", obj.Decl) return ErrNotFound } var buf bytes.Buffer if err := format.Node(&buf, fileset, astFile); err != nil { return fmt.Errorf("failed to format file after update: %w", err) } fh, err := os.Create(filename) if err != nil { return fmt.Errorf("failed to open file %v: %w", filename, err) } if _, err = fh.Write(buf.Bytes()); err != nil { return fmt.Errorf("failed to write file %v: %w", filename, err) } if err := fh.Sync(); err != nil { return fmt.Errorf("failed to sync file %v: %w", filename, err) } return nil } func getIdentForExpectedValueArg(expr []ast.Expr) (int, *ast.Ident) { for i := 1; i < 3; i++ { switch e := expr[i].(type) { case *ast.Ident: if strings.HasPrefix(strings.ToLower(e.Name), "expected") { return i, e } } } return -1, nil } gotest.tools-3.5.1/internal/source/version.go000066400000000000000000000014121463245364400213250ustar00rootroot00000000000000package source import ( "runtime" "strconv" "strings" ) // GoVersionLessThan returns true if runtime.Version() is semantically less than // version major.minor. Returns false if a release version can not be parsed from // runtime.Version(). func GoVersionLessThan(major, minor int64) bool { version := runtime.Version() // not a release version if !strings.HasPrefix(version, "go") { return false } version = strings.TrimPrefix(version, "go") parts := strings.Split(version, ".") if len(parts) < 2 { return false } rMajor, err := strconv.ParseInt(parts[0], 10, 32) if err != nil { return false } if rMajor != major { return rMajor < major } rMinor, err := strconv.ParseInt(parts[1], 10, 32) if err != nil { return false } return rMinor < minor } gotest.tools-3.5.1/pkg.go000066400000000000000000000002301463245364400153020ustar00rootroot00000000000000/* Package gotesttools is a collection of packages to augment `testing` and support common patterns. */ package gotesttools // import "gotest.tools/v3" gotest.tools-3.5.1/poll/000077500000000000000000000000001463245364400151455ustar00rootroot00000000000000gotest.tools-3.5.1/poll/check.go000066400000000000000000000020741463245364400165540ustar00rootroot00000000000000package poll import ( "net" "os" ) // Check is a function which will be used as check for the [WaitOn] method. type Check func(t LogT) Result // FileExists looks on filesystem and check that path exists. func FileExists(path string) Check { return func(t LogT) Result { if h, ok := t.(helperT); ok { h.Helper() } _, err := os.Stat(path) switch { case os.IsNotExist(err): t.Logf("waiting on file %s to exist", path) return Continue("file %s does not exist", path) case err != nil: return Error(err) default: return Success() } } } // Connection try to open a connection to the address on the // named network. See [net.Dial] for a description of the network and // address parameters. func Connection(network, address string) Check { return func(t LogT) Result { if h, ok := t.(helperT); ok { h.Helper() } _, err := net.Dial(network, address) if err != nil { t.Logf("waiting on socket %s://%s to be available...", network, address) return Continue("socket %s://%s not available", network, address) } return Success() } } gotest.tools-3.5.1/poll/check_test.go000066400000000000000000000016411463245364400176120ustar00rootroot00000000000000package poll import ( "fmt" "os" "testing" "gotest.tools/v3/assert" ) func TestWaitOnFile(t *testing.T) { fakeFilePath := "./fakefile" check := FileExists(fakeFilePath) t.Run("file does not exist", func(t *testing.T) { r := check(t) assert.Assert(t, !r.Done()) assert.Equal(t, r.Message(), fmt.Sprintf("file %s does not exist", fakeFilePath)) }) os.Create(fakeFilePath) defer os.Remove(fakeFilePath) t.Run("file exists", func(t *testing.T) { assert.Assert(t, check(t).Done()) }) } func TestWaitOnSocketWithTimeout(t *testing.T) { t.Run("connection to unavailable address", func(t *testing.T) { check := Connection("tcp", "foo.bar:55555") r := check(t) assert.Assert(t, !r.Done()) assert.Equal(t, r.Message(), "socket tcp://foo.bar:55555 not available") }) t.Run("connection to ", func(t *testing.T) { check := Connection("tcp", "google.com:80") assert.Assert(t, check(t).Done()) }) } gotest.tools-3.5.1/poll/example_test.go000066400000000000000000000016531463245364400201730ustar00rootroot00000000000000package poll_test import ( "fmt" "time" "gotest.tools/v3/poll" ) var t poll.TestingT func numOfProcesses() (int, error) { return 0, nil } func ExampleWaitOn() { desired := 10 check := func(t poll.LogT) poll.Result { actual, err := numOfProcesses() if err != nil { return poll.Error(fmt.Errorf("failed to get number of processes: %w", err)) } if actual == desired { return poll.Success() } t.Logf("waiting on process count to be %d...", desired) return poll.Continue("number of processes is %d, not %d", actual, desired) } poll.WaitOn(t, check) } func isDesiredState() bool { return false } func getState() string { return "" } func ExampleSettingOp() { check := func(t poll.LogT) poll.Result { if isDesiredState() { return poll.Success() } return poll.Continue("state is: %s", getState()) } poll.WaitOn(t, check, poll.WithTimeout(30*time.Second), poll.WithDelay(15*time.Millisecond)) } gotest.tools-3.5.1/poll/poll.go000066400000000000000000000102131463245364400164370ustar00rootroot00000000000000/*Package poll provides tools for testing asynchronous code. */ package poll // import "gotest.tools/v3/poll" import ( "fmt" "strings" "time" "gotest.tools/v3/assert/cmp" "gotest.tools/v3/internal/assert" ) // TestingT is the subset of [testing.T] used by [WaitOn] type TestingT interface { LogT Fatalf(format string, args ...interface{}) } // LogT is a logging interface that is passed to the [WaitOn] check function type LogT interface { Log(args ...interface{}) Logf(format string, args ...interface{}) } type helperT interface { Helper() } // Settings are used to configure the behaviour of [WaitOn] type Settings struct { // Timeout is the maximum time to wait for the condition. Defaults to 10s. Timeout time.Duration // Delay is the time to sleep between checking the condition. Defaults to // 100ms. Delay time.Duration } func defaultConfig() *Settings { return &Settings{Timeout: 10 * time.Second, Delay: 100 * time.Millisecond} } // SettingOp is a function which accepts and modifies Settings type SettingOp func(config *Settings) // WithDelay sets the delay to wait between polls func WithDelay(delay time.Duration) SettingOp { return func(config *Settings) { config.Delay = delay } } // WithTimeout sets the timeout func WithTimeout(timeout time.Duration) SettingOp { return func(config *Settings) { config.Timeout = timeout } } // Result of a check performed by [WaitOn] type Result interface { // Error indicates that the check failed and polling should stop, and the // the has failed Error() error // Done indicates that polling should stop, and the test should proceed Done() bool // Message provides the most recent state when polling has not completed Message() string } type result struct { done bool message string err error } func (r result) Done() bool { return r.done } func (r result) Message() string { return r.message } func (r result) Error() error { return r.err } // Continue returns a [Result] that indicates to [WaitOn] that it should continue // polling. The message text will be used as the failure message if the timeout // is reached. func Continue(message string, args ...interface{}) Result { return result{message: fmt.Sprintf(message, args...)} } // Success returns a [Result] where Done() returns true, which indicates to [WaitOn] // that it should stop polling and exit without an error. func Success() Result { return result{done: true} } // Error returns a [Result] that indicates to [WaitOn] that it should fail the test // and stop polling. func Error(err error) Result { return result{err: err} } // WaitOn a condition or until a timeout. Poll by calling check and exit when // check returns a done Result. To fail a test and exit polling with an error // return a error result. func WaitOn(t TestingT, check Check, pollOps ...SettingOp) { if ht, ok := t.(helperT); ok { ht.Helper() } config := defaultConfig() for _, pollOp := range pollOps { pollOp(config) } var lastMessage string after := time.After(config.Timeout) chResult := make(chan Result) for { go func() { chResult <- check(t) }() select { case <-after: if lastMessage == "" { lastMessage = "first check never completed" } t.Fatalf("timeout hit after %s: %s", config.Timeout, lastMessage) case result := <-chResult: switch { case result.Error() != nil: t.Fatalf("polling check failed: %s", result.Error()) case result.Done(): return } time.Sleep(config.Delay) lastMessage = result.Message() } } } // Compare values using the [cmp.Comparison]. If the comparison fails return a // result which indicates to WaitOn that it should continue waiting. // If the comparison is successful then [WaitOn] stops polling. func Compare(compare cmp.Comparison) Result { buf := new(logBuffer) if assert.RunComparison(buf, assert.ArgsAtZeroIndex, compare) { return Success() } return Continue(buf.String()) } type logBuffer struct { log [][]interface{} } func (c *logBuffer) Log(args ...interface{}) { c.log = append(c.log, args) } func (c *logBuffer) String() string { b := new(strings.Builder) for _, item := range c.log { b.WriteString(fmt.Sprint(item...) + " ") } return b.String() } gotest.tools-3.5.1/poll/poll_test.go000066400000000000000000000035611463245364400175060ustar00rootroot00000000000000package poll import ( "fmt" "testing" "time" "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" ) type fakeT struct { failed string } func (t *fakeT) Fatalf(format string, args ...interface{}) { t.failed = fmt.Sprintf(format, args...) panic("exit wait on") } func (t *fakeT) Log(args ...interface{}) {} func (t *fakeT) Logf(format string, args ...interface{}) {} func TestWaitOn(t *testing.T) { counter := 0 end := 4 check := func(t LogT) Result { if counter == end { return Success() } counter++ return Continue("counter is at %d not yet %d", counter-1, end) } WaitOn(t, check, WithDelay(0)) assert.Equal(t, end, counter) } func TestWaitOnWithTimeout(t *testing.T) { fakeT := &fakeT{} check := func(t LogT) Result { return Continue("not done") } assert.Assert(t, cmp.Panics(func() { WaitOn(fakeT, check, WithTimeout(time.Millisecond)) })) assert.Equal(t, "timeout hit after 1ms: not done", fakeT.failed) } func TestWaitOnWithCheckTimeout(t *testing.T) { fakeT := &fakeT{} check := func(t LogT) Result { time.Sleep(1 * time.Second) return Continue("not done") } assert.Assert(t, cmp.Panics(func() { WaitOn(fakeT, check, WithTimeout(time.Millisecond)) })) assert.Equal(t, "timeout hit after 1ms: first check never completed", fakeT.failed) } func TestWaitOnWithCheckError(t *testing.T) { fakeT := &fakeT{} check := func(t LogT) Result { return Error(fmt.Errorf("broke")) } assert.Assert(t, cmp.Panics(func() { WaitOn(fakeT, check) })) assert.Equal(t, "polling check failed: broke", fakeT.failed) } func TestWaitOn_WithCompare(t *testing.T) { fakeT := &fakeT{} check := func(t LogT) Result { return Compare(cmp.Equal(3, 4)) } assert.Assert(t, cmp.Panics(func() { WaitOn(fakeT, check, WithDelay(0), WithTimeout(10*time.Millisecond)) })) assert.Assert(t, cmp.Contains(fakeT.failed, "assertion failed: 3 (int) != 4 (int)")) } gotest.tools-3.5.1/scripts/000077500000000000000000000000001463245364400156665ustar00rootroot00000000000000gotest.tools-3.5.1/scripts/binary-gty-migrate-from-testify000077500000000000000000000001531463245364400237540ustar00rootroot00000000000000#!/usr/bin/env bash exec go build -o ./dist/gty-migrate-from-testify ./assert/cmd/gty-migrate-from-testify gotest.tools-3.5.1/scripts/test-unit000077500000000000000000000003141463245364400175460ustar00rootroot00000000000000#!/usr/bin/env bash set -eu -o pipefail paths=${@:-$(go list ./... | grep -v '/vendor/')} TESTFLAGS=${TESTFLAGS-} TESTTIMEOUT=${TESTTIMEOUT-30s} go test -test.timeout "$TESTTIMEOUT" $TESTFLAGS -v $paths gotest.tools-3.5.1/skip/000077500000000000000000000000001463245364400151455ustar00rootroot00000000000000gotest.tools-3.5.1/skip/example_test.go000066400000000000000000000015571463245364400201760ustar00rootroot00000000000000package skip_test import ( "testing" "gotest.tools/v3/skip" ) var apiVersion = "" type env struct{} func (e env) hasFeature(_ string) bool { return false } var testEnv = env{} func MissingFeature() bool { return false } var t = &testing.T{} func ExampleIf() { // --- SKIP: TestName (0.00s) // skip.go:19: MissingFeature skip.If(t, MissingFeature) // --- SKIP: TestName (0.00s) // skip.go:19: MissingFeature: coming soon skip.If(t, MissingFeature, "coming soon") } func ExampleIf_withExpression() { // --- SKIP: TestName (0.00s) // skip.go:19: apiVersion < version("v1.24") skip.If(t, apiVersion < version("v1.24")) // --- SKIP: TestName (0.00s) // skip.go:19: !textenv.hasFeature("build"): coming soon skip.If(t, !testEnv.hasFeature("build"), "coming soon") } func version(v string) string { return v } gotest.tools-3.5.1/skip/skip.go000066400000000000000000000045131463245364400164450ustar00rootroot00000000000000/* Package skip provides functions for skipping a test and printing the source code of the condition used to skip the test. */ package skip // import "gotest.tools/v3/skip" import ( "fmt" "path" "reflect" "runtime" "strings" "gotest.tools/v3/internal/format" "gotest.tools/v3/internal/source" ) type skipT interface { Skip(args ...interface{}) Log(args ...interface{}) } // Result of skip function type Result interface { Skip() bool Message() string } type helperT interface { Helper() } // BoolOrCheckFunc can be a bool, func() bool, or func() Result. Other types will panic type BoolOrCheckFunc interface{} // If the condition expression evaluates to true, skip the test. // // The condition argument may be one of three types: bool, func() bool, or // func() SkipResult. // When called with a bool, the test will be skip if the condition evaluates to true. // When called with a func() bool, the test will be skip if the function returns true. // When called with a func() Result, the test will be skip if the Skip method // of the result returns true. // The skip message will contain the source code of the expression. // Extra message text can be passed as a format string with args. func If(t skipT, condition BoolOrCheckFunc, msgAndArgs ...interface{}) { if ht, ok := t.(helperT); ok { ht.Helper() } switch check := condition.(type) { case bool: ifCondition(t, check, msgAndArgs...) case func() bool: if check() { t.Skip(format.WithCustomMessage(getFunctionName(check), msgAndArgs...)) } case func() Result: result := check() if result.Skip() { msg := getFunctionName(check) + ": " + result.Message() t.Skip(format.WithCustomMessage(msg, msgAndArgs...)) } default: panic(fmt.Sprintf("invalid type for condition arg: %T", check)) } } func getFunctionName(function interface{}) string { funcPath := runtime.FuncForPC(reflect.ValueOf(function).Pointer()).Name() return strings.SplitN(path.Base(funcPath), ".", 2)[1] } func ifCondition(t skipT, condition bool, msgAndArgs ...interface{}) { if ht, ok := t.(helperT); ok { ht.Helper() } if !condition { return } const ( stackIndex = 2 argPos = 1 ) source, err := source.FormattedCallExprArg(stackIndex, argPos) if err != nil { t.Log(err.Error()) t.Skip(format.Message(msgAndArgs...)) } t.Skip(format.WithCustomMessage(source, msgAndArgs...)) } gotest.tools-3.5.1/skip/skip_test.go000066400000000000000000000054331463245364400175060ustar00rootroot00000000000000package skip import ( "bytes" "fmt" "testing" "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" ) type fakeSkipT struct { reason string logs []string } func (f *fakeSkipT) Skip(args ...interface{}) { buf := new(bytes.Buffer) for _, arg := range args { buf.WriteString(fmt.Sprintf("%s", arg)) } f.reason = buf.String() } func (f *fakeSkipT) Log(args ...interface{}) { f.logs = append(f.logs, fmt.Sprintf("%s", args[0])) } func (f *fakeSkipT) Helper() {} func version(v string) string { return v } func TestIfCondition(t *testing.T) { skipT := &fakeSkipT{} apiVersion := "v1.4" If(skipT, apiVersion < version("v1.6")) assert.Equal(t, `apiVersion < version("v1.6")`, skipT.reason) assert.Assert(t, cmp.Len(skipT.logs, 0)) } func TestIfConditionWithMessage(t *testing.T) { skipT := &fakeSkipT{} apiVersion := "v1.4" If(skipT, apiVersion < "v1.6", "see notes") assert.Equal(t, `apiVersion < "v1.6": see notes`, skipT.reason) assert.Assert(t, cmp.Len(skipT.logs, 0)) } func TestIfConditionMultiline(t *testing.T) { skipT := &fakeSkipT{} apiVersion := "v1.4" If( skipT, apiVersion < "v1.6") assert.Equal(t, `apiVersion < "v1.6"`, skipT.reason) assert.Assert(t, cmp.Len(skipT.logs, 0)) } func TestIfConditionMultilineWithMessage(t *testing.T) { skipT := &fakeSkipT{} apiVersion := "v1.4" If( skipT, apiVersion < "v1.6", "see notes") assert.Equal(t, `apiVersion < "v1.6": see notes`, skipT.reason) assert.Assert(t, cmp.Len(skipT.logs, 0)) } func TestIfConditionNoSkip(t *testing.T) { skipT := &fakeSkipT{} If(skipT, false) assert.Equal(t, "", skipT.reason) assert.Assert(t, cmp.Len(skipT.logs, 0)) } func SkipBecauseISaidSo() bool { return true } func TestIf(t *testing.T) { skipT := &fakeSkipT{} If(skipT, SkipBecauseISaidSo) assert.Equal(t, "SkipBecauseISaidSo", skipT.reason) } func TestIfWithMessage(t *testing.T) { skipT := &fakeSkipT{} If(skipT, SkipBecauseISaidSo, "see notes") assert.Equal(t, "SkipBecauseISaidSo: see notes", skipT.reason) } func TestIf_InvalidCondition(t *testing.T) { skipT := &fakeSkipT{} assert.Assert(t, cmp.Panics(func() { If(skipT, "just a string") })) } func TestIfWithSkipResultFunc(t *testing.T) { t.Run("no extra message", func(t *testing.T) { skipT := &fakeSkipT{} If(skipT, alwaysSkipWithMessage) assert.Equal(t, "alwaysSkipWithMessage: skip because I said so!", skipT.reason) }) t.Run("with extra message", func(t *testing.T) { skipT := &fakeSkipT{} If(skipT, alwaysSkipWithMessage, "also %v", 4) assert.Equal(t, "alwaysSkipWithMessage: skip because I said so!: also 4", skipT.reason) }) } func alwaysSkipWithMessage() Result { return skipResult{} } type skipResult struct{} func (s skipResult) Skip() bool { return true } func (s skipResult) Message() string { return "skip because I said so!" } gotest.tools-3.5.1/x/000077500000000000000000000000001463245364400144465ustar00rootroot00000000000000gotest.tools-3.5.1/x/doc.go000066400000000000000000000004001463245364400155340ustar00rootroot00000000000000/* Package x is a namespace for experimental packages. Packages under x have looser compatibility requirements. Packages in this namespace may contain backwards incompatible changes within the same major version. */ package x // import "gotest.tools/v3/x" gotest.tools-3.5.1/x/generics/000077500000000000000000000000001463245364400162455ustar00rootroot00000000000000gotest.tools-3.5.1/x/generics/go.mod000066400000000000000000000001751463245364400173560ustar00rootroot00000000000000module gotest.tools/x/generics go 1.18 require gotest.tools/v3 v3.3.0 require github.com/google/go-cmp v0.5.5 // indirect gotest.tools-3.5.1/x/generics/go.sum000066400000000000000000000054651463245364400174120ustar00rootroot00000000000000github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= gotest.tools-3.5.1/x/generics/property/000077500000000000000000000000001463245364400201315ustar00rootroot00000000000000gotest.tools-3.5.1/x/generics/property/complete.go000066400000000000000000000165151463245364400223000ustar00rootroot00000000000000package property import ( "fmt" "math/rand" "os" "reflect" "strconv" "strings" "time" ) // CompleteOptions are the settings used by Complete. type CompleteOptions[T any] struct { // Operation should return true if the operation was successful, and false // otherwise. If Operation returns false, the test will be marked as failed // and the failure message will indicate which field of T was not used in // the operation. // // Operation should call the function being tested. Operation func(original T, modified T) bool // IgnoreFields is a list of struct field paths that should be skipped. These // fields are intentionally not part of the operation. Each value in the list // is the path to a field, using dotted notation for nested fields. // // The example below demonstrates the value that would be used to ignore // each of the fields on the struct. // // type Request struct { // URL string // "URL" // Meta struct { // "Meta" // Label string // "Meta.Label" // } // } IgnoreFields []string // New is a function that returns a value of the struct type used by Operation. // If New is nil, the zero value of T will be used. // // Every call to New must return a full copy of the struct, with the same values. // If New returns a shallow copy, or returns different values the assertion // will fail. Generally New will return a struct literal populated // with hardcoded values. // // If the operation being tested is Empty (IsZero, IsEmpty, etc.) then New // should return the zero value of the struct. For other operations New // can set the fields to any value. // // The value returned by New will be used in two ways: // // * the first value returned by New will be used as the first argument // in every call to Operation. // * the value returned by other calls to New will be modified and used as // the second argument to Operation. // New func() T } // Complete tests that opt.Operation uses all the fields of struct T. See // CompleteOptions for details about how to use Complete. // // Common operations that can be tested using Complete include: // // - equal // - empty or isZero // - round tripping between two transformations // - building a hash from struct fields or map key for a struct // // By default the random values use a seed of time.Now(). To reproduce a failure // that only occurs with a specific seed, set the TEST_SEED environment // variable to the seed number. func Complete[T any](t TestingT, opt CompleteOptions[T]) { t.Helper() seed := time.Now().UnixNano() if v, ok := os.LookupEnv("TEST_SEED"); ok { seed, _ = strconv.ParseInt(v, 10, 64) } t.Log("using random seed ", seed) newT := func() T { if opt.New == nil { return *new(T) } return opt.New() } orig := newT() cfg := config[T]{ testingT: t, rand: rand.New(rand.NewSource(seed)), newT: newT, op: func(modified T) bool { return opt.Operation(orig, modified) }, ignored: make(map[string]struct{}, len(opt.IgnoreFields)), } for _, k := range opt.IgnoreFields { cfg.ignored[k] = struct{}{} } pos := position{ structType: reflect.TypeOf(orig), getReflectValue: func(emptyT reflect.Value) reflect.Value { return emptyT }, } traverseStruct(cfg, pos) } // TestingT is the subset of testing.T used by functions in this package. type TestingT interface { Log(args ...interface{}) Fatalf(format string, args ...interface{}) Helper() } // config is the internal version of CompleteOptions that is used by traverseStruct type config[T any] struct { newT func() T op func(modified T) bool ignored map[string]struct{} rand *rand.Rand testingT TestingT } // position identifies the position in struct traversal. type position struct { // path is the string representation of the position, used to compare to // keys in config.ignored, and as part of the failure message. path string // structType is the reflect.Type of struct at this position. The type is // used to lookup fields of the struct. structType reflect.Type // getReflectValue is a function that receives a fresh copy of config.T from // config.newT, that is about to be modified. It returns the reflect.Value // for the field at this position. It will receive a random value and passed // as the argument to config.op. getReflectValue func(emptyT reflect.Value) reflect.Value } func traverseStruct[T any](cfg config[T], pos position) { cfg.testingT.Helper() for i := 0; i < pos.structType.NumField(); i++ { fieldType := pos.structType.Field(i) fieldPath := pos.path + fieldType.Name if _, ok := cfg.ignored[fieldPath]; ok { continue } modified := cfg.newT() field := pos.getReflectValue(reflect.ValueOf(&modified).Elem()).Field(i) nextPos := position{ path: fieldPath + ".", getReflectValue: func(emptyT reflect.Value) reflect.Value { return pos.getReflectValue(emptyT).Field(i) }, } switch f := reflect.Indirect(field); f.Kind() { case reflect.Struct: // TODO: limit traversal depth to prevent infinite recursion nextPos.structType = field.Type() traverseStruct(cfg, nextPos) default: fillValue(cfg, nextPos, field) if !cfg.op(modified) { cfg.testingT.Fatalf("not complete: field %v is not included", fieldPath) } } } } func fillValue[T any](cfg config[T], pos position, v reflect.Value) { cfg.testingT.Helper() if v.Kind() == reflect.Pointer { v.Set(reflect.New(v.Type().Elem())) v = v.Elem() } if !v.CanSet() || v.Kind() == reflect.Invalid { panic(fmt.Sprintf("%v is not settable", v)) } switch v.Kind() { case reflect.Bool: v.SetBool(!v.Bool()) case reflect.String: v.SetString(randString(cfg.rand)) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: for orig := v.Int(); orig == v.Int(); { v.SetInt(cfg.rand.Int63()) } case reflect.Float32, reflect.Float64: for orig := v.Float(); orig == v.Float(); { v.SetFloat(cfg.rand.Float64()) } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: for orig := v.Uint(); orig == v.Uint(); { v.SetUint(cfg.rand.Uint64()) } case reflect.Complex64, reflect.Complex128: for orig := v.Complex(); orig == v.Complex(); { v.SetComplex(complex(cfg.rand.Float64(), cfg.rand.Float64())) } case reflect.Slice: if v.Cap() == 0 || v.Len() == 0 { v.Set(reflect.MakeSlice(v.Type(), 1, 1)) } nextPos := pos nextPos.getReflectValue = func(emptyT reflect.Value) reflect.Value { // TODO: any way to avoid this duplication with above? s := pos.getReflectValue(emptyT) s.Set(reflect.MakeSlice(s.Type(), 1, 1)) return s.Index(0) } fillValue(cfg, nextPos, v.Index(0)) case reflect.Array: if v.Cap() > 0 { nextPos := pos nextPos.getReflectValue = func(emptyT reflect.Value) reflect.Value { return pos.getReflectValue(emptyT).Index(0) } fillValue(cfg, nextPos, v.Index(0)) } case reflect.Struct: nextPos := pos nextPos.structType = v.Type() traverseStruct(cfg, nextPos) default: panic(fmt.Sprintf("kind %v is not supported, maybe use IgnoreFields", v.Kind())) } } var chars = []rune("你好欢迎abcdefghistuvwxyzABCDEFGHIJKLMNOμεταβλητόςпеременная") func randString(rand *rand.Rand) string { length := rand.Intn(20) + 5 var out strings.Builder for i := 0; i <= length; i++ { out.WriteRune(chars[rand.Intn(len(chars))]) } return out.String() } gotest.tools-3.5.1/x/generics/property/complete_example_test.go000066400000000000000000000023071463245364400250440ustar00rootroot00000000000000package property_test import ( "testing" "gotest.tools/x/generics/property" ) type FoodRequest struct { Kind string Quantity int } func (f FoodRequest) Equal(o FoodRequest) bool { return f.Kind == o.Kind && f.Quantity == o.Quantity } func (f FoodRequest) IsZero() bool { return f.Equal(FoodRequest{}) } func ExampleComplete() { var t *testing.T // for example only // This subtest will fail if someone adds a new field to FoodRequest // and forgets to change the Equal method to include the field. t.Run("Equal is complete", func(t *testing.T) { property.Complete(t, property.CompleteOptions[FoodRequest]{ Operation: func(x, y FoodRequest) bool { // when any field is changed, the two values should not be equal return !x.Equal(y) }, }) }) // This subtest will fail if someone adds a new field to FoodRequest // and forgets to change the IsZero method to include the field. t.Run("IsZero is complete", func(t *testing.T) { property.Complete(t, property.CompleteOptions[FoodRequest]{ Operation: func(_, modified FoodRequest) bool { // when any field is changed, the modified value should no longer // be the zero value. return !modified.IsZero() }, }) }) } gotest.tools-3.5.1/x/generics/property/complete_test.go000066400000000000000000000161331463245364400233330ustar00rootroot00000000000000package property import ( "bytes" "crypto/sha256" "fmt" "reflect" "strconv" "testing" "gotest.tools/v3/assert" ) type exampleComplete struct { Field string Flag bool Count int32 Maybe *string Array [2]int Seq []byte Assoc map[int]string } func (s exampleComplete) Equal(o exampleComplete) bool { if s.Array != o.Array { return false } if !bytes.Equal(s.Seq, o.Seq) { return false } if !reflect.DeepEqual(s.Assoc, o.Assoc) { return false } return s.Field == o.Field && s.Flag == o.Flag && s.Count == o.Count && s.Maybe == o.Maybe } func (s exampleComplete) Empty() bool { return s.Equal(exampleComplete{}) } func (s exampleComplete) Key() []byte { h := sha256.New() h.Write([]byte(s.Field)) h.Write([]byte(strconv.Itoa(int(s.Count)))) h.Write([]byte(strconv.FormatBool(s.Flag))) if s.Maybe != nil { h.Write([]byte(*s.Maybe)) } for _, item := range s.Array { h.Write([]byte(strconv.Itoa(item))) } h.Write(s.Seq) for k, v := range s.Assoc { h.Write([]byte(strconv.Itoa(k))) h.Write([]byte(v)) } return h.Sum(nil) } type exampleIncomplete struct { Field string Flag bool Count int32 } func (s exampleIncomplete) Equal(o exampleIncomplete) bool { return s.Field == o.Field && s.Flag == o.Flag } func (s exampleIncomplete) Empty() bool { return s.Count == 0 && s.Field == "" } func (s exampleIncomplete) Key() []byte { h := sha256.New() h.Write([]byte(strconv.Itoa(int(s.Count)))) h.Write([]byte(strconv.FormatBool(s.Flag))) return h.Sum(nil) } func TestComplete_WithEqual(t *testing.T) { t.Run("complete", func(t *testing.T) { in := CompleteOptions[exampleComplete]{ New: func() exampleComplete { return exampleComplete{ Field: "field-one", Flag: true, Count: 3, Array: [2]int{1, 2}, } }, Operation: func(x, y exampleComplete) bool { return !x.Equal(y) }, IgnoreFields: []string{"Assoc"}, } for i := 0; i < 200; i++ { Complete(t, in) } }) t.Run("incomplete", func(t *testing.T) { in := CompleteOptions[exampleIncomplete]{ New: func() exampleIncomplete { return exampleIncomplete{ Field: "field-one", Flag: true, Count: 3, } }, Operation: func(x, y exampleIncomplete) bool { return !x.Equal(y) }, } fakeT := &fakeTestingT{} Complete(fakeT, in) expectCompleteFailure(t, fakeT, "not complete: field Count is not included") }) t.Run("complete with ignore fields", func(t *testing.T) { in := CompleteOptions[exampleIncomplete]{ New: func() exampleIncomplete { return exampleIncomplete{ Field: "field-one", Flag: true, Count: 3, } }, Operation: func(x, y exampleIncomplete) bool { return !x.Equal(y) }, IgnoreFields: []string{"Count"}, } for i := 0; i < 200; i++ { Complete(t, in) } }) t.Run("complete with default new", func(t *testing.T) { in := CompleteOptions[exampleComplete]{ Operation: func(x, y exampleComplete) bool { return !x.Equal(y) }, IgnoreFields: []string{"Count", "Assoc"}, } for i := 0; i < 200; i++ { Complete(t, in) } }) t.Run("incomplete with default new", func(t *testing.T) { in := CompleteOptions[exampleIncomplete]{ Operation: func(x, y exampleIncomplete) bool { return !x.Equal(y) }, } fakeT := &fakeTestingT{} Complete(fakeT, in) expectCompleteFailure(t, fakeT, "not complete: field Count is not included") }) } func expectCompleteFailure(t *testing.T, fakeT *fakeTestingT, expected string) { t.Helper() assert.Assert(t, fakeT.failed, "should have failed") if len(fakeT.msgs) < 2 { t.Fatalf("expected at least 2 log messages: %v", fakeT.msgs) } assert.Equal(t, fakeT.msgs[1], expected, "wrong failure message") } func TestComplete_WithKey(t *testing.T) { t.Run("complete", func(t *testing.T) { in := CompleteOptions[exampleComplete]{ New: func() exampleComplete { return exampleComplete{ Field: "field-one", Flag: true, Count: 3, } }, Operation: func(x, y exampleComplete) bool { return !bytes.Equal(x.Key(), y.Key()) }, IgnoreFields: []string{"Assoc"}, } for i := 0; i < 200; i++ { Complete(t, in) } }) t.Run("incomplete", func(t *testing.T) { in := CompleteOptions[exampleIncomplete]{ New: func() exampleIncomplete { return exampleIncomplete{ Field: "field-one", Flag: true, Count: 3, } }, Operation: func(x, y exampleIncomplete) bool { return !bytes.Equal(x.Key(), y.Key()) }, } fakeT := &fakeTestingT{} Complete(fakeT, in) expectCompleteFailure(t, fakeT, "not complete: field Field is not included") }) } func TestComplete_WithEmpty(t *testing.T) { t.Run("complete", func(t *testing.T) { in := CompleteOptions[exampleComplete]{ New: func() exampleComplete { return exampleComplete{} }, Operation: func(_, x exampleComplete) bool { return !x.Empty() }, IgnoreFields: []string{"Assoc"}, } for i := 0; i < 200; i++ { Complete(t, in) } }) t.Run("incomplete", func(t *testing.T) { in := CompleteOptions[exampleIncomplete]{ New: func() exampleIncomplete { return exampleIncomplete{} }, Operation: func(_, x exampleIncomplete) bool { return !x.Empty() }, } fakeT := &fakeTestingT{} Complete(fakeT, in) expectCompleteFailure(t, fakeT, "not complete: field Flag is not included") }) } type exampleNested struct { Sub exampleIncomplete Top int8 //Assoc map[exampleKey]exampleValue Seq []exampleValue exampleValue } func (s exampleNested) Equal(o exampleNested) bool { if s.Top != o.Top || !s.Sub.Equal(o.Sub) { return false } for i := range s.Seq { if len(o.Seq) <= i || s.Seq[i] != o.Seq[i] { return false } } if len(s.Seq) != len(o.Seq) { return false } return s.exampleValue.One == o.exampleValue.One } type exampleValue struct { One int Ok bool } func TestComplete_Nested(t *testing.T) { t.Run("incomplete", func(t *testing.T) { in := CompleteOptions[exampleNested]{ New: func() exampleNested { return exampleNested{ Sub: exampleIncomplete{ Field: "what", Flag: true, Count: 23, }, Top: 12, } }, Operation: func(x, y exampleNested) bool { return !x.Equal(y) }, } fakeT := &fakeTestingT{} Complete(fakeT, in) expectCompleteFailure(t, fakeT, "not complete: field Sub.Count is not included") }) t.Run("complete with ignore fields", func(t *testing.T) { in := CompleteOptions[exampleNested]{ New: func() exampleNested { return exampleNested{ Sub: exampleIncomplete{ Field: "what", Flag: true, Count: 23, }, Top: 12, } }, Operation: func(x, y exampleNested) bool { return !x.Equal(y) }, IgnoreFields: []string{"Sub.Count", "exampleValue.Ok"}, } for i := 0; i < 200; i++ { Complete(t, in) } }) // TODO: more test cases for nested (field as pointer to struct) } type fakeTestingT struct { failed bool msgs []string } func (f *fakeTestingT) Log(args ...interface{}) { f.msgs = append(f.msgs, fmt.Sprint(args...)) } func (f *fakeTestingT) Helper() {} func (f *fakeTestingT) Fatalf(format string, args ...interface{}) { f.failed = true f.msgs = append(f.msgs, fmt.Sprintf(format, args...)) } gotest.tools-3.5.1/x/generics/property/doc.go000066400000000000000000000001641463245364400212260ustar00rootroot00000000000000/* Package property provides experimental property assertions for writing property based tests. */ package property gotest.tools-3.5.1/x/subtest/000077500000000000000000000000001463245364400161375ustar00rootroot00000000000000gotest.tools-3.5.1/x/subtest/context.go000066400000000000000000000047611463245364400201620ustar00rootroot00000000000000/* Package subtest provides a TestContext to subtests which handles cleanup, and provides a testing.TB, and context.Context. This package was inspired by github.com/frankban/quicktest. # DEPRECATED With the addition of T.Cleanup() in go1.14 this package provides very little value. A context.Context can be managed by tests that need it with little enough boilerplate that it doesn't make sense to wrap testing.T in a TestContext. */ package subtest // import "gotest.tools/v3/x/subtest" import ( "context" "testing" "gotest.tools/v3/internal/cleanup" ) type testcase struct { testing.TB ctx context.Context cleanupFuncs []cleanupFunc } type cleanupFunc func() func (tc *testcase) Ctx() context.Context { if tc.ctx == nil { var cancel func() tc.ctx, cancel = context.WithCancel(context.Background()) cleanup.Cleanup(tc, cancel) } return tc.ctx } // cleanup runs all cleanup functions. Functions are run in the opposite order // in which they were added. Cleanup is called automatically before Run exits. func (tc *testcase) cleanup() { for _, f := range tc.cleanupFuncs { // Defer all cleanup functions so they all run even if one calls // t.FailNow() or panics. Deferring them also runs them in reverse order. defer f() } tc.cleanupFuncs = nil } func (tc *testcase) AddCleanup(f func()) { tc.cleanupFuncs = append(tc.cleanupFuncs, f) } func (tc *testcase) Parallel() { tp, ok := tc.TB.(parallel) if !ok { panic("Parallel called with a testing.B") } tp.Parallel() } type parallel interface { Parallel() } // Run a subtest. When subtest exits, every cleanup function added with // TestContext.AddCleanup will be run. func Run(t *testing.T, name string, subtest func(t TestContext)) bool { return t.Run(name, func(t *testing.T) { tc := &testcase{TB: t} defer tc.cleanup() subtest(tc) }) } // TestContext provides a testing.TB and a context.Context for a test case. type TestContext interface { testing.TB // AddCleanup function which will be run when before Run returns. // // Deprecated: Go 1.14+ now includes a testing.TB.Cleanup(func()) which // should be used instead. AddCleanup will be removed in a future release. AddCleanup(f func()) // Ctx returns a context for the test case. Multiple calls from the same subtest // will return the same context. The context is cancelled when Run // returns. Ctx() context.Context // Parallel calls t.Parallel on the testing.TB. Panics if testing.TB does // not implement Parallel. Parallel() } var _ TestContext = &testcase{} gotest.tools-3.5.1/x/subtest/context_test.go000066400000000000000000000011561463245364400212140ustar00rootroot00000000000000package subtest import ( "context" "testing" "gotest.tools/v3/assert" ) func TestTestcase_Run_CallsCleanup(t *testing.T) { calls := []int{} var ctx context.Context Run(t, "test-run-cleanup", func(t TestContext) { cleanup := func(n int) func() { return func() { calls = append(calls, n) } } ctx = t.Ctx() t.AddCleanup(cleanup(2)) t.AddCleanup(cleanup(1)) t.AddCleanup(cleanup(0)) }) assert.DeepEqual(t, calls, []int{0, 1, 2}) assert.Equal(t, ctx.Err(), context.Canceled) } func TestTestcase_Run_Parallel(t *testing.T) { Run(t, "test-parallel", func(t TestContext) { t.Parallel() }) } gotest.tools-3.5.1/x/subtest/example_test.go000066400000000000000000000023751463245364400211670ustar00rootroot00000000000000package subtest_test import ( "io" "net/http" "strings" "testing" "gotest.tools/v3/assert" "gotest.tools/v3/x/subtest" ) var t = &testing.T{} func ExampleRun_tableTest() { var testcases = []struct { data io.Reader expected int }{ { data: strings.NewReader("invalid input"), expected: 400, }, { data: strings.NewReader("valid input"), expected: 200, }, } for _, tc := range testcases { subtest.Run(t, "test-service-call", func(t subtest.TestContext) { // startFakeService can shutdown using t.AddCleanup url := startFakeService(t) req, err := http.NewRequest("POST", url, tc.data) assert.NilError(t, err) req = req.WithContext(t.Ctx()) client := newClient(t) resp, err := client.Do(req) assert.NilError(t, err) assert.Equal(t, resp.StatusCode, tc.expected) }) } } func startFakeService(t subtest.TestContext) string { return "url" } func newClient(_ subtest.TestContext) *http.Client { return &http.Client{} } func ExampleRun_testSuite() { // do suite setup before subtests subtest.Run(t, "test-one", func(t subtest.TestContext) { assert.Equal(t, 1, 1) }) subtest.Run(t, "test-two", func(t subtest.TestContext) { assert.Equal(t, 2, 2) }) // do suite teardown after subtests }