Skip to content

Commit 2f82683

Browse files
authored
Merge pull request #56 from radekg/dockertest
Add YugabyteDB Dockertest integration and utilities. #14
2 parents 25b616d + afb3d86 commit 2f82683

File tree

10 files changed

+1372
-3
lines changed

10 files changed

+1372
-3
lines changed

Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.DEFAULT_GOAL := build
22

3-
.PHONY: clean build docker-image git-tag
3+
.PHONY: clean build docker-image test git-tag
44

55
BINARY ?= ybdb-go-cli
66
SOURCES = $(shell find . -name '*.go' | grep -v /vendor/)
@@ -15,10 +15,13 @@ DOCKER_IMAGE_REPO ?= local/
1515
CURRENT_DIR=$(dir $(realpath $(firstword $(MAKEFILE_LIST))))
1616
TAG_VERSION ?= $(shell cat $(CURRENT_DIR)/.version)
1717

18+
TEST_TIMEOUT ?=120s
19+
1820
default: build
1921

2022
test:
21-
go test -v -count=1 ./...
23+
go clean -testcache
24+
go test -timeout ${TEST_TIMEOUT} -cover -v ./...
2225

2326
build: build/$(BINARY)
2427

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ go 1.16
55
require (
66
github.com/google/uuid v1.3.0
77
github.com/hashicorp/go-hclog v0.16.2
8+
// pq used in tests:
9+
github.com/lib/pq v1.9.0
10+
// dockertest/v3 used in tests:
11+
github.com/ory/dockertest/v3 v3.8.1
812
github.com/radekg/yugabyte-db-go-proto/v2 v2.11.0-2
913
github.com/spf13/cobra v1.2.1
1014
github.com/spf13/pflag v1.0.5

go.sum

Lines changed: 83 additions & 1 deletion
Large diffs are not rendered by default.

testutils/README.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# YugabyteDB Test Kit
2+
3+
YugabyteDB for embedding in go tests. Depends on Docker.
4+
5+
## Usage
6+
7+
Individual packages contain tests showing exact usage patterns for your own tests.
8+
9+
## Example
10+
11+
Run three masters with three TServers inside of the test and query YSQL on one of the TServers:
12+
13+
```go
14+
package myprogram
15+
16+
import (
17+
"testing"
18+
"time"
19+
20+
"github.com/hashicorp/go-hclog"
21+
"github.com/radekg/yugabyte-db-go-client/testutils/common"
22+
"github.com/radekg/yugabyte-db-go-client/testutils/master"
23+
"github.com/radekg/yugabyte-db-go-client/testutils/tserver"
24+
"github.com/radekg/yugabyte-db-go-client/client/implementation"
25+
"github.com/radekg/yugabyte-db-go-client/configs"
26+
27+
// Postgres library:
28+
_ "github.com/lib/pq"
29+
)
30+
31+
func TestClusterIntegration(t *testing.T) {
32+
33+
masterTestCtx := master.SetupMasters(t, &common.TestMasterConfiguration{
34+
ReplicationFactor: 3,
35+
MasterPrefix: "mytest",
36+
})
37+
defer masterTestCtx.Cleanup()
38+
39+
client, err := implementation.MasterLeaderConnectedClient(&configs.CliConfig{
40+
MasterHostPort: masterTestCtx.MasterExternalAddresses(),
41+
OpTimeout: time.Duration(time.Second * 5),
42+
}, hclog.Default())
43+
44+
if err != nil {
45+
t.Fatal(err)
46+
}
47+
defer client.Close()
48+
49+
common.Eventually(t, 15, func() error {
50+
listMastersPb, err := client.ListMasters()
51+
if err != nil {
52+
return err
53+
}
54+
t.Log(" ==> Received master list", listMastersPb)
55+
return nil
56+
})
57+
58+
// start a TServer:
59+
tserver1Ctx := tserver.SetupTServer(t, masterTestCtx, &common.TestTServerConfiguration{
60+
TServerID: "my-tserver-1",
61+
})
62+
defer tserver1Ctx.Cleanup()
63+
64+
// start a TServer:
65+
tserver2Ctx := tserver.SetupTServer(t, masterTestCtx, &common.TestTServerConfiguration{
66+
TServerID: "my-tserver-2",
67+
})
68+
defer tserver2Ctx.Cleanup()
69+
70+
// start a TServer:
71+
tserver3Ctx := tserver.SetupTServer(t, masterTestCtx, &common.TestTServerConfiguration{
72+
TServerID: "my-tserver-3",
73+
})
74+
defer tserver3Ctx.Cleanup()
75+
76+
common.Eventually(t, 15, func() error {
77+
listTServersPb, err := client.ListTabletServers(&configs.OpListTabletServersConfig{})
78+
if err != nil {
79+
return err
80+
}
81+
t.Log(" ==> Received TServer list", listTServersPb)
82+
return nil
83+
})
84+
85+
// try YSQL connection:
86+
t.Logf("connecting to YSQL at 127.0.0.1:%s", tserver1Ctx.TServerExternalYSQLPort())
87+
db, sqlOpenErr := sql.Open("postgres", fmt.Sprintf("host=127.0.0.1 port=%s user=%s password=%s dbname=%s sslmode=disable",
88+
tserver1Ctx.TServerExternalYSQLPort(), "yugabyte", "yugabyte", "yugabyte"))
89+
if sqlOpenErr != nil {
90+
t.Fatal("failed connecting to YSQL, reason:", sqlOpenErr)
91+
}
92+
defer db.Close()
93+
t.Log("connected to YSQL")
94+
95+
common.Eventually(t, 15, func() error {
96+
rows, sqlQueryErr := db.Query("select table_name from information_schema.tables")
97+
if sqlQueryErr != nil {
98+
return sqlQueryErr
99+
}
100+
nRows := 0
101+
for {
102+
if !rows.Next() {
103+
break
104+
}
105+
nRows = nRows + 1
106+
}
107+
t.Log("selected", nRows, " rows from YSQL")
108+
return nil
109+
})
110+
111+
}
112+
```

testutils/common/common.go

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package common
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net"
7+
"os"
8+
"strings"
9+
"sync"
10+
"testing"
11+
"time"
12+
13+
"github.com/ory/dockertest/v3"
14+
dc "github.com/ory/dockertest/v3/docker"
15+
)
16+
17+
// GetEnvOrDefault returns the value of an environment variable
18+
// or fallback value, if environment variable is undefined or empty.
19+
func GetEnvOrDefault(key, fallback string) string {
20+
value := os.Getenv(key)
21+
if len(value) == 0 {
22+
return fallback
23+
}
24+
return value
25+
}
26+
27+
// -- Random port supplier.
28+
29+
// RandomPortSupplier wraps the functionality for random port handling in tests.
30+
type RandomPortSupplier interface {
31+
Cleanup()
32+
Discover() error
33+
DiscoveredHost() (string, bool)
34+
DiscoveredPort() (string, bool)
35+
}
36+
37+
type listenerPortSupplier struct {
38+
closed bool
39+
discovered bool
40+
discoveredHost string
41+
discoveredPort string
42+
listener net.Listener
43+
lock *sync.Mutex
44+
}
45+
46+
// NewRandomPortSupplier creates an initialized instance of a random port supplier.
47+
func NewRandomPortSupplier() (RandomPortSupplier, error) {
48+
listener, err := net.Listen("tcp", "127.0.0.1:0")
49+
if err != nil {
50+
return nil, err
51+
}
52+
return &listenerPortSupplier{
53+
lock: &sync.Mutex{},
54+
listener: listener,
55+
}, nil
56+
}
57+
58+
func (l *listenerPortSupplier) Cleanup() {
59+
l.lock.Lock()
60+
defer l.lock.Unlock()
61+
if !l.closed {
62+
l.listener.Close()
63+
l.closed = true
64+
}
65+
}
66+
67+
func (l *listenerPortSupplier) Discover() error {
68+
l.lock.Lock()
69+
defer l.lock.Unlock()
70+
if l.closed {
71+
return errors.New("was-closed")
72+
}
73+
host, port, err := net.SplitHostPort(l.listener.Addr().String())
74+
if err != nil {
75+
return err
76+
}
77+
l.discoveredHost = host
78+
l.discoveredPort = port
79+
l.discovered = true
80+
return nil
81+
}
82+
83+
func (l *listenerPortSupplier) DiscoveredHost() (string, bool) {
84+
l.lock.Lock()
85+
defer l.lock.Unlock()
86+
return l.discoveredHost, l.discovered
87+
}
88+
89+
func (l *listenerPortSupplier) DiscoveredPort() (string, bool) {
90+
l.lock.Lock()
91+
defer l.lock.Unlock()
92+
return l.discoveredPort, l.discovered
93+
}
94+
95+
// WaitForContainerExit0 waits for the container to exist with code 0.
96+
func WaitForContainerExit0(t *testing.T, pool *dockertest.Pool, containerID string) error {
97+
finalState := "not started"
98+
finalStatus := ""
99+
100+
benchMigrateStart := time.Now()
101+
chanSuccess := make(chan struct{}, 1)
102+
chanError := make(chan error, 1)
103+
104+
go func() {
105+
poolRetryErr := pool.Retry(func() error {
106+
containers, _ := pool.Client.ListContainers(dc.ListContainersOptions{All: true})
107+
for _, container := range containers {
108+
if container.ID == containerID {
109+
time.Sleep(time.Millisecond * 50)
110+
if container.State == "running" {
111+
return errors.New("still running")
112+
}
113+
if container.State == "restarting" {
114+
t.Logf("container %s is restarting with status '%s'...", containerID, container.Status)
115+
time.Sleep(time.Second)
116+
continue
117+
}
118+
finalState = container.State
119+
finalStatus = container.Status
120+
return nil
121+
}
122+
}
123+
return errors.New("no container")
124+
})
125+
if poolRetryErr == nil {
126+
close(chanSuccess)
127+
return
128+
}
129+
chanError <- poolRetryErr
130+
}()
131+
132+
select {
133+
case <-chanSuccess:
134+
t.Logf("container %s finished successfully after: %s", containerID, time.Now().Sub(benchMigrateStart).String())
135+
case receivedError := <-chanError:
136+
return receivedError
137+
case <-time.After(time.Second * 10):
138+
return fmt.Errorf("container %s complete within timeout", containerID)
139+
}
140+
141+
if finalState != "exited" {
142+
return fmt.Errorf("expected container %s to be in state exited but received: '%s'", containerID, finalState)
143+
}
144+
// it was exited, ...
145+
if !strings.HasPrefix(strings.ToLower(finalStatus), "exited (0)") {
146+
return fmt.Errorf("expected container %s to exit with status 0, received full exit message: '%s'", containerID, finalStatus)
147+
}
148+
149+
return nil
150+
}
151+
152+
// CompareStringSlices cpmpares if two string slices have the same length and same values.
153+
func CompareStringSlices(t *testing.T, this, that []string) {
154+
if len(this) != len(that) {
155+
t.Fatalf("expected did not match received: '%v' vs '%v'", this, that)
156+
}
157+
for idx, exp := range this {
158+
if exp != that[idx] {
159+
t.Fatalf("expected did not match received at index %d: '%v' vs '%v'", idx, exp, that[idx])
160+
}
161+
}
162+
}
163+
164+
// StringSlicesEqual compares two slices without failing the test.
165+
func StringSlicesEqual(t *testing.T, this, that []string) bool {
166+
if len(this) != len(that) {
167+
return false
168+
}
169+
for idx, exp := range this {
170+
if exp != that[idx] {
171+
return false
172+
}
173+
}
174+
return true
175+
}
176+
177+
// --
178+
179+
// Eventually retries the request maximum number of tries and stops on success, or error.
180+
func Eventually(t *testing.T, maxTimes int, f func() error, messageLabels ...interface{}) {
181+
nTries := 0
182+
for {
183+
if nTries > maxTimes {
184+
t.Fatal(append([]interface{}{fmt.Sprintf("Failed running block %d times", maxTimes)}, messageLabels...)...)
185+
}
186+
if err := f(); err != nil {
187+
nTries = nTries + 1
188+
t.Log(append(messageLabels, err.Error())...)
189+
<-time.After(time.Second)
190+
continue
191+
}
192+
193+
break
194+
}
195+
}

0 commit comments

Comments
 (0)