kubernetes/test/e2e/framework/expect.go

353 lines
10 KiB
Go
Raw Normal View History

/*
Copyright 2014 The Kubernetes 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.
*/
package framework
import (
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
"context"
"errors"
"fmt"
"strings"
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
"time"
ginkgotypes "github.com/onsi/ginkgo/v2/types"
"github.com/onsi/gomega"
"github.com/onsi/gomega/format"
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
"github.com/onsi/gomega/types"
)
// MakeMatcher builds a gomega.Matcher based on a single callback function.
// That function is passed the actual value that is to be checked.
// There are three possible outcomes of the check:
// - An error is returned, which then is converted into a failure
// by Gomega.
// - A non-nil failure function is returned, which then is called
// by Gomega once a failure string is needed. This is useful
// to avoid unnecessarily preparing a failure string for intermediate
// failures in Eventually or Consistently.
// - Both function and error are nil, which means that the check
// succeeded.
func MakeMatcher[T interface{}](match func(actual T) (failure func() string, err error)) types.GomegaMatcher {
return &matcher[T]{
match: match,
}
}
type matcher[T interface{}] struct {
match func(actual T) (func() string, error)
failure func() string
}
func (m *matcher[T]) Match(actual interface{}) (success bool, err error) {
if actual, ok := actual.(T); ok {
failure, err := m.match(actual)
if err != nil {
return false, err
}
m.failure = failure
if failure != nil {
return false, nil
}
return true, nil
}
var empty T
return false, gomega.StopTrying(fmt.Sprintf("internal error: expected %T, got:\n%s", empty, format.Object(actual, 1)))
}
func (m *matcher[T]) FailureMessage(actual interface{}) string {
return m.failure()
}
func (m matcher[T]) NegatedFailureMessage(actual interface{}) string {
return m.failure()
}
var _ types.GomegaMatcher = &matcher[string]{}
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
// Gomega returns an interface that can be used like gomega to express
// assertions. The difference is that failed assertions are returned as an
// error:
//
// if err := Gomega().Expect(pod.Status.Phase).To(gomega.Equal(v1.Running)); err != nil {
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
// return fmt.Errorf("test pod not running: %w", err)
// }
//
// This error can get wrapped to provide additional context for the
// failure. The test then should use ExpectNoError to turn a non-nil error into
// a failure.
//
// When using this approach, there is no need for call offsets and extra
// descriptions for the Expect call because the call stack will be dumped when
// ExpectNoError is called and the additional description(s) can be added by
// wrapping the error.
//
// Asynchronous assertions use the framework's Poll interval and PodStart timeout
// by default.
func Gomega() GomegaInstance {
return gomegaInstance{}
}
type GomegaInstance interface {
Expect(actual interface{}) Assertion
Eventually(ctx context.Context, args ...interface{}) AsyncAssertion
Consistently(ctx context.Context, args ...interface{}) AsyncAssertion
}
type Assertion interface {
Should(matcher types.GomegaMatcher) error
ShouldNot(matcher types.GomegaMatcher) error
To(matcher types.GomegaMatcher) error
ToNot(matcher types.GomegaMatcher) error
NotTo(matcher types.GomegaMatcher) error
}
type AsyncAssertion interface {
Should(matcher types.GomegaMatcher) error
ShouldNot(matcher types.GomegaMatcher) error
WithTimeout(interval time.Duration) AsyncAssertion
WithPolling(interval time.Duration) AsyncAssertion
}
type gomegaInstance struct{}
var _ GomegaInstance = gomegaInstance{}
func (g gomegaInstance) Expect(actual interface{}) Assertion {
return assertion{actual: actual}
}
func (g gomegaInstance) Eventually(ctx context.Context, args ...interface{}) AsyncAssertion {
return newAsyncAssertion(ctx, args, false)
}
func (g gomegaInstance) Consistently(ctx context.Context, args ...interface{}) AsyncAssertion {
return newAsyncAssertion(ctx, args, true)
}
func newG() (*FailureError, gomega.Gomega) {
var failure FailureError
g := gomega.NewGomega(func(msg string, callerSkip ...int) {
failure = FailureError{
msg: msg,
}
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
})
return &failure, g
}
type assertion struct {
actual interface{}
}
func (a assertion) Should(matcher types.GomegaMatcher) error {
err, g := newG()
if !g.Expect(a.actual).Should(matcher) {
err.backtrace()
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
return *err
}
return nil
}
func (a assertion) ShouldNot(matcher types.GomegaMatcher) error {
err, g := newG()
if !g.Expect(a.actual).ShouldNot(matcher) {
err.backtrace()
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
return *err
}
return nil
}
func (a assertion) To(matcher types.GomegaMatcher) error {
err, g := newG()
if !g.Expect(a.actual).To(matcher) {
err.backtrace()
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
return *err
}
return nil
}
func (a assertion) ToNot(matcher types.GomegaMatcher) error {
err, g := newG()
if !g.Expect(a.actual).ToNot(matcher) {
err.backtrace()
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
return *err
}
return nil
}
func (a assertion) NotTo(matcher types.GomegaMatcher) error {
err, g := newG()
if !g.Expect(a.actual).NotTo(matcher) {
err.backtrace()
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
return *err
}
return nil
}
type asyncAssertion struct {
ctx context.Context
args []interface{}
timeout time.Duration
interval time.Duration
consistently bool
}
func newAsyncAssertion(ctx context.Context, args []interface{}, consistently bool) asyncAssertion {
return asyncAssertion{
ctx: ctx,
args: args,
// PodStart is used as default because waiting for a pod is the
// most common operation.
timeout: TestContext.timeouts.PodStart,
interval: TestContext.timeouts.Poll,
consistently: consistently,
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
}
}
func (a asyncAssertion) newAsync() (*FailureError, gomega.AsyncAssertion) {
err, g := newG()
var assertion gomega.AsyncAssertion
if a.consistently {
assertion = g.Consistently(a.ctx, a.args...)
} else {
assertion = g.Eventually(a.ctx, a.args...)
}
assertion = assertion.WithTimeout(a.timeout).WithPolling(a.interval)
return err, assertion
}
func (a asyncAssertion) Should(matcher types.GomegaMatcher) error {
err, assertion := a.newAsync()
if !assertion.Should(matcher) {
err.backtrace()
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
return *err
}
return nil
}
func (a asyncAssertion) ShouldNot(matcher types.GomegaMatcher) error {
err, assertion := a.newAsync()
if !assertion.ShouldNot(matcher) {
err.backtrace()
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
return *err
}
return nil
}
func (a asyncAssertion) WithTimeout(timeout time.Duration) AsyncAssertion {
a.timeout = timeout
return a
}
func (a asyncAssertion) WithPolling(interval time.Duration) AsyncAssertion {
a.interval = interval
return a
}
// FailureError is an error where the error string is meant to be passed to
// ginkgo.Fail directly, i.e. adding some prefix like "unexpected error" is not
// necessary. It is also not necessary to dump the error struct.
type FailureError struct {
msg string
fullStackTrace string
}
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
func (f FailureError) Error() string {
return f.msg
}
func (f FailureError) Backtrace() string {
return f.fullStackTrace
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
}
func (f FailureError) Is(target error) bool {
return target == ErrFailure
}
func (f *FailureError) backtrace() {
f.fullStackTrace = ginkgotypes.NewCodeLocationWithStackTrace(2).FullStackTrace
}
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
// ErrFailure is an empty error that can be wrapped to indicate that an error
// is a FailureError. It can also be used to test for a FailureError:.
//
// return fmt.Errorf("some problem%w", ErrFailure)
// ...
// err := someOperation()
// if errors.Is(err, ErrFailure) {
// ...
// }
var ErrFailure error = FailureError{}
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
// ExpectNoError checks if "err" is set, and if so, fails assertion while logging the error.
//
// As in [gomega.Expect], the explain parameters can be used to provide
// additional information in case of a failure in one of these two ways:
// - A single string is used as first line of the failure message directly.
// - A string with additional parameters is passed through [fmt.Sprintf].
func ExpectNoError(err error, explain ...interface{}) {
ExpectNoErrorWithOffset(1, err, explain...)
}
// ExpectNoErrorWithOffset checks if "err" is set, and if so, fails assertion while logging the error at "offset" levels above its caller
// (for example, for call chain f -> g -> ExpectNoErrorWithOffset(1, ...) error would be logged for "f").
//
// As in [gomega.Expect], the explain parameters can be used to provide
// additional information in case of a failure in one of these two ways:
// - A single string is used as first line of the failure message directly.
// - A string with additional parameters is passed through [fmt.Sprintf].
func ExpectNoErrorWithOffset(offset int, err error, explain ...interface{}) {
if err == nil {
return
}
// Errors usually contain unexported fields. We have to use
// a formatter here which can print those.
prefix := ""
if len(explain) > 0 {
if str, ok := explain[0].(string); ok {
prefix = fmt.Sprintf(str, explain[1:]...) + ": "
} else {
prefix = fmt.Sprintf("unexpected explain arguments, need format string: %v", explain)
}
}
// This intentionally doesn't use gomega.Expect. Instead we take
// full control over what information is presented where:
// - The complete error object is logged because it may contain
// additional information that isn't included in its error
// string.
// - It is not included in the failure message because
// it might make the failure message very large and/or
// cause error aggregation to work less well: two
// failures at the same code line might not be matched in
// https://go.k8s.io/triage because the error details are too
// different.
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
//
// Some errors include all relevant information in the Error
// string. For those we can skip the redundant log message.
// For our own failures we only log the additional stack backtrace
// because it is not included in the failure message.
var failure FailureError
if errors.As(err, &failure) && failure.Backtrace() != "" {
e2e framework: unify logging, support skipping helpers ginkgo.GinkgoHelper is a recent addition to ginkgo which allows functions to mark themselves as helper. This then changes which callstack gets reported for failures. It makes sense to support the same mechanism also for logging. There's also no reason why framework.Logf should produce output that is in a different format than klog log entries. Having time stamps formatted differently makes it hard to read test output which uses a mixture of both. Another user-visible advantage is that the error log entry from framework.ExpectNoError now references the test source code. With textlogger there is a simple replacement for klog that can be reconfigured to let the caller handle stack unwinding. klog itself doesn't support that and should be modified to support it (feature freeze). Emitting printf-style output via that logger would work, but become less readable because the message string would get quoted instead of printing it verbatim as before. So instead, the traditional klog header gets reproduced in the framework code. In this example, the first line is from klog, the second from Logf: I0111 11:00:54.088957 332873 factory.go:193] Registered Plugin "containerd" ... I0111 11:00:54.987534 332873 util.go:506] >>> kubeConfig: /var/run/kubernetes/admin.kubeconfig Indention is a bit different because the initial output is printed before installing the logger which writes through ginkgo.GinkgoWriter. One welcome side effect is that now "go vet" detects mismatched parameters for framework.Logf because fmt.Sprintf is called without mangling the format string. Some of the calls were incorrect.
2024-01-11 06:45:51 -05:00
log(offset+1, fmt.Sprintf("Failed inside E2E framework:\n %s", strings.ReplaceAll(failure.Backtrace(), "\n", "\n ")))
} else if !errors.Is(err, ErrFailure) {
e2e framework: unify logging, support skipping helpers ginkgo.GinkgoHelper is a recent addition to ginkgo which allows functions to mark themselves as helper. This then changes which callstack gets reported for failures. It makes sense to support the same mechanism also for logging. There's also no reason why framework.Logf should produce output that is in a different format than klog log entries. Having time stamps formatted differently makes it hard to read test output which uses a mixture of both. Another user-visible advantage is that the error log entry from framework.ExpectNoError now references the test source code. With textlogger there is a simple replacement for klog that can be reconfigured to let the caller handle stack unwinding. klog itself doesn't support that and should be modified to support it (feature freeze). Emitting printf-style output via that logger would work, but become less readable because the message string would get quoted instead of printing it verbatim as before. So instead, the traditional klog header gets reproduced in the framework code. In this example, the first line is from klog, the second from Logf: I0111 11:00:54.088957 332873 factory.go:193] Registered Plugin "containerd" ... I0111 11:00:54.987534 332873 util.go:506] >>> kubeConfig: /var/run/kubernetes/admin.kubeconfig Indention is a bit different because the initial output is printed before installing the logger which writes through ginkgo.GinkgoWriter. One welcome side effect is that now "go vet" detects mismatched parameters for framework.Logf because fmt.Sprintf is called without mangling the format string. Some of the calls were incorrect.
2024-01-11 06:45:51 -05:00
log(offset+1, fmt.Sprintf("Unexpected error: %s\n%s", prefix, format.Object(err, 1)))
e2e framework: gomega assertions as errors Calling gomega.Expect/Eventually/Consistently deep inside a helper call chain has several challenges: - the stack offset must be tracked correctly, otherwise the callstack for the failure starts at some helper code, which is often not informative - augmenting the failure message with additional information from each caller implies that each caller must pass down a string and/or format string plus arguments Both challenges can be solved by returning errors: - the stacktrace is taken at that level where the error is treated as a failure instead of passing back an error, i.e. inside the It callback - traditional error wrapping can add additional information, if desirable What was missing was some easy way to generate an error via a gomega assertion. The new infrastructure achieves that by mirroring the Gomega/Assertion/AsyncAssertion interfaces with errors as return values instead of calling a fail handler. It is intentionally less flexible than the gomega APIs: - A context must be passed to Eventually/Consistently as first parameter because that is needed for proper timeout handling. - No additional text can be added to the failure through this API because error wrapping is meant to be used for this. - No need to adjust the callstack offset because no backtrace is recorded when a failure occurs. To avoid the useless "unexpected error" log message when passing back a gomega failure, ExpectNoError gets extended to recognize such errors and then skips the logging.
2023-01-06 14:41:43 -05:00
}
Fail(prefix+err.Error(), 1+offset)
}