Skip to content

Commit

Permalink
Merge pull request #14 from qamania/master
Browse files Browse the repository at this point in the history
Fixed sync issues with parallel runs
  • Loading branch information
Ypurek authored Aug 5, 2024
2 parents 1003b9a + 77206fe commit b9250ea
Show file tree
Hide file tree
Showing 9 changed files with 93 additions and 143 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 2.6.1 (2024-08-05)

### Feat

- Fixed report final handling with pytest-xdist
- Small code refactoring

## 2.5.0 (2024-03-11)

### Feat
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ version_provider = "pep621"
update_changelog_on_bump = true
[project]
name = "pytestomatio"
version = "2.5.3"
version = "2.6.1"

dependencies = [
"requests>=2.29.0",
Expand All @@ -22,7 +22,6 @@ dependencies = [
"libcst==1.1.0",
"commitizen>=3.18.1",
"autopep8>=2.1.0",
"pytest-dotenv>=0.5.2",
]

authors = [
Expand Down
8 changes: 4 additions & 4 deletions pytestomatio/connect/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def create_test_run(self, title: str, group_title, env: str, label: str, shared_
"group_title": group_title,
"env": env,
"label": label,
"parallel": True,
"parallel": parallel,
}
filtered_request = {k: v for k, v in request.items() if v is not None}
print('create_test_run', filtered_request)
Expand All @@ -95,15 +95,15 @@ def create_test_run(self, title: str, group_title, env: str, label: str, shared_
log.info(f'Test run created {response.json()["uid"]}')
return response.json()

def update_test_run(self, id: str, title: str, group_title, env: str, label: str, shared_run: bool,
parallel) -> dict | None:
def update_test_run(self, id: str, title: str, group_title,
env: str, label: str, shared_run: bool, parallel) -> dict | None:
request = {
"api_key": self.api_key,
"title": title,
"group_title": group_title,
# "env": env, TODO: enabled when bug with 500 response fixed
# "label": label, TODO: enabled when bug with 500 response fixed
"parallel": True,
"parallel": parallel,
}
filtered_request = {k: v for k, v in request.items() if v is not None}

Expand Down
75 changes: 40 additions & 35 deletions pytestomatio/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os, pytest, logging, json

import time
from pytest import Parser, Session, Config, Item, CallInfo, hookimpl
from pytestomatio.connect.connector import Connector
from pytestomatio.decor.decorator_updater import update_tests
Expand All @@ -26,32 +26,38 @@ def pytest_addoption(parser: Parser) -> None:


def pytest_configure(config: Config):
# FYI hook runs before the xdist and later again within every worker
validations.validate_env_variables(config)
validations.validate_command_line_args(config)

config.addinivalue_line(
"markers", "testomatio(arg): built in marker to connect test case with testomat.io by unique id"
)

pytest.testomatio = Testomatio(TestRunConfig(**helper.read_env_test_run_cfg()))
option = validations.validate_option(config)
if option == 'debug':
return

is_parallel = config.getoption('numprocesses') is not None

if config.getoption(testomatio) and config.getoption(testomatio).lower() in ('sync', 'report', 'remove'):
url = config.getini('testomatio_url')
project = os.environ.get('TESTOMATIO')
pytest.testomatio = Testomatio(TestRunConfig(is_parallel))

pytest.testomatio.connector = Connector(url, project)
run_env = config.getoption('testRunEnv')
if run_env:
pytest.testomatio.test_run_config.set_env(run_env)
url = config.getini('testomatio_url')
project = os.environ.get('TESTOMATIO')

pytest.testomatio.connector = Connector(url, project)
run_env = config.getoption('testRunEnv')
if run_env:
pytest.testomatio.test_run_config.set_env(run_env)

if config.getoption(testomatio) and config.getoption(testomatio).lower() == 'report':
run: TestRunConfig = pytest.testomatio.test_run_config
if run.lock.get_run_id() is None:

# for xdist - main process
if not hasattr(config, 'workerinput'):
run_details = pytest.testomatio.connector.create_test_run(**run.to_dict())
run.lock.save_run_id(run_details.get('uid'))
run.save_run_id(run_details.get('uid'))
else:
run.test_run_id = run.lock.get_run_id()
# for xdist - worker process - do nothing
pass




def pytest_collection_modifyitems(session: Session, config: Config, items: list[Item]) -> None:
Expand All @@ -76,16 +82,15 @@ def pytest_collection_modifyitems(session: Session, config: Config, items: list[
update_tests(test_file, mapping, test_names, decorator_name, remove=True)
pytest.exit('Sync completed without test execution')
case 'report':
# assuming run already created in pytest_configure hook
# for xdist workers - get run id from the main process
run: TestRunConfig = pytest.testomatio.test_run_config
run.test_run_id = run.lock.get_run_id()
# lock file here as it runs in the worker
run.lock.lock()
run.get_run_id()

# send update without status just to get artifact details from the server
run_details = pytest.testomatio.connector.update_test_run(**run.to_dict())

if run_details is None:
log.error('Test run failed to create. Reporting skipped')
return
raise Exception('Test run failed to create. Reporting skipped')

artifact = run_details.get('artifacts')
if artifact:
Expand All @@ -102,7 +107,7 @@ def pytest_collection_modifyitems(session: Session, config: Config, items: list[
file.write(data)
pytest.exit('Debug file created. Exiting...')
case _:
pytest.exit('Unknown pytestomatio parameter. Use one of: add, remove, sync, debug')
raise Exception('Unknown pytestomatio parameter. Use one of: add, remove, sync, debug')


def pytest_runtest_makereport(item: Item, call: CallInfo):
Expand Down Expand Up @@ -176,17 +181,17 @@ def pytest_runtest_logfinish(nodeid, location):
pytest.testomatio.test_run_config.status_request = {}


@hookimpl(tryfirst=True)
def pytest_testnodedown(node, error):
run = pytest.testomatio.test_run_config
if run.test_run_id:
run.lock.unlock()
pytest.testomatio.connector.finish_test_run(run.test_run_id)

def pytest_unconfigure(config: Config):
if not hasattr(pytest, 'testomatio'):
return

@hookimpl(trylast=True)
def pytest_sessionfinish(session: Session, exitstatus: int):
run = pytest.testomatio.test_run_config
if run.test_run_id and is_xdist_controller(session):
run.lock.clear_run_id()
run: TestRunConfig = pytest.testomatio.test_run_config
# for xdist - main process
if not hasattr(config, 'workerinput'):
time.sleep(1)
pytest.testomatio.connector.finish_test_run(run.test_run_id, True)
run.clear_run_id()

# for xdist - worker process
else:
pytest.testomatio.connector.finish_test_run(run.test_run_id, False)
49 changes: 30 additions & 19 deletions pytestomatio/testomatio/testRunConfig.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
import os
import datetime as dt
import uuid
from re import sub
from pytestomatio.utils.worker_sync import SyncLock


class TestRunConfig:
def __init__(
self,
id: str = None,
title: str = None,
group_title: str = None,
environment: str = None,
label: str = None,
parallel: bool = True,
shared_run: bool = True
):
self.test_run_id = id
self.title = title if title else 'test run at ' + dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.environment = self.safe_string_list(environment)
self.label = self.safe_string_list(label)
self.group_title = group_title
def __init__(self, parallel: bool = True):
self.test_run_id = None
run = os.environ.get('TESTOMATIO_RUN')
title = os.environ.get('TESTOMATIO_TITLE')
run_or_title = run if run else title
self.title = run_or_title if run_or_title else 'test run at ' + dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.environment = self.safe_string_list(os.environ.get('TESTOMATIO_ENV'))
self.label = self.safe_string_list(os.environ.get('TESTOMATIO_LABEL'))
self.group_title = os.environ.get('TESTOMATIO_RUNGROUP_TITLE')
self.parallel = parallel
self.shared_run = shared_run
# stands for run with shards
self.shared_run = run_or_title is not None
self.status_request = {}
self.lock = SyncLock()

def to_dict(self) -> dict:
result = dict()
Expand All @@ -44,3 +37,21 @@ def safe_string_list(self, param: str):
if not param:
return None
return ",".join([sub(r"\s", "", part) for part in param.split(',')])

def save_run_id(self, run_id: str) -> None:
self.test_run_id = run_id
with open('.temp_test_run_id', 'w') as f:
f.write(run_id)

def get_run_id(self) -> str or None:
if self.test_run_id:
return self.test_run_id
if os.path.exists('.temp_test_run_id'):
with open('.temp_test_run_id', 'r') as f:
self.test_run_id = f.read()
return self.test_run_id
return None

def clear_run_id(self) -> None:
if os.path.exists('.temp_test_run_id'):
os.remove('.temp_test_run_id')
12 changes: 0 additions & 12 deletions pytestomatio/utils/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,6 @@ def add_and_enrich_tests(meta: list[TestItem], test_files: set,
update_tests(test_file, mapping, test_names, decorator_name)



def read_env_test_run_cfg() -> dict:
return {
'id': os.environ.get('TESTOMATIO_RUN'),
'title': os.environ.get('TESTOMATIO_TITLE'),
'group_title': os.environ.get('TESTOMATIO_RUNGROUP_TITLE'),
'environment': os.environ.get('TESTOMATIO_ENV'),
'shared_run': os.environ.get('TESTOMATIO_SHARED_RUN', default='false').lower() in ['true', '1'],
'label': os.environ.get('TESTOMATIO_LABEL'),
}


def read_env_s3_keys(artifact: dict) -> tuple:
return (
os.environ.get('ACCESS_KEY_ID') or artifact.get('ACCESS_KEY_ID'),
Expand Down
17 changes: 8 additions & 9 deletions pytestomatio/utils/validations.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import os
from typing import Literal
from pytest import Config


def validate_env_variables(config: Config):
if config.getoption('testomatio') and config.getoption('testomatio').lower() in ('sync', 'report', 'remove'):
def validate_option(config: Config) -> Literal['sync', 'report', 'remove', 'debug', None]:
option = config.getoption('testomatio')
option = option.lower() if option else None
if option in ('sync', 'report', 'remove'):
if os.getenv('TESTOMATIO') is None:
raise ValueError('TESTOMATIO env variable is not set')

# if os.getenv('TESTOMATIO_SHARED_RUN') and not os.getenv('TESTOMATIO_TITLE'):
# raise ValueError('TESTOMATIO_SHARED_RUN can only be used together with TESTOMATIO_TITLE')
if config.getoption('numprocesses') and option in ('sync', 'debug', 'remove'):
raise ValueError('Testomatio does not support parallel sync, remove or report. Remove --numprocesses option')


def validate_command_line_args(config: Config):
if config.getoption('numprocesses'):
if config.getoption('testomatio') and config.getoption('testomatio').lower() in ('sync', 'debug', 'remove'):
raise ValueError('Testomatio does not support parallel sync, remove or debug. Remove --numprocesses option')
return option
62 changes: 0 additions & 62 deletions pytestomatio/utils/worker_sync.py

This file was deleted.

3 changes: 3 additions & 0 deletions tests/test_class_root.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@

class TestClass:

@mark.testomatio('@T4a0527af')
def test_one_pass(self):
x = 'this'
assert 'h' in x

@mark.testomatio('@T4bc8a939')
def test_two_fail(self):
x = 'hello'
assert hasattr(x, 'check')

@mark.testomatio('@T3dd32910')
@mark.skip
def test_three_skip(self):
x = 'hello'
Expand Down

0 comments on commit b9250ea

Please sign in to comment.