mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-10 04:27:54 +00:00
e2e framework: track and report internal bugs
If something goes wrong during the test registration phase, the only solution so far was to panic. This is not user-friendly and only allows to report one problem at a time. If initialization can continue, then a better solution is to record a bug, continue, and then report all bugs together. This also works when just listing tests. The new verify-e2e-suites.sh uses that to check all test suites (identified as "packages that call framework.AfterReadingAllFlags", with some exceptions) as part of pull-kubernetes-verify. Example output for a fake framework.RecordBug(framework.NewBug("fake bug during SIGDescribe", 0)) in test/e2e/storage/volume_metrics.go: ``` $ hack/verify-e2e-suites.sh go version go1.21.1 linux/amd64 ERROR: E2E test suite invocation failed for test/e2e. ERROR: E2E suite initialization was faulty, these errors must be fixed: ERROR: test/e2e/storage/volume_metrics.go:49: fake bug during SIGDescribe E2E suite test/e2e_kubeadm passed. E2E suite test/e2e_node passed. ```
This commit is contained in:
parent
3afdcc03ea
commit
535ab74346
45
hack/verify-e2e-suites.sh
Executable file
45
hack/verify-e2e-suites.sh
Executable file
@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Copyright 2021 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.
|
||||||
|
|
||||||
|
# This script checks that all E2E test suites are sane, i.e. can be
|
||||||
|
# started without an error.
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
KUBE_ROOT=$(dirname "${BASH_SOURCE[0]}")/..
|
||||||
|
source "${KUBE_ROOT}/hack/lib/init.sh"
|
||||||
|
source "${KUBE_ROOT}/hack/lib/util.sh"
|
||||||
|
|
||||||
|
kube::golang::verify_go_version
|
||||||
|
|
||||||
|
cd "${KUBE_ROOT}"
|
||||||
|
|
||||||
|
kube::util::ensure-temp-dir
|
||||||
|
|
||||||
|
for suite in $(git grep -l framework.AfterReadingAllFlags | grep -v -e ^test/e2e/framework -e ^hack | xargs -n 1 dirname | sort -u); do
|
||||||
|
# Build a binary and run it in the root directory to get paths that are
|
||||||
|
# relative to that instead of the package directory.
|
||||||
|
out=""
|
||||||
|
if (cd "$suite" && go test -c -o "${KUBE_TEMP}/e2e.bin" .) && out=$("${KUBE_TEMP}/e2e.bin" --list-tests); then
|
||||||
|
echo "E2E suite $suite passed."
|
||||||
|
else
|
||||||
|
echo >&2 "ERROR: E2E test suite invocation failed for $suite."
|
||||||
|
# shellcheck disable=SC2001
|
||||||
|
echo "$out" | sed -e 's/^/ /'
|
||||||
|
fi
|
||||||
|
done
|
108
test/e2e/framework/bugs.go
Normal file
108
test/e2e/framework/bugs.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 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 (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/onsi/ginkgo/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
bugs []Bug
|
||||||
|
bugMutex sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// RecordBug stores information about a bug in the E2E suite source code that
|
||||||
|
// cannot be reported through ginkgo.Fail because it was found outside of some
|
||||||
|
// test, for example during test registration.
|
||||||
|
//
|
||||||
|
// This can be used instead of raising a panic. Then all bugs can be reported
|
||||||
|
// together instead of failing after the first one.
|
||||||
|
func RecordBug(bug Bug) {
|
||||||
|
bugMutex.Lock()
|
||||||
|
defer bugMutex.Unlock()
|
||||||
|
|
||||||
|
bugs = append(bugs, bug)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bug struct {
|
||||||
|
FileName string
|
||||||
|
LineNumber int
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBug creates a new bug with a location that is obtained by skipping a certain number
|
||||||
|
// of stack frames. Passing zero will record the source code location of the direct caller
|
||||||
|
// of NewBug.
|
||||||
|
func NewBug(message string, skip int) Bug {
|
||||||
|
location := types.NewCodeLocation(skip + 1)
|
||||||
|
return Bug{FileName: location.FileName, LineNumber: location.LineNumber, Message: message}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatBugs produces a report that includes all bugs recorded earlier via
|
||||||
|
// RecordBug. An error is returned with the report if there have been bugs.
|
||||||
|
func FormatBugs() error {
|
||||||
|
bugMutex.Lock()
|
||||||
|
defer bugMutex.Unlock()
|
||||||
|
|
||||||
|
if len(bugs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := make([]string, 0, len(bugs))
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get current directory: %v", err)
|
||||||
|
}
|
||||||
|
// Sort by file name, line number, message. For the sake of simplicity
|
||||||
|
// this uses the full file name even though the output the may use a
|
||||||
|
// relative path. Usually the result should be the same because full
|
||||||
|
// paths will all have the same prefix.
|
||||||
|
sort.Slice(bugs, func(i, j int) bool {
|
||||||
|
switch strings.Compare(bugs[i].FileName, bugs[j].FileName) {
|
||||||
|
case -1:
|
||||||
|
return true
|
||||||
|
case 1:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if bugs[i].LineNumber < bugs[j].LineNumber {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if bugs[i].LineNumber > bugs[j].LineNumber {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return bugs[i].Message < bugs[j].Message
|
||||||
|
})
|
||||||
|
for _, bug := range bugs {
|
||||||
|
// Use relative paths, if possible.
|
||||||
|
path := bug.FileName
|
||||||
|
if wd != "" {
|
||||||
|
if relpath, err := filepath.Rel(wd, bug.FileName); err == nil {
|
||||||
|
path = relpath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines = append(lines, fmt.Sprintf("ERROR: %s:%d: %s\n", path, bug.LineNumber, strings.TrimSpace(bug.Message)))
|
||||||
|
}
|
||||||
|
return errors.New(strings.Join(lines, ""))
|
||||||
|
}
|
79
test/e2e/framework/internal/unittests/bugs/bugs_test.go
Normal file
79
test/e2e/framework/internal/unittests/bugs/bugs_test.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 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 bugs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/test/e2e/framework"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The line number of the following code is checked in BugOutput below.
|
||||||
|
// Be careful when moving it around or changing the import statements above.
|
||||||
|
// Here are some intentionally blank lines that can be removed to compensate
|
||||||
|
// for future additional import statements.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// This must be line #50.
|
||||||
|
|
||||||
|
func helper() {
|
||||||
|
framework.RecordBug(framework.NewBug("new bug", 0))
|
||||||
|
framework.RecordBug(framework.NewBug("parent", 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordBugs() {
|
||||||
|
helper()
|
||||||
|
framework.RecordBug(framework.Bug{FileName: "buggy/buggy.go", LineNumber: 100, Message: "hello world"})
|
||||||
|
framework.RecordBug(framework.Bug{FileName: "some/relative/path/buggy.go", LineNumber: 200, Message: " with spaces \n"})
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
numBugs = 3
|
||||||
|
bugOutput = `ERROR: bugs_test.go:53: new bug
|
||||||
|
ERROR: bugs_test.go:58: parent
|
||||||
|
ERROR: buggy/buggy.go:100: hello world
|
||||||
|
ERROR: some/relative/path/buggy.go:200: with spaces
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBugs(t *testing.T) {
|
||||||
|
assert.NoError(t, framework.FormatBugs())
|
||||||
|
recordBugs()
|
||||||
|
err := framework.FormatBugs()
|
||||||
|
if assert.Error(t, err) {
|
||||||
|
assert.Equal(t, bugOutput, err.Error())
|
||||||
|
}
|
||||||
|
}
|
@ -507,8 +507,16 @@ func AfterReadingAllFlags(t *TestContextType) {
|
|||||||
gomega.SetDefaultEventuallyTimeout(t.timeouts.PodStart)
|
gomega.SetDefaultEventuallyTimeout(t.timeouts.PodStart)
|
||||||
gomega.SetDefaultConsistentlyDuration(t.timeouts.PodStartShort)
|
gomega.SetDefaultConsistentlyDuration(t.timeouts.PodStartShort)
|
||||||
|
|
||||||
|
// ginkgo.PreviewSpecs will expand all nodes and thus may find new bugs.
|
||||||
|
report := ginkgo.PreviewSpecs("Kubernetes e2e test statistics")
|
||||||
|
if err := FormatBugs(); err != nil {
|
||||||
|
// Refuse to do anything if the E2E suite is buggy.
|
||||||
|
fmt.Fprint(Output, "ERROR: E2E suite initialization was faulty, these errors must be fixed:")
|
||||||
|
fmt.Fprint(Output, "\n"+err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
if t.listLabels || t.listTests {
|
if t.listLabels || t.listTests {
|
||||||
listTestInformation(ginkgo.PreviewSpecs("Kubernetes e2e test statistics"))
|
listTestInformation(report)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user