Skip to content

junit

Build JUnit XML reports from build events.

JUnit XML schema (consumed by GitLab CI, Jenkins, etc.):

<testsuites>
  <testsuite name="..." tests="N" failures="M" errors="K" time="T">
    <testcase name="..." classname="..." time="T">
      <failure message="..." type="...">details</failure>
      <error message="...">details</error>
      <system-out>log lines</system-out>
      <system-err>error lines</system-err>
    </testcase>
  </testsuite>
</testsuites>

_make_text_elem(tag, lines)

Create a text element with joined lines content.

Parameters:

Name Type Description Default
tag str

XML tag name (e.g. "system-out").

required
lines list[str]

Lines of text to join with newlines.

required

Returns:

Type Description
Element

An xml.etree.ElementTree.Element with the joined text.

Source code in src/junix/junit.py
def _make_text_elem(tag: str, lines: list[str]) -> ET.Element:
    """Create a text element with joined lines content.

    Args:
        tag: XML tag name (e.g. `"system-out"`).
        lines: Lines of text to join with newlines.

    Returns:
        An xml.etree.ElementTree.Element with the joined text.
    """
    e = ET.Element(tag)
    e.text = "\n".join(lines) + "\n" if lines else ""
    return e

report_to_xml(builds, suite_name='nix-build', error_messages=None)

Convert build events into a JUnit XML string.

Parameters:

Name Type Description Default
builds list[BuildEvent]

Collected build events from NixEventHandler.

required
suite_name str

Value for the <testsuite name="..."> attribute.

'nix-build'
error_messages list[str] | None

Additional error messages (from the Nix logger) to include in the suite-level <system-err>.

None

Returns:

Type Description
str

A JUnit XML string.

Source code in src/junix/junit.py
def report_to_xml(
    builds: list[BuildEvent],
    suite_name: str = "nix-build",
    error_messages: list[str] | None = None,
) -> str:
    """Convert build events into a JUnit XML string.

    Args:
        builds: Collected build events from NixEventHandler.
        suite_name: Value for the `<testsuite name="...">` attribute.
        error_messages: Additional error messages (from the Nix logger)
            to include in the suite-level `<system-err>`.

    Returns:
        A JUnit XML string.
    """
    testsuite = ET.Element("testsuite")
    testsuite.set("name", suite_name)

    total = len(builds)
    failed = sum(1 for b in builds if b.stopped and not b.success)
    incomplete = sum(1 for b in builds if not b.stopped)
    skipped = sum(1 for b in builds if b.cached)
    testsuite.set("tests", str(total))
    testsuite.set("failures", str(failed))
    testsuite.set("errors", str(incomplete))
    testsuite.set("skipped", str(skipped))
    testsuite.set("time", "0")

    for build in sorted(builds, key=lambda b: b.name):
        tc = ET.SubElement(testsuite, "testcase")
        tc.set("name", build.name)
        tc.set("classname", build.drv_path)
        tc.set("time", "0")

        if build.cached:
            # Build was served from cache -- skipped
            skip_el = ET.SubElement(tc, "skipped")
            skip_el.set("message", "Cached / substituted")
            skip_el.set("type", "cached")
        elif not build.stopped:
            # Build never completed -- error
            error_el = ET.SubElement(tc, "error")
            error_el.set("message", "Build did not complete")
            error_el.set("type", "incomplete")
            if build.log_lines:
                tc.append(_make_text_elem("system-err", build.log_lines))
        elif not build.success:
            failure_el = ET.SubElement(tc, "failure")
            failure_el.set("message", "Build failed")
            failure_el.set("type", "failure")
            if build.log_lines:
                tc.append(_make_text_elem("system-out", build.log_lines))
        else:
            if build.log_lines:
                tc.append(_make_text_elem("system-out", build.log_lines))

    # Suite-level error output
    if error_messages:
        testsuite.append(_make_text_elem("system-err", error_messages))

    root = ET.Element("testsuites")
    root.append(testsuite)

    ET.indent(root, space="  ")
    return ET.tostring(root, encoding="unicode") + "\n"