youqu/docs/框架功能介绍.md

92 KiB
Raw Blame History

框架功能介绍

# =============================================
# Attribution : Chengdu Test Department
# Time        : 2023/1/12
# Author      : Mikigo
# =============================================

一句话简介

此文档主要想对 YouQu 框架一些重点的功能做详细的介绍,包括对一些功能开发的背景、要解决的问题、实现逻辑、代码等方面的介绍,以此让大家能更加清晰的理解各项功能,从而在使用的时候能更加的得心应手。

执行管理器

YouQu 的执行管理器 manage.py 提供了丰富的配置和命令行参数,可用于本地用例驱动执行、远程用例驱动执行、CSV 文件管理、PMS 与本地 CSV 文件标签关联管理、脚手架等功能;

1. 如何使用

【命令行使用】

所有功能的驱动执行都是通过 manage.py 进行的,它是全局的入口文件,后面提到的一些命令行参数也都默认是在 manage.py 之后添加使用;

你可以使用 -h--help 查看它的帮助:

youqu manage -h

这样可以查看它支持的子命令;

然后再通过子命令 -h--help 查看子命令的帮助,以子命令 run 举例:

youqu manage run -h

这样可以查看到子命令支持的各项参数及参数使用说明。

【配置文件】

配置文件在 setting 目录下,绝大部分的配置项均在 globalconfig.ini 文件中,为了方便描述后面经常提到的“配置文件”、“配置项”几乎都是指的 setting/globalconfig.ini

你可以在配置文件中每一个配置项上面看到该配置项的使用说明。

几乎所有的命令行参数都对应提供了配置文件配置项;

命令行参数的优先级高于配置文件配置,也就是说通过命令行参数指定了对应的参数,配置文件中不管是否配置均不生效。

下面介绍两个常用的用例执行的功能:

2. 本地执行

本地执行子命令为:run

youqu manage.py run

2.1. 命令行参数

通过命令行参数配置参数

以下为 youqu manage.py run 提供的一些常用的参数选项:

  -h, --help            show this help message and exit
  -a APP, --app APP     应用名称:deepin-music  autotest_deepin_music 
                        apps/autotest_deepin_music
  -k KEYWORDS, --keywords KEYWORDS
                        用例的关键词,支持and/or/not逻辑组合
  -t TAGS, --tags TAGS  用例的标签,支持and/or/not逻辑组合
  --rerun RERUN         失败重跑次数
  --record_failed_case RECORD_FAILED_CASE
                        失败录屏从第几次失败开始录制视频
  --clean {yes,}        清理环境
  --report_formats REPORT_FORMATS
                        测试报告格式
  --max_fail MAX_FAIL   最大失败率
  --log_level LOG_LEVEL
                        日志输出级别
  --timeout TIMEOUT     单条用例超时时间
  --resolution RESOLUTION
                        检查分辨率
  --debug DEBUG         调试模式
  --noskip {yes,}       csv文件里面标记了skip跳过的用例不生效
  --ifixed {yes,}       fixed不生效,仅通过skip跳过用例
  --send_pms {,async,finish}
                        数据回填
  --task_id TASK_ID     测试单ID
  --trigger {,auto,hand}
                        触发者
  -f CASE_FILE, --case_file CASE_FILE
                        根据文件执行用例
  --deb_path DEB_PATH   需要安装deb包的本地路径
  --pms_user PMS_USER   pms 用户名
  --pms_password PMS_PASSWORD
                        pms 密码
  --suite_id SUITE_ID   pms 测试套ID
  --pms_info_file PMS_INFO_FILE
                        pms 信息文件
  --top TOP             过程中记录top命令中的值
  --lastfailed          仅执行上次失败用例
  --duringfail          测试过程中立即显示报错
  --repeat REPEAT       指定用例执行次数
  --project_name PROJECT_NAME
                        工程名称(写入json文件
  --build_location BUILD_LOCATION
                        构建地区(写入json文件
  --line LINE           执行的业务线(写入json文件
  --autostart AUTOSTART 用例执行程序注册到开机自启服务

在一些 CI 环境下使用命令行参数会更加方便:

youqu manage.py run --app deepin-music --keywords "xxx" --tags "xxx"

--app 入参还支持 autotest_xxxapps/autotest_xxx 两种写法,方便在输入命令的过程中使用补全,下面的远程执行功能同样支持。

2.2. 配置文件

通过配置文件配置参数

在配置文件 setting/globalconfig.ini 里面支持配置对执行的一些参数进行配置,常用的如:

;=============================== RUN CONFIG ===================================
[run]
;执行的应用名称
;为空表示执行 apps/ 目录下所有应用的用例
APP_NAME =

;执行包含关键词的用例
KEYWORDS =

;执行包含用例标签的用例
;-----------------------------------------------------------
;1.KEYWORDS 和 TAGS 都为空表示执行 APP_NAME 的所有用例
;2.KEYWORDS 和 TAGS 都支持逻辑组合,即 and/or/not 的表达式
;比如TAGS = L1 or smoke ,表示执行标签带有 L1 或 somke 标签的用例;
;这两个参数也可以同时使用,可以组合出任意的用例集合,只有想不到没有办不到。
;-----------------------------------------------------------
TAGS =

;本地文件测试套,将要执行的用例写入指定的 csv 文件
;默认为空从基础框架根目录开始e.g. CASE_FILE = case_list.txt
;如果这里有值APP_NAME KEYWORDS TAGS 的配置均不生效
CASE_FILE =

;最大失败用例数量的占比
;比如:总执行用例数为 100, 若 MAX_FAIL = 0.5,则失败用例数达到 50 就会终止测试。
MAX_FAIL = 1

;单条用例的超时时间,如果一条用例的执行时间超时,这条用例会被停止,后续用例继续执行。
;单位为秒
;这是一个全局统一配置,如果某条用例需要单独配置超时时间,可以在用例中这样写:
;@pytest.mark.timeout(500)
;def test_xxx_001():
;    ...
;会话超时(所有用例执行的超时时间)是根据全局超时配置和用例单独超时配置自动计算的。
CASE_TIME_OUT = 200

;失败用例重跑次数
;注意RERUN = 1 表示重跑 1 次,即第一次用例执行失败会自动重跑 1 次,总共执行 2 次;
;如果第 2 次执行成功,结果成功,失败亦为失败。
RERUN = 1

;失败录屏从第几次失败开始录制视频。
;比如 RECORD_FAILED_CASE = 1 ,表示用例第 1 次执行失败之后开始录屏RERUN >= RECORD_FAILED_CASE。
;1.关闭录屏RECORD_FAILED_CASE > RERUN
;2.每条用例都录屏RECORD_FAILED_CASE = 0
RECORD_FAILED_CASE = 1

;yes 每条用例执行之后进行环境清理
CLEAN_ALL = yes

;检查测试机分辨率, 比如1920x1080
;no: 表示不做分辨率校验
RESOLUTION = 1920x1080

;不跳过用例csv文件里面标记了 skip-xxx的用例不跳过
NOSKIP = no

;ignore fixed
;no只要标记了fixed的用例即使标记了skip-,也会执行;
;yesfixed不生效仅通过skip跳过用例
IFIXED = no

;要安装deb包的路径
;e.g : ~/Downloads/ 安装下载目录下的deb包如果是远程执行会自动拷贝到远程并安装。
DEB_PATH =

;DEBUG 模式执行用例,只收集不执行用例,也不做设备分辨率的检查。
DEBUG = no

;记录top命令查询的系统资源占用情况TOP = 3 表示记录前3个进程。
TOP =

;指定用例执行次数
REPEAT =

;默认在所有测试完成之后输出报错信息.
;yes, 测试过程中立即显示报错
DURING_FAIL = no

;注册自启服务
AUTOSTART = no

;测试机的密码
PASSWORD = 1

;图像识别重试次数
IMAGE_MATCH_NUMBER = 1

;图像识别重试每次间隔等待时间
IMAGE_MATCH_WAIT_TIME = 1

;图像识别匹配度
IMAGE_RATE = 0.9

;截取当前屏幕实时图像保存路径,用于图像识别坐标
SCREEN_CACHE = /tmp/screen.png

;截取屏幕上指定区域图片,保存临时图片的路径
TMPDIR = /tmp/tmpdir

;系统主题
SYS_THEME = deepin

;OCR服务端地址不可随意修改
OCR_SERVER_HOST = youqu-dev.uniontech.com

;OpenCV服务端地址
OPENCV_SERVER_HOST = youqu-dev.uniontech.com

;=============================== REPORT CONFIG ===================================
[report]
;测试报告的title
REPORT_TITLE = YouQu Report

;测试报告的name
REPORT_NAME = YouQu Report

;测试报告的默认语言
;en:English
;ru:Русский
;zh:中文
;de:Deutsch
;nl:Nederlands
;he:Hebrew
;br:Brazil
;pl:Polski
;ja:日本語
;es:Español
;kr:한국어
;fr:Français
;az:Azərbaycanca
REPORT_LANGUAGE = zh

;用例执行完后生成的测试报告格式
;目前支持 allure, xml, json (支持同时生成)
REPORT_FORMAT = allure, xml, json

;指定报告生成的路径(相对项目根目录下)
ALLURE_REPORT_PATH = report/
XML_REPORT_PATH = report/
JSON_REPORT_PATH = report/

;=============================== PMS CONFIG ===================================
;PMS相关配置包含以下几个方面
;1.PMS测试套执行
;2.自动从PMS爬取数据并同步本地CSV文件
;3.PMS数据回填
[pmsctl]
;PMS的用户名,如: ut001234
PMS_USER =

;PMS的密码
PMS_PASSWORD =

;PMS测试套的ID
;在PMS上查看用例“套件”链接: https://pms.uniontech.com/testsuite-view-495.html
;测试套ID为: 495
SUITE_ID =

;数据回填必须关联PMS测试单
;在PMS上查看测试单链接: https://pms.uniontech.com/testtask-cases-20747.html
;测试单ID为: 20747
TASK_ID =

;将测试结果数据回填到PMS
;为空: 表示不回填,不会在每条用例执行完之后生成json结果文件;
;async: 表示逐条异步回填,后面一条执行开始时通过子线程对前一条用例的执行结果进行回填,如此实现时间效率最大化;
;finish: 表示所有用例执行完成之后逐个回填(PMS不支持并发);
SEND_PMS =

;数据回填的触发者
;auto: 框架自动回填,配合SEND_PMS配置使用,你可以选择在不同的阶段进行数据回填;
;hand: 手动回填,每条用例仍然会生成json文件,但框架不会进行数据回填,需要你可以在你想要发送的时间点手动触发回填;
TRIGGER = auto

;PMS回填的重试次数
;如果接口请求失败,会进行重试
SEND_PMS_RETRY_NUMBER = 2

;caselib: 用例库
;testcase: 产品库用例
CASE_FROM = caselib

[pmsctl-pms_link_csv]
;同步PMS数据到本地CSV文件必须要配置的配置项
;key是本地CSV文件的文件名称;
;value是对应PMS上的模块ID;
;比如要同步音乐的数据, 首先需要将配置 APP_NAME = deepin-music
;CSV文件名称为music.csv其在PMS上的音乐用例库的URL为: https://pms.uniontech.com/caselib-browse-81.html
;因此应该配置为: music = 81
;这样才能将PMS与本地CSV文件建立联系。
;如果你的应用分了很多模块,只需要将对应的信息依次配置好就行了。
music =

[csvctl]
;将py文件的case id同步到csv文件
;yes, 开启同步
PY_ID_TO_CSV = no

;导出 case_list.csv 文件时配置的字段名,用例名称默认存在第一列,无需添加
EXPORT_CSV_HEARD = 用例级别,用例类型,测试级别,是否跳过


[log_cli]
;日志相关配置(不打印构造函数和魔法函数的功能说明)
;批量执行时,终端输出的日志级别 DEBUG/INFO/ERROR
LOG_LEVEL = DEBUG

# ============= 自动输出日志的配置 ================
;支持类名以 xxx 开头的,自动将函数说明打印为日志, 多个参数以逗号隔开
CLASS_NAME_STARTSWITH = Assert

;支持类名以 xxx 结尾的,自动将函数说明打印为日志,多个参数以逗号隔开
CLASS_NAME_ENDSWITH = Widget

;支持类名包含 xxx 的,自动将函数说明打印为日志,多个参数以逗号隔开
CLASS_NAME_CONTAIN = ShortCut
# ==============================================

配置完成之后,直接在命令行执行 manage.py 就好了。

youqu manage.py run

3. 远程执行

远程执行就是用本地作为服务端控制远程机器执行,远程机器执行的用例相同;

使用 remote 命令:

youqu manage.py remote

3.1. 多机器分布式异步执行

多机器分布式异步执行就是由本地 YouQu 作为服务端,控制远程 N 台机器执行相同的用例,执行完之后所有测试机的测试结果会返回给服务端 report 目录下;

远程执行同样通过配置文件 setting/globalconfig.ini 进行用例相关配置;

需要重点说一下远程执行时的测试机信息配置,在配置文件 setting/remote.ini 里面配置测试机的用户名、IP、密码。

;=============================== CLIENT LIST =====================================
; 测试机配置列表
;[client{number}]     ;测试机别名,有多少台测试机就写多少个 client别名必须包含 client 字符,且不能重复。
;user =               ;测试机 user
;ip =                 ;测试机 ip
;password = 1         ;测试机的密码, 可以不配置此项,默认取 CLIENT_PASSWORD 的值;
                      ;如果你所有测试机密码都相同,那么只需要配置 CLIENT_PASSWORD 就可以了
;=================================================================================

[client1]
user = uos
ip = 10.8.15.xx

[client2]
user = uos
ip = 10.8.15.xx

[client3]
user = uos
ip = 10.8.11.xx

有多少台机器就像这样参考上面的格式写就行了。

然后在命令行:

youqu manage.py remote

这样运行是从配置文件去读取相关配置。

如果你不想通过配置文件,你仍然通过命令行参数进行传参,

以下为 python3 manage.py remote 提供的一些参数选项:

  -h, --help            show this help message and exit
  -c CLIENTS, --clients CLIENTS
                        远程机器的user@ip:password,多个机器用'/'连接,如果password不传入,默认取sett
                        ing/remote.ini中CLIENT_PASSWORD的值,比如: uos@10.8.13.33:1
                         uos@10.8.13.33
  -s, --send_code       发送代码到测试机(不含report目录
  -e, --build_env       搭建测试环境,如果为yes,不管send_code是否为yes都会发送代码到测试机.
  -p CLIENT_PASSWORD, --client_password CLIENT_PASSWORD
                        测试机密码(全局)
  -y PARALLEL, --parallel PARALLEL
                        yes:表示所有测试机并行跑,执行相同的测试用例;no:表示测试机分布式执行,服务端会根据收集到的测试用例自
                        动分配给各个测试机执行。

除了这些特有参数以外,它同样支持本地执行的所有参数;

在命令行这样运行:

youqu manage.py remote -a deepin-music -c uos@10.8.13.33/uos@10.8.13.34 -k "xxx" -t "xxx"

所有用例执行完之后会在 report 目录下回收各个测试机执行的测试报告。

注意,如果远程机器没有搭建自动化测试环境,记得加上参数 -e

youqu manage.py remote -a deepin-music -c uos@10.8.13.33/uos@10.8.13.34 -k "xxx" -t "xxx" -e

执行前确保远程机器已经开启了 ssh 服务,否则会提示无法连接,如果没有开启,请手动开启:

sudo systemctl restart ssh
sudo systemctl enable ssh

配置文件其他相关配置项详细说明,请查看配置文件中的注释内容。

3.2. 多机器分布式异步负载均衡执行

多机器分布式异步负载均衡执行也是用本地作为服务端控制远程机器执行,但远程机器执行的用例不同,而是所有远程机器执行的用例之和,为你想要执行的用例集;

似乎有点难以理解,我用大白话举例描述下就是,服务端想要执行 10 条用例,现在远程机器有 5 台;

然后服务端就先拿着第 1 条用例给远程 1 号机执行,拿第 2 条用例给远程 2 号机执行...,如此循环直到所有用例执行完,这就是负载均衡执行。

使用方法和前面一样,只是需要增加一个参数 --parallel

youqu manage.py remote -a deepin-music -c uos@10.8.13.33/uos@10.8.13.34 -k "xxx" -t "xxx" --parallel no

图像识别

图像识别在 UI 自动化中是不可缺少的,市面上甚至有完全基于图像识别的自动化测试框架,比如 AirtestSikuli 等,在游戏等特定领域也有不错的效果,这些工具实际上也是用的 OpenCV 进行了封装,YouQu 框架基于 OpenCV 开发了自己的图像识别功能,它可以方便的用于界面元素的定位和断言;

YouQu 的图像识别功能几乎满足了你的所有要求,我们在长时间的思考和摸索中,针对常规场景及一些特殊场景探索除了一些实用且有效的方案,且听我慢慢道来。

1. 常规识别

【背景】

常规识别很好理解,一句话讲就是,要获取到目标元素在屏幕中的位置。

【原理实现】

在测试过程中需要获取的坐标是相对于整个屏幕的坐标我们可以截取到整个屏幕的图片screen

在元素识别的过程中,我们需要截取某个元素的小图进行识别,比如截取播放按钮:

那么实际上,元素定位的问题就转换为,将截图的小图(play_btn)拿到整个屏幕的大图(screen)中去做匹配,如果匹配成功,返回小图在大图中的坐标( x, y )即可。

为了方便描述,以下我将整个屏幕的截图称为:大图,某个元素图片的截图称为:小图。

基于 OpenCV 的模板匹配 cv.matchTemplate() 功能,我们实现了图像定位的功能,框架提供了一个图像识别的底层接口(一般不对上层提供调用):

def _match_image_by_opencv(
    image_path: str, 
    rate: float = None, 
    multiple: bool = False, 
    picture_abspath: str = None, 
    screen_bbox: List[int] = None
):
    """
     图像识别,匹配小图在屏幕中的坐标 x, y
    :param image_path: 图像识别目标文件的存放路径
    :param rate: 匹配度
    :param multiple: 是否返回匹配到的多个目标
    :param picture_abspath: 大图,默认大图是截取屏幕,否则使用传入的图片;
    :param screen_bbox: 截取屏幕上指定区域图片仅支持X11下使用
        [x, y, w, h]
        x: 左上角横坐标y: 左上角纵坐标w: 宽度h: 高度;根据匹配度返回坐标
    """
    # 详细代码太长不贴了,感兴趣请查看源码

【参数介绍】

1image_path

image_path 是小图的绝对路径;

  • 通常在 AT 工程里面,我们约定将用于元素定位的图片资源放到 widget/pic_res 目录下,图片的名称以实际的元素名称命名,如:play_btn.png

  • 用于用例断言的图片资源放到 case/assert_res 目录下,图片的名称以用例的名称命名,如:music_001.png

这样是为了方便管理和维护。

2rate

图像识别的的匹配度或者说相似度,框架默认的配置为 0.9,也就是说小图在大图中存在一个相似度 90% 的图标即返回其在大图中的坐标;

如果你在用例中需要调整识别度,你可以在调用函数的时候,传入不同的识别度的值。

3multiple

默认情况下 multiple=False,表示只返回识别到的第一个,如果 multiple=True 返回匹配到的多个目标,因为大图中可能存在多个相同的小图,在某些场景下你可能需要全部获取到所有匹配到的坐标。

4picture_abspath

默认情况下 picture_abspath=None 表示大图为截取的屏幕截图,如果你不希望大图是屏幕的截图,而是你自定义传入的某个图片,你只需要将你的图片路径传递给这个参数就行,比如: picture_abspath="~/Desktop/big.png"

5screen_bbox

大图默认情况下是截取整个屏幕,screen_bbox = [x, y, w, h] 可以指定截取屏幕中的固定区域,某些场景下,可以排除部分区域对识别结果的影响。

【隐式等待】

用例执行过程中进行图像识别时,有时候页面跳转有延时,有可能存在识别的那一刻页面也没有跳转出来,或者或者识别的那一刻;

因此我们需要一种等待机制,即在一定的时间内,如果识别不到,重复去识别:

def find_image(
    	cls,
        *widget, rate: [float, int] = None,
        multiple: bool = False,
        match_number: int = None,
        picture_abspath: str = None,
        screen_bbox: List[int] = None
):
    """
     在屏幕中区寻找小图,返回坐标,
     如果找不到根据配置重试次数每次间隔1秒
    :param widget: 模板图片路径
    :param rate: 相似度
    :param multiple: 是否返回匹配到的多个目标
    :param match_number: 图像识别重试次数
    :return: 坐标元组
    """
    if rate is None:
        rate = float(GlobalConfig.IMAGE_RATE)
    try:
        for element in widget:
            for _ in range((match_number or int(GlobalConfig.IMAGE_MATCH_NUMBER)) + 1):
                locate = cls._match_image_by_opencv(
                    element,
                    rate,
                    multiple=multiple,
                    picture_abspath=picture_abspath,
                    screen_bbox=screen_bbox
                )
                if not locate:
                    sleep(int(GlobalConfig.IMAGE_MATCH_WAIT_TIME))
                else:
                    return locate
        raise TemplateElementNotFound(*widget)
    except Exception as e:
        raise e

参数 match_number 用于控制重复识别的次数,默认不传参,取全局配置 setting/globalconfig.ini 里面的 IMAGE_MATCH_NUMBER 配置项的值,默认IMAGE_MATCH_NUMBER = 1,即重试 1 次;

find_image 是框架提供的常规图像识别函数接口,这个函数提供了隐式等待的功能,且包含上面介绍的 _match_image_by_opencv 函数的所有功能。

2. 气泡识别

【背景】

气泡识别指的是,某些场景下要定位的元素是一些会消失的小弹窗,这类场景在用例执行过程中进行图像识别时就可能存在不稳定性,有可能图像识别的时候气泡已经消失了,也有可能气泡出现的时间太短了,不容易捕捉到,就像气泡一样,出现一下就消失,因此我们形象的称之为 “气泡识别”;

【原理实现】

为了能稳定的识别气泡类场景,我们采用的方案是:

在一段时间内(包含气泡从出现到消失),不停的截取这段时间内的大图,以此确保在截取的一堆图片中,肯定有至少一张图片能捕捉到气泡,最后再对这一堆图片逐个进行图像识别;

代码示例:

def get_during(
        cls,
        image_path: str,
        screen_time: [float, int],
        rate: float = None,
        pause: [int, float] = None,
        max_range: int = 10000
):
    """
    在一段时间内截图多张图片进行识别,其中有一张图片识别成功即返回结果;
    适用于气泡类的断言比如气泡在1秒内消失如果用常规的图像识别则有可能无法识别到
    :param image_path: 要识别的模板图片;
    :param screen_time: 截取屏幕图片的时间,单位秒;
    :param rate: 识别率;
    :param pause: 截取屏幕图片的间隔时间,默认不间隔;
    :param max_range: 截图的最大次数,这是一个预设值,一般情况下不涉及修改;
    """

【参数介绍】

1screen_time

截取屏幕图片的时间,在此时间内会不断的进行截图操作,就像录制视频一样;

2pause

每次截取图片的间隔时间默认情况下是一刻不停的截图如果你想每次截图存在一些间隔时间传入对应的时间间隔即可单位是秒比如pause = 0.03,表示 30 ms相当于帧率为 30 帧;

3. 不依赖 OpenCV 的图像识别方案

3.1. 自研图像识别技术

【原理】

为了实现识别图像的目的我们可以通过将图片的每个像素的RGB值与整个屏幕中的RGB进行对比如果小图上的RGB值与对应大图位置的RGB都相等则匹配成功即可返回小图在大图中的中心坐标点。

读取小图和大图的RGB值

1小图的RGB值

small_data = small_pic.load() 
# load()会将图片的RGB值获取到数据格式为一个二维列表赋值给一个变量small_data。

2大图的RGB值

big_data = big_pic.load()

将小图与大图的RGB值进行匹配

1匹配从大图的坐标00开始匹配匹配小图里面所有的坐标点00small_pic.widthsmall_pic.height

2如果在大图的00对应的所有小图的RGB值不相等则移动到下一个坐标点10同样匹配小图里面所有的坐标点00small_pic.widthsmall_pic.height

3按照这样的规律将这一行每移动一个坐标点都将小图所有的RGB与对应大图的值进行匹配

4如果在大图的其中一个坐标点上匹配到了小图的所有RGB值则此时返回小图在大图中的坐标点

5如果匹配了大图所有的坐标点都没有匹配到则说明大图中不存在小图匹配失败

【代码实现】

class ImageRgb:

    @staticmethod
    def _check_match(_x, _y, small, bdata, sdata, rate):
        """
        Matching degree of small graph and large graph matching
        """

    @staticmethod
    def _pre_random_point(small):
        """
        Pre matching, take 10-20 points at random each time,
        and take coordinates randomly in the small graph
        """

    @staticmethod
    def _pre_random_match(_x, _y, point_list, bdata, sdata, rate):
        """
        In the small graph, several points are randomly
        selected for matching, and the matching degree is
        also set for the random points
        """

    @classmethod
    def match_image_by_rgb(cls, image_name=None, image_path=None, rate=0.9):
        """
        By comparing the RGB values of the small image with the large
        image on the screen, the coordinates of the small image on
        the screen are returned.
        """

通过 match_image_by_rgb() 这个函数,传入目标小图的文件名称,即可返回在当前屏幕中的中心坐标。

有同学要问了,有 OpenCV 干嘛不用,有必要自己实现一个图像识别的功能吗,你们是不是闲的啊?

这么问的话,小了,格局小了;我们自己实现主要有几方面原因:

  • 减少环境依赖,不用安装 OpenCV 我们也能实现其功能,环境依赖这块后面会单独详细讲,减少环境依赖对于任何软件工程都非常重要;
  • OpenCV 在其他国产 CPU 架构上安装并不能保证100%成功,甚至有没有可能在一些架构上压根儿就不能安装使用 OpenCV
  • 有没有可能有一天国内无法使用 OpenCV ?就像有没有可能有一天国内无法使用 Windows 呢?这些问题值得思考。

当然,我们承认这套方案,虽然识别准确率没问题,但在识别效率上还没有达到 OpenCV 模板匹配的效果,我们的方案每次识别在 1.5s 左右,而 OpenCV 1s 左右;

整体识别效果来讲,我认为还是可以接受的,也希望有志之士能一起优化此方案,一起技术报国。

3.2、基于 RPC 服务实现图像识别

在远程服务器上部署 OpenCV 的环境,并将其部署为 RPC 服务,测试机上不用安装 OpenCV 依赖,而是通过请求 RPC 服务的方式进行图像识别;

【原理】

测试机截取当前屏幕图片以及模板图片,发送给 RPC 服务端,服务端拿到两张图片进行图像识别,最后将识别结果返回给测试机;

要特殊说明的是: RPC 是一种协议,许多语言都是支持的,比如说服务端也可以用 C++ 来实现,客户端使用 Python 也是可以调用的。

【代码实现】

服务端代码示意Service

from socketserver import ThreadingMixIn
from xmlrpc.server import SimpleXMLRPCServer

import cv2 as cv
import numpy as np

class ThreadXMLRPCServer(ThreadingMixIn, SimpleXMLRPCServer):
    pass

CURRENT_DIR = dirname(abspath(__file__))

def image_put(data):
    """上传图片"""

def _match_image_by_opencv(
    image_path: str, 
    rate: float = None, 
    multiple: bool = False, 
    picture_abspath: str = None, 
    screen_bbox: List[int] = None
):
    """
     图像识别,匹配小图在屏幕中的坐标 x, y
    :param image_path: 图像识别目标文件的存放路径
    :param rate: 匹配度
    :param multiple: 是否返回匹配到的多个目标
    :param picture_abspath: 大图,默认大图是截取屏幕,否则使用传入的图片;
    :param screen_bbox: 截取屏幕上指定区域图片仅支持X11下使用
        [x, y, w, h]
        x: 左上角横坐标y: 左上角纵坐标w: 宽度h: 高度;根据匹配度返回坐标
    """
    
if __name__ == "__main__":
    server = ThreadXMLRPCServer(("x.x.x.x", 8889), allow_none=True)
    server.register_function(image_put, "image_put")
    server.register_function(match_image_by_opencv, "match_image_by_opencv")
    server.serve_forever()

这样,我们基于 Python 标准库 xmlrpc 搭建了一个 RPC 服务器,注册了 image_putmatch_image_by_opencv 两个功能接口,在测试机上可以通过 IP 和端口进行 RPC 请求;

客户端代码示意Client

from xmlrpc.client import Binary
from xmlrpc.client import ServerProxy

server = ServerProxy(GlobalConfig.OPENCV_SERVER_HOST, allow_none=True)
screen_rb = open(screen, "rb")
template_rb = open(template_path, "rb")
try:
    screen_path = server.image_put(Binary(screen_rb.read()))
    screen_rb.close()
    tpl_path = server.image_put(Binary(template_rb.read()))
    template_rb.close()
    return server.match_image_by_opencv(
        tpl_path, screen_path, rate, multiple
    )
except OSError as exc:
    raise EnvironmentError(
        f"RPC服务器链接失败. {GlobalConfig.OPENCV_SERVER_HOST}"
    ) from exc

通过返回 server.match_image_by_opencv 就获取了在服务端图像识别的结果。

4. 右键菜单前后对比图像识别

4.1. 右键菜单定位的方案及问题

右键菜单的元素定位是一个难点,过去我们调研和使用过的元素定位操作方法有 4 种:

第一种:步长操作法

在右键菜单呼出来之后,通过键盘的 updown 按键,进行选择菜单选择,选中目标之后 enter 即可;比如:在桌面点击右键菜单之后,按 1 次 down ,会出现下图:

继续再按 2 次 down,会出现这样:

再按 enter,会出现这样:

如此,“排序方式”的步长为 3通过使用键盘上下键就实现了对右键菜单的操作

但是,这种方式有个很烦人的问题,就是右键菜单的选项位置不可能一直不变,在需求迭代的过程中,菜单选项的变化是很大的,甚至有些应用支持自定义菜单,比如文管右键菜单可以自定义;

也就是说你得经常去维护菜单选项的步长,一个选项现在的步长是 3下个迭代可能就是 4 或者 5。

第二种:常规图像识别法

把每个菜单选项单独截图保存,图片中仅包含一个菜单选项,如下图所示:

这样,每个菜单选项就可以通过图像识别的方式进行元素定位;

这种方式不用担心菜单选项的顺序或位置,但是需要保存大量的图片,且容易受到字体 UI 变更类需求的影响,比如:字体大小、字体间距等等需求变更都会影响,每次变更之后就需要进行大量图片资源的重新截图替换,是个比较麻烦的事情;

第三种:相对位移法

鼠标点击右键的时候,鼠标的当前坐标是可以获取到的,菜单选项的宽( w )一般是固定的,变化的是菜单的长度( h ),可以通过某个选项相对于鼠标的距离在确定菜单选项的坐标,如下图所示:

通过维护菜单选项(相对位置)相对于鼠标位置的距离,即可轻松计算出菜单选项在屏幕中的坐标。

从理论上此方案是可行的,但是这里仍然存在两个严重的问题:

  • 菜单顺序改变导致相对距离改变而且距离是通过像素px来表示的不想步长那么只管每次需要去量一下维护起来有点麻烦
  • 鼠标在桌面不同位置点击右键右键菜单出现的位置是不同的上图的菜单是在鼠标的左下方如果你移动鼠标到屏幕四边你会发现右键菜单可能出现在鼠标的四个方向也就是说你需要根据鼠标的不同位置来判断右键菜单出现的方向而不同的方向上计算方法是不同的比如右键菜单在鼠标的左上菜单选项的坐标计算方法为x - w / 2 y - h这样维护起来可以说非常复杂

基于以上两个原因,我们并不推荐这种操作方案。

第四种:属性定位

有同学说干嘛不通过属性定位呢,其实,我们最开始想到的方案就是通过属性定位,但是在属性的 DOM 树里怎么也找不到,无法定位到,我们也联合研发同学一起解决此问题,但最终还是没能解决,非常遗憾;

4.2、最新的右键菜单终极解决方案

由于右键菜单选项几乎都是文本,那么通过 OCR 识别,几乎是最优的方案:

  • 不用保存大量的图片;
  • 不会受到菜单选项顺序改变的影响;
  • 不会受到字体 UI 变化的影响;

关于 OCR 识别我们在后面会详细讲到,本章节主要讲基于 OCR 识别,我们在右键菜单识别上的突破和创新;

使用 OCR 识别右键菜单虽然已经很完美了,但是在一些情况下仍然存在一点问题,就是屏幕中出现多个和菜单选项文字相同的文字时,比如下面这种情况:

屏幕恰好出现了两个“复制”,此时要定位到菜单中的 “复制”就需要进一步做数据处理比如OCR 返回 2 个“复制”的坐标,用例里面来判断用哪个,从业务上将处理起来比较麻烦,因为你得明确知道菜单在左边还是右边,然后这本身就是不确定的;

怎么解决这个问题呢,这就要说道本章的主题:前后对比图像识别

【原理】

在点击右键菜单之前截一张图,点击右键菜单之后再截取一张图,两张图唯一变化的就是右键菜单,将其他相同的地方都屏蔽掉,只留下菜单界面,如此即可消除干扰,如下图所示:

将两种图做前后对比提取可以得到这样的图:

你看,通过前后图片的对比,将相同的部分给消除掉,再进行 OCR 识别,这样就不会有干扰了;

代码实现请查看 src/filter_image.py

代码示例:

def filter_image(action):
    """
    对比动作前后两张图片,提取不同的部分生成一张新的图片,并返回新图片的路径
    :param action: 动作函数的函数对象
    :return: 新图片的路径
    """

action 是鼠标事件,因为此类场景不仅仅在右键存在,单击、双击等等,只要操作前后后变化的都可以:

from src.mouse_key import MouseKey
action = (
    MouseKey.click,
    MouseKey.right_click,
    MouseKey.double_click,
    MouseKey.move_to,
)

5. 动态图像识别

【背景】

在桌面壁纸切换,或看图、相册切换图片类的测试场景,由于你的测试资源是不固定的(不同版本的系统壁纸不同、壁纸顺序不同,看图相册在图片资源不一定固定),那么在测试切换壁纸或者切换图片的场景时就会存在一个问题,就是你不知道预期是啥,用例操作动态的,也是极不稳定。

【原理】

在切换图片之前截图保存并返回图片的路径,切换图片之后再次识别这张图片,如果不存在,说明图片已经切换了;

示意图:

这样,我们截取了当前图片中比较有代表性的位置(一只鸟),在切换图片之后再用这张小图在当前屏幕中进行图像识别:

我们再拿着这张小图在当前屏幕中进行图像识别,这样在当前图片中,就不能找到这只鸟了,图像识别的结果是 False那么也就可以判断图片切换是成功的。

代码示意:

def save_temporary_picture(_x: int, _y: int, width: int, height: int):
    """
     截取屏幕上指定区域图片,保存临时图片,并返回图片路径
    :param x: 左上角横坐标
    :param y: 左上角纵坐标
    :param width: 宽度
    :param height: 高度
    :return: 图片路径
    """

此函数用于在操作之前截图一张临时图片,返回图片路径,最后在断言的时候再将图片路径作为参数传入断言语句即可;

代码示例:

基于 UI 的元素定位方案

1. 背景

基于 UI 的元素定位方案是我们自研的一种使用简单,且效率极高、稳定性好的元素定位方案,基于元素按钮在应用中的相对位置,动态获取元素在当前屏幕中的位置,适用于各种屏幕分辨率(包括高分屏、宽屏、带鱼屏),当元素按钮位置相对于应用界面位置发生修改之后,只需要根据 UI 设计图上的源数据修改对应坐标数据就好,维护非常的方便。

此类元素定位方案适用于一些元素位置相对与应用界面比较固定的应用比如音乐99% 的元素定位采用这种,效果非常好),不适用于界面不固定的应用,比如截图录屏,很明显不适用于这类元素定位方案。这种全新的元素定位方案有它的适用条件,如果你发现使用常规的(属性定位、图像定位)不好做时,不妨考虑使用这种,其效果一定能惊讶到你,并且迅速爱上他。

2. 实现原理

在 UI 设计图中我们是可以获取到元素按钮相对于应用边框的距离的,然后我们可以通过技术手段获取到应用界面在当前屏幕中的位置及应用窗口的大小,示意图如下:

x1, y1为应用左上角相对于屏幕左上角0, 0的位置x2, y2是按钮【播放所有】的左上角相对于应用窗口左上角x1, y1的坐标那么实际上【播放所有】左上角相对于屏幕左上角0, 0的位置为 x1 + x2, y1 + y2

是按钮【播放所有】的大小w2, h2可以从 UI 设计图上获取;

在 UI 设计稿上点击【播放所有】图标,然后移动鼠标就就可以看到上图的参考线及数据;

所以,我们可以轻松的获取到按钮【播放所有】的中心坐标为: x1 + x2 + (w2 / 2), y1 + y2 + ( h2 / 2)

详细源代码请查看 AT 基础框架: src/button_center.py

  • 获取应用窗口的信息

使用 xdotoolxwininfo 获取到应用窗口在当前屏幕中的位置(左上角)及大小。

代码示例:

app_id = CmdCtl.run_cmd(f"xdotool search --classname --onlyvisible {self.app_name}"
).split("\n")
app_id_list = [int(_id) for _id in app_id if _id]
return CmdCtl.run_cmd(f"xwininfo -id {app_id_list[self.number]}")

使用正则获取窗口的位置及大小。

  • 获取配置

ini 配置文件,获取元素按钮相对与应用窗口边界的位置及大小。

代码示例:

conf = ConfigParser()
conf.read(self.config_path)
direction = conf.get(btn_name, "direction")
position = [int(i.strip()) for i in conf.get(btn_name, "location").split(",")]
  • 计算元素按钮坐标

根据应用窗口在屏幕中的位置大小、元素按钮相对于应用窗口边界的位置大小,使用一定的算法即可计算出元素按钮在当前屏幕中的位置(中心坐标)。

3、使用方法

【配置方法】

基于 UI 的元素定位方案的数据源是应用库中的 ui.ini 配置文件:

;section
[新建歌单+] 
;key = value
direction = left_top
location = 180, 268, 21, 21
  • section 是你根据对应的元素按钮命名,你可以任意命名,但最好有具体含义,且能明确表示这个元素按钮的名称;用中括号括起来就行,比如:[新建歌单+]

  • direction 是配置该元素的参考系,分别为:

    • left_top 左上;

    • left_bottom 左下;

    • right_top 右上;

    • right_bottom 右下;

    参考系的选取标准:拉动改变窗口大小时,元素按钮相对于参考系位置是不变的;

  • location 是该元素按钮的相对与参考系的 x, y 的距离,及大小(w, h),这四个数据可以通过 UI 设计图上获取数据,在编辑模式下,点击 UI 图上的按钮,右侧就会出现该元素按钮的 x, y, w, h 数据。

如果 UI 图上没有提供你想要的元素数据,你可以直接在系统中使用截图录屏进行尺量,这是一种不推荐但能用的方法。

【调用方法】

在应用库方法层这样写:

def click_add_music_list_btn_in_music_by_ui(self):
    """点击新建歌单按钮"""
    self.click(*self.ui.btn_center("新建歌单+"))

self.ui.btn_center() 是固定写法,参数就是配置里面的 section

OCR 定位

1. 背景

传统的 OCR 方案大多采用谷歌 OCRTesseract)方案,但是它对于中文的识别非常差,经过大量的调研,我们使用 PaddleOCR,它是一个开源的基于深度学习的 OCR 识别工具,也是 PaddlePaddle 最有名的一个开源项目,感兴趣的可以点这里了解,多的不说了,你只需要知道它就是中文识别的天花板。

2. 实现原理

安装他是个很麻烦的事情,虽然操作很简单,但其实安装包有点大,因此我们不希望直接在 env.sh 中加入它,这会让整个自动化环境变得非常臃肿;

因此,我们想到将它做成一个 RPC 服务在其他机器上部署,测试机通过远程调用 RPC 服务的方式使用它;

RPC 的调用逻辑:

这样我们只需要在服务端部署好 OCR 识别的服务,然后通过 RPC 服务将功能提供出来,框架里面只需要调用对应的 RPC 接口就行了。

3. 使用说明

服务端代码示意Service

from socketserver import ThreadingMixIn
from xmlrpc.server import SimpleXMLRPCServer

from paddleocr import PaddleOCR

class ThreadXMLRPCServer(ThreadingMixIn, SimpleXMLRPCServer):
    pass

CURRENT_DIR = dirname(abspath(__file__))

def image_put(data):
	"""上传图片"""

def paddle_ocr(pic_path, lang):
    """
     Paddleocr目前支持的多语言语种可以通过修改lang参数进行切换
     例如`ch`, `en`, `fr`, `german`, `korean`, `japan`
    :param file_name:
    :param lang:
    :return:
    """

if __name__ == "__main__":
    IP = popen("hostname -I").read().split(" ")[0]
    PORT = 8890
    SCREEN_CACHE = "/tmp/screen.png"
    server = ThreadXMLRPCServer((IP, PORT), allow_none=True)
    server.register_function(image_put, "image_put")
    server.register_function(paddle_ocr, "paddle_ocr")
    print("监听客户端请求。。")
    server.serve_forever()

框架代码示意Client

from src import OCR

OCR.ocr(*target_strings, picture_abspath=None, similarity=0.6, return_default=False, return_first=False, lang="ch"):
# 通过 OCR 进行识别。
# target_strings:
#     目标字符,识别一个字符串或多个字符串,并返回其在图片中的坐标;
#     如果不传参,返回图片中识别到的所有字符串。
# picture_abspath: 要识别的图片路径,如果不传默认截取全屏识别。
# similarity: 匹配度。
# return_default: 返回识别的原生数据。
# return_first: 只返回第一个,默认为 False,返回识别到的所有数据。
# lang: `ch`, `en`, `fr`, `german`, `korean`, `japan`

此方案在框架内没有引入任何三方依赖完全采用标准库实现,而且使用方法非常简单,只需要通过 OCR.ocr() 即可;

对于一些文案的场景非常适用,此方法直接返回坐标,可以用于元素定位

也可以用于文字断言,代码示意:

    def assert_ocr_exist(
        *args, picture_abspath=None, similarity=0.6, return_first=False, lang="ch"
    ):
        """断言文案存在"""
        pic = None
        if picture_abspath is not None:
            pic = picture_abspath + ".png"
        res = OCR.ocr(
            *args,
            picture_abspath=pic,
            similarity=similarity,
            return_first=return_first,
            lang=lang,
        )
        if res is False:
            raise AssertionError(
                (f"通过OCR未识别到{args}", f"{pic if pic else GlobalConfig.SCREEN_CACHE}")
            )
        if isinstance(res, tuple):
            pass
        elif isinstance(res, dict) and False in res.values():
            res = filter(lambda x: x[1] is False, res.items())
            raise AssertionError(
                (
                    f"通过OCR未识别到{dict(res)}",
                    f"{pic if pic else GlobalConfig.SCREEN_CACHE}",
                )
            )

在用例中使用断言,示例:

    def test_font_manager_021(self):
        """右侧♥-收藏/取消收藏字体"""
		
        # 字体管理器界面右侧详情列表,选择未收藏字体,右键 / 收藏字体
        # 收藏字体,右键菜单显示“取消收藏”;
        ...
        self.assert_ocr_exist("取消收藏")

4. 服务端部署

我们目前是将 OCR 服务部署在普通的办公机上的,如果你觉得现有的 OCR 识别性能不够好,恰好你有更好的机器,可以考虑将其私有化部署。

4.1. 环境安装

推荐使用 pipenv 进行环境搭建;

安装 pipenv

sudo pip3 install pipenv

新建一个目录作为环境包 ocr_env

cd ~
mkdir ocr_env

创建 python 3.7 环境:

cd ocr_env
pipenv --python 3.7

安装 OCR 依赖包:

pipenv install paddlepaddle -i https://mirror.baidu.com/pypi/simple
pipenv install "paddleocr>=2.0.1" -i https://mirror.baidu.com/pypi/simple

不出意外,这样就把依赖安装好了。

4.2. 启动服务

将基础框架中的 scr/ocr/pdocr_rpc_server.py 文件拷贝到 ocr_env 目录,后台执行它就好了:

cd ocr_env
nohup pipenv run python pdocr_rpc_server.py &

4.3. 配置开机自启(通用)

你肯定不想每次机器重启之后都需要手动启动服务,因此我们需要配置开机自启。

写开机自启服务文件:

sudo vim /lib/systemd/system/ocr.service

autoocr 名称你可以自定义,写入以下内容:

[Unit]
Description=OCR Service
After=multi-user.target

[Service]
User=uos
Group=uos
Type=idle
WorkingDirectory=/home/uos/ocr_env
ExecStart=pipenv run python pdocr_rpc_server.py

[Install]
WantedBy=multi-user.target

注意替换你的${USER}

修改配置文件的权限:

sudo chmod 644 /lib/systemd/system/ocr.service

自启服务生效:

sudo systemctl daemon-reload
sudo systemctl enable ocr.service

查看服务状态:

sudo systemctl status ocr.service

你可以再重启下电脑,看看服务是不是正常启动了,没报错就 OK 了。

属性定位

1. 背景

传统的 UI 自动化大多都是基于浏览器的,核心是在网页的 DOM 树上查找元素,并对其进行定位操作;

Linux 桌面应用大多是采用 Qt 编写的,在 Qt 中也是从最顶层的 MainWindow 开始构建应用所以逻辑也是一样的Qt 应用的自动化测试同样可以通过 DOM 树(属性树)进行元素定位,我们称之为属性定位

借助开源工具 dogtail 我们可以对元素进行获取,从而进行定位操作。dogtail 的项目介绍可以猛戳这里

2. 使用方法

2.1. sniff嗅探器使用

在终端输入 sniff 启动 AT-SPI Browser

mikigo@mikigo-PC:~$ sniff

查看应用的标签

在 sniff 里面可以看到系统中已启动的应用,点击应用名称前面的三角形图标,可以展开应用的标签,所有标签以 tree 的形式展示,对应应用里面的父窗口和子窗口。

获取元素控件的标签名称

首先,为了方便查看元素控件对应的位置,建议现在上方工具栏点击 Actions,然后勾选 Hightlight Items,这样在 sniff 中鼠标选中元素标签的时候,应用中会有相应的光标锁定。

在 sniff 里面点击进入应用的标签 tree 中后,点击相应的元素控件,在工具下方,会展示元素控件的 Name,这个就是标签名称。

在 tree 中有些地方是空白的或者是 Form是因为开发人员在添加标签的时候没有添加或者有些父窗口不需要添加这种在实际业务中是不影响的我们只要保证自动化测试用例中要用到的元素都添加了标签即可。

2.2. 元素操作

获取应用对象

dogtail 获取应用对象的时候,使用的是 tree 库里面的 application() 方法:

from dogtail.tree import root
app_obj = root.application('deepin-music')

app_obj就是应用的对象。

  • 获取元素对象

获取元素对象是应用对象使用child()方法:

element = app_obj.child('element_name')

我们可以通过传入元素的 Name获取到相应元素的对象。Name 可以通过 sniff 查看。

  • 获取元素对象列表:
element_list = element.children

获取到这个元素下面所有的元素列表。

这个方法适用于有些标签没有添加,但是位置是固定的,我们通过索引可以取得元素。

element_list[0]
  • 对元素的操作

在获取到元素之后,我们就可以对元素进行相应的操作。

  • 单击
element.click(button=1)

button 1 —>左键2 —>滚轮3 —>右键,默认为 1

  • 双击
element.doubleClick(button=1)
  • 鼠标悬停
element.point()

鼠标移动到元素中心位置

  • 文本输入
element.typeText(string)

向元素对象输入字符串 ,比如输入框

  • 组合键
element.keyCombo(comboString)

2.3. 框架封装

代码示例:

# 详细代码请查看 src/dogtail_utils.py
class DogtailUtils:

    def __init__(self, name=None, description=None):
        self.name = name
        self.description = description
        self.obj = root.application(self.name, self.description)


    def app_element(self, *args, **kwargs):
        """
         获取app元素的对象
        :return: 元素的对象
        """
        return self.obj.child(*args, **kwargs, retry=False)
    
    def element_center(self, element) -> tuple:
        """
         获取元素的中心位置
        :param element:
        :return: 元素中心坐标
        """
        _x, _y, _w, _h = self.app_element(element).extents
        _x = _x + _w / 2
        _y = _y + _h / 2
        return _x, _y

框架提供的接口非常简洁,在调用时:

self.dog.element_center("播放")

这样就能获取到此元素的中心坐标。

重启类场景

对于重启类场景的用例需要解决的核心问题是,重启之后如何让用例能继续重启前的步骤继续执行,YouQu 集成了自研的 letmego 技术方案;

详细技术方案、实现细节、Demo可以看 letmego 官方在线文档;

1. 使用方法

使用方法很简单,只需要给应用方法层的唯一出口类加一个装饰器(@letmego.mark)即可:

import letmego

@letmego.mark
class DeepinMusicWidget(WindowWidget, TitleWidget, PopWidget):
    """音乐业务层"""

2. 用例注意事项

这类用例相对特殊,这里主要介绍写用例的时候注意事项:

1用例的前置和后置要写在同一个用例文件里面这点如果了解方案实现原理很容易理解

2重启步骤前面的步骤如果有对象实例化的需要处理实例化存在异常因为 YouQu 的对象实例化默认会检测应用是否启动,重启之后虽然重启步骤前面的步骤函数不会执行,但是方法类同样会进行实例化,所以需要处理这个问题;

# ignore import
class TestMusic(BaseCase):
    """
    音乐用例
    """
    def test_music_679537(self):
        try:
            music = DeepinMusicWidget()
            music.click_singer_btn_in_music_by_ui()
            music.click_icon_mode_in_music_by_ui()
        except ApplicationStartError:
            pass
        # ========== reboot ==========
        DeepinMusicWidget.reboot()
        # ========== 重启之后继续执行 =========
        DdeDockPublicWidget().open_music_in_dock_by_attr()
        DeepinMusicWidget.recovery_music_by_cmd()
        music = DeepinMusicWidget()
        music.first_add_music_by_ui()
        music = DeepinMusicWidget()
        music.click_singer_btn_in_music_by_ui()
        music.click_icon_mode_in_music_by_ui()
        self.assert_music_image_exist("music_679537")

3重启步骤最好是一个简单的reboot操作不建议在组合步骤中间插入一个reboot

@letmego.mark
class DeepinMusicWidget(WindowWidget, TitleWidget, PopWidget):

    @staticmethod
    def reboot():
        """letmego reboot"""
        os.system("echo '1' | sudo -S reboot")

3. 驱动执行

因为重启类场景需要注册自启服务以及对用例执行过程的处理,驱动执行的时候加 --autostart yes :

youqu manage.py run --autostart yes

标签化管理

1. 标签说明

根据现有业务需要,用例需要添加的标签有:

  • 脚本 ID自动化用例脚本/函数 ID
  • PMS用例IDPMS 上对应的用例 ID用例库 ID默认使用用例库 ID对于暂时没有使用用例库管理用例的项目可以使用产品库用例 ID
  • 用例级别:对应 PMS 上用例级别,分别用 L1、L2、L3、L4 表示;
  • 用例类型:FUNC(功能)、PERF(性能)、STR(压力)、SEC(安全)、CTS(兼容性)、API(接口)、BASELINE(基线-预留)
  • 设备类型:PPL(依赖外设的用例)、COL(依赖主控机的用例)
  • 用例来源:BUG(由 Bug 转的用例)
  • 上线对象:CICD,表示上线到 CICD 流水线的用例,后续可一键生成 case_list.csv 文件,用于导入到明道云 AT 用例列表中控制 CICD 跑测范围;
  • 测试级别:C1 / C2 / C3 / C4,具体定义参考流水线使用指导
  • 跳过原因:skip-XXX,用于控制用例是否执行;
  • 确认修复:fixed-XXX,用于标记用例的修复状态(后面详细讲解用法);
  • 废弃用例:removed-已废弃,用于标记已经废弃的用例,此用例标签不会被添加,也不会被执行;

示例:

脚本ID PMS用例ID 用例级别 用例类型 设备类型 用例来源 上线对象 测试级别 跳过原因 确认修复 废弃用例 ...
679537 679537 L1 FUNC PPL CICD C1 skip-XXX fixed-XXX removed-已废弃 ...

2. 操作步骤

2.1、在子项目目录下新建 csv 文件,用于保存用例标签,以用例的 py 文件除去首字符串 "test_" 和用例序号后的字符串作为 csv 的文件名。

例如:

  • 相册的用例文件为 test_album_xxx.pyxxx 表示用例的ID也可以是自定义的数字代表用例序号此时 csv 文件名就应为 album.csv

对于用例规模比较大的应用,比如文件管理器,建议分模块,每个模块建立一个 csv 文件,所有 csv 文件建议放在一个 tags 目录下。

是否分模块维护 csv 取决于应用的用例复杂度,同时我们应该充分考虑后期的可维护性,csv 文件太多了也是一个很糟糕的事情。

2.2、第一列为脚本 ID从第二列开始及之后的列每一列都是一个用例标签后续需要新增用例标签可以直接在 csv 文件里面添加对应的列即可;用例标签可以无序。

3. 增加说明

3.1. 跳过用例

传统跳过用例的方式是在用例脚本里面给用例添加装饰器(@pytest.mark.skip),解除跳过时将装饰器代码删掉,这种方式需要修改用例代码,而通过 csv 文件来管理跳过用例则会方便很多;

将跳过用例操作也整合进入用例标签,在 csv 文件中新增一列为“跳过原因”;

3.1.1. 固定跳过

示例:

脚本ID ...(各种用例标签) 跳过原因
679537 ... skip-受到某新需求影响
  • 如果应用受到新需求影响需要跳过,则在此列备注具体的跳过原因。跳过的原因统一标签开头为 “skip-XXX”;
  • 用例执行时判断 csv 文件里面跳过原因列是否存在跳过标签,存在跳过标签则用例也不会被执行,最终的用例状态会被标签为 SKIPED
3.1.2. 条件判断跳过

示例:

脚本ID ...(各种用例标签) 跳过原因
001 ... skipif_platform-aarch64&mips64
  • 某些用例会因为不同的环境判断用例是否执行,常见的场景为在不同架构上判断是否执行,跳过的原因标签为 “skipif_platform-” 加架构名,多个架构之间使用 “&” 拼接;
  • 以上例子为用例执行时,判断当前架构是否为 arrch64 或者 mips64,若是,则跳过用例不执行,若否则执行用例;

目前框架默认仅支持架构的条件判断,但条件判断跳过逻辑支持自定义添加。

在项目目录路径下存在文件 setting/skipif.py所有条件判断跳过的函数写在此文件中。

默认架构判断例子:

def skipif_platform(args: str):
    """平台跳过"""
    _skip_key = args.split("&")
    for key in _skip_key:
        if GlobalConfig.SYS_ARCH == key:
            return True
    return False

方法编写规范:

  • 方法名必须以 skipif 开头;
  • 方法必须有返回结果并且为布尔值(True 代表跳过,False 代表不跳过);
  • 方法只能有一个入参;

csv 文件跳过原因一栏中填写为 “{函数名}-{参数}”,例如:skipif_platform-aarch64;在用例收集阶段会以第一个 “-” 进行分割,截取的左侧字符串作为函数名,在 skipif.py 文件中查找是否有同名函数,并将截取的右侧作为参数传递给该函数,通过获取该函数返回的布尔值,返回 True则用例不执行返回 False则执行该用例。

若函数需要多个参数,可自定义多个参数之间的连接符,连接符号不可使用下划线和逗号,推荐统一使用 & 符号。

3.2. 确认修复

针对于某些用例修复后,但不能立即删除跳过原因(skip-XXX)的用例,新增一列标签名为 “确认修复”,作为标记该用例是否已经修复,固定填入字段为 “fixed-已修复”。这样这条用例即使同时标记了 skip-XXX 也会正常执行。

示例:

用例ID ...(各种用例标签) 跳过原因 确认修复
679537 ... skip-受到某新需求影响 fixed-已修复

【同时标记了skip 和 fixed但仍然想要跳过用例】

当 “跳过原因” 和 “确认修复” 中同时填入后,命令行传递参数 --ifixed yes,则代码不会执行该条用例。

python3 manage.py run --ifixed yes

看到这里有些同学可能要问了,我想恢复跳过执行,直接把 skip-XXX 这一列标签删掉不就好了,还搞什么确认修复干啥?

这里给各位看官稍微解释一下:

(以下流水线指的是每日构建的流水线,跑 AT 的全量用例)

首先,流水线上跑的是 AT 历史 Tag跳过用例的标签(skip-XXX) 是在最新的代码上提交的,我们采用最新的 csv 文件覆盖历史 csv 文件的设计来实现了对历史 Tag 上用例的跳过;

然后,在日常跳过用例的过程中,同时也在修复一些用例,修复后的这些用例在本地调试的时候我们不希望继续跳过,但是此时,修复的这些用例可能还不稳定,不适合马上放到流水线去跑,也就是说流水线上我们是希望他继续跳过的,因此,咱不能直接把 skip-XXX 干掉;

这里就矛盾了,一个需求是想修复了立马解除跳过,另一个需求又不想修复了立马解除跳过,怎么办呢?

我们使用“确认修复”来标记这条用例已经修复了,这样你本地调试用例的时候这条已修复的用例是会执行的,同时在流水线上将 --ifixed yes 参数加上,那么流水线上执行时这条用例仍然是跳过的状态,后续你打 Tag 的时候,把 “跳过原因” 和 “确认修复” 中的标签全部删掉就可以了。

这就是“确认修复”这个标签的背景,需要各位看官稍微品一品。

3.3. 废弃用例

针对某些用例,由于需求变更,环境影响或评估不再适用于自动化测试时,用例需要废弃,则新增一列标签名为 “废弃用例”,该列存在 “removed-{废弃原因}”,则用例不会执行。

用例ID ...(各种用例标签) 跳过原因 确认修复 废弃用例
679537 ... skip-受到某新需求影响 fixed-已修复 removed-已废弃

4. 设计思路

上面介绍 Pytest 框架提供的标签功能 mark使用时需要为每一个用例添加标签装饰器则操作复杂可维护性差其根本问题就是标签分散在每一条用例的装饰器上难以集中维护于是乎将所有标签使用 csv 文件进行集中管理,并通过 Pytest 的钩子函数,读取 csv 文件,动态添加标签到用中。

5. CSV文件格式

此配置文件需要维护大量的标签数据,且要方便能使用 Excel 打开进行编辑查看更重要的是我们不想引入三方依赖CSV 文件几乎是唯一能满足所有的要求的文件格式。

标签自动同步

1. 自动同步脚本ID到CSV文件

支持自动同步脚本ID用例 py 文件的 ID)到 CSV 文件;

【使用方法一】

配置文件方式,通过一下几个配置来控制:

[csvctl]
;将py文件的case id同步到csv文件
;yes, 开启同步
;no, 关闭同步
PY_ID_TO_CSV = yes

如果不存在 CSV 文件会直接创建一个并写入用例脚本的ID

此功能默认会将 CSV 文件中多余的ID行删掉以处理人工删除了用例脚本文件CSV 文件里面对应的ID 行未删除的问题;

【使用方法二】

命令行参数的方式:

youqu manage.py csvctl -p2c

不管配置文件是否配置,通过命令行参数的方式执行优先级总是最高的。

2. 从PMS自动同步标签信息

用于自动同步 PMS 用例标签数据至本地 CSV 文件;

【使用方法一】

配置文件方式,通过以下几个配置来控制:

APP_NAME =  # 这个参数可填可不填,但是填了可以提高用例的执行速度,因为在用例收集阶段可以指定到具体的应用库。(下同)
PMS_USER =  # PMS的用户名
PMS_PASSWORD =  # PMS的密码

[pmsctl-pms_link_csv] 节点下指定 CSV 文件名与 PMS 用例模块的对应关系,比如:

[pmsctl-pms_link_csv]
;同步PMS数据到本地CSV文件必须要配置的配置项
;key是本地CSV文件的文件名称;
;value是对应PMS上的模块ID;
;比如要同步音乐的数据, 首先需要将配置 APP_NAME = deepin-music
;CSV文件名称为music.csv其在PMS上的音乐用例库的URL为: https://pms.uniontech.com/caselib-browse-81.html
;因此应该配置为: music = 81
;这样才能将PMS与本地CSV文件建立联系。
;如果你的应用分了很多模块,只需要将对应的信息依次配置好就行了。
music = 53

将以上信息配置好之后,在命令行执行:

youqu manage.py pmsctl --p2c

每次执行时原 CSV 文件会自动备份在 report 目录下,因此你不用担心脚本执行导致你的数据丢失。

【使用方法二】

按照我们一贯的风格,你也可以不去管配置文件,完全通过命令行参数传入:

youqu manage.py pmsctl --p2c -u ut00xxxx -p you_password -pls music:81

导出 CSV 文件

框架提供导出指定标签用例的功能:

youqu manage.py csvctl -a deepin-album -t CICD -ec case_list.csv

表示导出 deepin-album 的用例中标记了 CICD 标签的用例,导出 CSV 文件的字段格式已经适配了 CICD 的要求。

日志系统

1. 背景

基于 YouQu 自动化测试框架操作方法封装写法,通常是这样的:类里面一个函数只包含一个操作或多次调用的一系列可合并的操作;

传统的日志输出方式,需要在每个函数里面主动编写日志输出代码 ,例如:

class XXXWidget:
    
    def click_xxx(self):
        """点击某个元素"""
        logger.info("点击某个元素")
        ...
        # 省略代码部分

一个函数里面至少包含一条日志信息,如果比较复杂操作步骤,可能包含多条日志信息;

一个应用含有大量的操作函数,也就是说我们需要写大量的日志代码;

通过经验观察,我们发现,函数说明以及函数操作日志,具有较高的重复度(从上面的例子也可以看出来),因此我们大胆的设想,能不能基于框架执行时,自动的将函数说明作为日志打印出来,从而减少大量日志代码量和重复编写,那真是妙啊~。

基于此“天真”的想法,我们设计出了 YouQu 的日志系统。

2. 实现原理

核心原理:

通过给类加上一个装饰器 @log,动态给该类下所有的函数及父类函数添加日志装饰器 @_trace,在此装饰器中我们能动态的捕获所调用函数的功能说明(func.__doc__),通过日志输出器将其输出即可。

实现步骤:

  1. 通过 inspect.getmembers 获取被装饰的类下所有函数,包含静态方法,类方法,实例方法;
  2. 通过 setattr(类, 方法, _trace 的方式,给符合条件的函数,动态添加日志装饰器;

3. 日志配置

[log_cli]
;日志相关配置(不打印构造函数和魔法函数的功能说明)
;批量执行时,终端输出的日志级别 DEBUG/INFO/ERROR
LOG_LEVEL = INFO

# ============= 自动输出日志的配置 ================
;支持类名以 xxx 开头的,自动将函数说明打印为日志, 多个参数以逗号隔开
CLASS_NAME_STARTSWITH = Assert

;支持类名以 xxx 结尾的,自动将函数说明打印为日志,多个参数以逗号隔开
CLASS_NAME_ENDSWITH = Widget

;支持类名包含 xxx 的,自动将函数说明打印为日志,多个参数以逗号隔开
CLASS_NAME_CONTAIN = ShortCut
# ==============================================

4. 使用方法

在应用库 widget 方法库里面,只需要在出口文件加上类装饰器,就可以实现自动输出日志了;

# dfm_widget.py
from src import log

@log
class DfmWidget():
    """文管方法库出口"""
    
    def click_xxx(self):
        """点击某个元素"""

注意,是类装饰器。

实例:

# dfm_widget.py
from src import log

@log
class DfmWidget():
    """文管方法库出口"""
    
    @classmethod
    def find_dfm_image(cls, *elements, multiple=False, rate=0.9):
        """
          查找图片 {{elements}}.png 在屏幕中相似度大于 {{rate}} 的坐标
        :param elements:
        :param multiple:
        :return:
        """
        element = tuple(map(lambda x: f"{Config.PIC_RES_PATH}/{x}", elements))
        return cls.find_image(*element, multiple=multiple)

调用方法时:

DfmWidget.find_dfm_image("dfm_001")

自动输出的日志:

>> x86_64-uos-6: 02/28 17:48:47 | INFO  | logger_utils: [find_dfm_image]: 查找图片 dfm_001.png 在屏幕中相似度大于 0.9 的坐标

加上装饰器 @log 之后,方法层所有的函数(包括实例方法、类方法、静态方法)都不需要再写日志代码,执行的时候会自动输出,并且所有调用的方法会以 allure.step 的方式,展示在 allure 报告中,在报告中更能一目了然的看到用例执行步骤。

【日志模板语法】

通过观察前面的例子,细心的同学可能还发现了一个小细节:函数功能说明中,两对大括号中的内容 {{elements}} 作为变量输出出来了;

没错,这就是我们参考Djangojinja2 的模板语法设计出的日志模板语法,使用方法很简单,用两对大括号把函数的参数括起来,这样在日志输出的时候就能把调用函数时参数的值输出出来。

PMS数据回填

测试单关联的用例,自动化测试对应的去跑这些关联的用例,并且将执行的结果回填的测试用例的状态里面。

PMS 数据回填主要有三种方式:

1异步回填

在用例执行的过程中,采用异步的方式去进行数据回填,直白的讲就是,第二条用例开始跑的时候,通过子线程去做第一条用例的数据回填,如此循环,直到所有用例执行结束;

这种方案的时间效率最高的,因为理论上用例的执行时间是大于数据回填的接口请求时间的,也就是说,当用例执行完之后,数据回填也完成了。

使用方法,在 globalconfig.ini 里面配置以下参数:(以下涉及到的参数配置都是在配置文件里面进行配置)

PMS_USER = PMS账号
PMS_PASSWORD = PMS密码
SEND_PMS = async
TASK_ID = 测试单ID
TRIGGER = auto
APP_NAME = 这个参数可填可不填,但是填了可以提高用例的执行速度,因为在用例收集阶段可以指定到具体的应用库。(下同)

2用例执行完之后回填

等所有用例执行完之后,再逐个进行回填的接口请求,此方案时间效率比较低。

使用方法:

PMS_USER = PMS账号
PMS_PASSWORD = PMS密码
SEND_PMS = finish
TASK_ID = 测试单ID
TRIGGER = auto
APP_NAME = 

3手动回填

所有用例执行完之后不做回填的接口请求,后续手动将结果进行回填请求。

用例执行时配置:

PMS_USER = PMS账号
PMS_PASSWORD = PMS密码
SEND_PMS = finish
TASK_ID = 测试单ID
TRIGGER = hand
APP_NAME = 

后续手动回填方法:

youqu manage.py pms --send2task yes

【可能遇到的“严重问题”】

有同学可能会发现,怎么回填一次之后,后面想再次回填就不生效了;

这是因为为了应对前面提到的多种数据回填的方式,在 report 目录下会有 pms_xxx 开头的目录,用于记录了用例的执行结果和回填情况,如果这条用例之前已经回填过了,后续就不会再此触发回填了;

如果你想重新做回填,你可以把 report/pms_xxx 目录删掉,这样就可以重新做数据回填了;

环境部署

1. 原则

YouQu 的环境依赖一直坚持 2 个原则:

  • 最小环境依赖原则

有些同学写一些功能,首先想到的就是去搜索引擎搜一下,看有没有现成的工具或者代码直接能用,美其名曰不要重复造轮子,别人已经造好了轮子咱们就不要重复去造了,然后一切似乎都变得那么的理所应当。

当然这样做无可厚非,我相信有这样想法并且一直践行这样做法的同学不在少数,但这样做有一个很严重的问题,就是当你写一个大型项目时,你会引入非常非常非常多的依赖,比如要做个接口请求要用 requests、读写个数据用 pandas、写个表格用 pyopenxl、解析个文本要用这个那个三方库 ... 。

我认为这是非常不好的习惯,在实现一个功能的时候,首先我们应该去想自己怎么样去实现,尽量使用标准库去做;

如果你不会,你可以去学习三方库的实现思路,别人三方库也是用标准库实现的,为什么咱们不行呢;如果别人的你实在是看不懂,想尽一切办法确实做不出来,那再考虑引入这个三方库,我认为这样是没问题的,毕竟咱们段位还不够,但希望将来有一天我们可以。

这样的做法才应该是“不要重复造轮子”正确的操作。不会没关系,看看别人是怎么实现的,自己再摸索着写出来,这样自己才能有所提高,不然你就只会用别人的东西;那些嘲笑、批评我们重复造轮子的人,先想想自己会造轮子吗。

本着自己实现能实现的一切的原则YouQu 框架做到了非常少的环境依赖,而且我们仍在不断努力,减少环境部署的依赖。

  • 最小仓库体积原则

大文件不能上传到工程里面,保持整个工程的轻量化,这样在克隆代码的时候才能非常快速方便使用,我们见过一个 AT 工程 clone 大小达到好几个 G简直不可思议。

其实代码文件的大小是很小的,也就是说,纯写代码随便写工程也不会太大,而文档(包含插图)等资源却是很占空间。

因此我们选择将 YouQu 的文档工程涉及到的图片资源都采用外链加载;

2. 安装

项目根目录下运行 env.sh 即可。

bash env.sh

注意,如果你的测试机密码不是 1 ,那你需要在全局配置文件 globalconfig.ini 里面将 PASSWORD 配置项修改为当前测试机的密码。

3. 定制依赖

3.1. 新增依赖

如果应用库还需要其他 Python 依赖库,只需要在应用库根目录下保存一个 requirement.txt 文件;

autotest_xxx
├── requirement.txt
├── case	
...

里面写入需要安装的三方依赖,比如像这样:

# requirement.txt
PyYAML==6.0  # 指定安装某个版本
requests  # 未指定版本则安装最新版

在执行 bash env.sh 时会一并将其安装。

如果多个应用库都存在 requirement.txt 文件,执行 env.sh 时会将多个 requirement.txt 文件一并加载;那么一定要注意多个 requirement.txt 文件可能存在相同的依赖被指定安装不同版本等等兼容性问题。

3.2. 裁剪依赖

在某些情况下,可能你只需要安装一些最最基础的依赖,其他的都不需要,比如纯接口自动化的项目,它不需要 UI 自动化相关的依赖。

你只需要在应用库根目录下,存放一个 BASICENV 的普通文件,里面不需要写任何内容,这样执行 env.sh 时,只会安装最基础的依赖。

autotest_xxx
├── BASICENV  # 可以使用touch BASICENV 创建文件
├── case	
...

基础依赖仅安装以下几个包:

pytest # pytest
pytest-rerunfailures # 失败重跑插件
pytest-timeout # 用例超时插件
allure-pytest # 生成原始报告文件插件

裁剪依赖新增依赖 是不冲突的,可以同时使用。

此方案默认为虚拟环境部署,如果你想直接部署在本机上:

bash env_dev.sh

4. 虚拟化部署

YouQu默认采用虚拟化部分虚拟化环境实际安装的位置是在 $HOME/.local/share/virtualenvs/youqu-oHTM7l7G 目录下

youqu-oHTM7l7G 此目录名称前面部分是你的代码根目录的名称,后面部分是生成的随机字符串,同学们在部署的时候随机字符串肯定和我这里的例子不一样;

同学们在远程机器上定位问题的时候,如果使用 Pycharm 调试,就将解释器指定到这个目录的就行了;

当然,我们也提供了本机开发环境部署:

bash env_dev.sh

如果你是本地开发环境可以用它,区别就是驱动执行的时候使用:python3 manage.py xxx

失败录屏

录屏其实是一种视频形式的日志,因为很多时候我们在查看日志之后仍然无法准确的定位到用例失败的具体原因,因此用例的录屏能让我们看到用例在执行过程;

【使用方法】

globalconfig.ini 里面配置关注 2 个参数;

;失败用例重跑次数
RERUN = 1

;失败录屏从第几次开始录制视频。
;比如 RECORD_FAILED_CASE = 1 表示用例第1次执行失败之后开始录屏。
;注意,用例失败重跑的次数不能小于失败录屏次数,即 RERUN >= RECORD_FAILED_CASE
RECORD_FAILED_CASE = 1

默认失败重跑的次数是 1 次;

RECORD_FAILED_CASE = 1 表示用例第1次执行失败之后开始录屏

RECORD_FAILED_CASE = 0 表示不管用例成功或失败都录屏;

录屏是要占用系统资源的,特别是在一些配置较低的机器上会很明显,所以你需要考虑是否有必要每条用例都录屏;

使用 manage.py 执行用例默认读取到 globalconfig.ini 里面的参数,执行完之后会在 report/record 目录下保存失败用例的录屏和断言的那个时间点的截屏,你可以通过视频文件看到失败用例执行过程都发生了什么。

Wayland 适配

Wayland 下自动化主要问题是 X11 下的键鼠操作方法无法使用,比如 XdotoolPyAutoGUIXwininfo 等等;

YouQu 在 Wayland 下兼容适配,env.shWayland 下执行时会安装自研的键鼠操作服务(可能存在一些依赖报错,按照注释解决即可),框架核心库也针对性的做了适配,上层用例完全不用关心机器是Wayland 还是 X11,框架会根据执行时状态自动判断走不同的逻辑;

简单讲就是,应用库只需要维护一套用例脚本即可。

【用例兼容】

因为 Wayland 下有些应用的界面显示和功能本身存在一些差异,用例层可能需要对这部分用例做逻辑判断,使用全局配置里面的常量进行逻辑编写即可:

from setting.globalconfig import GlobalConfig

# GlobalConfig.IS_WAYLAND 获取到当前的显示服务器bool
# 应用库 Config 继承 GlobalConfig
if Config.IS_WAYLAND:
    pass
if Config.IS_X11:
    pass

比如用例里面如果断言的图片不同:

if Config.IS_WAYLAND:
    self.assert_image_exist("wayland_XXX")
else:
    self.assert_image_exist("x11_XXX")

这样这条用例脚本在 WaylandX11 下都可以跑so easy 是不是?完全没必要专门拉新分支进行 Wayland 适配。

测试报告

1. 目录结构

执行时会在根目录下动态生成 report 目录,所有的报告相关的文件会统一存放在里面,示例:

/report
    ├── allure  # allure报告
    │   ├── ...
    │   └── ffb324f3-2199-4eea-8a6f-2d7e77ce1718-container.json
    ├── allure_html  # 生成的html报告
    │   ├── ...
    │   └── index.html 
    ├── allure_back  # allure报告备份
    │   └── 20221108114823
    │       ├── ...
    │       └── f8cab367-71f6-44aa-8810-b2ae5ab1d3a5-container.json
    ├── json  # json报告
    │   ├── ...
    │   └── result_deepin-music_20221109134736_1081333.json
    ├── logs  # 日志文件
    │   ├── 2022-11-10_debug.log 
    │   └── 2022-11-10_error.log
    ├── record  # 录屏
    │   └── 2022-11-09
    │       ├── ...
    │       └── 15时14分09秒_test_music_679537_2_autotest.mp4
    └── xml  # xml报告
        └── autotest_deepin_music-20221109134736.xml

默认情况下同时生成 html、xml、json三种形式的报告。

2. 定制报告

我们对 allure 报告进行了一系列的定制:

  • 定制 logo、title、报告默认语言为中文
  • 加入了用例断言失败时的屏幕截图,以及此时图像对比的模板图片;
  • 加入了失败录屏,你可以在 html 报告中直接看录制的视频;
  • 加入了 IP 地址、系统信息、镜像版本等;

3. 查看报告

  • 本地执行

report/allure 目录下会生成一堆文本文件,这些是 allure 插件生成的报告源数据,我们在 report/allure_html 目录下给你生成了 html 文件,但是你不能直接通过浏览器打开 index.html 文件,因为 allure 的报告都是基于在线的服务,你应该这样打开:

allure open report/allure_html

使用 manage.py 执行完之后,在终端的最后一行,我们会给你输出打开报告的命令,直接复制它在终端执行就可以查看了,像这样:

  终端执行命令: allure open /tmp/youqu/report/allure_html 查看报告 

你也可以直接在 Pycharm 里面找到 index.html 文件,然后右键选择浏览器打开,或者你可以用一个 http 服务打开。

你也可以直接在 report/allure 目录下查看报告:

  allure serve report/allure
  • 远程执行

远程执行结束之后,会将所有远程测试机的测试报告都收集到 report/allure 目录下,分别按照机器的 IP 等建了不同的目录,你可以在这些目录里面去查看对应的测试报告,查看方法和前面本地执行查看的方法一样。

静态代码扫描

1. 提前解决代码问题

为了帮助开发者统一代码风格,Python 社区提出了 PEP8 代码编码风格,Python 官方同时推出了一个检查代码风格是否符合 PEP8 的工具,名字也叫 PEP8

但是,Pycharm 里面的 PEP8 插件实际上并不能发现很多代码问题,这些问题并不会在运行时报错,因为从 Python 语言角度并不关心这些问题,在 Pycharm 编辑器里面使用快捷键 ctrl + alt + L,有一点点格式化的效果,能解决一些空格、换行等小问题。

这里推荐一个 Python 社区流行的代码格式化工具:

Black,号称不妥协的代码格式化工具,它检测到不符合规范的代码风格直接就帮你全部格式化好,就是这么刚!

安装:

sudo pip3 install black 

使用方法:

black ${CheckPath}

使用这个工具格式化之后,代码会被自动调整,刚开始你可能会觉得调整得很夸张,没关系坚持看,习惯之后,你会觉得很优雅,没错,这就是 Pythonic Code 的核心,请保持优雅~。

2. 代码扫描工具

使用根目录下 pylint.sh 扫描代码,在 report 目录下查看代码扫描报告,如果有代码问题请提前解决之后再提交。

此脚本已经使用 Python 社区最流行的代码扫描工具 Pylint 进行代码扫描。

使用方法:

bash pylint.sh

运行之后会提示你要扫描的目录,比如输入 apps ,则会扫描 apps 下的所有 Python 代码。

setting/pylintrc.cfg 配置文件里面进行相关配置,扫描完成之后在 report/pylints 目录下会生成扫描报告。

代码提交需通过 git review 提交到 gerrit ,人工 Code Review 通过之后合入代码。

提交代码

1. 安装依赖

sudo apt install git-review

2. 提交模板

~ 目录下新建文件,并命名为 gitcommit_template

将以下内容写入文件当中:

# commit type :fix问题修复、feat功能开发、style(风格修改)、refactor(重构)、docs文档、chore其他)、test(测试) + 简单描述. 默认fix,根据情况修改
fix: 

# 详细说明代码的改动,包含代码的实现思路,以及为什么这么做,可能会影响哪些功能。对于代码的审核者,需要从这段描述中能完全理解代码中所有改动的内容
Description: 

# 写一段面向于产品的总结性内容用于自动生成crp上的changlog需要注意的事这段描述必须从产品的角度考虑。
Log: 

# 关联pms上的bug号提交后则会自动在pms对应bug信息页面添加备注关联本次提交。若本次提交为修复bug相关则请取消注释
#Bug: 

# 修复 github issue 
#Issue: 

# 关联pms上的任务号提交后则会自动在pms对应任务信息页面添加备注关联本次提交。若本次提交为任务相关则请取消注释
#Task: 

命令行执行:

git config --global commit.template ~/gitcommit_template

此命令将模板加入到 git 的提交模板中。

后续提交的时候需要关注一下几点:

  • commit type 对应不同的修改类型:fix(问题修复)、feat(功能开发)、style (风格修改)、refactor (重构)、docs(文档)、chore(其他)、test (测试)
  • commit type 冒号后面加空格
  • Description 必要的情况下需要进行详细说明,比如对功能进行大改等。

3. 推送代码

首先添加 commit 信息

git add 后面加文件名称
git add . # 表示添加所有文件

使用这条命令注意有些临时文件不要提交到仓库了。

git commit -a

之后在 fix: 后面(注意冒号后面必须加空格,不然直接-1写本地提交的 commit 信息,然后就可以提交代码了

git review branch(当前分支) -r origin

最好一次 commit,一次 git review ,经常有同学在本地疯狂 commit 最后 git review 报错不知道咋回事儿。 若不想每次提交都加上 -r 选项,执行以下命令:

git config --global gitreview.remote origin 

提交代码,直接使用 git review branch<目标分支>,例如 git review master

4. Git标签

Git 标签实际上是 commit 的一个别名,主要用于管理历史版本。

想象一下领导让你把之前的某个版本的代码拉出来跑,你是不是乖乖的去把所有提交记录拉出来找,然后找到那个哈希值,通过哈希值来检出代码。

这一套操作下来是不是很麻烦,费时间还不一定能找得到,没人能记住每个提交哈希值以及这次提交都改了哪些代码。

因此给提交打个标签,就是把哈希值转换成人类能理解的语言,比如 v1.0,就可以直接通过标签找到对应的提交。

  • 新建标签
git tag -a <tagname>

回车之后Git 会打开你的编辑器,让你写一句标签注解,就像你给提交写注解一样。

  • 查看所有标签
git tag -l
# git tag 也行
  • 删除标签
git tag -d <tagname>
  • 推送标签至远程

git push 命令并不会传送标签到远程仓库服务器上,必须显式地推送标签。

git push origin <tagname>

推送所有不在远程仓库服务器上的标签

git push origin --tag
  • 删除远程标签
git push origin :refs/tag/<tagname>
  • 后期打标签

先用 git log --pretty=oneline 查询提交记录,获取校验码。

git tag -a <tagname> 9fceb02(校验码)
  • 检出标签
git checkout <tagname>

此时,仓库会处于“分离头指针(detached HEAD)”的状态。

在“分离头指针”状态下,如果你做了某些更改然后提交它们,标签不会发生变化, 但你的新提交将不属于任何分支,并且将无法访问,除非通过确切的提交哈希才能访问。

因此,如果你需要进行更改,比如你要修复旧版本中的错误,那么通常需要创建一个新分支:

git checkout -b <NewBranchName> <tagname>

新分支上可以进行提交代码。

常见问题

【提交代码时提示邮箱或者名称不对】

重新配置邮箱或者名称,然后重置生效:

git commit --amend --reset-author

【怎么回滚到之前的版本】

(1)查询历史提交记录

git log

找到你要回滚的版本,复制 hash 值。

  • 注意:是 commit 空格之后的 hash 值,之前有同学复制的 Change-Id: 这样肯定报错。

(2)回滚版本,不清除代码

git reset --soft ${hash}

(3)回滚版本,清除代码,慎用哈

git reset --hard ${hash}

【解决 git status 中文显示的问题】

git config --global core.quotePath false

apps 目录下颜色有些是黄色的】

Pycharmapps 目录下应用库文件是黄色的,编辑器识别不到代码新增和修改;

由于社区版 Pycharm 不能动态识别多仓库,需要在 setting 里面手动注册,操作步骤:

File —> Settings —> Version Control —> 点 + 号 —> Directory 选中应用库工程目录 —> VCS 选中 Git —> Apply

如此就可以了。

专业版 Pycharm 不存在这个问题。

【执行 env.sh 报错 $'\r':未找到命令

出现这个问题你应该是在 windows 上打开或编辑过 env.sh 脚本windows下的换行是回车符+换行符,也就是\r\n,而 Linxu 下是换行符 \nLinux 下不识别 \r,因此报错。

解决方案:

# 将 \r 替换为空
sudo sed -i 's/\r//' env.sh

【怎样为单独某一条用例配置执行超时时间】

在用例脚本中添加装饰器,如下:

@pytest.mark.timeout(300) # 单位秒
def test_xxx_001():
	pass

【如何修复子仓库 master 分支游离头detached head

修复所有子仓库默认master 分支游离头

cd  youqu
git submodule foreach -q --recursive 'git checkout $(git config -f $toplevel/.gitmodules submodule.$name.branch || echo master)'