Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions qase-python-commons/changelog.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 3 additions & 3 deletions qase-python-commons/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}]
Expand All @@ -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"
]

Expand Down
85 changes: 83 additions & 2 deletions qase-python-commons/src/qase/commons/client/api_v1_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@

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
from ..exceptions.reporter import ReporterException
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):
Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
27 changes: 27 additions & 0 deletions qase-python-commons/src/qase/commons/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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)

Expand Down
36 changes: 36 additions & 0 deletions qase-python-commons/src/qase/commons/models/config/testops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
123 changes: 123 additions & 0 deletions qase-python-commons/tests/tests_qase_commons/test_configurations.py
Original file line number Diff line number Diff line change
@@ -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