Skip to content
Open
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
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 = false
[project]
name = "pytestomatio"
version = "2.10.0"
version = "2.10.1b5"

dependencies = [
"requests>=2.29.0",
Expand Down
9 changes: 7 additions & 2 deletions pytestomatio/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os, pytest, logging, json, time
import warnings

from pytest import Parser, Session, Config, Item, CallInfo
from pytestomatio.connect.connector import Connector
Expand Down Expand Up @@ -92,8 +93,12 @@ def pytest_collection_modifyitems(session: Session, config: Config, items: list[
meta, test_files, test_names = collect_tests(items)
match config.getoption(testomatio):
case 'sync':
tests = [item for item in meta if item.type != 'bdd']
if not len(tests) == len(meta):
warnings.warn('BDD tests excluded from sync. You need to sync them separately into another project '
'via check-cucumber. For details, see https://github.com/testomatio/check-cucumber')
pytest.testomatio.connector.load_tests(
meta,
tests,
no_empty=config.getoption('no_empty'),
no_detach=config.getoption('no_detach'),
structure=config.getoption('keep_structure'),
Expand Down Expand Up @@ -151,7 +156,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo):
'status': None,
'title': test_item.exec_title,
'run_time': call.duration,
'suite_title': test_item.file_name,
'suite_title': test_item.suite_title,
'suite_id': None,
'test_id': test_id,
'message': None,
Expand Down
62 changes: 47 additions & 15 deletions pytestomatio/testing/testItem.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,39 @@
import inspect

MARKER = 'testomatio'
TEST_TYPES = [
(lambda f: hasattr(f, '__scenario__'), 'bdd'),
(lambda f: True, 'regular')
]


class TestItem:
def __init__(self, item: Item):
self.uid = uuid.uuid4()
self.id: str = TestItem.get_test_id(item)
self.type = self._get_test_type(item.function)
self.id: str = self.get_test_id(item)
self.title = self._get_pytest_title(item.name)
self.sync_title = self._get_sync_test_title(item)
self.resync_title = self._get_resync_test_title(item)
self.exec_title = self._get_execution_test_title(item)
self.parameters = self._get_test_parameter_key(item)
self.file_name = item.path.name
self.suite_title = self._get_suite_title(item.function)
self.abs_path = str(item.path)
self.file_path = item.location[0]
self.module = item.module.__name__
self.source_code = inspect.getsource(item.function)
self.class_name = item.cls.__name__ if item.cls else None
self.artifacts = item.stash.get("artifact_urls", [])

def to_dict(self) -> dict:
result = dict()
result['uid'] = str(self.uid)
result['id'] = self.id
result['title'] = self.title
result['fileName'] = self.file_name
result['type'] = self.type
result['suite_title'] = self.suite_title
result['absolutePath'] = self.abs_path
result['filePath'] = self.file_path
result['module'] = self.module
Expand All @@ -40,8 +50,11 @@ def to_dict(self) -> dict:
def json(self) -> str:
return json.dumps(self.to_dict(), indent=4)

@staticmethod
def get_test_id(item: Item) -> str | None:
def get_test_id(self, item: Item) -> str | None:
if self.type == 'bdd':
for marker in item.iter_markers():
if marker.name.startswith('T'):
return '@' + marker.name
for marker in item.iter_markers(MARKER):
if marker.args:
return marker.args[0]
Expand All @@ -58,13 +71,28 @@ def _get_pytest_title(self, name: str) -> str:
return name[0:point]
return name

def _get_test_type(self, test):
"""Returns test type based on predicate check."""
for predicate, test_type in TEST_TYPES:
if predicate(test):
return test_type

def _get_suite_title(self, test):
"""Returns suite title based on test type. For bdd test suite title equals Feature name,
for regular - filename"""
if self.type == 'bdd':
scenario = test.__scenario__
if scenario and hasattr(scenario, 'feature'):
return scenario.feature.name
return self.file_name

# Testomatio resolves test id on BE by parsing test name to find test id
def _get_sync_test_title(self, item: Item) -> str:
test_name = self.pytest_title_to_testomatio_title(item.name)
test_name = self._resolve_parameter_key_in_test_name(item, test_name)
# Test id is present on already synced tests
# New test don't have testomatio test id.
test_id = TestItem.get_test_id(item)
test_id = self.id
if (test_id):
test_name = f'{test_name} {test_id}'
# ex. "User adds item to cart"
Expand Down Expand Up @@ -95,9 +123,9 @@ def _get_resync_test_title(self, name: str) -> str:
else:
return name

def _get_test_parameter_key(self, item: Item):
def _get_test_parameter_key(self, item: Item) -> list:
"""Return a list of parameter names for a given test item."""
param_names = set()
param_names = []

# 1) Look for @pytest.mark.parametrize
for mark in item.iter_markers('parametrize'):
Expand All @@ -107,21 +135,24 @@ def _get_test_parameter_key(self, item: Item):
arg_string = mark.args[0]
# If the string has commas, split it into multiple names
if ',' in arg_string:
param_names.update(name.strip() for name in arg_string.split(','))
param_names.extend([name.strip() for name in arg_string.split(',') if name not in param_names])
else:
param_names.add(arg_string.strip())
param_names.append(arg_string.strip())

# 2) Look for fixture parameterization (including dynamically generated)
# via callspec, which holds *all* final parameters for an item.
callspec = getattr(item, 'callspec', None)
if callspec:
# callspec.params is a dict: fixture_name -> parameter_value
# We only want fixture names, not the values.
param_names.update(callspec.params.keys())

# Return them as a list, or keep it as a set—whatever you prefer.
return list(param_names)

callspec_params = callspec.params
if self.type == 'bdd':
bdd_fixture_wrapper_name = '_pytest_bdd_example'
if bdd_fixture_wrapper_name in param_names:
param_names.remove(bdd_fixture_wrapper_name)
callspec_params = callspec.params.get(bdd_fixture_wrapper_name, {})
param_names.extend([name for name in callspec_params.keys() if name not in param_names])
return param_names

def _resolve_parameter_key_in_test_name(self, item: Item, test_name: str) -> str:
test_params = self._get_test_parameter_key(item)
Expand All @@ -148,7 +179,8 @@ def _resolve_parameter_value_in_test_name(self, item: Item, test_name: str) -> s

def repl(match):
key = match.group(1)
value = item.callspec.params.get(key, '')
value = item.callspec.params.get(key, '') if not self.type == 'bdd' else \
item.callspec.params.get('_pytest_bdd_example', {}).get(key, '')

string_value = self._to_string_value(value)
# TODO: handle "value with space" on testomatio BE https://github.com/testomatio/check-tests/issues/147
Expand Down
34 changes: 34 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,40 @@ def test_sync_mode(self, mock_exit, mock_session, mock_config, multiple_test_ite
assert mock_add_enrich.call_count == 1
mock_exit.assert_called_once_with('Sync completed without test execution')

@patch('pytestomatio.main.pytest.exit')
def test_bdd_tests_excluded_from_sync(self, mock_exit, mock_session, mock_config, multiple_test_items):
"""Test sync mode"""
mock_config.getoption.side_effect = lambda x: {
'testomatio': 'sync',
'no_empty': False,
'no_detach': False,
'keep_structure': False,
'create': False,
'directory': None
}.get(x)
items = multiple_test_items.copy()

scenario_mock = Mock()
feature_mock = Mock()
feature_mock.name = 'mock feature'
scenario_mock.feature = feature_mock
items[0].function.__scenario__ = scenario_mock

pytest.testomatio = Mock()
pytest.testomatio.connector = Mock()
pytest.testomatio.connector.get_tests.return_value = []

with patch('pytestomatio.main.add_and_enrich_tests') as mock_add_enrich:
main.pytest_collection_modifyitems(mock_session, mock_config, items)

assert pytest.testomatio.connector.load_tests.call_count == 1
passed_meta = pytest.testomatio.connector.load_tests.call_args[0][0]
assert len(passed_meta) != len(items)
assert passed_meta[0].title == items[1].name

assert mock_add_enrich.call_count == 1
mock_exit.assert_called_once_with('Sync completed without test execution')

@patch('pytestomatio.main.update_tests')
@patch('pytestomatio.main.pytest.exit')
def test_remove_mode(self, mock_exit, mock_update_tests, mock_session, mock_config, single_test_item,
Expand Down
Loading
Loading