Skip to content

Commit 4cb4228

Browse files
committed
ktesting: improve unit test coverage
In particular ExpectNoError needed testing, as it was unused so far and not functional in its initial implementation.
1 parent 4ffa628 commit 4cb4228

File tree

7 files changed

+385
-37
lines changed

7 files changed

+385
-37
lines changed

test/utils/ktesting/assert_test.go

Lines changed: 106 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,14 @@ package ktesting
1919
import (
2020
"errors"
2121
"fmt"
22-
"regexp"
2322
"testing"
2423
"time"
2524

2625
"github.com/onsi/gomega"
27-
"github.com/stretchr/testify/assert"
2826
)
2927

30-
func TestAsync(t *testing.T) {
31-
for name, tc := range map[string]struct {
32-
cb func(TContext)
33-
expectNoFail bool
34-
expectError string
35-
expectDuration time.Duration
36-
}{
28+
func TestAssert(t *testing.T) {
29+
for name, tc := range map[string]testcase{
3730
"eventually-timeout": {
3831
cb: func(tCtx TContext) {
3932
Eventually(tCtx, func(tCtx TContext) int {
@@ -165,30 +158,114 @@ The function passed to Consistently returned the following error:
165158
expectError: `Timed out while waiting on TryAgainAfter after x.y s.
166159
told to try again after 1ms: intermittent error`,
167160
},
161+
162+
"expect-equal": {
163+
cb: func(tCtx TContext) {
164+
tCtx.Expect(1).To(gomega.Equal(42))
165+
},
166+
expectError: `Expected
167+
<int>: 1
168+
to equal
169+
<int>: 42`,
170+
},
171+
172+
"expect-no-error-success": {
173+
cb: func(tCtx TContext) {
174+
tCtx.ExpectNoError(nil)
175+
},
176+
expectNoFail: true,
177+
},
178+
"expect-no-error-normal-error": {
179+
cb: func(tCtx TContext) {
180+
tCtx.ExpectNoError(errors.New("fake error"))
181+
},
182+
expectError: `Unexpected error: fake error`,
183+
expectLog: `<klog header>: Unexpected error:
184+
<*errors.errorString | 0xXXXX>:
185+
fake error
186+
{s: "fake error"}
187+
`,
188+
},
189+
"expect-no-error-failure": {
190+
cb: func(tCtx TContext) {
191+
tCtx.ExpectNoError(fmt.Errorf("doing something: %w", FailureError{Msg: "fake error"}))
192+
},
193+
expectError: `doing something: fake error`,
194+
},
195+
"expect-no-error-explanation-string": {
196+
cb: func(tCtx TContext) {
197+
tCtx.ExpectNoError(fmt.Errorf("doing something: %w", FailureError{Msg: "fake error"}), "testing error checking")
198+
},
199+
expectError: `testing error checking: doing something: fake error`,
200+
},
201+
"expect-no-error-explanation-printf": {
202+
cb: func(tCtx TContext) {
203+
tCtx.ExpectNoError(fmt.Errorf("doing something: %w", FailureError{Msg: "fake error"}), "testing %s %d checking", "error", 42)
204+
},
205+
expectError: `testing error 42 checking: doing something: fake error`,
206+
},
207+
"expect-no-error-explanation-callback": {
208+
cb: func(tCtx TContext) {
209+
tCtx.ExpectNoError(fmt.Errorf("doing something: %w", FailureError{Msg: "fake error"}), func() string { return "testing error checking" })
210+
},
211+
expectError: `testing error checking: doing something: fake error`,
212+
},
213+
"expect-no-error-backtrace": {
214+
cb: func(tCtx TContext) {
215+
tCtx.ExpectNoError(fmt.Errorf("doing something: %w", FailureError{Msg: "fake error", FullStackTrace: "abc\nxyz"}))
216+
},
217+
expectError: `doing something: fake error`,
218+
expectLog: `<klog header>: Failed at:
219+
abc
220+
xyz
221+
`,
222+
},
223+
"expect-no-error-backtrace-and-explanation": {
224+
cb: func(tCtx TContext) {
225+
tCtx.ExpectNoError(fmt.Errorf("doing something: %w", FailureError{Msg: "fake error", FullStackTrace: "abc\nxyz"}), "testing error checking")
226+
},
227+
expectError: `testing error checking: doing something: fake error`,
228+
expectLog: `<klog header>: testing error checking
229+
<klog header>: Failed at:
230+
abc
231+
xyz
232+
`,
233+
},
234+
235+
"output": {
236+
cb: func(tCtx TContext) {
237+
tCtx.Log("Log", "a", "b", 42)
238+
tCtx.Logf("Logf %s %s %d", "a", "b", 42)
239+
tCtx.Error("Error", "a", "b", 42)
240+
tCtx.Errorf("Errorf %s %s %d", "a", "b", 42)
241+
},
242+
expectLog: `<klog header>: Log a b 42
243+
<klog header>: Logf a b 42
244+
`,
245+
expectError: `Error a b 42
246+
Errorf a b 42`,
247+
},
248+
"fatal": {
249+
cb: func(tCtx TContext) {
250+
tCtx.Fatal("Error", "a", "b", 42)
251+
// not reached
252+
tCtx.Log("Log")
253+
},
254+
expectError: `Error a b 42`,
255+
},
256+
"fatalf": {
257+
cb: func(tCtx TContext) {
258+
tCtx.Fatalf("Error %s %s %d", "a", "b", 42)
259+
// not reached
260+
tCtx.Log("Log")
261+
},
262+
expectError: `Error a b 42`,
263+
},
168264
} {
169265
tc := tc
170266
t.Run(name, func(t *testing.T) {
171267
t.Parallel()
172-
tCtx := Init(t)
173-
var err error
174-
tCtx, finalize := WithError(tCtx, &err)
175-
start := time.Now()
176-
func() {
177-
defer finalize()
178-
tc.cb(tCtx)
179-
}()
180-
duration := time.Since(start)
181-
assert.InDelta(t, tc.expectDuration.Seconds(), duration.Seconds(), 0.1, fmt.Sprintf("callback invocation duration %s", duration))
182-
assert.Equal(t, !tc.expectNoFail, tCtx.Failed(), "Failed()")
183-
if tc.expectError == "" {
184-
assert.NoError(t, err)
185-
} else if assert.NotNil(t, err) {
186-
t.Logf("Result:\n%s", err.Error())
187-
errMsg := err.Error()
188-
errMsg = regexp.MustCompile(`[[:digit:]]+\.[[:digit:]]+s`).ReplaceAllString(errMsg, "x.y s")
189-
errMsg = regexp.MustCompile(`0x[[:xdigit:]]+`).ReplaceAllString(errMsg, "0xXXXX")
190-
assert.Equal(t, tc.expectError, errMsg)
191-
}
268+
tc.run(t)
192269
})
193270
}
194271
}

test/utils/ktesting/helper_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package ktesting
18+
19+
import (
20+
"fmt"
21+
"regexp"
22+
"strings"
23+
"testing"
24+
"time"
25+
26+
"github.com/stretchr/testify/assert"
27+
)
28+
29+
// testcase wraps a callback which is called with a TContext that intercepts
30+
// errors and log output. Those get compared.
31+
type testcase struct {
32+
cb func(TContext)
33+
expectNoFail bool
34+
expectError string
35+
expectDuration time.Duration
36+
expectLog string
37+
}
38+
39+
func (tc testcase) run(t *testing.T) {
40+
bufferT := &logBufferT{T: t}
41+
tCtx := Init(bufferT)
42+
var err error
43+
tCtx, finalize := WithError(tCtx, &err)
44+
start := time.Now()
45+
func() {
46+
defer finalize()
47+
tc.cb(tCtx)
48+
}()
49+
50+
log := bufferT.log.String()
51+
t.Logf("Log output:\n%s\n", log)
52+
if tc.expectLog != "" {
53+
assert.Equal(t, tc.expectLog, normalize(log))
54+
} else if log != "" {
55+
t.Error("Expected no log output.")
56+
}
57+
58+
duration := time.Since(start)
59+
assert.InDelta(t, tc.expectDuration.Seconds(), duration.Seconds(), 0.1, fmt.Sprintf("callback invocation duration %s", duration))
60+
assert.Equal(t, !tc.expectNoFail, tCtx.Failed(), "Failed()")
61+
if tc.expectError == "" {
62+
assert.NoError(t, err)
63+
} else if assert.NotNil(t, err) {
64+
t.Logf("Result:\n%s", err.Error())
65+
assert.Equal(t, tc.expectError, normalize(err.Error()))
66+
}
67+
}
68+
69+
// normalize replaces parts of message texts which may vary with constant strings.
70+
func normalize(msg string) string {
71+
// duration
72+
msg = regexp.MustCompile(`[[:digit:]]+\.[[:digit:]]+s`).ReplaceAllString(msg, "x.y s")
73+
// hex pointer value
74+
msg = regexp.MustCompile(`0x[[:xdigit:]]+`).ReplaceAllString(msg, "0xXXXX")
75+
// per-test klog header
76+
msg = regexp.MustCompile(`[EI][[:digit:]]{4} [[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}\.[[:digit:]]{6}\]`).ReplaceAllString(msg, "<klog header>:")
77+
return msg
78+
}
79+
80+
type logBufferT struct {
81+
*testing.T
82+
log strings.Builder
83+
}
84+
85+
func (l *logBufferT) Log(args ...any) {
86+
l.log.WriteString(fmt.Sprintln(args...))
87+
}
88+
89+
func (l *logBufferT) Logf(format string, args ...any) {
90+
l.log.WriteString(fmt.Sprintf(format, args...))
91+
l.log.WriteRune('\n')
92+
}

test/utils/ktesting/main_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package ktesting
18+
19+
import (
20+
"flag"
21+
"fmt"
22+
"os"
23+
"testing"
24+
25+
"go.uber.org/goleak"
26+
)
27+
28+
func TestMain(m *testing.M) {
29+
// Bail out early when -help was given as parameter.
30+
flag.Parse()
31+
32+
// Must be called *before* creating new goroutines.
33+
goleakOpts := []goleak.Option{
34+
goleak.IgnoreCurrent(),
35+
}
36+
37+
result := m.Run()
38+
39+
if err := goleak.Find(goleakOpts...); err != nil {
40+
fmt.Fprintf(os.Stderr, "leaked Goroutines: %v", err)
41+
os.Exit(1)
42+
}
43+
44+
os.Exit(result)
45+
}

test/utils/ktesting/signals.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ var (
3030
interruptCtx context.Context
3131

3232
defaultProgressReporter = new(progressReporter)
33+
defaultSignalChannel chan os.Signal
3334
)
3435

3536
const ginkgoSpecContextKey = "GINKGO_SPEC_CONTEXT"
@@ -57,25 +58,35 @@ func init() {
5758
// probably cannot be in either Ginkgo or Gomega).
5859
interruptCtx = context.WithValue(cancelCtx, ginkgoSpecContextKey, defaultProgressReporter)
5960

60-
signalChannel := make(chan os.Signal, 1)
61+
defaultSignalChannel = make(chan os.Signal, 1)
6162
// progressSignals will be empty on Windows.
6263
if len(progressSignals) > 0 {
63-
signal.Notify(signalChannel, progressSignals...)
64+
signal.Notify(defaultSignalChannel, progressSignals...)
6465
}
6566

6667
// os.Stderr gets redirected by "go test". "go test -v" has to be
6768
// used to see the output while a test runs.
68-
go defaultProgressReporter.run(interruptCtx, os.Stderr, signalChannel)
69+
defaultProgressReporter.setOutput(os.Stderr)
70+
go defaultProgressReporter.run(interruptCtx, defaultSignalChannel)
6971
}
7072

7173
type progressReporter struct {
7274
mutex sync.Mutex
7375
reporterCounter int64
7476
reporters map[int64]func() string
77+
out io.Writer
7578
}
7679

7780
var _ ginkgoReporter = &progressReporter{}
7881

82+
func (p *progressReporter) setOutput(out io.Writer) io.Writer {
83+
p.mutex.Lock()
84+
defer p.mutex.Unlock()
85+
oldOut := p.out
86+
p.out = out
87+
return oldOut
88+
}
89+
7990
// AttachProgressReporter implements Gomega's contextWithAttachProgressReporter.
8091
func (p *progressReporter) AttachProgressReporter(reporter func() string) func() {
8192
p.mutex.Lock()
@@ -100,13 +111,13 @@ func (p *progressReporter) detachProgressReporter(id int64) {
100111
delete(p.reporters, id)
101112
}
102113

103-
func (p *progressReporter) run(ctx context.Context, out io.Writer, progressSignalChannel chan os.Signal) {
114+
func (p *progressReporter) run(ctx context.Context, progressSignalChannel chan os.Signal) {
104115
for {
105116
select {
106117
case <-ctx.Done():
107118
return
108119
case <-progressSignalChannel:
109-
p.dumpProgress(out)
120+
p.dumpProgress()
110121
}
111122
}
112123
}
@@ -117,7 +128,7 @@ func (p *progressReporter) run(ctx context.Context, out io.Writer, progressSigna
117128
//
118129
// But perhaps dumping goroutines and their callstacks is useful anyway? TODO:
119130
// look at how Ginkgo does it and replicate some of it.
120-
func (p *progressReporter) dumpProgress(out io.Writer) {
131+
func (p *progressReporter) dumpProgress() {
121132
p.mutex.Lock()
122133
defer p.mutex.Unlock()
123134

@@ -135,5 +146,5 @@ func (p *progressReporter) dumpProgress(out io.Writer) {
135146
}
136147
}
137148

138-
_, _ = out.Write([]byte(buffer.String()))
149+
_, _ = p.out.Write([]byte(buffer.String()))
139150
}

test/utils/ktesting/stepcontext.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func (sCtx *stepContext) Fatalf(format string, args ...any) {
7676
sCtx.TContext.Fatal(sCtx.what + ": " + strings.TrimSpace(fmt.Sprintf(format, args...)))
7777
}
7878

79-
// Value intercepts a search for the special
79+
// Value intercepts a search for the special "GINKGO_SPEC_CONTEXT".
8080
func (sCtx *stepContext) Value(key any) any {
8181
if s, ok := key.(string); ok && s == ginkgoSpecContextKey {
8282
if reporter, ok := sCtx.TContext.Value(key).(ginkgoReporter); ok {

0 commit comments

Comments
 (0)