Merge lp:~ev/ubuntu-ci-services-itself/time-sorted-tests into lp:ubuntu-ci-services-itself

Proposed by Evan
Status: Merged
Approved by: Evan
Approved revision: 410
Merged at revision: 402
Proposed branch: lp:~ev/ubuntu-ci-services-itself/time-sorted-tests
Merge into: lp:ubuntu-ci-services-itself
Diff against target: 213 lines (+122/-7)
7 files modified
bin/test_time_sorted_tests.py (+32/-0)
bin/time_sorted_tests.py (+67/-0)
branch-source-builder/bsbuilder/tests/test_upload.py (+3/-1)
branch-source-builder/bsbuilder/tests/test_v1.py (+3/-1)
ci-utils/ci_utils/image_store/tests/test-image-store.py (+6/-1)
ci-utils/ci_utils/tests/test_amqp.py (+3/-3)
lander/lander/tests/test_archiver.py (+8/-1)
To merge this branch: bzr merge lp:~ev/ubuntu-ci-services-itself/time-sorted-tests
Reviewer Review Type Date Requested Status
Andy Doan (community) Approve
Chris Johnston (community) Approve
PS Jenkins bot (community) continuous-integration Approve
Review via email: mp+211231@code.launchpad.net

Commit message

Shave two minutes off test execution. Add a small program to print sorted test execution time.

Description of the change

The kinds of things one discovers while on a ten hour flight. This branch knocks our worst case test time under ./run-tests from 2 minutes 12 seconds to 10 seconds. A few tests were setting up nonexistent hosts to talk to, then waiting for the socket timeout:

trunk:
Ran 289 tests in 133.913s
Ran 289 tests in 132.474s
Ran 289 tests in 132.500s

time-sorted-tests:
Ran 289 tests in 10.384s
Ran 289 tests in 10.727s
Ran 289 tests in 10.676s

It also adds a small tested program to sort the tests by their execution time, which was helpful in identifying long running tests. We should run this periodically (say as part of the MP review toolbelt) to ensure that our ability to iterate quickly isn't slowed by our growing test coverage.

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

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

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

review: Approve (continuous-integration)
Revision history for this message
Chris Johnston (cjohnston) wrote :

Took the tarmac time for me running it locally from 4:14 to 2:49... I like
that. :-) faster == better

I'm not a huge fan of the commenting for the mocks.. We don't do those
types of comments elsewhere, but thats just my thoughts..

 review approve

review: Approve
Revision history for this message
Andy Doan (doanac) wrote :

The whole "bin" idea is quite interesting. I've been thinking there are a few scripts we have that should get moved there.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'bin'
2=== added file 'bin/test_time_sorted_tests.py'
3--- bin/test_time_sorted_tests.py 1970-01-01 00:00:00 +0000
4+++ bin/test_time_sorted_tests.py 2014-03-17 02:09:01 +0000
5@@ -0,0 +1,32 @@
6+import time_sorted_tests
7+import unittest
8+import tempfile
9+import os
10+import sys
11+import mock
12+
13+
14+data = '''ticket.tests.test_read_api.TicketReadAPITest.test_get_ticket_steps_detail ... OK (0.049 secs)
15+ticket.tests.test_read_api.TicketReadAPITest.test_get_ticket_update_status_api ... OK (0.046 secs)'''
16+
17+
18+class T(unittest.TestCase):
19+ def test_gen_timings(self):
20+ t = time_sorted_tests.gen_timings(data)
21+ t = t.next()
22+ self.assertEqual(t[0], 'ticket.tests.test_read_api.TicketReadAPITest.test_get_ticket_steps_detail')
23+ self.assertEqual(t[1], 0.049)
24+
25+ def test_gen_timings_bad_data(self):
26+ t = time_sorted_tests.gen_timings(data + '\noutput from test.')
27+ # Iterate through all the data.
28+ [x for x in t]
29+
30+ def test_sort_timings(self):
31+ t = time_sorted_tests.gen_timings(data)
32+ s = time_sorted_tests.sort_timings(t)
33+ self.assertEqual(s[0][1], 0.046)
34+ self.assertEqual(s[1][1], 0.049)
35+
36+if __name__ == '__main__':
37+ unittest.main()
38
39=== added file 'bin/time_sorted_tests.py'
40--- bin/time_sorted_tests.py 1970-01-01 00:00:00 +0000
41+++ bin/time_sorted_tests.py 2014-03-17 02:09:01 +0000
42@@ -0,0 +1,67 @@
43+#!/usr/bin/env python
44+import os
45+import sys
46+import re
47+import subprocess
48+
49+
50+# test_name ... OK (0.1 secs)
51+pattern = re.compile('^(.*) \.\.\. .*\((.*) secs\)$')
52+
53+
54+def gen_timings(output):
55+ '''Filter out the test names and timing data into tuples.'''
56+ for line in output.split('\n'):
57+ try:
58+ test_name, seconds = re.match(pattern, line).groups()
59+ except AttributeError:
60+ continue
61+ seconds = float(seconds)
62+ yield test_name, seconds
63+
64+
65+def sort_timings(timings):
66+ '''Sort the timings based on the second value in the tuple, the time in
67+ seconds.'''
68+ return sorted(timings, cmp=lambda x, y: cmp(x[1], y[1]))
69+
70+
71+def run_tests(args, test_command=['./run-tests']):
72+ '''Call ./run-tests with any arguments passed to this program. Return
73+ stderr'''
74+ cmd = test_command + args
75+ # Drop stderr. The text output from tests is not needed here.
76+ with open('/dev/null', 'a') as devnull:
77+ kwargs = {'stdout': subprocess.PIPE, 'stderr': devnull}
78+ p = subprocess.Popen(cmd, **kwargs)
79+ try:
80+ return p.communicate()[0]
81+ except KeyboardInterrupt:
82+ # Clean up the subprocess when we've interrupted this script.
83+ if p.pid:
84+ # Terminate.
85+ os.kill(p.pid, 15)
86+ return None
87+
88+
89+def print_timings(timings):
90+ for test_name, seconds in timings:
91+ print '%s: %s' % (test_name, seconds)
92+
93+
94+def print_total_timing(timings):
95+ total = sum([x[1] for x in timings])
96+ output = 'Total test time: %.2f seconds' % total
97+ print
98+ print '-' * len(output)
99+ print output
100+
101+
102+if __name__ == '__main__':
103+ output = run_tests(sys.argv[1:])
104+ if output is None:
105+ sys.exit(0)
106+ timings = gen_timings(output)
107+ sorted_timings = sort_timings(timings)
108+ print_timings(sorted_timings)
109+ print_total_timing(sorted_timings)
110
111=== modified file 'branch-source-builder/bsbuilder/tests/test_upload.py'
112--- branch-source-builder/bsbuilder/tests/test_upload.py 2014-03-10 22:11:05 +0000
113+++ branch-source-builder/bsbuilder/tests/test_upload.py 2014-03-17 02:09:01 +0000
114@@ -69,7 +69,8 @@
115
116 @mock.patch('bsbuilder.upload_package.create_temp_dir')
117 @mock.patch('__builtin__.open')
118- def test_get_source_files(self, open_mock, temp_dir_mock):
119+ @mock.patch('bsbuilder.upload_package.get_url_contents')
120+ def test_get_source_files(self, url_mock, open_mock, temp_dir_mock):
121 temp_dir_mock.return_value = 'mock_location'
122 open_mock.return_value = self._get_context_manager_mock()
123 file_list = []
124@@ -84,3 +85,4 @@
125 self.assertEqual('/api/v1/fullsubticket/2/', result[0]['resource'])
126 self.assertEqual('mock_location', result[0]['location'])
127 self.assertItemsEqual(file_list, result[0]['files'])
128+ url_mock.assert_called_with('https://swift.url/package_0.1.dsc')
129
130=== modified file 'branch-source-builder/bsbuilder/tests/test_v1.py'
131--- branch-source-builder/bsbuilder/tests/test_v1.py 2014-03-14 10:37:28 +0000
132+++ branch-source-builder/bsbuilder/tests/test_v1.py 2014-03-17 02:09:01 +0000
133@@ -30,7 +30,9 @@
134 self.app = webtest.TestApp(wsgi.app, relative_to='.')
135
136 @mock.patch('ci_utils.amqp_utils.get_config')
137- def testStatus(self, get_config):
138+ # Don't actually try to talk to a Rabbit server.
139+ @mock.patch('urllib2.build_opener')
140+ def testStatus(self, build_opener, get_config):
141 get_config.return_value = None
142 resp = self.app.get('/api/v1/status', status=200)
143 data = json.loads(resp.body)
144
145=== modified file 'ci-utils/ci_utils/image_store/tests/test-image-store.py'
146--- ci-utils/ci_utils/image_store/tests/test-image-store.py 2014-01-24 22:42:57 +0000
147+++ ci-utils/ci_utils/image_store/tests/test-image-store.py 2014-03-17 02:09:01 +0000
148@@ -15,11 +15,16 @@
149 # along with this program. If not, see <http://www.gnu.org/licenses/>.
150
151 from ci_utils.image_store import ImageStore, ImageStoreException
152+from keystoneclient.exceptions import AuthorizationFailure
153 import unittest
154+import mock
155
156
157 class TestImageStore(unittest.TestCase):
158- def test_bad_auth(self):
159+ # Do not attempt to talk to a keystone server.
160+ @mock.patch('keystoneclient.httpclient.HTTPClient.authenticate',
161+ side_effect=AuthorizationFailure())
162+ def test_bad_auth(self, *args):
163 auth_data = {
164 'auth_url': 'https://keystone.canonistack.canonical.com:443/v2.0/',
165 'auth_user': 'bad data',
166
167=== modified file 'ci-utils/ci_utils/tests/test_amqp.py'
168--- ci-utils/ci_utils/tests/test_amqp.py 2014-01-31 10:59:22 +0000
169+++ ci-utils/ci_utils/tests/test_amqp.py 2014-03-17 02:09:01 +0000
170@@ -65,10 +65,10 @@
171 callback = mock.Mock()
172 channel.wait.side_effect = socket.error()
173 start = time.time()
174- retry_period = 2
175+ retry_period = 0.1
176 amqp_utils._run_forever(channel, 'foo', callback, retry_period)
177- # see if it was within 1/10 of a second of the retry_period
178- self.assertAlmostEqual(start + retry_period, time.time(), places=1)
179+ # see if it was within 1/100 of a second of the retry_period
180+ self.assertAlmostEqual(start + retry_period, time.time(), places=2)
181
182
183 class TestProgressTrigger(unittest.TestCase):
184
185=== modified file 'lander/lander/tests/test_archiver.py'
186--- lander/lander/tests/test_archiver.py 2014-03-10 22:25:00 +0000
187+++ lander/lander/tests/test_archiver.py 2014-03-17 02:09:01 +0000
188@@ -18,6 +18,7 @@
189 import os
190 import sys
191 import unittest
192+import urllib2
193
194 sys.path.append(os.path.join(os.path.dirname(__file__), '../../bin'))
195 import lander_archiver
196@@ -245,10 +246,16 @@
197 self.assertEqual(-2, self.archiver())
198
199 @mock.patch('lander_archiver.TicketApi')
200- def test_call_store_artifact_failure(self, ticket_api):
201+ @mock.patch('urllib2.urlopen')
202+ def test_call_store_artifact_failure(self, mock_urlopen, ticket_api):
203 '''Verify __call__ returns -3 on a store artifact failure.'''
204 config = {'username', 'user',
205 'password', 'pass'}
206+
207+ def side_effect(*args):
208+ # Don't hang around waiting for the Jenkins server to never reply.
209+ raise urllib2.URLError('Nope.')
210+ mock_urlopen.side_effect = side_effect
211 api_inst = mock.Mock()
212 ticket_api.return_value = api_inst
213 args = mock.Mock()

Subscribers

People subscribed via source and target branches

to all changes: