From 32c3f288b010838ecc9844f296085ee75a9c08a0 Mon Sep 17 00:00:00 2001 From: Iain Lane Date: Fri, 21 May 2021 13:36:05 +0100 Subject: [PATCH 01/39] autopkgtest: Check for kernel-triggered tests when migrating kernels We currently skip the ALWAYSFAIL/REGRESSION handling for kernels. This can lead to us missing genuine regressions in kernel uploads. The idea is that results from one kernel flavour shouldn't influence another. We can keep this idea but do better and actually check for regressions: when looking at results, if we're considering a kernel, only look at results which were triggered by this kernel. --- britney2/policies/autopkgtest.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/britney2/policies/autopkgtest.py b/britney2/policies/autopkgtest.py index 40ab279..20452bf 100644 --- a/britney2/policies/autopkgtest.py +++ b/britney2/policies/autopkgtest.py @@ -1164,7 +1164,18 @@ class AutopkgtestPolicy(BasePolicy): # determine current test result status until = self.find_max_lower_force_reset_test(src, ver, arch) - ever_passed = self.check_ever_passed_before(src, ver, arch, until) + + # Special-case triggers from linux-meta*: we cannot compare results + # against different kernels, as e. g. a DKMS module might work against + # the default kernel but fail against a different flavor; so for those, + # filter the considered results to only those against our kernel + if trigger.startswith('linux-meta'): + only_trigger = trigger.split('/', 1)[0] + self.logger.info('This is a kernel; we will only look for results triggered by %s when considering regressions', + trigger) + else: + only_trigger = None + ever_passed = self.check_ever_passed_before(src, ver, arch, until, only_trigger=only_trigger) fail_result = 'REGRESSION' if ever_passed else 'ALWAYSFAIL' @@ -1176,15 +1187,6 @@ class AutopkgtestPolicy(BasePolicy): run_id = r[2] if r[0] in {Result.FAIL, Result.OLD_FAIL}: - # Special-case triggers from linux-meta*: we cannot compare - # results against different kernels, as e. g. a DKMS module - # might work against the default kernel but fail against a - # different flavor; so for those, ignore the "ever - # passed" check; FIXME: check against trigsrc only - if self.options.adt_baseline != 'reference' and \ - (trigger.startswith('linux-meta') or trigger.startswith('linux/')): - baseline_result = Result.FAIL - if baseline_result == Result.FAIL: result = 'ALWAYSFAIL' elif baseline_result in {Result.NONE, Result.OLD_FAIL}: @@ -1256,7 +1258,7 @@ class AutopkgtestPolicy(BasePolicy): return (result, ver, run_id, url) - def check_ever_passed_before(self, src, max_ver, arch, min_ver=None): + def check_ever_passed_before(self, src, max_ver, arch, min_ver=None, only_trigger=None): '''Check if tests for src ever passed on arch for specified range If min_ver is specified, it checks that all versions in @@ -1264,7 +1266,11 @@ class AutopkgtestPolicy(BasePolicy): [min_ver, inf) have passed.''' # FIXME: add caching - for srcmap in self.test_results.values(): + for (trigger, srcmap) in self.test_results.items(): + if only_trigger: + trig = trigger.split('/', 1)[0] + if only_trigger != trig: + continue try: too_high = apt_pkg.version_compare(srcmap[src][arch][1], max_ver) > 0 too_low = apt_pkg.version_compare(srcmap[src][arch][1], min_ver) <= 0 if min_ver else False From 1f520269deb8f582c64b8ac990544f6e5c7a9ba7 Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Tue, 5 Oct 2021 13:21:44 -0700 Subject: [PATCH 02/39] Iterate over binary packages from a source package before iterating over hints The existing code attempts to "short circuit" processing of binaries in unstable by first checking if the package is subject to a removal hint. This is invalid in Ubuntu for two reasons: - we do not use removal hints for removing packages from the release pocket - there are 11,000 hints in Ubuntu (due to force-reset-test) - searching all hints is time-consuming and not a short-circuit at all. Reorder the code so we only scan the hints if there's otherwise an indication of something to do. --- britney2/excusefinder.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/britney2/excusefinder.py b/britney2/excusefinder.py index 75b7497..2b2b2ec 100644 --- a/britney2/excusefinder.py +++ b/britney2/excusefinder.py @@ -97,18 +97,6 @@ class ExcuseFinder(object): source_u.section and excuse.set_section(source_u.section) excuse.set_distribution(self.options.distribution) - # if there is a `remove' hint and the requested version is the same as the - # version in testing, then stop here and return False - # (as a side effect, a removal may generate such excuses for both the source - # package and its binary packages on each architecture) - for hint in self.hints.search('remove', package=src, version=source_t.version): - excuse.add_hint(hint) - excuse.policy_verdict = PolicyVerdict.REJECTED_PERMANENTLY - excuse.add_verdict_info(excuse.policy_verdict, "Removal request by %s" % (hint.user)) - excuse.add_verdict_info(excuse.policy_verdict, "Trying to remove package, not update it") - self.excuses[excuse.name] = excuse - return False - # the starting point is that there is nothing wrong and nothing worth doing anywrongver = False anyworthdoing = False @@ -249,6 +237,19 @@ class ExcuseFinder(object): # nothing worth doing, we don't add an excuse to the list, we just return false return False + # if there is a `remove' hint and the requested version is the same as the + # version in testing, then stop here and return False + # (as a side effect, a removal may generate such excuses for both the source + # package and its binary packages on each architecture) + for hint in self.hints.search('remove', package=src, version=source_t.version): + excuse = Excuse(item) + excuse.add_hint(hint) + excuse.policy_verdict = PolicyVerdict.REJECTED_PERMANENTLY + excuse.add_verdict_info(excuse.policy_verdict, "Removal request by %s" % (hint.user)) + excuse.add_verdict_info(excuse.policy_verdict, "Trying to remove package, not update it") + self.excuses[excuse.name] = excuse + return False + # there is something worth doing # we assume that this package will be ok, if not invalidated below excuse.policy_verdict = PolicyVerdict.PASS From 82fad57aa996cc8e4aacfe5995ee242602283299 Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Wed, 6 Oct 2021 12:54:11 -0700 Subject: [PATCH 03/39] Don't use expensive 'remove' hints Since we've never used remove hints in Ubuntu and never will, we shouldn't pay the cost for them in britney runs. Remove references to them entirely, to speed up britney runs. This is an Ubuntu delta not expected to be upstreamed, and can be dropped at some future point when we don't have 11,000 active hints. (This count of hints is expected to drop dramatically once we have autopkgtest baseline retesting.) --- britney2/excusefinder.py | 13 ------------- britney2/policies/policy.py | 5 ----- 2 files changed, 18 deletions(-) diff --git a/britney2/excusefinder.py b/britney2/excusefinder.py index 2b2b2ec..0395303 100644 --- a/britney2/excusefinder.py +++ b/britney2/excusefinder.py @@ -237,19 +237,6 @@ class ExcuseFinder(object): # nothing worth doing, we don't add an excuse to the list, we just return false return False - # if there is a `remove' hint and the requested version is the same as the - # version in testing, then stop here and return False - # (as a side effect, a removal may generate such excuses for both the source - # package and its binary packages on each architecture) - for hint in self.hints.search('remove', package=src, version=source_t.version): - excuse = Excuse(item) - excuse.add_hint(hint) - excuse.policy_verdict = PolicyVerdict.REJECTED_PERMANENTLY - excuse.add_verdict_info(excuse.policy_verdict, "Removal request by %s" % (hint.user)) - excuse.add_verdict_info(excuse.policy_verdict, "Trying to remove package, not update it") - self.excuses[excuse.name] = excuse - return False - # there is something worth doing # we assume that this package will be ok, if not invalidated below excuse.policy_verdict = PolicyVerdict.PASS diff --git a/britney2/policies/policy.py b/britney2/policies/policy.py index 226ff50..a30f06d 100644 --- a/britney2/policies/policy.py +++ b/britney2/policies/policy.py @@ -1523,11 +1523,6 @@ class ImplicitDependencyPolicy(BasePolicy): # source for pkg not in unstable: candidate for removal return True - source_t = target_suite.sources[src] - for hint in self.hints.search('remove', package=src, version=source_t.version): - # removal hint for the source in testing: candidate for removal - return True - if target_suite.is_cruft(pkg): # if pkg is cruft in testing, removal will be tried return True From 0b4cd56d68b373a5897932691baf76d7b9aaf736 Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Fri, 1 Oct 2021 12:09:36 -0700 Subject: [PATCH 04/39] Initial infrastructure for testing db-based autopkgtest results --- tests/__init__.py | 1 + tests/test_autopkgtest.py | 63 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/tests/__init__.py b/tests/__init__.py index c6cd09d..eaa9436 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -397,6 +397,7 @@ ADT_SHARED_RESULTS_CACHE = ADT_SWIFT_URL = http://localhost:18085 ADT_CI_URL = https://autopkgtest.ubuntu.com/ ADT_HUGE = 20 +ADT_DB_URL = ADT_SUCCESS_BOUNTY = ADT_REGRESSION_PENALTY = diff --git a/tests/test_autopkgtest.py b/tests/test_autopkgtest.py index 801c166..f8960f9 100644 --- a/tests/test_autopkgtest.py +++ b/tests/test_autopkgtest.py @@ -12,6 +12,7 @@ import fileinput import unittest import json import pprint +import sqlite3 import urllib.parse import apt_pkg @@ -44,11 +45,14 @@ class TestAutopkgtestBase(TestBase): def setUp(self): super().setUp() self.fake_amqp = os.path.join(self.data.path, 'amqp') + self.db_path = os.path.join(self.data.path, 'autopkgtest.db') - # Set fake AMQP and Swift server + # Set fake AMQP and Swift server and autopkgtest.db for line in fileinput.input(self.britney_conf, inplace=True): if 'ADT_AMQP' in line: print('ADT_AMQP = file://%s' % self.fake_amqp) + elif 'ADT_DB_URL' in line: + print('ADT_DB_URL = file://%s' % self.db_path) else: sys.stdout.write(line) @@ -82,8 +86,65 @@ class TestAutopkgtestBase(TestBase): self.swift = mock_swift.AutoPkgTestSwiftServer(port=18085) self.swift.set_results({}) + self.db = self.init_sqlite_db(self.db_path) + def tearDown(self): del self.swift + self.db.close() + try: + os.unlink(self.db_path) + except FileNotFoundError: pass + + # https://git.launchpad.net/autopkgtest-cloud/tree/charms/focal/autopkgtest-web/webcontrol/publish-db, + # https://git.launchpad.net/autopkgtest-cloud/tree/charms/focal/autopkgtest-web/webcontrol/helpers/utils.py + def init_sqlite_db(self, path): + """Create DB if it does not exist, and connect to it""" + + db = sqlite3.connect(path) + db.execute("PRAGMA journal_mode = MEMORY") + db.execute( + "CREATE TABLE current_version(" + " release CHAR[20], " + " pocket CHAR[40], " + " component CHAR[10]," + " package CHAR[50], " + " version CHAR[120], " + " PRIMARY KEY(release, package))" + ) + db.execute("CREATE INDEX IF NOT EXISTS current_version_pocket_ix " + "ON current_version(pocket, component)") + + db.execute( + "CREATE TABLE url_last_checked(" + " url CHAR[100], " + " timestamp CHAR[50], " + " PRIMARY KEY(url))" + ) + + db.execute('CREATE TABLE IF NOT EXISTS test (' + ' id INTEGER PRIMARY KEY, ' + ' release CHAR[20], ' + ' arch CHAR[20], ' + ' package char[120])') + db.execute('CREATE TABLE IF NOT EXISTS result (' + ' test_id INTEGER, ' + ' run_id CHAR[30], ' + ' version VARCHAR[200], ' + ' triggers TEXT, ' + ' duration INTEGER, ' + ' exitcode INTEGER, ' + ' requester TEXT, ' + ' PRIMARY KEY(test_id, run_id), ' + ' FOREIGN KEY(test_id) REFERENCES test(id))') + # /packages/ mostly benefits from the index on package (0.8s -> 0.01s), + # but adding the other fields improves it a further 50% to 0.005s. + db.execute('CREATE UNIQUE INDEX IF NOT EXISTS test_package_uix ON test(' + ' package, release, arch)') + db.execute('CREATE INDEX IF NOT EXISTS result_run_ix ON result(' + ' run_id desc)') + + db.commit() + return db def run_it(self, unstable_add, expect_status, expect_excuses={}): '''Run britney with some unstable packages and verify excuses. From 3814d6366672ad9d1e4bfedb153f6e9196b27fad Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Fri, 1 Oct 2021 12:13:57 -0700 Subject: [PATCH 05/39] Make download_retry work with non-HTTP urls urlopen() supports non-http URLs, but when called on them, http-related features are absent - such as getcode(). Make the code work with file:/// URLs. --- britney2/policies/autopkgtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/britney2/policies/autopkgtest.py b/britney2/policies/autopkgtest.py index 20452bf..700ebef 100644 --- a/britney2/policies/autopkgtest.py +++ b/britney2/policies/autopkgtest.py @@ -807,7 +807,7 @@ class AutopkgtestPolicy(BasePolicy): try: req = urlopen(url, timeout=30) code = req.getcode() - if 200 <= code < 300: + if not code or 200 <= code < 300: return req except socket.timeout as e: self.logger.info( From af8890bc4bdb22ec17c0ddf2777cc8b0855211c1 Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Fri, 1 Oct 2021 12:14:50 -0700 Subject: [PATCH 06/39] Preliminary code to make autopkgtest policy download autopkgtest.db Include test to assert britney failure if the database is unavailable --- britney2/policies/autopkgtest.py | 34 ++++++++++++++++++++++++++++++++ tests/test_autopkgtest.py | 23 +++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/britney2/policies/autopkgtest.py b/britney2/policies/autopkgtest.py index 700ebef..555b7be 100644 --- a/britney2/policies/autopkgtest.py +++ b/britney2/policies/autopkgtest.py @@ -124,6 +124,7 @@ class AutopkgtestPolicy(BasePolicy): self.pending_tests_file = os.path.join(self.state_dir, 'autopkgtest-pending.json') self.testsuite_triggers = {} self.result_in_baseline_cache = collections.defaultdict(dict) + self.database = os.path.join(self.state_dir, 'autopkgtest.db') # results map: trigger -> src -> arch -> [passed, version, run_id, seen] # - trigger is "source/version" of an unstable package that triggered @@ -140,6 +141,11 @@ class AutopkgtestPolicy(BasePolicy): else: self.results_cache_file = os.path.join(self.state_dir, 'autopkgtest-results.cache') + if hasattr(self.options,'adt_db_url') and self.options.adt_db_url: + if not self.fetch_db(): + self.logger.error('No autopkgtest db present, exiting') + sys.exit(1) + try: self.options.adt_ppas = self.options.adt_ppas.strip().split() except AttributeError: @@ -157,6 +163,34 @@ class AutopkgtestPolicy(BasePolicy): else: self.logger.info("Ignoring ADT_ARCHES %s as it is not in architectures list", arch) + def fetch_db(self): + f = None + try: + f = self.download_retry(self.options.adt_db_url) + http_code = f.getcode() + # file:/// urls don't have the http niceties + if not http_code or http_code == 200: + new_file = self.database + '.new' + with open(new_file,'wb') as f_out: + while True: + data=f.read(2048*1024) + if not data: + break + f_out.write(data) + if http_code and os.path.getsize(new_file) != int(f.getheader('content-length')): + self.logger.info('Short read downloading autopkgtest results') + os.unlink(new_file) + else: + os.rename(new_file, self.database) + else: + self.logger.error('Failure to fetch autopkgtest results %s: HTTP code=%d', self.options.adt_db_url, f.getcode()) + except IOError as e: + self.logger.error('Failure to fetch autopkgtest results %s: %s', self.options.adt_db_url, str(e)) + finally: + if f is not None: + f.close() + return os.path.exists(self.database) + def register_hints(self, hint_parser): hint_parser.register_hint_type('force-badtest', britney2.hints.split_into_one_hint_per_package) hint_parser.register_hint_type('force-skiptest', britney2.hints.split_into_one_hint_per_package) diff --git a/tests/test_autopkgtest.py b/tests/test_autopkgtest.py index f8960f9..56aef6f 100644 --- a/tests/test_autopkgtest.py +++ b/tests/test_autopkgtest.py @@ -242,6 +242,29 @@ class AT(TestAutopkgtestBase): # Tests for generic packages ################################################################ + def test_fail_on_missing_database(self): + '''Fails if autopkgtest.db is requested but not available''' + + os.unlink(self.db_path) + + self.data.add_default_packages(lightgreen=False) + + britney_failed = 0 + try: + self.run_it( + # uninstallable unstable version + [('lightgreen', {'Version': '1.1~beta', 'Depends': 'libc6 (>= 0.9), libgreen1 (>= 2)'}, 'autopkgtest')], + {'lightgreen': (False, {})}, + {'lightgreen': [('old-version', '1'), ('new-version', '1.1~beta'), + ('reason', 'depends'), + ('excuses', 'uninstallable on arch amd64, not running autopkgtest there') + ] + })[1] + except AssertionError as e: + britney_failed = 1 + + self.assertEqual(britney_failed, 1, "DB missing but britney succeeded") + def test_no_request_for_uninstallable(self): '''Does not request a test for an uninstallable package''' From 173deaff396c038b76e136f63486bc940c239b67 Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Fri, 1 Oct 2021 13:18:47 -0700 Subject: [PATCH 07/39] Support setting test results in swift and sqlite3 simultaneously --- tests/test_autopkgtest.py | 195 +++++++++++++++++++++++--------------- 1 file changed, 116 insertions(+), 79 deletions(-) diff --git a/tests/test_autopkgtest.py b/tests/test_autopkgtest.py index 56aef6f..5c7bb72 100644 --- a/tests/test_autopkgtest.py +++ b/tests/test_autopkgtest.py @@ -146,6 +146,43 @@ class TestAutopkgtestBase(TestBase): db.commit() return db + def set_results(self, results): + '''Wrapper to set autopkgtest results in both swift and sqlite3''' + self.swift.set_results(results) + + # swift bucket name is irrelevant for sqlite + for i in results.values(): + for k,v in i.items(): + (series, arch, discard, source, latest) = k.split('/') + retcode = v[0] + if not v[1]: + source_ver = None + else: + source_ver = v[1].split(' ')[1] + try: + trigger = v[2]['custom_environment'][0].split('=')[1] + except (IndexError, KeyError): + trigger = None + + try: + self.db.execute('INSERT INTO test (release, arch, package) ' + 'VALUES (?, ?, ?)', + (series, arch, source)) + except sqlite3.IntegrityError: + # Completely normal if we have more than one result for + # the same source package; ignore + pass + + self.db.execute('INSERT INTO result ' + '(test_id, run_id, version, triggers, ' + ' exitcode) ' + 'SELECT test.id, ?, ?, ?, ? FROM test ' + 'WHERE release=? AND arch=? AND package=?', + (latest, source_ver, trigger, retcode, + series, arch, source)) + + self.db.commit() + def run_it(self, unstable_add, expect_status, expect_excuses={}): '''Run britney with some unstable packages and verify excuses. @@ -299,7 +336,7 @@ class AT(TestAutopkgtestBase): self.sourceppa_cache['purple'] = {'2': ''} # The package has passed before on i386 - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/p/purple/20150101_100000@': (0, 'purple 1', tr('purple/1')), 'testing/amd64/p/purple/20150101_100000@': (0, 'purple 1', tr('purple/1')), 'testing/amd64/p/purple/20200101_100000@': (0, 'purple 2', tr('purple/2')), @@ -325,7 +362,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(darkgreen=False) # The package has failed before, and with a trigger too on amd64 - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (4, 'green 1'), 'testing/amd64/d/darkgreen/20150101_100000@': (4, 'green 1', tr('failedbefore/1')), }}) @@ -370,7 +407,7 @@ class AT(TestAutopkgtestBase): # green has passed on amd64 before # lightgreen has passed on i386, therefore we should block on it returning - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/amd64/g/green/20150101_100000@': (0, 'green 4', tr('green/1')), 'testing/i386/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('green/1')), }}) @@ -402,7 +439,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) # green has passed before on i386 only, therefore ALWAYSFAIL on amd64 - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/g/green/20150101_100000@': (0, 'green 1', tr('passedbefore/1')), }}) @@ -445,7 +482,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) # green has passed before on i386 only, therefore ALWAYSFAIL on amd64 - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/g/green/20150101_100000@': (0, 'green 1', tr('passedbefore/1')), }}) @@ -463,7 +500,7 @@ class AT(TestAutopkgtestBase): self.assertNotIn('brittle', exc['green']['policy_info']['autopkgtest']) # second run collects the results - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/amd64/d/darkgreen/20150101_100001@': (0, 'darkgreen 1', tr('green/2')), 'testing/i386/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('green/2')), @@ -507,7 +544,7 @@ class AT(TestAutopkgtestBase): # third run should not trigger any new tests, should all be in the # cache - self.swift.set_results({}) + self.set_results({}) out = self.run_it( [], {'green': (True, {'green/2': {'amd64': 'PASS', 'i386': 'PASS'}, @@ -525,7 +562,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) # green has passed before on i386 only, therefore ALWAYSFAIL on amd64 - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/g/green/20150101_100000@': (0, 'green 1', tr('passedbefore/1')), }}) @@ -540,7 +577,7 @@ class AT(TestAutopkgtestBase): {'green': [('old-version', '1'), ('new-version', '2')]}) # second run collects the results - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/amd64/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('green/1')), 'testing/amd64/l/lightgreen/20150101_100101@': (4, 'lightgreen 1', tr('green/2')), @@ -572,7 +609,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1'), 'testing/amd64/l/lightgreen/20150101_100100@': (0, 'lightgreen 1'), 'testing/amd64/l/lightgreen/20150101_100101@': (4, 'lightgreen 1'), @@ -601,7 +638,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/amd64/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/i386/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('green/1')), @@ -657,7 +694,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/amd64/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/i386/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('green/2')), @@ -686,7 +723,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/amd64/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/i386/l/lightgreen/20150101_100100@': (4, 'lightgreen 1', tr('green/1')), @@ -718,7 +755,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) # green has passed before on amd64, doesn't exist on i386 - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/amd64/g/green64/20150101_100000@': (0, 'green64 0.1', tr('passedbefore/1')), }}) @@ -753,7 +790,7 @@ class AT(TestAutopkgtestBase): 'green': ['amd64', 'i386']}}) # second run collects the results - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/amd64/d/darkgreen/20150101_100001@': (0, 'darkgreen 1', tr('green/2')), 'testing/i386/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('green/2')), @@ -844,7 +881,7 @@ class AT(TestAutopkgtestBase): 'Conflicts': 'blue'}, testsuite='autopkgtest', add_src=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/i386/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('green/2')), 'testing/i386/g/green/20150101_100200@': (0, 'green 2', tr('green/2')), @@ -876,7 +913,7 @@ class AT(TestAutopkgtestBase): 'Conflicts': 'blue'}, testsuite='autopkgtest', add_src=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/i386/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('green/2')), 'testing/i386/g/green/20150101_100200@': (0, 'green 2', tr('green/2')), @@ -901,7 +938,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False, lightgreen=False) # old lightgreen fails, thus new green should be held back - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/1.1')), 'testing/amd64/d/darkgreen/20150101_100001@': (0, 'darkgreen 1', tr('green/1.1')), 'testing/i386/l/lightgreen/20150101_100000@': (0, 'lightgreen 1', tr('green/1')), @@ -964,7 +1001,7 @@ class AT(TestAutopkgtestBase): 'debci-testing-i386:lightgreen {"triggers": ["lightgreen/2"]}'])) # next run collects the results - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/l/lightgreen/20150101_100200@': (0, 'lightgreen 2', tr('lightgreen/2')), 'testing/amd64/l/lightgreen/20150101_102000@': (0, 'lightgreen 2', tr('lightgreen/2')), }}) @@ -989,7 +1026,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/amd64/d/darkgreen/20150101_100001@': (0, 'darkgreen 1', tr('green/2')), 'testing/i386/l/lightgreen/20150101_100000@': (0, 'lightgreen 1', tr('green/2')), @@ -1030,7 +1067,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False, lightgreen=False) # old lightgreen fails, thus new green should be held back - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/1.1')), 'testing/amd64/d/darkgreen/20150101_100001@': (0, 'darkgreen 1', tr('green/1.1')), 'testing/i386/l/lightgreen/20150101_100000@': (0, 'lightgreen 1', tr('green/1')), @@ -1065,7 +1102,7 @@ class AT(TestAutopkgtestBase): self.assertEqual(self.pending_requests, {}) # lightgreen 2 stays unbuilt in britney, but we get a test result for it - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/l/lightgreen/20150101_100200@': (0, 'lightgreen 2', tr('green/1.1')), 'testing/amd64/l/lightgreen/20150101_102000@': (0, 'lightgreen 2', tr('green/1.1')), }}) @@ -1095,7 +1132,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False, lightgreen=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/l/lightgreen/20150101_100101@': (0, 'lightgreen 1', tr('lightgreen/1')), }}) @@ -1120,7 +1157,7 @@ class AT(TestAutopkgtestBase): self.assertEqual(len(self.amqp_requests), 6) # we only get a result for lightgreen 2, not for the requested 1 - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/amd64/d/darkgreen/20150101_100001@': (0, 'darkgreen 1', tr('green/2')), 'testing/i386/l/lightgreen/20150101_100100@': (0, 'lightgreen 0.5', tr('green/1')), @@ -1176,7 +1213,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False, lightgreen=False) # green has passed before on i386 only, therefore ALWAYSFAIL on amd64 - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/g/green/20150101_100000@': (0, 'green 1', tr('passedbefore/1')), }}) @@ -1242,7 +1279,7 @@ class AT(TestAutopkgtestBase): self.data.add('brown', False, {'Depends': 'grey'}, testsuite='autopkgtest') self.data.add('brown', True, {'Depends': 'grey'}, testsuite='autopkgtest') - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/amd64/b/black/20150101_100000@': (0, 'black 1', tr('black/1')), 'testing/amd64/b/black/20150102_100000@': (99, 'black blacklisted', tr('black/2')), 'testing/amd64/g/grey/20150101_100000@': (99, 'grey blacklisted', tr('grey/1')), @@ -1271,7 +1308,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(black=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/amd64/b/black/20150101_100000@': (0, 'black 1', tr('black/1')), 'testing/amd64/b/black/20150102_100000@': (99, 'black blacklisted', tr('black/2')), 'testing/i386/b/black/20150101_100000@': (0, 'black 1', tr('black/1')), @@ -1294,7 +1331,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(black=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/amd64/b/black/20150101_100000@': (0, 'black 1', tr('black/1')), 'testing/i386/b/black/20150101_100001@': (0, 'black 1', tr('black/1')), 'testing/amd64/b/black/20150102_100000@': (4, 'black 2', tr('black/2')), @@ -1316,7 +1353,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('newgreen/2')), 'testing/amd64/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('newgreen/2')), 'testing/i386/g/green/20150101_100000@': (0, 'green 1', tr('newgreen/2')), @@ -1345,7 +1382,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(darkgreen=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('darkgreen/1')), 'testing/amd64/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('darkgreen/1')), }}) @@ -1362,7 +1399,7 @@ class AT(TestAutopkgtestBase): {'darkgreen/2': {'darkgreen': ['amd64', 'i386']}}) # second run gets the results for darkgreen 2 - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100010@': (0, 'darkgreen 2', tr('darkgreen/2')), 'testing/amd64/d/darkgreen/20150101_100010@': (0, 'darkgreen 2', tr('darkgreen/2')), }}) @@ -1406,7 +1443,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/g/green/20150101_100000@': (0, 'green 1', tr('green/1')), 'testing/amd64/g/green/20150101_100000@': (0, 'green 1', tr('green/1')), 'testing/i386/g/green/20150101_100010@': (0, 'green 2', tr('green/2')), @@ -1446,7 +1483,7 @@ class AT(TestAutopkgtestBase): # third run gets the results for green and lightgreen, darkgreen is # still running - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/g/green/20150101_100020@': (0, 'green 3', tr('green/3')), 'testing/amd64/g/green/20150101_100020@': (0, 'green 3', tr('green/3')), 'testing/i386/l/lightgreen/20150101_100010@': (0, 'lightgreen 1', tr('green/3')), @@ -1464,7 +1501,7 @@ class AT(TestAutopkgtestBase): {'green/3': {'darkgreen': ['amd64', 'i386']}}) # fourth run finally gets the new darkgreen result - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100010@': (0, 'darkgreen 1', tr('green/3')), 'testing/amd64/d/darkgreen/20150101_100010@': (0, 'darkgreen 1', tr('green/3')), }}) @@ -1483,7 +1520,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('passedbefore/1')), 'testing/amd64/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('passedbefore/1')), }}) @@ -1494,7 +1531,7 @@ class AT(TestAutopkgtestBase): {'green': (False, {'darkgreen': {'amd64': 'RUNNING', 'i386': 'RUNNING'}})}) # second run: i386 result has version 1.1 - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100010@': (0, 'darkgreen 1.1', tr('green/2')) }}) self.run_it( @@ -1504,7 +1541,7 @@ class AT(TestAutopkgtestBase): })}) # third run: amd64 result has version 1.2 - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/amd64/d/darkgreen/20150101_100010@': (0, 'darkgreen 1.2', tr('green/2')), }}) self.run_it( @@ -1519,7 +1556,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(lightgreen=False) # one tmpfail result without testpkg-version, should be ignored - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/l/lightgreen/20150101_100000@': (0, 'lightgreen 1', tr('lightgreen/1')), 'testing/i386/l/lightgreen/20150101_100101@': (16, None, tr('lightgreen/2')), 'testing/amd64/l/lightgreen/20150101_100000@': (0, 'lightgreen 1', tr('lightgreen/1')), @@ -1533,7 +1570,7 @@ class AT(TestAutopkgtestBase): {'lightgreen/2': {'lightgreen': ['i386']}}) # one more tmpfail result, should not confuse britney with None version - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/l/lightgreen/20150101_100201@': (16, None, tr('lightgreen/2')), }}) self.run_it( @@ -1550,7 +1587,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) # first run fails - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/g/green/20150101_100000@': (0, 'green 2', tr('green/1')), 'testing/i386/g/green/20150101_100101@': (4, 'green 2', tr('green/2')), 'testing/amd64/g/green/20150101_100000@': (0, 'green 2', tr('green/1')), @@ -1574,7 +1611,7 @@ class AT(TestAutopkgtestBase): # re-running test manually succeeded (note: darkgreen result should be # cached already) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/g/green/20150101_100201@': (0, 'green 2', tr('green/2')), 'testing/amd64/g/green/20150101_100201@': (0, 'green 2', tr('green/2')), 'testing/i386/l/lightgreen/20150101_100201@': (0, 'lightgreen 1', tr('green/2')), @@ -1601,7 +1638,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(libc6=False) # new libc6 works fine with green - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/g/green/20150101_100000@': (0, 'green 1', tr('libc6/2')), 'testing/amd64/g/green/20150101_100000@': (0, 'green 1', tr('libc6/2')), }}) @@ -1625,7 +1662,7 @@ class AT(TestAutopkgtestBase): # new green fails; that's not libc6's fault though, so it should stay # valid - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/g/green/20150101_100100@': (4, 'green 2', tr('green/2')), 'testing/amd64/g/green/20150101_100100@': (4, 'green 2', tr('green/2')), }}) @@ -1647,7 +1684,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False, lightgreen=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/g/green/20150101_100101@': (0, 'green 1', tr('green/1')), 'testing/amd64/g/green/20150101_100101@': (0, 'green 1', tr('green/1')), 'testing/i386/g/green/20150101_100201@': (0, 'green 2', tr('green/2')), @@ -1674,7 +1711,7 @@ class AT(TestAutopkgtestBase): # green self.data.remove_all(True) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { # add new result for lightgreen 1 'testing/i386/l/lightgreen/20150101_100301@': (0, 'lightgreen 1', tr('green/2')), 'testing/amd64/l/lightgreen/20150101_100301@': (0, 'lightgreen 1', tr('green/2')), @@ -1710,7 +1747,7 @@ class AT(TestAutopkgtestBase): # # self.data.add_default_packages(lightgreen=False) # # # # # lightgreen has passed before on i386 only, therefore ALWAYSFAIL on amd64 -# # self.swift.set_results({'autopkgtest-testing': { +# # self.set_results({'autopkgtest-testing': { # # 'testing/i386/l/lightgreen/20150101_100000@': (0, 'lightgreen 1', tr('passedbefore/1')), # # }}) # # @@ -1770,7 +1807,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(lightgreen=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/r/rainbow/20150101_100000@': (0, 'rainbow 1', tr('passedbefore/1')), }}) @@ -1818,7 +1855,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/amd64/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/i386/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('green/1')), @@ -1846,7 +1883,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/amd64/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/i386/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('green/1')), @@ -1889,7 +1926,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/amd64/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/i386/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('green/1')), @@ -1931,7 +1968,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/amd64/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/i386/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('green/1')), @@ -1972,7 +2009,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/amd64/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/i386/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('green/1')), @@ -2001,7 +2038,7 @@ class AT(TestAutopkgtestBase): self.create_hint('autopkgtest', 'force-skiptest green/2') # regression of green, darkgreen ok, lightgreen running - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/g/green/20150101_100000@': (0, 'green 1', tr('passedbefore/1')), 'testing/i386/g/green/20150101_100200@': (4, 'green 2', tr('green/2')), 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), @@ -2025,7 +2062,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) # green has passed before on i386 only, therefore ALWAYSFAIL on amd64 - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/g/green/20150101_100000@': (0, 'green 1', tr('passedbefore/1')), }}) @@ -2048,7 +2085,7 @@ class AT(TestAutopkgtestBase): self.create_hint('freeze', 'block-all source') - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/l/lightgreen/20150101_100000@': (0, 'lightgreen 1', tr('passedbefore/1')), 'testing/amd64/l/lightgreen/20150101_100000@': (0, 'lightgreen 1', tr('passedbefore/1')), }}) @@ -2058,7 +2095,7 @@ class AT(TestAutopkgtestBase): {'lightgreen': (False, {'lightgreen': {'amd64': 'RUNNING', 'i386': 'RUNNING'}})} ) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/l/lightgreen/20150101_100100@': (0, 'lightgreen 2', tr('lightgreen/2')), 'testing/amd64/l/lightgreen/20150101_100100@': (0, 'lightgreen 2', tr('lightgreen/2')), }}) @@ -2074,7 +2111,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(lightgreen=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/amd64/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('lightgreen/1')), 'testing/amd64/l/lightgreen/20150101_100101@': (4, 'lightgreen 2', tr('lightgreen/2')), }}) @@ -2095,7 +2132,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(lightgreen=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/amd64/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('lightgreen/1')), 'testing/amd64/l/lightgreen/20150101_100101@': (4, 'lightgreen 2', tr('lightgreen/2')), 'testing/i386/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('lightgreen/1')), @@ -2118,7 +2155,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(lightgreen=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/amd64/l/lightgreen/20150101_100100@': (4, 'lightgreen 1', tr('lightgreen/1')), 'testing/amd64/l/lightgreen/20150102_100101@': (0, 'lightgreen 2', tr('lightgreen/2')), }}) @@ -2139,7 +2176,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(lightgreen=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/amd64/l/lightgreen/20150101_100100@': (4, 'lightgreen 1', tr('lightgreen/1')), 'testing/amd64/l/lightgreen/20150102_100101@': (0, 'lightgreen 2', tr('lightgreen/2')), 'testing/amd64/l/lightgreen/20150103_100101@': (4, 'lightgreen 3', tr('lightgreen/3')), @@ -2161,7 +2198,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/amd64/l/lightgreen/20150101_100100@': (4, 'lightgreen 0.1', tr('lightgreen/0.1')), 'testing/amd64/l/lightgreen/20150102_100101@': (0, 'lightgreen 1', tr('lightgreen/1')), 'testing/amd64/l/lightgreen/20150103_100101@': (4, 'lightgreen 1', tr('green/2')), @@ -2183,7 +2220,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False, lightgreen=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/amd64/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('lightgreen/1')), 'testing/amd64/l/lightgreen/20150102_100100@': (4, 'lightgreen 1', tr('green/2')), 'testing/amd64/l/lightgreen/20150103_100101@': (0, 'lightgreen 2', tr('lightgreen/2')), @@ -2235,7 +2272,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False, lightgreen=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/amd64/l/lightgreen/20150101_100101@': (0, 'lightgreen 1', tr('lightgreen/1')), 'testing/amd64/l/lightgreen/20150102_100101@': (4, 'lightgreen 1', tr('green/2')), 'testing/amd64/l/lightgreen/20150103_100102@': (0, 'lightgreen 2', tr('lightgreen/2')), @@ -2263,7 +2300,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False, lightgreen=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/amd64/l/lightgreen/20150101_100101@': (0, 'lightgreen 1', tr('lightgreen/1')), 'testing/amd64/l/lightgreen/20150102_100101@': (0, 'lightgreen 1', tr('green/2')), 'testing/amd64/l/lightgreen/20150103_100102@': (0, 'lightgreen 2', tr('lightgreen/2')), @@ -2296,7 +2333,7 @@ class AT(TestAutopkgtestBase): self.data.add('dkms', False, {}) self.data.add('fancy-dkms', False, {'Source': 'fancy', 'Depends': 'dkms (>= 1)'}, testsuite='autopkgtest-pkg-dkms') - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/f/fancy/20150101_100101@': (0, 'fancy 0.1', tr('passedbefore/1')) }}) @@ -2344,7 +2381,7 @@ class AT(TestAutopkgtestBase): # works against linux-meta and -64only, fails against grumpy i386, no # result yet for grumpy amd64 - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/amd64/f/fancy/20150101_100301@': (0, 'fancy 0.5', tr('passedbefore/1')), 'testing/i386/f/fancy/20150101_100101@': (0, 'fancy 1', tr('linux-meta/1')), 'testing/amd64/f/fancy/20150101_100101@': (0, 'fancy 1', tr('linux-meta/1')), @@ -2373,7 +2410,7 @@ class AT(TestAutopkgtestBase): # works against linux-meta and -64only, fails against grumpy i386, no # result yet for grumpy amd64 - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { # old results without trigger info 'testing/i386/f/fancy/20140101_100101@': (0, 'fancy 1', {}), 'testing/amd64/f/fancy/20140101_100101@': (8, 'fancy 1', {}), @@ -2415,7 +2452,7 @@ class AT(TestAutopkgtestBase): self.data.add('linux-libc-dev', False, {'Source': 'linux'}, testsuite='autopkgtest') self.data.add('linux-image', False, {'Source': 'linux-meta', 'Depends': 'linux-image-1'}) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/amd64/l/lxc/20150101_100101@': (0, 'lxc 0.1', tr('passedbefore/1')) }}) @@ -2449,7 +2486,7 @@ class AT(TestAutopkgtestBase): self.data.add('linux-image-1', False, {'Source': 'linux'}, testsuite='autopkgtest') self.data.add('linux-firmware', False, {'Source': 'linux-firmware'}, testsuite='autopkgtest') - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/f/fancy/20150101_090000@': (0, 'fancy 0.5', tr('passedbefore/1')), 'testing/i386/l/linux/20150101_100000@': (0, 'linux 2', tr('linux-meta/0.2')), 'testing/amd64/l/linux/20150101_100000@': (0, 'linux 2', tr('linux-meta/0.2')), @@ -2476,7 +2513,7 @@ class AT(TestAutopkgtestBase): ) # now linux-meta is ready to go - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/f/fancy/20150101_100000@': (0, 'fancy 1', tr('linux-meta/0.2')), 'testing/amd64/f/fancy/20150101_100000@': (0, 'fancy 1', tr('linux-meta/0.2')), }}) @@ -2503,7 +2540,7 @@ class AT(TestAutopkgtestBase): self.data.add('notme', False, {'Depends': 'libgcc1'}, testsuite='autopkgtest') # binutils has passed before on i386 only, therefore ALWAYSFAIL on amd64 - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/b/binutils/20150101_100000@': (0, 'binutils 1', tr('passedbefore/1')), }}) @@ -2519,7 +2556,7 @@ class AT(TestAutopkgtestBase): self.data.add('gcc-7', False, {}, testsuite='autopkgtest') # gcc-7 has passed before on i386 only, therefore ALWAYSFAIL on amd64 - self.swift.set_results({'autopkgtest-series': { + self.set_results({'autopkgtest-series': { 'series/i386/g/gcc-7/20150101_100000@': (0, 'gcc-7 1', tr('passedbefore/1')), }}) @@ -2594,7 +2631,7 @@ class AT(TestAutopkgtestBase): self.assertEqual(len(self.amqp_requests), 2) # add results to PPA specific swift container - self.swift.set_results({'autopkgtest-testing-awesome-developers-staging': { + self.set_results({'autopkgtest-testing-awesome-developers-staging': { 'testing/i386/l/lightgreen/20150101_100000@': (0, 'lightgreen 1', tr('passedbefore/1')), 'testing/i386/l/lightgreen/20150101_100100@': (4, 'lightgreen 2', tr('lightgreen/2')), 'testing/amd64/l/lightgreen/20150101_100101@': (0, 'lightgreen 2', tr('lightgreen/2')), @@ -2658,7 +2695,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(lightgreen=False) # first run to create autopkgtest-results.cache - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/l/lightgreen/20150101_100000@': (0, 'lightgreen 2', tr('lightgreen/2')), 'testing/amd64/l/lightgreen/20150101_100000@': (0, 'lightgreen 2', tr('lightgreen/2')), }}) @@ -2683,7 +2720,7 @@ class AT(TestAutopkgtestBase): sys.stdout.write(line) # second run, should now not update cache - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/l/lightgreen/20150101_100100@': (0, 'lightgreen 3', tr('lightgreen/3')), 'testing/amd64/l/lightgreen/20150101_100100@': (0, 'lightgreen 3', tr('lightgreen/3')), }}) @@ -2798,7 +2835,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) # green has passed before on i386 only, therefore ALWAYSFAIL on amd64 - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/g/green/20150101_100000@': (0, 'green 1', tr('passedbefore/1')), }}) @@ -2816,7 +2853,7 @@ class AT(TestAutopkgtestBase): self.assertEqual(exc['green']['policy_info']['age']['age-requirement'], 40) # second run collects the results - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/amd64/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('green/1')), 'testing/amd64/l/lightgreen/20150101_100101@': (4, 'lightgreen 1', tr('green/2')), @@ -2868,7 +2905,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/amd64/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('green/1')), 'testing/amd64/l/lightgreen/20150101_100101@': (4, 'lightgreen 1', tr('green/2')), @@ -2903,7 +2940,7 @@ class AT(TestAutopkgtestBase): self.data.add_default_packages(green=False) - self.swift.set_results({'autopkgtest-testing': { + self.set_results({'autopkgtest-testing': { 'testing/i386/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/amd64/d/darkgreen/20150101_100000@': (0, 'darkgreen 1', tr('green/2')), 'testing/i386/l/lightgreen/20150101_100100@': (0, 'lightgreen 1', tr('green/2')), From 794b14b3167f1adbe3aff056056972c8e62d178b Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Fri, 1 Oct 2021 18:53:58 -0700 Subject: [PATCH 08/39] Implement support for pulling autopkgtest results from sqlite db --- britney2/policies/autopkgtest.py | 85 +++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 7 deletions(-) diff --git a/britney2/policies/autopkgtest.py b/britney2/policies/autopkgtest.py index 555b7be..e635866 100644 --- a/britney2/policies/autopkgtest.py +++ b/britney2/policies/autopkgtest.py @@ -28,6 +28,7 @@ import io import itertools import re import socket +import sqlite3 import sys import time import urllib.parse @@ -124,7 +125,7 @@ class AutopkgtestPolicy(BasePolicy): self.pending_tests_file = os.path.join(self.state_dir, 'autopkgtest-pending.json') self.testsuite_triggers = {} self.result_in_baseline_cache = collections.defaultdict(dict) - self.database = os.path.join(self.state_dir, 'autopkgtest.db') + self.database_path = os.path.join(self.state_dir, 'autopkgtest.db') # results map: trigger -> src -> arch -> [passed, version, run_id, seen] # - trigger is "source/version" of an unstable package that triggered @@ -145,6 +146,7 @@ class AutopkgtestPolicy(BasePolicy): if not self.fetch_db(): self.logger.error('No autopkgtest db present, exiting') sys.exit(1) + self.db = sqlite3.connect(self.database_path) try: self.options.adt_ppas = self.options.adt_ppas.strip().split() @@ -170,7 +172,7 @@ class AutopkgtestPolicy(BasePolicy): http_code = f.getcode() # file:/// urls don't have the http niceties if not http_code or http_code == 200: - new_file = self.database + '.new' + new_file = self.database_path + '.new' with open(new_file,'wb') as f_out: while True: data=f.read(2048*1024) @@ -181,7 +183,7 @@ class AutopkgtestPolicy(BasePolicy): self.logger.info('Short read downloading autopkgtest results') os.unlink(new_file) else: - os.rename(new_file, self.database) + os.rename(new_file, self.database_path) else: self.logger.error('Failure to fetch autopkgtest results %s: HTTP code=%d', self.options.adt_db_url, f.getcode()) except IOError as e: @@ -189,7 +191,7 @@ class AutopkgtestPolicy(BasePolicy): finally: if f is not None: f.close() - return os.path.exists(self.database) + return os.path.exists(self.database_path) def register_hints(self, hint_parser): hint_parser.register_hint_type('force-badtest', britney2.hints.split_into_one_hint_per_package) @@ -1004,6 +1006,71 @@ class AutopkgtestPolicy(BasePolicy): for trigger in result_triggers: self.add_trigger_to_results(trigger, src, ver, arch, run_id, seen, result) + def fetch_sqlite_results(self, src, arch): + '''Retrieve new results for source package/arch from sqlite + + Remove matching pending_tests entries. + ''' + + # determine latest run_id from results + latest_run_id = '' + if not self.options.adt_shared_results_cache: + latest_run_id = self.latest_run_for_package(src, arch) + if not latest_run_id: + latest_run_id = '' + + cur = self.db.cursor() + for row in cur.execute('SELECT r.exitcode,r.version,r.triggers,' + ' r.run_id FROM test AS t ' + 'LEFT JOIN result AS r ON t.id=r.test_id ' + 'WHERE t.release=? AND t.arch=? ' + 'AND t.package=? AND r.run_id >= ?', + (self.options.series,arch, src, latest_run_id)): + exitcode, ver, triggers, run_id = row + if not ver: + if exitcode in (4, 12, 20): + # repair it + ver = "unknown" + else: + self.logger.error('%s/%s/%s is damaged, ignoring', + arch, src, run_id) + # ignore this; this will leave an orphaned request + # in autopkgtest-pending.json and thus require + # manual retries after fixing the tmpfail, but we + # can't just blindly attribute it to some pending + # test. + return + + # parse recorded triggers in test result + if triggers: + result_triggers = [i for i in triggers.split(' ', 1) if '/' in i] + else: + self.logger.error('%s result has no ADT_TEST_TRIGGERS, ignoring') + continue + + # 20200101_000000 is 15 chars long + seen = round(calendar.timegm(time.strptime(run_id[0:15], '%Y%m%d_%H%M%S'))) + + # allow some skipped tests, but nothing else + if exitcode in [0, 2]: + result = Result.PASS + elif exitcode == 8: + result = Result.NEUTRAL + else: + result = Result.FAIL + + self.logger.info( + 'Fetched test result for %s/%s/%s %s (triggers: %s): %s', + src, ver, arch, run_id, result_triggers, result.name.lower()) + + # remove matching test requests + for trigger in result_triggers: + self.remove_from_pending(trigger, src, arch) + + # add this result + for trigger in result_triggers: + self.add_trigger_to_results(trigger, src, ver, arch, run_id, seen, result) + def remove_from_pending(self, trigger, src, arch): try: arch_list = self.pending_tests[trigger][src] @@ -1105,17 +1172,21 @@ class AutopkgtestPolicy(BasePolicy): result[3] + int(self.options.adt_retry_older_than) * SECPERDAY < self._now: # We might want to retry this failure, so continue pass - elif not uses_swift: + elif not uses_swift and not hasattr(self,'db'): # We're done if we don't retrigger and we're not using swift return elif result_state in {Result.PASS, Result.NEUTRAL}: self.logger.debug('%s/%s triggered by %s already known', src, arch, trigger) return - # Without swift we don't expect new results - if uses_swift: + # Without swift or autopkgtest.db we don't expect new results + if hasattr(self,'db'): + self.fetch_sqlite_results(src, arch) + elif uses_swift: self.logger.info('Checking for new results for failed %s/%s for trigger %s', src, arch, trigger) self.fetch_swift_results(self.options.adt_swift_url, src, arch) + + if hasattr(self,'db') or uses_swift: # do we have one now? try: self.test_results[trigger][src][arch] From 6cd47f352b7eb6f435fe8a0618bdfffeea785394 Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Wed, 6 Oct 2021 23:49:18 -0700 Subject: [PATCH 09/39] In the autopkgtest policy, cache the list of force-reset-test hints once Due to the number of hints in standing use in Ubuntu, hints.search() is an expensive operation, and we call it once for *every single test* referenced from -proposed. Since force-reset-test are a small proportion of the hints in use, searching once for all the hints of this type and only searching this subset for each autopkgtest improves performance (with 23000 autopkgtests referenced in -proposed, this saves roughly 1 minute of runtime, or 11% on a 9-minute britney run; the number of packages in -proposed is typically much higher at other points in the release cycle, therefore the absolute improvement in performance is expected to be greater.) The force-reset-test hints are an Ubuntu delta so this is not expected to be upstreamed; and it could eventually be dropped if and when baseline retesting is implemented in Ubuntu and the number of hints required drops. This could be implemented with a more generic, elegant solution in HintsCollection, but again, the scalability problem of hints is hopefully short-lived so I didn't consider it worth the investment here. --- britney2/policies/autopkgtest.py | 42 ++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/britney2/policies/autopkgtest.py b/britney2/policies/autopkgtest.py index 20452bf..5229784 100644 --- a/britney2/policies/autopkgtest.py +++ b/britney2/policies/autopkgtest.py @@ -1286,32 +1286,38 @@ class AutopkgtestPolicy(BasePolicy): def find_max_lower_force_reset_test(self, src, ver, arch): '''Find the maximum force-reset-test hint before/including ver''' - hints = self.hints.search('force-reset-test', package=src) found_ver = None - if hints: - for hint in hints: - for mi in hint.packages: - if (mi.architecture in ['source', arch] and - mi.version != 'all' and - apt_pkg.version_compare(mi.version, ver) <= 0 and - (found_ver is None or apt_pkg.version_compare(found_ver, mi.version) < 0)): - found_ver = mi.version + if not hasattr(self, 'reset_hints'): + self.reset_hints = self.hints.search('force-reset-test') + + for hint in self.reset_hints: + for mi in hint.packages: + if mi.package != src: + continue + if (mi.architecture in ['source', arch] and + mi.version != 'all' and + apt_pkg.version_compare(mi.version, ver) <= 0 and + (found_ver is None or apt_pkg.version_compare(found_ver, mi.version) < 0)): + found_ver = mi.version return found_ver def has_higher_force_reset_test(self, src, ver, arch): '''Find if there is a minimum force-reset-test hint after/including ver''' - hints = self.hints.search('force-reset-test', package=src) - if hints: - self.logger.info('Checking hints for %s/%s/%s: %s' % (src, ver, arch, [str(h) for h in hints])) - for hint in hints: - for mi in hint.packages: - if (mi.architecture in ['source', arch] and - mi.version != 'all' and - apt_pkg.version_compare(mi.version, ver) >= 0): - return True + if not hasattr(self, 'reset_hints'): + self.reset_hints = self.hints.search('force-reset-test') + + for hint in self.reset_hints: + self.logger.info('Checking hints for %s/%s/%s: %s' % (src, ver, arch, str(hint))) + for mi in hint.packages: + if mi.package != src: + continue + if (mi.architecture in ['source', arch] and + mi.version != 'all' and + apt_pkg.version_compare(mi.version, ver) >= 0): + return True return False From a7b58b76e62ea8e883265e84262de7fb308f14c5 Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Thu, 7 Oct 2021 01:08:45 -0700 Subject: [PATCH 10/39] --print-uninst and --nuninst-cache are not mutually-exclusive This check has been present for a long time but there is no reason for it - there is code elsewhere that explicitly checks for both options being set together and DTRT. And this saves a minute on each britney run to not regenerate uninstallability information that was just generated. --- britney.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/britney.py b/britney.py index 3f21937..b5deff2 100755 --- a/britney.py +++ b/britney.py @@ -446,11 +446,8 @@ class Britney(object): pass # integrity checks - if self.options.nuninst_cache and self.options.print_uninst: # pragma: no cover - self.logger.error("nuninst_cache and print_uninst are mutually exclusive!") - sys.exit(1) # if the configuration file exists, then read it and set the additional options - elif not os.path.isfile(self.options.config): # pragma: no cover + if not os.path.isfile(self.options.config): # pragma: no cover self.logger.error("Unable to read the configuration file (%s), exiting!", self.options.config) sys.exit(1) From 383887a8e3f724a06d6420d2b4ca1c66119944ce Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Fri, 22 Oct 2021 16:12:26 -0700 Subject: [PATCH 11/39] bdmurray-linting --- britney2/policies/autopkgtest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/britney2/policies/autopkgtest.py b/britney2/policies/autopkgtest.py index e635866..cf14135 100644 --- a/britney2/policies/autopkgtest.py +++ b/britney2/policies/autopkgtest.py @@ -1025,7 +1025,7 @@ class AutopkgtestPolicy(BasePolicy): 'LEFT JOIN result AS r ON t.id=r.test_id ' 'WHERE t.release=? AND t.arch=? ' 'AND t.package=? AND r.run_id >= ?', - (self.options.series,arch, src, latest_run_id)): + (self.options.series, arch, src, latest_run_id)): exitcode, ver, triggers, run_id = row if not ver: if exitcode in (4, 12, 20): @@ -1049,10 +1049,10 @@ class AutopkgtestPolicy(BasePolicy): continue # 20200101_000000 is 15 chars long - seen = round(calendar.timegm(time.strptime(run_id[0:15], '%Y%m%d_%H%M%S'))) + seen = round(calendar.timegm(time.strptime(run_id[:15], '%Y%m%d_%H%M%S'))) # allow some skipped tests, but nothing else - if exitcode in [0, 2]: + if exitcode in (0, 2): result = Result.PASS elif exitcode == 8: result = Result.NEUTRAL From e3f9714dbf5a3af8151f66a5be6932322b662b99 Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Fri, 22 Oct 2021 16:49:18 -0700 Subject: [PATCH 12/39] Turn on support for pulling autopkgtest results from sqlite instead of swift --- britney.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/britney.conf b/britney.conf index f5429c3..4857c62 100644 --- a/britney.conf +++ b/britney.conf @@ -104,6 +104,8 @@ ADT_SHARED_RESULTS_CACHE = ADT_SWIFT_URL = https://autopkgtest.ubuntu.com/results/ # Base URL for autopkgtest site, used for links in the excuses ADT_CI_URL = https://autopkgtest.ubuntu.com/ +# URL for the autopkgtest database, if used +ADT_DB_URL = https://autopkgtest.ubuntu.com/static/autopkgtest.db ADT_HUGE = 20 # Autopkgtest results can be used to influence the aging From ef29d8e2c22e5229053cb657ae2cfb0edb10311c Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Fri, 5 Nov 2021 13:33:41 -0700 Subject: [PATCH 13/39] Set ADT_BASELINE=reference to honor baseline retests --- britney.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/britney.conf b/britney.conf index 4857c62..e6b1d4e 100644 --- a/britney.conf +++ b/britney.conf @@ -111,7 +111,7 @@ ADT_HUGE = 20 # Autopkgtest results can be used to influence the aging ADT_REGRESSION_PENALTY = ADT_SUCCESS_BOUNTY = -ADT_BASELINE = +ADT_BASELINE = reference ADT_RETRY_URL_MECH = ADT_RETRY_OLDER_THAN = ADT_REFERENCE_MAX_AGE = From b60734f1186e2924722d66366194081649d676e3 Mon Sep 17 00:00:00 2001 From: Julian Andres Klode Date: Thu, 12 Nov 2020 12:35:13 +0100 Subject: [PATCH 14/39] Do not add autopkgtest triggers that are already in testing If the version of the trigger in testing is the same, skip it. --- britney2/policies/autopkgtest.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/britney2/policies/autopkgtest.py b/britney2/policies/autopkgtest.py index 7f970be..5faba7e 100644 --- a/britney2/policies/autopkgtest.py +++ b/britney2/policies/autopkgtest.py @@ -562,6 +562,7 @@ class AutopkgtestPolicy(BasePolicy): pkg_universe = self.britney.pkg_universe target_suite = self.suite_info.target_suite source_suite = item.suite + sources_t = target_suite.sources sources_s = item.suite.sources packages_s_a = item.suite.binaries[arch] source_name = item.package @@ -646,9 +647,11 @@ class AutopkgtestPolicy(BasePolicy): if binary.architecture == arch: try: source_of_bin = packages_s_a[binary.package_name].source - triggers.add( - source_of_bin + '/' + - sources_s[source_of_bin].version) + if (sources_t.get(source_of_bin, None) is None or + sources_s[source_of_bin].version != sources_t[source_of_bin].version): + triggers.add( + source_of_bin + '/' + + sources_s[source_of_bin].version) except KeyError: # Apparently the package was removed from # unstable e.g. if packages are replaced @@ -657,9 +660,11 @@ class AutopkgtestPolicy(BasePolicy): if binary not in source_data_srcdist.binaries: for tdep_src in self.testsuite_triggers.get(binary.package_name, set()): try: - triggers.add( - tdep_src + '/' + - sources_s[tdep_src].version) + if (sources_t.get(tdep_src, None) is None or + sources_s[tdep_src].version != sources_t[tdep_src].version): + triggers.add( + tdep_src + '/' + + sources_s[tdep_src].version) except KeyError: # Apparently the source was removed from # unstable (testsuite_triggers are unified From e9576d55e2dadf29f820d42382eee2395fa50247 Mon Sep 17 00:00:00 2001 From: Julian Andres Klode Date: Fri, 5 Nov 2021 23:08:38 +0100 Subject: [PATCH 15/39] Check for new baseline results if we don't have any yet In Ubuntu, we only fetch results on demand, so we might not have seen the results yet. Debian always fetches results at the beginning so has all the data ready. --- britney2/policies/autopkgtest.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/britney2/policies/autopkgtest.py b/britney2/policies/autopkgtest.py index 7f970be..e87cecc 100644 --- a/britney2/policies/autopkgtest.py +++ b/britney2/policies/autopkgtest.py @@ -1228,7 +1228,21 @@ class AutopkgtestPolicy(BasePolicy): result_reference = [Result.NONE, None, '', 0] if self.options.adt_baseline == 'reference': try: - result_reference = self.test_results[REF_TRIG][src][arch] + try: + result_reference = self.test_results[REF_TRIG][src][arch] + except KeyError: + uses_swift = not self.options.adt_swift_url.startswith('file://') + # Without swift or autopkgtest.db we don't expect new results + if hasattr(self,'db'): + self.logger.info('Checking for new results for %s/%s for trigger %s', src, arch, REF_TRIG) + self.fetch_sqlite_results(src, arch) + elif uses_swift: + self.logger.info('Checking for new results for %s/%s for trigger %s', src, arch, REF_TRIG) + self.fetch_swift_results(self.options.adt_swift_url, src, arch) + + # do we have one now? + result_reference = self.test_results[REF_TRIG][src][arch] + self.logger.debug('Found result for src %s in reference: %s', src, result_reference[0].name) except KeyError: From 11a023d1338fb17c0a5ff4083b0c1c016b84dded Mon Sep 17 00:00:00 2001 From: Julian Andres Klode Date: Sat, 6 Nov 2021 00:06:51 +0100 Subject: [PATCH 16/39] Only log force-reset-test hints for matching packages Logging all force-reset-test hints for every package causes about 850 MB of logs in the last run of 880 MB of logs in total, let's only log ones matching the package instead, as we do for force-badtest. --- britney2/policies/autopkgtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/britney2/policies/autopkgtest.py b/britney2/policies/autopkgtest.py index 7f970be..2fbf7c1 100644 --- a/britney2/policies/autopkgtest.py +++ b/britney2/policies/autopkgtest.py @@ -1415,10 +1415,10 @@ class AutopkgtestPolicy(BasePolicy): self.reset_hints = self.hints.search('force-reset-test') for hint in self.reset_hints: - self.logger.info('Checking hints for %s/%s/%s: %s' % (src, ver, arch, str(hint))) for mi in hint.packages: if mi.package != src: continue + self.logger.info('Checking hints for %s/%s/%s: %s' % (src, ver, arch, str(hint))) if (mi.architecture in ['source', arch] and mi.version != 'all' and apt_pkg.version_compare(mi.version, ver) >= 0): From bbdd8105b274d88c189f846df5ec4628288a0c98 Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Sat, 6 Nov 2021 19:19:10 -0700 Subject: [PATCH 17/39] Split all triggers, not just the first one --- britney2/policies/autopkgtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/britney2/policies/autopkgtest.py b/britney2/policies/autopkgtest.py index 6ad3f07..d20198f 100644 --- a/britney2/policies/autopkgtest.py +++ b/britney2/policies/autopkgtest.py @@ -1043,7 +1043,7 @@ class AutopkgtestPolicy(BasePolicy): # parse recorded triggers in test result if triggers: - result_triggers = [i for i in triggers.split(' ', 1) if '/' in i] + result_triggers = [i for i in triggers.split(' ') if '/' in i] else: self.logger.error('%s result has no ADT_TEST_TRIGGERS, ignoring') continue From 36459dcdb75ddeba9abe9ef2f2ba291b3a3a64c3 Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Sat, 6 Nov 2021 22:09:02 -0700 Subject: [PATCH 18/39] Fix fencepost error when fetching autopkgtest results from sqlite When querying swift there is no way to take results only newer than a specified point, you can only query newer than or equal to. But for sqlite we can absolutely use > instead of >= and avoid re-processing results we've already seen. --- britney2/policies/autopkgtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/britney2/policies/autopkgtest.py b/britney2/policies/autopkgtest.py index d20198f..a05f52c 100644 --- a/britney2/policies/autopkgtest.py +++ b/britney2/policies/autopkgtest.py @@ -1024,7 +1024,7 @@ class AutopkgtestPolicy(BasePolicy): ' r.run_id FROM test AS t ' 'LEFT JOIN result AS r ON t.id=r.test_id ' 'WHERE t.release=? AND t.arch=? ' - 'AND t.package=? AND r.run_id >= ?', + 'AND t.package=? AND r.run_id > ?', (self.options.series, arch, src, latest_run_id)): exitcode, ver, triggers, run_id = row if not ver: From 8239fe9fd7026c436152852956d78f1d6cee6a94 Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Tue, 9 Nov 2021 13:59:12 -0800 Subject: [PATCH 19/39] RIP Adam :/ --- britney.conf | 1 - britney_nobreakall.conf | 1 - 2 files changed, 2 deletions(-) diff --git a/britney.conf b/britney.conf index e6b1d4e..c963e7a 100644 --- a/britney.conf +++ b/britney.conf @@ -57,7 +57,6 @@ BOUNTY_MIN_AGE = 2 HINTSDIR = data/%(SERIES)-proposed/Hints # hint permissions -HINTS_ADCONRAD = ALL HINTS_LANEY = ALL HINTS_STEFANOR = ALL HINTS_STGRABER = ALL diff --git a/britney_nobreakall.conf b/britney_nobreakall.conf index 5da361b..4576790 100644 --- a/britney_nobreakall.conf +++ b/britney_nobreakall.conf @@ -57,7 +57,6 @@ BOUNTY_MIN_AGE = 2 HINTSDIR = data/%(SERIES)-proposed/Hints # hint permissions -HINTS_ADCONRAD = ALL HINTS_LANEY = ALL HINTS_STEFANOR = ALL HINTS_STGRABER = ALL From e2847293dfef9b11fa1a05953d34dffe77ade735 Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Mon, 6 Dec 2021 13:06:10 -0800 Subject: [PATCH 20/39] Set SMOOTH_UPDATES = ALL, which is correct for how Ubuntu handles binaries --- britney.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/britney.conf b/britney.conf index c963e7a..e6387be 100644 --- a/britney.conf +++ b/britney.conf @@ -81,7 +81,7 @@ HINTS_FREEZE = block block-all # # naming a non-existent section will effectively disable new smooth # updates but still allow removals to occur -SMOOTH_UPDATES = badgers +SMOOTH_UPDATES = ALL IGNORE_CRUFT = 0 From 83e6102613b37f3e464ecd269b9dd9c621075021 Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Sun, 12 Dec 2021 14:37:37 -0800 Subject: [PATCH 21/39] Temporarily disable autohinter due to a recursion problem --- britney.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/britney.py b/britney.py index b5deff2..be389f1 100755 --- a/britney.py +++ b/britney.py @@ -1461,6 +1461,9 @@ class Britney(object): """ self.logger.info("> Processing hints from the auto hinter") + self.logger.info(">> Skipping auto hinting due to recursion problem") + return [] + sources_t = self.suite_info.target_suite.sources excuses = self.excuses From 0374455b6542069d1dd2fe5294cc429bb47b370b Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Mon, 13 Dec 2021 18:31:03 -0800 Subject: [PATCH 22/39] Revert "Temporarily disable autohinter due to a recursion problem" This reverts commit 83e6102613b37f3e464ecd269b9dd9c621075021. --- britney.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/britney.py b/britney.py index be389f1..b5deff2 100755 --- a/britney.py +++ b/britney.py @@ -1461,9 +1461,6 @@ class Britney(object): """ self.logger.info("> Processing hints from the auto hinter") - self.logger.info(">> Skipping auto hinting due to recursion problem") - return [] - sources_t = self.suite_info.target_suite.sources excuses = self.excuses From 86618a1b1d8044982a19043d418a6166766bee4e Mon Sep 17 00:00:00 2001 From: Julian Andres Klode Date: Thu, 27 Jan 2022 18:54:38 +0100 Subject: [PATCH 23/39] Only set SMOOTH_UPDATES = libs oldlibs Setting ALL has weird side effects with kernel upgrades and other random binary packages being removed from source packages. --- britney.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/britney.conf b/britney.conf index e6387be..52f5b4c 100644 --- a/britney.conf +++ b/britney.conf @@ -81,7 +81,7 @@ HINTS_FREEZE = block block-all # # naming a non-existent section will effectively disable new smooth # updates but still allow removals to occur -SMOOTH_UPDATES = ALL +SMOOTH_UPDATES = libs oldlibs IGNORE_CRUFT = 0 From 53aaf8f8e949823fe26fda51c0e358cb27beb432 Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Fri, 11 Feb 2022 09:21:28 +1300 Subject: [PATCH 24/39] allow retrying on "Reference test in progress, but real test failed already" --- britney2/policies/autopkgtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/britney2/policies/autopkgtest.py b/britney2/policies/autopkgtest.py index 1bf4d03..74429c7 100644 --- a/britney2/policies/autopkgtest.py +++ b/britney2/policies/autopkgtest.py @@ -475,7 +475,7 @@ class AutopkgtestPolicy(BasePolicy): history_url = cloud_url % { 'h': srchash(testsrc), 's': testsrc, 'r': self.options.series, 'a': arch} - if status == 'REGRESSION': + if status in ['REGRESSION', 'RUNNING-REFERENCE']: if self.options.adt_retry_url_mech == 'run_id': retry_url = self.options.adt_ci_url + 'api/v1/retry/' + run_id else: From fd20237673aac2a134246e2c22107069c6e16a13 Mon Sep 17 00:00:00 2001 From: Haw Loeung Date: Tue, 22 Mar 2022 10:58:40 +1100 Subject: [PATCH 25/39] Update email address used for emails @canonical.com is now DKIM signed and SPF published which means emails from proposed-migration running on snakefruit sending direct would likely be caught out. Since we're here, the project is Ubuntu related so switch to using an @ubuntu.com address instead. --- britney2/policies/email.py | 4 ++-- britney2/policies/sruadtregression.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/britney2/policies/email.py b/britney2/policies/email.py index f500a79..c6fe571 100644 --- a/britney2/policies/email.py +++ b/britney2/policies/email.py @@ -28,7 +28,7 @@ BOTS = { USER + "katie", } -MESSAGE = """From: Ubuntu Release Team +MESSAGE = """From: Ubuntu Release Team To: {recipients} X-Proposed-Migration: notice Subject: [proposed-migration] {source_name} {version} stuck in {series}-proposed for {age} day{plural}. @@ -287,7 +287,7 @@ class EmailPolicy(BasePolicy, Rest): ) ) server = smtplib.SMTP(self.email_host) - server.sendmail("noreply@canonical.com", emails, msg) + server.sendmail("noreply+proposed-migration@ubuntu.com", emails, msg) server.quit() # record the age at which the mail should have been sent last_sent = last_due diff --git a/britney2/policies/sruadtregression.py b/britney2/policies/sruadtregression.py index 1c8ac82..6fa8985 100644 --- a/britney2/policies/sruadtregression.py +++ b/britney2/policies/sruadtregression.py @@ -171,7 +171,7 @@ class SRUADTRegressionPolicy(BasePolicy, Rest): bug_mail = "%s@bugs.launchpad.net" % bug server = smtplib.SMTP(self.email_host) server.sendmail( - "noreply@canonical.com", + "noreply+proposed-migration@ubuntu.com", bug_mail, MESSAGE.format(**locals()), ) From f078aa28d099d48725c7301d3166e7ae80b40ddf Mon Sep 17 00:00:00 2001 From: Julian Andres Klode Date: Mon, 28 Mar 2022 10:53:42 +0200 Subject: [PATCH 26/39] do not group the ppc64el rebuilds by their bileto ppa --- britney2/policies/sourceppa.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/britney2/policies/sourceppa.py b/britney2/policies/sourceppa.py index ea01e76..b203fb8 100644 --- a/britney2/policies/sourceppa.py +++ b/britney2/policies/sourceppa.py @@ -15,6 +15,7 @@ from britney2.policies.policy import BasePolicy, PolicyVerdict LAUNCHPAD_URL = "https://api.launchpad.net/1.0/" PRIMARY = LAUNCHPAD_URL + "ubuntu/+archive/primary" INCLUDE = ["~bileto-ppa-service/", "~ci-train-ppa-service/"] +EXCLUDE = ["~ci-train-ppa-service/+archive/ubuntu/4810", "~ci-train-ppa-service/+archive/ubuntu/4815", "~ci-train-ppa-service/+archive/ubuntu/4816"] class SourcePPAPolicy(BasePolicy, Rest): @@ -105,6 +106,8 @@ class SourcePPAPolicy(BasePolicy, Rest): sourceppa = self.lp_get_source_ppa(source_name, version) or "" verdict = excuse.policy_verdict self.source_ppas_by_pkg[source_name][version] = sourceppa + if [team for team in EXCLUDE if team in sourceppa]: + return PolicyVerdict.PASS if not [team for team in INCLUDE if team in sourceppa]: return PolicyVerdict.PASS From d51e0be23a641a6a6ed2a4322845a7dbfd58839c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20=27sil2100=27=20Zemczak?= Date: Mon, 28 Mar 2022 18:46:07 +0200 Subject: [PATCH 27/39] Fix typo in bileto ppa name. --- britney2/policies/sourceppa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/britney2/policies/sourceppa.py b/britney2/policies/sourceppa.py index b203fb8..1099caa 100644 --- a/britney2/policies/sourceppa.py +++ b/britney2/policies/sourceppa.py @@ -15,7 +15,7 @@ from britney2.policies.policy import BasePolicy, PolicyVerdict LAUNCHPAD_URL = "https://api.launchpad.net/1.0/" PRIMARY = LAUNCHPAD_URL + "ubuntu/+archive/primary" INCLUDE = ["~bileto-ppa-service/", "~ci-train-ppa-service/"] -EXCLUDE = ["~ci-train-ppa-service/+archive/ubuntu/4810", "~ci-train-ppa-service/+archive/ubuntu/4815", "~ci-train-ppa-service/+archive/ubuntu/4816"] +EXCLUDE = ["~ci-train-ppa-service/+archive/ubuntu/4813", "~ci-train-ppa-service/+archive/ubuntu/4815", "~ci-train-ppa-service/+archive/ubuntu/4816"] class SourcePPAPolicy(BasePolicy, Rest): From 1c32e3014bb67ac938664c2c69b17d784f764df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20=27sil2100=27=20Zemczak?= Date: Wed, 30 Mar 2022 10:42:26 +0200 Subject: [PATCH 28/39] Re-add Bileto 4810 to the temporary exclude list. --- britney2/policies/sourceppa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/britney2/policies/sourceppa.py b/britney2/policies/sourceppa.py index 1099caa..bbef00d 100644 --- a/britney2/policies/sourceppa.py +++ b/britney2/policies/sourceppa.py @@ -15,7 +15,7 @@ from britney2.policies.policy import BasePolicy, PolicyVerdict LAUNCHPAD_URL = "https://api.launchpad.net/1.0/" PRIMARY = LAUNCHPAD_URL + "ubuntu/+archive/primary" INCLUDE = ["~bileto-ppa-service/", "~ci-train-ppa-service/"] -EXCLUDE = ["~ci-train-ppa-service/+archive/ubuntu/4813", "~ci-train-ppa-service/+archive/ubuntu/4815", "~ci-train-ppa-service/+archive/ubuntu/4816"] +EXCLUDE = ["~ci-train-ppa-service/+archive/ubuntu/4810", "~ci-train-ppa-service/+archive/ubuntu/4813", "~ci-train-ppa-service/+archive/ubuntu/4815", "~ci-train-ppa-service/+archive/ubuntu/4816"] class SourcePPAPolicy(BasePolicy, Rest): From d4dfa22ed167a4176132070627be3a17342c7ce7 Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Wed, 27 Apr 2022 14:39:49 -0700 Subject: [PATCH 29/39] Temporarily disable use of sqlite db for autopkgtests The initial db population for the series takes quite a while, so to not block on this for the release opening process we can let britney talk directly to swift in the short term. --- britney.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/britney.conf b/britney.conf index 52f5b4c..25b97be 100644 --- a/britney.conf +++ b/britney.conf @@ -104,7 +104,7 @@ ADT_SWIFT_URL = https://autopkgtest.ubuntu.com/results/ # Base URL for autopkgtest site, used for links in the excuses ADT_CI_URL = https://autopkgtest.ubuntu.com/ # URL for the autopkgtest database, if used -ADT_DB_URL = https://autopkgtest.ubuntu.com/static/autopkgtest.db +#ADT_DB_URL = https://autopkgtest.ubuntu.com/static/autopkgtest.db ADT_HUGE = 20 # Autopkgtest results can be used to influence the aging From e25ce3df68eb65d3e85390b16d1933d45d180cbd Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Mon, 2 May 2022 22:07:53 +0200 Subject: [PATCH 30/39] Revert "Temporarily disable use of sqlite db for autopkgtests" This reverts commit d4dfa22ed167a4176132070627be3a17342c7ce7. --- britney.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/britney.conf b/britney.conf index 25b97be..52f5b4c 100644 --- a/britney.conf +++ b/britney.conf @@ -104,7 +104,7 @@ ADT_SWIFT_URL = https://autopkgtest.ubuntu.com/results/ # Base URL for autopkgtest site, used for links in the excuses ADT_CI_URL = https://autopkgtest.ubuntu.com/ # URL for the autopkgtest database, if used -#ADT_DB_URL = https://autopkgtest.ubuntu.com/static/autopkgtest.db +ADT_DB_URL = https://autopkgtest.ubuntu.com/static/autopkgtest.db ADT_HUGE = 20 # Autopkgtest results can be used to influence the aging From 0998eeaa1adec497413dc731e811c95758ac7d65 Mon Sep 17 00:00:00 2001 From: Brian Murray Date: Wed, 29 Jun 2022 10:52:13 -0700 Subject: [PATCH 31/39] We moved away from freenode a while ago --- britney2/policies/email.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/britney2/policies/email.py b/britney2/policies/email.py index c6fe571..349bf91 100644 --- a/britney2/policies/email.py +++ b/britney2/policies/email.py @@ -45,7 +45,7 @@ http://people.canonical.com/~ubuntu-archive/proposed-migration/{series}/update_e https://wiki.ubuntu.com/ProposedMigration -If you have any questions about this email, please ask them in #ubuntu-release channel on Freenode IRC. +If you have any questions about this email, please ask them in #ubuntu-release channel on libera.chat. Regards, Ubuntu Release Team. """ From 479662c63e0dfa233a4fcb0887712b6d1802c4dd Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Tue, 23 Aug 2022 13:46:41 -0700 Subject: [PATCH 32/39] Don't query for baseline results we won't use. britney currently spends a majority of its runtime querying for baseline test results that it won't find, and that it doesn't need. Refactor to eliminate many of these excess queries. --- britney2/policies/autopkgtest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/britney2/policies/autopkgtest.py b/britney2/policies/autopkgtest.py index 74429c7..a9414a7 100644 --- a/britney2/policies/autopkgtest.py +++ b/britney2/policies/autopkgtest.py @@ -1168,11 +1168,12 @@ class AutopkgtestPolicy(BasePolicy): if has_result: result_state = result[0] version = result[1] - baseline = self.result_in_baseline(src, arch) if result_state in {Result.OLD_PASS, Result.OLD_FAIL, Result.OLD_NEUTRAL}: pass elif result_state == Result.FAIL and \ - baseline[0] in {Result.PASS, Result.NEUTRAL, Result.OLD_PASS, Result.OLD_NEUTRAL} and \ + self.result_in_baseline(src, arch)[0] in \ + {Result.PASS, Result.NEUTRAL, Result.OLD_PASS, + Result.OLD_NEUTRAL} and \ self.options.adt_retry_older_than and \ result[3] + int(self.options.adt_retry_older_than) * SECPERDAY < self._now: # We might want to retry this failure, so continue From de19e280b2bfb9ca0e31c032819e4af6c3f98805 Mon Sep 17 00:00:00 2001 From: Steve Langasek Date: Tue, 23 Aug 2022 15:05:50 -0700 Subject: [PATCH 33/39] More optimizations to not query unused baseline results --- britney2/policies/autopkgtest.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/britney2/policies/autopkgtest.py b/britney2/policies/autopkgtest.py index a9414a7..7ff8c1f 100644 --- a/britney2/policies/autopkgtest.py +++ b/britney2/policies/autopkgtest.py @@ -1284,9 +1284,6 @@ class AutopkgtestPolicy(BasePolicy): target_suite = self.suite_info.target_suite binaries_info = target_suite.binaries[arch] - # determine current test result status - baseline_result = self.result_in_baseline(src, arch)[0] - # determine current test result status until = self.find_max_lower_force_reset_test(src, ver, arch) @@ -1312,6 +1309,9 @@ class AutopkgtestPolicy(BasePolicy): run_id = r[2] if r[0] in {Result.FAIL, Result.OLD_FAIL}: + # determine current test result status + baseline_result = self.result_in_baseline(src, arch)[0] + if baseline_result == Result.FAIL: result = 'ALWAYSFAIL' elif baseline_result in {Result.NONE, Result.OLD_FAIL}: @@ -1369,6 +1369,7 @@ class AutopkgtestPolicy(BasePolicy): except KeyError: # no result for src/arch; still running? if arch in self.pending_tests.get(trigger, {}).get(src, []): + baseline_result = self.result_in_baseline(src, arch)[0] if baseline_result != Result.FAIL and not self.has_force_badtest(src, ver, arch): result = 'RUNNING' else: From 436626145b7746409a5dd01d5bf3c06526769f30 Mon Sep 17 00:00:00 2001 From: Aleksa Svitlica Date: Fri, 6 Jan 2023 15:26:48 -0500 Subject: [PATCH 34/39] Add Cloud Policy The cloud policy is currently used to test whether the proposed migration will break networking for an image in the Azure cloud. Cloud testing will likely increase in scope in the future. --- britney.py | 2 + britney2/policies/cloud.py | 285 ++++++++++++++++++ cloud_package_set | 600 +++++++++++++++++++++++++++++++++++++ tests/test_cloud.py | 181 +++++++++++ 4 files changed, 1068 insertions(+) create mode 100644 britney2/policies/cloud.py create mode 100644 cloud_package_set create mode 100644 tests/test_cloud.py diff --git a/britney.py b/britney.py index b5deff2..673a027 100755 --- a/britney.py +++ b/britney.py @@ -223,6 +223,7 @@ from britney2.policies.autopkgtest import AutopkgtestPolicy from britney2.policies.sourceppa import SourcePPAPolicy from britney2.policies.sruadtregression import SRUADTRegressionPolicy from britney2.policies.email import EmailPolicy +from britney2.policies.cloud import CloudPolicy from britney2.policies.lpexcusebugs import LPExcuseBugsPolicy from britney2.utils import (log_and_format_old_libraries, read_nuninst, write_nuninst, write_heidi, @@ -552,6 +553,7 @@ class Britney(object): self._policy_engine.add_policy(EmailPolicy(self.options, self.suite_info, dry_run=add_email_policy == 'dry-run')) + self._policy_engine.add_policy(CloudPolicy(self.options, self.suite_info, dry_run=self.options.dry_run)) @property def hints(self): diff --git a/britney2/policies/cloud.py b/britney2/policies/cloud.py new file mode 100644 index 0000000..f97453a --- /dev/null +++ b/britney2/policies/cloud.py @@ -0,0 +1,285 @@ +import json +import os +from pathlib import PurePath +import re +import shutil +import smtplib +import socket +import subprocess +import xunitparser + +from britney2 import SuiteClass +from britney2.policies.policy import BasePolicy +from britney2.policies import PolicyVerdict + +FAIL_MESSAGE = """From: Ubuntu Release Team +To: {recipients} +X-Proposed-Migration: notice +Subject: [proposed-migration] {package} {version} in {series}-{pocket} failed Cloud tests. + +Hi, + +{package} {version} needs attention. + +This package fails the following tests: + +{results} + +If you have any questions about this email, please ask them in #ubuntu-release channel on libera.chat. + +Regards, Ubuntu Release Team. +""" + +ERR_MESSAGE = """From: Ubuntu Release Team +To: {recipients} +X-Proposed-Migration: notice +Subject: [proposed-migration] {package} {version} in {series}-{pocket} had errors running Cloud Tests. + +Hi, + +During Cloud tests of {package} {version} the following errors occurred: + +{results} + +If you have any questions about this email, please ask them in #ubuntu-release channel on libera.chat. + +Regards, Ubuntu Release Team. +""" +class CloudPolicy(BasePolicy): + WORK_DIR = "cloud_tests" + PACKAGE_SET_FILE = "cloud_package_set" + EMAILS = ["cpc@canonical.com"] + SERIES_TO_URN = { + "lunar": "Canonical:0001-com-ubuntu-server-lunar-daily:23_04-daily-gen2:23.04.202301030", + "kinetic": "Canonical:0001-com-ubuntu-server-kinetic:22_10:22.10.202301040", + "jammy": "Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:22.04.202212140", + "focal": "Canonical:0001-com-ubuntu-server-focal:20_04-lts-gen2:20.04.202212140", + "bionic": "Canonical:UbuntuServer:18_04-lts-gen2:18.04.202212090" + } + + def __init__(self, options, suite_info, dry_run=False): + super().__init__( + "cloud", options, suite_info, {SuiteClass.PRIMARY_SOURCE_SUITE} + ) + self.dry_run = dry_run + if self.dry_run: + self.logger.info("Cloud Policy: Dry-run enabled") + + self.email_host = getattr(self.options, "email_host", "localhost") + self.logger.info( + "Cloud Policy: will send emails to: %s", self.email_host + ) + self.failures = {} + self.errors = {} + + def initialise(self, britney): + super().initialise(britney) + + primary_suite = self.suite_info.primary_source_suite + series, pocket = self._retrieve_series_and_pocket_from_path(primary_suite.path) + self.series = series + self.pocket = pocket + self.package_set = self._retrieve_cloud_package_set_for_series(self.series) + + def apply_src_policy_impl(self, policy_info, item, source_data_tdist, source_data_srcdist, excuse): + if item.package not in self.package_set: + return PolicyVerdict.PASS + + if self.dry_run: + self.logger.info( + "Cloud Policy: Dry run would test {} in {}-{}".format(item.package , self.series, self.pocket) + ) + return PolicyVerdict.PASS + + self._setup_work_directory() + self.failures = {} + self.errors = {} + + self._run_cloud_tests(item.package, self.series, self.pocket) + self._send_emails_if_needed(item.package, source_data_srcdist.version, self.series, self.pocket) + + self._cleanup_work_directory() + return PolicyVerdict.PASS + + def _retrieve_cloud_package_set_for_series(self, series): + """Retrieves a set of packages for the given series in which cloud + tests should be run. + + Temporarily a static list retrieved from file. Will be updated to + retrieve from a database at a later date. + + :param series The Ubuntu codename for the series (e.g. jammy) + """ + package_set = set() + + with open(self.PACKAGE_SET_FILE) as file: + for line in file: + package_set.add(line.strip()) + + return package_set + + def _retrieve_series_and_pocket_from_path(self, suite_path): + """Given the suite path return a tuple of series and pocket. + With input 'data/jammy-proposed' the expected output is a tuple of + (jammy, proposed) + + :param suite_path The path attribute of the suite + """ + result = (None, None) + match = re.search("([a-z]*)-([a-z]*)$", suite_path) + + if(match): + result = match.groups() + else: + raise RuntimeError( + "Could not determine series and pocket from the path: %s" %suite_path + ) + return result + + def _run_cloud_tests(self, package, series, pocket): + """Runs any cloud tests for the given package. + Returns a list of failed tests or an empty list if everything passed. + + :param package The name of the package to test + :param series The Ubuntu codename for the series (e.g. jammy) + :param pocket The name of the pocket the package should be installed from (e.g. proposed) + """ + self._run_azure_tests(package, series, pocket) + + def _send_emails_if_needed(self, package, version, series, pocket): + """Sends email(s) if there are test failures and/or errors + + :param package The name of the package that was tested + :param version The version number of the package + :param series The Ubuntu codename for the series (e.g. jammy) + :param pocket The name of the pocket the package should be installed from (e.g. proposed) + """ + if len(self.failures) > 0: + emails = self.EMAILS + message = self._format_email_message( + FAIL_MESSAGE, emails, package, version, self.failures + ) + self.logger.info("Cloud Policy: Sending failure email for {}, to {}".format(package, emails)) + self._send_email(emails, message) + + if len(self.errors) > 0: + emails = self.EMAILS + message = self._format_email_message( + ERR_MESSAGE, emails, package, version, self.errors + ) + self.logger.info("Cloud Policy: Sending error email for {}, to {}".format(package, emails)) + self._send_email(emails, message) + + def _run_azure_tests(self, package, series, pocket): + """Runs Azure's required package tests. + + :param package The name of the package to test + :param series The Ubuntu codename for the series (e.g. jammy) + :param pocket The name of the pocket the package should be installed from (e.g. proposed) + """ + urn = self.SERIES_TO_URN.get(series, None) + if urn is None: + return + + self.logger.info("Cloud Policy: Running Azure tests for: {} in {}-{}".format(package, series, pocket)) + subprocess.run( + [ + "/snap/bin/cloud-test-framework", + "--instance-prefix", "britney-{}-{}-{}".format(package, series, pocket), + "--install-archive-package", "{}/{}".format(package, pocket), + "azure_gen2", + "--location", "westeurope", + "--vm-size", "Standard_D2s_v5", + "--urn", urn, + "run-test", "package-install-with-reboot", + ], + cwd=self.WORK_DIR + ) + + results_file_paths = self._find_results_files(r"TEST-NetworkTests-[0-9]*.xml") + self._parse_xunit_test_results("Azure", results_file_paths) + + def _find_results_files(self, file_regex): + """Find any test results files that match the given regex pattern. + + :param file_regex A regex pattern to use for matching the name of the results file. + """ + file_paths = [] + for file in os.listdir(self.WORK_DIR): + if re.fullmatch(file_regex, file): + file_paths.append(PurePath(self.WORK_DIR, file)) + + return file_paths + + def _parse_xunit_test_results(self, cloud, results_file_paths): + """Parses and stores any failure or error test results. + + :param cloud The name of the cloud, use for storing the results. + :results_file_paths List of paths to results files + """ + for file_path in results_file_paths: + with open(file_path) as file: + ts, tr = xunitparser.parse(file) + + for testcase, message in tr.failures: + self.store_test_result(self.failures, cloud, testcase.methodname, message) + + for testcase, message in tr.errors: + self.store_test_result(self.errors, cloud, testcase.methodname, message) + + def store_test_result(self, results, cloud, test_name, message): + """Adds the test to the results hash under the given cloud. + + Results format: + { + cloud1: { + test_name1: message1 + test_name2: message2 + }, + cloud2: ... + } + + :param results A hash to add results to + :param cloud The name of the cloud + :param message The exception or assertion error given by the test + """ + if cloud not in results: + results[cloud] = {} + + results[cloud][test_name] = message + + def _format_email_message(self, template, emails, package, version, test_results): + """Insert given parameters into the email template.""" + series = self.series + pocket = self.pocket + results = json.dumps(test_results, indent=4) + recipients = ", ".join(emails) + message= template.format(**locals()) + + return message + + def _send_email(self, emails, message): + """Send an email + + :param emails List of emails to send to + :param message The content of the email + """ + try: + server = smtplib.SMTP(self.email_host) + server.sendmail("noreply+proposed-migration@ubuntu.com", emails, message) + server.quit() + except socket.error as err: + self.logger.error("Cloud Policy: Failed to send mail! Is SMTP server running?") + self.logger.error(err) + + def _setup_work_directory(self): + """Create a directory for tests to be run in.""" + self._cleanup_work_directory() + + os.makedirs(self.WORK_DIR) + + def _cleanup_work_directory(self): + """Delete the the directory used for running tests.""" + if os.path.exists(self.WORK_DIR): + shutil.rmtree(self.WORK_DIR) + diff --git a/cloud_package_set b/cloud_package_set new file mode 100644 index 0000000..f97a293 --- /dev/null +++ b/cloud_package_set @@ -0,0 +1,600 @@ +openssh-client +libmpfr6 +python3-problem-report +rsync +libdb5 +libattr1 +locales +xz-utils +libpam-modules-bin +strace +xkb-data +dosfstools +libpolkit-agent-1-0 +usbutils +dmsetup +libfwupdplugin1 +ucf +libdbus-1-3 +vim-runtime +parted +libxmlsec1-openssl +debconf-i18n +libdevmapper-event1 +libjson-c4 +libmbim-proxy +sound-theme-freedesktop +python3-urllib3 +libgcc-s1 +libp11-kit0 +libparted2 +keyutils +libplymouth5 +libsemanage-common +procps +python3-httplib2 +libxslt1 +glib-networking-common +nano +libkeyutils1 +libx11-6 +linux-base-sgx +libreadline8 +libtext-charwidth-perl +libk5crypto3 +pciutils +sensible-utils +init-system-helpers +libatm1 +python3-requests +python3-idna +pci +vim-tiny +libnss3 +ncurses-bin +udev +cryptsetup-initramfs +libasound2-data +modemmanager +bsdmainutils +libevent-2 +xfsprogs +libbsd0 +ubuntu-advantage-pro +libfido2-1 +libzstd1 +chrony +libxmlb2 +python3-pyrsistent +gnupg +dmidecode +os-prober +libbz2-1 +libfastjson4 +efibootmgr +libdconf1 +libtdb1 +libldap-common +libreadline5 +netcat-openbsd +python3-gi +libpng16-16 +packagekit-tools +python3-twisted-bin +python3-cryptography +software-properties-common +popularity-contest +sg3-utils +walinuxagent +btrfs-progs +python3-lib2to3 +libyaml-0-2 +python3-serial +base-passwd +libsigsegv2 +keyboard-configuration +libsasl2-2 +gpgv +python-apt-common +grub-pc +gpg-wks-client +manpages +python3-gdbm +apport +hdparm +libdns-export1109 +vim +xdg-user-dirs +libxmuu1 +python3-cffi-backend +gdisk +libstdc +lsb-base +libip6tc2 +htop +linux-image-azure +linux-tools-azure +kpartx +libcanberra0 +libpam-modules +liberror-perl +motd-news-config +libgpg-error0 +libarchive13 +squashfs-tools +lshw +python3-incremental +libogg0 +telnet +libgstreamer1 +libheimntlm0-heimdal +python3-jinja2 +libslang2 +libpam-systemd +dconf-service +mount +python3-automat +python3-debian +python3-jsonpatch +dbus +ubuntu-minimal +packagekit +python3-more-itertools +python3-distro-info +at +libtext-iconv-perl +libpci3 +base-files +libblockdev-part2 +libsqlite3-0 +libkmod2 +libkrb5support0 +iputils-ping +libwrap0 +libcom-err2 +irqbalance +bcache-tools +update-notifier-common +libncurses6 +logsave +ubuntu-release-upgrader-core +libmbim-glib4 +bash-completion +lxd-agent-loader +python3-json-pointer +usb-modeswitch-data +fdisk +libestr0 +libsystemd0 +git-man +findutils +libhcrypto4-heimdal +libpsl5 +perl-modules-5 +python3-importlib-metadata +xxd +libtinfo6 +sbsigntool +file +libext2fs2 +passwd +sysvinit-utils +libasound2 +libunistring2 +libksba8 +plymouth-theme-ubuntu-text +distro-info-data +libtevent0 +libmount1 +libsasl2-modules +python3-jwt +libntfs-3g883 +gzip +publicsuffix +openssh-server +python3-hamcrest +iputils-tracepath +gpg-agent +iproute2 +libmm-glib0 +apt +libfwupdplugin5 +libtext-wrapi18n-perl +libvolume-key1 +libsodium23 +kmod +cloud-guest-utils +python3-apport +python3-openssl +linux-cloud-tools-azure +gir1 +libx11-data +libuv1 +unattended-upgrades +python3-six +run-one +linux-headers-azure +byobu +bc +libudisks2-0 +libargon2-1 +libdevmapper1 +cryptsetup +policykit-1 +libcap-ng0 +libgusb2 +libkrb5-26-heimdal +gsettings-desktop-schemas +ssh-import-id +python3-requests-unixsocket +thin-provisioning-tools +libc6 +libefivar1 +libusb-1 +login +libssl1 +libklibc +dpkg +python3-distro +eatmydata +bash +python3-chardet +libsmartcols1 +usb +libheimbase1-heimdal +fonts-ubuntu-console +libnss-systemd +libroken18-heimdal +libpolkit-gobject-1-0 +zerofree +initramfs-tools +libnettle7 +bind9-dnsutils +ubuntu-server +glib-networking-services +linux-tools-common +python3-markupsafe +liburcu6 +diffutils +python3-pexpect +libpcap0 +ncurses-base +libxml2 +libuchardet0 +libblkid1 +powermgmt-base +dbus-user-session +ca-certificates +sosreport +libtasn1-6 +busybox-initramfs +mawk +eject +libblockdev-fs2 +e2fsprogs +libdrm-common +python3-secretstorage +vim-common +grub-efi-amd64-bin +libudev1 +systemd +python3-certifi +ed +libbrotli1 +linux-image-5 +libfuse2 +python3-click +python3-jsonschema +bind9-libs +libsgutils2-2 +distro-info +libssh-4 +libtss2-esys0 +plymouth +zlib1g +libeatmydata1 +dconf-gsettings-backend +libapparmor1 +libblockdev-swap2 +libfdisk1 +libgcrypt20 +friendly-recovery +libkrb5-3 +libgpm2 +gawk +initramfs-tools-core +pollinate +libelf1 +gettext-base +kbd +libxmlsec1 +gpg +libnetplan0 +python3 +libcurl3-gnutls +apport-symptoms +libgpgme11 +python3-debconf +libnewt0 +isc-dhcp-common +language-selector-common +coreutils +grub-common +libsoup2 +console-setup +sudo +command-not-found +libdebconfclient0 +libjson-glib-1 +python3-launchpadlib +grep +cifs-utils +whiptail +linux-cloud-tools-common +libjcat1 +libisc-export1105 +cron +python3-pkg-resources +libblockdev-part-err2 +libnuma1 +libxau6 +libaudit-common +libglib2 +libselinux1 +libicu66 +git +python3-wadllib +libsepol1 +tmux +python3-commandnotfound +isc-dhcp-client +libpython3 +dmeventd +liblzma5 +python3-setuptools +tpm-udev +libunwind8 +grub-efi-amd64-signed +libtalloc2 +openssl +libmagic-mgc +libmpdec2 +libisns0 +libnfnetlink0 +libpam0g +linux-modules-5 +libffi7 +libaio1 +klibc-utils +libsmbios-c2 +python3-yaml +python3-entrypoints +psmisc +libutempter0 +linux-azure +libmaxminddb0 +libhx509-5-heimdal +python3-zipp +grub-pc-bin +rsyslog +libfwupd2 +python3-update-manager +libgirepository-1 +liblz4-1 +lsb-release +fwupd-signed +libassuan0 +fwupd +screen +python3-distutils +python3-pyasn1-modules +libfl2 +usb-modeswitch +libpipeline1 +liblocale-gettext-perl +libltdl7 +libmagic1 +krb5-locales +libaccountsservice0 +libsemanage1 +libpcre2-8-0 +pastebinit +linux-tools-5 +groff-base +landscape-common +ubuntu-standard +libgmp10 +libproxy1v5 +curl +finalrd +ethtool +python3-netifaces +info +libasn1-8-heimdal +libgnutls30 +libuuid1 +libpam-cap +python3-pymacaroons +libexpat1 +busybox-static +shared-mime-info +libwind0-heimdal +open-iscsi +ncurses-term +libparted-fs-resize0 +libcbor0 +bsdutils +python3-oauthlib +ubuntu-keyring +overlayroot +python3-colorama +mime-support +python3-newt +libsasl2-modules-db +multipath-tools +iso-codes +libidn2-0 +perl-base +python3-simplejson +python3-hyperlink +cpio +libnftnl11 +fuse +libxcb1 +libnetfilter-conntrack3 +gnupg-utils +liblmdb0 +cloud-initramfs-copymods +libmspack0 +libss2 +open-vm-tools +ftp +libblockdev-crypto2 +perl +accountsservice +iptables +linux-azure-5 +gpg-wks-server +hostname +grub-gfxpayload-lists +libnpth0 +readline-common +apparmor +libapt-pkg6 +gcc-10-base +linux-headers-5 +bolt +alsa-ucm-conf +lvm2 +libhogweed5 +ntfs-3g +systemd-sysv +tzdata +libpython3-stdlib +libwbclient0 +pinentry-curses +gnupg-l10n +secureboot-db +libedit2 +libcap2 +sed +python3-zope +python3-service-identity +libgssapi-krb5-2 +python3-keyring +install-info +netplan +python3-software-properties +lsscsi +ufw +libgdbm-compat4 +libaudit1 +libacl1 +python3-lazr +lsof +mtr-tiny +libdw1 +libefiboot1 +libpopt0 +python3-distupgrade +libgssapi3-heimdal +adduser +dirmngr +libvorbis0a +bind9-host +cryptsetup-bin +ltrace +python3-dbus +update-manager-core +python3-configobj +libpam-runtime +python3-systemd +python3-twisted +dash +uuid-runtime +libvorbisfile3 +shim-signed +libpcre3 +liblzo2-2 +debconf +snapd +cloud-init +libblockdev-utils2 +linux-base +python3-nacl +mdadm +libcurl4 +python3-ptyprocess +networkd-dispatcher +cloud-initramfs-dyn-netconf +util-linux +libgcab-1 +libnghttp2-14 +gpgsm +libblockdev2 +man-db +python3-constantly +liblvm2cmd2 +python3-minimal +initramfs-tools-bin +libldap-2 +libappstream4 +python3-pyasn1 +libfribidi0 +libblockdev-loop2 +tcpdump +udisks2 +xauth +libxmlb1 +libmnl0 +libncursesw6 +libc-bin +libseccomp2 +tar +libqmi-glib5 +grub2-common +alsa-topology-conf +time +libqmi-proxy +libgdbm6 +libprocps8 +libxtables12 +python3-attr +gpgconf +ubuntu-advantage-tools +console-setup-linux +librtmp1 +mokutil +glib-networking +libxdmcp6 +cryptsetup-run +debianutils +libip4tc2 +lz4 +python3-blinker +libpackagekit-glib2-18 +patch +libstemmer0d +libfreetype6 +less +init +systemd-timesyncd +python3-parted +libcrypt1 +netbase +libcap2-bin +linux-cloud-tools-5 +libcryptsetup12 +bzip2 +libdrm2 +logrotate +libperl5 +libatasmart4 +libxext6 +apt-utils +openssh-sftp-server +libgudev-1 +libnspr4 +sg3-utils-udev +wget +python3-apt diff --git a/tests/test_cloud.py b/tests/test_cloud.py new file mode 100644 index 0000000..1182295 --- /dev/null +++ b/tests/test_cloud.py @@ -0,0 +1,181 @@ +#!/usr/bin/python3 +# (C) 2022 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 json +import os +import pathlib +import sys +import unittest +from unittest.mock import patch +import xml.etree.ElementTree as ET + +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, PROJECT_DIR) + +from britney2.policies.cloud import CloudPolicy, ERR_MESSAGE +from tests.test_sourceppa import FakeOptions + +class FakeItem: + package = "chromium-browser" + version = "0.0.1" + +class FakeSourceData: + version = "55.0" + +class T(unittest.TestCase): + def setUp(self): + self.policy = CloudPolicy(FakeOptions, {}) + self.policy._setup_work_directory() + + def tearDown(self): + self.policy._cleanup_work_directory() + + def test_retrieve_series_and_pocket_from_path(self): + """Retrieves the series and pocket from the suite path. + Ensure an exception is raised if the regex fails to match. + """ + result = self.policy._retrieve_series_and_pocket_from_path("data/jammy-proposed") + self.assertTupleEqual(result, ("jammy", "proposed")) + + self.assertRaises( + RuntimeError, self.policy._retrieve_series_and_pocket_from_path, "data/badpath" + ) + + @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests") + def test_run_cloud_tests_called_for_package_in_manifest(self, mock_run): + """Cloud tests should run for a package in the cloud package set. + """ + self.policy.package_set = set(["chromium-browser"]) + self.policy.series = "jammy" + self.policy.pocket = "proposed" + + self.policy.apply_src_policy_impl( + None, FakeItem, None, FakeSourceData, None + ) + + mock_run.assert_called_once_with("chromium-browser", "jammy", "proposed") + + @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests") + def test_run_cloud_tests_not_called_for_package_not_in_manifest(self, mock_run): + """Cloud tests should not run for packages not in the cloud package set""" + + self.policy.package_set = set(["vim"]) + self.policy.series = "jammy" + self.policy.pocket = "proposed" + + self.policy.apply_src_policy_impl( + None, FakeItem, None, FakeSourceData, None + ) + + mock_run.assert_not_called() + + @patch("britney2.policies.cloud.smtplib") + @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests") + def test_no_tests_run_during_dry_run(self, mock_run, smtp): + self.policy = CloudPolicy(FakeOptions, {}, dry_run=True) + self.policy.package_set = set(["chromium-browser"]) + self.policy.series = "jammy" + self.policy.pocket = "proposed" + + self.policy.apply_src_policy_impl( + None, FakeItem, None, FakeSourceData, None + ) + + mock_run.assert_not_called() + self.assertEqual(smtp.mock_calls, []) + + def test_finding_results_file(self): + """Ensure result file output from Cloud Test Framework can be found""" + path = pathlib.PurePath(CloudPolicy.WORK_DIR, "TEST-FakeTests-20230101010101.xml") + path2 = pathlib.PurePath(CloudPolicy.WORK_DIR, "Test-OtherTests-20230101010101.xml") + with open(path, "a"): pass + with open(path2, "a"): pass + + regex = r"TEST-FakeTests-[0-9]*.xml" + results_file_paths = self.policy._find_results_files(regex) + + self.assertEqual(len(results_file_paths), 1) + self.assertEqual(results_file_paths[0], path) + + def test_parsing_of_xunit_results_file(self): + """Test that parser correctly sorts and stores test failures and errors""" + path = self._create_fake_test_result_file(num_pass=4, num_err=2, num_fail=3) + self.policy._parse_xunit_test_results("Azure", [path]) + + azure_failures = self.policy.failures.get("Azure", {}) + azure_errors = self.policy.errors.get("Azure", {}) + + self.assertEqual(len(azure_failures), 3) + self.assertEqual(len(azure_errors), 2) + + test_names = azure_failures.keys() + self.assertIn("failing_test_1", test_names) + + self.assertEqual( + azure_failures.get("failing_test_1"), "AssertionError: A useful error message" + ) + + def test_email_formatting(self): + """Test that information is inserted correctly in the email template""" + failures = { + "Azure": { + "failing_test1": "Error reason 1", + "failing_test2": "Error reason 2" + } + } + self.policy.series = "jammy" + self.policy.pocket = "proposed" + message = self.policy._format_email_message(ERR_MESSAGE, ["work@canonical.com"], "vim", "9.0", failures) + + self.assertIn("To: work@canonical.com", message) + self.assertIn("vim 9.0", message) + self.assertIn("Error reason 2", message) + + def _create_fake_test_result_file(self, num_pass=1, num_err=0, num_fail=0): + """Helper function to generate an xunit test result file. + + :param num_pass The number of passing tests to include + :param num_err The number of erroring tests to include + :param num_fail The number of failing tests to include + + Returns the path to the created file. + """ + os.makedirs(CloudPolicy.WORK_DIR, exist_ok=True) + path = pathlib.PurePath(CloudPolicy.WORK_DIR, "TEST-FakeTests-20230101010101.xml") + + root = ET.Element("testsuite", attrib={"name": "FakeTests-1234567890"}) + + for x in range(0, num_pass): + case_attrib = {"classname": "FakeTests", "name": "passing_test_{}".format(x), "time":"0.001"} + ET.SubElement(root, "testcase", attrib=case_attrib) + + for x in range(0, num_err): + case_attrib = {"classname": "FakeTests", "name": "erroring_test_{}".format(x), "time":"0.001"} + testcase = ET.SubElement(root, "testcase", attrib=case_attrib) + + err_attrib = {"type": "Exception", "message": "A useful error message" } + ET.SubElement(testcase, "error", attrib=err_attrib) + + for x in range(0, num_fail): + case_attrib = {"classname": "FakeTests", "name": "failing_test_{}".format(x), "time":"0.001"} + testcase = ET.SubElement(root, "testcase", attrib=case_attrib) + + fail_attrib = {"type": "AssertionError", "message": "A useful error message" } + ET.SubElement(testcase, "failure", attrib=fail_attrib) + + + tree = ET.ElementTree(root) + ET.indent(tree, space="\t", level=0) + + with open(path, "w") as file: + tree.write(file, encoding="unicode", xml_declaration=True) + + return path + +if __name__ == "__main__": + unittest.main() From 25875e7a27ba401fa9bae9f030deb7bfebce8fb8 Mon Sep 17 00:00:00 2001 From: Aleksa Svitlica Date: Sun, 8 Jan 2023 11:10:48 -0500 Subject: [PATCH 35/39] Add CLOUD_ENABLE option to config Updated to match other optional policies in how they are enabled or set to dry-run. --- britney.conf | 3 +++ britney.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/britney.conf b/britney.conf index 52f5b4c..d1c7176 100644 --- a/britney.conf +++ b/britney.conf @@ -122,3 +122,6 @@ SRUREGRESSIONEMAIL_ENABLE = no # we don't run piuparts testing in Ubuntu PIUPARTS_ENABLE = no + +# run cloud tests on packages +CLOUD_ENABLE = yes diff --git a/britney.py b/britney.py index 673a027..8afb51a 100755 --- a/britney.py +++ b/britney.py @@ -553,7 +553,11 @@ class Britney(object): self._policy_engine.add_policy(EmailPolicy(self.options, self.suite_info, dry_run=add_email_policy == 'dry-run')) - self._policy_engine.add_policy(CloudPolicy(self.options, self.suite_info, dry_run=self.options.dry_run)) + add_cloud_policy = getattr(self.options, 'cloud_enable', 'no') + if add_cloud_policy in ('yes', 'dry-run'): + self._policy_engine.add_policy(CloudPolicy(self.options, + self.suite_info, + dry_run=add_cloud_policy == 'dry-run')) @property def hints(self): From c6e4e18a23506bdbcaf91cde57dd69a606fa0580 Mon Sep 17 00:00:00 2001 From: Aleksa Svitlica Date: Mon, 23 Jan 2023 16:10:41 -0500 Subject: [PATCH 36/39] CloudPolicy: Move hardcoded values to config Changes made - Multiple hardcoded fields moved to config - Series is now retrieved from options - Pocket is now called source and retrieved from config - Adds source type config which can be either archive or ppa - Returns REJECTED_PERMANENTLY policy verdict when test failures or errors occur. Adds verdict info the the excuse. --- britney.conf | 23 +++++- britney2/policies/cloud.py | 153 +++++++++++++++++++++---------------- tests/test_cloud.py | 99 +++++++++++++++++------- 3 files changed, 183 insertions(+), 92 deletions(-) diff --git a/britney.conf b/britney.conf index d1c7176..cd15063 100644 --- a/britney.conf +++ b/britney.conf @@ -124,4 +124,25 @@ SRUREGRESSIONEMAIL_ENABLE = no PIUPARTS_ENABLE = no # run cloud tests on packages -CLOUD_ENABLE = yes +CLOUD_ENABLE = no +# Where the package can be downloaded from in the cloud. Either an archive or ppa. +# For PPAs it should be defined in the format = +CLOUD_SOURCE = proposed +# Can be either 'archive' or 'ppa' +CLOUD_SOURCE_TYPE = archive +# A directory to store Cloud test results and logs. Is created at the start of +# each policy run and deleted after test results are parsed. +CLOUD_WORK_DIR = cloud_tests +# Who to notify regarding test failures +CLOUD_FAILURE_EMAILS = cpc@canonical.com +# Who to notify regarding test errors +CLOUD_ERROR_EMAILS = cpc@canonical.com +# A set of Azure specific settings +CLOUD_AZURE_LOCATION = westeurope +CLOUD_AZURE_VM_SIZE = Standard_D2s_v5 + +CLOUD_AZURE_LUNAR_URN = Canonical:0001-com-ubuntu-server-lunar-daily:23_04-daily-gen2:23.04.202301030 +CLOUD_AZURE_KINETIC_URN = Canonical:0001-com-ubuntu-server-kinetic:22_10:22.10.202301040 +CLOUD_AZURE_JAMMY_URN = Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:22.04.202212140 +CLOUD_AZURE_FOCAL_URN = Canonical:0001-com-ubuntu-server-focal:20_04-lts-gen2:20.04.202212140 +CLOUD_AZURE_BIONIC_URN = Canonical:UbuntuServer:18_04-lts-gen2:18.04.202212090 diff --git a/britney2/policies/cloud.py b/britney2/policies/cloud.py index f97453a..ebbd591 100644 --- a/britney2/policies/cloud.py +++ b/britney2/policies/cloud.py @@ -12,10 +12,13 @@ from britney2 import SuiteClass from britney2.policies.policy import BasePolicy from britney2.policies import PolicyVerdict +class MissingURNException(Exception): + pass + FAIL_MESSAGE = """From: Ubuntu Release Team To: {recipients} X-Proposed-Migration: notice -Subject: [proposed-migration] {package} {version} in {series}-{pocket} failed Cloud tests. +Subject: [proposed-migration] {package} {version} in {series}, {source} failed Cloud tests. Hi, @@ -33,7 +36,7 @@ Regards, Ubuntu Release Team. ERR_MESSAGE = """From: Ubuntu Release Team To: {recipients} X-Proposed-Migration: notice -Subject: [proposed-migration] {package} {version} in {series}-{pocket} had errors running Cloud Tests. +Subject: [proposed-migration] {package} {version} in {series}, {source} had errors running Cloud Tests. Hi, @@ -46,16 +49,8 @@ If you have any questions about this email, please ask them in #ubuntu-release c Regards, Ubuntu Release Team. """ class CloudPolicy(BasePolicy): - WORK_DIR = "cloud_tests" PACKAGE_SET_FILE = "cloud_package_set" - EMAILS = ["cpc@canonical.com"] - SERIES_TO_URN = { - "lunar": "Canonical:0001-com-ubuntu-server-lunar-daily:23_04-daily-gen2:23.04.202301030", - "kinetic": "Canonical:0001-com-ubuntu-server-kinetic:22_10:22.10.202301040", - "jammy": "Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:22.04.202212140", - "focal": "Canonical:0001-com-ubuntu-server-focal:20_04-lts-gen2:20.04.202212140", - "bionic": "Canonical:UbuntuServer:18_04-lts-gen2:18.04.202212090" - } + DEFAULT_EMAILS = ["cpc@canonical.com"] def __init__(self, options, suite_info, dry_run=False): super().__init__( @@ -69,17 +64,20 @@ class CloudPolicy(BasePolicy): self.logger.info( "Cloud Policy: will send emails to: %s", self.email_host ) + self.work_dir = getattr(self.options, "cloud_work_dir", "cloud_tests") + self.failure_emails = getattr(self.options, "cloud_failure_emails", self.DEFAULT_EMAILS) + self.error_emails = getattr(self.options, "cloud_error_emails", self.DEFAULT_EMAILS) + + self.source = getattr(self.options, "cloud_source") + self.source_type = getattr(self.options, "cloud_source_type") + self.failures = {} self.errors = {} def initialise(self, britney): super().initialise(britney) - primary_suite = self.suite_info.primary_source_suite - series, pocket = self._retrieve_series_and_pocket_from_path(primary_suite.path) - self.series = series - self.pocket = pocket - self.package_set = self._retrieve_cloud_package_set_for_series(self.series) + self.package_set = self._retrieve_cloud_package_set_for_series(self.options.series) def apply_src_policy_impl(self, policy_info, item, source_data_tdist, source_data_srcdist, excuse): if item.package not in self.package_set: @@ -87,7 +85,7 @@ class CloudPolicy(BasePolicy): if self.dry_run: self.logger.info( - "Cloud Policy: Dry run would test {} in {}-{}".format(item.package , self.series, self.pocket) + "Cloud Policy: Dry run would test {} in {}, {}".format(item.package , self.options.series, self.source) ) return PolicyVerdict.PASS @@ -95,11 +93,19 @@ class CloudPolicy(BasePolicy): self.failures = {} self.errors = {} - self._run_cloud_tests(item.package, self.series, self.pocket) - self._send_emails_if_needed(item.package, source_data_srcdist.version, self.series, self.pocket) + self._run_cloud_tests(item.package, self.options.series, self.source, self.source_type) - self._cleanup_work_directory() - return PolicyVerdict.PASS + if len(self.failures) > 0 or len(self.errors) > 0: + self._send_emails_if_needed(item.package, source_data_srcdist.version, self.options.series, self.source) + + self._cleanup_work_directory() + verdict = PolicyVerdict.REJECTED_PERMANENTLY + info = self._generate_verdict_info(self.failures, self.errors) + excuse.add_verdict_info(verdict, info) + return verdict + else: + self._cleanup_work_directory() + return PolicyVerdict.PASS def _retrieve_cloud_package_set_for_series(self, series): """Retrieves a set of packages for the given series in which cloud @@ -118,44 +124,27 @@ class CloudPolicy(BasePolicy): return package_set - def _retrieve_series_and_pocket_from_path(self, suite_path): - """Given the suite path return a tuple of series and pocket. - With input 'data/jammy-proposed' the expected output is a tuple of - (jammy, proposed) - - :param suite_path The path attribute of the suite - """ - result = (None, None) - match = re.search("([a-z]*)-([a-z]*)$", suite_path) - - if(match): - result = match.groups() - else: - raise RuntimeError( - "Could not determine series and pocket from the path: %s" %suite_path - ) - return result - - def _run_cloud_tests(self, package, series, pocket): + def _run_cloud_tests(self, package, series, source, source_type): """Runs any cloud tests for the given package. - Returns a list of failed tests or an empty list if everything passed. + Nothing is returned but test failures and errors are stored in instance variables. :param package The name of the package to test :param series The Ubuntu codename for the series (e.g. jammy) - :param pocket The name of the pocket the package should be installed from (e.g. proposed) + :param source Where the package should be installed from (e.g. proposed or a PPA) + :param source_type Either 'archive' or 'ppa' """ - self._run_azure_tests(package, series, pocket) + self._run_azure_tests(package, series, source, source_type) - def _send_emails_if_needed(self, package, version, series, pocket): + def _send_emails_if_needed(self, package, version, series, source): """Sends email(s) if there are test failures and/or errors :param package The name of the package that was tested :param version The version number of the package :param series The Ubuntu codename for the series (e.g. jammy) - :param pocket The name of the pocket the package should be installed from (e.g. proposed) + :param source Where the package should be installed from (e.g. proposed or a PPA) """ if len(self.failures) > 0: - emails = self.EMAILS + emails = self.failure_emails message = self._format_email_message( FAIL_MESSAGE, emails, package, version, self.failures ) @@ -163,51 +152,64 @@ class CloudPolicy(BasePolicy): self._send_email(emails, message) if len(self.errors) > 0: - emails = self.EMAILS + emails = self.error_emails message = self._format_email_message( ERR_MESSAGE, emails, package, version, self.errors ) self.logger.info("Cloud Policy: Sending error email for {}, to {}".format(package, emails)) self._send_email(emails, message) - def _run_azure_tests(self, package, series, pocket): + def _run_azure_tests(self, package, series, source, source_type): """Runs Azure's required package tests. :param package The name of the package to test :param series The Ubuntu codename for the series (e.g. jammy) - :param pocket The name of the pocket the package should be installed from (e.g. proposed) + :param source Where the package should be installed from (e.g. proposed or a PPA) + :param source_type Either 'archive' or 'ppa' """ - urn = self.SERIES_TO_URN.get(series, None) - if urn is None: - return + urn = self._retrieve_urn(series) + install_flag = self._determine_install_flag(source_type) - self.logger.info("Cloud Policy: Running Azure tests for: {} in {}-{}".format(package, series, pocket)) + self.logger.info("Cloud Policy: Running Azure tests for: {} in {}, {}".format(package, series, source)) subprocess.run( [ "/snap/bin/cloud-test-framework", - "--instance-prefix", "britney-{}-{}-{}".format(package, series, pocket), - "--install-archive-package", "{}/{}".format(package, pocket), + "--instance-prefix", "britney-{}-{}-{}".format(package, series, source), + install_flag, "{}/{}".format(package, source), "azure_gen2", "--location", "westeurope", "--vm-size", "Standard_D2s_v5", "--urn", urn, "run-test", "package-install-with-reboot", ], - cwd=self.WORK_DIR + cwd=self.work_dir ) results_file_paths = self._find_results_files(r"TEST-NetworkTests-[0-9]*.xml") self._parse_xunit_test_results("Azure", results_file_paths) + def _retrieve_urn(self, series): + """Retrieves an URN from the configuration options based on series. + An URN identifies a unique image in Azure. + + :param series The ubuntu codename for the series (e.g. jammy) + """ + urn = getattr(self.options, "cloud_azure_{}_urn".format(series), None) + + if urn is None: + raise MissingURNException("No URN configured for {}".format(series)) + + return urn + def _find_results_files(self, file_regex): """Find any test results files that match the given regex pattern. :param file_regex A regex pattern to use for matching the name of the results file. """ file_paths = [] - for file in os.listdir(self.WORK_DIR): + for file in os.listdir(self.work_dir): if re.fullmatch(file_regex, file): - file_paths.append(PurePath(self.WORK_DIR, file)) + file_paths.append(PurePath(self.work_dir, file)) return file_paths @@ -215,7 +217,7 @@ class CloudPolicy(BasePolicy): """Parses and stores any failure or error test results. :param cloud The name of the cloud, use for storing the results. - :results_file_paths List of paths to results files + :param results_file_paths List of paths to results files """ for file_path in results_file_paths: with open(file_path) as file: @@ -250,11 +252,11 @@ class CloudPolicy(BasePolicy): def _format_email_message(self, template, emails, package, version, test_results): """Insert given parameters into the email template.""" - series = self.series - pocket = self.pocket + series = self.options.series + source = self.source results = json.dumps(test_results, indent=4) recipients = ", ".join(emails) - message= template.format(**locals()) + message = template.format(**locals()) return message @@ -272,14 +274,35 @@ class CloudPolicy(BasePolicy): self.logger.error("Cloud Policy: Failed to send mail! Is SMTP server running?") self.logger.error(err) + def _generate_verdict_info(self, failures, errors): + info = "" + + if len(failures) > 0: + fail_clouds = ",".join(list(failures.keys())) + info += "Cloud testing failed for {}.".format(fail_clouds) + + if len(errors) > 0: + error_clouds = ",".join(list(errors.keys())) + info += " Cloud testing had errors for {}.".format(error_clouds) + + return info + + def _determine_install_flag(self, source_type): + if source_type == "archive": + return "--install-archive-package" + elif source_type == "ppa": + return "--install-ppa-package" + else: + raise RuntimeError("Cloud Policy: Unexpected source type, {}".format(source_type)) + def _setup_work_directory(self): """Create a directory for tests to be run in.""" self._cleanup_work_directory() - os.makedirs(self.WORK_DIR) + os.makedirs(self.work_dir) def _cleanup_work_directory(self): """Delete the the directory used for running tests.""" - if os.path.exists(self.WORK_DIR): - shutil.rmtree(self.WORK_DIR) + if os.path.exists(self.work_dir): + shutil.rmtree(self.work_dir) diff --git a/tests/test_cloud.py b/tests/test_cloud.py index 1182295..ac356ad 100644 --- a/tests/test_cloud.py +++ b/tests/test_cloud.py @@ -17,8 +17,7 @@ import xml.etree.ElementTree as ET PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, PROJECT_DIR) -from britney2.policies.cloud import CloudPolicy, ERR_MESSAGE -from tests.test_sourceppa import FakeOptions +from britney2.policies.cloud import CloudPolicy, ERR_MESSAGE, MissingURNException class FakeItem: package = "chromium-browser" @@ -27,6 +26,15 @@ class FakeItem: class FakeSourceData: version = "55.0" +class FakeOptions: + distribution = "testbuntu" + series = "zazzy" + unstable = "/tmp" + verbose = False + cloud_source = "zazzy-proposed" + cloud_source_type = "archive" + cloud_azure_zazzy_urn = "fake-urn-value" + class T(unittest.TestCase): def setUp(self): self.policy = CloudPolicy(FakeOptions, {}) @@ -35,38 +43,29 @@ class T(unittest.TestCase): def tearDown(self): self.policy._cleanup_work_directory() - def test_retrieve_series_and_pocket_from_path(self): - """Retrieves the series and pocket from the suite path. - Ensure an exception is raised if the regex fails to match. - """ - result = self.policy._retrieve_series_and_pocket_from_path("data/jammy-proposed") - self.assertTupleEqual(result, ("jammy", "proposed")) - - self.assertRaises( - RuntimeError, self.policy._retrieve_series_and_pocket_from_path, "data/badpath" - ) - @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests") def test_run_cloud_tests_called_for_package_in_manifest(self, mock_run): """Cloud tests should run for a package in the cloud package set. """ self.policy.package_set = set(["chromium-browser"]) - self.policy.series = "jammy" - self.policy.pocket = "proposed" + self.policy.options.series = "jammy" + self.policy.source = "jammy-proposed" self.policy.apply_src_policy_impl( None, FakeItem, None, FakeSourceData, None ) - mock_run.assert_called_once_with("chromium-browser", "jammy", "proposed") + mock_run.assert_called_once_with( + "chromium-browser", "jammy", "jammy-proposed", "archive" + ) @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests") def test_run_cloud_tests_not_called_for_package_not_in_manifest(self, mock_run): """Cloud tests should not run for packages not in the cloud package set""" self.policy.package_set = set(["vim"]) - self.policy.series = "jammy" - self.policy.pocket = "proposed" + self.policy.options.series = "jammy" + self.policy.source = "jammy-proposed" self.policy.apply_src_policy_impl( None, FakeItem, None, FakeSourceData, None @@ -79,8 +78,8 @@ class T(unittest.TestCase): def test_no_tests_run_during_dry_run(self, mock_run, smtp): self.policy = CloudPolicy(FakeOptions, {}, dry_run=True) self.policy.package_set = set(["chromium-browser"]) - self.policy.series = "jammy" - self.policy.pocket = "proposed" + self.policy.options.series = "jammy" + self.policy.source = "jammy-proposed" self.policy.apply_src_policy_impl( None, FakeItem, None, FakeSourceData, None @@ -91,8 +90,8 @@ class T(unittest.TestCase): def test_finding_results_file(self): """Ensure result file output from Cloud Test Framework can be found""" - path = pathlib.PurePath(CloudPolicy.WORK_DIR, "TEST-FakeTests-20230101010101.xml") - path2 = pathlib.PurePath(CloudPolicy.WORK_DIR, "Test-OtherTests-20230101010101.xml") + path = pathlib.PurePath(self.policy.work_dir, "TEST-FakeTests-20230101010101.xml") + path2 = pathlib.PurePath(self.policy.work_dir, "Test-OtherTests-20230101010101.xml") with open(path, "a"): pass with open(path2, "a"): pass @@ -128,14 +127,62 @@ class T(unittest.TestCase): "failing_test2": "Error reason 2" } } - self.policy.series = "jammy" - self.policy.pocket = "proposed" + self.policy.options.series = "jammy" + self.policy.source = "jammy-proposed" message = self.policy._format_email_message(ERR_MESSAGE, ["work@canonical.com"], "vim", "9.0", failures) self.assertIn("To: work@canonical.com", message) self.assertIn("vim 9.0", message) self.assertIn("Error reason 2", message) + def test_urn_retrieval(self): + """Test that URN retrieval throws the expected error when not configured.""" + self.assertRaises( + MissingURNException, self.policy._retrieve_urn, "jammy" + ) + + urn = self.policy._retrieve_urn("zazzy") + self.assertEqual(urn, "fake-urn-value") + + def test_generation_of_verdict_info(self): + """Test that the verdict info correctly states which clouds had failures and/or errors""" + failures = { + "cloud1": { + "test_name1": "message1", + "test_name2": "message2" + }, + "cloud2": { + "test_name3": "message3" + } + } + + errors = { + "cloud1": { + "test_name4": "message4", + }, + "cloud3": { + "test_name5": "message5" + } + } + + info = self.policy._generate_verdict_info(failures, errors) + + expected_failure_info = "Cloud testing failed for cloud1,cloud2." + expected_error_info = "Cloud testing had errors for cloud1,cloud3." + + self.assertIn(expected_failure_info, info) + self.assertIn(expected_error_info, info) + + def test_determine_install_flag(self): + """Ensure the correct flag is determined and errors are raised for unknown source types""" + install_flag = self.policy._determine_install_flag("archive") + self.assertEqual(install_flag, "--install-archive-package") + + install_flag = self.policy._determine_install_flag("ppa") + self.assertEqual(install_flag, "--install-ppa-package") + + self.assertRaises(RuntimeError, self.policy._determine_install_flag, "something") + def _create_fake_test_result_file(self, num_pass=1, num_err=0, num_fail=0): """Helper function to generate an xunit test result file. @@ -145,8 +192,8 @@ class T(unittest.TestCase): Returns the path to the created file. """ - os.makedirs(CloudPolicy.WORK_DIR, exist_ok=True) - path = pathlib.PurePath(CloudPolicy.WORK_DIR, "TEST-FakeTests-20230101010101.xml") + os.makedirs(self.policy.work_dir, exist_ok=True) + path = pathlib.PurePath(self.policy.work_dir, "TEST-FakeTests-20230101010101.xml") root = ET.Element("testsuite", attrib={"name": "FakeTests-1234567890"}) From 1c6f56ab79405021a3f14778ffbe96201503798b Mon Sep 17 00:00:00 2001 From: Aleksa Svitlica Date: Mon, 23 Jan 2023 20:17:01 -0500 Subject: [PATCH 37/39] Cloud Policy: Remove xunitparser dependency To avoid an external dependency the required xunit parsing has been implemented directly in the policy. --- britney2/policies/cloud.py | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/britney2/policies/cloud.py b/britney2/policies/cloud.py index ebbd591..d62bbcd 100644 --- a/britney2/policies/cloud.py +++ b/britney2/policies/cloud.py @@ -6,7 +6,7 @@ import shutil import smtplib import socket import subprocess -import xunitparser +import xml.etree.ElementTree as ET from britney2 import SuiteClass from britney2.policies.policy import BasePolicy @@ -221,13 +221,38 @@ class CloudPolicy(BasePolicy): """ for file_path in results_file_paths: with open(file_path) as file: - ts, tr = xunitparser.parse(file) + xml = ET.parse(file) + root = xml.getroot() - for testcase, message in tr.failures: - self.store_test_result(self.failures, cloud, testcase.methodname, message) + if root.tag == "testsuites": + for testsuite in root: + self._parse_xunit_testsuite(cloud, testsuite) + else: + self._parse_xunit_testsuite(cloud, root) - for testcase, message in tr.errors: - self.store_test_result(self.errors, cloud, testcase.methodname, message) + def _parse_xunit_testsuite(self, cloud, root): + """Parses the xunit testsuite and stores any failure or error test results. + + :param cloud The name of the cloud, used for storing the results. + :param root An XML tree root. + """ + for el in root: + if el.tag == "testcase": + for e in el: + if e.tag == "failure": + type = e.attrib.get('type') + message = e.attrib.get('message') + info = "{}: {}".format(type, message) + self.store_test_result( + self.failures, cloud, el.attrib.get('name'), info + ) + if e.tag == "error": + type = e.attrib.get('type') + message = e.attrib.get('message') + info = "{}: {}".format(type, message) + self.store_test_result( + self.errors, cloud, el.attrib.get('name'), info + ) def store_test_result(self, results, cloud, test_name, message): """Adds the test to the results hash under the given cloud. From 2bd1584c8454d434f62c56c142b044dce451c7a0 Mon Sep 17 00:00:00 2001 From: Aleksa Svitlica Date: Wed, 1 Feb 2023 21:23:59 -0500 Subject: [PATCH 38/39] CloudPolicy: Refactor to use adt_ppas Cloud policy now uses the ADT_PPAS field as the source but will only take private PPAS due to requiring a fingerprint. If ADT_PPAS is empty then the 'proposed' archive is used. Adds logic to try and retrieve the install source of a package from the logs. This is useful if multiple PPAs are defined since the policy won't explicitly know when contains the package under test. --- britney.conf | 5 -- britney2/policies/cloud.py | 178 +++++++++++++++++++++++++++++-------- tests/test_cloud.py | 121 ++++++++++++++++++++----- 3 files changed, 242 insertions(+), 62 deletions(-) diff --git a/britney.conf b/britney.conf index cd15063..e6955ae 100644 --- a/britney.conf +++ b/britney.conf @@ -125,11 +125,6 @@ PIUPARTS_ENABLE = no # run cloud tests on packages CLOUD_ENABLE = no -# Where the package can be downloaded from in the cloud. Either an archive or ppa. -# For PPAs it should be defined in the format = -CLOUD_SOURCE = proposed -# Can be either 'archive' or 'ppa' -CLOUD_SOURCE_TYPE = archive # A directory to store Cloud test results and logs. Is created at the start of # each policy run and deleted after test results are parsed. CLOUD_WORK_DIR = cloud_tests diff --git a/britney2/policies/cloud.py b/britney2/policies/cloud.py index d62bbcd..3fcc123 100644 --- a/britney2/policies/cloud.py +++ b/britney2/policies/cloud.py @@ -18,7 +18,7 @@ class MissingURNException(Exception): FAIL_MESSAGE = """From: Ubuntu Release Team To: {recipients} X-Proposed-Migration: notice -Subject: [proposed-migration] {package} {version} in {series}, {source} failed Cloud tests. +Subject: [proposed-migration] {package} {version} in {series} failed Cloud tests. Hi, @@ -36,7 +36,7 @@ Regards, Ubuntu Release Team. ERR_MESSAGE = """From: Ubuntu Release Team To: {recipients} X-Proposed-Migration: notice -Subject: [proposed-migration] {package} {version} in {series}, {source} had errors running Cloud Tests. +Subject: [proposed-migration] {package} {version} in {series} had errors running Cloud Tests. Hi, @@ -51,6 +51,7 @@ Regards, Ubuntu Release Team. class CloudPolicy(BasePolicy): PACKAGE_SET_FILE = "cloud_package_set" DEFAULT_EMAILS = ["cpc@canonical.com"] + TEST_LOG_FILE = "CTF.log" def __init__(self, options, suite_info, dry_run=False): super().__init__( @@ -68,8 +69,15 @@ class CloudPolicy(BasePolicy): self.failure_emails = getattr(self.options, "cloud_failure_emails", self.DEFAULT_EMAILS) self.error_emails = getattr(self.options, "cloud_error_emails", self.DEFAULT_EMAILS) - self.source = getattr(self.options, "cloud_source") - self.source_type = getattr(self.options, "cloud_source_type") + adt_ppas = getattr(self.options, "adt_ppas", []) + ppas = self._parse_ppas(adt_ppas) + + if len(ppas) == 0: + self.sources = ["proposed"] + self.source_type = "archive" + else: + self.sources = ppas + self.source_type = "ppa" self.failures = {} self.errors = {} @@ -85,7 +93,7 @@ class CloudPolicy(BasePolicy): if self.dry_run: self.logger.info( - "Cloud Policy: Dry run would test {} in {}, {}".format(item.package , self.options.series, self.source) + "Cloud Policy: Dry run would test {} in {}".format(item.package , self.options.series) ) return PolicyVerdict.PASS @@ -93,10 +101,10 @@ class CloudPolicy(BasePolicy): self.failures = {} self.errors = {} - self._run_cloud_tests(item.package, self.options.series, self.source, self.source_type) + self._run_cloud_tests(item.package, self.options.series, self.sources, self.source_type) if len(self.failures) > 0 or len(self.errors) > 0: - self._send_emails_if_needed(item.package, source_data_srcdist.version, self.options.series, self.source) + self._send_emails_if_needed(item.package, source_data_srcdist.version, self.options.series) self._cleanup_work_directory() verdict = PolicyVerdict.REJECTED_PERMANENTLY @@ -124,24 +132,23 @@ class CloudPolicy(BasePolicy): return package_set - def _run_cloud_tests(self, package, series, source, source_type): + def _run_cloud_tests(self, package, series, sources, source_type): """Runs any cloud tests for the given package. Nothing is returned but test failures and errors are stored in instance variables. :param package The name of the package to test :param series The Ubuntu codename for the series (e.g. jammy) - :param source Where the package should be installed from (e.g. proposed or a PPA) + :param sources List of sources where the package should be installed from (e.g. [proposed] or PPAs) :param source_type Either 'archive' or 'ppa' """ - self._run_azure_tests(package, series, source, source_type) + self._run_azure_tests(package, series, sources, source_type) - def _send_emails_if_needed(self, package, version, series, source): + def _send_emails_if_needed(self, package, version, series): """Sends email(s) if there are test failures and/or errors :param package The name of the package that was tested :param version The version number of the package :param series The Ubuntu codename for the series (e.g. jammy) - :param source Where the package should be installed from (e.g. proposed or a PPA) """ if len(self.failures) > 0: emails = self.failure_emails @@ -159,34 +166,42 @@ class CloudPolicy(BasePolicy): self.logger.info("Cloud Policy: Sending error email for {}, to {}".format(package, emails)) self._send_email(emails, message) - def _run_azure_tests(self, package, series, source, source_type): + def _run_azure_tests(self, package, series, sources, source_type): """Runs Azure's required package tests. :param package The name of the package to test :param series The Ubuntu codename for the series (e.g. jammy) - :param source Where the package should be installed from (e.g. proposed or a PPA) + :param sources List of sources where the package should be installed from (e.g. [proposed] or PPAs) :param source_type Either 'archive' or 'ppa' """ urn = self._retrieve_urn(series) - install_flag = self._determine_install_flag(source_type) - self.logger.info("Cloud Policy: Running Azure tests for: {} in {}, {}".format(package, series, source)) - subprocess.run( + self.logger.info("Cloud Policy: Running Azure tests for: {} in {}".format(package, series)) + params = [ + "/snap/bin/cloud-test-framework", + "--instance-prefix", "britney-{}-{}".format(package, series) + ] + params.extend(self._format_install_flags(package, sources, source_type)) + params.extend( [ - "/snap/bin/cloud-test-framework", - "--instance-prefix", "britney-{}-{}-{}".format(package, series, source), - install_flag, "{}/{}".format(package, source), "azure_gen2", - "--location", "westeurope", - "--vm-size", "Standard_D2s_v5", + "--location", getattr(self.options, "cloud_azure_location", "westeurope"), + "--vm-size", getattr(self.options, "cloud_azure_vm_size", "Standard_D2s_v5"), "--urn", urn, "run-test", "package-install-with-reboot", - ], - cwd=self.work_dir + ] ) + with open(PurePath(self.work_dir, self.TEST_LOG_FILE), "w") as file: + subprocess.run( + params, + cwd=self.work_dir, + stdout=file + ) + results_file_paths = self._find_results_files(r"TEST-NetworkTests-[0-9]*.xml") self._parse_xunit_test_results("Azure", results_file_paths) + self._store_extra_test_result_info(self, package) def _retrieve_urn(self, series): """Retrieves an URN from the configuration options based on series. @@ -243,18 +258,18 @@ class CloudPolicy(BasePolicy): type = e.attrib.get('type') message = e.attrib.get('message') info = "{}: {}".format(type, message) - self.store_test_result( + self._store_test_result( self.failures, cloud, el.attrib.get('name'), info ) if e.tag == "error": type = e.attrib.get('type') message = e.attrib.get('message') info = "{}: {}".format(type, message) - self.store_test_result( + self._store_test_result( self.errors, cloud, el.attrib.get('name'), info ) - def store_test_result(self, results, cloud, test_name, message): + def _store_test_result(self, results, cloud, test_name, message): """Adds the test to the results hash under the given cloud. Results format: @@ -275,10 +290,61 @@ class CloudPolicy(BasePolicy): results[cloud][test_name] = message + def _store_extra_test_result_info(self, cloud, package): + """Stores any information beyond the test results and stores it in the results dicts + under Cloud->extra_info + + Stores any information retrieved under the cloud's section in failures/errors but will + store nothing if failures/errors are empty. + + :param cloud The name of the cloud + :param package The name of the package to test + """ + if len(self.failures) == 0 and len(self.errors) == 0: + return + + extra_info = {} + + install_source = self._retrieve_package_install_source_from_test_output(package) + if install_source: + extra_info["install_source"] = install_source + + if len(self.failures.get(cloud, {})) > 0: + self._store_test_result(self.failures, cloud, "extra_info", extra_info) + + if len(self.errors.get(cloud, {})) > 0: + self._store_test_result(self.errors, cloud, "extra_info", extra_info) + + def _retrieve_package_install_source_from_test_output(self, package): + """Checks the test logs for apt logs which show where the package was installed from. + Useful if multiple PPA sources are defined since we won't explicitly know the exact source. + + Will return nothing unless exactly one matching line is found. + + :param package The name of the package to test + """ + possible_locations = [] + with open(PurePath(self.work_dir, self.TEST_LOG_FILE), "r") as file: + for line in file: + if package not in line: + continue + + if "Get:" not in line: + continue + + if " {} ".format(package) not in line: + continue + + possible_locations.append(line) + + if len(possible_locations) == 1: + return possible_locations[0] + else: + return None + def _format_email_message(self, template, emails, package, version, test_results): """Insert given parameters into the email template.""" series = self.options.series - source = self.source results = json.dumps(test_results, indent=4) recipients = ", ".join(emails) message = template.format(**locals()) @@ -312,13 +378,55 @@ class CloudPolicy(BasePolicy): return info - def _determine_install_flag(self, source_type): - if source_type == "archive": - return "--install-archive-package" - elif source_type == "ppa": - return "--install-ppa-package" - else: - raise RuntimeError("Cloud Policy: Unexpected source type, {}".format(source_type)) + def _format_install_flags(self, package, sources, source_type): + """Determine the flags required to install the package from the given sources + + :param package The name of the package to test + :param sources List of sources where the package should be installed from (e.g. [proposed] or PPAs) + :param source_type Either 'archive' or 'ppa' + """ + install_flags = [] + + for source in sources: + if source_type == "archive": + install_flags.append("--install-archive-package") + install_flags.append("{}/{}".format(package, source)) + elif source_type == "ppa": + install_flags.append("--install-ppa-package") + install_flags.append("{}/{}".format(package, source)) + else: + raise RuntimeError("Cloud Policy: Unexpected source type, {}".format(source_type)) + + return install_flags + + def _parse_ppas(self, ppas): + """Parse PPA list to store in format expected by cloud tests + + Since currently only private PPAs in Britney are provided with fingerprints we will + only use those. + + Britney private PPA format: + 'user:token@team/name:fingerprint' + Cloud private PPA format: + 'https://user:token@private-ppa.launchpadcontent.net/team/name/ubuntu=fingerprint + + :param ppas List of PPAs in Britney approved format + :return A list of PPAs in valid cloud test format. Can return an empty list if none found. + """ + cloud_ppas = [] + + for ppa in ppas: + if '@' in ppa: + match = re.match("^(?P.+:.+)@(?P.+):(?P.+$)", ppa) + if not match: + raise RuntimeError('Private PPA %s not following required format (user:token@team/name:fingerprint)', ppa) + + formatted_ppa = "https://{}@private-ppa.launchpadcontent.net/{}/ubuntu={}".format( + match.group("auth"), match.group("name"), match.group("fingerprint") + ) + cloud_ppas.append(formatted_ppa) + + return cloud_ppas def _setup_work_directory(self): """Create a directory for tests to be run in.""" diff --git a/tests/test_cloud.py b/tests/test_cloud.py index ac356ad..30363bf 100644 --- a/tests/test_cloud.py +++ b/tests/test_cloud.py @@ -6,10 +6,10 @@ # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. -import json import os import pathlib import sys +from types import SimpleNamespace import unittest from unittest.mock import patch import xml.etree.ElementTree as ET @@ -26,18 +26,18 @@ class FakeItem: class FakeSourceData: version = "55.0" -class FakeOptions: - distribution = "testbuntu" - series = "zazzy" - unstable = "/tmp" - verbose = False - cloud_source = "zazzy-proposed" - cloud_source_type = "archive" - cloud_azure_zazzy_urn = "fake-urn-value" - class T(unittest.TestCase): def setUp(self): - self.policy = CloudPolicy(FakeOptions, {}) + self.fake_options = SimpleNamespace( + distrubtion = "testbuntu", + series = "zazzy", + unstable = "/tmp", + verbose = False, + cloud_source = "zazzy-proposed", + cloud_source_type = "archive", + cloud_azure_zazzy_urn = "fake-urn-value" + ) + self.policy = CloudPolicy(self.fake_options, {}) self.policy._setup_work_directory() def tearDown(self): @@ -49,14 +49,13 @@ class T(unittest.TestCase): """ self.policy.package_set = set(["chromium-browser"]) self.policy.options.series = "jammy" - self.policy.source = "jammy-proposed" self.policy.apply_src_policy_impl( None, FakeItem, None, FakeSourceData, None ) mock_run.assert_called_once_with( - "chromium-browser", "jammy", "jammy-proposed", "archive" + "chromium-browser", "jammy", ["proposed"], "archive" ) @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests") @@ -65,7 +64,6 @@ class T(unittest.TestCase): self.policy.package_set = set(["vim"]) self.policy.options.series = "jammy" - self.policy.source = "jammy-proposed" self.policy.apply_src_policy_impl( None, FakeItem, None, FakeSourceData, None @@ -76,7 +74,7 @@ class T(unittest.TestCase): @patch("britney2.policies.cloud.smtplib") @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests") def test_no_tests_run_during_dry_run(self, mock_run, smtp): - self.policy = CloudPolicy(FakeOptions, {}, dry_run=True) + self.policy = CloudPolicy(self.fake_options, {}, dry_run=True) self.policy.package_set = set(["chromium-browser"]) self.policy.options.series = "jammy" self.policy.source = "jammy-proposed" @@ -173,15 +171,94 @@ class T(unittest.TestCase): self.assertIn(expected_failure_info, info) self.assertIn(expected_error_info, info) - def test_determine_install_flag(self): - """Ensure the correct flag is determined and errors are raised for unknown source types""" - install_flag = self.policy._determine_install_flag("archive") - self.assertEqual(install_flag, "--install-archive-package") + def test_format_install_flags_with_ppas(self): + """Ensure the correct flags are returned with PPA sources""" + expected_flags = [ + "--install-ppa-package", "tmux/ppa_url=fingerprint", + "--install-ppa-package", "tmux/ppa_url2=fingerprint" + ] + install_flags = self.policy._format_install_flags( + "tmux", ["ppa_url=fingerprint", "ppa_url2=fingerprint"], "ppa" + ) + + self.assertListEqual(install_flags, expected_flags) + + def test_format_install_flags_with_archive(self): + """Ensure the correct flags are returned with archive sources""" + expected_flags = ["--install-archive-package", "tmux/proposed"] + install_flags = self.policy._format_install_flags("tmux", ["proposed"], "archive") + + self.assertListEqual(install_flags, expected_flags) + + def test_format_install_flags_with_incorrect_type(self): + """Ensure errors are raised for unknown source types""" + + self.assertRaises(RuntimeError, self.policy._format_install_flags, "tmux", ["a_source"], "something") + + def test_parse_ppas(self): + """Ensure correct conversion from Britney format to cloud test format + Also check that public PPAs are not used due to fingerprint requirement for cloud + tests. + """ + input_ppas = [ + "deadsnakes/ppa", + "user:token@team/name:fingerprint" + ] + + expected_ppas = [ + "https://user:token@private-ppa.launchpadcontent.net/team/name/ubuntu=fingerprint" + ] + + output_ppas = self.policy._parse_ppas(input_ppas) + self.assertListEqual(output_ppas, expected_ppas) + + def test_errors_raised_if_invalid_ppa_input(self): + """Test that error are raised if input PPAs don't match expected format""" + self.assertRaises( + RuntimeError, self.policy._parse_ppas, ["user:token@team/name"] + ) + + self.assertRaises( + RuntimeError, self.policy._parse_ppas, ["user:token@team=fingerprint"] + ) + + def test_retrieve_package_install_source_from_test_output(self): + """Ensure retrieving the package install source from apt output only returns the line we + want and not other lines containing the package name. + + Ensure it returns nothing if multiple candidates are found because that means the parsing + needs to be updated. + """ + package = "tmux" - install_flag = self.policy._determine_install_flag("ppa") - self.assertEqual(install_flag, "--install-ppa-package") + with open(pathlib.PurePath(self.policy.work_dir, self.policy.TEST_LOG_FILE), "w") as file: + file.write("Get: something \n".format(package)) + file.write("Get: lib-{} \n".format(package)) - self.assertRaises(RuntimeError, self.policy._determine_install_flag, "something") + install_source = self.policy._retrieve_package_install_source_from_test_output(package) + self.assertIsNone(install_source) + + with open(pathlib.PurePath(self.policy.work_dir, self.policy.TEST_LOG_FILE), "a") as file: + file.write("Get: {} \n".format(package)) + + install_source = self.policy._retrieve_package_install_source_from_test_output(package) + self.assertEqual(install_source, "Get: tmux \n") + + @patch("britney2.policies.cloud.CloudPolicy._retrieve_package_install_source_from_test_output") + def test_store_extra_test_result_info(self, mock): + """Ensure nothing is done if there are no failures/errors. + Ensure that if there are failures/errors that any extra info retrieved is stored in the + results dict Results -> Cloud -> extra_info + """ + self.policy._store_extra_test_result_info("FakeCloud", "tmux") + mock.assert_not_called() + + self.policy.failures = {"FakeCloud": {"failing_test": "failure reason"}} + mock.return_value = "source information" + self.policy._store_extra_test_result_info("FakeCloud", "tmux") + self.assertEqual( + self.policy.failures["FakeCloud"]["extra_info"]["install_source"], "source information" + ) def _create_fake_test_result_file(self, num_pass=1, num_err=0, num_fail=0): """Helper function to generate an xunit test result file. From f21d94d3b22098c863a9b2c14692edae2bfcad78 Mon Sep 17 00:00:00 2001 From: Aleksa Svitlica Date: Fri, 10 Feb 2023 13:21:01 -0500 Subject: [PATCH 39/39] CloudPolicy: Support public PPAs with fingerprints Updates the _parse_ppas function to also allow public PPAs with fingerprints to be provided (team/name:fingerprint). --- britney2/policies/cloud.py | 18 +++++++++++++++--- tests/test_cloud.py | 7 ++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/britney2/policies/cloud.py b/britney2/policies/cloud.py index 3fcc123..6e1cc06 100644 --- a/britney2/policies/cloud.py +++ b/britney2/policies/cloud.py @@ -69,7 +69,7 @@ class CloudPolicy(BasePolicy): self.failure_emails = getattr(self.options, "cloud_failure_emails", self.DEFAULT_EMAILS) self.error_emails = getattr(self.options, "cloud_error_emails", self.DEFAULT_EMAILS) - adt_ppas = getattr(self.options, "adt_ppas", []) + adt_ppas = getattr(self.options, "adt_ppas", "").split() ppas = self._parse_ppas(adt_ppas) if len(ppas) == 0: @@ -402,13 +402,16 @@ class CloudPolicy(BasePolicy): def _parse_ppas(self, ppas): """Parse PPA list to store in format expected by cloud tests - Since currently only private PPAs in Britney are provided with fingerprints we will - only use those. + Only supports PPAs provided with a fingerprint Britney private PPA format: 'user:token@team/name:fingerprint' + Britney public PPA format: + 'team/name:fingerprint' Cloud private PPA format: 'https://user:token@private-ppa.launchpadcontent.net/team/name/ubuntu=fingerprint + Cloud public PPA format: + 'https://ppa.launchpadcontent.net/team/name/ubuntu=fingerprint :param ppas List of PPAs in Britney approved format :return A list of PPAs in valid cloud test format. Can return an empty list if none found. @@ -425,6 +428,15 @@ class CloudPolicy(BasePolicy): match.group("auth"), match.group("name"), match.group("fingerprint") ) cloud_ppas.append(formatted_ppa) + else: + match = re.match("^(?P.+):(?P.+$)", ppa) + if not match: + raise RuntimeError('Public PPA %s not following required format (team/name:fingerprint)', ppa) + + formatted_ppa = "https://ppa.launchpadcontent.net/{}/ubuntu={}".format( + match.group("name"), match.group("fingerprint") + ) + cloud_ppas.append(formatted_ppa) return cloud_ppas diff --git a/tests/test_cloud.py b/tests/test_cloud.py index 30363bf..bbd8595 100644 --- a/tests/test_cloud.py +++ b/tests/test_cloud.py @@ -201,11 +201,12 @@ class T(unittest.TestCase): tests. """ input_ppas = [ - "deadsnakes/ppa", + "deadsnakes/ppa:fingerprint", "user:token@team/name:fingerprint" ] expected_ppas = [ + "https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu=fingerprint", "https://user:token@private-ppa.launchpadcontent.net/team/name/ubuntu=fingerprint" ] @@ -214,6 +215,10 @@ class T(unittest.TestCase): def test_errors_raised_if_invalid_ppa_input(self): """Test that error are raised if input PPAs don't match expected format""" + self.assertRaises( + RuntimeError, self.policy._parse_ppas, ["team/name"] + ) + self.assertRaises( RuntimeError, self.policy._parse_ppas, ["user:token@team/name"] )