Merge lp:~allanlesage/uci-engine/coverage-extractor into lp:uci-engine
- coverage-extractor
- Merge into trunk
Status: | Needs review |
---|---|
Proposed branch: | lp:~allanlesage/uci-engine/coverage-extractor |
Merge into: | lp:uci-engine |
Diff against target: |
1319 lines (+1251/-0) 12 files modified
coverage-extractor/coverageextractor/__init__.py (+75/-0) coverage-extractor/coverageextractor/extraction.py (+226/-0) coverage-extractor/coverageextractor/nfss_client.py (+80/-0) coverage-extractor/coverageextractor/run_worker.py (+64/-0) coverage-extractor/coverageextractor/tests/__init__.py (+86/-0) coverage-extractor/coverageextractor/tests/test_extract_coverage_data.py (+162/-0) coverage-extractor/coverageextractor/tests/test_extraction.py (+233/-0) coverage-extractor/coverageextractor/tests/test_handle_request.py (+121/-0) coverage-extractor/coverageextractor/tests/test_nfss_client.py (+150/-0) coverage-extractor/setup.py (+34/-0) coverage-extractor/tox.ini (+19/-0) testing/run_tests.py (+1/-0) |
To merge this branch: | bzr merge lp:~allanlesage/uci-engine/coverage-extractor |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
PS Jenkins bot (community) | continuous-integration | Approve | |
Francis Ginther | Approve | ||
Review via email: mp+238782@code.launchpad.net |
Commit message
Extract line and branch coverage numbers from a coverage.xml artifact deposited in swift, post these to NFSS.
Description of the change
Extract line and branch coverage numbers from a coverage.xml artifact deposited in swift, post these to NFSS.
PS Jenkins bot (ps-jenkins) wrote : | # |
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:858
http://
Executed test runs:
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:859
http://
Executed test runs:
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:860
http://
Executed test runs:
Click here to trigger a rebuild:
http://
Francis Ginther (fginther) wrote : | # |
Here's my first round of feedback. I'm still going through the test modules.
General comments:
We should refactor this with coverage-retriever, they share a lot of code (possibly pushing stuff into ci-utils).
We'll need to figure out how to handle some bigger picture issues. There exists the possibility of multiple coverage results being generated for a source package and a ticket. We can probably punt on this for now, but need to make 'XXX' indications where necessary to know better what will need to be updated.
Will need to pass in the values for the nfss_insert. Ideally we need to make this such that the values aren't required to deploy, but things will fail gracefully if they are missing (other parts of the engine also need to handle this better).
Francis Ginther (fginther) wrote : | # |
Adding some more comments.
I also started executing the tests and run into:
Traceback (most recent call last):
File "./run-tests", line 32, in <module>
retval = run_tests.
File "/tmp/engine/
options.
File "/tmp/engine/
component_suite = load_component_
File "/tmp/engine/
suite.
File "/tmp/engine/
suite.
File "/tmp/engine/
suite.
File "/tmp/engine/
suite = self.loadTestsF
File "/tmp/engine/
suite.
File "/tmp/engine/
suite.
File "/tmp/engine/
suite = self.loadTestsF
File "/tmp/engine/
suite.
File "/tmp/engine/
suite.
File "/tmp/engine/
module = self.importFrom
File "/tmp/engine/
raise ImportError(msg)
ImportError: Failed to import britney.
Traceback (most recent call last):
File "/tmp/engine/
return importlib.
File "/usr/lib/
__import_
File "britney_
Allan LeSage (allanlesage) wrote : | # |
Francis, I've adapted your suggested changes with a couple of exceptions:
* Thomi explained the manual process of generating an OAuth1 key for NFSS to me--it doesn't sound like it's compatible with the automated deploy that you're explained via unit_config, as there are post-deploy steps. In any case I'd like to open a bug to discuss, and implement a later (likely the next) phase of our development.
* I've removed the NFSS deployment items from our juju_deploy template, meanwhile it seems unlikely that these would cause the deploy error you mention above as they're in a separate MP?
Allan LeSage (allanlesage) wrote : | # |
Note that we've created an Asana task for the NFSS deployment discussion, hopefully to happen this week while we're all together :) .
Francis Ginther (fginther) wrote : | # |
I should have found this format sooner. Please use the format for filing XXX comments:
# XXX <irc handle> <YYYYMMDD>
So:
# XXX alesage 20141022
# Something to revisit, blah blah.
Also, please keep these inside comments and not docstrings.
There are some additional comments inline, but this is much closer.
Allan LeSage (allanlesage) wrote : | # |
Francis, I've updated per your comments--can you be persuaded to accept a second MP concerning the NFSS deployment details? Thomi agrees to defer (even until next phase) but I intend to discuss and probably fix before EOW--unless this just doesn't make sense in your opinion.
Francis Ginther (fginther) wrote : | # |
Allan, I'm fine with a follow up MP. Also, I may have crossed my comments with your responses. As this component isn't deployed just yet. I'm ok to just address the TODO and XXX comments before top-approving. Please ping me tomorrow (or is it now today) if there is any confusion or my inline comments are not visible.
Allan LeSage (allanlesage) wrote : | # |
Ok I've updated my code-comments, amending them to XXX alesage <date> format, adding a link to this MP where helpful.
Francis Ginther (fginther) wrote : | # |
This looks good, thanks for the update.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:863
http://
Executed test runs:
Click here to trigger a rebuild:
http://
Francis Ginther (fginther) wrote : | # |
The unit tests for the coverage-extractor are failing. Looks like the reason is that the version of ucitests in setup.py don't match the other components. Should be 0.1.5.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:864
http://
Executed test runs:
Click here to trigger a rebuild:
http://
Allan LeSage (allanlesage) wrote : | # |
Top-approved with fginther's looking over my shoulder :) .
Ubuntu CI Bot (uci-bot) wrote : | # |
The attempt to merge lp:~allanlesage/uci-engine/coverage-extractor into lp:uci-engine failed. Below is the output from the failed tests.
INFO:root:Creating a virtualenv to run under...
Traceback (most recent call last):
File "/usr/lib/
main()
File "/usr/lib/
symlink=
File "/usr/lib/
site_
File "/usr/lib/
writefile(
File "/usr/lib/
f.write(
IOError: [Errno 28] No space left on device
INFO:root:
Traceback (most recent call last):
File "bin/called-
sys.
File "bin/called-
venv.install()
File "/tmp/tarmac/
path = create()
File "/tmp/tarmac/
subprocess.
File "/usr/lib/
raise CalledProcessEr
subprocess.
Francis Ginther (fginther) wrote : | # |
We're working on the tarmac deployment to resolve the out-of-space issue.
Unmerged revisions
- 864. By Allan LeSage
-
Update ucitests version to 0.1.5 for coverage-extractor.
- 863. By Allan LeSage
-
Coverage-extractor fix XXX comments.
- 862. By Allan LeSage
-
Adapt to some fginther suggestions for coverage-extractor.
- 861. By Allan LeSage
-
Amend a test docstring FTW.
- 860. By Allan LeSage
-
Amend some FIXME comments for coverage-extractor.
- 859. By Allan LeSage
-
Add setup.py :/ .
- 858. By Allan LeSage
-
Add coverage-extractor to run_tests.py.
- 857. By Allan LeSage
-
Merge trunk.
- 856. By Allan LeSage
-
Extract line and branch coverage from a coverage.xml artifact in swift, post to NFSS.
Preview Diff
1 | === added directory 'coverage-extractor' |
2 | === added directory 'coverage-extractor/coverageextractor' |
3 | === added file 'coverage-extractor/coverageextractor/__init__.py' |
4 | --- coverage-extractor/coverageextractor/__init__.py 1970-01-01 00:00:00 +0000 |
5 | +++ coverage-extractor/coverageextractor/__init__.py 2014-10-23 21:46:26 +0000 |
6 | @@ -0,0 +1,75 @@ |
7 | +# Ubuntu CI Engine |
8 | +# Copyright 2014 Canonical Ltd. |
9 | +# |
10 | +# This program is free software: you can redistribute it and/or modify it |
11 | +# under the terms of the GNU Affero General Public License version 3, as |
12 | +# published by the Free Software Foundation. |
13 | +# |
14 | +# This program is distributed in the hope that it will be useful, but |
15 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
16 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
17 | +# PURPOSE. See the GNU Affero General Public License for more details. |
18 | +# |
19 | +# You should have received a copy of the GNU Affero General Public License |
20 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
21 | + |
22 | +from functools import wraps |
23 | +import time |
24 | +import xml.etree.ElementTree as etree |
25 | + |
26 | + |
27 | +class CoverageExtractorException(ValueError): |
28 | + """Bad things happen to good extractors.""" |
29 | + |
30 | + |
31 | +def extract_coverage_data(coverage_xml): |
32 | + """Return line and branch numbers from coverage.xml. |
33 | + |
34 | + :param coverage_xml: coverage.xml string |
35 | + """ |
36 | + try: |
37 | + # root node is coverage, more complex patterns possible later |
38 | + coverage_node = etree.fromstring(coverage_xml) |
39 | + return ( |
40 | + float(coverage_node.attrib['line-rate']), |
41 | + float(coverage_node.attrib['branch-rate']), |
42 | + ) |
43 | + except etree.ParseError as e: |
44 | + raise CoverageExtractorException( |
45 | + "Failed to parse coverage.xml: {}".format(e.message) |
46 | + ) |
47 | + except ValueError as e: |
48 | + raise CoverageExtractorException( |
49 | + "Failed to parse coverage numbers: {}".format(e.message) |
50 | + ) |
51 | + |
52 | + |
53 | +def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None): |
54 | + """Retry calling the decorated function using an exponential backoff. |
55 | + |
56 | + http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/ |
57 | + Original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry |
58 | + |
59 | + :param ExceptionToCheck: the exception to check. may be a tuple of |
60 | + exceptions to check |
61 | + :param tries: number of times to try (not retry) before giving up |
62 | + :param delay: initial delay between retries in seconds |
63 | + :param backoff: backoff multiplier e.g. value of 2 will double the delay |
64 | + each retry |
65 | + :param logger: logger.Logger to use. |
66 | + """ |
67 | + def deco_retry(f): |
68 | + @wraps(f) |
69 | + def f_retry(*args, **kwargs): |
70 | + for i in range(tries): |
71 | + try: |
72 | + return f(*args, **kwargs) |
73 | + except ExceptionToCheck as e: |
74 | + d = delay * backoff ** i |
75 | + if logger: |
76 | + logger.warning( |
77 | + "%s, retrying in %d seconds. . . ." % (e, d)) |
78 | + time.sleep(d) |
79 | + return f(*args, **kwargs) |
80 | + return f_retry # true decorator |
81 | + return deco_retry |
82 | |
83 | === added file 'coverage-extractor/coverageextractor/extraction.py' |
84 | --- coverage-extractor/coverageextractor/extraction.py 1970-01-01 00:00:00 +0000 |
85 | +++ coverage-extractor/coverageextractor/extraction.py 2014-10-23 21:46:26 +0000 |
86 | @@ -0,0 +1,226 @@ |
87 | +#!/usr/bin/env python |
88 | +# Ubuntu CI Engine |
89 | +# Copyright 2014 Canonical Ltd. |
90 | +# |
91 | +# This program is free software: you can redistribute it and/or modify it |
92 | +# under the terms of the GNU Affero General Public License version 3, as |
93 | +# published by the Free Software Foundation. |
94 | +# |
95 | +# This program is distributed in the hope that it will be useful, but |
96 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
97 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
98 | +# PURPOSE. See the GNU Affero General Public License for more details. |
99 | +# |
100 | +# You should have received a copy of the GNU Affero General Public License |
101 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
102 | + |
103 | +import json |
104 | + |
105 | +from ci_utils import amqp_utils |
106 | +from ci_utils.data_store import DataStoreException |
107 | +from coverageextractor import ( |
108 | + CoverageExtractorException, |
109 | + extract_coverage_data, |
110 | + retry, |
111 | +) |
112 | + |
113 | +from nfss_client import ( |
114 | + NfssClient, |
115 | + NfssClientError, |
116 | + NFSS_CONFIG_PATH, |
117 | +) |
118 | + |
119 | +SUCCESS_TEMPLATE = u'Deposited coverage.xml into swift for ticket %s.' |
120 | +MISSING_PARAMS_TEMPLATE = (u'Failed to handle request: missing coverage.xml ' |
121 | + 'filename for ticket %s') |
122 | +PARSE_FAILURE_TEMPLATE = u'Failed to parse coverage.xml for ticket %s: %s' |
123 | +SWIFT_GET_FAILURE_TEMPLATE = (u'Failed to retrieve coverage.xml from swift ' |
124 | + 'for ticket %s: %s') |
125 | +SWIFT_PUT_FAILURE_TEMPLATE = (u'Failed to put coverage.xml into swift for ' |
126 | + 'ticket %s: %s') |
127 | +JSON_ENCODING_FAILURE_TEMPLATE = (u'Failed to encode JSON for ' |
128 | + 'ticket %s: %s') |
129 | +NFSS_CLIENT_ERROR_TEMPLATE = u'Failed to POST to NFSS for ticket %s: %s' |
130 | + |
131 | + |
132 | +def generate_and_put_artifact( |
133 | + data_store, |
134 | + name='coverage-extractor.output.log', |
135 | + type='LOGS', |
136 | + message=None, |
137 | + reference=None): |
138 | + """Return a uci-artifact generated and deposited in swift. |
139 | + |
140 | + NOTE that if no reference is supplied, the artifact is deposited |
141 | + into swift and the resulting link is reported in the returned |
142 | + artifact; provided for convenience in logging. |
143 | + |
144 | + :param data_store: DataStore into which to put |
145 | + :param name: filename of artifact in swift |
146 | + :param type: 'LOGS' for logs or 'RESULTS' for coverage.xml |
147 | + :param message: a log message |
148 | + :param reference: swift link; supplied upon deposit if not specified |
149 | + """ |
150 | + artifact = { |
151 | + 'name': name, |
152 | + 'type': type, |
153 | + } |
154 | + if message is not None: |
155 | + artifact['message'] = message |
156 | + if reference is None: |
157 | + reference = data_store.put_file(name, str(artifact), 'text/plain') |
158 | + artifact['reference'] = reference |
159 | + return artifact |
160 | + |
161 | + |
162 | +class ExtractionFailed(Exception): |
163 | + """Return a dict upon operation failure..""" |
164 | + |
165 | + def __init__(self, return_tuple): |
166 | + self.return_tuple = return_tuple |
167 | + |
168 | + |
169 | +class Extraction(object): |
170 | + |
171 | + def __init__(self, ticket_id, logger, data_store, nfss_config_path=None): |
172 | + self.ticket_id = ticket_id |
173 | + self.logger = logger |
174 | + self.data_store = data_store |
175 | + # XXX alesage 20141022 |
176 | + # Possibly we want to own the NFSS client here instead? |
177 | + self.nfss_config_path = nfss_config_path or NFSS_CONFIG_PATH |
178 | + self.result = dict(artifacts=[]) |
179 | + |
180 | + def try_get_project_and_version(self, params): |
181 | + """Extract project name and version from params. |
182 | + |
183 | + :param params: ticket system handle_request calling params |
184 | + """ |
185 | + # XXX alesage 20141022 |
186 | + # for the moment we treat the one-subticket case; a ticket may |
187 | + # report multiple builds corresponding to architectures, e.g. |
188 | + subticket = params['subtickets'][0] |
189 | + return subticket['name'], subticket['version'] |
190 | + |
191 | + def try_get_coverage_xml_path(self, params): |
192 | + """Extract coverage.xml path from dict of params. |
193 | + |
194 | + :param params: dict, hopefully with 'coverage_xml_path' as key |
195 | + """ |
196 | + # XXX alesage 20141022 |
197 | + # Consider the case of multiple coverage.xml? |
198 | + try: |
199 | + return params['coverage_xml_path'] |
200 | + except KeyError: |
201 | + self.report_failed( |
202 | + MISSING_PARAMS_TEMPLATE, |
203 | + self.ticket_id |
204 | + ) |
205 | + |
206 | + def try_get_coverage_xml(self, coverage_xml_path): |
207 | + """Get coverage.xml at given path from swift. |
208 | + |
209 | + :param coverage_xml_path: swift path from which to get |
210 | + """ |
211 | + try: |
212 | + @retry(DataStoreException, logger=self.logger) |
213 | + def get_build_log_actual(): |
214 | + return self.data_store.get_file(coverage_xml_path) |
215 | + return get_build_log_actual() |
216 | + except DataStoreException as e: |
217 | + self.report_failed( |
218 | + SWIFT_GET_FAILURE_TEMPLATE, |
219 | + self.ticket_id, |
220 | + e.message, |
221 | + ) |
222 | + |
223 | + def try_extract_coverage_data(self, coverage_xml): |
224 | + """Get line and branch percentages from coverage.xml. |
225 | + |
226 | + :param coverage_xml: coverage_xml string |
227 | + """ |
228 | + try: |
229 | + return extract_coverage_data(coverage_xml) |
230 | + except CoverageExtractorException as e: |
231 | + self.report_failed( |
232 | + PARSE_FAILURE_TEMPLATE, |
233 | + self.ticket_id, |
234 | + e.message, |
235 | + ) |
236 | + |
237 | + def try_compose_nfss_json( |
238 | + self, |
239 | + name, |
240 | + version, |
241 | + line_coverage, |
242 | + branch_coverage): |
243 | + """Compose a JSON string of data for submission to NFSS. |
244 | + |
245 | + :param name: package name |
246 | + :param version: package version string |
247 | + :param line_coverage: line coverage (float) |
248 | + :param branch_coverage: branch coverage (float) |
249 | + """ |
250 | + try: |
251 | + return json.dumps({ |
252 | + 'name': name, |
253 | + 'version': version, |
254 | + 'line_coverage': line_coverage, |
255 | + 'branch_coverage': branch_coverage, |
256 | + }) |
257 | + except ValueError as e: |
258 | + # This is pretty unlikely. |
259 | + self.report_failed( |
260 | + JSON_ENCODING_FAILURE_TEMPLATE, |
261 | + self.ticket_id, |
262 | + e.message |
263 | + ) |
264 | + |
265 | + def try_post_data_to_nfss(self, json, test_name, project_name='coverage'): |
266 | + """Post a JSON string of coverage data to NFSS. |
267 | + |
268 | + :param json: coverage data JSON to post to NFSS |
269 | + """ |
270 | + try: |
271 | + nfss_client = NfssClient(self.nfss_config_path) |
272 | + nfss_client.post_data(json, test_name, project_name) |
273 | + except NfssClientError as e: |
274 | + self.report_failed( |
275 | + NFSS_CLIENT_ERROR_TEMPLATE, |
276 | + self.ticket_id, |
277 | + e.message |
278 | + ) |
279 | + |
280 | + def report_failed(self, format_str, *str_args): |
281 | + """Convenience function to raise and log a failiure. |
282 | + |
283 | + :param format_str: template to log |
284 | + :str_args str_args: items with which to format format_str |
285 | + """ |
286 | + message = format_str % str_args |
287 | + self.logger.error(message) |
288 | + self.result['message'] = message |
289 | + self.result['artifacts'].append( |
290 | + generate_and_put_artifact(self.data_store, message=message) |
291 | + ) |
292 | + raise ExtractionFailed( |
293 | + (amqp_utils.progress_failed, self.result) |
294 | + ) |
295 | + |
296 | + def report_succeeded(self, ticket_id): |
297 | + """Deposit a result artifact and return a log artifact. |
298 | + |
299 | + :param ticket_id: ticket for which to report retrieval success |
300 | + """ |
301 | + message = SUCCESS_TEMPLATE % ticket_id |
302 | + # NOTE that logging compels us to pass params to info (can't |
303 | + # reuse message declared above) |
304 | + self.logger.info(SUCCESS_TEMPLATE, ticket_id) |
305 | + self.result['artifacts'].append( |
306 | + generate_and_put_artifact( |
307 | + self.data_store, |
308 | + message=message |
309 | + ) |
310 | + ) |
311 | + self.result['message'] = message |
312 | + return amqp_utils.progress_completed, self.result |
313 | |
314 | === added file 'coverage-extractor/coverageextractor/nfss_client.py' |
315 | --- coverage-extractor/coverageextractor/nfss_client.py 1970-01-01 00:00:00 +0000 |
316 | +++ coverage-extractor/coverageextractor/nfss_client.py 2014-10-23 21:46:26 +0000 |
317 | @@ -0,0 +1,80 @@ |
318 | +#!/usr/bin/env python |
319 | +# Ubuntu CI Engine |
320 | +# Copyright 2014 Canonical Ltd. |
321 | +# |
322 | +# This program is free software: you can redistribute it and/or modify it |
323 | +# under the terms of the GNU Affero General Public License version 3, as |
324 | +# published by the Free Software Foundation. |
325 | +# |
326 | +# This program is distributed in the hope that it will be useful, but |
327 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
328 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
329 | +# PURPOSE. See the GNU Affero General Public License for more details. |
330 | +# |
331 | +# You should have received a copy of the GNU Affero General Public License |
332 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
333 | + |
334 | +from ConfigParser import ConfigParser |
335 | + |
336 | +from requests_oauthlib import OAuth1Session |
337 | + |
338 | +# XXX alesage 20141022 |
339 | +# thomi, where to find this config? |
340 | +NFSS_CONFIG_PATH = "TODO-fake-nfss-config-path" |
341 | + |
342 | + |
343 | +class NfssClientError(Exception): |
344 | + """Non-functional non-functional stats service.""" |
345 | + |
346 | + |
347 | +def parse_nfss_config(nfss_config_path): |
348 | + """Return a ConfigParser config from NFSS config filepath. |
349 | + |
350 | + NOTE that we only return the 'nfss' section of the config. |
351 | + |
352 | + :param nfss_config_path: path to NFSS config |
353 | + """ |
354 | + # XXX alesage 20141022 |
355 | + # Discussed with thomi and fginther, need to sort the details of |
356 | + # the config deployment, defer for the moment; an Asana task has |
357 | + # been created to discuss in D.C. |
358 | + config = ConfigParser() |
359 | + config.read(nfss_config_path) |
360 | + # are all of our ducks in a row? |
361 | + for config_item in ( |
362 | + 'client_access_key', |
363 | + 'resource_owner_key', |
364 | + 'resource_owner_secret', |
365 | + 'backend'): |
366 | + # (we'll also raise KeyError on missing 'nfss' section) |
367 | + if config_item not in config['nfss']: |
368 | + raise KeyError(config_item) |
369 | + return config['nfss'] |
370 | + |
371 | + |
372 | +class NfssClient(object): |
373 | + |
374 | + def __init__(self, nfss_config_path=None): |
375 | + if nfss_config_path is None: |
376 | + self.nfss_config_path = NFSS_CONFIG_PATH |
377 | + try: |
378 | + # XXX alesage 20141022 |
379 | + # Discuss how to deploy this config properly with thomi |
380 | + # and fginther, following from |
381 | + # https://code.launchpad.net/~allanlesage/uci-engine/coverage-extractor/+merge/238782. # NOQA |
382 | + self.config = parse_nfss_config(nfss_config_path) |
383 | + except KeyError as e: |
384 | + raise NfssClientError('Failed to parse NFSS config: ' |
385 | + '%s' % e.message) |
386 | + self.session = OAuth1Session( |
387 | + self.config['client_access_key'], |
388 | + resource_owner_key=self.config['resource_owner_key'], |
389 | + resource_owner_secret=self.config['resource_owner_secret'], |
390 | + ) |
391 | + |
392 | + def post_data(self, json, project_name, test_name): |
393 | + """POST JSON result data to NFSS at a given project and test.""" |
394 | + url = '/'.join((self.config['backend'], project_name, test_name)) |
395 | + r = self.session.post(url, dict(data=json)) |
396 | + if r.status_code != 200: |
397 | + raise NfssClientError("Failed to POST to NFSS: %s" % r.content) |
398 | |
399 | === added file 'coverage-extractor/coverageextractor/run_worker.py' |
400 | --- coverage-extractor/coverageextractor/run_worker.py 1970-01-01 00:00:00 +0000 |
401 | +++ coverage-extractor/coverageextractor/run_worker.py 2014-10-23 21:46:26 +0000 |
402 | @@ -0,0 +1,64 @@ |
403 | +#!/usr/bin/env python |
404 | +# Ubuntu CI Engine |
405 | +# Copyright 2014 Canonical Ltd. |
406 | +# |
407 | +# This program is free software: you can redistribute it and/or modify it |
408 | +# under the terms of the GNU Affero General Public License version 3, as |
409 | +# published by the Free Software Foundation. |
410 | +# |
411 | +# This program is distributed in the hope that it will be useful, but |
412 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
413 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
414 | +# PURPOSE. See the GNU Affero General Public License for more details. |
415 | +# |
416 | +# You should have received a copy of the GNU Affero General Public License |
417 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
418 | + |
419 | +from ci_utils import amqp_utils, amqp_worker |
420 | +from coverageextractor.extraction import Extraction, ExtractionFailed |
421 | + |
422 | + |
423 | +class CoverageExtractorWorker(amqp_worker.AMQPWorker): |
424 | + |
425 | + def __init__(self): |
426 | + super(CoverageExtractorWorker, self).__init__('coverageextractor') |
427 | + |
428 | + def handle_request(self, params, logger): |
429 | + """Extract coverage numbers from coverage.xml and post to NFSS. |
430 | + |
431 | + :param params: dictionary of params from incoming message |
432 | + :param logger: logging.Logger for reporting |
433 | + """ |
434 | + # XXX alesage 20141022 |
435 | + # Consider multi-coverage.xml builds, e.g. tickets reporting |
436 | + # builds of a package for more than one architecture. These |
437 | + # could each initiate an Extraction, e.g.? Also beware naming |
438 | + # overlap for results. |
439 | + ticket_id = params['ticket_id'] |
440 | + data_store = self._create_data_store(ticket_id) |
441 | + extraction = Extraction( |
442 | + ticket_id, |
443 | + logger, |
444 | + data_store, |
445 | + ) |
446 | + try: |
447 | + project, version = extraction.try_get_project_and_version(params) |
448 | + coverage_xml_path = extraction.try_get_coverage_xml_path(params) |
449 | + coverage_xml = extraction.try_get_coverage_xml(coverage_xml_path) |
450 | + line_data, branch_data = extraction.try_extract_coverage_data( |
451 | + coverage_xml |
452 | + ) |
453 | + data = extraction.try_compose_nfss_json( |
454 | + project, |
455 | + version, |
456 | + line_data, |
457 | + branch_data, |
458 | + ) |
459 | + extraction.try_post_data_to_nfss(data, project) |
460 | + except ExtractionFailed as e: |
461 | + return e.return_tuple |
462 | + return extraction.report_succeeded(ticket_id) |
463 | + |
464 | + |
465 | +if __name__ == '__main__': |
466 | + CoverageExtractorWorker().main(amqp_utils.COVERAGE_RETRIEVER_QUEUE) |
467 | |
468 | === added directory 'coverage-extractor/coverageextractor/tests' |
469 | === added file 'coverage-extractor/coverageextractor/tests/__init__.py' |
470 | --- coverage-extractor/coverageextractor/tests/__init__.py 1970-01-01 00:00:00 +0000 |
471 | +++ coverage-extractor/coverageextractor/tests/__init__.py 2014-10-23 21:46:26 +0000 |
472 | @@ -0,0 +1,86 @@ |
473 | +# Ubuntu CI Engine |
474 | +# Copyright 2014 Canonical Ltd. |
475 | +# |
476 | +# This program is free software: you can redistribute it and/or modify it |
477 | +# under the terms of the GNU Affero General Public License version 3, as |
478 | +# published by the Free Software Foundation. |
479 | +# |
480 | +# This program is distributed in the hope that it will be useful, but |
481 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
482 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
483 | +# PURPOSE. See the GNU Affero General Public License for more details. |
484 | +# |
485 | +# You should have received a copy of the GNU Affero General Public License |
486 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
487 | + |
488 | +from contextlib import contextmanager |
489 | +import unittest |
490 | + |
491 | +from mock import patch |
492 | + |
493 | +from ci_utils.amqp_worker import AMQPWorker |
494 | +from ci_utils.testing.fixtures import FakeDataStore |
495 | + |
496 | +vanilla_coverage_xml = u"""<?xml version="1.0" ?> |
497 | +<!DOCTYPE coverage |
498 | + SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-03.dtd'> |
499 | +<coverage branch-rate="1.0" line-rate="1.0" timestamp="1410387089" version="gcovr 2.4 (r2774)"> |
500 | + <sources> |
501 | + <source>/tmp/tmpz8yrh4tz/dummy</source> |
502 | + </sources> |
503 | + <packages> |
504 | + <package branch-rate="1.0" complexity="0.0" line-rate="1.0" name=""> |
505 | + <classes> |
506 | + <class branch-rate="1.0" complexity="0.0" filename="fibonacci.cpp" line-rate="1.0" name="fibonacci_cpp"> |
507 | + <lines> |
508 | + <line branch="false" hits="10" number="4"/> |
509 | + <line branch="true" condition-coverage="100% (2/2)" hits="10" number="6"> |
510 | + <conditions> |
511 | + <condition coverage="100%" number="0" type="jump"/> |
512 | + </conditions> |
513 | + </line> |
514 | + <line branch="false" hits="3" number="7"/> |
515 | + <line branch="true" condition-coverage="100% (2/2)" hits="7" number="8"> |
516 | + <conditions> |
517 | + <condition coverage="100%" number="0" type="jump"/> |
518 | + </conditions> |
519 | + </line> |
520 | + <line branch="false" hits="4" number="9"/> |
521 | + <line branch="false" hits="3" number="10"/> |
522 | + </lines> |
523 | + </class> |
524 | + </classes> |
525 | + </package> |
526 | + </packages> |
527 | +</coverage> |
528 | + |
529 | +""" # NOQA |
530 | + |
531 | + |
532 | +class CoverageExtractorTestCase(unittest.TestCase): |
533 | + |
534 | + @contextmanager |
535 | + def patch_create_data_store(self): |
536 | + """Patch in a FakeDataStore for testing.""" |
537 | + self.fake_data_store = FakeDataStore('fake_ticket_id') |
538 | + self.addCleanup(self.fake_data_store.delete) |
539 | + with patch.object( |
540 | + AMQPWorker, |
541 | + '_create_data_store', |
542 | + return_value=self.fake_data_store) as create_data_store_patch: |
543 | + yield create_data_store_patch |
544 | + |
545 | + def put_fake_coverage_xml( |
546 | + self, |
547 | + data_store, |
548 | + coverage_xml=vanilla_coverage_xml): |
549 | + """Put a build log into our fake data store. |
550 | + |
551 | + :param data_store: FakeDataStore into which to put |
552 | + :param coverage_xml: str defaults to vanilla_coverage_xml |
553 | + """ |
554 | + data_store.put_file( |
555 | + 'coverage.xml', |
556 | + coverage_xml, |
557 | + content_type='text/plain' |
558 | + ) |
559 | |
560 | === added file 'coverage-extractor/coverageextractor/tests/test_extract_coverage_data.py' |
561 | --- coverage-extractor/coverageextractor/tests/test_extract_coverage_data.py 1970-01-01 00:00:00 +0000 |
562 | +++ coverage-extractor/coverageextractor/tests/test_extract_coverage_data.py 2014-10-23 21:46:26 +0000 |
563 | @@ -0,0 +1,162 @@ |
564 | +# Ubuntu CI Engine |
565 | +# Copyright 2014 Canonical Ltd. |
566 | +# |
567 | +# This program is free software: you can redistribute it and/or modify it |
568 | +# under the terms of the GNU Affero General Public License version 3, as |
569 | +# published by the Free Software Foundation. |
570 | +# |
571 | +# This program is distributed in the hope that it will be useful, but |
572 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
573 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
574 | +# PURPOSE. See the GNU Affero General Public License for more details. |
575 | +# |
576 | +# You should have received a copy of the GNU Affero General Public License |
577 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
578 | + |
579 | +import unittest |
580 | + |
581 | +from coverageextractor import ( |
582 | + CoverageExtractorException, |
583 | + extract_coverage_data, |
584 | +) |
585 | +from coverageextractor.tests import vanilla_coverage_xml |
586 | + |
587 | + |
588 | +# here we refuse to close our <sources> tag :| |
589 | +invalid_xml_coverage_xml = u"""<?xml version="1.0" ?> |
590 | +<!DOCTYPE coverage |
591 | + SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-03.dtd'> |
592 | +<coverage branch-rate="1.0" line-rate="1.0" timestamp="1410387089" version="gcovr 2.4 (r2774)"> |
593 | + <sources> |
594 | + <source>/tmp/tmpz8yrh4tz/dummy</source> |
595 | + <packages> |
596 | + <package branch-rate="1.0" complexity="0.0" line-rate="1.0" name=""> |
597 | + <classes> |
598 | + <class branch-rate="1.0" complexity="0.0" filename="fibonacci.cpp" line-rate="1.0" name="fibonacci_cpp"> |
599 | + <lines> |
600 | + <line branch="false" hits="10" number="4"/> |
601 | + <line branch="true" condition-coverage="100% (2/2)" hits="10" number="6"> |
602 | + <conditions> |
603 | + <condition coverage="100%" number="0" type="jump"/> |
604 | + </conditions> |
605 | + </line> |
606 | + <line branch="false" hits="3" number="7"/> |
607 | + <line branch="true" condition-coverage="100% (2/2)" hits="7" number="8"> |
608 | + <conditions> |
609 | + <condition coverage="100%" number="0" type="jump"/> |
610 | + </conditions> |
611 | + </line> |
612 | + <line branch="false" hits="4" number="9"/> |
613 | + <line branch="false" hits="3" number="10"/> |
614 | + </lines> |
615 | + </class> |
616 | + </classes> |
617 | + </package> |
618 | + </packages> |
619 | +</coverage> |
620 | +""" # NOQA |
621 | + |
622 | +# NOTE a *baseball* dingbat in sources filepath |
623 | +unicode_coverage_xml = unicode("""<?xml version="1.0" ?> |
624 | +<!DOCTYPE coverage |
625 | + SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-03.dtd'> |
626 | +<coverage branch-rate="1.0" line-rate="1.0" timestamp="1410387089" version="gcovr 2.4 (r2774)"> |
627 | + <sources> |
628 | + <source>/tmp/\u26BE/dummy</source> |
629 | + </sources> |
630 | + <packages> |
631 | + <package branch-rate="1.0" complexity="0.0" line-rate="1.0" name=""> |
632 | + <classes> |
633 | + <class branch-rate="1.0" complexity="0.0" filename="fibonacci.cpp" line-rate="1.0" name="fibonacci_cpp"> |
634 | + <lines> |
635 | + <line branch="false" hits="10" number="4"/> |
636 | + <line branch="true" condition-coverage="100% (2/2)" hits="10" number="6"> |
637 | + <conditions> |
638 | + <condition coverage="100%" number="0" type="jump"/> |
639 | + </conditions> |
640 | + </line> |
641 | + <line branch="false" hits="3" number="7"/> |
642 | + <line branch="true" condition-coverage="100% (2/2)" hits="7" number="8"> |
643 | + <conditions> |
644 | + <condition coverage="100%" number="0" type="jump"/> |
645 | + </conditions> |
646 | + </line> |
647 | + <line branch="false" hits="4" number="9"/> |
648 | + <line branch="false" hits="3" number="10"/> |
649 | + </lines> |
650 | + </class> |
651 | + </classes> |
652 | + </package> |
653 | + </packages> |
654 | +</coverage> |
655 | +""") # NOQA |
656 | + |
657 | +# we'll almost certainly never see garbled coverage data but hey |
658 | +garbled_numbers_coverage_xml = u"""<?xml version="1.0" ?> |
659 | +<!DOCTYPE coverage |
660 | + SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-03.dtd'> |
661 | +<coverage branch-rate="garblegarble" line-rate="1.0" timestamp="1410387089" version="gcovr 2.4 (r2774)"> |
662 | + <sources> |
663 | + <source>/tmp/tmpz8yrh4tz/dummy</source> |
664 | + </sources> |
665 | + <packages> |
666 | + <package branch-rate="1.0" complexity="0.0" line-rate="1.0" name=""> |
667 | + <classes> |
668 | + <class branch-rate="1.0" complexity="0.0" filename="fibonacci.cpp" line-rate="1.0" name="fibonacci_cpp"> |
669 | + <lines> |
670 | + <line branch="false" hits="10" number="4"/> |
671 | + <line branch="true" condition-coverage="100% (2/2)" hits="10" number="6"> |
672 | + <conditions> |
673 | + <condition coverage="100%" number="0" type="jump"/> |
674 | + </conditions> |
675 | + </line> |
676 | + <line branch="false" hits="3" number="7"/> |
677 | + <line branch="true" condition-coverage="100% (2/2)" hits="7" number="8"> |
678 | + <conditions> |
679 | + <condition coverage="100%" number="0" type="jump"/> |
680 | + </conditions> |
681 | + </line> |
682 | + <line branch="false" hits="4" number="9"/> |
683 | + <line branch="false" hits="3" number="10"/> |
684 | + </lines> |
685 | + </class> |
686 | + </classes> |
687 | + </package> |
688 | + </packages> |
689 | +</coverage> |
690 | +""" # NOQA |
691 | + |
692 | + |
693 | +class ExtractCoverageDataTestCase(unittest.TestCase): |
694 | + |
695 | + def test_extract_coverage_data(self): |
696 | + """Extract coverage data from a vanilla build log bytestring.""" |
697 | + line_coverage, branch_coverage = extract_coverage_data( |
698 | + vanilla_coverage_xml |
699 | + ) |
700 | + self.assertAlmostEqual(1.0, line_coverage) |
701 | + self.assertAlmostEqual(1.0, branch_coverage) |
702 | + |
703 | + def test_invalid_xml_raises(self): |
704 | + """We raise an exception if we find invalid XML.""" |
705 | + self.assertRaises( |
706 | + CoverageExtractorException, |
707 | + extract_coverage_data, |
708 | + invalid_xml_coverage_xml |
709 | + ) |
710 | + |
711 | + def test_garbled_numbers_raise(self): |
712 | + """We raise an exception if coverage numbers don't float.""" |
713 | + self.assertRaises( |
714 | + CoverageExtractorException, |
715 | + extract_coverage_data, |
716 | + garbled_numbers_coverage_xml |
717 | + ) |
718 | + |
719 | + def test_unicode_parses(self): |
720 | + """Extract coverage data from a build log with unicode characters.""" |
721 | + line_coverage, branch_coverage = extract_coverage_data( |
722 | + unicode_coverage_xml |
723 | + ) |
724 | + self.assertAlmostEqual(1.0, line_coverage) |
725 | + self.assertAlmostEqual(1.0, branch_coverage) |
726 | |
727 | === added file 'coverage-extractor/coverageextractor/tests/test_extraction.py' |
728 | --- coverage-extractor/coverageextractor/tests/test_extraction.py 1970-01-01 00:00:00 +0000 |
729 | +++ coverage-extractor/coverageextractor/tests/test_extraction.py 2014-10-23 21:46:26 +0000 |
730 | @@ -0,0 +1,233 @@ |
731 | +#!/usr/bin/env python |
732 | +# Ubuntu CI Engine |
733 | +# Copyright 2014 Canonical Ltd. |
734 | +# |
735 | +# This program is free software: you can redistribute it and/or modify it |
736 | +# under the terms of the GNU Affero General Public License version 3, as |
737 | +# published by the Free Software Foundation. |
738 | +# |
739 | +# This program is distributed in the hope that it will be useful, but |
740 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
741 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
742 | +# PURPOSE. See the GNU Affero General Public License for more details. |
743 | +# |
744 | +# You should have received a copy of the GNU Affero General Public License |
745 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
746 | + |
747 | +import logging |
748 | + |
749 | +from mock import patch, Mock |
750 | + |
751 | +from ci_utils.data_store import DataStoreException |
752 | +from coverageextractor import CoverageExtractorException |
753 | +from coverageextractor.nfss_client import NfssClient, NfssClientError |
754 | +from coverageextractor.tests import ( |
755 | + CoverageExtractorTestCase, |
756 | + vanilla_coverage_xml, |
757 | +) |
758 | +from coverageextractor.extraction import ( |
759 | + Extraction, |
760 | + ExtractionFailed, |
761 | + JSON_ENCODING_FAILURE_TEMPLATE, |
762 | + NFSS_CLIENT_ERROR_TEMPLATE, |
763 | + PARSE_FAILURE_TEMPLATE, |
764 | + SUCCESS_TEMPLATE, |
765 | + SWIFT_GET_FAILURE_TEMPLATE, |
766 | +) |
767 | + |
768 | + |
769 | +class ExtractionTestCase(CoverageExtractorTestCase): |
770 | + |
771 | + def test_try_get_project_and_version(self): |
772 | + """Vanilla get project and version from subticket.""" |
773 | + params = { |
774 | + 'subtickets': [ |
775 | + { |
776 | + 'name': 'libfibonacci', |
777 | + 'version': '0.1-0ubuntu1', |
778 | + }, |
779 | + ] |
780 | + } |
781 | + ext = Extraction('fake-ticket-id', 'fake-logger', 'fake-data-store') |
782 | + self.assertEqual( |
783 | + ('libfibonacci', '0.1-0ubuntu1'), |
784 | + ext.try_get_project_and_version(params) |
785 | + ) |
786 | + |
787 | + def test_try_get_project_and_version_raises(self): |
788 | + """Failure to find project and version raises.""" |
789 | + |
790 | + def test_try_get_coverage_xml_path(self): |
791 | + """We get the coverage.xml path from params.""" |
792 | + with self.patch_create_data_store() as fake_data_store: |
793 | + params = {'coverage_xml_path': 'coverage.xml'} |
794 | + ext = Extraction('fake-ticket-id', 'fake-logger', fake_data_store) |
795 | + self.assertEqual( |
796 | + 'coverage.xml', |
797 | + ext.try_get_coverage_xml_path(params), |
798 | + ) |
799 | + |
800 | + @patch.object(Extraction, 'report_failed') |
801 | + def test_extract_coverage_xml_path_failure(self, report_failed): |
802 | + """Failure to get the coverage.xml path from params fails.""" |
803 | + with self.patch_create_data_store() as fake_data_store: |
804 | + malevolent_params = {'gibberish': 'coverage.xml'} |
805 | + ext = Extraction('fake-ticket-id', 'fake-logger', fake_data_store) |
806 | + ext.try_get_coverage_xml_path(malevolent_params) |
807 | + report_failed.assert_called() |
808 | + |
809 | + def test_try_get_coverage_xml(self): |
810 | + """Retrieve our coverage.xml from swift.""" |
811 | + with self.patch_create_data_store() as fake_data_store: |
812 | + fake_data_store.get_file.return_value = vanilla_coverage_xml |
813 | + ext = Extraction('fake-ticket-id', 'fake-logger', fake_data_store) |
814 | + self.assertEqual( |
815 | + vanilla_coverage_xml, |
816 | + ext.try_get_coverage_xml('coverage.xml'), |
817 | + ) |
818 | + |
819 | + def test_try_get_coverage_xml_retries(self): |
820 | + """Retry if swift fails to yield our coverage.xml.""" |
821 | + fake_logger = Mock(spec_set=logging.Logger) |
822 | + with self.patch_create_data_store() as fake_data_store: |
823 | + ext = Extraction('fake-ticket-id', fake_logger, fake_data_store) |
824 | + fake_data_store.get_file.side_effect = [ |
825 | + DataStoreException("No log!"), |
826 | + DataStoreException("Still no log!"), |
827 | + vanilla_coverage_xml |
828 | + ] |
829 | + with patch('coverageextractor.time.sleep'): # for the impatient |
830 | + self.assertEqual( |
831 | + vanilla_coverage_xml, |
832 | + ext.try_get_coverage_xml('coverage.xml') |
833 | + ) |
834 | + self.assertEqual(3, fake_data_store.get_file.call_count) |
835 | + fake_logger.warning.assert_called() |
836 | + |
837 | + @patch.object(Extraction, 'report_failed') |
838 | + def test_try_get_coverage_xml_raises(self, report_failed): |
839 | + """Return failure if swift fails to yield our coverage.xml.""" |
840 | + fake_logger = Mock(spec_set=logging.Logger) |
841 | + with self.patch_create_data_store() as fake_data_store: |
842 | + ext = Extraction('fake-ticket-id', fake_logger, fake_data_store) |
843 | + fake_data_store.get_file.side_effect = DataStoreException( |
844 | + 'Drool...' |
845 | + ) |
846 | + with patch('coverageextractor.time.sleep'): # for the impatient |
847 | + ext.try_get_coverage_xml('coverage.xml') |
848 | + report_failed.assert_called_with( |
849 | + SWIFT_GET_FAILURE_TEMPLATE, |
850 | + 'fake-ticket-id', |
851 | + 'Drool...', |
852 | + ) |
853 | + |
854 | + def test_try_extract_coverage_data(self): |
855 | + """Extract coverage data from coverage.xml.""" |
856 | + ext = Extraction('fake-ticket-id', 'fake-logger', 'fake-data-store') |
857 | + line_coverage, branch_coverage = ext.try_extract_coverage_data( |
858 | + vanilla_coverage_xml |
859 | + ) |
860 | + self.assertAlmostEqual(1.0, line_coverage) |
861 | + self.assertAlmostEqual(1.0, branch_coverage) |
862 | + |
863 | + @patch.object(Extraction, 'report_failed') |
864 | + @patch('coverageextractor.extraction.extract_coverage_data') |
865 | + def test_try_extract_coverage_data_raises( |
866 | + self, |
867 | + extract_coverage_data, |
868 | + report_failed): |
869 | + """Report failure when we fail to extract coverage data.""" |
870 | + extract_coverage_data.side_effect = CoverageExtractorException( |
871 | + "No data for you!" |
872 | + ) |
873 | + ext = Extraction('fake-ticket-id', 'fake-logger', 'fake-data-store') |
874 | + ext.try_extract_coverage_data(vanilla_coverage_xml) |
875 | + report_failed.assert_called_with( |
876 | + PARSE_FAILURE_TEMPLATE, |
877 | + 'fake-ticket-id', |
878 | + "No data for you!", |
879 | + ) |
880 | + |
881 | + def test_compose_nfss_json(self): |
882 | + """Dump a string of data to put to NFSS.""" |
883 | + ext = Extraction('fake-ticket-id', 'fake-logger', 'fake-data-store') |
884 | + json = ext.try_compose_nfss_json( |
885 | + 'fake-package-name', |
886 | + '0.1.PBJ.BBQ-0ubuntu1', |
887 | + 1.0, |
888 | + 0.572, |
889 | + ) |
890 | + self.assertEqual( |
891 | + '{"version": "0.1.PBJ.BBQ-0ubuntu1", "name": "fake-package-name", "line_coverage": 1.0, "branch_coverage": 0.572}', # NOQA |
892 | + json |
893 | + ) |
894 | + |
895 | + @patch.object(Extraction, 'report_failed') |
896 | + @patch('coverageextractor.extraction.json.dumps') |
897 | + def test_compose_nfss_json_raises(self, dumps, report_failed): |
898 | + """Handle the error from un-jsonnable params. |
899 | + |
900 | + This is pretty unlikely. |
901 | + """ |
902 | + dumps.side_effect = ValueError('This is pretty unlikely.') |
903 | + ext = Extraction('fake-ticket-id', 'fake-logger', 'fake-data-store') |
904 | + ext.try_compose_nfss_json( |
905 | + 'fake-package-name', |
906 | + '0.1.PBJ.BBQ-0ubuntu1', |
907 | + 1.0, |
908 | + 0.572, |
909 | + ) |
910 | + report_failed.assert_called_with( |
911 | + JSON_ENCODING_FAILURE_TEMPLATE, |
912 | + 'fake-ticket-id', |
913 | + "This is pretty unlikely.", |
914 | + ) |
915 | + |
916 | + @patch.object(NfssClient, 'post_data') |
917 | + def test_try_post_data_to_nfss(self, post_data): |
918 | + """Post JSON result to NFSS.""" |
919 | + ext = Extraction('fake-ticket-id', 'fake-logger', 'fake-data-store') |
920 | + with patch.object(NfssClient, '__init__', new=Mock(return_value=None)): |
921 | + ext.try_post_data_to_nfss({'foo': 42}, 'libfibonacci', 'coverage') |
922 | + post_data.assert_called_with({'foo': 42}, 'libfibonacci', 'coverage') |
923 | + |
924 | + @patch.object(Extraction, 'report_failed') |
925 | + @patch.object(NfssClient, 'post_data') |
926 | + def test_nfss_doesnt_love_us(self, post_data, report_failed): |
927 | + """Bad post to NFSS reports failure.""" |
928 | + ext = Extraction('fake-ticket-id', 'fake-logger', 'fake-data-store') |
929 | + post_data.side_effect = NfssClientError('NFSS luvs u <3 .') |
930 | + with patch.object(NfssClient, '__init__', new=Mock(return_value=None)): |
931 | + ext.try_post_data_to_nfss({'foo': 42}, 'libfibonacci', 'coverage') |
932 | + report_failed.assert_called_with( |
933 | + NFSS_CLIENT_ERROR_TEMPLATE, |
934 | + 'fake-ticket-id', |
935 | + 'NFSS luvs u <3 .', |
936 | + ) |
937 | + |
938 | + def test_report_failed_raises(self): |
939 | + """Internal catch-all handler raises ExtractionFailed.""" |
940 | + with self.patch_create_data_store() as fake_data_store: |
941 | + fake_logger = Mock(spec_set=logging.Logger) |
942 | + ext = Extraction('fake-ticket-id', fake_logger, fake_data_store) |
943 | + self.assertRaises( |
944 | + ExtractionFailed, |
945 | + lambda: ext.report_failed("%s %s", 'foo', 'bar') |
946 | + ) |
947 | + fake_logger.error.assert_called_with("foo bar") |
948 | + |
949 | + @patch('coverageextractor.extraction.generate_and_put_artifact') |
950 | + def test_report_succeeded(self, generate_and_put_artifact): |
951 | + """Report success to swift and logging.""" |
952 | + generate_and_put_artifact.side_effect = ['coverage.xml', |
953 | + 'success-message'] |
954 | + with self.patch_create_data_store() as fake_data_store: |
955 | + fake_logger = Mock(spec_set=logging.Logger) |
956 | + ext = Extraction('fake-ticket-id', fake_logger, fake_data_store) |
957 | + ext.report_succeeded('fake-ticket-id') |
958 | + fake_logger.info.assert_called_with(SUCCESS_TEMPLATE, 'fake-ticket-id') |
959 | + generate_and_put_artifact.assert_any_call( |
960 | + fake_data_store, |
961 | + message=(SUCCESS_TEMPLATE % 'fake-ticket-id'), |
962 | + ) |
963 | + self.assertEqual(1, generate_and_put_artifact.call_count) |
964 | |
965 | === added file 'coverage-extractor/coverageextractor/tests/test_handle_request.py' |
966 | --- coverage-extractor/coverageextractor/tests/test_handle_request.py 1970-01-01 00:00:00 +0000 |
967 | +++ coverage-extractor/coverageextractor/tests/test_handle_request.py 2014-10-23 21:46:26 +0000 |
968 | @@ -0,0 +1,121 @@ |
969 | +# Ubuntu CI Engine |
970 | +# Copyright 2014 Canonical Ltd. |
971 | +# |
972 | +# This program is free software: you can redistribute it and/or modify it |
973 | +# under the terms of the GNU Affero General Public License version 3, as |
974 | +# published by the Free Software Foundation. |
975 | +# |
976 | +# This program is distributed in the hope that it will be useful, but |
977 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
978 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
979 | +# PURPOSE. See the GNU Affero General Public License for more details. |
980 | +# |
981 | +# You should have received a copy of the GNU Affero General Public License |
982 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
983 | + |
984 | +import logging |
985 | +import os |
986 | + |
987 | +from mock import Mock, patch |
988 | + |
989 | +from ci_utils import amqp_utils |
990 | +from coverageextractor.extraction import Extraction |
991 | +from coverageextractor.tests import CoverageExtractorTestCase |
992 | +from coverageextractor.extraction import ( |
993 | + MISSING_PARAMS_TEMPLATE, |
994 | + SUCCESS_TEMPLATE, |
995 | +) |
996 | +from coverageextractor.run_worker import CoverageExtractorWorker |
997 | + |
998 | + |
999 | +class HandleRequestTestCase(CoverageExtractorTestCase): |
1000 | + |
1001 | + @patch.object(Extraction, 'try_post_data_to_nfss') |
1002 | + def test_vanilla_handle_request(self, try_post_data_to_nfss): |
1003 | + """Large integration test for coverage extraction. |
1004 | + |
1005 | + NOTE that we do patch the NFSS call. |
1006 | + """ |
1007 | + fake_logger = Mock(spec_set=logging.Logger) |
1008 | + with self.patch_create_data_store(): |
1009 | + self.put_fake_coverage_xml(self.fake_data_store) |
1010 | + worker = CoverageExtractorWorker() |
1011 | + fake_params = { |
1012 | + 'coverage_xml_path': 'coverage.xml', |
1013 | + 'ticket_id': 'fake_ticket_id', |
1014 | + 'subtickets': [ |
1015 | + { |
1016 | + 'name': 'libfibonacci', |
1017 | + 'version': '0.1-0ubuntu1', |
1018 | + }, |
1019 | + ], |
1020 | + } |
1021 | + amqp_result, result_data = worker.handle_request( |
1022 | + fake_params, |
1023 | + fake_logger, |
1024 | + ) |
1025 | + self.assertEqual(amqp_utils.progress_completed, amqp_result) |
1026 | + self.assertEqual( |
1027 | + SUCCESS_TEMPLATE % 'fake_ticket_id', |
1028 | + result_data['message'], |
1029 | + ) |
1030 | + fake_logger.info.assert_called_with( |
1031 | + SUCCESS_TEMPLATE, 'fake_ticket_id' |
1032 | + ) |
1033 | + self.assertEqual(1, len(result_data['artifacts'])) |
1034 | + # find a log message as our only artifact |
1035 | + log_artifact = result_data['artifacts'][0] |
1036 | + self.assertEqual( |
1037 | + SUCCESS_TEMPLATE % 'fake_ticket_id', |
1038 | + log_artifact['message'], |
1039 | + ) |
1040 | + self.assertEqual('LOGS', log_artifact['type']) |
1041 | + self.assertEqual( |
1042 | + 'file://' + os.path.join( |
1043 | + os.path.abspath(os.curdir), |
1044 | + 'fake_ticket_id/coverage-extractor.output.log'), |
1045 | + log_artifact['reference'] |
1046 | + ) |
1047 | + |
1048 | + def test_exception_returns_failure(self): |
1049 | + """An exception in our imperatives returns a failure. |
1050 | + |
1051 | + Actuate a KeyError in try_extract_coverage_xml_path, inspect our |
1052 | + results to make sure we're logging, depositing a log in swift. |
1053 | + """ |
1054 | + fake_logger = Mock(spec_set=logging.Logger) |
1055 | + with self.patch_create_data_store() as fake_data_store: |
1056 | + self.put_fake_coverage_xml(fake_data_store) |
1057 | + worker = CoverageExtractorWorker() |
1058 | + # actuate a KeyError in try_extract_coverage_xml_path |
1059 | + malevolent_params = { |
1060 | + 'gibberish': 'coverage.xml', |
1061 | + 'ticket_id': 'fake_ticket_id', |
1062 | + 'subtickets': [ |
1063 | + { |
1064 | + 'name': 'libfibonacci', |
1065 | + 'version': '0.1-0ubuntu1', |
1066 | + }, |
1067 | + ], |
1068 | + } |
1069 | + amqp_result, result_data = worker.handle_request( |
1070 | + malevolent_params, |
1071 | + fake_logger |
1072 | + ) |
1073 | + self.assertEqual(amqp_utils.progress_failed, amqp_result) |
1074 | + self.assertEqual( |
1075 | + MISSING_PARAMS_TEMPLATE % 'fake_ticket_id', |
1076 | + result_data['message'] |
1077 | + ) |
1078 | + self.assertEqual(1, len(result_data['artifacts'])) |
1079 | + artifact = result_data['artifacts'][0] |
1080 | + self.assertEqual( |
1081 | + 'file://' + os.path.join( |
1082 | + os.path.abspath(os.curdir), |
1083 | + 'fake_ticket_id/coverage-extractor.output.log'), |
1084 | + artifact['reference']) |
1085 | + self.assertEqual( |
1086 | + MISSING_PARAMS_TEMPLATE % 'fake_ticket_id', |
1087 | + result_data['message'] |
1088 | + ) |
1089 | + self.assertEqual('LOGS', artifact['type']) |
1090 | |
1091 | === added file 'coverage-extractor/coverageextractor/tests/test_nfss_client.py' |
1092 | --- coverage-extractor/coverageextractor/tests/test_nfss_client.py 1970-01-01 00:00:00 +0000 |
1093 | +++ coverage-extractor/coverageextractor/tests/test_nfss_client.py 2014-10-23 21:46:26 +0000 |
1094 | @@ -0,0 +1,150 @@ |
1095 | +#!/usr/bin/env python |
1096 | +# Ubuntu CI Engine |
1097 | +# Copyright 2014 Canonical Ltd. |
1098 | +# |
1099 | +# This program is free software: you can redistribute it and/or modify it |
1100 | +# under the terms of the GNU Affero General Public License version 3, as |
1101 | +# published by the Free Software Foundation. |
1102 | +# |
1103 | +# This program is distributed in the hope that it will be useful, but |
1104 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
1105 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1106 | +# PURPOSE. See the GNU Affero General Public License for more details. |
1107 | +# |
1108 | +# You should have received a copy of the GNU Affero General Public License |
1109 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1110 | + |
1111 | +import unittest |
1112 | + |
1113 | +from mock import Mock, patch |
1114 | + |
1115 | +from coverageextractor.nfss_client import ( |
1116 | + NFSS_CONFIG_PATH, |
1117 | + NfssClient, |
1118 | + NfssClientError, |
1119 | + parse_nfss_config, |
1120 | +) |
1121 | + |
1122 | + |
1123 | +class ParseNfssConfigTestCase(unittest.TestCase): |
1124 | + |
1125 | + @patch('coverageextractor.nfss_client.ConfigParser') |
1126 | + def test_parse(self, config_parser): |
1127 | + """We can successfully parse an NFSS config.""" |
1128 | + nfss_config_dict = { |
1129 | + 'client_access_key': 'fake-client-acccess-key', |
1130 | + 'resource_owner_key': 'fake-resource-owner-key', |
1131 | + 'resource_owner_secret': 'fake-resource-owner-secret', |
1132 | + 'backend': 'fake-backend', |
1133 | + } |
1134 | + # yes these are weird due to ConfigParser backwardness |
1135 | + config_parser.return_value.read = Mock() |
1136 | + # "dance like no-one is watching" |
1137 | + config_parser.return_value.__getitem__ = Mock( |
1138 | + return_value=nfss_config_dict |
1139 | + ) |
1140 | + self.assertEqual( |
1141 | + nfss_config_dict, |
1142 | + parse_nfss_config('fake-config-path') |
1143 | + ) |
1144 | + |
1145 | + @patch('coverageextractor.nfss_client.ConfigParser') |
1146 | + def test_no_nfss_section_raises(self, config_parser): |
1147 | + """No 'nfss' section in config raises.""" |
1148 | + config_parser.return_value.read = Mock() |
1149 | + config_parser.return_value.__getitem__ = Mock( |
1150 | + side_effect=KeyError('client_access_key') |
1151 | + ) |
1152 | + with self.assertRaises(KeyError): |
1153 | + parse_nfss_config('fake-config-path') |
1154 | + |
1155 | + @patch('coverageextractor.nfss_client.ConfigParser') |
1156 | + def test_missing_key_raises(self, config_parser): |
1157 | + """A missing key in the config raises.""" |
1158 | + nfss_config_dict = {} |
1159 | + config_parser.return_value.read = Mock() |
1160 | + config_parser.return_value.__getitem__ = Mock( |
1161 | + return_value=nfss_config_dict |
1162 | + ) |
1163 | + with self.assertRaises(KeyError): |
1164 | + parse_nfss_config('fake-config-path') |
1165 | + |
1166 | + |
1167 | +class NfssClientTest(unittest.TestCase): |
1168 | + |
1169 | + @patch('coverageextractor.nfss_client.OAuth1Session') |
1170 | + @patch('coverageextractor.nfss_client.parse_nfss_config') |
1171 | + def test_init(self, parse_nfss_config, oauth_session): |
1172 | + """We can create a NfssClient.""" |
1173 | + parse_nfss_config.return_value = { |
1174 | + 'client_access_key': 'fake-client-access-key', |
1175 | + 'resource_owner_key': 'fake-resource-owner-key', |
1176 | + 'resource_owner_secret': 'fake-resource-owner-secret' |
1177 | + } |
1178 | + NfssClient('fake-config-path') |
1179 | + oauth_session.assert_called_with( |
1180 | + 'fake-client-access-key', |
1181 | + resource_owner_key='fake-resource-owner-key', |
1182 | + resource_owner_secret='fake-resource-owner-secret', |
1183 | + ) |
1184 | + |
1185 | + @patch('coverageextractor.nfss_client.OAuth1Session') |
1186 | + @patch('coverageextractor.nfss_client.parse_nfss_config') |
1187 | + def test_no_config_path(self, parse_nfss_config, oauth_session): |
1188 | + """Config path defaults if not passed in as param.""" |
1189 | + parse_nfss_config.return_value = { |
1190 | + 'client_access_key': 'fake-client-access-key', |
1191 | + 'resource_owner_key': 'fake-resource-owner-key', |
1192 | + 'resource_owner_secret': 'fake-resource-owner-secret' |
1193 | + } |
1194 | + nfss_client = NfssClient() |
1195 | + oauth_session.assert_called_with( |
1196 | + 'fake-client-access-key', |
1197 | + resource_owner_key='fake-resource-owner-key', |
1198 | + resource_owner_secret='fake-resource-owner-secret', |
1199 | + ) |
1200 | + self.assertEqual(NFSS_CONFIG_PATH, nfss_client.nfss_config_path) |
1201 | + |
1202 | + @patch('coverageextractor.nfss_client.parse_nfss_config') |
1203 | + def test_init_raises(self, parse_nfss_config): |
1204 | + """We can create a NfssClient.""" |
1205 | + parse_nfss_config.side_effect = KeyError('Mangled config!') |
1206 | + with self.assertRaises(NfssClientError): |
1207 | + NfssClient('fake-config-path') |
1208 | + |
1209 | + def test_post_data(self): |
1210 | + """Vanilla POST to NFSS.""" |
1211 | + with patch.object( |
1212 | + NfssClient, |
1213 | + '__init__', |
1214 | + new=Mock(return_value=None)): |
1215 | + nfss_client = NfssClient() |
1216 | + nfss_client.config = {'backend': 'http://NFSS.io'} |
1217 | + nfss_client.session = Mock( |
1218 | + # successful POST |
1219 | + post=Mock(return_value=Mock(status_code=200)), |
1220 | + ) |
1221 | + nfss_client.post_data( |
1222 | + {'foo': 'bar'}, |
1223 | + 'coverage', |
1224 | + 'libfibonacci', |
1225 | + ) |
1226 | + |
1227 | + def test_bad_post_data_raises(self): |
1228 | + """Bad POST to NFSS raises.""" |
1229 | + with patch.object( |
1230 | + NfssClient, |
1231 | + '__init__', |
1232 | + new=Mock(return_value=None)): |
1233 | + nfss_client = NfssClient() |
1234 | + nfss_client.config = {'backend': 'http://NFSS.io'} |
1235 | + nfss_client.session = Mock( |
1236 | + # successful POST |
1237 | + post=Mock(return_value=Mock(status_code=500)), |
1238 | + ) |
1239 | + with self.assertRaises(NfssClientError): |
1240 | + nfss_client.post_data( |
1241 | + {'foo': 'bar'}, |
1242 | + 'coverage', |
1243 | + 'libfibonacci', |
1244 | + ) |
1245 | |
1246 | === added file 'coverage-extractor/setup.py' |
1247 | --- coverage-extractor/setup.py 1970-01-01 00:00:00 +0000 |
1248 | +++ coverage-extractor/setup.py 2014-10-23 21:46:26 +0000 |
1249 | @@ -0,0 +1,34 @@ |
1250 | +#!/usr/bin/env python |
1251 | +# Ubuntu CI Engine |
1252 | +# Copyright 2014 Canonical Ltd. |
1253 | +# |
1254 | +# This program is free software: you can redistribute it and/or modify it |
1255 | +# under the terms of the GNU Affero General Public License version 3, as |
1256 | +# published by the Free Software Foundation. |
1257 | +# |
1258 | +# This program is distributed in the hope that it will be useful, but |
1259 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
1260 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1261 | +# PURPOSE. See the GNU Affero General Public License for more details. |
1262 | +# |
1263 | +# You should have received a copy of the GNU Affero General Public License |
1264 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1265 | + |
1266 | + |
1267 | +from setuptools import setup, find_packages |
1268 | + |
1269 | +requires = [ |
1270 | + 'amqplib==1.0.0', |
1271 | + 'python-swiftclient>=1.8.0', |
1272 | + 'PyYAML==3.10', |
1273 | + 'requests-oauthlib>=0.4.1', |
1274 | + 'ucitests==0.1.5', |
1275 | +] |
1276 | + |
1277 | +setup( |
1278 | + name='coverageextractor', |
1279 | + version='0.1', |
1280 | + packages=find_packages(), |
1281 | + description='Coverage Retriever', |
1282 | + install_requires=requires, |
1283 | +) |
1284 | |
1285 | === added file 'coverage-extractor/tox.ini' |
1286 | --- coverage-extractor/tox.ini 1970-01-01 00:00:00 +0000 |
1287 | +++ coverage-extractor/tox.ini 2014-10-23 21:46:26 +0000 |
1288 | @@ -0,0 +1,19 @@ |
1289 | +[tox] |
1290 | +envlist = py27, flake8 |
1291 | + |
1292 | +[testenv] |
1293 | +deps = |
1294 | + coverage |
1295 | + mock |
1296 | +commands = |
1297 | + {envpython} setup.py develop |
1298 | + coverage erase |
1299 | + coverage run --omit=.tox/*,*/tests/*,ci_utils/* -m unittest discover -s coverageextractor |
1300 | + coverage report -m |
1301 | + coverage html |
1302 | + |
1303 | +[testenv:flake8] |
1304 | +deps = |
1305 | + flake8 |
1306 | +commands = |
1307 | + flake8 coverageextractor |
1308 | |
1309 | === modified file 'testing/run_tests.py' |
1310 | --- testing/run_tests.py 2014-10-10 10:00:49 +0000 |
1311 | +++ testing/run_tests.py 2014-10-23 21:46:26 +0000 |
1312 | @@ -121,6 +121,7 @@ |
1313 | 'ppa-creator', |
1314 | 'validator', |
1315 | 'coverage-retriever', |
1316 | + 'coverage-extractor', |
1317 | ] |
1318 | loader = loaders.Loader() |
1319 | # setuptools tends to leave eggs all over the place |
FAILED: Continuous integration, rev:858 s-jenkins. ubuntu- ci:8080/ job/uci- engine- ci/1610/
http://
Executed test runs:
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/uci- engine- ci/1610/ rebuild
http://