junitxml.py
Overview
The [junitxml.py](/projects/286/67489) file is a pytest plugin module responsible for generating test reports in the **JUnit XML format**. This format is widely used by continuous integration (CI) systems like Jenkins, CircleCI, and others to consume and display test results. The module listens to pytest test lifecycle events, collects detailed information about each test's execution outcome (pass, fail, skip, error), and constructs a well-formed XML document that conforms to JUnit XML schemas (`legacy`, `xunit1`, `xunit2`).
Key features include:
Support for multiple JUnit XML schema variants for compatibility with various CI tools.
Capturing test metadata such as duration, classnames, file locations, and custom properties.
Inclusion of captured stdout, stderr, and logs in the XML output.
Fixtures allowing tests to add custom properties and XML attributes.
Handling complex scenarios like failures in teardown phases and parallel test execution (e.g., with xdist).
Integration with pytest’s configuration system (
pytest_addoption,pytest_configure) to enable command-line options and ini settings.
This module complements pytest’s test execution and terminal reporting by providing an extensible, standardized XML output format for automated test result processing.
Detailed Explanation of Classes, Functions, and Methods
Functions
bin_xml_escape(arg: object) -> str
Visually escapes characters in strings that are invalid in XML, replacing them with a visible `#xXX` hexadecimal notation. This is not a true XML escape (no ampersand prefix) but is intended to make illegal characters visible in XML output.
Parameters:
arg — The input object (usually a string) to escape.
Returns:
A string with illegal XML characters replaced by
#xXXrepresentations.
Usage Example:
escaped = bin_xml_escape("hello\aworld\b") # escaped == 'hello#x07world#x08'
merge_family(left, right) -> None
Merges two dictionaries representing attribute "families" for different JUnit XML schema variants by concatenating their attribute lists.
Parameters:
left— Dictionary representing the base family attributes.right— Dictionary representing additional attributes to merge.
Returns:
None. The
leftdictionary is updated in place.
mangle_test_address(address: str) -> list[str]
Transforms a pytest node ID (test address) into a list of strings representing the test "classname" and "name". It converts file paths to dotted module notation and preserves parameterized test suffixes.
Parameters:
address— The pytest nodeid string (e.g.,"test_module.py::TestClass::test_method[param]").
Returns:
A list of strings representing the class and test names (e.g.,
["test_module", "TestClass", "test_method[param]"]).
_warn_incompatibility_with_xunit2(request: FixtureRequest, fixture_name: str) -> None
Issues a pytest warning if a fixture is used that is incompatible with the newer `xunit2` JUnit family schemas.
Parameters:
request— The pytest fixture request object.fixture_name— Name of the fixture being warned about.
Returns:
None.
Fixtures
record_property(request: FixtureRequest) -> Callable[[str, object], None]
A pytest fixture that allows tests to add additional `` elements to the test’s JUnit XML report.
Usage:
def test_example(record_property): record_property("key", "value")Returns:
A callable accepting
nameandvalueparameters to add to the test’s properties.
Notes:
This fixture warns if used with
xunit2family because custom properties are incompatible there.
record_xml_attribute(request: FixtureRequest) -> Callable[[str, object], None]
Fixture for adding extra XML attributes directly to the test case element in the JUnit XML output.
Usage:
def test_example(record_xml_attribute): record_xml_attribute("custom-attr", "value")Returns:
A callable to add XML attributes.
Notes:
Experimental API; emits a warning on use.
Also warns on incompatibility with
xunit2.
record_testsuite_property(request: FixtureRequest) -> Callable[[str, object], None]
Session-scoped fixture to add global `` tags to the root `` element, suitable for adding metadata about the entire test session.
Usage:
def test_foo(record_testsuite_property): record_testsuite_property("ARCH", "PPC")Returns:
A callable accepting
nameandvalueto add to global properties.
Notes:
Does not currently work with pytest-xdist (parallel testing).
Class _NodeReporter
Internal helper class representing a single test case’s XML node and its associated data.
Initialization
_NodeReporter(nodeid: str | TestReport, xml: LogXML)
nodeid: Unique test identifier or aTestReportinstance.xml: Reference to the parentLogXMLinstance managing the overall XML report.
Properties
id: The node ID (test identifier).xml: ParentLogXMLinstance.add_stats: Shortcut toxml.add_statsmethod.family: JUnit XML family being used (legacy,xunit1, orxunit2).duration: Float, total test execution time in seconds.properties: List of(name, value)properties to include in XML.nodes: List of child XML elements (e.g., failure, error nodes).attrs: Dictionary of attributes for the<testcase>XML element.
Methods
append(node: ET.Element) -> None
Adds an XML element as a child node of the test case and updates stats.add_property(name: str, value: object) -> None
Adds a custom property to the test case.add_attribute(name: str, value: object) -> None
Adds an XML attribute to the<testcase>element.make_properties_node() -> ET.Element | None
Returns an XML<properties>node containing all custom properties orNone.record_testreport(testreport: TestReport) -> None
Processes a pytestTestReportto extract and set XML attributes such as classname, name, file, line, and URL, respecting the configured JUnit family.to_xml() -> ET.Element
Builds and returns the final<testcase>XML element with all children and attributes._add_simple(tag: str, message: str, data: str | None = None) -> None
Helper to add simple child nodes like<failure>,<error>, or<skipped>.write_captured_output(report: TestReport) -> None
Includes captured stdout, stderr, and logs into the XML output according to configuration.append_pass(report: TestReport) -> None
Marks the test as passed.append_failure(report: TestReport) -> None
Adds failure information from the test report.append_collect_error(report: TestReport) -> None
Adds collection error information.append_collect_skipped(report: TestReport) -> None
Adds collection skip information.append_error(report: TestReport) -> None
Adds error details from setup or teardown phase failures.append_skipped(report: TestReport) -> None
Adds skipped test information, includingxfailreasoning if applicable.finalize() -> None
Finalizes the test case XML node, preventing further modification.
Class LogXML
Core class managing the entire JUnit XML report lifecycle and integration with pytest hooks.
Initialization
LogXML(
logfile,
prefix: str | None,
suite_name: str = "pytest",
logging: str = "no",
report_duration: str = "total",
family="xunit1",
log_passing_tests: bool = True,
)
logfile: Path to output XML file.prefix: Optional prefix for classnames.suite_name: Name of the root test suite.logging: Level of captured output to include (no,log,system-out,system-err,out-err,all).report_duration: Which timing to report (totalorcall).family: JUnit XML family variant (legacy,xunit1,xunit2).log_passing_tests: Whether to include captured output for passing tests.
Key Properties
stats: Dictionary counting errors, passed, failures, skipped tests.node_reporters: Mapping(nodeid, worker)to_NodeReporterinstances.node_reporters_ordered: List of_NodeReporterin order of creation.global_properties: List of global properties for the entire test suite.open_reports: List ofTestReports that failed during call but pending teardown error.cnt_double_fail_tests: Count of tests with multiple failures (e.g., call + teardown).
Key Methods
pytest_runtest_logreport(report: TestReport) -> None
Main hook processing test reports for each test phase and updating XML nodes accordingly. Manages complex cases like interlaced reports from parallel workers and double failures.pytest_collectreport(report: TestReport) -> None
Reports collection errors/skips as test cases.pytest_internalerror(excrepr: ExceptionRepr) -> None
Records internal pytest errors as XML error nodes.pytest_sessionstart() -> None
Records the start time of the test session.pytest_sessionfinish() -> None
Writes the final JUnit XML file with all accumulated data, including stats and nested test cases.pytest_terminal_summary(terminalreporter: TerminalReporter) -> None
Prints a terminal message about the generated XML file at session end.node_reporter(report: TestReport | str) -> _NodeReporter
Retrieves or creates a_NodeReporterfor a specific test node.add_stats(key: str) -> None
Increments the count for a given test outcome category.update_testcase_duration(report: TestReport) -> None
Adds the duration from the current report phase to the corresponding test case.add_global_property(name: str, value: object) -> None
Adds a property to the root<testsuite>element._get_global_properties_node() -> ET.Element | None
Builds the XML<properties>node for global properties if any.
pytest Hooks and Configuration Integration
pytest_addoption(parser: Parser) -> None
Adds command-line options and ini-file entries for controlling junit xml output, such as--junitxmlpath,--junitprefix, logging level, suite name, duration report mode, and family.pytest_configure(config: Config) -> None
Initializes theLogXMLinstance and registers it as a pytest plugin if--junitxmlis specified and not running on a worker node.pytest_unconfigure(config: Config) -> None
Unregisters theLogXMLplugin and cleans up on pytest shutdown.
Important Implementation Details and Algorithms
XML Escaping:
Because XML disallows certain control characters,bin_xml_escapeis used to convert illegal characters into visible#xXXcodes, ensuring that the XML remains well-formed and human-readable even if test messages contain unusual characters.Test Node Identification and Mangling:
Themangle_test_addressfunction converts pytest node IDs (which use::and file paths) into dot-separated classnames and test names suitable for JUnit XMLclassnameandnameattributes.Handling Multiple JUnit Variants:
Thefamiliesdictionary defines allowed attributes per JUnit family (legacy,xunit1,xunit2) and enforces attribute filtering accordingly.Handling Complex Test Outcomes:
The module carefully handles cases where a test may fail during the call phase and then again during teardown, ensuring XML schema compliance by finalizing one<testcase>and creating a new one as needed.Captured Output Inclusion:
Depending on user configuration, captured stdout, stderr, and logs are included in<system-out>and<system-err>tags within each<testcase>.Parallel Test Execution Support:
Supports handling interlaced reports from parallel workers by keying reporters with(nodeid, worker)tuples.Fixtures for Custom Properties and Attributes:
Providesrecord_property,record_xml_attribute, andrecord_testsuite_propertyfixtures to allow tests and plugins to add metadata dynamically.
Interaction with Other Parts of the System
pytest Core:
Hooks into the pytest lifecycle events, especiallypytest_runtest_logreport,pytest_collectreport,pytest_sessionstart, andpytest_sessionfinish.Test Reporting:
Operates alongside pytest’s terminal reporting plugin but focuses on generating a machine-readable XML file instead of console output.Fixtures:
The provided fixtures (record_property, etc.) integrate with pytest’s fixture system, allowing test code to affect XML report content.Captured Output and Logging:
Reads captured stdout/stderr and logging output from theTestReportobjects.Parallel Execution Plugins (e.g., xdist):
Manages worker-specific test reports and merges them correctly.Configuration System:
Reads ini options and command-line parameters to control behavior and output format.
Usage Example
Add the following option when running pytest to generate a JUnit XML report:
pytest --junitxml=results.xml
Inside a test, add custom properties:
def test_example(record_property):
record_property("priority", "high")
assert 1 == 1
The generated `results.xml` will contain `` inside the corresponding `` element.
Visual Diagram
classDiagram
class LogXML {
+__init__(logfile, prefix, suite_name, logging, report_duration, family, log_passing_tests)
+pytest_runtest_logreport(report)
+pytest_collectreport(report)
+pytest_internalerror(excrepr)
+pytest_sessionstart()
+pytest_sessionfinish()
+pytest_terminal_summary(terminalreporter)
+node_reporter(report)
+add_stats(key)
+update_testcase_duration(report)
+add_global_property(name, value)
-_get_global_properties_node()
}
class _NodeReporter {
+__init__(nodeid, xml)
+append(node)
+add_property(name, value)
+add_attribute(name, value)
+make_properties_node()
+record_testreport(testreport)
+to_xml()
+_add_simple(tag, message, data)
+write_captured_output(report)
+append_pass(report)
+append_failure(report)
+append_collect_error(report)
+append_collect_skipped(report)
+append_error(report)
+append_skipped(report)
+finalize()
}
LogXML "1" o-- "*" _NodeReporter : manages
Summary
[junitxml.py](/projects/286/67489) is a pytest plugin module that generates JUnit-compatible XML reports for test results, providing rich metadata about test execution including outcome, duration, captured output, and custom user properties. Its robust design supports multiple XML schema versions, handles complex test lifecycle scenarios including parallel execution, and integrates seamlessly into pytest’s plugin and fixture systems. This enables integration with CI pipelines and other tools that consume JUnit XML, making it an essential component for automated testing ecosystems.