github.com/go-kivik/kivik/v4@v4.3.2/kiviktest/kt/kt.go (about) 1 // Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 // use this file except in compliance with the License. You may obtain a copy of 3 // the License at 4 // 5 // http://www.apache.org/licenses/LICENSE-2.0 6 // 7 // Unless required by applicable law or agreed to in writing, software 8 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 // License for the specific language governing permissions and limitations under 11 // the License. 12 13 // Package kt provides common utilities for Kivik tests. 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 // Context is a collection of client connections with different security access. 37 type Context struct { 38 // RW is true if we should run read-write tests. 39 RW bool 40 // Admin is a client connection with database admin privileges. 41 Admin *kivik.Client 42 // NoAuth isa client connection with no authentication. 43 NoAuth *kivik.Client 44 // Config is the suite config 45 Config SuiteConfig 46 // T is the *testing.T value 47 T *testing.T 48 } 49 50 // Child returns a shallow copy of itself with a new t. 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 // Skip will skip the currently running test if configuration dictates. 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 // Skipf is a wrapper around t.Skipf() 71 func (c *Context) Skipf(format string, args ...interface{}) { 72 c.T.Helper() 73 c.T.Skipf(format, args...) 74 } 75 76 // Logf is a wrapper around t.Logf() 77 func (c *Context) Logf(format string, args ...interface{}) { 78 c.T.Helper() 79 c.T.Logf(format, args...) 80 } 81 82 // Fatalf is a wrapper around t.Fatalf() 83 func (c *Context) Fatalf(format string, args ...interface{}) { 84 c.T.Helper() 85 c.T.Fatalf(format, args...) 86 } 87 88 // MustBeSet ends the test with a failure if the config key is not set. 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 // MustStringSlice returns a string slice, or fails if the value is unset. 97 func (c *Context) MustStringSlice(key string) []string { 98 c.T.Helper() 99 c.MustBeSet(key) 100 return c.StringSlice(key) 101 } 102 103 // MustBool returns a bool, or fails if the value is unset. 104 func (c *Context) MustBool(key string) bool { 105 c.T.Helper() 106 c.MustBeSet(key) 107 return c.Bool(key) 108 } 109 110 // IntSlice returns a []int from config. 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 // MustIntSlice returns a []int, or fails if the value is unset. 118 func (c *Context) MustIntSlice(key string) []int { 119 c.T.Helper() 120 c.MustBeSet(key) 121 return c.IntSlice(key) 122 } 123 124 // StringSlice returns a string slice from the config. 125 func (c *Context) StringSlice(key string) []string { 126 c.T.Helper() 127 return c.Config.StringSlice(c.T, key) 128 } 129 130 // String returns a string from config. 131 func (c *Context) String(key string) string { 132 c.T.Helper() 133 return c.Config.String(c.T, key) 134 } 135 136 // MustString returns a string from config, or fails if the value is unset. 137 func (c *Context) MustString(key string) string { 138 c.T.Helper() 139 c.MustBeSet(key) 140 return c.String(key) 141 } 142 143 // Int returns an int from the config. 144 func (c *Context) Int(key string) int { 145 c.T.Helper() 146 return c.Config.Int(c.T, key) 147 } 148 149 // MustInt returns an int from the config, or fails if the value is unset. 150 func (c *Context) MustInt(key string) int { 151 c.T.Helper() 152 c.MustBeSet(key) 153 return c.Int(key) 154 } 155 156 // Bool returns a bool from the config. 157 func (c *Context) Bool(key string) bool { 158 c.T.Helper() 159 return c.Config.Bool(c.T, key) 160 } 161 162 // Interface returns the configuration value as an interface{}. 163 func (c *Context) Interface(key string) interface{} { 164 c.T.Helper() 165 return c.Config.get(name(c.T), key) 166 } 167 168 // Options returns an options map value. 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 // MustInterface returns an interface{} from the config, or fails if the value is unset. 184 func (c *Context) MustInterface(key string) interface{} { 185 c.T.Helper() 186 c.MustBeSet(key) 187 return c.Interface(key) 188 } 189 190 // IsSet returns true if the value is set in the configuration. 191 func (c *Context) IsSet(key string) bool { 192 c.T.Helper() 193 return c.Interface(key) != nil 194 } 195 196 // Run wraps t.Run() 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 // tests is a map of the format map[suite]map[name]testFunc 210 var tests = make(map[string]testFunc) 211 212 // Register registers a test to be run for the given test suite. rw should 213 // be true if the test writes to the database. 214 func Register(name string, fn testFunc) { 215 tests[name] = fn 216 } 217 218 // RunSubtests executes the requested suites of tests against the client. 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 // TestDBPrefix is used to prefix temporary database names during tests. 235 const TestDBPrefix = "kivik$" 236 237 // TestDB creates a test database, regesters a cleanup function to destroy it, 238 // and returns its name. 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 // TestDBName generates a randomized string suitable for a database name for 253 // testing. 254 func (c *Context) TestDBName() string { 255 return TestDBName(c.T) 256 } 257 258 var invalidDBCharsRE = regexp.MustCompile(`[^a-z0-9_$\(\)+/-]`) 259 260 // TestDBName generates a randomized string suitable for a database name for 261 // testing. 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 // RunAdmin runs the test function iff c.Admin is set. 274 func (c *Context) RunAdmin(fn testFunc) { 275 if c.Admin != nil { 276 c.Run("Admin", fn) 277 } 278 } 279 280 // RunNoAuth runs the test function iff c.NoAuth is set. 281 func (c *Context) RunNoAuth(fn testFunc) { 282 if c.NoAuth != nil { 283 c.Run("NoAuth", fn) 284 } 285 } 286 287 // RunRW runs the test function iff c.RW is true. 288 func (c *Context) RunRW(fn testFunc) { 289 if c.RW { 290 c.Run("RW", fn) 291 } 292 } 293 294 // RunRO runs the test function iff c.RW is false. Note that unlike RunRW, this 295 // does not start a new subtest. This should usually be run in conjunction with 296 // RunRW, to run only RO or RW tests, in situations where running both would be 297 // redundant. 298 func (c *Context) RunRO(fn testFunc) { 299 if !c.RW { 300 fn(c) 301 } 302 } 303 304 // Errorf is a wrapper around t.Errorf() 305 func (c *Context) Errorf(format string, args ...interface{}) { 306 c.T.Helper() 307 c.T.Errorf(format, args...) 308 } 309 310 // Parallel is a wrapper around t.Parallel() 311 func (c *Context) Parallel() { 312 c.T.Parallel() 313 } 314 315 const maxRetries = 5 316 317 // Retry will try an operation up to maxRetries times, in case of one of the 318 // following failures. All other failures are returned. 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 // Seems string comparison is necessary in some cases. 359 msg := strings.TrimSpace(urlErr.Error()) 360 return strings.HasSuffix(msg, ": connection reset by peer") || // Observed in Go 1.18 361 strings.HasSuffix(msg, ": broken pipe") // Observed in Go 1.19 & 1.17 362 } 363 return false 364 // msg := strings.TrimSpace(err.Error()) 365 // return strings.HasSuffix(msg, "io: read/write on closed pipe") || 366 // strings.HasSuffix(msg, ": EOF") || 367 // strings.HasSuffix(msg, ": http: server closed idle connection") 368 } 369 370 // Body turns a string into a read closer, useful as a request or attachment 371 // body. 372 func Body(str string, args ...interface{}) io.ReadCloser { 373 return io.NopCloser(strings.NewReader(fmt.Sprintf(str, args...))) 374 }