Skip to content

Commit 16c7d26

Browse files
authored
Merge pull request #401 from pohly/ktesting-warning-delay
ktesting: tone down warning about leaked test goroutine
2 parents 2ee202a + cd24012 commit 16c7d26

File tree

2 files changed

+36
-9
lines changed

2 files changed

+36
-9
lines changed

ktesting/testinglogger.go

+26-4
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,19 @@ type tloggerShared struct {
217217
// t gets cleared when the test is completed.
218218
t TL
219219

220-
// We warn once when a leaked goroutine is detected because
221-
// it logs after test completion.
220+
// The time when the test completed.
221+
stopTime time.Time
222+
223+
// We warn once when a leaked goroutine logs after test completion.
224+
//
225+
// Not terminating immediately is fairly normal: many controllers
226+
// log "terminating" messages while they shut down, which often is
227+
// right after test completion, if that is when the test cancels the
228+
// context and then doesn't wait for goroutines (which is often
229+
// not possible).
230+
//
231+
// Therefore there is the [stopGracePeriod] during which messages get
232+
// logged to the global logger without the warning.
222233
goroutineWarningDone bool
223234

224235
formatter serialize.Formatter
@@ -228,10 +239,15 @@ type tloggerShared struct {
228239
callDepth int
229240
}
230241

242+
// Log output of a leaked goroutine during this period after test completion
243+
// does not trigger the warning.
244+
const stopGracePeriod = 10 * time.Second
245+
231246
func (ls *tloggerShared) stop() {
232247
ls.mutex.Lock()
233248
defer ls.mutex.Unlock()
234249
ls.t = nil
250+
ls.stopTime = time.Now()
235251
}
236252

237253
// tlogger is the actual LogSink implementation.
@@ -241,6 +257,8 @@ type tlogger struct {
241257
values []interface{}
242258
}
243259

260+
// fallbackLogger is called while l.shared.mutex is locked and after it has
261+
// been determined that the original testing.TB is no longer usable.
244262
func (l tlogger) fallbackLogger() logr.Logger {
245263
logger := klog.Background().WithValues(l.values...).WithName(l.shared.testName + " leaked goroutine")
246264
if l.prefix != "" {
@@ -250,8 +268,12 @@ func (l tlogger) fallbackLogger() logr.Logger {
250268
logger = logger.WithCallDepth(l.shared.callDepth + 1)
251269

252270
if !l.shared.goroutineWarningDone {
253-
logger.WithCallDepth(1).Error(nil, "WARNING: test kept at least one goroutine running after test completion", "callstack", string(dbg.Stacks(false)))
254-
l.shared.goroutineWarningDone = true
271+
duration := time.Since(l.shared.stopTime)
272+
if duration > stopGracePeriod {
273+
274+
logger.WithCallDepth(1).Info("WARNING: test kept at least one goroutine running after test completion", "timeSinceCompletion", duration.Round(time.Second), "callstack", string(dbg.Stacks(false)))
275+
l.shared.goroutineWarningDone = true
276+
}
255277
}
256278
return logger
257279
}

ktesting/testinglogger_test.go

+10-5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"runtime"
1717
"sync"
1818
"testing"
19+
"time"
1920

2021
"k8s.io/klog/v2"
2122
"k8s.io/klog/v2/internal/test/require"
@@ -198,6 +199,7 @@ func TestStop(t *testing.T) {
198199
_, _, line, _ = runtime.Caller(0)
199200
logger.Info("simple info message")
200201
logger.Error(nil, "error message")
202+
time.Sleep(15 * time.Second)
201203
logger.WithName("me").WithValues("completed", true).Info("complex info message", "anotherValue", 1)
202204
}()
203205
})
@@ -212,11 +214,14 @@ func TestStop(t *testing.T) {
212214
// Strip time and pid prefix.
213215
actual = regexp.MustCompile(`(?m)^.* testinglogger_test.go:`).ReplaceAllString(actual, `testinglogger_test.go:`)
214216

217+
// Strip duration.
218+
actual = regexp.MustCompile(`timeSinceCompletion="\d+s"`).ReplaceAllString(actual, `timeSinceCompletion="<...>s"`)
219+
215220
// All lines from the callstack get stripped. We can be sure that it was non-empty because otherwise we wouldn't
216221
// have the < > markers.
217222
//
218223
// Full output:
219-
// testinglogger_test.go:194] "WARNING: test kept at least one goroutine running after test completion" logger="TestStop/Sub leaked goroutine" callstack=<
224+
// testinglogger_test.go:194] "WARNING: test kept at least one goroutine running after test completion" logger="TestStop/Sub leaked goroutine.me" completed=true timeSinceCompletion="15s" callstack=<
220225
// goroutine 23 [running]:
221226
// k8s.io/klog/v2/internal/dbg.Stacks(0x0)
222227
// /nvme/gopath/src/k8s.io/klog/internal/dbg/dbg.go:34 +0x8a
@@ -233,13 +238,13 @@ func TestStop(t *testing.T) {
233238
// >
234239
actual = regexp.MustCompile(`(?m)^\t.*?\n`).ReplaceAllString(actual, ``)
235240

236-
expected := fmt.Sprintf(`testinglogger_test.go:%d] "WARNING: test kept at least one goroutine running after test completion" logger="TestStop/Sub leaked goroutine" callstack=<
237-
>
238-
testinglogger_test.go:%d] "simple info message" logger="TestStop/Sub leaked goroutine"
241+
expected := fmt.Sprintf(`testinglogger_test.go:%d] "simple info message" logger="TestStop/Sub leaked goroutine"
239242
testinglogger_test.go:%d] "error message" logger="TestStop/Sub leaked goroutine"
243+
testinglogger_test.go:%d] "WARNING: test kept at least one goroutine running after test completion" logger="TestStop/Sub leaked goroutine.me" completed=true timeSinceCompletion="<...>s" callstack=<
244+
>
240245
testinglogger_test.go:%d] "complex info message" logger="TestStop/Sub leaked goroutine.me" completed=true anotherValue=1
241246
`,
242-
line+1, line+1, line+2, line+3)
247+
line+1, line+2, line+4, line+4)
243248
if actual != expected {
244249
t.Errorf("Output does not match. Expected:\n%s\nActual:\n%s\n", expected, actual)
245250
}

0 commit comments

Comments
 (0)