forked from els-pnw/pytest_marker_bugzilla
-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathpytest_marker_bugzilla.py
482 lines (401 loc) · 14.9 KB
/
pytest_marker_bugzilla.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# -*- coding: utf-8 -*-
"""This plugin integrates pytest with bugzilla
It allows the tester to mark a test with a bug id. The test will be skipped
until the bug status is no longer NEW, ON_DEV, or ASSIGNED.
You must set the url either at the command line or in bugzilla.cfg.
An update to this plugin brings new possibilities. You can now add multiple
bugs to one test:
@pytest.mark.bugzilla(1234, 2345, "3456")
def test_something():
pass
In order to skip the test, all of the specified bugs must lead to skipping.
Even just one unskipped means that the test will not be skipped.
You can also add "conditional guards", which will xfail or skip the test when
condition is met:
@pytest.mark.bugzilla(1234, skip_when=lambda bug: bug.status == "POST")
or
@pytest.mark.bugzilla(
567, xfail_when=lambda bug, version: bug.fixed_in > version
)
The guard is a function, it will receive max. 2 parameters. It depends what
parameters you specify.
The parameters are:
`bug` which points to a specific BZ bug
`version` which is tested product version.
Order or presence does not matter.
In additional to the original parameters of this marker you can use:
--bugzilla-looseversion-fields
It accepts a string of field names comma separated. The specified fields have
getter function which returns instance of LooseVersion instead of string
allows you easy comparison in condition guards or inside tests.
--bugzilla-looseversion-fields=fixed_in,target_release
Authors:
Eric L. Sammons
Milan Falešník
"""
import inspect
import logging
import os
import re
from distutils.version import LooseVersion
from functools import wraps
import bugzilla
import pytest
import six
logger = logging.getLogger(__name__)
_bugs_pool = {} # Cache bugs for greater speed
_default_looseversion_fields = "fixed_in,target_release"
def get_value_from_config_parser(parser, option, default=None):
"""Wrapper around ConfigParser to do not fail on missing options."""
value = parser.defaults().get(option, default)
if value is not None and isinstance(value, six.string_types):
value = value.strip()
if not value:
value = default
return value
def kwargify(f):
"""Convert function having only positional args to a function taking
dictionary."""
@wraps(f)
def wrapped(**kwargs):
args = []
for arg in inspect.getargspec(f).args:
if arg not in kwargs:
raise TypeError(
"Required parameter {0} not found in the "
"context!".format(arg)
)
args.append(kwargs[arg])
return f(*args)
return wrapped
class BugWrapper(object):
def __init__(self, bug, loose):
self._bug = bug
# We need to generate looseversions for simple comparison of the
# version params.
for loose_version_param in loose:
param = getattr(bug, loose_version_param, "")
if param is None:
param = ""
if not isinstance(param, six.string_types):
param = str(param)
setattr(
self,
loose_version_param,
LooseVersion(re.sub(r"^[^0-9]+", "", param))
)
def __getattr__(self, attr):
"""Relay the query to the bug object if we did not override."""
return getattr(self._bug, attr)
class BugzillaBugs(object):
def __init__(self, bugzilla, loose, *bug_ids):
self.bugzilla = bugzilla
self.bug_ids = bug_ids
self.loose = loose
@property
def bugs_gen(self):
for bug_id in self.bug_ids:
if bug_id not in _bugs_pool:
bug = BugWrapper(self.bugzilla.getbug(bug_id), self.loose)
_bugs_pool[bug_id] = bug
yield _bugs_pool[bug_id]
def bug(self, id):
"""Returns Bugzilla's Bug object for given ID"""
id = int(id)
if id not in self.bug_ids:
raise ValueError("Could not find bug with id {0}".format(id))
if id in _bugs_pool:
return _bugs_pool[id]
bug = BugWrapper(self.bugzilla.getbug(id), self.loose)
_bugs_pool[id] = bug
return bug
class BugzillaHooks(object):
def __init__(self, config, bugzilla, loose, version="0"):
self.config = config
self.bugzilla = bugzilla
self.version = version
self.loose = loose
def add_bug_to_cache(self, bug_obj):
"""For test purposes only"""
_bugs_pool[str(bug_obj.id)] = BugWrapper(bug_obj, self.loose)
def _should_skip_due_to_api(self, item, engines):
if not engines:
return True
return item.parent.obj.api in engines
def _should_skip_due_to_storage(self, item, storages):
if not storages:
return True
if "storage" in item.fixturenames and hasattr(item, "callspec"):
parametrized_params = getattr(item.callspec, "params", {})
parametrized_storage = parametrized_params.get("storage")
if parametrized_storage:
for storage in storages:
if storage in parametrized_storage:
return True
return False
else:
return item.parent.obj.storage in storages
def _should_skip_due_to_ppc(self, item, is_ppc):
return is_ppc is None or is_ppc is True
def _should_skip_due_to_no_numa_support(self, item, no_numa):
return no_numa is None or no_numa is True
def _should_skip(self, item, bz_mark):
is_ppc_affected = self._should_skip_due_to_ppc(
item, bz_mark.get('ppc')
)
is_api_affected = self._should_skip_due_to_api(
item, bz_mark.get('engine')
)
is_storage_affected = self._should_skip_due_to_storage(
item, bz_mark.get('storage')
)
is_numa_affected = self._should_skip_due_to_no_numa_support(
item, bz_mark.get('no_numa')
)
if is_api_affected and is_storage_affected and is_ppc_affected and is_numa_affected:
return True
return False
def pytest_runtest_setup(self, item):
"""
Run test setup.
:param item: test being run.
"""
if "bugzilla" not in item.keywords:
return
bugs_in_cache = item.funcargs["bugs_in_cache"]
bugzilla_marker_related_to_case = item.get_closest_marker(name='bugzilla')
xfail = kwargify(
bugzilla_marker_related_to_case.kwargs.get(
"xfail_when", lambda: False
)
)
if xfail:
xfailed = self.evaluate_xfail(xfail, bugs_in_cache)
if xfailed:
url = "{0}?id=".format(
self.bugzilla.url.replace("xmlrpc.cgi", "show_bug.cgi"),
)
item.add_marker(
pytest.mark.xfail(
reason="xfailing due to bugs: {0}".format(
", ".join(
map(
lambda bug: "{0}{1}".format(
url, str(bug.id)
),
xfailed)
)
)
)
)
return
skip = kwargify(
bugzilla_marker_related_to_case.kwargs.get(
"skip_when", lambda: False
)
)
if skip:
self.evaluate_skip(skip, bugs_in_cache)
bugs_related_to_case = bugzilla_marker_related_to_case.args[0]
bugs_objs = []
for bug_id in bugs_related_to_case.keys():
bugs_objs.append(bugs_in_cache[bug_id])
skippers = []
for bz in bugs_objs:
for bug in bz.bugs_gen:
if bug.status == "CLOSED":
logger.info(
"Id:{0}; Status:{1}; Resolution:{2}; [RUNNING]".format(
bug.id, bug.status, bug.resolution
)
)
elif bug.status in ["VERIFIED", "ON_QA"]:
logger.info(
"Id: {0}; Status: {1}; [RUNNING]".format(
bug.id, bug.status
)
)
elif self._should_skip(
item, bugs_related_to_case[str(bug.id)]
):
skippers.append(bug)
logger.info(
"Id: {0}; Status: {1}; [SKIPPING]".format(
bug.id, bug.status
)
)
url = "{0}?id=".format(
self.bugzilla.url.replace("xmlrpc.cgi", "show_bug.cgi"),
)
if skippers:
skipping_summary = (
"Skipping due to: "
"\n".join(
[
"Bug summary: {0} Status: {1} URL: {2}{3}".format(
bug.summary, bug.status, url, bug.id
)
for bug in skippers
]
)
)
logger.info(
"Test case {0} will be skipped due to:\n {1}".format(
item.name, skipping_summary
)
)
pytest.skip(skipping_summary)
def evaluate_skip(self, skip, bugs):
bugs_obj = bugs.values()
for bug_obj in bugs_obj:
for bz in bug_obj.bugs_gen:
context = {"bug": bz}
if self.version:
context["version"] = LooseVersion(self.version)
if skip(**context):
pytest.skip(
"Skipped due to a given condition: {0}".format(
inspect.getsource(skip)
)
)
def evaluate_xfail(self, xfail, bugs):
results = []
# for bug in bugs.bugs_gen:
bugs_obj = bugs.values()
for bug_obj in bugs_obj:
for bz in bug_obj.bugs_gen:
context = {"bug": bz}
if self.version:
context["version"] = LooseVersion(self.version)
if xfail(**context):
results.append(bz)
return results
def pytest_collection_modifyitems(self, session, config, items):
reporter = config.pluginmanager.getplugin("terminalreporter")
# When run as xdist slave you don't have access to reporter
if reporter:
reporter.write("Checking for bugzilla-related tests\n", bold=True)
cache = {}
for item in items:
for marker in item.iter_markers(name='bugzilla'):
bugs = marker.args[0]
bugs_ids = bugs.keys()
for bz_id in bugs_ids:
if bz_id not in cache:
if reporter:
reporter.write(".")
cache[bz_id] = BugzillaBugs(
self.bugzilla, self.loose, bz_id
)
item.funcargs["bugs_in_cache"] = cache
if reporter:
reporter.write(
"\nChecking for bugzilla-related tests has finished\n",
bold=True,
)
reporter.write(
"{0} bug marker sets found.\n".format(len(cache)), bold=True,
)
def pytest_addoption(parser):
"""
Add options section to py.test --help for bugzilla integration.
Parse configuration file, bugzilla.cfg and / or the command line options
passed.
:param parser: Command line options.
"""
config = six.moves.configparser.ConfigParser()
config.read(
[
'/etc/bugzilla.cfg',
os.path.expanduser('~/bugzilla.cfg'),
'bugzilla.cfg',
]
)
group = parser.getgroup('Bugzilla integration')
group.addoption(
'--bugzilla',
action='store_true',
default=False,
dest='bugzilla',
help='Enable Bugzilla support.',
)
group.addoption(
'--bugzilla-url',
action='store',
dest='bugzilla_url',
default=get_value_from_config_parser(config, 'bugzilla_url'),
metavar='url',
help='Overrides the xmlrpc url for bugzilla found in bugzilla.cfg.',
)
group.addoption(
'--bugzilla-user',
action='store',
dest='bugzilla_username',
default=get_value_from_config_parser(config, 'bugzilla_username', ''),
metavar='username',
help='Overrides the bugzilla username in bugzilla.cfg.',
)
group.addoption(
'--bugzilla-password',
action='store',
dest='bugzilla_password',
default=get_value_from_config_parser(config, 'bugzilla_password', ''),
metavar='password',
help='Overrides the bugzilla password in bugzilla.cfg.',
)
group.addoption(
'--bugzilla-api-key',
action='store',
dest='bugzilla_api_key',
default=get_value_from_config_parser(config, 'bugzilla_api_key', ''),
metavar='api_key',
help='Overrides the bugzilla api key in bugzilla.cfg.',
)
group.addoption(
'--bugzilla-project-version',
action='store',
dest='bugzilla_version',
default=get_value_from_config_parser(config, 'bugzilla_version'),
metavar='version',
help='Overrides the project version in bugzilla.cfg.',
)
group.addoption(
'--bugzilla-looseversion-fields',
action='store',
dest='bugzilla_loose',
default=get_value_from_config_parser(
config, 'bugzilla_loose', _default_looseversion_fields,
),
metavar='loose',
help='Overrides the project loose in bugzilla.cfg.',
)
def pytest_configure(config):
"""
If bugzilla is neabled, setup a session
with bugzilla_url.
:param config: configuration object
"""
config.addinivalue_line(
"markers",
"bugzilla(*bug_ids, **guards): Bugzilla integration",
)
url = config.getvalue('bugzilla_url')
username = config.getvalue('bugzilla_username')
password = config.getvalue('bugzilla_password')
api_key = config.getvalue('bugzilla_api_key')
if config.getvalue("bugzilla") and url:
if username and password:
bz = bugzilla.Bugzilla(url=url, user=username, password=password)
elif api_key:
bz = bugzilla.Bugzilla(url=url, api_key=api_key)
else:
bz = bugzilla.Bugzilla(url=url)
version = config.getvalue('bugzilla_version')
loose = [
x.strip()
for x in config.getvalue('bugzilla_loose').strip().split(",", 1)
]
if len(loose) == 1 and not loose[0]:
loose = []
my = BugzillaHooks(config, bz, loose, version)
assert config.pluginmanager.register(my, "bugzilla_helper")