...

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

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

     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 kiviktest
    14  
    15  import (
    16  	"context"
    17  	"errors"
    18  	"flag"
    19  	"fmt"
    20  	"net/http"
    21  	"net/url"
    22  	"os"
    23  	"strings"
    24  	"sync"
    25  	"sync/atomic"
    26  	"testing"
    27  
    28  	"github.com/go-kivik/kivik/v4"
    29  	_ "github.com/go-kivik/kivik/v4/kiviktest/client" // Tests
    30  	_ "github.com/go-kivik/kivik/v4/kiviktest/db"     // Tests
    31  	"github.com/go-kivik/kivik/v4/kiviktest/kt"
    32  )
    33  
    34  // The available test suites
    35  const (
    36  	SuiteAuto        = "auto"
    37  	SuitePouchLocal  = "pouch"
    38  	SuitePouchRemote = "pouchRemote"
    39  	SuiteCouch22     = "couch22"
    40  	SuiteCouch23     = "couch23"
    41  	SuiteCouch30     = "couch30"
    42  	SuiteCouch31     = "couch31"
    43  	SuiteCouch32     = "couch32"
    44  	SuiteCouch33     = "couch33"
    45  	SuiteKivikServer = "kivikServer"
    46  	SuiteKivikMemory = "kivikMemory"
    47  	SuiteKivikSQLite = "kivikSQLite"
    48  	SuiteKivikFS     = "kivikFilesystem"
    49  )
    50  
    51  // AllSuites is a list of all defined suites.
    52  var AllSuites = []string{
    53  	SuitePouchLocal,
    54  	SuitePouchRemote,
    55  	SuiteCouch22,
    56  	SuiteCouch30,
    57  	SuiteCouch31,
    58  	SuiteCouch32,
    59  	SuiteCouch33,
    60  	SuiteKivikMemory,
    61  	SuiteKivikFS,
    62  	SuiteKivikSQLite,
    63  	SuiteKivikServer,
    64  }
    65  
    66  var driverMap = map[string]string{
    67  	SuitePouchLocal:  "pouch",
    68  	SuitePouchRemote: "pouch",
    69  	SuiteCouch22:     "couch",
    70  	SuiteCouch23:     "couch",
    71  	SuiteCouch30:     "couch",
    72  	SuiteCouch31:     "couch",
    73  	SuiteCouch32:     "couch",
    74  	SuiteCouch33:     "couch",
    75  	SuiteKivikServer: "couch",
    76  	SuiteKivikMemory: "memory",
    77  	SuiteKivikSQLite: "sqlite",
    78  	SuiteKivikFS:     "fs",
    79  }
    80  
    81  // ListTests prints a list of available test suites to stdout.
    82  func ListTests() {
    83  	fmt.Printf("Available test suites:\n\tauto\n")
    84  	for _, suite := range AllSuites {
    85  		fmt.Printf("\t%s\n", suite)
    86  	}
    87  }
    88  
    89  // Options are the options to run a test from the command line tool.
    90  type Options struct {
    91  	Driver  string
    92  	DSN     string
    93  	Verbose bool
    94  	RW      bool
    95  	Match   string
    96  	Suites  []string
    97  	Cleanup bool
    98  }
    99  
   100  // CleanupTests attempts to clean up any stray test databases created by a
   101  // previous test run.
   102  func CleanupTests(driver, dsn string, verbose bool) error {
   103  	client, err := kivik.New(driver, dsn)
   104  	if err != nil {
   105  		return err
   106  	}
   107  	count, err := doCleanup(client, verbose)
   108  	if verbose {
   109  		fmt.Printf("Deleted %d test databases\n", count)
   110  	}
   111  	return err
   112  }
   113  
   114  func doCleanup(client *kivik.Client, verbose bool) (int, error) {
   115  	ctx, cancel := context.WithCancel(context.Background())
   116  	defer cancel()
   117  	const chanCap = 3
   118  	errCh := make(chan error, chanCap)
   119  	var count int32
   120  	var wg sync.WaitGroup
   121  
   122  	wg.Add(1)
   123  	go func() {
   124  		defer wg.Done()
   125  		c, err := cleanupDatabases(ctx, client, verbose)
   126  		if err != nil {
   127  			cancel()
   128  		}
   129  		atomic.AddInt32(&count, int32(c))
   130  		errCh <- err
   131  	}()
   132  
   133  	wg.Add(1)
   134  	go func() {
   135  		defer wg.Done()
   136  		c, err := cleanupUsers(ctx, client, verbose)
   137  		if err != nil {
   138  			cancel()
   139  		}
   140  		atomic.AddInt32(&count, int32(c))
   141  		errCh <- err
   142  	}()
   143  
   144  	wg.Add(1)
   145  	go func() {
   146  		defer wg.Done()
   147  		c, err := cleanupReplications(ctx, client, verbose)
   148  		if err != nil {
   149  			cancel()
   150  		}
   151  		atomic.AddInt32(&count, int32(c))
   152  		errCh <- err
   153  	}()
   154  
   155  	wg.Wait()
   156  	err := <-errCh
   157  	for len(errCh) > 0 {
   158  		<-errCh
   159  	}
   160  	return int(count), err
   161  }
   162  
   163  func cleanupDatabases(ctx context.Context, client *kivik.Client, verbose bool) (int, error) {
   164  	if verbose {
   165  		fmt.Printf("Cleaning up stale databases\n")
   166  	}
   167  	allDBs, err := client.AllDBs(ctx)
   168  	if err != nil {
   169  		return 0, err
   170  	}
   171  	var count int
   172  	for _, dbName := range allDBs {
   173  		// FIXME: This filtering should be possible in AllDBs(), but all the
   174  		// backends need to support it first.
   175  		if strings.HasPrefix(dbName, kt.TestDBPrefix) {
   176  			if verbose {
   177  				fmt.Printf("\t--- Deleting %s\n", dbName)
   178  			}
   179  			if e := client.DestroyDB(ctx, dbName); e != nil && kivik.HTTPStatus(e) != http.StatusNotFound {
   180  				return count, e
   181  			}
   182  			count++
   183  		}
   184  	}
   185  	replicator := client.DB("_replicator")
   186  	if e := replicator.Err(); e != nil {
   187  		if kivik.HTTPStatus(e) != http.StatusNotFound && kivik.HTTPStatus(e) != http.StatusNotImplemented {
   188  			return count, e
   189  		}
   190  		return count, nil
   191  	}
   192  	docs := replicator.AllDocs(context.Background(), kivik.IncludeDocs())
   193  	if err := docs.Err(); err != nil {
   194  		if kivik.HTTPStatus(err) == http.StatusNotImplemented || kivik.HTTPStatus(err) == http.StatusNotFound {
   195  			return count, nil
   196  		}
   197  		return count, err
   198  	}
   199  	var replDoc struct {
   200  		Rev string `json:"_rev"`
   201  	}
   202  	for docs.Next() {
   203  		id, _ := docs.ID()
   204  		if strings.HasPrefix(id, "kivik$") {
   205  			if err := docs.ScanDoc(&replDoc); err != nil {
   206  				return count, err
   207  			}
   208  			if _, err := replicator.Delete(context.Background(), id, replDoc.Rev); err != nil {
   209  				return count, err
   210  			}
   211  			count++
   212  		}
   213  	}
   214  	return count, nil
   215  }
   216  
   217  func cleanupUsers(ctx context.Context, client *kivik.Client, verbose bool) (int, error) {
   218  	if verbose {
   219  		fmt.Printf("Cleaning up stale users\n")
   220  	}
   221  	db := client.DB("_users")
   222  	if err := db.Err(); err != nil {
   223  		switch kivik.HTTPStatus(err) {
   224  		case http.StatusNotFound, http.StatusNotImplemented:
   225  			return 0, nil
   226  		}
   227  		return 0, err
   228  	}
   229  	users := db.AllDocs(ctx, kivik.IncludeDocs())
   230  	if err := users.Err(); err != nil {
   231  		switch kivik.HTTPStatus(err) {
   232  		case http.StatusNotFound, http.StatusNotImplemented:
   233  			return 0, nil
   234  		}
   235  		return 0, err
   236  	}
   237  	var count int
   238  	for users.Next() {
   239  		id, _ := users.ID()
   240  		if strings.HasPrefix(id, "org.couchdb.user:kivik$") {
   241  			if verbose {
   242  				fmt.Printf("\t--- Deleting user %s\n", id)
   243  			}
   244  			var doc struct {
   245  				Rev string `json:"_rev"`
   246  			}
   247  			if err := users.ScanDoc(&doc); err != nil {
   248  				return count, err
   249  			}
   250  			if _, err := db.Delete(ctx, id, doc.Rev); err != nil {
   251  				return count, err
   252  			}
   253  			count++
   254  		}
   255  	}
   256  	return count, users.Err()
   257  }
   258  
   259  func cleanupReplications(ctx context.Context, client *kivik.Client, verbose bool) (int, error) {
   260  	if verbose {
   261  		fmt.Printf("Cleaning up stale replications\n")
   262  	}
   263  	db := client.DB("_replicator")
   264  	if err := db.Err(); err != nil {
   265  		switch kivik.HTTPStatus(err) {
   266  		case http.StatusNotFound, http.StatusNotImplemented:
   267  			return 0, nil
   268  		}
   269  		return 0, err
   270  	}
   271  	reps := db.AllDocs(ctx, kivik.IncludeDocs())
   272  	if err := reps.Err(); err != nil {
   273  		switch kivik.HTTPStatus(err) {
   274  		case http.StatusNotFound, http.StatusNotImplemented:
   275  			return 0, nil
   276  		}
   277  		return 0, err
   278  	}
   279  	var count int
   280  	for reps.Next() {
   281  		var doc struct {
   282  			Rev    string `json:"_rev"`
   283  			Source string `json:"source"`
   284  			Target string `json:"target"`
   285  		}
   286  		if err := reps.ScanDoc(&doc); err != nil {
   287  			return count, err
   288  		}
   289  		id, _ := reps.ID()
   290  		if strings.HasPrefix(id, "kivik$") ||
   291  			strings.HasPrefix(doc.Source, "kivik$") ||
   292  			strings.HasPrefix(doc.Target, "kivik$") {
   293  			if verbose {
   294  				fmt.Printf("\t--- Deleting replication %s\n", id)
   295  			}
   296  			if _, err := db.Delete(ctx, id, doc.Rev); err != nil {
   297  				return count, err
   298  			}
   299  			count++
   300  		}
   301  	}
   302  	return count, reps.Err()
   303  }
   304  
   305  // RunTests runs the requested test suites against the requested driver and DSN.
   306  func RunTests(opts Options) {
   307  	if opts.Cleanup {
   308  		err := CleanupTests(opts.Driver, opts.DSN, opts.Verbose)
   309  		if err != nil {
   310  			fmt.Printf("Cleanup failed: %s\n", err)
   311  			os.Exit(1)
   312  		}
   313  		os.Exit(0)
   314  	}
   315  	_ = flag.Set("test.run", opts.Match)
   316  	if opts.Verbose {
   317  		_ = flag.Set("test.v", "true")
   318  	}
   319  	tests := []testing.InternalTest{
   320  		{
   321  			Name: "MainTest",
   322  			F: func(t *testing.T) { //nolint:thelper // Not a helper
   323  				Test(t, opts.Driver, opts.DSN, opts.Suites, opts.RW)
   324  			},
   325  		},
   326  	}
   327  
   328  	mainStart(tests)
   329  }
   330  
   331  // Test is the main test entry point when running tests through the command line
   332  // tool.
   333  func Test(t *testing.T, driver, dsn string, testSuites []string, rw bool) {
   334  	clients, err := ConnectClients(t, driver, dsn, nil)
   335  	if err != nil {
   336  		t.Fatalf("Failed to connect to %s (%s driver): %s\n", dsn, driver, err)
   337  	}
   338  	clients.RW = rw
   339  	tests := make(map[string]struct{})
   340  	for _, test := range testSuites {
   341  		tests[test] = struct{}{}
   342  	}
   343  	if _, ok := tests[SuiteAuto]; ok {
   344  		t.Log("Detecting target service compatibility...")
   345  		suites, err := detectCompatibility(clients.Admin)
   346  		if err != nil {
   347  			t.Fatalf("Unable to determine server suite compatibility: %s\n", err)
   348  		}
   349  		tests = make(map[string]struct{})
   350  		for _, suite := range suites {
   351  			tests[suite] = struct{}{}
   352  		}
   353  	}
   354  	testSuites = make([]string, 0, len(tests))
   355  	for test := range tests {
   356  		testSuites = append(testSuites, test)
   357  	}
   358  	t.Logf("Running the following test suites: %s\n", strings.Join(testSuites, ", "))
   359  	for _, suite := range testSuites {
   360  		RunTestsInternal(clients, suite)
   361  	}
   362  }
   363  
   364  // RunTestsInternal is for internal use only.
   365  func RunTestsInternal(ctx *kt.Context, suite string) {
   366  	conf, ok := suites[suite]
   367  	if !ok {
   368  		ctx.Skipf("No configuration found for suite '%s'", suite)
   369  	}
   370  	ctx.Config = conf
   371  	// This is run as a sub-test so configuration will work nicely.
   372  	ctx.Run("PreCleanup", func(ctx *kt.Context) {
   373  		ctx.RunAdmin(func(ctx *kt.Context) {
   374  			count, err := doCleanup(ctx.Admin, true)
   375  			if count > 0 {
   376  				ctx.Logf("Pre-cleanup removed %d databases from previous test runs", count)
   377  			}
   378  			if err != nil {
   379  				ctx.Fatalf("Pre-cleanup failed: %s", err)
   380  			}
   381  		})
   382  	})
   383  	kt.RunSubtests(ctx)
   384  }
   385  
   386  func detectCompatibility(client *kivik.Client) ([]string, error) {
   387  	info, err := client.Version(context.Background())
   388  	if err != nil {
   389  		return nil, err
   390  	}
   391  	switch info.Vendor {
   392  	case "PouchDB":
   393  		return []string{SuitePouchLocal}, nil
   394  	case "Kivik Memory Adaptor":
   395  		return []string{SuiteKivikMemory}, nil
   396  	}
   397  	return []string{}, errors.New("Unable to automatically determine the proper test suite")
   398  }
   399  
   400  // ConnectClients connects clients.
   401  func ConnectClients(t *testing.T, driverName, dsn string, opts kivik.Option) (*kt.Context, error) {
   402  	t.Helper()
   403  	var noAuthDSN string
   404  	if parsed, err := url.Parse(dsn); err == nil {
   405  		if parsed.User == nil {
   406  			return nil, errors.New("DSN does not contain authentication credentials")
   407  		}
   408  		parsed.User = nil
   409  		noAuthDSN = parsed.String()
   410  	}
   411  	clients := &kt.Context{
   412  		T: t,
   413  	}
   414  	t.Logf("Connecting to %s ...\n", dsn)
   415  	if client, err := kivik.New(driverName, dsn, opts); err == nil {
   416  		clients.Admin = client
   417  	} else {
   418  		return nil, err
   419  	}
   420  
   421  	t.Logf("Connecting to %s ...\n", noAuthDSN)
   422  	if client, err := kivik.New(driverName, noAuthDSN, opts); err == nil {
   423  		clients.NoAuth = client
   424  	} else {
   425  		return nil, err
   426  	}
   427  	return clients, nil
   428  }
   429  
   430  // DoTest runs a suite of tests.
   431  func DoTest(t *testing.T, suite, envName string) { //nolint:thelper // Not a helper
   432  	opts, _ := suites[suite].Interface(t, "Options").(kivik.Option)
   433  
   434  	dsn := os.Getenv(envName)
   435  	if dsn == "" {
   436  		t.Skipf("%s: %s DSN not set; skipping tests", envName, suite)
   437  	}
   438  	clients, err := ConnectClients(t, driverMap[suite], dsn, opts)
   439  	if err != nil {
   440  		t.Errorf("Failed to connect to %s: %s\n", suite, err)
   441  		return
   442  	}
   443  	clients.RW = true
   444  	RunTestsInternal(clients, suite)
   445  }
   446  

View as plain text