1#!/usr/bin/env python
2# This Source Code Form is subject to the terms of the Mozilla Public
3# License, v. 2.0. If a copy of the MPL was not distributed with this
4# file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
6from __future__ import absolute_import
7
8import base64
9import cgi
10from datetime import datetime
11import os
12
13from .. import base
14
15from collections import defaultdict
16
17html = None
18raw = None
19
20base_path = os.path.split(__file__)[0]
21
22
23def do_defered_imports():
24    global html
25    global raw
26
27    from .xmlgen import html, raw
28
29
30class HTMLFormatter(base.BaseFormatter):
31    """Formatter that produces a simple HTML-formatted report."""
32
33    def __init__(self):
34        do_defered_imports()
35        self.suite_name = None
36        self.result_rows = []
37        self.test_count = defaultdict(int)
38        self.start_times = {}
39        self.suite_times = {"start": None,
40                            "end": None}
41        self.head = None
42        self.env = {}
43
44    def suite_start(self, data):
45        self.suite_times["start"] = data["time"]
46        self.suite_name = data["source"]
47        with open(os.path.join(base_path, "style.css")) as f:
48            self.head = html.head(
49                html.meta(charset="utf-8"),
50                html.title(data["source"]),
51                html.style(raw(f.read())))
52
53        date_format = "%d %b %Y %H:%M:%S"
54        version_info = data.get("version_info")
55        if version_info:
56            self.env["Device identifier"] = version_info.get("device_id")
57            self.env["Device firmware (base)"] = version_info.get("device_firmware_version_base")
58            self.env["Device firmware (date)"] = (
59                datetime.utcfromtimestamp(int(version_info.get("device_firmware_date")))
60                .strftime(date_format) if
61                "device_firmware_date" in version_info else None)
62            self.env["Device firmware (incremental)"] = version_info.get(
63                "device_firmware_version_incremental")
64            self.env["Device firmware (release)"] = version_info.get(
65                "device_firmware_version_release")
66            self.env["Gaia date"] = (
67                datetime.utcfromtimestamp(int(version_info.get("gaia_date")))
68                .strftime(date_format) if
69                "gaia_date" in version_info else None)
70            self.env["Gecko version"] = version_info.get("application_version")
71            self.env["Gecko build"] = version_info.get("application_buildid")
72
73            if version_info.get("application_changeset"):
74                self.env["Gecko revision"] = version_info.get("application_changeset")
75                if version_info.get("application_repository"):
76                    self.env["Gecko revision"] = html.a(
77                        version_info.get("application_changeset"),
78                        href="/".join([version_info.get("application_repository"),
79                                       version_info.get("application_changeset")]),
80                        target="_blank")
81
82            if version_info.get("gaia_changeset"):
83                self.env["Gaia revision"] = html.a(
84                    version_info.get("gaia_changeset")[:12],
85                    href="https://github.com/mozilla-b2g/gaia/commit/%s" % version_info.get(
86                        "gaia_changeset"),
87                    target="_blank")
88
89        device_info = data.get("device_info")
90        if device_info:
91            self.env["Device uptime"] = device_info.get("uptime")
92            self.env["Device memory"] = device_info.get("memtotal")
93            self.env["Device serial"] = device_info.get("id")
94
95    def suite_end(self, data):
96        self.suite_times["end"] = data["time"]
97        return self.generate_html()
98
99    def test_start(self, data):
100        self.start_times[data["test"]] = data["time"]
101
102    def test_end(self, data):
103        self.make_result_html(data)
104
105    def make_result_html(self, data):
106        tc_time = (data["time"] - self.start_times.pop(data["test"])) / 1000.
107        additional_html = []
108        debug = data.get("extra", {})
109        # Add support for log exported from wptrunner. The structure of
110        # reftest_screenshots is listed in wptrunner/executors/base.py.
111        if debug.get('reftest_screenshots'):
112            log_data = debug.get("reftest_screenshots", {})
113            debug = {
114                'image1': 'data:image/png;base64,' + log_data[0].get("screenshot", {}),
115                'image2': 'data:image/png;base64,' + log_data[2].get("screenshot", {}),
116                'differences': "Not Implemented",
117            }
118
119        links_html = []
120
121        status = status_name = data["status"]
122        expected = data.get("expected", status)
123
124        if status != expected:
125            status_name = "UNEXPECTED_" + status
126        elif status not in ("PASS", "SKIP"):
127            status_name = "EXPECTED_" + status
128
129        self.test_count[status_name] += 1
130
131        if status in ['SKIP', 'FAIL', 'ERROR']:
132            if debug.get('differences'):
133                images = [
134                    ('image1', 'Image 1 (test)'),
135                    ('image2', 'Image 2 (reference)')
136                ]
137                for title, description in images:
138                    screenshot = '%s' % debug[title]
139                    additional_html.append(html.div(
140                        html.a(html.img(src=screenshot), href="#"),
141                        html.br(),
142                        html.a(description),
143                        class_='screenshot'))
144
145            if debug.get('screenshot'):
146                screenshot = '%s' % debug['screenshot']
147                screenshot = 'data:image/png;base64,' + screenshot
148
149                additional_html.append(html.div(
150                    html.a(html.img(src=screenshot), href="#"),
151                    class_='screenshot'))
152
153            for name, content in debug.items():
154                if name in ['screenshot', 'image1', 'image2']:
155                    if not content.startswith('data:image/png;base64,'):
156                        href = 'data:image/png;base64,%s' % content
157                    else:
158                        href = content
159                else:
160                    # Encode base64 to avoid that some browsers (such as Firefox, Opera)
161                    # treats '#' as the start of another link if it is contained in the data URL.
162                    # Use 'charset=utf-8' to show special characters like Chinese.
163                    utf_encoded = unicode(content).encode('utf-8', 'xmlcharrefreplace')
164                    href = 'data:text/html;charset=utf-8;base64,%s' % base64.b64encode(utf_encoded)
165
166                links_html.append(html.a(
167                    name.title(),
168                    class_=name,
169                    href=href,
170                    target='_blank'))
171                links_html.append(' ')
172
173            log = html.div(class_='log')
174            output = data.get('stack', '').splitlines()
175            output.extend(data.get('message', '').splitlines())
176            for line in output:
177                separator = line.startswith(' ' * 10)
178                if separator:
179                    log.append(line[:80])
180                else:
181                    if line.lower().find("error") != -1 or line.lower().find("exception") != -1:
182                        log.append(html.span(raw(cgi.escape(line)), class_='error'))
183                    else:
184                        log.append(raw(cgi.escape(line)))
185                log.append(html.br())
186            additional_html.append(log)
187
188        self.result_rows.append(
189            html.tr([html.td(status_name, class_='col-result'),
190                     html.td(data['test'], class_='col-name'),
191                     html.td('%.2f' % tc_time, class_='col-duration'),
192                     html.td(links_html, class_='col-links'),
193                     html.td(additional_html, class_='debug')],
194                    class_=status_name.lower() + ' results-table-row'))
195
196    def generate_html(self):
197        generated = datetime.utcnow()
198        with open(os.path.join(base_path, "main.js")) as main_f:
199            doc = html.html(
200                self.head,
201                html.body(
202                    html.script(raw(main_f.read())),
203                    html.p('Report generated on %s at %s' % (
204                        generated.strftime('%d-%b-%Y'),
205                        generated.strftime('%H:%M:%S'))),
206                    html.h2('Environment'),
207                    html.table(
208                        [html.tr(html.td(k), html.td(v))
209                         for k, v in sorted(self.env.items()) if v],
210                        id='environment'),
211
212                    html.h2('Summary'),
213                    html.p('%i tests ran in %.1f seconds.' % (sum(self.test_count.itervalues()),
214                                                              (self.suite_times["end"] -
215                                                               self.suite_times["start"]) / 1000.),
216                           html.br(),
217                           html.span('%i passed' % self.test_count["PASS"], class_='pass'), ', ',
218                           html.span('%i skipped' % self.test_count["SKIP"], class_='skip'), ', ',
219                           html.span('%i failed' % self.test_count[
220                                     "UNEXPECTED_FAIL"], class_='fail'), ', ',
221                           html.span('%i errors' % self.test_count[
222                                     "UNEXPECTED_ERROR"], class_='error'), '.',
223                           html.br(),
224                           html.span('%i expected failures' % self.test_count["EXPECTED_FAIL"],
225                                     class_='expected_fail'), ', ',
226                           html.span('%i unexpected passes' % self.test_count["UNEXPECTED_PASS"],
227                                     class_='unexpected_pass'), '.'),
228                    html.h2('Results'),
229                    html.table([html.thead(
230                        html.tr([
231                            html.th('Result', class_='sortable', col='result'),
232                            html.th('Test', class_='sortable', col='name'),
233                            html.th('Duration', class_='sortable numeric', col='duration'),
234                            html.th('Links')]), id='results-table-head'),
235                        html.tbody(self.result_rows,
236                                   id='results-table-body')], id='results-table')))
237
238        return u"<!DOCTYPE html>\n" + doc.unicode(indent=2)
239