mirror of
https://git.launchpad.net/~ubuntu-release/britney/+git/britney2-ubuntu
synced 2025-02-13 15:37:02 +00:00
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:
parent
a3a523b4ed
commit
335073e901
171
autopkgtest.py
171
autopkgtest.py
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user