Merge lp:~pitti/britney/britney2-ubuntu-amqp

bzr-import-20160707
Steve Langasek 10 years ago
commit 2f9f5fd0eb

@ -26,6 +26,10 @@ from textwrap import dedent
import time import time
import apt_pkg import apt_pkg
import kombu
from consts import (AUTOPKGTEST, BINARIES, RDEPENDS, SOURCE)
adt_britney = os.path.expanduser("~/auto-package-testing/jenkins/adt-britney") adt_britney = os.path.expanduser("~/auto-package-testing/jenkins/adt-britney")
@ -53,7 +57,139 @@ class AutoPackageTest(object):
self.series = series self.series = series
self.debug = debug self.debug = debug
self.read() self.read()
self.rc_path = None self.rc_path = None # for adt-britney, obsolete
self.test_state_dir = os.path.join(britney.options.unstable,
'autopkgtest')
# map of requested tests from request()
# src -> ver -> {(triggering-src1, ver1), ...}
self.requested_tests = {}
# same map for tests requested in previous runs
self.pending_tests = None
self.pending_tests_file = os.path.join(self.test_state_dir, 'pending.txt')
if not os.path.isdir(self.test_state_dir):
os.mkdir(self.test_state_dir)
self.read_pending_tests()
def log_verbose(self, msg):
if self.britney.options.verbose:
print('I: [%s] - %s' % (time.asctime(), msg))
def log_error(self, msg):
print('E: [%s] - %s' % (time.asctime(), msg))
def tests_for_source(self, src, ver):
'''Iterate over all tests that should be run for given source'''
sources_info = self.britney.sources['unstable']
# FIXME: For now assume that amd64 has all binaries that we are
# interested in for reverse dependency checking
binaries_info = self.britney.binaries['unstable']['amd64'][0]
srcinfo = sources_info[src]
# we want to test the package itself, if it still has a test in
# unstable
if srcinfo[AUTOPKGTEST]:
yield (src, ver)
# plus all direct reverse dependencies of its binaries which have
# an autopkgtest
for binary in srcinfo[BINARIES]:
binary = binary.split('/')[0] # chop off arch
try:
rdeps = binaries_info[binary][RDEPENDS]
except KeyError:
self.log_verbose('Ignoring nonexistant binary %s (FTBFS/NBS)?' % binary)
continue
for rdep in rdeps:
rdep_src = binaries_info[rdep][SOURCE]
if sources_info[rdep_src][AUTOPKGTEST]:
# we don't care about the version of rdep
yield (rdep_src, None)
#
# AMQP/cloud interface helpers
#
def read_pending_tests(self):
'''Read pending test requests from previous britney runs
Read UNSTABLE/autopkgtest/requested.txt with the format:
srcpkg srcver triggering-srcpkg triggering-srcver
Initialize self.pending_tests with that data.
'''
assert self.pending_tests is None, 'already initialized'
self.pending_tests = {}
if not os.path.exists(self.pending_tests_file):
self.log_verbose('No %s, starting with no pending tests' %
self.pending_tests_file)
return
with open(self.pending_tests_file) as f:
for l in f:
l = l.strip()
if not l:
continue
try:
(src, ver, trigsrc, trigver) = l.split()
except ValueError:
self.log_error('ignoring malformed line in %s: %s' %
(self.pending_tests_file, l))
continue
if ver == '-':
ver = None
if trigver == '-':
trigver = None
self.pending_tests.setdefault(src, {}).setdefault(
ver, set()).add((trigsrc, trigver))
self.log_verbose('Read pending requested tests from %s: %s' %
(self.pending_tests_file, self.pending_tests))
def update_pending_tests(self):
'''Update pending tests after submitting requested tests
Update UNSTABLE/autopkgtest/requested.txt, see read_pending_tests() for
the format.
'''
# merge requested_tests into pending_tests
for src, verinfo in self.requested_tests.items():
for ver, triggers in verinfo.items():
self.pending_tests.setdefault(src, {}).setdefault(
ver, set()).update(triggers)
self.requested_tests = {}
# write it
with open(self.pending_tests_file + '.new', 'w') as f:
for src in sorted(self.pending_tests):
for ver in sorted(self.pending_tests[src]):
for (trigsrc, trigver) in sorted(self.pending_tests[src][ver]):
if ver is None:
ver = '-'
if trigver is None:
trigver = '-'
f.write('%s %s %s %s\n' % (src, ver, trigsrc, trigver))
os.rename(self.pending_tests_file + '.new', self.pending_tests_file)
self.log_verbose('Updated pending requested tests in %s' %
self.pending_tests_file)
def add_test_request(self, src, ver, trigsrc, trigver):
'''Add one test request to the local self.requested_tests queue
This will only be done if that test wasn't already requested in a
previous run, i. e. it is already in self.pending_tests.
versions can be None if you don't care about the particular version.
'''
if (trigsrc, trigver) in self.pending_tests.get(src, {}).get(ver, set()):
self.log_verbose('test %s/%s for %s/%s is already pending, not queueing' %
(src, ver, trigsrc, trigver))
return
self.requested_tests.setdefault(src, {}).setdefault(
ver, set()).add((trigsrc, trigver))
#
# obsolete adt-britney helpers
#
def _ensure_rc_file(self): def _ensure_rc_file(self):
if self.rc_path: if self.rc_path:
@ -137,10 +273,30 @@ class AutoPackageTest(object):
command.extend(args) command.extend(args)
subprocess.check_call(command) subprocess.check_call(command)
#
# Public API
#
def request(self, packages, excludes=None): def request(self, packages, excludes=None):
if excludes is None: if excludes is None:
excludes = [] excludes = []
self.log_verbose('Requested autopkgtests for %s, exclusions: %s' %
(['%s/%s' % i for i in packages], str(excludes)))
for src, ver in packages:
for (testsrc, testver) in self.tests_for_source(src, ver):
if testsrc not in excludes:
self.add_test_request(testsrc, testver, src, ver)
if self.britney.options.verbose:
for src, verinfo in self.requested_tests.items():
for ver, triggers in verinfo.items():
self.log_verbose('Requesting %s/%s autopkgtest to verify %s' %
(src, ver, ', '.join(['%s/%s' % i for i in triggers])))
# deprecated requests for old Jenkins/lp:auto-package-testing, will go
# away
self._ensure_rc_file() self._ensure_rc_file()
request_path = self._request_path request_path = self._request_path
if os.path.exists(request_path): if os.path.exists(request_path):
@ -164,9 +320,8 @@ class AutoPackageTest(object):
request_file.write(line) request_file.write(line)
else: else:
if self.britney.options.verbose: if self.britney.options.verbose:
print("I: [%s] - Requested autopkgtest for %s but " self.log_verbose("Requested autopkgtest for %s but "
"run_autopkgtest set to False" % "run_autopkgtest set to False" % src)
(time.asctime(), src))
for linebits in self._parse(request_path): for linebits in self._parse(request_path):
# Make sure that there's an entry in pkgcauses for each new # Make sure that there's an entry in pkgcauses for each new
@ -176,8 +331,8 @@ class AutoPackageTest(object):
src = linebits.pop(0) src = linebits.pop(0)
ver = linebits.pop(0) ver = linebits.pop(0)
if self.britney.options.verbose: if self.britney.options.verbose:
print("I: [%s] - Requested autopkgtest for %s_%s (%s)" % self.log_verbose("Requested autopkgtest for %s_%s (%s)" %
(time.asctime(), src, ver, " ".join(linebits))) (src, ver, " ".join(linebits)))
try: try:
status = linebits.pop(0).upper() status = linebits.pop(0).upper()
while True: while True:
@ -194,6 +349,40 @@ class AutoPackageTest(object):
pass pass
def submit(self): def submit(self):
# send AMQP requests for new test requests
# TODO: Once we support version constraints in AMQP requests, add them
queues = ['debci-%s-%s' % (self.series, arch)
for arch in self.britney.options.adt_arches.split()]
try:
amqp_url = self.britney.options.adt_amqp
except AttributeError:
self.log_error('ADT_AMQP not set, cannot submit requests')
return
if amqp_url.startswith('amqp://'):
with kombu.Connection(amqp_url) as conn:
for q in queues:
# don't use SimpleQueue here as it always declares queues;
# ACLs might not allow that
with kombu.Producer(conn, routing_key=q, auto_declare=False) as p:
for pkg in self.requested_tests:
p.publish(pkg)
elif amqp_url.startswith('file://'):
# in testing mode, adt_amqp will be a file:// URL
with open(amqp_url[7:], 'a') as f:
for pkg in self.requested_tests:
for q in queues:
f.write('%s:%s\n' % (q, pkg))
else:
self.log_error('Unknown ADT_AMQP schema in %s' %
self.britney.options.adt_amqp)
# mark them as pending now
self.update_pending_tests()
# deprecated requests for old Jenkins/lp:auto-package-testing, will go
# away
self._ensure_rc_file() self._ensure_rc_file()
request_path = self._request_path request_path = self._request_path
if os.path.exists(request_path): if os.path.exists(request_path):
@ -211,10 +400,9 @@ class AutoPackageTest(object):
for trigsrc in sorted(self.pkglist[src][ver]['causes']): for trigsrc in sorted(self.pkglist[src][ver]['causes']):
for trigver, status \ for trigver, status \
in self.pkglist[src][ver]['causes'][trigsrc]: in self.pkglist[src][ver]['causes'][trigsrc]:
print("I: [%s] - Collected autopkgtest status " self.log_verbose("Collected autopkgtest status "
"for %s_%s/%s_%s: " "%s" % ( "for %s_%s/%s_%s: " "%s" %
time.asctime(), src, ver, trigsrc, (src, ver, trigsrc, trigver, status))
trigver, status))
def results(self, trigsrc, trigver): def results(self, trigsrc, trigver):
for status, src, ver in self.pkgcauses[trigsrc][trigver]: for status, src, ver in self.pkgcauses[trigsrc][trigver]:

@ -65,6 +65,8 @@ REMOVE_OBSOLETE = no
ADT_ENABLE = yes ADT_ENABLE = yes
ADT_DEBUG = no ADT_DEBUG = no
ADT_ARCHES = amd64 i386 ADT_ARCHES = amd64 i386
# comment this to disable autopkgtest requests
ADT_AMQP = ampq://user:pwd@amqp.example.com
BOOTTEST_ENABLE = yes BOOTTEST_ENABLE = yes
BOOTTEST_DEBUG = yes BOOTTEST_DEBUG = yes

@ -564,6 +564,7 @@ class Britney(object):
[], [],
get_field('Maintainer'), get_field('Maintainer'),
False, False,
get_field('Testsuite', '').startswith('autopkgtest'),
] ]
return sources return sources

@ -65,3 +65,6 @@ REMOVE_OBSOLETE = no
ADT_ENABLE = yes ADT_ENABLE = yes
ADT_DEBUG = no ADT_DEBUG = no
ADT_ARCHES = amd64 i386 ADT_ARCHES = amd64 i386
# comment this to disable autopkgtest requests
ADT_AMQP = ampq://user:pwd@amqp.example.com

@ -24,6 +24,7 @@ SECTION = 1
BINARIES = 2 BINARIES = 2
MAINTAINER = 3 MAINTAINER = 3
FAKESRC = 4 FAKESRC = 4
AUTOPKGTEST = 5
# binary package # binary package
SOURCE = 2 SOURCE = 2

@ -63,7 +63,7 @@ class TestData:
def __del__(self): def __del__(self):
shutil.rmtree(self.path) shutil.rmtree(self.path)
def add(self, name, unstable, fields={}, add_src=True): def add(self, name, unstable, fields={}, add_src=True, testsuite=None):
'''Add a binary package to the index file. '''Add a binary package to the index file.
You need to specify at least the package name and in which list to put You need to specify at least the package name and in which list to put
@ -73,7 +73,8 @@ class TestData:
fields. fields.
Unless add_src is set to False, this will also automatically create a Unless add_src is set to False, this will also automatically create a
source record, based on fields['Source'] and name. source record, based on fields['Source'] and name. In that case, the
"Testsuite:" field is set to the testsuite argument.
''' '''
assert (name not in self.added_binaries[unstable]) assert (name not in self.added_binaries[unstable])
self.added_binaries[unstable].add(name) self.added_binaries[unstable].add(name)
@ -93,8 +94,11 @@ class TestData:
if add_src: if add_src:
src = fields.get('Source', name) src = fields.get('Source', name)
if src not in self.added_sources[unstable]: if src not in self.added_sources[unstable]:
self.add_src(src, unstable, {'Version': fields['Version'], srcfields = {'Version': fields['Version'],
'Section': fields['Section']}) 'Section': fields['Section']}
if testsuite:
srcfields['Testsuite'] = testsuite
self.add_src(src, unstable, srcfields)
def add_src(self, name, unstable, fields={}): def add_src(self, name, unstable, fields={}):
'''Add a source package to the index file. '''Add a source package to the index file.
@ -102,7 +106,8 @@ class TestData:
You need to specify at least the package name and in which list to put You need to specify at least the package name and in which list to put
it (unstable==True for unstable/proposed, or False for it (unstable==True for unstable/proposed, or False for
testing/release). fields specifies all additional entries, which can be testing/release). fields specifies all additional entries, which can be
Version (default: 1), Section (default: devel), and Extra-Source-Only. Version (default: 1), Section (default: devel), Testsuite (default:
none), and Extra-Source-Only.
''' '''
assert (name not in self.added_sources[unstable]) assert (name not in self.added_sources[unstable])
self.added_sources[unstable].add(name) self.added_sources[unstable].add(name)
@ -128,18 +133,14 @@ class TestBase(unittest.TestCase):
super(TestBase, self).setUp() super(TestBase, self).setUp()
self.data = TestData() self.data = TestData()
self.britney = os.path.join(PROJECT_DIR, 'britney.py') self.britney = os.path.join(PROJECT_DIR, 'britney.py')
self.britney_conf = os.path.join(PROJECT_DIR, 'britney.conf') # create temporary config so that tests can hack it
self.britney_conf = os.path.join(self.data.path, 'britney.conf')
shutil.copy(os.path.join(PROJECT_DIR, 'britney.conf'), self.britney_conf)
assert os.path.exists(self.britney) assert os.path.exists(self.britney)
assert os.path.exists(self.britney_conf)
def tearDown(self): def tearDown(self):
del self.data del self.data
def restore_config(self, content):
"""Helper for restoring configuration contents on cleanup."""
with open(self.britney_conf, 'w') as fp:
fp.write(content)
def run_britney(self, args=[]): def run_britney(self, args=[]):
'''Run britney. '''Run britney.

@ -11,6 +11,7 @@ import operator
import os import os
import sys import sys
import subprocess import subprocess
import fileinput
import unittest import unittest
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -27,9 +28,139 @@ apt_pkg.init()
class TestAutoPkgTest(TestBase): class TestAutoPkgTest(TestBase):
'''AMQP/cloud interface'''
def setUp(self): def setUp(self):
super(TestAutoPkgTest, self).setUp() super(TestAutoPkgTest, self).setUp()
self.fake_amqp = os.path.join(self.data.path, 'amqp')
# Disable boottests and set fake AMQP server
for line in fileinput.input(self.britney_conf, inplace=True):
if line.startswith('BOOTTEST_ENABLE'):
print('BOOTTEST_ENABLE = no')
elif line.startswith('ADT_AMQP'):
print('ADT_AMQP = file://%s' % self.fake_amqp)
else:
sys.stdout.write(line)
# fake adt-britney script; necessary until we drop that code
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
touch $HOME/proposed-migration/autopkgtest/work/adt.request.series
echo "$@" >> /%s/adt-britney.log ''' % self.data.path)
os.chmod(self.adt_britney, 0o755)
# 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'},
testsuite='autopkgtest')
self.data.add('lightgreen', False, {'Depends': 'libgreen1'},
testsuite='autopkgtest')
# autodep8 or similar test
self.data.add('darkgreen', False, {'Depends': 'libgreen1'},
testsuite='autopkgtest-pkg-foo')
self.data.add('blue', False, {'Depends': 'libc6 (>= 0.9)',
'Conflicts': 'green'},
testsuite='specialtest')
self.data.add('justdata', False, {'Architecture': 'all'})
def do_test(self, unstable_add, considered, excuses_expect=None, excuses_no_expect=None):
for (pkg, fields, testsuite) in unstable_add:
self.data.add(pkg, True, fields, True, testsuite)
(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 excuses_expect:
for re in excuses_expect:
self.assertRegexpMatches(excuses, re)
if excuses_no_expect:
for re in excuses_no_expect:
self.assertNotRegexpMatches(excuses, re)
self.amqp_requests = set()
try:
with open(self.fake_amqp) as f:
for line in f:
self.amqp_requests.add(line.strip())
except IOError:
pass
try:
with open(os.path.join(self.data.path, 'data/series-proposed/autopkgtest/pending.txt')) as f:
self.pending_requests = f.read()
except IOError:
self.pending_requests = None
def test_multi_rdepends_with_tests(self):
'''Multiple reverse dependencies with tests'''
# FIXME: while we only submit requests through AMQP, but don't consider
# their results, we don't expect this to hold back stuff.
self.do_test(
[('libgreen1', {'Version': '2', 'Source': 'green', 'Depends': 'libc6'}, 'autopkgtest')],
VALID_CANDIDATE,
[r'\bgreen\b.*>1</a> to .*>2<'])
# we expect the package's and its reverse dependencies' tests to get
# triggered
self.assertEqual(
self.amqp_requests,
set(['debci-series-i386:green', 'debci-series-amd64:green',
'debci-series-i386:lightgreen', 'debci-series-amd64:lightgreen',
'debci-series-i386:darkgreen', 'debci-series-amd64:darkgreen',
]))
os.unlink(self.fake_amqp)
expected_pending = '''darkgreen - green 2
green 2 green 2
lightgreen - green 2
'''
# ... and that they get recorded as pending
self.assertEqual(self.pending_requests, expected_pending)
# if we run britney again this should *not* trigger any new tests
self.do_test([], VALID_CANDIDATE, [r'\bgreen\b.*>1</a> to .*>2<'])
self.assertEqual(self.amqp_requests, set())
# but the set of pending tests doesn't change
self.assertEqual(self.pending_requests, expected_pending)
def test_no_amqp_config(self):
'''Run without autopkgtest requests'''
# Disable AMQP server config
for line in fileinput.input(self.britney_conf, inplace=True):
if not line.startswith('ADT_AMQP'):
sys.stdout.write(line)
self.do_test(
[('libgreen1', {'Version': '2', 'Source': 'green', 'Depends': 'libc6'}, 'autopkgtest')],
VALID_CANDIDATE,
[r'\bgreen\b.*>1</a> to .*>2<'], ['autopkgtest'])
self.assertEqual(self.amqp_requests, set())
self.assertEqual(self.pending_requests, None)
class TestAdtBritney(TestBase):
'''Legacy adt-britney/lp:auto-package-testing interface'''
def setUp(self):
super(TestAdtBritney, self).setUp()
# Mofify configuration according to the test context. # Mofify configuration according to the test context.
with open(self.britney_conf, 'r') as fp: with open(self.britney_conf, 'r') as fp:
@ -39,7 +170,6 @@ class TestAutoPkgTest(TestBase):
'BOOTTEST_ENABLE = yes', 'BOOTTEST_ENABLE = no') 'BOOTTEST_ENABLE = yes', 'BOOTTEST_ENABLE = no')
with open(self.britney_conf, 'w') as fp: with open(self.britney_conf, 'w') as fp:
fp.write(new_config) fp.write(new_config)
self.addCleanup(self.restore_config, original_config)
# fake adt-britney script # fake adt-britney script
self.adt_britney = os.path.join( self.adt_britney = os.path.join(

@ -143,7 +143,6 @@ class TestBoottestEnd2End(TestBase):
'BOOTTEST_FETCH = yes', 'BOOTTEST_FETCH = no') 'BOOTTEST_FETCH = yes', 'BOOTTEST_FETCH = no')
with open(self.britney_conf, 'w') as fp: with open(self.britney_conf, 'w') as fp:
fp.write(new_config) fp.write(new_config)
self.addCleanup(self.restore_config, original_config)
self.data.add('libc6', False, {'Architecture': 'armhf'}), self.data.add('libc6', False, {'Architecture': 'armhf'}),

Loading…
Cancel
Save