|
|
|
# Copyright 2015 gRPC authors.
|
|
|
|
#
|
|
|
|
# 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.
|
|
|
|
"""Generate XML and HTML test reports."""
|
|
|
|
|
|
|
|
try:
|
|
|
|
from mako import exceptions
|
|
|
|
from mako.runtime import Context
|
|
|
|
from mako.template import Template
|
|
|
|
except ImportError:
|
|
|
|
pass # Mako not installed but it is ok.
|
|
|
|
import datetime
|
|
|
|
import os
|
|
|
|
import string
|
|
|
|
import xml.etree.cElementTree as ET
|
|
|
|
|
|
|
|
import six
|
|
|
|
|
|
|
|
|
|
|
|
def _filter_msg(msg, output_format):
|
|
|
|
"""Filters out nonprintable and illegal characters from the message."""
|
|
|
|
if output_format in ["XML", "HTML"]:
|
|
|
|
if isinstance(msg, bytes):
|
|
|
|
decoded_msg = msg.decode("UTF-8", "ignore")
|
|
|
|
else:
|
|
|
|
decoded_msg = msg
|
|
|
|
# keep whitespaces but remove formfeed and vertical tab characters
|
|
|
|
# that make XML report unparsable.
|
|
|
|
filtered_msg = "".join(
|
|
|
|
filter(
|
|
|
|
lambda x: x in string.printable and x != "\f" and x != "\v",
|
|
|
|
decoded_msg,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
if output_format == "HTML":
|
|
|
|
filtered_msg = filtered_msg.replace('"', """)
|
|
|
|
return filtered_msg
|
|
|
|
else:
|
|
|
|
return msg
|
|
|
|
|
|
|
|
|
|
|
|
def new_junit_xml_tree():
|
|
|
|
return ET.ElementTree(ET.Element("testsuites"))
|
|
|
|
|
|
|
|
|
|
|
|
def render_junit_xml_report(
|
|
|
|
resultset,
|
|
|
|
report_file,
|
|
|
|
suite_package="grpc",
|
|
|
|
suite_name="tests",
|
|
|
|
replace_dots=True,
|
|
|
|
multi_target=False,
|
|
|
|
):
|
|
|
|
"""Generate JUnit-like XML report."""
|
|
|
|
if not multi_target:
|
|
|
|
tree = new_junit_xml_tree()
|
|
|
|
append_junit_xml_results(
|
|
|
|
tree, resultset, suite_package, suite_name, "1", replace_dots
|
|
|
|
)
|
|
|
|
create_xml_report_file(tree, report_file)
|
|
|
|
else:
|
|
|
|
# To have each test result displayed as a separate target by the Resultstore/Sponge UI,
|
|
|
|
# we generate a separate XML report file for each test result
|
|
|
|
for shortname, results in six.iteritems(resultset):
|
|
|
|
one_result = {shortname: results}
|
|
|
|
tree = new_junit_xml_tree()
|
|
|
|
append_junit_xml_results(
|
|
|
|
tree,
|
|
|
|
one_result,
|
|
|
|
"%s_%s" % (suite_package, shortname),
|
|
|
|
"%s_%s" % (suite_name, shortname),
|
|
|
|
"1",
|
|
|
|
replace_dots,
|
|
|
|
)
|
|
|
|
per_suite_report_file = os.path.join(
|
|
|
|
os.path.dirname(report_file),
|
|
|
|
shortname,
|
|
|
|
os.path.basename(report_file),
|
|
|
|
)
|
|
|
|
create_xml_report_file(tree, per_suite_report_file)
|
|
|
|
|
|
|
|
|
|
|
|
def create_xml_report_file(tree, report_file):
|
|
|
|
"""Generate JUnit-like report file from xml tree ."""
|
|
|
|
# env variable can be used to override the base location for the reports
|
|
|
|
base_dir = os.getenv("GRPC_TEST_REPORT_BASE_DIR", None)
|
|
|
|
if base_dir:
|
|
|
|
report_file = os.path.join(base_dir, report_file)
|
|
|
|
# ensure the report directory exists
|
|
|
|
report_dir = os.path.dirname(os.path.abspath(report_file))
|
|
|
|
if not os.path.exists(report_dir):
|
|
|
|
os.makedirs(report_dir)
|
|
|
|
tree.write(report_file, encoding="UTF-8")
|
|
|
|
|
|
|
|
|
|
|
|
def append_junit_xml_results(
|
|
|
|
tree, resultset, suite_package, suite_name, id, replace_dots=True
|
|
|
|
):
|
|
|
|
"""Append a JUnit-like XML report tree with test results as a new suite."""
|
|
|
|
if replace_dots:
|
|
|
|
# ResultStore UI displays test suite names containing dots only as the component
|
|
|
|
# after the last dot, which results bad info being displayed in the UI.
|
|
|
|
# We replace dots by another character to avoid this problem.
|
|
|
|
suite_name = suite_name.replace(".", "_")
|
|
|
|
testsuite = ET.SubElement(
|
|
|
|
tree.getroot(),
|
|
|
|
"testsuite",
|
|
|
|
id=id,
|
|
|
|
package=suite_package,
|
|
|
|
name=suite_name,
|
|
|
|
timestamp=datetime.datetime.now().isoformat(),
|
|
|
|
)
|
|
|
|
failure_count = 0
|
|
|
|
error_count = 0
|
|
|
|
for shortname, results in six.iteritems(resultset):
|
|
|
|
for result in results:
|
|
|
|
xml_test = ET.SubElement(testsuite, "testcase", name=shortname)
|
|
|
|
if result.elapsed_time:
|
|
|
|
xml_test.set("time", str(result.elapsed_time))
|
|
|
|
filtered_msg = _filter_msg(result.message, "XML")
|
|
|
|
if result.state == "FAILED":
|
|
|
|
ET.SubElement(
|
|
|
|
xml_test, "failure", message="Failure"
|
|
|
|
).text = filtered_msg
|
|
|
|
failure_count += 1
|
|
|
|
elif result.state == "TIMEOUT":
|
|
|
|
ET.SubElement(
|
|
|
|
xml_test, "error", message="Timeout"
|
|
|
|
).text = filtered_msg
|
|
|
|
error_count += 1
|
|
|
|
elif result.state == "SKIPPED":
|
|
|
|
ET.SubElement(xml_test, "skipped", message="Skipped")
|
|
|
|
testsuite.set("failures", str(failure_count))
|
|
|
|
testsuite.set("errors", str(error_count))
|
|
|
|
|
|
|
|
|
|
|
|
def render_interop_html_report(
|
|
|
|
client_langs,
|
|
|
|
server_langs,
|
|
|
|
test_cases,
|
|
|
|
auth_test_cases,
|
|
|
|
http2_cases,
|
|
|
|
http2_server_cases,
|
|
|
|
resultset,
|
|
|
|
num_failures,
|
|
|
|
cloud_to_prod,
|
|
|
|
prod_servers,
|
|
|
|
http2_interop,
|
|
|
|
):
|
|
|
|
"""Generate HTML report for interop tests."""
|
|
|
|
template_file = "tools/run_tests/interop/interop_html_report.template"
|
|
|
|
try:
|
|
|
|
mytemplate = Template(filename=template_file, format_exceptions=True)
|
|
|
|
except NameError:
|
|
|
|
print(
|
|
|
|
"Mako template is not installed. Skipping HTML report generation."
|
|
|
|
)
|
|
|
|
return
|
|
|
|
except IOError as e:
|
|
|
|
print(("Failed to find the template %s: %s" % (template_file, e)))
|
|
|
|
return
|
|
|
|
|
|
|
|
sorted_test_cases = sorted(test_cases)
|
|
|
|
sorted_auth_test_cases = sorted(auth_test_cases)
|
|
|
|
sorted_http2_cases = sorted(http2_cases)
|
|
|
|
sorted_http2_server_cases = sorted(http2_server_cases)
|
|
|
|
sorted_client_langs = sorted(client_langs)
|
|
|
|
sorted_server_langs = sorted(server_langs)
|
|
|
|
sorted_prod_servers = sorted(prod_servers)
|
|
|
|
|
|
|
|
args = {
|
|
|
|
"client_langs": sorted_client_langs,
|
|
|
|
"server_langs": sorted_server_langs,
|
|
|
|
"test_cases": sorted_test_cases,
|
|
|
|
"auth_test_cases": sorted_auth_test_cases,
|
|
|
|
"http2_cases": sorted_http2_cases,
|
|
|
|
"http2_server_cases": sorted_http2_server_cases,
|
|
|
|
"resultset": resultset,
|
|
|
|
"num_failures": num_failures,
|
|
|
|
"cloud_to_prod": cloud_to_prod,
|
|
|
|
"prod_servers": sorted_prod_servers,
|
|
|
|
"http2_interop": http2_interop,
|
|
|
|
}
|
|
|
|
|
|
|
|
html_report_out_dir = "reports"
|
|
|
|
if not os.path.exists(html_report_out_dir):
|
|
|
|
os.mkdir(html_report_out_dir)
|
|
|
|
html_file_path = os.path.join(html_report_out_dir, "index.html")
|
|
|
|
try:
|
|
|
|
with open(html_file_path, "w") as output_file:
|
|
|
|
mytemplate.render_context(Context(output_file, **args))
|
|
|
|
except:
|
|
|
|
print((exceptions.text_error_template().render()))
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
def render_perf_profiling_results(output_filepath, profile_names):
|
|
|
|
with open(output_filepath, "w") as output_file:
|
|
|
|
output_file.write("<ul>\n")
|
|
|
|
for name in profile_names:
|
|
|
|
output_file.write("<li><a href=%s>%s</a></li>\n" % (name, name))
|
|
|
|
output_file.write("</ul>\n")
|