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

Subscribers

People subscribed via source and target branches