欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

box

程序员文章站 2022-05-31 09:29:32
...

import contextlib
import csv
import logging
import time
from enum import Enum, unique
from unittest import TestCase as TC, SkipTest
from unittest.case import _ShouldStop
from unittest.suite import _DebugResult, _isnotsuite

#import win32gui
#import win32con

import pymysql
import xlrd
import yaml
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver import FirefoxProfile
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.select import Select
from selenium.webdriver.support.wait import WebDriverWait

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

class BoxDriver(object):
“”"
a simple demo of selenium framework tool
“”"

"""
私有全局变量
"""
_base_driver = None
_by_char = None

"""
构造方法
"""

def __init__(self, browser_type=0, download_path="c:\\Downloads", by_char=",", profile=None):
    """
    构造方法:实例化 BoxDriver 时候使用
    :param browser_type: 浏览器类型
    :param by_char: 分隔符,默认使用","
    :param profile:
        可选择的参数,如果不传递,就是None
        如果传递一个 profile,就会按照预先的设定启动火狐
        去掉遮挡元素的提示框等
    """
    self._by_char = by_char
    if browser_type == 0 or browser_type == Browser.Chrome:

        profile = webdriver.ChromeOptions()
        # profile.add_experimental_option("excludeSwitches", ["ignore-certificate-errors"])

        # download.default_directory:设置下载路径
        # profile.default_content_settings.popups:设置为 0 禁止弹出窗口
        prefs = {'profile.default_content_settings.popups': 0,
                 'download.default_directory': download_path}
        profile.add_experimental_option('prefs', prefs)

        driver = webdriver.Chrome(chrome_options=profile)
        # driver = webdriver.Chrome(executable_path='D:\\chromedriver.exe', chrome_options=options)


    elif browser_type == 1 or browser_type == Browser.Firefox:
        # if profile is not None:
            # profile = FirefoxProfile(profile)

        profile = webdriver.FirefoxProfile()
        # 指定下载路径
        profile.set_preference('browser.download.dir', download_path)
        # 设置成 2 表示使用自定义下载路径;设置成 0 表示下载到桌面;设置成 1 表示下载到默认路径
        profile.set_preference('browser.download.folderList', 2)
        # 在开始下载时是否显示下载管理器
        profile.set_preference('browser.download.manager.showWhenStarting', False)
        # 对所给出文件类型不再弹出框进行询问
        profile.set_preference('browser.helperApps.neverAsk.saveToDisk', 'application/zip')

        driver = webdriver.Firefox(firefox_profile=profile)

    elif browser_type == Browser.Ie:
        driver = webdriver.Ie()
    else:
        driver = webdriver.PhantomJS()
    try:
        self._base_driver = driver
        self._by_char = by_char
    except Exception:
        raise NameError("Browser %s Not Found! " % browser_type)

"""
私有方法
"""

def _convert_selector_to_locator(self, selector):
    """
    转换自定义的 selector 为 Selenium 支持的 locator
    :param selector: 定位字符,字符串类型,"i, xxx"
    :return: locator
    """
    if self._by_char not in selector:
        return By.ID, selector

    selector_by = selector.split(self._by_char)[0].strip()
    selector_value = selector.split(self._by_char)[1].strip()
    if selector_by == "i" or selector_by == 'id':
        locator = (By.ID, selector_value)
    elif selector_by == "n" or selector_by == 'name':
        locator = (By.NAME, selector_value)
    elif selector_by == "c" or selector_by == 'class_name':
        locator = (By.CLASS_NAME, selector_value)
    elif selector_by == "l" or selector_by == 'link_text':
        locator = (By.LINK_TEXT, selector_value)
    elif selector_by == "p" or selector_by == 'partial_link_text':
        locator = (By.PARTIAL_LINK_TEXT, selector_value)
    elif selector_by == "t" or selector_by == 'tag_name':
        locator = (By.TAG_NAME, selector_value)
    elif selector_by == "x" or selector_by == 'xpath':
        locator = (By.XPATH, selector_value)
    elif selector_by == "s" or selector_by == 'css_selector':
        locator = (By.CSS_SELECTOR, selector_value)
    else:
        raise NameError("Please enter a valid selector of targeting elements.")

    return locator

def _locate_element(self, selector):
    """
    to locate element by selector
    :arg
    selector should be passed by an example with "i,xxx"
    "x,//*[@id='langs']/button"
    :returns
    DOM element
    """
    locator = self._convert_selector_to_locator(selector)
    if locator is not None:
        element = self._base_driver.find_element(*locator)
    else:
        raise NameError("Please enter a valid locator of targeting elements.")

    return element

def _locate_elements(self, selector):
    """
    to locate element by selector
    :arg
    selector should be passed by an example with "i,xxx"
    "x,//*[@id='langs']/button"
    :returns
    DOM element
    """
    locator = self._convert_selector_to_locator(selector)
    if locator is not None:
        elements = self._base_driver.find_elements(*locator)
    else:
        raise NameError("Please enter a valid locator of targeting elements.")

    return elements

def get_attribute(self, selector, attr_name):
    """
    获取元素属性的值
    :param selector: 元素定位
    :param attr_name: 元素属性名,如:'type'
    :return: 
    """
    return self._locate_element(selector).get_attribute(attr_name)

def get_attr_exist(self, selector, attr_name):
    """
    判断元素属性是否存在
    :param selector: 元素定位
    :param attr_name: 元素属性名,如:'type'
    :return: True: 元素属性存在
    """
    """
    c = d.get_attribute('x,//*[@id="keepLoginon"]','checked')
    # None
    print(c)
    # False
    print(bool(c)) 
   """
    # 假如元素中没有 'checked',不会报错,只会返回结果: None
    if self.get_attribute(selector, attr_name) == None:
        return False
    else:
        return True
"""
cookie 相关方法
"""
def get_cookies(self):
    """
    获取页面 cookies
    :return:
    """
    return self._base_driver.get_cookies()

def clear_cookies(self):
    """
    clear all cookies after driver init
    """
    self._base_driver.delete_all_cookies()

def add_cookies(self, cookies):
    """
    Add cookie by dict
    :param cookies:
    :return:
    """
    self._base_driver.add_cookie(cookie_dict=cookies)

def add_cookie(self, cookie_dict):
    """
    Add single cookie by dict
    添加 单个 cookie
    如果该 cookie 已经存在,就先删除后,再添加
    :param cookie_dict: 字典类型,有两个key:name 和 value
    :return:
    """
    cookie_name = cookie_dict["name"]
    cookie_value = self._base_driver.get_cookie(cookie_name)
    if cookie_value is not None:
        self._base_driver.delete_cookie(cookie_name)

    self._base_driver.add_cookie(cookie_dict)

def remove_cookie(self, name):
    """
    移除指定 name 的cookie
    :param name: 
    :return: 
    """
    # 检查 cookie 是否存在,存在就移除
    old_cookie_value = self._base_driver.get_cookie(name)
    if old_cookie_value is not None:
        self._base_driver.delete_cookie(name)

"""
浏览器本身相关方法
"""

def refresh(self, url=None):
    """
    刷新页面
    如果 url 是空值,就刷新当前页面,否则就刷新指定页面
    :param url: 默认值是空的
    :return:
    """
    if url is None:
        self._base_driver.refresh()
    else:
        self._base_driver.get(url)

def maximize_window(self):
    """
    最大化当前浏览器的窗口
    :return:
    """
    self._base_driver.maximize_window()

def navigate(self, url):
    """
    打开 URL
    :param url:
    :return:
    """
    self._base_driver.get(url)

def quit(self):
    """
    退出驱动
    :return:
    """
    self._base_driver.quit()

def close_browser(self):
    """
    关闭浏览器
    :return:
    """
    self._base_driver.close()

"""
基本元素相关方法
"""

def type(self, selector, text):
    """
    Operation input box.

    Usage:
    driver.type("i,el","selenium")
    """
    el = self._locate_element(selector)
    el.clear()
    el.send_keys(text)

def click(self, selector):
    """
    It can click any text / image can be clicked
    Connection, check box, radio buttons, and even drop-down box etc..

    Usage:
    driver.click("i,el")
    """
    el = self._locate_element(selector)
    el.click()

def click_eles(self,selector):
    """
    循环点击一组元素中的每个元素
    :param selector:
    :return:
    """
    counts = self.count_elements(selector)
    for i in range(counts):
        eles = self._locate_elements(selector)
        eles[i].click()

def click_eles_i(self,selector,i):
    """
    点击一组元素中的第几个元素
    :param selector:
    :param i: 第几个元素
    :return:
    """
    eles = self._locate_elements(selector)
    eles[i].click()

def click_by_enter(self, selector):
    """
    It can type any text / image can be located  with ENTER key

    Usage:
    driver.click_by_enter("i,el")
    """
    el = self._locate_element(selector)
    el.send_keys(Keys.ENTER)

def click_by_text(self, text):
    """
    Click the element by the link text

    Usage:
    driver.click_text("新闻")
    """
    self._locate_element('p%s' % self._by_char + text).click()

def submit(self, selector):
    """
    Submit the specified form.

    Usage:
    driver.submit("i,el")
    """
    el = self._locate_element(selector)
    el.submit()

def move_to(self, selector):
    """
    to move mouse pointer to selector
    :param selector:
    :return:
    """
    el = self._locate_element(selector)
    ActionChains(self._base_driver).move_to_element(el).perform()

def right_click(self, selector):
    """
    鼠标右击
    :param selector:
    :return:
    """
    el = self._locate_element(selector)
    ActionChains(self._base_driver).context_click(el).perform()

def double_click(self,selector):
    """
    鼠标双击
    :param selector: (想要双击的元素)元素定位
    :return: 无
    """
    ele = self._locate_element(selector)
    ActionChains(self._base_driver).double_click(ele).perform()

def count_elements(self, selector):
    """
    数一下元素的个数
    :param selector: 定位符
    :return:
    """
    els = self._locate_elements(selector)
    return len(els)

def drag_element(self, source, target):
    """
    拖拽元素
    :param source:
    :param target:
    :return:
    """

    el_source = self._locate_element(source)
    el_target = self._locate_element(target)

    if self._base_driver.w3c:
        ActionChains(self._base_driver).drag_and_drop(el_source, el_target).perform()
    else:
        ActionChains(self._base_driver).click_and_hold(el_source).perform()
        ActionChains(self._base_driver).move_to_element(el_target).perform()
        ActionChains(self._base_driver).release(el_target).perform()

"""
<select> 元素相关
"""

def select_by_index(self, selector, index):
    """
    It can click any text / image can be clicked
    Connection, check box, radio buttons, and even drop-down box etc..

    Usage:
    driver.select_by_index("i,el")
    """
    el = self._locate_element(selector)
    Select(el).select_by_index(index)

def get_selected_text(self, selector):
    """
    获取 Select 元素的选择的内容
    :param selector: 选择字符 "i, xxx"
    :return: 字符串
    """
    el = self._locate_element(selector)
    selected_opt = Select(el).first_selected_option()
    return selected_opt.text

def select_by_visible_text(self, selector, text):
    """
    It can click any text / image can be clicked
    Connection, check box, radio buttons, and even drop-down box etc..

    Usage:
    driver.select_by_index("i,el")
    """
    el = self._locate_element(selector)
    Select(el).select_by_visible_text(text)

def select_by_value(self, selector, value):
    """
    It can click any text / image can be clicked
    Connection, check box, radio buttons, and even drop-down box etc..

    Usage:
    driver.select_by_index("i,el")
    """
    el = self._locate_element(selector)
    Select(el).select_by_value(value)

"""
JavaScript 相关
"""

def execute_js(self, script):
    """
    Execute JavaScript scripts.

    Usage:
    driver.js("window.scrollTo(200,1000);")
    """
    self._base_driver.execute_script(script)

"""
元素属性相关方法
"""

def get_value(self, selector):
    """
    返回元素的 value
    :param selector: 定位字符串
    :return:
    """
    el = self._locate_element(selector)
    return el.get_attribute("value")

def get_attribute(self, selector, attribute):
    """
    Gets the value of an element attribute.

    Usage:
    driver.get_attribute("i,el","type")
    """
    el = self._locate_element(selector)
    return el.get_attribute(attribute)

def get_text(self, selector):
    """
    Get element text information.

    Usage:
    driver.get_text("i,el")
    """
    el = self._locate_element(selector)
    return el.text

def get_displayed(self, selector):
    """
    Gets the element to display,The return result is true or false.

    Usage:
    driver.get_display("i,el")
    """
    el = self._locate_element(selector)
    return el.is_displayed()

def get_exist(self, selector):
    """
    该方法用来确认元素是否存在,如果存在返回flag=true,否则返回false
    :param self:
    :param selector: 元素定位,如'id,account'
    :return: 布尔值
    """
    flag = True
    try:
        self._locate_element(selector)
        return flag
    except:
        flag = False
        return flag

def get_enabled(self,selector):
    """
    判断页面元素是否可点击
    :param selector: 元素定位
    :return: 布尔值
    """
    if self._locate_element(selector).is_enabled():
        return True
    else:
        return False

def get_title(self):
    """
    Get window title.

    Usage:
    driver.get_title()
    """
    return self._base_driver.title

def get_url(self):
    """
    Get the URL address of the current page.

    Usage:
    driver.get_url()
    """
    return self._base_driver.current_url

def get_selected(self, selector):
    """
    to return the selected status of an WebElement
    :param selector: selector to locate
    :return: True False
    """
    el = self._locate_element(selector)
    return el.is_selected()

def get_text_list(self, selector):
    """
    根据selector 获取多个元素,取得元素的text 列表
    :param selector:
    :return: list
    """

    el_list = self._locate_elements(selector)

    results = []
    for el in el_list:
        results.append(el.text)

    return results

"""
弹出窗口相关方法
* 如果弹框的元素可以F12元素查看,则直接使用点击,获取元素等方法
* 如果弹框元素无法查看,则使用如下方法可以搞定
"""
def accept_alert(self):
    """
        Accept warning box.

        Usage:
        driver.accept_alert()
        """
    self._base_driver.switch_to.alert.accept()

def dismiss_alert(self):
    """
    Dismisses the alert available.

    Usage:
    driver.dismissAlert()
    """
    self._base_driver.switch_to.alert.dismiss()

def get_alert_text(self):
    """
    获取 alert 弹出框的文本信息
    :return: String
    """
    return self._base_driver.switch_to.alert.text

def type_in_alert(self,text):
    """在prompt对话框内输入内容"""
    self._base_driver.switch_to.alert.send_keys(text)
    self.forced_wait(1)

"""
进入框架、退出框架
"""
def switch_to_frame(self, selector):
    """
    Switch to the specified frame.

    Usage:
    driver.switch_to_frame("i,el")
    """
    el = self._locate_element(selector)
    self._base_driver.switch_to.frame(el)

def switch_to_default(self):
    """
    Returns the current form machine form at the next higher level.
    Corresponding relationship with switch_to_frame () method.

    Usage:
    driver.switch_to_frame_out()
    """
    self._base_driver.switch_to.default_content()

"""
切换不同页面窗口
"""
def switch_to_window_by_title(self, title):
    for handle in self._base_driver.window_handles:
        self._base_driver.switch_to.window(handle)
        if self._base_driver.title == title:
            break

        self._base_driver.switch_to.default_content()

def open_new_window(self, selector):
    """
    Open the new window and switch the handle to the newly opened window.

    Usage:
    driver.open_new_window()
    """
    original_windows = self._base_driver.current_window_handle
    el = self._locate_element(selector)
    el.click()
    all_handles = self._base_driver.window_handles
    for handle in all_handles:
        if handle != original_windows:
            self._base_driver._switch_to.window(handle)

def save_window_snapshot(self, file_name):
    """
    save screen snapshot
    :param file_name: the image file name and path
    :return:
    """
    driver = self._base_driver
    driver.save_screenshot(file_name)

def save_window_snapshot_by_io(self):
    """
    保存截图为文件流
    :return:
    """
    return self._base_driver.get_screenshot_as_base64()

def save_element_snapshot_by_io(self, selector):
    """
    控件截图
    :param selector:
    :return:
    """
    el = self._locate_element(selector)
    return el.screenshot_as_base64

"""
等待方法
"""

def forced_wait(self, seconds):
    """
    强制等待
    :param seconds:
    :return:
    """
    time.sleep(seconds)

def implicitly_wait(self, seconds):
    """
    Implicitly wait. All elements on the page.
    :param seconds 等待时间 秒
    隐式等待

    Usage:
    driver.implicitly_wait(10)
    """
    self._base_driver.implicitly_wait(seconds)

def explicitly_wait(self, selector, seconds):
    """
    显式等待
    :param selector: 定位字符
    :param seconds: 最长等待时间,秒
    :return:
    """
    locator = self._convert_selector_to_locator(selector)

    WebDriverWait(self._base_driver, seconds).until(expected_conditions.presence_of_element_located(locator))

"""上传"""
def upload_input(self,selector,file):
    """
    上传文件 ( 标签为 input 类型,此类型最常见,最简单)
    :param selector: 上传按钮定位
    :param file: 将要上传的文件(绝对路径)
    :return: 无
    """
    self._locate_element(selector).send_keys(file)

# def upload_not_input(self,file,browser_type='Chrome'):
#     """
#     上传文件 ( 标签不是 input 类型,使用 win32gui,得先安装 pywin32 依赖包)
#                                             pip install pywin32
#     :param browser_type: 浏览器类型(Chrome浏览器和Firefox浏览器的有区别)
#     :param file: 将要上传的文件(绝对路径)
#     单个文件:file1 = 'C:\\Users\\list_tuple_dict_test.py'
#     同时上传多个文件:file2 = '"C:\\Users\\list_tuple_dict_test.py" "C:\\Users\\class_def.py"'
#     :return: 无
#     """
#     # Chrome 浏览器是'打开'
#     # 对话框
#     # 下载个 Spy++ 工具,定位“打开”窗口,定位到窗口的类(L):#32770, '打开'为窗口标题
#     if browser_type == 'Chrome':
#         dialog = win32gui.FindWindow('#32770', u'打开')
#     elif browser_type == 'Firefox':
#         # Firefox 浏览器是'文件上传'
#         # 对话框
#         dialog = win32gui.FindWindow('#32770', u'文件上传')
#     ComboBoxEx32 = win32gui.FindWindowEx(dialog, 0, 'ComboBoxEx32', None)
#     ComboBox = win32gui.FindWindowEx(ComboBoxEx32, 0, 'ComboBox', None)
#     # 上面三句依次寻找对象,直到找到输入框Edit对象的句柄
#     Edit = win32gui.FindWindowEx(ComboBox, 0, 'Edit', None)
#     # 确定按钮Button
#     button = win32gui.FindWindowEx(dialog, 0, 'Button', None)
#     # 往输入框输入绝对地址
#     win32gui.SendMessage(Edit, win32con.WM_SETTEXT, None, file)
#     # 按button
#     win32gui.SendMessage(dialog, win32con.WM_COMMAND, 1, button)
#     # 获取属性
#     # print(upload.get_attribute('value'))

"""
表单数据提交:
    页面校验
    数据库校验
    某条记录选择,编辑,删除
"""
def del_edit_choose_the_row(self, selector_of_next_page, selector_of_trs_td, selector_of_del_edit_choose,expected_td_value):
    """
    页面表单,选中/编辑/删除 指定内容的行(带多页翻页功能)
    :param selector_of_next_page: ‘下一页’定位,如:'l,下页'
    :param selector_of_trs_td: 所有行的某一列的定位,如 rzhi 成员列表中,获取所有行的“真实姓名”那列:'x,/html/body/div/div/div/div[2]/div/div/table/tbody//tr/td[2]'
    :param selector_of_del_edit_choose: 指定要操作(删除/编辑/选择)的列,如 rzhi 成员列表中,获取期望删除的列:'x,/html/body/div/div/div/div[2]/div/div/table/tbody/tr[%d]/td[11]/a[3]'
    :param expected_td_value: 期望的列内容,如rzhi 成员列表中期望的“真实姓名”: '华仔'
    :return:无
    """

    td_values = self.get_text_list(selector_of_trs_td)
    for i in range(len(td_values)):
        if td_values[i] == expected_td_value:
            print('%s在第%d行显示(首页)!' % (td_values[i], i + 1))
            self.forced_wait(2)
            self.click(selector_of_del_edit_choose % (i + 1))
            break
    try:
        while (self.get_enabled(selector_of_next_page)):
            self.click(selector_of_next_page)
            self.forced_wait(2)
            td_values = self.get_text_list(selector_of_trs_td)
            for i in range(len(td_values)):
                if td_values[i] == expected_td_value:
                    print('%s在第%d行显示(非首页)' % (td_values[i], i + 1))
                    self.forced_wait(3)
                    self.click(selector_of_del_edit_choose % (i + 1))
            continue
    except Exception as e:
        print('%s 操作成功!' % expected_td_value)

def assert_new_record_exist_in_table(self, selector_of_next_page, selector_of_trs_td, expected_td_value):
    """
    此方法针对页面列表(带多页翻页功能),都可以判断新增记录是否添加成功!
    若新增成功,则返回 True 布尔值;否则返回 False 布尔值
    :param selector_of_next_page: "下一页"定位,如:'l,下页'
    :param selector_of_trs_td:所有行的某一列的定位,如: 'x,/html/body/div/div/div/div[2]/div/div/table/tbody//tr/td[2]'
    :param expected_td_value:期望的列内容,如:'华仔'
    :return: 布尔值
    """
    b = 1
    a = False
    real_records = self.get_text_list(selector_of_trs_td)
    for real_record in real_records:
        if real_record == expected_td_value:
            a = True
            b += 1
    # 如果第1页就找到了,就不用再进入“下一页”继续查找了
    if b == 1:
        try:
            while (self.get_enabled(selector_of_next_page)):
                self.click(selector_of_next_page)
                self.forced_wait(2)
                next_page_real_records = self.get_text_list(selector_of_trs_td)
                for next_page_real_record in next_page_real_records:
                    if next_page_real_record == expected_td_value:
                        a = True
                        break
        except :
            pass
    return a

def assert_new_record_exist_mysql(self, db_yaml_path, db_yaml_name,sql_file_path, select_field_num,expected_td_value):
    """
    数据库校验,True为数据库中存在该数据
    :param db_yaml_path: 数据库的yaml格式的配置文件路径
    :param db_yaml_name: 数据库的yaml格式的配置文件中设置的数据库名(默认是在'DbConfig'下面)
    :param sql_file_path: sql文件路径
    :param select_field_num: 查询语句中第几个字段(默认0表示第1个字段)
    :param expected_td_value: 期望要断言的值
    :return: True / False
    """
    ydata = YamlHelper().get_config_dict(db_yaml_path)
    host = ydata['DbConfig'][db_yaml_name]['host']
    port = ydata['DbConfig'][db_yaml_name]['port']
    user = ydata['DbConfig'][db_yaml_name]['user']
    # pwd 这里,主要要加上 str 类型转换,否则如果密码如123456,会报错"API..."
    pwd = str(ydata['DbConfig'][db_yaml_name]['pwd'])
    db = ydata['DbConfig'][db_yaml_name]['db']

    db_helper = DbHelper(host, port, user, pwd, db)
    sql = db_helper.read_sql(sql_file_path)
    result = db_helper.execute(sql)['data']
    db_helper.close()
    a = False
    # print(result)
    for i in result:
        if i[select_field_num] == expected_td_value:
            a = True
    return a

class BasePage(object):
“”"
测试系统的最基础的页面类,是所有其他页面的基类
“”"
# 变量
base_driver = None

# 方法
def __init__(self, driver: BoxDriver, logger=None):
    """
    构造方法
    :param driver: 指定了参数类型,BoxDriver
    """
    self.base_driver = driver

    self.logger = logger

def open(self, url):
    """
    打开页面
    :param url:
    :return:
    """
    self.base_driver.navigate(url)
    self.base_driver.maximize_window()
    self.base_driver.forced_wait(2)

def log(self, msg):
    """
    记录日志
    :param msg:
    :return:
    """
    if self.logger is not None:
        self.logger.info(msg)

class CsvHelper(object):

def read_data(self, f, encoding="utf-8-sig"):
    """
    读csv文件作为普通list
    :param f:csv文件名
    :return:列表方式的csv文件内容
    """
    data_ret = []
    with open(f, encoding=encoding, mode='r') as csv_file:
        csv_data = csv.reader(csv_file)
        for row in csv_data:
            data_ret.append(row)

    return data_ret

def read_data_as_dict(self, f, encoding="utf-8-sig"):
    """
    读csv文件作为普通list
    :param f:
    :return:
    """
    data_ret = []
    with open(f, encoding=encoding, mode='r') as csv_file:
        csv_dict = csv.DictReader(csv_file)
        for row in csv_dict:
            data_ret.append(row)

    return data_ret

class ExcelHelper(object):
“”"
读取Excel文件
“”"

def read_by_list(self, excel_file, sheet_index):
    """
    列表形式读取 Excel文件内容
    :param excel_file: excel文件名(如:工作簿1.xls)
    :param sheet_index: 第几个sheet(如1,表示第1个sheet)
    :return:列表形式的数据
    """
    data = xlrd.open_workbook(excel_file)
    # 获取某张表单
    sheet = data.sheet_by_index(sheet_index - 1)

    sheet_data = []
    for i in range(sheet.nrows):
        sheet_data.append(sheet.row_values(i))
    return sheet_data

def read_by_dict(self, excel_file, sheet_index):
    """
    字典形式读取 Excel文件内容
    :param excel_file: excel文件名(如:工作簿1.xls)
    :param sheet_index: 第几个sheet(如1,表示第1个sheet)
    :return: 字典形式的数据
    """
    st_data = self.read_by_list(excel_file, sheet_index)
    list_row1 = st_data[0]
    dict_data = []
    a = True
    for i in st_data:
        if a:
            a = False
            continue
        # 将2个列表list_row1和i合并为字典
        list_dict = dict(zip(list_row1, i))
        dict_data.append(list_dict)
    return dict_data

class DbHelper(object):
“”"
MySQ 数据库帮助类
“”"

# 使用方法
# 1. 实例化对象
# 2. 查询,得到结果
# 3. 关闭对象
"""
db_helper = MysqlDbHelper("localhost", 3306, 'root', '', 'tpshop2.0.5', "utf8")
for i in range(10000):

    result = db_helper.execute("select * from tp_goods order by 1 desc limit 1000;")
    print("第%d次,结果是%r" % (i, result))

db_helper.close()
"""

connect = None

def __init__(self, host, port, user, password, database, charset='utf8'):
    """
    构造方法
    :param host: 数据库的主机地址
    :param port: 数据库的端口号
    :param user: 用户名
    :param password: 密码
    :param database: 选择的数据库
    :param charset: 字符集
    """
    self.connect = pymysql.connect(host=host, port=port,
                                   user=user, password=password,
                                   db=database, charset=charset)

def read_sql(self, file, encoding="utf8"):
    """
    从 文件中读取 SQL 脚本
    :param file: 文件名 + 文件路径
    :return:
    """
    sql_file = open(file, "r", encoding=encoding)
    sql = sql_file.read()
    sql_file.close()
    return sql

def execute(self, sql):
    """
    执行 SQL 脚本查询并返回结果
    :param sql: 需要查询的 SQL 语句
    :return: 字典类型
        data 是数据,本身也是个字典类型
        count 是行数
    """
    cursor = self.connect.cursor()

    row_count = cursor.execute(sql)
    rows_data = cursor.fetchall()
    result = {
        "count": row_count,
        "data": rows_data
    }

    cursor.close()
    return result

def close(self):
    """
    关闭数据库连接
    :return:
    """
    self.connect.close()

class YamlHelper(object):

def get_config_dict(self, f):
    """
    获取所有配置
    :param f:
    :return:
    """
    with open(f, mode='r', encoding='utf8') as file_config:
        config_dict = yaml.load(file_config.read())
        return config_dict

class Email(object):
“”"
更改email_attachment方法中的如下3项即可:
1、 sender:发件人的邮箱,为163邮箱;
2、 pwd:发送邮箱的密码
注意:此密码不是登录密码,而是网络授权码
3、 receiver: 收件人的邮箱
“”"

def email_attachment(self,report_file):
    """配置发送附件测试报告到邮箱"""
    """发件相关参数"""
    try:
        # 发件服务器
        smtpserver = 'smtp.163.com'
        port = 25

        #  发送邮件,只需要更改这3处即可
        sender = '[email protected]'
        # 生成授权码,不是登陆密码
        psw = 'gk4636'
        receiver = '[email protected]'

        msg = MIMEMultipart()
        msg['from'] = sender
        msg['to'] = receiver
		#msg['to'] = ';'.join(receiver)
        msg['subject'] = '这个是baidu项目自动化测试报告'
        """读取测试报告内容"""
        with open(report_file, 'rb') as rp:
            baidu_mail_body = rp.read()
        """正文"""
        body = MIMEText(baidu_mail_body, 'html', 'utf8')
        msg.attach(body)
        """附件"""
        att = MIMEText(baidu_mail_body, 'base64', 'utf8')
        att['Content-Type'] = 'application/octet-stream'
        att['Content-Disposition'] = 'attachment;filename = "%s"' % report_file
        msg.attach(att)
        """发送邮件"""
        smtp = smtplib.SMTP()
        smtp.connect(smtpserver, port)
        smtp.login(sender, psw)
        smtp.sendmail(sender, receiver.split(';'), msg.as_string())  # 发送
        smtp.close()
        print("邮件发送成功!")
    except Exception as e:
        print(e)
        print("邮件发送失败!")

@unique
class Browser(Enum):
“”"
定义支持的浏览器,支持Chrome,Firefox,Ie
“”"
Chrome = 0
Firefox = 1
Ie = 2

class TestLogger:
def init(self, log_path):
# log_path:日志存放路径
# 文件命名

    self.file_name = log_path
    self.logger = logging.getLogger()
    self.logger.setLevel(logging.DEBUG)
    # 日志输出格式
    self.formatter = logging.Formatter('[%(asctime)s]-[%(filename)s]-[%(levelname)s]: %(message)s')

def info(self, message):
    """
    添加信息日志
    :param message:
    :return:
    """
    self._console("info", message)

def warning(self, message):
    """
    添加警告日志
    :param message:
    :return:
    """
    self._console("warning", message)

def error(self, message):
    """
    添加错误日志
    :param message:
    :return:
    """
    self._console("error", message)

def _console(self, level, message):
    # 创建一个FileHandler,用于写到本地
    fh = logging.FileHandler(self.file_name, 'a', encoding='utf8')
    fh.setLevel(logging.DEBUG)
    fh.setFormatter(self.formatter)
    self.logger.addHandler(fh)

    # 创建一个SteamHandler,用于输出到控制台
    ch = logging.StreamHandler(sys.stdout)
    ch.setLevel(logging.DEBUG)
    ch.setFormatter(self.formatter)

    self.logger.addHandler(ch)
    if level == 'info':
        self.logger.info(message)
    elif level == 'debug':
        self.logger.debug(message)
    elif level == 'warning':
        self.logger.warning(message)
    elif level == 'error':
        self.logger.error(message)

    # 这两行代码为了避免日志输出重复
    self.logger.removeHandler(ch)
    self.logger.removeHandler(fh)
    fh.close()

#TODO:参数化
“”"
参数化 :
paramunittest
ddt
“”"

class TestCase(TC):
“”"
测试用例类

"""
images = None
base_driver = None
logger = None

def __init__(self, methodName='runTest', logger_file=None):
    """
    重写构造方法
    """
    super().__init__(methodName)
    if self.images is None:
        self.images = []
    if logger_file is not None:
        self.logger = TestLogger(logger_file)
    else:
        test_time = time.strftime("%Y%m%d_%H%M%S", time.localtime())
        self.logger = TestLogger("test_log_%s.log" % test_time)

def set_up(self):
    """
    ddd
    :return:
    """
    pass

def tear_down(self):
    """
    dddd
    :return:
    """
    pass

def run(self, result=None):
    orig_result = result
    if result is None:
        result = self.defaultTestResult()
        startTestRun = getattr(result, 'startTestRun', None)
        if startTestRun is not None:
            startTestRun()

    result.startTest(self)

    testMethod = getattr(self, self._testMethodName)
    if (getattr(self.__class__, "__unittest_skip__", False) or
            getattr(testMethod, "__unittest_skip__", False)):
        # If the class or method was skipped.
        try:
            skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')
                        or getattr(testMethod, '__unittest_skip_why__', ''))
            self._addSkip(result, self, skip_why)
        finally:
            result.stopTest(self)
        return
    expecting_failure_method = getattr(testMethod,
                                       "__unittest_expecting_failure__", False)
    expecting_failure_class = getattr(self,
                                      "__unittest_expecting_failure__", False)
    expecting_failure = expecting_failure_class or expecting_failure_method
    outcome = _Outcome(result)
    try:

        self._outcome = outcome

        with outcome.testPartExecutor(self):
            self.set_up()
        if outcome.success:
            outcome.expecting_failure = expecting_failure
            with outcome.testPartExecutor(self, isTest=True):
                testMethod()
            outcome.expecting_failure = False

            # 尝试异常继续运行
            with outcome.testPartExecutor(self):
                self.tear_down()

        for test, reason in outcome.skipped:
            self._addSkip(result, test, reason)
        self._feedErrorsToResult(result, outcome.errors)
        if outcome.success:
            if expecting_failure:
                if outcome.expectedFailure:
                    self.snapshot()
                    self._addExpectedFailure(result, outcome.expectedFailure)
                else:
                    self._addUnexpectedSuccess(result)
            else:
                result.addSuccess(self)

        self.doCleanups()
        return result
    finally:
        result.stopTest(self)
        if orig_result is None:
            stopTestRun = getattr(result, 'stopTestRun', None)
            if stopTestRun is not None:
                stopTestRun()

        # explicitly break reference cycles:
        # outcome.errors -> frame -> outcome -> outcome.errors
        # outcome.expectedFailure -> frame -> outcome -> outcome.expectedFailure
        outcome.errors.clear()
        outcome.expectedFailure = None

        # clear the outcome, no more needed
        self._outcome = None

def snapshot(self):
    """
    ddd
    :return:
    """
    self.images.append(self.base_driver.save_window_snapshot_by_io())

def log(self, msg):
    """
    添加日志
    :param msg:
    :return:
    """
    if self.logger is not None:
        self.logger.info(msg)

def read_csv_as_dict(self, file_name):
    """
    读 CSV 作为 DICT 类型
    :type file_name: csv 文件路径 和名字
    :return:
    """
    return CsvHelper().read_data_as_dict(file_name)

def shortDescription(self):
    """Returns a one-line description of the test, or None if no
    description has been provided.

    The default implementation of this method returns the first line of
    the specified test method's docstring.
    """
    doc = self._testMethodDoc
    # return doc and doc.split("\n")[1].strip() or None
    # 用[1]报错,数组超出界限,报告是空的,改为[0]就可以了
    return doc and doc.split("\n")[0].strip() or None

class _Outcome(object):
def init(self, result=None):
self.expecting_failure = False
self.result = result
self.result_supports_subtests = hasattr(result, “addSubTest”)
self.success = True
self.skipped = []
self.expectedFailure = None
self.errors = []

@contextlib.contextmanager
def testPartExecutor(self, test_case, isTest=False):
    old_success = self.success
    self.success = True
    try:
        yield
    except KeyboardInterrupt:
        raise
    except SkipTest as e:
        self.success = False
        self.skipped.append((test_case, str(e)))
    except _ShouldStop:
        pass
    except:
        exc_info = sys.exc_info()
        if self.expecting_failure:
            self.expectedFailure = exc_info
        else:
            self.success = False
            # test_case.images.append(test_case.base_driver.save_window_snapshot_by_io())

            self.errors.append((test_case, exc_info))
        # explicitly break a reference cycle:
        # exc_info -> frame -> exc_info
        exc_info = None
    else:
        if self.result_supports_subtests and self.success:
            self.errors.append((test_case, None))
    finally:
        self.success = self.success and old_success

“”"
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 © 2004-2007, Wai Yip Tung
Copyright © 2016, Eason Han
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.
“”"

author = “gxx”
version = “1.1.0”

import datetime
import io
import sys
import unittest
from xml.sax import saxutils

def to_unicode(s):
try:
return s
except UnicodeDecodeError:
# s is non ascii byte string
return s.decode(‘unicode_escape’)

class OutputRedirect(object):
“”" Wrapper to redirect stdout or stderr “”"

def __init__(self, fp):
    self.fp = fp

def write(self, s):
    self.fp.write(to_unicode(s))

def writelines(self, lines):
    lines = map(to_unicode, lines)
    self.fp.writelines(lines)

def flush(self):
    self.fp.flush()

stdout_redirect = OutputRedirect(sys.stdout)
stderr_redirect = OutputRedirect(sys.stderr)

#Template

class _TemplateReport(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"""<!DOCTYPE html>
%(title)s %(stylesheet)s
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
  <script src="http://cdn.bootcss.com/html5shiv/3.7.2/html5shiv.min.js"></script>
  <script src="http://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->

/* 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,’&’);
s = s.replace(/</g,’<’);
s = s.replace(/>/g,’>’);
return s;
}

function show_img(obj) {
var obj1 = obj.nextElementSibling
obj1.style.display=‘block’
var index = 0;//每张图片的下标,
var len = obj1.getElementsByTagName(‘img’).length;
var imgyuan = obj1.getElementsByClassName(‘imgyuan’)[0]
//var start=setInterval(autoPlay,500);
obj1.οnmοuseοver=function(){//当鼠标光标停在图片上,则停止轮播
clearInterval(start);
}
obj1.οnmοuseοut=function(){//当鼠标光标停在图片上,则开始轮播
start=setInterval(autoPlay,1000);
}
for (var i = 0; i < len; i++) {
var font = document.createElement(‘font’)
imgyuan.appendChild(font)
}
var lis = obj1.getElementsByTagName(‘font’);//得到所有圆圈
changeImg(0)
var funny = function (i) {
lis[i].onmouseover = function () {
index=i
changeImg(i)
}
}
for (var i = 0; i < lis.length; i++) {
funny(i);
}

function autoPlay(){
    if(index>len-1){
        index=0;
        clearInterval(start); //运行一轮后停止
    }
    changeImg(index++);
}
imgyuan.style.width= 25*len +"px";
//对应圆圈和图片同步
function changeImg(index) {
    var list = obj1.getElementsByTagName('img');
    var list1 = obj1.getElementsByTagName('font');
    for (i = 0; i < list.length; i++) {
        list[i].style.display = 'none';
        list1[i].style.backgroundColor = 'white';
    }
    list[index].style.display = 'block';
    list1[index].style.backgroundColor = 'blue';
}

}
function hide_img(obj){
obj.parentElement.style.display = “none”;
obj.parentElement.getElementsByClassName(‘imgyuan’)[0].innerHTML = “”;
}

/* obsoleted by detail in


function showOutput(id, name) {
var w = window.open("", //url
name,
“resizable,scrollbars,status,width=800,height=450”);
d = w.document;
d.write("
");
d.write(html_escape(output_list[id]));
d.write("\n");
d.write(“close\n”);
d.write("
\n");
d.close();
}
*/
–>

%(heading)s %(report)s %(ending)s
""" # 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 = """

“”"

# ------------------------------------------------------------------------
# Heading
#

HEADING_TMPL = """<div class='heading'>

%(title)s

%(parameters)s

%(description)s

“”" # variables: (title, parameters, description)

HEADING_ATTRIBUTE_TMPL = """<p><strong>%(name)s:</strong> %(value)s</p>

“”" # variables: (name, value)

# ------------------------------------------------------------------------
# Report
#

REPORT_TMPL = """

Summary Failed All

%(test_list)s
Test Suite/Test Case 测试套件/用例 Count 个数 Pass 通过 Fail 不通过 Error 测试程序异常 View 查看
Total 总共 %(count)s %(Pass)s %(fail)s %(error)s  
""" # variables: (test_list, count, Pass, fail, error)
REPORT_CLASS_TMPL = r"""
%(desc)s %(count)s %(Pass)s %(fail)s %(error)s
%(desc)s
    <!--css div popup start-->
    <a class="popup_link btn btn-xs btn-default" 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;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>
<td colspan='1' align='center'>%(img)s</td>
""" # variables: (tid, Class, style, desc, status)
REPORT_TEST_NO_OUTPUT_TMPL = r"""
%(desc)s
%(status)s """ # variables: (tid, Class, style, desc, status)
REPORT_TEST_OUTPUT_TMPL = r"""

%(id)s: %(output)s
“”" # variables: (id, output)

# ------------------------------------------------------------------------
# ENDING
#

IMG_TMPL = r"""
        <a href="#"  onclick="show_img(this)">Snapshot 截图</a>
    <div align="center" class="screenshots"  style="display:none">
        <a class="close_shots"  href="#"   onclick="hide_img(this)"></a>
        %(images)s
        <div class="imgyuan"></div>
    </div>
    """

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):
    super().__init__(verbosity=verbosity)
    self.outputBuffer = io.StringIO()
    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
    stdout_redirect.fp = self.outputBuffer
    stderr_redirect.fp = self.outputBuffer
    self.stdout0 = sys.stdout
    self.stderr0 = sys.stderr
    sys.stdout = stdout_redirect
    sys.stderr = stderr_redirect

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)
    if getattr(test, 'logger', TestLogger):
        test.logger.info("测试用例执行成功")
    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('\n')
    else:
        sys.stderr.write('.')

def addError(self, test, err):
    self.error_count += 1
    TestResult.addError(self, test, err)
    if getattr(test, 'logger', TestLogger):
        test.logger.error("测试用例执行异常:%s" % str(err))

    _, _exc_str = self.errors[-1]
    output = self.complete_output()
    self.result.append((2, test, output, _exc_str))

    # if not getattr(test, "base_driver", ""):
    #     pass
    # else:
    #     try:
    #         driver = getattr(test, "base_driver")
    #         test.images.append(driver.save_window_snapshot_by_io())
    #     except Exception as e:
    #         pass

    if self.verbosity > 1:
        sys.stderr.write('E  ')
        sys.stderr.write(str(test))
        sys.stderr.write('\n')
    else:
        sys.stderr.write('E')

def addFailure(self, test, err):
    self.failure_count += 1
    TestResult.addFailure(self, test, err)
    if getattr(test, 'logger', TestLogger):
        test.logger.error("测试用例执行失败:%s" % str(err))
    _, _exc_str = self.failures[-1]
    output = self.complete_output()
    self.result.append((1, test, output, _exc_str))

    # if not getattr(test, "base_driver", ""):
    #     pass
    # else:
    #     try:
    #         driver = getattr(test, "base_driver")
    #         test.images.append(driver.save_window_snapshot_by_io())
    #     except Exception as e:
    #         pass

    if self.verbosity > 1:
        sys.stderr.write('F  ')
        sys.stderr.write(str(test))
        sys.stderr.write('\n')
    else:
        sys.stderr.write('F')

class TestRunner(_TemplateReport):
“”"
HtmlTestRunner
“”"
file_name = None

def __init__(self, file_name, verbosity=1, title=None, description=None):
    """
    initialize
    :param stream:
    :param verbosity:
    :param title:
    :param description:
    """

    self.file_name = file_name
    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.
    :param test:
    :return:
    """
    with open(self.file_name, mode="wb") as stream:
        self.stream = stream
        result = _TestResult(self.verbosity)
        test(result)
        self.stopTime = datetime.datetime.now()
        self.generate_report(test, result)
        print('Time Elapsed 花费时间: %s' % (self.stopTime - self.startTime))

    return result

def sort_result(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 get_report_attributes(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(
        '<span class="text text-success">Pass <strong>%s</strong></span>' % result.success_count)
    if result.failure_count: status.append(
        '<span class="text text-danger">Failure <strong>%s</strong></span>' % result.failure_count)
    if result.error_count:   status.append(
        '<span class="text text-warning">Error <strong>%s</strong></span>' % result.error_count)
    if status:
        status = ' '.join(status)
    else:
        status = 'none'
    return [
        ('Start Time 开始时间', startTime),
        ('Duration 用时', duration),
        ('Status 状态', status),
    ]

def generate_report(self, test, result):
    report_attrs = self.get_report_attributes(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())

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),
            name=name,
            value=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.sort_result(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("\n")[0] or ""
        desc = doc and '%s: %s' % (name, doc) or name


        row = self.REPORT_CLASS_TMPL % dict(
            style=ne > 0 and 'text text-warning' or nf > 0 and 'text text-danger' or 'text text-success',
            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 \n and mess up formating
        # uo = unicode(o.encode('string_escape'))
        uo = o
    else:
        uo = o
    if isinstance(e, str):
        # TODO: some problem with 'string_escape': it escape \n and mess up formating
        # ue = unicode(e.encode('string_escape'))
        ue = e
    else:
        ue = e

    script = self.REPORT_TEST_OUTPUT_TMPL % dict(
        id=tid,
        output=saxutils.escape(uo + ue),
    )

    # 处理截图
    if getattr(t, 'images', []):
        # 判断截图列表,如果有则追加
        tmp = u""
        for i, img in enumerate(t.images):
            if i == 0:
                tmp += """ <img src="data:image/jpg;base64,%s" style="display: block;" class="img"/>\n""" % img
            else:
                tmp += """ <img src="data:image/jpg;base64,%s" style="display: none;" class="img"/>\n""" % img
        images = self.IMG_TMPL % dict(images=tmp)
    else:
        images = u"""无截图"""

    row = tmpl % dict(
        tid=tid,
        # Class = (n == 0 and 'hiddenRow' or 'none'),
        Class=(n == 0 and 'hiddenRow' or 'text text-success'),
        # style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
        style=n == 2 and 'text text-warning' or (n == 1 and 'text text-danger' or 'text text-success'),
        desc=desc,
        script=script,
        status=self.STATUS[n],
        img=images,
    )
    rows.append(row)
    if not has_output:
        return

def _generate_ending(self):
    return self.ENDING_TMPL

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 = TestRunner(verbosity=self.verbosity)
    unittest.TestProgram.runTests(self)

class TestSuite(unittest.TestSuite):
“”"A test suite is a composite test consisting of a number of TestCases.

For use, create an instance of TestSuite, then add test case instances.
When all tests have been added, the suite can be passed to a test
runner, such as TextTestRunner. It will run the individual test cases
in the order in which they were added, aggregating the results. When
subclassing, do not forget to call the base class constructor.
"""

def run(self, result, debug=False):
    topLevel = False
    if getattr(result, '_testRunEntered', False) is False:
        result._testRunEntered = topLevel = True

    for index, test in enumerate(self):
        if result.shouldStop:
            break

        if _isnotsuite(test):
            self._tearDownPreviousClass(test, result)
            self._handleModuleFixture(test, result)
            self._handleClassSetUp(test, result)
            result._previousTestClass = test.__class__

            if (getattr(test.__class__, '_classSetupFailed', False) or
                    getattr(result, '_moduleSetUpFailed', False)):
                continue

        if not debug:
            test(result)
        else:
            test.debug()

        if self._cleanup:
            self._removeTestAtIndex(index)

    if topLevel:
        self._tearDownPreviousClass(None, result)
        self._handleModuleTearDown(result)
        result._testRunEntered = False
    return result

def debug(self):
    """Run the tests without collecting errors in a TestResult"""
    debug = _DebugResult()
    self.run(debug, True)

def add_test(self, test):
    """
    添加单个测试
    :param test: 测试用例的类实例化的对象
    :return:
    """
    self.addTest(test)

def add_tests(self, tests):
    """
    添加多个测试
    :param tests:
    :return:
    """
    self.addTests(tests)

main = TestProgram

################################################################
#Executing this module from the command line
#从命令行执行此模块
################################################################

if name == “main”:
main(module=None)