Skip to content

Commit

Permalink
Add support for subresults in junit report plugin flavor
Browse files Browse the repository at this point in the history
Define an experimental junit flavor for subresults.
  • Loading branch information
seberm committed Aug 29, 2024
1 parent 81b54df commit 83264f1
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 9 deletions.
38 changes: 29 additions & 9 deletions tmt/steps/report/junit.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import dataclasses
import functools
from collections.abc import Iterator
from typing import TYPE_CHECKING, Any, Optional, TypedDict, overload
from typing import TYPE_CHECKING, Any, Optional, TypedDict, Union, overload

from jinja2 import FileSystemLoader, select_autoescape

Expand Down Expand Up @@ -79,10 +79,29 @@ class PropertyDict(TypedDict):
name: str
value: str

def __init__(self, wrapped: tmt.Result) -> None:
def __init__(
self,
wrapped: Union[tmt.Result, tmt.result.SubResult],
subresults_context_class: 'type[ResultsContext]') -> None:
self._wrapped = wrapped
self._subresults_context_class = subresults_context_class
self._properties: dict[str, str] = {}

@property
def subresult(self) -> 'ResultsContext':
"""
Override the ``tmt.Result.subresult`` and wrap all the ``tmt.result.SubResult`` instances
into the ``ResultsContext``.
"""

# `tmt.result.SubResult.subresult` is not defined, just raise the AttributeError to silent
# the typing errors.
if isinstance(self._wrapped, tmt.result.SubResult):
raise AttributeError(
f"'{self._wrapped.__class__.__name__} object has no attribute 'subresult'")

return self._subresults_context_class(self._wrapped.subresult)

@property
def properties(self) -> list[PropertyDict]:
return [{'name': k, 'value': v} for k, v in self._properties.items()]
Expand All @@ -95,9 +114,10 @@ def __getattr__(self, name: str) -> Any:
return getattr(self._wrapped, name)


def _add_result_properties(result_instance: tmt.Result) -> ResultWrapper:
""" Dynamically decorate the ``tmt.Result`` instance - add ``properties`` attribute """
return ResultWrapper(result_instance)
def _wrap_result(result_instance: Union[tmt.Result, tmt.result.SubResult],
subresults_context_class: 'type[ResultsContext]') -> ResultWrapper:
""" Dynamically decorate the ``tmt.Result`` instance with more attributes """
return ResultWrapper(result_instance, subresults_context_class)


class ResultsContext:
Expand All @@ -106,9 +126,9 @@ class ResultsContext:
JUnit template. It wraps all the ``tmt.Result`` instances into the ``ResultWrapper``.
"""

def __init__(self, results: list[tmt.Result]) -> None:
# Decorate all the tmt.Results with more attributes
self._results: list[ResultWrapper] = [_add_result_properties(r) for r in results]
def __init__(self, results: Union[list[tmt.Result], list[tmt.result.SubResult]]) -> None:
""" Decorate/wrap all the ``tmt.Results`` with more attributes """
self._results: list[ResultWrapper] = [_wrap_result(r, self.__class__) for r in results]

def __iter__(self) -> Iterator[ResultWrapper]:
""" Possibility to iterate over results by iterating an instance """
Expand Down Expand Up @@ -306,7 +326,7 @@ class ReportJUnitData(tmt.steps.report.ReportStepData):
default=DEFAULT_FLAVOR_NAME,
option='--flavor',
metavar='FLAVOR',
choices=[DEFAULT_FLAVOR_NAME, CUSTOM_FLAVOR_NAME],
choices=[DEFAULT_FLAVOR_NAME, CUSTOM_FLAVOR_NAME, 'subresults'],
help=f"Name of a JUnit flavor to generate. By default, the '{DEFAULT_FLAVOR_NAME}' flavor "
"is used.")

Expand Down
78 changes: 78 additions & 0 deletions tmt/steps/report/junit/schemas/subresults.xsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" ?>

<!--
This schema extends the `default` flavor and adds support for TMT
subresults. It allows multiple occurrences of testsuite elements inside a
<testsuites>.
The <testsuite> is mapped to `tmt.Result` and the <testcase> is mapped to
`tmt.SubResult`.
-->

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">

<xs:element name="failure">
<xs:complexType mixed="true">
<xs:attribute name="type" type="xs:string" use="required"/>
<xs:attribute name="message" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>

<xs:element name="error">
<xs:complexType mixed="true">
<xs:attribute name="type" type="xs:string" use="required"/>
<xs:attribute name="message" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>

<xs:element name="skipped">
<xs:complexType mixed="true">
<xs:attribute name="type" type="xs:string" use="required"/>
<xs:attribute name="message" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
<xs:element name="system-err" type="xs:string"/>
<xs:element name="system-out" type="xs:string"/>

<xs:element name="testcase">
<xs:complexType>
<xs:sequence>
<xs:element ref="skipped" minOccurs="0" maxOccurs="1"/>
<xs:element ref="error" minOccurs="0" maxOccurs="1"/>
<xs:element ref="failure" minOccurs="0" maxOccurs="1"/>
<xs:element ref="system-out" minOccurs="0" maxOccurs="1"/>
<xs:element ref="system-err" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="time" type="xs:string" use="optional"/>
</xs:complexType>
</xs:element>

<xs:element name="testsuite">
<xs:complexType>
<xs:sequence>
<xs:element ref="testcase" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="tests" type="xs:string" use="required"/>
<xs:attribute name="failures" type="xs:string" use="required"/>
<xs:attribute name="errors" type="xs:string" use="required"/>
<xs:attribute name="disabled" type="xs:string" use="required"/>
<xs:attribute name="skipped" type="xs:string" use="required"/>
<xs:attribute name="time" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>

<xs:element name="testsuites">
<xs:complexType>
<xs:sequence>
<xs:element ref="testsuite" minOccurs="1" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="time" type="xs:string" use="required"/>
<xs:attribute name="tests" type="xs:string" use="optional"/>
<xs:attribute name="failures" type="xs:string" use="optional"/>
<xs:attribute name="disabled" type="xs:string" use="optional"/>
<xs:attribute name="errors" type="xs:string" use="optional"/>
</xs:complexType>
</xs:element>
</xs:schema>
40 changes: 40 additions & 0 deletions tmt/steps/report/junit/templates/subresults.xml.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{% extends "_base.xml.j2" %}

{% block content %}
<testsuites disabled="0" errors="{{ RESULTS.errored | length }}" failures="{{ RESULTS.failed | length }}" tests="{{ RESULTS | length }}" time="{{ RESULTS.duration }}">

{% block testsuites %}
{% for result in RESULTS %}

{# TODO: How to handle main result logs and duration? #}
{% set main_log = result.log | first | read_log %}
{% set main_log_failures = main_log | failures | e %}
{% set main_test_duration = result.duration | duration_to_seconds %}

<testsuite name="{{ result.name | trim | e }}" disabled="0" errors="{{ result.subresult.errored | length }}" failures="{{ result.subresult.failed | length }}" skipped="{{ result.subresult.skipped | length }}" tests="{{ result.subresult | length }}" time="{{ main_test_duration }}">

{% for subresult in result.subresult %}
{% set subresult_log = subresult.log | first | read_log %}
{% set subresult_log_failures = main_log | failures | e %}
{% set subresult_test_duration = subresult.duration | duration_to_seconds %}

<testcase name="{{ subresult.name | e }}" {% if subresult_test_duration %}time="{{ subresult_test_duration }}"{% endif %}>
{% if subresult.result.value == 'error' or subresult.result.value == 'warn' %}
<error type="error" message="{{ subresult.result.value | e }}">{{ subresult_log_failures }}</error>
{% elif subresult.result.value == 'fail' %}
<failure type="failure" message="{{ subresult.result.value | e }}">{{ subresult_log_failures }}</failure>
{% elif subresult.result.value == 'info' %}
<skipped type="skipped" message="{{ subresult.result.value | e }}">{{ subresult_log_failures }}</skipped>
{% endif %}

{% if INCLUDE_OUTPUT_LOG and subresult_log %}
<system-out>{{ subresult_log | e }}</system-out>
{% endif %}
</testcase>
{% endfor %}
</testsuite>
{% endfor %}
{% endblock %}

</testsuites>
{% endblock %}

0 comments on commit 83264f1

Please sign in to comment.