Merge lp:~cprov/britney/integration-tests into lp:~canonical-ci-engineering/britney/queued-announce-and-collect
- integration-tests
- Merge into queued-announce-and-collect
Proposed by
Celso Providelo
Status: | Merged |
---|---|
Merged at revision: | 436 |
Proposed branch: | lp:~cprov/britney/integration-tests |
Merge into: | lp:~canonical-ci-engineering/britney/queued-announce-and-collect |
Diff against target: |
392 lines (+245/-21) 4 files modified
britney.conf (+4/-0) britney.py (+7/-6) testclient.py (+3/-3) tests/test_testclient.py (+231/-12) |
To merge this branch: | bzr merge lp:~cprov/britney/integration-tests |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Thomi Richards (community) | Approve | ||
Review via email: mp+260046@code.launchpad.net |
Commit message
Adding integration tests for britney/
Description of the change
Adding integrations tests for testclient features. The down-side here is to depend on local rabbit installation for running the tests (kombu 'memory://' cannot be shared across processed and we are calling `britney` for tests).
To post a comment you must log in.
- 439. By Celso Providelo
-
Skip tests if no local rabbitmq is available.
Revision history for this message
Thomi Richards (thomir-deactivatedaccount) wrote : | # |
review:
Approve
Revision history for this message
Celso Providelo (cprov) wrote : | # |
Thomi,
Thanks for the review, comments addressed.
- 440. By Celso Providelo
-
Addressing review comments.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'britney.conf' |
2 | --- britney.conf 2015-03-05 14:57:03 +0000 |
3 | +++ britney.conf 2015-05-25 23:49:18 +0000 |
4 | @@ -70,3 +70,7 @@ |
5 | BOOTTEST_DEBUG = yes |
6 | BOOTTEST_ARCHES = armhf amd64 |
7 | BOOTTEST_FETCH = yes |
8 | + |
9 | +TESTCLIENT_ENABLE = yes |
10 | +TESTCLIENT_AMQP_URIS = amqp://guest:guest@162.213.32.181:5672// |
11 | +TESTCLIENT_REQUIRED_TESTS = |
12 | |
13 | === modified file 'britney.py' |
14 | --- britney.py 2015-05-22 02:51:53 +0000 |
15 | +++ britney.py 2015-05-25 23:49:18 +0000 |
16 | @@ -2019,22 +2019,23 @@ |
17 | self.hints.search('force-badtest', package=excuse.name)) |
18 | forces = [x for x in hints |
19 | if same_source(excuse.ver[1], x.version)] |
20 | - for test in testclient.getTests(excuse.name): |
21 | + for test in testclient.getTests(excuse.name, excuse.ver[1]): |
22 | label = TestClient.EXCUSE_LABELS.get( |
23 | - test.status, 'UNKNOWN STATUS') |
24 | + test.get('status'), 'UNKNOWN STATUS') |
25 | excuse.addhtml( |
26 | "%s result: %s (<a href=\"%s\">results</a>)" % ( |
27 | - test.name, label, test.result_url)) |
28 | + test.get('name').capitalize(), label, |
29 | + test.get('url'))) |
30 | if forces: |
31 | excuse.addhtml( |
32 | "Should wait for %s %s %s, but forced by " |
33 | "%s" % (excuse.name, excuse.ver[1], |
34 | test.name, forces[0].user)) |
35 | continue |
36 | - if test.name not in required_tests: |
37 | + if test.get('name') not in required_tests: |
38 | continue |
39 | - if test.status not in TestClient.VALID_STATUSES: |
40 | - excuse.addreason(test.name) |
41 | + if test.get('status') not in TestClient.VALID_STATUSES: |
42 | + excuse.addreason(test.get('name')) |
43 | if excuse.is_valid: |
44 | excuse.is_valid = False |
45 | excuse.addhtml("Not considered") |
46 | |
47 | === modified file 'testclient.py' |
48 | --- testclient.py 2015-05-22 13:55:57 +0000 |
49 | +++ testclient.py 2015-05-25 23:49:18 +0000 |
50 | @@ -68,9 +68,9 @@ |
51 | |
52 | VALID_STATUSES = ('PASS', 'SKIP') |
53 | |
54 | - LABELS = { |
55 | + EXCUSE_LABELS = { |
56 | "PASS": '<span style="background:#87d96c">Pass</span>', |
57 | - "SKIP": '<span style="background:#ffff00">Skip</span>', |
58 | + "SKIP": '<span style="background:#ffff00">Test skipped</span>', |
59 | "FAIL": '<span style="background:#ff6666">Regression</span>', |
60 | "RUNNING": '<span style="background:#99ddff">Test in progress</span>', |
61 | } |
62 | @@ -93,7 +93,7 @@ |
63 | """Announce new source candidates. |
64 | |
65 | Post a message to the EXCHANGE_CANDATIDATES for every new given |
66 | - excuses (cache announcementes so excuses do not get re-annouced). |
67 | + excuses (cache announcements so excuses do not get re-annouced). |
68 | """ |
69 | with nested(json_cached_info(self.cache_path), |
70 | kombu.Connection(self.amqp_uris)) as (cache, connection): |
71 | |
72 | === modified file 'tests/test_testclient.py' |
73 | --- tests/test_testclient.py 2015-05-22 17:39:54 +0000 |
74 | +++ tests/test_testclient.py 2015-05-25 23:49:18 +0000 |
75 | @@ -15,6 +15,7 @@ |
76 | import kombu |
77 | from kombu.pools import producers |
78 | |
79 | + |
80 | PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
81 | sys.path.insert(0, PROJECT_DIR) |
82 | |
83 | @@ -24,13 +25,14 @@ |
84 | make_cache_key, |
85 | TestClient, |
86 | ) |
87 | +from tests import TestBase |
88 | |
89 | |
90 | class TestJsonCachedInfo(unittest.TestCase): |
91 | |
92 | def setUp(self): |
93 | super(TestJsonCachedInfo, self).setUp() |
94 | - (_dummy, self.test_cache) = tempfile.mkstemp() |
95 | + _, self.test_cache = tempfile.mkstemp() |
96 | self.addCleanup(os.unlink, self.test_cache) |
97 | |
98 | def test_simple(self): |
99 | @@ -64,24 +66,27 @@ |
100 | self.path = tempfile.mkdtemp(prefix='testclient') |
101 | os.makedirs(os.path.join(self.path, 'testclient/')) |
102 | self.addCleanup(shutil.rmtree, self.path) |
103 | + |
104 | os.chdir(self.path) |
105 | + cwd = os.getcwd() |
106 | + self.addCleanup(os.chdir, cwd) |
107 | + |
108 | + self.amqp_uris = ['memory://'] |
109 | |
110 | def test_announce(self): |
111 | # 'announce' post messages to the EXCHANGE_CANDIDATES exchange and |
112 | # updates its internal cache. |
113 | - amqp_uris = ['memory://'] |
114 | - testclient = TestClient('vivid', amqp_uris) |
115 | + testclient = TestClient('vivid', self.amqp_uris) |
116 | test_excuses = [ |
117 | make_excuse('foo', '1.0'), |
118 | make_excuse('bar', '2.0'), |
119 | ] |
120 | |
121 | - with kombu.Connection(amqp_uris) as connection: |
122 | + with kombu.Connection(self.amqp_uris) as connection: |
123 | exchange = kombu.Exchange( |
124 | testclient.EXCHANGE_CANDIDATES, type="fanout") |
125 | queue = kombu.Queue('testing', exchange) |
126 | with connection.SimpleQueue(queue) as q: |
127 | - q.queue.purge() |
128 | testclient.announce(test_excuses) |
129 | self.assertEqual( |
130 | [{'series': 'vivid', |
131 | @@ -100,8 +105,7 @@ |
132 | def test_collect(self): |
133 | # 'collect' collects test results and aggregates them in its |
134 | # internal cache. |
135 | - amqp_uris = ['memory://'] |
136 | - testclient = TestClient('vivid', amqp_uris) |
137 | + testclient = TestClient('vivid', self.amqp_uris) |
138 | |
139 | result_payloads = [ |
140 | {'source_name': 'foo', |
141 | @@ -131,7 +135,7 @@ |
142 | 'test_url': 'http://ubuntu.com/foo'}, |
143 | ] |
144 | |
145 | - with kombu.Connection(amqp_uris) as connection: |
146 | + with kombu.Connection(self.amqp_uris) as connection: |
147 | with producers[connection].acquire(block=True) as producer: |
148 | # Just for binding destination queue to the exchange. |
149 | testclient.collect() |
150 | @@ -159,8 +163,7 @@ |
151 | def test_cleanup(self): |
152 | # `cleanup` remove cache entries that are not present in the |
153 | # given excuses list (i.e. not relevant for promotion anymore). |
154 | - amqp_uris = ['memory://'] |
155 | - testclient = TestClient('vivid', amqp_uris) |
156 | + testclient = TestClient('vivid', self.amqp_uris) |
157 | test_excuses = [ |
158 | make_excuse('foo', '1.0'), |
159 | make_excuse('bar', '2.0'), |
160 | @@ -181,8 +184,7 @@ |
161 | def test_getTests(self): |
162 | # `getTests` yields cached tests results for a given source name |
163 | # and version. |
164 | - amqp_uris = ['memory://'] |
165 | - testclient = TestClient('vivid', amqp_uris) |
166 | + testclient = TestClient('vivid', self.amqp_uris) |
167 | |
168 | with json_cached_info(testclient.cache_path) as cache: |
169 | cache[make_cache_key('foo', '1.0')] = [ |
170 | @@ -207,5 +209,222 @@ |
171 | [], list(testclient.getTests('bar', '1.0'))) |
172 | |
173 | |
174 | +def has_local_rabbitmq(): |
175 | + """Whether a local rabbitmq server is available with default creds.""" |
176 | + with kombu.Connection('amqp://guest:guest@localhost:5672//', |
177 | + connect_timeout=.1) as c: |
178 | + try: |
179 | + c.connect() |
180 | + except: |
181 | + return False |
182 | + return True |
183 | + |
184 | + |
185 | +@unittest.skipUnless(has_local_rabbitmq(), 'No local rabbitmq') |
186 | +class TestTestClientEnd2End(TestBase): |
187 | + """End2End tests (calling `britney`) for the TestClient usage.""" |
188 | + |
189 | + def setUp(self): |
190 | + super(TestTestClientEnd2End, self).setUp() |
191 | + |
192 | + # XXX cprov 20150525: unfortunately, this test requires a proper |
193 | + # amqp transport/server layer (rabbitmq) because kombu 'memory://' |
194 | + # cannot be shared across processes (britney & tests). |
195 | + self.amqp_uris = ['amqp://guest:guest@localhost:5672//'] |
196 | + |
197 | + self.path = tempfile.mkdtemp(prefix='testclient') |
198 | + os.makedirs(os.path.join(self.path, 'testclient/')) |
199 | + self.addCleanup(shutil.rmtree, self.path) |
200 | + |
201 | + os.chdir(self.path) |
202 | + cwd = os.getcwd() |
203 | + self.addCleanup(os.chdir, cwd) |
204 | + |
205 | + # Disable autopkgtests + boottest tests and use local rabbit |
206 | + # for this testing context. |
207 | + self.overrideConfig({ |
208 | + 'ADT_ENABLE': 'no', |
209 | + 'BOOTTEST_ENABLE': 'no', |
210 | + 'TESTCLIENT_AMQP_URIS': ' '.join(self.amqp_uris), |
211 | + }) |
212 | + |
213 | + # We publish a version of 'foo' source to make it 'known'. |
214 | + self.data.add('foo', False, {'Architecture': 'amd64'}) |
215 | + |
216 | + def overrideConfig(self, overrides): |
217 | + """Overrides briney configuration based on the given key-value map.""" |
218 | + with open(self.britney_conf, 'r') as fp: |
219 | + original_config = fp.read() |
220 | + new_config = [] |
221 | + for line in original_config.splitlines(): |
222 | + for k, v in overrides.iteritems(): |
223 | + if line.startswith(k): |
224 | + line = '{} = {}'.format(k, v) |
225 | + new_config.append(line) |
226 | + with open(self.britney_conf, 'w') as fp: |
227 | + fp.write('\n'.join(new_config)) |
228 | + self.addCleanup(self.restore_config, original_config) |
229 | + |
230 | + def publishTestResults(self, results): |
231 | + """Publish the given list of test results.""" |
232 | + with kombu.Connection(self.amqp_uris) as connection: |
233 | + results_exchange = kombu.Exchange( |
234 | + TestClient.EXCHANGE_RESULTS, type="fanout") |
235 | + with producers[connection].acquire(block=True) as producer: |
236 | + publisher = connection.ensure( |
237 | + producer, producer.publish, max_retries=3) |
238 | + for payload in results: |
239 | + publisher(payload, exchange=results_exchange) |
240 | + |
241 | + def getAnnouncements(self): |
242 | + """Yields announcements payloads.""" |
243 | + with kombu.Connection(self.amqp_uris) as connection: |
244 | + candidates_exchange = kombu.Exchange( |
245 | + TestClient.EXCHANGE_CANDIDATES, type="fanout") |
246 | + queue = kombu.Queue('testing', candidates_exchange) |
247 | + with connection.SimpleQueue(queue) as q: |
248 | + for i in range(len(q)): |
249 | + msg = q.get() |
250 | + msg.ack() |
251 | + yield msg.payload |
252 | + |
253 | + def do_test(self, context, expect=None, no_expect=None): |
254 | + """Process the given package context and assert britney results.""" |
255 | + for (pkg, fields) in context: |
256 | + self.data.add(pkg, True, fields) |
257 | + |
258 | + # Creates a queue for collecting announcements from |
259 | + # 'candidates.exchanges'. |
260 | + with kombu.Connection(self.amqp_uris) as connection: |
261 | + candidates_exchange = kombu.Exchange( |
262 | + TestClient.EXCHANGE_CANDIDATES, type="fanout") |
263 | + queue = kombu.Queue('testing', candidates_exchange) |
264 | + with connection.SimpleQueue(queue) as q: |
265 | + q.queue.purge() |
266 | + |
267 | + (excuses, out) = self.run_britney() |
268 | + |
269 | + #print('-------\nexcuses: %s\n-----' % excuses) |
270 | + if expect: |
271 | + for re in expect: |
272 | + self.assertRegexpMatches(excuses, re) |
273 | + if no_expect: |
274 | + for re in no_expect: |
275 | + self.assertNotRegexpMatches(excuses, re) |
276 | + |
277 | + def test_non_required_test(self): |
278 | + # Non-required test results are collected as part of the excuse |
279 | + # report but do not block source promotion (i.e. the excuse is |
280 | + # a 'Valid candidate' even if the test is 'in progress'). |
281 | + |
282 | + # Publish 'in-progress' results for 'bazinga for "foo_1.1"'. |
283 | + test_results = [{ |
284 | + 'source_name': 'foo', |
285 | + 'source_version': '1.1', |
286 | + 'series': self.data.series, |
287 | + 'test_name': 'bazinga', |
288 | + 'test_status': 'RUNNING', |
289 | + 'test_url': 'http://bazinga.com/foo', |
290 | + }] |
291 | + self.publishTestResults(test_results) |
292 | + |
293 | + # Run britney for 'foo_1.1' and valid candidated is recorded. |
294 | + context = [ |
295 | + ('foo', {'Source': 'foo', 'Version': '1.1', |
296 | + 'Architecture': 'amd64'}), |
297 | + ] |
298 | + self.do_test( |
299 | + context, |
300 | + [r'\bfoo\b.*>1</a> to .*>1.1<', |
301 | + r'<li>Bazinga result: .*>Test in progress.*' |
302 | + r'href="http://bazinga.com/foo">results', |
303 | + '<li>Valid candidate']) |
304 | + |
305 | + # 'foo_1.1' source candidate was announced. |
306 | + self.assertEqual( |
307 | + [{'source_name': 'foo', |
308 | + 'source_version': '1.1', |
309 | + 'series': self.data.series, |
310 | + }], list(self.getAnnouncements())) |
311 | + |
312 | + def test_required_test(self): |
313 | + # A required-test result is collected and blocks source package |
314 | + # promotion while it hasn't passed. |
315 | + |
316 | + # Make 'bazinga' a required test. |
317 | + self.overrideConfig({ |
318 | + 'TESTCLIENT_REQUIRED_TESTS': 'bazinga', |
319 | + }) |
320 | + |
321 | + # Publish 'in-progress' results for 'bazinga for "foo_1.1"'. |
322 | + test_results = [{ |
323 | + 'source_name': 'foo', |
324 | + 'source_version': '1.1', |
325 | + 'series': self.data.series, |
326 | + 'test_name': 'bazinga', |
327 | + 'test_status': 'RUNNING', |
328 | + 'test_url': 'http://bazinga.com/foo', |
329 | + }] |
330 | + self.publishTestResults(test_results) |
331 | + |
332 | + # Run britney for 'foo_1.1' and an unconsidered excuse is recorded. |
333 | + context = [ |
334 | + ('foo', {'Source': 'foo', 'Version': '1.1', |
335 | + 'Architecture': 'amd64'}), |
336 | + ] |
337 | + self.do_test( |
338 | + context, |
339 | + [r'\bfoo\b.*>1</a> to .*>1.1<', |
340 | + r'<li>Bazinga result: .*>Test in progress.*' |
341 | + r'href="http://bazinga.com/foo">results', |
342 | + '<li>Not considered']) |
343 | + |
344 | + # 'foo_1.1' source candidate was announced. |
345 | + self.assertEqual( |
346 | + [{'source_name': 'foo', |
347 | + 'source_version': '1.1', |
348 | + 'series': self.data.series, |
349 | + }], list(self.getAnnouncements())) |
350 | + |
351 | + def test_promoted(self): |
352 | + # When all required tests passed (or were skipped) the source |
353 | + # candidate can be promoted. |
354 | + |
355 | + # Make 'bazinga' and 'zoing' required test. |
356 | + self.overrideConfig({ |
357 | + 'TESTCLIENT_REQUIRED_TESTS': 'bazinga zoing', |
358 | + }) |
359 | + |
360 | + # Publish 'in-progress' results for 'bazinga for "foo_1.1"'. |
361 | + test_results = [{ |
362 | + 'source_name': 'foo', |
363 | + 'source_version': '1.1', |
364 | + 'series': self.data.series, |
365 | + 'test_name': 'bazinga', |
366 | + 'test_status': 'SKIP', |
367 | + 'test_url': 'http://bazinga.com/foo', |
368 | + }, { |
369 | + 'source_name': 'foo', |
370 | + 'source_version': '1.1', |
371 | + 'series': self.data.series, |
372 | + 'test_name': 'zoing', |
373 | + 'test_status': 'PASS', |
374 | + 'test_url': 'http://zoing.com/foo', |
375 | + }] |
376 | + self.publishTestResults(test_results) |
377 | + |
378 | + context = [ |
379 | + ('foo', {'Source': 'foo', 'Version': '1.1', |
380 | + 'Architecture': 'amd64'}), |
381 | + ] |
382 | + self.do_test( |
383 | + context, |
384 | + [r'\bfoo\b.*>1</a> to .*>1.1<', |
385 | + r'<li>Bazinga result: .*>Test skipped.*' |
386 | + 'href="http://bazinga.com/foo">results', |
387 | + r'<li>Zoing result: .*>Pass.*href="http://zoing.com/foo">results', |
388 | + '<li>Valid candidate']) |
389 | + |
390 | + |
391 | if __name__ == '__main__': |
392 | unittest.main() |
A few comments, otherwise looks good.