Christoph Erhardt 4deb12b789 Make unit tests compatible with Python 3.13
Python 3.13 has changed the behaviour of the `xml.dom.minidom` module:
double quotes used to be escaped everywhere, but they are now only
escaped inside attributes.

This was done by the following commit.
154477be72

As a result, the unit tests fail when executed on Python 3.13.

Fix this issue in the following way:
1. In all 88 reference-output XML files, replace all occurrences of
   `"` with `"`.
2. Change `tests/conftest.py` to force-replace all occurrences of
   `"` with `"` in the test output before comparing it to the
   reference output. This ensures that the subsequent equality assertion
   still holds on older versions of Python.

This solution works because the test data does not contain any instances
of `"` inside XML attributes.

Change-Id: Ied875014b1bb5d8225943303858658648efc12ad
2024-10-30 17:18:25 +00:00

244 lines
7.4 KiB
Python

import configparser
import pkg_resources
import xml.etree.ElementTree as XML
from pathlib import Path
import pytest
import yaml
from jenkins.plugins import Plugin
from jenkins_jobs.alphanum import AlphanumSort
from jenkins_jobs.config import JJBConfig
from jenkins_jobs.loader import Loader
from jenkins_jobs.modules import project_externaljob
from jenkins_jobs.modules import project_flow
from jenkins_jobs.modules import project_githuborg
from jenkins_jobs.modules import project_matrix
from jenkins_jobs.modules import project_maven
from jenkins_jobs.modules import project_multibranch
from jenkins_jobs.modules import project_multijob
from jenkins_jobs.registry import ModuleRegistry
from jenkins_jobs.xml_config import XmlJob, XmlJobGenerator, XmlViewGenerator
from jenkins_jobs import utils
from jenkins_jobs.roots import Roots
from jenkins_jobs.loader import load_files
# Avoid writing to ~/.cache/jenkins_jobs.
@pytest.fixture(autouse=True)
def job_cache_mocked(mocker):
mocker.patch("jenkins_jobs.builder.JobCache", autospec=True)
@pytest.fixture
def config_path(scenario):
return scenario.config_path
@pytest.fixture
def jjb_config(config_path):
config = JJBConfig(config_path)
config.validate()
return config
@pytest.fixture
def mock_iter_entry_points():
config = configparser.ConfigParser()
config.read(Path(__file__).parent / "../setup.cfg")
groups = {}
for key in config["entry_points"]:
groups[key] = list()
for line in config["entry_points"][key].split("\n"):
if "" == line.strip():
continue
groups[key].append(
pkg_resources.EntryPoint.parse(line, dist=pkg_resources.Distribution())
)
def iter_entry_points(group, name=None):
return (entry for entry in groups[group] if name is None or name == entry.name)
return iter_entry_points
@pytest.fixture
def input(scenario, jjb_config):
loader = Loader.empty(jjb_config)
return loader.load_path(scenario.in_path)
@pytest.fixture
def plugins_info(scenario):
if not scenario.plugins_info_path.exists():
return None
plugin_dict_list = yaml.safe_load(scenario.plugins_info_path.read_text())
return [Plugin(**plugin_dict) for plugin_dict in plugin_dict_list]
@pytest.fixture
def registry(mocker, mock_iter_entry_points, jjb_config, plugins_info):
mocker.patch("pkg_resources.iter_entry_points", side_effect=mock_iter_entry_points)
return ModuleRegistry(jjb_config, plugins_info)
@pytest.fixture
def project(input, registry):
type_to_class = {
"maven": project_maven.Maven,
"matrix": project_matrix.Matrix,
"flow": project_flow.Flow,
"githuborg": project_githuborg.GithubOrganization,
"multijob": project_multijob.MultiJob,
"multibranch": project_multibranch.WorkflowMultiBranch,
"multibranch-defaults": project_multibranch.WorkflowMultiBranchDefaults,
"externaljob": project_externaljob.ExternalJob,
}
try:
class_name = input["project-type"]
except KeyError:
return None
if class_name == "freestyle":
return None
cls = type_to_class[class_name]
return cls(registry)
@pytest.fixture
def expected_output(scenario):
if not scenario.out_paths:
# Do not check output if there are no files for it.
return None
return "".join(path.read_text() for path in sorted(scenario.out_paths))
@pytest.fixture
def expected_error(scenario):
if scenario.error_path.exists():
return scenario.error_path.read_text().rstrip()
else:
return None
# Tests use output files directories as expected folder name.
def check_folders(scenario, job_xml_list):
root_dir = scenario.in_path.parent
def name_parent(name):
*dirs, name = name.split("/")
return "/".join(dirs)
def path_parent(path):
if scenario.in_path.is_dir():
# In directory tests, output file directory does not
# indicate expected job folder.
base_dir = scenario.in_path
else:
base_dir = root_dir
dir = str(path.relative_to(base_dir).parent)
if dir == ".":
return ""
else:
return dir
actual_dirs = list(sorted(set(name_parent(jx.name) for jx in job_xml_list)))
expected_dirs = list(sorted(path_parent(path) for path in scenario.out_paths))
assert expected_dirs == actual_dirs
@pytest.fixture
def check_generator(scenario, input, expected_output, jjb_config, registry, project):
def check(Generator):
if project:
xml = project.root_xml(input)
else:
xml = XML.Element("project")
generator = Generator(registry)
generator.gen_xml(xml, input)
pretty_xml = (
XmlJob(xml, "fixturejob")
.output()
.decode()
.replace("&quot;", '"') # Ensure compatibility with Python < 3.13
)
assert expected_output == pretty_xml
return check
@pytest.fixture
def check_parser(jjb_config, registry):
def check(in_path):
roots = Roots(jjb_config)
load_files(jjb_config, roots, [in_path])
registry.set_macros(roots.macros)
job_data_list = roots.generate_jobs()
view_data_list = roots.generate_views()
generator = XmlJobGenerator(registry)
_ = generator.generateXML(job_data_list)
_ = generator.generateXML(view_data_list)
return check
@pytest.fixture
def check_job(scenario, expected_output, jjb_config, registry):
def check():
roots = Roots(jjb_config)
if jjb_config.recursive:
path_list = [Path(p) for p in utils.recurse_path(str(scenario.in_path))]
else:
path_list = [scenario.in_path]
load_files(jjb_config, roots, path_list)
registry.set_macros(roots.macros)
job_data_list = roots.generate_jobs()
registry.amend_job_dicts(job_data_list)
generator = XmlJobGenerator(registry)
job_xml_list = generator.generateXML(job_data_list)
job_xml_list.sort(key=AlphanumSort)
pretty_xml = (
"\n".join(job.output().decode() for job in job_xml_list)
.strip()
.replace("\n\n", "\n")
.replace("&quot;", '"') # Ensure compatibility with Python < 3.13
)
if expected_output is None:
return
stripped_expected_output = (
expected_output.strip().replace("<BLANKLINE>", "").replace("\n\n", "\n")
)
assert stripped_expected_output == pretty_xml
check_folders(scenario, job_xml_list)
return check
@pytest.fixture
def check_view(scenario, expected_output, jjb_config, registry):
def check():
roots = Roots(jjb_config)
load_files(jjb_config, roots, [scenario.in_path])
registry.set_macros(roots.macros)
view_data_list = roots.generate_views()
generator = XmlViewGenerator(registry)
view_xml_list = generator.generateXML(view_data_list)
view_xml_list.sort(key=AlphanumSort)
pretty_xml = (
"\n".join(view.output().decode() for view in view_xml_list)
.strip()
.replace("\n\n", "\n")
)
if expected_output is None:
return
stripped_expected_output = (
expected_output.strip().replace("<BLANKLINE>", "").replace("\n\n", "\n")
)
assert stripped_expected_output == pretty_xml
return check