Add requesting tests through AMQP

Until now, autopkgtests were triggered via an external "adt-britney" command
from lp:auto-package-testing. This duplicated a lot of effort (such as its own
chdist and reverse dependency calculation), maintains a lot of state files,
uses hardcoded absolute paths to these external tools, and is quite hard to
understand and maintain. We want to get rid of all this.

Add logic to AutoPackageTest.request() to use britney's existing reverse
dependency maps and figure out the set of tests to run for packages in
unstable. These are being tracked in "requested_tests".

Add logic to AutoPackageTest.submit() to send test requests to the AMQP server
specified in the new "ADT_AMQP" config key. For testing this can be a file://
URL, and if not set no test requests will be sent at all.

The set of tests which were requested in previous runs are tracked in
UNSTABLE/autopkgtest/pending.txt, so that we don't re-request tests in
subsequent runs.

There is no implementation for collect() and results() yet, these will be done
in a separate commit.

Add initial test cases.
This commit is contained in:
Martin Pitt 2015-07-02 17:41:49 +02:00
parent a3a523b4ed
commit 335073e901
3 changed files with 303 additions and 1 deletions

View File

@ -26,6 +26,10 @@ from textwrap import dedent
import time
import apt_pkg
import kombu
from consts import (AUTOPKGTEST, BINARIES, RDEPENDS, SOURCE)
adt_britney = os.path.expanduser("~/auto-package-testing/jenkins/adt-britney")
@ -53,7 +57,19 @@ class AutoPackageTest(object):
self.series = series
self.debug = debug
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:
@ -62,6 +78,88 @@ class AutoPackageTest(object):
def log_error(self, msg):
print('E: [%s] - %s' % (time.asctime(), msg))
#
# AMQP/cloud interface helpers
#
def read_pending_tests(self):
'''Read pending test requests from previous britney runs
Read test_state_dir/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 test_state_dir/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)
# 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):
if self.rc_path:
return
@ -144,10 +242,48 @@ class AutoPackageTest(object):
command.extend(args)
subprocess.check_call(command)
#
# Public API
#
def request(self, packages, excludes=None):
if excludes is None:
excludes = []
self.log_verbose('Requested autopkgtests for %s, exclusions: %s' %
(['%s/%s' % i for i in packages], str(excludes)))
# add new test requests to self.requested_tests from reverse
# dependencies, unless they are already in self.pending_tests
# build a source -> {triggering-source1, ...} map from reverse
# dependencies
sources_info = self.britney.sources['unstable']
# FIXME: For now assume that amd64 has all binaries that we are
binaries_info = self.britney.binaries['unstable']['amd64'][0]
# interested in for reverse dependency checking
for src, ver in packages:
srcinfo = sources_info[src]
# we want to test the package itself, if it still has a test in
# unstable
if srcinfo[AUTOPKGTEST] and src not in excludes:
self.add_test_request(src, ver, 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
for rdep in binaries_info[binary][RDEPENDS]:
rdep_src = binaries_info[rdep][SOURCE]
if sources_info[rdep_src][AUTOPKGTEST] and rdep_src not in excludes:
# we don't care about the version of rdep
self.add_test_request(rdep_src, None, src, ver)
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()
request_path = self._request_path
if os.path.exists(request_path):
@ -200,6 +336,39 @@ class AutoPackageTest(object):
pass
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:
amqp_queue = conn.SimpleQueue(q)
for pkg in self.requested_tests:
amqp_queue.put(pkg)
amqp_queue.close()
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()
request_path = self._request_path
if os.path.exists(request_path):

View File

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

View File

@ -11,6 +11,7 @@ import operator
import os
import sys
import subprocess
import fileinput
import unittest
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -27,9 +28,139 @@ apt_pkg.init()
class TestAutoPkgTest(TestBase):
'''AMQP/cloud interface'''
def setUp(self):
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.
with open(self.britney_conf, 'r') as fp: