testit-adapter-pytest 4.2.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- testit_adapter_pytest/__init__.py +0 -0
- testit_adapter_pytest/fixture_context.py +33 -0
- testit_adapter_pytest/listener.py +388 -0
- testit_adapter_pytest/models/__init__.py +0 -0
- testit_adapter_pytest/models/executable_test.py +33 -0
- testit_adapter_pytest/plugin.py +223 -0
- testit_adapter_pytest/utils.py +440 -0
- testit_adapter_pytest-4.2.8.dist-info/METADATA +309 -0
- testit_adapter_pytest-4.2.8.dist-info/RECORD +12 -0
- testit_adapter_pytest-4.2.8.dist-info/WHEEL +5 -0
- testit_adapter_pytest-4.2.8.dist-info/entry_points.txt +2 -0
- testit_adapter_pytest-4.2.8.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from uuid import uuid4
|
|
2
|
+
|
|
3
|
+
from testit_python_commons.services import Utils, TmsPluginManager
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FixtureContext:
|
|
7
|
+
def __init__(self, fixture_function, parent_uuid=None, name=None):
|
|
8
|
+
self._fixture_function = fixture_function
|
|
9
|
+
self._parent_uuid = parent_uuid
|
|
10
|
+
self.__title = name if name else fixture_function.__name__
|
|
11
|
+
self.__description = fixture_function.__doc__
|
|
12
|
+
self._uuid = uuid4()
|
|
13
|
+
self.parameters = None
|
|
14
|
+
|
|
15
|
+
def __call__(self, *args, **kwargs):
|
|
16
|
+
self.parameters = Utils.get_function_parameters(self._fixture_function, *args, **kwargs)
|
|
17
|
+
|
|
18
|
+
with self:
|
|
19
|
+
return self._fixture_function(*args, **kwargs)
|
|
20
|
+
|
|
21
|
+
def __enter__(self):
|
|
22
|
+
TmsPluginManager.get_plugin_manager().hook.start_fixture(
|
|
23
|
+
parent_uuid=self._parent_uuid,
|
|
24
|
+
uuid=self._uuid,
|
|
25
|
+
title=self.__title,
|
|
26
|
+
parameters=self.parameters)
|
|
27
|
+
|
|
28
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
29
|
+
TmsPluginManager.get_plugin_manager().hook.stop_fixture(
|
|
30
|
+
uuid=self._uuid,
|
|
31
|
+
exc_type=exc_type,
|
|
32
|
+
exc_val=exc_val,
|
|
33
|
+
exc_tb=exc_tb)
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import pickle
|
|
2
|
+
from importlib.metadata import metadata
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from packaging import version
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
import testit_python_commons.services as adapter
|
|
10
|
+
from testit_python_commons.models.outcome_type import OutcomeType
|
|
11
|
+
from testit_python_commons.models.status_type import StatusType
|
|
12
|
+
from testit_python_commons.models.fixture import FixtureResult, FixturesContainer
|
|
13
|
+
from testit_python_commons.services import AdapterManager, StepManager, FixtureManager
|
|
14
|
+
from testit_python_commons.services.logger import adapter_logger
|
|
15
|
+
|
|
16
|
+
import testit_adapter_pytest.utils as utils
|
|
17
|
+
from testit_adapter_pytest.fixture_context import FixtureContext
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
STATUS_TYPE = {
|
|
21
|
+
'passed': StatusType.SUCCEEDED,
|
|
22
|
+
'failed': StatusType.FAILED,
|
|
23
|
+
'xfailed': StatusType.FAILED,
|
|
24
|
+
'skipped': StatusType.INCOMPLETE,
|
|
25
|
+
}
|
|
26
|
+
STEP_STATUS = {
|
|
27
|
+
'passed': OutcomeType.PASSED,
|
|
28
|
+
'failed': OutcomeType.FAILED,
|
|
29
|
+
'skipped': OutcomeType.SKIPPED,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ItemCache:
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self._items = dict()
|
|
36
|
+
|
|
37
|
+
def get(self, _id):
|
|
38
|
+
return self._items.get(id(_id))
|
|
39
|
+
|
|
40
|
+
def push(self, _id):
|
|
41
|
+
return self._items.setdefault(id(_id), uuid4())
|
|
42
|
+
|
|
43
|
+
def pop(self, _id):
|
|
44
|
+
return self._items.pop(id(_id), None)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SeparationOfTests:
|
|
48
|
+
def __init__(self):
|
|
49
|
+
self._selected_items = []
|
|
50
|
+
self._deselected_items = []
|
|
51
|
+
|
|
52
|
+
def add_item_to_selected_items(self, item):
|
|
53
|
+
self._selected_items.append(item)
|
|
54
|
+
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
def add_item_to_deselected_items(self, item):
|
|
58
|
+
self._deselected_items.append(item)
|
|
59
|
+
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def get_selected_items(self) -> list:
|
|
63
|
+
return self._selected_items
|
|
64
|
+
|
|
65
|
+
def get_deselected_items(self) -> list:
|
|
66
|
+
return self._deselected_items
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TmsListener(object):
|
|
70
|
+
__executable_test = None
|
|
71
|
+
__pytest_info = None
|
|
72
|
+
__pytest_check_info = None
|
|
73
|
+
__failures = None
|
|
74
|
+
|
|
75
|
+
def __init__(self, adapter_manager: AdapterManager, step_manager: StepManager, fixture_manager: FixtureManager):
|
|
76
|
+
self.__adapter_manager = adapter_manager
|
|
77
|
+
self.__step_manager = step_manager
|
|
78
|
+
self.__fixture_manager = fixture_manager
|
|
79
|
+
self._cache = ItemCache()
|
|
80
|
+
|
|
81
|
+
@pytest.hookimpl
|
|
82
|
+
def pytest_configure(self, config):
|
|
83
|
+
self.__pytest_info = metadata("pytest")
|
|
84
|
+
|
|
85
|
+
if hasattr(config, "workerinput"):
|
|
86
|
+
config.test_run_id = pickle.loads(config.workerinput["test_run_id"])
|
|
87
|
+
elif not hasattr(config, "test_run_id"):
|
|
88
|
+
config.test_run_id = self.__adapter_manager.get_test_run_id()
|
|
89
|
+
|
|
90
|
+
self.__adapter_manager.set_test_run_id(config.test_run_id)
|
|
91
|
+
|
|
92
|
+
@pytest.hookimpl
|
|
93
|
+
def pytest_configure_node(self, node):
|
|
94
|
+
if hasattr(node.config, "test_run_id"):
|
|
95
|
+
node.workerinput["test_run_id"] = pickle.dumps(node.config.test_run_id)
|
|
96
|
+
|
|
97
|
+
@adapter.hookimpl
|
|
98
|
+
def get_pytest_check_outcome(self):
|
|
99
|
+
if self.__pytest_check_info:
|
|
100
|
+
if version.parse(self.__pytest_check_info.version) < version.parse("1.1.0"):
|
|
101
|
+
from pytest_check import check_methods as logs
|
|
102
|
+
else:
|
|
103
|
+
from pytest_check import check_log as logs
|
|
104
|
+
|
|
105
|
+
failures = logs.get_failures()
|
|
106
|
+
|
|
107
|
+
if failures and self.__failures != failures:
|
|
108
|
+
self.__failures = failures[:]
|
|
109
|
+
return 'Failed'
|
|
110
|
+
return 'Passed'
|
|
111
|
+
|
|
112
|
+
@pytest.hookimpl
|
|
113
|
+
def pytest_collection_modifyitems(self, config, items):
|
|
114
|
+
self.__add_pytest_check_info(config.pluginmanager.list_plugin_distinfo())
|
|
115
|
+
|
|
116
|
+
resolved_autotests = self.__adapter_manager.get_autotests_for_launch()
|
|
117
|
+
separation_of_tests = self.__get_separation_of_tests(items, resolved_autotests)
|
|
118
|
+
|
|
119
|
+
if resolved_autotests:
|
|
120
|
+
if not separation_of_tests.get_selected_items():
|
|
121
|
+
raise Exception('The specified tests were not found!')
|
|
122
|
+
|
|
123
|
+
config.hook.pytest_deselected(items=separation_of_tests.get_deselected_items())
|
|
124
|
+
items[:] = separation_of_tests.get_selected_items()
|
|
125
|
+
|
|
126
|
+
def __add_pytest_check_info(self, plugin_info):
|
|
127
|
+
for plugin, dist in plugin_info:
|
|
128
|
+
if 'pytest-check' == dist.project_name:
|
|
129
|
+
self.__pytest_check_info = dist
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
@adapter_logger
|
|
134
|
+
def __get_separation_of_tests(cls, items, resolved_autotests) -> SeparationOfTests:
|
|
135
|
+
separation_of_tests = SeparationOfTests()
|
|
136
|
+
index = 0
|
|
137
|
+
|
|
138
|
+
for item in items:
|
|
139
|
+
if hasattr(item.function, 'test_external_id'):
|
|
140
|
+
item.test_external_id = item.function.test_external_id
|
|
141
|
+
else:
|
|
142
|
+
item.test_external_id = utils.get_hash(item.parent.nodeid + item.function.__name__)
|
|
143
|
+
|
|
144
|
+
if item.own_markers:
|
|
145
|
+
for mark in item.own_markers:
|
|
146
|
+
if mark.name == 'parametrize':
|
|
147
|
+
if not hasattr(item, 'array_parametrize_mark_id'):
|
|
148
|
+
item.array_parametrize_mark_id = []
|
|
149
|
+
item.array_parametrize_mark_id.append(
|
|
150
|
+
item.own_markers.index(mark))
|
|
151
|
+
|
|
152
|
+
params = utils.get_all_parameters(item)
|
|
153
|
+
item.test_external_id = utils.collect_parameters_in_string_attribute(
|
|
154
|
+
item.test_external_id,
|
|
155
|
+
params)
|
|
156
|
+
|
|
157
|
+
item.index = index
|
|
158
|
+
item_id = items.index(item)
|
|
159
|
+
index = index + 1 if len(items) > item_id + 1 and items[item_id + 1].originalname == item.originalname \
|
|
160
|
+
else 0
|
|
161
|
+
|
|
162
|
+
if cls.__check_external_id_in_resolved_autotests(item.test_external_id, resolved_autotests):
|
|
163
|
+
separation_of_tests.add_item_to_selected_items(item)
|
|
164
|
+
else:
|
|
165
|
+
separation_of_tests.add_item_to_deselected_items(item)
|
|
166
|
+
|
|
167
|
+
return separation_of_tests
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
@adapter_logger
|
|
171
|
+
def __check_external_id_in_resolved_autotests(external_id: str, resolved_autotests: dict) -> bool:
|
|
172
|
+
return resolved_autotests is not None and external_id in resolved_autotests
|
|
173
|
+
|
|
174
|
+
@pytest.hookimpl(tryfirst=True)
|
|
175
|
+
def pytest_runtest_protocol(self, item):
|
|
176
|
+
self.__executable_test = utils.form_test(item)
|
|
177
|
+
self.__adapter_manager.on_running_started()
|
|
178
|
+
|
|
179
|
+
@pytest.hookimpl(hookwrapper=True)
|
|
180
|
+
def pytest_runtest_setup(self, item):
|
|
181
|
+
if not self.__executable_test:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
yield
|
|
185
|
+
|
|
186
|
+
self._update_fixtures_external_ids(item)
|
|
187
|
+
|
|
188
|
+
@pytest.hookimpl(hookwrapper=True)
|
|
189
|
+
def pytest_fixture_setup(self, fixturedef):
|
|
190
|
+
fixture_name = getattr(fixturedef.func, 'test_displayname', fixturedef.argname)
|
|
191
|
+
|
|
192
|
+
container_uuid = self._cache.get(fixturedef)
|
|
193
|
+
|
|
194
|
+
if not container_uuid:
|
|
195
|
+
container_uuid = self._cache.push(fixturedef)
|
|
196
|
+
container = FixturesContainer(uuid=container_uuid)
|
|
197
|
+
self.__fixture_manager.start_group(container_uuid, container)
|
|
198
|
+
|
|
199
|
+
self.__fixture_manager.update_group(container_uuid)
|
|
200
|
+
|
|
201
|
+
before_fixture_uuid = uuid4()
|
|
202
|
+
before_fixture = FixtureResult(title=fixture_name)
|
|
203
|
+
|
|
204
|
+
self.__fixture_manager.start_before_fixture(container_uuid, before_fixture_uuid, before_fixture)
|
|
205
|
+
|
|
206
|
+
outcome = yield
|
|
207
|
+
|
|
208
|
+
results_steps_data = self.__step_manager.get_steps_tree()
|
|
209
|
+
|
|
210
|
+
self.__fixture_manager.stop_before_fixture(before_fixture_uuid,
|
|
211
|
+
outcome=utils.get_outcome_status(outcome), steps=results_steps_data)
|
|
212
|
+
|
|
213
|
+
finalizers = getattr(fixturedef, '_finalizers', [])
|
|
214
|
+
for index, finalizer in enumerate(finalizers):
|
|
215
|
+
finalizer_name = getattr(finalizer, "__name__", index)
|
|
216
|
+
name = f'{fixture_name}::{finalizer_name}'
|
|
217
|
+
finalizers[index] = FixtureContext(finalizer, parent_uuid=container_uuid, name=name)
|
|
218
|
+
|
|
219
|
+
@pytest.hookimpl(hookwrapper=True, trylast=True)
|
|
220
|
+
def pytest_runtest_call(self):
|
|
221
|
+
yield
|
|
222
|
+
|
|
223
|
+
if not self.__executable_test:
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
test_results_steps = self.__step_manager.get_steps_tree()
|
|
227
|
+
self.__executable_test.step_results = test_results_steps
|
|
228
|
+
|
|
229
|
+
@pytest.hookimpl(hookwrapper=True)
|
|
230
|
+
def pytest_fixture_post_finalizer(self, fixturedef):
|
|
231
|
+
if not self.__executable_test:
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
yield
|
|
235
|
+
|
|
236
|
+
if hasattr(fixturedef, 'cached_result') and self._cache.get(fixturedef):
|
|
237
|
+
group_uuid = self._cache.pop(fixturedef)
|
|
238
|
+
self.__fixture_manager.stop_group(group_uuid)
|
|
239
|
+
|
|
240
|
+
@pytest.hookimpl
|
|
241
|
+
def pytest_runtest_logreport(self, report):
|
|
242
|
+
if self.__executable_test:
|
|
243
|
+
if report.when == 'setup':
|
|
244
|
+
self.__executable_test.outcome = report.outcome
|
|
245
|
+
self.__executable_test.status_type = STATUS_TYPE.get(report.outcome, None)
|
|
246
|
+
if report.longreprtext:
|
|
247
|
+
self.__executable_test.message = report.longreprtext
|
|
248
|
+
|
|
249
|
+
if report.when == 'call':
|
|
250
|
+
self.__executable_test.outcome = report.outcome
|
|
251
|
+
self.__executable_test.status_type = STATUS_TYPE.get(report.outcome, None)
|
|
252
|
+
|
|
253
|
+
if report.failed or report.outcome == 'rerun':
|
|
254
|
+
self.__executable_test.outcome = 'failed'
|
|
255
|
+
self.__executable_test.status_type = STATUS_TYPE.get('failed', None)
|
|
256
|
+
|
|
257
|
+
if report.longreprtext:
|
|
258
|
+
self.__executable_test.traces = report.longreprtext
|
|
259
|
+
|
|
260
|
+
if hasattr(report, 'wasxfail') and not report.passed:
|
|
261
|
+
self.__executable_test.outcome = 'xfailed'
|
|
262
|
+
self.__executable_test.status_type = STATUS_TYPE.get('xfailed', None)
|
|
263
|
+
|
|
264
|
+
if report.longreprtext:
|
|
265
|
+
self.__executable_test.traces = report.longreprtext
|
|
266
|
+
|
|
267
|
+
self.__executable_test.duration += report.duration * 1000
|
|
268
|
+
|
|
269
|
+
@pytest.hookimpl
|
|
270
|
+
def pytest_runtest_logfinish(self):
|
|
271
|
+
if not self.__executable_test:
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
self.__adapter_manager.write_test(
|
|
275
|
+
utils.convert_executable_test_to_test_result_model(self.__executable_test))
|
|
276
|
+
|
|
277
|
+
@pytest.hookimpl
|
|
278
|
+
def pytest_sessionfinish(self, session):
|
|
279
|
+
self.__adapter_manager.on_block_completed()
|
|
280
|
+
self.__adapter_manager.write_tests()
|
|
281
|
+
|
|
282
|
+
@adapter.hookimpl
|
|
283
|
+
def add_link(self, link):
|
|
284
|
+
if self.__executable_test:
|
|
285
|
+
self.__executable_test.result_links.append(link)
|
|
286
|
+
|
|
287
|
+
@adapter.hookimpl
|
|
288
|
+
def add_message(self, test_message: str):
|
|
289
|
+
if self.__executable_test:
|
|
290
|
+
self.__executable_test.message = str(test_message)
|
|
291
|
+
|
|
292
|
+
@adapter.hookimpl
|
|
293
|
+
def add_attachments(self, attach_paths: list or tuple):
|
|
294
|
+
if self.__executable_test:
|
|
295
|
+
self.__executable_test.attachments += self.__adapter_manager.load_attachments(attach_paths)
|
|
296
|
+
|
|
297
|
+
@adapter.hookimpl
|
|
298
|
+
def create_attachment(self, body, name: str):
|
|
299
|
+
if self.__executable_test:
|
|
300
|
+
self.__executable_test.attachments += self.__adapter_manager.create_attachment(body, name)
|
|
301
|
+
|
|
302
|
+
@adapter.hookimpl
|
|
303
|
+
def add_work_item_id(self, test_work_item_id: str):
|
|
304
|
+
if self.__executable_test:
|
|
305
|
+
self.__executable_test.work_item_ids.append(test_work_item_id)
|
|
306
|
+
|
|
307
|
+
@adapter.hookimpl
|
|
308
|
+
def add_display_name(self, test_display_name: str):
|
|
309
|
+
if self.__executable_test:
|
|
310
|
+
self.__executable_test.name = test_display_name
|
|
311
|
+
|
|
312
|
+
@adapter.hookimpl
|
|
313
|
+
def add_namespace(self, test_namespace: str):
|
|
314
|
+
if self.__executable_test:
|
|
315
|
+
self.__executable_test.namespace = test_namespace
|
|
316
|
+
|
|
317
|
+
@adapter.hookimpl
|
|
318
|
+
def add_classname(self, test_classname: str):
|
|
319
|
+
if self.__executable_test:
|
|
320
|
+
self.__executable_test.classname = test_classname
|
|
321
|
+
|
|
322
|
+
@adapter.hookimpl
|
|
323
|
+
def add_external_id(self, test_external_id: str):
|
|
324
|
+
if self.__executable_test:
|
|
325
|
+
self.__executable_test.external_id = test_external_id
|
|
326
|
+
|
|
327
|
+
@adapter.hookimpl
|
|
328
|
+
def add_title(self, test_title: str):
|
|
329
|
+
if self.__executable_test:
|
|
330
|
+
self.__executable_test.title = str(test_title)
|
|
331
|
+
|
|
332
|
+
@adapter.hookimpl
|
|
333
|
+
def add_description(self, test_description: str):
|
|
334
|
+
if self.__executable_test:
|
|
335
|
+
self.__executable_test.description = test_description
|
|
336
|
+
|
|
337
|
+
@adapter.hookimpl
|
|
338
|
+
def add_label(self, test_label: str):
|
|
339
|
+
if self.__executable_test:
|
|
340
|
+
self.__executable_test.labels.append({'name': test_label})
|
|
341
|
+
|
|
342
|
+
@adapter.hookimpl
|
|
343
|
+
def add_parameter(self, name: str, value: str):
|
|
344
|
+
if self.__executable_test:
|
|
345
|
+
self.__executable_test.parameters[name] = value
|
|
346
|
+
|
|
347
|
+
@adapter.hookimpl
|
|
348
|
+
def start_fixture(self, parent_uuid, uuid, title):
|
|
349
|
+
after_fixture = FixtureResult(title=title)
|
|
350
|
+
self.__fixture_manager.start_after_fixture(parent_uuid, uuid, after_fixture)
|
|
351
|
+
|
|
352
|
+
@adapter.hookimpl
|
|
353
|
+
def stop_fixture(self, uuid, exc_type, exc_val, exc_tb):
|
|
354
|
+
results_steps_data = self.__step_manager.get_steps_tree()
|
|
355
|
+
|
|
356
|
+
self.__fixture_manager.stop_after_fixture(uuid,
|
|
357
|
+
outcome=utils.get_status(exc_val),
|
|
358
|
+
steps=results_steps_data,
|
|
359
|
+
message=utils.get_message(exc_type, exc_val),
|
|
360
|
+
stacktrace=utils.get_traceback(exc_tb))
|
|
361
|
+
|
|
362
|
+
def _update_fixtures_external_ids(self, item):
|
|
363
|
+
for fixturedef in self._test_fixtures(item):
|
|
364
|
+
group_uuid = self._cache.get(fixturedef)
|
|
365
|
+
if group_uuid:
|
|
366
|
+
group = self.__fixture_manager.get_item(group_uuid)
|
|
367
|
+
else:
|
|
368
|
+
group_uuid = self._cache.push(fixturedef)
|
|
369
|
+
group = FixturesContainer(uuid=group_uuid)
|
|
370
|
+
self.__fixture_manager.start_group(group_uuid, group)
|
|
371
|
+
if self.__executable_test.external_id not in group.external_ids:
|
|
372
|
+
self.__fixture_manager.update_group(group_uuid, external_ids=self.__executable_test.external_id)
|
|
373
|
+
|
|
374
|
+
def _test_fixtures(self, item):
|
|
375
|
+
fixturemanager = item.session._fixturemanager
|
|
376
|
+
fixturedefs = []
|
|
377
|
+
|
|
378
|
+
if hasattr(item, "_request") and hasattr(item._request, "fixturenames"):
|
|
379
|
+
for name in item._request.fixturenames:
|
|
380
|
+
if self.__pytest_info and version.parse(self.__pytest_info["version"]) >= version.parse("8.1.0"):
|
|
381
|
+
fixturedefs_pytest = fixturemanager.getfixturedefs(name, item)
|
|
382
|
+
else:
|
|
383
|
+
fixturedefs_pytest = fixturemanager.getfixturedefs(name, item.nodeid)
|
|
384
|
+
|
|
385
|
+
if fixturedefs_pytest:
|
|
386
|
+
fixturedefs.extend(fixturedefs_pytest)
|
|
387
|
+
|
|
388
|
+
return fixturedefs
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from attr import attrs, attrib
|
|
2
|
+
from attr import Factory
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@attrs
|
|
6
|
+
class ExecutableTest:
|
|
7
|
+
external_id = attrib()
|
|
8
|
+
name = attrib()
|
|
9
|
+
steps = attrib(default=Factory(list))
|
|
10
|
+
step_results = attrib(default=Factory(list))
|
|
11
|
+
setup_steps = attrib(default=Factory(list))
|
|
12
|
+
setup_step_results = attrib(default=Factory(list))
|
|
13
|
+
teardown_steps = attrib(default=Factory(list))
|
|
14
|
+
teardown_step_results = attrib(default=Factory(list))
|
|
15
|
+
result_links = attrib(default=Factory(list))
|
|
16
|
+
duration = attrib(default=None)
|
|
17
|
+
outcome = attrib(default=None)
|
|
18
|
+
status_type = attrib(default=None)
|
|
19
|
+
failure_reason_names = attrib(default=Factory(list))
|
|
20
|
+
traces = attrib(default=None)
|
|
21
|
+
attachments = attrib(default=Factory(list))
|
|
22
|
+
parameters = attrib(default=Factory(dict))
|
|
23
|
+
properties = attrib(default=Factory(dict))
|
|
24
|
+
namespace = attrib(default=None)
|
|
25
|
+
classname = attrib(default=None)
|
|
26
|
+
title = attrib(default=None)
|
|
27
|
+
description = attrib(default=None)
|
|
28
|
+
links = attrib(default=Factory(list))
|
|
29
|
+
labels = attrib(default=Factory(list))
|
|
30
|
+
tags = attrib(default=Factory(list))
|
|
31
|
+
work_item_ids = attrib(default=Factory(list))
|
|
32
|
+
message = attrib(default=None)
|
|
33
|
+
node_id = attrib(default=None)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import json
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from testit_adapter_pytest.listener import TmsListener
|
|
7
|
+
from testit_python_commons.services import TmsPluginManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _adapter_mode_type(value):
|
|
11
|
+
if value is None:
|
|
12
|
+
raise ValueError("Adapter mode cannot be None! Valid modes: 0, 1, 2")
|
|
13
|
+
valid_modes = ['0', '1', '2']
|
|
14
|
+
if value not in valid_modes:
|
|
15
|
+
raise ValueError(f"Unknown adapter mode '{value}'! Valid modes: {', '.join(valid_modes)}")
|
|
16
|
+
return value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _boolean_type(value):
|
|
20
|
+
if value is None:
|
|
21
|
+
raise ValueError("Boolean value cannot be None! Must be 'true' or 'false'")
|
|
22
|
+
valid_values = ['true', 'false']
|
|
23
|
+
if value.lower() not in valid_values:
|
|
24
|
+
raise ValueError(f"Invalid value '{value}'! Must be 'true' or 'false'")
|
|
25
|
+
return value.lower()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _uuid_type(value):
|
|
29
|
+
if value is None:
|
|
30
|
+
raise ValueError("UUID cannot be None!")
|
|
31
|
+
uuid_pattern = re.compile(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', re.I)
|
|
32
|
+
if not uuid_pattern.match(value):
|
|
33
|
+
raise ValueError(f"Invalid UUID format: '{value}'!")
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _url_type(value):
|
|
38
|
+
if value is None:
|
|
39
|
+
raise ValueError("URL cannot be None!")
|
|
40
|
+
if not value.startswith(('http://', 'https://')):
|
|
41
|
+
raise ValueError(f"Invalid URL format: '{value}'!")
|
|
42
|
+
url = urlparse(value)
|
|
43
|
+
if not all([url.scheme, url.netloc]):
|
|
44
|
+
raise ValueError(f"Invalid URL format: '{value}'!")
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _proxy_type(value):
|
|
49
|
+
if value is None:
|
|
50
|
+
raise ValueError("Proxy cannot be None!")
|
|
51
|
+
try:
|
|
52
|
+
proxy_dict = json.loads(value)
|
|
53
|
+
if not isinstance(proxy_dict, dict):
|
|
54
|
+
raise ValueError(f"Proxy must be a JSON object, got {type(proxy_dict).__name__}")
|
|
55
|
+
|
|
56
|
+
valid_keys = {'http', 'https'}
|
|
57
|
+
for key in proxy_dict.keys():
|
|
58
|
+
if key not in valid_keys:
|
|
59
|
+
raise ValueError(f"Invalid proxy key '{key}'! Must be 'http' or 'https'")
|
|
60
|
+
|
|
61
|
+
for key, url in proxy_dict.items():
|
|
62
|
+
if not isinstance(url, str):
|
|
63
|
+
raise ValueError(f"Proxy URL for '{key}' must be string, got {type(url).__name__}")
|
|
64
|
+
if not url.startswith(('http://', 'https://')):
|
|
65
|
+
raise ValueError(f"Invalid {key} proxy URL: '{url}'! Must start with http:// or https://")
|
|
66
|
+
parsed = urlparse(url)
|
|
67
|
+
if not parsed.netloc:
|
|
68
|
+
raise ValueError(f"Invalid {key} proxy URL: '{url}'! Missing hostname")
|
|
69
|
+
|
|
70
|
+
return value
|
|
71
|
+
except json.JSONDecodeError:
|
|
72
|
+
raise ValueError(f"Invalid JSON format for proxy: '{value}'!")
|
|
73
|
+
except ValueError as e:
|
|
74
|
+
raise ValueError(str(e))
|
|
75
|
+
|
|
76
|
+
def pytest_addoption(parser):
|
|
77
|
+
parser.getgroup('testit').addoption(
|
|
78
|
+
'--testit',
|
|
79
|
+
action='store_true',
|
|
80
|
+
dest="tms_report",
|
|
81
|
+
help='Pytest plugin for Test IT'
|
|
82
|
+
)
|
|
83
|
+
parser.getgroup('testit').addoption(
|
|
84
|
+
'--tmsUrl',
|
|
85
|
+
action="store",
|
|
86
|
+
dest="set_url",
|
|
87
|
+
type=_url_type,
|
|
88
|
+
metavar="https://demo.testit.software",
|
|
89
|
+
help='Set location of the TMS instance'
|
|
90
|
+
)
|
|
91
|
+
parser.getgroup('testit').addoption(
|
|
92
|
+
'--tmsPrivateToken',
|
|
93
|
+
action="store",
|
|
94
|
+
dest="set_private_token",
|
|
95
|
+
metavar="T2lKd2pLZGI4WHRhaVZUejNl",
|
|
96
|
+
help='Set API secret key'
|
|
97
|
+
)
|
|
98
|
+
parser.getgroup('testit').addoption(
|
|
99
|
+
'--tmsProjectId',
|
|
100
|
+
action="store",
|
|
101
|
+
dest="set_project_id",
|
|
102
|
+
type=_uuid_type,
|
|
103
|
+
metavar="15dbb164-c1aa-4cbf-830c-8c01ae14f4fb",
|
|
104
|
+
help='Set project ID'
|
|
105
|
+
)
|
|
106
|
+
parser.getgroup('testit').addoption(
|
|
107
|
+
'--tmsConfigurationId',
|
|
108
|
+
action="store",
|
|
109
|
+
dest="set_configuration_id",
|
|
110
|
+
type=_uuid_type,
|
|
111
|
+
metavar="d354bdac-75dc-4e3d-84d4-71186c0dddfc",
|
|
112
|
+
help='Set configuration ID'
|
|
113
|
+
)
|
|
114
|
+
parser.getgroup('testit').addoption(
|
|
115
|
+
'--tmsTestRunId',
|
|
116
|
+
action="store",
|
|
117
|
+
dest="set_test_run_id",
|
|
118
|
+
type=_uuid_type,
|
|
119
|
+
metavar="5236eb3f-7c05-46f9-a609-dc0278896464",
|
|
120
|
+
help='Set test run ID (optional)'
|
|
121
|
+
)
|
|
122
|
+
parser.getgroup('debug').addoption(
|
|
123
|
+
'--tmsProxy',
|
|
124
|
+
action="store",
|
|
125
|
+
dest="set_tms_proxy",
|
|
126
|
+
type=_proxy_type,
|
|
127
|
+
metavar='{"http":"http://localhost:8888","https":"http://localhost:8888"}',
|
|
128
|
+
help='Set proxy for sending requests (optional)'
|
|
129
|
+
)
|
|
130
|
+
parser.getgroup('testit').addoption(
|
|
131
|
+
'--tmsTestRunName',
|
|
132
|
+
action="store",
|
|
133
|
+
dest="set_test_run_name",
|
|
134
|
+
metavar="Custom name of test run",
|
|
135
|
+
help='Set custom name of test run (optional)'
|
|
136
|
+
)
|
|
137
|
+
parser.getgroup('testit').addoption(
|
|
138
|
+
'--tmsAdapterMode',
|
|
139
|
+
action="store",
|
|
140
|
+
type=_adapter_mode_type,
|
|
141
|
+
dest="set_adapter_mode",
|
|
142
|
+
metavar="1",
|
|
143
|
+
help="""
|
|
144
|
+
Set adapter mode with test run (optional):
|
|
145
|
+
0 - with filtering autotests by launch\'s suite in TMS (Default)
|
|
146
|
+
1 - without filtering autotests by launch\'s suite in TMS
|
|
147
|
+
2 - create new test run in TMS
|
|
148
|
+
"""
|
|
149
|
+
)
|
|
150
|
+
parser.getgroup('testit').addoption(
|
|
151
|
+
'--tmsConfigFile',
|
|
152
|
+
action="store",
|
|
153
|
+
dest="set_config_file",
|
|
154
|
+
metavar="tmsConfigFile",
|
|
155
|
+
help='Set custom name of configuration file'
|
|
156
|
+
)
|
|
157
|
+
parser.getgroup('testit').addoption(
|
|
158
|
+
'--tmsCertValidation',
|
|
159
|
+
action="store",
|
|
160
|
+
dest="set_cert_validation",
|
|
161
|
+
type=_boolean_type,
|
|
162
|
+
metavar="false",
|
|
163
|
+
help='Set certificate validation (true/false)'
|
|
164
|
+
)
|
|
165
|
+
parser.getgroup('testit').addoption(
|
|
166
|
+
'--tmsAutomaticCreationTestCases',
|
|
167
|
+
action="store",
|
|
168
|
+
dest="set_automatic_creation_test_cases",
|
|
169
|
+
type=_boolean_type,
|
|
170
|
+
metavar="false",
|
|
171
|
+
help="""
|
|
172
|
+
Set mode of automatic creation test cases (optional):
|
|
173
|
+
true - create a test case linked to the created autotest (not to the updated autotest)
|
|
174
|
+
false - not create a test case (Default)
|
|
175
|
+
"""
|
|
176
|
+
)
|
|
177
|
+
parser.getgroup('testit').addoption(
|
|
178
|
+
'--tmsAutomaticUpdationLinksToTestCases',
|
|
179
|
+
action="store",
|
|
180
|
+
dest="set_automatic_updation_links_to_test_cases",
|
|
181
|
+
type=_boolean_type,
|
|
182
|
+
metavar="false",
|
|
183
|
+
help="""
|
|
184
|
+
Set mode of automatic updation links to test cases (optional):
|
|
185
|
+
true - update links to test cases
|
|
186
|
+
false - not update links to test cases (Default)
|
|
187
|
+
"""
|
|
188
|
+
)
|
|
189
|
+
parser.getgroup('testit').addoption(
|
|
190
|
+
'--tmsImportRealtime',
|
|
191
|
+
action="store",
|
|
192
|
+
dest="set_import_realtime",
|
|
193
|
+
type=_boolean_type,
|
|
194
|
+
metavar="false",
|
|
195
|
+
help="""
|
|
196
|
+
Set mode of import type selection when launching autotests (optional):
|
|
197
|
+
true - the adapter will create/update each autotest in real time (Default)
|
|
198
|
+
false - the adapter will create/update multiple autotests
|
|
199
|
+
"""
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@pytest.hookimpl(tryfirst=True)
|
|
204
|
+
def pytest_load_initial_conftests(early_config, parser, args):
|
|
205
|
+
ns = getattr(early_config, "known_args_namespace", None)
|
|
206
|
+
if ns is not None and hasattr(ns, "tms_report"):
|
|
207
|
+
TmsPluginManager.set_pytest_tms_report(ns.tms_report)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def pytest_unconfigure(config):
|
|
211
|
+
TmsPluginManager.set_pytest_tms_report(None)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@pytest.hookimpl(tryfirst=True)
|
|
215
|
+
def pytest_cmdline_main(config):
|
|
216
|
+
if config.option.tms_report:
|
|
217
|
+
listener = TmsListener(
|
|
218
|
+
TmsPluginManager.get_adapter_manager(config.option),
|
|
219
|
+
TmsPluginManager.get_step_manager(),
|
|
220
|
+
TmsPluginManager.get_fixture_manager())
|
|
221
|
+
|
|
222
|
+
config.pluginmanager.register(listener)
|
|
223
|
+
TmsPluginManager.get_plugin_manager().register(listener)
|