Skip to content

Commit

Permalink
implemented and tested working xdist lock
Browse files Browse the repository at this point in the history
  • Loading branch information
Ypurek committed May 8, 2024
1 parent 7532092 commit 50d37da
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 53 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
## 2.5.0 (2024-03-11)

### Feat

- Plugin code refactored
- Introduced ENV variable `TESTOMATIO_CODE_STYLE` to select code style formating **PEP8** or don't format at all
- Updated pytest version supported to 8+
- Set parallel run as default parameter. ENV variable `TESTOMATIO_SHARED_RUN` is not needed
- Introduced sync lock to be used with pytest-xdist. Env variable `TESTOMATIO_TITLE` becomes optional

### Fix

- Fixed NPE when some params not set
- Fixed pytest exception with using **xdist** lib
- Fixed test parameters transfer for DDT tests


## 2.2.0 (2024-03-11)

### Feat
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ If you are not sure, don't set this variable. Default value is 'default'
```bash
TESTOMATIO_CODE_STYLE=pep8
```
Set test run name in Testomat.io
```bash
TESTOMATIO_RUN=test_run_name
```
Set test run environment in Testomat.io
```bash
TESTOMATIO_ENV=linux,chrome,1920x1080
```
Set test run labels in Testomat.io
TESTOMATIO_LABELS=smoke,regression

```bash

Expand Down
2 changes: 1 addition & 1 deletion 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.4.1"
version = "2.5.0"

dependencies = [
"requests>=2.29.0",
Expand Down
4 changes: 1 addition & 3 deletions pytestomatio/connect/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ def create_test_run(self, title: str, group_title, env: str, label: str, shared_
"env": env,
"label": label,
"parallel": True,
"shared_run": True,
}
filtered_request = {k: v for k, v in request.items() if v is not None}
print('create_test_run', filtered_request)
Expand Down Expand Up @@ -105,7 +104,6 @@ def update_test_run(self, id: str, title: str, group_title, env: str, label: str
# "env": env, TODO: enabled when bug with 500 response fixed
# "label": label, TODO: enabled when bug with 500 response fixed
"parallel": True,
"shared_run": True
}
filtered_request = {k: v for k, v in request.items() if v is not None}

Expand Down Expand Up @@ -171,7 +169,7 @@ def update_test_status(self, run_id: str,

# TODO: I guess this class should be just an API client and used within testRun (testRunConfig)
def finish_test_run(self, run_id: str, is_final=False) -> None:
status_event = "finish_parallel" if is_final else 'finish'
status_event = 'finish_parallel' if is_final else 'finish'
try:
self.session.put(f'{self.base_url}/api/reporter/{run_id}?api_key={self.api_key}',
json={"status_event": status_event})
Expand Down
56 changes: 39 additions & 17 deletions pytestomatio/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from pytestomatio.utils.parser_setup import parser_options
from pytestomatio.utils import helper
from pytestomatio.utils import validations
import multiprocessing

log = logging.getLogger(__name__)
log.setLevel('INFO')
Expand All @@ -22,15 +23,26 @@

@fixture(autouse=True)
def testomatio_skip_test_fixture(request: FixtureRequest):
if request.config.getoption(testomatio, default='').lower() in ['sync', 'remove', 'debug']:
if request.config.getoption(testomatio) and request.config.getoption(testomatio).lower() in ['sync', 'remove',
'debug']:
pytest.skip("Skipping this test because of some condition")


def pytest_addoption(parser: Parser) -> None:
parser_options(parser, testomatio)


#
# def pytest_xdist_setupnodes(config, specs):
# pass
#
#
# def pytest_xdist_node_collection_finished(node, ids):
# pass


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

Expand All @@ -39,10 +51,8 @@ def pytest_configure(config: Config):
)

pytest.testomatio = Testomatio(TestRunConfig(**helper.read_env_test_run_cfg()))
# TODO this executes before workers init!!!
pytest.testomatio.test_run_config.lock.lock()

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

Expand All @@ -51,6 +61,14 @@ def pytest_configure(config: Config):
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:
run_details = pytest.testomatio.connector.create_test_run(**run.to_dict())
run.lock.save_run_id(run_details.get('uid'))
else:
run.test_run_id = run.lock.get_run_id()


def pytest_collection_modifyitems(session: Session, config: Config, items: list[Item]) -> None:
if config.getoption(testomatio):
Expand All @@ -72,15 +90,12 @@ def pytest_collection_modifyitems(session: Session, config: Config, items: list[
for test_file in test_files:
update_tests(test_file, mapping, test_names, decorator_name, remove=True)
case 'report':
# assuming run already created in pytest_configure hook
run: TestRunConfig = pytest.testomatio.test_run_config
if run.test_run_id is None and run.lock.get_run_id() is None:
run_details = pytest.testomatio.connector.create_test_run(**run.to_dict())
uid = run_details.get('uid')
run.lock.save_run_id(uid)
uid = run.lock.get_run_id()
run.test_run_id = uid
if run.test_run_id:
run_details = pytest.testomatio.connector.update_test_run(**run.to_dict())
run.test_run_id = run.lock.get_run_id()
# lock file here as it runs in the worker
run.lock.lock()
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')
Expand Down Expand Up @@ -177,8 +192,15 @@ def pytest_runtest_logfinish(nodeid, location):
def pytest_sessionfinish(session: Session, exitstatus: int):
run = pytest.testomatio.test_run_config
if run.test_run_id:
pytest.testomatio.connector.finish_test_run(run.test_run_id)
is_last = run.lock.unlock()
if is_last:
run.lock.clear_run_id()
pytest.testomatio.connector.finish_test_run(run.test_run_id, True)
run.lock.unlock()
is_last = run.lock.clear_run_id()
pytest.testomatio.connector.finish_test_run(run.test_run_id, True)


# @pytest.fixture(scope='session')
# def final_worker_standing():
# yield
# run = pytest.testomatio.test_run_config
# if run.test_run_id:
# is_last = run.lock.clear_run_id()
# pytest.testomatio.connector.finish_test_run(run.test_run_id, is_last)
6 changes: 3 additions & 3 deletions pytestomatio/utils/validations.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ def validate_env_variables():
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 os.getenv('TESTOMATIO_SHARED_RUN') and not os.getenv('TESTOMATIO_TITLE'):
# raise ValueError('TESTOMATIO_SHARED_RUN can only be used together with TESTOMATIO_TITLE')


def validate_command_line_args(config: Config):
if config.getoption('numprocesses'):
if config.getoption('testomatio', default='').lower() in ('sync', 'debug', 'remove'):
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')
53 changes: 24 additions & 29 deletions pytestomatio/utils/worker_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,8 @@ def __init__(self):
self.worker_id = str(uuid.uuid4())
self.test_run_id: str or None = None

def lock(self) -> bool:
'''
:return: True if this worker is the first to lock the file, False otherwise
'''
time.sleep(random.randint(1, 1000) / 1000)
is_first = len([file for file in os.listdir() if file.startswith(prefix)]) == 0
def lock(self) -> None:
open(prefix + self.worker_id, 'x').close()
return is_first

def save_run_id(self, run_id: str) -> None:
try:
Expand All @@ -35,33 +29,34 @@ def save_run_id(self, run_id: str) -> None:
def get_run_id(self) -> str or None:
if self.test_run_id is not None:
return self.test_run_id

time.sleep(fixed_delay)
try:
self.test_run_id = self._read_run_file()
with open(sync_run_file, 'r') as f:
self.test_run_id = f.read()
return self.test_run_id
except FileNotFoundError:
time.sleep(fixed_delay)
try:
self.test_run_id = self._read_run_file()
return self.test_run_id
except FileNotFoundError:
return None

def _read_run_file(self) -> str:
with open(sync_run_file, 'r') as f:
return f.read()
return None

def unlock(self) -> bool:
'''
:return: True if this worker is the last to unlock the file, False otherwise
'''
def unlock(self) -> None:
os.remove(prefix + self.worker_id)
is_last = len([file for file in os.listdir() if file.startswith(prefix)]) == 0
return is_last

def clear_run_id(self) -> None:
def clear_run_id(self) -> bool:
file_count = len([file for file in os.listdir() if file.startswith(prefix)])
if file_count == 0 and not os.path.exists(sync_run_file):
return True
try:
os.remove(sync_run_file)
with open(sync_run_file, 'w'):
for _ in range(10_000):
file_count = len([file for file in os.listdir() if file.startswith(prefix)])
if file_count == 0:
break
time.sleep(fixed_delay)
except PermissionError:
return False
except FileNotFoundError:
pass
return True
try:
os.remove(sync_run_file)
except PermissionError:
return False
time.sleep(0.1)
return True
3 changes: 3 additions & 0 deletions tests/test_root.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ def test_pass_fix(dummy_fixture):
assert 3 + 3 == 6


# -------------------------------------------
@mark.testomatio('@Tefe6c6a2')
def test_fail():
assert 2 + 2 == 11


# -------------------------------------------

@mark.testomatio('@Tca8a4366')
@mark.parametrize('data', [8, 1, 2, 3, 4, 5, 'a', b'123', b'asdasd', {'hello': 'world'}, [1, 2, 3]])
def test_ddt_parametrized(data):
Expand Down

0 comments on commit 50d37da

Please sign in to comment.