From 759ea0a8e512ea369770c75137298fccb67259e4 Mon Sep 17 00:00:00 2001 From: AenEnlil Date: Sun, 16 Nov 2025 16:23:32 +0200 Subject: [PATCH 1/2] implemented --kind option --- pytestomatio/connect/connector.py | 6 +- pytestomatio/main.py | 5 +- pytestomatio/testomatio/testRunConfig.py | 5 +- pytestomatio/utils/constants.py | 2 + pytestomatio/utils/parser_setup.py | 3 + pytestomatio/utils/validations.py | 8 +++ tests/test_connect/test_connector.py | 8 ++- tests/test_testomatio/test_testRunConfig.py | 25 +++++++- tests/test_utils/test_parser_setup.py | 14 ++++- tests/test_utils/test_validations.py | 63 +++++++++++++++++---- 10 files changed, 120 insertions(+), 19 deletions(-) create mode 100644 pytestomatio/utils/constants.py diff --git a/pytestomatio/connect/connector.py b/pytestomatio/connect/connector.py index ee78515..aad0347 100644 --- a/pytestomatio/connect/connector.py +++ b/pytestomatio/connect/connector.py @@ -121,7 +121,7 @@ def get_tests(self, test_metadata: list[TestItem]) -> dict: return response.json() def create_test_run(self, access_event: str, title: str, group_title, env: str, label: str, shared_run: bool, shared_run_timeout: str, - parallel, ci_build_url: str) -> dict | None: + parallel, ci_build_url: str, kind: str) -> dict | None: request = { "access_event": access_event, "api_key": self.api_key, @@ -129,6 +129,7 @@ def create_test_run(self, access_event: str, title: str, group_title, env: str, "group_title": group_title, "env": env, "label": label, + "kind": kind, "parallel": parallel, "ci_build_url": ci_build_url, "shared_run": shared_run, @@ -151,7 +152,7 @@ def create_test_run(self, access_event: str, title: str, group_title, env: str, log.info(f'Test run created {response.json()["uid"]}') return response.json() - def update_test_run(self, id: str, access_event: str, title: str, group_title, + def update_test_run(self, id: str, access_event: str, title: str, group_title, kind: str, env: str, label: str, shared_run: bool, shared_run_timeout: str, parallel, ci_build_url: str) -> dict | None: request = { "access_event": access_event, @@ -159,6 +160,7 @@ def update_test_run(self, id: str, access_event: str, title: str, group_title, "title": title, "group_title": group_title, "env": env, + "kind": kind, "label": label, "parallel": parallel, "ci_build_url": ci_build_url, diff --git a/pytestomatio/main.py b/pytestomatio/main.py index 2b0d4fc..736cb60 100644 --- a/pytestomatio/main.py +++ b/pytestomatio/main.py @@ -44,7 +44,10 @@ def pytest_configure(config: Config): if option == 'debug': return - pytest.testomatio = Testomatio(TestRunConfig()) + run_kind_option = config.getoption('kind') + run_kind_option = run_kind_option.lower() if run_kind_option else 'automated' + + pytest.testomatio = Testomatio(TestRunConfig(run_kind_option)) url = os.environ.get('TESTOMATIO_URL') or config.getini('testomatio_url') or TESTOMATIO_URL project = os.environ.get('TESTOMATIO') diff --git a/pytestomatio/testomatio/testRunConfig.py b/pytestomatio/testomatio/testRunConfig.py index 5496254..bb17ca2 100644 --- a/pytestomatio/testomatio/testRunConfig.py +++ b/pytestomatio/testomatio/testRunConfig.py @@ -6,8 +6,9 @@ TESTOMATIO_TEST_RUN_LOCK_FILE = ".testomatio_test_run_id_lock" + class TestRunConfig: - def __init__(self): + def __init__(self, kind: str = 'automated'): run_id = os.environ.get('TESTOMATIO_RUN_ID') or os.environ.get('TESTOMATIO_RUN') title = os.environ.get('TESTOMATIO_TITLE') if os.environ.get('TESTOMATIO_TITLE') else 'test run at ' + dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S") shared_run = os.environ.get('TESTOMATIO_SHARED_RUN') in ['True', 'true', '1'] @@ -17,6 +18,7 @@ def __init__(self): self.access_event = 'publish' if os.environ.get("TESTOMATIO_PUBLISH") else None self.test_run_id = run_id self.title = title + self.kind = kind self.environment = safe_string_list(os.environ.get('TESTOMATIO_ENV')) self.exclude_skipped = exclude_skipped self.label = safe_string_list(os.environ.get('TESTOMATIO_LABEL')) @@ -41,6 +43,7 @@ def to_dict(self) -> dict: result['group_title'] = self.group_title result['env'] = self.environment result['label'] = self.label + result['kind'] = self.kind result['parallel'] = self.parallel result['shared_run'] = self.shared_run result['shared_run_timeout'] = self.shared_run_timeout diff --git a/pytestomatio/utils/constants.py b/pytestomatio/utils/constants.py new file mode 100644 index 0000000..b2e80e7 --- /dev/null +++ b/pytestomatio/utils/constants.py @@ -0,0 +1,2 @@ + +RUN_KINDS = ('automated', 'manual', 'mixed') diff --git a/pytestomatio/utils/parser_setup.py b/pytestomatio/utils/parser_setup.py index 35dba0d..e432844 100644 --- a/pytestomatio/utils/parser_setup.py +++ b/pytestomatio/utils/parser_setup.py @@ -14,6 +14,9 @@ def parser_options(parser: Parser, testomatio='testomatio') -> None: group.addoption(f'--{testomatio}', action='store', help=help_text) + group.addoption(f'--kind', + action='store', + help="Specify kind of test run to be created") group.addoption(f'--testRunEnv', action='store', help=f'specify test run environment for testomat.io. Works only with --testomatio report') diff --git a/pytestomatio/utils/validations.py b/pytestomatio/utils/validations.py index bf17fee..a33d109 100644 --- a/pytestomatio/utils/validations.py +++ b/pytestomatio/utils/validations.py @@ -1,6 +1,7 @@ import os from typing import Literal from pytest import Config +from pytestomatio.utils.constants import RUN_KINDS from _pytest.config.exceptions import UsageError @@ -18,6 +19,13 @@ def validate_option(config: Config) -> Literal['sync', 'report', 'remove', 'debu raise ValueError('Test Run id was passed. Please unset TESTOMATIO_RUN_ID or ' 'TESTOMATIO_RUN env variablses to create an empty run') + run_kind_option = config.getoption('kind') + run_kind_option = run_kind_option.lower() if run_kind_option else None + if run_kind_option and option != "launch": + raise ValueError("You can choose run kind only for 'launch' option") + elif run_kind_option and run_kind_option not in RUN_KINDS: + raise ValueError(f"Not supported run kind. Choose one of next kinds: {RUN_KINDS}") + xdist_plugin = config.pluginmanager.getplugin('xdist') if xdist_plugin and option in ('sync', 'debug', 'remove'): if config.option.numprocesses == 0: diff --git a/tests/test_connect/test_connector.py b/tests/test_connect/test_connector.py index dad11a0..c28f766 100644 --- a/tests/test_connect/test_connector.py +++ b/tests/test_connect/test_connector.py @@ -143,6 +143,7 @@ def test_create_test_run_success(self, mock_post, connector): access_event="publish", group_title="Group 1", env="linux,chrome", + kind="automated", label="smoke", shared_run=False, shared_run_timeout="2", @@ -158,6 +159,7 @@ def test_create_test_run_success(self, mock_post, connector): "title": "Test Run", "group_title": "Group 1", "env": "linux,chrome", + "kind": "automated", "label": "smoke", "shared_run": False, "shared_run_timeout": "2", @@ -181,6 +183,7 @@ def test_create_test_run_filters_none_values(self, mock_post, connector): access_event=None, group_title=None, env=None, + kind="automated", label="smoke", shared_run=False, shared_run_timeout=None, @@ -192,6 +195,7 @@ def test_create_test_run_filters_none_values(self, mock_post, connector): expected_payload = { "api_key": "test_api_key_123", "title": "Test Run", + "kind": "automated", "label": "smoke", "shared_run": False, "parallel": True @@ -203,7 +207,7 @@ def test_create_test_run_http_error(self, mock_post, connector): """Test HTTP error handled wher create test run""" mock_post.side_effect = HTTPError("HTTP Error") - result = connector.create_test_run("Test", None, None, None, None, False, True, None, None) + result = connector.create_test_run("Test", None, None, None, None, False, True, None, None, "automated") assert result is None @patch('requests.Session.put') @@ -220,6 +224,7 @@ def test_update_test_run_success(self, mock_put, connector): title="Updated Run", group_title="Group", env="windows", + kind="automated", label="regression", shared_run=True, shared_run_timeout='2', @@ -235,6 +240,7 @@ def test_update_test_run_success(self, mock_put, connector): "title": "Updated Run", "group_title": "Group", "env": "windows", + "kind": "automated", "label": "regression", "shared_run": True, "shared_run_timeout": '2', diff --git a/tests/test_testomatio/test_testRunConfig.py b/tests/test_testomatio/test_testRunConfig.py index 3008909..3d59a47 100644 --- a/tests/test_testomatio/test_testRunConfig.py +++ b/tests/test_testomatio/test_testRunConfig.py @@ -3,6 +3,7 @@ from unittest.mock import patch, mock_open from pytestomatio.testomatio.testRunConfig import TestRunConfig, TESTOMATIO_TEST_RUN_LOCK_FILE +from pytestomatio.utils.constants import RUN_KINDS class TestTestRunConfig: @@ -20,6 +21,7 @@ def test_init_default_values(self): assert config.test_run_id is None assert config.title == "test run at 2024-01-15 10:30:45" assert config.environment is None + assert config.kind == 'automated' assert config.exclude_skipped is False assert config.label is None assert config.group_title is None @@ -58,6 +60,12 @@ def test_init_with_env_variables(self): assert config.update_code is True assert config.meta == {'linux': None, 'browser': 'chrome', '1920x1080': None} + @pytest.mark.parametrize("run_kind", RUN_KINDS) + def test_init_with_kind_passed(self, run_kind): + config = TestRunConfig(run_kind) + + assert config.kind == run_kind + @pytest.mark.parametrize('value', ['True', 'true', '1']) def test_init_shared_run_true_variations(self, value): """Test different true values for TESTOMATIO_SHARED_RUN""" @@ -110,8 +118,9 @@ def test_init_exclude_skipped_false_variations(self, value): assert config.exclude_skipped is False - def test_to_dict_full_data(self): - """Test to_dict with full data""" + @pytest.mark.parametrize("run_kind", RUN_KINDS) + def test_to_dict_full_data(self, run_kind): + """Test to_dict with full data and different run kinds""" env_vars = { 'TESTOMATIO_RUN_ID': 'run_123', 'TESTOMATIO_TITLE': 'Test Run', @@ -124,7 +133,7 @@ def test_to_dict_full_data(self): } with patch.dict(os.environ, env_vars, clear=True): - config = TestRunConfig() + config = TestRunConfig(kind=run_kind) result = config.to_dict() @@ -134,6 +143,7 @@ def test_to_dict_full_data(self): 'title': 'Test Run', 'group_title': 'Group 1', 'env': 'env1,env2', + 'kind': run_kind, 'label': 'label1,label2', 'parallel': False, 'shared_run': True, @@ -153,6 +163,15 @@ def test_to_dict_without_run_id(self): assert 'id' not in result assert result['title'] == 'No ID Run' + def test_to_dict_without_specifying_run_kind(self): + """Test to_dict without run kind passed""" + with patch.dict(os.environ, {}, clear=True): + config = TestRunConfig() + + result = config.to_dict() + + assert result['kind'] == 'automated' + def test_set_env(self): """Test set_env""" config = TestRunConfig() diff --git a/tests/test_utils/test_parser_setup.py b/tests/test_utils/test_parser_setup.py index 7300ac8..a560e60 100644 --- a/tests/test_utils/test_parser_setup.py +++ b/tests/test_utils/test_parser_setup.py @@ -45,6 +45,18 @@ def test_parser_options_adds_main_testomatio_option(self, mock_parser): help=help_text ) + def test_parser_options_adds_kind_option(self, mock_parser): + """Test --kind option is added""" + mock_group = mock_parser.getgroup.return_value + + parser_options(mock_parser) + + mock_group.addoption.assert_any_call( + '--kind', + action='store', + help="Specify kind of test run to be created" + ) + def test_parser_options_adds_test_run_env_option(self, mock_parser): """Test --testRunEnv option is added""" mock_group = mock_parser.getgroup.return_value @@ -189,7 +201,7 @@ def test_parser_options_call_count(self, mock_parser): parser_options(mock_parser) - assert mock_group.addoption.call_count == 8 + assert mock_group.addoption.call_count == 9 assert mock_parser.addini.call_count == 1 diff --git a/tests/test_utils/test_validations.py b/tests/test_utils/test_validations.py index 2b8d876..c70d36c 100644 --- a/tests/test_utils/test_validations.py +++ b/tests/test_utils/test_validations.py @@ -3,6 +3,7 @@ from unittest.mock import Mock, patch from _pytest.config.exceptions import UsageError +from pytestomatio.utils.constants import RUN_KINDS from pytestomatio.utils.validations import validate_option @@ -19,16 +20,16 @@ def mock_config(self): config.option.numprocesses = 0 return config - def test_validate_option_returns_none_when_no_option(self, mock_config): + def test_validate_option_returns_none_when_no_option(self, mock_config: Mock): """Test return None when testomatio option not set""" mock_config.getoption.return_value = None result = validate_option(mock_config) assert result is None - mock_config.getoption.assert_called_once_with('testomatio') + assert 'testomatio' in mock_config.mock_calls[0].args - @pytest.mark.parametrize("option_value", ['sync', 'report', 'remove']) + @pytest.mark.parametrize("option_value", ['sync', 'report', 'remove', 'launch', 'finish']) def test_validate_option_raises_error_when_no_testomatio_env(self, mock_config, option_value): """Test ValueError raised when no TESTOMATIO env""" mock_config.getoption.return_value = option_value @@ -40,7 +41,7 @@ def test_validate_option_raises_error_when_no_testomatio_env(self, mock_config, @pytest.mark.parametrize("option_value", ['sync', 'report', 'remove']) def test_validate_option_passes_when_testomatio_env_set(self, mock_config, option_value): """Test TESTOMATIO env validation passes""" - mock_config.getoption.return_value = option_value + mock_config.getoption.side_effect = lambda opt: option_value if opt == 'testomatio' else None with patch.dict(os.environ, {'TESTOMATIO': 'test_token_123'}): result = validate_option(mock_config) @@ -49,7 +50,7 @@ def test_validate_option_passes_when_testomatio_env_set(self, mock_config, optio def test_validate_option_debug_doesnt_require_testomatio_env(self, mock_config): """Test debug option doesnt require TESTOMATIO env""" - mock_config.getoption.return_value = 'debug' + mock_config.getoption.side_effect = lambda opt: 'debug' if opt == 'testomatio' else None with patch.dict(os.environ, {}, clear=True): result = validate_option(mock_config) @@ -64,6 +65,48 @@ def test_validate_launch_option_requires_api_key(self, mock_config): with pytest.raises(ValueError, match='TESTOMATIO env variable is not set'): validate_option(mock_config) + @pytest.mark.parametrize("run_kind", RUN_KINDS) + def test_validate_launch_option_can_be_used_with_kind_option(self, mock_config, run_kind): + """Test validation passes for launch option when run kind specified""" + options = {'testomatio': 'launch', 'kind': run_kind} + mock_config.getoption.side_effect = lambda opt: options.get(opt) + + with patch.dict(os.environ, {'TESTOMATIO': 'test_token_123'}, clear=True): + result = validate_option(mock_config) + + assert result == 'launch' + + def test_validate_launch_option_can_be_used_without_kind_option(self, mock_config): + """Test validation passes for launch option if kind not specified""" + options = {'testomatio': 'launch', 'kind': None} + mock_config.getoption.side_effect = lambda opt: options.get(opt) + + with patch.dict(os.environ, {'TESTOMATIO': 'test_token_123'}, clear=True): + result = validate_option(mock_config) + + assert result == 'launch' + + @pytest.mark.parametrize("run_kind", ['Incorrect_type', 'half-manual']) + def test_validate_launch_option_raises_error_if_incorrect_kind(self, mock_config, run_kind): + """Test validation not passed if incorrect testrun kind""" + options = {'testomatio': 'launch', 'kind': run_kind} + mock_config.getoption.side_effect = lambda opt: options.get(opt) + + with patch.dict(os.environ, {'TESTOMATIO': 'test_token_123'}, clear=True): + with pytest.raises(ValueError, match=f"Not supported run kind. Choose one of next kinds: \\({RUN_KINDS}\\)"): + validate_option(mock_config) + + @pytest.mark.parametrize("run_kind", RUN_KINDS) + @pytest.mark.parametrize("testomatio_option", ['sync', 'report', 'remove', 'debug', 'finish']) + def test_validate_kind_option_can_be_used_only_with_launch_option(self, mock_config, run_kind, testomatio_option): + """Test error raised if kind option used without launch option""" + options = {'testomatio': testomatio_option, 'kind': run_kind} + mock_config.getoption.side_effect = lambda opt: options.get(opt) + + with patch.dict(os.environ, {'TESTOMATIO': 'test_token_123', 'TESTOMATIO_RUN': 're23a'}, clear=True): + with pytest.raises(ValueError, match="You can choose run kind only for 'launch' option"): + validate_option(mock_config) + def test_validate_finish_option_requires_api_key(self, mock_config): """Test validation failed if no api key for finish option""" mock_config.getoption.return_value = 'launch' @@ -90,7 +133,7 @@ def test_validate_finish_option_error_if_run_id_not_set(self, mock_config): def test_validate_option_unknown_option_passes_through(self, mock_config): """Test unknown options passes through""" - mock_config.getoption.return_value = 'unknown_option' + mock_config.getoption.side_effect = lambda opt: 'unknown_option' if opt == 'testomatio' else None with patch.dict(os.environ, {}, clear=True): result = validate_option(mock_config) @@ -99,7 +142,7 @@ def test_validate_option_unknown_option_passes_through(self, mock_config): def test_validate_option_no_xdist_plugin_passes(self, mock_config): """Test validation without xdist plugin successful""" - mock_config.getoption.return_value = 'sync' + mock_config.getoption.side_effect = lambda opt: 'sync' if opt == 'testomatio' else None mock_config.pluginmanager.getplugin.return_value = None with patch.dict(os.environ, {'TESTOMATIO': 'test_token'}): @@ -111,7 +154,7 @@ def test_validate_option_no_xdist_plugin_passes(self, mock_config): @pytest.mark.parametrize("option_value", ['sync', 'debug', 'remove']) def test_validate_option_xdist_with_zero_processes_passes(self, mock_config, option_value): """Test xdist with 0 processes pass validation """ - mock_config.getoption.return_value = option_value + mock_config.getoption.side_effect = lambda opt: option_value if opt == 'testomatio' else None mock_config.pluginmanager.getplugin.return_value = Mock() mock_config.option.numprocesses = 0 @@ -124,7 +167,7 @@ def test_validate_option_xdist_with_zero_processes_passes(self, mock_config, opt @pytest.mark.parametrize("num_processes", [1, 2, 4, 8]) def test_validate_option_xdist_with_parallel_processes_raises_error(self, mock_config, option_value, num_processes): """Test xdist with і > 0 processes raise UsageError""" - mock_config.getoption.return_value = option_value + mock_config.getoption.side_effect = lambda opt: option_value if opt == 'testomatio' else None mock_config.pluginmanager.getplugin.return_value = Mock() mock_config.option.numprocesses = num_processes @@ -141,7 +184,7 @@ def test_validate_option_xdist_with_parallel_processes_raises_error(self, mock_c def test_validate_option_report_with_xdist_parallel_passes(self, mock_config): """Test report mode works with xdist""" - mock_config.getoption.return_value = 'report' + mock_config.getoption.side_effect = lambda opt: 'report' if opt == 'testomatio' else None mock_config.pluginmanager.getplugin.return_value = Mock() mock_config.option.numprocesses = 4 From 511fcd0dbb1d713e9149b3d4a948aba528fe4fd3 Mon Sep 17 00:00:00 2001 From: AenEnlil Date: Sun, 16 Nov 2025 20:03:57 +0200 Subject: [PATCH 2/2] bump: version 2.10.2 -> 2.11.0b2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5cae11c..9dd984f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ version_provider = "pep621" update_changelog_on_bump = false [project] name = "pytestomatio" -version = "2.10.2" +version = "2.11.0b2" dependencies = [