zoukankan      html  css  js  c++  java
  • 六、Python-自动化安装依赖、多线程、多进程、单元测试

    (一)自动化安装依赖方式

    1、把依赖的文件列表与已安装的列表对比,没有的就安装

    2、执行用例之前就安装一遍依赖

    3、判断依赖文件内容有没有变化,如果有变化就执行一次安装(推荐这种方式)

    4、自动依赖脑图分析:

    例如Windows系统,家目录的路径为:用户-->XXX

      

    5、编写程序

    参考实例:auto_install.py

    # pip itall -r requriments.txt
    # pip freeze > requirements_local.txt
    # pip install xxx
    import hashlib
    import os
    user_home = os.environ['USERPROFILE'] if os.environ.get('USERPROFILE') else os.environ.get('HOME')
    
    class InstallRequire:
        # 存放本地缓存的文件
        local_file = os.path.join(user_home,'.requirments_cache')
        require_file = 'requirements.txt'
        command = 'pip install -r requirements.txt'
    
        @property
        def check_local_file(self):
            if os.path.exists(self.local_file):
                return True
    
        #静态方法
        @staticmethod
        def read_file(file_name):
            with open(file_name,encoding='utf-8') as fr:
                return fr.read()
    
        @staticmethod
        def write_file(file_name,content):
            with open(file_name,'w',encoding='utf-8') as fr:
                return fr.write(content)
    
        @staticmethod
        def md5(msg):
            msg = str(msg)
            m =hashlib.md5(msg.encode())
            return m.hexdigest()
    
        def main(self):
            require_file_md5 = self.md5(self.read_file(self.require_file))
            if self.check_local_file:
                local_file_md5 = self.read_file(self.local_file)
                if local_file_md5 != require_file_md5:
                    print('依赖文件有变化,开始安装!')
                    os.system(self.command)
                    self.write_file(self.local_file,require_file_md5)
                    print('安装完成!')
                else:
                    print('依赖文件没有变化,继续执行!')
            else:
                print('未发现本地缓存文件,开始安装依赖!')
                os.system(self.command)
                self.write_file(self.local_file, require_file_md5)
    
    if __name__ == '__main__':
        ir = InstallRequire()
        ir.main()

    执行截图所示:

    (二)多线程和多进程

    1、进程和线程的含义

    (1)进程:一个程序,它是一组资源的集合;一个进程里面默认是有一个线程的,主线程

    (2)线程:最小的执行单位;线程和线程之间是互相独立的;主线程等待子线程执行结束;线程和线程之间,数据是共享的。

    (3)一个进程包含多个线程

    2、单线程例子:

    import threading
    import time
    
    def clean():
        print('打扫卫生')
        time.sleep(2)
    
    def wash_clothes():
        print('洗衣服')
        time.sleep(3)
    
    def cook():
        print('做饭')
        time.sleep(1)
    
    # 单线程的方式
    clean()
    wash_clothes()
    cook()

    3、多线程例一:

    import threading
    import time
    
    def clean():
        print('打扫卫生')
        time.sleep(2)
    
    def wash_clothes():
        print('洗衣服')
        time.sleep(3)
    
    def cook():
        print('做饭')
        time.sleep(1)
    
    # 多线程
    start_time = time.time()
    t = threading.Thread(target=clean)
    t2 = threading.Thread(target=wash_clothes)
    t3 = threading.Thread(target=cook)
    
    t.start()
    t2.start()
    t3.start()
    
    t.join()
    t2.join()
    t3.join()
    
    end_time = time.time()
    print(end_time-start_time)
    print(threading.activeCount() )  # 获取当前的线程数

    4、多线程例二:(Python的多线程是利用不了多核CPU的)

    (1)等待多个子线程执行结束,把启动的子线程放到list中,再循环调用t.join

    import threading
    import time
    import random
    
    def export_data():
        print(threading.current_thread())
        print('export_data')
        time.sleep(random.randint(1,5))
    
    #1、等待多个子线程执行结束,把启动的子线程放到list中,再循环调用t.join
    thread_list = []
    for i in range(10):
        t = threading.Thread(target=export_data)
        thread_list.append(t)
        t.start()
    
    for t in thread_list:
        t.join()
    
    print('数据都导完了')

    (2)等待多个子线程执行结束,通过判断当前线程数

    import threading
    import time
    import random
    
    def export_data():
        print(threading.current_thread())
        print('export_data')
        time.sleep(random.randint(1,5))
    
    #2、等待多个子线程执行结束,通过判断当前线程数
    for i in range(10):
        t = threading.Thread(target=export_data)
        t.start()
    
    while threading.active_count() !=1:
        pass
    
    print('数据都导完了')

    5、函数传参

    实例:多线程函数传参.py

    import threading
    import requests
    import auto_install
    
    def down_load_pic(url):
        print('开始下载',url)
        r = requests.get(url)
        file_name = auto_install.InstallRequire.md5(url) + '.jpg'
        with open(file_name,'wb') as fw:
            fw.write(r.content)
    
    urls = [
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606147468333&di=20d70afc041d614c95ed4bece923a30f&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190624%2F14%2F1561358630-nlyshjVPwq.jpg',
        'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2726503334,2969167273&fm=26&gp=0.jpg',
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606147501828&di=53cbd54babc4a99fcfe1f8da6ecee1f5&imgtype=0&src=http%3A%2F%2Fimgsrc.baidu.com%2Fforum%2Fw%3D580%2Fsign%3D1f6bfa4da06eddc426e7b4f309dab6a2%2F7b4a031b0ef41bd5bcf13c5c5cda81cb38db3d48.jpg',
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606147467777&di=2d4404ca7bc33c49c008749a6d4a04c2&imgtype=0&src=http%3A%2F%2Fimg.article.pchome.net%2F00%2F25%2F47%2F87%2Fpic_lib%2Fs960x639%2F004s960x639.jpg'
    ]
    
    for url in urls:
        t = threading.Thread(target=down_load_pic,args=[url])
        t.start()
    
    while threading.active_count()!=1:
        pass
    
    print('所有图片下载完成!')

    6、线程锁:多个线程同时操作同一个数据的时候,需要加锁和解锁:(线程安全)

    import threading
    count = 0
    
    lock = threading.Lock()
    
    def add():
        global count
        for i in range(10000):
            # # 加锁
            # lock.acquire()
            # count+=1
            # #解锁
            # lock.release()
            with lock:
                count +=1
    
    for i in range(2):
        t = threading.Thread(target=add)
        t.start()
    
    while threading.active_count() !=1:
        pass
    
    print(count)

    7、守护线程:守护主线程,只要主线程结束,不管子线程有没有执行完成,全部都结束。

    import threading
    import time
    import random
    
    def talk(name):
        print('正在和%s聊天'%name)
        time.sleep(random.randint(1,5))
        print('和%s聊完了'%name)
    
    t = threading.Thread(target=talk,args=['Nancy'])
    # 设成守护线程
    t.setDaemon(True)
    t.start()
    
    t = threading.Thread(target=talk,args=['Mick'])
    # 设成守护线程
    t.setDaemon(True)
    t.start()
    
    t = threading.Thread(target=talk,args=['Bob'])
    # 设成守护线程
    t.setDaemon(True)
    t.start()
    
    t = threading.Thread(target=talk,args=['Bill'])
    # 设成守护线程
    t.setDaemon(True)
    t.start()
    
    while threading.active_count() !=1:
        pass
    
    print('退出QQ')

    8、队列:异步处理,保证顺序

    (1)队列.py

    #队列 list:异步化处理
    import queue
    import random
    import time
    import threading
    orders_q = queue.Queue()
    
    # 生产者/消费者模式
    def producer():
        for i in range(100):
            order_id = random.randint(1,999)
            print('订单生成,orderid:%s'%order_id)
            orders_q.put(order_id)
            time.sleep(1)
    
    def consumer():
        while True :
            if orders_q.qsize() >0:
                order_id = orders_q.get()
                print('订单落库',order_id)
    
    t = threading.Thread(target=producer)
    t.start()
    
    t = threading.Thread(target=consumer)
    t.start()

    (2)队列多个消费者:

    import queue
    import random
    import time
    import threading
    orders_q = queue.Queue()
    
    # 生产者/消费者模式
    def producer():
        for i in range(100):
            order_id = random.randint(1,999)
            print('订单生成,orderid:%s'%order_id)
            orders_q.put(order_id)
            time.sleep(1)
    
    def consumer():
        while True :
            if orders_q.qsize() >0:
                order_id = orders_q.get()
                print('consumer1,订单落库',order_id)
    
    def consumer2():
        while True :
            if orders_q.qsize() >0:
                order_id = orders_q.get()
                print('consumer2,订单落库',order_id)
    
    t = threading.Thread(target=producer)
    t.start()
    
    t = threading.Thread(target=consumer)
    t.start()
    
    t = threading.Thread(target=consumer2)
    t.start()

    9、线程池:

    (1)需要安装线程池:

    pip install threadpool

    (2)线程池.py

    import threadpool
    import requests
    import threading
    import auto_install
    import os
    
    def down_load_pics(url):
        print(threading.current_thread())
        print('开始下载',url)
        r = requests.get(url)
        file_name = auto_install.InstallRequire.md5(url)+'.jpg'
        with open(os.path.join('imgs',file_name),'wb') as fw:
                fw.write(r.content)
    
    urls = [
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606147468333&di=20d70afc041d614c95ed4bece923a30f&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190624%2F14%2F1561358630-nlyshjVPwq.jpg',
        'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2726503334,2969167273&fm=26&gp=0.jpg',
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606147501828&di=53cbd54babc4a99fcfe1f8da6ecee1f5&imgtype=0&src=http%3A%2F%2Fimgsrc.baidu.com%2Fforum%2Fw%3D580%2Fsign%3D1f6bfa4da06eddc426e7b4f309dab6a2%2F7b4a031b0ef41bd5bcf13c5c5cda81cb38db3d48.jpg',
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606147467777&di=2d4404ca7bc33c49c008749a6d4a04c2&imgtype=0&src=http%3A%2F%2Fimg.article.pchome.net%2F00%2F25%2F47%2F87%2Fpic_lib%2Fs960x639%2F004s960x639.jpg',
        'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2121278104,33728762&fm=26&gp=0.jpg',
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000c=1606323851635&di=113f81718f7c64a36edb0c60908a5bee&imgtype=0&src=http%3A%2F%2Fwww.euro-premium.cn%2Fsites%2Fdefault%2Ffiles%2F2018%2F11%2F2018-11-22-601.jpg',
        'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=4220879049,1581383431&fm=26&gp=0.jpg',
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606323851632&di=be753a1b6588ca82d7d198e4facc58a8&imgtype=0&src=http%3A%2F%2Fpic4.zhimg.com%2Fv2-3ea59767a18b17cefc2d47544f75cc73_b.jpg',
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606323851618&di=6020618ac70ba8cc4ca5fb1cc95018d8&imgtype=0&src=http%3A%2F%2Fpic5.58cdn.com.cn%2Fp1%2Fbig%2Fn_v1bl2lwkd2dcwvr2gxjeiq_d4473fafff50fcf7.jpg',
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606323851617&di=5b3ceb961409c8761669367ad4e989cd&imgtype=0&src=http%3A%2F%2Fn.sinaimg.cn%2Fsinacn12%2F276%2Fw575h501%2F20180707%2F95e8-hexfcvm1029102.png',
        'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2386572285,3431827098&fm=26&gp=0.jpg',
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606324141028&di=25b7667857e706069b7332a4cd056ea6&imgtype=0&src=http%3A%2F%2Fwww.goupuzi.com%2Fnewatt%2FMon_1909%2F1_174217_f7de79ed750c14f.png',
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606324141027&di=4698c9dba49ca37ee3cf317019c75031&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201807%2F31%2F20180731112006_owdwm.jpg',
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606324141025&di=5603beb7e5b9cc233517b8224494591f&imgtype=0&src=http%3A%2F%2Finews.gtimg.com%2Fnewsapp_bt%2F0%2F10488054738%2F1000.jpg',
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606324141024&di=2ec0d81314a0d9d28b09c3a802033428&imgtype=0&src=http%3A%2F%2Fimg.mp.itc.cn%2Fupload%2F20170331%2F3152b322ec994c648b2187231403d13f_th.jpg',
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606324141023&di=c841e552dcfd7700c35dca775c1f8cf1&imgtype=0&src=http%3A%2F%2Fimage.uczzd.cn%2F1473046732427962250.jpg%3Fid%3D0',
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606324141019&di=0e5c216e09f1ad9158fbf5e38dd256ac&imgtype=0&src=http%3A%2F%2Fspider.ws.126.net%2Fc5638d86b52baf7e3238309b3cc0731b.jpeg'
    ]
    
    pool = threadpool.ThreadPool(5)
    
    # 让它给每个线程分配数据
    reqs = threadpool.makeRequests(down_load_pics,urls)
    #[pool.putRequest(req) for req in reqs]
    
    # 开始运行起来
    for req in reqs:
        pool.putRequest(req)
    
    # 等待子线程执行结束
    pool.wait()
    
    print('运行完成!')

    (3)Python中有个全局解释器锁:GIL

    (4)CPU密集型任务:消耗CPU比较多,例如数据的排序,通过运行计算之类的

    (5)iOS密集型任务:input/output,例如写文件,读文件,上传下载操作

    10、多进程:是可以利用多核CPU的

    多进程.py

    import multiprocessing
    # lock = multiprocessing.Lock()
    import time
    
    def make_money():
        print('开始挣钱')
        time.sleep(10)
    
    def start_process():
        for i in range(5):
            p = multiprocessing.Process(target=make_money)
            p.start()
        print(multiprocessing.active_children())
        while len(multiprocessing.active_children()) !=1:
            pass
        print('运行结束!')
    
    if __name__ == '__main__':
        start_process()

    (三)unittest基本使用

    1、单元测试

    (1)自己测试自己写的代码

             单元测试框架:自动校验结果

        unittest

        pytest

    (2)如何写测试用例

    (3)如何查找测试用例:unittest.defaultTestLoader.discover()

    # 查找某个目录下的测试用例
        test_suite = unittest.defaultTestLoader.discover('cases','test*.py')

    注:如果cases在别的其他路径下,而不在当前路径,直接将case文件夹名称改为绝对路径

    (4)参数化

    需要安装一个模块:

    pip install parameterized

    实例如下:

     # 参数化
        @parameterized.parameterized.expand(
           [
               [1,2,3,'参数化第一条'],
               [-1,2,1,'参数化第二条'],
               [-1,2,2,'参数化第三条'],
            ]
         )
        def test_param_add(self,a,b,c,desc):
            self._testMethodDoc = desc
            self.desc = desc
            result = add(a,b)
            self.assertEqual(c,result,'预期是%s,实际结果是%s'%(c,result))

    (5)产生测试报告

    实例1:my_function

    def write_file(file_name,content):
        with open(file_name,'w',encoding='utf-8') as fw:
            fw.write(content)
    
    def add(a,b):
        return a+b
    
    if __name__ == '__main__':
        print(add(1,2))
        print(add(-1, 2))
        print(add(-1, -2))
        print(add(-1.1, -2.0))
        print(add(-1.1, 2))

    引用模块:HTMLTestRunner.py

    """
    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.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'
    
        # 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.
    """
    
    # URL: http://tungwaiyip.info/software/HTMLTestRunner.html
    
    __author__ = "Wai Yip Tung"
    __version__ = "0.8.2"
    
    
    """
    Change History
    
    Version 0.8.2
    * Show output inline instead of popup window (Viorel Lupu).
    
    Version in 0.8.1
    * Validated XHTML (Wolfgang Borgert).
    * Added description of test classes and test cases.
    
    Version in 0.8.0
    * Define Template_mixin class for customization.
    * Workaround a IE 6 bug that it does not treat <script> block as CDATA.
    
    Version in 0.7.1
    * Back port to Python 2.3 (Frank Horowitz).
    * Fix missing scroll bars in detail log (Podi).
    """
    
    # TODO: color stderr
    # TODO: simplify javascript using ,ore than 1 class in the class attribute?
    
    import datetime
    import io
    import sys
    import time
    import unittest
    from xml.sax import saxutils
    
    
    # ------------------------------------------------------------------------
    # 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
        +------------------------+
        |<html>                  |
        |  <head>                |
        |                        |
        |   STYLESHEET           |
        |   +----------------+   |
        |   |                |   |
        |   +----------------+   |
        |                        |
        |  </head>               |
        |                        |
        |  <body>                |
        |                        |
        |   HEADING              |
        |   +----------------+   |
        |   |                |   |
        |   +----------------+   |
        |                        |
        |   REPORT               |
        |   +----------------+   |
        |   |                |   |
        |   +----------------+   |
        |                        |
        |   ENDING               |
        |   +----------------+   |
        |   |                |   |
        |   +----------------+   |
        |                        |
        |  </body>               |
        |</html>                 |
        +------------------------+
        """
    
        STATUS = {
        0: 'pass',
        1: 'fail',
        2: 'error',
        }
    
        DEFAULT_TITLE = 'Unit Test Report'
        DEFAULT_DESCRIPTION = ''
    
        # ------------------------------------------------------------------------
        # HTML Template
    
        HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>%(title)s</title>
        <meta name="generator" content="%(generator)s"/>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
        %(stylesheet)s
    </head>
    <body>
    <script language="javascript" type="text/javascript"><!--
    output_list = Array();
    
    /* level - 0:Summary; 1:Failed; 2:All */
    function showCase(level) {
        trs = document.getElementsByTagName("tr");
        for (var i = 0; i < trs.length; i++) {
            tr = trs[i];
            id = tr.id;
            if (id.substr(0,2) == 'ft') {
                if (level < 1) {
                    tr.className = 'hiddenRow';
                }
                else {
                    tr.className = '';
                }
            }
            if (id.substr(0,2) == 'pt') {
                if (level > 1) {
                    tr.className = '';
                }
                else {
                    tr.className = 'hiddenRow';
                }
            }
        }
    }
    
    
    function showClassDetail(cid, count) {
        var id_list = Array(count);
        var toHide = 1;
        for (var i = 0; i < count; i++) {
            tid0 = 't' + cid.substr(1) + '.' + (i+1);
            tid = 'f' + tid0;
            tr = document.getElementById(tid);
            if (!tr) {
                tid = 'p' + tid0;
                tr = document.getElementById(tid);
            }
            id_list[i] = tid;
            if (tr.className) {
                toHide = 0;
            }
        }
        for (var i = 0; i < count; i++) {
            tid = id_list[i];
            if (toHide) {
                document.getElementById('div_'+tid).style.display = 'none'
                document.getElementById(tid).className = 'hiddenRow';
            }
            else {
                document.getElementById(tid).className = '';
            }
        }
    }
    
    
    function showTestDetail(div_id){
        var details_div = document.getElementById(div_id)
        var displayState = details_div.style.display
        // alert(displayState)
        if (displayState != 'block' ) {
            displayState = 'block'
            details_div.style.display = 'block'
        }
        else {
            details_div.style.display = 'none'
        }
    }
    
    
    function html_escape(s) {
        s = s.replace(/&/g,'&amp;');
        s = s.replace(/</g,'&lt;');
        s = s.replace(/>/g,'&gt;');
        return s;
    }
    
    /* obsoleted by detail in <div>
    function showOutput(id, name) {
        var w = window.open("", //url
                        name,
                        "resizable,scrollbars,status,width=800,height=450");
        d = w.document;
        d.write("<pre>");
        d.write(html_escape(output_list[id]));
        d.write("
    ");
        d.write("<a href='javascript:window.close()'>close</a>
    ");
        d.write("</pre>
    ");
        d.close();
    }
    */
    --></script>
    
    %(heading)s
    %(report)s
    %(ending)s
    
    </body>
    </html>
    """
        # variables: (title, generator, stylesheet, heading, report, ending)
    
    
        # ------------------------------------------------------------------------
        # Stylesheet
        #
        # alternatively use a <link> for external style sheet, e.g.
        #   <link rel="stylesheet" href="$url" type="text/css">
    
        STYLESHEET_TMPL = """
    <style type="text/css" media="screen">
    body        { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; }
    table       { font-size: 100%; }
    pre         { }
    
    /* -- heading ---------------------------------------------------------------------- */
    h1 {
        font-size: 16pt;
        color: gray;
    }
    .heading {
        margin-top: 0ex;
        margin-bottom: 1ex;
    }
    
    .heading .attribute {
        margin-top: 1ex;
        margin-bottom: 0;
    }
    
    .heading .description {
        margin-top: 4ex;
        margin-bottom: 6ex;
    }
    
    /* -- css div popup ------------------------------------------------------------------------ */
    a.popup_link {
    }
    
    a.popup_link:hover {
        color: red;
    }
    
    .popup_window {
        display: none;
        position: relative;
        left: 0px;
        top: 0px;
        /*border: solid #627173 1px; */
        padding: 10px;
        background-color: #E6E6D6;
        font-family: "Lucida Console", "Courier New", Courier, monospace;
        text-align: left;
        font-size: 8pt;
         500px;
    }
    
    }
    /* -- report ------------------------------------------------------------------------ */
    #show_detail_line {
        margin-top: 3ex;
        margin-bottom: 1ex;
    }
    #result_table {
         80%;
        border-collapse: collapse;
        border: 1px solid #777;
    }
    #header_row {
        font-weight: bold;
        color: white;
        background-color: #777;
    }
    #result_table td {
        border: 1px solid #777;
        padding: 2px;
    }
    #total_row  { font-weight: bold; }
    .passClass  { background-color: #6c6; }
    .failClass  { background-color: #c60; }
    .errorClass { background-color: #c00; }
    .passCase   { color: #6c6; }
    .failCase   { color: #c60; font-weight: bold; }
    .errorCase  { color: #c00; font-weight: bold; }
    .hiddenRow  { display: none; }
    .testcase   { margin-left: 2em; }
    
    
    /* -- ending ---------------------------------------------------------------------- */
    #ending {
    }
    
    </style>
    """
    
    
    
        # ------------------------------------------------------------------------
        # Heading
        #
    
        HEADING_TMPL = """<div class='heading'>
    <h1>%(title)s</h1>
    %(parameters)s
    <p class='description'>%(description)s</p>
    </div>
    
    """ # variables: (title, parameters, description)
    
        HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
    """ # variables: (name, value)
    
    
    
        # ------------------------------------------------------------------------
        # Report
        #
    
        REPORT_TMPL = """
    <p id='show_detail_line'>Show
    <a href='javascript:showCase(0)'>Summary</a>
    <a href='javascript:showCase(1)'>Failed</a>
    <a href='javascript:showCase(2)'>All</a>
    </p>
    <table id='result_table'>
    <colgroup>
    <col align='left' />
    <col align='right' />
    <col align='right' />
    <col align='right' />
    <col align='right' />
    <col align='right' />
    </colgroup>
    <tr id='header_row'>
        <td>Test Group/Test case</td>
        <td>Count</td>
        <td>Pass</td>
        <td>Fail</td>
        <td>Error</td>
        <td>View</td>
    </tr>
    %(test_list)s
    <tr id='total_row'>
        <td>Total</td>
        <td>%(count)s</td>
        <td>%(Pass)s</td>
        <td>%(fail)s</td>
        <td>%(error)s</td>
        <td>&nbsp;</td>
    </tr>
    </table>
    """ # variables: (test_list, count, Pass, fail, error)
    
        REPORT_CLASS_TMPL = r"""
    <tr class='%(style)s'>
        <td>%(desc)s</td>
        <td>%(count)s</td>
        <td>%(Pass)s</td>
        <td>%(fail)s</td>
        <td>%(error)s</td>
        <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td>
    </tr>
    """ # variables: (style, desc, count, Pass, fail, error, cid)
    
    
        REPORT_TEST_WITH_OUTPUT_TMPL = r"""
    <tr id='%(tid)s' class='%(Class)s'>
        <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
        <td colspan='5' align='center'>
    
        <!--css div popup start-->
        <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
            %(status)s</a>
    
        <div id='div_%(tid)s' class="popup_window">
            <div style='text-align: right; color:red;cursor:pointer'>
            <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " >
               [x]</a>
            </div>
            <pre>
            %(script)s
            </pre>
        </div>
        <!--css div popup end-->
    
        </td>
    </tr>
    """ # variables: (tid, Class, style, desc, status)
    
    
        REPORT_TEST_NO_OUTPUT_TMPL = r"""
    <tr id='%(tid)s' class='%(Class)s'>
        <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
        <td colspan='5' align='center'>%(status)s</td>
    </tr>
    """ # variables: (tid, Class, style, desc, status)
    
    
        REPORT_TEST_OUTPUT_TMPL = r"""
    %(id)s: %(output)s
    """ # variables: (id, output)
    
    
    
        # ------------------------------------------------------------------------
        # ENDING
        #
    
        ENDING_TMPL = """<div id='ending'>&nbsp;</div>"""
    
    # -------------------- The end of the Template class -------------------
    
    
    TestResult = unittest.TestResult
    
    class _TestResult(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):
            TestResult.__init__(self)
            self.stdout0 = None
            self.stderr0 = None
            self.success_count = 0
            self.failure_count = 0
            self.error_count = 0
            self.verbosity = verbosity
    
            # 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 = []
    
    
        def startTest(self, test):
            TestResult.startTest(self, test)
            # 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
    
    
        def complete_output(self):
            """
            Disconnect output redirection and return buffer.
            Safe to call multiple times.
            """
            if self.stdout0:
                sys.stdout = self.stdout0
                sys.stderr = self.stderr0
                self.stdout0 = None
                self.stderr0 = None
            return self.outputBuffer.getvalue()
    
    
        def stopTest(self, 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()
    
    
        def addSuccess(self, test):
            self.success_count += 1
            TestResult.addSuccess(self, test)
            output = self.complete_output()
            self.result.append((0, test, output, ''))
            if self.verbosity > 1:
                sys.stderr.write('ok ')
                sys.stderr.write(str(test))
                sys.stderr.write('
    ')
            else:
                sys.stderr.write('.')
    
        def addError(self, test, err):
            self.error_count += 1
            TestResult.addError(self, test, err)
            _, _exc_str = self.errors[-1]
            output = self.complete_output()
            self.result.append((2, test, output, _exc_str))
            if self.verbosity > 1:
                sys.stderr.write('E  ')
                sys.stderr.write(str(test))
                sys.stderr.write('
    ')
            else:
                sys.stderr.write('E')
    
        def addFailure(self, test, err):
            self.failure_count += 1
            TestResult.addFailure(self, test, err)
            _, _exc_str = self.failures[-1]
            output = self.complete_output()
            self.result.append((1, test, output, _exc_str))
            if self.verbosity > 1:
                sys.stderr.write('F  ')
                sys.stderr.write(str(test))
                sys.stderr.write('
    ')
            else:
                sys.stderr.write('F')
    
    
    class HTMLTestRunner(Template_mixin):
        """
        """
        def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
            self.stream = stream
            self.verbosity = verbosity
            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
    
            self.startTime = datetime.datetime.now()
    
    
        def run(self, test):
            "Run the given test case or test suite."
            result = _TestResult(self.verbosity)
            test(result)
            self.stopTime = datetime.datetime.now()
            self.generateReport(test, result)
            # print >> sys.stderr, '
    Time Elapsed: %s' % (self.stopTime-self.startTime)
            print(sys.stderr, '
    Time Elapsed: %s' % (self.stopTime-self.startTime))
            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 in result_list:
                cls = t.__class__
                if not cls in rmap:
                    rmap[cls] = []
                    classes.append(cls)
                rmap[cls].append((n,t,o,e))
            r = [(cls, rmap[cls]) for cls in classes]
            return r
    
    
        def getReportAttributes(self, result):
            """
            Return report attributes as a list of (name, value).
            Override this to add custom attributes.
            """
            startTime = str(self.startTime)[:19]
            duration = str(self.stopTime - self.startTime)
            status = []
            if result.success_count: status.append('Pass %s'    % result.success_count)
            if result.failure_count: status.append('Failure %s' % result.failure_count)
            if result.error_count:   status.append('Error %s'   % result.error_count  )
            if status:
                status = ' '.join(status)
            else:
                status = 'none'
            return [
                ('Start Time', startTime),
                ('Duration', duration),
                ('Status', status),
            ]
    
    
        def generateReport(self, test, result):
            report_attrs = self.getReportAttributes(result)
            generator = 'HTMLTestRunner %s' % __version__
            stylesheet = self._generate_stylesheet()
            heading = self._generate_heading(report_attrs)
            report = self._generate_report(result)
            ending = self._generate_ending()
            output = self.HTML_TMPL % dict(
                title = saxutils.escape(self.title),
                generator = generator,
                stylesheet = stylesheet,
                heading = heading,
                report = report,
                ending = ending,
            )
            self.stream.write(output.encode('utf8'))
    
    
        def _generate_stylesheet(self):
            return self.STYLESHEET_TMPL
    
    
        def _generate_heading(self, report_attrs):
            a_lines = []
            for name, value in report_attrs:
                line = self.HEADING_ATTRIBUTE_TMPL % dict(
                        name = saxutils.escape(name),
                        value = saxutils.escape(value),
                    )
                a_lines.append(line)
            heading = self.HEADING_TMPL % dict(
                title = saxutils.escape(self.title),
                parameters = ''.join(a_lines),
                description = saxutils.escape(self.description),
            )
            return heading
    
    
        def _generate_report(self, result):
            rows = []
            sortedResult = self.sortResult(result.result)
            for cid, (cls, cls_results) in enumerate(sortedResult):
                # subtotal for a class
                np = nf = ne = 0
                for n,t,o,e in cls_results:
                    if n == 0: np += 1
                    elif n == 1: nf += 1
                    else: ne += 1
    
                # format class description
                if cls.__module__ == "__main__":
                    name = cls.__name__
                else:
                    name = "%s.%s" % (cls.__module__, cls.__name__)
                doc = cls.__doc__ and cls.__doc__.split("
    ")[0] or ""
                desc = doc and '%s: %s' % (name, doc) or name
    
                row = self.REPORT_CLASS_TMPL % dict(
                    style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
                    desc = desc,
                    count = np+nf+ne,
                    Pass = np,
                    fail = nf,
                    error = ne,
                    cid = 'c%s' % (cid+1),
                )
                rows.append(row)
    
                for tid, (n,t,o,e) in enumerate(cls_results):
                    self._generate_report_test(rows, cid, tid, n, t, o, e)
    
            report = self.REPORT_TMPL % dict(
                test_list = ''.join(rows),
                count = str(result.success_count+result.failure_count+result.error_count),
                Pass = str(result.success_count),
                fail = str(result.failure_count),
                error = str(result.error_count),
            )
            return report
    
    
        def _generate_report_test(self, rows, cid, tid, n, t, o, e):
            # e.g. 'pt1.1', 'ft1.1', etc
            has_output = bool(o or e)
            tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
            name = t.id().split('.')[-1]
            doc = t.shortDescription() or ""
            desc = doc and ('%s: %s' % (name, doc)) or name
            tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
    
            # o and e should be byte string because they are collected from stdout and stderr?
            if isinstance(o,str):
                # TODO: some problem with 'string_escape': it escape 
     and mess up formating
                # uo = unicode(o.encode('string_escape'))
                # uo = o.decode('latin-1')
                uo = e
            else:
                uo = o
            if isinstance(e,str):
                # TODO: some problem with 'string_escape': it escape 
     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 = saxutils.escape(str(uo)+ue),
            )
    
            row = tmpl % dict(
                tid = tid,
                Class = (n == 0 and 'hiddenRow' or 'none'),
                style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
                desc = desc,
                script = script,
                status = self.STATUS[n],
            )
            rows.append(row)
            if not has_output:
                return
    
        def _generate_ending(self):
            return self.ENDING_TMPL
    
    
    ##############################################################################
    # Facilities for running tests from the command line
    ##############################################################################
    
    # Note: Reuse unittest.TestProgram to launch test. In the future we may
    # build our own launcher to support more specific command line
    # parameters like test title, CSS, etc.
    class TestProgram(unittest.TestProgram):
        """
        A variation of the unittest.TestProgram. Please refer to the base
        class for command line parameters.
        """
        def runTests(self):
            # Pick HTMLTestRunner as the default test runner.
            # base class's testRunner parameter is not useful because it means
            # we have to instantiate HTMLTestRunner before we know self.verbosity.
            if self.testRunner is None:
                self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
            unittest.TestProgram.runTests(self)
    
    main = TestProgram
    
    ##############################################################################
    # Executing this module from the command line
    ##############################################################################
    
    if __name__ == "__main__":
        main(module=None)

    实例2:单元测试.py

    import unittest
    import my_function
    import HTMLTestRunner
    class TestAdd(unittest.TestCase):
    
        def test_add_normal(self):
            result = my_function.add(1,2)
            self.assertEqual(3,result)
    
        def test_add_error1(self):
            result = my_function.add(1,2)
            self.assertEqual(4,result)
    
        def test_add_error2(self):
            result = my_function.add(1,2)
            self.assertEqual(4,result,'正常整数加法,没有通过')
    
    if __name__ == '__main__':
        # 不产生测试报告
        # unittest.main()
        
        # 单个运行某个测试用例
        # test_suite = unittest.TestSuite()
        # test_suite.addTest(TestAdd('test_add_error2'))
        # test_suite.addTest(TestAdd('test_add_error1'))
        
        # 运行某个类里面所有的测试用例
        test_suite = unittest.makeSuite(TestAdd)
        with open('report.html','wb') as fw:
            runner = HTMLTestRunner.HTMLTestRunner(stream=fw,title='天马座测试报告',
                                                   description='天马座接口测试报告',
                                                   verbosity=2
                                                )
            runner.run(test_suite)

    引用模块:HTMLTestRunnerNew.py

    # -*- coding: utf-8 -*-
    """
    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.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'
    
        # 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.
    """
    
    # URL: http://tungwaiyip.info/software/HTMLTestRunner.html
    
    __author__ = "Wai Yip Tung"
    __version__ = "0.9.1"
    
    """
    Change History
    Version 0.9.1
    * 用Echarts添加执行情况统计图 (灰蓝)
    
    Version 0.9.0
    * 改成Python 3.x (灰蓝)
    
    Version 0.8.3
    * 使用 Bootstrap稍加美化 (灰蓝)
    * 改为中文 (灰蓝)
    
    Version 0.8.2
    * Show output inline instead of popup window (Viorel Lupu).
    
    Version in 0.8.1
    * Validated XHTML (Wolfgang Borgert).
    * Added description of test classes and test cases.
    
    Version in 0.8.0
    * Define Template_mixin class for customization.
    * Workaround a IE 6 bug that it does not treat <script> block as CDATA.
    
    Version in 0.7.1
    * Back port to Python 2.3 (Frank Horowitz).
    * Fix missing scroll bars in detail log (Podi).
    """
    
    # TODO: color stderr
    # TODO: simplify javascript using ,ore than 1 class in the class attribute?
    
    import datetime
    import sys
    import io
    import time
    import unittest
    from xml.sax import saxutils
    
    
    # ------------------------------------------------------------------------
    # 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
        +------------------------+
        |<html>                  |
        |  <head>                |
        |                        |
        |   STYLESHEET           |
        |   +----------------+   |
        |   |                |   |
        |   +----------------+   |
        |                        |
        |  </head>               |
        |                        |
        |  <body>                |
        |                        |
        |   HEADING              |
        |   +----------------+   |
        |   |                |   |
        |   +----------------+   |
        |                        |
        |   REPORT               |
        |   +----------------+   |
        |   |                |   |
        |   +----------------+   |
        |                        |
        |   ENDING               |
        |   +----------------+   |
        |   |                |   |
        |   +----------------+   |
        |                        |
        |  </body>               |
        |</html>                 |
        +------------------------+
        """
    
        STATUS = {
            0: u'通过',
            1: u'失败',
            2: u'错误',
        }
    
        DEFAULT_TITLE = 'Unit Test Report'
        DEFAULT_DESCRIPTION = ''
    
        # ------------------------------------------------------------------------
        # HTML Template
    
        HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>%(title)s</title>
        <meta name="generator" content="%(generator)s"/>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
        
        <link href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet">
        <script src="https://cdn.bootcss.com/echarts/3.8.5/echarts.common.min.js"></script>
        <!-- <script type="text/javascript" src="js/echarts.common.min.js"></script> -->
        
        %(stylesheet)s
        
    </head>
    <body>
        <script language="javascript" type="text/javascript"><!--
        output_list = Array();
    
        /* level - 0:Summary; 1:Failed; 2:All */
        function showCase(level) {
            trs = document.getElementsByTagName("tr");
            for (var i = 0; i < trs.length; i++) {
                tr = trs[i];
                id = tr.id;
                if (id.substr(0,2) == 'ft') {
                    if (level < 1) {
                        tr.className = 'hiddenRow';
                    }
                    else {
                        tr.className = '';
                    }
                }
                if (id.substr(0,2) == 'pt') {
                    if (level > 1) {
                        tr.className = '';
                    }
                    else {
                        tr.className = 'hiddenRow';
                    }
                }
            }
        }
    
    
        function showClassDetail(cid, count) {
            var id_list = Array(count);
            var toHide = 1;
            for (var i = 0; i < count; i++) {
                tid0 = 't' + cid.substr(1) + '.' + (i+1);
                tid = 'f' + tid0;
                tr = document.getElementById(tid);
                if (!tr) {
                    tid = 'p' + tid0;
                    tr = document.getElementById(tid);
                }
                id_list[i] = tid;
                if (tr.className) {
                    toHide = 0;
                }
            }
            for (var i = 0; i < count; i++) {
                tid = id_list[i];
                if (toHide) {
                    document.getElementById('div_'+tid).style.display = 'none'
                    document.getElementById(tid).className = 'hiddenRow';
                }
                else {
                    document.getElementById(tid).className = '';
                }
            }
        }
    
    
        function showTestDetail(div_id){
            var details_div = document.getElementById(div_id)
            var displayState = details_div.style.display
            // alert(displayState)
            if (displayState != 'block' ) {
                displayState = 'block'
                details_div.style.display = 'block'
            }
            else {
                details_div.style.display = 'none'
            }
        }
    
    
        function html_escape(s) {
            s = s.replace(/&/g,'&amp;');
            s = s.replace(/</g,'&lt;');
            s = s.replace(/>/g,'&gt;');
            return s;
        }
    
        /* obsoleted by detail in <div>
        function showOutput(id, name) {
            var w = window.open("", //url
                            name,
                            "resizable,scrollbars,status,width=800,height=450");
            d = w.document;
            d.write("<pre>");
            d.write(html_escape(output_list[id]));
            d.write("
    ");
            d.write("<a href='javascript:window.close()'>close</a>
    ");
            d.write("</pre>
    ");
            d.close();
        }
        */
        --></script>
    
        <div id="div_base">
            %(heading)s
            %(report)s
            %(ending)s
            %(chart_script)s
        </div>
    </body>
    </html>
    """  # variables: (title, generator, stylesheet, heading, report, ending, chart_script)
    
        ECHARTS_SCRIPT = """
        <script type="text/javascript">
            // 基于准备好的dom,初始化echarts实例
            var myChart = echarts.init(document.getElementById('chart'));
    
            // 指定图表的配置项和数据
            var option = {
                title : {
                    text: '测试执行情况',
                    x:'center'
                },
                tooltip : {
                    trigger: 'item',
                    formatter: "{a} <br/>{b} : {c} ({d}%%)"
                },
                color: ['#95b75d', 'grey', '#b64645'],
                legend: {
                    orient: 'vertical',
                    left: 'left',
                    data: ['通过','失败','错误']
                },
                series : [
                    {
                        name: '测试执行情况',
                        type: 'pie',
                        radius : '60%%',
                        center: ['50%%', '60%%'],
                        data:[
                            {value:%(Pass)s, name:'通过'},
                            {value:%(fail)s, name:'失败'},
                            {value:%(error)s, name:'错误'}
                        ],
                        itemStyle: {
                            emphasis: {
                                shadowBlur: 10,
                                shadowOffsetX: 0,
                                shadowColor: 'rgba(0, 0, 0, 0.5)'
                            }
                        }
                    }
                ]
            };
    
            // 使用刚指定的配置项和数据显示图表。
            myChart.setOption(option);
        </script>
        """  # variables: (Pass, fail, error)
    
        # ------------------------------------------------------------------------
        # Stylesheet
        #
        # alternatively use a <link> for external style sheet, e.g.
        #   <link rel="stylesheet" href="$url" type="text/css">
    
        STYLESHEET_TMPL = """
    <style type="text/css" media="screen">
        body        { font-family: Microsoft YaHei,Consolas,arial,sans-serif; font-size: 80%; }
        table       { font-size: 100%; }
        pre         { white-space: pre-wrap;word-wrap: break-word; }
    
        /* -- heading ---------------------------------------------------------------------- */
        h1 {
            font-size: 16pt;
            color: gray;
        }
        .heading {
            margin-top: 0ex;
            margin-bottom: 1ex;
        }
    
        .heading .attribute {
            margin-top: 1ex;
            margin-bottom: 0;
        }
    
        .heading .description {
            margin-top: 2ex;
            margin-bottom: 3ex;
        }
    
        /* -- css div popup ------------------------------------------------------------------------ */
        a.popup_link {
        }
    
        a.popup_link:hover {
            color: red;
        }
    
        .popup_window {
            display: none;
            position: relative;
            left: 0px;
            top: 0px;
            /*border: solid #627173 1px; */
            padding: 10px;
            /*background-color: #E6E6D6; */
            font-family: "Lucida Console", "Courier New", Courier, monospace;
            text-align: left;
            font-size: 8pt;
            /*  500px;*/
        }
    
        }
        /* -- report ------------------------------------------------------------------------ */
        #show_detail_line {
            margin-top: 3ex;
            margin-bottom: 1ex;
        }
        #result_table {
             99%;
        }
        #header_row {
            font-weight: bold;
            color: #303641;
            background-color: #ebebeb;
        }
        #total_row  { font-weight: bold; }
        .passClass  { background-color: #bdedbc; }
        .failClass  { background-color: #ffefa4; }
        .errorClass { background-color: #ffc9c9; }
        .passCase   { color: #6c6; }
        .failCase   { color: #FF6600; font-weight: bold; }
        .errorCase  { color: #c00; font-weight: bold; }
        .hiddenRow  { display: none; }
        .testcase   { margin-left: 2em; }
    
    
        /* -- ending ---------------------------------------------------------------------- */
        #ending {
        }
    
        #div_base {
                    position:absolute;
                    top:0%;
                    left:5%;
                    right:5%;
                     auto;
                    height: auto;
                    margin: -15px 0 0 0;
        }
    </style>
    """
    
        # ------------------------------------------------------------------------
        # Heading
        #
    
        HEADING_TMPL = """
        <div class='page-header'>
            <h1>%(title)s</h1>
        %(parameters)s
        </div>
        <div style="float: left;50%%;"><p class='description'>%(description)s</p></div>
        <div id="chart" style="50%%;height:400px;float:left;"></div>
    """  # variables: (title, parameters, description)
    
        HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
    """  # variables: (name, value)
    
        # ------------------------------------------------------------------------
        # Report
        #
    
        REPORT_TMPL = u"""
        <div class="btn-group btn-group-sm">
            <button class="btn btn-default" onclick='javascript:showCase(0)'>总结</button>
            <button class="btn btn-default" onclick='javascript:showCase(1)'>失败</button>
            <button class="btn btn-default" onclick='javascript:showCase(2)'>全部</button>
        </div>
        <p></p>
        <table id='result_table' class="table table-bordered">
            <colgroup>
                <col align='left' />
                <col align='right' />
                <col align='right' />
                <col align='right' />
                <col align='right' />
                <col align='right' />
            </colgroup>
            <tr id='header_row'>
                <td>测试套件/测试用例</td>
                <td>总数</td>
                <td>通过</td>
                <td>失败</td>
                <td>错误</td>
                <td>查看</td>
            </tr>
            %(test_list)s
            <tr id='total_row'>
                <td>总计</td>
                <td>%(count)s</td>
                <td>%(Pass)s</td>
                <td>%(fail)s</td>
                <td>%(error)s</td>
                <td>&nbsp;</td>
            </tr>
        </table>
    """  # variables: (test_list, count, Pass, fail, error)
    
        REPORT_CLASS_TMPL = u"""
        <tr class='%(style)s'>
            <td>%(desc)s</td>
            <td>%(count)s</td>
            <td>%(Pass)s</td>
            <td>%(fail)s</td>
            <td>%(error)s</td>
            <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">详情</a></td>
        </tr>
    """  # variables: (style, desc, count, Pass, fail, error, cid)
    
        REPORT_TEST_WITH_OUTPUT_TMPL = r"""
    <tr id='%(tid)s' class='%(Class)s'>
        <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
        <td colspan='5' align='center'>
    
        <!--css div popup start-->
        <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
            %(status)s</a>
    
        <div id='div_%(tid)s' class="popup_window">
            <pre>%(script)s</pre>
        </div>
        <!--css div popup end-->
    
        </td>
    </tr>
    """  # variables: (tid, Class, style, desc, status)
    
        REPORT_TEST_NO_OUTPUT_TMPL = r"""
    <tr id='%(tid)s' class='%(Class)s'>
        <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
        <td colspan='5' align='center'>%(status)s</td>
    </tr>
    """  # variables: (tid, Class, style, desc, status)
    
        REPORT_TEST_OUTPUT_TMPL = r"""%(id)s: %(output)s"""  # variables: (id, output)
    
        # ------------------------------------------------------------------------
        # ENDING
        #
    
        ENDING_TMPL = """<div id='ending'>&nbsp;</div>"""
    
    # -------------------- The end of the Template class -------------------
    
    
    TestResult = unittest.TestResult
    
    
    class _TestResult(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):
            TestResult.__init__(self)
            self.stdout0 = None
            self.stderr0 = None
            self.success_count = 0
            self.failure_count = 0
            self.error_count = 0
            self.verbosity = verbosity
    
            # 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.subtestlist = []
    
        def startTest(self, test):
            TestResult.startTest(self, test)
            # 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
    
        def complete_output(self):
            """
            Disconnect output redirection and return buffer.
            Safe to call multiple times.
            """
            if self.stdout0:
                sys.stdout = self.stdout0
                sys.stderr = self.stderr0
                self.stdout0 = None
                self.stderr0 = None
            return self.outputBuffer.getvalue()
    
        def stopTest(self, 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()
    
        def addSuccess(self, test):
            if test not in self.subtestlist:
                self.success_count += 1
                TestResult.addSuccess(self, test)
                output = self.complete_output()
                self.result.append((0, test, output, ''))
                if self.verbosity > 1:
                    sys.stderr.write('ok ')
                    sys.stderr.write(str(test))
                    sys.stderr.write('
    ')
                else:
                    sys.stderr.write('.')
    
        def addError(self, test, err):
            self.error_count += 1
            TestResult.addError(self, test, err)
            _, _exc_str = self.errors[-1]
            output = self.complete_output()
            self.result.append((2, test, output, _exc_str))
            if self.verbosity > 1:
                sys.stderr.write('E  ')
                sys.stderr.write(str(test))
                sys.stderr.write('
    ')
            else:
                sys.stderr.write('E')
    
        def addFailure(self, test, err):
            self.failure_count += 1
            TestResult.addFailure(self, test, err)
            _, _exc_str = self.failures[-1]
            output = self.complete_output()
            self.result.append((1, test, output, _exc_str))
            if self.verbosity > 1:
                sys.stderr.write('F  ')
                sys.stderr.write(str(test))
                sys.stderr.write('
    ')
            else:
                sys.stderr.write('F')
    
        def addSubTest(self, test, subtest, err):
            if err is not None:
                if getattr(self, 'failfast', False):
                    self.stop()
                if issubclass(err[0], test.failureException):
                    self.failure_count += 1
                    errors = self.failures
                    errors.append((subtest, self._exc_info_to_string(err, subtest)))
                    output = self.complete_output()
                    self.result.append((1, test, output + '
    SubTestCase Failed:
    ' + str(subtest),
                                        self._exc_info_to_string(err, subtest)))
                    if self.verbosity > 1:
                        sys.stderr.write('F  ')
                        sys.stderr.write(str(subtest))
                        sys.stderr.write('
    ')
                    else:
                        sys.stderr.write('F')
                else:
                    self.error_count += 1
                    errors = self.errors
                    errors.append((subtest, self._exc_info_to_string(err, subtest)))
                    output = self.complete_output()
                    self.result.append(
                        (2, test, output + '
    SubTestCase Error:
    ' + str(subtest), self._exc_info_to_string(err, subtest)))
                    if self.verbosity > 1:
                        sys.stderr.write('E  ')
                        sys.stderr.write(str(subtest))
                        sys.stderr.write('
    ')
                    else:
                        sys.stderr.write('E')
                self._mirrorOutput = True
            else:
                self.subtestlist.append(subtest)
                self.subtestlist.append(test)
                self.success_count += 1
                output = self.complete_output()
                self.result.append((0, test, output + '
    SubTestCase Pass:
    ' + str(subtest), ''))
                if self.verbosity > 1:
                    sys.stderr.write('ok ')
                    sys.stderr.write(str(subtest))
                    sys.stderr.write('
    ')
                else:
                    sys.stderr.write('.')
    
    
    class HTMLTestRunner(Template_mixin):
    
        def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
            self.stream = stream
            self.verbosity = verbosity
            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
    
            self.startTime = datetime.datetime.now()
    
        def run(self, test):
            "Run the given test case or test suite."
            result = _TestResult(self.verbosity)
            test(result)
            self.stopTime = datetime.datetime.now()
            self.generateReport(test, result)
            print('
    Time Elapsed: %s' % (self.stopTime-self.startTime), file=sys.stderr)
            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 in result_list:
                cls = t.__class__
                if cls not in rmap:
                    rmap[cls] = []
                    classes.append(cls)
                rmap[cls].append((n,t,o,e))
            r = [(cls, rmap[cls]) for cls in classes]
            return r
    
        def getReportAttributes(self, result):
            """
            Return report attributes as a list of (name, value).
            Override this to add custom attributes.
            """
            startTime = str(self.startTime)[:19]
            duration = str(self.stopTime - self.startTime)
            status = []
            if result.success_count: status.append(u'通过 %s' % result.success_count)
            if result.failure_count: status.append(u'失败 %s' % result.failure_count)
            if result.error_count:   status.append(u'错误 %s' % result.error_count  )
            if status:
                status = ' '.join(status)
            else:
                status = 'none'
            return [
                (u'开始时间', startTime),
                (u'运行时长', duration),
                (u'状态', status),
            ]
    
        def generateReport(self, test, result):
            report_attrs = self.getReportAttributes(result)
            generator = 'HTMLTestRunner %s' % __version__
            stylesheet = self._generate_stylesheet()
            heading = self._generate_heading(report_attrs)
            report = self._generate_report(result)
            ending = self._generate_ending()
            chart = self._generate_chart(result)
            output = self.HTML_TMPL % dict(
                title = saxutils.escape(self.title),
                generator = generator,
                stylesheet = stylesheet,
                heading = heading,
                report = report,
                ending = ending,
                chart_script = chart
            )
            self.stream.write(output.encode('utf8'))
    
        def _generate_stylesheet(self):
            return self.STYLESHEET_TMPL
    
        def _generate_heading(self, report_attrs):
            a_lines = []
            for name, value in report_attrs:
                line = self.HEADING_ATTRIBUTE_TMPL % dict(
                    name = saxutils.escape(name),
                    value = saxutils.escape(value),
                )
                a_lines.append(line)
            heading = self.HEADING_TMPL % dict(
                title = saxutils.escape(self.title),
                parameters = ''.join(a_lines),
                description = saxutils.escape(self.description),
            )
            return heading
    
        def _generate_report(self, result):
            rows = []
            sortedResult = self.sortResult(result.result)
            for cid, (cls, cls_results) in enumerate(sortedResult):
                # subtotal for a class
                np = nf = ne = 0
                for n,t,o,e in cls_results:
                    if n == 0: np += 1
                    elif n == 1: nf += 1
                    else: ne += 1
    
                # format class description
                if cls.__module__ == "__main__":
                    name = cls.__name__
                else:
                    name = "%s.%s" % (cls.__module__, cls.__name__)
                doc = cls.__doc__ and cls.__doc__.split("
    ")[0] or ""
                desc = doc and '%s: %s' % (name, doc) or name
    
                row = self.REPORT_CLASS_TMPL % dict(
                    style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
                    desc = desc,
                    count = np+nf+ne,
                    Pass = np,
                    fail = nf,
                    error = ne,
                    cid = 'c%s' % (cid+1),
                )
                rows.append(row)
    
                for tid, (n,t,o,e) in enumerate(cls_results):
                    self._generate_report_test(rows, cid, tid, n, t, o, e)
    
            report = self.REPORT_TMPL % dict(
                test_list = ''.join(rows),
                count = str(result.success_count+result.failure_count+result.error_count),
                Pass = str(result.success_count),
                fail = str(result.failure_count),
                error = str(result.error_count),
            )
            return report
    
        def _generate_chart(self, result):
            chart = self.ECHARTS_SCRIPT % dict(
                Pass=str(result.success_count),
                fail=str(result.failure_count),
                error=str(result.error_count),
            )
            return chart
    
        def _generate_report_test(self, rows, cid, tid, n, t, o, e):
            # e.g. 'pt1.1', 'ft1.1', etc
            has_output = bool(o or e)
            tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
            name = t.id().split('.')[-1]
            doc = t.shortDescription() or ""
            desc = doc and ('%s: %s' % (name, doc)) or name
            tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
    
            script = self.REPORT_TEST_OUTPUT_TMPL % dict(
                id=tid,
                output=saxutils.escape(o+e),
            )
    
            row = tmpl % dict(
                tid=tid,
                Class=(n == 0 and 'hiddenRow' or 'none'),
                style=(n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none')),
                desc=desc,
                script=script,
                status=self.STATUS[n],
            )
            rows.append(row)
            if not has_output:
                return
    
        def _generate_ending(self):
            return self.ENDING_TMPL
    
    
    ##############################################################################
    # Facilities for running tests from the command line
    ##############################################################################
    
    # Note: Reuse unittest.TestProgram to launch test. In the future we may
    # build our own launcher to support more specific command line
    # parameters like test title, CSS, etc.
    class TestProgram(unittest.TestProgram):
        """
        A variation of the unittest.TestProgram. Please refer to the base
        class for command line parameters.
        """
        def runTests(self):
            # Pick HTMLTestRunner as the default test runner.
            # base class's testRunner parameter is not useful because it means
            # we have to instantiate HTMLTestRunner before we know self.verbosity.
            if self.testRunner is None:
                self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
            unittest.TestProgram.runTests(self)
    
    main = TestProgram
    
    ##############################################################################
    # Executing this module from the command line
    ##############################################################################
    
    if __name__ == "__main__":
        main(module=None)

    测试报告:饼图+列表

    实例3:单元测试.py

    import unittest
    import HTMLTestRunner
    import HTMLTestRunnerNew
    def add(a,b):
        return a+b
    
    class TestAdd(unittest.TestCase):
        '''测试add方法的类'''
    
        def test_add_normal(self):
            '''正常测试加法的'''
            result = add(1,2)
            self.assertEqual(3,result)
    
        def test_add_error1(self):
            '''测试失败使用'''
            result = add(1,2)
            self.assertEqual(4,result)
    
        def test_add_error2(self):
            '''测试失败有message的'''
            result = add(1,2)
            self.assertEqual(4,result,'正常整数加法,没有通过')
    
    if __name__ == '__main__':
        # 不产生测试报告
        # unittest.main()
    
        # 单个运行某个测试用例
        # test_suite = unittest.TestSuite()
        # test_suite.addTest(TestAdd('test_add_error2'))
        # test_suite.addTest(TestAdd('test_error1'))
    
        # 运行某个类里面所有的测试用例
        test_suite = unittest.makeSuite(TestAdd)
        with open('report.html','wb') as fw:
            runner = HTMLTestRunnerNew.HTMLTestRunner(stream=fw,title='天马座测试报告',
                                                   description='天马座接口测试报告',
                                                   verbosity=2
                                                )
            runner.run(test_suite)

    执行结果如下:

     2、设置python文件模板(如图是Pycharm:)

    (1)设置模板文件

    (2)验证设置的Python模板是否生效: 

    进入画布页面,新建一个Python文件,自动带入设置的模板:

    实例全部代码:单元测试.py

    import unittest
    import HTMLTestRunner
    import HTMLTestRunnerNew
    import parameterized
    def add(a,b):
        return a+b
    
    class TestAdd(unittest.TestCase):
        '''测试add方法的类'''
    
        def test_add_normal(self):
            '''正常测试加法的'''
            result = add(1,2)
            self.assertEqual(3,result)
    
        def test_add_error1(self):
            '''测试失败使用'''
            result = add(1,2)
            self.assertEqual(4,result)
    
        def test_add_error2(self):
            '''测试失败有message的'''
            result = add(1,2)
            self.assertEqual(4,result,'正常整数加法,没有通过')
    
        # 参数化
        @parameterized.parameterized.expand(
           [
               [1,2,3,'参数化第一条'],
               [-1,2,1,'参数化第二条'],
               [-1,2,2,'参数化第三条'],
            ]
         )
        def test_param_add(self,a,b,c,desc):
            self._testMethodDoc = desc
            self.desc = desc
            result = add(a,b)
            self.assertEqual(c,result,'预期是%s,实际结果是%s'%(c,result))
    
    if __name__ == '__main__':
        # 不产生测试报告
        # unittest.main()
    
        # 单个运行某个测试用例
        # test_suite = unittest.TestSuite()
        # test_suite.addTest(TestAdd('test_add_error2'))
        # test_suite.addTest(TestAdd('test_error1'))
    
        # 运行某个类里面所有的测试用例
        test_suite = unittest.makeSuite(TestAdd)
    
        # 查找某个目录下的测试用例
        # test_suite = unittest.defaultTestLoader.discover('cases','test*.py')
    
        with open('report.html','wb') as fw:
            runner = HTMLTestRunnerNew.HTMLTestRunner(stream=fw,title='天马座测试报告',
                                                   description='天马座接口测试报告',
                                                   verbosity=2
                                                )
            runner.run(test_suite)
    温故而知新
  • 相关阅读:
    文件载入功能
    代码调试功能
    实用项
    连贯操作
    AR模式
    表名操作
    字段映射
    ThinkPHP中的模型二
    创建数据对象
    HDU 4888 Redraw Beautiful Drawings(最大流+判最大流网络是否唯一)
  • 原文地址:https://www.cnblogs.com/krystal-xiao/p/13843890.html
Copyright © 2011-2022 走看看