...

Source file src/github.com/go-kivik/kivik/v4/kiviktest/kt/kt.go

Documentation: github.com/go-kivik/kivik/v4/kiviktest/kt

     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  }
   375  

View as plain text