Merge branch 'feat/add-cloud-policy' of git+ssh://git.launchpad.net/~aleksa-svitlica/cloudware/+git/britney2-ubuntu into sil2100/private-runs
commit
be55223a67
@ -0,0 +1,453 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import PurePath
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import smtplib
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
from britney2 import SuiteClass
|
||||||
|
from britney2.policies.policy import BasePolicy
|
||||||
|
from britney2.policies import PolicyVerdict
|
||||||
|
|
||||||
|
class MissingURNException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
FAIL_MESSAGE = """From: Ubuntu Release Team <noreply+proposed-migration@ubuntu.com>
|
||||||
|
To: {recipients}
|
||||||
|
X-Proposed-Migration: notice
|
||||||
|
Subject: [proposed-migration] {package} {version} in {series} failed Cloud tests.
|
||||||
|
|
||||||
|
Hi,
|
||||||
|
|
||||||
|
{package} {version} needs attention.
|
||||||
|
|
||||||
|
This package fails the following tests:
|
||||||
|
|
||||||
|
{results}
|
||||||
|
|
||||||
|
If you have any questions about this email, please ask them in #ubuntu-release channel on libera.chat.
|
||||||
|
|
||||||
|
Regards, Ubuntu Release Team.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ERR_MESSAGE = """From: Ubuntu Release Team <noreply+proposed-migration@ubuntu.com>
|
||||||
|
To: {recipients}
|
||||||
|
X-Proposed-Migration: notice
|
||||||
|
Subject: [proposed-migration] {package} {version} in {series} had errors running Cloud Tests.
|
||||||
|
|
||||||
|
Hi,
|
||||||
|
|
||||||
|
During Cloud tests of {package} {version} the following errors occurred:
|
||||||
|
|
||||||
|
{results}
|
||||||
|
|
||||||
|
If you have any questions about this email, please ask them in #ubuntu-release channel on libera.chat.
|
||||||
|
|
||||||
|
Regards, Ubuntu Release Team.
|
||||||
|
"""
|
||||||
|
class CloudPolicy(BasePolicy):
|
||||||
|
PACKAGE_SET_FILE = "cloud_package_set"
|
||||||
|
DEFAULT_EMAILS = ["cpc@canonical.com"]
|
||||||
|
TEST_LOG_FILE = "CTF.log"
|
||||||
|
|
||||||
|
def __init__(self, options, suite_info, dry_run=False):
|
||||||
|
super().__init__(
|
||||||
|
"cloud", options, suite_info, {SuiteClass.PRIMARY_SOURCE_SUITE}
|
||||||
|
)
|
||||||
|
self.dry_run = dry_run
|
||||||
|
if self.dry_run:
|
||||||
|
self.logger.info("Cloud Policy: Dry-run enabled")
|
||||||
|
|
||||||
|
self.email_host = getattr(self.options, "email_host", "localhost")
|
||||||
|
self.logger.info(
|
||||||
|
"Cloud Policy: will send emails to: %s", self.email_host
|
||||||
|
)
|
||||||
|
self.work_dir = getattr(self.options, "cloud_work_dir", "cloud_tests")
|
||||||
|
self.failure_emails = getattr(self.options, "cloud_failure_emails", self.DEFAULT_EMAILS)
|
||||||
|
self.error_emails = getattr(self.options, "cloud_error_emails", self.DEFAULT_EMAILS)
|
||||||
|
|
||||||
|
adt_ppas = getattr(self.options, "adt_ppas", "").split()
|
||||||
|
ppas = self._parse_ppas(adt_ppas)
|
||||||
|
|
||||||
|
if len(ppas) == 0:
|
||||||
|
self.sources = ["proposed"]
|
||||||
|
self.source_type = "archive"
|
||||||
|
else:
|
||||||
|
self.sources = ppas
|
||||||
|
self.source_type = "ppa"
|
||||||
|
|
||||||
|
self.failures = {}
|
||||||
|
self.errors = {}
|
||||||
|
|
||||||
|
def initialise(self, britney):
|
||||||
|
super().initialise(britney)
|
||||||
|
|
||||||
|
self.package_set = self._retrieve_cloud_package_set_for_series(self.options.series)
|
||||||
|
|
||||||
|
def apply_src_policy_impl(self, policy_info, item, source_data_tdist, source_data_srcdist, excuse):
|
||||||
|
if item.package not in self.package_set:
|
||||||
|
return PolicyVerdict.PASS
|
||||||
|
|
||||||
|
if self.dry_run:
|
||||||
|
self.logger.info(
|
||||||
|
"Cloud Policy: Dry run would test {} in {}".format(item.package , self.options.series)
|
||||||
|
)
|
||||||
|
return PolicyVerdict.PASS
|
||||||
|
|
||||||
|
self._setup_work_directory()
|
||||||
|
self.failures = {}
|
||||||
|
self.errors = {}
|
||||||
|
|
||||||
|
self._run_cloud_tests(item.package, self.options.series, self.sources, self.source_type)
|
||||||
|
|
||||||
|
if len(self.failures) > 0 or len(self.errors) > 0:
|
||||||
|
self._send_emails_if_needed(item.package, source_data_srcdist.version, self.options.series)
|
||||||
|
|
||||||
|
self._cleanup_work_directory()
|
||||||
|
verdict = PolicyVerdict.REJECTED_PERMANENTLY
|
||||||
|
info = self._generate_verdict_info(self.failures, self.errors)
|
||||||
|
excuse.add_verdict_info(verdict, info)
|
||||||
|
return verdict
|
||||||
|
else:
|
||||||
|
self._cleanup_work_directory()
|
||||||
|
return PolicyVerdict.PASS
|
||||||
|
|
||||||
|
def _retrieve_cloud_package_set_for_series(self, series):
|
||||||
|
"""Retrieves a set of packages for the given series in which cloud
|
||||||
|
tests should be run.
|
||||||
|
|
||||||
|
Temporarily a static list retrieved from file. Will be updated to
|
||||||
|
retrieve from a database at a later date.
|
||||||
|
|
||||||
|
:param series The Ubuntu codename for the series (e.g. jammy)
|
||||||
|
"""
|
||||||
|
package_set = set()
|
||||||
|
|
||||||
|
with open(self.PACKAGE_SET_FILE) as file:
|
||||||
|
for line in file:
|
||||||
|
package_set.add(line.strip())
|
||||||
|
|
||||||
|
return package_set
|
||||||
|
|
||||||
|
def _run_cloud_tests(self, package, series, sources, source_type):
|
||||||
|
"""Runs any cloud tests for the given package.
|
||||||
|
Nothing is returned but test failures and errors are stored in instance variables.
|
||||||
|
|
||||||
|
:param package The name of the package to test
|
||||||
|
:param series The Ubuntu codename for the series (e.g. jammy)
|
||||||
|
:param sources List of sources where the package should be installed from (e.g. [proposed] or PPAs)
|
||||||
|
:param source_type Either 'archive' or 'ppa'
|
||||||
|
"""
|
||||||
|
self._run_azure_tests(package, series, sources, source_type)
|
||||||
|
|
||||||
|
def _send_emails_if_needed(self, package, version, series):
|
||||||
|
"""Sends email(s) if there are test failures and/or errors
|
||||||
|
|
||||||
|
:param package The name of the package that was tested
|
||||||
|
:param version The version number of the package
|
||||||
|
:param series The Ubuntu codename for the series (e.g. jammy)
|
||||||
|
"""
|
||||||
|
if len(self.failures) > 0:
|
||||||
|
emails = self.failure_emails
|
||||||
|
message = self._format_email_message(
|
||||||
|
FAIL_MESSAGE, emails, package, version, self.failures
|
||||||
|
)
|
||||||
|
self.logger.info("Cloud Policy: Sending failure email for {}, to {}".format(package, emails))
|
||||||
|
self._send_email(emails, message)
|
||||||
|
|
||||||
|
if len(self.errors) > 0:
|
||||||
|
emails = self.error_emails
|
||||||
|
message = self._format_email_message(
|
||||||
|
ERR_MESSAGE, emails, package, version, self.errors
|
||||||
|
)
|
||||||
|
self.logger.info("Cloud Policy: Sending error email for {}, to {}".format(package, emails))
|
||||||
|
self._send_email(emails, message)
|
||||||
|
|
||||||
|
def _run_azure_tests(self, package, series, sources, source_type):
|
||||||
|
"""Runs Azure's required package tests.
|
||||||
|
|
||||||
|
:param package The name of the package to test
|
||||||
|
:param series The Ubuntu codename for the series (e.g. jammy)
|
||||||
|
:param sources List of sources where the package should be installed from (e.g. [proposed] or PPAs)
|
||||||
|
:param source_type Either 'archive' or 'ppa'
|
||||||
|
"""
|
||||||
|
urn = self._retrieve_urn(series)
|
||||||
|
|
||||||
|
self.logger.info("Cloud Policy: Running Azure tests for: {} in {}".format(package, series))
|
||||||
|
params = [
|
||||||
|
"/snap/bin/cloud-test-framework",
|
||||||
|
"--instance-prefix", "britney-{}-{}".format(package, series)
|
||||||
|
]
|
||||||
|
params.extend(self._format_install_flags(package, sources, source_type))
|
||||||
|
params.extend(
|
||||||
|
[
|
||||||
|
"azure_gen2",
|
||||||
|
"--location", getattr(self.options, "cloud_azure_location", "westeurope"),
|
||||||
|
"--vm-size", getattr(self.options, "cloud_azure_vm_size", "Standard_D2s_v5"),
|
||||||
|
"--urn", urn,
|
||||||
|
"run-test", "package-install-with-reboot",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(PurePath(self.work_dir, self.TEST_LOG_FILE), "w") as file:
|
||||||
|
subprocess.run(
|
||||||
|
params,
|
||||||
|
cwd=self.work_dir,
|
||||||
|
stdout=file
|
||||||
|
)
|
||||||
|
|
||||||
|
results_file_paths = self._find_results_files(r"TEST-NetworkTests-[0-9]*.xml")
|
||||||
|
self._parse_xunit_test_results("Azure", results_file_paths)
|
||||||
|
self._store_extra_test_result_info(self, package)
|
||||||
|
|
||||||
|
def _retrieve_urn(self, series):
|
||||||
|
"""Retrieves an URN from the configuration options based on series.
|
||||||
|
An URN identifies a unique image in Azure.
|
||||||
|
|
||||||
|
:param series The ubuntu codename for the series (e.g. jammy)
|
||||||
|
"""
|
||||||
|
urn = getattr(self.options, "cloud_azure_{}_urn".format(series), None)
|
||||||
|
|
||||||
|
if urn is None:
|
||||||
|
raise MissingURNException("No URN configured for {}".format(series))
|
||||||
|
|
||||||
|
return urn
|
||||||
|
|
||||||
|
def _find_results_files(self, file_regex):
|
||||||
|
"""Find any test results files that match the given regex pattern.
|
||||||
|
|
||||||
|
:param file_regex A regex pattern to use for matching the name of the results file.
|
||||||
|
"""
|
||||||
|
file_paths = []
|
||||||
|
for file in os.listdir(self.work_dir):
|
||||||
|
if re.fullmatch(file_regex, file):
|
||||||
|
file_paths.append(PurePath(self.work_dir, file))
|
||||||
|
|
||||||
|
return file_paths
|
||||||
|
|
||||||
|
def _parse_xunit_test_results(self, cloud, results_file_paths):
|
||||||
|
"""Parses and stores any failure or error test results.
|
||||||
|
|
||||||
|
:param cloud The name of the cloud, use for storing the results.
|
||||||
|
:param results_file_paths List of paths to results files
|
||||||
|
"""
|
||||||
|
for file_path in results_file_paths:
|
||||||
|
with open(file_path) as file:
|
||||||
|
xml = ET.parse(file)
|
||||||
|
root = xml.getroot()
|
||||||
|
|
||||||
|
if root.tag == "testsuites":
|
||||||
|
for testsuite in root:
|
||||||
|
self._parse_xunit_testsuite(cloud, testsuite)
|
||||||
|
else:
|
||||||
|
self._parse_xunit_testsuite(cloud, root)
|
||||||
|
|
||||||
|
def _parse_xunit_testsuite(self, cloud, root):
|
||||||
|
"""Parses the xunit testsuite and stores any failure or error test results.
|
||||||
|
|
||||||
|
:param cloud The name of the cloud, used for storing the results.
|
||||||
|
:param root An XML tree root.
|
||||||
|
"""
|
||||||
|
for el in root:
|
||||||
|
if el.tag == "testcase":
|
||||||
|
for e in el:
|
||||||
|
if e.tag == "failure":
|
||||||
|
type = e.attrib.get('type')
|
||||||
|
message = e.attrib.get('message')
|
||||||
|
info = "{}: {}".format(type, message)
|
||||||
|
self._store_test_result(
|
||||||
|
self.failures, cloud, el.attrib.get('name'), info
|
||||||
|
)
|
||||||
|
if e.tag == "error":
|
||||||
|
type = e.attrib.get('type')
|
||||||
|
message = e.attrib.get('message')
|
||||||
|
info = "{}: {}".format(type, message)
|
||||||
|
self._store_test_result(
|
||||||
|
self.errors, cloud, el.attrib.get('name'), info
|
||||||
|
)
|
||||||
|
|
||||||
|
def _store_test_result(self, results, cloud, test_name, message):
|
||||||
|
"""Adds the test to the results hash under the given cloud.
|
||||||
|
|
||||||
|
Results format:
|
||||||
|
{
|
||||||
|
cloud1: {
|
||||||
|
test_name1: message1
|
||||||
|
test_name2: message2
|
||||||
|
},
|
||||||
|
cloud2: ...
|
||||||
|
}
|
||||||
|
|
||||||
|
:param results A hash to add results to
|
||||||
|
:param cloud The name of the cloud
|
||||||
|
:param message The exception or assertion error given by the test
|
||||||
|
"""
|
||||||
|
if cloud not in results:
|
||||||
|
results[cloud] = {}
|
||||||
|
|
||||||
|
results[cloud][test_name] = message
|
||||||
|
|
||||||
|
def _store_extra_test_result_info(self, cloud, package):
|
||||||
|
"""Stores any information beyond the test results and stores it in the results dicts
|
||||||
|
under Cloud->extra_info
|
||||||
|
|
||||||
|
Stores any information retrieved under the cloud's section in failures/errors but will
|
||||||
|
store nothing if failures/errors are empty.
|
||||||
|
|
||||||
|
:param cloud The name of the cloud
|
||||||
|
:param package The name of the package to test
|
||||||
|
"""
|
||||||
|
if len(self.failures) == 0 and len(self.errors) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
extra_info = {}
|
||||||
|
|
||||||
|
install_source = self._retrieve_package_install_source_from_test_output(package)
|
||||||
|
if install_source:
|
||||||
|
extra_info["install_source"] = install_source
|
||||||
|
|
||||||
|
if len(self.failures.get(cloud, {})) > 0:
|
||||||
|
self._store_test_result(self.failures, cloud, "extra_info", extra_info)
|
||||||
|
|
||||||
|
if len(self.errors.get(cloud, {})) > 0:
|
||||||
|
self._store_test_result(self.errors, cloud, "extra_info", extra_info)
|
||||||
|
|
||||||
|
def _retrieve_package_install_source_from_test_output(self, package):
|
||||||
|
"""Checks the test logs for apt logs which show where the package was installed from.
|
||||||
|
Useful if multiple PPA sources are defined since we won't explicitly know the exact source.
|
||||||
|
|
||||||
|
Will return nothing unless exactly one matching line is found.
|
||||||
|
|
||||||
|
:param package The name of the package to test
|
||||||
|
"""
|
||||||
|
possible_locations = []
|
||||||
|
with open(PurePath(self.work_dir, self.TEST_LOG_FILE), "r") as file:
|
||||||
|
for line in file:
|
||||||
|
if package not in line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "Get:" not in line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if " {} ".format(package) not in line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
possible_locations.append(line)
|
||||||
|
|
||||||
|
if len(possible_locations) == 1:
|
||||||
|
return possible_locations[0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _format_email_message(self, template, emails, package, version, test_results):
|
||||||
|
"""Insert given parameters into the email template."""
|
||||||
|
series = self.options.series
|
||||||
|
results = json.dumps(test_results, indent=4)
|
||||||
|
recipients = ", ".join(emails)
|
||||||
|
message = template.format(**locals())
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
|
def _send_email(self, emails, message):
|
||||||
|
"""Send an email
|
||||||
|
|
||||||
|
:param emails List of emails to send to
|
||||||
|
:param message The content of the email
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
server = smtplib.SMTP(self.email_host)
|
||||||
|
server.sendmail("noreply+proposed-migration@ubuntu.com", emails, message)
|
||||||
|
server.quit()
|
||||||
|
except socket.error as err:
|
||||||
|
self.logger.error("Cloud Policy: Failed to send mail! Is SMTP server running?")
|
||||||
|
self.logger.error(err)
|
||||||
|
|
||||||
|
def _generate_verdict_info(self, failures, errors):
|
||||||
|
info = ""
|
||||||
|
|
||||||
|
if len(failures) > 0:
|
||||||
|
fail_clouds = ",".join(list(failures.keys()))
|
||||||
|
info += "Cloud testing failed for {}.".format(fail_clouds)
|
||||||
|
|
||||||
|
if len(errors) > 0:
|
||||||
|
error_clouds = ",".join(list(errors.keys()))
|
||||||
|
info += " Cloud testing had errors for {}.".format(error_clouds)
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
def _format_install_flags(self, package, sources, source_type):
|
||||||
|
"""Determine the flags required to install the package from the given sources
|
||||||
|
|
||||||
|
:param package The name of the package to test
|
||||||
|
:param sources List of sources where the package should be installed from (e.g. [proposed] or PPAs)
|
||||||
|
:param source_type Either 'archive' or 'ppa'
|
||||||
|
"""
|
||||||
|
install_flags = []
|
||||||
|
|
||||||
|
for source in sources:
|
||||||
|
if source_type == "archive":
|
||||||
|
install_flags.append("--install-archive-package")
|
||||||
|
install_flags.append("{}/{}".format(package, source))
|
||||||
|
elif source_type == "ppa":
|
||||||
|
install_flags.append("--install-ppa-package")
|
||||||
|
install_flags.append("{}/{}".format(package, source))
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Cloud Policy: Unexpected source type, {}".format(source_type))
|
||||||
|
|
||||||
|
return install_flags
|
||||||
|
|
||||||
|
def _parse_ppas(self, ppas):
|
||||||
|
"""Parse PPA list to store in format expected by cloud tests
|
||||||
|
|
||||||
|
Only supports PPAs provided with a fingerprint
|
||||||
|
|
||||||
|
Britney private PPA format:
|
||||||
|
'user:token@team/name:fingerprint'
|
||||||
|
Britney public PPA format:
|
||||||
|
'team/name:fingerprint'
|
||||||
|
Cloud private PPA format:
|
||||||
|
'https://user:token@private-ppa.launchpadcontent.net/team/name/ubuntu=fingerprint
|
||||||
|
Cloud public PPA format:
|
||||||
|
'https://ppa.launchpadcontent.net/team/name/ubuntu=fingerprint
|
||||||
|
|
||||||
|
:param ppas List of PPAs in Britney approved format
|
||||||
|
:return A list of PPAs in valid cloud test format. Can return an empty list if none found.
|
||||||
|
"""
|
||||||
|
cloud_ppas = []
|
||||||
|
|
||||||
|
for ppa in ppas:
|
||||||
|
if '@' in ppa:
|
||||||
|
match = re.match("^(?P<auth>.+:.+)@(?P<name>.+):(?P<fingerprint>.+$)", ppa)
|
||||||
|
if not match:
|
||||||
|
raise RuntimeError('Private PPA %s not following required format (user:token@team/name:fingerprint)', ppa)
|
||||||
|
|
||||||
|
formatted_ppa = "https://{}@private-ppa.launchpadcontent.net/{}/ubuntu={}".format(
|
||||||
|
match.group("auth"), match.group("name"), match.group("fingerprint")
|
||||||
|
)
|
||||||
|
cloud_ppas.append(formatted_ppa)
|
||||||
|
else:
|
||||||
|
match = re.match("^(?P<name>.+):(?P<fingerprint>.+$)", ppa)
|
||||||
|
if not match:
|
||||||
|
raise RuntimeError('Public PPA %s not following required format (team/name:fingerprint)', ppa)
|
||||||
|
|
||||||
|
formatted_ppa = "https://ppa.launchpadcontent.net/{}/ubuntu={}".format(
|
||||||
|
match.group("name"), match.group("fingerprint")
|
||||||
|
)
|
||||||
|
cloud_ppas.append(formatted_ppa)
|
||||||
|
|
||||||
|
return cloud_ppas
|
||||||
|
|
||||||
|
def _setup_work_directory(self):
|
||||||
|
"""Create a directory for tests to be run in."""
|
||||||
|
self._cleanup_work_directory()
|
||||||
|
|
||||||
|
os.makedirs(self.work_dir)
|
||||||
|
|
||||||
|
def _cleanup_work_directory(self):
|
||||||
|
"""Delete the the directory used for running tests."""
|
||||||
|
if os.path.exists(self.work_dir):
|
||||||
|
shutil.rmtree(self.work_dir)
|
||||||
|
|
@ -0,0 +1,600 @@
|
|||||||
|
openssh-client
|
||||||
|
libmpfr6
|
||||||
|
python3-problem-report
|
||||||
|
rsync
|
||||||
|
libdb5
|
||||||
|
libattr1
|
||||||
|
locales
|
||||||
|
xz-utils
|
||||||
|
libpam-modules-bin
|
||||||
|
strace
|
||||||
|
xkb-data
|
||||||
|
dosfstools
|
||||||
|
libpolkit-agent-1-0
|
||||||
|
usbutils
|
||||||
|
dmsetup
|
||||||
|
libfwupdplugin1
|
||||||
|
ucf
|
||||||
|
libdbus-1-3
|
||||||
|
vim-runtime
|
||||||
|
parted
|
||||||
|
libxmlsec1-openssl
|
||||||
|
debconf-i18n
|
||||||
|
libdevmapper-event1
|
||||||
|
libjson-c4
|
||||||
|
libmbim-proxy
|
||||||
|
sound-theme-freedesktop
|
||||||
|
python3-urllib3
|
||||||
|
libgcc-s1
|
||||||
|
libp11-kit0
|
||||||
|
libparted2
|
||||||
|
keyutils
|
||||||
|
libplymouth5
|
||||||
|
libsemanage-common
|
||||||
|
procps
|
||||||
|
python3-httplib2
|
||||||
|
libxslt1
|
||||||
|
glib-networking-common
|
||||||
|
nano
|
||||||
|
libkeyutils1
|
||||||
|
libx11-6
|
||||||
|
linux-base-sgx
|
||||||
|
libreadline8
|
||||||
|
libtext-charwidth-perl
|
||||||
|
libk5crypto3
|
||||||
|
pciutils
|
||||||
|
sensible-utils
|
||||||
|
init-system-helpers
|
||||||
|
libatm1
|
||||||
|
python3-requests
|
||||||
|
python3-idna
|
||||||
|
pci
|
||||||
|
vim-tiny
|
||||||
|
libnss3
|
||||||
|
ncurses-bin
|
||||||
|
udev
|
||||||
|
cryptsetup-initramfs
|
||||||
|
libasound2-data
|
||||||
|
modemmanager
|
||||||
|
bsdmainutils
|
||||||
|
libevent-2
|
||||||
|
xfsprogs
|
||||||
|
libbsd0
|
||||||
|
ubuntu-advantage-pro
|
||||||
|
libfido2-1
|
||||||
|
libzstd1
|
||||||
|
chrony
|
||||||
|
libxmlb2
|
||||||
|
python3-pyrsistent
|
||||||
|
gnupg
|
||||||
|
dmidecode
|
||||||
|
os-prober
|
||||||
|
libbz2-1
|
||||||
|
libfastjson4
|
||||||
|
efibootmgr
|
||||||
|
libdconf1
|
||||||
|
libtdb1
|
||||||
|
libldap-common
|
||||||
|
libreadline5
|
||||||
|
netcat-openbsd
|
||||||
|
python3-gi
|
||||||
|
libpng16-16
|
||||||
|
packagekit-tools
|
||||||
|
python3-twisted-bin
|
||||||
|
python3-cryptography
|
||||||
|
software-properties-common
|
||||||
|
popularity-contest
|
||||||
|
sg3-utils
|
||||||
|
walinuxagent
|
||||||
|
btrfs-progs
|
||||||
|
python3-lib2to3
|
||||||
|
libyaml-0-2
|
||||||
|
python3-serial
|
||||||
|
base-passwd
|
||||||
|
libsigsegv2
|
||||||
|
keyboard-configuration
|
||||||
|
libsasl2-2
|
||||||
|
gpgv
|
||||||
|
python-apt-common
|
||||||
|
grub-pc
|
||||||
|
gpg-wks-client
|
||||||
|
manpages
|
||||||
|
python3-gdbm
|
||||||
|
apport
|
||||||
|
hdparm
|
||||||
|
libdns-export1109
|
||||||
|
vim
|
||||||
|
xdg-user-dirs
|
||||||
|
libxmuu1
|
||||||
|
python3-cffi-backend
|
||||||
|
gdisk
|
||||||
|
libstdc
|
||||||
|
lsb-base
|
||||||
|
libip6tc2
|
||||||
|
htop
|
||||||
|
linux-image-azure
|
||||||
|
linux-tools-azure
|
||||||
|
kpartx
|
||||||
|
libcanberra0
|
||||||
|
libpam-modules
|
||||||
|
liberror-perl
|
||||||
|
motd-news-config
|
||||||
|
libgpg-error0
|
||||||
|
libarchive13
|
||||||
|
squashfs-tools
|
||||||
|
lshw
|
||||||
|
python3-incremental
|
||||||
|
libogg0
|
||||||
|
telnet
|
||||||
|
libgstreamer1
|
||||||
|
libheimntlm0-heimdal
|
||||||
|
python3-jinja2
|
||||||
|
libslang2
|
||||||
|
libpam-systemd
|
||||||
|
dconf-service
|
||||||
|
mount
|
||||||
|
python3-automat
|
||||||
|
python3-debian
|
||||||
|
python3-jsonpatch
|
||||||
|
dbus
|
||||||
|
ubuntu-minimal
|
||||||
|
packagekit
|
||||||
|
python3-more-itertools
|
||||||
|
python3-distro-info
|
||||||
|
at
|
||||||
|
libtext-iconv-perl
|
||||||
|
libpci3
|
||||||
|
base-files
|
||||||
|
libblockdev-part2
|
||||||
|
libsqlite3-0
|
||||||
|
libkmod2
|
||||||
|
libkrb5support0
|
||||||
|
iputils-ping
|
||||||
|
libwrap0
|
||||||
|
libcom-err2
|
||||||
|
irqbalance
|
||||||
|
bcache-tools
|
||||||
|
update-notifier-common
|
||||||
|
libncurses6
|
||||||
|
logsave
|
||||||
|
ubuntu-release-upgrader-core
|
||||||
|
libmbim-glib4
|
||||||
|
bash-completion
|
||||||
|
lxd-agent-loader
|
||||||
|
python3-json-pointer
|
||||||
|
usb-modeswitch-data
|
||||||
|
fdisk
|
||||||
|
libestr0
|
||||||
|
libsystemd0
|
||||||
|
git-man
|
||||||
|
findutils
|
||||||
|
libhcrypto4-heimdal
|
||||||
|
libpsl5
|
||||||
|
perl-modules-5
|
||||||
|
python3-importlib-metadata
|
||||||
|
xxd
|
||||||
|
libtinfo6
|
||||||
|
sbsigntool
|
||||||
|
file
|
||||||
|
libext2fs2
|
||||||
|
passwd
|
||||||
|
sysvinit-utils
|
||||||
|
libasound2
|
||||||
|
libunistring2
|
||||||
|
libksba8
|
||||||
|
plymouth-theme-ubuntu-text
|
||||||
|
distro-info-data
|
||||||
|
libtevent0
|
||||||
|
libmount1
|
||||||
|
libsasl2-modules
|
||||||
|
python3-jwt
|
||||||
|
libntfs-3g883
|
||||||
|
gzip
|
||||||
|
publicsuffix
|
||||||
|
openssh-server
|
||||||
|
python3-hamcrest
|
||||||
|
iputils-tracepath
|
||||||
|
gpg-agent
|
||||||
|
iproute2
|
||||||
|
libmm-glib0
|
||||||
|
apt
|
||||||
|
libfwupdplugin5
|
||||||
|
libtext-wrapi18n-perl
|
||||||
|
libvolume-key1
|
||||||
|
libsodium23
|
||||||
|
kmod
|
||||||
|
cloud-guest-utils
|
||||||
|
python3-apport
|
||||||
|
python3-openssl
|
||||||
|
linux-cloud-tools-azure
|
||||||
|
gir1
|
||||||
|
libx11-data
|
||||||
|
libuv1
|
||||||
|
unattended-upgrades
|
||||||
|
python3-six
|
||||||
|
run-one
|
||||||
|
linux-headers-azure
|
||||||
|
byobu
|
||||||
|
bc
|
||||||
|
libudisks2-0
|
||||||
|
libargon2-1
|
||||||
|
libdevmapper1
|
||||||
|
cryptsetup
|
||||||
|
policykit-1
|
||||||
|
libcap-ng0
|
||||||
|
libgusb2
|
||||||
|
libkrb5-26-heimdal
|
||||||
|
gsettings-desktop-schemas
|
||||||
|
ssh-import-id
|
||||||
|
python3-requests-unixsocket
|
||||||
|
thin-provisioning-tools
|
||||||
|
libc6
|
||||||
|
libefivar1
|
||||||
|
libusb-1
|
||||||
|
login
|
||||||
|
libssl1
|
||||||
|
libklibc
|
||||||
|
dpkg
|
||||||
|
python3-distro
|
||||||
|
eatmydata
|
||||||
|
bash
|
||||||
|
python3-chardet
|
||||||
|
libsmartcols1
|
||||||
|
usb
|
||||||
|
libheimbase1-heimdal
|
||||||
|
fonts-ubuntu-console
|
||||||
|
libnss-systemd
|
||||||
|
libroken18-heimdal
|
||||||
|
libpolkit-gobject-1-0
|
||||||
|
zerofree
|
||||||
|
initramfs-tools
|
||||||
|
libnettle7
|
||||||
|
bind9-dnsutils
|
||||||
|
ubuntu-server
|
||||||
|
glib-networking-services
|
||||||
|
linux-tools-common
|
||||||
|
python3-markupsafe
|
||||||
|
liburcu6
|
||||||
|
diffutils
|
||||||
|
python3-pexpect
|
||||||
|
libpcap0
|
||||||
|
ncurses-base
|
||||||
|
libxml2
|
||||||
|
libuchardet0
|
||||||
|
libblkid1
|
||||||
|
powermgmt-base
|
||||||
|
dbus-user-session
|
||||||
|
ca-certificates
|
||||||
|
sosreport
|
||||||
|
libtasn1-6
|
||||||
|
busybox-initramfs
|
||||||
|
mawk
|
||||||
|
eject
|
||||||
|
libblockdev-fs2
|
||||||
|
e2fsprogs
|
||||||
|
libdrm-common
|
||||||
|
python3-secretstorage
|
||||||
|
vim-common
|
||||||
|
grub-efi-amd64-bin
|
||||||
|
libudev1
|
||||||
|
systemd
|
||||||
|
python3-certifi
|
||||||
|
ed
|
||||||
|
libbrotli1
|
||||||
|
linux-image-5
|
||||||
|
libfuse2
|
||||||
|
python3-click
|
||||||
|
python3-jsonschema
|
||||||
|
bind9-libs
|
||||||
|
libsgutils2-2
|
||||||
|
distro-info
|
||||||
|
libssh-4
|
||||||
|
libtss2-esys0
|
||||||
|
plymouth
|
||||||
|
zlib1g
|
||||||
|
libeatmydata1
|
||||||
|
dconf-gsettings-backend
|
||||||
|
libapparmor1
|
||||||
|
libblockdev-swap2
|
||||||
|
libfdisk1
|
||||||
|
libgcrypt20
|
||||||
|
friendly-recovery
|
||||||
|
libkrb5-3
|
||||||
|
libgpm2
|
||||||
|
gawk
|
||||||
|
initramfs-tools-core
|
||||||
|
pollinate
|
||||||
|
libelf1
|
||||||
|
gettext-base
|
||||||
|
kbd
|
||||||
|
libxmlsec1
|
||||||
|
gpg
|
||||||
|
libnetplan0
|
||||||
|
python3
|
||||||
|
libcurl3-gnutls
|
||||||
|
apport-symptoms
|
||||||
|
libgpgme11
|
||||||
|
python3-debconf
|
||||||
|
libnewt0
|
||||||
|
isc-dhcp-common
|
||||||
|
language-selector-common
|
||||||
|
coreutils
|
||||||
|
grub-common
|
||||||
|
libsoup2
|
||||||
|
console-setup
|
||||||
|
sudo
|
||||||
|
command-not-found
|
||||||
|
libdebconfclient0
|
||||||
|
libjson-glib-1
|
||||||
|
python3-launchpadlib
|
||||||
|
grep
|
||||||
|
cifs-utils
|
||||||
|
whiptail
|
||||||
|
linux-cloud-tools-common
|
||||||
|
libjcat1
|
||||||
|
libisc-export1105
|
||||||
|
cron
|
||||||
|
python3-pkg-resources
|
||||||
|
libblockdev-part-err2
|
||||||
|
libnuma1
|
||||||
|
libxau6
|
||||||
|
libaudit-common
|
||||||
|
libglib2
|
||||||
|
libselinux1
|
||||||
|
libicu66
|
||||||
|
git
|
||||||
|
python3-wadllib
|
||||||
|
libsepol1
|
||||||
|
tmux
|
||||||
|
python3-commandnotfound
|
||||||
|
isc-dhcp-client
|
||||||
|
libpython3
|
||||||
|
dmeventd
|
||||||
|
liblzma5
|
||||||
|
python3-setuptools
|
||||||
|
tpm-udev
|
||||||
|
libunwind8
|
||||||
|
grub-efi-amd64-signed
|
||||||
|
libtalloc2
|
||||||
|
openssl
|
||||||
|
libmagic-mgc
|
||||||
|
libmpdec2
|
||||||
|
libisns0
|
||||||
|
libnfnetlink0
|
||||||
|
libpam0g
|
||||||
|
linux-modules-5
|
||||||
|
libffi7
|
||||||
|
libaio1
|
||||||
|
klibc-utils
|
||||||
|
libsmbios-c2
|
||||||
|
python3-yaml
|
||||||
|
python3-entrypoints
|
||||||
|
psmisc
|
||||||
|
libutempter0
|
||||||
|
linux-azure
|
||||||
|
libmaxminddb0
|
||||||
|
libhx509-5-heimdal
|
||||||
|
python3-zipp
|
||||||
|
grub-pc-bin
|
||||||
|
rsyslog
|
||||||
|
libfwupd2
|
||||||
|
python3-update-manager
|
||||||
|
libgirepository-1
|
||||||
|
liblz4-1
|
||||||
|
lsb-release
|
||||||
|
fwupd-signed
|
||||||
|
libassuan0
|
||||||
|
fwupd
|
||||||
|
screen
|
||||||
|
python3-distutils
|
||||||
|
python3-pyasn1-modules
|
||||||
|
libfl2
|
||||||
|
usb-modeswitch
|
||||||
|
libpipeline1
|
||||||
|
liblocale-gettext-perl
|
||||||
|
libltdl7
|
||||||
|
libmagic1
|
||||||
|
krb5-locales
|
||||||
|
libaccountsservice0
|
||||||
|
libsemanage1
|
||||||
|
libpcre2-8-0
|
||||||
|
pastebinit
|
||||||
|
linux-tools-5
|
||||||
|
groff-base
|
||||||
|
landscape-common
|
||||||
|
ubuntu-standard
|
||||||
|
libgmp10
|
||||||
|
libproxy1v5
|
||||||
|
curl
|
||||||
|
finalrd
|
||||||
|
ethtool
|
||||||
|
python3-netifaces
|
||||||
|
info
|
||||||
|
libasn1-8-heimdal
|
||||||
|
libgnutls30
|
||||||
|
libuuid1
|
||||||
|
libpam-cap
|
||||||
|
python3-pymacaroons
|
||||||
|
libexpat1
|
||||||
|
busybox-static
|
||||||
|
shared-mime-info
|
||||||
|
libwind0-heimdal
|
||||||
|
open-iscsi
|
||||||
|
ncurses-term
|
||||||
|
libparted-fs-resize0
|
||||||
|
libcbor0
|
||||||
|
bsdutils
|
||||||
|
python3-oauthlib
|
||||||
|
ubuntu-keyring
|
||||||
|
overlayroot
|
||||||
|
python3-colorama
|
||||||
|
mime-support
|
||||||
|
python3-newt
|
||||||
|
libsasl2-modules-db
|
||||||
|
multipath-tools
|
||||||
|
iso-codes
|
||||||
|
libidn2-0
|
||||||
|
perl-base
|
||||||
|
python3-simplejson
|
||||||
|
python3-hyperlink
|
||||||
|
cpio
|
||||||
|
libnftnl11
|
||||||
|
fuse
|
||||||
|
libxcb1
|
||||||
|
libnetfilter-conntrack3
|
||||||
|
gnupg-utils
|
||||||
|
liblmdb0
|
||||||
|
cloud-initramfs-copymods
|
||||||
|
libmspack0
|
||||||
|
libss2
|
||||||
|
open-vm-tools
|
||||||
|
ftp
|
||||||
|
libblockdev-crypto2
|
||||||
|
perl
|
||||||
|
accountsservice
|
||||||
|
iptables
|
||||||
|
linux-azure-5
|
||||||
|
gpg-wks-server
|
||||||
|
hostname
|
||||||
|
grub-gfxpayload-lists
|
||||||
|
libnpth0
|
||||||
|
readline-common
|
||||||
|
apparmor
|
||||||
|
libapt-pkg6
|
||||||
|
gcc-10-base
|
||||||
|
linux-headers-5
|
||||||
|
bolt
|
||||||
|
alsa-ucm-conf
|
||||||
|
lvm2
|
||||||
|
libhogweed5
|
||||||
|
ntfs-3g
|
||||||
|
systemd-sysv
|
||||||
|
tzdata
|
||||||
|
libpython3-stdlib
|
||||||
|
libwbclient0
|
||||||
|
pinentry-curses
|
||||||
|
gnupg-l10n
|
||||||
|
secureboot-db
|
||||||
|
libedit2
|
||||||
|
libcap2
|
||||||
|
sed
|
||||||
|
python3-zope
|
||||||
|
python3-service-identity
|
||||||
|
libgssapi-krb5-2
|
||||||
|
python3-keyring
|
||||||
|
install-info
|
||||||
|
netplan
|
||||||
|
python3-software-properties
|
||||||
|
lsscsi
|
||||||
|
ufw
|
||||||
|
libgdbm-compat4
|
||||||
|
libaudit1
|
||||||
|
libacl1
|
||||||
|
python3-lazr
|
||||||
|
lsof
|
||||||
|
mtr-tiny
|
||||||
|
libdw1
|
||||||
|
libefiboot1
|
||||||
|
libpopt0
|
||||||
|
python3-distupgrade
|
||||||
|
libgssapi3-heimdal
|
||||||
|
adduser
|
||||||
|
dirmngr
|
||||||
|
libvorbis0a
|
||||||
|
bind9-host
|
||||||
|
cryptsetup-bin
|
||||||
|
ltrace
|
||||||
|
python3-dbus
|
||||||
|
update-manager-core
|
||||||
|
python3-configobj
|
||||||
|
libpam-runtime
|
||||||
|
python3-systemd
|
||||||
|
python3-twisted
|
||||||
|
dash
|
||||||
|
uuid-runtime
|
||||||
|
libvorbisfile3
|
||||||
|
shim-signed
|
||||||
|
libpcre3
|
||||||
|
liblzo2-2
|
||||||
|
debconf
|
||||||
|
snapd
|
||||||
|
cloud-init
|
||||||
|
libblockdev-utils2
|
||||||
|
linux-base
|
||||||
|
python3-nacl
|
||||||
|
mdadm
|
||||||
|
libcurl4
|
||||||
|
python3-ptyprocess
|
||||||
|
networkd-dispatcher
|
||||||
|
cloud-initramfs-dyn-netconf
|
||||||
|
util-linux
|
||||||
|
libgcab-1
|
||||||
|
libnghttp2-14
|
||||||
|
gpgsm
|
||||||
|
libblockdev2
|
||||||
|
man-db
|
||||||
|
python3-constantly
|
||||||
|
liblvm2cmd2
|
||||||
|
python3-minimal
|
||||||
|
initramfs-tools-bin
|
||||||
|
libldap-2
|
||||||
|
libappstream4
|
||||||
|
python3-pyasn1
|
||||||
|
libfribidi0
|
||||||
|
libblockdev-loop2
|
||||||
|
tcpdump
|
||||||
|
udisks2
|
||||||
|
xauth
|
||||||
|
libxmlb1
|
||||||
|
libmnl0
|
||||||
|
libncursesw6
|
||||||
|
libc-bin
|
||||||
|
libseccomp2
|
||||||
|
tar
|
||||||
|
libqmi-glib5
|
||||||
|
grub2-common
|
||||||
|
alsa-topology-conf
|
||||||
|
time
|
||||||
|
libqmi-proxy
|
||||||
|
libgdbm6
|
||||||
|
libprocps8
|
||||||
|
libxtables12
|
||||||
|
python3-attr
|
||||||
|
gpgconf
|
||||||
|
ubuntu-advantage-tools
|
||||||
|
console-setup-linux
|
||||||
|
librtmp1
|
||||||
|
mokutil
|
||||||
|
glib-networking
|
||||||
|
libxdmcp6
|
||||||
|
cryptsetup-run
|
||||||
|
debianutils
|
||||||
|
libip4tc2
|
||||||
|
lz4
|
||||||
|
python3-blinker
|
||||||
|
libpackagekit-glib2-18
|
||||||
|
patch
|
||||||
|
libstemmer0d
|
||||||
|
libfreetype6
|
||||||
|
less
|
||||||
|
init
|
||||||
|
systemd-timesyncd
|
||||||
|
python3-parted
|
||||||
|
libcrypt1
|
||||||
|
netbase
|
||||||
|
libcap2-bin
|
||||||
|
linux-cloud-tools-5
|
||||||
|
libcryptsetup12
|
||||||
|
bzip2
|
||||||
|
libdrm2
|
||||||
|
logrotate
|
||||||
|
libperl5
|
||||||
|
libatasmart4
|
||||||
|
libxext6
|
||||||
|
apt-utils
|
||||||
|
openssh-sftp-server
|
||||||
|
libgudev-1
|
||||||
|
libnspr4
|
||||||
|
sg3-utils-udev
|
||||||
|
wget
|
||||||
|
python3-apt
|
@ -0,0 +1,310 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
# (C) 2022 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 pathlib
|
||||||
|
import sys
|
||||||
|
from types import SimpleNamespace
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, PROJECT_DIR)
|
||||||
|
|
||||||
|
from britney2.policies.cloud import CloudPolicy, ERR_MESSAGE, MissingURNException
|
||||||
|
|
||||||
|
class FakeItem:
|
||||||
|
package = "chromium-browser"
|
||||||
|
version = "0.0.1"
|
||||||
|
|
||||||
|
class FakeSourceData:
|
||||||
|
version = "55.0"
|
||||||
|
|
||||||
|
class T(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.fake_options = SimpleNamespace(
|
||||||
|
distrubtion = "testbuntu",
|
||||||
|
series = "zazzy",
|
||||||
|
unstable = "/tmp",
|
||||||
|
verbose = False,
|
||||||
|
cloud_source = "zazzy-proposed",
|
||||||
|
cloud_source_type = "archive",
|
||||||
|
cloud_azure_zazzy_urn = "fake-urn-value"
|
||||||
|
)
|
||||||
|
self.policy = CloudPolicy(self.fake_options, {})
|
||||||
|
self.policy._setup_work_directory()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.policy._cleanup_work_directory()
|
||||||
|
|
||||||
|
@patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests")
|
||||||
|
def test_run_cloud_tests_called_for_package_in_manifest(self, mock_run):
|
||||||
|
"""Cloud tests should run for a package in the cloud package set.
|
||||||
|
"""
|
||||||
|
self.policy.package_set = set(["chromium-browser"])
|
||||||
|
self.policy.options.series = "jammy"
|
||||||
|
|
||||||
|
self.policy.apply_src_policy_impl(
|
||||||
|
None, FakeItem, None, FakeSourceData, None
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_run.assert_called_once_with(
|
||||||
|
"chromium-browser", "jammy", ["proposed"], "archive"
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests")
|
||||||
|
def test_run_cloud_tests_not_called_for_package_not_in_manifest(self, mock_run):
|
||||||
|
"""Cloud tests should not run for packages not in the cloud package set"""
|
||||||
|
|
||||||
|
self.policy.package_set = set(["vim"])
|
||||||
|
self.policy.options.series = "jammy"
|
||||||
|
|
||||||
|
self.policy.apply_src_policy_impl(
|
||||||
|
None, FakeItem, None, FakeSourceData, None
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_run.assert_not_called()
|
||||||
|
|
||||||
|
@patch("britney2.policies.cloud.smtplib")
|
||||||
|
@patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests")
|
||||||
|
def test_no_tests_run_during_dry_run(self, mock_run, smtp):
|
||||||
|
self.policy = CloudPolicy(self.fake_options, {}, dry_run=True)
|
||||||
|
self.policy.package_set = set(["chromium-browser"])
|
||||||
|
self.policy.options.series = "jammy"
|
||||||
|
self.policy.source = "jammy-proposed"
|
||||||
|
|
||||||
|
self.policy.apply_src_policy_impl(
|
||||||
|
None, FakeItem, None, FakeSourceData, None
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_run.assert_not_called()
|
||||||
|
self.assertEqual(smtp.mock_calls, [])
|
||||||
|
|
||||||
|
def test_finding_results_file(self):
|
||||||
|
"""Ensure result file output from Cloud Test Framework can be found"""
|
||||||
|
path = pathlib.PurePath(self.policy.work_dir, "TEST-FakeTests-20230101010101.xml")
|
||||||
|
path2 = pathlib.PurePath(self.policy.work_dir, "Test-OtherTests-20230101010101.xml")
|
||||||
|
with open(path, "a"): pass
|
||||||
|
with open(path2, "a"): pass
|
||||||
|
|
||||||
|
regex = r"TEST-FakeTests-[0-9]*.xml"
|
||||||
|
results_file_paths = self.policy._find_results_files(regex)
|
||||||
|
|
||||||
|
self.assertEqual(len(results_file_paths), 1)
|
||||||
|
self.assertEqual(results_file_paths[0], path)
|
||||||
|
|
||||||
|
def test_parsing_of_xunit_results_file(self):
|
||||||
|
"""Test that parser correctly sorts and stores test failures and errors"""
|
||||||
|
path = self._create_fake_test_result_file(num_pass=4, num_err=2, num_fail=3)
|
||||||
|
self.policy._parse_xunit_test_results("Azure", [path])
|
||||||
|
|
||||||
|
azure_failures = self.policy.failures.get("Azure", {})
|
||||||
|
azure_errors = self.policy.errors.get("Azure", {})
|
||||||
|
|
||||||
|
self.assertEqual(len(azure_failures), 3)
|
||||||
|
self.assertEqual(len(azure_errors), 2)
|
||||||
|
|
||||||
|
test_names = azure_failures.keys()
|
||||||
|
self.assertIn("failing_test_1", test_names)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
azure_failures.get("failing_test_1"), "AssertionError: A useful error message"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_email_formatting(self):
|
||||||
|
"""Test that information is inserted correctly in the email template"""
|
||||||
|
failures = {
|
||||||
|
"Azure": {
|
||||||
|
"failing_test1": "Error reason 1",
|
||||||
|
"failing_test2": "Error reason 2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.policy.options.series = "jammy"
|
||||||
|
self.policy.source = "jammy-proposed"
|
||||||
|
message = self.policy._format_email_message(ERR_MESSAGE, ["work@canonical.com"], "vim", "9.0", failures)
|
||||||
|
|
||||||
|
self.assertIn("To: work@canonical.com", message)
|
||||||
|
self.assertIn("vim 9.0", message)
|
||||||
|
self.assertIn("Error reason 2", message)
|
||||||
|
|
||||||
|
def test_urn_retrieval(self):
|
||||||
|
"""Test that URN retrieval throws the expected error when not configured."""
|
||||||
|
self.assertRaises(
|
||||||
|
MissingURNException, self.policy._retrieve_urn, "jammy"
|
||||||
|
)
|
||||||
|
|
||||||
|
urn = self.policy._retrieve_urn("zazzy")
|
||||||
|
self.assertEqual(urn, "fake-urn-value")
|
||||||
|
|
||||||
|
def test_generation_of_verdict_info(self):
|
||||||
|
"""Test that the verdict info correctly states which clouds had failures and/or errors"""
|
||||||
|
failures = {
|
||||||
|
"cloud1": {
|
||||||
|
"test_name1": "message1",
|
||||||
|
"test_name2": "message2"
|
||||||
|
},
|
||||||
|
"cloud2": {
|
||||||
|
"test_name3": "message3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errors = {
|
||||||
|
"cloud1": {
|
||||||
|
"test_name4": "message4",
|
||||||
|
},
|
||||||
|
"cloud3": {
|
||||||
|
"test_name5": "message5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info = self.policy._generate_verdict_info(failures, errors)
|
||||||
|
|
||||||
|
expected_failure_info = "Cloud testing failed for cloud1,cloud2."
|
||||||
|
expected_error_info = "Cloud testing had errors for cloud1,cloud3."
|
||||||
|
|
||||||
|
self.assertIn(expected_failure_info, info)
|
||||||
|
self.assertIn(expected_error_info, info)
|
||||||
|
|
||||||
|
def test_format_install_flags_with_ppas(self):
|
||||||
|
"""Ensure the correct flags are returned with PPA sources"""
|
||||||
|
expected_flags = [
|
||||||
|
"--install-ppa-package", "tmux/ppa_url=fingerprint",
|
||||||
|
"--install-ppa-package", "tmux/ppa_url2=fingerprint"
|
||||||
|
]
|
||||||
|
install_flags = self.policy._format_install_flags(
|
||||||
|
"tmux", ["ppa_url=fingerprint", "ppa_url2=fingerprint"], "ppa"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertListEqual(install_flags, expected_flags)
|
||||||
|
|
||||||
|
def test_format_install_flags_with_archive(self):
|
||||||
|
"""Ensure the correct flags are returned with archive sources"""
|
||||||
|
expected_flags = ["--install-archive-package", "tmux/proposed"]
|
||||||
|
install_flags = self.policy._format_install_flags("tmux", ["proposed"], "archive")
|
||||||
|
|
||||||
|
self.assertListEqual(install_flags, expected_flags)
|
||||||
|
|
||||||
|
def test_format_install_flags_with_incorrect_type(self):
|
||||||
|
"""Ensure errors are raised for unknown source types"""
|
||||||
|
|
||||||
|
self.assertRaises(RuntimeError, self.policy._format_install_flags, "tmux", ["a_source"], "something")
|
||||||
|
|
||||||
|
def test_parse_ppas(self):
|
||||||
|
"""Ensure correct conversion from Britney format to cloud test format
|
||||||
|
Also check that public PPAs are not used due to fingerprint requirement for cloud
|
||||||
|
tests.
|
||||||
|
"""
|
||||||
|
input_ppas = [
|
||||||
|
"deadsnakes/ppa:fingerprint",
|
||||||
|
"user:token@team/name:fingerprint"
|
||||||
|
]
|
||||||
|
|
||||||
|
expected_ppas = [
|
||||||
|
"https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu=fingerprint",
|
||||||
|
"https://user:token@private-ppa.launchpadcontent.net/team/name/ubuntu=fingerprint"
|
||||||
|
]
|
||||||
|
|
||||||
|
output_ppas = self.policy._parse_ppas(input_ppas)
|
||||||
|
self.assertListEqual(output_ppas, expected_ppas)
|
||||||
|
|
||||||
|
def test_errors_raised_if_invalid_ppa_input(self):
|
||||||
|
"""Test that error are raised if input PPAs don't match expected format"""
|
||||||
|
self.assertRaises(
|
||||||
|
RuntimeError, self.policy._parse_ppas, ["team/name"]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
RuntimeError, self.policy._parse_ppas, ["user:token@team/name"]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
RuntimeError, self.policy._parse_ppas, ["user:token@team=fingerprint"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_retrieve_package_install_source_from_test_output(self):
|
||||||
|
"""Ensure retrieving the package install source from apt output only returns the line we
|
||||||
|
want and not other lines containing the package name.
|
||||||
|
|
||||||
|
Ensure it returns nothing if multiple candidates are found because that means the parsing
|
||||||
|
needs to be updated.
|
||||||
|
"""
|
||||||
|
package = "tmux"
|
||||||
|
|
||||||
|
with open(pathlib.PurePath(self.policy.work_dir, self.policy.TEST_LOG_FILE), "w") as file:
|
||||||
|
file.write("Get: something \n".format(package))
|
||||||
|
file.write("Get: lib-{} \n".format(package))
|
||||||
|
|
||||||
|
install_source = self.policy._retrieve_package_install_source_from_test_output(package)
|
||||||
|
self.assertIsNone(install_source)
|
||||||
|
|
||||||
|
with open(pathlib.PurePath(self.policy.work_dir, self.policy.TEST_LOG_FILE), "a") as file:
|
||||||
|
file.write("Get: {} \n".format(package))
|
||||||
|
|
||||||
|
install_source = self.policy._retrieve_package_install_source_from_test_output(package)
|
||||||
|
self.assertEqual(install_source, "Get: tmux \n")
|
||||||
|
|
||||||
|
@patch("britney2.policies.cloud.CloudPolicy._retrieve_package_install_source_from_test_output")
|
||||||
|
def test_store_extra_test_result_info(self, mock):
|
||||||
|
"""Ensure nothing is done if there are no failures/errors.
|
||||||
|
Ensure that if there are failures/errors that any extra info retrieved is stored in the
|
||||||
|
results dict Results -> Cloud -> extra_info
|
||||||
|
"""
|
||||||
|
self.policy._store_extra_test_result_info("FakeCloud", "tmux")
|
||||||
|
mock.assert_not_called()
|
||||||
|
|
||||||
|
self.policy.failures = {"FakeCloud": {"failing_test": "failure reason"}}
|
||||||
|
mock.return_value = "source information"
|
||||||
|
self.policy._store_extra_test_result_info("FakeCloud", "tmux")
|
||||||
|
self.assertEqual(
|
||||||
|
self.policy.failures["FakeCloud"]["extra_info"]["install_source"], "source information"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_fake_test_result_file(self, num_pass=1, num_err=0, num_fail=0):
|
||||||
|
"""Helper function to generate an xunit test result file.
|
||||||
|
|
||||||
|
:param num_pass The number of passing tests to include
|
||||||
|
:param num_err The number of erroring tests to include
|
||||||
|
:param num_fail The number of failing tests to include
|
||||||
|
|
||||||
|
Returns the path to the created file.
|
||||||
|
"""
|
||||||
|
os.makedirs(self.policy.work_dir, exist_ok=True)
|
||||||
|
path = pathlib.PurePath(self.policy.work_dir, "TEST-FakeTests-20230101010101.xml")
|
||||||
|
|
||||||
|
root = ET.Element("testsuite", attrib={"name": "FakeTests-1234567890"})
|
||||||
|
|
||||||
|
for x in range(0, num_pass):
|
||||||
|
case_attrib = {"classname": "FakeTests", "name": "passing_test_{}".format(x), "time":"0.001"}
|
||||||
|
ET.SubElement(root, "testcase", attrib=case_attrib)
|
||||||
|
|
||||||
|
for x in range(0, num_err):
|
||||||
|
case_attrib = {"classname": "FakeTests", "name": "erroring_test_{}".format(x), "time":"0.001"}
|
||||||
|
testcase = ET.SubElement(root, "testcase", attrib=case_attrib)
|
||||||
|
|
||||||
|
err_attrib = {"type": "Exception", "message": "A useful error message" }
|
||||||
|
ET.SubElement(testcase, "error", attrib=err_attrib)
|
||||||
|
|
||||||
|
for x in range(0, num_fail):
|
||||||
|
case_attrib = {"classname": "FakeTests", "name": "failing_test_{}".format(x), "time":"0.001"}
|
||||||
|
testcase = ET.SubElement(root, "testcase", attrib=case_attrib)
|
||||||
|
|
||||||
|
fail_attrib = {"type": "AssertionError", "message": "A useful error message" }
|
||||||
|
ET.SubElement(testcase, "failure", attrib=fail_attrib)
|
||||||
|
|
||||||
|
|
||||||
|
tree = ET.ElementTree(root)
|
||||||
|
ET.indent(tree, space="\t", level=0)
|
||||||
|
|
||||||
|
with open(path, "w") as file:
|
||||||
|
tree.write(file, encoding="unicode", xml_declaration=True)
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
Loading…
Reference in new issue