Merge lp:~allanlesage/uci-engine/coverage-extractor into lp:uci-engine

Proposed by Allan LeSage
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
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.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:858
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1610/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1610/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:858
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1615/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1615/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:859
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1616/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1616/rebuild

review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:860
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1617/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1617/rebuild

review: Approve (continuous-integration)
Revision history for this message
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).

review: Needs Fixing
Revision history for this message
Francis Ginther (fginther) wrote :
Download full text (3.4 KiB)

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.main(sys.argv[1:], sys.stdout, sys.stderr)
  File "/tmp/engine/coverage-extractor/testing/run_tests.py", line 443, in main
    options.exclude_regexps)
  File "/tmp/engine/coverage-extractor/testing/run_tests.py", line 134, in load_regular_component_tests
    component_suite = load_component_tests(loader, c)
  File "/tmp/engine/coverage-extractor/testing/run_tests.py", line 99, in load_component_tests
    suite.addTests(sub_loader.loadTestsFromTree('.'))
  File "/tmp/engine/coverage-extractor/.venv/local/lib/python2.7/site-packages/ucitests-0.1.4-py2.7.egg/ucitests/loaders.py", line 145, in loadTestsFromTree
    suite.addTests(self.loadTestsFromFiles(dir_path, names))
  File "/tmp/engine/coverage-extractor/.venv/local/lib/python2.7/site-packages/ucitests-0.1.4-py2.7.egg/ucitests/loaders.py", line 207, in loadTestsFromFiles
    suite.addTests(self.loadTestsFromTree(rel_path))
  File "/tmp/engine/coverage-extractor/.venv/local/lib/python2.7/site-packages/ucitests-0.1.4-py2.7.egg/ucitests/loaders.py", line 140, in loadTestsFromTree
    suite = self.loadTestsFromPackage(dir_path)
  File "/tmp/engine/coverage-extractor/.venv/local/lib/python2.7/site-packages/ucitests-0.1.4-py2.7.egg/ucitests/loaders.py", line 181, in loadTestsFromPackage
    suite.addTests(self.loadTestsFromFiles(dir_path, file_names))
  File "/tmp/engine/coverage-extractor/.venv/local/lib/python2.7/site-packages/ucitests-0.1.4-py2.7.egg/ucitests/loaders.py", line 207, in loadTestsFromFiles
    suite.addTests(self.loadTestsFromTree(rel_path))
  File "/tmp/engine/coverage-extractor/.venv/local/lib/python2.7/site-packages/ucitests-0.1.4-py2.7.egg/ucitests/loaders.py", line 140, in loadTestsFromTree
    suite = self.loadTestsFromPackage(dir_path)
  File "/tmp/engine/coverage-extractor/.venv/local/lib/python2.7/site-packages/ucitests-0.1.4-py2.7.egg/ucitests/loaders.py", line 181, in loadTestsFromPackage
    suite.addTests(self.loadTestsFromFiles(dir_path, file_names))
  File "/tmp/engine/coverage-extractor/.venv/local/lib/python2.7/site-packages/ucitests-0.1.4-py2.7.egg/ucitests/loaders.py", line 204, in loadTestsFromFiles
    suite.addTests(self.loadTestsFromFile(rel_path))
  File "/tmp/engine/coverage-extractor/.venv/local/lib/python2.7/site-packages/ucitests-0.1.4-py2.7.egg/ucitests/loaders.py", line 219, in loadTestsFromFile
    module = self.importFromPath(path)
  File "/tmp/engine/coverage-extractor/.venv/local/lib/python2.7/site-packages/ucitests-0.1.4-py2.7.egg/ucitests/loaders.py", line 278, in importFromPath
    raise ImportError(msg)
ImportError: Failed to import britney.tests.test_process_requests at ./britney/tests/test_process_requests.py:
Traceback (most recent call last):
  File "/tmp/engine/coverage-extractor/.venv/local/lib/python2.7/site-packages/ucitests-0.1.4-py2.7.egg/ucitests/loaders.py", line 274, in importFromPath
    return importlib.import_module(mod_name)
  File "/usr/lib/python2.7/importlib/__init__.py", line 37, in import_module
    __import__(name)
  File "britney_proxy/britney/...

Read more...

review: Needs Fixing
Revision history for this message
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?

Revision history for this message
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 :) .

Revision history for this message
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.

review: Needs Fixing
Revision history for this message
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.

Revision history for this message
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.

review: Needs Fixing
Revision history for this message
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.

Revision history for this message
Francis Ginther (fginther) wrote :

This looks good, thanks for the update.

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:863
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1631/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1631/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
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.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:864
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1632/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1632/rebuild

review: Approve (continuous-integration)
Revision history for this message
Allan LeSage (allanlesage) wrote :

Top-approved with fginther's looking over my shoulder :) .

Revision history for this message
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/python2.7/dist-packages/virtualenv.py", line 2339, in <module>
    main()
  File "/usr/lib/python2.7/dist-packages/virtualenv.py", line 825, in main
    symlink=options.symlink)
  File "/usr/lib/python2.7/dist-packages/virtualenv.py", line 985, in create_environment
    site_packages=site_packages, clear=clear, symlink=symlink))
  File "/usr/lib/python2.7/dist-packages/virtualenv.py", line 1193, in install_python
    writefile(site_filename_dst, SITE_PY)
  File "/usr/lib/python2.7/dist-packages/virtualenv.py", line 487, in writefile
    f.write(content.encode('utf-8'))
IOError: [Errno 28] No space left on device
INFO:root:virtualenv created in 0.10s.
Traceback (most recent call last):
  File "bin/called-by-tarmac.py", line 140, in <module>
    sys.exit(main())
  File "bin/called-by-tarmac.py", line 110, in main
    venv.install()
  File "/tmp/tarmac/branch.NK5zgD/bin/../testing/venv.py", line 139, in install
    path = create()
  File "/tmp/tarmac/branch.NK5zgD/bin/../testing/venv.py", line 53, in create
    subprocess.check_call(cmd, stdout=devnull)
  File "/usr/lib/python2.7/subprocess.py", line 540, in check_call
    raise CalledProcessError(retcode, cmd)
subprocess.CalledProcessError: Command '['virtualenv', '/dev/shm/venv-xZxs_5', '-p', 'python2.7']' returned non-zero exit status 1

Revision history for this message
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches