github.com/go-kivik/kivik/v4@v4.3.2/kiviktest/test.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 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 }