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.sru-regression-messages
parent
daaadf5bbd
commit
1690624d11
@ -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…
Reference in new issue