diff --git a/README.md b/README.md index f815716c..9e51e8b0 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,6 @@ module paths pin to version `v2.3.0`. 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 diff --git a/assert/assert.go b/assert/assert.go index 133e87e1..b1973ebc 100644 --- a/assert/assert.go +++ b/assert/assert.go @@ -88,7 +88,8 @@ import ( "gotest.tools/v3/internal/assert" ) -// BoolOrComparison can be a bool, or cmp.Comparison. See Assert() for usage. +// 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 used by the assert package. @@ -120,6 +121,11 @@ type helperT interface { // 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 t.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. diff --git a/assert/skip.go b/assert/skip.go new file mode 100644 index 00000000..65807ab2 --- /dev/null +++ b/assert/skip.go @@ -0,0 +1,102 @@ +package assert + +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{}) +} + +// SkipResult may be returned by a function used with SkipIf to provide a +// detailed message to use as part of the skip message. +type SkipResult interface { + Skip() bool + Message() string +} + +// BoolOrCheckFunc can be a bool, func() bool, or func() SkipResult. Other +// types will panic. See SkipIf for details about how this type is used. +type BoolOrCheckFunc interface{} + +// SkipIf skips the test if the condition evaluates to true. If the condition +// evaluates to false then SkipIf does nothing. SkipIf is a convenient way of +// skipping tests and using the literal source of the condition as the text of +// the skip message. +// +// For example, this usage would produce the following skip message: +// +// assert.SkipIf(t, runtime.GOOS == "windows", "not supported") +// // filename.go:11: runtime.GOOS == "windows": not supported +// +// The condition argument may be one of the following: +// +// bool +// The test will be skipped if the value is true. The literal source of the +// expression passed to SkipIf will be used as the skip message. +// +// func() bool +// The test will be skipped if the function returns true. The name of the +// function will be used as the skip message. +// +// func() SkipResult +// The test will be skipped if SkipResult.Skip return true. Both the name +// of the function and the return value of SkipResult.Message will be used +// as the skip message. +// +// Extra details can be added to the skip message using msgAndArgs. msgAndArgs +// may be either a single string, or a format string and args that will be +// passed to fmt.Sprintf. +func SkipIf(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() SkipResult: + 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...)) +} diff --git a/assert/skip_example_test.go b/assert/skip_example_test.go new file mode 100644 index 00000000..65eff9d6 --- /dev/null +++ b/assert/skip_example_test.go @@ -0,0 +1,39 @@ +package assert_test + +import ( + "gotest.tools/v3/assert" +) + +var apiVersion = "" + +type env struct{} + +func (e env) hasFeature(_ string) bool { return false } + +var testEnv = env{} + +func MissingFeature() bool { return false } + +func ExampleSkipIf() { + assert.SkipIf(t, MissingFeature) + // --- SKIP: TestName (0.00s) + // skip.go:18: MissingFeature + + assert.SkipIf(t, MissingFeature, "coming soon") + // --- SKIP: TestName (0.00s) + // skip.go:22: MissingFeature: coming soon +} + +func ExampleSkipIf_withExpression() { + assert.SkipIf(t, apiVersion < version("v1.24")) + // --- SKIP: TestName (0.00s) + // skip.go:28: apiVersion < version("v1.24") + + assert.SkipIf(t, !testEnv.hasFeature("build"), "coming soon") + // --- SKIP: TestName (0.00s) + // skip.go:32: !textenv.hasFeature("build"): coming soon +} + +func version(v string) string { + return v +} diff --git a/assert/skip_test.go b/assert/skip_test.go new file mode 100644 index 00000000..5f792c0a --- /dev/null +++ b/assert/skip_test.go @@ -0,0 +1,135 @@ +package assert + +import ( + "bytes" + "fmt" + "testing" + + "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 TestSkipIFCondition(t *testing.T) { + skipT := &fakeSkipT{} + apiVersion := "v1.4" + SkipIf(skipT, apiVersion < version("v1.6")) + + Equal(t, `apiVersion < version("v1.6")`, skipT.reason) + Assert(t, cmp.Len(skipT.logs, 0)) +} + +func TestSkipIfConditionWithMessage(t *testing.T) { + skipT := &fakeSkipT{} + apiVersion := "v1.4" + SkipIf(skipT, apiVersion < "v1.6", "see notes") + + Equal(t, `apiVersion < "v1.6": see notes`, skipT.reason) + Assert(t, cmp.Len(skipT.logs, 0)) +} + +func TestSkipIfConditionMultiline(t *testing.T) { + skipT := &fakeSkipT{} + apiVersion := "v1.4" + SkipIf( + skipT, + apiVersion < "v1.6") + + Equal(t, `apiVersion < "v1.6"`, skipT.reason) + Assert(t, cmp.Len(skipT.logs, 0)) +} + +func TestSkipIfConditionMultilineWithMessage(t *testing.T) { + skipT := &fakeSkipT{} + apiVersion := "v1.4" + SkipIf( + skipT, + apiVersion < "v1.6", + "see notes") + + Equal(t, `apiVersion < "v1.6": see notes`, skipT.reason) + Assert(t, cmp.Len(skipT.logs, 0)) +} + +func TestSkipIfConditionNoSkip(t *testing.T) { + skipT := &fakeSkipT{} + SkipIf(skipT, false) + + Equal(t, "", skipT.reason) + Assert(t, cmp.Len(skipT.logs, 0)) +} + +func SkipBecauseISaidSo() bool { + return true +} + +func TestSkipIf(t *testing.T) { + skipT := &fakeSkipT{} + SkipIf(skipT, SkipBecauseISaidSo) + + Equal(t, "SkipBecauseISaidSo", skipT.reason) +} + +func TestSkipIfWithMessage(t *testing.T) { + skipT := &fakeSkipT{} + SkipIf(skipT, SkipBecauseISaidSo, "see notes") + + Equal(t, "SkipBecauseISaidSo: see notes", skipT.reason) +} + +func TestSkipIf_InvalidCondition(t *testing.T) { + skipT := &fakeSkipT{} + Assert(t, cmp.Panics(func() { + SkipIf(skipT, "just a string") + })) +} + +func TestSkipIfWithSkipResultFunc(t *testing.T) { + t.Run("no extra message", func(t *testing.T) { + skipT := &fakeSkipT{} + SkipIf(skipT, alwaysSkipWithMessage) + + Equal(t, "alwaysSkipWithMessage: skip because I said so!", skipT.reason) + }) + t.Run("with extra message", func(t *testing.T) { + skipT := &fakeSkipT{} + SkipIf(skipT, alwaysSkipWithMessage, "also %v", 4) + + Equal(t, "alwaysSkipWithMessage: skip because I said so!: also 4", skipT.reason) + }) +} + +func alwaysSkipWithMessage() SkipResult { + return skipResult{} +} + +type skipResult struct{} + +func (s skipResult) Skip() bool { + return true +} + +func (s skipResult) Message() string { + return "skip because I said so!" +} diff --git a/env/env_test.go b/env/env_test.go index 54458f20..13b4ead6 100644 --- a/env/env_test.go +++ b/env/env_test.go @@ -9,7 +9,6 @@ import ( "gotest.tools/v3/assert" "gotest.tools/v3/fs" "gotest.tools/v3/internal/source" - "gotest.tools/v3/skip" ) func TestPatchFromUnset(t *testing.T) { @@ -23,7 +22,7 @@ func TestPatchFromUnset(t *testing.T) { } func TestPatch(t *testing.T) { - skip.If(t, os.Getenv("PATH") == "") + assert.SkipIf(t, os.Getenv("PATH") == "") oldVal := os.Getenv("PATH") key, value := "PATH", "NEWVALUE" @@ -35,7 +34,7 @@ func TestPatch(t *testing.T) { } func TestPatch_IntegrationWithCleanup(t *testing.T) { - skip.If(t, source.GoVersionLessThan(1, 14)) + assert.SkipIf(t, source.GoVersionLessThan(1, 14)) key := "totally_unique_env_var_key" t.Run("cleanup in subtest", func(t *testing.T) { @@ -67,7 +66,7 @@ func TestPatchAll(t *testing.T) { } func TestPatchAllWindows(t *testing.T) { - skip.If(t, runtime.GOOS != "windows") + assert.SkipIf(t, runtime.GOOS != "windows") oldEnv := os.Environ() newEnv := map[string]string{ "FIRST": "STARS", @@ -92,7 +91,7 @@ func sorted(source []string) []string { } func TestPatchAll_IntegrationWithCleanup(t *testing.T) { - skip.If(t, source.GoVersionLessThan(1, 14)) + assert.SkipIf(t, source.GoVersionLessThan(1, 14)) key := "totally_unique_env_var_key" t.Run("cleanup in subtest", func(t *testing.T) { @@ -145,7 +144,7 @@ func TestChangeWorkingDir(t *testing.T) { } func TestChangeWorkingDir_IntegrationWithCleanup(t *testing.T) { - skip.If(t, source.GoVersionLessThan(1, 14)) + assert.SkipIf(t, source.GoVersionLessThan(1, 14)) tmpDir := fs.NewDir(t, t.Name()) defer tmpDir.Remove() diff --git a/fs/file_test.go b/fs/file_test.go index 5e8be4da..fdc0ee28 100644 --- a/fs/file_test.go +++ b/fs/file_test.go @@ -10,7 +10,6 @@ import ( "gotest.tools/v3/assert" "gotest.tools/v3/fs" "gotest.tools/v3/internal/source" - "gotest.tools/v3/skip" ) func TestNewDirWithOpsAndManifestEqual(t *testing.T) { @@ -67,7 +66,7 @@ func TestNewFile(t *testing.T) { } func TestNewFile_IntegrationWithCleanup(t *testing.T) { - skip.If(t, source.GoVersionLessThan(1, 14)) + assert.SkipIf(t, source.GoVersionLessThan(1, 14)) var tmpFile *fs.File t.Run("cleanup in subtest", func(t *testing.T) { tmpFile = fs.NewFile(t, t.Name()) @@ -82,7 +81,7 @@ func TestNewFile_IntegrationWithCleanup(t *testing.T) { } func TestNewDir_IntegrationWithCleanup(t *testing.T) { - skip.If(t, source.GoVersionLessThan(1, 14)) + assert.SkipIf(t, source.GoVersionLessThan(1, 14)) var tmpFile *fs.Dir t.Run("cleanup in subtest", func(t *testing.T) { tmpFile = fs.NewDir(t, t.Name()) diff --git a/fs/report_test.go b/fs/report_test.go index 389d6085..c085f14b 100644 --- a/fs/report_test.go +++ b/fs/report_test.go @@ -8,7 +8,6 @@ import ( "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" - "gotest.tools/v3/skip" ) func TestEqualMissingRoot(t *testing.T) { @@ -224,7 +223,7 @@ func TestMatchExtraFilesGlob(t *testing.T) { }) t.Run("matching globs with wrong mode", func(t *testing.T) { - skip.If(t, runtime.GOOS == "windows", "expect mode does not match on windows") + assert.SkipIf(t, runtime.GOOS == "windows", "expect mode does not match on windows") manifest := Expected(t, MatchFilesWithGlob("*.go", MatchAnyFileMode, MatchAnyFileContent), MatchFilesWithGlob("*.yml", MatchAnyFileContent, WithMode(0700))) diff --git a/internal/source/source_test.go b/internal/source/source_test.go index 3c218194..939da572 100644 --- a/internal/source/source_test.go +++ b/internal/source/source_test.go @@ -11,7 +11,6 @@ import ( "gotest.tools/v3/assert" "gotest.tools/v3/internal/source" - "gotest.tools/v3/skip" ) func TestFormattedCallExprArg_SingleLine(t *testing.T) { @@ -46,7 +45,7 @@ func shim(_, _, _ string) (string, error) { } func TestFormattedCallExprArg_InDefer(t *testing.T) { - skip.If(t, isGoVersion18) + assert.SkipIf(t, isGoVersion18) cap := &capture{} func() { defer cap.shim("first", "second") @@ -82,7 +81,7 @@ func TestFormattedCallExprArg_InAnonymousDefer(t *testing.T) { } func TestFormattedCallExprArg_InDeferMultipleDefers(t *testing.T) { - skip.If(t, isGoVersion18) + assert.SkipIf(t, isGoVersion18) cap := &capture{} func() { fmt.Println() diff --git a/skip/example_test.go b/skip/example_test.go deleted file mode 100644 index 31c2148e..00000000 --- a/skip/example_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package 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 -} diff --git a/skip/skip.go b/skip/skip.go index cb899f78..f2a59a52 100644 --- a/skip/skip.go +++ b/skip/skip.go @@ -1,5 +1,6 @@ -/*Package skip provides functions for skipping a test and printing the source code -of the condition used to skip the test. +/*Package skip is deprecated. + +Deprecated: use assert.SkipIf */ package skip // import "gotest.tools/v3/skip" @@ -34,14 +35,7 @@ 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. +// Deprecated: use assert.SkipIf func If(t skipT, condition BoolOrCheckFunc, msgAndArgs ...interface{}) { if ht, ok := t.(helperT); ok { ht.Helper()