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