|
|
|
#!/usr/bin/python3
|
|
|
|
# (C) 2016 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 unittest
|
|
|
|
from unittest.mock import DEFAULT, patch
|
|
|
|
|
|
|
|
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.sourceppa import LAUNCHPAD_URL, SourcePPAPolicy
|
|
|
|
|
|
|
|
|
|
|
|
CACHE_FILE = os.path.join(PROJECT_DIR, 'tests', 'data', 'sourceppa.json')
|
|
|
|
|
|
|
|
|
|
|
|
class FakeOptions:
|
|
|
|
distribution = 'testbuntu'
|
|
|
|
series = 'zazzy'
|
|
|
|
unstable = '/tmp'
|
|
|
|
verbose = False
|
|
|
|
|
|
|
|
|
|
|
|
class FakeExcuse:
|
|
|
|
ver = ('1.0', '2.0')
|
|
|
|
is_valid = True
|
|
|
|
policy_info = {}
|
|
|
|
name = "foo"
|
|
|
|
|
|
|
|
def addreason(self, reason):
|
|
|
|
"""Ignore reasons."""
|
|
|
|
|
|
|
|
def addhtml(self, reason):
|
|
|
|
"""Ignore reasons."""
|
|
|
|
|
|
|
|
|
|
|
|
class FakeBritney:
|
|
|
|
def __init__(self):
|
|
|
|
self.excuses = dict(
|
|
|
|
pal=FakeExcuse(),
|
|
|
|
buddy=FakeExcuse(),
|
|
|
|
friend=FakeExcuse(),
|
|
|
|
noppa=FakeExcuse())
|
|
|
|
|
|
|
|
|
|
|
|
class FakeData:
|
|
|
|
version = '2.0'
|
|
|
|
|
|
|
|
|
|
|
|
class T(unittest.TestCase):
|
|
|
|
maxDiff = None
|
|
|
|
|
|
|
|
@patch('britney2.policies.sourceppa.urllib.request.urlopen')
|
|
|
|
def test_lp_rest_api_no_entries(self, urlopen):
|
|
|
|
"""Don't explode if LP reports no entries match pkg/version"""
|
|
|
|
context = urlopen.return_value.__enter__.return_value
|
|
|
|
context.getcode.return_value = 200
|
|
|
|
context.read.return_value = b'{"entries": []}'
|
|
|
|
pol = SourcePPAPolicy(FakeOptions, {})
|
|
|
|
self.assertEqual(pol.lp_get_source_ppa('hello', '1.0'), 'IndexError')
|
|
|
|
|
|
|
|
@patch('britney2.policies.sourceppa.urllib.request.urlopen')
|
|
|
|
def test_lp_rest_api_no_source_ppa(self, urlopen):
|
|
|
|
"""Identify when package has no source PPA"""
|
|
|
|
context = urlopen.return_value.__enter__.return_value
|
|
|
|
context.getcode.return_value = 200
|
|
|
|
context.read.return_value = b'{"entries": [{"self_link": "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/12345", "build_link": "https://api.launchpad.net/1.0/ubuntu/+source/gcc-5/5.4.1-7ubuntu1/+build/12066956", "other_stuff": "ignored"}]}'
|
|
|
|
pol = SourcePPAPolicy(FakeOptions, {})
|
|
|
|
self.assertEqual(pol.lp_get_source_ppa('hello', '1.0'), '')
|
|
|
|
|
|
|
|
@patch('britney2.policies.sourceppa.urllib.request.urlopen')
|
|
|
|
def test_lp_rest_api_with_source_ppa(self, urlopen):
|
|
|
|
"""Identify source PPA"""
|
|
|
|
context = urlopen.return_value.__enter__.return_value
|
|
|
|
context.getcode.return_value = 200
|
|
|
|
context.read.return_value = b'{"entries": [{"self_link": "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/12345", "build_link": "https://api.launchpad.net/1.0/~ci-train-ppa-service/+archive/ubuntu/2516/+build/12063031", "other_stuff": "ignored"}]}'
|
|
|
|
pol = SourcePPAPolicy(FakeOptions, {})
|
|
|
|
self.assertEqual(pol.lp_get_source_ppa('hello', '1.0'), 'https://api.launchpad.net/1.0/~ci-train-ppa-service/+archive/ubuntu/2516')
|
|
|
|
|
|
|
|
@patch('britney2.policies.sourceppa.urllib.request.urlopen')
|
|
|
|
def test_lp_rest_api_errors(self, urlopen):
|
|
|
|
"""Report errors instead of swallowing them"""
|
|
|
|
context = urlopen.return_value.__enter__.return_value
|
|
|
|
context.getcode.return_value = 500
|
|
|
|
context.read.return_value = b''
|
|
|
|
pol = SourcePPAPolicy(FakeOptions, {})
|
|
|
|
with self.assertRaisesRegex(ConnectionError, 'HTTP 500'):
|
|
|
|
pol.lp_get_source_ppa('hello', '1.0')
|
|
|
|
# Yes, I have really seen "success with no json returned" in the wild
|
|
|
|
context.getcode.return_value = 200
|
|
|
|
context.read.return_value = b''
|
|
|
|
with self.assertRaisesRegex(ValueError, 'Expecting value'):
|
|
|
|
pol.lp_get_source_ppa('hello', '1.0')
|
|
|
|
|
|
|
|
@patch('britney2.policies.sourceppa.urllib.request.urlopen')
|
|
|
|
def test_lp_rest_api_timeout(self, urlopen):
|
|
|
|
"""If we get a timeout connecting to LP, we try 5 times"""
|
|
|
|
import socket
|
|
|
|
# test that we're retried 5 times on timeout
|
|
|
|
urlopen.side_effect = socket.timeout
|
|
|
|
pol = SourcePPAPolicy(FakeOptions, {})
|
|
|
|
with self.assertRaises(socket.timeout):
|
|
|
|
pol.lp_get_source_ppa('hello', '1.0')
|
|
|
|
self.assertEqual(urlopen.call_count, 5)
|
|
|
|
|
|
|
|
@patch('britney2.policies.sourceppa.urllib.request.urlopen')
|
|
|
|
def test_lp_rest_api_unavailable(self, urlopen):
|
|
|
|
"""If we get a 503 connecting to LP, we try 5 times"""
|
|
|
|
from urllib.error import HTTPError
|
|
|
|
# test that we're retried 5 times on 503
|
|
|
|
urlopen.side_effect = HTTPError(None,
|
|
|
|
503,
|
|
|
|
'Service Temporarily Unavailable',
|
|
|
|
None,
|
|
|
|
None)
|
|
|
|
pol = SourcePPAPolicy(FakeOptions, {})
|
|
|
|
with self.assertRaises(HTTPError):
|
|
|
|
pol.lp_get_source_ppa('hello', '1.0')
|
|
|
|
self.assertEqual(urlopen.call_count, 5)
|
|
|
|
|
|
|
|
@patch('britney2.policies.sourceppa.urllib.request.urlopen')
|
|
|
|
def test_lp_rest_api_flaky(self, urlopen):
|
|
|
|
"""If we get a 503, then a 200, we get the right result"""
|
|
|
|
from urllib.error import HTTPError
|
|
|
|
|
|
|
|
def l():
|
|
|
|
for i in range(3):
|
|
|
|
yield HTTPError(None,
|
|
|
|
503,
|
|
|
|
'Service Temporarily Unavailable',
|
|
|
|
None,
|
|
|
|
None)
|
|
|
|
while True:
|
|
|
|
yield DEFAULT
|
|
|
|
|
|
|
|
context = urlopen.return_value.__enter__.return_value
|
|
|
|
context.getcode.return_value = 200
|
|
|
|
context.read.return_value = b'{"entries": [{"self_link": "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/12345", "build_link": "https://api.launchpad.net/1.0/~ci-train-ppa-service/+archive/ubuntu/2516/+build/12063031", "other_stuff": "ignored"}]}'
|
|
|
|
urlopen.side_effect = l()
|
|
|
|
pol = SourcePPAPolicy(FakeOptions, {})
|
|
|
|
pol.lp_get_source_ppa('hello', '1.0')
|
|
|
|
self.assertEqual(urlopen.call_count, 5)
|
|
|
|
self.assertEqual(pol.lp_get_source_ppa('hello', '1.0'), 'https://api.launchpad.net/1.0/~ci-train-ppa-service/+archive/ubuntu/2516')
|
|
|
|
|
|
|
|
def test_approve_ppa(self):
|
|
|
|
"""Approve packages by their PPA."""
|
|
|
|
shortppa = '~ci-train-ppa-service/+archive/NNNN'
|
|
|
|
pol = SourcePPAPolicy(FakeOptions, {})
|
|
|
|
pol.filename = CACHE_FILE
|
|
|
|
pol.initialise(FakeBritney())
|
|
|
|
output = {}
|
|
|
|
for pkg in ('pal', 'buddy', 'friend', 'noppa'):
|
|
|
|
self.assertEqual(pol.apply_policy_impl(output, None, pkg, None, FakeData, FakeExcuse), PolicyVerdict.PASS)
|
|
|
|
self.assertEqual(output, dict(pal=shortppa, buddy=shortppa, friend=shortppa))
|
|
|
|
|
|
|
|
def test_ignore_ppa(self):
|
|
|
|
"""Ignore packages in non-bileto PPAs."""
|
|
|
|
shortppa = '~kernel-or-whatever/+archive/ppa'
|
|
|
|
pol = SourcePPAPolicy(FakeOptions, {})
|
|
|
|
pol.filename = CACHE_FILE
|
|
|
|
pol.initialise(FakeBritney())
|
|
|
|
for name, versions in pol.cache.items():
|
|
|
|
for version in versions:
|
|
|
|
pol.cache[name][version] = shortppa
|
|
|
|
output = {}
|
|
|
|
for pkg in ('pal', 'buddy', 'friend', 'noppa'):
|
|
|
|
self.assertEqual(pol.apply_policy_impl(output, None, pkg, None, FakeData, FakeExcuse), PolicyVerdict.PASS)
|
|
|
|
self.assertEqual(output, dict())
|
|
|
|
|
|
|
|
def test_reject_ppa(self):
|
|
|
|
"""Reject packages by their PPA."""
|
|
|
|
shortppa = '~ci-train-ppa-service/+archive/NNNN'
|
|
|
|
pol = SourcePPAPolicy(FakeOptions, {})
|
|
|
|
pol.filename = CACHE_FILE
|
|
|
|
brit = FakeBritney()
|
|
|
|
brit.excuses['buddy'].is_valid = False # Just buddy is invalid but whole ppa fails
|
|
|
|
pol.initialise(brit)
|
|
|
|
output = {}
|
|
|
|
# This one passes because the rejection isn't known yet
|
|
|
|
self.assertEqual(pol.apply_policy_impl(output, None, 'pal', None, FakeData, brit.excuses['pal']), PolicyVerdict.PASS)
|
|
|
|
# This one fails because it is itself invalid.
|
|
|
|
self.assertEqual(pol.apply_policy_impl(output, None, 'buddy', None, FakeData, brit.excuses['buddy']), PolicyVerdict.REJECTED_PERMANENTLY)
|
|
|
|
# This one fails because buddy failed before it.
|
|
|
|
self.assertEqual(pol.apply_policy_impl(output, None, 'friend', None, FakeData, brit.excuses['friend']), PolicyVerdict.REJECTED_PERMANENTLY)
|
|
|
|
# 'noppa' not from PPA so not rejected
|
|
|
|
self.assertEqual(pol.apply_policy_impl(output, None, 'noppa', None, FakeData, FakeExcuse), PolicyVerdict.PASS)
|
|
|
|
# All are rejected however
|
|
|
|
for pkg in ('pal', 'buddy', 'friend'):
|
|
|
|
self.assertFalse(brit.excuses[pkg].is_valid)
|
|
|
|
self.assertDictEqual(pol.pkgs_by_source_ppa, {
|
|
|
|
LAUNCHPAD_URL + shortppa: {'pal', 'buddy', 'friend'}})
|
|
|
|
self.assertEqual(output, dict(pal=shortppa, buddy=shortppa, friend=shortppa))
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
unittest.main()
|