diff --git a/.github/workflows/reusable_testing.yml b/.github/workflows/reusable_testing.yml index 54cd379c6..987d1d734 100644 --- a/.github/workflows/reusable_testing.yml +++ b/.github/workflows/reusable_testing.yml @@ -38,36 +38,8 @@ jobs: with: go-version: 1.13 - - name: Run base tests - run: | - mkdir snap xlog - TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) - go clean -testcache && go test -v - kill $TNT_PID - - - name: Run queue tests - working-directory: ./queue - run: | - mkdir snap xlog - tarantoolctl rocks install queue 1.1.0 - TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) - go clean -testcache && go test -v - kill $TNT_PID + - name: Install test dependencies + run: make deps - - name: Run uuid tests - working-directory: ./uuid - run: | - mkdir snap xlog - TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) - go clean -testcache && go test -v - kill $TNT_PID - if: ${{ !startsWith(env.TNT_VERSION, 'Tarantool 1.10') }} - - - name: Run multi tests - working-directory: ./multi - run: | - mkdir -p m1/{snap,xlog} m2/{snap,xlog} - TNT_PID_1=$(tarantool ./config_m1.lua > tarantool_m1.log 2>&1 & echo $!) - TNT_PID_2=$(tarantool ./config_m2.lua > tarantool_m2.log 2>&1 & echo $!) - go clean -testcache && go test -v - kill $TNT_PID_1 $TNT_PID_2 + - name: Run tests + run: make test diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 68cf3d6b0..a2d99cfa2 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -39,38 +39,8 @@ jobs: with: go-version: 1.13 - - name: Run base tests - run: | - mkdir snap xlog - TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) - go clean -testcache && go test -v - kill $TNT_PID + - name: Install test dependencies + run: make deps - - name: Run queue tests - working-directory: ./queue - run: | - rm -rf snap - rm -rf xlog - mkdir snap xlog - tarantoolctl rocks install queue 1.1.0 - TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) - go clean -testcache && go test -v - kill $TNT_PID - - - name: Run uuid tests - working-directory: ./uuid - run: | - mkdir snap xlog - TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) - go clean -testcache && go test -v - kill $TNT_PID - if: ${{ matrix.tarantool != 1.10 }} - - - name: Run multi tests - working-directory: ./multi - run: | - mkdir -p m1/{snap,xlog} m2/{snap,xlog} - TNT_PID_1=$(tarantool ./config_m1.lua > tarantool_m1.log 2>&1 & echo $!) - TNT_PID_2=$(tarantool ./config_m2.lua > tarantool_m2.log 2>&1 & echo $!) - go clean -testcache && go test -v - kill $TNT_PID_1 $TNT_PID_2 + - name: Run tests + run: make test diff --git a/.gitignore b/.gitignore index 8fcb790f1..8958050e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ *.DS_Store *.swp .idea/ -snap -xlog +work_dir* +.rocks diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 000000000..4e8998348 --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,28 @@ +redefined = false + +globals = { + 'box', + 'utf8', + 'checkers', + '_TARANTOOL' +} + +include_files = { + '**/*.lua', + '*.luacheckrc', + '*.rockspec' +} + +exclude_files = { + '**/*.rocks/' +} + +max_line_length = 120 + +ignore = { + "212/self", -- Unused argument . + "411", -- Redefining a local variable. + "421", -- Shadowing a local variable. + "431", -- Shadowing an upvalue. + "432", -- Shadowing an upvalue argument. +} diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..ec51cca52 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +SHELL := /bin/bash + +.PHONY: clean +clean: + ( cd ./queue; rm -rf .rocks ) + +.PHONY: deps +deps: clean + ( cd ./queue; tarantoolctl rocks install queue 1.1.0 ) + +.PHONY: test +test: + go clean -testcache + go test ./... -v -p 1 diff --git a/config.lua b/config.lua index c2a9ae966..2528e7b81 100644 --- a/config.lua +++ b/config.lua @@ -1,62 +1,66 @@ +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. box.cfg{ - listen = 3013, - wal_dir='xlog', - snap_dir='snap', + work_dir = os.getenv("TEST_TNT_WORK_DIR"), } box.once("init", function() -local s = box.schema.space.create('test', { - id = 512, - if_not_exists = true, -}) -s:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) - -local st = box.schema.space.create('schematest', { - id = 514, - temporary = true, - if_not_exists = true, - field_count = 7, - format = { - {name = "name0", type = "unsigned"}, - {name = "name1", type = "unsigned"}, - {name = "name2", type = "string"}, - {name = "name3", type = "unsigned"}, - {name = "name4", type = "unsigned"}, - {name = "name5", type = "string"}, - }, -}) -st:create_index('primary', { - type = 'hash', - parts = {1, 'uint'}, - unique = true, - if_not_exists = true, -}) -st:create_index('secondary', { - id = 3, - type = 'tree', - unique = false, - parts = { 2, 'uint', 3, 'string' }, - if_not_exists = true, -}) -st:truncate() - ---box.schema.user.grant('guest', 'read,write,execute', 'universe') -box.schema.func.create('box.info') -box.schema.func.create('simple_incr') - --- auth testing: access control -box.schema.user.create('test', {password = 'test'}) -box.schema.user.grant('test', 'execute', 'universe') -box.schema.user.grant('test', 'read,write', 'space', 'test') -box.schema.user.grant('test', 'read,write', 'space', 'schematest') + local s = box.schema.space.create('test', { + id = 512, + if_not_exists = true, + }) + s:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) + + local st = box.schema.space.create('schematest', { + id = 514, + temporary = true, + if_not_exists = true, + field_count = 7, + format = { + {name = "name0", type = "unsigned"}, + {name = "name1", type = "unsigned"}, + {name = "name2", type = "string"}, + {name = "name3", type = "unsigned"}, + {name = "name4", type = "unsigned"}, + {name = "name5", type = "string"}, + }, + }) + st:create_index('primary', { + type = 'hash', + parts = {1, 'uint'}, + unique = true, + if_not_exists = true, + }) + st:create_index('secondary', { + id = 3, + type = 'tree', + unique = false, + parts = { 2, 'uint', 3, 'string' }, + if_not_exists = true, + }) + st:truncate() + + --box.schema.user.grant('guest', 'read,write,execute', 'universe') + box.schema.func.create('box.info') + box.schema.func.create('simple_incr') + + -- auth testing: access control + box.schema.user.create('test', {password = 'test'}) + box.schema.user.grant('test', 'execute', 'universe') + box.schema.user.grant('test', 'read,write', 'space', 'test') + box.schema.user.grant('test', 'read,write', 'space', 'schematest') end) -function simple_incr(a) - return a+1 +local function simple_incr(a) + return a + 1 end +rawset(_G, 'simple_incr', simple_incr) box.space.test:truncate() -local console = require 'console' -console.listen '0.0.0.0:33015' --box.schema.user.revoke('guest', 'read,write,execute', 'universe') + +-- Set listen only when every other thing is configured. +box.cfg{ + listen = os.getenv("TEST_TNT_LISTEN"), +} diff --git a/multi/config.lua b/multi/config.lua new file mode 100644 index 000000000..25b0eb4f9 --- /dev/null +++ b/multi/config.lua @@ -0,0 +1,21 @@ +local nodes_load = require("config_load_nodes") + +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. +box.cfg{ + work_dir = os.getenv("TEST_TNT_WORK_DIR"), +} + +-- Function to call for getting address list, part of tarantool/multi API. +local get_cluster_nodes = nodes_load.get_cluster_nodes +rawset(_G, 'get_cluster_nodes', get_cluster_nodes) + +box.once("init", function() + box.schema.user.create('test', { password = 'test' }) + box.schema.user.grant('test', 'read,write,execute', 'universe') +end) + +-- Set listen only when every other thing is configured. +box.cfg{ + listen = os.getenv("TEST_TNT_LISTEN"), +} diff --git a/multi/config_m1.lua b/multi/config_m1.lua deleted file mode 100644 index 5e364b5cc..000000000 --- a/multi/config_m1.lua +++ /dev/null @@ -1,14 +0,0 @@ -local nodes_load = require("config_load_nodes") - -box.cfg { - listen = 3013, - wal_dir = 'm1/xlog', - snap_dir = 'm1/snap', -} - -get_cluster_nodes = nodes_load.get_cluster_nodes - -box.once("init", function() - box.schema.user.create('test', { password = 'test' }) - box.schema.user.grant('test', 'read,write,execute', 'universe') -end) diff --git a/multi/config_m2.lua b/multi/config_m2.lua deleted file mode 100644 index cf73da319..000000000 --- a/multi/config_m2.lua +++ /dev/null @@ -1,14 +0,0 @@ -local nodes_load = require("config_load_nodes") - -box.cfg { - listen = 3014, - wal_dir = 'm2/xlog', - snap_dir = 'm2/snap', -} - -get_cluster_nodes = nodes_load.get_cluster_nodes - -box.once("init", function() - box.schema.user.create('test', { password = 'test' }) - box.schema.user.grant('test', 'read,write,execute', 'universe') -end) diff --git a/multi/multi_test.go b/multi/multi_test.go index 4515c6232..65ae439f8 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -1,10 +1,13 @@ package multi import ( + "log" + "os" "testing" "time" "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/test_helpers" ) var server1 = "127.0.0.1:3013" @@ -204,3 +207,54 @@ func TestRefresh(t *testing.T) { t.Error("Expect to get data after reconnect") } } + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + initScript := "config.lua" + waitStart := 100 * time.Millisecond + var connectRetry uint = 3 + retryTimeout := 500 * time.Millisecond + + inst1, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: initScript, + Listen: server1, + WorkDir: "work_dir1", + User: connOpts.User, + Pass: connOpts.Pass, + WaitStart: waitStart, + ConnectRetry: connectRetry, + RetryTimeout: retryTimeout, + }) + defer test_helpers.StopTarantoolWithCleanup(inst1) + + if err != nil { + log.Fatalf("Failed to prepare test tarantool: %s", err) + } + + inst2, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: initScript, + Listen: server2, + WorkDir: "work_dir2", + User: connOpts.User, + Pass: connOpts.Pass, + WaitStart: waitStart, + ConnectRetry: connectRetry, + RetryTimeout: retryTimeout, + }) + defer test_helpers.StopTarantoolWithCleanup(inst2) + + if err != nil { + log.Fatalf("Failed to prepare test tarantool: %s", err) + } + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} diff --git a/queue/config.lua b/queue/config.lua index 57aef4d20..cb64f4df8 100644 --- a/queue/config.lua +++ b/queue/config.lua @@ -1,47 +1,53 @@ -queue = require 'queue' +local queue = require('queue') +rawset(_G, 'queue', queue) +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. box.cfg{ - listen = 3013, - wal_dir='xlog', - snap_dir='snap', + work_dir = os.getenv("TEST_TNT_WORK_DIR"), } -box.once("init", function() -box.schema.user.create('test', {password = 'test'}) -box.schema.func.create('queue.tube.test_queue:ack') -box.schema.func.create('queue.tube.test_queue:put') -box.schema.func.create('queue.tube.test_queue:drop') -box.schema.func.create('queue.tube.test_queue:peek') -box.schema.func.create('queue.tube.test_queue:kick') -box.schema.func.create('queue.tube.test_queue:take') -box.schema.func.create('queue.tube.test_queue:delete') -box.schema.func.create('queue.tube.test_queue:release') -box.schema.func.create('queue.tube.test_queue:bury') -box.schema.func.create('queue.statistics') -box.schema.user.grant('test', 'create', 'space') -box.schema.user.grant('test', 'write', 'space', '_schema') -box.schema.user.grant('test', 'write', 'space', '_space') -box.schema.user.grant('test', 'read,write', 'space', '_space_sequence') -box.schema.user.grant('test', 'write', 'space', '_index') -box.schema.user.grant('test', 'read, write', 'space', '_queue_session_ids') -box.schema.user.grant('test', 'execute', 'universe') -box.schema.user.grant('test', 'read,write', 'space', '_queue') -box.schema.user.grant('test', 'read,write', 'space', '_schema') -box.schema.user.grant('test', 'read,write', 'space', '_space') -box.schema.user.grant('test', 'read,write', 'space', '_index') -box.schema.user.grant('test', 'read,write', 'space', '_queue_consumers') -box.schema.user.grant('test', 'read,write', 'space', '_priv') -box.schema.user.grant('test', 'read,write', 'space', '_queue_taken_2') -if box.space._trigger ~= nil then - box.schema.user.grant('test', 'read', 'space', '_trigger') -end -if box.space._fk_constraint ~= nil then - box.schema.user.grant('test', 'read', 'space', '_fk_constraint') -end -if box.space._ck_constraint ~= nil then - box.schema.user.grant('test', 'read', 'space', '_ck_constraint') -end -if box.space._func_index ~= nil then - box.schema.user.grant('test', 'read', 'space', '_func_index') -end + box.once("init", function() + box.schema.user.create('test', {password = 'test'}) + box.schema.func.create('queue.tube.test_queue:ack') + box.schema.func.create('queue.tube.test_queue:put') + box.schema.func.create('queue.tube.test_queue:drop') + box.schema.func.create('queue.tube.test_queue:peek') + box.schema.func.create('queue.tube.test_queue:kick') + box.schema.func.create('queue.tube.test_queue:take') + box.schema.func.create('queue.tube.test_queue:delete') + box.schema.func.create('queue.tube.test_queue:release') + box.schema.func.create('queue.tube.test_queue:bury') + box.schema.func.create('queue.statistics') + box.schema.user.grant('test', 'create', 'space') + box.schema.user.grant('test', 'write', 'space', '_schema') + box.schema.user.grant('test', 'write', 'space', '_space') + box.schema.user.grant('test', 'read,write', 'space', '_space_sequence') + box.schema.user.grant('test', 'write', 'space', '_index') + box.schema.user.grant('test', 'read, write', 'space', '_queue_session_ids') + box.schema.user.grant('test', 'execute', 'universe') + box.schema.user.grant('test', 'read,write', 'space', '_queue') + box.schema.user.grant('test', 'read,write', 'space', '_schema') + box.schema.user.grant('test', 'read,write', 'space', '_space') + box.schema.user.grant('test', 'read,write', 'space', '_index') + box.schema.user.grant('test', 'read,write', 'space', '_queue_consumers') + box.schema.user.grant('test', 'read,write', 'space', '_priv') + box.schema.user.grant('test', 'read,write', 'space', '_queue_taken_2') + if box.space._trigger ~= nil then + box.schema.user.grant('test', 'read', 'space', '_trigger') + end + if box.space._fk_constraint ~= nil then + box.schema.user.grant('test', 'read', 'space', '_fk_constraint') + end + if box.space._ck_constraint ~= nil then + box.schema.user.grant('test', 'read', 'space', '_ck_constraint') + end + if box.space._func_index ~= nil then + box.schema.user.grant('test', 'read', 'space', '_func_index') + end end) + +-- Set listen only when every other thing is configured. +box.cfg{ + listen = os.getenv("TEST_TNT_LISTEN"), +} diff --git a/queue/queue_test.go b/queue/queue_test.go index 4419e12eb..db531eb4c 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -2,12 +2,15 @@ package queue_test import ( "fmt" + "log" "math" + "os" "testing" "time" . "github.com/tarantool/go-tarantool" "github.com/tarantool/go-tarantool/queue" + "github.com/tarantool/go-tarantool/test_helpers" "gopkg.in/vmihailenco/msgpack.v2" ) @@ -818,3 +821,33 @@ func TestUtube_Put(t *testing.T) { t.Fatalf("Blocking time is less than expected: actual = %.2fs, expected = 1s", end.Sub(start).Seconds()) } } + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: "config.lua", + Listen: server, + WorkDir: "work_dir", + User: opts.User, + Pass: opts.Pass, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, + }) + defer test_helpers.StopTarantoolWithCleanup(inst) + + if err != nil { + log.Fatalf("Failed to prepare test tarantool: %s", err) + } + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} diff --git a/smallbuf.go b/smallbuf.go index c1e3cf740..27b79e864 100644 --- a/smallbuf.go +++ b/smallbuf.go @@ -52,9 +52,9 @@ func (s *smallBuf) Bytes() []byte { } type smallWBuf struct { - b []byte + b []byte sum uint - n uint + n uint } func (s *smallWBuf) Write(b []byte) (int, error) { @@ -85,11 +85,11 @@ func (s *smallWBuf) Trunc(n int) { } func (s *smallWBuf) Reset() { - s.sum = uint(uint64(s.sum) * 15 / 16) + uint(len(s.b)) + s.sum = uint(uint64(s.sum)*15/16) + uint(len(s.b)) if s.n < 16 { s.n++ } - if cap(s.b) > 1024 && s.sum / s.n < uint(cap(s.b))/4 { + if cap(s.b) > 1024 && s.sum/s.n < uint(cap(s.b))/4 { s.b = make([]byte, 0, s.sum/s.n) } else { s.b = s.b[:0] diff --git a/tarantool_test.go b/tarantool_test.go index ae725fdc7..41bdfe830 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -2,12 +2,15 @@ package tarantool_test import ( "fmt" + "log" + "os" "strings" "sync" "testing" "time" . "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/test_helpers" "gopkg.in/vmihailenco/msgpack.v2" ) @@ -1005,3 +1008,33 @@ func TestComplexStructs(t *testing.T) { return } } + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: "config.lua", + Listen: server, + WorkDir: "work_dir", + User: opts.User, + Pass: opts.Pass, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, + }) + defer test_helpers.StopTarantoolWithCleanup(inst) + + if err != nil { + log.Fatalf("Failed to prepare test tarantool: %s", err) + } + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} diff --git a/test_helpers/main.go b/test_helpers/main.go new file mode 100644 index 000000000..e5c73bfe5 --- /dev/null +++ b/test_helpers/main.go @@ -0,0 +1,234 @@ +package test_helpers + +import ( + "errors" + "fmt" + "log" + "os" + "os/exec" + "regexp" + "strconv" + "time" + + "github.com/tarantool/go-tarantool" +) + +type StartOpts struct { + // InitScript is a Lua script for tarantool to run on start. + InitScript string + + // Listen is box.cfg listen parameter for tarantool. + // Use this address to connect to tarantool after configuration. + // https://www.tarantool.io/en/doc/latest/reference/configuration/#cfg-basic-listen + Listen string + + // WorkDir is box.cfg work_dir parameter for tarantool. + // Specify folder to store tarantool data files. + // Folder must be unique for each tarantool process used simultaneously. + // https://www.tarantool.io/en/doc/latest/reference/configuration/#confval-work_dir + WorkDir string + + // User is a username used to connect to tarantool. + // All required grants must be given in InitScript. + User string + + // Pass is a password for specified User. + Pass string + + // WaitStart is a time to wait before starting to ping tarantool. + WaitStart time.Duration + + // ConnectRetry is a count of attempts to ping tarantool. + ConnectRetry uint + + // RetryTimeout is a time between tarantool ping retries. + RetryTimeout time.Duration +} + +// TarantoolInstance is a data for instance graceful shutdown and cleanup. +type TarantoolInstance struct { + // Cmd is a Tarantool command. Used to kill Tarantool process. + Cmd *exec.Cmd + + // WorkDir is a directory with tarantool data. Cleaned up after run. + WorkDir string +} + +func isReady(server string, opts *tarantool.Opts) error { + var err error + var conn *tarantool.Connection + var resp *tarantool.Response + + conn, err = tarantool.Connect(server, *opts) + if err != nil { + return err + } + if conn == nil { + return errors.New("Conn is nil after connect") + } + defer conn.Close() + + resp, err = conn.Ping() + if err != nil { + return err + } + if resp == nil { + return errors.New("Response is nil after ping") + } + + return nil +} + +var ( + // Used to extract Tarantool version (major.minor.patch). + tarantoolVersionRegexp *regexp.Regexp +) + +func init() { + tarantoolVersionRegexp = regexp.MustCompile(`Tarantool (?:Enterprise )?(\d+)\.(\d+)\.(\d+).*`) +} + +// atoiUint64 parses string to uint64. +func atoiUint64(str string) (uint64, error) { + res, err := strconv.ParseUint(str, 10, 64) + if err != nil { + return 0, fmt.Errorf("cast to number error (%s)", err) + } + return res, nil +} + +// IsTarantoolVersionLess checks if tarantool version is less +// than passed . Returns error if failed +// to extract version. +func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) (bool, error) { + var major, minor, patch uint64 + + out, err := exec.Command("tarantool", "--version").Output() + + if err != nil { + return true, err + } + + parsed := tarantoolVersionRegexp.FindStringSubmatch(string(out)) + + if parsed == nil { + return true, errors.New("regexp parse failed") + } + + if major, err = atoiUint64(parsed[1]); err != nil { + return true, fmt.Errorf("failed to parse major: %s", err) + } + + if minor, err = atoiUint64(parsed[2]); err != nil { + return true, fmt.Errorf("failed to parse minor: %s", err) + } + + if patch, err = atoiUint64(parsed[3]); err != nil { + return true, fmt.Errorf("failed to parse patch: %s", err) + } + + if major != majorMin { + return major < majorMin, nil + } else if minor != minorMin { + return minor < minorMin, nil + } else { + return patch < patchMin, nil + } + + return false, nil +} + +// StartTarantool starts a tarantool instance for tests +// with specifies parameters (refer to StartOpts). +// Process must be stopped with StopTarantool. +func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { + // Prepare tarantool command. + var inst TarantoolInstance + inst.Cmd = exec.Command("tarantool", startOpts.InitScript) + + inst.Cmd.Env = append( + os.Environ(), + fmt.Sprintf("TEST_TNT_WORK_DIR=%s", startOpts.WorkDir), + fmt.Sprintf("TEST_TNT_LISTEN=%s", startOpts.Listen), + ) + + // Clean up existing work_dir. + err := os.RemoveAll(startOpts.WorkDir) + if err != nil { + return inst, err + } + + // Create work_dir. + err = os.Mkdir(startOpts.WorkDir, 0755) + if err != nil { + return inst, err + } + + inst.WorkDir = startOpts.WorkDir + + // Start tarantool. + err = inst.Cmd.Start() + if err != nil { + return inst, err + } + + // Try to connect and ping tarantool. + // Using reconnect opts do not help on Connect, + // see https://github.com/tarantool/go-tarantool/issues/136 + time.Sleep(startOpts.WaitStart) + + opts := tarantool.Opts{ + Timeout: 500 * time.Millisecond, + User: startOpts.User, + Pass: startOpts.Pass, + SkipSchema: true, + } + + var i uint + for i = 0; i <= startOpts.ConnectRetry; i++ { + err = isReady(startOpts.Listen, &opts) + + // Both connect and ping is ok. + if err == nil { + break + } + + if i != startOpts.ConnectRetry { + time.Sleep(startOpts.RetryTimeout) + } + } + + return inst, err +} + +// StopTarantool stops a tarantool instance started +// with StartTarantool. Waits until any resources +// associated with the process is released. If something went wrong, fails. +func StopTarantool(inst TarantoolInstance) { + if inst.Cmd != nil && inst.Cmd.Process != nil { + if err := inst.Cmd.Process.Kill(); err != nil { + log.Fatalf("Failed to kill tarantool (pid %d), got %s", inst.Cmd.Process.Pid, err) + } + + // Wait releases any resources associated with the Process. + if _, err := inst.Cmd.Process.Wait(); err != nil { + log.Fatalf("Failed to wait for Tarantool process to exit, got %s", err) + } + + inst.Cmd = nil + } +} + +// StopTarantoolWithCleanup stops a tarantool instance started +// with StartTarantool. Waits until any resources +// associated with the process is released. +// Cleans work directory after stop. If something went wrong, fails. +func StopTarantoolWithCleanup(inst TarantoolInstance) { + StopTarantool(inst) + + if inst.WorkDir != "" { + if err := os.RemoveAll(inst.WorkDir); err != nil { + log.Fatalf("Failed to clean work directory, got %s", err) + } + } +} diff --git a/uuid/config.lua b/uuid/config.lua index 34b5d26c4..b8fe1fe08 100644 --- a/uuid/config.lua +++ b/uuid/config.lua @@ -1,10 +1,10 @@ local uuid = require('uuid') local msgpack = require('msgpack') +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. box.cfg{ - listen = 3013, - wal_dir = 'xlog', - snap_dir = 'snap', + work_dir = os.getenv("TEST_TNT_WORK_DIR"), } box.schema.user.create('test', { password = 'test' , if_not_exists = true }) @@ -29,3 +29,8 @@ s:truncate() box.schema.user.grant('test', 'read,write', 'space', 'testUUID', { if_not_exists = true }) s:insert({ uuid.fromstr("c8f0fa1f-da29-438c-a040-393f1126ad39") }) + +-- Set listen only when every other thing is configured. +box.cfg{ + listen = os.getenv("TEST_TNT_LISTEN"), +} diff --git a/uuid/uuid.go b/uuid/uuid.go index f50b6d92a..11d3153e1 100644 --- a/uuid/uuid.go +++ b/uuid/uuid.go @@ -31,7 +31,7 @@ func encodeUUID(e *msgpack.Encoder, v reflect.Value) error { } func decodeUUID(d *msgpack.Decoder, v reflect.Value) error { - var bytesCount int = 16; + var bytesCount int = 16 bytes := make([]byte, bytesCount) n, err := d.Buffered().Read(bytes) diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go index 61eb632d2..b8987b0bc 100644 --- a/uuid/uuid_test.go +++ b/uuid/uuid_test.go @@ -2,15 +2,23 @@ package uuid_test import ( "fmt" + "log" + "os" "testing" "time" + "github.com/google/uuid" . "github.com/tarantool/go-tarantool" - _ "github.com/tarantool/go-tarantool/uuid" + "github.com/tarantool/go-tarantool/test_helpers" + _ "github.com/tarantool/go-tarantool/uuid" "gopkg.in/vmihailenco/msgpack.v2" - "github.com/google/uuid" ) +// There is no way to skip tests in testing.M, +// so we use this variable to pass info +// to each testing.T that it should skip. +var isUUIDSupported = false + var server = "127.0.0.1:3013" var opts = Opts{ Timeout: 500 * time.Millisecond, @@ -53,23 +61,6 @@ func connectWithValidation(t *testing.T) *Connection { return conn } -func skipIfUUIDUnsupported(t *testing.T, conn *Connection) { - resp, err := conn.Eval("return pcall(require('msgpack').encode, require('uuid').new())", []interface{}{}) - if err != nil { - t.Errorf("Failed to Eval: %s", err.Error()) - } - if resp == nil { - t.Errorf("Response is nil after Eval") - } - if len(resp.Data) < 1 { - t.Errorf("Response.Data is empty after Eval") - } - val := resp.Data[0].(bool) - if val != true { - t.Skip("Skipping test for Tarantool without UUID support in msgpack") - } -} - func tupleValueIsId(t *testing.T, tuples []interface{}, id uuid.UUID) { if len(tuples) != 1 { t.Errorf("Response Data len != 1") @@ -88,17 +79,19 @@ func tupleValueIsId(t *testing.T, tuples []interface{}, id uuid.UUID) { } func TestSelect(t *testing.T) { + if isUUIDSupported == false { + t.Skip("Skipping test for Tarantool without UUID support in msgpack") + } + conn := connectWithValidation(t) defer conn.Close() - skipIfUUIDUnsupported(t, conn) - id, uuidErr := uuid.Parse("c8f0fa1f-da29-438c-a040-393f1126ad39") if uuidErr != nil { t.Errorf("Failed to prepare test uuid: %s", uuidErr) } - resp, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{ id }) + resp, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{id}) if errSel != nil { t.Errorf("UUID select failed: %s", errSel.Error()) } @@ -108,7 +101,7 @@ func TestSelect(t *testing.T) { tupleValueIsId(t, resp.Data, id) var tuples []TupleUUID - errTyp := conn.SelectTyped(space, index, 0, 1, IterEq, []interface{}{ id }, &tuples) + errTyp := conn.SelectTyped(space, index, 0, 1, IterEq, []interface{}{id}, &tuples) if errTyp != nil { t.Errorf("Failed to SelectTyped: %s", errTyp.Error()) } @@ -121,17 +114,19 @@ func TestSelect(t *testing.T) { } func TestReplace(t *testing.T) { + if isUUIDSupported == false { + t.Skip("Skipping test for Tarantool without UUID support in msgpack") + } + conn := connectWithValidation(t) defer conn.Close() - skipIfUUIDUnsupported(t, conn) - id, uuidErr := uuid.Parse("64d22e4d-ac92-4a23-899a-e59f34af5479") if uuidErr != nil { t.Errorf("Failed to prepare test uuid: %s", uuidErr) } - respRep, errRep := conn.Replace(space, []interface{}{ id }) + respRep, errRep := conn.Replace(space, []interface{}{id}) if errRep != nil { t.Errorf("UUID replace failed: %s", errRep) } @@ -140,7 +135,7 @@ func TestReplace(t *testing.T) { } tupleValueIsId(t, respRep.Data, id) - respSel, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{ id }) + respSel, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{id}) if errSel != nil { t.Errorf("UUID select failed: %s", errSel) } @@ -149,3 +144,46 @@ func TestReplace(t *testing.T) { } tupleValueIsId(t, respSel.Data, id) } + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + isLess, err := test_helpers.IsTarantoolVersionLess(2, 4, 1) + if err != nil { + log.Fatalf("Failed to extract tarantool version: %s", err) + } + + if isLess { + log.Println("Skipping UUID tests...") + isUUIDSupported = false + return m.Run() + } else { + isUUIDSupported = true + } + + inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: "config.lua", + Listen: server, + WorkDir: "work_dir", + User: opts.User, + Pass: opts.Pass, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, + }) + defer test_helpers.StopTarantoolWithCleanup(inst) + + if err != nil { + log.Fatalf("Failed to prepare test tarantool: %s", err) + } + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +}