Merge lp:~canonical-platform-qa/britney/tests into lp:~ubuntu-release/britney/britney2-ubuntu

Proposed by Martin Pitt
Status: Merged
Approved by: Colin Watson
Approved revision: 408
Merged at revision: 397
Proposed branch: lp:~canonical-platform-qa/britney/tests
Merge into: lp:~ubuntu-release/britney/britney2-ubuntu
Diff against target: 451 lines (+446/-0)
1 file modified
tests/autopkgtest.py (+446/-0)
To merge this branch: bzr merge lp:~canonical-platform-qa/britney/tests
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+207982@code.launchpad.net

Description of the change

Add tests and reproduce some important bugs

This reproduces two bugs which we've recently encountered, and an additional
one which came up when writing the tests:

 * If a new source builds an existing binary, britney ignores all autopkgtests.
   This is what caused the "break trusty" disaster when uploading gccgo-4.9
   which built an empty/broken libgcc1.

 * Britney requests autopkgtest runs for uninstallable packages, causing
   needless test failures and manual intervention to re-try packages once they
   become installable again.

 * Britney does not cross-check the version number that a test was run with,
   and just applies the test result to the currently pending version. This
   hasn't demonstrably caused any ill effect in practice as adt-britney should
   already ensure that the requested version was tested. It might be a good
   idea to verify this anyway though, for robustness.

Simply run the tests with "tests/autopkgtest.py" after checking out the tree,
building lib, and creating the britneymodule.so -> lib/britneymodule.so link.

To post a comment you must log in.
399. By Martin Pitt

use symbolic constants instead of True/False for considered status

400. By Martin Pitt

some more tests

401. By Martin Pitt

add --debug option mock adt-britney

402. By Martin Pitt

run britney in verbose mode

403. By Martin Pitt

merge with trunk

404. By Martin Pitt

merge trunk

405. By Martin Pitt

add test for uninstallable binary built from new source package

406. By Martin Pitt

fix source package name in cause of new_source tests

Revision history for this message
Martin Pitt (pitti) wrote :

> * If a new source builds an existing binary, britney ignores all autopkgtests.
> This is what caused the "break trusty" disaster when uploading gccgo-4.9
> which built an empty/broken libgcc1.

While the actual bug is still there somewhere (it did happen, after all), these tests don't reproduce that unfortunately. Turned out the failures were due to a copy&paste error, fixed in r406. They pass now.

407. By Jean-Baptiste Lallement

merged trunk

408. By Martin Pitt

Mark test_result_from_older_version as XFAIL

Revision history for this message
Colin Watson (cjwatson) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added directory 'tests'
=== added file 'tests/autopkgtest.py'
--- tests/autopkgtest.py 1970-01-01 00:00:00 +0000
+++ tests/autopkgtest.py 2014-05-12 12:05:05 +0000
@@ -0,0 +1,446 @@
1#!/usr/bin/python
2# (C) 2014 Canonical Ltd.
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8
9import tempfile
10import shutil
11import os
12import sys
13import subprocess
14import unittest
15
16architectures = ['amd64', 'arm64', 'armhf', 'i386', 'powerpc', 'ppc64el']
17
18my_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19
20NOT_CONSIDERED = False
21VALID_CANDIDATE = True
22
23
24class TestData:
25 def __init__(self):
26 '''Construct local test package indexes.
27
28 The archive is initially empty. You can create new packages with
29 create_deb(). self.path contains the path of the archive, and
30 self.apt_source provides an apt source "deb" line.
31
32 It is kept in a temporary directory which gets removed when the Archive
33 object gets deleted.
34 '''
35 self.path = tempfile.mkdtemp(prefix='testarchive.')
36 self.apt_source = 'deb file://%s /' % self.path
37 self.dirs = {False: os.path.join(self.path, 'data', 'testing'),
38 True: os.path.join(self.path, 'data', 'unstable')}
39 os.makedirs(self.dirs[False])
40 os.mkdir(self.dirs[True])
41 self.added_sources = {False: set(), True: set()}
42 self.added_binaries = {False: set(), True: set()}
43
44 # pre-create all files for all architectures
45 for arch in architectures:
46 for dir in self.dirs.values():
47 with open(os.path.join(dir, 'Packages_' + arch), 'w'):
48 pass
49 for dir in self.dirs.values():
50 for fname in ['Dates', 'Blocks']:
51 with open(os.path.join(dir, fname), 'w'):
52 pass
53 for dname in ['Hints']:
54 os.mkdir(os.path.join(dir, dname))
55
56 os.mkdir(os.path.join(self.path, 'output'))
57
58 # create temporary home dir for proposed-migration autopktest status
59 self.home = os.path.join(self.path, 'home')
60 os.environ['HOME'] = self.home
61 os.makedirs(os.path.join(self.home, 'proposed-migration',
62 'autopkgtest', 'work'))
63
64 def __del__(self):
65 shutil.rmtree(self.path)
66
67 def add(self, name, unstable, fields={}, add_src=True):
68 '''Add a binary package to the index file.
69
70 You need to specify at least the package name and in which list to put
71 it (unstable==True for unstable/proposed, or False for
72 testing/release). fields specifies all additional entries, e. g.
73 {'Depends': 'foo, bar', 'Conflicts: baz'}. There are defaults for most
74 fields.
75
76 Unless add_src is set to False, this will also automatically create a
77 source record, based on fields['Source'] and name.
78 '''
79 assert (name not in self.added_binaries[unstable])
80 self.added_binaries[unstable].add(name)
81
82 fields.setdefault('Architecture', architectures[0])
83 fields.setdefault('Version', '1')
84 fields.setdefault('Priority', 'optional')
85 fields.setdefault('Section', 'devel')
86 fields.setdefault('Description', 'test pkg')
87 if fields['Architecture'] == 'all':
88 for a in architectures:
89 self._append(name, unstable, 'Packages_' + a, fields)
90 else:
91 self._append(name, unstable, 'Packages_' + fields['Architecture'],
92 fields)
93
94 if add_src:
95 src = fields.get('Source', name)
96 if src not in self.added_sources[unstable]:
97 self.add_src(src, unstable, {'Version': fields['Version'],
98 'Section': fields['Section']})
99
100 def add_src(self, name, unstable, fields={}):
101 '''Add a source package to the index file.
102
103 You need to specify at least the package name and in which list to put
104 it (unstable==True for unstable/proposed, or False for
105 testing/release). fields specifies all additional entries, which can be
106 Version (default: 1), Section (default: devel), and Extra-Source-Only.
107 '''
108 assert (name not in self.added_sources[unstable])
109 self.added_sources[unstable].add(name)
110
111 fields.setdefault('Version', '1')
112 fields.setdefault('Section', 'devel')
113 self._append(name, unstable, 'Sources', fields)
114
115 def _append(self, name, unstable, file_name, fields):
116 with open(os.path.join(self.dirs[unstable], file_name), 'a') as f:
117 f.write('''Package: %s
118Maintainer: Joe <joe@example.com>
119''' % name)
120
121 for k, v in fields.items():
122 f.write('%s: %s\n' % (k, v))
123 f.write('\n')
124
125
126class Test(unittest.TestCase):
127 def setUp(self):
128 self.data = TestData()
129
130 # add a bunch of packages to testing to avoid repetition
131 self.data.add('libc6', False)
132 self.data.add('libgreen1', False, {'Source': 'green',
133 'Depends': 'libc6 (>= 0.9)'})
134 self.data.add('green', False, {'Depends': 'libc6 (>= 0.9), libgreen1',
135 'Conflicts': 'blue'})
136 self.data.add('lightgreen', False, {'Depends': 'libgreen1'})
137 self.data.add('darkgreen', False, {'Depends': 'libgreen1'})
138 self.data.add('blue', False, {'Depends': 'libc6 (>= 0.9)',
139 'Conflicts': 'green'})
140 self.data.add('justdata', False, {'Architecture': 'all'})
141
142 self.britney = os.path.join(my_dir, 'britney.py')
143 self.britney_conf = os.path.join(my_dir, 'britney.conf')
144 assert os.path.exists(self.britney)
145 assert os.path.exists(self.britney_conf)
146
147 # fake adt-britney script
148 self.adt_britney = os.path.join(self.data.home, 'auto-package-testing',
149 'jenkins', 'adt-britney')
150 os.makedirs(os.path.dirname(self.adt_britney))
151
152 with open(self.adt_britney, 'w') as f:
153 f.write('''#!/bin/sh -e
154echo "$@" >> /%s/adt-britney.log ''' % self.data.path)
155 os.chmod(self.adt_britney, 0o755)
156
157 def tearDown(self):
158 del self.data
159
160 def make_adt_britney(self, request):
161 with open(self.adt_britney, 'w') as f:
162 f.write('''#!%(py)s
163import argparse, shutil,sys
164
165def request():
166 if args.req:
167 shutil.copy(args.req, '%(path)s/adt-britney.requestarg')
168 with open(args.output, 'w') as f:
169 f.write("""%(rq)s""".replace('PASS', 'NEW').replace('FAIL', 'NEW').replace('RUNNING', 'NEW'))
170
171def submit():
172 with open(args.req, 'w') as f:
173 f.write("""%(rq)s""".replace('PASS', 'RUNNING').
174 replace('FAIL', 'RUNNING'))
175
176def collect():
177 with open(args.output, 'w') as f:
178 f.write("""%(rq)s""")
179
180p = argparse.ArgumentParser()
181p.add_argument('-c', '--config')
182p.add_argument('-a', '--arch')
183p.add_argument('-r', '--release')
184p.add_argument('-P', '--use-proposed', action='store_true')
185p.add_argument('-d', '--debug', action='store_true')
186p.add_argument('-U', '--no-update', action='store_true')
187sp = p.add_subparsers()
188
189prequest = sp.add_parser('request')
190prequest.add_argument('-O', '--output')
191prequest.add_argument('req', nargs='?')
192prequest.set_defaults(func=request)
193
194psubmit = sp.add_parser('submit')
195psubmit.add_argument('req')
196psubmit.set_defaults(func=submit)
197
198pcollect = sp.add_parser('collect')
199pcollect.add_argument('-O', '--output')
200pcollect.add_argument('-n', '--new-only', action='store_true', default=False)
201pcollect.set_defaults(func=collect)
202
203args = p.parse_args()
204args.func()
205''' % {'py': sys.executable, 'path': self.data.path, 'rq': request})
206
207 def run_britney(self, args=[]):
208 '''Run britney.
209
210 Assert that it succeeds and does not produce anything on stderr.
211 Return (excuses.html, britney_out).
212 '''
213 britney = subprocess.Popen([self.britney, '-v', '-c', self.britney_conf],
214 stdout=subprocess.PIPE,
215 stderr=subprocess.PIPE,
216 cwd=self.data.path,
217 universal_newlines=True)
218 (out, err) = britney.communicate()
219 self.assertEqual(britney.returncode, 0, out + err)
220 self.assertEqual(err, '')
221
222 with open(os.path.join(self.data.path, 'output', 'excuses.html')) as f:
223 excuses = f.read()
224
225 return (excuses, out)
226
227 def test_no_request_for_uninstallable(self):
228 '''Does not request a test for an uninstallable package'''
229
230 self.do_test(
231 # uninstallable unstable version
232 [('green', {'Version': '1.1~beta', 'Depends': 'libc6 (>= 0.9), libgreen1 (>= 2)'})],
233 'green 1.1~beta RUNNING green 1.1~beta\n',
234 NOT_CONSIDERED,
235 [r'\bgreen\b.*>1</a> to .*>1.1~beta<',
236 'green/amd64 unsatisfiable Depends: libgreen1 \(>= 2\)'],
237 # autopkgtest should not be triggered for uninstallable pkg
238 ['autopkgtest'])
239
240 def test_request_for_installable_running(self):
241 '''Requests a test for an installable package, test still running'''
242
243 self.do_test(
244 [('green', {'Version': '1.1~beta', 'Depends': 'libc6 (>= 0.9), libgreen1'})],
245 'green 1.1~beta RUNNING green 1.1~beta\n',
246 NOT_CONSIDERED,
247 [r'\bgreen\b.*>1</a> to .*>1.1~beta<',
248 '<li>autopkgtest for green 1.1~beta: RUNNING'])
249
250 def test_request_for_installable_fail(self):
251 '''Requests a test for an installable package, test fail'''
252
253 self.do_test(
254 [('green', {'Version': '1.1~beta', 'Depends': 'libc6 (>= 0.9), libgreen1'})],
255 'green 1.1~beta FAIL green 1.1~beta\n',
256 NOT_CONSIDERED,
257 [r'\bgreen\b.*>1</a> to .*>1.1~beta<',
258 '<li>autopkgtest for green 1.1~beta: FAIL'])
259
260 def test_request_for_installable_pass(self):
261 '''Requests a test for an installable package, test pass'''
262
263 self.do_test(
264 [('green', {'Version': '1.1~beta', 'Depends': 'libc6 (>= 0.9), libgreen1'})],
265 'green 1.1~beta PASS green 1.1~beta\n',
266 VALID_CANDIDATE,
267 [r'\bgreen\b.*>1</a> to .*>1.1~beta<',
268 '<li>autopkgtest for green 1.1~beta: PASS'])
269
270 def test_multi_rdepends_with_tests_running(self):
271 '''Multiple reverse dependencies with tests (still running)'''
272
273 self.do_test(
274 [('libgreen1', {'Version': '2', 'Source': 'green', 'Depends': 'libc6'})],
275 'lightgreen 1 PASS green 2\n'
276 'darkgreen 1 RUNNING green 2\n',
277 NOT_CONSIDERED,
278 [r'\bgreen\b.*>1</a> to .*>2<',
279 '<li>autopkgtest for lightgreen 1: PASS',
280 '<li>autopkgtest for darkgreen 1: RUNNING'])
281
282 def test_multi_rdepends_with_tests_fail(self):
283 '''Multiple reverse dependencies with tests (fail)'''
284
285 self.do_test(
286 [('libgreen1', {'Version': '2', 'Source': 'green', 'Depends': 'libc6'})],
287 'lightgreen 1 PASS green 2\n'
288 'darkgreen 1 FAIL green 2\n',
289 NOT_CONSIDERED,
290 [r'\bgreen\b.*>1</a> to .*>2<',
291 '<li>autopkgtest for lightgreen 1: PASS',
292 '<li>autopkgtest for darkgreen 1: FAIL'])
293
294 def test_multi_rdepends_with_tests_pass(self):
295 '''Multiple reverse dependencies with tests (pass)'''
296
297 self.do_test(
298 [('libgreen1', {'Version': '2', 'Source': 'green', 'Depends': 'libc6'})],
299 'lightgreen 1 PASS green 2\n'
300 'darkgreen 1 PASS green 2\n',
301 VALID_CANDIDATE,
302 [r'\bgreen\b.*>1</a> to .*>2<',
303 '<li>autopkgtest for lightgreen 1: PASS',
304 '<li>autopkgtest for darkgreen 1: PASS'])
305
306 def test_multi_rdepends_with_some_tests_running(self):
307 '''Multiple reverse dependencies with some tests (running)'''
308
309 # add a third reverse dependency to libgreen1 which does not have a test
310 self.data.add('mint', False, {'Depends': 'libgreen1'})
311
312 self.do_test(
313 [('libgreen1', {'Version': '2', 'Source': 'green', 'Depends': 'libc6'})],
314 'lightgreen 1 RUNNING green 2\n'
315 'darkgreen 1 RUNNING green 2\n',
316 NOT_CONSIDERED,
317 [r'\bgreen\b.*>1</a> to .*>2<',
318 '<li>autopkgtest for lightgreen 1: RUNNING',
319 '<li>autopkgtest for darkgreen 1: RUNNING'])
320
321 def test_multi_rdepends_with_some_tests_fail(self):
322 '''Multiple reverse dependencies with some tests (fail)'''
323
324 # add a third reverse dependency to libgreen1 which does not have a test
325 self.data.add('mint', False, {'Depends': 'libgreen1'})
326
327 self.do_test(
328 [('libgreen1', {'Version': '2', 'Source': 'green', 'Depends': 'libc6'})],
329 'lightgreen 1 PASS green 2\n'
330 'darkgreen 1 FAIL green 2\n',
331 NOT_CONSIDERED,
332 [r'\bgreen\b.*>1</a> to .*>2<',
333 '<li>autopkgtest for lightgreen 1: PASS',
334 '<li>autopkgtest for darkgreen 1: FAIL'])
335
336 def test_multi_rdepends_with_some_tests_pass(self):
337 '''Multiple reverse dependencies with some tests (pass)'''
338
339 # add a third reverse dependency to libgreen1 which does not have a test
340 self.data.add('mint', False, {'Depends': 'libgreen1'})
341
342 self.do_test(
343 [('libgreen1', {'Version': '2', 'Source': 'green', 'Depends': 'libc6'})],
344 'lightgreen 1 PASS green 2\n'
345 'darkgreen 1 PASS green 2\n',
346 VALID_CANDIDATE,
347 [r'\bgreen\b.*>1</a> to .*>2<',
348 '<li>autopkgtest for lightgreen 1: PASS',
349 '<li>autopkgtest for darkgreen 1: PASS'])
350
351 def test_binary_from_new_source_package_running(self):
352 '''building an existing binary for a new source package (running)'''
353
354 self.do_test(
355 [('libgreen1', {'Version': '2', 'Source': 'newgreen', 'Depends': 'libc6'})],
356 'lightgreen 1 PASS newgreen 2\n'
357 'darkgreen 1 RUNNING newgreen 2\n',
358 NOT_CONSIDERED,
359 [r'\bnewgreen\b.*\(- to .*>2<',
360 '<li>autopkgtest for lightgreen 1: PASS',
361 '<li>autopkgtest for darkgreen 1: RUNNING'])
362
363 def test_binary_from_new_source_package_fail(self):
364 '''building an existing binary for a new source package (fail)'''
365
366 self.do_test(
367 [('libgreen1', {'Version': '2', 'Source': 'newgreen', 'Depends': 'libc6'})],
368 'lightgreen 1 PASS newgreen 2\n'
369 'darkgreen 1 FAIL newgreen 2\n',
370 NOT_CONSIDERED,
371 [r'\bnewgreen\b.*\(- to .*>2<',
372 '<li>autopkgtest for lightgreen 1: PASS',
373 '<li>autopkgtest for darkgreen 1: FAIL'])
374
375 def test_binary_from_new_source_package_pass(self):
376 '''building an existing binary for a new source package (pass)'''
377
378 self.do_test(
379 [('libgreen1', {'Version': '2', 'Source': 'newgreen', 'Depends': 'libc6'})],
380 'lightgreen 1 PASS newgreen 2\n'
381 'darkgreen 1 PASS newgreen 2\n',
382 VALID_CANDIDATE,
383 [r'\bnewgreen\b.*\(- to .*>2<',
384 '<li>autopkgtest for lightgreen 1: PASS',
385 '<li>autopkgtest for darkgreen 1: PASS'])
386
387 def test_binary_from_new_source_package_uninst(self):
388 '''building an existing binary for a new source package (uninstallable)'''
389
390 self.do_test(
391 [('libgreen1', {'Version': '2', 'Source': 'newgreen', 'Depends': 'libc6, nosuchpkg'})],
392 'darkgreen 1 FAIL newgreen 2\n',
393 NOT_CONSIDERED,
394 [r'\bnewgreen\b.*\(- to .*>2<',
395 'libgreen1/amd64 unsatisfiable Depends: nosuchpkg'],
396 # autopkgtest should not be triggered for uninstallable pkg
397 ['autopkgtest'])
398
399 @unittest.expectedFailure
400 def test_result_from_older_version(self):
401 '''test result from older version than the uploaded one'''
402
403 self.do_test(
404 [('green', {'Version': '1.1~beta', 'Depends': 'libc6 (>= 0.9), libgreen1'})],
405 'green 1.1~alpha PASS green 1.1~beta\n',
406 NOT_CONSIDERED,
407 [r'\bgreen\b.*>1</a> to .*>1.1~beta<',
408 # it's not entirely clear what precisely it should say here
409 '<li>autopkgtest for green 1.1~beta: RUNNING'])
410
411 def do_test(self, unstable_add, adt_request, considered, expect=None,
412 no_expect=None):
413 for (pkg, fields) in unstable_add:
414 self.data.add(pkg, True, fields)
415
416 self.make_adt_britney(adt_request)
417
418 (excuses, out) = self.run_britney()
419 #print('-------\nexcuses: %s\n-----' % excuses)
420 #print('-------\nout: %s\n-----' % out)
421 #print('run:\n%s -c %s\n' % (self.britney, self.britney_conf))
422 #subprocess.call(['bash', '-i'], cwd=self.data.path)
423 if considered:
424 self.assertIn('Valid candidate', excuses)
425 else:
426 self.assertIn('Not considered', excuses)
427
428 if expect:
429 for re in expect:
430 self.assertRegexpMatches(excuses, re)
431 if no_expect:
432 for re in no_expect:
433 self.assertNotRegexpMatches(excuses, re)
434
435 def shell(self):
436 # uninstallable unstable version
437 self.data.add('yellow', True, {'Version': '1.1~beta',
438 'Depends': 'libc6 (>= 0.9), nosuchpkg'})
439
440 self.make_adt_britney('yellow 1.1~beta RUNNING yellow 1.1~beta\n')
441
442 print('run:\n%s -c %s\n' % (self.britney, self.britney_conf))
443 subprocess.call(['bash', '-i'], cwd=self.data.path)
444
445
446unittest.main()

Subscribers

People subscribed via source and target branches