unittest-based test runner with Ant/JUnit like XML reporting.
Transformation utilities for ensuring XML report compatibility with various versions of Jenkins xUnit plugins. Addresses schema validation differences and provides tools for adapting reports to specific CI/CD requirements.
Transform XML reports to be compatible with specific versions of Jenkins xUnit plugin that have stricter schema validation requirements.
def transform(xml_data):
"""
Transform XML report for Jenkins xUnit plugin compatibility.
Removes attributes that cause validation failures in Jenkins xUnit plugin
version 1.104+ which uses stricter XSD validation.
Parameters:
- xml_data: bytes, input XML report data
Returns:
- bytes: transformed XML report data
Dependencies:
- lxml: required for XML transformation
"""The standard JUnit plugin (https://plugins.jenkins.io/junit/) has relaxed validation:
import unittest
import xmlrunner
# Standard output works with Jenkins JUnit plugin
unittest.main(
testRunner=xmlrunner.XMLTestRunner(output='test-reports'),
failfast=False, buffer=False, catchbreak=False
)Older version with more lenient XSD validation:
Newer version with strict XSD validation:
import io
import unittest
import xmlrunner
from xmlrunner.extra.xunit_plugin import transform
# Generate XML report in memory
output = io.BytesIO()
unittest.main(
testRunner=xmlrunner.XMLTestRunner(output=output),
failfast=False, buffer=False, catchbreak=False, exit=False
)
# Transform for Jenkins xUnit plugin compatibility
transformed_xml = transform(output.getvalue())
# Write to file for Jenkins consumption
with open('TEST-report.xml', 'wb') as report:
report.write(transformed_xml)# ci_test_runner.py
import io
import sys
import unittest
import xmlrunner
from xmlrunner.extra.xunit_plugin import transform
def run_tests_for_jenkins():
"""Run tests and generate Jenkins-compatible XML reports."""
# Capture XML output
output = io.BytesIO()
# Run tests with XML reporting
runner = xmlrunner.XMLTestRunner(
output=output,
verbosity=2,
elapsed_times=True
)
# Discover and run tests
loader = unittest.TestLoader()
suite = loader.discover('tests', pattern='test_*.py')
result = runner.run(suite)
# Transform for Jenkins compatibility
xml_data = output.getvalue()
transformed_xml = transform(xml_data)
# Write Jenkins-compatible report
with open('junit-report.xml', 'wb') as report_file:
report_file.write(transformed_xml)
# Return exit code based on test results
return 0 if result.wasSuccessful() else 1
if __name__ == '__main__':
sys.exit(run_tests_for_jenkins())import os
import glob
from xmlrunner.extra.xunit_plugin import transform
def transform_reports_directory(input_dir, output_dir):
"""Transform all XML reports in a directory for Jenkins compatibility."""
os.makedirs(output_dir, exist_ok=True)
for xml_file in glob.glob(os.path.join(input_dir, 'TEST-*.xml')):
# Read original report
with open(xml_file, 'rb') as f:
xml_data = f.read()
# Transform for compatibility
transformed_xml = transform(xml_data)
# Write transformed report
filename = os.path.basename(xml_file)
output_path = os.path.join(output_dir, filename)
with open(output_path, 'wb') as f:
f.write(transformed_xml)
print(f"Transformed {xml_file} -> {output_path}")
# Usage
transform_reports_directory('raw-reports', 'jenkins-reports')The transformation removes the following attributes that cause validation failures:
All other elements and attributes are preserved:
The transformation uses XSLT (Extensible Stylesheet Language Transformations):
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="xml" indent="yes" />
<!-- Remove problematic attributes -->
<xsl:template match="//testcase/@file" />
<xsl:template match="//testcase/@line" />
<xsl:template match="//testcase/@timestamp" />
<!-- Copy everything else -->
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*" />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>The transformation requires lxml:
try:
from xmlrunner.extra.xunit_plugin import transform
except ImportError:
print("lxml is required for Jenkins compatibility transformation")
print("Install with: pip install lxml")
sys.exit(1)from lxml import etree
from xmlrunner.extra.xunit_plugin import transform
def safe_transform(xml_data):
"""Safely transform XML with error handling."""
try:
return transform(xml_data)
except etree.XMLSyntaxError as e:
print(f"Invalid XML input: {e}")
return xml_data # Return original on error
except Exception as e:
print(f"Transformation error: {e}")
return xml_dataimport os
from xmlrunner.extra.xunit_plugin import transform
def maybe_transform_for_jenkins(xml_data):
"""Transform XML only if Jenkins xUnit plugin version requires it."""
jenkins_version = os.getenv('JENKINS_XUNIT_VERSION', '1.100')
if jenkins_version >= '1.104':
return transform(xml_data)
return xml_datafrom xmlrunner.extra.xunit_plugin import transform
import lxml.etree as etree
def custom_jenkins_transform(xml_data, remove_timestamps=True,
remove_file_info=True):
"""Custom transformation with configurable options."""
if not (remove_timestamps or remove_file_info):
return xml_data
# Parse XML
doc = etree.XML(xml_data)
# Remove attributes based on options
if remove_file_info:
for elem in doc.xpath('//testcase'):
elem.attrib.pop('file', None)
elem.attrib.pop('line', None)
if remove_timestamps:
for elem in doc.xpath('//testcase'):
elem.attrib.pop('timestamp', None)
# Return transformed XML
return etree.tostring(doc, pretty_print=True, encoding='UTF-8')This transformation system ensures unittest-xml-reporting works seamlessly with various Jenkins plugin versions while maintaining all essential test result information.
Install with Tessl CLI
npx tessl i tessl/pypi-unittest-xml-reporting