From ec720434a7b3ab60723e1b34883b3b7a287c5749 Mon Sep 17 00:00:00 2001 From: Mikhail Elhimov Date: Thu, 10 Oct 2024 13:13:25 +0300 Subject: [PATCH] test: improve test coverage --- test/conftest.py | 7 + test/integration/clean/multi_inst_data_app | 1 + test/integration/clean/test_clean.py | 101 +++++++++ test/integration/kill/multi_inst_app | 1 + test/integration/kill/test_kill.py | 98 ++++++++ test/integration/logrotate/multi_inst_app | 1 + test/integration/logrotate/test_logrotate.py | 77 +++++++ test/integration/restart/multi_inst_app | 1 + test/integration/restart/test_restart.py | 100 ++++++++- .../running/multi_inst_data_app/init.lua | 16 ++ .../running/multi_inst_data_app/instances.yml | 7 + .../multi_inst_data_app/router.init.lua | 12 + .../running/multi_inst_data_app/tt.yaml | 2 + test/integration/running/test_running.py | 104 --------- test/integration/start/multi_inst_app | 1 + test/integration/start/test_start.py | 64 ++++++ test/integration/stop/multi_inst_app | 1 + test/integration/stop/test_stop.py | 94 +++++++- test/utils.py | 212 ++++++++++++++++++ 19 files changed, 794 insertions(+), 106 deletions(-) create mode 120000 test/integration/clean/multi_inst_data_app create mode 100644 test/integration/clean/test_clean.py create mode 120000 test/integration/kill/multi_inst_app create mode 100644 test/integration/kill/test_kill.py create mode 120000 test/integration/logrotate/multi_inst_app create mode 100644 test/integration/logrotate/test_logrotate.py create mode 120000 test/integration/restart/multi_inst_app create mode 100644 test/integration/running/multi_inst_data_app/init.lua create mode 100644 test/integration/running/multi_inst_data_app/instances.yml create mode 100644 test/integration/running/multi_inst_data_app/router.init.lua create mode 100644 test/integration/running/multi_inst_data_app/tt.yaml create mode 120000 test/integration/start/multi_inst_app create mode 100644 test/integration/start/test_start.py create mode 120000 test/integration/stop/multi_inst_app diff --git a/test/conftest.py b/test/conftest.py index f8f9fe976..dc16f648d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -124,6 +124,13 @@ def cartridge_app_session(request, tt_cmd, tmp_path_factory): return cartridge_app +@pytest.fixture(scope="function") +def tt_app(tt_cmd, tmpdir_with_cfg, request): + app = utils.TtApp(tt_cmd, tmpdir_with_cfg, request.module.app_path, request.module.inst_names) + yield app + app.stop(auto_confirm="-y") + + @pytest.fixture def cartridge_app(request, cartridge_app_session): bootstrap_vshard = True diff --git a/test/integration/clean/multi_inst_data_app b/test/integration/clean/multi_inst_data_app new file mode 120000 index 000000000..fd8039f82 --- /dev/null +++ b/test/integration/clean/multi_inst_data_app @@ -0,0 +1 @@ +../running/multi_inst_data_app \ No newline at end of file diff --git a/test/integration/clean/test_clean.py b/test/integration/clean/test_clean.py new file mode 100644 index 000000000..bd272f8b0 --- /dev/null +++ b/test/integration/clean/test_clean.py @@ -0,0 +1,101 @@ +import itertools +import os +import re + +import pytest + +from utils import (TtApp, initial_snap, initial_xlog, log_file, + tt_app_remove_app_script, tt_app_start, + tt_app_wait_data_files) + +################################################################ +# multi-instance app + +app_path = os.path.join(os.path.dirname(__file__), "multi_inst_data_app") +app_name = "multi_inst_data_app" +inst_names = ["router", "master", "replica", "stateboard"] + + +def check_multi_inst_clean(tt_app, post_start, target, **kwargs): + # Start the application. + tt_app_start(tt_app, [TtApp.app_target], post_start) + tt_app_wait_data_files(tt_app, TtApp.app_target) + + # Do clean. + rc, out = tt_app.clean(target, **kwargs) + assert rc == 0 + status = tt_app.status() + + # Check that clean warns about application is running. + for inst_name in tt_app.inst_names: + inst_id = tt_app.inst_id(inst_name) + assert status[inst_id]["STATUS"] == "RUNNING" + msg = f"instance `{inst_name}` must be stopped" + if tt_app.inst_match_target(inst_name, target): + assert msg in out + else: + assert msg not in out + + # Stop the application. + rc, out = tt_app.stop(TtApp.app_target, auto_confirm='-y') + assert rc == 0 + + # Do clean. + rc, out = tt_app.clean(target, **kwargs) + assert rc == 0 + print(f"out:{out}") + status = tt_app.status() + + # Check that clean is working. + input = kwargs.get('input') + auto_confirm = kwargs.get('auto_confirm') + is_confirms = itertools.repeat(True) if auto_confirm is not None else input.is_confirm + for inst_name, is_confirm in zip(tt_app.inst_names, is_confirms): + inst_id = tt_app.inst_id(inst_name) + if tt_app.inst_match_target(inst_name, target): + if is_confirm: + # https://github.com/tarantool/tt/issues/735 + msg = r"{}...\t\[OK\]".format(inst_id) + assert len(re.findall(msg, out)) == 1 + not os.path.exists(tt_app.log_path(inst_name, log_file)) + not os.path.exists(tt_app.lib_path(inst_name, initial_snap)) + not os.path.exists(tt_app.lib_path(inst_name, initial_xlog)) + else: + msg = r"{}: cleaning...\t\[ERR\] cancelled by user".format(inst_name) + assert re.search(msg, out) + else: + assert inst_id not in out + + +# Test with auto-confirmation (short option). +@pytest.mark.parametrize("post_start", [ + None, + # post_start_make_identical_subdir, + tt_app_remove_app_script, +]) +@pytest.mark.parametrize("target", [ + pytest.param(None, id="NO_TARGET"), + pytest.param(TtApp.app_target, id="APP_TARGET"), + pytest.param(TtApp.Target("master"), id="master"), + pytest.param(TtApp.Target("router"), id="router"), +]) +def test_multi_inst_clean_auto_confirm_shortopt(tt_app, post_start, target): + check_multi_inst_clean(tt_app, post_start, target, auto_confirm="-f") + + +# Test with auto-confirmation (long option; less variations, just a few common cases). +def test_multi_inst_clean_auto_confirm_longopt(tt_app): + check_multi_inst_clean(tt_app, None, TtApp.app_target, auto_confirm="--force") + + +# Test with the various inputs. +@pytest.mark.skipif(True, reason="Parsing of multiple answers from stdin is not supported.") +@pytest.mark.parametrize("input", [ + TtApp.Input("y\ny\nY\nY\n", [True, True, True, True]), # confirm all + TtApp.Input("n\nn\nN\nN\n", [False, False, False, False]), # discard all + TtApp.Input("y\nn\nN\nY\n", [True, False, False, True]), # mix #1 + TtApp.Input("n\ny\nY\nN\n", [False, True, True, False]), # mix #2 + TtApp.Input("ny\ny\nyn\nn\nY\nN\n", [True, False, True, False]), # mix #3 +]) +def test_multi_inst_clean_input(tt_app, input): + check_multi_inst_clean(tt_app, tt_app_wait_data_files, TtApp.app_target, input=input) diff --git a/test/integration/kill/multi_inst_app b/test/integration/kill/multi_inst_app new file mode 120000 index 000000000..a432b10ce --- /dev/null +++ b/test/integration/kill/multi_inst_app @@ -0,0 +1 @@ +../running/multi_inst_app \ No newline at end of file diff --git a/test/integration/kill/test_kill.py b/test/integration/kill/test_kill.py new file mode 100644 index 000000000..f28ee747c --- /dev/null +++ b/test/integration/kill/test_kill.py @@ -0,0 +1,98 @@ +import os +import re + +import pytest + +from utils import TtApp, pid_file, tt_app_remove_app_script, tt_app_start + +################################################################ +# multi-instance app + +app_path = os.path.join(os.path.dirname(__file__), "multi_inst_app") +app_name = "multi_inst_app" +inst_names = ["router", "master", "replica", "stateboard"] + + +def check_multi_inst_kill(tt_app, running_targets, post_start, target, **kwargs): + # Prepare the application considering the test parameters. + orig_status = tt_app_start(tt_app, running_targets, post_start) + + # Do kill. + rc, out = tt_app.kill(target, **kwargs) + assert rc == 0 + status = tt_app.status() + print(f"status: {status}") + + # Check the discarding. + if not tt_app.is_confirm(**kwargs): + for inst_name in tt_app.inst_names: + inst_id = tt_app.inst_id(inst_name) + assert status[inst_id]["STATUS"] == orig_status[inst_id]["STATUS"] + if tt_app.inst_match_target(inst_name, target) and \ + tt_app.inst_match_targets(inst_name, running_targets): + assert status[inst_id]["PID"] == orig_status[inst_id]["PID"] + return + + # Check the confirmation. + for inst_name in tt_app.inst_names: + inst_id = tt_app.inst_id(inst_name) + if tt_app.inst_match_target(inst_name, target): + assert status[inst_id]["STATUS"] == "NOT RUNNING" + if tt_app.inst_match_targets(inst_name, running_targets): + orig_pid = orig_status[inst_id]["PID"] + assert f"The instance {inst_id} (PID = {orig_pid}) has been killed." in out + else: + pid_path = tt_app.run_path(inst_name, pid_file) + msg = r"failed to kill the processes:.*{}".format(pid_path) + assert re.search(msg, out) + else: + assert status[inst_id]["STATUS"] == orig_status[inst_id]["STATUS"] + assert inst_id not in out + + +# Test with auto-confirmation (short option). +@pytest.mark.parametrize("running_targets", [ + pytest.param([], id="running:none"), + pytest.param([TtApp.app_target], id="running:all"), + pytest.param([TtApp.Target("master")], id="running:master"), + pytest.param([TtApp.Target("master"), TtApp.Target("router")], id="running:master,router"), +]) +@pytest.mark.parametrize("post_start", [ + None, + # post_start_make_identical_subdir, + tt_app_remove_app_script, +]) +@pytest.mark.parametrize("target", [ + pytest.param(None, id="NO_TARGET"), + pytest.param(TtApp.app_target, id="APP_TARGET"), + pytest.param(TtApp.Target("master"), id="master"), + pytest.param(TtApp.Target("router"), id="router"), +]) +def test_multi_inst_kill_auto_confirm_shortopt(tt_app, running_targets, target, post_start): + check_multi_inst_kill(tt_app, running_targets, post_start, target, auto_confirm="-f") + + +# Test with auto-confirmation (long option; less variations, just a few common cases). +@pytest.mark.parametrize("running_targets", [ + pytest.param([], id="running:none"), + pytest.param([TtApp.app_target], id="running:all"), +]) +def test_multi_inst_kill_auto_confirm_longopt(tt_app, running_targets): + check_multi_inst_kill(tt_app, running_targets, None, TtApp.app_target, auto_confirm="--force") + + +# Test with the various inputs. +@pytest.mark.parametrize("running_targets", [ + pytest.param([], id="running:none"), + pytest.param([TtApp.app_target], id="running:all"), +]) +@pytest.mark.parametrize("input", [ + TtApp.Input("y\n", True), # confirm (lowercase) + TtApp.Input("Y\n", True), # confirm (uppercase) + TtApp.Input("nn\nny\ny\n", True), # confirm preceded with the wrong answers + TtApp.Input("n\n", False), # discard (lowercase) + TtApp.Input("N\n", False), # discard (uppercase) + TtApp.Input("yy\nyn\nn\n", False), # discard preceded with the wrong answers +]) +def test_multi_inst_kill_input(tt_app, running_targets, input): + check_multi_inst_kill(tt_app, running_targets, None, TtApp.app_target, input=input) diff --git a/test/integration/logrotate/multi_inst_app b/test/integration/logrotate/multi_inst_app new file mode 120000 index 000000000..a432b10ce --- /dev/null +++ b/test/integration/logrotate/multi_inst_app @@ -0,0 +1 @@ +../running/multi_inst_app \ No newline at end of file diff --git a/test/integration/logrotate/test_logrotate.py b/test/integration/logrotate/test_logrotate.py new file mode 100644 index 000000000..d6f554b6a --- /dev/null +++ b/test/integration/logrotate/test_logrotate.py @@ -0,0 +1,77 @@ +import os + +import pytest + +from utils import (TtApp, log_file, tt_app_remove_app_script, tt_app_start, + tt_app_wait_log_files) + +################################################################ +# multi-instance app + +app_path = os.path.join(os.path.dirname(__file__), "multi_inst_app") +app_name = "multi_inst_app" +inst_names = ["router", "master", "replica", "stateboard"] + + +def check_multi_inst_logrotate(tt_app, running_targets, post_start, target): + # Prepare the application considering the test parameters. + orig_status = tt_app_start(tt_app, running_targets, post_start) + + # Rename log files. + for inst_name in tt_app.inst_names: + inst_id = tt_app.inst_id(inst_name) + if tt_app.inst_match_target(inst_name, target): + if orig_status[inst_id]["STATUS"] == "RUNNING": + log_path = tt_app.log_path(inst_name, log_file) + os.rename(log_path, log_path + '0') + + # Do logrotate. + rc, out = tt_app.logrotate(target) + + # If any of the requested instances is not running return failure. + for inst_name in tt_app.inst_names: + if tt_app.inst_match_target(inst_name, target) and \ + not tt_app.inst_match_targets(inst_name, running_targets): + assert rc != 0 + assert "NOT RUNNING" in out + return + + # Wait for the files to be re-created. + assert rc == 0 + status = tt_app.status() + log_paths = [] + for inst_name in tt_app.inst_names: + inst_id = tt_app.inst_id(inst_name) + log_path = tt_app.log_path(inst_name, log_file) + if tt_app.inst_match_target(inst_name, target): + assert status[inst_id]["STATUS"] == "RUNNING" + assert status[inst_id] == orig_status[inst_id] + pid = orig_status[inst_id]["PID"] + assert f"{inst_id}: logs has been rotated. PID: {pid}" in out + log_paths.append(log_path) + tt_app_wait_log_files(tt_app, target) + for log_path in log_paths: + with open(log_path) as f: + assert "reopened" in f.read() + + +# Test various scenarios. +@pytest.mark.parametrize("running_targets", [ + pytest.param([], id="running:none"), + pytest.param([TtApp.app_target], id="running:all"), + pytest.param([TtApp.Target("master")], id="running:master"), + pytest.param([TtApp.Target("master"), TtApp.Target("router")], id="running:master,router"), +]) +@pytest.mark.parametrize("post_start", [ + None, + # post_start_make_identical_subdir, + tt_app_remove_app_script, +]) +@pytest.mark.parametrize("target", [ + pytest.param(None, id="NO_TARGET"), + pytest.param(TtApp.app_target, id="APP_TARGET"), + pytest.param(TtApp.Target("master"), id="master"), + pytest.param(TtApp.Target("router"), id="router"), +]) +def test_multi_inst_logrotate(tt_app, running_targets, post_start, target): + check_multi_inst_logrotate(tt_app, running_targets, post_start, target) diff --git a/test/integration/restart/multi_inst_app b/test/integration/restart/multi_inst_app new file mode 120000 index 000000000..a432b10ce --- /dev/null +++ b/test/integration/restart/multi_inst_app @@ -0,0 +1 @@ +../running/multi_inst_app \ No newline at end of file diff --git a/test/integration/restart/test_restart.py b/test/integration/restart/test_restart.py index b8b81fde6..f163f7612 100644 --- a/test/integration/restart/test_restart.py +++ b/test/integration/restart/test_restart.py @@ -2,7 +2,10 @@ import shutil import subprocess -from utils import pid_file, run_path, wait_file +import pytest + +from utils import (TtApp, pid_file, run_path, tt_app_start, + tt_app_wait_pid_files_changed, wait_file) def app_cmd(tt_cmd, tmpdir_with_cfg, cmd, input): @@ -94,3 +97,98 @@ def test_restart_no_args(tt_cmd, tmp_path): finally: app_cmd(tt_cmd, test_app_path, ["stop"], ["y\n"]) + + +################################################################ +# multi-instance app + +app_path = os.path.join(os.path.dirname(__file__), "multi_inst_app") +app_name = "multi_inst_app" +inst_names = ["router", "master", "replica", "stateboard"] + + +def check_multi_inst_restart(tt_app, running_targets, post_start, target, **kwargs): + # Prepare the application considering the test parameters. + orig_status = tt_app_start(tt_app, running_targets, post_start) + + # Do restart. + rc, out = tt_app.restart(target, **kwargs) + assert rc == 0 + + # Check the discarding. + if not tt_app.is_confirm(**kwargs): + status = tt_app.status() + assert "Restart is cancelled." in out + for inst_name in tt_app.inst_names: + inst_id = tt_app.inst_id(inst_name) + assert status[inst_id]["STATUS"] == orig_status[inst_id]["STATUS"] + if tt_app.inst_match_target(inst_name, target) and \ + tt_app.inst_match_targets(inst_name, running_targets): + assert status[inst_id]["PID"] == orig_status[inst_id]["PID"] + return + + # Make sure all involved PIDs are updated. + tt_app_wait_pid_files_changed(tt_app, target, orig_status) + status = tt_app.status() + print(f"status*: {status}") + + # Check the confirmation. + for inst_name in tt_app.inst_names: + inst_id = tt_app.inst_id(inst_name) + if tt_app.inst_match_target(inst_name, target): + if tt_app.inst_match_targets(inst_name, running_targets): + assert status[inst_id]["PID"] != orig_status[inst_id]["PID"] + orig_pid = orig_status[inst_id]["PID"] + assert f"The Instance {inst_id} (PID = {orig_pid}) has been terminated." in out + assert status[inst_id]["STATUS"] == "RUNNING" + assert f"Starting an instance [{inst_id}]" in out + else: + assert status[inst_id]["STATUS"] == orig_status[inst_id]["STATUS"] + assert inst_id not in out + + +# Test with auto-confirmation (short option). +@pytest.mark.parametrize("running_targets", [ + pytest.param([], id="running:none"), + pytest.param([TtApp.app_target], id="running:all"), + pytest.param([TtApp.Target("master")], id="running:master"), + pytest.param([TtApp.Target("master"), TtApp.Target("router")], id="running:master,router"), +]) +@pytest.mark.parametrize("post_start", [ + None, + # post_start_make_identical_subdir, +]) +@pytest.mark.parametrize("target", [ + pytest.param(None, id="NO_TARGET"), + pytest.param(TtApp.app_target, id="APP_TARGET"), + pytest.param(TtApp.Target("master"), id="master"), + pytest.param(TtApp.Target("router"), id="router"), +]) +def test_multi_inst_restart_auto_confirm_shortopt(tt_app, running_targets, post_start, target): + check_multi_inst_restart(tt_app, running_targets, post_start, target, auto_confirm="-y") + + +# Test with auto-confirmation (long option; less variations, just a few common cases). +@pytest.mark.parametrize("running_targets", [ + pytest.param([], id="running:none"), + pytest.param([TtApp.app_target], id="running:all"), +]) +def test_multi_inst_restart_auto_confirm_longopt(tt_app, running_targets): + check_multi_inst_restart(tt_app, running_targets, None, TtApp.app_target, auto_confirm="--yes") + + +# Test with the various inputs. +@pytest.mark.parametrize("running_targets", [ + pytest.param([], id="running:none"), + pytest.param([TtApp.app_target], id="running:all"), +]) +@pytest.mark.parametrize("input", [ + TtApp.Input("y\n", True), # confirm (lowercase) + TtApp.Input("Y\n", True), # confirm (uppercase) + TtApp.Input("nn\nny\ny\n", True), # confirm preceded with the wrong answers + TtApp.Input("n\n", False), # discard (lowercase) + TtApp.Input("N\n", False), # discard (uppercase) + TtApp.Input("yy\nyn\nn\n", False), # discard preceded with the wrong answers +]) +def test_multi_inst_restart_input(tt_app, running_targets, input): + check_multi_inst_restart(tt_app, running_targets, None, TtApp.app_target, input=input) diff --git a/test/integration/running/multi_inst_data_app/init.lua b/test/integration/running/multi_inst_data_app/init.lua new file mode 100644 index 000000000..9c7c97281 --- /dev/null +++ b/test/integration/running/multi_inst_data_app/init.lua @@ -0,0 +1,16 @@ +local inst_name = os.getenv('TARANTOOL_INSTANCE_NAME') +local app_name = os.getenv('TARANTOOL_APP_NAME') + +box.cfg{} + +-- Create something to generate xlogs. +box.schema.space.create('customers') + +while true do + if app_name ~= nil and inst_name ~= nil then + print(app_name .. ":" .. inst_name) + else + print("unknown instance") + end + require("fiber").sleep(1) +end diff --git a/test/integration/running/multi_inst_data_app/instances.yml b/test/integration/running/multi_inst_data_app/instances.yml new file mode 100644 index 000000000..7d607c3ee --- /dev/null +++ b/test/integration/running/multi_inst_data_app/instances.yml @@ -0,0 +1,7 @@ +router: + +master: + +replica: + +app-stateboard: diff --git a/test/integration/running/multi_inst_data_app/router.init.lua b/test/integration/running/multi_inst_data_app/router.init.lua new file mode 100644 index 000000000..35207c3b9 --- /dev/null +++ b/test/integration/running/multi_inst_data_app/router.init.lua @@ -0,0 +1,12 @@ +local inst_name = os.getenv('TARANTOOL_INSTANCE_NAME') +local app_name = os.getenv('TARANTOOL_APP_NAME') + +while true do + print("custom init file...") + if app_name ~= nil and inst_name ~= nil then + print(app_name .. ":" .. inst_name) + else + print("unknown instance") + end + require("fiber").sleep(1) +end diff --git a/test/integration/running/multi_inst_data_app/tt.yaml b/test/integration/running/multi_inst_data_app/tt.yaml new file mode 100644 index 000000000..fe2ff3ab2 --- /dev/null +++ b/test/integration/running/multi_inst_data_app/tt.yaml @@ -0,0 +1,2 @@ +env: + instances_enabled: . diff --git a/test/integration/running/test_running.py b/test/integration/running/test_running.py index 6cec5e1d7..ad2dd0163 100644 --- a/test/integration/running/test_running.py +++ b/test/integration/running/test_running.py @@ -242,110 +242,6 @@ def test_clean(tt_cmd, tmpdir_with_cfg): assert_file_cleaned(os.path.join(lib_dir, initial_xlog), app_name, clean_out) -def test_running_base_functionality_working_dir_app(tt_cmd): - test_app_path_src = os.path.join(os.path.dirname(__file__), "multi_inst_app") - instances = ["router", "master", "replica", "stateboard"] - - # Default temporary directory may have very long path. This can cause socket path buffer - # overflow. Create our own temporary directory. - with tempfile.TemporaryDirectory() as tmpdir: - test_app_path = os.path.join(tmpdir, "app") - shutil.copytree(test_app_path_src, test_app_path) - - for subdir in ["", "app"]: - if subdir != "": - os.mkdir(os.path.join(test_app_path, "app")) - # Start an instance. - start_cmd = [tt_cmd, "start", "app"] - instance_process = subprocess.Popen( - start_cmd, - cwd=test_app_path, - stderr=subprocess.STDOUT, - stdout=subprocess.PIPE, - text=True - ) - start_output = instance_process.stdout.readline() - assert re.search(r"Starting an instance \[app:(router|master|replica|stateboard)\]", - start_output) - - # Check status. - for instName in instances: - print(os.path.join(test_app_path, "run", "app", instName)) - file = wait_file(os.path.join(test_app_path, run_path, instName), pid_file, []) - assert file != "" - - status_cmd = [tt_cmd, "status", "app"] - status_rc, status_out = run_command_and_get_output(status_cmd, cwd=test_app_path) - assert status_rc == 0 - status_out = extract_status(status_out) - - for instName in instances: - assert status_out[f"app:{instName}"]["STATUS"] == "RUNNING" - - # Stop the application. - stop_cmd = [tt_cmd, "stop", "-y", "app"] - stop_rc, stop_out = run_command_and_get_output(stop_cmd, cwd=test_app_path) - assert stop_rc == 0 - assert re.search(r"The Instance app:(router|master|replica|stateboard) \(PID = \d+\) " - r"has been terminated.", stop_out) - - # Check that the process was terminated correctly. - instance_process_rc = instance_process.wait(1) - assert instance_process_rc == 0 - - -def test_running_base_functionality_working_dir_app_no_app_name(tt_cmd): - test_app_path_src = os.path.join(os.path.dirname(__file__), "multi_inst_app") - instances = ["router", "master", "replica", "stateboard"] - - # Default temporary directory may have very long path. This can cause socket path buffer - # overflow. Create our own temporary directory. - with tempfile.TemporaryDirectory() as tmpdir: - test_app_path = os.path.join(tmpdir, "app") - shutil.copytree(test_app_path_src, test_app_path) - - for subdir in ["", "app"]: - if subdir != "": - os.mkdir(os.path.join(test_app_path, "app")) - # Start an instance. - start_cmd = [tt_cmd, "start"] - instance_process = subprocess.Popen( - start_cmd, - cwd=test_app_path, - stderr=subprocess.STDOUT, - stdout=subprocess.PIPE, - text=True - ) - start_output = instance_process.stdout.readline() - assert re.search(r"Starting an instance \[app:(router|master|replica|stateboard)\]", - start_output) - - # Check status. - for instName in instances: - print(os.path.join(test_app_path, "run", "app", instName)) - file = wait_file(os.path.join(test_app_path, run_path, instName), pid_file, []) - assert file != "" - - status_cmd = [tt_cmd, "status"] - status_rc, status_out = run_command_and_get_output(status_cmd, cwd=test_app_path) - assert status_rc == 0 - status_out = extract_status(status_out) - - for instName in instances: - assert status_out[f"app:{instName}"]["STATUS"] == "RUNNING" - - # Stop the application. - stop_cmd = [tt_cmd, "stop", "-y"] - stop_rc, stop_out = run_command_and_get_output(stop_cmd, cwd=test_app_path) - assert stop_rc == 0 - assert re.search(r"The Instance app:(router|master|replica|stateboard) \(PID = \d+\) " - r"has been terminated.", stop_out) - - # Check that the process was terminated correctly. - instance_process_rc = instance_process.wait(1) - assert instance_process_rc == 0 - - def test_running_instance_from_multi_inst_app(tt_cmd): test_app_path_src = os.path.join(os.path.dirname(__file__), "multi_inst_app") diff --git a/test/integration/start/multi_inst_app b/test/integration/start/multi_inst_app new file mode 120000 index 000000000..a432b10ce --- /dev/null +++ b/test/integration/start/multi_inst_app @@ -0,0 +1 @@ +../running/multi_inst_app \ No newline at end of file diff --git a/test/integration/start/test_start.py b/test/integration/start/test_start.py new file mode 100644 index 000000000..b723f5d05 --- /dev/null +++ b/test/integration/start/test_start.py @@ -0,0 +1,64 @@ +import os + +import pytest + +from utils import TtApp, tt_app_start, tt_app_wait_pid_files + +################################################################ +# multi-instance app + +app_path = os.path.join(os.path.dirname(__file__), "multi_inst_app") +app_name = "multi_inst_app" +inst_names = ["router", "master", "replica", "stateboard"] + + +def check_multi_inst_start(tt_app, running_targets, post_start, target): + # Prepare the application considering the test parameters. + orig_status = tt_app_start(tt_app, running_targets, post_start) + + # Do start. + rc, out = tt_app.start(target) + assert rc == 0 + assert tt_app_wait_pid_files(tt_app, target) + status = tt_app.status() + + for inst_name in tt_app.inst_names: + inst_id = tt_app.inst_id(inst_name) + # Figure out if this instance was already running. + already_running = tt_app.inst_match_targets(inst_name, running_targets) + if tt_app.inst_match_target(inst_name, target): + assert status[inst_id]["STATUS"] == "RUNNING" + pid = status[inst_id]["PID"] + msg_done = f"Starting an instance [{inst_id}]" + msg_warn = f"The instance {inst_id} (PID = {pid}) is already running." + if already_running: + assert pid == orig_status[inst_id]["PID"] + assert msg_done not in out + assert msg_warn in out + else: + assert msg_done in out + assert msg_warn not in out + else: + assert status[inst_id]["STATUS"] == orig_status[inst_id]["STATUS"] + assert not already_running or status[inst_id]["PID"] == orig_status[inst_id]["PID"] + assert inst_id not in out + + +@pytest.mark.parametrize("running_targets", [ + pytest.param([], id="running:none"), + pytest.param([TtApp.app_target], id="running:all"), + pytest.param([TtApp.Target("master")], id="running:master"), + pytest.param([TtApp.Target("master"), TtApp.Target("router")], id="running:master,router"), +]) +@pytest.mark.parametrize("post_start", [ + None, + # post_start_make_identical_subdir, +]) +@pytest.mark.parametrize("target", [ + pytest.param(None, id="NO_TARGET"), + pytest.param(TtApp.app_target, id="APP_TARGET"), + pytest.param(TtApp.Target("master"), id="master"), + pytest.param(TtApp.Target("router"), id="router"), +]) +def test_multi_inst_start(tt_app, running_targets, post_start, target): + check_multi_inst_start(tt_app, running_targets, post_start, target) diff --git a/test/integration/stop/multi_inst_app b/test/integration/stop/multi_inst_app new file mode 120000 index 000000000..a432b10ce --- /dev/null +++ b/test/integration/stop/multi_inst_app @@ -0,0 +1 @@ +../running/multi_inst_app \ No newline at end of file diff --git a/test/integration/stop/test_stop.py b/test/integration/stop/test_stop.py index 79bdfd2bc..4b5af116c 100644 --- a/test/integration/stop/test_stop.py +++ b/test/integration/stop/test_stop.py @@ -1,7 +1,10 @@ import os import shutil -from utils import pid_file, run_command_and_get_output, run_path, wait_file +import pytest + +from utils import (TtApp, pid_file, run_command_and_get_output, run_path, + tt_app_remove_app_script, tt_app_start, wait_file) def test_stop(tt_cmd, tmpdir_with_cfg): @@ -126,3 +129,92 @@ def test_stop_no_prompt(tt_cmd, tmpdir_with_cfg): finally: stop_cmd = [tt_cmd, "stop", "-y", app_name] run_command_and_get_output(stop_cmd, cwd=tmpdir_with_cfg) + + +################################################################ +# multi-instance app + +app_path = os.path.join(os.path.dirname(__file__), "multi_inst_app") +app_name = "multi_inst_app" +inst_names = ["router", "master", "replica", "stateboard"] + + +def check_multi_inst_stop(tt_app, running_targets, post_start, target, **kwargs): + # Prepare the application considering the test parameters. + orig_status = tt_app_start(tt_app, running_targets, post_start) + + # Do stop. + rc, out = tt_app.stop(target, **kwargs) + assert rc == 0 + status = tt_app.status() + + # Check the discarding. + if not tt_app.is_confirm(**kwargs): + assert "Stop is cancelled." in out + for inst_name in tt_app.inst_names: + inst_id = tt_app.inst_id(inst_name) + assert status[inst_id]["STATUS"] == orig_status[inst_id]["STATUS"] + if tt_app.inst_match_target(inst_name, target) and \ + tt_app.inst_match_targets(inst_name, running_targets): + assert status[inst_id]["PID"] == orig_status[inst_id]["PID"] + return + + # Check the confirmation. + for inst_name in tt_app.inst_names: + inst_id = tt_app.inst_id(inst_name) + if tt_app.inst_match_target(inst_name, target): + assert status[inst_id]["STATUS"] == "NOT RUNNING" + if tt_app.inst_match_targets(inst_name, running_targets): + orig_pid = orig_status[inst_id]["PID"] + assert f"The Instance {inst_id} (PID = {orig_pid}) has been terminated." in out + else: + assert status[inst_id]["STATUS"] == orig_status[inst_id]["STATUS"] + assert inst_id not in out + + +# Test with auto-confirmation (short option). +@pytest.mark.parametrize("running_targets", [ + pytest.param([], id="running:none"), + pytest.param([TtApp.app_target], id="running:all"), + pytest.param([TtApp.Target("master")], id="running:master"), + pytest.param([TtApp.Target("master"), TtApp.Target("router")], id="running:master,router"), +]) +@pytest.mark.parametrize("post_start", [ + None, + # post_start_make_identical_subdir, + tt_app_remove_app_script, +]) +@pytest.mark.parametrize("target", [ + pytest.param(None, id="NO_TARGET"), + pytest.param(TtApp.app_target, id="APP_TARGET"), + pytest.param(TtApp.Target("master"), id="master"), + pytest.param(TtApp.Target("router"), id="router"), +]) +def test_multi_inst_stop_auto_confirm_shortopt(tt_app, running_targets, post_start, target): + check_multi_inst_stop(tt_app, running_targets, post_start, target, auto_confirm="-y") + + +# Test with auto-confirmation (long option; less variations, just a few common cases). +@pytest.mark.parametrize("running_targets", [ + pytest.param([], id="running:none"), + pytest.param([TtApp.app_target], id="running:all"), +]) +def test_multi_inst_stop_auto_confirm_longopt(tt_app, running_targets): + check_multi_inst_stop(tt_app, running_targets, None, TtApp.app_target, auto_confirm="--yes") + + +# Test with the various inputs. +@pytest.mark.parametrize("running_targets", [ + pytest.param([], id="running:none"), + pytest.param([TtApp.app_target], id="running:all"), +]) +@pytest.mark.parametrize("input", [ + TtApp.Input("y\n", True), # confirm (lowercase) + TtApp.Input("Y\n", True), # confirm (uppercase) + TtApp.Input("nn\nny\ny\n", True), # confirm preceded with the wrong answers + TtApp.Input("n\n", False), # discard (lowercase) + TtApp.Input("N\n", False), # discard (uppercase) + TtApp.Input("yy\nyn\nn\n", False), # discard preceded with the wrong answers +]) +def test_multi_inst_stop_input(tt_app, running_targets, input): + check_multi_inst_stop(tt_app, running_targets, None, TtApp.app_target, input=input) diff --git a/test/utils.py b/test/utils.py index fd78d3f4e..333552c0a 100644 --- a/test/utils.py +++ b/test/utils.py @@ -5,6 +5,7 @@ import socket import subprocess import time +from collections import namedtuple import netifaces import psutil @@ -187,6 +188,217 @@ def wait_event(timeout, event_func, interval=0.1): return False +def wait_file_path(file_path, timeout=2, interval=0.1): + def is_file_exist(): + return os.path.exists(file_path) + return wait_event(timeout, is_file_exist, interval) + + +def wait_files(timeout, files, interval=0.1): + def all_files_exist(): + for file in files: + if not os.path.exists(file): + return False + return True + return wait_event(timeout, all_files_exist, interval) + + +def wait_file_changed(file_path, orig_content, timeout=2, interval=0.1): + def is_file_changed(): + with open(file_path) as f: + return f.read() != orig_content + if not os.path.exists(file_path): + return wait_file_path(file_path, timeout, interval) + return wait_event(timeout, is_file_changed, interval) + + +class TtApp(object): + # Helper type to represent target. Commands (start, stop, etc.) treat it as following: + # None - no target at all (ex: tt start) + # Target(None) - app name only (ex: tt start some_app) + # Target('some_inst') - app:inst target (ex: tt start some_app:some_inst) + Target = namedtuple('Target', ['inst_name']) + # This class variable can be used when + app_target = Target(None) + + @classmethod + def inst_match_target(cls, inst_name, target): + assert target is None or isinstance(target, cls.Target) + return target is None or target.inst_name is None or target.inst_name == inst_name + + @classmethod + def inst_match_targets(cls, inst_name, targets): + for target in targets: + if cls.inst_match_target(inst_name, target): + return True + return False + + Input = namedtuple('Input', ['str', 'is_confirm']) + + @classmethod + def is_confirm(cls, **kwargs): + input = kwargs.get('input') + auto_confirm = kwargs.get('auto_confirm') + assert auto_confirm is None or input is None + assert input is None or isinstance(input, cls.Input) + return auto_confirm is not None or input.is_confirm + + def __init__(self, tt_cmd, tmpdir, app_path, inst_names): + self.__app_name = os.path.basename(app_path) + self.__inst_names = inst_names + self.__tt_cmd = tt_cmd + # if False: + # # Copy the test application to the "run" directory. + # if os.path.isdir(app_path): + # print("shutil.copytree") + # shutil.copytree(app_path, tmpdir) + # app_name = os.path.basename(app_path) + # else: + # print("shutil.copy") + # shutil.copy(app_path, tmpdir) + # app_name = os.path.splitext(app_path)[0] + # print(f"app_path={app_path}") + # print(f"app_name={app_name}") + # print(f"tmpdir={tmpdir}") + # print("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^") + self.__app_dir = shutil.copytree(app_path, os.path.join(tmpdir, self.__app_name)) + + # def __del__(self): + # self.stop() + + def __run_tt(self, cmd, target, **kwargs): + cmd = [self.__tt_cmd, cmd] + input = kwargs.get('input') + auto_confirm = kwargs.get('auto_confirm') + assert input is None or auto_confirm is None + if auto_confirm is not None: + cmd.append(auto_confirm) + elif input is not None: + assert isinstance(input, self.Input) + input = input.str + if target is not None: + if target.inst_name is None: + cmd.append(self.__app_name) + else: + cmd.append(f'{self.__app_name}:{target.inst_name}') + return run_command_and_get_output(cmd, cwd=self.__app_dir, input=input) + + def inst_id(self, inst_name): + return f"{self.__app_name}:{inst_name}" + + @property + def app_dir(self): + return self.__app_dir + + @property + def inst_names(self): + return self.__inst_names + + def status(self, target=app_target): + rc, out = self.__run_tt("status", target) + assert rc == 0 + return extract_status(out) + + def app_path(self, *paths): + return os.path.join(self.app_dir, *paths) + + def run_path(self, inst_name, *paths): + return os.path.join(self.app_dir, run_path, inst_name, *paths) + + def lib_path(self, inst_name, *paths): + return os.path.join(self.app_dir, lib_path, inst_name, *paths) + + def log_path(self, inst_name, *paths): + return os.path.join(self.app_dir, log_path, inst_name, *paths) + + def start(self, target=app_target): + return self.__run_tt("start", target) + + def stop(self, target=app_target, **kwargs): + return self.__run_tt("stop", target, **kwargs) + + def restart(self, target=app_target, **kwargs): + return self.__run_tt("restart", target, **kwargs) + + def kill(self, target=app_target, **kwargs): + return self.__run_tt("kill", target, **kwargs) + + def clean(self, target=app_target, **kwargs): + return self.__run_tt("clean", target, **kwargs) + + def logrotate(self, target=app_target): + return self.__run_tt("logrotate", target) + + +def tt_app_start(tt_app, targets, post_start=None): + for target in targets: + rc, _ = tt_app.start(target) + assert rc == 0 + assert tt_app_wait_pid_files(tt_app, target) + if post_start is not None: + post_start(tt_app) + return tt_app.status() + + +def tt_app_make_identical_subdir(app): + os.mkdir(app.app_path(os.path.basename(app.app_dir))) + + +def tt_app_remove_app_script(app): + time.sleep(0.5) + os.remove(app.app_path("init.lua")) + + +def tt_app_wait_pid_files(tt_app, target, timeout=2, interval=0.1): + files = [] + for inst_name in tt_app.inst_names: + if tt_app.inst_match_target(inst_name, target): + files.append(tt_app.run_path(inst_name, pid_file)) + return wait_files(timeout, files, interval) + + +def tt_app_wait_log_files(tt_app, target, timeout=2, interval=0.1): + files = [] + for inst_name in tt_app.inst_names: + if tt_app.inst_match_target(inst_name, target): + files.append(tt_app.run_path(inst_name, log_file)) + return wait_files(timeout, files, interval) + + +def tt_app_wait_data_files(tt_app, target, timeout=5, interval=0.1): + files = [] + for inst_name in tt_app.inst_names: + if tt_app.inst_match_target(inst_name, target): + if inst_name in ["master", "replica"]: + files.append(tt_app.lib_path(inst_name, initial_snap)) + files.append(tt_app.lib_path(inst_name, initial_xlog)) + files.append(tt_app.log_path(inst_name, log_file)) + return wait_files(timeout, files, interval) + + +def tt_app_wait_pid_files_changed(tt_app, target, orig_status): + def all_pids_changed(): + def read_file(path): + try: + with open(path) as f: + return f.read() + except OSError: + return None + return None + + for inst_name in tt_app.inst_names: + if not tt_app.inst_match_target(inst_name, target): + continue + pid_path = tt_app.run_path(inst_name, pid_file) + inst_id = tt_app.inst_id(inst_name) + orig_pid = str(orig_status[inst_id]["PID"]) if "PID" in orig_status[inst_id] else None + pid = read_file(pid_path) + if pid is None or pid == orig_pid: + return False + return True + return wait_event(10, all_pids_changed, 0.1) + + class TarantoolTestInstance: """Create test tarantool instance via subprocess.Popen with given cfg file.