Skip to content

Commit ecdfe05

Browse files
committed
tests: add helpers for TcS
Simple helpers to make easy create tests required Taranatool centralized configuration storage.
1 parent 5368646 commit ecdfe05

File tree

4 files changed

+342
-5
lines changed

4 files changed

+342
-5
lines changed

test_helpers/main.go

+72-5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ type StartOpts struct {
3333
// InitScript is a Lua script for tarantool to run on start.
3434
InitScript string
3535

36+
// ConfigFile is a path to a configuration file for a Tarantool instance.
37+
// Required in pair with InstanceName.
38+
ConfigFile string
39+
40+
// InstanceName is a name of an instance to run.
41+
// Required in pair with ConfigFile.
42+
InstanceName string
43+
3644
// Listen is box.cfg listen parameter for tarantool.
3745
// Use this address to connect to tarantool after configuration.
3846
// https://www.tarantool.io/en/doc/latest/reference/configuration/#cfg-basic-listen
@@ -77,6 +85,32 @@ type TarantoolInstance struct {
7785

7886
// Dialer to check that connection established.
7987
Dialer tarantool.Dialer
88+
89+
done chan error
90+
is_done bool
91+
result error
92+
}
93+
94+
// Status checks if Tarantool instance is still running.
95+
// Return true if it is running, false if it is not.
96+
// If instance was exit and error is nil - process completed success with zero status code.
97+
func (t *TarantoolInstance) Status() (bool, error) {
98+
if t.is_done {
99+
return false, t.result
100+
}
101+
102+
select {
103+
case t.result = <-t.done:
104+
t.is_done = true
105+
return false, t.result
106+
default:
107+
return true, nil
108+
}
109+
}
110+
111+
func (t *TarantoolInstance) checkDone() {
112+
t.done = make(chan error, 1)
113+
t.done <- t.Cmd.Wait()
80114
}
81115

82116
func isReady(dialer tarantool.Dialer, opts *tarantool.Opts) error {
@@ -108,7 +142,7 @@ var (
108142
)
109143

110144
func init() {
111-
tarantoolVersionRegexp = regexp.MustCompile(`Tarantool (?:Enterprise )?(\d+)\.(\d+)\.(\d+).*`)
145+
tarantoolVersionRegexp = regexp.MustCompile(`Tarantool (Enterprise )?(\d+)\.(\d+)\.(\d+).*`)
112146
}
113147

114148
// atoiUint64 parses string to uint64.
@@ -145,15 +179,15 @@ func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) (
145179
return true, fmt.Errorf("failed to parse output %q", out)
146180
}
147181

148-
if major, err = atoiUint64(parsed[1]); err != nil {
182+
if major, err = atoiUint64(parsed[2]); err != nil {
149183
return true, fmt.Errorf("failed to parse major from output %q: %w", out, err)
150184
}
151185

152-
if minor, err = atoiUint64(parsed[2]); err != nil {
186+
if minor, err = atoiUint64(parsed[3]); err != nil {
153187
return true, fmt.Errorf("failed to parse minor from output %q: %w", out, err)
154188
}
155189

156-
if patch, err = atoiUint64(parsed[3]); err != nil {
190+
if patch, err = atoiUint64(parsed[4]); err != nil {
157191
return true, fmt.Errorf("failed to parse patch from output %q: %w", out, err)
158192
}
159193

@@ -166,6 +200,21 @@ func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) (
166200
}
167201
}
168202

203+
// IsTarantoolEE checks if Tarantool is Enterprise edition.
204+
func IsTarantoolEE() (bool, error) {
205+
out, err := exec.Command(getTarantoolExec(), "--version").Output()
206+
if err != nil {
207+
return true, err
208+
}
209+
210+
parsed := tarantoolVersionRegexp.FindStringSubmatch(string(out))
211+
if parsed == nil {
212+
return true, fmt.Errorf("failed to parse output %q", out)
213+
}
214+
215+
return parsed[1] != "", nil
216+
}
217+
169218
// RestartTarantool restarts a tarantool instance for tests
170219
// with specifies parameters (refer to StartOpts)
171220
// which were specified in inst parameter.
@@ -211,6 +260,7 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) {
211260
}
212261

213262
inst.Cmd = exec.Command(getTarantoolExec(), startOpts.InitScript)
263+
inst.Cmd.Dir = startOpts.WorkDir
214264

215265
inst.Cmd.Env = append(
216266
os.Environ(),
@@ -219,6 +269,11 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) {
219269
fmt.Sprintf("TEST_TNT_MEMTX_USE_MVCC_ENGINE=%t", startOpts.MemtxUseMvccEngine),
220270
fmt.Sprintf("TEST_TNT_AUTH_TYPE=%s", startOpts.Auth),
221271
)
272+
if startOpts.ConfigFile != "" && startOpts.InstanceName != "" {
273+
inst.Cmd.Env = append(inst.Cmd.Env, fmt.Sprintf("TT_CONFIG=%s", startOpts.ConfigFile))
274+
inst.Cmd.Env = append(inst.Cmd.Env,
275+
fmt.Sprintf("TT_INSTANCE_NAME=%s", startOpts.InstanceName))
276+
}
222277

223278
// Copy SSL certificates.
224279
if startOpts.SslCertsDir != "" {
@@ -242,6 +297,8 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) {
242297
// see https://github.com/tarantool/go-tarantool/issues/136
243298
time.Sleep(startOpts.WaitStart)
244299

300+
go inst.checkDone()
301+
245302
opts := tarantool.Opts{
246303
Timeout: 500 * time.Millisecond,
247304
SkipSchema: true,
@@ -261,7 +318,17 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) {
261318
}
262319
}
263320

264-
return inst, err
321+
if err != nil {
322+
StopTarantool(inst)
323+
return TarantoolInstance{}, fmt.Errorf("failed to connect Tarantool: %w", err)
324+
}
325+
326+
working, err := inst.Status()
327+
if !working || err != nil {
328+
StopTarantool(inst)
329+
return TarantoolInstance{}, fmt.Errorf("unexpected terminated Tarantool: %w", err)
330+
}
331+
return inst, nil
265332
}
266333

267334
// StopTarantool stops a tarantool instance started

test_helpers/tcs/prepare.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package tcs
2+
3+
import (
4+
_ "embed"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"time"
9+
10+
"github.com/tarantool/go-tarantool/v2"
11+
"github.com/tarantool/go-tarantool/v2/test_helpers"
12+
)
13+
14+
const (
15+
waitTimeout = 500 * time.Millisecond
16+
connectRetry = 3
17+
TcsUser = "client"
18+
TcsPassword = "secret"
19+
)
20+
21+
//go:embed testdata/config.yaml
22+
var tcsConfig []byte
23+
24+
func makeOpts(port int) (test_helpers.StartOpts, error) {
25+
opts := test_helpers.StartOpts{}
26+
dir, err := os.MkdirTemp("", "tcs_dir")
27+
if err != nil {
28+
return opts, err
29+
}
30+
os.WriteFile(filepath.Join(dir, "config.yaml"), tcsConfig, 0644)
31+
32+
address := fmt.Sprintf("localhost:%d", port)
33+
34+
opts = test_helpers.StartOpts{
35+
ConfigFile: "config.yaml",
36+
WorkDir: dir,
37+
WaitStart: waitTimeout,
38+
ConnectRetry: connectRetry,
39+
RetryTimeout: waitTimeout,
40+
InstanceName: "master",
41+
Listen: address,
42+
Dialer: tarantool.NetDialer{
43+
Address: address,
44+
User: TcsUser,
45+
Password: TcsPassword,
46+
},
47+
}
48+
return opts, nil
49+
}

test_helpers/tcs/tcs.go

+187
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package tcs
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net"
8+
"testing"
9+
10+
"github.com/tarantool/go-tarantool/v2"
11+
"github.com/tarantool/go-tarantool/v2/test_helpers"
12+
)
13+
14+
var ErrNotSupported = errors.New("required Tarantool EE 3.3+")
15+
var ErrNoValue = errors.New("required Value not found")
16+
17+
// TCS is a Tarantool centralized configuration storage connection.
18+
type TCS struct {
19+
inst test_helpers.TarantoolInstance
20+
conn *tarantool.Connection
21+
tb testing.TB
22+
port int
23+
}
24+
25+
// dataResponse content of TcS response in data array.
26+
type dataResponse struct {
27+
Path string `msgpack:"path"`
28+
Value string `msgpack:"value"`
29+
ModRevision int64 `msgpack:"mod_revision"`
30+
}
31+
32+
func isSupported() bool {
33+
if less, err := test_helpers.IsTarantoolEE(); less || err != nil {
34+
return false
35+
}
36+
if less, err := test_helpers.IsTarantoolVersionLess(3, 3, 0); less || err != nil {
37+
return false
38+
}
39+
return true
40+
41+
}
42+
43+
// findEmptyPort returns some random unused port if @port is passed with zero.
44+
// If @port not zero, it checks if the port is free.
45+
func findEmptyPort(port int) (int, error) {
46+
listener, err := net.Listen("tcp4", fmt.Sprintf(":%d", port)) // Bind to port 0
47+
if err != nil {
48+
return 0, err
49+
}
50+
defer listener.Close() // Ensure the listener is closed after use
51+
addr := listener.Addr().(*net.TCPAddr)
52+
return addr.Port, nil // Return the assigned port
53+
}
54+
55+
// Start starts a Tarantool centralized configuration storage.
56+
// Use `port = 0` to use any unused port.
57+
// Returns a Tcs instance and a cleanup function.
58+
func Start(port int) (TCS, error) {
59+
tcs := TCS{}
60+
if !isSupported() {
61+
return tcs, ErrNotSupported
62+
}
63+
var err error
64+
tcs.port, err = findEmptyPort(port)
65+
if err != nil {
66+
if port == 0 {
67+
return tcs, fmt.Errorf("failed to detect an empty port: %w", err)
68+
} else {
69+
return tcs, fmt.Errorf("port %d can't be used: %w", port, err)
70+
}
71+
}
72+
opts, err := makeOpts(tcs.port)
73+
if err != nil {
74+
return tcs, err
75+
}
76+
77+
tcs.inst, err = test_helpers.StartTarantool(opts)
78+
if err != nil {
79+
return tcs, fmt.Errorf("failed to start Tarantool config storage: %w", err)
80+
}
81+
82+
tcs.conn, err = tarantool.Connect(context.Background(), tcs.inst.Dialer, tarantool.Opts{})
83+
if err != nil {
84+
return tcs, fmt.Errorf("failed to connect to Tarantool config storage: %w", err)
85+
}
86+
87+
return tcs, nil
88+
}
89+
90+
// Start starts a Tarantool centralized configuration storage.
91+
// Returns a Tcs instance and a cleanup function.
92+
func StartTesting(tb testing.TB, port int) TCS {
93+
tcs, err := Start(port)
94+
if err != nil {
95+
tb.Fatal(err)
96+
}
97+
return tcs
98+
}
99+
100+
// Doer return interface for interacting with Tarantool.
101+
func (t *TCS) Doer() tarantool.Doer {
102+
return t.conn
103+
}
104+
105+
// Dialer returns a dialer to connect to Tarantool.
106+
func (t *TCS) Dialer() tarantool.Dialer {
107+
return t.inst.Dialer
108+
}
109+
110+
// Endpoints returns a list of addresses to connect.
111+
func (t *TCS) Endpoints() []string {
112+
return []string{fmt.Sprintf("localhost:%d", t.port)}
113+
}
114+
115+
// Credentials returns a user name and password to connect.
116+
func (t *TCS) Credentials() (string, string) {
117+
return TcsUser, TcsPassword
118+
}
119+
120+
// Stop stops the Tarantool centralized configuration storage.
121+
func (t *TCS) Stop() {
122+
if t.tb != nil {
123+
t.tb.Helper()
124+
}
125+
if t.conn != nil {
126+
t.conn.Close()
127+
}
128+
test_helpers.StopTarantoolWithCleanup(t.inst)
129+
}
130+
131+
// Put implements "config.storage.put" method.
132+
func (t *TCS) Put(ctx context.Context, path string, value string) error {
133+
if t.tb != nil {
134+
t.tb.Helper()
135+
}
136+
req := tarantool.NewCallRequest("config.storage.put").
137+
Args([]any{path, value}).
138+
Context(ctx)
139+
if _, err := t.conn.Do(req).GetResponse(); err != nil {
140+
return fmt.Errorf("failed save data to tarantool: %w", err)
141+
}
142+
return nil
143+
}
144+
145+
// Delete implements "config.storage.delete" method.
146+
func (t *TCS) Delete(ctx context.Context, path string) error {
147+
if t.tb != nil {
148+
t.tb.Helper()
149+
}
150+
req := tarantool.NewCallRequest("config.storage.delete").
151+
Args([]any{path}).
152+
Context(ctx)
153+
if _, err := t.conn.Do(req).GetResponse(); err != nil {
154+
return fmt.Errorf("failed delete data from tarantool: %w", err)
155+
}
156+
return nil
157+
}
158+
159+
// Get implements "config.storage.get" method.
160+
func (t *TCS) Get(ctx context.Context, path string) (string, error) {
161+
if t.tb != nil {
162+
t.tb.Helper()
163+
}
164+
req := tarantool.NewCallRequest("config.storage.get").
165+
Args([]any{path}).
166+
Context(ctx)
167+
168+
resp := []struct {
169+
Data []dataResponse `msgpack:"data"`
170+
}{}
171+
172+
err := t.conn.Do(req).GetTyped(&resp)
173+
if err != nil {
174+
return "", fmt.Errorf("failed to fetch data from tarantool: %w", err)
175+
}
176+
if len(resp) != 1 {
177+
return "", errors.New("unexpected response from tarantool")
178+
}
179+
if len(resp[0].Data) == 0 {
180+
return "", ErrNoValue
181+
}
182+
if len(resp[0].Data) != 1 {
183+
return "", errors.New("to many data in response from tarantool")
184+
}
185+
186+
return resp[0].Data[0].Value, nil
187+
}

0 commit comments

Comments
 (0)