e2etest/newe2e_asserter.go (160 lines of code) (raw):
package e2etest
import (
"context"
"fmt"
"github.com/google/uuid"
"strings"
"testing"
)
var _ Asserter = &FrameworkAsserter{}
var _ ScenarioAsserter = &ScenarioVariationManager{} // covers all 3 interfaces
// ====== Asserter ======
type Asserter interface {
NoError(comment string, err error, failNow ...bool)
// Assert fails the test, but does not exit.
Assert(comment string, assertion Assertion, items ...any)
// AssertNow wraps Assert, and exits if failed.
AssertNow(comment string, assertion Assertion, items ...any)
// Error fails the test, exiting immediately.
Error(reason string)
// Skip skips the test, exiting immediately.
Skip(reason string)
// Log wraps t.Log with fmt.Sprintf
Log(format string, a ...any)
// Failed returns if the test has already failed.
Failed() bool
// HelperMarker returns the associated *testing.T, and if there is none, a NilHelperMarker.
HelperMarker() HelperMarker
GetTestName() string
}
type DryrunAsserter interface {
Asserter
Dryrun() bool
Invalid() bool
InvalidateScenario()
}
type CleanupFunc func(a Asserter)
type ScenarioAsserter interface {
DryrunAsserter
ContextManager
Cleanup(CleanupFunc)
UUID() uuid.UUID
}
type ContextManager interface {
Context() context.Context
SetContext(ctx context.Context)
}
// HelperMarker handles the fact that testing.T can be sometimes nil, and that we can't indicate a depth to ignore with Helper()
type HelperMarker interface {
Helper()
}
type NilHelperMarker struct{}
func (NilHelperMarker) Helper() {}
// ====== Assertion ======
type Assertion interface {
Name() string
// MaxArgs must be >= 1; or 0 to indicate no maximum
MaxArgs() int
// MinArgs must be 0 or => MaxArgs
MinArgs() int
// Assert must operate over all provided items
Assert(items ...any) bool
}
type FormattedAssertion interface {
Assertion
// Format must explain the reason for success or failure in a human-readable format.
Format(items ...any) string
}
// FrameworkAsserter should only be used for the very roots of the testing framework. It should never be used inside a real test itself.
type FrameworkAsserter struct {
t *testing.T
SuiteName string // todo new naming scheme
ScenarioName string
VariationName string // todo: do we just go through and use fmt.Sprint on all the objects in the variation in order?
}
func NewFrameworkAsserter(t *testing.T) Asserter {
nameSplits := strings.Split(t.Name(), "/")
nameSplits = nameSplits[1:]
tryIndex := func(idx int) string {
if len(nameSplits) > idx {
return nameSplits[idx]
}
return ""
}
return &FrameworkAsserter{
t: t,
SuiteName: tryIndex(0),
ScenarioName: tryIndex(1),
VariationName: tryIndex(2),
}
}
func (ta *FrameworkAsserter) GetTestName() string {
out := ""
if ta.SuiteName != "" { // Follow the logical progression to produce "Suite/Scenario (Variation)" where available.
out = ta.SuiteName
if ta.ScenarioName != "" {
out += "/" + ta.ScenarioName
if ta.VariationName != "" {
out += " (" + ta.VariationName + ")"
}
}
} else {
// Have a fallback for if a FrameworkAsserter exists without an associated Suite/Scenario/Variation
// if the SuiteManager has something to say, it should still be able to, and it should still be clear from whence it came.
out = "<FRAMEWORK>"
}
return out
}
func (ta *FrameworkAsserter) PrintFinalizingMessage(reasonFormat string, a ...any) {
ta.t.Helper()
ta.Log("========== %s ===========", ta.GetTestName())
ta.Log(reasonFormat, a...)
}
func (ta *FrameworkAsserter) Log(format string, a ...any) {
ta.t.Helper()
ta.t.Log(fmt.Sprintf(format, a...))
}
func (ta *FrameworkAsserter) NoError(comment string, err error, failNow ...bool) {
ta.t.Helper()
if err != nil {
ta.t.Logf("Error was not nil (%s): %v", comment, err)
if FirstOrZero(failNow) {
ta.t.FailNow()
} else {
ta.t.Fail()
}
}
}
func (ta *FrameworkAsserter) AssertNow(comment string, assertion Assertion, items ...any) {
ta.t.Helper()
if (assertion.MinArgs() > 0 && len(items) < assertion.MinArgs()) || (assertion.MaxArgs() > 0 && len(items) > assertion.MaxArgs()) {
ta.PrintFinalizingMessage("Failed to assert: Assertion %s supports argument counts between %d and %d, but received %d args.", assertion.Name(), assertion.MinArgs(), assertion.MaxArgs(), len(items))
ta.t.FailNow()
}
if !assertion.Assert(items...) {
if fa, ok := assertion.(FormattedAssertion); ok {
ta.PrintFinalizingMessage("Failed assertion %s: %s; %s", fa.Name(), fa.Format(items...), comment)
} else {
ta.PrintFinalizingMessage("Failed assertion %s with item(s): %v; %s", assertion.Name(), items, comment)
}
ta.t.FailNow()
}
}
func (ta *FrameworkAsserter) Assert(comment string, assertion Assertion, items ...any) {
ta.t.Helper()
if (assertion.MinArgs() > 0 && len(items) < assertion.MinArgs()) || (assertion.MaxArgs() > 0 && len(items) > assertion.MaxArgs()) {
ta.PrintFinalizingMessage("Failed to assert: Assertion %s supports argument counts between %d and %d, but received %d args.", assertion.Name(), assertion.MinArgs(), assertion.MaxArgs(), len(items))
ta.t.FailNow()
}
if !assertion.Assert(items...) {
if fa, ok := assertion.(FormattedAssertion); ok {
ta.PrintFinalizingMessage("Failed assertion %s: %s; %s", fa.Name(), fa.Format(items...), comment)
} else {
ta.PrintFinalizingMessage("Failed assertion %s with item(s): %v; %s", assertion.Name(), items, comment)
}
ta.t.Fail()
}
}
func (ta *FrameworkAsserter) Error(reason string) {
ta.t.Helper()
ta.PrintFinalizingMessage("Test failed: %s", reason)
ta.t.FailNow()
}
func (ta *FrameworkAsserter) Skip(reason string) {
ta.t.Helper()
ta.PrintFinalizingMessage("Test skipped: %s", reason)
ta.t.SkipNow()
}
func (ta *FrameworkAsserter) Failed() bool {
ta.t.Helper()
return ta.t.Failed()
}
func (ta *FrameworkAsserter) HelperMarker() HelperMarker {
if ta.t != nil {
return ta.t
}
return NilHelperMarker{}
}