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
=== added directory 'coverage-extractor'
=== added directory 'coverage-extractor/coverageextractor'
=== added file 'coverage-extractor/coverageextractor/__init__.py'
--- coverage-extractor/coverageextractor/__init__.py 1970-01-01 00:00:00 +0000
+++ coverage-extractor/coverageextractor/__init__.py 2014-10-23 21:46:26 +0000
@@ -0,0 +1,75 @@
1# Ubuntu CI Engine
2# Copyright 2014 Canonical Ltd.
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU Affero General Public License version 3, as
6# published by the Free Software Foundation.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranties of
10# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11# PURPOSE. See the GNU Affero General Public License for more details.
12#
13# You should have received a copy of the GNU Affero General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16from functools import wraps
17import time
18import xml.etree.ElementTree as etree
19
20
21class CoverageExtractorException(ValueError):
22 """Bad things happen to good extractors."""
23
24
25def extract_coverage_data(coverage_xml):
26 """Return line and branch numbers from coverage.xml.
27
28 :param coverage_xml: coverage.xml string
29 """
30 try:
31 # root node is coverage, more complex patterns possible later
32 coverage_node = etree.fromstring(coverage_xml)
33 return (
34 float(coverage_node.attrib['line-rate']),
35 float(coverage_node.attrib['branch-rate']),
36 )
37 except etree.ParseError as e:
38 raise CoverageExtractorException(
39 "Failed to parse coverage.xml: {}".format(e.message)
40 )
41 except ValueError as e:
42 raise CoverageExtractorException(
43 "Failed to parse coverage numbers: {}".format(e.message)
44 )
45
46
47def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None):
48 """Retry calling the decorated function using an exponential backoff.
49
50 http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/
51 Original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry
52
53 :param ExceptionToCheck: the exception to check. may be a tuple of
54 exceptions to check
55 :param tries: number of times to try (not retry) before giving up
56 :param delay: initial delay between retries in seconds
57 :param backoff: backoff multiplier e.g. value of 2 will double the delay
58 each retry
59 :param logger: logger.Logger to use.
60 """
61 def deco_retry(f):
62 @wraps(f)
63 def f_retry(*args, **kwargs):
64 for i in range(tries):
65 try:
66 return f(*args, **kwargs)
67 except ExceptionToCheck as e:
68 d = delay * backoff ** i
69 if logger:
70 logger.warning(
71 "%s, retrying in %d seconds. . . ." % (e, d))
72 time.sleep(d)
73 return f(*args, **kwargs)
74 return f_retry # true decorator
75 return deco_retry
076
=== added file 'coverage-extractor/coverageextractor/extraction.py'
--- coverage-extractor/coverageextractor/extraction.py 1970-01-01 00:00:00 +0000
+++ coverage-extractor/coverageextractor/extraction.py 2014-10-23 21:46:26 +0000
@@ -0,0 +1,226 @@
1#!/usr/bin/env python
2# Ubuntu CI Engine
3# Copyright 2014 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Affero General Public License version 3, as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17import json
18
19from ci_utils import amqp_utils
20from ci_utils.data_store import DataStoreException
21from coverageextractor import (
22 CoverageExtractorException,
23 extract_coverage_data,
24 retry,
25)
26
27from nfss_client import (
28 NfssClient,
29 NfssClientError,
30 NFSS_CONFIG_PATH,
31)
32
33SUCCESS_TEMPLATE = u'Deposited coverage.xml into swift for ticket %s.'
34MISSING_PARAMS_TEMPLATE = (u'Failed to handle request: missing coverage.xml '
35 'filename for ticket %s')
36PARSE_FAILURE_TEMPLATE = u'Failed to parse coverage.xml for ticket %s: %s'
37SWIFT_GET_FAILURE_TEMPLATE = (u'Failed to retrieve coverage.xml from swift '
38 'for ticket %s: %s')
39SWIFT_PUT_FAILURE_TEMPLATE = (u'Failed to put coverage.xml into swift for '
40 'ticket %s: %s')
41JSON_ENCODING_FAILURE_TEMPLATE = (u'Failed to encode JSON for '
42 'ticket %s: %s')
43NFSS_CLIENT_ERROR_TEMPLATE = u'Failed to POST to NFSS for ticket %s: %s'
44
45
46def generate_and_put_artifact(
47 data_store,
48 name='coverage-extractor.output.log',
49 type='LOGS',
50 message=None,
51 reference=None):
52 """Return a uci-artifact generated and deposited in swift.
53
54 NOTE that if no reference is supplied, the artifact is deposited
55 into swift and the resulting link is reported in the returned
56 artifact; provided for convenience in logging.
57
58 :param data_store: DataStore into which to put
59 :param name: filename of artifact in swift
60 :param type: 'LOGS' for logs or 'RESULTS' for coverage.xml
61 :param message: a log message
62 :param reference: swift link; supplied upon deposit if not specified
63 """
64 artifact = {
65 'name': name,
66 'type': type,
67 }
68 if message is not None:
69 artifact['message'] = message
70 if reference is None:
71 reference = data_store.put_file(name, str(artifact), 'text/plain')
72 artifact['reference'] = reference
73 return artifact
74
75
76class ExtractionFailed(Exception):
77 """Return a dict upon operation failure.."""
78
79 def __init__(self, return_tuple):
80 self.return_tuple = return_tuple
81
82
83class Extraction(object):
84
85 def __init__(self, ticket_id, logger, data_store, nfss_config_path=None):
86 self.ticket_id = ticket_id
87 self.logger = logger
88 self.data_store = data_store
89 # XXX alesage 20141022
90 # Possibly we want to own the NFSS client here instead?
91 self.nfss_config_path = nfss_config_path or NFSS_CONFIG_PATH
92 self.result = dict(artifacts=[])
93
94 def try_get_project_and_version(self, params):
95 """Extract project name and version from params.
96
97 :param params: ticket system handle_request calling params
98 """
99 # XXX alesage 20141022
100 # for the moment we treat the one-subticket case; a ticket may
101 # report multiple builds corresponding to architectures, e.g.
102 subticket = params['subtickets'][0]
103 return subticket['name'], subticket['version']
104
105 def try_get_coverage_xml_path(self, params):
106 """Extract coverage.xml path from dict of params.
107
108 :param params: dict, hopefully with 'coverage_xml_path' as key
109 """
110 # XXX alesage 20141022
111 # Consider the case of multiple coverage.xml?
112 try:
113 return params['coverage_xml_path']
114 except KeyError:
115 self.report_failed(
116 MISSING_PARAMS_TEMPLATE,
117 self.ticket_id
118 )
119
120 def try_get_coverage_xml(self, coverage_xml_path):
121 """Get coverage.xml at given path from swift.
122
123 :param coverage_xml_path: swift path from which to get
124 """
125 try:
126 @retry(DataStoreException, logger=self.logger)
127 def get_build_log_actual():
128 return self.data_store.get_file(coverage_xml_path)
129 return get_build_log_actual()
130 except DataStoreException as e:
131 self.report_failed(
132 SWIFT_GET_FAILURE_TEMPLATE,
133 self.ticket_id,
134 e.message,
135 )
136
137 def try_extract_coverage_data(self, coverage_xml):
138 """Get line and branch percentages from coverage.xml.
139
140 :param coverage_xml: coverage_xml string
141 """
142 try:
143 return extract_coverage_data(coverage_xml)
144 except CoverageExtractorException as e:
145 self.report_failed(
146 PARSE_FAILURE_TEMPLATE,
147 self.ticket_id,
148 e.message,
149 )
150
151 def try_compose_nfss_json(
152 self,
153 name,
154 version,
155 line_coverage,
156 branch_coverage):
157 """Compose a JSON string of data for submission to NFSS.
158
159 :param name: package name
160 :param version: package version string
161 :param line_coverage: line coverage (float)
162 :param branch_coverage: branch coverage (float)
163 """
164 try:
165 return json.dumps({
166 'name': name,
167 'version': version,
168 'line_coverage': line_coverage,
169 'branch_coverage': branch_coverage,
170 })
171 except ValueError as e:
172 # This is pretty unlikely.
173 self.report_failed(
174 JSON_ENCODING_FAILURE_TEMPLATE,
175 self.ticket_id,
176 e.message
177 )
178
179 def try_post_data_to_nfss(self, json, test_name, project_name='coverage'):
180 """Post a JSON string of coverage data to NFSS.
181
182 :param json: coverage data JSON to post to NFSS
183 """
184 try:
185 nfss_client = NfssClient(self.nfss_config_path)
186 nfss_client.post_data(json, test_name, project_name)
187 except NfssClientError as e:
188 self.report_failed(
189 NFSS_CLIENT_ERROR_TEMPLATE,
190 self.ticket_id,
191 e.message
192 )
193
194 def report_failed(self, format_str, *str_args):
195 """Convenience function to raise and log a failiure.
196
197 :param format_str: template to log
198 :str_args str_args: items with which to format format_str
199 """
200 message = format_str % str_args
201 self.logger.error(message)
202 self.result['message'] = message
203 self.result['artifacts'].append(
204 generate_and_put_artifact(self.data_store, message=message)
205 )
206 raise ExtractionFailed(
207 (amqp_utils.progress_failed, self.result)
208 )
209
210 def report_succeeded(self, ticket_id):
211 """Deposit a result artifact and return a log artifact.
212
213 :param ticket_id: ticket for which to report retrieval success
214 """
215 message = SUCCESS_TEMPLATE % ticket_id
216 # NOTE that logging compels us to pass params to info (can't
217 # reuse message declared above)
218 self.logger.info(SUCCESS_TEMPLATE, ticket_id)
219 self.result['artifacts'].append(
220 generate_and_put_artifact(
221 self.data_store,
222 message=message
223 )
224 )
225 self.result['message'] = message
226 return amqp_utils.progress_completed, self.result
0227
=== added file 'coverage-extractor/coverageextractor/nfss_client.py'
--- coverage-extractor/coverageextractor/nfss_client.py 1970-01-01 00:00:00 +0000
+++ coverage-extractor/coverageextractor/nfss_client.py 2014-10-23 21:46:26 +0000
@@ -0,0 +1,80 @@
1#!/usr/bin/env python
2# Ubuntu CI Engine
3# Copyright 2014 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Affero General Public License version 3, as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17from ConfigParser import ConfigParser
18
19from requests_oauthlib import OAuth1Session
20
21# XXX alesage 20141022
22# thomi, where to find this config?
23NFSS_CONFIG_PATH = "TODO-fake-nfss-config-path"
24
25
26class NfssClientError(Exception):
27 """Non-functional non-functional stats service."""
28
29
30def parse_nfss_config(nfss_config_path):
31 """Return a ConfigParser config from NFSS config filepath.
32
33 NOTE that we only return the 'nfss' section of the config.
34
35 :param nfss_config_path: path to NFSS config
36 """
37 # XXX alesage 20141022
38 # Discussed with thomi and fginther, need to sort the details of
39 # the config deployment, defer for the moment; an Asana task has
40 # been created to discuss in D.C.
41 config = ConfigParser()
42 config.read(nfss_config_path)
43 # are all of our ducks in a row?
44 for config_item in (
45 'client_access_key',
46 'resource_owner_key',
47 'resource_owner_secret',
48 'backend'):
49 # (we'll also raise KeyError on missing 'nfss' section)
50 if config_item not in config['nfss']:
51 raise KeyError(config_item)
52 return config['nfss']
53
54
55class NfssClient(object):
56
57 def __init__(self, nfss_config_path=None):
58 if nfss_config_path is None:
59 self.nfss_config_path = NFSS_CONFIG_PATH
60 try:
61 # XXX alesage 20141022
62 # Discuss how to deploy this config properly with thomi
63 # and fginther, following from
64 # https://code.launchpad.net/~allanlesage/uci-engine/coverage-extractor/+merge/238782. # NOQA
65 self.config = parse_nfss_config(nfss_config_path)
66 except KeyError as e:
67 raise NfssClientError('Failed to parse NFSS config: '
68 '%s' % e.message)
69 self.session = OAuth1Session(
70 self.config['client_access_key'],
71 resource_owner_key=self.config['resource_owner_key'],
72 resource_owner_secret=self.config['resource_owner_secret'],
73 )
74
75 def post_data(self, json, project_name, test_name):
76 """POST JSON result data to NFSS at a given project and test."""
77 url = '/'.join((self.config['backend'], project_name, test_name))
78 r = self.session.post(url, dict(data=json))
79 if r.status_code != 200:
80 raise NfssClientError("Failed to POST to NFSS: %s" % r.content)
081
=== added file 'coverage-extractor/coverageextractor/run_worker.py'
--- coverage-extractor/coverageextractor/run_worker.py 1970-01-01 00:00:00 +0000
+++ coverage-extractor/coverageextractor/run_worker.py 2014-10-23 21:46:26 +0000
@@ -0,0 +1,64 @@
1#!/usr/bin/env python
2# Ubuntu CI Engine
3# Copyright 2014 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Affero General Public License version 3, as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17from ci_utils import amqp_utils, amqp_worker
18from coverageextractor.extraction import Extraction, ExtractionFailed
19
20
21class CoverageExtractorWorker(amqp_worker.AMQPWorker):
22
23 def __init__(self):
24 super(CoverageExtractorWorker, self).__init__('coverageextractor')
25
26 def handle_request(self, params, logger):
27 """Extract coverage numbers from coverage.xml and post to NFSS.
28
29 :param params: dictionary of params from incoming message
30 :param logger: logging.Logger for reporting
31 """
32 # XXX alesage 20141022
33 # Consider multi-coverage.xml builds, e.g. tickets reporting
34 # builds of a package for more than one architecture. These
35 # could each initiate an Extraction, e.g.? Also beware naming
36 # overlap for results.
37 ticket_id = params['ticket_id']
38 data_store = self._create_data_store(ticket_id)
39 extraction = Extraction(
40 ticket_id,
41 logger,
42 data_store,
43 )
44 try:
45 project, version = extraction.try_get_project_and_version(params)
46 coverage_xml_path = extraction.try_get_coverage_xml_path(params)
47 coverage_xml = extraction.try_get_coverage_xml(coverage_xml_path)
48 line_data, branch_data = extraction.try_extract_coverage_data(
49 coverage_xml
50 )
51 data = extraction.try_compose_nfss_json(
52 project,
53 version,
54 line_data,
55 branch_data,
56 )
57 extraction.try_post_data_to_nfss(data, project)
58 except ExtractionFailed as e:
59 return e.return_tuple
60 return extraction.report_succeeded(ticket_id)
61
62
63if __name__ == '__main__':
64 CoverageExtractorWorker().main(amqp_utils.COVERAGE_RETRIEVER_QUEUE)
065
=== added directory 'coverage-extractor/coverageextractor/tests'
=== added file 'coverage-extractor/coverageextractor/tests/__init__.py'
--- coverage-extractor/coverageextractor/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ coverage-extractor/coverageextractor/tests/__init__.py 2014-10-23 21:46:26 +0000
@@ -0,0 +1,86 @@
1# Ubuntu CI Engine
2# Copyright 2014 Canonical Ltd.
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU Affero General Public License version 3, as
6# published by the Free Software Foundation.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranties of
10# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11# PURPOSE. See the GNU Affero General Public License for more details.
12#
13# You should have received a copy of the GNU Affero General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16from contextlib import contextmanager
17import unittest
18
19from mock import patch
20
21from ci_utils.amqp_worker import AMQPWorker
22from ci_utils.testing.fixtures import FakeDataStore
23
24vanilla_coverage_xml = u"""<?xml version="1.0" ?>
25<!DOCTYPE coverage
26 SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-03.dtd'>
27<coverage branch-rate="1.0" line-rate="1.0" timestamp="1410387089" version="gcovr 2.4 (r2774)">
28 <sources>
29 <source>/tmp/tmpz8yrh4tz/dummy</source>
30 </sources>
31 <packages>
32 <package branch-rate="1.0" complexity="0.0" line-rate="1.0" name="">
33 <classes>
34 <class branch-rate="1.0" complexity="0.0" filename="fibonacci.cpp" line-rate="1.0" name="fibonacci_cpp">
35 <lines>
36 <line branch="false" hits="10" number="4"/>
37 <line branch="true" condition-coverage="100% (2/2)" hits="10" number="6">
38 <conditions>
39 <condition coverage="100%" number="0" type="jump"/>
40 </conditions>
41 </line>
42 <line branch="false" hits="3" number="7"/>
43 <line branch="true" condition-coverage="100% (2/2)" hits="7" number="8">
44 <conditions>
45 <condition coverage="100%" number="0" type="jump"/>
46 </conditions>
47 </line>
48 <line branch="false" hits="4" number="9"/>
49 <line branch="false" hits="3" number="10"/>
50 </lines>
51 </class>
52 </classes>
53 </package>
54 </packages>
55</coverage>
56
57""" # NOQA
58
59
60class CoverageExtractorTestCase(unittest.TestCase):
61
62 @contextmanager
63 def patch_create_data_store(self):
64 """Patch in a FakeDataStore for testing."""
65 self.fake_data_store = FakeDataStore('fake_ticket_id')
66 self.addCleanup(self.fake_data_store.delete)
67 with patch.object(
68 AMQPWorker,
69 '_create_data_store',
70 return_value=self.fake_data_store) as create_data_store_patch:
71 yield create_data_store_patch
72
73 def put_fake_coverage_xml(
74 self,
75 data_store,
76 coverage_xml=vanilla_coverage_xml):
77 """Put a build log into our fake data store.
78
79 :param data_store: FakeDataStore into which to put
80 :param coverage_xml: str defaults to vanilla_coverage_xml
81 """
82 data_store.put_file(
83 'coverage.xml',
84 coverage_xml,
85 content_type='text/plain'
86 )
087
=== added file 'coverage-extractor/coverageextractor/tests/test_extract_coverage_data.py'
--- coverage-extractor/coverageextractor/tests/test_extract_coverage_data.py 1970-01-01 00:00:00 +0000
+++ coverage-extractor/coverageextractor/tests/test_extract_coverage_data.py 2014-10-23 21:46:26 +0000
@@ -0,0 +1,162 @@
1# Ubuntu CI Engine
2# Copyright 2014 Canonical Ltd.
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU Affero General Public License version 3, as
6# published by the Free Software Foundation.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranties of
10# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11# PURPOSE. See the GNU Affero General Public License for more details.
12#
13# You should have received a copy of the GNU Affero General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16import unittest
17
18from coverageextractor import (
19 CoverageExtractorException,
20 extract_coverage_data,
21)
22from coverageextractor.tests import vanilla_coverage_xml
23
24
25# here we refuse to close our <sources> tag :|
26invalid_xml_coverage_xml = u"""<?xml version="1.0" ?>
27<!DOCTYPE coverage
28 SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-03.dtd'>
29<coverage branch-rate="1.0" line-rate="1.0" timestamp="1410387089" version="gcovr 2.4 (r2774)">
30 <sources>
31 <source>/tmp/tmpz8yrh4tz/dummy</source>
32 <packages>
33 <package branch-rate="1.0" complexity="0.0" line-rate="1.0" name="">
34 <classes>
35 <class branch-rate="1.0" complexity="0.0" filename="fibonacci.cpp" line-rate="1.0" name="fibonacci_cpp">
36 <lines>
37 <line branch="false" hits="10" number="4"/>
38 <line branch="true" condition-coverage="100% (2/2)" hits="10" number="6">
39 <conditions>
40 <condition coverage="100%" number="0" type="jump"/>
41 </conditions>
42 </line>
43 <line branch="false" hits="3" number="7"/>
44 <line branch="true" condition-coverage="100% (2/2)" hits="7" number="8">
45 <conditions>
46 <condition coverage="100%" number="0" type="jump"/>
47 </conditions>
48 </line>
49 <line branch="false" hits="4" number="9"/>
50 <line branch="false" hits="3" number="10"/>
51 </lines>
52 </class>
53 </classes>
54 </package>
55 </packages>
56</coverage>
57""" # NOQA
58
59# NOTE a *baseball* dingbat in sources filepath
60unicode_coverage_xml = unicode("""<?xml version="1.0" ?>
61<!DOCTYPE coverage
62 SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-03.dtd'>
63<coverage branch-rate="1.0" line-rate="1.0" timestamp="1410387089" version="gcovr 2.4 (r2774)">
64 <sources>
65 <source>/tmp/\u26BE/dummy</source>
66 </sources>
67 <packages>
68 <package branch-rate="1.0" complexity="0.0" line-rate="1.0" name="">
69 <classes>
70 <class branch-rate="1.0" complexity="0.0" filename="fibonacci.cpp" line-rate="1.0" name="fibonacci_cpp">
71 <lines>
72 <line branch="false" hits="10" number="4"/>
73 <line branch="true" condition-coverage="100% (2/2)" hits="10" number="6">
74 <conditions>
75 <condition coverage="100%" number="0" type="jump"/>
76 </conditions>
77 </line>
78 <line branch="false" hits="3" number="7"/>
79 <line branch="true" condition-coverage="100% (2/2)" hits="7" number="8">
80 <conditions>
81 <condition coverage="100%" number="0" type="jump"/>
82 </conditions>
83 </line>
84 <line branch="false" hits="4" number="9"/>
85 <line branch="false" hits="3" number="10"/>
86 </lines>
87 </class>
88 </classes>
89 </package>
90 </packages>
91</coverage>
92""") # NOQA
93
94# we'll almost certainly never see garbled coverage data but hey
95garbled_numbers_coverage_xml = u"""<?xml version="1.0" ?>
96<!DOCTYPE coverage
97 SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-03.dtd'>
98<coverage branch-rate="garblegarble" line-rate="1.0" timestamp="1410387089" version="gcovr 2.4 (r2774)">
99 <sources>
100 <source>/tmp/tmpz8yrh4tz/dummy</source>
101 </sources>
102 <packages>
103 <package branch-rate="1.0" complexity="0.0" line-rate="1.0" name="">
104 <classes>
105 <class branch-rate="1.0" complexity="0.0" filename="fibonacci.cpp" line-rate="1.0" name="fibonacci_cpp">
106 <lines>
107 <line branch="false" hits="10" number="4"/>
108 <line branch="true" condition-coverage="100% (2/2)" hits="10" number="6">
109 <conditions>
110 <condition coverage="100%" number="0" type="jump"/>
111 </conditions>
112 </line>
113 <line branch="false" hits="3" number="7"/>
114 <line branch="true" condition-coverage="100% (2/2)" hits="7" number="8">
115 <conditions>
116 <condition coverage="100%" number="0" type="jump"/>
117 </conditions>
118 </line>
119 <line branch="false" hits="4" number="9"/>
120 <line branch="false" hits="3" number="10"/>
121 </lines>
122 </class>
123 </classes>
124 </package>
125 </packages>
126</coverage>
127""" # NOQA
128
129
130class ExtractCoverageDataTestCase(unittest.TestCase):
131
132 def test_extract_coverage_data(self):
133 """Extract coverage data from a vanilla build log bytestring."""
134 line_coverage, branch_coverage = extract_coverage_data(
135 vanilla_coverage_xml
136 )
137 self.assertAlmostEqual(1.0, line_coverage)
138 self.assertAlmostEqual(1.0, branch_coverage)
139
140 def test_invalid_xml_raises(self):
141 """We raise an exception if we find invalid XML."""
142 self.assertRaises(
143 CoverageExtractorException,
144 extract_coverage_data,
145 invalid_xml_coverage_xml
146 )
147
148 def test_garbled_numbers_raise(self):
149 """We raise an exception if coverage numbers don't float."""
150 self.assertRaises(
151 CoverageExtractorException,
152 extract_coverage_data,
153 garbled_numbers_coverage_xml
154 )
155
156 def test_unicode_parses(self):
157 """Extract coverage data from a build log with unicode characters."""
158 line_coverage, branch_coverage = extract_coverage_data(
159 unicode_coverage_xml
160 )
161 self.assertAlmostEqual(1.0, line_coverage)
162 self.assertAlmostEqual(1.0, branch_coverage)
0163
=== added file 'coverage-extractor/coverageextractor/tests/test_extraction.py'
--- coverage-extractor/coverageextractor/tests/test_extraction.py 1970-01-01 00:00:00 +0000
+++ coverage-extractor/coverageextractor/tests/test_extraction.py 2014-10-23 21:46:26 +0000
@@ -0,0 +1,233 @@
1#!/usr/bin/env python
2# Ubuntu CI Engine
3# Copyright 2014 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Affero General Public License version 3, as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17import logging
18
19from mock import patch, Mock
20
21from ci_utils.data_store import DataStoreException
22from coverageextractor import CoverageExtractorException
23from coverageextractor.nfss_client import NfssClient, NfssClientError
24from coverageextractor.tests import (
25 CoverageExtractorTestCase,
26 vanilla_coverage_xml,
27)
28from coverageextractor.extraction import (
29 Extraction,
30 ExtractionFailed,
31 JSON_ENCODING_FAILURE_TEMPLATE,
32 NFSS_CLIENT_ERROR_TEMPLATE,
33 PARSE_FAILURE_TEMPLATE,
34 SUCCESS_TEMPLATE,
35 SWIFT_GET_FAILURE_TEMPLATE,
36)
37
38
39class ExtractionTestCase(CoverageExtractorTestCase):
40
41 def test_try_get_project_and_version(self):
42 """Vanilla get project and version from subticket."""
43 params = {
44 'subtickets': [
45 {
46 'name': 'libfibonacci',
47 'version': '0.1-0ubuntu1',
48 },
49 ]
50 }
51 ext = Extraction('fake-ticket-id', 'fake-logger', 'fake-data-store')
52 self.assertEqual(
53 ('libfibonacci', '0.1-0ubuntu1'),
54 ext.try_get_project_and_version(params)
55 )
56
57 def test_try_get_project_and_version_raises(self):
58 """Failure to find project and version raises."""
59
60 def test_try_get_coverage_xml_path(self):
61 """We get the coverage.xml path from params."""
62 with self.patch_create_data_store() as fake_data_store:
63 params = {'coverage_xml_path': 'coverage.xml'}
64 ext = Extraction('fake-ticket-id', 'fake-logger', fake_data_store)
65 self.assertEqual(
66 'coverage.xml',
67 ext.try_get_coverage_xml_path(params),
68 )
69
70 @patch.object(Extraction, 'report_failed')
71 def test_extract_coverage_xml_path_failure(self, report_failed):
72 """Failure to get the coverage.xml path from params fails."""
73 with self.patch_create_data_store() as fake_data_store:
74 malevolent_params = {'gibberish': 'coverage.xml'}
75 ext = Extraction('fake-ticket-id', 'fake-logger', fake_data_store)
76 ext.try_get_coverage_xml_path(malevolent_params)
77 report_failed.assert_called()
78
79 def test_try_get_coverage_xml(self):
80 """Retrieve our coverage.xml from swift."""
81 with self.patch_create_data_store() as fake_data_store:
82 fake_data_store.get_file.return_value = vanilla_coverage_xml
83 ext = Extraction('fake-ticket-id', 'fake-logger', fake_data_store)
84 self.assertEqual(
85 vanilla_coverage_xml,
86 ext.try_get_coverage_xml('coverage.xml'),
87 )
88
89 def test_try_get_coverage_xml_retries(self):
90 """Retry if swift fails to yield our coverage.xml."""
91 fake_logger = Mock(spec_set=logging.Logger)
92 with self.patch_create_data_store() as fake_data_store:
93 ext = Extraction('fake-ticket-id', fake_logger, fake_data_store)
94 fake_data_store.get_file.side_effect = [
95 DataStoreException("No log!"),
96 DataStoreException("Still no log!"),
97 vanilla_coverage_xml
98 ]
99 with patch('coverageextractor.time.sleep'): # for the impatient
100 self.assertEqual(
101 vanilla_coverage_xml,
102 ext.try_get_coverage_xml('coverage.xml')
103 )
104 self.assertEqual(3, fake_data_store.get_file.call_count)
105 fake_logger.warning.assert_called()
106
107 @patch.object(Extraction, 'report_failed')
108 def test_try_get_coverage_xml_raises(self, report_failed):
109 """Return failure if swift fails to yield our coverage.xml."""
110 fake_logger = Mock(spec_set=logging.Logger)
111 with self.patch_create_data_store() as fake_data_store:
112 ext = Extraction('fake-ticket-id', fake_logger, fake_data_store)
113 fake_data_store.get_file.side_effect = DataStoreException(
114 'Drool...'
115 )
116 with patch('coverageextractor.time.sleep'): # for the impatient
117 ext.try_get_coverage_xml('coverage.xml')
118 report_failed.assert_called_with(
119 SWIFT_GET_FAILURE_TEMPLATE,
120 'fake-ticket-id',
121 'Drool...',
122 )
123
124 def test_try_extract_coverage_data(self):
125 """Extract coverage data from coverage.xml."""
126 ext = Extraction('fake-ticket-id', 'fake-logger', 'fake-data-store')
127 line_coverage, branch_coverage = ext.try_extract_coverage_data(
128 vanilla_coverage_xml
129 )
130 self.assertAlmostEqual(1.0, line_coverage)
131 self.assertAlmostEqual(1.0, branch_coverage)
132
133 @patch.object(Extraction, 'report_failed')
134 @patch('coverageextractor.extraction.extract_coverage_data')
135 def test_try_extract_coverage_data_raises(
136 self,
137 extract_coverage_data,
138 report_failed):
139 """Report failure when we fail to extract coverage data."""
140 extract_coverage_data.side_effect = CoverageExtractorException(
141 "No data for you!"
142 )
143 ext = Extraction('fake-ticket-id', 'fake-logger', 'fake-data-store')
144 ext.try_extract_coverage_data(vanilla_coverage_xml)
145 report_failed.assert_called_with(
146 PARSE_FAILURE_TEMPLATE,
147 'fake-ticket-id',
148 "No data for you!",
149 )
150
151 def test_compose_nfss_json(self):
152 """Dump a string of data to put to NFSS."""
153 ext = Extraction('fake-ticket-id', 'fake-logger', 'fake-data-store')
154 json = ext.try_compose_nfss_json(
155 'fake-package-name',
156 '0.1.PBJ.BBQ-0ubuntu1',
157 1.0,
158 0.572,
159 )
160 self.assertEqual(
161 '{"version": "0.1.PBJ.BBQ-0ubuntu1", "name": "fake-package-name", "line_coverage": 1.0, "branch_coverage": 0.572}', # NOQA
162 json
163 )
164
165 @patch.object(Extraction, 'report_failed')
166 @patch('coverageextractor.extraction.json.dumps')
167 def test_compose_nfss_json_raises(self, dumps, report_failed):
168 """Handle the error from un-jsonnable params.
169
170 This is pretty unlikely.
171 """
172 dumps.side_effect = ValueError('This is pretty unlikely.')
173 ext = Extraction('fake-ticket-id', 'fake-logger', 'fake-data-store')
174 ext.try_compose_nfss_json(
175 'fake-package-name',
176 '0.1.PBJ.BBQ-0ubuntu1',
177 1.0,
178 0.572,
179 )
180 report_failed.assert_called_with(
181 JSON_ENCODING_FAILURE_TEMPLATE,
182 'fake-ticket-id',
183 "This is pretty unlikely.",
184 )
185
186 @patch.object(NfssClient, 'post_data')
187 def test_try_post_data_to_nfss(self, post_data):
188 """Post JSON result to NFSS."""
189 ext = Extraction('fake-ticket-id', 'fake-logger', 'fake-data-store')
190 with patch.object(NfssClient, '__init__', new=Mock(return_value=None)):
191 ext.try_post_data_to_nfss({'foo': 42}, 'libfibonacci', 'coverage')
192 post_data.assert_called_with({'foo': 42}, 'libfibonacci', 'coverage')
193
194 @patch.object(Extraction, 'report_failed')
195 @patch.object(NfssClient, 'post_data')
196 def test_nfss_doesnt_love_us(self, post_data, report_failed):
197 """Bad post to NFSS reports failure."""
198 ext = Extraction('fake-ticket-id', 'fake-logger', 'fake-data-store')
199 post_data.side_effect = NfssClientError('NFSS luvs u <3 .')
200 with patch.object(NfssClient, '__init__', new=Mock(return_value=None)):
201 ext.try_post_data_to_nfss({'foo': 42}, 'libfibonacci', 'coverage')
202 report_failed.assert_called_with(
203 NFSS_CLIENT_ERROR_TEMPLATE,
204 'fake-ticket-id',
205 'NFSS luvs u <3 .',
206 )
207
208 def test_report_failed_raises(self):
209 """Internal catch-all handler raises ExtractionFailed."""
210 with self.patch_create_data_store() as fake_data_store:
211 fake_logger = Mock(spec_set=logging.Logger)
212 ext = Extraction('fake-ticket-id', fake_logger, fake_data_store)
213 self.assertRaises(
214 ExtractionFailed,
215 lambda: ext.report_failed("%s %s", 'foo', 'bar')
216 )
217 fake_logger.error.assert_called_with("foo bar")
218
219 @patch('coverageextractor.extraction.generate_and_put_artifact')
220 def test_report_succeeded(self, generate_and_put_artifact):
221 """Report success to swift and logging."""
222 generate_and_put_artifact.side_effect = ['coverage.xml',
223 'success-message']
224 with self.patch_create_data_store() as fake_data_store:
225 fake_logger = Mock(spec_set=logging.Logger)
226 ext = Extraction('fake-ticket-id', fake_logger, fake_data_store)
227 ext.report_succeeded('fake-ticket-id')
228 fake_logger.info.assert_called_with(SUCCESS_TEMPLATE, 'fake-ticket-id')
229 generate_and_put_artifact.assert_any_call(
230 fake_data_store,
231 message=(SUCCESS_TEMPLATE % 'fake-ticket-id'),
232 )
233 self.assertEqual(1, generate_and_put_artifact.call_count)
0234
=== added file 'coverage-extractor/coverageextractor/tests/test_handle_request.py'
--- coverage-extractor/coverageextractor/tests/test_handle_request.py 1970-01-01 00:00:00 +0000
+++ coverage-extractor/coverageextractor/tests/test_handle_request.py 2014-10-23 21:46:26 +0000
@@ -0,0 +1,121 @@
1# Ubuntu CI Engine
2# Copyright 2014 Canonical Ltd.
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU Affero General Public License version 3, as
6# published by the Free Software Foundation.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranties of
10# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11# PURPOSE. See the GNU Affero General Public License for more details.
12#
13# You should have received a copy of the GNU Affero General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16import logging
17import os
18
19from mock import Mock, patch
20
21from ci_utils import amqp_utils
22from coverageextractor.extraction import Extraction
23from coverageextractor.tests import CoverageExtractorTestCase
24from coverageextractor.extraction import (
25 MISSING_PARAMS_TEMPLATE,
26 SUCCESS_TEMPLATE,
27)
28from coverageextractor.run_worker import CoverageExtractorWorker
29
30
31class HandleRequestTestCase(CoverageExtractorTestCase):
32
33 @patch.object(Extraction, 'try_post_data_to_nfss')
34 def test_vanilla_handle_request(self, try_post_data_to_nfss):
35 """Large integration test for coverage extraction.
36
37 NOTE that we do patch the NFSS call.
38 """
39 fake_logger = Mock(spec_set=logging.Logger)
40 with self.patch_create_data_store():
41 self.put_fake_coverage_xml(self.fake_data_store)
42 worker = CoverageExtractorWorker()
43 fake_params = {
44 'coverage_xml_path': 'coverage.xml',
45 'ticket_id': 'fake_ticket_id',
46 'subtickets': [
47 {
48 'name': 'libfibonacci',
49 'version': '0.1-0ubuntu1',
50 },
51 ],
52 }
53 amqp_result, result_data = worker.handle_request(
54 fake_params,
55 fake_logger,
56 )
57 self.assertEqual(amqp_utils.progress_completed, amqp_result)
58 self.assertEqual(
59 SUCCESS_TEMPLATE % 'fake_ticket_id',
60 result_data['message'],
61 )
62 fake_logger.info.assert_called_with(
63 SUCCESS_TEMPLATE, 'fake_ticket_id'
64 )
65 self.assertEqual(1, len(result_data['artifacts']))
66 # find a log message as our only artifact
67 log_artifact = result_data['artifacts'][0]
68 self.assertEqual(
69 SUCCESS_TEMPLATE % 'fake_ticket_id',
70 log_artifact['message'],
71 )
72 self.assertEqual('LOGS', log_artifact['type'])
73 self.assertEqual(
74 'file://' + os.path.join(
75 os.path.abspath(os.curdir),
76 'fake_ticket_id/coverage-extractor.output.log'),
77 log_artifact['reference']
78 )
79
80 def test_exception_returns_failure(self):
81 """An exception in our imperatives returns a failure.
82
83 Actuate a KeyError in try_extract_coverage_xml_path, inspect our
84 results to make sure we're logging, depositing a log in swift.
85 """
86 fake_logger = Mock(spec_set=logging.Logger)
87 with self.patch_create_data_store() as fake_data_store:
88 self.put_fake_coverage_xml(fake_data_store)
89 worker = CoverageExtractorWorker()
90 # actuate a KeyError in try_extract_coverage_xml_path
91 malevolent_params = {
92 'gibberish': 'coverage.xml',
93 'ticket_id': 'fake_ticket_id',
94 'subtickets': [
95 {
96 'name': 'libfibonacci',
97 'version': '0.1-0ubuntu1',
98 },
99 ],
100 }
101 amqp_result, result_data = worker.handle_request(
102 malevolent_params,
103 fake_logger
104 )
105 self.assertEqual(amqp_utils.progress_failed, amqp_result)
106 self.assertEqual(
107 MISSING_PARAMS_TEMPLATE % 'fake_ticket_id',
108 result_data['message']
109 )
110 self.assertEqual(1, len(result_data['artifacts']))
111 artifact = result_data['artifacts'][0]
112 self.assertEqual(
113 'file://' + os.path.join(
114 os.path.abspath(os.curdir),
115 'fake_ticket_id/coverage-extractor.output.log'),
116 artifact['reference'])
117 self.assertEqual(
118 MISSING_PARAMS_TEMPLATE % 'fake_ticket_id',
119 result_data['message']
120 )
121 self.assertEqual('LOGS', artifact['type'])
0122
=== added file 'coverage-extractor/coverageextractor/tests/test_nfss_client.py'
--- coverage-extractor/coverageextractor/tests/test_nfss_client.py 1970-01-01 00:00:00 +0000
+++ coverage-extractor/coverageextractor/tests/test_nfss_client.py 2014-10-23 21:46:26 +0000
@@ -0,0 +1,150 @@
1#!/usr/bin/env python
2# Ubuntu CI Engine
3# Copyright 2014 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Affero General Public License version 3, as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17import unittest
18
19from mock import Mock, patch
20
21from coverageextractor.nfss_client import (
22 NFSS_CONFIG_PATH,
23 NfssClient,
24 NfssClientError,
25 parse_nfss_config,
26)
27
28
29class ParseNfssConfigTestCase(unittest.TestCase):
30
31 @patch('coverageextractor.nfss_client.ConfigParser')
32 def test_parse(self, config_parser):
33 """We can successfully parse an NFSS config."""
34 nfss_config_dict = {
35 'client_access_key': 'fake-client-acccess-key',
36 'resource_owner_key': 'fake-resource-owner-key',
37 'resource_owner_secret': 'fake-resource-owner-secret',
38 'backend': 'fake-backend',
39 }
40 # yes these are weird due to ConfigParser backwardness
41 config_parser.return_value.read = Mock()
42 # "dance like no-one is watching"
43 config_parser.return_value.__getitem__ = Mock(
44 return_value=nfss_config_dict
45 )
46 self.assertEqual(
47 nfss_config_dict,
48 parse_nfss_config('fake-config-path')
49 )
50
51 @patch('coverageextractor.nfss_client.ConfigParser')
52 def test_no_nfss_section_raises(self, config_parser):
53 """No 'nfss' section in config raises."""
54 config_parser.return_value.read = Mock()
55 config_parser.return_value.__getitem__ = Mock(
56 side_effect=KeyError('client_access_key')
57 )
58 with self.assertRaises(KeyError):
59 parse_nfss_config('fake-config-path')
60
61 @patch('coverageextractor.nfss_client.ConfigParser')
62 def test_missing_key_raises(self, config_parser):
63 """A missing key in the config raises."""
64 nfss_config_dict = {}
65 config_parser.return_value.read = Mock()
66 config_parser.return_value.__getitem__ = Mock(
67 return_value=nfss_config_dict
68 )
69 with self.assertRaises(KeyError):
70 parse_nfss_config('fake-config-path')
71
72
73class NfssClientTest(unittest.TestCase):
74
75 @patch('coverageextractor.nfss_client.OAuth1Session')
76 @patch('coverageextractor.nfss_client.parse_nfss_config')
77 def test_init(self, parse_nfss_config, oauth_session):
78 """We can create a NfssClient."""
79 parse_nfss_config.return_value = {
80 'client_access_key': 'fake-client-access-key',
81 'resource_owner_key': 'fake-resource-owner-key',
82 'resource_owner_secret': 'fake-resource-owner-secret'
83 }
84 NfssClient('fake-config-path')
85 oauth_session.assert_called_with(
86 'fake-client-access-key',
87 resource_owner_key='fake-resource-owner-key',
88 resource_owner_secret='fake-resource-owner-secret',
89 )
90
91 @patch('coverageextractor.nfss_client.OAuth1Session')
92 @patch('coverageextractor.nfss_client.parse_nfss_config')
93 def test_no_config_path(self, parse_nfss_config, oauth_session):
94 """Config path defaults if not passed in as param."""
95 parse_nfss_config.return_value = {
96 'client_access_key': 'fake-client-access-key',
97 'resource_owner_key': 'fake-resource-owner-key',
98 'resource_owner_secret': 'fake-resource-owner-secret'
99 }
100 nfss_client = NfssClient()
101 oauth_session.assert_called_with(
102 'fake-client-access-key',
103 resource_owner_key='fake-resource-owner-key',
104 resource_owner_secret='fake-resource-owner-secret',
105 )
106 self.assertEqual(NFSS_CONFIG_PATH, nfss_client.nfss_config_path)
107
108 @patch('coverageextractor.nfss_client.parse_nfss_config')
109 def test_init_raises(self, parse_nfss_config):
110 """We can create a NfssClient."""
111 parse_nfss_config.side_effect = KeyError('Mangled config!')
112 with self.assertRaises(NfssClientError):
113 NfssClient('fake-config-path')
114
115 def test_post_data(self):
116 """Vanilla POST to NFSS."""
117 with patch.object(
118 NfssClient,
119 '__init__',
120 new=Mock(return_value=None)):
121 nfss_client = NfssClient()
122 nfss_client.config = {'backend': 'http://NFSS.io'}
123 nfss_client.session = Mock(
124 # successful POST
125 post=Mock(return_value=Mock(status_code=200)),
126 )
127 nfss_client.post_data(
128 {'foo': 'bar'},
129 'coverage',
130 'libfibonacci',
131 )
132
133 def test_bad_post_data_raises(self):
134 """Bad POST to NFSS raises."""
135 with patch.object(
136 NfssClient,
137 '__init__',
138 new=Mock(return_value=None)):
139 nfss_client = NfssClient()
140 nfss_client.config = {'backend': 'http://NFSS.io'}
141 nfss_client.session = Mock(
142 # successful POST
143 post=Mock(return_value=Mock(status_code=500)),
144 )
145 with self.assertRaises(NfssClientError):
146 nfss_client.post_data(
147 {'foo': 'bar'},
148 'coverage',
149 'libfibonacci',
150 )
0151
=== added file 'coverage-extractor/setup.py'
--- coverage-extractor/setup.py 1970-01-01 00:00:00 +0000
+++ coverage-extractor/setup.py 2014-10-23 21:46:26 +0000
@@ -0,0 +1,34 @@
1#!/usr/bin/env python
2# Ubuntu CI Engine
3# Copyright 2014 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Affero General Public License version 3, as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17
18from setuptools import setup, find_packages
19
20requires = [
21 'amqplib==1.0.0',
22 'python-swiftclient>=1.8.0',
23 'PyYAML==3.10',
24 'requests-oauthlib>=0.4.1',
25 'ucitests==0.1.5',
26]
27
28setup(
29 name='coverageextractor',
30 version='0.1',
31 packages=find_packages(),
32 description='Coverage Retriever',
33 install_requires=requires,
34)
035
=== added file 'coverage-extractor/tox.ini'
--- coverage-extractor/tox.ini 1970-01-01 00:00:00 +0000
+++ coverage-extractor/tox.ini 2014-10-23 21:46:26 +0000
@@ -0,0 +1,19 @@
1[tox]
2envlist = py27, flake8
3
4[testenv]
5deps =
6 coverage
7 mock
8commands =
9 {envpython} setup.py develop
10 coverage erase
11 coverage run --omit=.tox/*,*/tests/*,ci_utils/* -m unittest discover -s coverageextractor
12 coverage report -m
13 coverage html
14
15[testenv:flake8]
16deps =
17 flake8
18commands =
19 flake8 coverageextractor
020
=== modified file 'testing/run_tests.py'
--- testing/run_tests.py 2014-10-10 10:00:49 +0000
+++ testing/run_tests.py 2014-10-23 21:46:26 +0000
@@ -121,6 +121,7 @@
121 'ppa-creator',121 'ppa-creator',
122 'validator',122 'validator',
123 'coverage-retriever',123 'coverage-retriever',
124 'coverage-extractor',
124 ]125 ]
125 loader = loaders.Loader()126 loader = loaders.Loader()
126 # setuptools tends to leave eggs all over the place127 # setuptools tends to leave eggs all over the place

Subscribers

People subscribed via source and target branches