From c4a7f02e370112a76fe41e94c05a7ac15c5d2abf Mon Sep 17 00:00:00 2001
From: Dmitriy Gertsog <dmyger@gmail.com>
Date: Thu, 6 Mar 2025 15:50:36 +0300
Subject: [PATCH 1/3] ci: bump Ubuntu up to 22.04

---
 .github/workflows/reusable_testing.yml |  2 +-
 .github/workflows/testing.yml          | 15 +++++++++++----
 2 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/.github/workflows/reusable_testing.yml b/.github/workflows/reusable_testing.yml
index 352653101..6c821fe51 100644
--- a/.github/workflows/reusable_testing.yml
+++ b/.github/workflows/reusable_testing.yml
@@ -11,7 +11,7 @@ on:
 
 jobs:
   run_tests:
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-22.04
     steps:
       - name: Clone the go-tarantool connector
         uses: actions/checkout@v4
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index d82dc3ef8..758bc89a1 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -23,7 +23,7 @@ jobs:
 
     # We could replace it with ubuntu-latest after fixing the bug:
     # https://github.com/tarantool/setup-tarantool/issues/37
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-22.04
 
     strategy:
       fail-fast: false
@@ -100,9 +100,10 @@ jobs:
         run: make deps
 
       - name: Run regression tests
-        run: |
-          make test
-          make testrace
+        run: make test
+
+      - name: Run race tests
+        run: make testrace
 
       - name: Run fuzzing tests
         if: ${{ matrix.fuzzing }}
@@ -116,6 +117,7 @@ jobs:
           make coveralls
 
       - name: Check workability of benchmark tests
+        if: matrix.golang == 'stable'
         run: make bench-deps bench DURATION=1x COUNT=1
 
   testing_mac_os:
@@ -270,6 +272,10 @@ jobs:
         run: |
           cd "${SRCDIR}"
           make test
+
+      - name: Run race tests
+        run: |
+          cd "${SRCDIR}"
           make testrace
 
       - name: Run fuzzing tests
@@ -279,6 +285,7 @@ jobs:
           make fuzzing TAGS="go_tarantool_decimal_fuzzing"
 
       - name: Check workability of benchmark tests
+        if: matrix.golang == 'stable'
         run: |
           cd "${SRCDIR}"
           make bench-deps bench DURATION=1x COUNT=1

From 800bbf7bd4acba1914a7952585097b585ff10f53 Mon Sep 17 00:00:00 2001
From: Dmitriy Gertsog <dmyger@gmail.com>
Date: Fri, 7 Mar 2025 16:04:28 +0300
Subject: [PATCH 2/3] tests: turn off coverage checks for test_helpers

---
 Makefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Makefile b/Makefile
index ce3505afc..556fad418 100644
--- a/Makefile
+++ b/Makefile
@@ -108,7 +108,7 @@ test-main:
 coverage:
 	go clean -testcache
 	go get golang.org/x/tools/cmd/cover
-	go test -tags "$(TAGS)" ./... -v -p 1 -covermode=atomic -coverprofile=$(COVERAGE_FILE)
+	go test -tags "$(TAGS)" $(go list ./... | grep -v test_helpers) -v -p 1 -covermode=atomic -coverprofile=$(COVERAGE_FILE)
 	go tool cover -func=$(COVERAGE_FILE)
 
 .PHONY: coveralls

From bcc3fc9e92338ea255b08b6374dfae7826238b40 Mon Sep 17 00:00:00 2001
From: Dmitriy Gertsog <dmyger@gmail.com>
Date: Thu, 6 Mar 2025 15:49:02 +0300
Subject: [PATCH 3/3] tests: add helpers for TcS

Simple helpers to make easy create tests required Taranatool
centralized configuration storage.
---
 CHANGELOG.md                          |  12 ++
 pool/connection_pool_test.go          |  14 +-
 queue/queue_test.go                   |   4 +-
 shutdown_test.go                      |  83 +++------
 tarantool_test.go                     |   4 +-
 test_helpers/main.go                  | 259 ++++++++++++++++++++++----
 test_helpers/pool_helper.go           |   6 +-
 test_helpers/tcs/prepare.go           |  66 +++++++
 test_helpers/tcs/tcs.go               | 180 ++++++++++++++++++
 test_helpers/tcs/testdata/config.yaml |  39 ++++
 test_helpers/utils.go                 |  26 +++
 11 files changed, 579 insertions(+), 114 deletions(-)
 create mode 100644 test_helpers/tcs/prepare.go
 create mode 100644 test_helpers/tcs/tcs.go
 create mode 100644 test_helpers/tcs/testdata/config.yaml

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6dc5bdd5b..1d595e230 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,11 +12,23 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
 
 - Extend box with replication information (#427).
 - The Instance info has been added to ConnectionInfo for GetInfo response (#429).
+- Added helpers to run Tarantool config storage (#431).
 
 ### Changed
 
+- Changed helpers API `StartTarantool` and `StopTarantool`, now it uses
+  pointer on `TarantoolInstance`:
+  * `StartTarantool()` returns `*TarantoolInstance`;
+  * `StopTarantool()` and `StopTarantoolWithCleanup()` accepts
+    `*TarantoolInstance` as arguments.
+- Field `Cmd` in `TarantoolInstance` struct declared as deprecated.
+  Suggested `Wait()`, `Stop()` and `Signal()` methods as safer to use
+  instead of direct `Cmd.Process` access (#431).
+
 ### Fixed
 
+- Fixed flaky test detection on fail start Tarantool instance (#431).
+
 ## [v2.2.1] - 2024-12-17
 
 The release fixes a schema lost after a reconnect.
diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go
index e6f1f50c2..323ce37c3 100644
--- a/pool/connection_pool_test.go
+++ b/pool/connection_pool_test.go
@@ -98,7 +98,7 @@ var connOpts = tarantool.Opts{
 var defaultCountRetry = 5
 var defaultTimeoutRetry = 500 * time.Millisecond
 
-var helpInstances []test_helpers.TarantoolInstance
+var helpInstances []*test_helpers.TarantoolInstance
 
 func TestConnect_error_duplicate(t *testing.T) {
 	ctx, cancel := test_helpers.GetPoolConnectContext()
@@ -404,7 +404,7 @@ func TestReconnect(t *testing.T) {
 		defaultCountRetry, defaultTimeoutRetry)
 	require.Nil(t, err)
 
-	err = test_helpers.RestartTarantool(&helpInstances[0])
+	err = test_helpers.RestartTarantool(helpInstances[0])
 	require.Nilf(t, err, "failed to restart tarantool")
 
 	args = test_helpers.CheckStatusesArgs{
@@ -453,7 +453,7 @@ func TestDisconnect_withReconnect(t *testing.T) {
 	require.Nil(t, err)
 
 	// Restart the server after success.
-	err = test_helpers.RestartTarantool(&helpInstances[serverId])
+	err = test_helpers.RestartTarantool(helpInstances[serverId])
 	require.Nilf(t, err, "failed to restart tarantool")
 
 	args = test_helpers.CheckStatusesArgs{
@@ -501,10 +501,10 @@ func TestDisconnectAll(t *testing.T) {
 		defaultCountRetry, defaultTimeoutRetry)
 	require.Nil(t, err)
 
-	err = test_helpers.RestartTarantool(&helpInstances[0])
+	err = test_helpers.RestartTarantool(helpInstances[0])
 	require.Nilf(t, err, "failed to restart tarantool")
 
-	err = test_helpers.RestartTarantool(&helpInstances[1])
+	err = test_helpers.RestartTarantool(helpInstances[1])
 	require.Nilf(t, err, "failed to restart tarantool")
 
 	args = test_helpers.CheckStatusesArgs{
@@ -1362,10 +1362,10 @@ func TestRequestOnClosed(t *testing.T) {
 	_, err = connPool.Ping(pool.ANY)
 	require.NotNilf(t, err, "err is nil after Ping")
 
-	err = test_helpers.RestartTarantool(&helpInstances[0])
+	err = test_helpers.RestartTarantool(helpInstances[0])
 	require.Nilf(t, err, "failed to restart tarantool")
 
-	err = test_helpers.RestartTarantool(&helpInstances[1])
+	err = test_helpers.RestartTarantool(helpInstances[1])
 	require.Nilf(t, err, "failed to restart tarantool")
 }
 
diff --git a/queue/queue_test.go b/queue/queue_test.go
index d23bd2c9c..e43c47113 100644
--- a/queue/queue_test.go
+++ b/queue/queue_test.go
@@ -23,8 +23,6 @@ const (
 var servers = []string{"127.0.0.1:3014", "127.0.0.1:3015"}
 var server = "127.0.0.1:3013"
 
-var instances []test_helpers.TarantoolInstance
-
 var dialer = NetDialer{
 	Address:  server,
 	User:     user,
@@ -931,7 +929,7 @@ func runTestMain(m *testing.M) int {
 		})
 	}
 
-	instances, err = test_helpers.StartTarantoolInstances(poolInstsOpts)
+	instances, err := test_helpers.StartTarantoolInstances(poolInstsOpts)
 
 	if err != nil {
 		log.Printf("Failed to prepare test tarantool pool: %s", err)
diff --git a/shutdown_test.go b/shutdown_test.go
index b3a09eff0..4df34aef5 100644
--- a/shutdown_test.go
+++ b/shutdown_test.go
@@ -78,7 +78,7 @@ func testGracefulShutdown(t *testing.T, conn *Connection, inst *test_helpers.Tar
 
 	// SIGTERM the server.
 	shutdownStart := time.Now()
-	require.Nil(t, inst.Cmd.Process.Signal(syscall.SIGTERM))
+	require.Nil(t, inst.Signal(syscall.SIGTERM))
 
 	// Check that we can't send new requests after shutdown starts.
 	// Retry helps to wait a bit until server starts to shutdown
@@ -108,14 +108,11 @@ func testGracefulShutdown(t *testing.T, conn *Connection, inst *test_helpers.Tar
 	// Wait until server go down.
 	// Server will go down only when it process all requests from our connection
 	// (or on timeout).
-	_, err = inst.Cmd.Process.Wait()
+	err = inst.Wait()
 	require.Nil(t, err)
 	shutdownFinish := time.Now()
 	shutdownTime := shutdownFinish.Sub(shutdownStart)
 
-	// Help test helpers to properly clean up.
-	inst.Cmd.Process = nil
-
 	// Check that it wasn't a timeout.
 	require.Lessf(t,
 		shutdownTime,
@@ -129,18 +126,16 @@ func testGracefulShutdown(t *testing.T, conn *Connection, inst *test_helpers.Tar
 func TestGracefulShutdown(t *testing.T) {
 	test_helpers.SkipIfWatchersUnsupported(t)
 
-	var inst test_helpers.TarantoolInstance
 	var conn *Connection
-	var err error
 
-	inst, err = test_helpers.StartTarantool(shtdnSrvOpts)
+	inst, err := test_helpers.StartTarantool(shtdnSrvOpts)
 	require.Nil(t, err)
 	defer test_helpers.StopTarantoolWithCleanup(inst)
 
 	conn = test_helpers.ConnectWithValidation(t, shtdnDialer, shtdnClntOpts)
 	defer conn.Close()
 
-	testGracefulShutdown(t, conn, &inst)
+	testGracefulShutdown(t, conn, inst)
 }
 
 func TestCloseGraceful(t *testing.T) {
@@ -190,26 +185,23 @@ func TestCloseGraceful(t *testing.T) {
 func TestGracefulShutdownWithReconnect(t *testing.T) {
 	test_helpers.SkipIfWatchersUnsupported(t)
 
-	var inst test_helpers.TarantoolInstance
-	var err error
-
-	inst, err = test_helpers.StartTarantool(shtdnSrvOpts)
+	inst, err := test_helpers.StartTarantool(shtdnSrvOpts)
 	require.Nil(t, err)
 	defer test_helpers.StopTarantoolWithCleanup(inst)
 
 	conn := test_helpers.ConnectWithValidation(t, shtdnDialer, shtdnClntOpts)
 	defer conn.Close()
 
-	testGracefulShutdown(t, conn, &inst)
+	testGracefulShutdown(t, conn, inst)
 
-	err = test_helpers.RestartTarantool(&inst)
+	err = test_helpers.RestartTarantool(inst)
 	require.Nilf(t, err, "Failed to restart tarantool")
 
 	connected := test_helpers.WaitUntilReconnected(conn, shtdnClntOpts.MaxReconnects,
 		shtdnClntOpts.Reconnect)
 	require.Truef(t, connected, "Reconnect success")
 
-	testGracefulShutdown(t, conn, &inst)
+	testGracefulShutdown(t, conn, inst)
 }
 
 func TestNoGracefulShutdown(t *testing.T) {
@@ -219,14 +211,12 @@ func TestNoGracefulShutdown(t *testing.T) {
 	noShtdDialer.RequiredProtocolInfo = ProtocolInfo{}
 	test_helpers.SkipIfWatchersSupported(t)
 
-	var inst test_helpers.TarantoolInstance
 	var conn *Connection
-	var err error
 
 	testSrvOpts := shtdnSrvOpts
 	testSrvOpts.Dialer = noShtdDialer
 
-	inst, err = test_helpers.StartTarantool(testSrvOpts)
+	inst, err := test_helpers.StartTarantool(testSrvOpts)
 	require.Nil(t, err)
 	defer test_helpers.StopTarantoolWithCleanup(inst)
 
@@ -249,21 +239,18 @@ func TestNoGracefulShutdown(t *testing.T) {
 
 	// SIGTERM the server.
 	shutdownStart := time.Now()
-	require.Nil(t, inst.Cmd.Process.Signal(syscall.SIGTERM))
+	require.Nil(t, inst.Signal(syscall.SIGTERM))
 
 	// Check that request was interrupted.
 	_, err = fut.Get()
 	require.NotNilf(t, err, "sleep request error")
 
 	// Wait until server go down.
-	_, err = inst.Cmd.Process.Wait()
+	err = inst.Wait()
 	require.Nil(t, err)
 	shutdownFinish := time.Now()
 	shutdownTime := shutdownFinish.Sub(shutdownStart)
 
-	// Help test helpers to properly clean up.
-	inst.Cmd.Process = nil
-
 	// Check that server finished without waiting for eval to finish.
 	require.Lessf(t,
 		shutdownTime,
@@ -274,11 +261,9 @@ func TestNoGracefulShutdown(t *testing.T) {
 func TestGracefulShutdownRespectsClose(t *testing.T) {
 	test_helpers.SkipIfWatchersUnsupported(t)
 
-	var inst test_helpers.TarantoolInstance
 	var conn *Connection
-	var err error
 
-	inst, err = test_helpers.StartTarantool(shtdnSrvOpts)
+	inst, err := test_helpers.StartTarantool(shtdnSrvOpts)
 	require.Nil(t, err)
 	defer test_helpers.StopTarantoolWithCleanup(inst)
 
@@ -314,7 +299,7 @@ func TestGracefulShutdownRespectsClose(t *testing.T) {
 
 	// SIGTERM the server.
 	shutdownStart := time.Now()
-	require.Nil(t, inst.Cmd.Process.Signal(syscall.SIGTERM))
+	require.Nil(t, inst.Signal(syscall.SIGTERM))
 
 	// Close the connection.
 	conn.Close()
@@ -327,14 +312,11 @@ func TestGracefulShutdownRespectsClose(t *testing.T) {
 	require.NotNilf(t, err, "sleep request error")
 
 	// Wait until server go down.
-	_, err = inst.Cmd.Process.Wait()
+	err = inst.Wait()
 	require.Nil(t, err)
 	shutdownFinish := time.Now()
 	shutdownTime := shutdownFinish.Sub(shutdownStart)
 
-	// Help test helpers to properly clean up.
-	inst.Cmd.Process = nil
-
 	// Check that server finished without waiting for eval to finish.
 	require.Lessf(t,
 		shutdownTime,
@@ -354,11 +336,9 @@ func TestGracefulShutdownRespectsClose(t *testing.T) {
 func TestGracefulShutdownNotRacesWithRequestReconnect(t *testing.T) {
 	test_helpers.SkipIfWatchersUnsupported(t)
 
-	var inst test_helpers.TarantoolInstance
 	var conn *Connection
-	var err error
 
-	inst, err = test_helpers.StartTarantool(shtdnSrvOpts)
+	inst, err := test_helpers.StartTarantool(shtdnSrvOpts)
 	require.Nil(t, err)
 	defer test_helpers.StopTarantoolWithCleanup(inst)
 
@@ -397,16 +377,13 @@ func TestGracefulShutdownNotRacesWithRequestReconnect(t *testing.T) {
 	fut := conn.Do(req)
 
 	// SIGTERM the server.
-	require.Nil(t, inst.Cmd.Process.Signal(syscall.SIGTERM))
+	require.Nil(t, inst.Signal(syscall.SIGTERM))
 
 	// Wait until server go down.
 	// Server is expected to go down on timeout.
-	_, err = inst.Cmd.Process.Wait()
+	err = inst.Wait()
 	require.Nil(t, err)
 
-	// Help test helpers to properly clean up.
-	inst.Cmd.Process = nil
-
 	// Check that request failed by server disconnect, not a client timeout.
 	_, err = fut.Get()
 	require.NotNil(t, err)
@@ -425,11 +402,9 @@ func TestGracefulShutdownNotRacesWithRequestReconnect(t *testing.T) {
 func TestGracefulShutdownCloseConcurrent(t *testing.T) {
 	test_helpers.SkipIfWatchersUnsupported(t)
 
-	var inst test_helpers.TarantoolInstance
-	var err error
 	var srvShtdnStart, srvShtdnFinish time.Time
 
-	inst, err = test_helpers.StartTarantool(shtdnSrvOpts)
+	inst, err := test_helpers.StartTarantool(shtdnSrvOpts)
 	require.Nil(t, err)
 	defer test_helpers.StopTarantoolWithCleanup(inst)
 
@@ -487,22 +462,19 @@ func TestGracefulShutdownCloseConcurrent(t *testing.T) {
 	go func(inst *test_helpers.TarantoolInstance) {
 		srvToStop.Wait()
 		srvShtdnStart = time.Now()
-		cerr := inst.Cmd.Process.Signal(syscall.SIGTERM)
+		cerr := inst.Signal(syscall.SIGTERM)
 		if cerr != nil {
 			sret = cerr
 		}
 		srvStop.Done()
-	}(&inst)
+	}(inst)
 
 	srvStop.Wait()
 	require.Nil(t, sret, "No errors on server SIGTERM")
 
-	_, err = inst.Cmd.Process.Wait()
+	err = inst.Wait()
 	require.Nil(t, err)
 
-	// Help test helpers to properly clean up.
-	inst.Cmd.Process = nil
-
 	srvShtdnFinish = time.Now()
 	srvShtdnTime := srvShtdnFinish.Sub(srvShtdnStart)
 
@@ -515,11 +487,9 @@ func TestGracefulShutdownCloseConcurrent(t *testing.T) {
 func TestGracefulShutdownConcurrent(t *testing.T) {
 	test_helpers.SkipIfWatchersUnsupported(t)
 
-	var inst test_helpers.TarantoolInstance
-	var err error
 	var srvShtdnStart, srvShtdnFinish time.Time
 
-	inst, err = test_helpers.StartTarantool(shtdnSrvOpts)
+	inst, err := test_helpers.StartTarantool(shtdnSrvOpts)
 	require.Nil(t, err)
 	defer test_helpers.StopTarantoolWithCleanup(inst)
 
@@ -577,12 +547,12 @@ func TestGracefulShutdownConcurrent(t *testing.T) {
 	go func(inst *test_helpers.TarantoolInstance) {
 		srvToStop.Wait()
 		srvShtdnStart = time.Now()
-		cerr := inst.Cmd.Process.Signal(syscall.SIGTERM)
+		cerr := inst.Signal(syscall.SIGTERM)
 		if cerr != nil {
 			sret = cerr
 		}
 		srvStop.Done()
-	}(&inst)
+	}(inst)
 
 	srvStop.Wait()
 	require.Nil(t, sret, "No errors on server SIGTERM")
@@ -590,12 +560,9 @@ func TestGracefulShutdownConcurrent(t *testing.T) {
 	caseWg.Wait()
 	require.Nil(t, ret, "No errors on concurrent wait")
 
-	_, err = inst.Cmd.Process.Wait()
+	err = inst.Wait()
 	require.Nil(t, err)
 
-	// Help test helpers to properly clean up.
-	inst.Cmd.Process = nil
-
 	srvShtdnFinish = time.Now()
 	srvShtdnTime := srvShtdnFinish.Sub(srvShtdnStart)
 
diff --git a/tarantool_test.go b/tarantool_test.go
index e6adebc6d..2be3e793b 100644
--- a/tarantool_test.go
+++ b/tarantool_test.go
@@ -3609,7 +3609,7 @@ func TestConnection_NewWatcher_reconnect(t *testing.T) {
 	<-events
 
 	test_helpers.StopTarantool(inst)
-	if err := test_helpers.RestartTarantool(&inst); err != nil {
+	if err := test_helpers.RestartTarantool(inst); err != nil {
 		t.Fatalf("Unable to restart Tarantool: %s", err)
 	}
 
@@ -3902,7 +3902,7 @@ func TestConnection_named_index_after_reconnect(t *testing.T) {
 		t.Fatalf("An error expected.")
 	}
 
-	if err := test_helpers.RestartTarantool(&inst); err != nil {
+	if err := test_helpers.RestartTarantool(inst); err != nil {
 		t.Fatalf("Unable to restart Tarantool: %s", err)
 	}
 
diff --git a/test_helpers/main.go b/test_helpers/main.go
index f452f3e97..4ebe4c622 100644
--- a/test_helpers/main.go
+++ b/test_helpers/main.go
@@ -15,6 +15,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"io/fs"
 	"log"
 	"os"
 	"os/exec"
@@ -33,6 +34,14 @@ type StartOpts struct {
 	// InitScript is a Lua script for tarantool to run on start.
 	InitScript string
 
+	// ConfigFile is a path to a configuration file for a Tarantool instance.
+	// Required in pair with InstanceName.
+	ConfigFile string
+
+	// InstanceName is a name of an instance to run.
+	// Required in pair with ConfigFile.
+	InstanceName 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
@@ -67,9 +76,19 @@ type StartOpts struct {
 	Dialer tarantool.Dialer
 }
 
+type state struct {
+	done    chan struct{}
+	ret     error
+	stopped bool
+}
+
 // TarantoolInstance is a data for instance graceful shutdown and cleanup.
 type TarantoolInstance struct {
 	// Cmd is a Tarantool command. Used to kill Tarantool process.
+	//
+	// Deprecated: Cmd field will be removed in the next major version.
+	// Use `Wait()` and `Stop()` methods, instead of calling `Cmd.Process.Wait()` or
+	// `Cmd.Process.Kill()` directly.
 	Cmd *exec.Cmd
 
 	// Options for restarting a tarantool instance.
@@ -77,6 +96,89 @@ type TarantoolInstance struct {
 
 	// Dialer to check that connection established.
 	Dialer tarantool.Dialer
+
+	st chan state
+}
+
+// IsExit checks if Tarantool process was terminated.
+func (t *TarantoolInstance) IsExit() bool {
+	st := <-t.st
+	t.st <- st
+
+	select {
+	case <-st.done:
+	default:
+		return false
+	}
+
+	return st.ret != nil
+}
+
+func (t *TarantoolInstance) result() error {
+	st := <-t.st
+	t.st <- st
+
+	select {
+	case <-st.done:
+	default:
+		return nil
+	}
+
+	return st.ret
+}
+
+func (t *TarantoolInstance) checkDone() {
+	ret := t.Cmd.Wait()
+
+	st := <-t.st
+
+	st.ret = ret
+	close(st.done)
+
+	t.st <- st
+
+	if !st.stopped {
+		log.Printf("Tarantool %q was unexpectedly terminated: %v", t.Opts.Listen, t.result())
+	}
+}
+
+// Wait waits until Tarantool process is terminated.
+// Returns error as process result status.
+func (t *TarantoolInstance) Wait() error {
+	st := <-t.st
+	t.st <- st
+
+	<-st.done
+	t.Cmd.Process = nil
+
+	st = <-t.st
+	t.st <- st
+
+	return st.ret
+}
+
+// Stop terminates Tarantool process and waits until it exit.
+func (t *TarantoolInstance) Stop() error {
+	st := <-t.st
+	st.stopped = true
+	t.st <- st
+
+	if t.IsExit() {
+		return nil
+	}
+	if t.Cmd != nil && t.Cmd.Process != nil {
+		if err := t.Cmd.Process.Kill(); err != nil && !t.IsExit() {
+			return fmt.Errorf("failed to kill tarantool %q (pid %d), got %s",
+				t.Opts.Listen, t.Cmd.Process.Pid, err)
+		}
+		t.Wait()
+	}
+	return nil
+}
+
+// Signal sends a signal to the Tarantool instance.
+func (t *TarantoolInstance) Signal(sig os.Signal) error {
+	return t.Cmd.Process.Signal(sig)
 }
 
 func isReady(dialer tarantool.Dialer, opts *tarantool.Opts) error {
@@ -108,7 +210,7 @@ var (
 )
 
 func init() {
-	tarantoolVersionRegexp = regexp.MustCompile(`Tarantool (?:Enterprise )?(\d+)\.(\d+)\.(\d+).*`)
+	tarantoolVersionRegexp = regexp.MustCompile(`Tarantool (Enterprise )?(\d+)\.(\d+)\.(\d+).*`)
 }
 
 // atoiUint64 parses string to uint64.
@@ -145,15 +247,15 @@ func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) (
 		return true, fmt.Errorf("failed to parse output %q", out)
 	}
 
-	if major, err = atoiUint64(parsed[1]); err != nil {
+	if major, err = atoiUint64(parsed[2]); err != nil {
 		return true, fmt.Errorf("failed to parse major from output %q: %w", out, err)
 	}
 
-	if minor, err = atoiUint64(parsed[2]); err != nil {
+	if minor, err = atoiUint64(parsed[3]); err != nil {
 		return true, fmt.Errorf("failed to parse minor from output %q: %w", out, err)
 	}
 
-	if patch, err = atoiUint64(parsed[3]); err != nil {
+	if patch, err = atoiUint64(parsed[4]); err != nil {
 		return true, fmt.Errorf("failed to parse patch from output %q: %w", out, err)
 	}
 
@@ -166,6 +268,21 @@ func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) (
 	}
 }
 
+// IsTarantoolEE checks if Tarantool is Enterprise edition.
+func IsTarantoolEE() (bool, error) {
+	out, err := exec.Command(getTarantoolExec(), "--version").Output()
+	if err != nil {
+		return true, err
+	}
+
+	parsed := tarantoolVersionRegexp.FindStringSubmatch(string(out))
+	if parsed == nil {
+		return true, fmt.Errorf("failed to parse output %q", out)
+	}
+
+	return parsed[1] != "", nil
+}
+
 // RestartTarantool restarts a tarantool instance for tests
 // with specifies parameters (refer to StartOpts)
 // which were specified in inst parameter.
@@ -175,42 +292,96 @@ func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) (
 // Process must be stopped with StopTarantool.
 func RestartTarantool(inst *TarantoolInstance) error {
 	startedInst, err := StartTarantool(inst.Opts)
+
 	inst.Cmd.Process = startedInst.Cmd.Process
+	inst.st = startedInst.st
+
 	return err
 }
 
+func removeByMask(dir string, masks ...string) error {
+	err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
+		if d.IsDir() {
+			return nil
+		}
+
+		for _, mask := range masks {
+			if ok, err := filepath.Match(mask, d.Name()); err != nil {
+				return err
+			} else if ok {
+				if err = os.Remove(path); err != nil {
+					return err
+				}
+			}
+		}
+		return nil
+	})
+
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func prepareDir(workDir string) (string, error) {
+	if workDir == "" {
+		dir, err := os.MkdirTemp("", "work_dir")
+		if err != nil {
+			return "", err
+		}
+		return dir, nil
+	}
+	// Create work_dir.
+	err := os.MkdirAll(workDir, 0755)
+	if err != nil {
+		return "", err
+	}
+
+	// Clean up existing work_dir.
+	err = removeByMask(workDir, "*.snap", "*.xlog")
+	if err != nil {
+		return "", err
+	}
+	return workDir, 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) {
+func StartTarantool(startOpts StartOpts) (*TarantoolInstance, error) {
 	// Prepare tarantool command.
-	var inst TarantoolInstance
-	var dir string
-	var err error
+	inst := &TarantoolInstance{
+		st: make(chan state, 1),
+	}
+	init := state{
+		done: make(chan struct{}),
+	}
+	inst.st <- init
 
+	var err error
 	inst.Dialer = startOpts.Dialer
+	startOpts.WorkDir, err = prepareDir(startOpts.WorkDir)
+	if err != nil {
+		return inst, fmt.Errorf("failed to prepare working dir %q: %w", startOpts.WorkDir, err)
+	}
 
-	if startOpts.WorkDir == "" {
-		dir, err = os.MkdirTemp("", "work_dir")
-		if err != nil {
-			return inst, err
-		}
-		startOpts.WorkDir = dir
-	} else {
-		// 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
+	args := []string{}
+	if startOpts.InitScript != "" {
+		if !filepath.IsAbs(startOpts.InitScript) {
+			cwd, err := os.Getwd()
+			if err != nil {
+				return inst, fmt.Errorf("failed to get current working directory: %w", err)
+			}
+			startOpts.InitScript = filepath.Join(cwd, startOpts.InitScript)
 		}
+		args = append(args, startOpts.InitScript)
 	}
-
-	inst.Cmd = exec.Command(getTarantoolExec(), startOpts.InitScript)
+	if startOpts.ConfigFile != "" && startOpts.InstanceName != "" {
+		args = append(args, "--config", startOpts.ConfigFile)
+		args = append(args, "--name", startOpts.InstanceName)
+	}
+	inst.Cmd = exec.Command(getTarantoolExec(), args...)
+	inst.Cmd.Dir = startOpts.WorkDir
 
 	inst.Cmd.Env = append(
 		os.Environ(),
@@ -242,6 +413,8 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) {
 	// see https://github.com/tarantool/go-tarantool/issues/136
 	time.Sleep(startOpts.WaitStart)
 
+	go inst.checkDone()
+
 	opts := tarantool.Opts{
 		Timeout:    500 * time.Millisecond,
 		SkipSchema: true,
@@ -261,24 +434,28 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) {
 		}
 	}
 
-	return inst, err
+	if inst.IsExit() && inst.result() != nil {
+		StopTarantool(inst)
+		return nil, fmt.Errorf("unexpected terminated Tarantool %q: %w",
+			inst.Opts.Listen, inst.result())
+	}
+
+	if err != nil {
+		StopTarantool(inst)
+		return nil, fmt.Errorf("failed to connect Tarantool %q: %w",
+			inst.Opts.Listen, err)
+	}
+
+	return inst, nil
 }
 
 // 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.Process = nil
+func StopTarantool(inst *TarantoolInstance) {
+	err := inst.Stop()
+	if err != nil {
+		log.Fatal(err)
 	}
 }
 
@@ -286,7 +463,7 @@ func StopTarantool(inst TarantoolInstance) {
 // 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) {
+func StopTarantoolWithCleanup(inst *TarantoolInstance) {
 	StopTarantool(inst)
 
 	if inst.Opts.WorkDir != "" {
diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go
index 729a69e44..b67d05f02 100644
--- a/test_helpers/pool_helper.go
+++ b/test_helpers/pool_helper.go
@@ -227,8 +227,8 @@ func SetClusterRO(dialers []tarantool.Dialer, connOpts tarantool.Opts,
 	return nil
 }
 
-func StartTarantoolInstances(instsOpts []StartOpts) ([]TarantoolInstance, error) {
-	instances := make([]TarantoolInstance, 0, len(instsOpts))
+func StartTarantoolInstances(instsOpts []StartOpts) ([]*TarantoolInstance, error) {
+	instances := make([]*TarantoolInstance, 0, len(instsOpts))
 
 	for _, opts := range instsOpts {
 		instance, err := StartTarantool(opts)
@@ -243,7 +243,7 @@ func StartTarantoolInstances(instsOpts []StartOpts) ([]TarantoolInstance, error)
 	return instances, nil
 }
 
-func StopTarantoolInstances(instances []TarantoolInstance) {
+func StopTarantoolInstances(instances []*TarantoolInstance) {
 	for _, instance := range instances {
 		StopTarantoolWithCleanup(instance)
 	}
diff --git a/test_helpers/tcs/prepare.go b/test_helpers/tcs/prepare.go
new file mode 100644
index 000000000..c7b87d0f6
--- /dev/null
+++ b/test_helpers/tcs/prepare.go
@@ -0,0 +1,66 @@
+package tcs
+
+import (
+	_ "embed"
+	"fmt"
+	"os"
+	"path/filepath"
+	"text/template"
+	"time"
+
+	"github.com/tarantool/go-tarantool/v2"
+	"github.com/tarantool/go-tarantool/v2/test_helpers"
+)
+
+const (
+	waitTimeout  = 500 * time.Millisecond
+	connectRetry = 3
+	tcsUser      = "client"
+	tcsPassword  = "secret"
+)
+
+//go:embed testdata/config.yaml
+var tcsConfig []byte
+
+func writeConfig(name string, port int) error {
+	cfg, err := os.Create(name)
+	if err != nil {
+		return err
+	}
+	defer cfg.Close()
+
+	cfg.Chmod(0644)
+
+	t := template.Must(template.New("config").Parse(string(tcsConfig)))
+	return t.Execute(cfg, map[string]interface{}{
+		"host": "localhost",
+		"port": port,
+	})
+}
+
+func makeOpts(port int) (test_helpers.StartOpts, error) {
+	opts := test_helpers.StartOpts{}
+	var err error
+	opts.WorkDir, err = os.MkdirTemp("", "tcs_dir")
+	if err != nil {
+		return opts, err
+	}
+
+	opts.ConfigFile = filepath.Join(opts.WorkDir, "config.yaml")
+	err = writeConfig(opts.ConfigFile, port)
+	if err != nil {
+		return opts, fmt.Errorf("can't save file %q: %w", opts.ConfigFile, err)
+	}
+
+	opts.Listen = fmt.Sprintf("localhost:%d", port)
+	opts.WaitStart = waitTimeout
+	opts.ConnectRetry = connectRetry
+	opts.RetryTimeout = waitTimeout
+	opts.InstanceName = "master"
+	opts.Dialer = tarantool.NetDialer{
+		Address:  opts.Listen,
+		User:     tcsUser,
+		Password: tcsPassword,
+	}
+	return opts, nil
+}
diff --git a/test_helpers/tcs/tcs.go b/test_helpers/tcs/tcs.go
new file mode 100644
index 000000000..1ba26a19e
--- /dev/null
+++ b/test_helpers/tcs/tcs.go
@@ -0,0 +1,180 @@
+package tcs
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net"
+	"testing"
+
+	"github.com/tarantool/go-tarantool/v2"
+	"github.com/tarantool/go-tarantool/v2/test_helpers"
+)
+
+// ErrNotSupported identifies result of `Start()` why storage was not started.
+var ErrNotSupported = errors.New("required Tarantool EE 3.3+")
+
+// ErrNoValue used to show that `Get()` was successful, but no values were found.
+var ErrNoValue = errors.New("required value not found")
+
+// TCS is a Tarantool centralized configuration storage connection.
+type TCS struct {
+	inst *test_helpers.TarantoolInstance
+	conn *tarantool.Connection
+	tb   testing.TB
+	port int
+}
+
+// dataResponse content of TcS response in data array.
+type dataResponse struct {
+	Path        string `msgpack:"path"`
+	Value       string `msgpack:"value"`
+	ModRevision int64  `msgpack:"mod_revision"`
+}
+
+// findEmptyPort returns some random unused port if @port is passed with zero.
+func findEmptyPort(port int) (int, error) {
+	listener, err := net.Listen("tcp4", fmt.Sprintf(":%d", port))
+	if err != nil {
+		return 0, err
+	}
+	defer listener.Close()
+
+	addr := listener.Addr().(*net.TCPAddr)
+	return addr.Port, nil
+}
+
+// Start starts a Tarantool centralized configuration storage.
+// Use `port = 0` to use any unused port.
+// Returns a Tcs instance and a cleanup function.
+func Start(port int) (TCS, error) {
+	tcs := TCS{}
+	if ok, err := test_helpers.IsTcsSupported(); !ok || err != nil {
+		return tcs, errors.Join(ErrNotSupported, err)
+	}
+	var err error
+	tcs.port, err = findEmptyPort(port)
+	if err != nil {
+		if port == 0 {
+			return tcs, fmt.Errorf("failed to detect an empty port: %w", err)
+		} else {
+			return tcs, fmt.Errorf("port %d can't be used: %w", port, err)
+		}
+	}
+
+	opts, err := makeOpts(tcs.port)
+	if err != nil {
+		return tcs, err
+	}
+
+	tcs.inst, err = test_helpers.StartTarantool(opts)
+	if err != nil {
+		return tcs, fmt.Errorf("failed to start Tarantool config storage: %w", err)
+	}
+
+	tcs.conn, err = tarantool.Connect(context.Background(), tcs.inst.Dialer, tarantool.Opts{})
+	if err != nil {
+		return tcs, fmt.Errorf("failed to connect to Tarantool config storage: %w", err)
+	}
+
+	return tcs, nil
+}
+
+// Start starts a Tarantool centralized configuration storage.
+// Returns a Tcs instance and a cleanup function.
+func StartTesting(tb testing.TB, port int) TCS {
+	tcs, err := Start(port)
+	if err != nil {
+		tb.Fatal(err)
+	}
+	return tcs
+}
+
+// Doer returns interface for interacting with Tarantool.
+func (t *TCS) Doer() tarantool.Doer {
+	return t.conn
+}
+
+// Dialer returns a dialer to connect to Tarantool.
+func (t *TCS) Dialer() tarantool.Dialer {
+	return t.inst.Dialer
+}
+
+// Endpoints returns a list of addresses to connect.
+func (t *TCS) Endpoints() []string {
+	return []string{fmt.Sprintf("127.0.0.1:%d", t.port)}
+}
+
+// Credentials returns a user name and password to connect.
+func (t *TCS) Credentials() (string, string) {
+	return tcsUser, tcsPassword
+}
+
+// Stop stops the Tarantool centralized configuration storage.
+func (t *TCS) Stop() {
+	if t.tb != nil {
+		t.tb.Helper()
+	}
+	if t.conn != nil {
+		t.conn.Close()
+	}
+	test_helpers.StopTarantoolWithCleanup(t.inst)
+}
+
+// Put implements "config.storage.put" method.
+func (t *TCS) Put(ctx context.Context, path string, value string) error {
+	if t.tb != nil {
+		t.tb.Helper()
+	}
+	req := tarantool.NewCallRequest("config.storage.put").
+		Args([]any{path, value}).
+		Context(ctx)
+	if _, err := t.conn.Do(req).GetResponse(); err != nil {
+		return fmt.Errorf("failed to save data to tarantool: %w", err)
+	}
+	return nil
+}
+
+// Delete implements "config.storage.delete" method.
+func (t *TCS) Delete(ctx context.Context, path string) error {
+	if t.tb != nil {
+		t.tb.Helper()
+	}
+	req := tarantool.NewCallRequest("config.storage.delete").
+		Args([]any{path}).
+		Context(ctx)
+	if _, err := t.conn.Do(req).GetResponse(); err != nil {
+		return fmt.Errorf("failed to delete data from tarantool: %w", err)
+	}
+	return nil
+}
+
+// Get implements "config.storage.get" method.
+func (t *TCS) Get(ctx context.Context, path string) (string, error) {
+	if t.tb != nil {
+		t.tb.Helper()
+	}
+	req := tarantool.NewCallRequest("config.storage.get").
+		Args([]any{path}).
+		Context(ctx)
+
+	resp := []struct {
+		Data []dataResponse `msgpack:"data"`
+	}{}
+
+	err := t.conn.Do(req).GetTyped(&resp)
+	if err != nil {
+		return "", fmt.Errorf("failed to fetch data from tarantool: %w", err)
+	}
+	if len(resp) != 1 {
+		return "", errors.New("unexpected response from tarantool")
+	}
+	if len(resp[0].Data) == 0 {
+		return "", ErrNoValue
+	}
+	if len(resp[0].Data) != 1 {
+		return "", errors.New("too much data in response from tarantool")
+	}
+
+	return resp[0].Data[0].Value, nil
+}
diff --git a/test_helpers/tcs/testdata/config.yaml b/test_helpers/tcs/testdata/config.yaml
new file mode 100644
index 000000000..5d42e32aa
--- /dev/null
+++ b/test_helpers/tcs/testdata/config.yaml
@@ -0,0 +1,39 @@
+credentials:
+  users:
+    replicator:
+      password: "topsecret"
+      roles: [replication]
+    client:
+      password: "secret"
+      privileges:
+        - permissions: [execute]
+          universe: true
+        - permissions: [read, write]
+          spaces: [config_storage, config_storage_meta]
+
+iproto:
+  advertise:
+    peer:
+      login: replicator
+
+replication:
+  failover: election
+
+database:
+  use_mvcc_engine: true
+
+groups:
+  group-001:
+    replicasets:
+      replicaset-001:
+        roles: [config.storage]
+        roles_cfg:
+          config_storage:
+            status_check_interval: 3
+        instances:
+          master:
+            iproto:
+              listen:
+                - uri: "{{.host}}:{{.port}}"
+                  params:
+                    transport: plain
diff --git a/test_helpers/utils.go b/test_helpers/utils.go
index 049dff92d..eff7e1dbe 100644
--- a/test_helpers/utils.go
+++ b/test_helpers/utils.go
@@ -217,6 +217,32 @@ func SkipIfCrudSpliceBroken(t *testing.T) {
 	SkipIfFeatureUnsupported(t, "crud update splice", 2, 0, 0)
 }
 
+// IsTcsSupported checks if Tarantool supports centralized storage.
+// Tarantool supports centralized storage with Enterprise since 3.3.0 version.
+func IsTcsSupported() (bool, error) {
+
+	if isEe, err := IsTarantoolEE(); !isEe || err != nil {
+		return false, err
+	}
+	if isLess, err := IsTarantoolVersionLess(3, 3, 0); isLess || err != nil {
+		return false, err
+	}
+	return true, nil
+}
+
+// SkipIfTCSUnsupported skips test if no centralized storage support.
+func SkipIfTcsUnsupported(t testing.TB) {
+	t.Helper()
+
+	ok, err := IsTcsSupported()
+	if err != nil {
+		t.Fatalf("Could not check the Tarantool version: %s", err)
+	}
+	if !ok {
+		t.Skip("not found Tarantool EE 3.3+")
+	}
+}
+
 // CheckEqualBoxErrors checks equivalence of tarantool.BoxError objects.
 //
 // Tarantool errors are not comparable by nature: