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
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ A powerful pytest plugin that integrates your tests with testomat.io platform fo
- [Additional options](#additional-options)
- [Configuration](#configuration-with-environment-variables)
- [Test artifacts](#submitting-test-artifacts)
- [User functions](#user-functions)
- [Cross-Platform Testing](#cross-platform-testing)
- [Contributing](#contributing)

## Installation
Expand Down Expand Up @@ -339,6 +341,7 @@ You can use environment variable to control certain features of testomat.io. Env
| TESTOMATIO_STACK_PASSED | Enables logs for passed tests. Disabled by default. | TESTOMATIO_STACK_PASSED=true pytest --testomatio report |
| TESTOMATIO_SHARED_RUN | Report parallel execution to the same run matching it by title. If the run was created more than 20 minutes ago, a new run will be created instead. | TESTOMATIO_TITLE="Run1" TESTOMATIO_SHARED_RUN=1 pytest --testomatio report |
| TESTOMATIO_SHARED_RUN_TIMEOUT | Changes timeout of shared run. After timeout, shared run won`t accept other runs with same name, and new runs will be created. Timeout is set in minutes, default is 20 minutes. | TESTOMATIO_TITLE="Run1" TESTOMATIO_SHARED_RUN=1 TESTOMATIO_SHARED_RUN_TIMEOUT=10 pytest --testomatio report |
| TESTOMATIO_DISABLE_ARTIFACTS | Disables artifacts uploading during testrun. | TESTOMATIO_DISABLE_ARTIFACTS=1 pytest --testomatio report |
| TESTOMATIO_EXCLUDE_FILES_FROM_REPORT_GLOB_PATTERN | Excludes tests from report using glob patterns. You can specify multiple patterns using **;** as separator | TESTOMATIO_EXCLUDE_FILES_FROM_REPORT_GLOB_PATTERN="**/*_auth.py;directory" pytest --testomatio report |
| TESTOMATIO_CREATE | Create test which are not yet exist in a project | TESTOMATIO_CREATE=1 pytest --testomatio report |
| TESTOMATIO_WORKDIR | Specify a custom working directory for relative file paths in test reports. When tests are created with **TESTOMATIO_CREATE=1**, file paths will be relative to this directory. | TESTOMATIO_WORKDIR=new_dir pytest --testomatio report |
Expand Down Expand Up @@ -426,13 +429,88 @@ def handle_artifacts(page: Page, request):
⚠️ Please take into account s3_connector available only after **pytest_collection_modifyitems()** hook is executed.

2) If you prefer to use pytest hooks - add `pytest_runtest_makereport` hook in your `conftest.py` file.
3) Automatically upload artifacts using [add_artifact](#add-artifact) function

```python
def pytest_runtest_makereport(item, call):
artifact_url = pytest.testomatio.upload_file(screenshot_path, filename)
pytest.testomatio.add_artifacts([artifact_url])
```

### User functions
This functions gives you more flexibility in reporting and make your reports more powerful

**Available functions**
- [add_artifact](#add-artifact)
- [add_meta](#add-meta)
- [add_label](#add-label)
- [link_jira](#link-jira)
- [link_test](#link-test)

#### Add Artifact
Adds file to the test report. File will be uploaded during test run.

**Note:** S3 must be configured

```python
from pytestomatio.functions import add_artifact

def test_my_test():
path_to_file = 'path/to/file/image.png'
add_artifact(path_to_file)
assert True
```
#### Add Meta
Adds meta information to test. Meta information is a key:value pair(s), which is used to add additional information to the test report. E.g. browser, environment, etc.

**Note:** Test run metadata have higher priority than test metadata.
Therefore, if the test metadata and the test run metadata have the same keys, then the values from the test run metadata will be set for these keys

```python
from pytestomatio.functions import add_meta

def test_my_test():
add_meta({'browser': 'chrome', 'server': 'staging'})
assert True
```

#### Add Label
Adds a label to the reported test. Unlike *meta* label will be persisted to the test case itself, not just to reported run. If the label
does not exist in Testomat.io, it will be automatically created and linked to the test during the test run.
You can pass also a label value, if the label was created as a custom field

```python
from pytestomatio.functions import add_label

def test_my_test():
add_label('Browser')
add_label('Area', 'Auth')
assert True
```

#### Link Jira
Links JIRA issue IDs to the test report. This creates a connection between your test execution and JIRA issues.

```python
from pytestomatio.functions import link_jira

def test_my_test():
link_jira('PROJ-456', 'PROJ-564')
assert True
```

#### Link Test
Links test IDs to the current test in the report. This allows you to associate multiple test cases with the current test execution.

```python
from pytestomatio.functions import link_test

def test_my_test():
link_test('@T5147babc', '47d31979')
assert True
```


### Cross-Platform Testing
The plugin supports reporting the same test multiple times in a single run. This is especially useful for Cross-Platform
testing, when you run the same test on different environments.
Expand Down
6 changes: 4 additions & 2 deletions pytestomatio/connect/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,8 @@ def update_test_status(self, run_id: str,
example: dict,
file: str | None,
overwrite: bool | None,
meta: dict) -> None:
meta: dict,
links: list | None) -> None:

log.info(f'Reporting test. Id: {test_id}. Title: {title}')
request = {
Expand All @@ -302,7 +303,8 @@ def update_test_status(self, run_id: str,
"code": code,
"overwrite": overwrite,
"rid": rid,
"meta": meta
"meta": meta,
"links": links
}
filtered_request = {k: v for k, v in request.items() if v is not None}
url = f'{self.base_url}/api/reporter/{run_id}/testrun?api_key={self.api_key}'
Expand Down
3 changes: 2 additions & 1 deletion pytestomatio/connect/s3_connector.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from typing import Optional
import boto3
import logging
Expand Down Expand Up @@ -64,7 +65,7 @@ def upload_file(self, file_path: str, key: str = None, bucket_name: str = None)
log.warning('s3 session is not created, creating new one')
return
if not key:
key = file_path
key = os.path.basename(file_path)
key = f"{self.bucker_prefix}/{key}"
if not bucket_name:
bucket_name = self.bucket_name
Expand Down
63 changes: 63 additions & 0 deletions pytestomatio/functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from pytestomatio.services.artifact_storage import artifact_storage
from pytestomatio.services.meta_storage import meta_storage
from pytestomatio.services.link_storage import link_storage
from pytestomatio.utils.helper import get_current_test_id


def add_meta(data: dict):
"""
Add metadata to the reported test
:param data: metadata to add
"""
test_id = get_current_test_id()
if not test_id:
return
meta_storage.put(test_id, data)


def add_label(key: str, value: str = None):
"""
Add a single label to the reported test
:param key: label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
:param value: optional label value (e.g. 'high', 'login')
"""
test_id = get_current_test_id()
if not test_id:
return
data = f'{key}:{value}' if value else key
link_storage.put(test_id, {'label': data})


def link_jira(*args: str):
"""
Links JIRA issue IDs to the test report
:param args: JIRA issue IDs to link
"""
test_id = get_current_test_id()
if not test_id:
return
for jira_id in args:
link_storage.put(test_id, {'jira': jira_id})


def link_test(*args: str):
"""
Links test IDs to the current test in the report
:param args: test IDs to link
"""
test_id = get_current_test_id()
if not test_id:
return
for test in args:
link_storage.put(test_id, {'test': test})


def add_artifact(path: str):
"""
Add artifacts to the current test in the report
:param path: path to artifact
"""
test_id = get_current_test_id()
if not test_id:
return
artifact_storage.put(test_id, path)
35 changes: 31 additions & 4 deletions pytestomatio/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
from pytestomatio.testomatio.testomatio import Testomatio
from pytestomatio.testomatio.filter_plugin import TestomatioFilterPlugin

from pytestomatio.services.artifact_storage import artifact_storage
from pytestomatio.services.meta_storage import meta_storage
from pytestomatio.services.link_storage import link_storage

log = logging.getLogger(__name__)
log.setLevel('INFO')

Expand All @@ -45,6 +49,8 @@ def pytest_runtest_teardown(item, nextitem):
_step_managers.pop(item.nodeid)
pytest._current_item = None
clear_test_logs(item.nodeid)
meta_storage.clear(item.nodeid)
link_storage.clear(item.nodeid)


def pytest_collection(session):
Expand All @@ -53,7 +59,6 @@ def pytest_collection(session):
# We'll store the items in a session attribute for later use.
session._pytestomatio_original_collected_items = []


def pytest_configure(config: Config):
config.addinivalue_line(
"markers", "testomatio(arg): built in marker to connect test case with testomat.io by unique id"
Expand Down Expand Up @@ -188,7 +193,7 @@ def pytest_collection_modifyitems(session: Session, config: Config, items: list[
if all(s3_details):
pytest.testomatio.s3_connector = S3Connector(*s3_details)
pytest.testomatio.s3_connector.login()

case 'debug':
with open(metadata_file, 'w') as file:
data = json.dumps([i.to_dict() for i in meta], indent=4)
Expand All @@ -212,6 +217,27 @@ def pytest_runtest_makereport(item: Item, call: CallInfo):
test_id = test_item.id if not test_item.id.startswith("@T") else test_item.id[2:]
rid = f'{pytest.testomatio.test_run_config.environment}-{item.name}-{test_id}'

meta, links = None, None
if call.when == 'call':
meta = meta_storage.get(item.nodeid)
test_run_meta = pytest.testomatio.test_run_config.meta
if test_run_meta:
meta.update(test_run_meta)

stored_links = link_storage.get(item.nodeid)
links = stored_links if stored_links else None

artifacts = None
# Artifacts handling. We handle them in the teardown phase to be able to process artifacts added in
# the teardown phase of fixtures
if call.when == 'teardown' and not pytest.testomatio.test_run_config.disable_artifacts:
artifacts = test_item.artifacts
attached_artifacts = artifact_storage.get(item.nodeid)
if attached_artifacts and pytest.testomatio.s3_connector:
urls = pytest.testomatio.s3_connector.upload_files([(path, None) for path in attached_artifacts])
artifacts.extend(urls)
artifact_storage.clear(item.nodeid)

request = {
'status': None,
'title': test_item.exec_title,
Expand All @@ -224,13 +250,14 @@ def pytest_runtest_makereport(item: Item, call: CallInfo):
'message': None,
'stack': None,
'example': None,
'artifacts': test_item.artifacts,
'artifacts': artifacts,
'steps': None,
'code': None,
'timestamp': None,
'overwrite': None,
'rid': rid,
'meta': pytest.testomatio.test_run_config.meta
'meta': meta,
'links': links
}

if pytest.testomatio.test_run_config.update_code and test_item.type != 'bdd':
Expand Down
Empty file.
20 changes: 20 additions & 0 deletions pytestomatio/services/artifact_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from pytestomatio.services.base_storage import BaseStorage


class ArtifactStorage(BaseStorage):

def put(self, test_id, data: str):
existing_data = self.get(test_id)
if existing_data:
existing_data.append(data)
return
self.storage.update({test_id: [data]})

def get(self, test_id):
return self.storage.get(test_id, [])

def clear(self, test_id):
self.storage.pop(test_id, None)


artifact_storage = ArtifactStorage()
26 changes: 26 additions & 0 deletions pytestomatio/services/base_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from abc import abstractmethod, ABC


class BaseStorage(ABC):

_instances = {}

def __new__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__new__(cls)
return cls._instances.get(cls)

def __init__(self):
self.storage = {}

@abstractmethod
def get(self, identifier):
raise NotImplemented

@abstractmethod
def put(self, identifier, data):
raise NotImplemented

@abstractmethod
def clear(self, identifier):
raise NotImplemented
20 changes: 20 additions & 0 deletions pytestomatio/services/link_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from pytestomatio.services.base_storage import BaseStorage


class LinkStorage(BaseStorage):

def put(self, test_id, data: dict):
existing_data = self.get(test_id)
if existing_data:
existing_data.append(data)
return
self.storage.update({test_id: [data]})

def get(self, test_id):
return self.storage.get(test_id, [])

def clear(self, test_id):
self.storage.pop(test_id, None)


link_storage = LinkStorage()
21 changes: 21 additions & 0 deletions pytestomatio/services/meta_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from pytestomatio.services.base_storage import BaseStorage


class MetaStorage(BaseStorage):

def put(self, test_id, data: dict):
existing_data = self.get(test_id)
if existing_data:
existing_data.update(data)
data = existing_data
self.storage.update({test_id: data})

def get(self, test_id):
return self.storage.get(test_id, {})

def clear(self, test_id):
self.storage.pop(test_id, None)


meta_storage = MetaStorage()

2 changes: 2 additions & 0 deletions pytestomatio/testomatio/testRunConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ def __init__(self, kind: str = 'automated'):
exclude_skipped = os.environ.get('TESTOMATIO_EXCLUDE_SKIPPED', False) in ['True', 'true', '1']
stack_passed = os.environ.get('TESTOMATIO_STACK_PASSED', False) in ['True', 'true', '1']
shared_run_timeout = os.environ.get('TESTOMATIO_SHARED_RUN_TIMEOUT', '')
disable_artifacts_upload = os.environ.get('TESTOMATIO_DISABLE_ARTIFACTS')
self.access_event = 'publish' if os.environ.get("TESTOMATIO_PUBLISH") else None
self.disable_artifacts = disable_artifacts_upload in ['True', 'true', '1']
self.test_run_id = run_id
self.title = title
self.enable_steps_for_passed_test = enable_steps_for_passed_test
Expand Down
Loading
Loading