Skip to content

Commit 2c22a16

Browse files
authored
test(all): add Retry func to testutil from samples repository (#4902)
* test(all): add retry functions to testutil
1 parent f91a0c3 commit 2c22a16

2 files changed

Lines changed: 192 additions & 0 deletions

File tree

internal/testutil/retry.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package testutil
16+
17+
import (
18+
"bytes"
19+
"fmt"
20+
"path/filepath"
21+
"runtime"
22+
"strconv"
23+
"testing"
24+
"time"
25+
)
26+
27+
// Retry runs function f for up to maxAttempts times until f returns successfully, and reports whether f was run successfully.
28+
// It will sleep for the given period between invocations of f.
29+
// Use the provided *testutil.R instead of a *testing.T from the function.
30+
func Retry(t *testing.T, maxAttempts int, sleep time.Duration, f func(r *R)) bool {
31+
for attempt := 1; attempt <= maxAttempts; attempt++ {
32+
r := &R{Attempt: attempt, log: &bytes.Buffer{}}
33+
34+
f(r)
35+
36+
if !r.failed {
37+
if r.log.Len() != 0 {
38+
t.Logf("Success after %d attempts:%s", attempt, r.log.String())
39+
}
40+
return true
41+
}
42+
43+
if attempt == maxAttempts {
44+
t.Logf("FAILED after %d attempts:%s", attempt, r.log.String())
45+
t.Fail()
46+
}
47+
48+
time.Sleep(sleep)
49+
}
50+
return false
51+
}
52+
53+
// RetryWithoutTest is a variant of Retry that does not use a testing parameter.
54+
// It is meant for testing utilities that do not pass around the testing context, such as cloudrunci.
55+
func RetryWithoutTest(maxAttempts int, sleep time.Duration, f func(r *R)) bool {
56+
for attempt := 1; attempt <= maxAttempts; attempt++ {
57+
r := &R{Attempt: attempt, log: &bytes.Buffer{}}
58+
59+
f(r)
60+
61+
if !r.failed {
62+
if r.log.Len() != 0 {
63+
r.Logf("Success after %d attempts:%s", attempt, r.log.String())
64+
}
65+
return true
66+
}
67+
68+
if attempt == maxAttempts {
69+
r.Logf("FAILED after %d attempts:%s", attempt, r.log.String())
70+
return false
71+
}
72+
73+
time.Sleep(sleep)
74+
}
75+
return false
76+
}
77+
78+
// R is passed to each run of a flaky test run, manages state and accumulates log statements.
79+
type R struct {
80+
// The number of current attempt.
81+
Attempt int
82+
83+
failed bool
84+
log *bytes.Buffer
85+
}
86+
87+
// Fail marks the run as failed, and will retry once the function returns.
88+
func (r *R) Fail() {
89+
r.failed = true
90+
}
91+
92+
// Errorf is equivalent to Logf followed by Fail.
93+
func (r *R) Errorf(s string, v ...interface{}) {
94+
r.logf(s, v...)
95+
r.Fail()
96+
}
97+
98+
// Logf formats its arguments and records it in the error log.
99+
// The text is only printed for the final unsuccessful run or the first successful run.
100+
func (r *R) Logf(s string, v ...interface{}) {
101+
r.logf(s, v...)
102+
}
103+
104+
func (r *R) logf(s string, v ...interface{}) {
105+
fmt.Fprint(r.log, "\n")
106+
fmt.Fprint(r.log, lineNumber())
107+
fmt.Fprintf(r.log, s, v...)
108+
}
109+
110+
func lineNumber() string {
111+
_, file, line, ok := runtime.Caller(3) // logf, public func, user function
112+
if !ok {
113+
return ""
114+
}
115+
return filepath.Base(file) + ":" + strconv.Itoa(line) + ": "
116+
}

internal/testutil/retry_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package testutil
16+
17+
import (
18+
"testing"
19+
"time"
20+
)
21+
22+
func TestRetry(t *testing.T) {
23+
Retry(t, 5, time.Millisecond, func(r *R) {
24+
if r.Attempt == 2 {
25+
return
26+
}
27+
r.Fail()
28+
})
29+
}
30+
31+
func TestRetryAttempts(t *testing.T) {
32+
var attempts int
33+
Retry(t, 10, time.Millisecond, func(r *R) {
34+
r.Logf("This line should appear only once.")
35+
r.Logf("attempt=%d", r.Attempt)
36+
attempts = r.Attempt
37+
38+
// Retry 5 times.
39+
if r.Attempt == 5 {
40+
return
41+
}
42+
r.Fail()
43+
})
44+
45+
if attempts != 5 {
46+
t.Errorf("attempts=%d; want %d", attempts, 5)
47+
}
48+
}
49+
50+
func TestRetryWithoutTest(t *testing.T) {
51+
RetryWithoutTest(5, time.Millisecond, func(r *R) {
52+
if r.Attempt == 2 {
53+
return
54+
}
55+
r.Fail()
56+
})
57+
}
58+
59+
func TestRetryWithoutTestAttempts(t *testing.T) {
60+
var attempts int
61+
RetryWithoutTest(10, time.Millisecond, func(r *R) {
62+
r.Logf("This line should appear only once.")
63+
r.Logf("attempt=%d", r.Attempt)
64+
attempts = r.Attempt
65+
66+
// Retry 5 times.
67+
if r.Attempt == 5 {
68+
return
69+
}
70+
r.Fail()
71+
})
72+
73+
if attempts != 5 {
74+
t.Errorf("attempts=%d; want %d", attempts, 5)
75+
}
76+
}

0 commit comments

Comments
 (0)