kubernetes/test/utils/ktesting/stepcontext_test.go
Patrick Ohly 36bcd43fca ktesting: abort entire test suite on SIGINT
When aborting an integration test with CTRL-C while it runs,
the current test fails and etcd exits. But additional tests were still being
started and the failed slowly because they couldn't connect to etcd.

It's better to fail additional tests in ktesting.Init when the test run has
already been interrupted.

While at it, also make it a bit more obvious that testing was interrupted by
logging it and update one comment about this and clean up the naming of
contexts in the code.

Example:

    $ go test -v ./test/integration/quota
    ...
    I1106 11:42:48.857162  147325 etcd.go:416] "Not using watch cache" resource="events.events.k8s.io"
    I1106 11:42:48.857204  147325 handler.go:286] Adding GroupVersion events.k8s.io v1 to ResourceManager
    W1106 11:42:48.857209  147325 genericapiserver.go:765] Skipping API events.k8s.io/v1beta1 because it has no resources.
    ^C

    INFO: canceling test context: received interrupt signal

    {"level":"warn","ts":"2024-11-06T11:42:48.984676+0100","caller":"embed/serve.go:160","msg":"stopping insecure grpc server due to error","error":"accept tcp 127.0.0.1:44177: use of closed network connection"}
    ...
    I1106 11:42:50.042430  147325 handler.go:142] kube-apiserver: GET "/apis/rbac.authorization.k8s.io/v1/clusterroles" satisfied by gorestful with webservice /apis/rbac.authorization.k8s.io/v1
        test_server.go:241: timed out waiting for the condition
    --- FAIL: TestQuota (11.45s)
    === RUN   TestQuotaLimitedResourceDenial
        quota_test.go:292: testing has been interrupted: received interrupt signal
    --- FAIL: TestQuotaLimitedResourceDenial (0.00s)
    === RUN   TestQuotaLimitService
        quota_test.go:418: testing has been interrupted: received interrupt signal
    --- FAIL: TestQuotaLimitService (0.00s)
    FAIL
2026-01-30 12:35:57 +01:00

137 lines
3.9 KiB
Go

/*
Copyright 2024 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 ktesting
import (
"context"
"io"
"os"
"testing"
"time"
"github.com/onsi/gomega"
"go.uber.org/goleak"
)
func TestStepContext(t *testing.T) {
for name, tc := range map[string]testcase{
"output": {
cb: func(tCtx TContext) {
tCtx = WithStep(tCtx, "step")
tCtx.Log("Log", "a", "b", 42)
tCtx.Logf("Logf %s %s %d", "a", "b", 42)
tCtx.Error("Error", "a", "b", 42)
tCtx.Errorf("Errorf %s %s %d", "a", "b", 42)
},
expectTrace: `(LOG) <klog header>: step: Log a b 42
(LOG) <klog header>: step: Logf a b 42
(ERROR) ERROR: <klog header>:
step: Error a b 42
(ERROR) ERROR: <klog header>:
step: Errorf a b 42
`,
},
"fatal": {
cb: func(tCtx TContext) {
tCtx = WithStep(tCtx, "step")
tCtx.Fatal("Error", "a", "b", 42)
// not reached
tCtx.Log("Log")
},
expectTrace: `(FATAL) FATAL ERROR: <klog header>:
step: Error a b 42
`,
},
"fatalf": {
cb: func(tCtx TContext) {
tCtx = WithStep(tCtx, "step")
tCtx.Fatalf("Error %s %s %d", "a", "b", 42)
// not reached
tCtx.Log("Log")
},
expectTrace: `(FATAL) FATAL ERROR: <klog header>:
step: Error a b 42
`,
},
} {
t.Run(name, func(t *testing.T) {
tc.run(t)
})
}
}
func TestProgressReport(t *testing.T) {
oldOut := defaultProgressReporter.out
out := newOutputStream()
defaultProgressReporter.out = out
t.Cleanup(func() {
goleak.VerifyNone(t)
defaultProgressReporter.out = oldOut
// If we get here, the defaultProgressReporter is not active anymore,
// but the interrupt context should still be canceled.
gomega.NewGomegaWithT(t).Expect(defaultProgressReporter.usageCount).To(gomega.Equal(int64(0)), "usage count")
gomega.NewGomegaWithT(t).Expect(context.Cause(interruptCtx)).To(gomega.MatchError(gomega.Equal("received interrupt signal")), "interrupted persistently")
// Reset for next test.
interruptCtx, interrupted = context.WithCancelCause(context.Background())
})
// This must use a real testing.T, otherwise Init doesn't initialize signal handling.
tCtx := Init(t)
tCtx = WithStep(tCtx, "step")
removeReporter := tCtx.Value("GINKGO_SPEC_CONTEXT").(ginkgoReporter).AttachProgressReporter(func() string { return "hello world" })
defer removeReporter()
tCtx.Expect(tCtx.Value("some other key")).To(gomega.BeNil(), "value for unknown context value key")
// Trigger report and wait for it.
defaultProgressReporter.progressChannel <- os.Interrupt
report := <-out.stream
tCtx.Expect(report).To(gomega.Equal(`You requested a progress report.
step: hello world
`), "report")
gomega.NewGomegaWithT(t).Expect(context.Cause(interruptCtx)).To(gomega.Succeed(), "not interrupted yet")
defaultProgressReporter.signalChannel <- os.Interrupt
message := <-out.stream
tCtx.Expect(message).To(gomega.Equal(`
INFO: canceling test context: received interrupt signal
`))
gomega.NewGomegaWithT(t).Eventually(func() error { return context.Cause(tCtx) }).WithTimeout(30*time.Second).To(gomega.MatchError(gomega.Equal("received interrupt signal")), "interrupted")
}
// outputStream forwards exactly one Write call to a stream.
// A second Write call is an error and will panic.
type outputStream struct {
stream chan string
}
var _ io.Writer = &outputStream{}
func newOutputStream() *outputStream {
return &outputStream{
stream: make(chan string),
}
}
func (s *outputStream) Write(buf []byte) (int, error) {
s.stream <- string(buf)
return len(buf), nil
}