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