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)