def _populate_html_log_div(self, log, report): if report.longrepr: # longreprtext is only filled out on failure by pytest # otherwise will be None. # Use full_text if longreprtext is None-ish # we added full_text elsewhere in this file. text = report.longreprtext or report.full_text for line in text.splitlines(): separator = line.startswith("_ " * 10) if separator: log.append(line[:80]) else: exception = line.startswith("E ") if exception: log.append( html.span(raw(escape(line)), class_="error")) else: log.append(raw(escape(line))) log.append(html.br()) for section in report.sections: header, content = map(escape, section) log.append(f" {header:-^80} ") log.append(html.br()) if ansi_support(): converter = ansi_support().Ansi2HTMLConverter( inline=False, escaped=False) content = converter.convert(content, full=False) else: content = _remove_ansi_escape_sequences(content) log.append(raw(content)) log.append(html.br())
def append_log_html(self, report, additional_html): log = html.div(class_='log') if report.longrepr: for line in str(report.longrepr).splitlines(): if not PY3: line = line.decode('utf-8') separator = line.startswith('_ ' * 10) if separator: log.append(line[:80]) else: exception = line.startswith("E ") if exception: log.append( html.span(raw(escape(line)), class_='error')) else: log.append(raw(escape(line))) log.append(html.br()) for section in report.sections: header, content = map(escape, section) log.append(' {0} '.format(header).center(80, '-')) log.append(html.br()) if ANSI: converter = Ansi2HTMLConverter(inline=False, escaped=False) content = converter.convert(content, full=False) log.append(raw(content)) if len(log) == 0: log = html.div(class_='empty log') log.append('No log output captured.') additional_html.append(log)
def append_log_html(self, report, additional_html): log = html.div(class_='log') if report.longrepr: for line in str(report.longrepr).splitlines(): if not PY3: line = line.decode('utf-8') separator = line.startswith('_ ' * 10) if separator: log.append(line[:80]) else: exception = line.startswith("E ") if exception: log.append(html.span(raw(escape(line)), class_='error')) else: log.append(raw(escape(line))) log.append(html.br()) for header, content in report.sections: log.append(' {0} '.format(header).center(80, '-')) log.append(html.br()) log.append(content) if len(log) == 0: log = html.div(class_='empty log') log.append('No log output captured.') additional_html.append(log)
def _make_media_html_div( self, extra, extra_index, test_index, base_extra_string, base_extra_class ): content = extra.get("content") try: is_uri_or_path = content.startswith(("file", "http")) or isfile(content) except ValueError: # On Windows, os.path.isfile throws this exception when # passed a b64 encoded image. is_uri_or_path = False if is_uri_or_path: if self.self_contained: warnings.warn(f"独立的 HTML 报告, 包含所有外链资源,资源: {content}") html_div = html.a( raw(base_extra_string.format(extra.get("content"))), href=content ) elif self.self_contained: src = f"data:{extra.get('mime_type')};base64,{content}" html_div = raw(base_extra_string.format(src)) else: content = b64decode(content.encode("utf-8")) href = src = self.create_asset( content, extra_index, test_index, extra.get("extension"), "wb" ) html_div = html.a( raw(base_extra_string.format(src)), class_=base_extra_class, target="_blank", href=href, ) return html_div
def append_log_html(self, report, additional_html): log = html.div(class_='log') if report.longrepr: for line in report.longreprtext.splitlines(): separator = line.startswith('_ ' * 10) if separator: log.append(line[:80]) else: exception = line.startswith("E ") if exception: log.append(html.span(raw(escape(line)), class_='error')) else: log.append(raw(escape(line))) log.append(html.br()) for section in report.sections: #header, content = map(escape, section) header = section[0] content = escape(section[1].replace("\\","\\\\")) #print('header1 : ',header) #print('content1 : ',content) log.append(' {0} '.format(header).center(80, '-')) log.append(html.br()) if ANSI: converter = Ansi2HTMLConverter(inline=False, escaped=False) content = converter.convert(content, full=False) log.append(raw(content)) if len(log) == 0: log = html.div(class_='empty log') log.append('No log output captured.') additional_html.append(log)
def append_log_html(self, report, additional_html): log = html.div(class_="log") if report.longrepr: for line in report.longreprtext.splitlines(): separator = line.startswith("_ " * 10) if separator: log.append(line[:80]) else: exception = line.startswith("E ") if exception: log.append( html.span(raw(escape(line)), class_="error")) else: log.append(raw(escape(line))) log.append(html.br()) for section in report.sections: header, content = map(escape, section) log.append(f" {header:-^80} ") log.append(html.br()) if ANSI: converter = Ansi2HTMLConverter(inline=False, escaped=False) content = converter.convert(content, full=False) log.append(raw(content)) log.append(html.br()) if len(log) == 0: log = html.div(class_="empty log") log.append("No log output captured.") additional_html.append(log)
def pytest_html_results_table_html(report, data): if report.failed: # Extract important lines failure_lines = [ line for line in lxmlhtml.fromstring(str(data[-1])).xpath( '//div/text()') if line.startswith(">") or " def " in line ] error_spans = [ str(html.span("* " + line[1:].lstrip().rstrip(), class_='error')) for line in lxmlhtml.fromstring( str(data[-1])).xpath('//span/text()') if "Fail Message(s) for test" not in line ] # Reconstruct error log # Taken from append_log_html in https://github.com/pytest-dev/pytest-html/blob/master/pytest_html/plugin.py del data[-1] log = html.div(class_='log') for line in failure_lines: log.append(raw(line)) log.append(html.br()) for line in error_spans: log.append(html.span(raw(line), class_='error')) log.append(html.br()) data.append(log) elif report.passed: del data[1]
def append_log_html(self, report, additional_html): log = html.div(class_='log') if report.longrepr: for line in report.longreprtext.splitlines(): separator = line.startswith('_ ' * 10) if separator: log.append(line[:80]) else: exception = line.startswith("E ") if exception: log.append(html.span(raw(escape(line)), class_='error')) else: log.append(raw(escape(line))) log.append(html.br()) for section in report.sections: header, content = map(escape, section) log.append(' {0} '.format(header).center(80, '-')) log.append(html.br()) if ANSI: converter = Ansi2HTMLConverter(inline=False, escaped=False) content = converter.convert(content, full=False) log.append(raw(content)) if len(log) == 0: log = html.div(class_='empty log') log.append('No log output captured.') additional_html.append(log)
def _appendrow(self, result, report): time = getattr(report, 'duration', 0.0) additional_html = [] links_html = [] for extra in getattr(report, 'extra', []): href = None if extra.get('format') == extras.FORMAT_IMAGE: href = '#' image = 'data:image/png;base64,%s' % extra.get('content') additional_html.append(html.div( html.a(html.img(src=image), href="#"), class_='image')) elif extra.get('format') == extras.FORMAT_HTML: additional_html.append(extra.get('content')) elif extra.get('format') == extras.FORMAT_JSON: href = data_uri(json.dumps(extra.get('content')), mime_type='application/json') elif extra.get('format') == extras.FORMAT_TEXT: href = data_uri(extra.get('content')) elif extra.get('format') == extras.FORMAT_URL: href = extra.get('content') if href is not None: links_html.append(html.a( extra.get('name'), class_=extra.get('format'), href=href, target='_blank')) links_html.append(' ') if 'Passed' not in result: if report.longrepr: log = html.div(class_='log') for line in str(report.longrepr).splitlines(): if not PY3: line = line.decode('utf-8') separator = line.startswith('_ ' * 10) if separator: log.append(line[:80]) else: exception = line.startswith("E ") if exception: log.append(html.span(raw(escape(line)), class_='error')) else: log.append(raw(escape(line))) log.append(html.br()) additional_html.append(log) self.test_logs.append(html.tr([ html.td(result, class_='col-result'), html.td(report.nodeid, class_='col-name'), html.td('%.2f' % time, class_='col-duration'), html.td(links_html, class_='col-links'), html.td(additional_html, class_='extra')], class_=result.lower() + ' results-table-row'))
def make_result_html(self, data): tc_time = (data["time"] - self.start_times.pop(data["test"])) / 1000. additional_html = [] debug = data.get("extra", {}) links_html = [] status = status_name = data["status"] expected = data.get("expected", status) if status != expected: status_name = "UNEXPECTED_" + status elif status != "PASS": status_name = "EXPECTED_" + status self.test_count[status_name] += 1 if status in ['SKIP', 'FAIL', 'ERROR']: if debug.get('screenshot'): screenshot = 'data:image/png;base64,%s' % debug['screenshot'] additional_html.append(html.div( html.a(html.img(src=screenshot), href="#"), class_='screenshot')) for name, content in debug.items(): if 'screenshot' in name: href = '#' else: # use base64 to avoid that some browser (such as Firefox, Opera) # treats '#' as the start of another link if the data URL contains. # use 'charset=utf-8' to show special characters like Chinese. href = 'data:text/plain;charset=utf-8;base64,%s' % base64.b64encode(content.encode('utf-8')) links_html.append(html.a( name.title(), class_=name, href=href, target='_blank')) links_html.append(' ') log = html.div(class_='log') output = data.get('stack', '').splitlines() output.extend(data.get('message', '').splitlines()) for line in output: separator = line.startswith(' ' * 10) if separator: log.append(line[:80]) else: if line.lower().find("error") != -1 or line.lower().find("exception") != -1: log.append(html.span(raw(cgi.escape(line)), class_='error')) else: log.append(raw(cgi.escape(line))) log.append(html.br()) additional_html.append(log) self.result_rows.append( html.tr([html.td(status_name, class_='col-result'), html.td(data['test'], class_='col-name'), html.td('%.2f' % tc_time, class_='col-duration'), html.td(links_html, class_='col-links'), html.td(additional_html, class_='debug')], class_=status_name.lower() + ' results-table-row'))
def _appendrow(self, result, report): time = getattr(report, 'duration', 0.0) additional_html = [] links_html = [] if 'Passed' not in result: for extra in getattr(report, 'extra', []): href = None if type(extra) is Image: href = '#' image = 'data:image/png;base64,%s' % extra.content additional_html.append(html.div( html.a(html.img(src=image), href="#"), class_='image')) elif type(extra) is HTML: additional_html.append(extra.content) elif type(extra) is Text: href = 'data:text/plain;charset=utf-8;base64,%s' % \ b64encode(extra.content) elif type(extra) is URL: href = extra.content if href is not None: links_html.append(html.a( extra.name, class_=extra.__class__.__name__.lower(), href=href, target='_blank')) links_html.append(' ') if report.longrepr: log = html.div(class_='log') for line in str(report.longrepr).splitlines(): line = line.decode('utf-8') separator = line.startswith('_ ' * 10) if separator: log.append(line[:80]) else: exception = line.startswith("E ") if exception: log.append(html.span(raw(cgi.escape(line)), class_='error')) else: log.append(raw(cgi.escape(line))) log.append(html.br()) additional_html.append(log) self.test_logs.append(html.tr([ html.td(result, class_='col-result'), html.td(report.nodeid, class_='col-name'), html.td('%.2f' % time, class_='col-duration'), html.td(links_html, class_='col-links'), html.td(additional_html, class_='extra')], class_=result.lower() + ' results-table-row'))
def make_body(self): body_parts = [ html.div( html.script(raw( pkg_resources.resource_string( rcname, os.path.sep.join( ['resources', 'htmlreport', 'jquery.js']))), type='text/javascript'), html.script(raw( pkg_resources.resource_string( rcname, os.path.sep.join( ['resources', 'htmlreport', 'main.js']))), type='text/javascript'), html.h1("FirefoxOS Certification Suite Report"), html.p("Run at %s" % self.time.strftime("%Y-%m-%d %H:%M:%S"))) ] if self.logs: device_profile_object = None with open(self.logs[-1]) as f: device_profile_object = json.load(f)['result']['contact'] device_profile = [html.h2('Device Information')] device_table = html.table() for key in device_profile_object: device_table.append( html.tr(html.td(key), html.td(device_profile_object[key]))) #device_profile.append(html.p("%s, %s"% (key, device_profile_object[key]))) device_profile.append(device_table) body_parts.extend(device_profile) if self.summary_results.has_errors: body_parts.append(html.h2("Errors During Run")) body_parts.append( self.make_errors_table(self.summary_results.errors)) body_parts.append(html.h2("Test Results")) body_parts.append(self.make_result_table()) if self.logs: ulbody = [] for log_path in self.logs: details_log = '' with open(log_path, 'r') as f: details_log = f.read() href = 'data:text/plain;charset=utf-8;base64,%s' % base64.b64encode( details_log) ulbody.append( html.li( html.a(os.path.basename(log_path), href=href, target='_blank'))) device_profile_object = None body_parts.append(html.h2("Details log information")) body_parts.append(html.ul(ulbody)) return html.body(body_parts)
def _extract_html(test, class_name, duration=0, text='', result='passed', debug=None): cls_name = class_name tc_name = unicode(test) tc_time = duration additional_html = [] debug = debug or {} links_html = [] if result in ['skipped', 'failure', 'expected failure', 'error']: if debug.get('screenshot'): screenshot = 'data:image/png;base64,%s' % debug['screenshot'] additional_html.append(html.div( html.a(html.img(src=screenshot), href="#"), class_='screenshot')) for name, content in debug.items(): try: if 'screenshot' in name: href = '#' else: # use base64 to avoid that some browser (such as Firefox, Opera) # treats '#' as the start of another link if the data URL contains. # use 'charset=utf-8' to show special characters like Chinese. href = 'data:text/plain;charset=utf-8;base64,%s' % base64.b64encode(content) links_html.append(html.a( name.title(), class_=name, href=href, target='_blank')) links_html.append(' ') except: pass log = html.div(class_='log') for line in text.splitlines(): separator = line.startswith(' ' * 10) if separator: log.append(line[:80]) else: if line.lower().find("error") != -1 or line.lower().find("exception") != -1: log.append(html.span(raw(cgi.escape(line)), class_='error')) else: log.append(raw(cgi.escape(line))) log.append(html.br()) additional_html.append(log) test_logs.append(html.tr([ html.td(result.title(), class_='col-result'), html.td(cls_name, class_='col-class'), html.td(tc_name, class_='col-name'), html.td(tc_time, class_='col-duration'), html.td(links_html, class_='col-links'), html.td(additional_html, class_='debug')], class_=result.lower() + ' results-table-row'))
def _extract_html(test, class_name, duration=0, text="", result="passed", debug=None): cls_name = class_name tc_name = unicode(test) tc_time = duration additional_html = [] debug = debug or {} links_html = [] if result in ["skipped", "failure", "expected failure", "error"]: if debug.get("screenshot"): screenshot = "data:image/png;base64,%s" % debug["screenshot"] additional_html.append(html.div(html.a(html.img(src=screenshot), href="#"), class_="screenshot")) for name, content in debug.items(): try: if "screenshot" in name: href = "#" else: # use base64 to avoid that some browser (such as Firefox, Opera) # treats '#' as the start of another link if the data URL contains. # use 'charset=utf-8' to show special characters like Chinese. href = "data:text/plain;charset=utf-8;base64,%s" % base64.b64encode(content) links_html.append(html.a(name.title(), class_=name, href=href, target="_blank")) links_html.append(" ") except: pass log = html.div(class_="log") for line in text.splitlines(): separator = line.startswith(" " * 10) if separator: log.append(line[:80]) else: if line.lower().find("error") != -1 or line.lower().find("exception") != -1: log.append(html.span(raw(cgi.escape(line)), class_="error")) else: log.append(raw(cgi.escape(line))) log.append(html.br()) additional_html.append(log) test_logs.append( html.tr( [ html.td(result.title(), class_="col-result"), html.td(cls_name, class_="col-class"), html.td(tc_name, class_="col-name"), html.td(tc_time, class_="col-duration"), html.td(links_html, class_="col-links"), html.td(additional_html, class_="debug"), ], class_=result.lower() + " results-table-row", ) )
def make_head(self): with open(os.path.join(here, "report.css")) as f: style = html.style(raw(f.read())) return html.head( html.meta(charset="utf-8"), html.title("FirefoxOS Certification Suite Report"), html.style(raw(pkg_resources.resource_string( rcname, os.path.sep.join(['resources', 'htmlreport', 'style.css']))), type='text/css'), style )
def make_head(self): with open(os.path.join(here, "report.css")) as f: style = html.style(raw(f.read())) return html.head( html.meta(charset="utf-8"), html.title("FirefoxOS Certification Suite Report"), html.style(raw( pkg_resources.resource_string( rcname, os.path.sep.join(['resources', 'htmlreport', 'style.css']))), type='text/css'), style)
def _appendrow(self, result, report): names = mangle_testnames(report.nodeid.split("::")) testclass = (names[:-1]) if self.prefix: testclass.insert(0, self.prefix) testclass = ".".join(testclass) testmethod = names[-1] time = getattr(report, 'duration', 0.0) additional_html = [] if not 'Passed' in result: if report.longrepr: log = html.div(class_='log') for line in str(report.longrepr).splitlines(): line = line.decode('utf8') separator = line.startswith('_ ' * 10) if separator: log.append(line[:80]) else: exception = line.startswith("E ") if exception: log.append( html.span(raw(cgi.escape(line)), class_='error')) else: log.append(raw(cgi.escape(line))) log.append(html.br()) if not os.path.exists(self.screenshot_path): os.makedirs(self.screenshot_path) self.append_screenshot(testmethod, log) additional_html.append(log) output = self._write_captured_output(report) info = output.split(" ") links_html = [] for i in range(0, len(info)): if ("http" in info[i] and "browserstack.com" in info[i]): links_html.append(html.a("link", href=info[i], target='_blank')) links_html.append(' ') self.test_logs.append( html.tr([ html.td(result, class_='col-result'), html.td(testclass, class_='col-class'), html.td(testmethod, class_='col-name'), html.td(round(time), class_='col-duration'), html.td(links_html, class_='col-links'), html.td(additional_html, class_='debug') ], class_=result.lower() + ' results-table-row'))
def _append_exception_section(self, div, exc): for line in exc.splitlines(): separator = line.startswith('_ ' * 10) if separator: div.append(line[:80]) div.append(html.br()) else: exception = line.startswith("E ") if exception: div.append(html.span(raw(escape(line)), class_='error')) div.append(html.br()) else: div.append(raw(escape(line))) div.append(html.br())
def _create_test_steps_tr(self, test_step_main_div, user_id, status_code, request_headers_main_div): test_steps_tr = html.tr(class_="steps_table_tr") test_steps_tr.append( html.td(test_step_main_div, class_="tr_step_name")) test_steps_tr.append( html.td(raw(user_id), class_="tr_step_user_id")) test_steps_tr.append( html.td(raw(status_code), class_="tr_step_status_code")) test_steps_tr.append( html.td(request_headers_main_div, class_="tr_step_request_headers")) return test_steps_tr
def make_body(self): body_parts = [html.div( html.script(raw(pkg_resources.resource_string( rcname, os.path.sep.join(['resources', 'htmlreport', 'jquery.js']))), type='text/javascript'), html.script(raw(pkg_resources.resource_string( rcname, os.path.sep.join(['resources', 'htmlreport', 'main.js']))), type='text/javascript'), html.h1("FirefoxOS Certification Suite Report"), html.p("Run at %s" % self.time.strftime("%Y-%m-%d %H:%M:%S")) )] if self.logs: device_profile_object = None with open(self.logs[-1]) as f: device_profile_object = json.load(f)['result']['contact'] device_profile = [html.h2('Device Information')] device_table = html.table() for key in device_profile_object: device_table.append( html.tr( html.td(key), html.td(device_profile_object[key]) ) ) #device_profile.append(html.p("%s, %s"% (key, device_profile_object[key]))) device_profile.append(device_table) body_parts.extend(device_profile); if self.summary_results.has_errors: body_parts.append(html.h2("Errors During Run")) body_parts.append(self.make_errors_table(self.summary_results.errors)) body_parts.append(html.h2("Test Results")) body_parts.append(self.make_result_table()) if self.logs: ulbody = []; for log_path in self.logs: details_log = '' with open(log_path, 'r') as f: details_log = f.read() href = 'data:text/plain;charset=utf-8;base64,%s' % base64.b64encode(details_log) ulbody.append(html.li(html.a(os.path.basename(log_path), href=href, target='_blank'))) device_profile_object = None body_parts.append(html.h2("Details log information")) body_parts.append(html.ul(ulbody)) return html.body(body_parts)
def _generate_environment(self, config): if not hasattr(config, "_metadata") or config._metadata is None: return [] metadata = config._metadata environment = [html.h2("测试环境")] rows = [] keys = [k for k in metadata.keys()] # if not isinstance(metadata, OrderedDict): # keys.sort() for key in keys: value = metadata[key] if isinstance(value, str) and value.startswith("http"): value = html.a(value, href=value, target="_blank") elif isinstance(value, (list, tuple, set)): value = ", ".join(str(i) for i in sorted(map(str, value))) elif isinstance(value, dict): sorted_dict = {k: value[k] for k in sorted(value)} value = json.dumps(sorted_dict) raw_value_string = raw(str(value)) rows.append(html.tr(html.td(TEST_ENV.get(key)), html.td(raw_value_string))) environment.append(html.table(rows, id="environment")) return environment
def _append_stacktrace_section(log, report): log.append(html.h3('Stacktrace')) stacktrace_p = html.p(class_='stacktrace') for line in str(report.longrepr).splitlines(): separator = line.startswith('_ ' * 10) if separator: stacktrace_p.append(line[:80]) else: exception = line.startswith("E ") if exception: stacktrace_p.append(html.span(raw(escape(line)), class_='error')) else: stacktrace_p.append(raw(escape(line))) stacktrace_p.append(html.br()) log.append(stacktrace_p)
def suite_start(self, data): self.suite_times["start"] = data["time"] self.suite_name = data["source"] with open(os.path.join(base_path, "style.css")) as f: self.head = html.head(html.meta(charset="utf-8"), html.title(data["source"]), html.style(raw(f.read())))
def generate_html(self): generated = datetime.datetime.now() with open(os.path.join(base_path, "main.js")) as main_f: doc = html.html( self.head, html.body( html.script(raw(main_f.read())), html.p( "Report generated on %s at %s" % (generated.strftime("%d-%b-%Y"), generated.strftime("%H:%M:%S")), html.h2("Summary"), html.p( "%i tests ran in %.1f seconds." % ( sum(self.test_count.itervalues()), (self.suite_times["end"] - self.suite_times["start"]) / 1000.0, ), html.br(), html.span("%i passed" % self.test_count["PASS"], class_="pass"), ", ", html.span("%i skipped" % self.test_count["SKIP"], class_="skip"), ", ", html.span("%i failed" % self.test_count["UNEXPECTED_FAIL"], class_="fail"), ", ", html.span("%i errors" % self.test_count["UNEXPECTED_ERROR"], class_="error"), ".", html.br(), html.span( "%i expected failures" % self.test_count["EXPECTED_FAIL"], class_="expected_fail" ), ", ", html.span( "%i unexpected passes" % self.test_count["UNEXPECTED_PASS"], class_="unexpected_pass" ), ".", ), html.h2("Results"), html.table( [ html.thead( html.tr( [ html.th("Result", class_="sortable", col="result"), html.th("Class", class_="sortable", col="class"), html.th("Test Name", class_="sortable", col="name"), html.th("Duration", class_="sortable numeric", col="duration"), html.th("Links"), ] ), id="results-table-head", ), html.tbody(self.result_rows, id="results-table-body"), ], id="results-table", ), ), ), ) return u"<!DOCTYPE html>\n" + doc.unicode(indent=2)
def add_row(self, lineno, text): if text == ['']: text = [raw(' ')] else: text = prepare_line(text, self.tokenizer, self.encoding) self.tbody.append(html.tr(html.td(str(lineno), class_='lineno'), html.td(class_='code', *text)))
def _generate_environment(self, config): if not hasattr(config, "_metadata") or config._metadata is None: return [] metadata = config._metadata environment = [html.h2("Environment")] rows = [] keys = [k for k in metadata.keys()] if not isinstance(metadata, OrderedDict): keys.sort() for key in keys: value = metadata[key] if isinstance(value, str) and value.startswith("http"): value = html.a(value, href=value, target="_blank") elif isinstance(value, (list, tuple, set)): value = ", ".join(str(i) for i in sorted(map(str, value))) elif isinstance(value, dict): key_value_list = [f"'{k}': {value[k]}" for k in sorted(value)] value = ", ".join(key_value_list) value = "{" + value + "}" raw_value_string = raw(str(value)) rows.append(html.tr(html.td(key), html.td(raw_value_string))) environment.append(html.table(rows, id="environment")) return environment
def append_extra_html(self, extra, additional_html, links_html): href = None if extra.get('format') == extras.FORMAT_IMAGE: href = '#' image = 'data:image/png;base64,{0}'.format( extra.get('content')) additional_html.append(html.div( html.a(html.img(src=image), href="#"), class_='image')) elif extra.get('format') == extras.FORMAT_HTML: additional_html.append(html.div(raw(extra.get('content')))) elif extra.get('format') == extras.FORMAT_JSON: href = data_uri(json.dumps(extra.get('content')), mime_type='application/json') elif extra.get('format') == extras.FORMAT_TEXT: href = data_uri(extra.get('content')) elif extra.get('format') == extras.FORMAT_URL: href = extra.get('content') if href is not None: links_html.append(html.a( extra.get('name'), class_=extra.get('format'), href=href, target='_blank')) links_html.append(' ')
def startDocument(self): super(PageHandler, self).startDocument() self.head.append(html.link(type='text/css', rel='stylesheet', href='style.css')) title = self.title[0] breadcrumb = ''.join([unicode(el) for el in self.breadcrumb(title)]) self.body.append(html.div(raw(breadcrumb), class_='breadcrumb'))
def _appendrow(self, result, report): names = mangle_testnames(report.nodeid.split("::")) testclass = (names[:-1]) if self.prefix: testclass.insert(0, self.prefix) testclass = ".".join(testclass) testmethod = names[-1] time = getattr(report, 'duration', 0.0) additional_html = [] if not 'Passed' in result: if report.longrepr: log = html.div(class_='log') for line in str(report.longrepr).splitlines(): line = line.decode('utf8') separator = line.startswith('_ ' * 10) if separator: log.append(line[:80]) else: exception = line.startswith("E ") if exception: log.append(html.span(raw(cgi.escape(line)), class_='error')) else: log.append(raw(cgi.escape(line))) log.append(html.br()) if not os.path.exists(self.screenshot_path): os.makedirs(self.screenshot_path) self.append_screenshot(testmethod, log) additional_html.append(log) output = self._write_captured_output(report) info = output.split(" ") links_html = [] for i in range(0, len(info)): if ("http" in info[i] and "browserstack.com" in info[i]): links_html.append(html.a("link", href=info[i], target='_blank')) links_html.append(' ') self.test_logs.append(html.tr([ html.td(result, class_='col-result'), html.td(testclass, class_='col-class'), html.td(testmethod, class_='col-name'), html.td(round(time), class_='col-duration'), html.td(links_html, class_='col-links'), html.td(additional_html, class_='debug')], class_=result.lower() + ' results-table-row'))
def suite_start(self, data): self.suite_times["start"] = data["time"] self.suite_name = data["source"] with open(os.path.join(base_path, "style.css")) as f: self.head = html.head( html.meta(charset="utf-8"), html.title(data["source"]), html.style(raw(f.read())))
def make_head(self): with open(os.path.join(here, "report.css")) as f: style = html.style(raw(f.read())) return html.head( html.meta(charset="utf-8"), html.title("FirefoxOS Certification Suite Report: %s" % self.results.name), style)
def make_head(self): with open(os.path.join(here, "report.css")) as f: style = html.style(raw(f.read())) return html.head( html.meta(charset="utf-8"), html.title("FirefoxOS Certification Suite Report: %s" % self.results.name), style )
def append_extra_html(self, extra, extra_index, test_index): href = None if extra.get('format') == extras.FORMAT_IMAGE: content = extra.get('content') if content.startswith(('file', 'http')) or \ os.path.isfile(content): if self.self_contained: warnings.warn('Self-contained HTML report ' 'includes link to external ' 'resource: {}'.format(content)) html_div = html.a(html.img(src=content), href=content) elif self.self_contained: src = 'data:{0};base64,{1}'.format(extra.get('mime_type'), content) html_div = html.img(src=src) else: if PY3: content = b64decode(content.encode('utf-8')) else: content = b64decode(content) href = src = self.create_asset(content, extra_index, test_index, extra.get('extension'), 'wb') html_div = html.a(html.img(src=src), href=href) self.additional_html.append(html.div(html_div, class_='image')) elif extra.get('format') == extras.FORMAT_HTML: self.additional_html.append(html.div(raw( extra.get('content')))) elif extra.get('format') == extras.FORMAT_JSON: content = json.dumps(extra.get('content')) if self.self_contained: href = data_uri(content, mime_type=extra.get('mime_type')) else: href = self.create_asset(content, extra_index, test_index, extra.get('extension')) elif extra.get('format') == extras.FORMAT_TEXT: content = extra.get('content') if self.self_contained: href = data_uri(content) else: href = self.create_asset(content, extra_index, test_index, extra.get('extension')) elif extra.get('format') == extras.FORMAT_URL: href = extra.get('content') if href is not None: self.links_html.append( html.a(extra.get('name'), class_=extra.get('format'), href=href, target='_blank')) self.links_html.append(' ')
def generate_diff_result(self): html_report_css = """ h1 { text-align: center; } .container { width: 1170px; margin-left: auto; margin-right: auto; } table { font-family:monospace, "Courier New", Courier; border-collapse: collapse; border: 1px solid gray; width: 100%; margin-left: auto; margin-right: auto; margin-bottom: 20px; } table th { background-color: #e0e0e0; border: 1px solid gray; } table td { font-size: 10px; } /* For diff table */ .diff_header { background-color:#e0e0e0; } td.diff_header { text-align:right; } .diff_next { background-color:#c0c0c0; } .diff_add { background-color:#aaffaa; } .diff_chg { background-color:#ffff77; } .diff_sub { background-color:#ffaaaa; } """ html_report = html.html( html.head( html.meta(name='Content-Type', value='text/html; charset=utf-8'), html.title('Diff Report'), html.style(raw(html_report_css), type='text/css')), html.body(html.h1('Diff Report'), self.div_container)) self.report = html_report return self.report
def viewlocsummary(model): t = html.table( row("total number of lines", model.totallines, raw(" ")), row("number of testlines", model.testlines, percent(model.testlines, model.totallines)), row("number of non-testlines", model.notestlines, percent(model.notestlines, model.totallines)), row("total number of files", model.totalfiles, raw(" ")), row("number of testfiles", model.testfiles, percent(model.testfiles, model.totalfiles)), row("number of non-testfiles", model.notestfiles, percent(model.notestfiles, model.totalfiles)), ) if model.docfiles: t.append(row("number of docfiles", model.docfiles, percent(model.docfiles, model.totalfiles))) t.append(row("number of doclines", model.doclines, percent(model.doclines, model.totallines))) return t
def generate_html(self): generated = datetime.datetime.now() with open(os.path.join(base_path, "main.js")) as main_f: doc = html.html( self.head, html.body( html.script(raw(main_f.read()), ), html.p( 'Report generated on %s at %s' % (generated.strftime('%d-%b-%Y'), generated.strftime('%H:%M:%S')), html.h2('Summary'), html.p( '%i tests ran in %.1f seconds.' % (sum(self.test_count.itervalues()), (self.suite_times["end"] - self.suite_times["start"]) / 1000.), html.br(), html.span('%i passed' % self.test_count["PASS"], class_='pass'), ', ', html.span('%i skipped' % self.test_count["SKIP"], class_='skip'), ', ', html.span('%i failed' % self.test_count["UNEXPECTED_FAIL"], class_='fail'), ', ', html.span('%i errors' % self.test_count["UNEXPECTED_ERROR"], class_='error'), '.', html.br(), html.span('%i expected failures' % self.test_count["EXPECTED_FAIL"], class_='expected_fail'), ', ', html.span('%i unexpected passes' % self.test_count["UNEXPECTED_PASS"], class_='unexpected_pass'), '.'), html.h2('Results'), html.table([ html.thead(html.tr([ html.th( 'Result', class_='sortable', col='result'), html.th( 'Class', class_='sortable', col='class'), html.th('Test Name', class_='sortable', col='name'), html.th('Duration', class_='sortable numeric', col='duration'), html.th('Links') ]), id='results-table-head'), html.tbody(self.result_rows, id='results-table-body') ], id='results-table')))) return u"<!DOCTYPE html>\n" + doc.unicode(indent=2)
def diff_file(self, file1, file2): html_diff = difflib.HtmlDiff(wrapcolumn=HTMLDiffHelper.html_wrapcolumn) with codecs.open(file1, 'r', encoding='utf8') as f1: with codecs.open(file2, 'r', encoding='utf8') as f2: table = html_diff.make_table( f1.readlines(), f2.readlines(), fromdesc=file1, todesc=file2, context=self.contextual_differences) self.div_container.append(raw(table))
def make_errors_table(self, errors): rows = [] for error in errors: error_message = error.get("message", "") log = html.div(class_='log') for line in error_message.splitlines(): separator = line.startswith(' ' * 10) if separator: log.append(line[:80]) else: if line.lower().find("error") != -1 or line.lower().find("exception") != -1: log.append(html.span(raw(cgi.escape(line)), class_='error')) else: log.append(raw(cgi.escape(line))) log.append(html.br()) rows.append(html.tr( html.td(error["level"], class_="log_%s" % error["level"]), html.td(log, class_='log') )) return html.table(rows, id_="errors")
def suite_start(self, data): self.suite_times["start"] = data["time"] self.suite_name = data["source"] with open(os.path.join(base_path, "style.css")) as f: self.head = html.head(html.meta(charset="utf-8"), html.title(data["source"]), html.style(raw(f.read()))) date_format = "%d %b %Y %H:%M:%S" version_info = data.get("version_info") if version_info: self.env["Device identifier"] = version_info.get("device_id") self.env["Device firmware (base)"] = version_info.get( "device_firmware_version_base") self.env["Device firmware (date)"] = (datetime.utcfromtimestamp( int(version_info.get("device_firmware_date"))).strftime( date_format) if "device_firmware_date" in version_info else None) self.env["Device firmware (incremental)"] = version_info.get( "device_firmware_version_incremental") self.env["Device firmware (release)"] = version_info.get( "device_firmware_version_release") self.env["Gaia date"] = (datetime.utcfromtimestamp( int(version_info.get("gaia_date"))).strftime(date_format) if "gaia_date" in version_info else None) self.env["Gecko version"] = version_info.get("application_version") self.env["Gecko build"] = version_info.get("application_buildid") if version_info.get("application_changeset"): self.env["Gecko revision"] = version_info.get( "application_changeset") if version_info.get("application_repository"): self.env["Gecko revision"] = html.a( version_info.get("application_changeset"), href="/".join([ version_info.get("application_repository"), version_info.get("application_changeset") ]), target="_blank") if version_info.get("gaia_changeset"): self.env["Gaia revision"] = html.a( version_info.get("gaia_changeset")[:12], href="https://github.com/mozilla-b2g/gaia/commit/%s" % version_info.get("gaia_changeset"), target="_blank") device_info = data.get("device_info") if device_info: self.env["Device uptime"] = device_info.get("uptime") self.env["Device memory"] = device_info.get("memtotal") self.env["Device serial"] = device_info.get("id")
def append_extra_html(self, extra, extra_index, test_index): href = None if extra.get('format') == extras.FORMAT_IMAGE: if self.self_contained: src = 'data:{0};base64,{1}'.format(extra.get('mime_type'), extra.get('content')) self.additional_html.append( html.div(html.img(src=src), class_='image')) else: content = extra.get('content') if PY3: content = b64decode(content.encode('utf-8')) else: content = b64decode(content) href = src = self.create_asset(content, extra_index, test_index, extra.get('extension'), 'wb') self.additional_html.append( html.div(html.a(html.img(src=src), href=href), class_='image')) elif extra.get('format') == extras.FORMAT_HTML: self.additional_html.append(html.div(raw( extra.get('content')))) elif extra.get('format') == extras.FORMAT_JSON: content = json.dumps(extra.get('content')) if self.self_contained: href = data_uri(content, mime_type=extra.get('mime_type')) else: href = self.create_asset(content, extra_index, test_index, extra.get('extension')) elif extra.get('format') == extras.FORMAT_TEXT: content = extra.get('content') if self.self_contained: href = data_uri(content) else: href = self.create_asset(content, extra_index, test_index, extra.get('extension')) elif extra.get('format') == extras.FORMAT_URL: href = extra.get('content') if href is not None: self.links_html.append( html.a(extra.get('name'), class_=extra.get('format'), href=href, target='_blank')) self.links_html.append(' ')
def make_errors_table(self, errors): rows = [] for error in errors: error_message = error.get("message", "") log = html.div(class_='log') for line in error_message.splitlines(): separator = line.startswith(' ' * 10) if separator: log.append(line[:80]) else: if line.lower().find("error") != -1 or line.lower().find( "exception") != -1: log.append( html.span(raw(cgi.escape(line)), class_='error')) else: log.append(raw(cgi.escape(line))) log.append(html.br()) rows.append( html.tr( html.td(error["level"], class_="log_%s" % error["level"]), html.td(log, class_='log'))) return html.table(rows, id_="errors")
def _get_log_from_report(self, report): log = html.div(class_='log') if report.longrepr: for line in report.longreprtext.splitlines(): separator = line.startswith('_ ' * 10) if separator: log.append(line[:80]) else: exception = line.startswith("E ") if exception: log.append(html.span(raw(escape(line)), class_='error')) else: log.append(raw(escape(line))) log.append(html.br()) for section in report.sections: header, content = map(escape, section) log.append(' {0} '.format(header).center(80, '-')) log.append(html.br()) if ANSI: converter = Ansi2HTMLConverter(inline=False, escaped=False) content = converter.convert(content, full=False) log.append(raw(content)) if len(log) == 0: log = html.div(class_='empty log') log.append('No log output captured.') unicode_log = log.unicode(indent=2) if PY3: # Fix encoding issues, e.g. with surrogates unicode_log = unicode_log.encode('utf-8', errors='xmlcharrefreplace') unicode_log = unicode_log.decode('utf-8') return unicode_log
def _create_request_headers_divs(self, request_headers): # Create request headers main div request_headers_main_div = html.div(class_="request_headers_main") # Create request headers show/hide div request_headers_label_div = html.div( html.span("show/hide headers", class_="hint"), class_="request_headers_label") # Create request headers content div request_headers_content_div = html.div( raw(request_headers), class_="request_headers_content", style="display:none") request_headers_main_div.append(request_headers_label_div) request_headers_main_div.append(request_headers_content_div) return request_headers_main_div
def suite_start(self, data): self.suite_times["start"] = data["time"] self.suite_name = data["source"] with open(os.path.join(base_path, "style.css")) as f: self.head = html.head( html.meta(charset="utf-8"), html.title(data["source"]), html.style(raw(f.read()))) date_format = "%d %b %Y %H:%M:%S" version_info = data.get("version_info") if version_info: self.env["Device identifier"] = version_info.get("device_id") self.env["Device firmware (base)"] = version_info.get("device_firmware_version_base") self.env["Device firmware (date)"] = ( datetime.utcfromtimestamp(int(version_info.get("device_firmware_date"))).strftime(date_format) if "device_firmware_date" in version_info else None) self.env["Device firmware (incremental)"] = version_info.get("device_firmware_version_incremental") self.env["Device firmware (release)"] = version_info.get("device_firmware_version_release") self.env["Gaia date"] = ( datetime.utcfromtimestamp(int(version_info.get("gaia_date"))).strftime(date_format) if "gaia_date" in version_info else None) self.env["Gecko version"] = version_info.get("application_version") self.env["Gecko build"] = version_info.get("application_buildid") if version_info.get("application_changeset"): self.env["Gecko revision"] = version_info.get("application_changeset") if version_info.get("application_repository"): self.env["Gecko revision"] = html.a( version_info.get("application_changeset"), href="/".join([version_info.get("application_repository"), version_info.get("application_changeset")]), target="_blank") if version_info.get("gaia_changeset"): self.env["Gaia revision"] = html.a( version_info.get("gaia_changeset")[:12], href="https://github.com/mozilla-b2g/gaia/commit/%s" % version_info.get("gaia_changeset"), target="_blank") device_info = data.get("device_info") if device_info: self.env["Device uptime"] = device_info.get("uptime") self.env["Device memory"] = device_info.get("memtotal") self.env["Device serial"] = device_info.get("id")
def _create_be_stack_trace_divs(self, be_stack_trace): # Create be stack trace main div be_stack_trace_main_div = html.div(class_="be_stack_trace_main") # Create be stack trace show/hide div be_stack_trace_label_div = html.div("Backend stack trace", html.span(" (show/hide)", class_="hint"), class_="be_stack_trace_label") # Create be stack trace content div be_stack_trace_content_div = html.div( raw(be_stack_trace), class_="be_stack_trace_content", style="display:none") be_stack_trace_main_div.append(be_stack_trace_label_div) be_stack_trace_main_div.append(be_stack_trace_content_div) return be_stack_trace_main_div
def append_extra_html(self, extra, extra_index, test_index): href = None if extra.get("format") == extras.FORMAT_IMAGE: self._append_image(extra, extra_index, test_index) elif extra.get("format") == extras.FORMAT_HTML: self.additional_html.append(html.div(raw(extra.get("content")))) elif extra.get("format") == extras.FORMAT_JSON: content = json.dumps(extra.get("content")) if self.self_contained: href = data_uri(content, mime_type=extra.get("mime_type")) else: href = self.create_asset( content, extra_index, test_index, extra.get("extension") ) elif extra.get("format") == extras.FORMAT_TEXT: content = extra.get("content") if isinstance(content, bytes): content = content.decode("utf-8") if self.self_contained: href = data_uri(content) else: href = self.create_asset( content, extra_index, test_index, extra.get("extension") ) elif extra.get("format") == extras.FORMAT_URL: href = extra.get("content") elif extra.get("format") == extras.FORMAT_VIDEO: self._append_video(extra, extra_index, test_index) if href is not None: self.links_html.append( html.a( extra.get("name"), class_=extra.get("format"), href=href, target="_blank", ) ) self.links_html.append(" ")
def _create_test_step_divs(self, step_name, step_log): # Create test step main div test_step_main_div = html.div(class_="test_step_main") # Create test step show/hide div test_step_label_div = html.div(step_name, html.span(" (show/hide)", class_="hint"), class_="test_step_label", name=step_name) # Create test step content div test_step_content_div = html.div(raw(step_log), class_="test_step_content", name=step_name, style="display:none") test_step_main_div.append(test_step_label_div) test_step_main_div.append(test_step_content_div) return test_step_main_div
def generate_html(self): generated = datetime.utcnow() with open(os.path.join(base_path, "main.js")) as main_f: doc = html.html( self.head, html.body( html.script(raw(main_f.read())), html.p('Report generated on %s at %s' % ( generated.strftime('%d-%b-%Y'), generated.strftime('%H:%M:%S'))), html.h2('Environment'), html.table( [html.tr(html.td(k), html.td(v)) for k, v in sorted(self.env.items()) if v], id='environment'), html.h2('Summary'), html.p('%i tests ran in %.1f seconds.' % (sum(self.test_count.itervalues()), (self.suite_times["end"] - self.suite_times["start"]) / 1000.), html.br(), html.span('%i passed' % self.test_count["PASS"], class_='pass'), ', ', html.span('%i skipped' % self.test_count["SKIP"], class_='skip'), ', ', html.span('%i failed' % self.test_count["UNEXPECTED_FAIL"], class_='fail'), ', ', html.span('%i errors' % self.test_count["UNEXPECTED_ERROR"], class_='error'), '.', html.br(), html.span('%i expected failures' % self.test_count["EXPECTED_FAIL"], class_='expected_fail'), ', ', html.span('%i unexpected passes' % self.test_count["UNEXPECTED_PASS"], class_='unexpected_pass'), '.'), html.h2('Results'), html.table([html.thead( html.tr([ html.th('Result', class_='sortable', col='result'), html.th('Test', class_='sortable', col='name'), html.th('Duration', class_='sortable numeric', col='duration'), html.th('Links')]), id='results-table-head'), html.tbody(self.result_rows, id='results-table-body')], id='results-table'))) return u"<!DOCTYPE html>\n" + doc.unicode(indent=2)
def prepare_module(path, tokenizer, encoding): path = py.path.local(path) try: mod = parse_path(path) except: # XXX don't try to catch SystemExit: it's actually raised by one # of the modules in the py lib on import :( exc, e, tb = py.std.sys.exc_info() del tb raise CompilationException('while compiling %s: %s - %s' % ( path, e.__class__.__name__, e)) lines = [unicode(l, encoding) for l in path.readlines()] enchanter = HtmlEnchanter(mod) ret = [] for i, line in enumerate(lines): text = enchanter.enchant_row(i + 1, line) if text == ['']: text = [raw(' ')] else: text = prepare_line(text, tokenizer, encoding) ret.append(text) return ret
def generate_html(self, results_list): tests = sum([results.testsRun for results in results_list]) failures = sum([len(results.failures) for results in results_list]) expected_failures = sum([len(results.expectedFailures) for results in results_list]) skips = sum([len(results.skipped) for results in results_list]) errors = sum([len(results.errors) for results in results_list]) passes = sum([results.passed for results in results_list]) unexpected_passes = sum([len(results.unexpectedSuccesses) for results in results_list]) test_time = self.elapsedtime.total_seconds() test_logs = [] def _extract_html(test, class_name, duration=0, text="", result="passed", debug=None): cls_name = class_name tc_name = unicode(test) tc_time = duration additional_html = [] debug = debug or {} links_html = [] if result in ["skipped", "failure", "expected failure", "error"]: if debug.get("screenshot"): screenshot = "data:image/png;base64,%s" % debug["screenshot"] additional_html.append(html.div(html.a(html.img(src=screenshot), href="#"), class_="screenshot")) for name, content in debug.items(): try: if "screenshot" in name: href = "#" else: # use base64 to avoid that some browser (such as Firefox, Opera) # treats '#' as the start of another link if the data URL contains. # use 'charset=utf-8' to show special characters like Chinese. href = "data:text/plain;charset=utf-8;base64,%s" % base64.b64encode(content) links_html.append(html.a(name.title(), class_=name, href=href, target="_blank")) links_html.append(" ") except: pass log = html.div(class_="log") for line in text.splitlines(): separator = line.startswith(" " * 10) if separator: log.append(line[:80]) else: if line.lower().find("error") != -1 or line.lower().find("exception") != -1: log.append(html.span(raw(cgi.escape(line)), class_="error")) else: log.append(raw(cgi.escape(line))) log.append(html.br()) additional_html.append(log) test_logs.append( html.tr( [ html.td(result.title(), class_="col-result"), html.td(cls_name, class_="col-class"), html.td(tc_name, class_="col-name"), html.td(tc_time, class_="col-duration"), html.td(links_html, class_="col-links"), html.td(additional_html, class_="debug"), ], class_=result.lower() + " results-table-row", ) ) for results in results_list: for test in results.tests_passed: _extract_html(test.name, test.test_class) for result in results.skipped: _extract_html(result.name, result.test_class, text="\n".join(result.output), result="skipped") for result in results.failures: _extract_html( result.name, result.test_class, text="\n".join(result.output), result="failure", debug=result.debug ) for result in results.expectedFailures: _extract_html( result.name, result.test_class, text="\n".join(result.output), result="expected failure", debug=result.debug, ) for test in results.unexpectedSuccesses: _extract_html(test.name, test.test_class, result="unexpected pass") for result in results.errors: _extract_html( result.name, result.test_class, text="\n".join(result.output), result="error", debug=result.debug ) generated = datetime.datetime.now() doc = html.html( html.head( html.meta(charset="utf-8"), html.title("Test Report"), html.style( raw( pkg_resources.resource_string(__name__, os.path.sep.join(["resources", "report", "style.css"])) ), type="text/css", ), ), html.body( html.script( raw( pkg_resources.resource_string(__name__, os.path.sep.join(["resources", "report", "jquery.js"])) ), type="text/javascript", ), html.script( raw(pkg_resources.resource_string(__name__, os.path.sep.join(["resources", "report", "main.js"]))), type="text/javascript", ), html.p( "Report generated on %s at %s by %s %s" % (generated.strftime("%d-%b-%Y"), generated.strftime("%H:%M:%S"), __name__, __version__) ), html.h2("Summary"), html.p( "%i tests ran in %i seconds." % (tests, test_time), html.br(), html.span("%i passed" % passes, class_="passed"), ", ", html.span("%i skipped" % skips, class_="skipped"), ", ", html.span("%i failed" % failures, class_="failed"), ", ", html.span("%i errors" % errors, class_="error"), ".", html.br(), html.span("%i expected failures" % expected_failures, class_="expected failure"), ", ", html.span("%i unexpected passes" % unexpected_passes, class_="unexpected pass"), ".", ), html.h2("Results"), html.table( [ html.thead( html.tr( [ html.th("Result", class_="sortable", col="result"), html.th("Class", class_="sortable", col="class"), html.th("Test Name", class_="sortable", col="name"), html.th("Duration", class_="sortable numeric", col="duration"), html.th("Links"), ] ), id="results-table-head", ), html.tbody(test_logs, id="results-table-body"), ], id="results-table", ), ), ) return doc.unicode(indent=2)
def _appendrow(self, result, report): import pytest_mozwebqa (testclass, testmethod) = pytest_mozwebqa.split_class_and_test_names(report.nodeid) time = getattr(report, 'duration', 0.0) links = {} if hasattr(report, 'debug') and any(report.debug.values()): (relative_path, full_path) = self._debug_paths(testclass, testmethod) if report.debug['screenshots']: filename = 'screenshot.png' f = open(os.path.join(full_path, filename), 'wb') f.write(base64.decodestring(report.debug['screenshots'][-1])) links.update({'Screenshot': os.path.join(relative_path, filename)}) if report.debug['html']: filename = 'html.txt' f = open(os.path.join(full_path, filename), 'wb') f.write(report.debug['html'][-1]) links.update({'HTML': os.path.join(relative_path, filename)}) # Log may contain passwords, etc so we only capture it for tests marked as public if report.debug['logs'] and 'public' in report.keywords: filename = 'log.txt' f = open(os.path.join(full_path, filename), 'wb') f.write(report.debug['logs'][-1]) links.update({'Log': os.path.join(relative_path, filename)}) if report.debug['network_traffic']: filename = 'networktraffic.json' f = open(os.path.join(full_path, filename), 'wb') f.write(report.debug['network_traffic'][-1]) links.update({'Network Traffic': os.path.join(relative_path, filename)}) if report.debug['urls']: links.update({'Failing URL': report.debug['urls'][-1]}) if self.config.option.sauce_labs_credentials_file and hasattr(report, 'session_id'): self.sauce_labs_job = sauce_labs.Job(report.session_id) if hasattr(self, 'sauce_labs_job'): links['Sauce Labs Job'] = self.sauce_labs_job.url links_html = [] for name, path in links.iteritems(): links_html.append(html.a(name, href=path)) links_html.append(' ') additional_html = [] if not 'Passed' in result: if hasattr(self, 'sauce_labs_job'): additional_html.append(self.sauce_labs_job.video_html) if 'Screenshot' in links: additional_html.append( html.div( html.a(html.img(src=links['Screenshot']), href=links['Screenshot']), class_='screenshot')) if report.longrepr: log = html.div(class_='log') for line in str(report.longrepr).splitlines(): separator = line.startswith('_ ' * 10) if separator: log.append(line[:80]) else: exception = line.startswith("E ") if exception: log.append(html.span(raw(cgi.escape(line)), class_='error')) else: log.append(raw(cgi.escape(line))) log.append(html.br()) additional_html.append(log) self.test_logs.append(html.tr([ html.td(result, class_='col-result'), html.td(testclass, class_='col-class'), html.td(testmethod, class_='col-name'), html.td(round(time), class_='col-duration'), html.td(links_html, class_='col-links'), html.td(additional_html, class_='debug')], class_=result.lower() + ' results-table-row'))
def pytest_sessionfinish(self, session, exitstatus, __multicall__): self._make_report_dir() logfile = py.std.codecs.open(self.logfile, 'w', encoding='utf-8') suite_stop_time = time.time() suite_time_delta = suite_stop_time - self.suite_start_time numtests = self.passed + self.failed + self.xpassed + self.xfailed server = self.config.option.sauce_labs_credentials_file and \ 'Sauce Labs' or 'http://%s:%s' % (self.config.option.host, self.config.option.port) browser = self.config.option.browser_name and \ self.config.option.browser_version and \ self.config.option.platform and \ '%s %s on %s' % (str(self.config.option.browser_name).title(), self.config.option.browser_version, str(self.config.option.platform).title()) or \ self.config.option.environment or \ self.config.option.browser generated = datetime.datetime.now() configuration = { 'Base URL': self.config.option.base_url, 'Build': self.config.option.build, 'Selenium API': self.config.option.api, 'Driver': self.config.option.driver, 'Firefox Path': self.config.option.firefox_path, 'Google Chrome Path': self.config.option.chrome_path, 'Selenium Server': server, 'Browser': browser, 'Timeout': self.config.option.webqatimeout, 'Capture Network Traffic': self.config.option.capture_network, 'Credentials': self.config.option.credentials_file, 'Sauce Labs Credentials': self.config.option.sauce_labs_credentials_file} import pytest_mozwebqa doc = html.html( html.head( html.title('Test Report'), html.style( 'body {font-family: Helvetica, Arial, sans-serif; font-size: 12px}\n', 'body * {box-sizing: -moz-border-box; box-sizing: -webkit-border-box; box-sizing: border-box}\n', 'a {color: #999}\n', 'h2 {font-size: 16px}\n', 'table {border: 1px solid #e6e6e6; color: #999; font-size: 12px; border-collapse: collapse}\n', '#configuration tr:nth-child(odd) {background-color: #f6f6f6}\n', '#results {width:100%}\n', 'th, td {padding: 5px; border: 1px solid #E6E6E6; text-align: left}\n', 'th {font-weight: bold}\n', 'tr.passed, tr.skipped, tr.xfailed, tr.error, tr.failed, tr.xpassed {color: inherit}\n' 'tr.passed + tr.additional {display: none}\n', '.passed {color: green}\n', '.skipped, .xfailed {color: orange}\n', '.error, .failed, .xpassed {color: red}\n', '.log:only-child {height: inherit}\n', raw('.log {background-color: #e6e6e6; border: 1px solid #e6e6e6; color: black; display: block; font-family: "Courier New", Courier, monospace; height: 230px; overflow-y: scroll; padding: 5px; white-space:pre-wrap}\n'), '.screenshot, .video {border: 1px solid #e6e6e6; float:right; height:240px; margin-left:5px; overflow:hidden; width:320px}\n', '.screenshot img {width: 320px}')), html.body( html.p('Report generated on %s at %s by pytest-mozwebqa %s' % ( generated.strftime('%d-%b-%Y'), generated.strftime('%H:%M:%S'), pytest_mozwebqa.__version__)), html.h2('Configuration'), html.table( [html.tr(html.td(k), html.td(v)) for k, v in sorted(configuration.items()) if v], id='configuration'), html.h2('Summary'), html.p( '%i tests ran in %i seconds.' % (numtests, suite_time_delta), html.br(), html.span('%i passed' % self.passed, class_='passed'), ', ', html.span('%i skipped' % self.skipped, class_='skipped'), ', ', html.span('%i failed' % self.failed, class_='failed'), ', ', html.span('%i errors' % self.errors, class_='error'), '.', html.br(), html.span('%i expected failures' % self.xfailed, class_='skipped'), ', ', html.span('%i unexpected passes' % self.xpassed, class_='failed'), '.'), html.h2('Results'), html.table( html.tr(html.th('Result'), html.th('Class'), html.th('Name'), html.th('Duration'), html.th('Links')), *self.test_logs, id='results'))) logfile.write(doc.unicode(indent=2)) logfile.close()
def append_extra_html(self, extra, extra_index, test_index): href = None if extra.get('format') == extras.FORMAT_IMAGE: content = extra.get('content') try: is_uri_or_path = (content.startswith(('file', 'http')) or isfile(content)) except ValueError: # On Windows, os.path.isfile throws this exception when # passed a b64 encoded image. is_uri_or_path = False if is_uri_or_path: if self.self_contained: warnings.warn('Self-contained HTML report ' 'includes link to external ' 'resource: {}'.format(content)) html_div = html.a(html.img(src=content), href=content) elif self.self_contained: src = 'data:{0};base64,{1}'.format( extra.get('mime_type'), content) html_div = html.img(src=src) else: if PY3: content = b64decode(content.encode('utf-8')) else: content = b64decode(content) href = src = self.create_asset( content, extra_index, test_index, extra.get('extension'), 'wb') html_div = html.a(html.img(src=src), href=href) self.additional_html.append(html.div(html_div, class_='image')) elif extra.get('format') == extras.FORMAT_HTML: self.additional_html.append(html.div( raw(extra.get('content')))) elif extra.get('format') == extras.FORMAT_JSON: content = json.dumps(extra.get('content')) if self.self_contained: href = data_uri(content, mime_type=extra.get('mime_type')) else: href = self.create_asset(content, extra_index, test_index, extra.get('extension')) elif extra.get('format') == extras.FORMAT_TEXT: content = extra.get('content') if isinstance(content, bytes): content = content.decode('utf-8') if self.self_contained: href = data_uri(content) else: href = self.create_asset(content, extra_index, test_index, extra.get('extension')) elif extra.get('format') == extras.FORMAT_URL: href = extra.get('content') if href is not None: self.links_html.append(html.a( extra.get('name'), class_=extra.get('format'), href=href, target='_blank')) self.links_html.append(' ')
def _generate_report(self, session): suite_stop_time = time.time() suite_time_delta = suite_stop_time - self.suite_start_time numtests = self.passed + self.failed + self.xpassed + self.xfailed generated = datetime.datetime.now() self.style_css = pkg_resources.resource_string( __name__, os.path.join('resources', 'style.css')) if PY3: self.style_css = self.style_css.decode('utf-8') if ANSI: ansi_css = [ '\n/******************************', ' * ANSI2HTML STYLES', ' ******************************/\n'] ansi_css.extend([str(r) for r in style.get_styles()]) self.style_css += '\n'.join(ansi_css) # <DF> Add user-provided CSS for path in self.config.getoption('css') or []: self.style_css += '\n/******************************' self.style_css += '\n * CUSTOM CSS' self.style_css += '\n * {}'.format(path) self.style_css += '\n ******************************/\n\n' with open(path, 'r') as f: self.style_css += f.read() css_href = '{0}/{1}'.format('assets', 'style.css') html_css = html.link(href=css_href, rel='stylesheet', type='text/css') if self.self_contained: html_css = html.style(raw(self.style_css)) head = html.head( html.meta(charset='utf-8'), html.title('Test Report'), html_css) class Outcome: def __init__(self, outcome, total=0, label=None, test_result=None, class_html=None): self.outcome = outcome self.label = label or outcome self.class_html = class_html or outcome self.total = total self.test_result = test_result or outcome self.generate_checkbox() self.generate_summary_item() def generate_checkbox(self): checkbox_kwargs = {'data-test-result': self.test_result.lower()} if self.total == 0: checkbox_kwargs['disabled'] = 'true' self.checkbox = html.input(type='checkbox', checked='true', onChange='filter_table(this)', name='filter_checkbox', class_='filter', hidden='true', **checkbox_kwargs) def generate_summary_item(self): self.summary_item = html.span('{0} {1}'. format(self.total, self.label), class_=self.class_html) outcomes = [Outcome('passed', self.passed), Outcome('skipped', self.skipped), Outcome('failed', self.failed), Outcome('error', self.errors, label='errors'), Outcome('xfailed', self.xfailed, label='expected failures'), Outcome('xpassed', self.xpassed, label='unexpected passes')] if self.rerun is not None: outcomes.append(Outcome('rerun', self.rerun)) summary = [html.p( '{0} tests ran in {1:.2f} seconds. '.format( numtests, suite_time_delta)), html.p('(Un)check the boxes to filter the results.', class_='filter', hidden='true')] for i, outcome in enumerate(outcomes, start=1): summary.append(outcome.checkbox) summary.append(outcome.summary_item) if i < len(outcomes): summary.append(', ') cells = [ html.th('Result', class_='sortable result initial-sort', col='result'), html.th('Test', class_='sortable', col='name'), html.th('Duration', class_='sortable numeric', col='duration'), html.th('Links')] session.config.hook.pytest_html_results_table_header(cells=cells) results = [html.h2('Results'), html.table([html.thead( html.tr(cells), html.tr([ html.th('No results found. Try to check the filters', colspan=len(cells))], id='not-found-message', hidden='true'), id='results-table-head'), self.test_logs], id='results-table')] main_js = pkg_resources.resource_string( __name__, os.path.join('resources', 'main.js')) if PY3: main_js = main_js.decode('utf-8') body = html.body( html.script(raw(main_js)), html.h1(os.path.basename(session.config.option.htmlpath)), html.p('Report generated on {0} at {1} by'.format( generated.strftime('%d-%b-%Y'), generated.strftime('%H:%M:%S')), html.a(' pytest-html', href=__pypi_url__), ' v{0}'.format(__version__)), onLoad='init()') body.extend(self._generate_environment(session.config)) summary_prefix, summary_postfix = [], [] session.config.hook.pytest_html_results_summary( prefix=summary_prefix, summary=summary, postfix=summary_postfix) body.extend([html.h2('Summary')] + summary_prefix + summary + summary_postfix) body.extend(results) doc = html.html(head, body) unicode_doc = u'<!DOCTYPE html>\n{0}'.format(doc.unicode(indent=2)) if PY3: # Fix encoding issues, e.g. with surrogates unicode_doc = unicode_doc.encode('utf-8', errors='xmlcharrefreplace') unicode_doc = unicode_doc.decode('utf-8') return unicode_doc
def _appendrow(self, result, report): time = getattr(report, 'duration', 0.0) additional_html = [] links_html = [] for extra in getattr(report, 'extra', []): href = None if extra.get('format') == extras.FORMAT_IMAGE: href = '#' image = 'data:image/png;base64,{0}'.format( extra.get('content')) additional_html.append(html.div( html.a(html.img(src=image), href="#"), class_='image')) elif extra.get('format') == extras.FORMAT_HTML: additional_html.append(html.div(raw(extra.get('content')))) elif extra.get('format') == extras.FORMAT_JSON: href = data_uri(json.dumps(extra.get('content')), mime_type='application/json') elif extra.get('format') == extras.FORMAT_TEXT: href = data_uri(extra.get('content')) elif extra.get('format') == extras.FORMAT_URL: href = extra.get('content') if href is not None: links_html.append(html.a( extra.get('name'), class_=extra.get('format'), href=href, target='_blank')) links_html.append(' ') if report.longrepr: log = html.div(class_='log') for line in str(report.longrepr).splitlines(): if not PY3: line = line.decode('utf-8') separator = line.startswith('_ ' * 10) if separator: log.append(line[:80]) else: exception = line.startswith("E ") if exception: log.append(html.span(raw(escape(line)), class_='error')) else: log.append(raw(escape(line))) log.append(html.br()) for header, content in report.sections: log.append(' {0} '.format(header).center(80, '-')) log.append(html.br()) log.append(content) else: log = html.div(class_='empty log') log.append('No log output captured.') additional_html.append(log) test_id = report.nodeid if report.when != 'call': test_id = '::'.join([report.nodeid, report.when]) self.test_logs.append(html.tr([ html.td(result, class_='col-result'), html.td(test_id, class_='col-name'), html.td('{0:.2f}'.format(time), class_='col-duration'), html.td(links_html, class_='col-links'), html.td(additional_html, class_='extra')], class_=result.lower() + ' results-table-row'))
def pytest_sessionfinish(self, session): if not os.path.exists(os.path.dirname(self.logfile)): os.makedirs(os.path.dirname(self.logfile)) logfile = open(self.logfile, 'w', encoding='utf-8') suite_stop_time = time.time() suite_time_delta = suite_stop_time - self.suite_start_time numtests = self.passed + self.failed + self.xpassed + self.xfailed generated = datetime.datetime.now() style_css = pkg_resources.resource_string( __name__, os.path.join('resources', 'style.css')) if PY3: style_css = style_css.decode('utf-8') head = html.head( html.meta(charset='utf-8'), html.title('Test Report'), html.style(raw(style_css))) summary = [html.h2('Summary'), html.p( '{0} tests ran in {1:.2f} seconds.'.format( numtests, suite_time_delta), html.br(), html.span('{0} passed'.format( self.passed), class_='passed'), ', ', html.span('{0} skipped'.format( self.skipped), class_='skipped'), ', ', html.span('{0} failed'.format( self.failed), class_='failed'), ', ', html.span('{0} errors'.format( self.errors), class_='error'), '.', html.br(), html.span('{0} expected failures'.format( self.xfailed), class_='skipped'), ', ', html.span('{0} unexpected passes'.format( self.xpassed), class_='failed'), '.')] results = [html.h2('Results'), html.table([html.thead( html.tr([ html.th('Result', class_='sortable initial-sort result', col='result'), html.th('Test', class_='sortable', col='name'), html.th('Duration', class_='sortable numeric', col='duration'), html.th('Links')]), id='results-table-head'), html.tbody(*self.test_logs, id='results-table-body')], id='results-table')] main_js = pkg_resources.resource_string( __name__, os.path.join('resources', 'main.js')) if PY3: main_js = main_js.decode('utf-8') body = html.body( html.script(raw(main_js)), html.p('Report generated on {0} at {1}'.format( generated.strftime('%d-%b-%Y'), generated.strftime('%H:%M:%S')))) if session.config._environment: environment = set(session.config._environment) body.append(html.h2('Environment')) body.append(html.table( [html.tr(html.td(e[0]), html.td(e[1])) for e in sorted( environment, key=lambda e: e[0]) if e[1]], id='environment')) body.extend(summary) body.extend(results) doc = html.html(head, body) logfile.write('<!DOCTYPE html>') unicode_doc = doc.unicode(indent=2) if PY3: # Fix encoding issues, e.g. with surrogates unicode_doc = unicode_doc.encode('utf-8', errors='xmlcharrefreplace') unicode_doc = unicode_doc.decode('utf-8') logfile.write(unicode_doc) logfile.close()
def _generate_report(self, session): suite_stop_time = time.time() suite_time_delta = suite_stop_time - self.suite_start_time numtests = self.passed + self.failed + self.xpassed + self.xfailed generated = datetime.datetime.now() self.style_css = pkg_resources.resource_string( __name__, os.path.join("resources", "style.css")).decode("utf-8") if ANSI: ansi_css = [ "\n/******************************", " * ANSI2HTML STYLES", " ******************************/\n", ] ansi_css.extend([str(r) for r in style.get_styles()]) self.style_css += "\n".join(ansi_css) # <DF> Add user-provided CSS for path in self.config.getoption("css"): self.style_css += "\n/******************************" self.style_css += "\n * CUSTOM CSS" self.style_css += f"\n * {path}" self.style_css += "\n ******************************/\n\n" with open(path, "r") as f: self.style_css += f.read() css_href = "assets/style.css" html_css = html.link(href=css_href, rel="stylesheet", type="text/css") if self.self_contained: html_css = html.style(raw(self.style_css)) head = html.head( html.meta(charset="utf-8"), html.meta( content= "width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=2.0, user-scalable=no" ), html.title("Test Report"), html_css) class Outcome: def __init__(self, outcome, total=0, label=None, test_result=None, class_html=None): self.outcome = outcome self.label = label or outcome self.class_html = class_html or outcome self.total = total self.test_result = test_result or outcome self.generate_checkbox() self.generate_summary_item() def generate_checkbox(self): checkbox_kwargs = { "data-test-result": self.test_result.lower() } if self.total == 0: checkbox_kwargs["disabled"] = "true" self.checkbox = html.input( type="checkbox", checked="true", onChange="filter_table(this)", name="filter_checkbox", class_="filter", hidden="true", **checkbox_kwargs, ) def generate_summary_item(self): self.summary_item = html.span(f"{self.total} {self.label}", class_=self.class_html) outcomes = [ Outcome("passed", self.passed), Outcome("skipped", self.skipped), Outcome("failed", self.failed), Outcome("error", self.errors, label="errors"), Outcome("xfailed", self.xfailed, label="expected failures"), Outcome("xpassed", self.xpassed, label="unexpected passes"), ] if self.rerun is not None: outcomes.append(Outcome("rerun", self.rerun)) summary = [ html.p( f"{numtests} tests ran in {suite_time_delta:.2f} seconds. "), html.p( "(Un)check the boxes to filter the results.", class_="filter", hidden="true", ), ] for i, outcome in enumerate(outcomes, start=1): summary.append(outcome.checkbox) summary.append(outcome.summary_item) if i < len(outcomes): summary.append(", ") cells = [ html.th("Result", class_="sortable result initial-sort", col="result"), html.th("Test", class_="sortable", col="name"), html.th("Duration", class_="sortable numeric", col="duration"), html.th("Links"), ] session.config.hook.pytest_html_results_table_header(cells=cells) results = [ html.h2("Results"), html.table( [ html.thead( html.tr(cells), html.tr( [ html.th( "No results found. Try to check the filters", colspan=len(cells), ) ], id="not-found-message", hidden="true", ), id="results-table-head", ), self.test_logs, ], id="results-table", ), ] main_js = pkg_resources.resource_string( __name__, os.path.join("resources", "main.js")).decode("utf-8") report_title = self.logfile.split('/')[-4] + '测试报告如下:' body = html.body( html.script(raw(main_js)), # html.h1(os.path.basename(self.logfile)), html.h1(report_title), # 测试报告title html. p("Report generated on {} at {}.".format( generated.strftime("%Y-%m-%d"), generated.strftime("%H:%M:%S")), # html.a("pytest-html", href=__pypi_url__), # f" v{__version__}", ), onLoad="init()", ) body.extend(self._generate_environment(session.config)) summary_prefix, summary_postfix = [], [] session.config.hook.pytest_html_results_summary( prefix=summary_prefix, summary=summary, postfix=summary_postfix) body.extend([html.h2("Summary")] + summary_prefix + summary + summary_postfix) body.extend(results) doc = html.html(head, body) unicode_doc = "<!DOCTYPE html>\n{}".format(doc.unicode(indent=2)) # Fix encoding issues, e.g. with surrogates unicode_doc = unicode_doc.encode("utf-8", errors="xmlcharrefreplace") return unicode_doc.decode("utf-8")
def generate_html(self, results_list): tests = sum([results.testsRun for results in results_list]) failures = sum([len(results.failures) for results in results_list]) expected_failures = sum([len(results.expectedFailures) for results in results_list]) skips = sum([len(results.skipped) for results in results_list]) errors = sum([len(results.errors) for results in results_list]) passes = sum([results.passed for results in results_list]) unexpected_passes = sum([len(results.unexpectedSuccesses) for results in results_list]) test_time = self.elapsedtime.total_seconds() test_logs = [] def _extract_html(test, class_name, duration=0, text='', result='passed', debug=None): cls_name = class_name tc_name = unicode(test) tc_time = duration additional_html = [] debug = debug or {} links_html = [] if result in ['skipped', 'failure', 'expected failure', 'error']: if debug.get('screenshot'): screenshot = 'data:image/png;base64,%s' % debug['screenshot'] additional_html.append(html.div( html.a(html.img(src=screenshot), href="#"), class_='screenshot')) for name, content in debug.items(): try: if 'screenshot' in name: href = '#' else: # use base64 to avoid that some browser (such as Firefox, Opera) # treats '#' as the start of another link if the data URL contains. # use 'charset=utf-8' to show special characters like Chinese. href = 'data:text/plain;charset=utf-8;base64,%s' % base64.b64encode(content) links_html.append(html.a( name.title(), class_=name, href=href, target='_blank')) links_html.append(' ') except: pass log = html.div(class_='log') for line in text.splitlines(): separator = line.startswith(' ' * 10) if separator: log.append(line[:80]) else: if line.lower().find("error") != -1 or line.lower().find("exception") != -1: log.append(html.span(raw(cgi.escape(line)), class_='error')) else: log.append(raw(cgi.escape(line))) log.append(html.br()) additional_html.append(log) test_logs.append(html.tr([ html.td(result.title(), class_='col-result'), html.td(cls_name, class_='col-class'), html.td(tc_name, class_='col-name'), html.td(tc_time, class_='col-duration'), html.td(links_html, class_='col-links'), html.td(additional_html, class_='debug')], class_=result.lower() + ' results-table-row')) for results in results_list: for test in results.tests_passed: _extract_html(test.name, test.test_class) for result in results.skipped: _extract_html(result.name, result.test_class, text='\n'.join(result.output), result='skipped') for result in results.failures: _extract_html(result.name, result.test_class, text='\n'.join(result.output), result='failure', debug=result.debug) for result in results.expectedFailures: _extract_html(result.name, result.test_class, text='\n'.join(result.output), result='expected failure', debug=result.debug) for test in results.unexpectedSuccesses: _extract_html(test.name, test.test_class, result='unexpected pass') for result in results.errors: _extract_html(result.name, result.test_class, text='\n'.join(result.output), result='error', debug=result.debug) generated = datetime.datetime.now() doc = html.html( html.head( html.meta(charset='utf-8'), html.title('Test Report'), html.style(raw(pkg_resources.resource_string( __name__, os.path.sep.join(['resources', 'report', 'style.css']))), type='text/css')), html.body( html.script(raw(pkg_resources.resource_string( __name__, os.path.sep.join(['resources', 'report', 'jquery.js']))), type='text/javascript'), html.script(raw(pkg_resources.resource_string( __name__, os.path.sep.join(['resources', 'report', 'main.js']))), type='text/javascript'), html.p('Report generated on %s at %s by %s %s' % ( generated.strftime('%d-%b-%Y'), generated.strftime('%H:%M:%S'), __name__, __version__)), html.h2('Summary'), html.p('%i tests ran in %i seconds.' % (tests, test_time), html.br(), html.span('%i passed' % passes, class_='passed'), ', ', html.span('%i skipped' % skips, class_='skipped'), ', ', html.span('%i failed' % failures, class_='failed'), ', ', html.span('%i errors' % errors, class_='error'), '.', html.br(), html.span('%i expected failures' % expected_failures, class_='expected failure'), ', ', html.span('%i unexpected passes' % unexpected_passes, class_='unexpected pass'), '.'), html.h2('Results'), html.table([html.thead( html.tr([ html.th('Result', class_='sortable', col='result'), html.th('Class', class_='sortable', col='class'), html.th('Test Name', class_='sortable', col='name'), html.th('Duration', class_='sortable numeric', col='duration'), html.th('Links')]), id='results-table-head'), html.tbody(test_logs, id='results-table-body')], id='results-table'))) return doc.unicode(indent=2)