task report: generate a JUnit report

This allows users to feed reports to tools such as Jenkins.

Change-Id: I55f5fa282af8a8130be5c07bcdf8030fc591f6b7
Implements: blueprint task-report-junit
This commit is contained in:
Cyril Roelandt 2015-04-08 13:57:04 +00:00
parent dd13801de9
commit fb6333a226
6 changed files with 168 additions and 11 deletions

View File

@ -29,7 +29,7 @@ _rally()
OPTS["task_delete"]="--force --uuid"
OPTS["task_detailed"]="--uuid --iterations-data"
OPTS["task_list"]="--deployment --all-deployments --status --uuids-only"
OPTS["task_report"]="--tasks --out --open"
OPTS["task_report"]="--tasks --out --open --html --junit"
OPTS["task_results"]="--uuid"
OPTS["task_sla_check"]="--uuid --json"
OPTS["task_start"]="--deployment --task --task-args --task-args-file --tag --no-use --abort-on-sla-failure"
@ -78,4 +78,4 @@ _rally()
fi
return 0
}
complete -F _rally rally
complete -F _rally rally

View File

@ -32,6 +32,7 @@ from rally.cli import cliutils
from rally.cli import envutils
from rally.common import fileutils
from rally.common.i18n import _
from rally.common import junit
from rally.common import log as logging
from rally.common import utils as rutils
from rally import consts
@ -414,6 +415,10 @@ class TaskCommands(object):
print(_("* To plot HTML graphics with this data, run:"))
print("\trally task report %s --out output.html" % task["uuid"])
print()
print(_("* To generate a JUnit report, run:"))
print("\trally task report %s --junit --out output.xml" %
task["uuid"])
print()
print(_("* To get raw JSON output of task results, run:"))
print("\trally task results %s\n" % task["uuid"])
@ -511,18 +516,25 @@ class TaskCommands(object):
help="Path to output file.")
@cliutils.args("--open", dest="open_it", action="store_true",
help="Open it in browser.")
@cliutils.args("--html", dest="out_format",
action="store_const", const="html",
help="Generate the report in HTML.")
@cliutils.args("--junit", dest="out_format",
action="store_const", const="junit",
help="Generate the report in the JUnit format.")
@cliutils.deprecated_args(
"--uuid", dest="tasks", nargs="+",
help="uuids of tasks or json files with task results")
@envutils.default_from_global("tasks", envutils.ENV_TASK, "--uuid")
@cliutils.suppress_warnings
def report(self, tasks=None, out=None, open_it=False):
"""Generate HTML report file for specified task.
def report(self, tasks=None, out=None, open_it=False, out_format="html"):
"""Generate report file for specified task.
:param task_id: UUID, task identifier
:param tasks: list, UUIDs od tasks or pathes files with tasks results
:param out: str, output html file name
:param out: str, output file name
:param open_it: bool, whether to open output file in web browser
:param out_format: output format (junit or html)
"""
tasks = isinstance(tasks, list) and tasks or [tasks]
@ -572,11 +584,29 @@ class TaskCommands(object):
results.append(task_result)
output_file = os.path.expanduser(out)
with open(output_file, "w+") as f:
f.write(plot.plot(results))
if open_it:
webbrowser.open_new_tab("file://" + os.path.realpath(out))
if out_format == "html":
with open(output_file, "w+") as f:
f.write(plot.plot(results))
if open_it:
webbrowser.open_new_tab("file://" + os.path.realpath(out))
elif out_format == "junit":
test_suite = junit.JUnit("Rally test suite")
for result in results:
if (isinstance(result["sla"], list) and
not all([sla["success"] for sla in result["sla"]])):
outcome = junit.JUnit.FAILURE
else:
outcome = junit.JUnit.SUCCESS
test_suite.add_test(result["key"]["name"],
result["full_duration"],
outcome=outcome)
with open(output_file, "w+") as f:
f.write(test_suite.to_xml())
else:
print(_("Invalid output format: %s") % out_format,
file=sys.stderr)
return 1
@cliutils.args("--force", action="store_true", help="force delete")
@cliutils.args("--uuid", type=str, dest="task_id", nargs="*",

60
rally/common/junit.py Normal file
View File

@ -0,0 +1,60 @@
# Copyright 2015: eNovance
# 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 xml.etree.ElementTree as ET
class JUnit(object):
SUCCESS = 0
FAILURE = 1
ERROR = 2
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):
class_name, name = test_name.split(".", 1)
self.test_cases.append({
"classname": class_name,
"name": name,
"time": str("%.2f" % time),
})
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:
xml.append(ET.Element("testcase", test_case))
return ET.tostring(xml, encoding="utf-8").decode("utf-8")

View File

@ -138,6 +138,12 @@ class TaskTestCase(unittest.TestCase):
rally.gen_report_path(extension="html")))
self.assertRaises(utils.RallyCliError,
rally, "task report --report %s" % FAKE_TASK_UUID)
rally("task report --junit --out %s" %
rally.gen_report_path(extension="junit"))
self.assertTrue(os.path.exists(
rally.gen_report_path(extension="junit")))
self.assertRaises(utils.RallyCliError,
rally, "task report --report %s" % FAKE_TASK_UUID)
def test_report_bunch_uuids(self):
rally = utils.Rally()

View File

@ -330,11 +330,11 @@ class TaskCommandsTestCase(test.TestCase):
mock_os, mock_validate):
task_id = "eb290c30-38d8-4c8f-bbcc-fc8f74b004ae"
data = [
{"key": {"name": "test", "pos": 0},
{"key": {"name": "class.test", "pos": 0},
"data": {"raw": "foo_raw", "sla": "foo_sla",
"load_duration": 0.1,
"full_duration": 1.2}},
{"key": {"name": "test", "pos": 0},
{"key": {"name": "class.test", "pos": 0},
"data": {"raw": "bar_raw", "sla": "bar_sla",
"load_duration": 2.1,
"full_duration": 2.2}}]
@ -359,6 +359,11 @@ class TaskCommandsTestCase(test.TestCase):
mock_open.side_effect().write.assert_called_once_with("html_report")
mock_get.assert_called_once_with(task_id)
reset_mocks()
self.task.report(tasks=task_id, out="/tmp/%s.html" % task_id,
out_format="junit")
mock_open.assert_called_once_with("/tmp/%s.html" % task_id, "w+")
reset_mocks()
self.task.report(task_id, out="spam.html", open_it=True)
mock_web.open_new_tab.assert_called_once_with(
@ -483,6 +488,18 @@ class TaskCommandsTestCase(test.TestCase):
out="/tmp/tmp.hsml")
self.assertEqual(ret, 1)
@mock.patch("rally.cli.commands.task.sys.stderr")
@mock.patch("rally.cli.commands.task.os.path.exists", return_value=True)
@mock.patch("rally.cli.commands.task.json.load")
@mock.patch("rally.cli.commands.task.open", create=True)
def test_report_invalid_format(self, mock_open, mock_json_load,
mock_path_exists, mock_stderr):
result = self.task.report(tasks="/tmp/task.json", out="/tmp/tmp.html",
out_format="invalid")
self.assertEqual(1, result)
expected_out = "Invalid output format: invalid"
mock_stderr.write.assert_has_calls([mock.call(expected_out)])
@mock.patch("rally.cli.commands.task.cliutils.print_list")
@mock.patch("rally.cli.commands.task.envutils.get_global",
return_value="123456789")

View File

@ -0,0 +1,44 @@
# Copyright 2015: eNovance
# 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.
from rally.common import junit
from tests.unit import test
class JUnitTestCase(test.TestCase):
def test_basic_testsuite(self):
j = junit.JUnit("test")
j.add_test("Foo.Bar", 3.14)
j.add_test("Foo.Baz", 13.37, outcome=junit.JUnit.FAILURE)
j.add_test("Eggs.Spam", 42.00, outcome=junit.JUnit.ERROR)
expected = """
<testsuite errors="1" failures="1" name="test" tests="3" time="58.51">
<testcase classname="Foo" name="Bar" time="3.14" />
<testcase classname="Foo" name="Baz" time="13.37" />
<testcase classname="Eggs" name="Spam" time="42.00" />
</testsuite>"""
self.assertEqual(expected.replace("\n", ""), j.to_xml())
def test_empty_testsuite(self):
j = junit.JUnit("test")
expected = """
<testsuite errors="0" failures="0" name="test" tests="0" time="0.00" />"""
self.assertEqual(expected.replace("\n", ""), j.to_xml())
def test_invalid_outcome(self):
j = junit.JUnit("test")
self.assertRaises(ValueError, j.add_test, "Foo.Bar", 1.23,
outcome=1024)