diff --git a/Business/Class/HTMLTestRunner.py b/Business/Class/HTMLTestRunner.py
new file mode 100644
index 0000000..1c4d327
--- /dev/null
+++ b/Business/Class/HTMLTestRunner.py
@@ -0,0 +1,1329 @@
+"""
+A TestRunner for use with the Python unit testing framework. It
+generates a HTML report to show the result at a glance.
+
+The simplest way to use this is to invoke its main method. E.g.
+
+ import unittest
+ import HTMLTestRunner
+
+ ... define your tests ...
+
+ if __name__ == '__main__':
+ HTMLTestRunner.main()
+
+
+For more customization options, instantiates a HTMLTestRunner object.
+HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.
+
+ # output to a file
+ fp = file('my_report.html', 'wb')
+ runner = HTMLTestRunner.HTMLTestRunner(
+ stream=fp,
+ title='My unit test',
+ description='This demonstrates the report output by HTMLTestRunner.'
+ )
+
+ # Use an external stylesheet.
+ # See the Template_mixin class for more customizable options
+ runner.STYLES_TMPL = ' '
+
+ # run the test
+ runner.run(my_test_suite)
+
+
+------------------------------------------------------------------------
+Copyright (c) 2004-2007, Wai Yip Tung
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+* Neither the name Wai Yip Tung nor the names of its contributors may be
+ used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+import os, re, sys, io, time, datetime, json, unittest, logging
+from xml.sax import saxutils
+
+_global_dict = {}
+
+
+class GlobalMsg(object):
+ def __init__(self):
+ global _global_dict
+ _global_dict = {}
+
+ @staticmethod
+ def set_value(name, value):
+ _global_dict[name] = value
+
+ @staticmethod
+ def get_value(name):
+ try:
+ return _global_dict[name]
+ except KeyError:
+ return None
+
+
+# ------------------------------------------------------------------------
+# The redirectors below are used to capture output during testing. Output
+# sent to sys.stdout and sys.stderr are automatically captured. However
+# in some cases sys.stdout is already cached before HTMLTestRunner is
+# invoked (e.g. calling logging.basicConfig). In order to capture those
+# output, use the redirectors for the cached stream.
+#
+# e.g.
+# >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
+# >>>
+
+
+class OutputRedirector(object):
+ """ Wrapper to redirect stdout or 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)
+
+
+# ----------------------------------------------------------------------
+# Template
+
+
+class Template_mixin(object):
+ """
+ Define a HTML template for report customerization and generation.
+
+ Overall structure of an HTML report
+
+ HTML
+ +------------------------+
+ | |
+ |
|
+ | |
+ | STYLES |
+ | +----------------+ |
+ | | | |
+ | +----------------+ |
+ | |
+ | |
+ | |
+ | |
+ | |
+ | HEADER |
+ | +----------------+ |
+ | | | |
+ | +----------------+ |
+ | |
+ | REPORT |
+ | +----------------+ |
+ | | | |
+ | +----------------+ |
+ | |
+ | FOOTER |
+ | +----------------+ |
+ | | | |
+ | +----------------+ |
+ | |
+ | |
+ | |
+ +------------------------+
+ """
+
+ STATUS = {
+ 0: '通过',
+ 1: '失败',
+ 2: '错误',
+ }
+
+ DEFAULT_TITLE = '测试报告'
+ DEFAULT_DESCRIPTION = ''
+ DEFAULT_TESTER = 'Tester'
+
+ # ------------------------------------------------------------------------
+ # 网页模板开始
+ # 网页模板,变量列表 title, generator, styles, header, report, footer
+ HTML_TMPL = r"""
+
+
+ %(title)s
+
+
+
+
+
+
+
+
+ %(styles)s
+
+
+
+%(header)s
+%(report)s
+%(footer)s
+
+
+
+"""
+ # 网页模板结束
+ # ------------------------------------------------------------------------
+
+ # ------------------------------------------------------------------------
+ # Stylesheet
+ #
+ # alternatively use a for external style sheet, e.g.
+ #
+
+ STYLES_TMPL = """
+
+ """
+
+ # ------------------------------------------------------------------------
+ # 头部信息开始
+ # 添加显示截图和统计图div,变量列表 title, parameters, description
+ HEADER_TMPL = """
+
+
+
+
+
+
+
+ """
+
+ # 测试信息模板,变量列表 name, value
+ HEADER_ATTRIBUTE_TMPL = """
+ %(name)s : %(value)s
+ """
+ # 头部信息结束
+ # ------------------------------------------------------------------------
+
+ # ------------------------------------------------------------------------
+ # 报告模板开始
+ # 变量列表 test_list, counts, passed, failed, errors ,passrate
+ REPORT_TMPL = """
+
+
+
+
+
+
+
+
+
+
+
+
+
+ %(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
+
+ %(status)s
+
+
+
+ 截图信息
+
+ %(screenshot)s
+
+
+
+ """
+
+ # 失败样式(无截图列),变量列表 tid, Class, style, desc, status
+ REPORT_TEST_WITH_OUTPUT_TMPL_0 = """
+
+ %(name)s
+ %(docs)s
+
+ %(status)s
+
+
+
+
+ """
+
+ # 通过样式,变量列表 tid, Class, style, desc, status
+ REPORT_TEST_NO_OUTPUT_TMPL = """
+
+ %(name)s
+ %(docs)s
+ %(status)s
+
+
+ """
+
+ # 测试输出内容
+ REPORT_TEST_OUTPUT_TMPL = '%(id)s:' + "\n" + '%(output)s'
+
+ # 返回顶部按钮
+ FOOTER_TMPL = """
+
+
+ """
+ # 报告模板结束
+ # ------------------------------------------------------------------------
+
+
+def _findMark(mark='', text=''):
+ return re.findall('\[' + mark + '](.*?)\[/' + mark + ']' + '*?', text)
+
+
+def _makeMark(mark='', cont=''):
+ return '[' + mark + ']' + cont +'[/' + mark + ']'
+
+
+def _Color(fc=0, bc=0, bo=0, text=''):
+ if "PYCHARM" not in os.environ.keys():
+ return text
+ return "\033[" + str(bo) + ['', ';' + str(fc)][fc > 0] + ['', ';' + str(bc)][bc > 0] + "m" + text + "\033[0m"
+
+
+class _TestResult(unittest.TestResult):
+ # Note: _TestResult is a pure representation of results.
+ # It lacks the output and reporting ability compares to unittest._TextTestResult.
+
+ def __init__(self, verbosity=1, log=None):
+ super().__init__(verbosity=verbosity)
+ self.fh = None
+ self.lh = None
+ self.ch = None
+ self.verbosity = verbosity
+ self.logoutput = log
+ 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
+ # result is a list of result in 4 tuple
+ # (
+ # result code (0: success; 1: fail; 2: error),
+ # TestCase object,
+ # Test output (byte string),
+ # stack trace,
+ # )
+ self.result = []
+ self.passrate = float(0)
+ # 分类统计数量耗时
+ self.casesort = {}
+ # 增加失败用例合集
+ self.failedCase = ""
+ self.failedCaseList = []
+ # 增加错误用例合集
+ self.errorsCase = ""
+ self.errorsCaseList = []
+
+ self.logger = logging.getLogger('test')
+
+ 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 >= 0:
+ 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.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 self.logoutput:
+ self.fh = logging.FileHandler(filename=self.logoutput, 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 complete_output(self):
+ # 单条用例执行结束后,添加结果前的动作
+ # 添加结果需要调用的方法
+ """
+ Disconnect output redirection and return buffer.
+ Safe to call multiple times.
+ """
+ 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)
+ # Usually one of addSuccess, addError or addFailure would have been called.
+ # But there are some path in unittest that would bypass this.
+ # We must disconnect stdout in stopTest(), which is guaranteed to be called.
+ # self.complete_output()
+ # 移除日志Handler
+ for value in [self.fh, self.lh, self.ch]:
+ if value:
+ self.logger.removeHandler(value)
+
+ def addSuccess(self, test):
+ # 单条用例执行结束后,添加结果时的动作
+ term_mark = '='
+ term_head = 'Passed'
+ term_clor = 32
+ self.passed_count += 1
+ super().addSuccess(test)
+ output = self.complete_output()
+ utime = round(self.etime - self.stime, 3)
+ self.sortCount(cls=test.__class__.__qualname__, res=term_head, dur=utime)
+ self.result.append((0, test, output[1], '', utime))
+ # 单条用例执行结束后在终端打印结果
+ if self.verbosity >= 1:
+ 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(utime).strftime('%H:%M:%S.%f')[0:12]),
+ _Color(fc=term_clor, bo=1, 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=term_clor, bo=1, text='<='),
+ _Color(fc=37, bo=0, text=str(test.__dict__["_testMethodDoc"] or ""))
+ ))
+
+ def addError(self, test, err):
+ # 单条用例执行结束后,添加结果时的动作
+ term_mark = '?'
+ term_head = 'Errors'
+ term_clor = 33
+ self.errors_count += 1
+ super().addError(test, err)
+ _, _exc_str = self.errors[-1]
+ output = self.complete_output()
+ utime = round(self.etime - self.stime, 3)
+ self.sortCount(cls=test.__class__.__qualname__, res=term_head, dur=utime)
+ self.result.append((2, test, output[0] + output[1], _exc_str, utime))
+ # 单条用例执行结束后在终端打印结果
+ if self.verbosity >= 1:
+ 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(utime).strftime('%H:%M:%S.%f')[0:12]),
+ _Color(fc=term_clor, bo=1, 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=term_clor, bo=1, text='<='),
+ _Color(fc=37, bo=0, text=str(test.__dict__["_testMethodDoc"] or ""))
+ ))
+ # 收集错误测试用例名称以在测试报告中显示
+ casename = str(test.__module__).strip('_') + '.' + str(test.__class__.__qualname__) + '.' + str(
+ test.__dict__["_testMethodName"])
+ self.errorsCase += "" + casename + " "
+ self.errorsCaseList.append(casename)
+
+ def addFailure(self, test, err):
+ # 单条用例执行结束后,添加结果时的动作
+ term_mark = '!'
+ term_head = 'Failed'
+ term_clor = 31
+ self.failed_count += 1
+ super().addFailure(test, err)
+ _, _exc_str = self.failures[-1]
+ output = self.complete_output()
+ utime = round(self.etime - self.stime, 3)
+ self.sortCount(cls=test.__class__.__qualname__, res=term_head, dur=utime)
+ self.result.append((1, test, output[0] + output[1], _exc_str, utime))
+ # 单条用例执行结束后在终端打印结果
+ if self.verbosity >= 1:
+ 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(utime).strftime('%H:%M:%S.%f')[0:12]),
+ _Color(fc=term_clor, bo=1, 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=term_clor, bo=1, text='<='),
+ _Color(fc=37, bo=0, text=str(test.__dict__["_testMethodDoc"] or ""))
+ ))
+ # 收集失败测试用例名称以在测试报告中显示
+ casename = str(test.__module__).strip('_') + '.' + str(test.__class__.__qualname__) + '.' + str(
+ test.__dict__["_testMethodName"])
+ self.failedCase += "" + casename + " "
+ self.failedCaseList.append(casename)
+
+
+class HTMLTestRunner(Template_mixin):
+ # 新增 errormsg 参数,-1为无需截图,否则需要截图
+ def __init__(self, report=None, log=None, stream=None, verbosity=1, title=None, description=None,
+ info=None, logo='', sign=''):
+ self.passrate = None
+ self.errormsg = None
+ self.logspath = log and os.path.abspath(log)
+ self.logo = logo
+ self.sign = sign
+ self.stream = stream or (report and open(report, 'wb'))
+ self.verbosity = verbosity
+ self.runstime = None
+ self.runetime = None
+
+ if title is None:
+ self.title = self.DEFAULT_TITLE
+ else:
+ self.title = title
+ if description is None:
+ self.description = self.DEFAULT_DESCRIPTION
+ else:
+ self.description = description
+ if info is None:
+ self.testinfo = []
+ elif isinstance(info, list):
+ self.testinfo = info
+ elif isinstance(info, dict):
+ self.testinfo = []
+ for key, value in info.items():
+ self.testinfo.append([key, value])
+ else:
+ self.testinfo = []
+
+ def run(self, test):
+ """
+ Run the given test case or test suite.
+ """
+ # The final result is output when the verbosity is 1,
+ # and the result of each use case is output when the verbosity is 2.
+ # Start testing.
+ sys.stderr.write(_Color(
+ fc=38, bo=1, text='* * * * * * * * * * * * * * * * * * 开始测试 * * * * * * * * * * * * * * * * * *') + '\n')
+ self.runstime = round(time.time(), 3)
+ result = _TestResult(verbosity=self.verbosity, log=self.logspath)
+ test(result)
+ self.runetime = round(time.time(), 3)
+ sys.stderr.write(_Color(
+ fc=38, bo=1, text='* * * * * * * * * * * * * * * * * * 结束测试 * * * * * * * * * * * * * * * * * *') + '\n')
+ # Generate test report.
+ self.generateReport(test, result)
+ 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='%-16s' % ('用例集合') + "\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='%-16s' % (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" +
+ '%.3f' % (round(value["d"], 3)) + '秒' + "\t"
+ ) + "\n")
+ return result
+
+ def sortResult(self, result_list):
+ # unittest does not seems to run in any particular order.
+ # Here at least we want to group them together by class.
+ 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):
+ """
+ Return report attributes as a list of (name, value).
+ Override this to add custom attributes.
+ """
+ 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 = '无'
+
+ return self.testinfo + [
+ ['开始时间', runstime],
+ ['合计耗时', duration],
+ ['测试结果', 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),
+ logo=self.logo,
+ sign=self.sign,
+ 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'))
+
+ 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(
+ 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 'errorClass' or nf > 0 and 'failClass' or 'passClass',
+ name=cls.__name__,
+ docs=cls.__doc__ and cls.__doc__.split("\n")[0] 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):
+ # e.g. 'pt1_1', 'ft1_1', 'et1_1'
+ hasout = bool(o or e)
+ # ID修改点为下划线,支持Bootstrap折叠展开特效
+ match n:
+ case 0:
+ tid_flag = 'p'
+ case 1:
+ tid_flag = 'f'
+ case 2:
+ tid_flag = 'e'
+ case _:
+ tid_flag = 'u'
+ 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)))
+
+ # 截图名称通过抛出异常在标准错误中
+ output = uo + ue
+ # 先判断是否需要截图
+ self.errormsg = output.find("TestError")
+
+ 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],
+ )
+ else:
+ # 包含截图信息
+ template = hasout and self.REPORT_TEST_WITH_OUTPUT_TMPL_1 or self.REPORT_TEST_NO_OUTPUT_TMPL
+ screenshot_list = _findMark(mark='TestErrorImg', text=output)
+ screenshot = ''
+ for value in screenshot_list:
+ try:
+ bn = os.path.basename(value)
+ except:
+ bn = '点击查看'
+ 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],
+ screenshot=screenshot
+ )
+ return row
+
+ def _generate_footer(self):
+ return self.FOOTER_TMPL
diff --git a/HTMLTestRunner.py b/HTMLTestRunner.py
index 32425aa..78a480a 100644
--- a/HTMLTestRunner.py
+++ b/HTMLTestRunner.py
@@ -303,15 +303,15 @@ class Template_mixin(object):
},
series: [{
name: '通过',
- color: '#64BB64',
+ color: '#64bb64',
data: %(casesets_passed)s
}, {
name: '失败',
- color: '#F16D7E',
+ color: '#f16d7e',
data: %(casesets_failed)s
}, {
name: '错误',
- color: '#FDC68C',
+ color: '#fdc68c',
data: %(casesets_errors)s
}]
})
@@ -342,7 +342,7 @@ class Template_mixin(object):
pie: {
allowPointSelect: true,
cursor: 'pointer',
- colors: ['#81ca9d', '#f16d7e', '#fdc68c'],
+ colors: ['#64bb64', '#f16d7e', '#fdc68c'],
dataLabels: {
enabled: true,
format: '{point.name} : {point.percentage:.1f} %%',
@@ -505,7 +505,10 @@ function html_escape(s) {
%(header)s
%(report)s
%(footer)s
-
+