Add a new policy to message bugs on SRU regressions

It works like this. We wait until all tests have finished running. and
then grab their results. If there are any regressions, we mail each bug
with a link to pending-sru.html. There's a state file which records the
mails we've sent out, so that we don't mail the same bug multiple times.
master
Łukasz 'sil2100' Zemczak 6 years ago committed by Iain Lane
parent daaadf5bbd
commit 1690624d11
No known key found for this signature in database
GPG Key ID: E352D5C51C5041D4

@ -205,6 +205,7 @@ from britney2.policies.policy import AgePolicy, RCBugPolicy, PiupartsPolicy, Pol
from britney2.policies.policy import LPBlockBugPolicy
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.utils import (old_libraries_format, undo_changes,
compute_reverse_tree, possibly_compressed,
@ -543,6 +544,11 @@ class Britney(object):
self.policies.append(EmailPolicy(self.options,
self.suite_info,
dry_run=add_email_policy == 'dry-run'))
add_sruregression_policy = getattr(self.options, 'sruregression_enable', 'no')
if add_sruregression_policy in ('yes', 'dry-run'):
self.policies.append(SRUADTRegressionPolicy(self.options,
self.suite_info,
dry_run=add_sruregression_policy == 'dry-run'))
for policy in self.policies:
policy.register_hints(self._hint_parser)

@ -0,0 +1,164 @@
import os
import json
import smtplib
from urllib.request import urlopen, URLError
from britney2.policies.rest import Rest
from britney2.policies.policy import BasePolicy, PolicyVerdict
MESSAGE = """From: Ubuntu Stable Release Team <noreply@canonical.com>
To: {bug_mail}
X-Proposed-Migration: notice
Subject: Autopkgtest regression report ({source_name}/{version})
All autopkgtests for the newly accepted {source_name} ({version}) for {series_name} have finished running.
There have been regressions in tests triggered by the package. Please visit the sru report page and investigate the failures.
https://people.canonical.com/~ubuntu-archive/pending-sru.html#{series_name}
"""
class SRUADTRegressionPolicy(BasePolicy, Rest):
def __init__(self, options, suite_info, dry_run=False):
super().__init__('sruadtregression', options, suite_info, {'unstable'})
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.log('Loaded state file %s' % self.state_filename)
tmp = self.state_filename + '.new'
if os.path.exists(tmp):
with open(tmp, encoding='utf-8') as data:
self.state.update(json.load(data))
self.restore_state()
# 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 l in changes:
if l.startswith('Launchpad-Bugs-Fixed: '):
bugs = {int(b) for b in l.split()[1:]}
break
return bugs
def apply_policy_impl(self, policy_info, suite, source_name, source_data_tdist, source_data_srcdist, excuse):
# If all the autopkgtests have finished running.
if (excuse.current_policy_verdict == PolicyVerdict.REJECTED_TEMPORARILY or
excuse.current_policy_verdict == PolicyVerdict.PASS_HINTED):
return PolicyVerdict.PASS
# We only care about autopkgtest regressions
if 'autopkgtest' not in excuse.reason or not excuse.reason['autopkgtest']:
return PolicyVerdict.PASS
version = source_data_srcdist.version
distro_name = self.options.distribution
series_name = self.options.series
try:
if self.state[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,
# which is expected to happen when bileto runs britney.
except StopIteration:
self.log('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',
})
bugs = self.bugs_from_changes(changes_url)
# Now leave a comment informing about the ADT regressions on each bug
for bug in bugs:
if not self.dry_run:
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()
self.log('Sending ADT regression message to LP: #%s '
'regarding %s/%s in %s' % (
bug, source_name, version, series_name))
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
tmp = self.state_filename + '.new'
with open(tmp, 'w', encoding='utf-8') as data:
json.dump(self.state, data)
def restore_state(self):
try:
os.rename(self.state_filename + '.new', self.state_filename)
# If we haven't written any state, don't clobber the old one
except FileNotFoundError:
pass
self.log('Wrote SRU ADT regression state to %s' % self.state_filename)
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]

@ -0,0 +1,320 @@
#!/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 contextlib import ExitStack
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 = \
"""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 <ubuntu-devel-discuss@lists.ubuntu.com>
Changed-By: Foo Bar <foo.bar@ubuntu.com>
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
class FakeExcuseRunning:
is_valid = True
daysold = 0
reason = {'autopkgtest': 1}
current_policy_verdict = PolicyVerdict.REJECTED_TEMPORARILY
class FakeExcusePass:
is_valid = True
daysold = 0
reason = {}
current_policy_verdict = PolicyVerdict.PASS
class FakeExcuseHinted:
is_valid = True
daysold = 0
reason = {'skiptest': 1}
current_policy_verdict = PolicyVerdict.PASS_HINTED
class T(unittest.TestCase):
def setUp(self):
super().setUp()
def test_bugs_from_changes(self):
"""Check extraction of bug numbers from .changes files"""
with ExitStack() as resources:
options = FakeOptions
options.unstable = resources.enter_context(TemporaryDirectory())
urlopen_mock = resources.enter_context(
patch('britney2.policies.sruadtregression.urlopen'))
urlopen_mock.return_value = iter(FAKE_CHANGES.split('\n'))
pol = SRUADTRegressionPolicy(options, {})
bugs = pol.bugs_from_changes('http://some.url')
self.assertEqual(len(bugs), 4)
self.assertSetEqual(bugs, set((1, 4, 2, 31337)))
def test_bugs_from_changes_retry(self):
"""Check .changes extraction retry mechanism"""
with ExitStack() as resources:
options = FakeOptions
options.unstable = resources.enter_context(TemporaryDirectory())
urlopen_mock = resources.enter_context(
patch('britney2.policies.sruadtregression.urlopen'))
urlopen_mock.side_effect = URLError('timeout')
pol = SRUADTRegressionPolicy(options, {})
self.assertRaises(URLError, pol.bugs_from_changes, 'http://some.url')
self.assertEqual(urlopen_mock.call_count, 3)
def test_comment_on_regression_and_update_state(self):
"""Verify bug commenting about ADT regressions and save the state"""
with ExitStack() as resources:
options = FakeOptions
options.unstable = resources.enter_context(TemporaryDirectory())
pkg_mock = Mock()
pkg_mock.self_link = 'https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/9870565'
smtp_mock = Mock()
resources.enter_context(
patch('britney2.policies.sruadtregression.SRUADTRegressionPolicy.bugs_from_changes',
return_value=set([1, 2])))
lp = resources.enter_context(
patch('britney2.policies.sruadtregression.SRUADTRegressionPolicy.query_lp_rest_api',
return_value={'entries': [pkg_mock]}))
smtp = resources.enter_context(
patch('britney2.policies.sruadtregression.smtplib.SMTP', return_value=smtp_mock))
previous_state = {
'testbuntu': {
'zazzy': {
'testpackage': '54.0',
'ignored': '0.1',
}
},
'ghostdistro': {
'spooky': {
'ignored': '0.1',
}
}
}
pol = SRUADTRegressionPolicy(options, {})
# Set a base state
pol.state = previous_state
status = pol.apply_policy_impl(None, None, 'testpackage', None, FakeSourceData, FakeExcuse)
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.mock_calls, [
call('localhost:1337'),
call('localhost:1337')
])
self.assertEqual(smtp_mock.sendmail.call_count, 2)
# 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)
def test_no_comment_if_running(self):
"""Don't comment if tests still running"""
with ExitStack() as resources:
options = FakeOptions
options.unstable = resources.enter_context(TemporaryDirectory())
pkg_mock = Mock()
pkg_mock.self_link = 'https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/9870565'
smtp_mock = Mock()
bugs_from_changes = resources.enter_context(
patch('britney2.policies.sruadtregression.SRUADTRegressionPolicy.bugs_from_changes',
return_value=set([1, 2])))
lp = resources.enter_context(
patch('britney2.policies.sruadtregression.SRUADTRegressionPolicy.query_lp_rest_api',
return_value={'entries': [pkg_mock]}))
smtp = resources.enter_context(
patch('britney2.policies.sruadtregression.smtplib.SMTP', return_value=smtp_mock))
pol = SRUADTRegressionPolicy(options, {})
status = pol.apply_policy_impl(None, None, 'testpackage', None, FakeSourceData, FakeExcuseRunning)
self.assertEqual(status, PolicyVerdict.PASS)
self.assertEqual(bugs_from_changes.call_count, 0)
self.assertEqual(lp.call_count, 0)
self.assertEqual(smtp_mock.sendmail.call_count, 0)
def test_no_comment_if_passed(self):
"""Don't comment if all tests passed"""
with ExitStack() as resources:
options = FakeOptions
options.unstable = resources.enter_context(TemporaryDirectory())
pkg_mock = Mock()
pkg_mock.self_link = 'https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/9870565'
smtp_mock = Mock()
bugs_from_changes = resources.enter_context(
patch('britney2.policies.sruadtregression.SRUADTRegressionPolicy.bugs_from_changes',
return_value=set([1, 2])))
lp = resources.enter_context(
patch('britney2.policies.sruadtregression.SRUADTRegressionPolicy.query_lp_rest_api',
return_value={'entries': [pkg_mock]}))
smtp = resources.enter_context(
patch('britney2.policies.sruadtregression.smtplib.SMTP', return_value=smtp_mock))
pol = SRUADTRegressionPolicy(options, {})
status = pol.apply_policy_impl(None, None, 'testpackage', None, FakeSourceData, FakeExcusePass)
self.assertEqual(status, PolicyVerdict.PASS)
self.assertEqual(bugs_from_changes.call_count, 0)
self.assertEqual(lp.call_count, 0)
self.assertEqual(smtp_mock.sendmail.call_count, 0)
def test_no_comment_if_hinted(self):
"""Don't comment if package has been hinted in"""
with ExitStack() as resources:
options = FakeOptions
options.unstable = resources.enter_context(TemporaryDirectory())
pkg_mock = Mock()
pkg_mock.self_link = 'https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/9870565'
smtp_mock = Mock()
bugs_from_changes = resources.enter_context(
patch('britney2.policies.sruadtregression.SRUADTRegressionPolicy.bugs_from_changes',
return_value=set([1, 2])))
lp = resources.enter_context(
patch('britney2.policies.sruadtregression.SRUADTRegressionPolicy.query_lp_rest_api',
return_value={'entries': [pkg_mock]}))
smtp = resources.enter_context(
patch('britney2.policies.sruadtregression.smtplib.SMTP', return_value=smtp_mock))
pol = SRUADTRegressionPolicy(options, {})
status = pol.apply_policy_impl(None, None, 'testpackage', None, FakeSourceData, FakeExcuseHinted)
self.assertEqual(status, PolicyVerdict.PASS)
self.assertEqual(bugs_from_changes.call_count, 0)
self.assertEqual(lp.call_count, 0)
self.assertEqual(smtp_mock.sendmail.call_count, 0)
def test_initialize(self):
"""Check state load, old package cleanup and LP login"""
with ExitStack() as resources:
options = FakeOptions
options.unstable = resources.enter_context(TemporaryDirectory())
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 = resources.enter_context(
patch('britney2.policies.sruadtregression.SRUADTRegressionPolicy.query_lp_rest_api',
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')
if __name__ == '__main__':
unittest.main()
Loading…
Cancel
Save