#!/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 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 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('britney2.policies.sruadtregression.SRUADTRegressionPolicy.log') @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_policy_impl(None, 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_policy_impl(None, 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_policy_impl(None, 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_policy_impl(None, 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_policy_impl(None, 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_policy_impl(None, 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('britney2.policies.sruadtregression.SRUADTRegressionPolicy.log') @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_policy_impl(None, 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()