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
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()

Subscribers

People subscribed via source and target branches