diff --git a/rally/common/utils.py b/rally/common/utils.py
index 5e9a2fe10b..8146e43855 100644
--- a/rally/common/utils.py
+++ b/rally/common/utils.py
@@ -827,3 +827,25 @@ class BackupHelper(object):
if os.path.exists(path):
LOG.debug("Deleting %s" % path)
shutil.rmtree(path)
+
+
+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
diff --git a/rally/plugins/common/exporters/junit.py b/rally/plugins/common/exporters/junit.py
index f46d67dad7..ff85cc8791 100644
--- a/rally/plugins/common/exporters/junit.py
+++ b/rally/plugins/common/exporters/junit.py
@@ -12,10 +12,14 @@
# License for the specific language governing permissions and limitations
# under the License.
+import datetime as dt
import itertools
import os
+import xml.etree.ElementTree as ET
-from rally.common.io import junit
+from rally.common import utils
+from rally.common import version
+from rally import consts
from rally.task import exporter
@@ -29,39 +33,87 @@ class JUnitXMLExporter(exporter.TaskExporter):
.. code-block:: xml
-
-
-
+
+
+
+
+
+ ooops
+
+
+
"""
def generate(self):
- test_suite = junit.JUnit("Rally test suite")
- for task in self.tasks_results:
+ 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))))
+
+ 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 = []
for workload in itertools.chain(
- *[s["workloads"] for s in task["subtasks"]]):
- w_sla = workload["sla_results"].get("sla", [])
+ *[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"]
+ }
+ if not workload["pass_sla"]:
+ task["failures"] += 1
+ test_case["failure"] = "\n".join(
+ [s["detail"]
+ for s in workload["sla_results"]["sla"]
+ if not s["success"]])
+ test_cases.append(test_case)
- message = ",".join([sla["detail"] for sla in w_sla
- if not sla["success"]])
- if message:
- outcome = junit.JUnit.FAILURE
- else:
- outcome = junit.JUnit.SUCCESS
- test_suite.add_test(workload["name"],
- workload["full_duration"], outcome,
- message)
+ task["tests"] = str(len(test_cases))
+ task["failures"] = str(task["failures"])
- result = test_suite.to_xml()
+ 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")
if self.output_destination:
- return {"files": {self.output_destination: result},
+ return {"files": {self.output_destination: raw_report},
"open": "file://" + os.path.abspath(
self.output_destination)}
else:
- return {"print": result}
+ return {"print": raw_report}
diff --git a/rally/plugins/common/verification/reporters.py b/rally/plugins/common/verification/reporters.py
index 2018eac851..d6e6da04dd 100644
--- a/rally/plugins/common/verification/reporters.py
+++ b/rally/plugins/common/verification/reporters.py
@@ -18,9 +18,10 @@ import json
import re
import xml.etree.ElementTree as ET
+from rally.common import utils
from rally.common import version
from rally import consts
-from rally.ui import utils
+from rally.ui import utils as ui_utils
from rally.verification import reporter
@@ -297,7 +298,7 @@ class HTMLReporter(JSONReporter):
# about the comparison strategy
show_comparison_note = True
- template = utils.get_template("verification/report.html")
+ template = ui_utils.get_template("verification/report.html")
context = {"uuids": uuids,
"verifications": report["verifications"],
"tests": report["tests"],
@@ -408,27 +409,6 @@ class JUnitXMLReporter(reporter.VerificationReporter):
def validate(cls, output_destination):
pass
- def _prettify_xml(self, 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:
- self._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 generate(self):
root = ET.Element("testsuites")
@@ -495,7 +475,7 @@ class JUnitXMLReporter(reporter.VerificationReporter):
# wtf is it?! we should add validation of results...
pass
- self._prettify_xml(root)
+ utils.prettify_xml(root)
raw_report = ET.tostring(root, encoding="utf-8").decode("utf-8")
if self.output_destination:
diff --git a/tests/unit/plugins/common/exporters/junit_report.xml b/tests/unit/plugins/common/exporters/junit_report.xml
new file mode 100644
index 0000000000..c039f42ac9
--- /dev/null
+++ b/tests/unit/plugins/common/exporters/junit_report.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+ ooops
+
+
+
diff --git a/tests/unit/plugins/common/exporters/test_junit.py b/tests/unit/plugins/common/exporters/test_junit.py
index 39c0647ada..bb669d5fa7 100644
--- a/tests/unit/plugins/common/exporters/test_junit.py
+++ b/tests/unit/plugins/common/exporters/test_junit.py
@@ -12,70 +12,82 @@
# License for the specific language governing permissions and limitations
# under the License.
+import datetime as dt
import os
+import mock
+
from rally.plugins.common.exporters import junit
from tests.unit import test
def get_tasks_results():
task_id = "2fa4f5ff-7d23-4bb0-9b1f-8ee235f7f1c8"
- workload = {"created_at": "2017-06-04T05:14:44",
- "updated_at": "2017-06-04T05:15:14",
- "task_uuid": task_id,
- "position": 0,
- "name": "CinderVolumes.list_volumes",
- "description": "List all volumes.",
- "data": {"raw": []},
- "full_duration": 29.969523191452026,
- "sla": {},
- "sla_results": {"sla": []},
- "load_duration": 2.03029203414917,
- "hooks": [],
- "id": 3}
- task = {"subtasks": [
- {"task_uuid": task_id,
- "workloads": [workload]}]}
- return [task]
+ return [{
+ "uuid": "task-uu-ii-dd",
+ "created_at": "2017-06-04T05:14:00",
+ "updated_at": "2017-06-04T05:15:15",
+ "subtasks": [
+ {"task_uuid": task_id,
+ "workloads": [
+ {
+ "uuid": "workload-1-uuid",
+ "created_at": "2017-06-04T05:14:44",
+ "updated_at": "2017-06-04T05:15:14",
+ "task_uuid": task_id,
+ "position": 0,
+ "name": "CinderVolumes.list_volumes",
+ "full_duration": 29.969523191452026,
+ "sla_results": {"sla": []},
+ "pass_sla": True
+ },
+ {
+ "uuid": "workload-2-uuid",
+ "created_at": "2017-06-04T05:15:15",
+ "updated_at": "2017-06-04T05:16:14",
+ "task_uuid": task_id,
+ "position": 1,
+ "name": "NovaServers.list_keypairs",
+ "full_duration": 5,
+ "sla_results": {"sla": [
+ {"criterion": "Failing",
+ "success": False,
+ "detail": "ooops"},
+ {"criterion": "Ok",
+ "success": True,
+ "detail": None},
+ ]},
+ "pass_sla": False
+ },
+ ]}]}]
class JUnitXMLExporterTestCase(test.TestCase):
+ def setUp(self):
+ super(JUnitXMLExporterTestCase, self).setUp()
+ self.datetime = dt.datetime
- def test_generate(self):
- content = (""
- ""
- "")
+ patcher = mock.patch("rally.plugins.common.exporters.junit.dt")
+ self.dt = patcher.start()
+ self.dt.datetime.strptime.side_effect = self.datetime.strptime
+ self.addCleanup(patcher.stop)
+
+ @mock.patch("rally.plugins.common.exporters.junit.version.version_string")
+ def test_generate(self, mock_version_string):
+ now = self.dt.datetime.utcnow.return_value
+ now.strftime.return_value = "$TIME"
+ mock_version_string.return_value = "$VERSION"
+
+ with open(os.path.join(os.path.dirname(__file__),
+ "junit_report.xml")) as f:
+ expected_report = f.read()
reporter = junit.JUnitXMLExporter(get_tasks_results(),
output_destination=None)
- self.assertEqual({"print": content}, reporter.generate())
+ self.assertEqual({"print": expected_report}, reporter.generate())
reporter = junit.JUnitXMLExporter(get_tasks_results(),
output_destination="path")
- self.assertEqual({"files": {"path": content},
+ self.assertEqual({"files": {"path": expected_report},
"open": "file://" + os.path.abspath("path")},
reporter.generate())
-
- def test_generate_fail(self):
- tasks_results = get_tasks_results()
- tasks_results[0]["subtasks"][0]["workloads"][0]["sla_results"] = {
- "sla": [{"success": False, "detail": "error"}]}
- content = (""
- ""
- ""
- "")
- reporter = junit.JUnitXMLExporter(tasks_results,
- output_destination=None)
- self.assertEqual({"print": content}, reporter.generate())
diff --git a/tests/unit/plugins/common/verification/test_reporters.py b/tests/unit/plugins/common/verification/test_reporters.py
index 68cdb37934..8c4a2f86e1 100644
--- a/tests/unit/plugins/common/verification/test_reporters.py
+++ b/tests/unit/plugins/common/verification/test_reporters.py
@@ -287,13 +287,13 @@ class JSONReporterTestCase(test.TestCase):
@ddt.ddt
class HTMLReporterTestCase(test.TestCase):
- @mock.patch("%s.utils" % PATH)
+ @mock.patch("%s.ui_utils" % PATH)
@mock.patch("%s.json.dumps" % PATH)
@ddt.data((reporters.HTMLReporter, False),
(reporters.HTMLStaticReporter, True))
@ddt.unpack
- def test_generate(self, cls, include_libs, mock_dumps, mock_utils):
- mock_render = mock_utils.get_template.return_value.render
+ def test_generate(self, cls, include_libs, mock_dumps, mock_ui_utils):
+ mock_render = mock_ui_utils.get_template.return_value.render
reporter = cls(get_verifications(), None)
@@ -301,7 +301,7 @@ class HTMLReporterTestCase(test.TestCase):
reporter.generate())
mock_render.assert_called_once_with(data=mock_dumps.return_value,
include_libs=include_libs)
- mock_utils.get_template.assert_called_once_with(
+ mock_ui_utils.get_template.assert_called_once_with(
"verification/report.html")
self.assertEqual(1, mock_dumps.call_count)