diff --git a/CHANGELOG.md b/CHANGELOG.md index d340d02c..e819dd11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt ## Unreleased +### Added +- Provide testing.T-compatible interface on test context, allowing usage of assertion libraries such as testify's assert/require - ([571](https://github.com/cucumber/godog/pull/571) - [mrsheepuk](https://github.com/mrsheepuk)) + ## [v0.14.0] ### Added - Improve ErrSkip handling, add test for Summary and operations order ([584](https://github.com/cucumber/godog/pull/584) - [vearutop](https://github.com/vearutop)) diff --git a/README.md b/README.md index ba78d34e..1bbf2087 100644 --- a/README.md +++ b/README.md @@ -495,31 +495,12 @@ If you want to filter scenarios by tags, you can use the `-t=` or `- A more extensive example can be [found here](/_examples/assert-godogs). ```go -func thereShouldBeRemaining(remaining int) error { - return assertExpectedAndActual( - assert.Equal, Godogs, remaining, - "Expected %d godogs to be remaining, but there is %d", remaining, Godogs, - ) -} - -// assertExpectedAndActual is a helper function to allow the step function to call -// assertion functions where you want to compare an expected and an actual value. -func assertExpectedAndActual(a expectedAndActualAssertion, expected, actual interface{}, msgAndArgs ...interface{}) error { - var t asserter - a(&t, expected, actual, msgAndArgs...) - return t.err -} - -type expectedAndActualAssertion func(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool - -// asserter is used to be able to retrieve the error reported by the called assertion -type asserter struct { - err error -} - -// Errorf is used by the called assertion to report an error -func (a *asserter) Errorf(format string, args ...interface{}) { - a.err = fmt.Errorf(format, args...) +func thereShouldBeRemaining(ctx context.Context, remaining int) error { + assert.Equal( + godog.T(ctx), Godogs, remaining, + "Expected %d godogs to be remaining, but there is %d", remaining, Godogs, + ) + return nil } ``` diff --git a/_examples/assert-godogs/godogs_test.go b/_examples/assert-godogs/godogs_test.go index 180339c8..6aec222a 100644 --- a/_examples/assert-godogs/godogs_test.go +++ b/_examples/assert-godogs/godogs_test.go @@ -2,7 +2,6 @@ package main import ( "context" - "fmt" "os" "testing" @@ -36,31 +35,22 @@ func thereAreGodogs(available int) error { return nil } -func iEat(num int) error { - err := assertExpectedAndActual( - assert.GreaterOrEqual, Godogs, num, - "You cannot eat %d godogs, there are %d available", num, Godogs, - ) - if err != nil { - return err +func iEat(ctx context.Context, num int) error { + if !assert.GreaterOrEqual(godog.T(ctx), Godogs, num, "You cannot eat %d godogs, there are %d available", num, Godogs) { + return nil } - Godogs -= num return nil } -func thereShouldBeRemaining(remaining int) error { - return assertExpectedAndActual( - assert.Equal, Godogs, remaining, - "Expected %d godogs to be remaining, but there is %d", remaining, Godogs, - ) +func thereShouldBeRemaining(ctx context.Context, remaining int) error { + assert.Equal(godog.T(ctx), Godogs, remaining, "Expected %d godogs to be remaining, but there is %d", remaining, Godogs) + return nil } -func thereShouldBeNoneRemaining() error { - return assertActual( - assert.Empty, Godogs, - "Expected none godogs to be remaining, but there is %d", Godogs, - ) +func thereShouldBeNoneRemaining(ctx context.Context) error { + assert.Empty(godog.T(ctx), Godogs, "Expected none godogs to be remaining, but there is %d", Godogs) + return nil } func InitializeScenario(ctx *godog.ScenarioContext) { @@ -74,34 +64,3 @@ func InitializeScenario(ctx *godog.ScenarioContext) { ctx.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining) ctx.Step(`^there should be none remaining$`, thereShouldBeNoneRemaining) } - -// assertExpectedAndActual is a helper function to allow the step function to call -// assertion functions where you want to compare an expected and an actual value. -func assertExpectedAndActual(a expectedAndActualAssertion, expected, actual interface{}, msgAndArgs ...interface{}) error { - var t asserter - a(&t, expected, actual, msgAndArgs...) - return t.err -} - -type expectedAndActualAssertion func(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool - -// assertActual is a helper function to allow the step function to call -// assertion functions where you want to compare an actual value to a -// predined state like nil, empty or true/false. -func assertActual(a actualAssertion, actual interface{}, msgAndArgs ...interface{}) error { - var t asserter - a(&t, actual, msgAndArgs...) - return t.err -} - -type actualAssertion func(t assert.TestingT, actual interface{}, msgAndArgs ...interface{}) bool - -// asserter is used to be able to retrieve the error reported by the called assertion -type asserter struct { - err error -} - -// Errorf is used by the called assertion to report an error -func (a *asserter) Errorf(format string, args ...interface{}) { - a.err = fmt.Errorf(format, args...) -} diff --git a/features/formatter/events.feature b/features/formatter/events.feature index b33554cc..6ccc257a 100644 --- a/features/formatter/events.feature +++ b/features/formatter/events.feature @@ -13,7 +13,7 @@ Feature: event stream formatter """ Scenario: should process simple scenario - Given a feature path "features/load.feature:26" + Given a feature path "features/load.feature:27" When I run feature suite with formatter "events" Then the following events should be fired: """ @@ -34,7 +34,7 @@ Feature: event stream formatter """ Scenario: should process outline scenario - Given a feature path "features/load.feature:34" + Given a feature path "features/load.feature:35" When I run feature suite with formatter "events" Then the following events should be fired: """ diff --git a/features/formatter/pretty.feature b/features/formatter/pretty.feature index 197c1f50..81cf7b1d 100644 --- a/features/formatter/pretty.feature +++ b/features/formatter/pretty.feature @@ -350,7 +350,7 @@ Feature: pretty formatter Scenario: Should scenarios identified with path:line and preserve the order. Given a feature path "features/load.feature:6" And a feature path "features/multistep.feature:6" - And a feature path "features/load.feature:26" + And a feature path "features/load.feature:27" And a feature path "features/multistep.feature:23" When I run feature suite with formatter "pretty" Then the rendered output will be as follows: @@ -363,7 +363,7 @@ Feature: pretty formatter Scenario: load features within path # features/load.feature:6 Given a feature path "features" # suite_context_test.go:0 -> *godogFeaturesScenario When I parse features # suite_context_test.go:0 -> *godogFeaturesScenario - Then I should have 13 feature files: # suite_context_test.go:0 -> *godogFeaturesScenario + Then I should have 14 feature files: # suite_context_test.go:0 -> *godogFeaturesScenario \"\"\" features/background.feature features/events.feature @@ -378,6 +378,7 @@ Feature: pretty formatter features/run.feature features/snippets.feature features/tags.feature + features/testingt.feature \"\"\" Feature: run features with nested steps @@ -407,7 +408,7 @@ Feature: pretty formatter As a test suite I need to be able to load features - Scenario: load a specific feature file # features/load.feature:26 + Scenario: load a specific feature file # features/load.feature:27 Given a feature path "features/load.feature" # suite_context_test.go:0 -> *godogFeaturesScenario When I parse features # suite_context_test.go:0 -> *godogFeaturesScenario Then I should have 1 feature file: # suite_context_test.go:0 -> *godogFeaturesScenario diff --git a/features/lang.feature b/features/lang.feature index 0b41014c..c5f87058 100644 --- a/features/lang.feature +++ b/features/lang.feature @@ -8,7 +8,7 @@ Savybė: užkrauti savybes Scenarijus: savybių užkrovimas iš aplanko Duota savybių aplankas "features" Kai aš išskaitau savybes - Tada aš turėčiau turėti 13 savybių failus: + Tada aš turėčiau turėti 14 savybių failus: """ features/background.feature features/events.feature @@ -23,4 +23,5 @@ Savybė: užkrauti savybes features/run.feature features/snippets.feature features/tags.feature + features/testingt.feature """ diff --git a/features/load.feature b/features/load.feature index 3bc2865f..0b9689f9 100644 --- a/features/load.feature +++ b/features/load.feature @@ -6,7 +6,7 @@ Feature: load features Scenario: load features within path Given a feature path "features" When I parse features - Then I should have 13 feature files: + Then I should have 14 feature files: """ features/background.feature features/events.feature @@ -21,6 +21,7 @@ Feature: load features features/run.feature features/snippets.feature features/tags.feature + features/testingt.feature """ Scenario: load a specific feature file diff --git a/features/testingt.feature b/features/testingt.feature new file mode 100644 index 00000000..1125d819 --- /dev/null +++ b/features/testingt.feature @@ -0,0 +1,194 @@ +Feature: providing testingT compatibility + In order to test application behavior using standard go assertion techniques + As a test suite + I need to be able to provide a testing.T compatible interface + + Scenario Outline: should fail test with no message if called on testing T + Given a feature "failed.feature" file: + """ + Feature: failed feature + + Scenario: fail a scenario + Given passing step + When my step fails the test by calling on testing T + """ + When I run feature suite + Then the suite should have failed + And the following steps should be passed: + """ + passing step + """ + And the following step should be failed: + """ + my step fails the test by calling on testing T + """ + Examples: + | op | + | Fail | + | FailNow | + + Scenario Outline: should fail test with message if called on T + Given a feature "failed.feature" file: + """ + Feature: failed feature + + Scenario: fail a scenario + Given passing step + When my step fails the test by calling on testing T with message "an unformatted message" + """ + When I run feature suite + Then the suite should have failed + And the following steps should be passed: + """ + passing step + """ + And the following step should be failed: + """ + my step fails the test by calling on testing T with message "an unformatted message" + """ + Examples: + | op | + | Error | + | Fatal | + + + Scenario Outline: should fail test with formatted message if called on T + Given a feature "failed.feature" file: + """ + Feature: failed feature + + Scenario: fail a scenario + Given passing step + When my step fails the test by calling on testing T with message "a formatted message %s" and argument "arg1" + """ + When I run feature suite + Then the suite should have failed + And the following steps should be passed: + """ + passing step + """ + And the following step should be failed: + """ + my step fails the test by calling on testing T with message "a formatted message %s" and argument "arg1" + """ + Examples: + | op | + | Errorf | + | Fatalf | + + Scenario: should pass test when testify assertions pass + Given a feature "testify.feature" file: + """ + Feature: passed feature + + Scenario: pass a scenario + Given passing step + When my step calls testify's assert.Equal with expected "exp" and actual "exp" + When my step calls testify's require.Equal with expected "exp" and actual "exp" + """ + When I run feature suite + Then the suite should have passed + And the following steps should be passed: + """ + passing step + my step calls testify's assert.Equal with expected "exp" and actual "exp" + my step calls testify's require.Equal with expected "exp" and actual "exp" + """ + + Scenario: should fail test when testify assertions do not pass + Given a feature "testify.feature" file: + """ + Feature: failed feature + + Scenario: fail a scenario + Given passing step + When my step calls testify's assert.Equal with expected "exp" and actual "not" + And my step calls testify's assert.Equal with expected "exp2" and actual "not" + """ + When I run feature suite + Then the suite should have failed + And the following steps should be passed: + """ + passing step + """ + And the following steps should be failed: + """ + my step calls testify's assert.Equal with expected "exp" and actual "not" + """ + And the following steps should be skipped: + """ + my step calls testify's assert.Equal with expected "exp2" and actual "not" + """ + + Scenario: should fail test when multiple testify assertions are used in a step + Given a feature "testify.feature" file: + """ + Feature: failed feature + + Scenario: fail a scenario + Given passing step + When my step calls testify's assert.Equal 3 times + """ + When I run feature suite + Then the suite should have failed + And the following steps should be passed: + """ + passing step + """ + And the following steps should be failed: + """ + my step calls testify's assert.Equal 3 times + """ + + Scenario: should pass test when multiple testify assertions are used successfully in a step + Given a feature "testify.feature" file: + """ + Feature: passed feature + + Scenario: pass a scenario + Given passing step + When my step calls testify's assert.Equal 3 times with match + """ + When I run feature suite + Then the suite should have passed + And the following steps should be passed: + """ + passing step + my step calls testify's assert.Equal 3 times with match + """ + + Scenario Outline: should skip test when is called on the testing.T + Given a feature "testify.feature" file: + """ + Feature: skipped feature + + Scenario: skip a scenario + Given passing step + When my step skips the test by calling on testing T + """ + When I run feature suite + Then the suite should have passed + And the following steps should be passed: + """ + passing step + """ + And the following steps should be skipped: + """ + my step skips the test by calling on testing T + """ + Examples: + | op | + | Skip | + | SkipNow | + + Scenario: should log when Logf/Log called on testing.T + When my step calls Logf on testing T with message "format this %s" and argument "formatparam1" + And my step calls Log on testing T with message "log this message" + Then the logged messages should include "format this formatparam1" + And the logged messages should include "log this message" + + Scenario: should log when godog.Logf/Log called + When my step calls godog.Logf with message "format this %s" and argument "formatparam1" + And my step calls godog.Log with message "log this message" + Then the logged messages should include "format this formatparam1" + And the logged messages should include "log this message" diff --git a/run_test.go b/run_test.go index 94f71744..6a070d68 100644 --- a/run_test.go +++ b/run_test.go @@ -525,11 +525,12 @@ func Test_AllFeaturesRun(t *testing.T) { ...................................................................... 210 ...................................................................... 280 ...................................................................... 350 -...... 356 +...................................................................... 420 +... 423 -94 scenarios (94 passed) -356 steps (356 passed) +108 scenarios (108 passed) +423 steps (423 passed) 0s ` @@ -553,11 +554,12 @@ func Test_AllFeaturesRunAsSubtests(t *testing.T) { ...................................................................... 210 ...................................................................... 280 ...................................................................... 350 -...... 356 +...................................................................... 420 +... 423 -94 scenarios (94 passed) -356 steps (356 passed) +108 scenarios (108 passed) +423 steps (423 passed) 0s ` diff --git a/suite.go b/suite.go index 3de07dec..fdfcb197 100644 --- a/suite.go +++ b/suite.go @@ -85,12 +85,18 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena // user multistep definitions may panic defer func() { if e := recover(); e != nil { - if err != nil { + pe, isErr := e.(error) + switch { + case isErr && errors.Is(pe, errStopNow): + // FailNow or SkipNow called on dogTestingT, so clear the error to let the normal + // below getTestingT(ctx).isFailed() call handle the reasons. + err = nil + case err != nil: err = &traceError{ msg: fmt.Sprintf("%s: %v", err.Error(), e), stack: callStack(), } - } else { + default: err = &traceError{ msg: fmt.Sprintf("%v", e), stack: callStack(), @@ -100,6 +106,11 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena earlyReturn := scenarioErr != nil || errors.Is(err, ErrUndefined) + // Check for any calls to Fail on dogT + if err == nil { + err = getTestingT(ctx).isFailed() + } + switch { case errors.Is(err, ErrPending): status = StepPending @@ -509,10 +520,15 @@ func (s *suite) runPickle(pickle *messages.Pickle) (err error) { s.fmt.Pickle(pickle) + dt := &testingT{ + name: pickle.Name, + } + ctx = setContextTestingT(ctx, dt) // scenario if s.testingT != nil { // Running scenario as a subtest. s.testingT.Run(pickle.Name, func(t *testing.T) { + dt.t = t ctx, err = s.runSteps(ctx, pickle, pickle.Steps) if s.shouldFail(err) { t.Errorf("%+v", err) diff --git a/suite_context_test.go b/suite_context_test.go index 6df223b7..5217f674 100644 --- a/suite_context_test.go +++ b/suite_context_test.go @@ -161,6 +161,19 @@ func InitializeScenario(ctx *ScenarioContext) { } }) + // introduced to test testingT + ctx.Step(`^my step (?:fails|skips) the test by calling (FailNow|Fail|SkipNow|Skip) on testing T$`, tc.myStepCallsTFailErrorSkip) + ctx.Step(`^my step fails the test by calling (Fatal|Error) on testing T with message "([^"]*)"$`, tc.myStepCallsTErrorFatal) + ctx.Step(`^my step fails the test by calling (Fatalf|Errorf) on testing T with message "([^"]*)" and argument "([^"]*)"$`, tc.myStepCallsTErrorfFatalf) + ctx.Step(`^my step calls Log on testing T with message "([^"]*)"$`, tc.myStepCallsTLog) + ctx.Step(`^my step calls Logf on testing T with message "([^"]*)" and argument "([^"]*)"$`, tc.myStepCallsTLogf) + ctx.Step(`^my step calls testify's assert.Equal with expected "([^"]*)" and actual "([^"]*)"$`, tc.myStepCallsTestifyAssertEqual) + ctx.Step(`^my step calls testify's require.Equal with expected "([^"]*)" and actual "([^"]*)"$`, tc.myStepCallsTestifyRequireEqual) + ctx.Step(`^my step calls testify's assert.Equal ([0-9]+) times(| with match)$`, tc.myStepCallsTestifyAssertEqualMultipleTimes) + ctx.Step(`^my step calls godog.Log with message "([^"]*)"$`, tc.myStepCallsDogLog) + ctx.Step(`^my step calls godog.Logf with message "([^"]*)" and argument "([^"]*)"$`, tc.myStepCallsDogLogf) + ctx.Step(`^the logged messages should include "([^"]*)"$`, tc.theLoggedMessagesShouldInclude) + ctx.StepContext().Before(tc.inject) } @@ -385,6 +398,101 @@ func (tc *godogFeaturesScenario) iShouldSeeTheContextInTheNextStep(ctx context.C return nil } +func (tc *godogFeaturesScenario) myStepCallsTFailErrorSkip(ctx context.Context, op string) error { + switch op { + case "FailNow": + T(ctx).FailNow() + case "Fail": + T(ctx).Fail() + case "SkipNow": + T(ctx).SkipNow() + case "Skip": + T(ctx).Skip() + default: + return fmt.Errorf("operation %s not supported by iCallTFailErrorSkip", op) + } + return nil +} + +func (tc *godogFeaturesScenario) myStepCallsTErrorFatal(ctx context.Context, op string, message string) error { + switch op { + case "Error": + T(ctx).Error(message) + case "Fatal": + T(ctx).Fatal(message) + default: + return fmt.Errorf("operation %s not supported by iCallTErrorFatal", op) + } + return nil +} + +func (tc *godogFeaturesScenario) myStepCallsTErrorfFatalf(ctx context.Context, op string, message string, arg string) error { + switch op { + case "Errorf": + T(ctx).Errorf(message, arg) + case "Fatalf": + T(ctx).Fatalf(message, arg) + default: + return fmt.Errorf("operation %s not supported by iCallTErrorfFatalf", op) + } + return nil +} + +func (tc *godogFeaturesScenario) myStepCallsTestifyAssertEqual(ctx context.Context, a string, b string) error { + assert.Equal(T(ctx), a, b) + return nil +} + +func (tc *godogFeaturesScenario) myStepCallsTestifyAssertEqualMultipleTimes(ctx context.Context, times string, withMatch string) error { + timesInt, err := strconv.Atoi(times) + if err != nil { + return fmt.Errorf("test step has invalid times value %s: %w", times, err) + } + for i := 0; i < timesInt; i++ { + if withMatch == " with match" { + assert.Equal(T(ctx), fmt.Sprintf("exp%v", i), fmt.Sprintf("exp%v", i)) + } else { + assert.Equal(T(ctx), "exp", fmt.Sprintf("notexp%v", i)) + } + } + return nil +} + +func (tc *godogFeaturesScenario) myStepCallsTestifyRequireEqual(ctx context.Context, a string, b string) error { + require.Equal(T(ctx), a, b) + return nil +} + +func (tc *godogFeaturesScenario) myStepCallsTLog(ctx context.Context, message string) error { + T(ctx).Log(message) + return nil +} + +func (tc *godogFeaturesScenario) myStepCallsTLogf(ctx context.Context, message string, arg string) error { + T(ctx).Logf(message, arg) + return nil +} + +func (tc *godogFeaturesScenario) myStepCallsDogLog(ctx context.Context, message string) error { + Log(ctx, message) + return nil +} + +func (tc *godogFeaturesScenario) myStepCallsDogLogf(ctx context.Context, message string, arg string) error { + Logf(ctx, message, arg) + return nil +} + +func (tc *godogFeaturesScenario) theLoggedMessagesShouldInclude(ctx context.Context, message string) error { + messages := LoggedMessages(ctx) + for _, m := range messages { + if strings.Contains(m, message) { + return nil + } + } + return fmt.Errorf("the message %q was not logged (logged messages: %v)", message, messages) +} + func (tc *godogFeaturesScenario) followingStepsShouldHave(status string, steps *DocString) error { var expected = strings.Split(steps.Content, "\n") var actual, unmatched, matched []string diff --git a/testingt.go b/testingt.go new file mode 100644 index 00000000..25981b89 --- /dev/null +++ b/testingt.go @@ -0,0 +1,206 @@ +package godog + +import ( + "context" + "fmt" + "strings" + "testing" +) + +// T returns a TestingT compatible interface from the current test context. It will return nil if +// called outside the context of a test. This can be used with (for example) testify's assert and +// require packages. +func T(ctx context.Context) TestingT { + return getTestingT(ctx) +} + +// TestingT is a subset of the public methods implemented by go's testing.T. It allows assertion +// libraries to be used with godog, provided they depend only on this subset of methods. +type TestingT interface { + // Name returns the name of the current pickle under test + Name() string + // Log will log to the current testing.T log if set, otherwise it will log to stdout + Log(args ...interface{}) + // Logf will log a formatted string to the current testing.T log if set, otherwise it will log + // to stdout + Logf(format string, args ...interface{}) + // Error fails the current test and logs the provided arguments. Equivalent to calling Log then + // Fail. + Error(args ...interface{}) + // Errorf fails the current test and logs the formatted message. Equivalent to calling Logf then + // Fail. + Errorf(format string, args ...interface{}) + // Fail marks the current test as failed, but does not halt execution of the step. + Fail() + // FailNow marks the current test as failed and halts execution of the step. + FailNow() + // Fatal logs the provided arguments, marks the test as failed and halts execution of the step. + Fatal(args ...interface{}) + // Fatal logs the formatted message, marks the test as failed and halts execution of the step. + Fatalf(format string, args ...interface{}) + // Skip logs the provided arguments and marks the test as skipped but does not halt execution + // of the step. + Skip(args ...interface{}) + // Skipf logs the formatted message and marks the test as skipped but does not halt execution + // of the step. + Skipf(format string, args ...interface{}) + // SkipNow marks the current test as skipped and halts execution of the step. + SkipNow() + // Skipped returns true if the test has been marked as skipped. + Skipped() bool +} + +// Logf will log test output. If called in the context of a test and testing.T has been registered, +// this will log using the step's testing.T, else it will simply log to stdout. +func Logf(ctx context.Context, format string, args ...interface{}) { + if t := getTestingT(ctx); t != nil { + t.Logf(format, args...) + return + } + fmt.Printf(format+"\n", args...) +} + +// Log will log test output. If called in the context of a test and testing.T has been registered, +// this will log using the step's testing.T, else it will simply log to stdout. +func Log(ctx context.Context, args ...interface{}) { + if t := getTestingT(ctx); t != nil { + t.Log(args...) + return + } + fmt.Println(args...) +} + +// LoggedMessages returns an array of any logged messages that have been recorded during the test +// through calls to godog.Log / godog.Logf or via operations against godog.T(ctx) +func LoggedMessages(ctx context.Context) []string { + if t := getTestingT(ctx); t != nil { + return t.logMessages + } + return nil +} + +// errStopNow should be returned inside a panic within the test to immediately halt execution of that +// test +var errStopNow = fmt.Errorf("FailNow or SkipNow called") + +type testingT struct { + name string + t *testing.T + failed bool + skipped bool + failMessages []string + logMessages []string +} + +// check interface against our testingT and the upstream testing.B/F/T: +var ( + _ TestingT = &testingT{} + _ TestingT = (*testing.T)(nil) +) + +func (dt *testingT) Name() string { + if dt.t != nil { + return dt.t.Name() + } + return dt.name +} + +func (dt *testingT) Log(args ...interface{}) { + dt.logMessages = append(dt.logMessages, fmt.Sprint(args...)) + if dt.t != nil { + dt.t.Log(args...) + return + } + fmt.Println(args...) +} + +func (dt *testingT) Logf(format string, args ...interface{}) { + dt.logMessages = append(dt.logMessages, fmt.Sprintf(format, args...)) + if dt.t != nil { + dt.t.Logf(format, args...) + return + } + fmt.Printf(format+"\n", args...) +} + +func (dt *testingT) Error(args ...interface{}) { + dt.Log(args...) + dt.failMessages = append(dt.failMessages, fmt.Sprintln(args...)) + dt.Fail() +} + +func (dt *testingT) Errorf(format string, args ...interface{}) { + dt.Logf(format, args...) + dt.failMessages = append(dt.failMessages, fmt.Sprintf(format, args...)) + dt.Fail() +} + +func (dt *testingT) Fail() { + dt.failed = true +} + +func (dt *testingT) FailNow() { + dt.Fail() + panic(errStopNow) +} + +func (dt *testingT) Fatal(args ...interface{}) { + dt.Log(args...) + dt.FailNow() +} + +func (dt *testingT) Fatalf(format string, args ...interface{}) { + dt.Logf(format, args...) + dt.FailNow() +} + +func (dt *testingT) Skip(args ...interface{}) { + dt.Log(args...) + dt.skipped = true +} + +func (dt *testingT) Skipf(format string, args ...interface{}) { + dt.Logf(format, args...) + dt.skipped = true +} + +func (dt *testingT) SkipNow() { + dt.skipped = true + panic(errStopNow) +} + +func (dt *testingT) Skipped() bool { + return dt.skipped +} + +// isFailed will return an error representing the calls to Fail made during this test +func (dt *testingT) isFailed() error { + if dt.skipped { + return ErrSkip + } + if !dt.failed { + return nil + } + switch len(dt.failMessages) { + case 0: + return fmt.Errorf("fail called on TestingT") + case 1: + return fmt.Errorf(dt.failMessages[0]) + default: + return fmt.Errorf("checks failed:\n* %s", strings.Join(dt.failMessages, "\n* ")) + } +} + +type testingTCtxVal struct{} + +func setContextTestingT(ctx context.Context, dt *testingT) context.Context { + return context.WithValue(ctx, testingTCtxVal{}, dt) +} + +func getTestingT(ctx context.Context) *testingT { + dt, ok := ctx.Value(testingTCtxVal{}).(*testingT) + if !ok { + return nil + } + return dt +}