diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 2784ce18dc8..93032c185b0 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -67,6 +67,7 @@ import ( _ "k8s.io/kubernetes/test/e2e/framework/debug/init" _ "k8s.io/kubernetes/test/e2e/framework/metrics/init" _ "k8s.io/kubernetes/test/e2e/framework/node/init" + _ "k8s.io/kubernetes/test/utils/format" ) // handleFlags sets up all flags and parses the command line. diff --git a/test/e2e/framework/test_context.go b/test/e2e/framework/test_context.go index afb7654ec2b..6108cddf740 100644 --- a/test/e2e/framework/test_context.go +++ b/test/e2e/framework/test_context.go @@ -32,6 +32,7 @@ import ( "github.com/onsi/ginkgo/v2" "github.com/onsi/ginkgo/v2/reporters" "github.com/onsi/ginkgo/v2/types" + gomegaformat "github.com/onsi/gomega/format" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" @@ -307,6 +308,9 @@ func (tc TestContextType) ClusterIsIPv6() bool { // options themselves, copy flags from test/e2e/framework/config // as shown in HandleFlags. func RegisterCommonFlags(flags *flag.FlagSet) { + // The default is too low for objects like pods, even when using YAML. We double the default. + flags.IntVar(&gomegaformat.MaxLength, "gomega-max-length", 8000, "Sets the maximum size for the gomega formatter (= gomega.MaxLength). Use 0 to disable truncation.") + flags.StringVar(&TestContext.GatherKubeSystemResourceUsageData, "gather-resource-usage", "false", "If set to 'true' or 'all' framework will be monitoring resource usage of system all add-ons in (some) e2e tests, if set to 'master' framework will be monitoring master node only, if set to 'none' of 'false' monitoring will be turned off.") flags.BoolVar(&TestContext.GatherLogsSizes, "gather-logs-sizes", false, "If set to true framework will be monitoring logs sizes on all machines running e2e tests.") flags.IntVar(&TestContext.MaxNodesToGather, "max-nodes-to-gather-from", 20, "The maximum number of nodes to gather extended info from on test failure.") @@ -526,6 +530,14 @@ func AfterReadingAllFlags(t *TestContextType) { } } +const ( + // This is the traditional gomega.Format default of 4000 for an object + // dump plus some extra room for the message. + maxFailureMessageSize = 5000 + + truncatedMsg = "\n[... see output for full dump ...]\n" +) + // writeJUnitReport generates a JUnit file in the e2e report directory that is // shorter than the one normally written by `ginkgo --junit-report`. This is // needed because the full report can become too large for tools like Spyglass @@ -542,6 +554,18 @@ func writeJUnitReport(report ginkgo.Report) { if specReport.State != types.SpecStateFailed { specReport.CapturedGinkgoWriterOutput = "" specReport.CapturedStdOutErr = "" + } else { + // Truncate the failure message if it is too large. + msgLen := len(specReport.Failure.Message) + if msgLen > maxFailureMessageSize { + // Insert full message at the beginning where it is easy to find. + specReport.CapturedGinkgoWriterOutput = + "Full failure message:\n" + + specReport.Failure.Message + "\n\n" + + strings.Repeat("=", 70) + "\n\n" + + specReport.CapturedGinkgoWriterOutput + specReport.Failure.Message = specReport.Failure.Message[0:maxFailureMessageSize/2] + truncatedMsg + specReport.Failure.Message[msgLen-maxFailureMessageSize/2:msgLen] + } } // Remove report entries generated by ginkgo.By("doing diff --git a/test/e2e_kubeadm/e2e_kubeadm_suite_test.go b/test/e2e_kubeadm/e2e_kubeadm_suite_test.go index aa74639e0ea..08e54318dc1 100644 --- a/test/e2e_kubeadm/e2e_kubeadm_suite_test.go +++ b/test/e2e_kubeadm/e2e_kubeadm_suite_test.go @@ -32,6 +32,7 @@ import ( _ "k8s.io/kubernetes/test/e2e/framework/debug/init" _ "k8s.io/kubernetes/test/e2e/framework/metrics/init" _ "k8s.io/kubernetes/test/e2e/framework/node/init" + _ "k8s.io/kubernetes/test/utils/format" ) func TestMain(m *testing.M) { diff --git a/test/e2e_node/e2e_node_suite_test.go b/test/e2e_node/e2e_node_suite_test.go index 3330aa4678c..92f0e425e45 100644 --- a/test/e2e_node/e2e_node_suite_test.go +++ b/test/e2e_node/e2e_node_suite_test.go @@ -57,6 +57,7 @@ import ( _ "k8s.io/kubernetes/test/e2e/framework/debug/init" _ "k8s.io/kubernetes/test/e2e/framework/metrics/init" _ "k8s.io/kubernetes/test/e2e/framework/node/init" + _ "k8s.io/kubernetes/test/utils/format" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" diff --git a/test/utils/format/format.go b/test/utils/format/format.go new file mode 100644 index 00000000000..a77f697ad02 --- /dev/null +++ b/test/utils/format/format.go @@ -0,0 +1,80 @@ +/* +Copyright 2022 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 format is an extension of Gomega's format package which +// improves printing of objects that can be serialized well as YAML, +// like the structs in the Kubernetes API. +// +// Just importing it is enough to activate this special YAML support +// in Gomega. +package format + +import ( + "reflect" + "strings" + + "github.com/onsi/gomega/format" + + "sigs.k8s.io/yaml" +) + +func init() { + format.RegisterCustomFormatter(handleYAML) +} + +// Object makes Gomega's [format.Object] available without having to import that +// package. +func Object(object interface{}, indentation uint) string { + return format.Object(object, indentation) +} + +// handleYAML formats all values as YAML where the result +// is likely to look better as YAML: +// - pointer to struct or struct where all fields +// have `json` tags +// - slices containing such a value +// - maps where the key or value are such a value +func handleYAML(object interface{}) (string, bool) { + value := reflect.ValueOf(object) + if !useYAML(value.Type()) { + return "", false + } + y, err := yaml.Marshal(object) + if err != nil { + return "", false + } + return "\n" + strings.TrimSpace(string(y)), true +} + +func useYAML(t reflect.Type) bool { + switch t.Kind() { + case reflect.Pointer, reflect.Slice, reflect.Array: + return useYAML(t.Elem()) + case reflect.Map: + return useYAML(t.Key()) || useYAML(t.Elem()) + case reflect.Struct: + // All fields must have a `json` tag. + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if _, ok := field.Tag.Lookup("json"); !ok { + return false + } + } + return true + default: + return false + } +} diff --git a/test/utils/format/format_test.go b/test/utils/format/format_test.go new file mode 100644 index 00000000000..9c29b626195 --- /dev/null +++ b/test/utils/format/format_test.go @@ -0,0 +1,81 @@ +/* +Copyright 2022 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 format_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/onsi/gomega/format" + "github.com/stretchr/testify/assert" + + v1 "k8s.io/api/core/v1" +) + +func TestGomegaFormatObject(t *testing.T) { + for name, test := range map[string]struct { + value interface{} + expected string + indentation uint + }{ + "int": {value: 1, expected: `: 1`}, + "string": {value: "hello world", expected: `: "hello world"`}, + "struct": {value: myStruct{a: 1, b: 2}, expected: `: {a: 1, b: 2}`}, + "gomegastringer": {value: typeWithGomegaStringer(2), expected: `: my stringer 2`}, + "pod": {value: v1.Pod{}, expected: `: + metadata: + creationTimestamp: null + spec: + containers: null + status: {}`}, + "pod-indented": {value: v1.Pod{}, indentation: 1, expected: ` : + metadata: + creationTimestamp: null + spec: + containers: null + status: {}`}, + "pod-ptr": {value: &v1.Pod{}, expected: `<*v1.Pod | >: + metadata: + creationTimestamp: null + spec: + containers: null + status: {}`}, + "pod-hash": {value: map[string]v1.Pod{}, expected: `: + {}`}, + "podlist": {value: v1.PodList{}, expected: `: + items: null + metadata: {}`}, + } { + t.Run(name, func(t *testing.T) { + actual := format.Object(test.value, test.indentation) + actual = regexp.MustCompile(`\| 0x[a-z0-9]+`).ReplaceAllString(actual, `| `) + assert.Equal(t, test.expected, actual) + }) + } + +} + +type typeWithGomegaStringer int + +func (v typeWithGomegaStringer) GomegaString() string { + return fmt.Sprintf("my stringer %d", v) +} + +type myStruct struct { + a, b int +}