1
2
3
4
5
6
7
8
9
10
11
12
13
14 package kt
15
16 import (
17 "context"
18 "errors"
19 "fmt"
20 "io"
21 "math/rand"
22 "net/http"
23 "net/url"
24 "regexp"
25 "strings"
26 "sync"
27 "syscall"
28 "testing"
29 "time"
30
31 "github.com/cenkalti/backoff/v4"
32
33 kivik "github.com/go-kivik/kivik/v4"
34 )
35
36
37 type Context struct {
38
39 RW bool
40
41 Admin *kivik.Client
42
43 NoAuth *kivik.Client
44
45 Config SuiteConfig
46
47 T *testing.T
48 }
49
50
51 func (c *Context) Child(t *testing.T) *Context {
52 t.Helper()
53 return &Context{
54 RW: c.RW,
55 Admin: c.Admin,
56 NoAuth: c.NoAuth,
57 Config: c.Config,
58 T: t,
59 }
60 }
61
62
63 func (c *Context) Skip() {
64 c.T.Helper()
65 if c.Config.Bool(c.T, "skip") {
66 c.T.Skip("Test skipped by suite configuration")
67 }
68 }
69
70
71 func (c *Context) Skipf(format string, args ...interface{}) {
72 c.T.Helper()
73 c.T.Skipf(format, args...)
74 }
75
76
77 func (c *Context) Logf(format string, args ...interface{}) {
78 c.T.Helper()
79 c.T.Logf(format, args...)
80 }
81
82
83 func (c *Context) Fatalf(format string, args ...interface{}) {
84 c.T.Helper()
85 c.T.Fatalf(format, args...)
86 }
87
88
89 func (c *Context) MustBeSet(key string) {
90 c.T.Helper()
91 if !c.IsSet(key) {
92 c.T.Fatalf("'%s' not set. Please configure this test.", key)
93 }
94 }
95
96
97 func (c *Context) MustStringSlice(key string) []string {
98 c.T.Helper()
99 c.MustBeSet(key)
100 return c.StringSlice(key)
101 }
102
103
104 func (c *Context) MustBool(key string) bool {
105 c.T.Helper()
106 c.MustBeSet(key)
107 return c.Bool(key)
108 }
109
110
111 func (c *Context) IntSlice(key string) []int {
112 c.T.Helper()
113 v, _ := c.Config.Interface(c.T, key).([]int)
114 return v
115 }
116
117
118 func (c *Context) MustIntSlice(key string) []int {
119 c.T.Helper()
120 c.MustBeSet(key)
121 return c.IntSlice(key)
122 }
123
124
125 func (c *Context) StringSlice(key string) []string {
126 c.T.Helper()
127 return c.Config.StringSlice(c.T, key)
128 }
129
130
131 func (c *Context) String(key string) string {
132 c.T.Helper()
133 return c.Config.String(c.T, key)
134 }
135
136
137 func (c *Context) MustString(key string) string {
138 c.T.Helper()
139 c.MustBeSet(key)
140 return c.String(key)
141 }
142
143
144 func (c *Context) Int(key string) int {
145 c.T.Helper()
146 return c.Config.Int(c.T, key)
147 }
148
149
150 func (c *Context) MustInt(key string) int {
151 c.T.Helper()
152 c.MustBeSet(key)
153 return c.Int(key)
154 }
155
156
157 func (c *Context) Bool(key string) bool {
158 c.T.Helper()
159 return c.Config.Bool(c.T, key)
160 }
161
162
163 func (c *Context) Interface(key string) interface{} {
164 c.T.Helper()
165 return c.Config.get(name(c.T), key)
166 }
167
168
169 func (c *Context) Options(key string) kivik.Option {
170 c.T.Helper()
171 testName := name(c.T)
172 i := c.Config.get(testName, key)
173 if i == nil {
174 return nil
175 }
176 o, ok := i.(kivik.Option)
177 if !ok {
178 panic(fmt.Sprintf("Options field %s/%s of unsupported type: %T", testName, key, i))
179 }
180 return o
181 }
182
183
184 func (c *Context) MustInterface(key string) interface{} {
185 c.T.Helper()
186 c.MustBeSet(key)
187 return c.Interface(key)
188 }
189
190
191 func (c *Context) IsSet(key string) bool {
192 c.T.Helper()
193 return c.Interface(key) != nil
194 }
195
196
197 func (c *Context) Run(name string, fn testFunc) {
198 c.T.Helper()
199 c.T.Run(name, func(t *testing.T) {
200 c.T.Helper()
201 ctx := c.Child(t)
202 ctx.Skip()
203 fn(ctx)
204 })
205 }
206
207 type testFunc func(*Context)
208
209
210 var tests = make(map[string]testFunc)
211
212
213
214 func Register(name string, fn testFunc) {
215 tests[name] = fn
216 }
217
218
219 func RunSubtests(ctx *Context) {
220 for name, fn := range tests {
221 ctx.Run(name, fn)
222 }
223 }
224
225 var (
226 rnd *rand.Rand
227 rndMU = &sync.Mutex{}
228 )
229
230 func init() {
231 rnd = rand.New(rand.NewSource(time.Now().UnixNano()))
232 }
233
234
235 const TestDBPrefix = "kivik$"
236
237
238
239 func (c *Context) TestDB() string {
240 c.T.Helper()
241 dbname := c.TestDBName()
242 err := Retry(func() error {
243 return c.Admin.CreateDB(context.Background(), dbname, c.Options("db"))
244 })
245 if err != nil {
246 c.Fatalf("Failed to create database: %s", err)
247 }
248 c.T.Cleanup(func() { c.DestroyDB(dbname) })
249 return dbname
250 }
251
252
253
254 func (c *Context) TestDBName() string {
255 return TestDBName(c.T)
256 }
257
258 var invalidDBCharsRE = regexp.MustCompile(`[^a-z0-9_$\(\)+/-]`)
259
260
261
262 func TestDBName(t *testing.T) string {
263 id := strings.ToLower(t.Name())
264 id = invalidDBCharsRE.ReplaceAllString(id, "_")
265 id = id[strings.Index(id, "/")+1:]
266 id = strings.ReplaceAll(id, "/", "_") + "$"
267 rndMU.Lock()
268 dbname := fmt.Sprintf("%s%s%016x", TestDBPrefix, id, rnd.Int63())
269 rndMU.Unlock()
270 return dbname
271 }
272
273
274 func (c *Context) RunAdmin(fn testFunc) {
275 if c.Admin != nil {
276 c.Run("Admin", fn)
277 }
278 }
279
280
281 func (c *Context) RunNoAuth(fn testFunc) {
282 if c.NoAuth != nil {
283 c.Run("NoAuth", fn)
284 }
285 }
286
287
288 func (c *Context) RunRW(fn testFunc) {
289 if c.RW {
290 c.Run("RW", fn)
291 }
292 }
293
294
295
296
297
298 func (c *Context) RunRO(fn testFunc) {
299 if !c.RW {
300 fn(c)
301 }
302 }
303
304
305 func (c *Context) Errorf(format string, args ...interface{}) {
306 c.T.Helper()
307 c.T.Errorf(format, args...)
308 }
309
310
311 func (c *Context) Parallel() {
312 c.T.Parallel()
313 }
314
315 const maxRetries = 5
316
317
318
319 func Retry(fn func() error) error {
320 bo := backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries)
321 var i int
322 return backoff.Retry(func() error {
323 err := fn()
324 if err != nil {
325 if shouldRetry(err) {
326 fmt.Printf("Retrying after error: %s\n", err)
327 i++
328 return fmt.Errorf("attempt #%d failed: %w", i, err)
329 }
330 return backoff.Permanent(err)
331 }
332 return nil
333 }, bo)
334 }
335
336 func shouldRetry(err error) bool {
337 if err == nil {
338 return false
339 }
340 var statusErr interface {
341 error
342 HTTPStatus() int
343 }
344 if errors.As(err, &statusErr) {
345 if status := statusErr.HTTPStatus(); status < http.StatusInternalServerError {
346 return false
347 }
348 }
349 var errno syscall.Errno
350 if errors.As(err, &errno) {
351 switch errno {
352 case syscall.ECONNRESET, syscall.EPIPE:
353 return true
354 }
355 }
356 urlErr := new(url.Error)
357 if errors.As(err, &urlErr) {
358
359 msg := strings.TrimSpace(urlErr.Error())
360 return strings.HasSuffix(msg, ": connection reset by peer") ||
361 strings.HasSuffix(msg, ": broken pipe")
362 }
363 return false
364
365
366
367
368 }
369
370
371
372 func Body(str string, args ...interface{}) io.ReadCloser {
373 return io.NopCloser(strings.NewReader(fmt.Sprintf(str, args...)))
374 }
375
View as plain text