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
9from datetime import datetime
10import os
11import json
12
13from .. import base
14
15from collections import defaultdict
16import six
17
18html = None
19raw = None
20
21if six.PY2:
22    from cgi import escape
23else:
24    from html import escape
25
26base_path = os.path.split(__file__)[0]
27
28
29def do_defered_imports():
30    global html
31    global raw
32
33    from .xmlgen import html, raw
34
35
36class HTMLFormatter(base.BaseFormatter):
37    """Formatter that produces a simple HTML-formatted report."""
38
39    def __init__(self):
40        do_defered_imports()
41        self.suite_name = None
42        self.result_rows = []
43        self.test_count = defaultdict(int)
44        self.start_times = {}
45        self.suite_times = {"start": None, "end": None}
46        self.head = None
47        self.env = {}
48
49    def suite_start(self, data):
50        self.suite_times["start"] = data["time"]
51        self.suite_name = data["source"]
52        with open(os.path.join(base_path, "style.css")) as f:
53            self.head = html.head(
54                html.meta(charset="utf-8"),
55                html.title(data["source"]),
56                html.style(raw(f.read())),
57            )
58
59        date_format = "%d %b %Y %H:%M:%S"
60        version_info = data.get("version_info")
61        if version_info:
62            self.env["Device identifier"] = version_info.get("device_id")
63            self.env["Device firmware (base)"] = version_info.get(
64                "device_firmware_version_base"
65            )
66            self.env["Device firmware (date)"] = (
67                datetime.utcfromtimestamp(
68                    int(version_info.get("device_firmware_date"))
69                ).strftime(date_format)
70                if "device_firmware_date" in version_info
71                else None
72            )
73            self.env["Device firmware (incremental)"] = version_info.get(
74                "device_firmware_version_incremental"
75            )
76            self.env["Device firmware (release)"] = version_info.get(
77                "device_firmware_version_release"
78            )
79            self.env["Gaia date"] = (
80                datetime.utcfromtimestamp(int(version_info.get("gaia_date"))).strftime(
81                    date_format
82                )
83                if "gaia_date" in version_info
84                else None
85            )
86            self.env["Gecko version"] = version_info.get("application_version")
87            self.env["Gecko build"] = version_info.get("application_buildid")
88
89            if version_info.get("application_changeset"):
90                self.env["Gecko revision"] = version_info.get("application_changeset")
91                if version_info.get("application_repository"):
92                    self.env["Gecko revision"] = html.a(
93                        version_info.get("application_changeset"),
94                        href="/rev/".join(
95                            [
96                                version_info.get("application_repository"),
97                                version_info.get("application_changeset"),
98                            ]
99                        ),
100                        target="_blank",
101                    )
102
103            if version_info.get("gaia_changeset"):
104                self.env["Gaia revision"] = html.a(
105                    version_info.get("gaia_changeset")[:12],
106                    href="https://github.com/mozilla-b2g/gaia/commit/%s"
107                    % version_info.get("gaia_changeset"),
108                    target="_blank",
109                )
110
111        device_info = data.get("device_info")
112        if device_info:
113            self.env["Device uptime"] = device_info.get("uptime")
114            self.env["Device memory"] = device_info.get("memtotal")
115            self.env["Device serial"] = device_info.get("id")
116
117    def suite_end(self, data):
118        self.suite_times["end"] = data["time"]
119        return self.generate_html()
120
121    def test_start(self, data):
122        self.start_times[data["test"]] = data["time"]
123
124    def test_end(self, data):
125        self.make_result_html(data)
126
127    def make_result_html(self, data):
128        tc_time = (data["time"] - self.start_times.pop(data["test"])) / 1000.0
129        additional_html = []
130        debug = data.get("extra", {})
131        # Add support for log exported from wptrunner. The structure of
132        # reftest_screenshots is listed in wptrunner/executors/base.py.
133        if debug.get("reftest_screenshots"):
134            log_data = debug.get("reftest_screenshots", {})
135            debug = {
136                "image1": "data:image/png;base64," + log_data[0].get("screenshot", {}),
137                "image2": "data:image/png;base64," + log_data[2].get("screenshot", {}),
138                "differences": "Not Implemented",
139            }
140
141        links_html = []
142
143        status = status_name = data["status"]
144        expected = data.get("expected", status)
145        known_intermittent = data.get("known_intermittent", [])
146
147        if status != expected and status not in known_intermittent:
148            status_name = "UNEXPECTED_" + status
149        elif status in known_intermittent:
150            status_name = "KNOWN_INTERMITTENT"
151        elif status not in ("PASS", "SKIP"):
152            status_name = "EXPECTED_" + status
153
154        self.test_count[status_name] += 1
155
156        if status in ["SKIP", "FAIL", "PRECONDITION_FAILED", "ERROR"]:
157            if debug.get("differences"):
158                images = [
159                    ("image1", "Image 1 (test)"),
160                    ("image2", "Image 2 (reference)"),
161                ]
162                for title, description in images:
163                    screenshot = "%s" % debug[title]
164                    additional_html.append(
165                        html.div(
166                            html.a(html.img(src=screenshot), href="#"),
167                            html.br(),
168                            html.a(description),
169                            class_="screenshot",
170                        )
171                    )
172
173            if debug.get("screenshot"):
174                screenshot = "%s" % debug["screenshot"]
175                screenshot = "data:image/png;base64," + screenshot
176
177                additional_html.append(
178                    html.div(
179                        html.a(html.img(src=screenshot), href="#"), class_="screenshot"
180                    )
181                )
182
183            for name, content in debug.items():
184                if name in ["screenshot", "image1", "image2"]:
185                    if not content.startswith("data:image/png;base64,"):
186                        href = "data:image/png;base64,%s" % content
187                    else:
188                        href = content
189                else:
190                    if not isinstance(content, (six.text_type, six.binary_type)):
191                        # All types must be json serializable
192                        content = json.dumps(content)
193                        # Decode to text type if JSON output is byte string
194                        if not isinstance(content, six.text_type):
195                            content = content.decode("utf-8")
196                    # Encode base64 to avoid that some browsers (such as Firefox, Opera)
197                    # treats '#' as the start of another link if it is contained in the data URL.
198                    if isinstance(content, six.text_type):
199                        is_known_utf8 = True
200                        content_bytes = six.text_type(content).encode(
201                            "utf-8", "xmlcharrefreplace"
202                        )
203                    else:
204                        is_known_utf8 = False
205                        content_bytes = content
206
207                    meta = ["text/html"]
208                    if is_known_utf8:
209                        meta.append("charset=utf-8")
210
211                    # base64 is ascii only, which means we don't have to care about encoding
212                    # in the case where we don't know the encoding of the input
213                    b64_bytes = base64.b64encode(content_bytes)
214                    b64_text = b64_bytes.decode()
215                    href = "data:%s;base64,%s" % (";".join(meta), b64_text)
216                links_html.append(
217                    html.a(name.title(), class_=name, href=href, target="_blank")
218                )
219                links_html.append(" ")
220
221            log = html.div(class_="log")
222            output = data.get("stack", "").splitlines()
223            output.extend(data.get("message", "").splitlines())
224            for line in output:
225                separator = line.startswith(" " * 10)
226                if separator:
227                    log.append(line[:80])
228                else:
229                    if (
230                        line.lower().find("error") != -1
231                        or line.lower().find("exception") != -1
232                    ):
233                        log.append(html.span(raw(escape(line)), class_="error"))
234                    else:
235                        log.append(raw(escape(line)))
236                log.append(html.br())
237            additional_html.append(log)
238
239        self.result_rows.append(
240            html.tr(
241                [
242                    html.td(status_name, class_="col-result"),
243                    html.td(data["test"], class_="col-name"),
244                    html.td("%.2f" % tc_time, class_="col-duration"),
245                    html.td(links_html, class_="col-links"),
246                    html.td(additional_html, class_="debug"),
247                ],
248                class_=status_name.lower() + " results-table-row",
249            )
250        )
251
252    def generate_html(self):
253        generated = datetime.utcnow()
254        with open(os.path.join(base_path, "main.js")) as main_f:
255            doc = html.html(
256                self.head,
257                html.body(
258                    html.script(raw(main_f.read())),
259                    html.p(
260                        "Report generated on %s at %s"
261                        % (
262                            generated.strftime("%d-%b-%Y"),
263                            generated.strftime("%H:%M:%S"),
264                        )
265                    ),
266                    html.h2("Environment"),
267                    html.table(
268                        [
269                            html.tr(html.td(k), html.td(v))
270                            for k, v in sorted(self.env.items())
271                            if v
272                        ],
273                        id="environment",
274                    ),
275                    html.h2("Summary"),
276                    html.p(
277                        "%i tests ran in %.1f seconds."
278                        % (
279                            sum(six.itervalues(self.test_count)),
280                            (self.suite_times["end"] - self.suite_times["start"])
281                            / 1000.0,
282                        ),
283                        html.br(),
284                        html.span("%i passed" % self.test_count["PASS"], class_="pass"),
285                        ", ",
286                        html.span(
287                            "%i skipped" % self.test_count["SKIP"], class_="skip"
288                        ),
289                        ", ",
290                        html.span(
291                            "%i failed" % self.test_count["UNEXPECTED_FAIL"],
292                            class_="fail",
293                        ),
294                        ", ",
295                        html.span(
296                            "%i errors" % self.test_count["UNEXPECTED_ERROR"],
297                            class_="error",
298                        ),
299                        ".",
300                        html.br(),
301                        html.span(
302                            "%i expected failures" % self.test_count["EXPECTED_FAIL"],
303                            class_="expected_fail",
304                        ),
305                        ", ",
306                        html.span(
307                            "%i unexpected passes" % self.test_count["UNEXPECTED_PASS"],
308                            class_="unexpected_pass",
309                        ),
310                        ", ",
311                        html.span(
312                            "%i known intermittent results"
313                            % self.test_count["KNOWN_INTERMITTENT"],
314                            class_="known_intermittent",
315                        ),
316                        ".",
317                    ),
318                    html.h2("Results"),
319                    html.table(
320                        [
321                            html.thead(
322                                html.tr(
323                                    [
324                                        html.th(
325                                            "Result", class_="sortable", col="result"
326                                        ),
327                                        html.th("Test", class_="sortable", col="name"),
328                                        html.th(
329                                            "Duration",
330                                            class_="sortable numeric",
331                                            col="duration",
332                                        ),
333                                        html.th("Links"),
334                                    ]
335                                ),
336                                id="results-table-head",
337                            ),
338                            html.tbody(self.result_rows, id="results-table-body"),
339                        ],
340                        id="results-table",
341                    ),
342                ),
343            )
344
345        return u"<!DOCTYPE html>\n" + doc.unicode(indent=2)
346