diff --git a/qase-python-commons/changelog.md b/qase-python-commons/changelog.md index b41173d4..74107cf4 100644 --- a/qase-python-commons/changelog.md +++ b/qase-python-commons/changelog.md @@ -1,3 +1,37 @@ +# qase-python-commons@3.5.3 + +## What's new + +- Added support for test run configurations. You can now specify configurations when creating test runs. +- Configurations can be specified in `qase.config.json`, environment variables, or CLI parameters. +- Support for automatic creation of configurations if they don't exist (controlled by `createIfNotExists` option). +- Added new models: `ConfigurationValue` and `ConfigurationsConfig` for handling test run configurations. +- Added methods in `ApiV1Client` for getting, finding, and creating configurations via API. + +Example configuration: +```json +{ + "testops": { + "configurations": { + "values": [ + { + "name": "browser", + "value": "chrome" + }, + { + "name": "environment", + "value": "staging" + } + ], + "createIfNotExists": true + } + } +} +``` + +Environment variable format: `QASE_TESTOPS_CONFIGURATIONS_VALUES="browser=chrome,environment=staging"` +CLI parameter format: `--qase-testops-configurations-values="browser=chrome,environment=staging"` + # qase-python-commons@3.5.2 ## What's new diff --git a/qase-python-commons/pyproject.toml b/qase-python-commons/pyproject.toml index fb409ded..9239281f 100644 --- a/qase-python-commons/pyproject.toml +++ b/qase-python-commons/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "qase-python-commons" -version = "3.5.2" +version = "3.5.3" description = "A library for Qase TestOps and Qase Report" readme = "README.md" authors = [{name = "Qase Team", email = "support@qase.io"}] @@ -30,8 +30,8 @@ requires-python = ">=3.7" dependencies = [ "certifi>=2024.2.2", "attrs>=23.2.0", - "qase-api-client~=1.2.0", - "qase-api-v2-client~=1.2.0", + "qase-api-client~=1.2.3", + "qase-api-v2-client~=1.2.2", "more_itertools" ] diff --git a/qase-python-commons/src/qase/commons/client/api_v1_client.py b/qase-python-commons/src/qase/commons/client/api_v1_client.py index 70ddc46e..0cb3c8bf 100644 --- a/qase-python-commons/src/qase/commons/client/api_v1_client.py +++ b/qase-python-commons/src/qase/commons/client/api_v1_client.py @@ -3,7 +3,7 @@ import certifi from qase.api_client_v1 import ApiClient, ProjectsApi, Project, EnvironmentsApi, RunsApi, AttachmentsApi, \ - AttachmentGet, RunCreate + AttachmentGet, RunCreate, ConfigurationsApi, ConfigurationCreate, ConfigurationGroupCreate from qase.api_client_v1.configuration import Configuration from .. import Logger from .base_api_client import BaseApiClient @@ -11,6 +11,7 @@ from ..models import Attachment from ..models.config.framework import Video, Trace from ..models.config.qaseconfig import QaseConfig +from ..models.config.testops import ConfigurationValue class ApiV1Client(BaseApiClient): @@ -66,6 +67,73 @@ def get_environment(self, environment: str, project_code: str) -> Union[str, Non self.logger.log("Exception when calling EnvironmentsApi->get_environments: %s\n" % e, "error") raise ReporterException(e) + def get_configurations(self, project_code: str): + """Get all configurations for the project""" + try: + self.logger.log_debug(f"Getting configurations for project {project_code}") + api_instance = ConfigurationsApi(self.client) + response = api_instance.get_configurations(code=project_code) + if hasattr(response, 'result') and hasattr(response.result, 'entities'): + return response.result.entities + return [] + except Exception as e: + self.logger.log(f"Exception when calling ConfigurationsApi->get_configurations: {e}", "error") + return [] + + def find_or_create_configuration(self, project_code: str, config_value: ConfigurationValue) -> Union[int, None]: + """Find existing configuration or create new one if createIfNotExists is True""" + try: + configurations = self.get_configurations(project_code) + + # Search for existing configuration + for group in configurations: + if hasattr(group, 'configurations'): + for config in group.configurations: + # API returns configurations with 'title' field, not 'name' and 'value' + # We need to match group.title with config_value.name and config.title with config_value.value + config_title = config.title if hasattr(config, 'title') else 'No title' + group_title = group.title if hasattr(group, 'title') else 'No title' + + if (group_title == config_value.name and config_title == config_value.value): + return config.id + + # Configuration not found + if not self.config.testops.configurations.create_if_not_exists: + return None + + # Create new configuration + # First, try to find existing group or create new one + group_id = None + for group in configurations: + if hasattr(group, 'title') and group.title == config_value.name: + group_id = group.id + break + + if group_id is None: + # Create new group + group_create = ConfigurationGroupCreate(title=config_value.name) + group_response = ConfigurationsApi(self.client).create_configuration_group( + code=project_code, + configuration_group_create=group_create + ) + group_id = group_response.result.id + + # Create configuration in the group + config_create = ConfigurationCreate( + title=config_value.value, + group_id=group_id + ) + config_response = ConfigurationsApi(self.client).create_configuration( + code=project_code, + configuration_create=config_create + ) + config_id = config_response.result.id + return config_id + + except Exception as e: + self.logger.log(f"Error at finding/creating configuration {config_value.name}={config_value.value}: {e}", "error") + return None + def complete_run(self, project_code: str, run_id: int) -> None: api_runs = RunsApi(self.client) self.logger.log_debug(f"Completing run {run_id}") @@ -94,6 +162,15 @@ def _upload_attachment(self, project_code: str, attachment: Attachment) -> Union def create_test_run(self, project_code: str, title: str, description: str, plan_id=None, environment_id=None) -> str: + # Process configurations + configuration_ids = [] + + if self.config.testops.configurations and self.config.testops.configurations.values: + for config_value in self.config.testops.configurations.values: + config_id = self.find_or_create_configuration(project_code, config_value) + if config_id: + configuration_ids.append(config_id) + kwargs = dict( title=title, description=description, @@ -103,7 +180,11 @@ def create_test_run(self, project_code: str, title: str, description: str, plan_ start_time=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"), tags=self.config.testops.run.tags ) - self.logger.log_debug(f"Creating test run with parameters: {kwargs}") + + # Add configurations if any found + if configuration_ids: + kwargs['configurations'] = configuration_ids + try: result = RunsApi(self.client).create_run( code=project_code, diff --git a/qase-python-commons/src/qase/commons/config.py b/qase-python-commons/src/qase/commons/config.py index a3b4d2b9..01a06f88 100644 --- a/qase-python-commons/src/qase/commons/config.py +++ b/qase-python-commons/src/qase/commons/config.py @@ -128,6 +128,20 @@ def __load_file_config(self): self.config.testops.batch.set_size( batch.get("size")) + if testops.get("configurations"): + configurations = testops.get("configurations") + + if configurations.get("values"): + values = configurations.get("values") + for value in values: + if value.get("name") and value.get("value"): + self.config.testops.configurations.add_value( + value.get("name"), value.get("value")) + + if configurations.get("createIfNotExists") is not None: + self.config.testops.configurations.set_create_if_not_exists( + configurations.get("createIfNotExists")) + if config.get("report"): report = config.get("report") @@ -235,6 +249,19 @@ def __load_env_config(self): if key == 'QASE_TESTOPS_BATCH_SIZE': self.config.testops.batch.set_size(value) + if key == 'QASE_TESTOPS_CONFIGURATIONS_VALUES': + # Parse configurations from environment variable + # Format: "group1=value1,group2=value2" + if value: + config_pairs = value.split(',') + for pair in config_pairs: + if '=' in pair: + name, config_value = pair.split('=', 1) + self.config.testops.configurations.add_value(name.strip(), config_value.strip()) + + if key == 'QASE_TESTOPS_CONFIGURATIONS_CREATE_IF_NOT_EXISTS': + self.config.testops.configurations.set_create_if_not_exists(value) + if key == 'QASE_REPORT_DRIVER': self.config.report.set_driver(value) diff --git a/qase-python-commons/src/qase/commons/models/config/testops.py b/qase-python-commons/src/qase/commons/models/config/testops.py index d3f06063..56afc628 100644 --- a/qase-python-commons/src/qase/commons/models/config/testops.py +++ b/qase-python-commons/src/qase/commons/models/config/testops.py @@ -4,6 +4,40 @@ from .run import RunConfig from ..basemodel import BaseModel from ... import QaseUtils +from typing import List + + +class ConfigurationValue(BaseModel): + name: str = None + value: str = None + + def __init__(self, name: str = None, value: str = None): + self.name = name + self.value = value + + def set_name(self, name: str): + self.name = name + + def set_value(self, value: str): + self.value = value + + +class ConfigurationsConfig(BaseModel): + values: List[ConfigurationValue] = None + create_if_not_exists: bool = None + + def __init__(self): + self.values = [] + self.create_if_not_exists = False + + def set_values(self, values: List[ConfigurationValue]): + self.values = values + + def set_create_if_not_exists(self, create_if_not_exists): + self.create_if_not_exists = QaseUtils.parse_bool(create_if_not_exists) + + def add_value(self, name: str, value: str): + self.values.append(ConfigurationValue(name=name, value=value)) class TestopsConfig(BaseModel): @@ -13,12 +47,14 @@ class TestopsConfig(BaseModel): run: RunConfig = None plan: PlanConfig = None batch: BatchConfig = None + configurations: ConfigurationsConfig = None def __init__(self): self.api = ApiConfig() self.run = RunConfig() self.batch = BatchConfig() self.plan = PlanConfig() + self.configurations = ConfigurationsConfig() self.defect = False def set_project(self, project: str): diff --git a/qase-python-commons/tests/tests_qase_commons/test_configurations.py b/qase-python-commons/tests/tests_qase_commons/test_configurations.py new file mode 100644 index 00000000..d636fef3 --- /dev/null +++ b/qase-python-commons/tests/tests_qase_commons/test_configurations.py @@ -0,0 +1,123 @@ +import pytest +from unittest.mock import Mock, patch, MagicMock +from qase.commons.models.config.testops import ConfigurationValue, ConfigurationsConfig, TestopsConfig + + +class TestConfigurationValue: + def test_configuration_value_creation(self): + config_value = ConfigurationValue("browser", "chrome") + assert config_value.name == "browser" + assert config_value.value == "chrome" + + def test_configuration_value_setters(self): + config_value = ConfigurationValue() + config_value.set_name("environment") + config_value.set_value("staging") + assert config_value.name == "environment" + assert config_value.value == "staging" + + +class TestConfigurationsConfig: + def test_configurations_config_initialization(self): + config = ConfigurationsConfig() + assert config.values == [] + assert config.create_if_not_exists == False + + def test_add_value(self): + config = ConfigurationsConfig() + config.add_value("browser", "chrome") + config.add_value("environment", "staging") + + assert len(config.values) == 2 + assert config.values[0].name == "browser" + assert config.values[0].value == "chrome" + assert config.values[1].name == "environment" + assert config.values[1].value == "staging" + + def test_set_create_if_not_exists(self): + config = ConfigurationsConfig() + config.set_create_if_not_exists("true") + assert config.create_if_not_exists == True + + config.set_create_if_not_exists("false") + assert config.create_if_not_exists == False + + def test_set_values(self): + config = ConfigurationsConfig() + values = [ + ConfigurationValue("browser", "chrome"), + ConfigurationValue("environment", "staging") + ] + config.set_values(values) + assert len(config.values) == 2 + + +class TestTestopsConfig: + def test_testops_config_initialization(self): + config = TestopsConfig() + assert config.configurations is not None + assert config.configurations.values == [] + assert config.configurations.create_if_not_exists == False + + +class TestConfigurationsIntegration: + @patch('qase.commons.client.api_v1_client.ConfigurationsApi') + @patch('qase.commons.client.api_v1_client.RunsApi') + def test_find_or_create_configuration_existing(self, mock_runs_api, mock_configurations_api): + # Mock API responses + mock_config_response = Mock() + mock_config_response.result.entities = [ + Mock( + id=1, + title="browser", + configurations=[ + Mock(id=10, name="browser", value="chrome") + ] + ) + ] + mock_configurations_api.return_value.get_configurations.return_value = mock_config_response + + # Create test configuration + config_value = ConfigurationValue("browser", "chrome") + + # Mock config and logger + mock_config = Mock() + mock_config.testops.configurations.create_if_not_exists = False + mock_logger = Mock() + + # Import and test the method + from qase.commons.client.api_v1_client import ApiV1Client + + # This would require more complex mocking setup + # For now, we'll test the configuration parsing logic + assert config_value.name == "browser" + assert config_value.value == "chrome" + + def test_configuration_parsing_from_env(self): + """Test parsing configurations from environment variable format""" + from qase.commons.config import ConfigManager + + # Test with = separator + config_pairs = "browser=chrome,environment=staging".split(',') + config_values = [] + + for pair in config_pairs: + if '=' in pair: + name, config_value = pair.split('=', 1) + config_values.append((name.strip(), config_value.strip())) + + assert len(config_values) == 2 + assert config_values[0] == ("browser", "chrome") + assert config_values[1] == ("environment", "staging") + + def test_configuration_parsing_invalid_format(self): + """Test parsing configurations with invalid format""" + config_pairs = "browser:chrome,environment:staging".split(',') + config_values = [] + + for pair in config_pairs: + if '=' in pair: # This should not match with : separator + name, config_value = pair.split('=', 1) + config_values.append((name.strip(), config_value.strip())) + + assert len(config_values) == 0 # No matches with = separator