Merge lp:~cprov/britney/integration-tests into lp:~canonical-ci-engineering/britney/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
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/testclient.py

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.
lp:~cprov/britney/integration-tests updated
439. By Celso Providelo

Skip tests if no local rabbitmq is available.

Revision history for this message
Thomi Richards (thomir-deactivatedaccount) wrote :

A few comments, otherwise looks good.

review: Approve
Revision history for this message
Celso Providelo (cprov) wrote :

Thomi,

Thanks for the review, comments addressed.

lp:~cprov/britney/integration-tests updated
440. By Celso Providelo

Addressing review comments.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'britney.conf'
--- britney.conf 2015-03-05 14:57:03 +0000
+++ britney.conf 2015-05-25 23:49:18 +0000
@@ -70,3 +70,7 @@
70BOOTTEST_DEBUG = yes70BOOTTEST_DEBUG = yes
71BOOTTEST_ARCHES = armhf amd6471BOOTTEST_ARCHES = armhf amd64
72BOOTTEST_FETCH = yes72BOOTTEST_FETCH = yes
73
74TESTCLIENT_ENABLE = yes
75TESTCLIENT_AMQP_URIS = amqp://guest:guest@162.213.32.181:5672//
76TESTCLIENT_REQUIRED_TESTS =
7377
=== modified file 'britney.py'
--- britney.py 2015-05-22 02:51:53 +0000
+++ britney.py 2015-05-25 23:49:18 +0000
@@ -2019,22 +2019,23 @@
2019 self.hints.search('force-badtest', package=excuse.name))2019 self.hints.search('force-badtest', package=excuse.name))
2020 forces = [x for x in hints2020 forces = [x for x in hints
2021 if same_source(excuse.ver[1], x.version)]2021 if same_source(excuse.ver[1], x.version)]
2022 for test in testclient.getTests(excuse.name):2022 for test in testclient.getTests(excuse.name, excuse.ver[1]):
2023 label = TestClient.EXCUSE_LABELS.get(2023 label = TestClient.EXCUSE_LABELS.get(
2024 test.status, 'UNKNOWN STATUS')2024 test.get('status'), 'UNKNOWN STATUS')
2025 excuse.addhtml(2025 excuse.addhtml(
2026 "%s result: %s (<a href=\"%s\">results</a>)" % (2026 "%s result: %s (<a href=\"%s\">results</a>)" % (
2027 test.name, label, test.result_url))2027 test.get('name').capitalize(), label,
2028 test.get('url')))
2028 if forces:2029 if forces:
2029 excuse.addhtml(2030 excuse.addhtml(
2030 "Should wait for %s %s %s, but forced by "2031 "Should wait for %s %s %s, but forced by "
2031 "%s" % (excuse.name, excuse.ver[1],2032 "%s" % (excuse.name, excuse.ver[1],
2032 test.name, forces[0].user))2033 test.name, forces[0].user))
2033 continue2034 continue
2034 if test.name not in required_tests:2035 if test.get('name') not in required_tests:
2035 continue2036 continue
2036 if test.status not in TestClient.VALID_STATUSES:2037 if test.get('status') not in TestClient.VALID_STATUSES:
2037 excuse.addreason(test.name)2038 excuse.addreason(test.get('name'))
2038 if excuse.is_valid:2039 if excuse.is_valid:
2039 excuse.is_valid = False2040 excuse.is_valid = False
2040 excuse.addhtml("Not considered")2041 excuse.addhtml("Not considered")
20412042
=== modified file 'testclient.py'
--- testclient.py 2015-05-22 13:55:57 +0000
+++ testclient.py 2015-05-25 23:49:18 +0000
@@ -68,9 +68,9 @@
6868
69 VALID_STATUSES = ('PASS', 'SKIP')69 VALID_STATUSES = ('PASS', 'SKIP')
7070
71 LABELS = {71 EXCUSE_LABELS = {
72 "PASS": '<span style="background:#87d96c">Pass</span>',72 "PASS": '<span style="background:#87d96c">Pass</span>',
73 "SKIP": '<span style="background:#ffff00">Skip</span>',73 "SKIP": '<span style="background:#ffff00">Test skipped</span>',
74 "FAIL": '<span style="background:#ff6666">Regression</span>',74 "FAIL": '<span style="background:#ff6666">Regression</span>',
75 "RUNNING": '<span style="background:#99ddff">Test in progress</span>',75 "RUNNING": '<span style="background:#99ddff">Test in progress</span>',
76 }76 }
@@ -93,7 +93,7 @@
93 """Announce new source candidates.93 """Announce new source candidates.
9494
95 Post a message to the EXCHANGE_CANDATIDATES for every new given95 Post a message to the EXCHANGE_CANDATIDATES for every new given
96 excuses (cache announcementes so excuses do not get re-annouced).96 excuses (cache announcements so excuses do not get re-annouced).
97 """ 97 """
98 with nested(json_cached_info(self.cache_path),98 with nested(json_cached_info(self.cache_path),
99 kombu.Connection(self.amqp_uris)) as (cache, connection):99 kombu.Connection(self.amqp_uris)) as (cache, connection):
100100
=== modified file 'tests/test_testclient.py'
--- tests/test_testclient.py 2015-05-22 17:39:54 +0000
+++ tests/test_testclient.py 2015-05-25 23:49:18 +0000
@@ -15,6 +15,7 @@
15import kombu15import kombu
16from kombu.pools import producers16from kombu.pools import producers
1717
18
18PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))19PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19sys.path.insert(0, PROJECT_DIR)20sys.path.insert(0, PROJECT_DIR)
2021
@@ -24,13 +25,14 @@
24 make_cache_key,25 make_cache_key,
25 TestClient,26 TestClient,
26)27)
28from tests import TestBase
2729
2830
29class TestJsonCachedInfo(unittest.TestCase):31class TestJsonCachedInfo(unittest.TestCase):
3032
31 def setUp(self):33 def setUp(self):
32 super(TestJsonCachedInfo, self).setUp()34 super(TestJsonCachedInfo, self).setUp()
33 (_dummy, self.test_cache) = tempfile.mkstemp()35 _, self.test_cache = tempfile.mkstemp()
34 self.addCleanup(os.unlink, self.test_cache)36 self.addCleanup(os.unlink, self.test_cache)
3537
36 def test_simple(self):38 def test_simple(self):
@@ -64,24 +66,27 @@
64 self.path = tempfile.mkdtemp(prefix='testclient')66 self.path = tempfile.mkdtemp(prefix='testclient')
65 os.makedirs(os.path.join(self.path, 'testclient/'))67 os.makedirs(os.path.join(self.path, 'testclient/'))
66 self.addCleanup(shutil.rmtree, self.path)68 self.addCleanup(shutil.rmtree, self.path)
69
67 os.chdir(self.path)70 os.chdir(self.path)
71 cwd = os.getcwd()
72 self.addCleanup(os.chdir, cwd)
73
74 self.amqp_uris = ['memory://']
6875
69 def test_announce(self):76 def test_announce(self):
70 # 'announce' post messages to the EXCHANGE_CANDIDATES exchange and77 # 'announce' post messages to the EXCHANGE_CANDIDATES exchange and
71 # updates its internal cache.78 # updates its internal cache.
72 amqp_uris = ['memory://']79 testclient = TestClient('vivid', self.amqp_uris)
73 testclient = TestClient('vivid', amqp_uris)
74 test_excuses = [80 test_excuses = [
75 make_excuse('foo', '1.0'),81 make_excuse('foo', '1.0'),
76 make_excuse('bar', '2.0'),82 make_excuse('bar', '2.0'),
77 ]83 ]
7884
79 with kombu.Connection(amqp_uris) as connection:85 with kombu.Connection(self.amqp_uris) as connection:
80 exchange = kombu.Exchange(86 exchange = kombu.Exchange(
81 testclient.EXCHANGE_CANDIDATES, type="fanout")87 testclient.EXCHANGE_CANDIDATES, type="fanout")
82 queue = kombu.Queue('testing', exchange)88 queue = kombu.Queue('testing', exchange)
83 with connection.SimpleQueue(queue) as q:89 with connection.SimpleQueue(queue) as q:
84 q.queue.purge()
85 testclient.announce(test_excuses)90 testclient.announce(test_excuses)
86 self.assertEqual(91 self.assertEqual(
87 [{'series': 'vivid',92 [{'series': 'vivid',
@@ -100,8 +105,7 @@
100 def test_collect(self):105 def test_collect(self):
101 # 'collect' collects test results and aggregates them in its106 # 'collect' collects test results and aggregates them in its
102 # internal cache.107 # internal cache.
103 amqp_uris = ['memory://']108 testclient = TestClient('vivid', self.amqp_uris)
104 testclient = TestClient('vivid', amqp_uris)
105109
106 result_payloads = [110 result_payloads = [
107 {'source_name': 'foo',111 {'source_name': 'foo',
@@ -131,7 +135,7 @@
131 'test_url': 'http://ubuntu.com/foo'},135 'test_url': 'http://ubuntu.com/foo'},
132 ]136 ]
133137
134 with kombu.Connection(amqp_uris) as connection:138 with kombu.Connection(self.amqp_uris) as connection:
135 with producers[connection].acquire(block=True) as producer:139 with producers[connection].acquire(block=True) as producer:
136 # Just for binding destination queue to the exchange.140 # Just for binding destination queue to the exchange.
137 testclient.collect()141 testclient.collect()
@@ -159,8 +163,7 @@
159 def test_cleanup(self):163 def test_cleanup(self):
160 # `cleanup` remove cache entries that are not present in the164 # `cleanup` remove cache entries that are not present in the
161 # given excuses list (i.e. not relevant for promotion anymore).165 # given excuses list (i.e. not relevant for promotion anymore).
162 amqp_uris = ['memory://']166 testclient = TestClient('vivid', self.amqp_uris)
163 testclient = TestClient('vivid', amqp_uris)
164 test_excuses = [167 test_excuses = [
165 make_excuse('foo', '1.0'),168 make_excuse('foo', '1.0'),
166 make_excuse('bar', '2.0'),169 make_excuse('bar', '2.0'),
@@ -181,8 +184,7 @@
181 def test_getTests(self):184 def test_getTests(self):
182 # `getTests` yields cached tests results for a given source name185 # `getTests` yields cached tests results for a given source name
183 # and version.186 # and version.
184 amqp_uris = ['memory://']187 testclient = TestClient('vivid', self.amqp_uris)
185 testclient = TestClient('vivid', amqp_uris)
186188
187 with json_cached_info(testclient.cache_path) as cache:189 with json_cached_info(testclient.cache_path) as cache:
188 cache[make_cache_key('foo', '1.0')] = [190 cache[make_cache_key('foo', '1.0')] = [
@@ -207,5 +209,222 @@
207 [], list(testclient.getTests('bar', '1.0')))209 [], list(testclient.getTests('bar', '1.0')))
208210
209211
212def has_local_rabbitmq():
213 """Whether a local rabbitmq server is available with default creds."""
214 with kombu.Connection('amqp://guest:guest@localhost:5672//',
215 connect_timeout=.1) as c:
216 try:
217 c.connect()
218 except:
219 return False
220 return True
221
222
223@unittest.skipUnless(has_local_rabbitmq(), 'No local rabbitmq')
224class TestTestClientEnd2End(TestBase):
225 """End2End tests (calling `britney`) for the TestClient usage."""
226
227 def setUp(self):
228 super(TestTestClientEnd2End, self).setUp()
229
230 # XXX cprov 20150525: unfortunately, this test requires a proper
231 # amqp transport/server layer (rabbitmq) because kombu 'memory://'
232 # cannot be shared across processes (britney & tests).
233 self.amqp_uris = ['amqp://guest:guest@localhost:5672//']
234
235 self.path = tempfile.mkdtemp(prefix='testclient')
236 os.makedirs(os.path.join(self.path, 'testclient/'))
237 self.addCleanup(shutil.rmtree, self.path)
238
239 os.chdir(self.path)
240 cwd = os.getcwd()
241 self.addCleanup(os.chdir, cwd)
242
243 # Disable autopkgtests + boottest tests and use local rabbit
244 # for this testing context.
245 self.overrideConfig({
246 'ADT_ENABLE': 'no',
247 'BOOTTEST_ENABLE': 'no',
248 'TESTCLIENT_AMQP_URIS': ' '.join(self.amqp_uris),
249 })
250
251 # We publish a version of 'foo' source to make it 'known'.
252 self.data.add('foo', False, {'Architecture': 'amd64'})
253
254 def overrideConfig(self, overrides):
255 """Overrides briney configuration based on the given key-value map."""
256 with open(self.britney_conf, 'r') as fp:
257 original_config = fp.read()
258 new_config = []
259 for line in original_config.splitlines():
260 for k, v in overrides.iteritems():
261 if line.startswith(k):
262 line = '{} = {}'.format(k, v)
263 new_config.append(line)
264 with open(self.britney_conf, 'w') as fp:
265 fp.write('\n'.join(new_config))
266 self.addCleanup(self.restore_config, original_config)
267
268 def publishTestResults(self, results):
269 """Publish the given list of test results."""
270 with kombu.Connection(self.amqp_uris) as connection:
271 results_exchange = kombu.Exchange(
272 TestClient.EXCHANGE_RESULTS, type="fanout")
273 with producers[connection].acquire(block=True) as producer:
274 publisher = connection.ensure(
275 producer, producer.publish, max_retries=3)
276 for payload in results:
277 publisher(payload, exchange=results_exchange)
278
279 def getAnnouncements(self):
280 """Yields announcements payloads."""
281 with kombu.Connection(self.amqp_uris) as connection:
282 candidates_exchange = kombu.Exchange(
283 TestClient.EXCHANGE_CANDIDATES, type="fanout")
284 queue = kombu.Queue('testing', candidates_exchange)
285 with connection.SimpleQueue(queue) as q:
286 for i in range(len(q)):
287 msg = q.get()
288 msg.ack()
289 yield msg.payload
290
291 def do_test(self, context, expect=None, no_expect=None):
292 """Process the given package context and assert britney results."""
293 for (pkg, fields) in context:
294 self.data.add(pkg, True, fields)
295
296 # Creates a queue for collecting announcements from
297 # 'candidates.exchanges'.
298 with kombu.Connection(self.amqp_uris) as connection:
299 candidates_exchange = kombu.Exchange(
300 TestClient.EXCHANGE_CANDIDATES, type="fanout")
301 queue = kombu.Queue('testing', candidates_exchange)
302 with connection.SimpleQueue(queue) as q:
303 q.queue.purge()
304
305 (excuses, out) = self.run_britney()
306
307 #print('-------\nexcuses: %s\n-----' % excuses)
308 if expect:
309 for re in expect:
310 self.assertRegexpMatches(excuses, re)
311 if no_expect:
312 for re in no_expect:
313 self.assertNotRegexpMatches(excuses, re)
314
315 def test_non_required_test(self):
316 # Non-required test results are collected as part of the excuse
317 # report but do not block source promotion (i.e. the excuse is
318 # a 'Valid candidate' even if the test is 'in progress').
319
320 # Publish 'in-progress' results for 'bazinga for "foo_1.1"'.
321 test_results = [{
322 'source_name': 'foo',
323 'source_version': '1.1',
324 'series': self.data.series,
325 'test_name': 'bazinga',
326 'test_status': 'RUNNING',
327 'test_url': 'http://bazinga.com/foo',
328 }]
329 self.publishTestResults(test_results)
330
331 # Run britney for 'foo_1.1' and valid candidated is recorded.
332 context = [
333 ('foo', {'Source': 'foo', 'Version': '1.1',
334 'Architecture': 'amd64'}),
335 ]
336 self.do_test(
337 context,
338 [r'\bfoo\b.*>1</a> to .*>1.1<',
339 r'<li>Bazinga result: .*>Test in progress.*'
340 r'href="http://bazinga.com/foo">results',
341 '<li>Valid candidate'])
342
343 # 'foo_1.1' source candidate was announced.
344 self.assertEqual(
345 [{'source_name': 'foo',
346 'source_version': '1.1',
347 'series': self.data.series,
348 }], list(self.getAnnouncements()))
349
350 def test_required_test(self):
351 # A required-test result is collected and blocks source package
352 # promotion while it hasn't passed.
353
354 # Make 'bazinga' a required test.
355 self.overrideConfig({
356 'TESTCLIENT_REQUIRED_TESTS': 'bazinga',
357 })
358
359 # Publish 'in-progress' results for 'bazinga for "foo_1.1"'.
360 test_results = [{
361 'source_name': 'foo',
362 'source_version': '1.1',
363 'series': self.data.series,
364 'test_name': 'bazinga',
365 'test_status': 'RUNNING',
366 'test_url': 'http://bazinga.com/foo',
367 }]
368 self.publishTestResults(test_results)
369
370 # Run britney for 'foo_1.1' and an unconsidered excuse is recorded.
371 context = [
372 ('foo', {'Source': 'foo', 'Version': '1.1',
373 'Architecture': 'amd64'}),
374 ]
375 self.do_test(
376 context,
377 [r'\bfoo\b.*>1</a> to .*>1.1<',
378 r'<li>Bazinga result: .*>Test in progress.*'
379 r'href="http://bazinga.com/foo">results',
380 '<li>Not considered'])
381
382 # 'foo_1.1' source candidate was announced.
383 self.assertEqual(
384 [{'source_name': 'foo',
385 'source_version': '1.1',
386 'series': self.data.series,
387 }], list(self.getAnnouncements()))
388
389 def test_promoted(self):
390 # When all required tests passed (or were skipped) the source
391 # candidate can be promoted.
392
393 # Make 'bazinga' and 'zoing' required test.
394 self.overrideConfig({
395 'TESTCLIENT_REQUIRED_TESTS': 'bazinga zoing',
396 })
397
398 # Publish 'in-progress' results for 'bazinga for "foo_1.1"'.
399 test_results = [{
400 'source_name': 'foo',
401 'source_version': '1.1',
402 'series': self.data.series,
403 'test_name': 'bazinga',
404 'test_status': 'SKIP',
405 'test_url': 'http://bazinga.com/foo',
406 }, {
407 'source_name': 'foo',
408 'source_version': '1.1',
409 'series': self.data.series,
410 'test_name': 'zoing',
411 'test_status': 'PASS',
412 'test_url': 'http://zoing.com/foo',
413 }]
414 self.publishTestResults(test_results)
415
416 context = [
417 ('foo', {'Source': 'foo', 'Version': '1.1',
418 'Architecture': 'amd64'}),
419 ]
420 self.do_test(
421 context,
422 [r'\bfoo\b.*>1</a> to .*>1.1<',
423 r'<li>Bazinga result: .*>Test skipped.*'
424 'href="http://bazinga.com/foo">results',
425 r'<li>Zoing result: .*>Pass.*href="http://zoing.com/foo">results',
426 '<li>Valid candidate'])
427
428
210if __name__ == '__main__':429if __name__ == '__main__':
211 unittest.main()430 unittest.main()

Subscribers

People subscribed via source and target branches