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