Merge lp:~canonical-platform-qa/qakit/test-execution-gathering into lp:qakit
- test-execution-gathering
- Merge into trunk
Proposed by
Allan LeSage
Status: | Merged |
---|---|
Approved by: | Christopher Lee |
Approved revision: | 31 |
Merged at revision: | 22 |
Proposed branch: | lp:~canonical-platform-qa/qakit/test-execution-gathering |
Merge into: | lp:qakit |
Diff against target: |
811 lines (+720/-3) 7 files modified
qakit/metrics/practitest/instances.py (+102/-0) qakit/metrics/practitest/testsets.py (+144/-0) qakit/metrics/test_execution.py (+131/-0) qakit/metrics/tests/test_practitest_instances.py (+113/-0) qakit/metrics/tests/test_practitest_testsets.py (+80/-0) qakit/metrics/tests/test_test_execution.py (+99/-0) qakit/practitest/practitest.py (+51/-3) |
To merge this branch: | bzr merge lp:~canonical-platform-qa/qakit/test-execution-gathering |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Christopher Lee (community) | Approve | ||
Review via email: mp+275778@code.launchpad.net |
Commit message
Gather test execution data from PractiTest.
Description of the change
Gather test execution data from PractiTest and deposit in a mongodb database.
To post a comment you must log in.
- 25. By Allan LeSage
-
De-shebangify.
- 26. By Allan LeSage
-
Adjust count insertion.
- 27. By Allan LeSage
-
Adjust a warning log message.
- 28. By Allan LeSage
-
Adjust a docstring to call out list.
- 29. By Allan LeSage
-
Move no last testset logic up a level, some whitespace and naming changes thrown in.
- 30. By Allan LeSage
-
Resolve flake8 config conflict, remove NOQA.
- 31. By Allan LeSage
-
Just raise.
Revision history for this message
Allan LeSage (allanlesage) wrote : | # |
Addressed in numbered revisions, thanks Chris.
Revision history for this message
Christopher Lee (veebers) wrote : | # |
LGTM, thanks for the changes.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added directory 'qakit/metrics/practitest' |
2 | === added file 'qakit/metrics/practitest/instances.py' |
3 | --- qakit/metrics/practitest/instances.py 1970-01-01 00:00:00 +0000 |
4 | +++ qakit/metrics/practitest/instances.py 2015-10-29 22:05:12 +0000 |
5 | @@ -0,0 +1,102 @@ |
6 | +# UESQA Metrics |
7 | +# Copyright (C) 2015 Canonical |
8 | +# |
9 | +# This program is free software: you can redistribute it and/or modify |
10 | +# it under the terms of the GNU General Public License as published by |
11 | +# the Free Software Foundation, either version 3 of the License, or |
12 | +# (at your option) any later version. |
13 | +# |
14 | +# This program is distributed in the hope that it will be useful, |
15 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | +# GNU General Public License for more details. |
18 | +# |
19 | +# You should have received a copy of the GNU General Public License |
20 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
21 | + |
22 | + |
23 | +import logging |
24 | + |
25 | +import qakit.practitest.practitest as practitest |
26 | + |
27 | +logger = logging.getLogger(__name__) |
28 | + |
29 | + |
30 | +def append_testset_id(instance): |
31 | + """Append TestSet ID to the given PractiTest instance dict. |
32 | + |
33 | + This makes querying much easier later, else it has to be parsed |
34 | + out of a string. |
35 | + |
36 | + :param instance: a PractiTest instance |
37 | + |
38 | + """ |
39 | + instance['testset_id'] = int(instance['id'].split(':')[0]) |
40 | + return instance |
41 | + |
42 | + |
43 | +def append_data_to_instance_dict(instance): |
44 | + """Append TestSet ID and last run datetime to the given instance. |
45 | + |
46 | + :param instance: a PractiTest instance to which to append data |
47 | + |
48 | + """ |
49 | + instance = practitest.append_last_run_datetime(instance) |
50 | + instance = append_testset_id(instance) |
51 | + return instance |
52 | + |
53 | + |
54 | +def update_testset_instances(db, session, testset_id): |
55 | + """Retrieve instances from PractiTest for the given TestSet ID. |
56 | + |
57 | + NOTE that old runs get crushed on the PractiTest side: e.g. if a |
58 | + test fails and it's re-run, the failure is lost to the API. |
59 | + |
60 | + :param db: MongoClient db in which to store PractiTest results |
61 | + :param practitest_session: PractitestSession |
62 | + :param testset_id: the PractiTest TestSet for which to retrieve instances. |
63 | + |
64 | + """ |
65 | + logger.info("Retrieving instances for TestSet {}.".format(testset_id)) |
66 | + try: |
67 | + instances = session.get_instances(testset_id) |
68 | + except ValueError as e: |
69 | + logger.warn('TestSet not found for id: {}: '.format( |
70 | + testset_id), str(e)) |
71 | + return 0 |
72 | + count = 0 |
73 | + for instance in instances: |
74 | + instance = append_data_to_instance_dict(instance) |
75 | + result = db.instances.update({'system_id': instance['system_id']}, |
76 | + instance, |
77 | + upsert=True) |
78 | + count += result['n'] |
79 | + logger.info("Retrieved {} instances for TestSet {}.".format(count, |
80 | + testset_id)) |
81 | + return count |
82 | + |
83 | + |
84 | +def update_testsets_instances(db, practitest_session, testset_ids=None): |
85 | + """Update instances for the given testsets, finding new instances. |
86 | + |
87 | + An instance represents a test execution in PractiTest. Note that if a |
88 | + test is executed more than once, only the last execution is reported via |
89 | + the API: e.g. if a test fails and then is run again and passes, the |
90 | + failure is lost to us. |
91 | + |
92 | + :param db: a MongoClient db in which to store PractiTest instances |
93 | + :param practitest_session: a PractitestSession |
94 | + :param testset_ids: list of PractiTest TestSet ids for which to get |
95 | + instances: numbers appear in header of TestSet webpage. If no ids are |
96 | + given, retrieve instances for all *known* TestSets (i.e. all already |
97 | + in the given database). |
98 | + |
99 | + """ |
100 | + logger.info("Updating TestSets with new instance runs.") |
101 | + if testset_ids is None: |
102 | + testset_ids = [testset['id'] for testset in db.testsets.find()] |
103 | + count = 0 |
104 | + for testset_id in testset_ids: |
105 | + count += update_testset_instances( |
106 | + db, practitest_session, testset_id) |
107 | + logger.info("Updated {} instances.".format(count)) |
108 | |
109 | === added file 'qakit/metrics/practitest/testsets.py' |
110 | --- qakit/metrics/practitest/testsets.py 1970-01-01 00:00:00 +0000 |
111 | +++ qakit/metrics/practitest/testsets.py 2015-10-29 22:05:12 +0000 |
112 | @@ -0,0 +1,144 @@ |
113 | +# UESQA Metrics |
114 | +# Copyright (C) 2015 Canonical |
115 | +# |
116 | +# This program is free software: you can redistribute it and/or modify |
117 | +# it under the terms of the GNU General Public License as published by |
118 | +# the Free Software Foundation, either version 3 of the License, or |
119 | +# (at your option) any later version. |
120 | +# |
121 | +# This program is distributed in the hope that it will be useful, |
122 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
123 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
124 | +# GNU General Public License for more details. |
125 | +# |
126 | +# You should have received a copy of the GNU General Public License |
127 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
128 | + |
129 | +import logging |
130 | +import pymongo |
131 | +from qakit.practitest.practitest import append_last_run_datetime |
132 | + |
133 | +logger = logging.getLogger(__name__) |
134 | + |
135 | + |
136 | +def retrieve_testset(db, practitest_session, testset_id): |
137 | + """Retrieve a testset of the given ID, storing it in our database. |
138 | + |
139 | + :param db: a MongoClient collection in which testsets are stored |
140 | + :param practitest_session: a PractiTestSession |
141 | + :param testset_id: the ID of the PractiTest testset to retrieve |
142 | + :raises ValueError: if testset_id is not found in PractiTest |
143 | + |
144 | + """ |
145 | + try: |
146 | + testset = practitest_session.get_testset(testset_id) |
147 | + except ValueError: |
148 | + logger.warn("Testset {} not found!".format(testset_id)) |
149 | + raise |
150 | + testset = append_last_run_datetime(testset) |
151 | + result = db.testsets.update( |
152 | + {'system_id': testset['system_id']}, |
153 | + testset, |
154 | + upsert=True) |
155 | + if result['updatedExisting']: |
156 | + logger.info("Updated testset {}.".format(testset_id)) |
157 | + else: |
158 | + logger.info("Inserted testset {}.".format(testset_id)) |
159 | + return result |
160 | + |
161 | + |
162 | +def _get_last_known_testset_id(db): |
163 | + """Get the last known testset ID from a db. |
164 | + |
165 | + :param db: a MongoClient collection in which testsets are stored |
166 | + |
167 | + """ |
168 | + testsets = db.testsets.find() |
169 | + try: |
170 | + return testsets.sort('id', pymongo.DESCENDING).limit(1)[0]['id'] |
171 | + except IndexError: |
172 | + raise ValueError("No testsets found in db.") |
173 | + |
174 | + |
175 | +def update_testsets(db, practitest_session, testset_ids=None): |
176 | + """Update PractiTest testsets with new data. |
177 | + |
178 | + Note that we update testsets themselves only--instances (i.e. test |
179 | + executions) should be updated separately. |
180 | + |
181 | + :param db: a MongoClient collection in which testsets are stored |
182 | + :param practitest_session: a PractiTestSession |
183 | + :param testset_ids: a list of testsets to retrieve; if not specified, |
184 | + all known testsets in the given db are updated |
185 | + |
186 | + """ |
187 | + logger.info("Updating existing testsets.") |
188 | + if testset_ids is None: |
189 | + testset_ids = [testset['id'] for testset in db.testsets.find()] |
190 | + count = 0 |
191 | + for testset_id in testset_ids: |
192 | + try: |
193 | + result = retrieve_testset(db, practitest_session, testset_id) |
194 | + count += result['n'] |
195 | + except ValueError: |
196 | + logger.warn( |
197 | + "Testset {} not found, assume deleted.".format(testset_id)) |
198 | + logger.info("Updated {} testsets.".format(count)) |
199 | + |
200 | + |
201 | +def _scan_for_testsets(db, practitest_session, testset_id_range): |
202 | + """Scan a range of testset IDs, retrieving each if it exists in PractiTest. |
203 | + |
204 | + Use this to scout ahead while discovering new testsets. |
205 | + |
206 | + :param db: a MongoClient collection in which testsets are stored |
207 | + :param practitest_session: a PractiTestSession |
208 | + :param testset_id_range: a range of testset IDs |
209 | + |
210 | + """ |
211 | + count = 0 |
212 | + for testset_id in testset_id_range: |
213 | + try: |
214 | + result = retrieve_testset(db, practitest_session, testset_id) |
215 | + count += result['n'] |
216 | + except ValueError: |
217 | + logger.debug("Testset {} not found.".format(testset_id)) |
218 | + return count |
219 | + |
220 | + |
221 | +def discover_new_testsets(db, practitest_session, increment=50): |
222 | + """Discover and retrieve new testsets in PractiTest. |
223 | + |
224 | + PractiTest API won't give a list of IDs of testsets belonging to a |
225 | + project. IDs are assigned incrementally and occasionally deleted; |
226 | + scan a given number of IDs ahead and attempt to find a testset at |
227 | + each ID, continue until a full scan comes up empty. |
228 | + |
229 | + :param db: a MongoClient collection in which testsets are stored |
230 | + :param practitest_session: a PractiTestSession |
231 | + :param increment: the increment by which to scan ahead, defaults to 50 |
232 | + |
233 | + """ |
234 | + logger.info("Discovering new testsets in PractiTest.") |
235 | + try: |
236 | + logger.info("Last known testset is {}.".format(last_testset_id)) |
237 | + last_testset_id = _get_last_known_testset_id(db) |
238 | + except ValueError as e: |
239 | + logger.info("No testsets in db.") |
240 | + last_testset_id = 0 |
241 | + total_count = 0 |
242 | + testsets_found = True |
243 | + next_testset_id = last_testset_id + 1 |
244 | + while testsets_found: |
245 | + testset_id_range = range(next_testset_id, next_testset_id+increment) |
246 | + scan_count = _scan_for_testsets( |
247 | + db, |
248 | + practitest_session, |
249 | + testset_id_range) |
250 | + if scan_count is 0: |
251 | + testsets_found = False |
252 | + else: |
253 | + next_testset_id = testset_id_range[-1] + 1 |
254 | + total_count += scan_count |
255 | + logger.info("Retrieved {} testsets.".format(total_count)) |
256 | + return total_count |
257 | |
258 | === added file 'qakit/metrics/test_execution.py' |
259 | --- qakit/metrics/test_execution.py 1970-01-01 00:00:00 +0000 |
260 | +++ qakit/metrics/test_execution.py 2015-10-29 22:05:12 +0000 |
261 | @@ -0,0 +1,131 @@ |
262 | +#!/usr/bin/python3 |
263 | +# UESQA Metrics |
264 | +# Copyright (C) 2015 Canonical |
265 | +# |
266 | +# This program is free software: you can redistribute it and/or modify |
267 | +# it under the terms of the GNU General Public License as published by |
268 | +# the Free Software Foundation, either version 3 of the License, or |
269 | +# (at your option) any later version. |
270 | +# |
271 | +# This program is distributed in the hope that it will be useful, |
272 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
273 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
274 | +# GNU General Public License for more details. |
275 | +# |
276 | +# You should have received a copy of the GNU General Public License |
277 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
278 | + |
279 | + |
280 | +import argparse |
281 | +import configparser |
282 | +import logging |
283 | +import sys |
284 | + |
285 | +import pymongo |
286 | + |
287 | +import qakit.config as qakit_config |
288 | +import qakit.metrics.practitest.instances as instances |
289 | +import qakit.metrics.practitest.testsets as testsets |
290 | +import qakit.metrics.util as util |
291 | +import qakit.practitest.practitest as practitest |
292 | + |
293 | +logger = logging.getLogger(__name__) |
294 | + |
295 | + |
296 | +def update_testsets(db, practitest_session, testset_ids=None): |
297 | + """Discover new and update existing PractiTest testsets. |
298 | + |
299 | + :param db: a MongoClient collection in which PractiTest data is stored |
300 | + :param practitest_session: a PractiTestSession |
301 | + :param testsets: a list of PractiTest testset ids to update; if not |
302 | + specified, new testsets are discovered and all existing testsets are |
303 | + updated |
304 | + |
305 | + """ |
306 | + if not testset_ids: |
307 | + testset_ids = [testset['id'] for testset in db.testsets.find()] |
308 | + # (newly-discovered testsets will be updated upon discovery) |
309 | + testsets.discover_new_testsets(db, practitest_session) |
310 | + testsets.update_testsets(db, practitest_session, testset_ids=testset_ids) |
311 | + |
312 | + |
313 | +def update_instances(db, practitest_session, testset_ids=None): |
314 | + """Discover new and update existing instances for the given testset IDs. |
315 | + |
316 | + :param db: a MongoClient collection in which PractiTest data is stored |
317 | + :param practitest_session: a PractiTestSession |
318 | + :param testsets: a list of PractiTest testset ids to update; if not |
319 | + specified, all known testsets are updated |
320 | + |
321 | + """ |
322 | + if not testset_ids: |
323 | + testset_ids = [testset['id'] for testset in db.testsets.find()] |
324 | + instances.update_testsets_instances( |
325 | + db, |
326 | + practitest_session, |
327 | + testset_ids=testset_ids) |
328 | + |
329 | + |
330 | +def update_test_execution_data(db, practitest_session, testset_ids=None): |
331 | + """Update PractiTest test execution data: testsets and instances. |
332 | + |
333 | + :param db: a MongoClient collection in which PractiTest data is stored |
334 | + :param practitest_session: a PractiTestSession |
335 | + :param testsets: a list of PractiTest testset ids to update; if not |
336 | + specified, new testsets are discovered and all existing testsets are |
337 | + updated |
338 | + |
339 | + """ |
340 | + update_testsets(db, practitest_session, testset_ids=testset_ids) |
341 | + update_instances(db, practitest_session, testset_ids=testset_ids) |
342 | + |
343 | + |
344 | +def _read_config(config_filepath): |
345 | + """Parse the config at the given filepath, returning a config dict. |
346 | + |
347 | + :param config_filepath: the filepath to a configuration file |
348 | + |
349 | + """ |
350 | + config_file = configparser.ConfigParser() |
351 | + config_file.read(config_filepath) |
352 | + config = {} |
353 | + for var_name in ('PRACTITEST_PROJECT_ID', |
354 | + 'PRACTITEST_API_KEY', |
355 | + 'PRACTITEST_API_SECRET_KEY'): |
356 | + new_var_name = var_name.lower().replace('practitest_', '') |
357 | + config[new_var_name] = config_file['DEFAULT'][var_name] |
358 | + return config |
359 | + |
360 | + |
361 | +def _parse_arguments(): |
362 | + parser = argparse.ArgumentParser( |
363 | + "Retrieve PractiTest results, storing them in a MongoDB database.") |
364 | + parser.add_argument( |
365 | + nargs='*', |
366 | + dest='testsets', |
367 | + help='TestSet from which to retrieve instances', |
368 | + type=int) |
369 | + return parser.parse_args() |
370 | + |
371 | + |
372 | +def main(): |
373 | + util.setup_logging() |
374 | + config_dict = _read_config(qakit_config.get_config_file_location()) |
375 | + args = _parse_arguments() |
376 | + conn = pymongo.MongoClient() |
377 | + db = conn.metrics |
378 | + practitest_session = practitest.PractitestSession( |
379 | + config_dict['project_id'], |
380 | + config_dict['api_key'], |
381 | + config_dict['api_secret_key']) |
382 | + update_test_execution_data( |
383 | + db, |
384 | + practitest_session, |
385 | + testset_ids=args.testsets) |
386 | + |
387 | + |
388 | +if __name__ == '__main__': |
389 | + try: |
390 | + sys.exit(main()) |
391 | + except Exception as e: |
392 | + logger.error(e, exc_info=True) |
393 | |
394 | === added file 'qakit/metrics/tests/test_practitest_instances.py' |
395 | --- qakit/metrics/tests/test_practitest_instances.py 1970-01-01 00:00:00 +0000 |
396 | +++ qakit/metrics/tests/test_practitest_instances.py 2015-10-29 22:05:12 +0000 |
397 | @@ -0,0 +1,113 @@ |
398 | +# UESQA Metrics |
399 | +# Copyright (C) 2015 Canonical |
400 | +# |
401 | +# This program is free software: you can redistribute it and/or modify |
402 | +# it under the terms of the GNU General Public License as published by |
403 | +# the Free Software Foundation, either version 3 of the License, or |
404 | +# (at your option) any later version. |
405 | +# |
406 | +# This program is distributed in the hope that it will be useful, |
407 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
408 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
409 | +# GNU General Public License for more details. |
410 | +# |
411 | +# You should have received a copy of the GNU General Public License |
412 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
413 | + |
414 | +import unittest |
415 | +from unittest import mock |
416 | + |
417 | +from bson import ObjectId |
418 | + |
419 | +import qakit.metrics.practitest.instances as instances |
420 | + |
421 | + |
422 | +INSTANCE = { |
423 | + "_id": ObjectId("5590253dca21169c04a38b51"), |
424 | + "tester": { |
425 | + "value": "iahmad", |
426 | + "name": "Tester" |
427 | + }, |
428 | + "last_run_week_number": 20, |
429 | + "name": "Complete Edges Intro", |
430 | + "last_run": { |
431 | + "value": "21-May-2015 20:59", |
432 | + "name": "Last Run" |
433 | + }, |
434 | + "___f_10578": { |
435 | + "value": None, |
436 | + "name": "Build #" |
437 | + }, |
438 | + "id": "10:1", |
439 | + "system_id": 1393092, |
440 | + "run_status": { |
441 | + "value": "FAILED", |
442 | + "name": "Run Status" |
443 | + } |
444 | +} |
445 | + |
446 | + |
447 | +TESTSETS = [ |
448 | + {'id': 761}, |
449 | + {'id': 762}, |
450 | + {'id': 763}, |
451 | + {'id': 764}, |
452 | + {'id': 765}, |
453 | + {'id': 766}, |
454 | + {'id': 767} |
455 | +] |
456 | + |
457 | + |
458 | +class AppendTestSetIdTestCase(unittest.TestCase): |
459 | + |
460 | + def test_testset_id_appended_correctly(self): |
461 | + instance = instances.append_testset_id(INSTANCE) |
462 | + self.assertEqual(10, instance['testset_id']) |
463 | + |
464 | + |
465 | +class AppendDataToInstanceDictTestCase(unittest.TestCase): |
466 | + |
467 | + @mock.patch('qakit.practitest.practitest.append_last_run_datetime') |
468 | + @mock.patch('qakit.metrics.practitest.instances.append_testset_id') |
469 | + def test_appends_called(self, |
470 | + mock_append_testset_id, |
471 | + mock_append_last_run_datetime): |
472 | + # I agree, this should never fail :) |
473 | + mock_append_last_run_datetime.return_value = 'fake-instance' |
474 | + instances.append_data_to_instance_dict(INSTANCE) |
475 | + mock_append_last_run_datetime.assert_called_with(INSTANCE) |
476 | + mock_append_testset_id.assert_called_with('fake-instance') |
477 | + |
478 | + |
479 | +@mock.patch('qakit.metrics.practitest.instances.update_testset_instances') |
480 | +class UpdateTestSetInstancesTestCase(unittest.TestCase): |
481 | + |
482 | + def test_no_testset_ids_specified(self, mock_update_testset_instances): |
483 | + db = mock.Mock( |
484 | + testsets=mock.Mock( |
485 | + find=mock.Mock(return_value=TESTSETS))) |
486 | + instances.update_testsets_instances( |
487 | + db, |
488 | + 'fake-practitest-session', |
489 | + None) |
490 | + self.assertEqual(7, mock_update_testset_instances.call_count) |
491 | + |
492 | + def test_one_testset_specified(self, mock_update_testset_instances): |
493 | + instances.update_testsets_instances( |
494 | + 'fake-db', |
495 | + 'fake-practitest-session', |
496 | + [761]) |
497 | + self.assertEqual(1, mock_update_testset_instances.call_count) |
498 | + mock_update_testset_instances.assert_called_with( |
499 | + 'fake-db', 'fake-practitest-session', 761) |
500 | + |
501 | + def test_several_testsets_specified(self, mock_update_testset_instances): |
502 | + testset_ids = [761, 762, 763] |
503 | + instances.update_testsets_instances( |
504 | + 'fake-db', |
505 | + 'fake-practitest-session', |
506 | + testset_ids) |
507 | + self.assertEqual(3, mock_update_testset_instances.call_count) |
508 | + for testset_id in testset_ids: |
509 | + mock_update_testset_instances.assert_any_call( |
510 | + 'fake-db', 'fake-practitest-session', testset_id) |
511 | |
512 | === added file 'qakit/metrics/tests/test_practitest_testsets.py' |
513 | --- qakit/metrics/tests/test_practitest_testsets.py 1970-01-01 00:00:00 +0000 |
514 | +++ qakit/metrics/tests/test_practitest_testsets.py 2015-10-29 22:05:12 +0000 |
515 | @@ -0,0 +1,80 @@ |
516 | +# UESQA Metrics |
517 | +# Copyright (C) 2015 Canonical |
518 | +# |
519 | +# This program is free software: you can redistribute it and/or modify |
520 | +# it under the terms of the GNU General Public License as published by |
521 | +# the Free Software Foundation, either version 3 of the License, or |
522 | +# (at your option) any later version. |
523 | +# |
524 | +# This program is distributed in the hope that it will be useful, |
525 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
526 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
527 | +# GNU General Public License for more details. |
528 | +# |
529 | +# You should have received a copy of the GNU General Public License |
530 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
531 | + |
532 | +import unittest |
533 | +from unittest import mock |
534 | + |
535 | +import qakit.metrics.practitest.testsets as testsets |
536 | + |
537 | +TESTSETS = [ |
538 | + {'id': 761}, |
539 | + {'id': 762}, |
540 | + {'id': 763}, |
541 | + {'id': 764}, |
542 | + {'id': 765}, |
543 | + {'id': 766}, |
544 | + {'id': 767} |
545 | +] |
546 | + |
547 | + |
548 | +class GetLastKnownTestsetIdTestCase(unittest.TestCase): |
549 | + |
550 | + def testset_exists(self): |
551 | + db = mock.Mock( |
552 | + testsets=mock.Mock( |
553 | + find=mock.Mock( |
554 | + return_value=mock.Mock( |
555 | + sort=mock.Mock( |
556 | + return_value=mock.Mock( |
557 | + limit=mock.Mock( |
558 | + return_value=TESTSETS[:1]))))))) |
559 | + self.assertEqual( |
560 | + 761, |
561 | + testsets._get_last_known_testset_id(db)) |
562 | + |
563 | + def test_no_testsets_raises_valueerror(self): |
564 | + testsets_ = mock.MagicMock() |
565 | + get_item = testsets_.sort.return_value.limit.return_value.__getitem__ |
566 | + get_item.side_effect = IndexError |
567 | + db = mock.Mock( |
568 | + testsets=mock.Mock( |
569 | + find=mock.Mock( |
570 | + return_value=testsets_))) |
571 | + with self.assertRaises(ValueError): |
572 | + testsets._get_last_known_testset_id(db) |
573 | + |
574 | + |
575 | +@mock.patch('qakit.metrics.practitest.testsets.retrieve_testset') |
576 | +class UpdateTestsetsTestCase(unittest.TestCase): |
577 | + |
578 | + def test_no_testsets_specified(self, mock_retrieve_testset): |
579 | + db = mock.Mock( |
580 | + testsets=mock.Mock( |
581 | + find=mock.Mock( |
582 | + return_value=TESTSETS))) |
583 | + mock_retrieve_testset.return_value = {'n': 1} |
584 | + testsets.update_testsets(db, 'fake-practitest-session') |
585 | + for testset_id in [testset['id'] for testset in TESTSETS]: |
586 | + mock_retrieve_testset.assert_any_call( |
587 | + db, 'fake-practitest-session', testset_id) |
588 | + |
589 | + def test_testsets_specified(self, mock_retrieve_testset): |
590 | + mock_retrieve_testset.return_value = {'n': 1} |
591 | + testsets_ = [testset['id'] for testset in TESTSETS] |
592 | + testsets.update_testsets('fake-db', 'fake-practitest-session', testsets_) |
593 | + for testset_id in [testset['id'] for testset in TESTSETS]: |
594 | + mock_retrieve_testset.assert_any_call( |
595 | + 'fake-db', 'fake-practitest-session', testset_id) |
596 | |
597 | === added file 'qakit/metrics/tests/test_test_execution.py' |
598 | --- qakit/metrics/tests/test_test_execution.py 1970-01-01 00:00:00 +0000 |
599 | +++ qakit/metrics/tests/test_test_execution.py 2015-10-29 22:05:12 +0000 |
600 | @@ -0,0 +1,99 @@ |
601 | +# UESQA Metrics |
602 | +# Copyright (C) 2015 Canonical |
603 | +# |
604 | +# This program is free software: you can redistribute it and/or modify |
605 | +# it under the terms of the GNU General Public License as published by |
606 | +# the Free Software Foundation, either version 3 of the License, or |
607 | +# (at your option) any later version. |
608 | +# |
609 | +# This program is distributed in the hope that it will be useful, |
610 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
611 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
612 | +# GNU General Public License for more details. |
613 | +# |
614 | +# You should have received a copy of the GNU General Public License |
615 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
616 | + |
617 | +import unittest |
618 | +from unittest import mock |
619 | + |
620 | +import qakit.metrics.test_execution as test_execution |
621 | + |
622 | + |
623 | +TESTSETS = [ |
624 | + {'id': 761}, |
625 | + {'id': 762}, |
626 | + {'id': 763}, |
627 | + {'id': 764}, |
628 | + {'id': 765}, |
629 | + {'id': 766}, |
630 | + {'id': 767} |
631 | +] |
632 | + |
633 | + |
634 | +@mock.patch('qakit.metrics.test_execution.update_testsets') |
635 | +@mock.patch('qakit.metrics.test_execution.update_instances') |
636 | +class UpdateTestExecutionDataTestCase(unittest.TestCase): |
637 | + |
638 | + def test_update_both_testsets_and_instances( |
639 | + self, |
640 | + mock_update_instances, |
641 | + mock_update_testsets): |
642 | + test_execution.update_test_execution_data( |
643 | + 'fake-db', 'fake-practitest-session', 'fake-testset-ids') |
644 | + for f in mock_update_instances, mock_update_testsets: |
645 | + f.assert_called_with( |
646 | + 'fake-db', |
647 | + 'fake-practitest-session', |
648 | + testset_ids='fake-testset-ids') |
649 | + |
650 | + |
651 | +@mock.patch('qakit.metrics.practitest.testsets.update_testsets') |
652 | +class UpdateTestSetsTestCase(unittest.TestCase): |
653 | + |
654 | + def test_testset_ids_specified(self, mock_update_testsets): |
655 | + test_execution.update_testsets( |
656 | + 'fake-db', 'fake-practitest-session', 'fake-testset-ids') |
657 | + mock_update_testsets.assert_called_with( |
658 | + 'fake-db', |
659 | + 'fake-practitest-session', |
660 | + testset_ids='fake-testset-ids') |
661 | + |
662 | + @mock.patch('qakit.metrics.practitest.testsets.discover_new_testsets') |
663 | + def test_no_testset_ids_specified( |
664 | + self, |
665 | + mock_discover_new_testsets, |
666 | + mock_update_testsets): |
667 | + db = mock.Mock( |
668 | + testsets=mock.Mock( |
669 | + find=mock.Mock(return_value=TESTSETS))) |
670 | + test_execution.update_testsets( |
671 | + db, 'fake-practitest-session', testset_ids=None) |
672 | + mock_discover_new_testsets.assert_called_with( |
673 | + db, 'fake-practitest-session') |
674 | + testset_ids = [testset['id'] for testset in TESTSETS] |
675 | + mock_update_testsets.assert_called_with( |
676 | + db, 'fake-practitest-session', testset_ids=testset_ids) |
677 | + |
678 | + |
679 | +@mock.patch('qakit.metrics.practitest.instances.update_testsets_instances') |
680 | +class UpdateInstancesTestCase(unittest.TestCase): |
681 | + |
682 | + def test_testset_ids_specified(self, mock_update_testsets_instances): |
683 | + test_execution.update_instances( |
684 | + 'fake-db', 'fake-practitest-session', 'fake-testset-ids') |
685 | + mock_update_testsets_instances.assert_called_with( |
686 | + 'fake-db', |
687 | + 'fake-practitest-session', |
688 | + testset_ids='fake-testset-ids') |
689 | + |
690 | + def test_no_testset_ids_specified(self, mock_update_testsets_instances): |
691 | + db = mock.Mock( |
692 | + testsets=mock.Mock( |
693 | + find=mock.Mock(return_value=TESTSETS))) |
694 | + test_execution.update_instances(db, 'fake-practitest-session', None) |
695 | + testset_ids = [testset['id'] for testset in TESTSETS] |
696 | + mock_update_testsets_instances.assert_called_with( |
697 | + db, |
698 | + 'fake-practitest-session', |
699 | + testset_ids=testset_ids) |
700 | |
701 | === modified file 'qakit/practitest/practitest.py' |
702 | --- qakit/practitest/practitest.py 2015-09-03 13:32:10 +0000 |
703 | +++ qakit/practitest/practitest.py 2015-10-29 22:05:12 +0000 |
704 | @@ -15,6 +15,7 @@ |
705 | # You should have received a copy of the GNU General Public License |
706 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
707 | |
708 | +import datetime |
709 | import json |
710 | import logging |
711 | import requests |
712 | @@ -28,6 +29,35 @@ |
713 | PROD_PRACTITEST = 'https://prod.practitest.com/api' |
714 | |
715 | |
716 | +def practitest_time_to_datetime(practitest_time): |
717 | + """Convert from PractiTest time to Python datetime. |
718 | + |
719 | + PractiTest times look like '13-Apr-2015 22:02'. |
720 | + |
721 | + """ |
722 | + return datetime.datetime.strptime( |
723 | + practitest_time, '%d-%b-%Y %H:%M') |
724 | + |
725 | + |
726 | +def append_last_run_datetime(practitest_dict): |
727 | + """Append a Python datetime for 'last_run' to a given PractiTest dict. |
728 | + |
729 | + PractiTest gives us its datetimes as strings; translating to a Python |
730 | + datetime makes querying, sorting, etc. much easier. |
731 | + |
732 | + :param practitest_dict: a PractiTest JSON dict such as an instance or a |
733 | + testset |
734 | + |
735 | + """ |
736 | + try: |
737 | + practitest_dict['last_run_datetime'] = practitest_time_to_datetime( |
738 | + practitest_dict['last_run']['value']) |
739 | + except ValueError: |
740 | + # no last_run, no harm done |
741 | + pass |
742 | + return practitest_dict |
743 | + |
744 | + |
745 | class PractitestSession: |
746 | |
747 | def __init__(self, project_id, api_key, api_secret_key, user_email=None): |
748 | @@ -54,12 +84,14 @@ |
749 | def _get(self, url, params={}): |
750 | """Get the given url, retrying on failure.""" |
751 | params['project_id'] = self.project_id |
752 | - return requests.get( |
753 | + response = requests.get( |
754 | url=url, |
755 | headers=auth.compose_headers( |
756 | self.api_key, |
757 | self.api_secret_key), |
758 | data=json.dumps(params)) |
759 | + response.raise_for_status() |
760 | + return response |
761 | |
762 | |
763 | def _get_all(self, url, params={}): |
764 | @@ -69,6 +101,8 @@ |
765 | return int(pagination['total_entities']) - total_read > 0 |
766 | |
767 | json_data = [] |
768 | + response = None |
769 | + params['page'] = 1 |
770 | while True: |
771 | response = self._get(url, params) |
772 | if not response.ok: |
773 | @@ -79,6 +113,7 @@ |
774 | params['page'] = str(pagination['page'] + 1) |
775 | else: |
776 | break |
777 | + response.raise_for_status() |
778 | return json_data |
779 | |
780 | |
781 | @@ -159,7 +194,14 @@ |
782 | |
783 | """ |
784 | url = PROD_PRACTITEST + '/sets/{}.json'.format(id) |
785 | - return self._get(url).json() |
786 | + try: |
787 | + return self._get(url).json() |
788 | + except requests.exceptions.HTTPError as e: |
789 | + if "TestSet was not found" in e.response.text: |
790 | + raise ValueError("TestSet {} not found!".format(id)) |
791 | + else: |
792 | + raise |
793 | + |
794 | |
795 | def create_testset(self, name, priority, device, |
796 | level, release, build, buildinfo): |
797 | @@ -187,7 +229,13 @@ |
798 | |
799 | """ |
800 | url = PROD_PRACTITEST + '/sets/{}/instances.json'.format(id) |
801 | - return self._get_all(url) |
802 | + try: |
803 | + return self._get_all(url) |
804 | + except requests.exceptions.HTTPError as e: |
805 | + if "TestSet was not found" in e.response.text: |
806 | + raise ValueError("TestSet {} not found!".format(id)) |
807 | + else: |
808 | + raise |
809 | |
810 | def create_instances(self, set_id, test_ids): |
811 | """ |
Looking good, a couple of inline comments.