rally/rally/plugins/common/verification/reporters.py

486 lines
20 KiB
Python

# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import collections
import datetime as dt
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 as ui_utils
from rally.verification import reporter
SKIP_RE = re.compile(r"Skipped until Bug: ?(?P<bug_number>\d+) is resolved.")
LP_BUG_LINK = "https://launchpad.net/bugs/%s"
TIME_FORMAT = consts.TimeFormat.ISO8601
@reporter.configure("json")
class JSONReporter(reporter.VerificationReporter):
"""Generates verification report in JSON format.
An example of the report (All dates, numbers, names appearing in this
example are fictitious. Any resemblance to real things is purely
coincidental):
.. code-block:: json
{"verifications": {
"verification-uuid-1": {
"status": "finished",
"skipped": 1,
"started_at": "2001-01-01T00:00:00",
"finished_at": "2001-01-01T00:05:00",
"tests_duration": 5,
"run_args": {
"pattern": "set=smoke",
"xfail_list": {"some.test.TestCase.test_xfail":
"Some reason why it is expected."},
"skip_list": {"some.test.TestCase.test_skipped":
"This test was skipped intentionally"},
},
"success": 1,
"expected_failures": 1,
"tests_count": 3,
"failures": 0,
"unexpected_success": 0
},
"verification-uuid-2": {
"status": "finished",
"skipped": 1,
"started_at": "2002-01-01T00:00:00",
"finished_at": "2002-01-01T00:05:00",
"tests_duration": 5,
"run_args": {
"pattern": "set=smoke",
"xfail_list": {"some.test.TestCase.test_xfail":
"Some reason why it is expected."},
"skip_list": {"some.test.TestCase.test_skipped":
"This test was skipped intentionally"},
},
"success": 1,
"expected_failures": 1,
"tests_count": 3,
"failures": 1,
"unexpected_success": 0
}
},
"tests": {
"some.test.TestCase.test_foo[tag1,tag2]": {
"name": "some.test.TestCase.test_foo",
"tags": ["tag1","tag2"],
"by_verification": {
"verification-uuid-1": {
"status": "success",
"duration": "1.111"
},
"verification-uuid-2": {
"status": "success",
"duration": "22.222"
}
}
},
"some.test.TestCase.test_skipped[tag1]": {
"name": "some.test.TestCase.test_skipped",
"tags": ["tag1"],
"by_verification": {
"verification-uuid-1": {
"status": "skipped",
"duration": "0",
"details": "Skipped until Bug: 666 is resolved."
},
"verification-uuid-2": {
"status": "skipped",
"duration": "0",
"details": "Skipped until Bug: 666 is resolved."
}
}
},
"some.test.TestCase.test_xfail": {
"name": "some.test.TestCase.test_xfail",
"tags": [],
"by_verification": {
"verification-uuid-1": {
"status": "xfail",
"duration": "3",
"details": "Some reason why it is expected.\\n\\n"
"Traceback (most recent call last): \\n"
" File "fake.py", line 13, in <module>\\n"
" yyy()\\n"
" File "fake.py", line 11, in yyy\\n"
" xxx()\\n"
" File "fake.py", line 8, in xxx\\n"
" bar()\\n"
" File "fake.py", line 5, in bar\\n"
" foo()\\n"
" File "fake.py", line 2, in foo\\n"
" raise Exception()\\n"
"Exception"
},
"verification-uuid-2": {
"status": "xfail",
"duration": "3",
"details": "Some reason why it is expected.\\n\\n"
"Traceback (most recent call last): \\n"
" File "fake.py", line 13, in <module>\\n"
" yyy()\\n"
" File "fake.py", line 11, in yyy\\n"
" xxx()\\n"
" File "fake.py", line 8, in xxx\\n"
" bar()\\n"
" File "fake.py", line 5, in bar\\n"
" foo()\\n"
" File "fake.py", line 2, in foo\\n"
" raise Exception()\\n"
"Exception"
}
}
},
"some.test.TestCase.test_failed": {
"name": "some.test.TestCase.test_failed",
"tags": [],
"by_verification": {
"verification-uuid-2": {
"status": "fail",
"duration": "4",
"details": "Some reason why it is expected.\\n\\n"
"Traceback (most recent call last): \\n"
" File "fake.py", line 13, in <module>\\n"
" yyy()\\n"
" File "fake.py", line 11, in yyy\\n"
" xxx()\\n"
" File "fake.py", line 8, in xxx\\n"
" bar()\\n"
" File "fake.py", line 5, in bar\\n"
" foo()\\n"
" File "fake.py", line 2, in foo\\n"
" raise Exception()\\n"
"Exception"
}
}
}
}
}
"""
@classmethod
def validate(cls, output_destination):
"""Validate destination of report.
:param output_destination: Destination of report
"""
# nothing to check :)
pass
def _generate(self):
"""Prepare raw report."""
verifications = collections.OrderedDict()
tests = {}
for v in self.verifications:
verifications[v.uuid] = {
"started_at": v.created_at.strftime(TIME_FORMAT),
"finished_at": v.updated_at.strftime(TIME_FORMAT),
"status": v.status,
"run_args": v.run_args,
"tests_count": v.tests_count,
"tests_duration": v.tests_duration,
"skipped": v.skipped,
"success": v.success,
"expected_failures": v.expected_failures,
"unexpected_success": v.unexpected_success,
"failures": v.failures,
}
for test_id, result in v.tests.items():
if test_id not in tests:
# NOTE(ylobankov): It is more convenient to see test ID
# at the first place in the report.
tags = sorted(result.get("tags", []), reverse=True,
key=lambda tag: tag.startswith("id-"))
tests[test_id] = {"tags": tags,
"name": result["name"],
"by_verification": {}}
tests[test_id]["by_verification"][v.uuid] = {
"status": result["status"],
"duration": result["duration"]
}
reason = result.get("reason", "")
if reason:
match = SKIP_RE.match(reason)
if match:
link = LP_BUG_LINK % match.group("bug_number")
reason = re.sub(match.group("bug_number"), link,
reason)
traceback = result.get("traceback", "")
sep = "\n\n" if reason and traceback else ""
d = (reason + sep + traceback.strip()) or None
if d:
tests[test_id]["by_verification"][v.uuid]["details"] = d
return {"verifications": verifications, "tests": tests}
def generate(self):
raw_report = json.dumps(self._generate(), indent=4)
if self.output_destination:
return {"files": {self.output_destination: raw_report},
"open": self.output_destination}
else:
return {"print": raw_report}
@reporter.configure("html")
class HTMLReporter(JSONReporter):
"""Generates verification report in HTML format."""
INCLUDE_LIBS = False
# "T" separator of ISO 8601 is not user-friendly enough.
TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
def generate(self):
report = self._generate()
uuids = report["verifications"].keys()
show_comparison_note = False
for test in report["tests"].values():
# make as much as possible processing here to reduce processing
# at JS side
test["has_details"] = False
for test_info in test["by_verification"].values():
if "details" not in test_info:
test_info["details"] = None
elif not test["has_details"]:
test["has_details"] = True
durations = []
# iter by uuids to store right order for comparison
for uuid in uuids:
if uuid in test["by_verification"]:
durations.append(test["by_verification"][uuid]["duration"])
if float(durations[-1]) < 0.001:
durations[-1] = "0"
# not to display such little duration in the report
test["by_verification"][uuid]["duration"] = ""
if len(durations) > 1 and not (
durations[0] == "0" and durations[-1] == "0"):
# compare result with result of the first verification
diff = float(durations[-1]) - float(durations[0])
result = "%s (" % durations[-1]
if diff >= 0:
result += "+"
result += "%s)" % diff
test["by_verification"][uuid]["duration"] = result
if not show_comparison_note and len(durations) > 2:
# NOTE(andreykurilin): only in case of comparison of more
# than 2 results of the same test we should display a note
# about the comparison strategy
show_comparison_note = True
template = ui_utils.get_template("verification/report.html")
context = {"uuids": list(uuids),
"verifications": report["verifications"],
"tests": report["tests"],
"show_comparison_note": show_comparison_note}
raw_report = template.render(data=json.dumps(context),
include_libs=self.INCLUDE_LIBS)
# in future we will support html_static and will need to save more
# files
if self.output_destination:
return {"files": {self.output_destination: raw_report},
"open": self.output_destination}
else:
return {"print": raw_report}
@reporter.configure("html-static")
class HTMLStaticReporter(HTMLReporter):
"""Generates verification report in HTML format with embedded JS/CSS."""
INCLUDE_LIBS = True
@reporter.configure("junit-xml")
class JUnitXMLReporter(reporter.VerificationReporter):
"""Generates verification report in JUnit-XML format.
An example of the report (All dates, numbers, names appearing in this
example are fictitious. Any resemblance to real things is purely
coincidental):
.. code-block:: xml
<testsuites>
<!--Report is generated by Rally 0.8.0 at 2002-01-01T00:00:00-->
<testsuite id="verification-uuid-1"
tests="9"
time="1.111"
errors="0"
failures="3"
skipped="0"
timestamp="2001-01-01T00:00:00">
<testcase classname="some.test.TestCase"
name="test_foo"
time="8"
timestamp="2001-01-01T00:01:00" />
<testcase classname="some.test.TestCase"
name="test_skipped"
time="0"
timestamp="2001-01-01T00:02:00">
<skipped>Skipped until Bug: 666 is resolved.</skipped>
</testcase>
<testcase classname="some.test.TestCase"
name="test_xfail"
time="3"
timestamp="2001-01-01T00:03:00">
<!--It is an expected failure due to: something-->
<!--Traceback:
HEEELP-->
</testcase>
<testcase classname="some.test.TestCase"
name="test_uxsuccess"
time="3"
timestamp="2001-01-01T00:04:00">
<failure>
It is an unexpected success. The test should fail due to:
It should fail, I said!
</failure>
</testcase>
</testsuite>
<testsuite id="verification-uuid-2"
tests="99"
time="22.222"
errors="0"
failures="33"
skipped="0"
timestamp="2002-01-01T00:00:00">
<testcase classname="some.test.TestCase"
name="test_foo"
time="8"
timestamp="2001-02-01T00:01:00" />
<testcase classname="some.test.TestCase"
name="test_failed"
time="8"
timestamp="2001-02-01T00:02:00">
<failure>HEEEEEEELP</failure>
</testcase>
<testcase classname="some.test.TestCase"
name="test_skipped"
time="0"
timestamp="2001-02-01T00:03:00">
<skipped>Skipped until Bug: 666 is resolved.</skipped>
</testcase>
<testcase classname="some.test.TestCase"
name="test_xfail"
time="4"
timestamp="2001-02-01T00:04:00">
<!--It is an expected failure due to: something-->
<!--Traceback:
HEEELP-->
</testcase>
</testsuite>
</testsuites>
"""
@classmethod
def validate(cls, output_destination):
pass
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(TIME_FORMAT))))
for v in self.verifications:
verification = ET.SubElement(root, "testsuite", {
"id": v.uuid,
"time": str(v.tests_duration),
"tests": str(v.tests_count),
"errors": "0",
"skipped": str(v.skipped),
"failures": str(v.failures + v.unexpected_success),
"timestamp": v.created_at.strftime(TIME_FORMAT)
})
tests = sorted(v.tests.values(),
key=lambda t: (t.get("timestamp", ""), t["name"]))
for result in tests:
class_name, name = result["name"].rsplit(".", 1)
test_case = {
"time": result["duration"],
"name": name, "classname": class_name
}
test_id = [tag[3:] for tag in result.get("tags", [])
if tag.startswith("id-")]
if test_id:
test_case["id"] = test_id[0]
if "timestamp" in result:
test_case["timestamp"] = result["timestamp"]
test_case_element = ET.SubElement(verification, "testcase",
test_case)
if result["status"] == "success":
# nothing to add
pass
elif result["status"] == "uxsuccess":
# NOTE(andreykurilin): junit doesn't support uxsuccess
# status, so let's display it like "fail" with proper
# comment.
failure = ET.SubElement(test_case_element, "failure")
failure.text = ("It is an unexpected success. The test "
"should fail due to: %s" %
result.get("reason", "Unknown reason"))
elif result["status"] == "fail":
failure = ET.SubElement(test_case_element, "failure")
failure.text = result.get("traceback", None)
elif result["status"] == "xfail":
# NOTE(andreykurilin): junit doesn't support xfail status,
# so let's display it like "success" with proper comment
test_case_element.append(ET.Comment(
"It is an expected failure due to: %s" %
result.get("reason", "Unknown reason")))
trace = result.get("traceback", None)
if trace:
test_case_element.append(ET.Comment(
"Traceback:\n%s" % trace))
elif result["status"] == "skip":
skipped = ET.SubElement(test_case_element, "skipped")
skipped.text = result.get("reason", "Unknown reason")
else:
# wtf is it?! we should add validation of results...
pass
utils.prettify_xml(root)
raw_report = ET.tostring(root, encoding="utf-8").decode("utf-8")
if self.output_destination:
return {"files": {self.output_destination: raw_report},
"open": self.output_destination}
else:
return {"print": raw_report}