diff --git a/britney.py b/britney.py index 8ecf7c0..2f70306 100755 --- a/britney.py +++ b/britney.py @@ -221,6 +221,7 @@ from britney2.policies.policy import (AgePolicy, ) 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.lpexcusebugs import LPExcuseBugsPolicy from britney2.utils import (log_and_format_old_libraries, @@ -533,6 +534,11 @@ class Britney(object): self._policy_engine.add_policy(LPExcuseBugsPolicy(self.options, self.suite_info)) self._policy_engine.add_policy(SourcePPAPolicy(self.options, self.suite_info)) self._policy_engine.add_policy(LinuxPolicy(self.options, self.suite_info)) + add_sruregression_policy = getattr(self.options, 'sruregressionemail_enable', 'no') + if add_sruregression_policy in ('yes', 'dry-run'): + self._policy_engine.add_policy(SRUADTRegressionPolicy(self.options, + self.suite_info, + dry_run=add_sruregression_policy == 'dry-run')) add_email_policy = getattr(self.options, 'email_enable', 'no') if add_email_policy in ('yes', 'dry-run'): self._policy_engine.add_policy(EmailPolicy(self.options, diff --git a/britney2/policies/sruadtregression.py b/britney2/policies/sruadtregression.py new file mode 100644 index 0000000..1c8ac82 --- /dev/null +++ b/britney2/policies/sruadtregression.py @@ -0,0 +1,218 @@ +import os +import json +import socket +import smtplib + +from collections import defaultdict +from urllib.request import urlopen, URLError + +from britney2 import SuiteClass +from britney2.policies.rest import Rest +from britney2.policies.policy import BasePolicy, PolicyVerdict + + +MESSAGE = """From: Ubuntu Stable Release Team +To: {bug_mail} +Subject: Autopkgtest regression report ({source_name}/{version}) + +All autopkgtests for the newly accepted {source_name} ({version}) for {series_name} have finished running. +The following regressions have been reported in tests triggered by the package: + +{failures} + +Please visit the excuses page listed below and investigate the failures, proceeding afterwards as per the StableReleaseUpdates policy regarding autopkgtest regressions [1]. + +https://people.canonical.com/~ubuntu-archive/proposed-migration/{series_name}/update_excuses.html#{source_name} + +[1] https://wiki.ubuntu.com/StableReleaseUpdates#Autopkgtest_Regressions + +Thank you! +""" # noqa:E501 + + +class SRUADTRegressionPolicy(BasePolicy, Rest): + def __init__(self, options, suite_info, dry_run=False): + super().__init__( + "sruadtregression", + options, + suite_info, + {SuiteClass.PRIMARY_SOURCE_SUITE}, + ) + self.state_filename = os.path.join( + options.unstable, "sru_regress_inform_state" + ) + self.state = {} + self.dry_run = dry_run + self.email_host = getattr(self.options, "email_host", "localhost") + + def initialise(self, britney): + super().initialise(britney) + if os.path.exists(self.state_filename): + with open(self.state_filename, encoding="utf-8") as data: + self.state = json.load(data) + self.logger.info("Loaded state file %s" % self.state_filename) + # Remove any old entries from the statefile + self.cleanup_state() + + def bugs_from_changes(self, change_url): + """Return bug list from a .changes file URL""" + last_exception = None + # Querying LP can timeout a lot, retry 3 times + for i in range(3): + try: + changes = urlopen(change_url) + break + except URLError as e: + last_exception = e + pass + else: + raise last_exception + bugs = set() + for ln in changes: + ln = ln.decode("utf-8") + if ln.startswith("Launchpad-Bugs-Fixed: "): + bugs = {int(b) for b in ln.split()[1:]} + break + return bugs + + def apply_src_policy_impl( + self, policy_info, item, source_data_tdist, source_data_srcdist, excuse + ): + source_name = item.package + # We only care about autopkgtest regressions + if ( + "autopkgtest" not in excuse.reason + or not excuse.reason["autopkgtest"] + ): + return PolicyVerdict.PASS + # Check the policy_info to see if all tests finished and, if yes, if we + # have any failures to report on. + if "autopkgtest" not in excuse.policy_info: + return PolicyVerdict.PASS + regressions = defaultdict(list) + for pkg in excuse.policy_info["autopkgtest"]: + # Unfortunately the verdict of the policy lives at the same level + # as its detailed output + if pkg == "verdict": + continue + for arch in excuse.policy_info["autopkgtest"][pkg]: + if excuse.policy_info["autopkgtest"][pkg][arch][0] == "RUNNING": + # If there's at least one test still running, we just stop + # and not proceed any further. + return PolicyVerdict.PASS + elif ( + excuse.policy_info["autopkgtest"][pkg][arch][0] + == "REGRESSION" + ): + regressions[pkg].append(arch) + if not regressions: + return PolicyVerdict.PASS + + version = source_data_srcdist.version + distro_name = self.options.distribution + series_name = self.options.series + try: + if self.state[distro_name][series_name][source_name] == version: + # We already informed about the regression. + return PolicyVerdict.PASS + except KeyError: + # Expected when no failure has been reported so far for this + # source - we want to continue in that case. Easier than + # doing n number of if-checks. + pass + data = self.query_lp_rest_api( + "%s/+archive/primary" % distro_name, + { + "ws.op": "getPublishedSources", + "distro_series": "/%s/%s" % (distro_name, series_name), + "exact_match": "true", + "order_by_date": "true", + "pocket": "Proposed", + "source_name": source_name, + "version": version, + }, + ) + try: + src = next(iter(data["entries"])) + # IndexError means no packages in -proposed matched this name/version. + except StopIteration: + self.logger.info( + "No packages matching %s/%s the %s/%s main archive, not " + "informing of ADT regressions" + % (source_name, version, distro_name, series_name) + ) + return PolicyVerdict.PASS + changes_url = self.query_lp_rest_api( + src["self_link"], {"ws.op": "changesFileUrl"} + ) + if not changes_url: + return PolicyVerdict.PASS + # Prepare a helper string that lists all the ADT failures + failures = "" + for pkg, arches in regressions.items(): + failures += "%s (%s)\n" % (pkg, ", ".join(arches)) + + bugs = self.bugs_from_changes(changes_url) + # Now leave a comment informing about the ADT regressions on each bug + for bug in bugs: + self.logger.info( + "%sSending ADT regression message to LP: #%s " + "regarding %s/%s in %s" + % ( + "[dry-run] " if self.dry_run else "", + bug, + source_name, + version, + series_name, + ) + ) + if not self.dry_run: + try: + bug_mail = "%s@bugs.launchpad.net" % bug + server = smtplib.SMTP(self.email_host) + server.sendmail( + "noreply@canonical.com", + bug_mail, + MESSAGE.format(**locals()), + ) + server.quit() + except socket.error as err: + self.logger.info( + "Failed to send mail! Is SMTP server running?" + ) + self.logger.info(err) + self.save_progress(source_name, version, distro_name, series_name) + return PolicyVerdict.PASS + + def save_progress(self, source, version, distro, series): + if self.dry_run: + return + if distro not in self.state: + self.state[distro] = {} + if series not in self.state[distro]: + self.state[distro][series] = {} + self.state[distro][series][source] = version + with open(self.state_filename, "w", encoding="utf-8") as data: + json.dump(self.state, data) + + def cleanup_state(self): + """Remove all no-longer-valid package entries from the statefile""" + for distro_name in self.state: + for series_name, pkgs in self.state[distro_name].items(): + for source_name, version in pkgs.copy().items(): + data = self.query_lp_rest_api( + "%s/+archive/primary" % distro_name, + { + "ws.op": "getPublishedSources", + "distro_series": "/%s/%s" + % (distro_name, series_name), + "exact_match": "true", + "order_by_date": "true", + "pocket": "Proposed", + "status": "Published", + "source_name": source_name, + "version": version, + }, + ) + if not data["entries"]: + del self.state[distro_name][series_name][source_name] diff --git a/tests/test_sruadtregression.py b/tests/test_sruadtregression.py new file mode 100755 index 0000000..121bacf --- /dev/null +++ b/tests/test_sruadtregression.py @@ -0,0 +1,589 @@ +#!/usr/bin/python3 +# (C) 2018 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 logging +import os +import sys +import json +import unittest +from tempfile import TemporaryDirectory +from unittest.mock import DEFAULT, Mock, patch, call +from urllib.request import URLError + +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, PROJECT_DIR) + +from britney2.policies.policy import PolicyVerdict +from britney2.policies.sruadtregression import SRUADTRegressionPolicy +from tests.test_sourceppa import FakeBritney + + +FAKE_CHANGES = b"""Format: 1.8 +Date: Mon, 16 Jul 2018 17:05:18 -0500 +Source: test +Binary: test +Architecture: source +Version: 1.0 +Distribution: bionic +Urgency: medium +Maintainer: Ubuntu Developers +Changed-By: Foo Bar +Description: + test - A test package +Launchpad-Bugs-Fixed: 1 4 2 31337 31337 +Changes: + test (1.0) bionic; urgency=medium + . + * Test build +Checksums-Sha1: + fb11f859b85e09513d395a293dbb0808d61049a7 2454 test_1.0.dsc + 06500cc627bc04a02b912a11417fca5a1109ec97 69852 test_1.0.debian.tar.xz +Checksums-Sha256: + d91f369a4b7fc4cba63540a81bd6f1492ca86661cf2e3ccac6028fb8c98d5ff5 2454 test_1.0.dsc + ffb460269ea2acb3db24adb557ba7541fe57fde0b10f6b6d58e8958b9a05b0b9 69852 test_1.0.debian.tar.xz +Files: + 2749eba6cae4e49c00f25e870b228871 2454 libs extra test_1.0.dsc + 95bf4af1ba0766b6f83dd3c3a33b0272 69852 libs extra test_1.0.debian.tar.xz +""" + + +class FakeOptions: + distribution = "testbuntu" + series = "zazzy" + unstable = "/tmp" + verbose = False + email_host = "localhost:1337" + + +class FakeSourceData: + version = "55.0" + + +class TestPackage: + package = "testpackage" + + +class FakeExcuse: + is_valid = True + daysold = 0 + reason = {"autopkgtest": 1} + current_policy_verdict = PolicyVerdict.REJECTED_PERMANENTLY + policy_info = { + "autopkgtest": { + "testpackage/55.0": { + "amd64": ["PASS", None, None, None, None], + "i386": ["REGRESSION", None, None, None, None], + }, + "rdep1/1.1-0ubuntu1": { + "amd64": ["REGRESSION", None, None, None, None], + "i386": ["REGRESSION", None, None, None, None], + }, + "rdep2/0.1-0ubuntu2": { + "amd64": ["PASS", None, None, None, None], + "i386": ["PASS", None, None, None, None], + }, + } + } + + +class FakeExcuseRunning: + is_valid = True + daysold = 0 + reason = {"autopkgtest": 1} + current_policy_verdict = PolicyVerdict.REJECTED_TEMPORARILY + policy_info = { + "autopkgtest": { + "testpackage/55.0": { + "amd64": ["PASS", None, None, None, None], + "i386": ["REGRESSION", None, None, None, None], + }, + "rdep1/1.1-0ubuntu1": { + "amd64": ["RUNNING", None, None, None, None], + "i386": ["PASS", None, None, None, None], + }, + "rdep2/0.1-0ubuntu2": { + "amd64": ["PASS", None, None, None, None], + "i386": ["PASS", None, None, None, None], + }, + } + } + + +class FakeExcusePass: + is_valid = True + daysold = 0 + reason = {} + current_policy_verdict = PolicyVerdict.PASS + policy_info = { + "autopkgtest": { + "testpackage/55.0": { + "amd64": ["PASS", None, None, None, None], + "i386": ["PASS", None, None, None, None], + }, + "rdep1/1.1-0ubuntu1": { + "amd64": ["PASS", None, None, None, None], + "i386": ["PASS", None, None, None, None], + }, + "rdep2/0.1-0ubuntu2": { + "amd64": ["PASS", None, None, None, None], + "i386": ["PASS", None, None, None, None], + }, + } + } + + +class FakeExcuseHinted: + is_valid = True + daysold = 0 + reason = {"skiptest": 1} + current_policy_verdict = PolicyVerdict.PASS_HINTED + policy_info = { + "autopkgtest": { + "testpackage/55.0": { + "amd64": ["PASS", None, None, None, None], + "i386": ["ALWAYSFAIL", None, None, None, None], + }, + "rdep1/1.1-0ubuntu1": { + "amd64": ["IGNORE-FAIL", None, None, None, None], + "i386": ["IGNORE-FAIL", None, None, None, None], + }, + "rdep2/0.1-0ubuntu2": { + "amd64": ["PASS", None, None, None, None], + "i386": ["PASS", None, None, None, None], + }, + } + } + + +class FakeExcuseVerdictOverriden: + is_valid = True + daysold = 0 + reason = {"skiptest": 1} + current_policy_verdict = PolicyVerdict.REJECTED_PERMANENTLY + policy_info = { + "autopkgtest": { + "testpackage/55.0": { + "amd64": ["PASS", None, None, None, None], + "i386": ["PASS", None, None, None, None], + }, + "rdep1/1.1-0ubuntu1": { + "amd64": ["PASS", None, None, None, None], + "i386": ["PASS", None, None, None, None], + }, + "rdep2/0.1-0ubuntu2": { + "amd64": ["PASS", None, None, None, None], + "i386": ["PASS", None, None, None, None], + }, + } + } + + +class T(unittest.TestCase): + def setUp(self): + super().setUp() + + @patch( + "britney2.policies.sruadtregression.urlopen", + return_value=iter(FAKE_CHANGES.split(b"\n")), + ) + def test_bugs_from_changes(self, urlopen_mock): + """Check extraction of bug numbers from .changes files""" + with TemporaryDirectory() as tmpdir: + options = FakeOptions + options.unstable = tmpdir + pol = SRUADTRegressionPolicy(options, {}) + bugs = pol.bugs_from_changes("http://some.url") + self.assertEqual(len(bugs), 4) + self.assertSetEqual(bugs, set((1, 4, 2, 31337))) + + @patch( + "britney2.policies.sruadtregression.urlopen", + side_effect=URLError("timeout"), + ) + def test_bugs_from_changes_retry(self, urlopen_mock): + """Check .changes extraction retry mechanism""" + with TemporaryDirectory() as tmpdir: + options = FakeOptions + options.unstable = tmpdir + pol = SRUADTRegressionPolicy(options, {}) + self.assertRaises( + URLError, pol.bugs_from_changes, "http://some.url" + ) + self.assertEqual(urlopen_mock.call_count, 3) + + @patch.object(logging.Logger, "info") + @patch("smtplib.SMTP") + @patch( + "britney2.policies.sruadtregression.SRUADTRegressionPolicy.bugs_from_changes", + return_value={1, 2}, + ) + @patch( + "britney2.policies.sruadtregression.SRUADTRegressionPolicy.query_lp_rest_api" + ) + def test_comment_on_regression_and_update_state( + self, lp, bugs_from_changes, smtp, log + ): + """Verify bug commenting about ADT regressions and save the state""" + with TemporaryDirectory() as tmpdir: + options = FakeOptions + options.unstable = tmpdir + + pkg_mock = {} + pkg_mock[ + "self_link" + ] = "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/9870565" + + lp.return_value = {"entries": [pkg_mock]} + + previous_state = { + "testbuntu": { + "zazzy": {"testpackage": "54.0", "ignored": "0.1"} + }, + "ghostdistro": {"spooky": {"ignored": "0.1"}}, + } + excuse = FakeExcuse + pol = SRUADTRegressionPolicy(options, {}) + # Set a base state + pol.state = previous_state + status = pol.apply_src_policy_impl( + None, TestPackage, None, FakeSourceData, excuse + ) + self.assertEqual(status, PolicyVerdict.PASS) + # Assert that we were looking for the right package as per + # FakeSourceData contents + self.assertSequenceEqual( + lp.mock_calls, + [ + call( + "testbuntu/+archive/primary", + { + "distro_series": "/testbuntu/zazzy", + "exact_match": "true", + "order_by_date": "true", + "pocket": "Proposed", + "source_name": "testpackage", + "version": "55.0", + "ws.op": "getPublishedSources", + }, + ), + call( + "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/9870565", + {"ws.op": "changesFileUrl"}, + ), + ], + ) + # The .changes file only lists 2 bugs, make sure only those are + # commented on + self.assertSequenceEqual( + smtp.call_args_list, + [call("localhost:1337"), call("localhost:1337")], + ) + self.assertEqual(smtp().sendmail.call_count, 2) + # call_args is a tuple (args, kwargs), and the email content is + # the third non-named argument of sendmail() - hence [0][2] + message = smtp().sendmail.call_args[0][2] + # Check if the e-mail/comment we were sending includes info about + # which tests have failed + self.assertIn("testpackage/55.0 (i386)", message) + self.assertIn("rdep1/1.1-0ubuntu1 (amd64, i386)", message) + # Check if the state has been saved and not overwritten + expected_state = { + "testbuntu": { + "zazzy": {"testpackage": "55.0", "ignored": "0.1"} + }, + "ghostdistro": {"spooky": {"ignored": "0.1"}}, + } + self.assertDictEqual(pol.state, expected_state) + log.assert_called_with( + "Sending ADT regression message to LP: #2 regarding testpackage/55.0 in zazzy" + ) + # But also test if the state has been correctly recorded to the + # state file + state_path = os.path.join( + options.unstable, "sru_regress_inform_state" + ) + with open(state_path) as f: + saved_state = json.load(f) + self.assertDictEqual(saved_state, expected_state) + + @patch("smtplib.SMTP") + @patch( + "britney2.policies.sruadtregression.SRUADTRegressionPolicy.bugs_from_changes", + return_value={1, 2}, + ) + @patch( + "britney2.policies.sruadtregression.SRUADTRegressionPolicy.query_lp_rest_api" + ) + def test_no_comment_if_running(self, lp, bugs_from_changes, smtp): + """Don't comment if tests still running""" + with TemporaryDirectory() as tmpdir: + options = FakeOptions + options.unstable = tmpdir + pkg_mock = Mock() + pkg_mock.self_link = "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/9870565" + + lp.return_value = {"entries": [pkg_mock]} + + excuse = FakeExcuseRunning + pol = SRUADTRegressionPolicy(options, {}) + status = pol.apply_src_policy_impl( + None, TestPackage, None, FakeSourceData, excuse + ) + self.assertEqual(status, PolicyVerdict.PASS) + bugs_from_changes.assert_not_called() + lp.assert_not_called() + smtp.sendmail.assert_not_called() + + @patch("smtplib.SMTP") + @patch( + "britney2.policies.sruadtregression.SRUADTRegressionPolicy.bugs_from_changes", + return_value={1, 2}, + ) + @patch( + "britney2.policies.sruadtregression.SRUADTRegressionPolicy.query_lp_rest_api" + ) + def test_no_comment_if_passed(self, lp, bugs_from_changes, smtp): + """Don't comment if all tests passed""" + with TemporaryDirectory() as tmpdir: + options = FakeOptions + options.unstable = tmpdir + pkg_mock = Mock() + pkg_mock.self_link = "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/9870565" + + bugs_from_changes.return_value = {"entries": [pkg_mock]} + + excuse = FakeExcusePass + pol = SRUADTRegressionPolicy(options, {}) + status = pol.apply_src_policy_impl( + None, TestPackage, None, FakeSourceData, excuse + ) + self.assertEqual(status, PolicyVerdict.PASS) + bugs_from_changes.assert_not_called() + lp.assert_not_called() + smtp.sendmail.assert_not_called() + + @patch("smtplib.SMTP") + @patch( + "britney2.policies.sruadtregression.SRUADTRegressionPolicy.bugs_from_changes", + return_value={1, 2}, + ) + @patch( + "britney2.policies.sruadtregression.SRUADTRegressionPolicy.query_lp_rest_api" + ) + def test_no_comment_if_hinted(self, lp, bugs_from_changes, smtp): + """Don't comment if package has been hinted in""" + with TemporaryDirectory() as tmpdir: + options = FakeOptions + options.unstable = tmpdir + pkg_mock = Mock() + pkg_mock.self_link = "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/9870565" + bugs_from_changes.return_value = {"entries": [pkg_mock]} + + excuse = FakeExcuseHinted + pol = SRUADTRegressionPolicy(options, {}) + status = pol.apply_src_policy_impl( + None, TestPackage, None, FakeSourceData, excuse + ) + self.assertEqual(status, PolicyVerdict.PASS) + bugs_from_changes.assert_not_called() + lp.assert_not_called() + smtp.sendmail.assert_not_called() + + @patch("smtplib.SMTP") + @patch( + "britney2.policies.sruadtregression.SRUADTRegressionPolicy.bugs_from_changes", + return_value={1, 2}, + ) + @patch( + "britney2.policies.sruadtregression.SRUADTRegressionPolicy.query_lp_rest_api" + ) + def test_no_comment_if_commented(self, lp, bugs_from_changes, smtp): + """Don't comment if package has been already commented on""" + with TemporaryDirectory() as tmpdir: + options = FakeOptions + options.unstable = tmpdir + pkg_mock = Mock() + pkg_mock.self_link = "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/9870565" + bugs_from_changes.return_value = {"entries": [pkg_mock]} + + previous_state = { + "testbuntu": { + "zazzy": {"testpackage": "55.0", "ignored": "0.1"} + } + } + excuse = FakeExcuse + pol = SRUADTRegressionPolicy(options, {}) + # Set a base state + pol.state = previous_state + status = pol.apply_src_policy_impl( + None, TestPackage, None, FakeSourceData, excuse + ) + self.assertEqual(status, PolicyVerdict.PASS) + bugs_from_changes.assert_not_called() + lp.assert_not_called() + smtp.sendmail.assert_not_called() + + @patch("smtplib.SMTP") + @patch( + "britney2.policies.sruadtregression.SRUADTRegressionPolicy.bugs_from_changes", + return_value={1, 2}, + ) + @patch( + "britney2.policies.sruadtregression.SRUADTRegressionPolicy.query_lp_rest_api" + ) + def test_no_comment_if_verdict_overriden(self, lp, bugs_from_changes, smtp): + """Make sure the previous 'bug' of commenting on uploads that had no + failing tests (or had them still running) because of the + current_policy_verdict getting reset to REJECTED_PERMANENTLY + by some other policy does not happen anymore.""" + with TemporaryDirectory() as tmpdir: + options = FakeOptions + options.unstable = tmpdir + pkg_mock = Mock() + pkg_mock.self_link = "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/9870565" + + lp.return_value = {"entries": [pkg_mock]} + + # This should no longer be a valid bug as we switched how we + # actually determine if a regression is present, but still + # - better to have an explicit test case for it. + excuse = FakeExcuseVerdictOverriden + pol = SRUADTRegressionPolicy(options, {}) + status = pol.apply_src_policy_impl( + None, TestPackage, None, FakeSourceData, excuse + ) + self.assertEqual(status, PolicyVerdict.PASS) + bugs_from_changes.assert_not_called() + lp.assert_not_called() + smtp.sendmail.assert_not_called() + + @patch( + "britney2.policies.sruadtregression.SRUADTRegressionPolicy.query_lp_rest_api" + ) + def test_initialize(self, lp): + """Check state load, old package cleanup and LP login""" + with TemporaryDirectory() as tmpdir: + options = FakeOptions + options.unstable = tmpdir + pkg_mock1 = Mock() + pkg_mock1.source_name = "testpackage" + pkg_mock2 = Mock() + pkg_mock2.source_name = "otherpackage" + + # Since we want to be as accurate as possible, we return query + # results per what query has been performed. + def query_side_effect(link, query): + if query["source_name"] == "testpackage": + return {"entries": [pkg_mock1]} + elif query["source_name"] == "otherpackage": + return {"entries": [pkg_mock2]} + return {"entries": []} + + lp.side_effect = query_side_effect + state = { + "testbuntu": { + "zazzy": { + "testpackage": "54.0", + "toremove": "0.1", + "otherpackage": "13ubuntu1", + } + } + } + # Prepare the state file + state_path = os.path.join( + options.unstable, "sru_regress_inform_state" + ) + with open(state_path, "w") as f: + json.dump(state, f) + pol = SRUADTRegressionPolicy(options, {}) + pol.initialise(FakeBritney()) + # Check if the stale packages got purged and others not + expected_state = { + "testbuntu": { + "zazzy": { + "testpackage": "54.0", + "otherpackage": "13ubuntu1", + } + } + } + # Make sure the state file has been loaded correctly + self.assertDictEqual(pol.state, expected_state) + # Check if we logged in with the right LP credentials + self.assertEqual(pol.email_host, "localhost:1337") + + @patch.object(logging.Logger, "info") + @patch("smtplib.SMTP") + @patch( + "britney2.policies.sruadtregression.SRUADTRegressionPolicy.bugs_from_changes", + return_value={1, 2}, + ) + @patch( + "britney2.policies.sruadtregression.SRUADTRegressionPolicy.query_lp_rest_api" + ) + def test_no_comment_dry_run(self, lp, bugs_from_changes, smtp, log): + """Verify bug commenting about ADT regressions and save the state""" + with TemporaryDirectory() as tmpdir: + options = FakeOptions + options.unstable = tmpdir + + pkg_mock = {} + pkg_mock[ + "self_link" + ] = "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/9870565" + + lp.return_value = {"entries": [pkg_mock]} + + previous_state = { + "testbuntu": { + "zazzy": {"testpackage": "54.0", "ignored": "0.1"} + }, + "ghostdistro": {"spooky": {"ignored": "0.1"}}, + } + excuse = FakeExcuse + pol = SRUADTRegressionPolicy(options, {}, dry_run=True) + # Set a base state + pol.state = previous_state + status = pol.apply_src_policy_impl( + None, TestPackage, None, FakeSourceData, excuse + ) + self.assertEqual(status, PolicyVerdict.PASS) + # Assert that we were looking for the right package as per + # FakeSourceData contents + self.assertSequenceEqual( + lp.mock_calls, + [ + call( + "testbuntu/+archive/primary", + { + "distro_series": "/testbuntu/zazzy", + "exact_match": "true", + "order_by_date": "true", + "pocket": "Proposed", + "source_name": "testpackage", + "version": "55.0", + "ws.op": "getPublishedSources", + }, + ), + call( + "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/9870565", + {"ws.op": "changesFileUrl"}, + ), + ], + ) + + # Nothing happened + smtp.assert_not_called() + smtp.sendmail.assert_not_called() + self.assertDictEqual(pol.state, previous_state) + log.assert_called_with( + "[dry-run] Sending ADT regression message to LP: #2 regarding testpackage/55.0 in zazzy" + ) + + +if __name__ == "__main__": + unittest.main()