1# Copyright 2015 gRPC authors.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Generate XML and HTML test reports."""
15
16from __future__ import print_function
17
18try:
19    from mako.runtime import Context
20    from mako.template import Template
21    from mako import exceptions
22except (ImportError):
23    pass  # Mako not installed but it is ok.
24import datetime
25import os
26import string
27import xml.etree.cElementTree as ET
28import six
29
30
31def _filter_msg(msg, output_format):
32    """Filters out nonprintable and illegal characters from the message."""
33    if output_format in ['XML', 'HTML']:
34        # keep whitespaces but remove formfeed and vertical tab characters
35        # that make XML report unparseable.
36        filtered_msg = filter(
37            lambda x: x in string.printable and x != '\f' and x != '\v',
38            msg.decode('UTF-8', 'ignore'))
39        if output_format == 'HTML':
40            filtered_msg = filtered_msg.replace('"', '"')
41        return filtered_msg
42    else:
43        return msg
44
45
46def new_junit_xml_tree():
47    return ET.ElementTree(ET.Element('testsuites'))
48
49
50def render_junit_xml_report(resultset,
51                            report_file,
52                            suite_package='grpc',
53                            suite_name='tests',
54                            replace_dots=True):
55    """Generate JUnit-like XML report."""
56    tree = new_junit_xml_tree()
57    append_junit_xml_results(tree, resultset, suite_package, suite_name, '1',
58                             replace_dots)
59    create_xml_report_file(tree, report_file)
60
61
62def create_xml_report_file(tree, report_file):
63    """Generate JUnit-like report file from xml tree ."""
64    # ensure the report directory exists
65    report_dir = os.path.dirname(os.path.abspath(report_file))
66    if not os.path.exists(report_dir):
67        os.makedirs(report_dir)
68    tree.write(report_file, encoding='UTF-8')
69
70
71def append_junit_xml_results(tree,
72                             resultset,
73                             suite_package,
74                             suite_name,
75                             id,
76                             replace_dots=True):
77    """Append a JUnit-like XML report tree with test results as a new suite."""
78    if replace_dots:
79        # ResultStore UI displays test suite names containing dots only as the component
80        # after the last dot, which results bad info being displayed in the UI.
81        # We replace dots by another character to avoid this problem.
82        suite_name = suite_name.replace('.', '_')
83    testsuite = ET.SubElement(
84        tree.getroot(),
85        'testsuite',
86        id=id,
87        package=suite_package,
88        name=suite_name,
89        timestamp=datetime.datetime.now().isoformat())
90    failure_count = 0
91    error_count = 0
92    for shortname, results in six.iteritems(resultset):
93        for result in results:
94            xml_test = ET.SubElement(testsuite, 'testcase', name=shortname)
95            if result.elapsed_time:
96                xml_test.set('time', str(result.elapsed_time))
97            filtered_msg = _filter_msg(result.message, 'XML')
98            if result.state == 'FAILED':
99                ET.SubElement(
100                    xml_test, 'failure', message='Failure').text = filtered_msg
101                failure_count += 1
102            elif result.state == 'TIMEOUT':
103                ET.SubElement(
104                    xml_test, 'error', message='Timeout').text = filtered_msg
105                error_count += 1
106            elif result.state == 'SKIPPED':
107                ET.SubElement(xml_test, 'skipped', message='Skipped')
108    testsuite.set('failures', str(failure_count))
109    testsuite.set('errors', str(error_count))
110
111
112def render_interop_html_report(client_langs, server_langs, test_cases,
113                               auth_test_cases, http2_cases, http2_server_cases,
114                               resultset, num_failures, cloud_to_prod,
115                               prod_servers, http2_interop):
116    """Generate HTML report for interop tests."""
117    template_file = 'tools/run_tests/interop/interop_html_report.template'
118    try:
119        mytemplate = Template(filename=template_file, format_exceptions=True)
120    except NameError:
121        print(
122            'Mako template is not installed. Skipping HTML report generation.')
123        return
124    except IOError as e:
125        print('Failed to find the template %s: %s' % (template_file, e))
126        return
127
128    sorted_test_cases = sorted(test_cases)
129    sorted_auth_test_cases = sorted(auth_test_cases)
130    sorted_http2_cases = sorted(http2_cases)
131    sorted_http2_server_cases = sorted(http2_server_cases)
132    sorted_client_langs = sorted(client_langs)
133    sorted_server_langs = sorted(server_langs)
134    sorted_prod_servers = sorted(prod_servers)
135
136    args = {
137        'client_langs': sorted_client_langs,
138        'server_langs': sorted_server_langs,
139        'test_cases': sorted_test_cases,
140        'auth_test_cases': sorted_auth_test_cases,
141        'http2_cases': sorted_http2_cases,
142        'http2_server_cases': sorted_http2_server_cases,
143        'resultset': resultset,
144        'num_failures': num_failures,
145        'cloud_to_prod': cloud_to_prod,
146        'prod_servers': sorted_prod_servers,
147        'http2_interop': http2_interop
148    }
149
150    html_report_out_dir = 'reports'
151    if not os.path.exists(html_report_out_dir):
152        os.mkdir(html_report_out_dir)
153    html_file_path = os.path.join(html_report_out_dir, 'index.html')
154    try:
155        with open(html_file_path, 'w') as output_file:
156            mytemplate.render_context(Context(output_file, **args))
157    except:
158        print(exceptions.text_error_template().render())
159        raise
160
161
162def render_perf_profiling_results(output_filepath, profile_names):
163    with open(output_filepath, 'w') as output_file:
164        output_file.write('<ul>\n')
165        for name in profile_names:
166            output_file.write('<li><a href=%s>%s</a></li>\n' % (name, name))
167        output_file.write('</ul>\n')
168