Merge lp:~canonical-platform-qa/qakit/test-execution-gathering into lp:qakit

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
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.
Revision history for this message
Christopher Lee (veebers) wrote :

Looking good, a couple of inline comments.

review: Needs Fixing
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 """

Subscribers

People subscribed via source and target branches

to all changes: