-
Notifications
You must be signed in to change notification settings - Fork 15
/
pytest_marker_bugzilla.py
351 lines (303 loc) · 11.1 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
# -*- coding: utf-8 -*-
import sys
import six
import bugzilla
import inspect
import os
import pytest
import re
from distutils.version import LooseVersion
from functools import wraps
"""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, MODIFIED, POST 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
"""
_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 = []
if sys.version_info.major >= 3:
function_args = inspect.getfullargspec(f).args
else:
function_args = inspect.getargspec(f).args
for arg in function_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]
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[bug_obj.id] = BugWrapper(bug_obj, self.loose)
def pytest_runtest_setup(self, item):
"""
Run test setup.
:param item: test being run.
"""
if "bugzilla" not in item.keywords:
return
bugs = item.funcargs["bugs"]
skippers = []
for bz in bugs.bugs_gen:
if bz.status in ["NEW", "ASSIGNED", "ON_DEV", "MODIFIED", "POST"]:
skippers.append(bz)
url = "{0}?id=".format(
self.bugzilla.url.replace("xmlrpc.cgi", "show_bug.cgi"),
)
if skippers:
pytest.skip(
"Skipping this test because all of these assigned bugs:\n"
"{0}".format(
"\n".join(
["{0} {1}{2}".format(bug.status, url, bug.id)for bug in skippers]
)
)
)
marker = item.get_closest_marker(name='bugzilla')
xfail = kwargify(marker.kwargs.get("xfail_when", lambda: False))
skip = kwargify(marker.kwargs.get("skip_when", lambda: False))
if skip:
self.evaluate_skip(skip, bugs)
if xfail:
xfailed = self.evaluate_xfail(xfail, bugs)
if xfailed:
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)
)
)
)
)
def evaluate_skip(self, skip, bugs):
for bz in bugs.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 bz in bugs.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 = tuple(sorted(set(map(int, marker.args))))
if bugs not in cache:
if reporter:
reporter.write(".")
cache[bugs] = BugzillaBugs(self.bugzilla, self.loose, *bugs)
item.funcargs["bugs"] = cache[bugs]
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 a 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")