""" 这是一个用于unittest框架的TestRunner,用它可以生成HTML测试报告,包含用例执行情况以 及图表; 使用它最简单方法是调用main方法,例如: ------------------------------------------------------------------------ # import unittest # import HTMLTestRunner # 在这里, # 定义你的测试代码。 # if __name__ == '__main__': # HTMLTestRunner.main() ------------------------------------------------------------------------ """ import os, re, sys, io, time, datetime, platform, json, unittest, logging, shutil from xml.sax import saxutils class OutputRedirector(object): """ 重定向stdout和stderr以便于在测试过程中捕获输出 """ def __init__(self, fp): self.fp = fp def write(self, s): self.fp.write(s) def writelines(self, lines): self.fp.writelines(lines) def flush(self): self.fp.flush() stdout_redirector = OutputRedirector(sys.stdout) stderr_redirector = OutputRedirector(sys.stderr) class ReportTemplate: """ 报告模板 """ STATUS = { 0: '通过', 1: '失败', 2: '错误', } DEFAULT_TITLE = '自动化测试报告' DEFAULT_DESCRIPTION = '暂无描述' # ------------------------------------------------------------------------ # 网页模板 # 变量列表 title, generator, styles, header, report, footer HTML_TMPL = """ %(title)s %(styles)s %(header)s %(report)s %(footer)s """ # ------------------------------------------------------------------------ # 网页样式 STYLES_TMPL = """ """ # ------------------------------------------------------------------------ # 报告标题、测试信息、报告描述、截图显示容器、图表容器 # 变量列表 title, parameters, description, logo, sign HEADER_TMPL = """
%(sign)s

%(title)s

%(parameters)s

%(description)s

""" # ------------------------------------------------------------------------ # 测试信息 # 变量列表 name, value HEADER_ATTRIBUTE_TMPL = """

%(name)s : %(value)s

""" # ------------------------------------------------------------------------ # 报告模板 # 变量列表 test_list, counts, passed, failed, errors ,passrate REPORT_TMPL = """

概要 %(passrate)s 通过 %(passed)s 失败 %(failed)s 错误 %(errors)s 全部 %(counts)s

%(test_list)s
测试用例 说明 总计 通过 失败 错误 耗时 详细
总计 %(counts)s %(passed)s %(failed)s %(errors)s %(time_usage)s 通过:%(passrate)s
""" # ------------------------------------------------------------------------ # 变量列表 style, desc, counts, passed, failed, errors, cid REPORT_CLASS_TMPL = """ %(name)s %(docs)s %(counts)s %(passed)s %(failed)s %(errors)s %(time_usage)s 查看全部 """ # ------------------------------------------------------------------------ # 失败样式(有截图列) # 变量列表 tid, Class, style, desc, status REPORT_TEST_WITH_OUTPUT_TMPL_1 = """
%(name)s
%(docs)s
%(script)s
截图信息
%(screenshot)s
""" # ------------------------------------------------------------------------ # 失败样式(无截图列) # 变量列表 tid, Class, style, desc, status REPORT_TEST_WITH_OUTPUT_TMPL_0 = """
%(name)s
%(docs)s
%(script)s
""" # ------------------------------------------------------------------------ # 通过样式 # 变量列表 tid, Class, style, desc, status REPORT_TEST_NO_OUTPUT_TMPL = """
%(name)s
%(docs)s """ # ------------------------------------------------------------------------ # 测试输出内容 REPORT_TEST_OUTPUT_TMPL = '%(id)s:' + "\n" + '%(output)s' # ------------------------------------------------------------------------ # 页面底部、返回顶部 FOOTER_TMPL = """ """ # ------------------------------------------------------------------------ def _findMark(mark='', data=''): return re.findall('\\[' + mark + '](.*?)\\[/' + mark + ']' + '*?', data) def _makeMark(mark='', cont=''): return '[' + mark + ']' + cont + '[/' + mark + ']' def _Color(fc=0, bc=0, bo=0, text=''): b = 'PYCHARM_HOSTED' in os.environ.keys() return "\033[" + str(bo) + ['', ';' + str(fc)][fc > 0] + ['', ';' + str(bc)][bc > 0] + "m" + text + "\033[0m" if b else text class _TestResult(unittest.TestResult): def __init__(self, verbosity=1, log='', success_log_in_report=False): super().__init__(verbosity=verbosity) self.verbosity = verbosity self.log = log self.success_log_in_report = success_log_in_report self.fh = None self.lh = None self.ch = None self.loggerStream = None self.outputBuffer = None self.stdout0 = None self.stderr0 = None self.passed_count = 0 self.failed_count = 0 self.errors_count = 0 self.stime = None self.etime = None self.passrate = float(0) # 分类统计数量耗时 self.casesort = {} # 增加失败用例合集 self.failedCase = '' self.failedCaseList = [] # 增加错误用例合集 self.errorsCase = '' self.errorsCaseList = [] self.logger = logging.getLogger('test') # result is a list of result in 4 tuple: # 1. result code (0: passed; 1: failed; 2: errors), # 2. testcase object, # 3. test output (byte string), # 4. stack trace. self.result = [] def sortCount(self, cls, res, dur=float(0)): """ 分类统计 """ s = self.casesort if cls not in s.keys(): s[cls] = {'p': 0, 'f': 0, 'e': 0, 'd': 0} if str(res).upper().startswith('P'): s[cls]['p'] += 1 if str(res).upper().startswith('F'): s[cls]['f'] += 1 if str(res).upper().startswith('E'): s[cls]['e'] += 1 s[cls]['d'] += dur return True def startTest(self, test): """ 单条用例执行开始前的动作 """ if self.verbosity >= 2: sys.stderr.write(_Color(fc=39, bc=4, bo=1, text='%-79s' % ('%04d @ Testing...' % (len(self.result) + 1))) + "\n") # 开始测试 super().startTest(test) # 终端日志 if self.verbosity >= 2: self.ch = logging.StreamHandler(sys.stderr) self.ch.setLevel(logging.DEBUG) self.ch.setFormatter(logging.Formatter(fmt='{asctime} - {levelname[0]}: {message}', style='{')) self.logger.addHandler(self.ch) # 报告日志 if True: self.loggerStream = io.StringIO() self.lh = logging.StreamHandler(self.loggerStream) self.lh.setLevel(logging.DEBUG) self.lh.setFormatter(logging.Formatter(fmt='{asctime} - {levelname[0]}: {message}', style='{')) self.logger.addHandler(self.lh) # 文件日志 if self.log: self.fh = logging.FileHandler(filename=self.log, mode='a+', encoding='utf-8') self.fh.setLevel(logging.DEBUG) self.fh.setFormatter(logging.Formatter(fmt='{asctime} - {levelname[0]}: {module}.{funcName}\t{message}', style='{')) self.logger.addHandler(self.fh) # 用例执行过程中的输出 # Just one buffer for both stdout and stderr self.outputBuffer = io.StringIO() stdout_redirector.fp = self.outputBuffer stderr_redirector.fp = self.outputBuffer self.stdout0 = sys.stdout self.stderr0 = sys.stderr sys.stdout = stdout_redirector sys.stderr = stderr_redirector self.stime = round(time.time(), 3) def completeOutput(self): """ 单条用例执行结束后,添加结果前的动作; 添加结果需要调用的方法; 断开输出重定向; 返回日志和输出; """ self.etime = round(time.time(), 3) if self.stdout0: sys.stdout = self.stdout0 sys.stderr = self.stderr0 self.stdout0 = None self.stderr0 = None return [ self.loggerStream.getvalue(), self.outputBuffer.getvalue() ] def stopTest(self, test): """ 单条用例执行结束后,添加结果后的动作; """ super().stopTest(test) # 移除日志Handler for handler in [self.fh, self.lh, self.ch]: if handler: self.logger.removeHandler(handler) def singleEndConsolePrint(self, test, term_mark, term_head, term_clor, duration): """ 将用例执行结果打印到终端显示 """ self.verbosity >= 1 and sys.stderr.write('%s %s %s %s.%s.%-18s\t%s %s\n' % ( _Color(fc=term_clor, bo=1, text='%04d' % (len(self.result)) + ' ' + term_mark + ' ' + term_head + ':'), _Color(fc=37, bo=0, text=datetime.datetime.utcfromtimestamp(duration).strftime('%H:%M:%S.%f')[0:12]), _Color(fc=37, bo=0, text='<='), _Color(fc=37, bo=0, text=str(test.__module__).strip('_')), _Color(fc=37, bo=0, text=str(test.__class__.__qualname__)), _Color(fc=37, bo=0, text=str(test.__dict__['_testMethodName'])), _Color(fc=37, bo=0, text='<='), _Color(fc=37, bo=0, text=str(test.__dict__['_testMethodDoc'] and test.__dict__['_testMethodDoc'].strip().splitlines()[0].strip() or '')) )) def addSuccess(self, test): """ 单条用例执行结束后添加成功结果动作 """ term_mark, term_head, term_clor = '=', 'Passed', 32 self.passed_count += 1 super().addSuccess(test) output = self.completeOutput() duration = round(self.etime - self.stime, 3) self.sortCount(cls=test.__class__.__qualname__, res=term_head, dur=duration) # 添加测试结果 self.result.append((0, test, ['', output[0]][bool(self.success_log_in_report)] + output[1], '', duration)) # 单条用例执行结束后在终端打印结果 self.singleEndConsolePrint(test, term_mark, term_head, term_clor, duration) def addError(self, test, err): """ 单条用例执行结束后添加错误结果动作 """ term_mark, term_head, term_clor = '?', 'Errors', 33 self.errors_count += 1 super().addError(test, err) _, _exc_str = self.errors[-1] output = self.completeOutput() duration = round(self.etime - self.stime, 3) self.sortCount(cls=test.__class__.__qualname__, res=term_head, dur=duration) # 添加测试结果 self.result.append((2, test, output[0] + output[1], _exc_str, duration)) # 用例执行结束后在终端打印结果 self.singleEndConsolePrint(test, term_mark, term_head, term_clor, duration) # 收集错误测试用例名称以在测试报告中显示 testcase_name = '%s.%s.%s' % (str(test.__module__).strip('_'), test.__class__.__qualname__, test.__dict__['_testMethodName']) self.errorsCase += '
  • %s
  • ' % testcase_name self.errorsCaseList.append(testcase_name) def addFailure(self, test, err): """ 单条用例执行结束后添加失败结果动作 """ term_mark, term_head, term_clor = '!', 'Failed', 31 self.failed_count += 1 super().addFailure(test, err) _, _exc_str = self.failures[-1] output = self.completeOutput() duration = round(self.etime - self.stime, 3) self.sortCount(cls=test.__class__.__qualname__, res=term_head, dur=duration) # 添加测试结果 self.result.append((1, test, output[0] + output[1], _exc_str, duration)) # 用例执行结束后在终端打印结果 self.singleEndConsolePrint(test, term_mark, term_head, term_clor, duration) # 收集失败测试用例名称以在测试报告中显示 testcase_name = '%s.%s.%s' % (str(test.__module__).strip('_'), test.__class__.__qualname__, test.__dict__['_testMethodName']) self.failedCase += '
  • %s
  • ' % testcase_name self.failedCaseList.append(testcase_name) class HTMLTestRunner(ReportTemplate): def __init__( self, stream=None, verbosity: int = 1, title: str = None, description: str = None, success_log_in_report=False, report_home: str = None, report_home_latest_name: str = None, report: str = None, log: str = None, logo='', sign='' ): """ This is HTMLTestRunner. :param stream: HTML report write file stream. :param verbosity: 0: Terminal only shows the test results summary. 1: Terminal shows the results summary and results of each case test. 2: Terminal shows the results summary and results of each case test, and print log. :param title: Title of report. :param description: Description of report. :param success_log_in_report: The testcase passed is also displayed in the HTML report. :param report_home: Set home directory of the report for this test, all the results related files will be storage inside(including HTML report, log, images), at this time, other related parameters will fail. :param report_home_latest_name: Create directory on the path where the report directory is located. :param report: HTML report file location(useless when stream parameter inset). :param log: Log file. :param logo: Logo of report, pass an image URL. :param sign: Sign of report, Pass in one piece short text. """ if report_home: try: os.makedirs(report_home) except Exception: raise Exception('Report directory already exists.') stream = report = log = None report, log = '%s/index.html' % report_home, '%s/HTMLTestRunner.log' % report_home self.report_home = report_home self.result_associate_files = [] self.verbosity = verbosity self.success_log_in_report = success_log_in_report self.report_home_latest_name = report_home_latest_name self.stream = stream or (report and open(os.path.abspath(report), 'wb')) self.report = None if self.stream: stream_name = os.path.abspath(self.stream.name) self.result_associate_files.append(stream_name) os.environ['HTML_REPORT_ROOT'] = os.path.dirname(stream_name) self.report = stream_name else: os.environ['HTML_REPORT_ROOT'] = '' self.log = log self.log_location = log and os.path.abspath(log) self.log_location and self.result_associate_files.append(self.log_location) self.log_location and open(self.log_location, 'wb').close() self.title = self.DEFAULT_TITLE if title is None else title self.description = self.DEFAULT_DESCRIPTION if description is None else description self.logo = logo or '' self.sign = sign or '' self.passrate = None self.errormsg = None self.runstime = None self.runetime = None def run(self, test): sys.stderr.write(_Color(fc=38, bo=1, text='* * * * * * * * * * * * * * * * * * 开始测试 * * * * * * * * * * * * * * * * * *') + '\n') self.verbosity >= 2 and sys.stderr.write("\n") self.runstime = round(time.time(), 3) # 获取测试结果 result = _TestResult(verbosity=self.verbosity, log=self.log_location, success_log_in_report=self.success_log_in_report) test(result) self.runetime = round(time.time(), 3) self.verbosity >= 2 and sys.stderr.write("\n") sys.stderr.write(_Color(fc=38, bo=1, text='* * * * * * * * * * * * * * * * * * 结束测试 * * * * * * * * * * * * * * * * * *') + '\n') # 生成测试报告 self.generateReport(test, result) if len(result.result) == 0: raise Exception('No testcases found.') case_count = { 't': len(result.result), 'p': result.passed_count, 'f': result.failed_count, 'e': result.errors_count } dura = time.strftime('%H:%M:%S', time.gmtime((self.runetime - self.runstime))) rate_passed = str("%.2f%%" % (float(case_count['p'] / case_count['t'] * 100))) rate_failed = str("%.2f%%" % (float(case_count['f'] / case_count['t'] * 100))) rate_errors = str("%.2f%%" % (float(case_count['e'] / case_count['t'] * 100))) list_failed = result.failedCaseList list_errors = result.errorsCaseList casesort = result.casesort # 终端打印结果 sys.stderr.write(_Color(fc=36, bo=1, text='结果概要') + "\n") sys.stderr.write( _Color(fc=37, bo=0, text='总共' + "\x20" + str(case_count["t"]) + "\x20") + _Color(fc=37, bo=0, text='通过' + "\x20" + str(case_count["p"]) + "\x20") + _Color(fc=37, bo=0, text='失败' + "\x20" + str(case_count["f"]) + "\x20") + _Color(fc=37, bo=0, text='错误' + "\x20" + str(case_count["e"]) + "\x20") + _Color(fc=37, bo=0, text='耗时' + "\x20" + dura + "\x20") + _Color(fc=37, bo=0, text='通过率' + "\x20" + rate_passed + "\x20") + _Color(fc=37, bo=0, text='失败率' + "\x20" + rate_failed + "\x20") + _Color(fc=37, bo=0, text='错误率' + "\x20" + rate_errors + "\x20") + "\n" ) for value in [['失败用例', list_failed, 31], ['错误用例', list_errors, 33]]: if len(value[1]): sys.stderr.write(_Color(fc=value[2], bo=1, text=value[0]) + "\n") for i in range(len(value[1])): sys.stderr.write(_Color(fc=37, bo=0, text=str(i + 1) + '. ' + value[1][i]) + "\n") if len(casesort.keys()): sys.stderr.write( _Color(fc=34, bo=1, text='%-22s' % ('用例集合') + "\t" + '%-4s' % ('总计') + "\t" + '%-4s' % ('通过') + "\t" + '%-4s' % ('失败') + "\t" + '%-4s' % ('错误') + "\t" + '%-8s' % ('耗时') + "\t") + "\n") for key, value in casesort.items(): sys.stderr.write( _Color(fc=37, bo=0, text='%-22s' % (str(key)) + "\t" + '%-4s' % (str(value["p"] + value["f"] + value["e"])) + "\t" + '%-4s' % (str(value["p"])) + "\t" + '%-4s' % (str(value["f"])) + "\t" + '%-4s' % (str(value["e"])) + "\t" + '%-8s' % ('%.3f' % (round(value["d"], 3)) + '秒') + "\t") + "\n") return result @staticmethod def sortResult(result_list): rmap = {} classes = [] for n, t, o, e, s in result_list: cls = t.__class__ if cls not in rmap: rmap[cls] = [] classes.append(cls) rmap[cls].append((n, t, o, e, s)) return [(cls, rmap[cls]) for cls in classes] def getReportAttributes(self, result): runstime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.runstime)) duration = time.strftime('%H:%M:%S', time.gmtime((self.runetime - self.runstime))) status = ','.join([ '总共 %s' % (result.passed_count + result.failed_count + result.errors_count), '通过 %s' % (result.passed_count), '失败 %s' % (result.failed_count), '错误 %s' % (result.errors_count) ]) if (result.passed_count + result.failed_count + result.errors_count) > 0: self.passrate = str("%.2f%%" % (float(result.passed_count) / float( result.passed_count + result.failed_count + result.errors_count) * 100)) else: self.passrate = '0.00%' if len(result.failedCase) > 0: failedCase = result.failedCase else: failedCase = '无' if len(result.errorsCase) > 0: errorsCase = result.errorsCase else: errorsCase = '无' uname = platform.uname() return [ ['开始时间', runstime], ['合计耗时', duration], ['主机名称', '%s (%s %s) %s' % (uname.node, uname.system, uname.release, uname.machine.lower())], ['测试结果', '%s,通过率%s' % (status, self.passrate)], ['失败用例', failedCase], ['错误用例', errorsCase], ] def generateReport(self, test, result): report_attrs = self.getReportAttributes(result) report_count = self._generate_report(result) output = self.HTML_TMPL % dict( title=saxutils.escape(self.title), generator='HTMLTestRunner', styles=self._generate_styles(), passed=report_count['passed'], failed=report_count['failed'], errors=report_count['errors'], casesets=report_count['casesets'], casesets_passed=report_count['casesets_passed'], casesets_failed=report_count['casesets_failed'], casesets_errors=report_count['casesets_errors'], header=self._generate_header(report_attrs), report=report_count['report'], footer=self._generate_footer(), ) # 写入报告文件 self.stream and self.stream.write(output.encode('utf-8')) self.stream and self.stream.close() if self.report_home and self.report_home_latest_name: latest = '%s/%s' % (os.path.dirname(self.report_home), self.report_home_latest_name) os.path.exists(latest) and shutil.rmtree(latest) shutil.copytree(self.report_home, latest) if self.report_home: open('%s/latest.ini' % os.path.dirname(self.report_home), mode='w').write(os.path.basename(self.report_home)) def _generate_styles(self): return self.STYLES_TMPL def _generate_header(self, report_attrs): line_list = [] for name, value in report_attrs: match name: case '失败用例': if value == "无": line = self.HEADER_ATTRIBUTE_TMPL % dict(name=name, value=value) else: line = self.HEADER_ATTRIBUTE_TMPL % dict( name=name, value="点击查看" "
      " + value + "
    " ) case '错误用例': if value == "无": line = self.HEADER_ATTRIBUTE_TMPL % dict(name=name, value=value) else: line = self.HEADER_ATTRIBUTE_TMPL % dict( name=name, value="点击查看" "
      " + value + "
    " ) case _: line = self.HEADER_ATTRIBUTE_TMPL % dict(name=saxutils.escape(str(name)), value=saxutils.escape(str(value))) line_list.append(line) return self.HEADER_TMPL % dict( rail_hidden='none' if not self.logo and not self.sign else 'flex', logo=self.logo, sign=self.sign, title=saxutils.escape(self.title), parameters=''.join(line_list), description=saxutils.escape(self.description) ) def _generate_report(self, result): rows = [] sortedResult = self.sortResult(result.result) dura_caseset = 0 for cid, (cls, cls_results) in enumerate(sortedResult): np = nf = ne = ns = 0 for n, t, o, e, s in cls_results: # 遍历每条用例 match n: case 0: np += 1 case 1: nf += 1 case 2: ne += 1 ns += s # 单个用例集合耗时 ns = round(ns, 3) dura_caseset += ns row = self.REPORT_CLASS_TMPL % dict( style=ne > 0 and 'errorsClass' or nf > 0 and 'failedClass' or 'passedClass', name=cls.__qualname__, docs=cls.__doc__ and cls.__doc__.strip().splitlines()[0].strip() or '', counts=np + nf + ne, passed=np, failed=nf, errors=ne, cid='c%s' % (cid + 1), time_usage='%.3f秒' % ns ) rows.append(row) for tid, (n, t, o, e, s) in enumerate(cls_results): rows.append(self._generate_report_test(rows, cid, tid, n, t, o, e)) # 全部用例集合耗时 dura_caseset = round(dura_caseset, 3) report = self.REPORT_TMPL % dict( test_list=''.join(rows), counts=str(result.passed_count + result.failed_count + result.errors_count), passed=str(result.passed_count), failed=str(result.failed_count), errors=str(result.errors_count), time_usage='%.3f秒' % dura_caseset, passrate=self.passrate ) casesets = list(result.casesort.keys()) casesets_passed = [] for value in casesets: casesets_passed.append(result.casesort[value]["p"]) casesets_failed = [] for value in casesets: casesets_failed.append(result.casesort[value]["f"]) casesets_errors = [] for value in casesets: casesets_errors.append(result.casesort[value]["e"]) return { "report": report, "passed": str(result.passed_count), "failed": str(result.failed_count), "errors": str(result.errors_count), "casesets": json.dumps(casesets, ensure_ascii=False), "casesets_passed": json.dumps(casesets_passed, ensure_ascii=False), "casesets_failed": json.dumps(casesets_failed, ensure_ascii=False), "casesets_errors": json.dumps(casesets_errors, ensure_ascii=False) } def _generate_report_test(self, rows, cid, tid, n, t, o, e): hasout = bool(o or e) match n: case 0: tid_flag = 'p' pre_clor = '#119611' case 1: tid_flag = 'f' pre_clor = '#e52000' case 2: tid_flag = 'e' pre_clor = '#e54f00' case _: tid_flag = 'u' pre_clor = '#808080' # ID修改点为下划线; # 支持Bootstrap折叠展开特效; # 例如:'pt1_1', 'ft1_1', 'et1_1' tid = tid_flag + 't%s_%s' % (cid + 1, tid + 1) name = t.id().split('.')[-1] docs = t.shortDescription() or '' # o and e should be byte string because they are collected from stdout and stderr. if isinstance(o, str): # some problem with 'string_escape': it escape \n and mess up formating # uo = unicode(o.encode('string_escape')) # uo = o.decode('latin-1') uo = o else: uo = o if isinstance(e, str): # some problem with 'string_escape': it escape \n and mess up formating # ue = unicode(e.encode('string_escape')) # ue = e.decode('latin-1') ue = e else: ue = e script = self.REPORT_TEST_OUTPUT_TMPL % dict(id=tid, output=re.compile('\\[[A-Za-z\\-]+].*?\\[/[A-Za-z\\-]+][\r\n]').sub( '', saxutils.escape(uo + ue))) # 截图名称通过抛出异常在 # 标准错误当中, # 判断是否包含截图信息 # 实际检测输出当中是否包含report-screenshot关键字; output = uo + ue self.errormsg = output.find('report-screenshot') if self.errormsg == -1: # 没有截图信息 template = hasout and self.REPORT_TEST_WITH_OUTPUT_TMPL_0 or self.REPORT_TEST_NO_OUTPUT_TMPL row = template % dict( tid=tid, Class=n == 0 and 'hiddenRow' or 'none', style=n == 2 and 'errorsCase' or (n == 1 and 'failedCase' or 'passedCase'), name=name, docs=docs, script=script, status=self.STATUS[n], pre_color=pre_clor ) else: # 包含截图信息 template = hasout and self.REPORT_TEST_WITH_OUTPUT_TMPL_1 or self.REPORT_TEST_NO_OUTPUT_TMPL screenshot_list = _findMark(mark='report-screenshot', data=output) screenshot = '' for image in screenshot_list: bn = os.path.basename(image) abs_image = os.path.abspath(image) self.result_associate_files.append(abs_image) # 移动图片位置到报告所在目录 try: self.report and shutil.move(abs_image, '%s/%s' % (os.path.dirname(self.report), bn)) except Exception: pass screenshot += '' + bn + '
    ' row = template % dict( tid=tid, Class=n == 0 and 'hiddenRow' or 'none', style=n == 2 and 'errorsCase' or (n == 1 and 'failedCase' or 'passedCase'), name=name, docs=docs, script=script, status=self.STATUS[n], pre_color=pre_clor, screenshot=screenshot ) return row def _generate_footer(self): return self.FOOTER_TMPL