Skip to content

Commit

Permalink
Rework inner code for JUnit reports
Browse files Browse the repository at this point in the history
Now we have unified code for both task exporter and verification
reporter. Also, difference in attributes order between python versions
are fixed.

Covers https://bugs.python.org/issue34160

Change-Id: I9a86e995d2ecb78a3ec9a69eaa5815296fcb4a6e
  • Loading branch information
andreykurilin committed Mar 3, 2020
1 parent 74a2f78 commit 384fbf4
Show file tree
Hide file tree
Showing 10 changed files with 235 additions and 207 deletions.
195 changes: 144 additions & 51 deletions rally/common/io/junit.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# Copyright 2015: eNovance
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
Expand All @@ -13,56 +12,150 @@
# License for the specific language governing permissions and limitations
# under the License.

import collections
import datetime as dt
import xml.etree.ElementTree as ET

from rally.common import version

class JUnit(object):
SUCCESS = "success"
FAILURE = "failure"
ERROR = "error"

def __init__(self, test_suite_name):
self.test_suite_name = test_suite_name
self.test_cases = []
self.n_tests = 0
self.n_failures = 0
self.n_errors = 0
self.total_time = 0.0

def add_test(self, test_name, time, outcome=SUCCESS, message=""):
class_name, name = test_name.split(".", 1)
self.test_cases.append({
"classname": class_name,
"name": name,
"time": str("%.2f" % time),
"outcome": outcome,
"message": message
})

if outcome == JUnit.FAILURE:
self.n_failures += 1
elif outcome == JUnit.ERROR:
self.n_errors += 1
elif outcome != JUnit.SUCCESS:
raise ValueError("Unexpected outcome %s" % outcome)

self.n_tests += 1
self.total_time += time

def to_xml(self):
xml = ET.Element("testsuite", {
"name": self.test_suite_name,
"tests": str(self.n_tests),
"time": str("%.2f" % self.total_time),
"failures": str(self.n_failures),
"errors": str(self.n_errors),
})
for test_case in self.test_cases:
outcome = test_case.pop("outcome")
message = test_case.pop("message")
if outcome in [JUnit.FAILURE, JUnit.ERROR]:
sub = ET.SubElement(xml, "testcase", test_case)
sub.append(ET.Element(outcome, {"message": message}))
else:
xml.append(ET.Element("testcase", test_case))
return ET.tostring(xml, encoding="utf-8").decode("utf-8")

def _prettify_xml(elem, level=0):
"""Adds indents.
Code of this method was copied from
http://effbot.org/zone/element-lib.htm#prettyprint
"""
i = "\n" + level * " "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
_prettify_xml(elem, level + 1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i


def _filter_attrs(**attrs):
return collections.OrderedDict(
(k, v) for k, v in sorted(attrs.items()) if v is not None)


class _TestCase(object):
def __init__(self, parent, classname, name, id=None, time=None,
timestamp=None):
self._parent = parent
attrs = _filter_attrs(id=id, time=time, classname=classname,
name=name, timestamp=timestamp)
self._elem = ET.SubElement(self._parent._elem, "testcase", **attrs)

def _add_details(self, tag=None, text=None, *comments):
if tag:
elem = ET.SubElement(self._elem, tag)
if text:
elem.text = text
for comment in comments:
if comment:
self._elem.append(ET.Comment(comment))

def mark_as_failed(self, details):
self._add_details("failure", details)
self._parent._increment("failures")

def mark_as_uxsuccess(self, reason=None):
# NOTE(andreykurilin): junit doesn't support uxsuccess
# status, so let's display it like "fail" with proper comment.
self.mark_as_failed(
f"It is an unexpected success. The test "
f"should fail due to: {reason or 'Unknown reason'}"
)

def mark_as_xfail(self, reason=None, details=None):
reason = (f"It is an expected failure due to: "
f"{reason or 'Unknown reason'}")
self._add_details(None, None, reason, details)

def mark_as_skipped(self, reason):
self._add_details("skipped", reason or "Unknown reason")
self._parent._increment("skipped")


class _TestSuite(object):
def __init__(self, parent, id, time, timestamp):
self._parent = parent
attrs = _filter_attrs(id=id, time=time, tests="0",
errors="0", skipped="0",
failures="0", timestamp=timestamp)
self._elem = ET.SubElement(self._parent, "testsuite", **attrs)

self._finalized = False
self._calculate = True
self._total = 0
self._skipped = 0
self._failures = 0

def _finalize(self):
if not self._finalized and self._calculate:
self._setup_final_stats(tests=str(self._total),
skipped=str(self._skipped),
failures=str(self._failures))
self._finalized = True

def _setup_final_stats(self, tests, skipped, failures):
self._elem.set("tests", tests)
self._elem.set("skipped", skipped)
self._elem.set("failures", failures)

def setup_final_stats(self, tests, skipped, failures):
"""Turn off calculation of final stats."""
self._calculate = False
self._setup_final_stats(tests, skipped, failures)

def _increment(self, status):
if self._calculate:
key = f"_{status}"
value = getattr(self, key) + 1
setattr(self, key, value)
self._finalized = False

def add_test_case(self, classname, name, id=None, time=None,
timestamp=None):
self._increment("total")
return _TestCase(self, id=id, classname=classname, name=name,
time=time, timestamp=timestamp)


class JUnitXML(object):
"""A helper class to build JUnit-XML report without knowing XML."""

def __init__(self):
self._root = ET.Element("testsuites")
self._test_suites = []

self._root.append(
ET.Comment("Report is generated by Rally %s at %s" % (
version.version_string(),
dt.datetime.utcnow().isoformat()))
)

def __str__(self):
return self.to_string()

def to_string(self):
for test_suite in self._test_suites:
test_suite._finalize()

_prettify_xml(self._root)

return ET.tostring(self._root, encoding="utf-8").decode("utf-8")

def add_test_suite(self, id, time, timestamp):
test_suite = _TestSuite(
self._root, id=id, time=time, timestamp=timestamp)
self._test_suites.append(test_suite)
return test_suite
22 changes: 3 additions & 19 deletions rally/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

from six import moves

from rally.common.io import junit
from rally.common import logging
from rally import exceptions

Expand Down Expand Up @@ -786,23 +787,6 @@ def __del__(self):
shutil.rmtree(path)


@logging.log_deprecated("it was an inner helper.", rally_version="3.0.0")
def prettify_xml(elem, level=0):
"""Adds indents.
Code of this method was copied from
http://effbot.org/zone/element-lib.htm#prettyprint
"""
i = "\n" + level * " "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
prettify_xml(elem, level + 1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
return junit._prettify_xml(elem, level=level)
61 changes: 19 additions & 42 deletions rally/plugins/common/exporters/junit.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@
import datetime as dt
import itertools
import os
import xml.etree.ElementTree as ET

from rally.common import utils
from rally.common import version
from rally import consts
from rally.common.io import junit
from rally.task import exporter


Expand Down Expand Up @@ -59,57 +56,37 @@ class JUnitXMLExporter(exporter.TaskExporter):
"""

def generate(self):
root = ET.Element("testsuites")
root.append(ET.Comment("Report is generated by Rally %s at %s" % (
version.version_string(),
dt.datetime.utcnow().strftime(consts.TimeFormat.ISO8601))))
root = junit.JUnitXML()

for t in self.tasks_results:
created_at = dt.datetime.strptime(t["created_at"],
"%Y-%m-%dT%H:%M:%S")
updated_at = dt.datetime.strptime(t["updated_at"],
"%Y-%m-%dT%H:%M:%S")
task = {
"id": t["uuid"],
"tests": 0,
"errors": "0",
"skipped": "0",
"failures": 0,
"time": "%.2f" % (updated_at - created_at).total_seconds(),
"timestamp": t["created_at"],
}
test_cases = []
test_suite = root.add_test_suite(
id=t["uuid"],
time="%.2f" % (updated_at - created_at).total_seconds(),
timestamp=t["created_at"]
)
for workload in itertools.chain(
*[s["workloads"] for s in t["subtasks"]]):
class_name, name = workload["name"].split(".", 1)
test_case = {
"id": workload["uuid"],
"time": "%.2f" % workload["full_duration"],
"name": name,
"classname": class_name,
"timestamp": workload["created_at"]
}
test_case = test_suite.add_test_case(
id=workload["uuid"],
time="%.2f" % workload["full_duration"],
classname=class_name,
name=name,
timestamp=workload["created_at"]
)
if not workload["pass_sla"]:
task["failures"] += 1
test_case["failure"] = "\n".join(
details = "\n".join(
[s["detail"]
for s in workload["sla_results"]["sla"]
if not s["success"]])
test_cases.append(test_case)
if not s["success"]]
)
test_case.mark_as_failed(details)

task["tests"] = str(len(test_cases))
task["failures"] = str(task["failures"])

testsuite = ET.SubElement(root, "testsuite", task)
for test_case in test_cases:
failure = test_case.pop("failure", None)
test_case = ET.SubElement(testsuite, "testcase", test_case)
if failure:
ET.SubElement(test_case, "failure").text = failure

utils.prettify_xml(root)

raw_report = ET.tostring(root, encoding="utf-8").decode("utf-8")
raw_report = root.to_string()

if self.output_destination:
return {"files": {self.output_destination: raw_report},
Expand Down

0 comments on commit 384fbf4

Please sign in to comment.