diff --git a/tests/autopkgtest.py b/tests/autopkgtest.py new file mode 100755 index 0000000..308767a --- /dev/null +++ b/tests/autopkgtest.py @@ -0,0 +1,446 @@ +#!/usr/bin/python +# (C) 2014 Canonical Ltd. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +import tempfile +import shutil +import os +import sys +import subprocess +import unittest + +architectures = ['amd64', 'arm64', 'armhf', 'i386', 'powerpc', 'ppc64el'] + +my_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +NOT_CONSIDERED = False +VALID_CANDIDATE = True + + +class TestData: + def __init__(self): + '''Construct local test package indexes. + + The archive is initially empty. You can create new packages with + create_deb(). self.path contains the path of the archive, and + self.apt_source provides an apt source "deb" line. + + It is kept in a temporary directory which gets removed when the Archive + object gets deleted. + ''' + self.path = tempfile.mkdtemp(prefix='testarchive.') + self.apt_source = 'deb file://%s /' % self.path + self.dirs = {False: os.path.join(self.path, 'data', 'testing'), + True: os.path.join(self.path, 'data', 'unstable')} + os.makedirs(self.dirs[False]) + os.mkdir(self.dirs[True]) + self.added_sources = {False: set(), True: set()} + self.added_binaries = {False: set(), True: set()} + + # pre-create all files for all architectures + for arch in architectures: + for dir in self.dirs.values(): + with open(os.path.join(dir, 'Packages_' + arch), 'w'): + pass + for dir in self.dirs.values(): + for fname in ['Dates', 'Blocks']: + with open(os.path.join(dir, fname), 'w'): + pass + for dname in ['Hints']: + os.mkdir(os.path.join(dir, dname)) + + os.mkdir(os.path.join(self.path, 'output')) + + # create temporary home dir for proposed-migration autopktest status + self.home = os.path.join(self.path, 'home') + os.environ['HOME'] = self.home + os.makedirs(os.path.join(self.home, 'proposed-migration', + 'autopkgtest', 'work')) + + def __del__(self): + shutil.rmtree(self.path) + + def add(self, name, unstable, fields={}, add_src=True): + '''Add a binary package to the index file. + + You need to specify at least the package name and in which list to put + it (unstable==True for unstable/proposed, or False for + testing/release). fields specifies all additional entries, e. g. + {'Depends': 'foo, bar', 'Conflicts: baz'}. There are defaults for most + fields. + + Unless add_src is set to False, this will also automatically create a + source record, based on fields['Source'] and name. + ''' + assert (name not in self.added_binaries[unstable]) + self.added_binaries[unstable].add(name) + + fields.setdefault('Architecture', architectures[0]) + fields.setdefault('Version', '1') + fields.setdefault('Priority', 'optional') + fields.setdefault('Section', 'devel') + fields.setdefault('Description', 'test pkg') + if fields['Architecture'] == 'all': + for a in architectures: + self._append(name, unstable, 'Packages_' + a, fields) + else: + self._append(name, unstable, 'Packages_' + fields['Architecture'], + fields) + + if add_src: + src = fields.get('Source', name) + if src not in self.added_sources[unstable]: + self.add_src(src, unstable, {'Version': fields['Version'], + 'Section': fields['Section']}) + + def add_src(self, name, unstable, fields={}): + '''Add a source package to the index file. + + You need to specify at least the package name and in which list to put + it (unstable==True for unstable/proposed, or False for + testing/release). fields specifies all additional entries, which can be + Version (default: 1), Section (default: devel), and Extra-Source-Only. + ''' + assert (name not in self.added_sources[unstable]) + self.added_sources[unstable].add(name) + + fields.setdefault('Version', '1') + fields.setdefault('Section', 'devel') + self._append(name, unstable, 'Sources', fields) + + def _append(self, name, unstable, file_name, fields): + with open(os.path.join(self.dirs[unstable], file_name), 'a') as f: + f.write('''Package: %s +Maintainer: Joe +''' % name) + + for k, v in fields.items(): + f.write('%s: %s\n' % (k, v)) + f.write('\n') + + +class Test(unittest.TestCase): + def setUp(self): + self.data = TestData() + + # add a bunch of packages to testing to avoid repetition + self.data.add('libc6', False) + self.data.add('libgreen1', False, {'Source': 'green', + 'Depends': 'libc6 (>= 0.9)'}) + self.data.add('green', False, {'Depends': 'libc6 (>= 0.9), libgreen1', + 'Conflicts': 'blue'}) + self.data.add('lightgreen', False, {'Depends': 'libgreen1'}) + self.data.add('darkgreen', False, {'Depends': 'libgreen1'}) + self.data.add('blue', False, {'Depends': 'libc6 (>= 0.9)', + 'Conflicts': 'green'}) + self.data.add('justdata', False, {'Architecture': 'all'}) + + self.britney = os.path.join(my_dir, 'britney.py') + self.britney_conf = os.path.join(my_dir, 'britney.conf') + assert os.path.exists(self.britney) + assert os.path.exists(self.britney_conf) + + # fake adt-britney script + self.adt_britney = os.path.join(self.data.home, 'auto-package-testing', + 'jenkins', 'adt-britney') + os.makedirs(os.path.dirname(self.adt_britney)) + + with open(self.adt_britney, 'w') as f: + f.write('''#!/bin/sh -e +echo "$@" >> /%s/adt-britney.log ''' % self.data.path) + os.chmod(self.adt_britney, 0o755) + + def tearDown(self): + del self.data + + def make_adt_britney(self, request): + with open(self.adt_britney, 'w') as f: + f.write('''#!%(py)s +import argparse, shutil,sys + +def request(): + if args.req: + shutil.copy(args.req, '%(path)s/adt-britney.requestarg') + with open(args.output, 'w') as f: + f.write("""%(rq)s""".replace('PASS', 'NEW').replace('FAIL', 'NEW').replace('RUNNING', 'NEW')) + +def submit(): + with open(args.req, 'w') as f: + f.write("""%(rq)s""".replace('PASS', 'RUNNING'). + replace('FAIL', 'RUNNING')) + +def collect(): + with open(args.output, 'w') as f: + f.write("""%(rq)s""") + +p = argparse.ArgumentParser() +p.add_argument('-c', '--config') +p.add_argument('-a', '--arch') +p.add_argument('-r', '--release') +p.add_argument('-P', '--use-proposed', action='store_true') +p.add_argument('-d', '--debug', action='store_true') +p.add_argument('-U', '--no-update', action='store_true') +sp = p.add_subparsers() + +prequest = sp.add_parser('request') +prequest.add_argument('-O', '--output') +prequest.add_argument('req', nargs='?') +prequest.set_defaults(func=request) + +psubmit = sp.add_parser('submit') +psubmit.add_argument('req') +psubmit.set_defaults(func=submit) + +pcollect = sp.add_parser('collect') +pcollect.add_argument('-O', '--output') +pcollect.add_argument('-n', '--new-only', action='store_true', default=False) +pcollect.set_defaults(func=collect) + +args = p.parse_args() +args.func() +''' % {'py': sys.executable, 'path': self.data.path, 'rq': request}) + + def run_britney(self, args=[]): + '''Run britney. + + Assert that it succeeds and does not produce anything on stderr. + Return (excuses.html, britney_out). + ''' + britney = subprocess.Popen([self.britney, '-v', '-c', self.britney_conf], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=self.data.path, + universal_newlines=True) + (out, err) = britney.communicate() + self.assertEqual(britney.returncode, 0, out + err) + self.assertEqual(err, '') + + with open(os.path.join(self.data.path, 'output', 'excuses.html')) as f: + excuses = f.read() + + return (excuses, out) + + def test_no_request_for_uninstallable(self): + '''Does not request a test for an uninstallable package''' + + self.do_test( + # uninstallable unstable version + [('green', {'Version': '1.1~beta', 'Depends': 'libc6 (>= 0.9), libgreen1 (>= 2)'})], + 'green 1.1~beta RUNNING green 1.1~beta\n', + NOT_CONSIDERED, + [r'\bgreen\b.*>1 to .*>1.1~beta<', + 'green/amd64 unsatisfiable Depends: libgreen1 \(>= 2\)'], + # autopkgtest should not be triggered for uninstallable pkg + ['autopkgtest']) + + def test_request_for_installable_running(self): + '''Requests a test for an installable package, test still running''' + + self.do_test( + [('green', {'Version': '1.1~beta', 'Depends': 'libc6 (>= 0.9), libgreen1'})], + 'green 1.1~beta RUNNING green 1.1~beta\n', + NOT_CONSIDERED, + [r'\bgreen\b.*>1 to .*>1.1~beta<', + '
  • autopkgtest for green 1.1~beta: RUNNING']) + + def test_request_for_installable_fail(self): + '''Requests a test for an installable package, test fail''' + + self.do_test( + [('green', {'Version': '1.1~beta', 'Depends': 'libc6 (>= 0.9), libgreen1'})], + 'green 1.1~beta FAIL green 1.1~beta\n', + NOT_CONSIDERED, + [r'\bgreen\b.*>1 to .*>1.1~beta<', + '
  • autopkgtest for green 1.1~beta: FAIL']) + + def test_request_for_installable_pass(self): + '''Requests a test for an installable package, test pass''' + + self.do_test( + [('green', {'Version': '1.1~beta', 'Depends': 'libc6 (>= 0.9), libgreen1'})], + 'green 1.1~beta PASS green 1.1~beta\n', + VALID_CANDIDATE, + [r'\bgreen\b.*>1 to .*>1.1~beta<', + '
  • autopkgtest for green 1.1~beta: PASS']) + + def test_multi_rdepends_with_tests_running(self): + '''Multiple reverse dependencies with tests (still running)''' + + self.do_test( + [('libgreen1', {'Version': '2', 'Source': 'green', 'Depends': 'libc6'})], + 'lightgreen 1 PASS green 2\n' + 'darkgreen 1 RUNNING green 2\n', + NOT_CONSIDERED, + [r'\bgreen\b.*>1 to .*>2<', + '
  • autopkgtest for lightgreen 1: PASS', + '
  • autopkgtest for darkgreen 1: RUNNING']) + + def test_multi_rdepends_with_tests_fail(self): + '''Multiple reverse dependencies with tests (fail)''' + + self.do_test( + [('libgreen1', {'Version': '2', 'Source': 'green', 'Depends': 'libc6'})], + 'lightgreen 1 PASS green 2\n' + 'darkgreen 1 FAIL green 2\n', + NOT_CONSIDERED, + [r'\bgreen\b.*>1 to .*>2<', + '
  • autopkgtest for lightgreen 1: PASS', + '
  • autopkgtest for darkgreen 1: FAIL']) + + def test_multi_rdepends_with_tests_pass(self): + '''Multiple reverse dependencies with tests (pass)''' + + self.do_test( + [('libgreen1', {'Version': '2', 'Source': 'green', 'Depends': 'libc6'})], + 'lightgreen 1 PASS green 2\n' + 'darkgreen 1 PASS green 2\n', + VALID_CANDIDATE, + [r'\bgreen\b.*>1 to .*>2<', + '
  • autopkgtest for lightgreen 1: PASS', + '
  • autopkgtest for darkgreen 1: PASS']) + + def test_multi_rdepends_with_some_tests_running(self): + '''Multiple reverse dependencies with some tests (running)''' + + # add a third reverse dependency to libgreen1 which does not have a test + self.data.add('mint', False, {'Depends': 'libgreen1'}) + + self.do_test( + [('libgreen1', {'Version': '2', 'Source': 'green', 'Depends': 'libc6'})], + 'lightgreen 1 RUNNING green 2\n' + 'darkgreen 1 RUNNING green 2\n', + NOT_CONSIDERED, + [r'\bgreen\b.*>1 to .*>2<', + '
  • autopkgtest for lightgreen 1: RUNNING', + '
  • autopkgtest for darkgreen 1: RUNNING']) + + def test_multi_rdepends_with_some_tests_fail(self): + '''Multiple reverse dependencies with some tests (fail)''' + + # add a third reverse dependency to libgreen1 which does not have a test + self.data.add('mint', False, {'Depends': 'libgreen1'}) + + self.do_test( + [('libgreen1', {'Version': '2', 'Source': 'green', 'Depends': 'libc6'})], + 'lightgreen 1 PASS green 2\n' + 'darkgreen 1 FAIL green 2\n', + NOT_CONSIDERED, + [r'\bgreen\b.*>1 to .*>2<', + '
  • autopkgtest for lightgreen 1: PASS', + '
  • autopkgtest for darkgreen 1: FAIL']) + + def test_multi_rdepends_with_some_tests_pass(self): + '''Multiple reverse dependencies with some tests (pass)''' + + # add a third reverse dependency to libgreen1 which does not have a test + self.data.add('mint', False, {'Depends': 'libgreen1'}) + + self.do_test( + [('libgreen1', {'Version': '2', 'Source': 'green', 'Depends': 'libc6'})], + 'lightgreen 1 PASS green 2\n' + 'darkgreen 1 PASS green 2\n', + VALID_CANDIDATE, + [r'\bgreen\b.*>1 to .*>2<', + '
  • autopkgtest for lightgreen 1: PASS', + '
  • autopkgtest for darkgreen 1: PASS']) + + def test_binary_from_new_source_package_running(self): + '''building an existing binary for a new source package (running)''' + + self.do_test( + [('libgreen1', {'Version': '2', 'Source': 'newgreen', 'Depends': 'libc6'})], + 'lightgreen 1 PASS newgreen 2\n' + 'darkgreen 1 RUNNING newgreen 2\n', + NOT_CONSIDERED, + [r'\bnewgreen\b.*\(- to .*>2<', + '
  • autopkgtest for lightgreen 1: PASS', + '
  • autopkgtest for darkgreen 1: RUNNING']) + + def test_binary_from_new_source_package_fail(self): + '''building an existing binary for a new source package (fail)''' + + self.do_test( + [('libgreen1', {'Version': '2', 'Source': 'newgreen', 'Depends': 'libc6'})], + 'lightgreen 1 PASS newgreen 2\n' + 'darkgreen 1 FAIL newgreen 2\n', + NOT_CONSIDERED, + [r'\bnewgreen\b.*\(- to .*>2<', + '
  • autopkgtest for lightgreen 1: PASS', + '
  • autopkgtest for darkgreen 1: FAIL']) + + def test_binary_from_new_source_package_pass(self): + '''building an existing binary for a new source package (pass)''' + + self.do_test( + [('libgreen1', {'Version': '2', 'Source': 'newgreen', 'Depends': 'libc6'})], + 'lightgreen 1 PASS newgreen 2\n' + 'darkgreen 1 PASS newgreen 2\n', + VALID_CANDIDATE, + [r'\bnewgreen\b.*\(- to .*>2<', + '
  • autopkgtest for lightgreen 1: PASS', + '
  • autopkgtest for darkgreen 1: PASS']) + + def test_binary_from_new_source_package_uninst(self): + '''building an existing binary for a new source package (uninstallable)''' + + self.do_test( + [('libgreen1', {'Version': '2', 'Source': 'newgreen', 'Depends': 'libc6, nosuchpkg'})], + 'darkgreen 1 FAIL newgreen 2\n', + NOT_CONSIDERED, + [r'\bnewgreen\b.*\(- to .*>2<', + 'libgreen1/amd64 unsatisfiable Depends: nosuchpkg'], + # autopkgtest should not be triggered for uninstallable pkg + ['autopkgtest']) + + @unittest.expectedFailure + def test_result_from_older_version(self): + '''test result from older version than the uploaded one''' + + self.do_test( + [('green', {'Version': '1.1~beta', 'Depends': 'libc6 (>= 0.9), libgreen1'})], + 'green 1.1~alpha PASS green 1.1~beta\n', + NOT_CONSIDERED, + [r'\bgreen\b.*>1 to .*>1.1~beta<', + # it's not entirely clear what precisely it should say here + '
  • autopkgtest for green 1.1~beta: RUNNING']) + + def do_test(self, unstable_add, adt_request, considered, expect=None, + no_expect=None): + for (pkg, fields) in unstable_add: + self.data.add(pkg, True, fields) + + self.make_adt_britney(adt_request) + + (excuses, out) = self.run_britney() + #print('-------\nexcuses: %s\n-----' % excuses) + #print('-------\nout: %s\n-----' % out) + #print('run:\n%s -c %s\n' % (self.britney, self.britney_conf)) + #subprocess.call(['bash', '-i'], cwd=self.data.path) + if considered: + self.assertIn('Valid candidate', excuses) + else: + self.assertIn('Not considered', excuses) + + if expect: + for re in expect: + self.assertRegexpMatches(excuses, re) + if no_expect: + for re in no_expect: + self.assertNotRegexpMatches(excuses, re) + + def shell(self): + # uninstallable unstable version + self.data.add('yellow', True, {'Version': '1.1~beta', + 'Depends': 'libc6 (>= 0.9), nosuchpkg'}) + + self.make_adt_britney('yellow 1.1~beta RUNNING yellow 1.1~beta\n') + + print('run:\n%s -c %s\n' % (self.britney, self.britney_conf)) + subprocess.call(['bash', '-i'], cwd=self.data.path) + + +unittest.main()