parent
b2cc2177f1
commit
6555c23b65
|
@ -17,7 +17,6 @@
|
|||
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/youqu?color=%23F79431)
|
||||
![Static Badge](https://img.shields.io/badge/UOS%2FDeepin/Ubuntu/Debian-Platform?style=flat&label=OS&color=%23F79431)
|
||||
![Static Badge](https://img.shields.io/badge/Linux-Platform?style=flat&label=Platform&color=%23F79431)
|
||||
![Built with Material for MkDocs](https://img.shields.io/badge/Material_for_MkDocs-526CFE?style=flat&logo=MaterialForMkDocs&logoColor=white&color=%23F79431)
|
||||
|
||||
[![Downloads](https://static.pepy.tech/badge/youqu/week)](https://pepy.tech/project/youqu)
|
||||
[![Downloads](https://static.pepy.tech/badge/youqu/month)](https://pepy.tech/project/youqu)
|
||||
|
@ -32,9 +31,7 @@
|
|||
|
||||
<a href="https://github.com/linuxdeepin/deepin-autotest-framework" target="_blank">GitHub</a> / <a href="https://gitee.com/deepin-community/deepin-autotest-framework" target="_blank">Gitee</a>
|
||||
|
||||
<a href="https://linuxdeepin.github.io/deepin-autotest-framework" target="_blank">在线文档</a> / <a href="https://deepin-community.gitee.io/deepin-autotest-framework/" target="_blank">在线文档(国内加速)</a> / <a href="http://youqu.uniontech.com/" target="_blank">在线文档(公司内网)</a>
|
||||
|
||||
<a href="http://youqu.uniontech.com/docs" target="_blank">知识库(公司内网)</a> / <a href="http://youqu.uniontech.com/tool" target="_blank">工具库(公司内网)</a>
|
||||
<a href="https://linuxdeepin.github.io/deepin-autotest-framework" target="_blank">在线文档</a>
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import timeline from "vitepress-markdown-timeline";
|
|||
// https://vitepress.dev/reference/site-config
|
||||
export default withMermaid(
|
||||
defineConfig({
|
||||
// base: '/docs/',
|
||||
// base: '/docs/',
|
||||
lang: 'zh-CN',
|
||||
title: "YouQu | Linux自动化测试利器",
|
||||
description: "",
|
||||
|
@ -26,6 +26,7 @@ export default withMermaid(
|
|||
siteTitle: "官方中文文档",
|
||||
nav: [
|
||||
{text: '快速开始', link: '/快速开始'},
|
||||
{text: '功能介绍', link: '/框架功能介绍'},
|
||||
],
|
||||
search: {
|
||||
provider: 'local'
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
# AT 基础框架设计方案
|
||||
|
||||
```shell
|
||||
# ====================================
|
||||
# Author : mikigo
|
||||
# ====================================
|
||||
```
|
||||
|
||||
## 一、目标
|
||||
|
||||
做一个简单、好用、功能强大的自动化测试框架。
|
||||
|
||||
- 作为自动化测试的一个基础框架,任何应用都能很方便的接入进来;
|
||||
- 提供所有自动化测试所需要的底层功能,并对外提供唯一的接口,方便应用库使用;
|
||||
- 整合公共库,减少应用库代码量和方法维护。
|
||||
|
||||
## 二、设计方案
|
||||
|
||||
### 1、统一概念
|
||||
|
||||
- **基础框架**
|
||||
|
||||
本项目是一个自动化测试的基础框架(`AutoTest Basic Frame`),全称为“自动化测试基础框架(AT 基础框架)”。
|
||||
|
||||
AT 基础框架是做为一个自动化测试的基础框架来设计的,它提供了自动化测试所要用的基础功能,任何应用接入进来都能快速方便的进行方法和用例编写,同时在用例标签化管理、批量跳过、标签化执行、分布式执行、多种报告输出、优美的执行日志、失败自动录屏和截图、自动环境清理等等,它都提供了完善的解决方案,更重要的是这一切使用起来都非常简单。
|
||||
|
||||
基础框架中包含应用库(`apps`)、核心库(`src`)、公共方法库(`public`)。
|
||||
|
||||
- **应用库(`apps`)**
|
||||
|
||||
各个应用自己的仓库,里面主要包含的是用例、方法及一些测试资源,比如文件管理器叫 `autotest-dde-file-manager` 。
|
||||
|
||||
- **核心库(`src`)**
|
||||
|
||||
核心库自动化测试所要用的到所有底层核心功能,这是最重要的一个部分,灵魂。具体功能模块及说明会在下面第四个章节详细描述。
|
||||
|
||||
- **公共方法库(`public`)**
|
||||
|
||||
多个应用都要用到的一些操作方法,按照应用划分存放,主要是为了减少代码量,消除应用间的耦合关系。
|
||||
|
||||
### 2、架构图
|
||||
|
||||
整体的框架设计在《自动化测试架构设计》文档里面已经有详细描述了,这里贴一下整体的架构图:
|
||||
|
||||
???+ note "架构图"
|
||||
![](https://pic.imgdb.cn/item/64f054c4661c6c8e54ff4948.png)
|
||||
|
||||
为了突显本文的重点,抽取其中重要功能模块,如下图:
|
||||
|
||||
???+ note "核心模块"
|
||||
![](https://pic.imgdb.cn/item/64f054c2661c6c8e54ff4770.png)
|
||||
|
||||
### 3、目录结构
|
||||
|
||||
```shell title="框架结构"
|
||||
autotest-basic-frame # 自动化测试基础框架
|
||||
├── apps # 应用库
|
||||
│ ├── autotest-dde-file-manager # 单独的应用仓库(应用库详细目录结构请看应用库设计方案)
|
||||
│ │ ├── case # 用例
|
||||
│ │ ├── config.ini # 局部配置
|
||||
│ │ ├── config.py # 局部配置
|
||||
│ │ ├── conftest.py # Pytest 本地插件
|
||||
│ │ ├── dfm_assert.py # 断言方法模块
|
||||
│ │ ├── assert_res # 断言的图片资源
|
||||
│ │ ├── tag # 标签
|
||||
│ │ └── widget # 方法库
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── base_widget.py
|
||||
│ │ ├── ui.ini # UI 定位的坐标配置文件
|
||||
│ │ ├── case_res # 测试数据(用例需要用到的资源)
|
||||
│ │ └── pic_res # 图像识别元素定位要用的图片
|
||||
│ │ ├── xxx.png
|
||||
│ │ ...
|
||||
│ ├── autotest_deepin_music # 单独的应用仓库
|
||||
│ │ ├── case
|
||||
│ │ ├── ...
|
||||
│ ...
|
||||
├── public # 公共方法库
|
||||
│ ├── __init__.py
|
||||
│ ├── dde_dock_public_widget # dde_dock 的公共方法 package
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── dde_dock_public_widget.py
|
||||
│ │ ├── ui.ini
|
||||
│ │ └── res
|
||||
│ ├── dde_desktop_public_widget # dde_desktop 的公共方法 package
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── dde_desktop_public_widget.py
|
||||
│ │ ├── ui.ini
|
||||
│ │ └── res
|
||||
│ ...
|
||||
├── src # 核心库
|
||||
│ ├── __init__.py
|
||||
│ ├── button_center.py
|
||||
│ ├── cmdtrl.py
|
||||
│ ├── dogtails.py
|
||||
│ ├── find_image.py
|
||||
│ ├── global_config.py
|
||||
│ ├── ...
|
||||
│ ...
|
||||
├── setting # 全局配置模块
|
||||
│ ├── __init__.py
|
||||
│ ├── globalconfig.py
|
||||
│ └── globalconfig.ini
|
||||
├── conftest.py # Pytest 本地插件模块(Hook)
|
||||
├── pytest.ini # Pytest 默认配置文件
|
||||
├── docs # 文档目录
|
||||
└── manage.py # 执行器
|
||||
```
|
||||
|
||||
## 三、详细方案
|
||||
|
||||
### 1、==应用库(apps)==
|
||||
|
||||
所有应用库均放置在基础框架下的 `apps` 目录下(见第二章节第3段目录结构内容)。应用库的架构设计可以参考《AT 应用库设计方案》文档。
|
||||
|
||||
### 2、==核心库(src)==
|
||||
|
||||
在 `src` 目录下为自动化测试的底层核心组件,通常来讲如果你需要使用到其中某一个功能模块,那么你需要显示的导入这个模块,然后才能使用该模块下的功能,如果你用到了十个功能模块,那你就需要导入十个。但是我们想让事情变得简单,一次导入,使用所有。
|
||||
|
||||
将所有的功能模块都进入到 `src` 的名称空间,在 `src/__init__.py` 里面我们设计成这样:
|
||||
|
||||
```python title="src/__init__.py"
|
||||
from .cmdctl import CmdCtl
|
||||
from .dogtails import Dogtail
|
||||
from .find_image import FindImage
|
||||
from .button_center import ButtonCenter
|
||||
|
||||
|
||||
class Src(CmdCtl, FindImage):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""dogtail or button center param
|
||||
:param kwargs: app_name, desc, number
|
||||
"""
|
||||
if kwargs:
|
||||
app_name = kwargs.get("APP_NAME")
|
||||
desc = kwargs.get("DESC")
|
||||
if app_name is None or desc is None:
|
||||
raise ValueError
|
||||
number = kwargs.get("number")
|
||||
number = number if isinstance(number, int) else -1
|
||||
# 对象组合
|
||||
self.dog = Dogtail(app_name, desc, number)
|
||||
self.ui = ButtonCenter(app_name, number)
|
||||
```
|
||||
|
||||
需要传递参数的采用对象组合,没有参数的使用继承,继承的类符合 `Mixin` 设计模式。
|
||||
|
||||
应用库里面使用的时候在 `widget/base_widget.py` 里面只需要唯一继承 `Src` :
|
||||
|
||||
```python title="widget/base_widget.py"
|
||||
# 应用库里面方法基类
|
||||
from src import Src
|
||||
|
||||
class BaseWidget(Src):
|
||||
"""方法基类"""
|
||||
|
||||
APP_NAME = "dde-file-manager"
|
||||
DESC = "/usr/bin/dde-file-manager"
|
||||
|
||||
def __init__(self, number=-1):
|
||||
Src.__init__(self, APP_NAME=self.APP_NAME, DESC=self.DESC, number=number)
|
||||
```
|
||||
|
||||
要使用核心库的功能,只需要写一个导入 `from src import Src` 即可。
|
||||
|
||||
写 `__init__` 构造函数的原因是通过参数构造应用,并且传递 `number` 进来,可以实现多窗口的控制。
|
||||
|
||||
### 3、==公共方法库(public)==
|
||||
|
||||
公共方法库里面每个应用都是一个单独的 `py` 文件,相互之间是独立的,每个 `py` 文件里面是该应用的方法类,比如:最常用的方法类 `dde_desktop_public_widget.py`
|
||||
|
||||
```python title="dde_desktop_public_widget.py" hl_lines="3 13"
|
||||
from src import Src
|
||||
|
||||
class _DdeDesktopPublicBaseWidget(Src):
|
||||
"""桌面公共方法基类"""
|
||||
|
||||
APP_NAME = "dde-desktop"
|
||||
DESC = "/usr/bin/dde-desktop"
|
||||
|
||||
def __init__(self):
|
||||
Src.__init__(self, APP_NAME=self.APP_NAME, DESC=self.DESC)
|
||||
|
||||
|
||||
class DdeDesktopPublicWidget(_DdeDesktopPublicBaseWidget):
|
||||
"""桌面公共方法类"""
|
||||
|
||||
def click_music_dir_by_ui(self):
|
||||
"""在文件选择框点击音乐目录"""
|
||||
self.click(*self.ui.btn_center("侧边栏-音乐"))
|
||||
|
||||
def click_xxx_by_attr(self):
|
||||
pass
|
||||
```
|
||||
|
||||
==公共方法库意义==:
|
||||
|
||||
- 公共方法库里面所封装的一些操作方法都是至少被 2 个应用都用到的,这样做可以减少整体代码量,从而减轻应用库代码的维护工作。
|
||||
|
||||
- 公共方法库里面的编写形式(命名规则、定位的方案写法、注释的写法等等)具有一定的模板作用,这样即使是各个应用库都独立维护,所有的编码风格都是趋于相同的,因为大家都应该参照公共库里面的一些写法来写自己应用库里面的一些方法类,这样使得从公司的角度去看所有应用的自动化测试项目都是统一的。
|
||||
- 可以通过公共方法库里面的一些方法,了解到其他应用的功能,对于我们需求理解,了解系统的方方面面也有好处。
|
||||
|
||||
当然,如果你的应用本身是属于根本就不需要和其他应用交互的,那么你可能不会用到这里面的功能,没关系,你所有的方法都可以直接写在应用库的业务层。
|
||||
|
||||
### 4、==conftest.py==
|
||||
|
||||
`conftest.py` 从功能上将是属于核心库(`src`)的内容,但是由于它的特殊性,即它是对应用库中的用例生效的,而且它的作用域是当前目录及以下,因此我们将它放到项目根目录。我们框架中有不少核心功能都是在这里面进行开发的,后面第四章会讲到细节。
|
||||
|
||||
`pytest.ini` 作用和 `conftest.py` 类似,是 `Pytest` 框架的固定配置文件,目前配置了一些通用的命令行执行参数,也放在项目根目录。
|
||||
|
||||
根目录下的 `conftest.py` 文件只会用来写 `Hook` 函数,`apps` 目录下的 的 `conftest.py` 文件只会用来写 `fixture` 。
|
||||
|
||||
### 5、==setting==
|
||||
|
||||
全局配置模块,包含了以下配置文件:
|
||||
|
||||
(1)`ini` 配置文件
|
||||
|
||||
主要配置一些全局的配置项,譬如:失败重跑次数、是否失败录屏、单条用例超时时间、会话超时时间、执行时日志级别、生成的报告类型、以及分布式执行的一些策略配置项等等。
|
||||
|
||||
(2)`config.py` 配置文件
|
||||
|
||||
主要提供配置文件读取、动态获取一些常量(如项目根目录绝对路径 `(BASE_DIR)`、系统架构(`SYS_FRAME`)、时间字符串(`strftime`)、本机 `USERNAME` `IP` 等等)、公共 URL 等。
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
# AT 应用库设计方案
|
||||
```shell
|
||||
# =================================================
|
||||
# Author : mikigo
|
||||
# =================================================
|
||||
```
|
||||
|
||||
## 一、目标
|
||||
|
||||
AT 应用库改造是基于自动化测试基础框架进行用例方法和业务逻辑的重新设计,以实现应用库高效的编写、维护用例及其方法。
|
||||
|
||||
## 二、方案设计
|
||||
|
||||
文件管理器从业务复杂度和用例量来讲,在系统所有应用中,是很有代表性的,难度也是最大的,因此我们选取文件管理器作为应用库改造的实验应用,给后续其他应用改造提供切实可行的思路和方案。
|
||||
|
||||
### 1、分层设计图
|
||||
|
||||
整体仍然遵循 PO 设计理念,根据业务需要,将文管业务层进行 3 层划分:
|
||||
|
||||
???+ note "应用库架构图(文件管理器)"
|
||||
![](https://pic.imgdb.cn/item/64f054c3661c6c8e54ff47db.png)
|
||||
|
||||
### 2、目录结构
|
||||
|
||||
```shell
|
||||
autotest_dde_file_manager # 应用仓库
|
||||
├── case # 用例
|
||||
│ ├── assert_res # 断言的图片资源目录
|
||||
│ ├── test_xxx_001.py
|
||||
│ ...
|
||||
├── widget # 方法
|
||||
│ ├── __init__.py
|
||||
│ ├── case_res # 测试数据(用例需要用到的资源)
|
||||
│ ├── pic_res # 图像识别元素定位要用的图片
|
||||
│ ├── base_widget.py # 方法基类
|
||||
│ ├── title_widget.py # title 模块方法类
|
||||
│ ├── right_view_widget.py # right view 模块方法类
|
||||
│ ├── left_view_widget.py # left view 模块方法类
|
||||
│ ├── pop_widget.py # pop 模块方法类
|
||||
│ ├── dfm_widget.py # 方法的统一出口
|
||||
│ └── ui.ini # UI 定位的坐标配置文件
|
||||
├── tag # 用例标签目录
|
||||
│ ├── xxx.csv # 用例标签文件
|
||||
│ ...
|
||||
├── config.ini # 局部配置
|
||||
├── config.py # 局部配置
|
||||
├── conftest.py # Pytest 本地插件
|
||||
└── dfm_assert.py # 断言方法模块
|
||||
```
|
||||
|
||||
## 三、详细方案
|
||||
|
||||
### 1、基类(base_widget.py)
|
||||
|
||||
- 继承核心层(src.Src);
|
||||
```python
|
||||
from src import Src
|
||||
|
||||
class BaseWidget(Src):
|
||||
"""方法基类"""
|
||||
|
||||
APP_NAME = "dde-file-manager"
|
||||
DESC = "/usr/bin/dde-file-manager"
|
||||
|
||||
def __init__(self, number=-1):
|
||||
Src.__init__(self, APP_NAME=self.APP_NAME, DESC=self.DESC, number=number)
|
||||
```
|
||||
- 抽取操作层的一些基础方法;
|
||||
- 元素定位操作的一些公共方法;
|
||||
- 路径组装方法;
|
||||
- 一些**业务层**相关的变量、常量、shell命令、坐标;
|
||||
|
||||
### 2、操作层
|
||||
|
||||
- 模块划分
|
||||
|
||||
按照文件管理器的界面区域划分为:==TitleWidget 、RightViewWidget、LeftViewWidget 、PopWidget== ;
|
||||
|
||||
文管界面分为四个区域:==标题栏、右边视图区域、左边视图区域、弹窗[^1]==;
|
||||
|
||||
[^1]: 设置界面弹窗、保险箱弹窗、删除确认弹窗、及各种网络弹窗.
|
||||
|
||||
???+ note "主界面区域划分"
|
||||
![](https://pic.imgdb.cn/item/64f054c3661c6c8e54ff4806.png)
|
||||
???+ note "弹窗区域"
|
||||
![](https://pic.imgdb.cn/item/64f054c8661c6c8e54ff4d1b.png)
|
||||
|
||||
|
||||
- 各个模块只继承基类
|
||||
|
||||
```python title="标题栏" hl_lines="1 3"
|
||||
from apps.autotest_dde_file_manager.widget import BaseWidget
|
||||
|
||||
class TitleWidget(BaseWidget):
|
||||
"""标题栏方法类"""
|
||||
|
||||
def click_xxx_in_title_by_ui(self):
|
||||
# self.dog.find_element_by_attr("xxxx").click()
|
||||
self.click(*self.ui.btn_center("xxx"))
|
||||
```
|
||||
|
||||
- 不同的定位方案调用不同的定位工具对象。
|
||||
|
||||
```python
|
||||
self.dog
|
||||
self.ui
|
||||
```
|
||||
|
||||
- 方法编写
|
||||
|
||||
- 动作开头,注意是动词
|
||||
|
||||
```python
|
||||
click
|
||||
double_click
|
||||
right_click
|
||||
get
|
||||
make
|
||||
```
|
||||
|
||||
- 元素对象名称
|
||||
|
||||
界面元素直接与元素名称相同,没有名称的就取一个好听易懂的名字。
|
||||
|
||||
- 加上类的关键词
|
||||
|
||||
避免方法重名,同时可以标记区域。
|
||||
|
||||
- 标定操作方法
|
||||
|
||||
```python
|
||||
by_ui
|
||||
by_attr
|
||||
by_mk
|
||||
by_img
|
||||
```
|
||||
|
||||
### 3、应用层
|
||||
|
||||
- 继承操作层的所有类。
|
||||
|
||||
- 仅仅用于用例中导入方便,不做其他事情。
|
||||
|
||||
```python
|
||||
class DfmWidget(TitleWidget, RightViewWidget, LeftViewWidget, PopWidget):
|
||||
pass
|
||||
```
|
||||
|
||||
- `DfmAssert` 直接在用例里面继承,方便使用断言语句。
|
||||
|
||||
```python hl_lines="2 4 7"
|
||||
from apps.dde_file_manager.widget.dfm_widget import DfmWidget
|
||||
from public.assert import Assert
|
||||
|
||||
class DfmAssert(Assert):
|
||||
|
||||
def assert_file_exists_in_desktop(self, file_name):
|
||||
self.assert_file_exists(f"~/Desktop{file_name}")
|
||||
...
|
||||
DfmWidget().get_file_in_desktop()
|
||||
```
|
||||
|
||||
- 用例里面直接继承,方便在用例里面使用 self 进行断言,更符合断言的使用习惯,用例逻辑上更清楚。
|
||||
|
||||
```python hl_lines="1 3" title="case/base_case.py"
|
||||
from apps.autotest_dde_file_manager.dfm_assert import DfmAssert
|
||||
|
||||
class BaseCase(DfmAssert):
|
||||
pass
|
||||
```
|
||||
|
||||
```python hl_lines="1 3 5" title="case/test_xxx_001.py"
|
||||
from apps.autotest_dde_file_manager.case import BaseCase
|
||||
|
||||
class TestFileManager(BaseCase):
|
||||
def test_xxx_001(self):
|
||||
self.assert_file_exists_in_desktop("xxx")
|
||||
```
|
||||
|
||||
### 4、逻辑举例
|
||||
|
||||
用例代码调用逻辑举例:
|
||||
|
||||
```python title="widget/base_widget.py"
|
||||
class BaseWidget(Src):
|
||||
"""方法基类"""
|
||||
APP_NAME = "dde-file-manager"
|
||||
DESC = "/usr/bin/dde-file-manager"
|
||||
|
||||
def __init__(self, number=-1):
|
||||
Src.__init__(self, APP_NAME=self.APP_NAME, DESC=self.DESC, number=number)
|
||||
```
|
||||
|
||||
```python title="widget/title_widget.py"
|
||||
from apps.autotest_dde_file_manager.widget import BaseWidget
|
||||
|
||||
class TitleWidget(BaseWidget):
|
||||
|
||||
def __init__(self, nubmer=-1):
|
||||
BaseWidget.__init__(self, nubmer=nubmer)
|
||||
|
||||
def click_xxx_title_by_ui(self):
|
||||
print(self.dog.app_name)
|
||||
self.ui.print_number()
|
||||
```
|
||||
|
||||
```python title="widget/right_view_widget.py"
|
||||
from apps.autotest_dde_file_manager.widget import BaseWidget
|
||||
|
||||
class RightViewWidget(BaseWidget):
|
||||
|
||||
def __init__(self, nubmer=-1):
|
||||
BaseWidget.__init__(self, nubmer=nubmer)
|
||||
|
||||
def click_xxx_right_by_ui(self):
|
||||
print(self.dog.app_name)
|
||||
self.ui.print_number()
|
||||
```
|
||||
|
||||
```python title="widget/dfm_widget.py"
|
||||
from apps.autotest_dde_file_manager.widget import TitleWidget
|
||||
from apps.autotest_dde_file_manager.widget import RightViewWidget
|
||||
|
||||
class DfmWidget(TitleWidget, RightViewWidget):
|
||||
pass
|
||||
```
|
||||
|
||||
```python title="case/test_xxx_002.py"
|
||||
from apps.dde_file_manager.widget import DfmWidget
|
||||
from apps.autotest_dde_file_manager.case import BaseCase
|
||||
|
||||
class TestDdeFileManager(BaseCase):
|
||||
|
||||
def test_xxx_002(self):
|
||||
dfm = DfmWidget()
|
||||
dfm.click_xxx_title_by_ui()
|
||||
dfm.click_xxx_right_by_ui()
|
||||
dfm.dog.print_desc()
|
||||
dfm.ui.print_number()
|
||||
```
|
||||
|
||||
## 四、工程改造实施步骤
|
||||
|
||||
### 1、基础框架代码拉取
|
||||
|
||||
1.1. 将自动化基础框架的功能拉到本地(参考《快速开始》章节)
|
||||
|
||||
1.2. 将应用库代码拉到基础框架下 `apps` 目录下,应用库的仓库命名应该是长这样的 `autotest_deepin_xxx`。
|
||||
|
||||
### 2、调整工程目录
|
||||
|
||||
按照 `方案设计-目录结构` 进行目录调整,尽量使用编辑器进行相应的调整,编辑器推荐使用 `Pycharm` ,以下操作均在 `Pycharm` 里面可实现。
|
||||
|
||||
2.1. 需要移动 `py` 文件或目录,直接在编辑器里面,使用鼠标选中并按住,之后拖动到目标位置即可,`Pycharm` 会尽可能的自动解决移动导致的路径问题。注意,我说的是“尽可能”,有些骚操作编辑器是无法自动处理的。如果没有被编辑器自动处理的路径问题,后续只能手动解决。
|
||||
|
||||
2.2. 需要重命名文件或目录,在编辑器里面选中文件,然后使用快捷键 `Shift + F6` (如果快捷键没反应,文件右键菜单 `Refactor —— Rename`),然后在输入框中输入要重命名的名称,同时,确认勾选 `Search for references` 和 `Search in comments and strings` 这两个选项,最后按回车或者鼠标点 `Refactor` 。
|
||||
|
||||
注意,此时编辑器可能会提示你,你这个重命名的操作关联了多个模块,它被多个地方都使用到了,相关的模块是否也一起改名了,这不废话吗,用这个功能就是想把关联到的都修改,不然我为什么不用文管的重命名功能呢,别想了,直接点左下角的 `Refactor` 按钮,就是干。
|
||||
|
||||
类名、函数名的重命名都尽量采用这种方案,因为编辑器会自动给我们找到关联的地方,然后同步修改掉。你可千万别直接删了修改名称,不然你可能会花上一天的时间来解决重命名的问题。
|
||||
|
||||
### 3、实现核心库接口
|
||||
|
||||
3.1. `BaseWidget`
|
||||
|
||||
在 `BaseWidget` 里面把该写的都写好,你可以参考上面的设计理念来写。
|
||||
|
||||
如果你嫌麻烦,你可以参考文件管理器的实际工程代码 `autotest_dde_file_manager` : [https://gerrit.uniontech.com/admin/repos/autotest_dde_file_manager](https://gerrit.uniontech.com/admin/repos/autotest_dde_file_manager )
|
||||
|
||||
3.2. 操作层
|
||||
|
||||
如果你是新写项目,你会发现一切都是那么的简单、直接,按照我们提供给你的接口写用例的操作方法就好了。
|
||||
|
||||
如果你是想对原来的工程进行改造,你需要按照核心库方法的调用,将你之前写的每个方案进行对应的修改,包括类和方法的命名、方法内所要用到不同定位方法的写法修改。
|
||||
|
||||
这时候你可能你的代码中可能会有一些报错,不用担心,你可以从业务逻辑出发,想清楚这个方法是干什么的、操作的对象是什么、参数是什么,注意这些修改是会影响到用例代码里面的,没关系,用例里面本来也应该被关联修改。
|
||||
|
||||
3.3. 把配置模块写好,这部分基本可以复制文管的代码。
|
||||
|
||||
### 4、路径处理
|
||||
|
||||
4.1. 导入路径
|
||||
|
||||
方法和用例中都会涉及到导入路径的修改,在修改路径时,建议你使用 `Ctrl + Shift + R` 全局替换,会将整个项目下的相同地方都修改到,当然,你也可以在小弹窗中修改全局替换为局部目录下替换。处理那种没有关联关系,但是又是相同名称的重命名,我也推荐使用这种方式进行重命名。
|
||||
|
||||
注意,全局替换的方式任然无法保证真的全局被替换了(可能是编辑器的 Bug 吧),所以你仍然需要手动看下各处是否修改到位。
|
||||
|
||||
4.2. 资源路径
|
||||
|
||||
一些用例资源需要根据 `config.py` 里面的路径配置进行资源路径的拼接,如果你原来本来就有一个函数专门用于组装路径的,那你只需要修改这一个地方就好了,如果你之前并没有这样的设计,那可能需要修改大量涉及到资源路径的地方。
|
||||
|
||||
### 5、调试和编写用例
|
||||
|
||||
以上几个步骤做完,基本就可以进行用例代码的调试了,这部分工作主要解决你之前几个步骤遗漏的问题,如果所有用例都调试通过了,那么工程改造就全部完成了。
|
|
@ -0,0 +1,535 @@
|
|||
# AT 开发规范
|
||||
|
||||
```shell
|
||||
# ================================================
|
||||
# Author : mikigo
|
||||
# ================================================
|
||||
```
|
||||
|
||||
AT 开发规范是根据自动化测试运行多年以来,遇到问题解决问题而形成的一些解决方案,或者说经验总结;
|
||||
|
||||
这些经验符合我们现阶段 AT 所应用的场景需要,也是我们经过长期思考,不断试错不断修正,并在自动化测试项目实践中检验过可行的。
|
||||
|
||||
以此,希望能帮助参与到自动化的相关人员减少试错成本,更好、更快的编写用例及维护用例。
|
||||
|
||||
## 1. 版本及依赖
|
||||
|
||||
基础框架会根据自身的功能开发进行版本迭代发布,==基础框架不与某个应用版本绑定==;
|
||||
|
||||
但是,==应用库会依赖于基础框架的版本==。因此,我们建议在 ==应用库== 目录下保存一个文本文件用于记录所依赖的基础框架版本,类似于开发应用的 `debian/control` 文件的功能,为了保持统一,这个文件就命名为 `control`,放在应用库根目录下。
|
||||
|
||||
## 2. 命名规范
|
||||
|
||||
- ==用例 ID==
|
||||
|
||||
每个应用自己维护一套 ID,可以是你自定义的 ID 值,也可以是用某些特有的 ID(比如 PMS 用例ID);
|
||||
|
||||
一个用例类里面有多个用例时,在用例名称后面加序号。
|
||||
|
||||
```python title="多用例函数命名"
|
||||
class TestFileManager(BaseCase):
|
||||
"""文管用例"""
|
||||
|
||||
def test_xxx_015_1(self):
|
||||
pass
|
||||
def test_xxx_015_2(self):
|
||||
pass
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
- ==方法函数命名==
|
||||
|
||||
???+ note "方法函数命名关键词列表"
|
||||
| 名称 | 单词 |
|
||||
| :----------------- | :----------------------------- |
|
||||
| 左键点击 | click |
|
||||
| 右键点击 | right_click |
|
||||
| 双击 | double_click |
|
||||
| 移动 | move_to |
|
||||
| 拖动 | drag |
|
||||
| 新建 | new |
|
||||
| 拖动到 | drag_to |
|
||||
| 从哪里拖动到哪里 | drag_something_from_xxx_to_xxx |
|
||||
| 获取 | get |
|
||||
| 获取某个元素的坐标 | get_location |
|
||||
| 非特殊文件 | file |
|
||||
| word文件 | doc |
|
||||
| text文件 | text |
|
||||
| 文件夹 | dir |
|
||||
|
||||
- ==常量关键词命名==
|
||||
|
||||
???+ note "常量关键词列表"
|
||||
| 名称 | 单词 |
|
||||
| :----------------------------- | :--------- |
|
||||
| 应用名称 | `APP_NAME` |
|
||||
| 应用描述 | `DESC` |
|
||||
| 本应用以外的其他应用,比如帮助 | `HELP` |
|
||||
|
||||
- ==方法层文件名==
|
||||
|
||||
???+ note "方法层文件名称列表"
|
||||
| 名称 | 单词 |
|
||||
| ---------------------------- | :----------------------------------------------------------- |
|
||||
| 方法包名 | widget |
|
||||
| 方法文件名<br />(文管举例) | `dfm_widget.py`<br />`title_widget.py`<br />`right_view_widget.py`<br />`left_view_widget.py`<br />`pop_widget.py`<br />`base_widget.py`<br />`dfm_assert.py` |
|
||||
|
||||
- ==断言语句名称==
|
||||
|
||||
???+ note "断言语句命名规范"
|
||||
断言语句都是以 assert 开头
|
||||
|
||||
| 断言 | 语句 |
|
||||
| :------------------------- | :----------------------------------------------------------- |
|
||||
| 判断文件是否存在 | assert_file_exists<br />assert_file_not_exists |
|
||||
| 判断桌面目录下文件是否存在 | assert_file_exists_in_desktop<br />assert_file_not_exists_in_desktop |
|
||||
| 判断图片存在 | assert_image_exists<br />assert_image_not_exists |
|
||||
| 判断影院中是否存在图片 | assert_image_exists_in_movie<br />assert_image_not_exists_in_movie |
|
||||
| 判断元素是否存在 | assert_element_exist<br />assert_element_not_exist |
|
||||
| 判断是否相等 | assert_equal<br />assert_not_equal |
|
||||
| 判断是否为真 | assert_true<br />assert_false |
|
||||
|
||||
## 3. Fixture 规范
|
||||
|
||||
为统一编码风格方便后续用例代码维护,现做以下规范说明:
|
||||
|
||||
- 不建议使用 `Xunit` 的写法,统一采用 `Pytest` `fixture` 的写法。
|
||||
- 应用内 `fixture` 谨慎使用 `autouse=True` ,非必要的情况下非常不建议使用这个参数。
|
||||
- 调用 `fixture` 不能使用 `@pytest.mark.usefixture()`,使用直接在用例里面传入函数对象。
|
||||
- 建议在一个 `conftest.py` 里面去写 `fixture`,一个应用也尽量维护一个 `conftest.py `文件。
|
||||
- `fixture` 也需要写功能说明,函数名称要有具体含义。
|
||||
|
||||
## 4. 方法编写&调用规范
|
||||
|
||||
### 4.1. 方法编写
|
||||
|
||||
- ==写方法的时候注意方法归属;==
|
||||
|
||||
比如文件管理器的界面区域划分为:`TitleWidget` 、`RightViewWidget`、`LeftViewWidget` 、`PopWidget`,方法是在哪个区域操作的,就写在哪个类里面。
|
||||
|
||||
举例:
|
||||
|
||||
```python hl_lines="3"
|
||||
from apps.autotest_dde_file_manager.widget import BaseWidget
|
||||
|
||||
class TitleWidget(BaseWidget):
|
||||
"""标题栏方法类"""
|
||||
|
||||
def click_xxx_in_title_by_ui(self):
|
||||
"""点击标题栏xxx"""
|
||||
# self.dog.find_element_by_attr("xxxx").click()
|
||||
self.click(*self.ui.btn_center("xxx"))
|
||||
```
|
||||
|
||||
- ==动作开头,注意是动词;==
|
||||
|
||||
```asciiarmor
|
||||
click
|
||||
double_click
|
||||
right_click
|
||||
get
|
||||
make
|
||||
```
|
||||
|
||||
- ==元素对象名称;==
|
||||
|
||||
界面元素直接与元素名称相同,没有名称的就取一个好听易懂的名字。
|
||||
|
||||
- ==加上类的关键词;==
|
||||
|
||||
避免方法重名,同时可以标记区域。
|
||||
|
||||
- ==标定操作方法类型;==
|
||||
|
||||
```asciiarmor
|
||||
by_ui
|
||||
by_attr
|
||||
by_mk
|
||||
by_img
|
||||
```
|
||||
|
||||
- ==正确使用方法类型;==
|
||||
|
||||
```python title="方法类型使用逻辑"
|
||||
if 没有用到实例对象:
|
||||
if 没有用到类对象:
|
||||
写静态方法,函数前加 @staticmethod
|
||||
else:
|
||||
写类方法,函数前加 @classmethod
|
||||
else:
|
||||
直接写实例方法
|
||||
```
|
||||
|
||||
举例:
|
||||
|
||||
```python hl_lines="2-3 6-7 10-11"
|
||||
class TitleWidget:
|
||||
|
||||
def click_xxx_by_ui(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def click_xxx_by_ui():
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def click_xxx_by_ui(cls):
|
||||
pass
|
||||
|
||||
```
|
||||
|
||||
- 函数名称尽量不出现数字,需要表示数量的用单词表示。
|
||||
|
||||
- ==函数功能注释;==
|
||||
|
||||
- 没有参数,没有返回,直接写函数功能说明;
|
||||
|
||||
```python
|
||||
"""点击某个元素"""
|
||||
```
|
||||
|
||||
- 有参数,没有返回,需要写各参数说明;
|
||||
|
||||
```python
|
||||
"""点击某个元素
|
||||
arg1:xxx
|
||||
arg2:xxx
|
||||
"""
|
||||
```
|
||||
|
||||
- 有参数,有返回,需要写返回值说明;
|
||||
|
||||
```python
|
||||
"""点击某个元素
|
||||
arg1:xxx
|
||||
arg1:xxx
|
||||
return: xxx
|
||||
"""
|
||||
```
|
||||
|
||||
用 `Pycharm` 的注释模板也可以,只要体现了参数的类型和返回就行了。
|
||||
|
||||
- 暂不要求写类型注解。
|
||||
|
||||
### 4.2. 方法调用
|
||||
|
||||
在用例中调用方法,通过该应用唯一的出口进行调用,比如文件管理器的统一出口类:
|
||||
|
||||
```python hl_lines="1"
|
||||
class DfmWidget(TitleWidget, RightViewWidget, LeftViewWidget, PopWidget):
|
||||
pass
|
||||
```
|
||||
|
||||
在用例里面只需要导入这一个类即可;
|
||||
|
||||
```python hl_lines="1 9"
|
||||
from apps.autotest_dde_file_manager.widget import DfmWidget
|
||||
from apps.autotest_dde_file_manager.case.base_case import BaseCase
|
||||
|
||||
class TestDdeFileManager(BaseCase):
|
||||
"""文件管理器用例"""
|
||||
|
||||
def test_xxx_001(self):
|
||||
"""xxx"""
|
||||
dfm = DfmWidget()
|
||||
dfm.click_xxx_by_attr()
|
||||
```
|
||||
|
||||
==尽量不要在用例中单独去调用 TitleWidget 、RightViewWidget、LeftViewWidget 、PopWidget 这些类==,否则后期用例会变得不好维护;
|
||||
|
||||
## 5. 用例编写规范
|
||||
|
||||
### 5.1. 基于类写用例
|
||||
|
||||
所有用例都应该基于类去写:
|
||||
|
||||
```python hl_lines="1"
|
||||
class TestMusic(BaseCase):
|
||||
"""音乐用例"""
|
||||
|
||||
def test_music_679537(self):
|
||||
"""音乐启动"""
|
||||
```
|
||||
|
||||
注意以下几点:
|
||||
|
||||
- ==类名不要随便取==,同一个应用应该使用同一个类名,用例类名称必须以 Test 开头,遵循大驼峰命名规范;
|
||||
|
||||
- ==用例类继承 BaseCase==,一个应用只有一个 `BaseCase` ;
|
||||
|
||||
- ==一个 py 文件里面只有一个类==,我们称为一个测试类;
|
||||
|
||||
- ==一个类里面可以有多个用例函数==,这取决这条用例有多少个测试点:
|
||||
|
||||
```python title="test_music_679537.py" hl_lines="4 7 10"
|
||||
class TestMusic(BaseCase):
|
||||
"""音乐用例"""
|
||||
|
||||
def test_music_679537_1(self):
|
||||
"""任务栏启动音乐"""
|
||||
|
||||
def test_music_679537_2(self):
|
||||
"""启动器启动音乐"""
|
||||
|
||||
def test_music_679537_3(self):
|
||||
"""桌面启动音乐"""
|
||||
```
|
||||
|
||||
### 5.2. 用例函数规范
|
||||
|
||||
- 用例函数以 test 开头,遵循蛇形命名规范,中间为用例的模块名称,后面加用例 ID,最后加测试点序号,即:
|
||||
|
||||
```shell
|
||||
test_{module}_{case_id}[_{index}]
|
||||
```
|
||||
|
||||
比如:`test_music_679537_1`,`index` 从 1 开始。
|
||||
|
||||
- ==函数功能说明里面写用例标题,直接复制 PMS 上用例标题即可,注意用三对双引号==;
|
||||
|
||||
- ==复制 PMS 用例步骤==
|
||||
|
||||
直接将 `PMS` 上用例步骤和预期复制进来,然后进行批量注释( ++ctrl+"/"++ ),在注释的基础上去写用例脚本会更加方便全面,也比你自己写注释更节约时间:
|
||||
|
||||
举例:
|
||||
???+ note "PMS用例"
|
||||
![](https://pic.imgdb.cn/item/64f054c8661c6c8e54ff4c71.png)
|
||||
|
||||
直接选中用例内容,复制下来,然后粘贴到自动化用例脚本中:
|
||||
|
||||
```python title="test_music_679537.py" hl_lines="7-12"
|
||||
class TestMusic(BaseCase):
|
||||
"""音乐用例"""
|
||||
|
||||
def test_music_679537(self):
|
||||
"""演唱者-平铺视图下进入演唱者详情页""" <-- 从PMS上复制的用例标题
|
||||
|
||||
# 1
|
||||
# 点击右上角的【平铺视图】按钮
|
||||
# 切换为平铺视图
|
||||
# 2
|
||||
# 双击任意演唱者封面
|
||||
# 进入演唱者详情页面
|
||||
```
|
||||
|
||||
上例中井号(#)注释部分就是直接从 `PMS` 上复制过来的,在此基础上写用例:
|
||||
|
||||
```python title="test_music_679537.py"
|
||||
class TestMusic(BaseCase):
|
||||
"""音乐用例"""
|
||||
|
||||
def test_music_679537(self):
|
||||
"""演唱者-平铺视图下进入演唱者详情页"""
|
||||
music = DeepinMusicWidget()
|
||||
music.click_singer_btn_in_music_by_ui()
|
||||
# 1
|
||||
# 点击右上角的【平铺视图】按钮
|
||||
music.click_icon_mode_in_music_by_ui()
|
||||
# 切换为平铺视图
|
||||
# 2
|
||||
# 双击任意演唱者封面
|
||||
music.double_click_first_singer_in_singer_icon_view_by_ui()
|
||||
# 进入演唱者详情页面
|
||||
self.assert_xxx
|
||||
```
|
||||
|
||||
你看,非常清楚每一步在做什么,重点是省去了写注释的时间,真的炒鸡方便。
|
||||
|
||||
|
||||
### 5.3. 数据驱动
|
||||
|
||||
- 如果用例操作步骤是相同的,只是一些参数变化,尽量使用数据驱动来实现用例;
|
||||
|
||||
- 如果你需要使用外部文件 ==存放数据驱动的数据,尽量不要因此引入依赖==,可以使用一些标准库能读取的文件格式,比如 `json、ini、CSV、xml、txt` 等文件格式;不建议使用 `Yaml、Excel、MySQL` 等数据格式;
|
||||
|
||||
- ==读取数据时也尽量使用标准库去做==,如使用 `pandas` 处理 `CSV` 就属于大材小用了,正常的数据驱动还没到需要大数据分析来处理的地步;
|
||||
|
||||
- 数据驱动的 ==外部文件存放在widget/ddt/== 目录下;
|
||||
|
||||
- ==数据驱动的写法:==
|
||||
|
||||
```python hl_lines="1"
|
||||
@pytest.mark.parametrize("value", data)
|
||||
def test_smb_049(self, value):
|
||||
...
|
||||
```
|
||||
|
||||
以上这种参数化的写法本身没什么问题;
|
||||
|
||||
但是,这里必须要补充一个没有用的小知识:
|
||||
|
||||
- ==使用 ids 参数;==
|
||||
|
||||
==加 ids 参数之前:==
|
||||
|
||||
如果参数化数据里面的字符会原封不动的输出到 `item.name` 里面,显示非常不优雅,而且可能会引入一些意想不到的问题,可以感受一下:
|
||||
|
||||
参数:
|
||||
|
||||
```python
|
||||
data = [
|
||||
"一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三",
|
||||
"qwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyui",
|
||||
"12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678",
|
||||
]
|
||||
```
|
||||
|
||||
终端日志打印出来,现象是这样色儿的:
|
||||
|
||||
```shell
|
||||
test_smb_049.py::TestFileManager::test_smb_049[一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三]
|
||||
test_smb_049.py::TestFileManager::test_smb_049[qwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyui]
|
||||
test_smb_049.py::TestFileManager::test_smb_049[12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678]
|
||||
```
|
||||
|
||||
说实话,看着心里堵得慌,如果这里面包含一些**特殊字符**或者是**超长**,可能还会有一些很奇妙的事情发生。
|
||||
|
||||
==加 ids 参数之后:==
|
||||
|
||||
```python hl_lines="1"
|
||||
@pytest.mark.parametrize("value", data, ids=[1, 2, 3])
|
||||
def test_smb_049(self, value):
|
||||
...
|
||||
```
|
||||
|
||||
再来感受一下:
|
||||
|
||||
```shell
|
||||
test_smb_049.py::TestFileManager::test_smb_049[1]
|
||||
test_smb_049.py::TestFileManager::test_smb_049[2]
|
||||
test_smb_049.py::TestFileManager::test_smb_049[3]
|
||||
```
|
||||
|
||||
明显好多了,所以尽量使用 ids 这个参数。
|
||||
|
||||
- 不建议使用 `fixture` 的数据驱动方式,框架虽然支持,但可读性比较差;
|
||||
|
||||
如果你不知道这句话在说啥,那你可以忽略,我也不打算详细说这种实现方式,操作比较骚。
|
||||
|
||||
### 5.4. 断言资源
|
||||
|
||||
- ==用例断言的图片资源==,直接放在 ==用例模块的同级目录下的 assert_res 目录== 下,图片名称以 ==用例的模块名称 + 用例 ID 命名==;
|
||||
- 图像识别断言,不要截取一张很大的图,图片资源包含的元素太多了,非常容易受到需求影响,建议是进行局部的断言;
|
||||
|
||||
### 5.5. 元素定位
|
||||
|
||||
- 用于 ==用例操作步骤中进行元素定位的图片资源==,放到 ==widget/pic_res 目录== 下,图片名称命名为该元素的名称;
|
||||
- 用于元素定位的图片截取时尽量精确到这个具体的按钮,图片也不要太大;
|
||||
- 基于 UI 定位的操作较快,合理加入等待时间能提高用例的稳定性。
|
||||
|
||||
### 5.6. 用例资源
|
||||
|
||||
- 用例执行过程中需要使用到的一些资源,==存放在 widget/case_res 目录== 下,前提是这些资源不超过 ==10M==;
|
||||
|
||||
- 如果是一些比较大的资源,建议放到统一的 ftp 服务器,需要执行用例的时候再下载下来;
|
||||
|
||||
- ==确保一个资源在一次用例执行中只需要下载一次==,如果每次使用的时候都去下载,这样可能会耗费大量的网络资源,而因为先判断本地是否存在此资源,如果不存在再去下载;
|
||||
|
||||
- 测试用例执行过程中,你可能需要将资源拷贝到对应的测试目录下;
|
||||
|
||||
比如将 mp3 文件拷贝到 `~/Music` 目录下,但是我们更建议你使用发送快捷链接的方式替代拷贝的操作,因为在拷贝大文件时是很消耗系统资源的,而创建链接则不会;
|
||||
|
||||
```python
|
||||
class DeepinMusicWidget:
|
||||
|
||||
@classmethod
|
||||
def recovery_many_movies_in_movie_by_cmd(cls):
|
||||
"""恢复多个视频文件至视频目录中"""
|
||||
work_path = f"/home/{Config.USERNAME}/Videos/auto"
|
||||
code_path = f"{Config.CASE_RES_PATH}/auto"
|
||||
cls.run_cmd(f"rm -rf {work_path};mkdir {work_path}")
|
||||
sleep(1)
|
||||
flag = False
|
||||
if not exists(code_path):
|
||||
cls.run_cmd(f"mkdir -p {code_path}")
|
||||
flag = True
|
||||
logger.info(f"ln -s {code_path}/* {work_path}/")
|
||||
cls.run_cmd(
|
||||
f"cd {code_path}/;"
|
||||
f"{cls.wget_file('auto.zip') if flag else ''}"
|
||||
f"ln -s {code_path}/* {work_path}/ > /dev/null 2>&1"
|
||||
)
|
||||
```
|
||||
|
||||
资源下载过程中注意 ==超时== 的问题;
|
||||
|
||||
如果你的测试资源很大,要特别注意这问题,如果你使用强制等待下载结束( `os.system` ),可能会造成用例执行时长变得不可接受;
|
||||
|
||||
在持续集成环境执行时网络下载速度很慢,所以超时机制是很有必要的;`run_cmd` 方法有一个默认超时的时间,你可以根据资源大小对超时时间进行调整;
|
||||
|
||||
|
||||
## 6. 标签化管理规范
|
||||
|
||||
### 6.1. 对应关系
|
||||
|
||||
写完自动化用例之后,请在 `CSV` 文件中标记用例的 ID、等级等标签。
|
||||
|
||||
为了提醒标记,执行用例时在首行会输出 `ERROR` 日志: `CSV 文件里面没有对应的 ID`;
|
||||
|
||||
==如果 CSV 文件里面没有对应 ID,后续在批量执行的时候,这些用例是不会执行的。==
|
||||
|
||||
### 6.2. 名称一致
|
||||
|
||||
==CSV 文件的文件名==、==用例 py 文件中间的名称==、==用例函数中间的名称==,这三个名称一致。
|
||||
|
||||
举例:
|
||||
|
||||
```python title="test_music_679537.py" hl_lines="3"
|
||||
class TestMusic:
|
||||
|
||||
def test_music_679537():
|
||||
"""用例标题"""
|
||||
```
|
||||
|
||||
那么 `CSV` 文件的名称为 ==music.csv==。
|
||||
|
||||
框架底层代码实现是将 ==CSV 文件的名称== 与 ==用例脚本名称== 进行对应(建立映射);
|
||||
|
||||
## 7. 子应用Tag管理规范
|
||||
|
||||
- 应用库 tag 根据应用交付节点生成,每次打 tag 之前,相关测试人员需要进行用例调试;
|
||||
|
||||
- 调试用例是指的在全架构上调试通过;
|
||||
|
||||
- ==tag 号怎么打?==
|
||||
|
||||
根据持续集成的要求生成,其中应用版本号需要与项目经理确认本次即将集成的应用版本号是多少;
|
||||
|
||||
tag 的 commit 信息格式:
|
||||
|
||||
```ini title="# commit msg"
|
||||
version:5.6.5
|
||||
```
|
||||
|
||||
其中 `5.6.5` 写应用的集成版本号。
|
||||
|
||||
## 8. 其他规范
|
||||
|
||||
- 不写 `if __name__ == '__main__':`,不写多余的代码;
|
||||
- 统一文件注释头。
|
||||
|
||||
```python title="xxx.py"
|
||||
#!/usr/bin/env python3
|
||||
# _*_ coding:utf-8 _*_
|
||||
"""
|
||||
:Author:email@uniontech.com
|
||||
:Date :${DATE} ${TIME}
|
||||
"""
|
||||
```
|
||||
|
||||
- 日志打印要在方法最前面,否则代码报错没有日志输出,不好定位问题;
|
||||
- hook 函数只能写到根目录下的 `conftest.py` 里面;
|
||||
- `apps` 目录下的 `conftest.py` 原则上不会写 `fixture`;
|
||||
- 固定目录或元素控件的操作,将操作方法写死,类似文件的操作可以将文件名留参数;
|
||||
- 路径拼接规范:
|
||||
- 系统中固定目录,路径拼接时使用波浪符号,比如:`~/Desktop/`,下层使用 `os.path.expanduser()`,它可以自动识别波浪符号;
|
||||
- 项目下路径使用配置文件中的路径,比如:`Config.BASE_PATH`,因为项目是可以在任意路径运行的,需要动态拼接路径。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
# AT 用例筛选指北
|
||||
|
||||
```shell
|
||||
# ================================================
|
||||
# Author : mikigo
|
||||
# ================================================
|
||||
```
|
||||
|
||||
AT 用例筛选标准,是挑选合适的文本用例进行自动化用例转化的一些通用标准。
|
||||
|
||||
过去我们在自动化测试实践过程中,写了不少自动化用例,随着需求的迭代,自动化用例会自然腐化,因而需要在迭代的过程中对自动化用例进行维护,在维护自动化用例的过程中我们发现,有些用例维护起来太难了,调试用例耗费大量时间,成本很高。
|
||||
|
||||
因此,我们总结一些自动化用例选型的标准,以优化后续自动化用例结构,期待我们能编写出高效、稳定、易于维护的自动化用例。
|
||||
|
||||
# 一、不适合做自动化的用例
|
||||
|
||||
自动化测试可以分为常规自动化测试和专项自动化测试,常规自动化测试一般只针对一些常规的功能测试,其他非常规功能的用例,应该采用专项的自动化测试方案;
|
||||
|
||||
以下是不适合做自动化用例的情况:
|
||||
|
||||
## 1. 稳定性、压测用例
|
||||
|
||||
稳定性测试、压测用例应该是属于性能相关的测试场景,如:反复多次点击按钮,检验其功能稳定性。因为这些用例断言的预期可能不是很明确,更有甚者不需要断言,用例时长比较长,而且不可控,因此不适合做常规自动化测试。
|
||||
|
||||
目前我们有性能自动化的专项方案,以及一些稳定性测试的专项方案,可以考虑将此类用例放到专项自动化里面去做。
|
||||
|
||||
## 2. 涉及多个用户操作
|
||||
|
||||
常见与文管的用例中,多用户操作的用例一般都会涉及到在系统中进行多个用户间的切换,自动化脚本一般是运行在一个用户空间下的,所以还无法实现多用户间的切换。
|
||||
|
||||
此类不建议采用自动化执行。
|
||||
|
||||
## 3. 与开关机、注销、重启交互
|
||||
|
||||
此类场景多为应用的异常场景测试,比如:复制大文件过程中注销机器。
|
||||
|
||||
自动化脚本在系统关机、注销、重启之后是会停止运行的;此类用例可以做自动化,但是同样需要做专项方案,用例执行过程的处理逻辑完全不同。
|
||||
|
||||
后续自动化需要攻关的点:
|
||||
|
||||
- 自动化脚本在本机运行,自动化测试框架至少需要提供以下功能:
|
||||
|
||||
- 重启之后用例能自动运行;可以考虑将执行用例写成一个开机自启的服务;
|
||||
|
||||
- 能记录关机之前用例执行的位置,重启之后能从此位置开始执行;
|
||||
|
||||
- 像性能自动化一样,由服务端控制;
|
||||
|
||||
- 完全基于图形识别进行元素定位;
|
||||
- 串口线控制键鼠稳定性;
|
||||
|
||||
## 4. 测试执行时间长
|
||||
|
||||
用例执行时间控制在2分钟以内,极端情况不要超过5分钟,所有用例的平均时长应该在1分钟左右;
|
||||
|
||||
用例执行时间太长,编写用例、调试用例、执行用例、维护用例都非常的耗时,人力成本时间成本投入太高。
|
||||
|
||||
你想,这条用例执行需要5分钟,那你写这条用例的时间不可能一遍过,写的过程中跑不通还得重跑至少15分钟不过分,写完了还得在其他架构上调试,半个小时过去了,这还只是第一次写用例的时候,后续如果用例维护过程中,你可能需要经常投入相当长时间,这样其实是失去了自动化的意义的。常规自动化测试就应该是快准狠。
|
||||
|
||||
## 5. 边缘性测试
|
||||
|
||||
有些功能模块本身是属于比较边缘性的,即极少用户会使用到的功能,这类功能模块不需要大规模实现自动化,一两个场景覆盖基本功能就行了,因此我们在挑选此类用例的时候可以结合整个功能模块的覆盖情况来决定。
|
||||
|
||||
## 6. 需要依赖人工介入的
|
||||
|
||||
自动化执行的过程中是没有人工介入的,如果这条用例是比如要人工介入的,那么就背离了自动化的意义。
|
||||
|
||||
比如光驱模块,光驱刻录是核心功能,但是光驱刻录之后光盘会弹出来,需要人工更换光盘,再手动推进光驱中。
|
||||
|
||||
这种功能模块即使做自动化,能涉及到的功能非常有限,而且不是核心功能,所以明显就不适合做自动化。
|
||||
|
||||
## 7. 难以实现的
|
||||
|
||||
用例所涉及到的方法实现起来可能有难度,甚至现阶段无法实现的,此类用例不适合做自动化。
|
||||
|
||||
比如影院测试画面清晰度、流畅度、截图录屏录制视频之后的视频是否存在卡顿等用例,现阶段还无法实现。
|
||||
|
||||
## 8. 功能不稳定
|
||||
|
||||
这个不是说还在开发阶段的功能,还在开发阶段的功能不稳定是正常的,这种我们可以等开发提测之后再写自动化用例。
|
||||
|
||||
功能不稳定指的是用例所涉及的功能,可能存在多种变化,每次执行用例可能断言的预期都不同。
|
||||
|
||||
## 9. 动态断言
|
||||
|
||||
用例执行过程中直到用例结束,一直是一个动态变化的过程,而且无法通过其他手段停下来;
|
||||
|
||||
比如:截图录屏录制视频时,右下角的录制时间是一直在变化的,用例需要测试录制1分钟之后,录制时间是刚好停在60秒;
|
||||
|
||||
目前自动化用例执行还做不到时间精准控制,程序执行时间是有波动的,而且要适配多个平台,所以此类用例大概率会执行失败。特别是在性能较差的国产化架构上,此类用例不太可能稳定执行。
|
||||
|
||||
## 10. 探索性测试
|
||||
|
||||
探索性测试通常是一些不确定的操作,多变切复杂的测试场景,预期也可能是不确定的,简单讲就是骚操作太多了,因此不适合做自动化测试。
|
||||
|
||||
此类用例适合采用手工测试,是手工测试真正的价值体现。
|
||||
|
||||
## 11. 易用性测试
|
||||
|
||||
对于易用性的判断通常是主观的,对于自动化来讲很难去定义什么样的情况对用户是易用的,自动化需要有明确的可实现的预期。
|
||||
|
||||
# 二、适合做自动化的用例
|
||||
|
||||
前面讲了不适合做自动化的一些用例情况,有同学就说了,适合做自动化的用例不用讲了,就是不适合的反面。没毛病哈,适合做自动化的用例,我们大体还可以分为两类:
|
||||
|
||||
- 第一类:自动化效果好,应该尽可能实现的自动化用例;
|
||||
- 第二类:自动化效果一般,需要投入更多精力才能实现的自动化用例;
|
||||
|
||||
以下介绍一些我们应该尽可能实现自动化的情况:
|
||||
|
||||
## 1. 操作相对简单
|
||||
|
||||
用例步骤相对较少,这种用例通常是一些路径很浅的用例,一般也是一些基础用例。用例编写、维护都可以轻松搞定。
|
||||
|
||||
## 2. 容易实现
|
||||
|
||||
现有技术是可实现的,没有难度,应该尽可能做自动化。
|
||||
|
||||
## 3. 经常用于回归测试
|
||||
|
||||
如果这条用例是每次回归测试必须要执行的用例,那么这条用例应该尽可能实现自动化。
|
||||
|
||||
回归测试时我们首先应该保证应用历史基本功能正常,所以那些每次在回归测试时都要执行的用例适合转化为自动化用例。
|
||||
|
||||
## 4. 功能稳定的核心功能
|
||||
|
||||
涉及核心功能的用例是最最重要的用例,也是我们交付时必须要保证质量的相关功能,因此应该尽可能的转化为自动化。
|
||||
|
||||
## 5. 输入数据测试
|
||||
|
||||
输入数据测试通常是检验被测应用接受有效数据、拒绝无效数据的能力。如:提交超长字符串、特殊字符串等等。
|
||||
|
||||
此类用例手工测试很难比较全面的覆盖到所有的输入情况,但是对于自动化来讲,此类用例操作步骤都是一样的,变化的仅是输入参数和断言结果,采用数据驱动,可以轻松实现。
|
||||
|
||||
## 6. 格式覆盖
|
||||
|
||||
原因同输入数据测试类用例一样,多种格式的覆盖也是一个很繁琐的事情,交给自动化来做再合适不过了。
|
||||
|
||||
比如影院对影片格式的检查、截图录屏对保存格式分辨率的检查。
|
||||
|
||||
## 7. 检查元素状态
|
||||
|
||||
查看被测应用的元素属性。如:图片的大小和排列;按钮的可用和不可用。
|
||||
|
|
@ -0,0 +1,317 @@
|
|||
# AT 经验总结
|
||||
|
||||
> 欢迎所有人提交你在自动化测试方面的优秀实践经验,以帮助大家解决可能遇到的问题。
|
||||
|
||||
## 用例调试技巧
|
||||
|
||||
### 1. 日志
|
||||
|
||||
- 一定要先看报错,看 error 日志,通常能明确的告诉你具体哪里代码报错;
|
||||
- 结合报错点前面的error、 info 和 debug 日志看否是正常。
|
||||
|
||||
### 2. 断点调试
|
||||
|
||||
- 方法报错:
|
||||
- 在方法库中找到对应的方法,单独调用,看是否能正常执行;
|
||||
- 通常单个方法的执行是比较简单的,如果单个方法报错,很快就能排查出问题;在方法内部打断点,使用 `Debug` 运行,看方法内部数据传递是否存在问题;
|
||||
- 如果单个方法调用没问题,那么在用例中报错的方法前面打断点,使用 `Debug` 运行,看用例的业务逻辑和数据传递是否存在问题;
|
||||
- 断言报错:
|
||||
- 断言为数据断言,根据表达式进行断言语句进行修改;
|
||||
- 文件生成类断言,查看是否需要加等待时间;
|
||||
- 图像断言,在断言语句处打断点,使用 `Debug` 运行,用例运行到断言处会停止,查看此时断言的图片与用例执行的现场存在什么差异,此时也可以进行重新截图,从而替换新的图片;
|
||||
|
||||
### 3. 远程执行
|
||||
|
||||
- 远程执行指的是编辑器通过指定远程解释器执行自动化代码;
|
||||
|
||||
远程执行的好处是可以很方便的 `Debug` 运行,不用在测试机上打开编辑器,用例执行速度更快;
|
||||
|
||||
支持远程执行功能的编辑器:
|
||||
|
||||
- 专业版 `Pycharm`
|
||||
- `VScode` ,需要使用插件 `Remote-SSH`
|
||||
|
||||
- 远程执行配置
|
||||
|
||||
以 `Pycharm` 为例:
|
||||
|
||||
`File` —> `Settings` —> `Project` —> `Python Interpreter` —> `右边设置按钮` —> `Add...` —> `SSH Interpreter` —> `New server configuration(填入host和username)` —> `Next` —> `password(测试机密码)` —> `Interpreter(选择远程解释器)` —> `Finish`
|
||||
|
||||
### 4. 环境清理
|
||||
|
||||
- 如果用例里面的 `teardown` 没有执行,大概率是因为 `setup` 里面代码报错,这两个是一对的,`setup` 里面报错,`teardown` 里面的代码不会执行;
|
||||
- 目前我们已经将各应用的 `clean_all` 这个 `fixture` 改成了终结器,确保始终能执行到这步,但是用例里面的 `fixture`,还是需要我们小心处理;
|
||||
- 要执行 `clean_all` 需要在编辑器运行参数加 `--clean yes`,写用例的时候请加上,不然你不确定用例执行之后的环境是否恢复;
|
||||
- `setup` 可以不要,将 `setup` 放到用例里面是一种稳妥的做法,`teardown` 一定要。
|
||||
|
||||
### 5. 元素定位不准(坐标返回不对)
|
||||
|
||||
- 基于 `UI` 定位的方法,可能受到窗口 ID 的变化,导致坐标返回不准,默认取最新的一个窗口用于定位,但如果实际需要定位的不是最新的窗口,那么在用例中需要重新实例化方法类对象,并在类中传入对应的窗口序号;
|
||||
- 基于属性定位的方法,目前遇到的笔记本上,由于屏幕缩放比例为 1.25,导致坐标返回不准,我们默认使用缩放比例为 1;
|
||||
- 基于图像定位的方法,如果当前屏幕中存在多个相同的目标元素,可能出现定位不准;支持通过参数控制,返回多个坐标;
|
||||
- 基于 OCR 定位的方法,如果当前屏幕中存在多个相同的文字元素,可能出现定位不准;同样支持通过参数控制,返回多个坐标;
|
||||
|
||||
### 6. 键鼠操作不准
|
||||
|
||||
- 鼠标操作不生效,比如右键、双击无响应;
|
||||
- 键盘操作不生效,或者延迟输入,比如用例需要输入“我是中国人”,实际只输入了“我国人”;
|
||||
|
||||
以上问题排除应用卡顿等问题,大概率是由于工具的问题,目前键鼠操作我们使用三个工具:`Dogtail` 提供的键鼠工具、`PyAutoGUI`、`Xdotool` ;
|
||||
|
||||
有同学可能要说为啥要用三个啊,用一个不就好了,简单讲就是各有优点各有缺点。
|
||||
|
||||
如果你遇到键鼠的问题,可以试试通过不同的工具操作;键盘输入延迟的问题,一般是因为输入速度太快了,系统没反应过来,常见于 `ARM` 和 `MIPS` 上,修改参数 delay_time 的值,单位为毫秒;
|
||||
|
||||
```python
|
||||
# mouse_key.py
|
||||
|
||||
@classmethod
|
||||
def input_message(cls, message, delay_time=300, interval=0.2):
|
||||
"""
|
||||
输入字符串
|
||||
:param message: 输入的内容
|
||||
:param delay_time: 延迟时间
|
||||
:param interval:
|
||||
:return:
|
||||
"""
|
||||
```
|
||||
|
||||
如果不是方法的问题,则需要继续和开发一起排除,是否为应用接受键鼠信号处理的问题,这类情况我们也是遇到过的,具体问题具体分析。
|
||||
|
||||
比如影院就重写了一个右键的方法:
|
||||
|
||||
```python
|
||||
# base_widget.py
|
||||
|
||||
@classmethod
|
||||
def right_click(cls, _x=None, _y=None):
|
||||
"""
|
||||
重写底层单击鼠标右键
|
||||
解决影院右键触发release事件的问题(右键主窗口会播放视频)
|
||||
"""
|
||||
cls.mouse_down(_x, _y, button=3)
|
||||
sleep(0.1)
|
||||
cls.mouse_up(button=3)
|
||||
```
|
||||
|
||||
## 终结器
|
||||
|
||||
```shell
|
||||
# litaoa@uniontech.com
|
||||
```
|
||||
|
||||
前置/后置步骤
|
||||
|
||||
Pytest 实现前置/后置步骤的方式有两种,yield 和终结函数;
|
||||
|
||||
yield 实现,yield前面为用例的前置步骤,yield 后面为用例的后置步骤。
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def clean_env():
|
||||
print("setup")
|
||||
yield
|
||||
print("teardown")
|
||||
```
|
||||
|
||||
终结函数实现,使用 request.addfinalizer 注册用例的后置步骤
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def clean_env(request):
|
||||
def clean():
|
||||
print("teardown")
|
||||
request.addfinalizer(clean)
|
||||
print("setup")
|
||||
```
|
||||
|
||||
yield的优缺点:
|
||||
|
||||
优点:代码简洁,直观,可使用yield在用例中获取前置步骤的返回值
|
||||
|
||||
缺点:若前置步骤中出现错误,则后置步骤不会执行
|
||||
|
||||
终结函数:
|
||||
|
||||
优点:前置步骤失败的话,后置步骤仍会执行且可以注册多个后置步骤(前提:需要在代码报错之前注册后置步骤),支持灵活使用后置条件
|
||||
|
||||
缺点:代码较为复杂,无法获取前置步骤的返回值(本人目前未实现)
|
||||
|
||||
总结:在前置步骤保证绝对不会出错时,使用yield更佳简便,当前置步骤易出现问题时,推荐使用终结函数。
|
||||
|
||||
场景:保险箱用例,前置步骤中开启保险箱,后置步骤删除保险箱。
|
||||
|
||||
```python
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def vault_fixture(request):
|
||||
DfmWidget.reset_vault_by_cmd() # 1、重置保险箱
|
||||
DdeDockPublicWidget().close_file_manager_by_cmd() # 2、关闭文管
|
||||
DdeDockPublicWidget().open_file_manager_in_dock_by_attr() # 3、开启文管
|
||||
vault = DfmWidget()
|
||||
vault.create_file_vault() # 4、创建保险箱
|
||||
|
||||
def delete_vault(): # 8、删除保险箱的后置步骤
|
||||
sleep(2)
|
||||
DdeDockPublicWidget().open_file_manager_in_dock_by_attr()
|
||||
vault = DfmWidget()
|
||||
vault.delete_file_vault()
|
||||
DfmWidget.reset_vault_by_cmd()
|
||||
DdeDockPublicWidget().close_file_manager_by_cmd()
|
||||
request.addfinalizer(delete_vault) # 5、注册后置步骤
|
||||
DdeDockPublicWidget().close_file_manager_by_cmd() # 6、关闭文管
|
||||
sleep(1)
|
||||
DdeDockPublicWidget().open_file_manager_in_dock_by_attr() # 7、开启文管
|
||||
```
|
||||
|
||||
代码按注释中的序号执行步骤:
|
||||
|
||||
- 代码在步骤 1 - 4 任意位置报错,则不会执行步骤 8,因为未执行到步骤 5,步骤8还未注册;
|
||||
- 代码在步骤 6 - 7 报错,仍会执行步骤 7,因为在步骤 5 中已经将步骤 7 注册;
|
||||
|
||||
可以灵活注册后置步骤,能实现某个前置步骤执行之后,才会执行后置步骤。
|
||||
|
||||
## 启动应用的方式
|
||||
|
||||
```shell
|
||||
# huangmingqiang@uniontech.com
|
||||
```
|
||||
|
||||
(1)命令行启动
|
||||
|
||||
在 AT 代码中使用命令行启动应用,举例:
|
||||
|
||||
```python
|
||||
os.popen("deepin-music")
|
||||
```
|
||||
|
||||
这种方式启动存在一个问题,就是当使用 ssh 远程执行用例时,`dogtail` 无法获取到元素。
|
||||
|
||||
(2)通过 UI 操作启动
|
||||
|
||||
通过任务栏、启动器、桌面等 UI 方式启动,比如双击打开、右键打开等,这种操作方式不存在 ssh 远程执行用例时 `dogtail` 无法获取到元素的问题,也更加符合用户的操作行为。
|
||||
|
||||
## 文件选择框属性定位偶现无法找到
|
||||
文件选择框存在一个问题,在调用文件选择框时,有一定的概率出现,界面已经渲染出来了,但是属性树并没有写入,导致通过属性无法找到元素,目前也没有很好的解决方案,为了用例稳定性,文件选择框的操作建议使用 UI 或者图片定位的方式,可以通过搜索内容固定文件位置。
|
||||
|
||||
```shell
|
||||
# litaoa@uniontech.com
|
||||
```
|
||||
|
||||
```python
|
||||
desk = DdeDesktopPublicWidget()
|
||||
# 选择视频目录
|
||||
desk.click_videos_dir_in_desktop_plugs_by_ui()
|
||||
sleep(1)
|
||||
desk.ctrl_f()
|
||||
sleep(1)
|
||||
desk.input_message("元素名称")
|
||||
sleep(0.5)
|
||||
desk.enter()
|
||||
sleep(1)
|
||||
# 选择第一个文件
|
||||
desk.click_list_view_btn_in_desktop_plugs_by_ui()
|
||||
sleep(1)
|
||||
desk.click_first_file_in_desktop_plugs_by_ui()
|
||||
sleep(1)
|
||||
# 文管插件中点击打开
|
||||
desk.click_open_btn_in_desktop_plugs_by_ui()
|
||||
```
|
||||
|
||||
## 应用启动
|
||||
|
||||
```shell
|
||||
# mikigo
|
||||
```
|
||||
|
||||
在UI自动化测试中,一切操作的都是从应用启动开始的,而在Linux桌面应用自动化测试中,我们启动应用的方法有多种,下面做一个简单的介绍:
|
||||
|
||||
**【使用dogtail启动】**
|
||||
|
||||
dogtail提供了应用启动的方法,在utils库中,使用run方法启动:
|
||||
|
||||
首先导入方法:
|
||||
|
||||
```python
|
||||
from dogtail.utils import *
|
||||
```
|
||||
|
||||
调用run方法
|
||||
|
||||
```python
|
||||
run('deepin-music')
|
||||
```
|
||||
|
||||
即可启动音乐
|
||||
![img](https://img2020.cnblogs.com/blog/1898113/202012/1898113-20201218162414410-1539488857.png)
|
||||
|
||||
这种方法的优点是采用进程的方式直接启动,不依赖与UI,无论桌面或任务栏上是否存在应用图标,都可以正常启动。
|
||||
|
||||
但是在实际项目中,仍然存在一个问题,
|
||||
|
||||
如果使用ssh远程调用,或者 Jenkins 中执行测试脚本的时候,在 sniff 中会出现找不到应用,经过分析,可能是因为使用这种方法启动的时候,实际是采用一个子进程启动了应用,dogtail 无法识别到。
|
||||
|
||||
**【从任务栏启动】**
|
||||
|
||||
使用dogtail点击任务栏上的应用图标
|
||||
|
||||
通常有两种方法:
|
||||
|
||||
(1)使用dogtail点击任务栏上的应用图标。
|
||||
|
||||
(2)已知应用图标在任务栏上的位置,然后使用鼠标点击对应坐标。
|
||||
|
||||
第二种方法的缺点是位置必须固定,如果移动位置就不行了,而使用第一种方法,无论位置在哪里,只要图标在任务栏上存在即可。
|
||||
|
||||
**【点击桌面图标启动】**
|
||||
|
||||
桌面图标目前是采用图像识别技术,定位到应用图标的坐标,然后通过pyauogui进行点击操作。
|
||||
|
||||
详细技术方案可以参考我的另外两篇博客:
|
||||
|
||||
[基于opencv的模板匹配实现图像识别,返回在屏幕中的坐标](https://www.cnblogs.com/mikigo/p/13489143.html)
|
||||
|
||||
[Python三方库PyAutoGui的使用方法](https://www.cnblogs.com/mikigo/p/13182619.html)
|
||||
|
||||
**【从启动器启动(俗称开始菜单)】**
|
||||
|
||||
启动中启动的实现逻辑实际和任务栏上启动差不多。
|
||||
|
||||
首先,需要使用鼠标点击任务栏上的启动器图标,或者键盘super键,将启动器呼出来,
|
||||
|
||||
然后,在启动器中点击对应的图标,
|
||||
|
||||
但是这里有个问题,启动器中的应用列表,一页展示不完,所以如果我们要点击的应用图标不在第一页怎么办,通常解决方案有两种:
|
||||
|
||||
(1)需要进行向下滑动,这里就涉及到相应的识别方案,判断如果不在第一页就往下滑动翻页。
|
||||
|
||||
(2)启动器提供搜索的功能,输入应用名称搜索,然后进行点击。
|
||||
|
||||
从实际操作中来看,采用第二种方法的效率会高一点。
|
||||
|
||||
**【终端命令启动】**
|
||||
|
||||
在 Python 中,使用 `os.popen()` 或 `os.system()` 或 `subprocess.Popen()`,实现命令行启动,比如:
|
||||
|
||||
```python
|
||||
import os
|
||||
os.popen('deepin-music')
|
||||
```
|
||||
|
||||
这种方式启动是比较简单的,但是在实际项目中,仍然存在远程执行脚本的时候,dogtail 无法识别的问题。
|
||||
|
||||
**【总结】**
|
||||
|
||||
以上几种方法,各有优缺点,在实际项目中:
|
||||
|
||||
(1)如果需要在 Jenkins 中做持续集成,建议使用第二种任务栏启动的方法。
|
||||
|
||||
(2)如果不会采用远程执行的,建议采用第一种或者最后一种方案。
|
||||
|
||||
(3)第三种和第四种启动方法,通常在测试用例中会涉及到,所以偶尔会用。
|
||||
|
||||
## 其他不为人知的细节
|
||||
|
||||
- 在一段时间内尽量编写同一个应用或模块的用例,能对该用例已有方法熟悉,避免过多重复业务代码的封装;
|
||||
- 相同的场景下,各架构等待时间不同,建议使用框架提供的 sleep,我们做了不同架构的倍数放大;
|
||||
- 编写用例时,尽量考虑到每一步异常后的环境恢复,需要建议这种意识,随时要考虑到,这步操作有没有可能出错,出错了改怎么办;
|
||||
- 提交代码的时候注意不要把一些临时的测试资源提交进去了,比如测试了一个影片,有些同学习惯使用 `git add .` ,然后就全部提交到代码仓库了,这样即使后期把大文件删了,`.git` 文件里面也会很大,造成代码仓库变得十分臃肿。
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
hide:
|
||||
- navigation
|
||||
- toc.integrate
|
||||
---
|
||||
|
||||
--8<-- "RELEASE.md"
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
template: no_busuanzi.html
|
||||
hide:
|
||||
- date
|
||||
- git-revision-date-localized
|
||||
- navigation
|
||||
- toc
|
||||
- git-authors
|
||||
comments: true
|
||||
---
|
||||
|
||||
|
||||
|
||||
# 留言
|
||||
|
||||
<div>
|
||||
<span style="color:#707070;font-size: small">
|
||||
英雄不问出处,留言不管字数。
|
||||
</span>
|
||||
</div>
|
|
@ -0,0 +1,106 @@
|
|||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%">
|
||||
<a href="https://github.com/mikigo">
|
||||
<img src="https://github.com/mikigo.png?size=50" width="80px;" height="80px;" alt="mikigo"/>
|
||||
<br/>
|
||||
<sub>
|
||||
<b>mikigo</b>
|
||||
</sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="top" width="14.28%">
|
||||
<a href="https://github.com/zhao-george">
|
||||
<img src="https://github.com/zhao-george.png?size=50" width="80px;" height="80px;" alt="zhao-george"/>
|
||||
<br/>
|
||||
<sub>
|
||||
<b>zhao-george</b>
|
||||
</sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="top" width="14.28%">
|
||||
<a href="https://github.com/lu-xianseng">
|
||||
<img src="https://avatars.githubusercontent.com/u/79616939?v=4&s=50" width="80px;" height="80px;"
|
||||
alt="lu-xianseng"/>
|
||||
<br/>
|
||||
<sub>
|
||||
<b>lu-xianseng</b>
|
||||
</sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="top" width="14.28%">
|
||||
<a href="https://github.com/momiji33">
|
||||
<img src="https://avatars.githubusercontent.com/u/153154080?v=4&s=50" width="80px;" height="80px;"
|
||||
alt="momiji33"/>
|
||||
<br/>
|
||||
<sub>
|
||||
<b>momiji33</b>
|
||||
</sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="top" width="14.28%">
|
||||
<a href="https://github.com/king123666">
|
||||
<img src="https://avatars.githubusercontent.com/u/106379303?v=4&s=50" width="80px;" height="80px;"
|
||||
alt="king123666"/>
|
||||
<br/>
|
||||
<sub>
|
||||
<b>king123666</b>
|
||||
</sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="top" width="14.28%">
|
||||
<a href="https://gitlabbj.uniontech.com/ut003620">
|
||||
<img src="https://avatars.githubusercontent.com/u/105044004?v=4&s=50" width="80px;" height="80px;"
|
||||
alt="ut003620"/>
|
||||
<br/>
|
||||
<sub>
|
||||
<b>ut003620</b>
|
||||
</sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="top" width="14.28%">
|
||||
<a href="https://github.com/Jimijun">
|
||||
<img src="https://avatars.githubusercontent.com/u/81949103?v=4&s=50" width="80px;" height="80px;"
|
||||
alt="Jimijun"/>
|
||||
<br/>
|
||||
<sub>
|
||||
<b>Jimijun</b>
|
||||
</sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%">
|
||||
<a href="https://github.com/rb-union">
|
||||
<img src="https://avatars.githubusercontent.com/u/105044004?v=4&s=50" width="80px;" height="80px;"
|
||||
alt="rb-union"/>
|
||||
<br/>
|
||||
<sub>
|
||||
<b>rb-union</b>
|
||||
</sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="top" width="14.28%">
|
||||
<a href="https://github.com/saifeiLee">
|
||||
<img src="https://avatars.githubusercontent.com/u/28710808?v=4" width="80px;" height="80px;"
|
||||
alt="saifeiLee"/>
|
||||
<br/>
|
||||
<sub>
|
||||
<b>saifeiLee</b>
|
||||
</sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="top" width="14.28%">
|
||||
<a href="https://github.com/DarkLii">
|
||||
<img src="https://avatars.githubusercontent.com/u/36026560?v=4" width="80px;" height="80px;"
|
||||
alt="DarkLii"/>
|
||||
<br/>
|
||||
<sub>
|
||||
<b>DarkLii</b>
|
||||
</sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
hide:
|
||||
- navigation
|
||||
- toc.integrate
|
||||
---
|
||||
|
||||
# 视频教程
|
||||
|
||||
## 【系列一】自动化架构设计规划、基础框架设计方案
|
||||
|
||||
<div>
|
||||
<span style="color:#707070;font-size: small">
|
||||
建议从 4 分 30 秒开始
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<video id="video" controls="" preload="none" poster="./logo.png">
|
||||
<source id="mp4" src="//youqu.uniontech.com/videos/Videos/自动化系列培训分享之系列一《框架设计》2020240124_黄明强.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
------------------------
|
||||
|
||||
## 【系列二】应用库设计方案、YouQu功能介绍(一)
|
||||
|
||||
<div>
|
||||
<span style="color:#707070;font-size: small">
|
||||
建议从 2 分 15 秒开始
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<video id="video" controls="" preload="none" poster="./logo.png">
|
||||
<source id="mp4" src="//youqu.uniontech.com/videos/Videos/应用库设计方案&YouQu功能介绍一.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
------------------------
|
||||
|
||||
## 【系列三】YouQu功能介绍(二)
|
||||
|
||||
<div>
|
||||
<span style="color:#707070;font-size: small">
|
||||
建议从 3 分 44 秒开始
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<video id="video" controls="" preload="none" poster="./logo.png">
|
||||
<source id="mp4" src="//youqu.uniontech.com/videos/Videos/自动化系列培训分享之系列三.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
------------------------
|
|
@ -0,0 +1,98 @@
|
|||
---
|
||||
hide:
|
||||
- navigation
|
||||
---
|
||||
|
||||
## 1. 提交代码时提示邮箱或者名称不对
|
||||
|
||||
重新配置邮箱或者名称,然后重置生效:
|
||||
|
||||
```shell
|
||||
git commit --amend --reset-author
|
||||
```
|
||||
|
||||
---------------
|
||||
|
||||
## 2. 怎么回滚到之前的版本
|
||||
|
||||
(1)查询历史提交记录
|
||||
|
||||
```shell
|
||||
git log
|
||||
```
|
||||
|
||||
找到你要回滚的版本,复制 `hash` 值。
|
||||
|
||||
- 注意:是 `commit` 空格之后的 `hash` 值,之前有同学复制的 `Change-Id:` 这样肯定报错。
|
||||
|
||||
(2)回滚版本,不清除代码
|
||||
|
||||
```shell
|
||||
git reset --soft ${hash}
|
||||
```
|
||||
|
||||
(3)回滚版本,清除代码,慎用哈
|
||||
|
||||
```shell
|
||||
git reset --hard ${hash}
|
||||
```
|
||||
|
||||
---------------------------
|
||||
|
||||
## 3. 解决 git status 中文显示的问题
|
||||
|
||||
```shell
|
||||
git config --global core.quotePath false
|
||||
```
|
||||
|
||||
---------------------
|
||||
|
||||
## 4. `apps` 目录下颜色有些是黄色的
|
||||
|
||||
在 `Pycharm` 中 `apps` 目录下应用库文件是黄色的,编辑器识别不到代码新增和修改;
|
||||
|
||||
由于社区版 `Pycharm` 不能动态识别多仓库,需要在 setting 里面手动注册,操作步骤:
|
||||
|
||||
`File` —`Settings` —`Version Control` —点 `+` 号 —`Directory` 选中应用库工程目录 —`VCS` 选中 `Git` —`Apply`
|
||||
|
||||
如此就可以了。
|
||||
|
||||
专业版 `Pycharm` 一般不存在这个问题。
|
||||
|
||||
------------------------------
|
||||
|
||||
## 5. 执行 `env.sh` 报错 `$'\r':未找到命令`
|
||||
|
||||
出现这个问题你应该是在 windows 上打开或编辑过 `env.sh` 脚本,windows下的换行是回车符+换行符,也就是`\r\n`,而 `Linxu` 下是换行符 `\n`,`Linux` 下不识别 `\r`,因此报错。
|
||||
|
||||
解决方案:
|
||||
|
||||
```shell
|
||||
# 将 \r 替换为空
|
||||
sudo sed -i 's/\r//' env.sh
|
||||
```
|
||||
|
||||
---------------------------
|
||||
|
||||
## 6. 怎样为单独某一条用例配置执行超时时间
|
||||
|
||||
在用例脚本中添加装饰器,如下:
|
||||
|
||||
```python
|
||||
@pytest.mark.timeout(300) # 单位秒
|
||||
def test_xxx_001():
|
||||
pass
|
||||
```
|
||||
|
||||
-----------------------
|
||||
|
||||
## 7. 如何修复子仓库 master 分支游离头(detached head)
|
||||
|
||||
修复所有子仓库默认master 分支游离头
|
||||
|
||||
```shell
|
||||
cd youqu
|
||||
git submodule foreach -q --recursive 'git checkout $(git config -f $toplevel/.gitmodules submodule.$name.branch || echo master)'
|
||||
```
|
||||
|
||||
-------------------------
|
|
@ -0,0 +1,626 @@
|
|||
# 智能化功能测试
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c2661c6c8e54ff477b.png)
|
||||
|
||||
```python
|
||||
# Attribution :Chengdu Test Team
|
||||
# Date :2021/08/20
|
||||
```
|
||||
|
||||
仓库地址:[https://gitlabcd.uniontech.com/autotest/cd-desktop-aitest](ttps://gitlabcd.uniontech.com/autotest/cd-desktop-aitest)
|
||||
|
||||
## 一、方案概述
|
||||
|
||||
基于深度学习智能识别应用的元素控件,用于在自动化测试操作过程中的元素定位以及判断结果时的断言处理;自动化测试过程中,测试机上的画面通过采集盒传输到服务器上,服务器对视频画面进行智能识别后,服务器智能判断用例是否执行成功,然后对测试机下发下一步操作指令,测试机接受并执行下一步操作。
|
||||
|
||||
服务器与测试机之间的链接:
|
||||
|
||||
* 服务器通过 USB 串口设备(USB-HID 协议)模拟鼠标键盘对测试机进行操作。
|
||||
* 通过视频采集设备捕获测试机的实时画面传输给服务器。
|
||||
|
||||
服务器对采集的画面进行智能识别,识别其中的元素控件,并返回元素控件在屏幕中的坐标,服务器通过智能化分析,如果测试结果与预期一致将下一步操作指令通过串口模拟键鼠信号发送给测试机;如果测试结果与预期不一致将会进入中断处理及异常结果输出,继续执行后续场景,直至所有用例测试完成。
|
||||
|
||||
## 二、深度学习环境搭建
|
||||
|
||||
模型训练主要对 GPU 有要求,训练模型的机器显存不低于 6G,其他配置无特殊要求。
|
||||
|
||||
**推荐性价比较高配置**
|
||||
|
||||
```shell
|
||||
操作系统: UnionTech OS Desktop 20 Professional Linux version 4.19.0-desktop-amd64 (deepin@deepin-PC) (Uos 8.3.0.3-3+rebuild)
|
||||
处理器: Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz (八核 / 十六逻辑处理器)
|
||||
主板: B460M-HDV(RD)
|
||||
内存: 8GB(TF32D4U2S1MEH-8 DDR4 2933MHz (0.3ns))/8GB(TF32D4U2S1MEH-8 DDR4 2933MHz (0.3ns))
|
||||
显示适配器: TU116 [GeForce GTX 1660]
|
||||
```
|
||||
|
||||
### 1、虚拟环境安装
|
||||
|
||||
```shell
|
||||
cd ~
|
||||
wget -c https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh
|
||||
bash Miniconda3-latest-Linux-x86_64.sh
|
||||
```
|
||||
|
||||
一路回车
|
||||
|
||||
第一次提示输入 yes/no :输入 `yes`
|
||||
|
||||
继续一路回车
|
||||
|
||||
第二次提示:输入 `no`
|
||||
|
||||
```shell
|
||||
cd ~/miniconda3/bin
|
||||
sudo chmod 777 activate
|
||||
```
|
||||
|
||||
激活conda环境
|
||||
|
||||
```shell
|
||||
. ./activate
|
||||
```
|
||||
|
||||
添加公司内网源
|
||||
|
||||
```shell
|
||||
conda config --add channels bioconda
|
||||
conda config --add channels conda-forge
|
||||
```
|
||||
|
||||
如果是外网添加外网源
|
||||
|
||||
```shell
|
||||
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
|
||||
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
|
||||
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/
|
||||
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/bioconda/
|
||||
```
|
||||
|
||||
如果要删除源
|
||||
|
||||
```shell
|
||||
conda config --remove-key channels
|
||||
```
|
||||
|
||||
### 2、安装依赖
|
||||
|
||||
#### (1)创建虚拟环境
|
||||
|
||||
```shell
|
||||
conda remove --name mmlab --all # 移除所有虚拟环境
|
||||
```
|
||||
```shell
|
||||
conda create -n mmlab python=3.7
|
||||
```
|
||||
|
||||
```shell
|
||||
conda activate mmlab
|
||||
```
|
||||
|
||||
|
||||
#### (2)安装 Pytorch
|
||||
|
||||
在mmlab虚拟环境中执行
|
||||
|
||||
```shell
|
||||
pip install torch==1.7.0+cu101 torchvision==0.8.1+cu101 torchaudio==0.7.0 -f https://download.pytorch.org/whl/torch_stable.html -i http://pypi.douban.com/simple --trusted-host pypi.douban.com
|
||||
```
|
||||
#### (3)安装 MMCV
|
||||
|
||||
在mmlab虚拟环境中执行
|
||||
|
||||
```shell
|
||||
pip install mmcv-full==1.3.3 -f https://download.openmmlab.com/mmcv/dist/cu101/torch1.7.0/index.html -i http://pypi.douban.com/simple --trusted-host pypi.douban.com
|
||||
```
|
||||
|
||||
#### (4)安装显卡驱动
|
||||
|
||||
根据你的显卡下载对应驱动,比如我的显卡为 `GTX1660`,驱动版本为 430,驱动下载地址:https://www.nvidia.cn/Download/Find.aspx?lang=cn
|
||||
|
||||
```shell
|
||||
CTRL+ALT+F2 进入tty
|
||||
```
|
||||
|
||||
禁用 nouveau 驱动
|
||||
|
||||
```shell
|
||||
sudo vim /etc/modprobe.d/blacklist-nouveau.conf
|
||||
```
|
||||
|
||||
填入:
|
||||
|
||||
```shell
|
||||
blacklist nouveau
|
||||
options nouveau modeset=0
|
||||
```
|
||||
|
||||
刷新配置文件
|
||||
|
||||
```shell
|
||||
sudo update-initramfs -u
|
||||
```
|
||||
|
||||
reboot 重启后再进入 tty
|
||||
|
||||
关闭图像界面,输入命令关闭图像界面
|
||||
|
||||
```shell
|
||||
sudo service lightdm stop
|
||||
```
|
||||
|
||||
安装驱动
|
||||
|
||||
```shell
|
||||
sudo chmod a+x NVIDIA-Linux-x86_64-430.run
|
||||
sudo ./NVIDIA-Linux-x86_64-430.run
|
||||
```
|
||||
|
||||
reboot重启,nvidia-smi查看安装状态。注意看下cuda版本,10.1,driver版。
|
||||
|
||||
## 三、数据标注
|
||||
|
||||
### 1、数据标注
|
||||
|
||||
标注之前需要先转换图片大小,并且以数字命名,每组命名递增
|
||||
|
||||
#### (1)转换大小并重命名
|
||||
|
||||
rename_pic.py
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
import cv2
|
||||
import time
|
||||
import getpass
|
||||
username = getpass.getuser()
|
||||
|
||||
source_path = f"/home/{username}/Desktop/right_menu" # 图片路径
|
||||
|
||||
source_dest = os.path.join('/'.join(source_path.split("/")[:-1]), 'tmp')
|
||||
if not os.path.exists(source_dest):
|
||||
os.mkdir(source_dest)
|
||||
|
||||
start_name = sys.argv[1]
|
||||
|
||||
file_name_list = list()
|
||||
for file in os.listdir(source_path):
|
||||
if file.endswith('.png'):
|
||||
file_name_list.append(file)
|
||||
start_name = int(start_name)
|
||||
for file in file_name_list:
|
||||
os.rename(os.path.join(source_path,file), os.path.join(source_path, f"{str(start_name)}.png"))
|
||||
start_name += 1
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
for file in os.listdir(source_path):
|
||||
image = os.path.join(source_path, str(file))
|
||||
src = cv2.imread(image)
|
||||
result = cv2.resize(src, (960, 540))
|
||||
resizeImage = os.path.join(source_dest, str(file))
|
||||
print(resizeImage)
|
||||
cv2.imwrite(str(resizeImage), result)
|
||||
cv2.waitKey(0)
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
os.system(f'rm -rf {source_path}/*')
|
||||
os.system(f'mv {source_dest}/* {source_path}')
|
||||
os.system(f'rm -rf {source_dest}')
|
||||
|
||||
print("下一个序号:", start_name)
|
||||
```
|
||||
|
||||
根据终端输出的下一个序号的提示,执行 Python 文件的时候传参。
|
||||
|
||||
```shell
|
||||
python rename_pic.py 249
|
||||
```
|
||||
|
||||
#### (2)工具标注
|
||||
|
||||
使用工具 labelImg 标注
|
||||
|
||||
```shell
|
||||
sudo pip3 install PyQt5==5.13
|
||||
sudo pip3 install labelImg
|
||||
```
|
||||
|
||||
终端直接输入 labelImg,回车
|
||||
|
||||
标注模式选择:`PascalVOC`
|
||||
|
||||
### 2、智能标注
|
||||
|
||||
打开屏幕录制软件,录制手动操作一次测试用例,然后将录制的视频进行分帧,然后对比前两帧图标相似度,剔除相似度过高的图片,在保证素材多样性的前提下,剔除重复多余的图片生成待标注的素材集,然后取出控件模板数据集,通过 OpenCV 模板匹配获取控件在待标注的素材图片中所在的左上、左下、右上、右下四处坐标,从而根据模板数据集生成标注数据。参考目录 AnnotationMaterial
|
||||
|
||||
#### (1)模板数据集准备
|
||||
|
||||
1、在界面截取需要标注的元素控件,放在目录下 AnnotationMaterial/template/img
|
||||
|
||||
2、维护控件名与元素控件的键对,一个控件名可对应多个元素控件
|
||||
|
||||
```yaml
|
||||
menu:
|
||||
- menu.jpg
|
||||
- menu1.jpg
|
||||
- menu2.jpg
|
||||
- menu3.jpg
|
||||
```
|
||||
|
||||
#### (2)素材集准备
|
||||
|
||||
录制视频或或屏幕截图放置目录 AnnotationMaterial/source
|
||||
|
||||
#### (3)开始标注
|
||||
|
||||
```shell
|
||||
python3 AnnotationMaterial/main.py
|
||||
```
|
||||
|
||||
生成的标注数据存放在 AnnotationMaterial/result
|
||||
|
||||
## 四、MMDetection
|
||||
|
||||
### 1、 MMDetection 代码
|
||||
|
||||
MMDetection 是一个有名的深度学习目标检测开源项目,也是 `openMMlab` 的招牌项目,为什么选择它,小孩儿没娘说来话长,感兴趣可以自行深入了解;
|
||||
|
||||
你可以直接在 GitHub 上克隆代码,也可以直接使用咱们仓库下 Train 目录中代码,Train 中的代码也是从 GitHub 上拉取的,不过我们根据项目需要做了一些二次开发,推荐直接使用它。
|
||||
|
||||
如果你想从 GitHub 上拉:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/open-mmlab/mmdetection.git
|
||||
# 不能直接拉取主分支,建议使用2.12版本,不同的版本对应的mmcv版本是不同的
|
||||
```
|
||||
|
||||
### 2、拉取 voc2coco 代码
|
||||
|
||||
```shell
|
||||
git clone https://github.com/Tony607/voc2coco.git
|
||||
```
|
||||
|
||||
### 3、转换 coco 数据集
|
||||
|
||||
将所有的图片和xml文件放入train2017,从中挑选几组放入val2017(测试集)
|
||||
|
||||
```shell
|
||||
cd mmdetection/data/coco
|
||||
python voc2coco.py train2017 annotations/instances_train2017.json
|
||||
python voc2coco.py val2017 annotations/instances_val2017.json
|
||||
```
|
||||
|
||||
生成json文件
|
||||
|
||||
### 4、修改配置
|
||||
|
||||
**(1)读取模型名称**
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
module_name = []
|
||||
with open('./instances_train2017.json', "r+") as f:
|
||||
json_file = f.read()
|
||||
json_dict = json.loads(json_file)
|
||||
module_list = json_dict.get('categories')
|
||||
for module_info in module_list:
|
||||
name = module_info.get('name')
|
||||
module_name.append(name)
|
||||
print(module_name)
|
||||
print("module_num:", len(module_name))
|
||||
```
|
||||
|
||||
```shell
|
||||
# 注意对比instances_train2017.json里面模型名称的顺序,与CLASSES和coco_classes里面的顺序保持一致。
|
||||
```
|
||||
|
||||
**(2)修改 faster_rcnn_r101_2x_coco.py**
|
||||
|
||||
`mmdetection/xianjin/faster_rcnn_r101_2x_coco.py`
|
||||
|
||||
修改46行,`num_clasess`的值,新增1个,就 +1;
|
||||
|
||||
**(3)修改 coco.py**
|
||||
|
||||
`mmdetection/mmdet/datasets/coco.py`
|
||||
|
||||
CLASSES = (),在里面添加模型名称;
|
||||
|
||||
**(4)修改 class_names.py**
|
||||
|
||||
`mmdetection/mmdet/core/evaluation/class_names.py`
|
||||
|
||||
`coco_classes` 里面添加模型名称;
|
||||
|
||||
### 5、缓存清理
|
||||
|
||||
删除 mmdetection/build 目录
|
||||
|
||||
```python
|
||||
python setup.py install
|
||||
```
|
||||
|
||||
### 6、训练模型
|
||||
|
||||
(1)指定自己配置的训练模型;
|
||||
```shell
|
||||
python tools/train.py xianjin/faster_rcnn_r101_fpn_2x_coco.py --gpus 1
|
||||
```
|
||||
|
||||
(2)查看训练结果的测试集结果;
|
||||
|
||||
```shell
|
||||
python tools/train.py xianjin/faster_rcnn_r101_fpn_2x_coco.py xianjin/epoch_24.pth --show
|
||||
```
|
||||
(3)查看训练结果的准确度;
|
||||
```shell
|
||||
python tools/analysis_tools/analyze_logs.py plot_curve xianjin/20210530_011907.log.json --keys acc
|
||||
```
|
||||
|
||||
### 7、快捷操作
|
||||
|
||||
```shell
|
||||
python3 run.py
|
||||
```
|
||||
|
||||
将以上 4 - 7 步操作整合成 run.py,一键完成。
|
||||
|
||||
## 五、硬件环境
|
||||
|
||||
#### 1、设备清单
|
||||
|
||||
- 测试机至少一台;
|
||||
- 服务端一台(目前仅支持AMD架构);
|
||||
- 采集盒一个 (ACASIS hdmi视频采集卡,https://item.jd.com/10024310742602.html );
|
||||
- USB串口转USB键鼠协议线(优胜电子科技USB串口转USB键鼠协议线B类,https://item.taobao.com/item.htm?spm=a1z10.3-c.w4002-1385258877.57.7ea037891D6nKh&id=611894882981 );
|
||||
|
||||
#### 2、硬件环境搭建
|
||||
|
||||
- 采集盒,HDMI 端连接测试机, USB 端连接到服务器上;
|
||||
- USB 串口线,白色端连接服务器,黑色端连接测试机;
|
||||
- 如果需要可以接USB延长线;
|
||||
- 测试机上可以不接显示器;
|
||||
|
||||
#### 3、推荐配置
|
||||
|
||||
执行用例的服务端对配置没有特殊要求,如果条件允许配置越高,识别速度用例执行速度越快。
|
||||
|
||||
```shell
|
||||
处理器: Intel(R) Core(TM) i3-10100 CPU @ 3.60GHz (四核 / 八逻辑处理器)
|
||||
主板: B460-N2(J)
|
||||
内存: 8GB(TF32D4U2S1MEH-8 DDR4 3200MHz (0.3ns))
|
||||
显示适配器: UHD Graphics 630
|
||||
存储设备: FORESEE P900F256GBH (256 GB)/ST1000DM003-1SB102 (1.00 TB)
|
||||
```
|
||||
|
||||
测试机上不执行任何脚本,具体配置根据测试需要决定。
|
||||
|
||||
## 六、USB_MK串口驱动方法
|
||||
|
||||
位于control_method目录下的usb_mk.py文件
|
||||
|
||||
(1)列出可通信的端口
|
||||
|
||||
```shell
|
||||
python3 -m serial.tools.list_ports -v
|
||||
```
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054cb661c6c8e54ff5068.png)
|
||||
|
||||
(2)修改串口的权限
|
||||
|
||||
```shell
|
||||
sudo chmod 777 /dev/ttyACM0
|
||||
```
|
||||
|
||||
(3)实例化USB_MK
|
||||
|
||||
```python
|
||||
import UsbMk
|
||||
usb_mk = UsbMk("/dev/ttyACM0", 9600) # 设备默认9600为波特率,控制传输速率
|
||||
```
|
||||
|
||||
(4)USB串口设备默认波特率为9600(经测试波特率9600准确性最好)。
|
||||
|
||||
### 1、键盘操作
|
||||
(1)按下键盘按键
|
||||
```python
|
||||
usb_mk.press_key("enter") # 按下键盘enter键
|
||||
```
|
||||
|
||||
(2)按下键盘按键并且不放
|
||||
```python
|
||||
usb_mk.press_key_down("enter") # 按下键盘enter键不放
|
||||
```
|
||||
|
||||
(3)组合按键
|
||||
```python
|
||||
usb_mk.hot_key("ctrl", "alt", "T") # 按下组合按键ctrl+alt+T调起终端
|
||||
```
|
||||
|
||||
(4)组合按键不放
|
||||
```python
|
||||
usb_mk.hot_key_down("ctrl", "alt", "T") # 按下组合按键ctrl+alt+T调起终端不放
|
||||
```
|
||||
|
||||
(5)释放所有键盘按键
|
||||
```python
|
||||
usb_mk.key_up() # 释放所有键盘按键
|
||||
```
|
||||
|
||||
(6)键盘输入字符串(汉字会自动转成拼音输入)
|
||||
```python
|
||||
usb_mk.hot_key("ctrl", "alt", "T") # 按下组合按键ctrl+alt+T调起终端
|
||||
usb_mk.input_text("reboot") # 输入字符串reboot
|
||||
usb_mk.press_key("enter") # 按下键盘enter键
|
||||
```
|
||||
|
||||
### 2、鼠标操作
|
||||
(1)按下鼠标左键
|
||||
```python
|
||||
usb_mk.click() # 按下鼠标左键
|
||||
```
|
||||
|
||||
(2)按下鼠标左键不放
|
||||
```python
|
||||
usb_mk.mouse_down() # 按下鼠标左键不放
|
||||
```
|
||||
|
||||
(3)释放鼠标所有按键
|
||||
```python
|
||||
usb_mk.mouse_up() # 释放鼠标所有按键
|
||||
```
|
||||
|
||||
(3)按下鼠标右键
|
||||
```python
|
||||
usb_mk.right_click() # 释放鼠标所有按键
|
||||
```
|
||||
|
||||
(4)鼠标左键双击
|
||||
```python
|
||||
usb_mk.double_click() # 释放鼠标所有按键
|
||||
```
|
||||
|
||||
(5)恢复鼠标至初始位置,默认左上角
|
||||
```python
|
||||
usb_mk.move_to_init() # 恢复鼠标至初始位置,默认左上角
|
||||
```
|
||||
|
||||
(6)移动鼠标至相对坐标
|
||||
```python
|
||||
usb_mk.move_rel(100, 200) # 鼠标向左边移动100个像素,向下移动200个像素
|
||||
```
|
||||
|
||||
(7)以屏幕左上角为圆心坐标移动鼠标至屏幕绝对坐标
|
||||
```python
|
||||
usb_mk.move_to(100, 200) # 鼠标移动至屏幕坐标(100, 200)
|
||||
# 注:因Linux系统不支持鼠标绝对路径,所以会鼠标会先移动到初始位置
|
||||
```
|
||||
|
||||
(8)按下鼠标左键,拖动到绝对坐标位置
|
||||
```python
|
||||
usb_mk.drag_to(100, 200) # 按下鼠标拖动至屏幕坐标(100, 200)
|
||||
# 注:因Linux系统不支持鼠标绝对路径,所以会鼠标会先移动到初始位置
|
||||
```
|
||||
|
||||
(9)按下鼠标左键,拖动到相对坐标位置
|
||||
```python
|
||||
usb_mk.drag_rel(100, 200) # 鼠标向左边移动100个像素,向下移动200个像素
|
||||
```
|
||||
|
||||
## 七、方法调用入参规则
|
||||
|
||||
### 1、基类
|
||||
|
||||
```python
|
||||
class Base:
|
||||
|
||||
def __init__(self):
|
||||
self._obj = image
|
||||
self._usb = usb_mk
|
||||
|
||||
def find_element_by_ai(self, element):
|
||||
return self._obj.find_element(element)
|
||||
```
|
||||
|
||||
### 2、入参说明
|
||||
|
||||
图像识别方法入口为 Base 类下的 find_element_by_ai 。仅接受一个参数 element,类型为 string。
|
||||
|
||||
可识别的 string 格式:
|
||||
|
||||
(1)大图标( 图标大于 40 像素):识别某个大图标,例:
|
||||
|
||||
```python
|
||||
Base().find_element_by_ai("window")
|
||||
```
|
||||
|
||||
(2)大图标 / 小图标(图标小于 40 像素):识别某个小图标,例:window/search_btn
|
||||
|
||||
```python
|
||||
Base().find_element_by_ai("window/search_btn")
|
||||
```
|
||||
|
||||
(3)大图标 / OCR:识别某个大图标内的文字,例:menu/属性
|
||||
|
||||
```python
|
||||
Base().find_element_by_ai("menu/属性")
|
||||
```
|
||||
|
||||
(4)OCR:全屏识别文字
|
||||
|
||||
```python
|
||||
Base().find_element_by_ai("搜索")
|
||||
```
|
||||
|
||||
## 八、隐藏鼠标
|
||||
|
||||
通过采集盒传输过来的视频流,在识别过程中可能受到鼠标影响,因此用例执行过程中需要隐藏鼠标。
|
||||
|
||||
屏蔽鼠标光标显示
|
||||
|
||||
```shell script
|
||||
/etc/lightdm/lightdm.conf
|
||||
```
|
||||
在`[Seat:*]`下面添加参数
|
||||
```shell script
|
||||
xserver-command=X -bs -core -nocursor
|
||||
```
|
||||
|
||||
|
||||
## 九、智能自动化的意义
|
||||
|
||||
**1、改善 Acessibility 定位的稳定性问题**
|
||||
|
||||
Acessibility 属性定位存在偶尔失效的情况
|
||||
|
||||
(1)开发在做新需求开发时可能涉及到对属性标签的修改或者层级的调整,可能造成自动化用例无法定位到元素。
|
||||
|
||||
- 比如1040阶段,音乐重构的时候,开发对部分属性和层级做了修改,导致自动化用例定位方法80%需要修改。
|
||||
- 开发新功能的时候偶尔也会对属性做调整,遇到5次。
|
||||
|
||||
(2)目前主要使用Dogtail识别应用Acessibility属性进行定位,是获取应用当前界面的属性tree,在应用跳转后,Dogtail存在无法及时获取当前属性列表的情况。
|
||||
|
||||
- 多媒体调用文管窗口的时候,出现找不到文管里面元素的情况,目前出现10+次。
|
||||
- 应用设置页面的属性偶尔存在找不到的情况,遇到5次。
|
||||
|
||||
(3)Dogtail工具本身存在问题,有时定位元素的时候很慢,让人无法接受,目前出现过很多次。
|
||||
|
||||
- 执行用例的时候,定位元素的时间比平常慢3倍,出现4次。
|
||||
|
||||
**2、解决了使用绝对图像识别定位的容错性差的问题**
|
||||
|
||||
部分控件无法添加 Acessibility 属性从而使用图像识别定位出现的容错性差的问题。
|
||||
|
||||
(1)图像识别定位方法需要维护大量的目标图片资源,在 UI 发生变化之后,会涉及到目标资源的替换,比较耗费人力。
|
||||
|
||||
(2)图像识别的定位方法对比精度比较搞,如果UI 的变更会造成无法准确定位到,自动化脚本健壮性不足。
|
||||
|
||||
**3、使用机器分离,解决了自动化脚本对应用的影响,真实还原用户的使用场景**
|
||||
|
||||
机器分离的架构设计,保证了测试机的测试环境完全等同于用户,排除测试脚本的影响。
|
||||
|
||||
(1)目前的功能自动化是在测试机上直接运行自动化测试脚本,加上用例执行过程中我们添加了一些进程监控、视频录制等功能,自动化脚本本身会消耗一些系统资源,无法还原真是的用户使用场景。
|
||||
|
||||
(2)性能测试需要排除其他程序的资源消耗影响,性能自动化采用机器分离,保证了性能数据的准确性。
|
||||
|
||||
以前的用例执行耗时没条在30秒左右,现在用例耗时在10秒。
|
||||
|
||||
**4、解决了UI 调整导致定位失败的不稳定性问题**
|
||||
|
||||
(1)解决了因 UI 调整导致控件位置变化或色差变化后定位不准确的问题,只要控件文案和控件外边框无变化均可准确定位;
|
||||
|
||||
(2)目前也会采用基于UI的定位方案,如果UI位置有调整,会导致元素无法定位。
|
||||
|
||||
**5、简化了用例结构,提升编写友好度**
|
||||
|
||||
(1)智能自动化识别元素的方法只有一个,就是基于我们训练的模型识别视频流中的元素,经过脚本封装之后,在自动化用例中所有定位的操作都只需要调用这一个方法就行了,编写自动化脚本的难度大大降低。
|
||||
|
||||
(2)也是由于调用方法简单,在 UI 自动化测试框架中的分层结构将会减少,多继承的情况会减少,自动化代码的调用结构也会变得简单,而且功能测试同学编写自动化用例会更加容易上手。
|
||||
|
||||
以前写用例由于需要结合不同的模块,调用不同的方法,编写一条用例并完成调试的时间平均在20-30分钟,现在写用例脚本能在5分钟左右完成。
|
|
@ -0,0 +1,803 @@
|
|||
# 智能化性能测试
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c7661c6c8e54ff4be0.png)
|
||||
|
||||
~~~shell
|
||||
# =============================================
|
||||
# Attribution :Chengdu Test Team
|
||||
# Time : 2023/1/3
|
||||
# =============================================
|
||||
~~~
|
||||
|
||||
## 一、简述
|
||||
|
||||
智能化性能测试
|
||||
|
||||
通过自动化手段对应用进行性能测试,提供方便的环境部署、运行配置、用例编写等,用于桌面应用的冷热启动、资源拷贝、页面跳转等性能场景测试。
|
||||
|
||||
仓库地址:[https://gerrit.uniontech.com/admin/repos/autotest-perf-aitest](https://gerrit.uniontech.com/admin/repos/autotest-perf-aitest)
|
||||
|
||||
## 二、代码结构
|
||||
|
||||
```shell
|
||||
autotest-perf-aitest
|
||||
├── apps # 存放所有应用性能测试脚本
|
||||
├── conftest.py # pytest hook相关方法
|
||||
├── CURRENT # 版本内容
|
||||
├── docs # 文档
|
||||
│ ├── 键值对照表.pdf # 串口线code与按键的对照
|
||||
│ ├── 性能自动化测试脚本执行器使用指南.md # 执行器脚本使用指南
|
||||
│ ├── USB串口转USB键盘鼠标线协议文件V1.0.pdf # 串口协议
|
||||
│ └── xserver-xorg-input-libinput-0.28.2.zip # Linux下绝对坐标操作依赖的源码,可根据架构打包
|
||||
├── manage.py # 功能主入口
|
||||
├── pytest.ini # pytst 配置
|
||||
├── README.md
|
||||
├── setting # 设置
|
||||
│ ├── client.ini # 测试机 IP 设置
|
||||
│ ├── config.py # 全局配置
|
||||
│ ├── dev_env.sh # 宿主机环境部署脚本
|
||||
│ ├── docker-compose_no_relay.yml # 无继电器容器集群
|
||||
│ ├── docker-compose.yml # 有继电器容器集群
|
||||
│ ├── docker_login.sh # docker 登陆
|
||||
│ ├── __init__.py
|
||||
│ ├── res
|
||||
│ │ ├── black.png # 全黑图片,用于修改壁纸
|
||||
│ │ ├── xserver-xorg-input-libinput_0.28.2-2_5000.deb # 3A5000 架构鼠标绝对坐标依赖
|
||||
│ │ ├── xserver-xorg-input-libinput_0.28.2-2_amd64.deb # amd 架构鼠标绝对坐标依赖
|
||||
│ │ ├── xserver-xorg-input-libinput_0.28.2-2_arm64.deb # arm 架构鼠标绝对坐标依赖
|
||||
│ │ ├── xserver-xorg-input-libinput_0.28.2-2_mips64.deb # mips 架构鼠标绝对坐标依赖
|
||||
│ │ └── xserver-xorg-input-libinput_0.28.2-2_sw_64.deb # sw 架构鼠标绝对坐标依赖
|
||||
│ ├── server.ini # 服务器配置
|
||||
│ ├── setup.sh # 容器环境部署脚本
|
||||
│ ├── template # 创建 app 基础脚本模板
|
||||
│ │ └── app_template
|
||||
│ │ ├── ${app_name}_widget.py-tpl
|
||||
│ │ ├── control-tpl
|
||||
│ │ ├── images
|
||||
│ │ │ └── readme
|
||||
│ │ ├── __init__.py-tpl
|
||||
│ │ ├── test_${app_name}_01_scene.json-tpl
|
||||
│ │ └── test_${app_name}_01_scene.py-tpl
|
||||
│ └── uos_docker # 容器环境配置
|
||||
│ ├── dockerfile
|
||||
│ └── requirements.txt
|
||||
└── src
|
||||
├── analysis_save.py # 解析图片流结果,汇总生成 excel 报告
|
||||
├── base_widget.py # 封装的基础操作,应用调用的主入口方法
|
||||
├── cmdctl.py # 调用命令行使用
|
||||
├── global_value.py # 全局变量
|
||||
├── image_utils.py # 图片相关方法
|
||||
├── __init__.py
|
||||
├── logger_utils.py # 日志配置
|
||||
├── pdocr.py # ocr 配置
|
||||
├── public_method.py # 一些方便的封住
|
||||
├── record.py # 录屏相关
|
||||
├── rtk # 运行相关
|
||||
│ ├── _base.py # 一些枚举
|
||||
│ ├── __init__.py
|
||||
│ ├── install_env.py # 安装环境
|
||||
│ └── local_runner.py # 运行用例
|
||||
├── serial_communication # 串口相关
|
||||
│ ├── __init__.py
|
||||
│ ├── power_control.py # 继电器操作,使用继电器连接主机电源,控制开关机
|
||||
│ ├── serial_common.py # 串口公共方法
|
||||
│ └── usb_mk.py # 键鼠操作基础封装
|
||||
├── shortcut_key.py # 快捷键的封装
|
||||
├── singleton.py # 单例模式
|
||||
├── start_app.py # 创建 app
|
||||
└── write_xlsx.py # 生成 excel
|
||||
|
||||
# __init__.py 文件为标识该目录是一个Python的模块包,项目中的__init__.py均为空文件。
|
||||
```
|
||||
|
||||
## 三、环境搭建
|
||||
|
||||
### 1、硬件环境
|
||||
|
||||
#### (1)设备清单
|
||||
|
||||
- 测试机至少一台。
|
||||
- 服务端一台。(仅支持 `AMD` 架构,若需其他架构,咨询相关开发人员)
|
||||
- 采集盒一个。 (`ACASIS` `HDMI` 视频采集盒,https://item.jd.com/100013302599.html#crumb-wrap )
|
||||
- USB串口转USB键鼠协议线。(优胜电子科技USB串口转USB键鼠协议线B类,https://item.taobao.com/item.htm?spm=a1z10.3-c.w4002-1385258877.57.7ea037891D6nKh&id=611894882981 )
|
||||
|
||||
|
||||
#### (2)硬件环境搭建
|
||||
|
||||
- 采集盒,`HDMI` 端连接测试机, `USB` 端连接到服务器上。
|
||||
- `USB` 串口线,白色端连接服务器,黑色端连接测试机(`USB3.0` 接口上)。
|
||||
- 如果需要可以接 `USB` 延长线。
|
||||
- 测试机上可以不接显示器。
|
||||
|
||||
#### (3)服务端推荐配置
|
||||
|
||||
配置越高,解析速度越快。
|
||||
|
||||
```shell
|
||||
处理器: Intel(R) Core(TM) i3-10100 CPU @ 3.60GHz (四核 / 八逻辑处理器)
|
||||
主板: B460-N2(J)
|
||||
内存: 8GB(TF32D4U2S1MEH-8 DDR4 3200MHz (0.3ns))
|
||||
显示适配器: UHD Graphics 630
|
||||
存储设备: FORESEE P900F256GBH (256 GB)/ST1000DM003-1SB102 (1.00 TB)
|
||||
```
|
||||
|
||||
### 2、测试机环境搭建
|
||||
|
||||
- 打开控制中心,账户设置为【自动登录】和【无密码登录】,电源管理中使用电源全部设置为【从不】,开启开发者模式。
|
||||
|
||||
- 根据应用性能测试要求,导入相关测试资源,比如:导入相册、影院、音乐的测试资源。
|
||||
|
||||
- 在服务端打开相机应用后,将测试机桌面壁纸更换为纯色壁纸(注意,服务端需提前开启相机后再设置测试机壁纸,建议纯黑壁纸 `setting/res/black.png`)
|
||||
|
||||
- 在文件管理器中,将选项 主菜单-设置-预览-图片预览 取消勾选(如不取消,会影响截图录屏应用的性能数据)
|
||||
|
||||
- 移动或删除系统 Logo ,排除桌面右下角 Logo 的影响(重启生效)。
|
||||
|
||||
```shell
|
||||
sudo mv /usr/share/deepin/uos_logo.svg ~/Pictures/
|
||||
```
|
||||
|
||||
- 安装鼠标绝对坐标操作依赖(必须安装!!)
|
||||
- `AMD` 安装 `setting/res ` 目录下 `xserver-xorg-input-libinput_0.28.2-2_amd64.deb`
|
||||
|
||||
- `ARM` 安装 `setting/res ` 目录下 `xserver-xorg-input-libinput_0.28.2-2_arm64.deb`
|
||||
|
||||
- `MIPS` 安装 setting/`res ` 目录下 `xserver-xorg-input-libinput_0.28.2-2_mips64.deb`
|
||||
|
||||
- `3A5000` 安装 `setting/res ` 目录下 `xserver-xorg-input-libinput_0.28.2-2_5000.deb`
|
||||
|
||||
- 安装命令
|
||||
|
||||
```shell
|
||||
sudo dkpg -i xserver-xorg-input-libinput_0.28.2-2_amd64.deb
|
||||
```
|
||||
|
||||
安装时会提示已存在相同版本,请忽略,直接覆盖安装,安装完成后,必须重启主机才生效
|
||||
|
||||
### 3、服务端一键环境部署
|
||||
|
||||
环境搭建前必须先连接上 `USB` 串口线和采集盒。
|
||||
|
||||
宿主机环境安装
|
||||
|
||||
```shell
|
||||
python3 manage.py install --env host
|
||||
```
|
||||
|
||||
容器环境安装
|
||||
|
||||
```shell
|
||||
python3 manage.py install --env docker
|
||||
```
|
||||
|
||||
## 四、配置项
|
||||
|
||||
### 1、服务端配置
|
||||
|
||||
`setting/config/server.ini`
|
||||
|
||||
用于配置测试过程中的一些配置项,包含了所有服务端所有的可配置项。
|
||||
|
||||
- **本地无网络运行**
|
||||
- 将配置项 `network=0` 即可开始测试。
|
||||
- **本地有网络运行**
|
||||
- 配置项 `network=1`
|
||||
- (1)配置项 `client_alias=` 配置为你要测试的测试机别名,如 `Alias1` 测试机别名在 `setting/config/client.ini` 中进行配置,此时,直接运行 `python3 manager` 即可开始测试。
|
||||
- (2)配置项 `client_alias=` 为空,测试机别名在 `config/client.ini` 中进行配置,此时,直接运行 `python3 manager --client_alias Alias1`,即可开始测试。(`Alias1` 为你配置的测试机别名)
|
||||
- (1)和(2)只要使用其一即可。
|
||||
- **有网络运行,且需要通过 `Jenkins` 上运行一个 `job` ,同时控制多台机器。**
|
||||
- 配置项 `network=1`
|
||||
- 配置项 `client_alias=` 不用配置,在运行 `run.sh` 的时候直接空格加别名即可,比如:测试机别名为 `Alias1` ,且已经在 `config/client.ini` 中配置了 `Alias1` 的`user` `ip` `password` ,则运行 `bash run.sh Alias1` 即可开始测试。
|
||||
- 在 `Jenkins` `pipeline` 里面通过并行 `stage` 或并行调用下层任务的方式可以实现大规模的性能测试需求。
|
||||
|
||||
**详细配置项说明:**
|
||||
|
||||
```ini
|
||||
;=============================== CASE CONFIG ===================================
|
||||
[case]
|
||||
;执行的应用名称
|
||||
;为空表示执行 apps/ 目录下所有应用的用例
|
||||
APP_NAME =
|
||||
;执行包含关键词的用例
|
||||
KEYWORDS =
|
||||
;执行包含用例标签的用例
|
||||
TAGS =
|
||||
;1、KEYWORDS 和 TAGS 都为空表示执行 APP_NAME 的所有用例
|
||||
;2、KEYWORDS 和 TAGS 都支持逻辑组合,即 and/or/not 的表达式, e.g. TAGS = L1 or smoke
|
||||
|
||||
[case_conf]
|
||||
# 尾帧识别的方式stage按阶段划分,ocr通过文案识别,match 图片匹配,ocr文案识别较慢,不推荐使用
|
||||
find_by = stage
|
||||
# 最后一个稳定阶段的帧数识别
|
||||
# 首帧存在后的第一个大于该配置帧数的阶段的首帧作为性能尾帧
|
||||
final_stable_num=100
|
||||
# 从该帧数开始识别文案或图片,减少分析时间
|
||||
start_num = 150
|
||||
# 识别的文案
|
||||
text=
|
||||
# 识别图片
|
||||
match=apps/xxx/images/match.png
|
||||
# 识别相似度
|
||||
match_rate=0.9
|
||||
# 用例操作执行之后,停止录屏之前的等待时间,单位秒
|
||||
# 比如热启动用例,从点击打开到停止录屏,中间等待5秒;
|
||||
duration = 15
|
||||
# 单条用例执行的次数
|
||||
count = 5
|
||||
# 稳定阶段划分的相似度
|
||||
stable_similarity = 0.9997
|
||||
|
||||
[server]
|
||||
# 调试模式=1,为开启调试模式,0为关闭。
|
||||
# 调试模式下默认只执行一次用例,不关机
|
||||
# 编写用例的过程中建议开启maintain。
|
||||
MAINTAIN = 0
|
||||
# 运行的环境docker容器环境,host宿主机环境
|
||||
ENV = docker
|
||||
# 服务端密码
|
||||
PASSWORD=1
|
||||
# 指定测试机别名
|
||||
# 测试机别名在 config/client.ini 中进行配置,比如:
|
||||
# [AMD]
|
||||
# ip=10.8.15.86
|
||||
# user=uos
|
||||
# 其中 AMD 为测试机别名,则client_alias=AMD
|
||||
CLIENT_ALIAS = x86(Intel-i7)
|
||||
# 1 开启网络, 0 关闭网络
|
||||
# (1)服务端与测试机须在同一网段下,你可以尝试在服务端通过ssh连接测试机,如果可以连接,说明网络正常。
|
||||
# (2)当input_type=FILE_NAME 时,必须开启网络。
|
||||
NETWORK = 1
|
||||
# 性能结果统计单位 毫秒—ms, 秒—s
|
||||
UNIT=ms
|
||||
# 测试机开机进入桌面后等待时间,单位/秒
|
||||
STARTUP_DELAY = 60
|
||||
# 1为删除缓存,0为不删除缓存
|
||||
# 用例执行完后会在report目录下生成智能解析的一些缓存图片。
|
||||
CACHE = 0
|
||||
# 默认为空,报告会保存在项目下report目录中;
|
||||
# 指定报告目录则存放到指定目录。
|
||||
REPORT_PATH = report
|
||||
# 服务端从测试机获取的icon资源会缓存在项目下input目录中
|
||||
INPUT = input/
|
||||
# 1为测试完关机,0为测试完不关机
|
||||
SHUTDOWN = 0
|
||||
# 解析进程数(不推荐修改)
|
||||
# 3个进程解析视频已经能够满足,即使增加进程数也不能减少整体耗时
|
||||
# 另外还要看CPU核数是否支持。
|
||||
PARSING_PROCESS = 3
|
||||
# 1为使用继电器开关机,0为使用reboot命令重启。依赖继电器硬件设备,需要使用联系开发者
|
||||
RELAY = 0
|
||||
# 获取测试应用的版本, %s 占位应用名
|
||||
# 如果该配置为空,则不获取应用的版本信息。
|
||||
COMMAND = apt policy %s 2>/dev/null|grep "已安装"|awk -F ":" "{print \$2}"
|
||||
# 关机等待开启时间,单位秒
|
||||
# 根据测试机性能不同,开关机耗时可能不同,根据实际测试机情况配置。
|
||||
SHUTDOWN_DELAY = 30
|
||||
# 可用于图像识别的图片格式,以英文逗号隔开。
|
||||
# 该配置项不建议修改,目前我们仅验证了jpg,jpeg,png这三种常用的图片格式。
|
||||
INPUT_TYPES = jpg,jpeg,png
|
||||
# 性能分析补帧,等于或低于该值的不稳定阶段视为稳定阶段,在最后计算性能结果时需要减掉。
|
||||
UN_STAB_START = 3
|
||||
# 服务端获取测试机图像的帧率,即每秒获取多少张测试机上的图像。
|
||||
# 默认帧率为30帧/秒,该配置项不建议修改,可能会对解析结果造成偏差。
|
||||
FPS = 30
|
||||
# 以下场景类型会重启测试机,多种场景使用逗号隔开
|
||||
SCENE = cold
|
||||
;OCR服务端地址(不可随意修改)
|
||||
OCR_SERVER_HOST = http://10.8.13.78:8890
|
||||
#屏幕图片临时存放位置
|
||||
SCREEN_CACHE = /tmp/screen
|
||||
|
||||
# =======================================
|
||||
[log]
|
||||
# 日志级别,OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL
|
||||
# 用例执行过程中的一些日志输出,log_cli=INFO表示 仅输出INFO及以上级别的日志。
|
||||
LOG_LEVEL = DEBUG
|
||||
# 日志文件保存路径
|
||||
LOG_PATH= logs
|
||||
|
||||
# =======================================
|
||||
[dockerhub]
|
||||
# 容器登陆账号
|
||||
DOCKER_USER = user
|
||||
# 容器登陆密码
|
||||
DOCKER_PASSWORD = User1234
|
||||
```
|
||||
|
||||
### 2、测试机配置
|
||||
|
||||
`setting/config/client.ini`
|
||||
|
||||
用于配置测试机的信息。
|
||||
|
||||
- 有网络的情况下,服务端需要动态获取测试机上的一些资源,因此你需要写入 `测试机别名` 、 `user` 、`ip`、 `password` 。
|
||||
|
||||
```ini
|
||||
# 测试机别名
|
||||
[Alias1]
|
||||
# 测试机ip
|
||||
ip=10.8.15.86
|
||||
# 测试机用户名
|
||||
user=uos
|
||||
```
|
||||
|
||||
命名建议:`AliasA1`、`AliasA2`、`AliasB1`、`AliasB2`,不要重名。
|
||||
|
||||
- 无网络情况下,仅需要关注测试机分辨率配置项 `width=`、 `height=` 即可。
|
||||
|
||||
**详细配置项说明:**
|
||||
|
||||
```ini
|
||||
[client]
|
||||
# 默认测试机用户名,密码,ip
|
||||
ip=
|
||||
user=
|
||||
|
||||
# 测试机密码
|
||||
password=1
|
||||
|
||||
# 测试机主题
|
||||
theme=bloom
|
||||
|
||||
# 测试机icon目录
|
||||
path=/usr/share/icons
|
||||
|
||||
# 系统desktop文件的存放目录,UOS系统默认为/usr/share/applications
|
||||
desktop_path=/usr/share/applications
|
||||
|
||||
# 用于判断测试机是否开机进入桌面的识别标识。
|
||||
# 1、通过桌面图标识别。
|
||||
# - type=FILE_NAME时,服务器和测试机必须在同一局域网内。
|
||||
# - power_on=dde-computer表示通过桌面计算机图标判断开机进入桌面。
|
||||
# 2、通过本地图片作为开机识别。
|
||||
# - type= 传入为图片格式,type支持的类型包含在server.ini内配置项input_type=jpg,jpeg,png。
|
||||
# - power_on= 传入的是本地图片的路径。
|
||||
type=FILE_NAME
|
||||
power_on=dde-computer
|
||||
|
||||
# 测试机分辨率配置(测试机连接采集盒,采集盒的分辨率)
|
||||
height=1080
|
||||
width=1920
|
||||
|
||||
[AliasA1]
|
||||
ip=10.8.15.86
|
||||
user=uos
|
||||
# password=1 如果这里不写密码,默认取上面client里面的password
|
||||
|
||||
[AliasA2]
|
||||
ip=10.8.15.20
|
||||
user=uos
|
||||
# password=1 如果这里不写密码,默认取上面client里面的password
|
||||
```
|
||||
|
||||
### 3、场景个性化配置
|
||||
|
||||
随着测试场景的不断引入,发现现有服务端里的配置项并不适用于所有的性能场景,于是将服务端配置进行一个提取,可做单个场景的个性化配置,当然原来的服务端配置功能保留,只是当你配置了场景个性化配置后,优先使用个性化配置里面的内容,以下是对个性化配置的介绍。
|
||||
|
||||
目前每一个 py 文件对应一个性能测试场景,于是在同级目录下创建一个与之同名的 json 文件,作为个性化配置文件
|
||||
|
||||
```shell
|
||||
apps
|
||||
├── album
|
||||
├── test_album_01_start.json # 个性化配置文件
|
||||
└── test_album_01_start.py # 性能场景文件
|
||||
```
|
||||
|
||||
以下是对 json 文件中配置的解释,若某项配置不需要,可以缺失,但不能为空;
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "test_album_start", # 标题;必须
|
||||
"app_name": "deepin-album", # 场景所属的应用;必须
|
||||
"scene": "hot", # 场景别名;必须
|
||||
"icon": ["deepin-album"], # 这条用例里面需要用到的所有icon,用例执行之前会从测试机上拷贝所有需要用的icon到服务端,用于图像识别。;可缺失
|
||||
# 注意:列表中元素为应用desktop文件的文件名,相册的desktop文件名为deepin-album.desktop,
|
||||
# 只需要写deepin-album即可,“.desktop”已经在底层做了拼接。
|
||||
"size": { # 图片对比的区域设置项,某些应用当前屏幕的部分区域需要将其去掉,可以设置此项,如果没有则可不写;可确实
|
||||
"x-coordinate": 0, # 左上角横坐标
|
||||
"y-coordinate": 0, # 左上角纵坐标
|
||||
"width": 850, # 横向长度
|
||||
"height": 1920 # 纵向高度
|
||||
},
|
||||
"count": 5, # 场景执行的次数;必须
|
||||
"duration": 5, # 该条用例操作执行之后,停止录屏之前的等待时间,单位秒,针对于某些特定场景,该配置为可选项,覆盖 server.ini 中的配置;可缺失
|
||||
"stable_similarity": 0.9997, # 性能场景分阶段时的图片相似度对比,前后图片低于该值,则分为不稳定阶段;可缺失
|
||||
"find_by": "stage", # 尾帧识别的方式stage按阶段划分,ocr通过文案识别,match 图片匹配
|
||||
"final_stable_num": 100, # 查找最后一个大于该值的稳定阶段的首帧作为性能的尾祯;可缺失
|
||||
"start_num": 150,
|
||||
"text": "", # ocr识别的文案
|
||||
"match": "apps/xxx/images/match.png", # 图像匹配的图片文件路径
|
||||
"match_rate": 0.9 # 匹配相似度
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 五、测试流程
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054cb661c6c8e54ff5034.png)
|
||||
|
||||
## 六、用例编写及方法参数指引
|
||||
|
||||
### 1、用例实例说明
|
||||
|
||||
```python
|
||||
# 标准库导入
|
||||
from time import sleep
|
||||
# 导入三方库pytest
|
||||
import pytest
|
||||
# 导入用例执行要用到的Widget类
|
||||
from src.base_widget import BaseWidget
|
||||
|
||||
|
||||
class TestStart:
|
||||
|
||||
# class级别的fixture,测试用例执行之前执行。autouse=True 自动在当前作用域下使用。
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
# 测试热启动之前需要预启动。
|
||||
def application_pre_start(self):
|
||||
# 入参规则在第2点中详细描述。
|
||||
BaseWidget().right_click_element("deepin-album")
|
||||
# 入参规则在第2点中详细描述。
|
||||
BaseWidget().click_element("apps/autotest_deepin_album/picture/open.png", input_type="png")
|
||||
sleep(2)
|
||||
# 快捷键关闭窗口
|
||||
BaseWidget().hot_key("alt", "f4")
|
||||
sleep(2)
|
||||
|
||||
# 默认为function级别的fixture,用例执行之前,class级别fixture执行之后执行。
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_start(self):
|
||||
# 右键点击 deepin-album 图标
|
||||
BaseWidget().right_click_element("deepin-album")
|
||||
# 移动到右键菜单中打开(open.png)
|
||||
# 入参规则在第2点中详细描述。
|
||||
BaseWidget().move_to(*BaseWidget().find_element("apps/autotest_deepin_album/picture/open.png", input_type="png"))
|
||||
# 等待移动完成
|
||||
sleep(0.5)
|
||||
|
||||
def test_album_start(self):
|
||||
|
||||
# 点击“打开”
|
||||
BaseWidget().click()
|
||||
```
|
||||
|
||||
### 2、方法参数说明
|
||||
|
||||
2.1、**`find_element` 方法**
|
||||
|
||||
```python
|
||||
def find_element(element: str, input_type: str = "FILE_NAME", fill_color_position: tuple = (-1, -1), size: tuple = (25, 70)):
|
||||
pass
|
||||
```
|
||||
|
||||
- **参数 `element` 和 `input_type`**
|
||||
|
||||
**(1)通过应用 `icon` 识别元素**
|
||||
|
||||
- `input_type` 参数为默认参数,默认值为 `FIEL_NAME` ,表示通过应用 `icon` 寻找元素,此方法会动态获取测试机上的应用 `icon` 图标。
|
||||
- 举例:默认情况下 `input_type ` 参数不需要传入, `find_element("deepin-album")`,表示通过 `deepin-album` 的应用 `icon` 来获取坐标,注意,此时传入的 `deepin-album` 参数实际是 `desktop`文件的文件名,和2.3中的 `icon` 变量一致。
|
||||
- 我们建议优先使用这种方法,但此方法依赖网络,请根据测试环境实际情况选择。
|
||||
|
||||
**(2)通过本地图片识别元素**
|
||||
|
||||
- 参数 `input_type` 传入图片格式,比如截取的本地图片格式为 `png`,那么 `input_type="png"`,此时参数 `element ` 传入图片的路径,建议存放在对应的产品 `apps/app/picture/` 目录下,路径为项目根目录下的相对路径。
|
||||
- 举例:`find_element("apps/autotest_deepin_album/picture/open.png", input_type="png"))`,表示通过本地图片 `open.png` 识别元素。
|
||||
|
||||
- **参数 `fill_color_position`**
|
||||
|
||||
- `fill_color_position` 参数为默认参数,默认值为 `(-1, -1)`,`(-1, -1)` 表示取屏幕中右下角最后一个像素点作为填充坐标,在 `1920x1080` 屏幕分辨率下即实际坐标为 `(1919, 1079)` ,这样的写法可以适配不同大小的屏幕。
|
||||
- 任务栏在屏幕不同方位:
|
||||
- 任务栏在屏幕下方,从任务栏启动不需要传入该参数。
|
||||
- 任务栏在屏幕左边,`fill_color_position=(1, 1)`
|
||||
- 任务栏在屏幕右边,`fill_color_position=(-1, -1)`
|
||||
- 任务栏在屏幕上边,`fill_color_position=(1, 1)`
|
||||
- 从桌面启动应用,则需要传入除任务栏以外其他位置的坐标,比如(100, 100),只要这个坐标为除应用和任务栏区域都可以。
|
||||
- 从启动器里面启动应用,需要填充的坐标为启动器区域。
|
||||
|
||||
注:通过应用 `icon` 识别时,从测试机获取的 `icon` 原文件为 `svg` 矢量图,转换为 `png` 之后需要填充边缘透明部分。
|
||||
|
||||
- **参数 `size`**
|
||||
|
||||
- `size` 参数为默认参数,默认值为 `(25, 70)`,表示测试机上 `dock` 栏上应用图标的大小范围。
|
||||
- `UOS` 系统 `dock` 栏是可以上下拖动大小的,随着 `dock` 栏大小改变,应用图标的范围为25到70之间。
|
||||
- 如果你的测试机操作系统上 `dock` 栏大小不一样,可以根据实际情况传入大小范围。
|
||||
|
||||
2.2、`click_element`、 `right_click_element`, `double_click_element` 这几个方法参数规则与 `find_element` 方法一样。他们实际上是在 `find_element` 方法基础之上做了二次封装,增加了鼠标的动作,如果你不想使用这几个方法,在用例中也可以通过 `find_element` 获取到坐标,然后将坐标传入到键鼠的方法中,可以实现同样的效果。
|
||||
|
||||
2.3、用例类中有个类变量 `icon = ["deepin-album"] ` 需要特殊说明,列表中的元素为这条用例里面需要用到的所有 `icon`,用例执行之前会从测试机上拷贝所有需要用的 `icon` 到服务端用于图像识别。列表中元素为应用 `desktop` 文件的文件名,相册的 `desktop` 文件名为 `deepin-album.desktop`,只需要写 `deepin-album` 即可,`.desktop ` 已经在底层做了拼接。
|
||||
|
||||
## 七、执行入口
|
||||
|
||||
### 1、用例组织
|
||||
|
||||
在 `setting/config/server.ini` 文件中 `APP_NAME= ` 字段为用例的执行的目录,默认为 `apps` 目录下所有用例。如果你想指定执行某一个应用的用例,你可以将该配置项指定到应用的用例目录,比如:`APP_NAME=deepin_album`, 表示执行相册目录下的用例。
|
||||
|
||||
### 2、命令行参数
|
||||
|
||||
通过命令行参数配置参数
|
||||
|
||||
以下为 `python3 manage.py run` 提供的一些参数选项:
|
||||
|
||||
```shell
|
||||
-a APP, --app APP 应用名称:deepin-music
|
||||
-k KEYWORDS, --keywords KEYWORDS
|
||||
用例的关键词
|
||||
-t TAGS, --tags TAGS 用例的标签
|
||||
--log_level LOG_LEVEL 日志输出级别
|
||||
--env {host,docker} 在宿主机或容器环境执行
|
||||
--client_alias 选择测试机别名, 可选项为client.ini中已配置, 默认值为 zx-x86.
|
||||
--count 指定用例执行次数
|
||||
```
|
||||
|
||||
在一些 CI 环境下使用命令行参数会更加方便:
|
||||
|
||||
```shell
|
||||
python3 manage.py run --app deepin-music --keywords "xxx" --tags "xxx" --env docker
|
||||
```
|
||||
|
||||
注:在容器中执行时,若强制退出,容器并不会停止运行!
|
||||
|
||||
## 八、测试报告
|
||||
|
||||
### 1、Json 报告
|
||||
|
||||
自研视频解析工具会对单条用例执行一轮生成一个 `json` 文件,存放于 `report/{app_name}/{scene}/{run_num}` 目录下,命名格式为:
|
||||
|
||||
```shell
|
||||
report.json
|
||||
```
|
||||
|
||||
`json` 内容举例:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"Stab1": {
|
||||
"images": [1,2...150,151],
|
||||
"startStub": 0,
|
||||
"type": "Stab"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Stab2": {
|
||||
"images": [152,153...169,170],
|
||||
"startStub": 1,
|
||||
"type": "UnStab"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Stab3": {
|
||||
"images": [171,172...300,301],
|
||||
"startStub": 0,
|
||||
"type": "UnStab"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `Stab` :表示解析过程中不同的阶段。
|
||||
- `images` :表示每一帧的图片序号。
|
||||
- `startStub`: 为识别尾帧的阶段表示,如果 `"startStub": 1` ,说明尾帧出现在该阶段。
|
||||
- `type`: 为 `Stab` 或 `UnStab` ,表示稳定阶段或不稳定阶段。
|
||||
|
||||
各阶段有帧数补偿,默认补偿的帧数为 3 帧(可以通过 `server.ini` 里面 `un_stab_start=3` 进行配置),表示小于等于 3 帧的不稳定阶段视为稳定阶段,在最后计算性能结果时前后均需要减掉。
|
||||
|
||||
应用启动阶段就是在 `Stab2` 阶段,启动时间为:171 减 151 再减掉前后补偿帧数 6 帧,即 14 帧,视频帧率 30 帧/秒,因此启动耗时为:14 * 33 = 462 ms
|
||||
|
||||
### 2、Excel 报告
|
||||
|
||||
所有用例执行并解析完之后会 `report` 目录下生成一个汇总数据的 `Excel` 文件,文件名为:`{time}_perf_time.xlsx`。
|
||||
|
||||
注意,`report` 目录为可配置项,默认情况下会在项目根目录下生成,如果你将 `report`,指定到项目下其他路径,则会在对应的路径下生成 `json` 和 `Excel` 文件。
|
||||
|
||||
你也可以根据需要读取所有 `json` / `excel` 报告的数据,生成你自己想要的一些测试报告形式。
|
||||
|
||||
|
||||
## 九、USB 串口驱动键鼠方法
|
||||
|
||||
位于 `src/utils/control/serial_communication` 目录下的 `usb_mk.py` 文件
|
||||
|
||||
```python
|
||||
usb_mk = UsbMk()
|
||||
```
|
||||
|
||||
### 1、键盘操作
|
||||
|
||||
(1)按下键盘按键
|
||||
|
||||
```python
|
||||
usb_mk.press_key("enter") # 按下键盘enter键
|
||||
```
|
||||
|
||||
(2)按下键盘按键并且不放
|
||||
|
||||
```python
|
||||
usb_mk.press_key_down("enter") # 按下键盘enter键不放
|
||||
```
|
||||
|
||||
(3)组合按键
|
||||
|
||||
```python
|
||||
usb_mk.hot_key("ctrl", "alt", "T") # 按下组合按键ctrl+alt+T调起终端
|
||||
```
|
||||
|
||||
(4)组合按键不放
|
||||
|
||||
```python
|
||||
usb_mk.hot_key_down("ctrl", "alt", "T") # 按下组合按键ctrl+alt+T调起终端不放
|
||||
```
|
||||
|
||||
(5)释放所有键盘按键
|
||||
|
||||
```python
|
||||
usb_mk.key_up()
|
||||
```
|
||||
|
||||
(6)键盘输入字符串(支持传入汉字,会自动转成拼音输入)
|
||||
|
||||
```python
|
||||
usb_mk.hot_key("ctrl", "alt", "T") # 按下组合按键ctrl+alt+T调起终端
|
||||
usb_mk.input_text("reboot") # 输入字符串reboot
|
||||
usb_mk.press_key("enter") # 按下键盘enter键
|
||||
```
|
||||
|
||||
### 2、鼠标操作
|
||||
|
||||
(1)按下鼠标左键
|
||||
|
||||
```python
|
||||
usb_mk.click()
|
||||
```
|
||||
|
||||
(2)按下鼠标左键不放
|
||||
|
||||
```python
|
||||
usb_mk.mouse_down()
|
||||
```
|
||||
|
||||
(3)释放鼠标所有按键
|
||||
|
||||
```python
|
||||
usb_mk.mouse_up()
|
||||
```
|
||||
|
||||
(4)按下鼠标右键
|
||||
|
||||
```python
|
||||
usb_mk.right_click()
|
||||
```
|
||||
|
||||
(5)鼠标左键双击
|
||||
|
||||
```python
|
||||
usb_mk.double_click()
|
||||
```
|
||||
|
||||
(6)恢复鼠标至初始位置,默认左上角
|
||||
|
||||
```python
|
||||
usb_mk.move_to_init()
|
||||
```
|
||||
|
||||
(7)移动鼠标至相对坐标
|
||||
|
||||
```python
|
||||
usb_mk.move_rel(100, 200) # 鼠标向左边移动100个像素,向下移动200个像素
|
||||
```
|
||||
|
||||
(8)以屏幕左上角为圆心坐标移动鼠标至屏幕绝对坐标
|
||||
|
||||
```python
|
||||
usb_mk.move_to(100, 200) # 鼠标移动至屏幕坐标(100, 200)
|
||||
```
|
||||
|
||||
(9)按下鼠标左键,拖动到绝对坐标位置
|
||||
|
||||
```python
|
||||
usb_mk.drag_to(100, 200) # 按下鼠标拖动至屏幕坐标(100, 200)
|
||||
```
|
||||
|
||||
(10)按下鼠标左键,拖动到相对坐标位置
|
||||
|
||||
```python
|
||||
usb_mk.drag_rel(100, 200) # 鼠标向左边移动100个像素,向下移动200个像素
|
||||
```
|
||||
|
||||
## 十、继电器控制主机开机/重启
|
||||
|
||||
(若不使用继电器,可忽略该步骤,执行时会通过命令 `reboot`)
|
||||
|
||||
### 1、继电器设备
|
||||
|
||||
`LCUS-2` 型 双路 `USB` 智能串口控制继电器。
|
||||
|
||||
https://item.taobao.com/item.htm?spm=a1z09.2.0.0.41d72e8dUYx2pi&id=582653718178&_u=i25r20ia6a2e
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054cb661c6c8e54ff5091.png)
|
||||
|
||||
|
||||
|
||||
### 2、USB 延长线
|
||||
|
||||
普通的 `USB` 线即可。
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054cb661c6c8e54ff50a9.png)
|
||||
|
||||
### 3、杜邦线
|
||||
|
||||
3.1、`40P` 母对公杜邦线。
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054cb661c6c8e54ff50db.png)
|
||||
|
||||
|
||||
|
||||
3.2、一母二公杜邦线。
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054cb661c6c8e54ff5102.png)
|
||||
|
||||
|
||||
|
||||
### 4、安装图文教程
|
||||
|
||||
4.1、使用 `USB` 延长线连接控制端和继电器。
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054cb661c6c8e54ff5140.png)
|
||||
|
||||
4.2、在继电器常端和公共端均连接一根杜邦线。
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054cc661c6c8e54ff5182.png)
|
||||
|
||||
|
||||
|
||||
4.3、在主板上开机针和重启针上插上一母二公杜邦线。
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054cc661c6c8e54ff51c2.png)
|
||||
|
||||
4.4、将开机针外接的一母二公杜邦线的两根公线分别接入继电器的1路继电器(如图 1左侧继电器)和原电源开关。
|
||||
|
||||
4.5、将重启针外接的一母二公杜邦线的两根公线分别接入继电器的2路继电器(如图 1 右侧继电器)和原电源开关。
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054cc661c6c8e54ff523e.png)
|
||||
|
||||
若想保留原电源开关的电源灯,可根据如上图所示,使用杜邦线连接主板上的电源灯和原电源开关
|
||||
|
||||
## 十一、常见问题说明
|
||||
|
||||
**1、**`ERROR: for uos Cannot restart container 2301b1a1395d7959ee6523d61b61c87084649af530786cdb8fb5b3ecbcbd1068: linux runtime spec devices: error gathering device information while adding custom device "/dev/ttyACM0": no such file or directory`
|
||||
|
||||
检查 USB 串口线连接状态,查看 `/dev/ttyACM0` 是否存在,若存在 `/dev/ttyACMn`(n为任意数字),重启电脑解决。
|
||||
|
||||
**2、**`Error response from daemon: Container 5e700fdc769e8c74666d68a7f2bc3de58268b4594cf88706609d59f8dbc7362d is not running`
|
||||
|
||||
容器启动失败
|
||||
|
||||
1)检查哪一个容器未启动
|
||||
|
||||
```shell
|
||||
sudo docker container ls # 列出运行中的 Docker 容器
|
||||
```
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054cc661c6c8e54ff524b.png)
|
||||
|
||||
2)缺少 uos 容器,检查 USB 串口连接线和采集盒连接线是否正常,重启后执行
|
||||
|
||||
```shell
|
||||
bash install/setup.up # 重新完整部署环境
|
||||
```
|
||||
|
||||
或执行
|
||||
|
||||
```shell
|
||||
sudo docker-compose up -d --build # 仅重新构建 Docker 镜像
|
||||
```
|
||||
|
||||
**3、**若部署时提示挂载 `ttyACM0`、`video0` 失败,请检查 USB 串口线及采集盒是否接触良好,若接触良好,重启即可解决。
|
||||
|
||||
4、若出现图片能识别,但是鼠标未移动操作时,安装绝对鼠标操作的依赖包,见第三节第二小节测试机环境搭建,安装完成后,必须重启测试机。
|
||||
|
||||
5、由于通过采集卡获取的测试机实时画面,包含了鼠标的样式,所以对图片识别时,鼠标若停留在图标上层,则会对识别结果造成影响,需要将鼠标移动至其他位置,或者隐藏鼠标解决。
|
||||
|
||||
6、Excel 报告中,测试场景列与用例中的不一致,关键字scene、app_name、架构别名,不能包含下划线和英文符号的圆括号。
|
|
@ -0,0 +1,218 @@
|
|||
## OCR识别
|
||||
|
||||
### 1. 背景
|
||||
|
||||
传统的 OCR 方案大多采用谷歌 OCR(`Tesseract`)方案,但是它对于中文的识别非常差,经过大量的调研,我们使用 `PaddleOCR`,它是一个开源的基于深度学习的 OCR 识别工具,也是 `PaddlePaddle` 最有名的一个开源项目,感兴趣的可以点[这里](https://github.com/PaddlePaddle/PaddleOCR)了解,多的不说了,你只需要知道它就是中文识别的天花板。
|
||||
|
||||
### 2. 实现原理
|
||||
|
||||
安装它是个很麻烦的事情,虽然操作很简单,但其实安装包有点大,我们并不希望直接在 `env.sh` 中加入它,这会让整个自动化环境变得非常臃肿;
|
||||
|
||||
因此,我们想到将它做成一个 `RPC` 服务在其他机器上部署,测试机通过远程调用 `RPC` 服务的方式使用它;
|
||||
|
||||
RPC 的调用逻辑:
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c3661c6c8e54ff47b5.png)
|
||||
|
||||
这样我们只需要在服务端部署好 OCR 识别的服务,然后通过 RPC 服务将功能提供出来,框架里面只需要调用对应的 RPC 接口就行了。
|
||||
|
||||
### 3. 使用说明
|
||||
|
||||
框架代码示意(Client):
|
||||
|
||||
```python
|
||||
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`
|
||||
```
|
||||
|
||||
服务端代码示意(Service):
|
||||
|
||||
```python
|
||||
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()
|
||||
```
|
||||
|
||||
此方案在**框架内没有引入任何三方依赖**完全采用标准库实现,而且使用方法非常简单,只需要通过 `OCR.ocr()` 即可;
|
||||
|
||||
对于一些文案的场景非常适用,此方法直接返回坐标,可以用于**元素定位**。
|
||||
|
||||
也可以用于**文字断言**,代码示意:
|
||||
|
||||
```python
|
||||
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}",
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
在用例中使用断言,示例:
|
||||
|
||||
```python
|
||||
def test_font_manager_021(self):
|
||||
"""右侧♥-收藏/取消收藏字体"""
|
||||
|
||||
# 字体管理器界面右侧详情列表,选择未收藏字体,右键 / 收藏字体
|
||||
# 收藏字体,右键菜单显示“取消收藏”;
|
||||
...
|
||||
self.assert_ocr_exist("取消收藏")
|
||||
```
|
||||
|
||||
### 4. 服务端部署
|
||||
|
||||
我们目前是将 `OCR` 服务部署机器性能较一般,如果你觉得现有的 `OCR` 识别性能不够好,恰好你有更好的机器,可以考虑将其私有化部署。
|
||||
|
||||
#### 4.1. 环境安装
|
||||
|
||||
推荐使用 `pipenv` 进行环境搭建;
|
||||
|
||||
安装 `pipenv` :
|
||||
|
||||
```shell
|
||||
sudo pip3 install pipenv
|
||||
```
|
||||
|
||||
新建一个目录作为环境包 `ocr_env`:
|
||||
|
||||
```shell
|
||||
cd ~
|
||||
mkdir ocr_env
|
||||
```
|
||||
|
||||
创建 `python 3.7` 环境:
|
||||
|
||||
```shell
|
||||
cd ocr_env
|
||||
pipenv --python 3.7
|
||||
```
|
||||
|
||||
安装 `OCR` 依赖包:
|
||||
|
||||
```shell
|
||||
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` 目录,后台执行它就好了:
|
||||
|
||||
```shell
|
||||
cd ocr_env
|
||||
nohup pipenv run python pdocr_rpc_server.py &
|
||||
```
|
||||
|
||||
#### 4.3. 配置开机自启
|
||||
|
||||
你肯定不想每次机器重启之后都需要手动启动服务,因此我们需要配置开机自启。
|
||||
|
||||
写开机自启服务文件:
|
||||
|
||||
```shell
|
||||
sudo vim /lib/systemd/system/ocr.service
|
||||
```
|
||||
|
||||
`autoocr` 名称你可以自定义,写入以下内容:
|
||||
|
||||
```shell
|
||||
[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}
|
||||
|
||||
修改配置文件的权限:
|
||||
|
||||
```shell
|
||||
sudo chmod 644 /lib/systemd/system/ocr.service
|
||||
```
|
||||
|
||||
自启服务生效:
|
||||
|
||||
```shell
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable ocr.service
|
||||
```
|
||||
|
||||
查看服务状态:
|
||||
|
||||
```shell
|
||||
sudo systemctl status ocr.service
|
||||
```
|
||||
|
||||
你可以再重启下电脑,看看服务是不是正常启动了,没报错就 OK 了。
|
|
@ -0,0 +1,98 @@
|
|||
## PMS数据回填
|
||||
|
||||
测试单关联的用例,自动化测试对应的去跑这些关联的用例,并且将执行的结果回填的测试用例的状态里面。
|
||||
|
||||
### 1. 本机执行时回填
|
||||
|
||||
PMS 数据回填主要有三种方式:
|
||||
|
||||
**(1)异步回填**
|
||||
|
||||
在用例执行的过程中,采用异步的方式去进行数据回填,直白的讲就是,第二条用例开始跑的时候,通过子线程去做第一条用例的数据回填,如此循环,直到所有用例执行结束;
|
||||
|
||||
这种方案的时间**效率最高**的,因为理论上用例的执行时间是大于数据回填的接口请求时间的,也就是说,当用例执行完之后,数据回填也完成了。
|
||||
|
||||
**【方法一】配置文件**
|
||||
|
||||
使用方法,在 `globalconfig.ini` 里面配置以下参数:(以下涉及到的参数配置都是在配置文件里面进行配置)
|
||||
|
||||
```ini
|
||||
PMS_USER = PMS账号
|
||||
PMS_PASSWORD = PMS密码
|
||||
SEND_PMS = async
|
||||
TASK_ID = 测试单ID
|
||||
TRIGGER = auto
|
||||
APP_NAME = 这个参数可填可不填,但是填了可以提高用例的执行速度,因为在用例收集阶段可以指定到具体的应用库。(下同)
|
||||
```
|
||||
|
||||
配置完之后,命令行触发:
|
||||
|
||||
```shell
|
||||
youqu manage.py run
|
||||
```
|
||||
|
||||
**【方法二】命令行传参**
|
||||
|
||||
```shell
|
||||
youqu manage.py run -a apps/autotest_xxx -u ut001234 -p my_password --task_id xxxx --send_pms async
|
||||
```
|
||||
|
||||
**(2)用例执行完之后回填**
|
||||
|
||||
等所有用例执行完之后,再逐个进行回填的接口请求,此方案时间效率比较低。
|
||||
|
||||
使用方法:
|
||||
|
||||
```ini
|
||||
PMS_USER = PMS账号
|
||||
PMS_PASSWORD = PMS密码
|
||||
SEND_PMS = finish
|
||||
TASK_ID = 测试单ID
|
||||
TRIGGER = auto
|
||||
APP_NAME =
|
||||
```
|
||||
|
||||
命令行使用方式和前面一样,只需要修改 `--send_pms finish` 即可。
|
||||
|
||||
**(3)手动回填**
|
||||
|
||||
所有用例执行完之后不做回填的接口请求,后续手动将结果进行回填请求。
|
||||
|
||||
用例执行时配置:
|
||||
|
||||
```ini
|
||||
PMS_USER = PMS账号
|
||||
PMS_PASSWORD = PMS密码
|
||||
SEND_PMS = finish
|
||||
TASK_ID = 测试单ID
|
||||
TRIGGER = hand
|
||||
APP_NAME =
|
||||
```
|
||||
|
||||
后续手动回填方法:
|
||||
|
||||
```shell
|
||||
youqu manage.py pms --send2task yes
|
||||
```
|
||||
|
||||
### 2. 远程执行时回填
|
||||
|
||||
远程执行需要控制多台测试机同时执行用例,也就是说同一条用例会在多台机器上同时执行,但是执行结果可能不一致;
|
||||
|
||||
因此,远程执行的数据回填需要等所有测试机执行结束之后,服务端收集到测试结果,并对测试结果做处理(只要有1个机器上用例失败,那此用例为失败)之后,在进行数据回填;
|
||||
|
||||
使用方法:
|
||||
|
||||
```shell
|
||||
youqu manage.py remote -c uos@10.8.13.xx/uos@10.8.13.yy -a apps/autotest_xxx -u ut001234 -p my_password --task_id xxxx --send_pms finish
|
||||
```
|
||||
|
||||
执行结束之后在 `report` 目录下会有 `pms_xxx` 开头的目录,里面保存了所有远程测试机的测试结果,以及汇总的结果;
|
||||
|
||||
### 3. 可能遇到的“严重问题”
|
||||
|
||||
有同学可能会发现,怎么回填一次之后,后面想再次回填就不生效了;
|
||||
|
||||
这是因为为了应对前面提到的多种数据回填的方式,在 `report` 目录下会有 `pms_xxx` 开头的目录,用于记录了用例的执行结果和回填情况,如果这条用例之前已经回填过了,后续就不会再此触发回填了;
|
||||
|
||||
如果你想重新做回填,你可以把 `report/pms_xxx` 目录删掉,这样就可以重新做数据回填了;
|
|
@ -0,0 +1,33 @@
|
|||
## Wayland 适配
|
||||
|
||||
`Wayland` 下自动化主要问题是 `X11` 下的键鼠操作方法无法使用,比如 `Xdotool`、 `PyAutoGUI`、`Xwininfo` 等等;
|
||||
|
||||
YouQu 在 `Wayland` 下兼容适配,`env.sh` 在 `Wayland` 下执行时会安装自研的键鼠操作服务(可能存在一些依赖报错,按照注释解决即可),框架核心库也针对性的做了适配,上层用例完全不用关心机器是`Wayland` 还是 `X11`,框架会根据执行时状态自动判断走不同的逻辑;
|
||||
|
||||
简单讲就是,应用库只需要维护一套用例脚本即可。
|
||||
|
||||
【用例兼容】
|
||||
|
||||
因为 `Wayland` 下有些应用的界面显示和功能本身存在一些差异,用例层可能需要对这部分用例做逻辑判断,使用全局配置里面的常量进行逻辑编写即可:
|
||||
|
||||
```python
|
||||
from setting import conf
|
||||
|
||||
# GlobalConfig.IS_WAYLAND 获取到当前的显示服务器(bool)
|
||||
# 应用库 Config 继承 GlobalConfig
|
||||
if conf.IS_WAYLAND:
|
||||
pass
|
||||
if conf.IS_X11:
|
||||
pass
|
||||
```
|
||||
|
||||
比如用例里面如果断言的图片不同:
|
||||
|
||||
```python
|
||||
if conf.IS_WAYLAND:
|
||||
self.assert_image_exist("wayland_XXX")
|
||||
else:
|
||||
self.assert_image_exist("x11_XXX")
|
||||
```
|
||||
|
||||
这样这条用例脚本在 `Wayland` 和 `X11` 下都可以跑,so easy 是不是?完全没必要专门拉新分支进行 `Wayland` 适配。
|
|
@ -0,0 +1,56 @@
|
|||
## 全局配置-setting.conf
|
||||
|
||||
### 1. 配置文件
|
||||
|
||||
全局配置模块 `setting` 包含了以下配置文件:
|
||||
|
||||
(1)`ini` 配置文件
|
||||
|
||||
主要配置一些全局的配置项,譬如:失败重跑次数、是否失败录屏、单条用例超时时间、会话超时时间、执行时日志级别、生成的报告类型、以及分布式执行的一些策略配置项等等。
|
||||
|
||||
(2)`py` 配置文件
|
||||
|
||||
主要提供配置文件读取、动态获取一些常量(如项目根目录绝对路径 `(BASE_DIR)`、系统架构(`SYS_FRAME`)、时间字符串(`strftime`)、本机 `USERNAME` `IP` 等等)、公共 URL 等。
|
||||
|
||||
一些支持人工修改或自定义的配置项都在 `ini` 配置文件里面,`py` 文件是不需要人工去修改的;
|
||||
|
||||
### 2. 配置对象获取
|
||||
|
||||
导入全局配置对象
|
||||
|
||||
```py
|
||||
from setting import conf
|
||||
```
|
||||
|
||||
通过 `conf` 对象能获取到所有可获取的配置项的值,比如:
|
||||
|
||||
```py
|
||||
conf.PASSWORD
|
||||
```
|
||||
|
||||
这样可以获取到 `globalconfig.ini` 配置文件中 `PASSWORD` 配置的值。
|
||||
|
||||
```ini
|
||||
--8<-- "setting/globalconfig.ini"
|
||||
```
|
||||
|
||||
除了上面这种导入方式,还可以这样导入:
|
||||
|
||||
```py
|
||||
from setting.globalconfig import GlobalConfig
|
||||
```
|
||||
|
||||
`GlobalConfig` 也是全局配置对象,实际上 `conf` 是 `GlobalConfig` 的别名,你可以根据自己喜欢选择用哪个;
|
||||
|
||||
### 3. 应用库配置对象
|
||||
|
||||
所有应用库配置对象都是继承框架的全局配置类的:
|
||||
|
||||
```py
|
||||
from setting.globalconfig import _GlobalConfig
|
||||
|
||||
class _Config(_GlobalConfig):
|
||||
pass
|
||||
```
|
||||
|
||||
这样在子项目就可以使用到所有的全局配置。
|
|
@ -0,0 +1,114 @@
|
|||
## 去干扰识别
|
||||
|
||||
以右键菜单来讲解此方案;
|
||||
|
||||
### 1. 现有右键菜单定位的方案及问题
|
||||
|
||||
右键菜单的元素定位是一个难点,过去我们调研和使用过的元素定位操作方法有 4 种:
|
||||
|
||||
#### 1.1. 步长操作法
|
||||
|
||||
在右键菜单呼出来之后,通过键盘的 `up`、`down` 按键,进行选择菜单选择,选中目标之后 `enter` 即可;比如:在桌面点击右键菜单之后,按 1 次 `down` ,会出现下图:
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c5661c6c8e54ff49b4.png)
|
||||
|
||||
继续再按 2 次 `down`,会出现这样:
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c5661c6c8e54ff49e5.png)
|
||||
|
||||
再按 `enter`,会出现这样:
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c5661c6c8e54ff4a06.png)
|
||||
|
||||
如此,“排序方式”的步长为 3;通过使用键盘上下键,就实现了对右键菜单的操作;
|
||||
|
||||
但是,这种方式有个很烦人的问题,就是右键菜单的选项位置不可能一直不变,在需求迭代的过程中,菜单选项的变化是很大的,甚至有些应用支持自定义菜单,比如文管右键菜单可以自定义;
|
||||
|
||||
也就是说你得经常去维护菜单选项的步长,一个选项现在的步长是 3,下个迭代可能就是 4 或者 5。
|
||||
|
||||
#### 1.2. 常规图像识别法
|
||||
|
||||
把每个菜单选项单独截图保存,图片中仅包含一个菜单选项,如下图所示:
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c5661c6c8e54ff4aa9.png)
|
||||
|
||||
这样,每个菜单选项就可以通过图像识别的方式进行元素定位;
|
||||
|
||||
这种方式不用担心菜单选项的顺序或位置,但是需要保存大量的图片,且容易受到字体 UI 变更类需求的影响,比如:字体大小、字体间距等等需求变更都会影响,每次变更之后就需要进行大量图片资源的重新截图替换,是个比较麻烦的事情;
|
||||
|
||||
#### 1.3. 相对位移法
|
||||
|
||||
鼠标点击右键的时候,鼠标的当前坐标是可以获取到的,菜单选项的宽( w )一般是固定的,变化的是菜单的长度( h ),可以通过某个选项相对于鼠标的距离在确定菜单选项的坐标,如下图所示:
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c6661c6c8e54ff4af1.png)
|
||||
|
||||
通过维护菜单选项(相对位置)相对于鼠标位置的距离,即可轻松计算出菜单选项在屏幕中的坐标。
|
||||
|
||||
从理论上此方案是可行的,但是这里仍然存在两个严重的问题:
|
||||
|
||||
- 菜单顺序改变,导致相对距离改变,而且距离是通过像素(px)来表示的,不想步长那么只管,每次需要去量一下,维护起来有点麻烦;
|
||||
- 鼠标在桌面不同位置点击右键,右键菜单出现的位置是不同的,上图的菜单是在鼠标的左下方,如果你移动鼠标到屏幕四边,你会发现,右键菜单可能出现在鼠标的四个方向,也就是说你需要根据鼠标的不同位置来判断右键菜单出现的方向,而不同的方向上计算方法是不同的,比如:右键菜单在鼠标的左上,菜单选项的坐标计算方法为(x - w / 2, y - h),这样维护起来可以说非常复杂;
|
||||
|
||||
基于以上两个原因,我们并不推荐这种操作方案。
|
||||
|
||||
#### 1.4. 属性定位
|
||||
|
||||
有同学说干嘛不通过属性定位呢,其实,我们最开始想到的方案就是通过属性定位,但是在属性的 DOM 树里怎么也找不到,无法定位到,我们也联合研发同学一起解决此问题,但最终还是没能解决,非常遗憾;
|
||||
|
||||
### 2. 去干扰识别
|
||||
|
||||
由于右键菜单选项几乎都是文本,那么通过 OCR 识别,几乎是最优的方案:
|
||||
|
||||
- 不用保存大量的图片;
|
||||
- 不会受到菜单选项顺序改变的影响;
|
||||
- 不会受到字体 UI 变化的影响;
|
||||
|
||||
关于 OCR 识别我们在前面已经讲到,本章节主要讲基于 OCR 识别,我们在右键菜单识别上的突破和创新;
|
||||
|
||||
**使用 OCR 识别右键菜单虽然已经很完美了,但是在一些情况下仍然存在一点问题**,就是屏幕中出现多个和菜单选项文字相同的文字时,比如下面这种情况:
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c6661c6c8e54ff4b0c.png)
|
||||
|
||||
屏幕恰好出现了两个“复制”,此时要定位到菜单中的 “复制”,就需要进一步做数据处理,比如:OCR 返回 2 个“复制”的坐标,用例里面来判断用哪个,从业务上将处理起来比较麻烦,因为你得明确知道菜单在左边还是右边,然后这本身就是不确定的;
|
||||
|
||||
怎么解决这个问题呢,这就要说到本章的主题:**去干扰图像识别**。
|
||||
|
||||
【原理】
|
||||
|
||||
在点击右键菜单之前截一张图,点击右键菜单之后再截取一张图,两张图唯一变化的就是右键菜单,将其他相同的地方都屏蔽掉,只留下菜单界面,如此即可消除干扰,如下图所示:
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c7661c6c8e54ff4b48.png)
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c7661c6c8e54ff4b93.png)
|
||||
|
||||
将两种图做前后对比提取可以得到这样的图:
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c7661c6c8e54ff4bbd.png)
|
||||
|
||||
你看,通过前后图片的对比,将相同的部分给消除掉,再进行 OCR 识别,这样就不会有干扰了;
|
||||
|
||||
代码实现请查看 `src/filter_image.py` ;
|
||||
|
||||
代码示例:
|
||||
|
||||
```python
|
||||
def filter_image(action):
|
||||
"""
|
||||
对比动作前后两张图片,提取不同的部分生成一张新的图片,并返回新图片的路径
|
||||
:param action: 动作函数的函数对象
|
||||
:return: 新图片的路径
|
||||
"""
|
||||
```
|
||||
|
||||
`action` 是鼠标事件,因为此类场景不仅仅在右键存在,单击、双击等等,只要操作前后后变化的都可以:
|
||||
|
||||
```python
|
||||
from src.mouse_key import MouseKey
|
||||
action = (
|
||||
MouseKey.click,
|
||||
MouseKey.right_click,
|
||||
MouseKey.double_click,
|
||||
MouseKey.move_to,
|
||||
)
|
||||
```
|
||||
|
|
@ -0,0 +1,399 @@
|
|||
## 图像识别
|
||||
|
||||
图像识别在 `UI` 自动化中是不可缺少的,市面上甚至有完全基于图像识别的自动化测试框架,比如 `Airtest`、`Sikuli` 等,在游戏等特定领域也有不错的效果,这些工具实际上也是用的 `OpenCV` 进行了封装,`YouQu` 框架基于 `OpenCV` 开发了自己的图像识别功能,它可以方便的用于界面元素的定位和断言;
|
||||
|
||||
`YouQu` 的图像识别功能几乎满足了你的所有要求,我们在长时间的思考和摸索中,针对常规场景及一些特殊场景探索出了一些实用且有效的方案,且听我慢慢道来。
|
||||
|
||||
### 1. 常规识别
|
||||
|
||||
【背景】
|
||||
|
||||
常规识别很好理解,一句话讲就是,要获取到目标元素在屏幕中的位置。
|
||||
|
||||
【原理实现】
|
||||
|
||||
在测试过程中需要获取的坐标是相对于整个屏幕的坐标,我们可以截取到整个屏幕的图片(screen);
|
||||
|
||||
在元素识别的过程中,我们需要截取某个元素的小图进行识别,比如截取播放按钮:
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c8661c6c8e54ff4c33.png)
|
||||
|
||||
那么实际上,元素定位的问题就转换为,将截图的小图(`play_btn`)拿到整个屏幕的大图(`screen`)中去做匹配,如果匹配成功,返回小图在大图中的坐标( x, y )即可。
|
||||
|
||||
为了方便描述,以下我将整个屏幕的截图称为:大图,某个元素图片的截图称为:小图。
|
||||
|
||||
基于 `OpenCV` 的模板匹配 `cv.matchTemplate()` 功能,我们实现了图像定位的功能,框架提供了一个图像识别的底层接口(一般不对上层提供调用):
|
||||
|
||||
```python
|
||||
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: 高度;根据匹配度返回坐标
|
||||
"""
|
||||
# 详细代码太长不贴了,感兴趣请查看源码
|
||||
```
|
||||
|
||||
【参数介绍】
|
||||
|
||||
**(1)image_path**
|
||||
|
||||
`image_path` 是小图的绝对路径;
|
||||
|
||||
- 通常在 AT 工程里面,我们约定将用于元素定位的图片资源放到 `widget/pic_res` 目录下,图片的名称以实际的元素名称命名,如:`play_btn.png` ;
|
||||
|
||||
- 用于用例断言的图片资源放到 `case/assert_res` 目录下,图片的名称以用例的名称命名,如:`music_001.png` ;
|
||||
|
||||
这样是为了方便管理和维护。
|
||||
|
||||
**(2)rate**
|
||||
|
||||
图像识别的的匹配度或者说相似度,框架默认的配置为 `0.9`,也就是说小图在大图中存在一个相似度 90% 的图标即返回其在大图中的坐标;
|
||||
|
||||
如果你在用例中需要调整识别度,你可以在调用函数的时候,传入不同的识别度的值。
|
||||
|
||||
**(3)multiple**
|
||||
|
||||
默认情况下 `multiple=False`,表示只返回识别到的第一个,如果 `multiple=True` 返回匹配到的多个目标,因为大图中可能存在多个相同的小图,在某些场景下你可能需要全部获取到所有匹配到的坐标。
|
||||
|
||||
**(4)picture_abspath**
|
||||
|
||||
默认情况下 `picture_abspath=None` 表示大图为截取的屏幕截图,如果你不希望大图是屏幕的截图,而是你自定义传入的某个图片,你只需要将你的图片路径传递给这个参数就行,比如: `picture_abspath="~/Desktop/big.png"` ;
|
||||
|
||||
**(5)screen_bbox**
|
||||
|
||||
大图默认情况下是截取整个屏幕,`screen_bbox = [x, y, w, h]` 可以指定截取屏幕中的固定区域,某些场景下,可以排除部分区域对识别结果的影响。
|
||||
|
||||
【隐式等待】
|
||||
|
||||
用例执行过程中进行图像识别时,有时候页面跳转有延时,有可能存在识别的那一刻页面也没有跳转出来,或者或者识别的那一刻;
|
||||
|
||||
因此我们需要一种等待机制,即在一定的时间内,如果识别不到,重复去识别:
|
||||
|
||||
```python
|
||||
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. 气泡识别
|
||||
|
||||
【背景】
|
||||
|
||||
气泡识别指的是,某些场景下要定位的元素是一些会消失的小弹窗,这类场景在用例执行过程中进行图像识别时就可能存在不稳定性,有可能图像识别的时候气泡已经消失了,也有可能气泡出现的时间太短了,不容易捕捉到,就像气泡一样,出现一下就消失,因此我们形象的称之为 “气泡识别”;
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c8661c6c8e54ff4cde.png)
|
||||
|
||||
【原理实现】
|
||||
|
||||
为了能稳定的识别气泡类场景,我们采用的方案是:
|
||||
|
||||
在一段时间内(包含气泡从出现到消失),不停的截取这段时间内的大图,以此确保在截取的一堆图片中,肯定有至少一张图片能捕捉到气泡,最后再对这一堆图片逐个进行图像识别;
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c8661c6c8e54ff4ca5.png)
|
||||
|
||||
代码示例:
|
||||
|
||||
```python
|
||||
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: 截图的最大次数,这是一个预设值,一般情况下不涉及修改;
|
||||
"""
|
||||
```
|
||||
|
||||
【参数介绍】
|
||||
|
||||
(1)screen_time
|
||||
|
||||
截取屏幕图片的时间,在此时间内会不断的进行截图操作,就像录制视频一样;
|
||||
|
||||
(2)pause
|
||||
|
||||
每次截取图片的间隔时间,默认情况下是一刻不停的截图,如果你想每次截图存在一些间隔时间传入对应的时间间隔即可,单位是秒,比如:pause = 0.03,表示 30 ms,相当于帧率为 30 帧;
|
||||
|
||||
|
||||
|
||||
### 3. 不依赖 OpenCV 的图像识别方案
|
||||
|
||||
#### 3.1. 自研图像识别技术
|
||||
|
||||
【原理】
|
||||
|
||||
为了实现识别图像的目的,我们可以通过将图片的每个像素的RGB值,与整个屏幕中的RGB进行对比,如果小图上的RGB值与对应大图位置的RGB都相等,则匹配成功,即可返回小图在大图中的中心坐标点。
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c9661c6c8e54ff4d85.png)
|
||||
|
||||
**读取小图和大图的RGB值**
|
||||
|
||||
(1)小图的RGB值
|
||||
|
||||
```shell
|
||||
small_data = small_pic.load()
|
||||
# load()会将图片的RGB值获取到,数据格式为一个二维列表,赋值给一个变量small_data。
|
||||
```
|
||||
|
||||
(2)大图的RGB值
|
||||
|
||||
```shell
|
||||
big_data = big_pic.load()
|
||||
```
|
||||
|
||||
**将小图与大图的RGB值进行匹配**
|
||||
|
||||
(1)匹配从大图的坐标(0,0)开始匹配,匹配小图里面所有的坐标点(0,0)—(small_pic.width,small_pic.height);
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054ca661c6c8e54ff4f3f.png)
|
||||
|
||||
(2)如果在大图的(0,0)对应的所有小图的RGB值不相等,则移动到下一个坐标点(1,0),同样匹配小图里面所有的坐标点(0,0)—(small_pic.width,small_pic.height);
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c9661c6c8e54ff4daa.png)
|
||||
|
||||
(3)按照这样的规律将这一行每移动一个坐标点,都将小图所有的RGB与对应大图的值进行匹配;
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c5661c6c8e54ff49a0.png)
|
||||
|
||||
(4)如果在大图的其中一个坐标点上匹配到了小图的所有RGB值,则此时返回小图在大图中的坐标点;
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c2661c6c8e54ff4799.png)
|
||||
|
||||
(5)如果匹配了大图所有的坐标点,都没有匹配到,则说明大图中不存在小图,匹配失败;
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c4661c6c8e54ff4915.png)
|
||||
|
||||
【代码实现】
|
||||
|
||||
```python
|
||||
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 服务端,服务端拿到两张图片进行图像识别,最后将识别结果返回给测试机;
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c3661c6c8e54ff47b5.png)
|
||||
|
||||
要特殊说明的是: RPC 是一种协议,许多语言都是支持的,比如说服务端也可以用 C++ 来实现,客户端使用 Python 也是可以调用的。
|
||||
|
||||
【代码实现】
|
||||
|
||||
服务端代码示意(Service):
|
||||
|
||||
```python
|
||||
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_put` 和 `match_image_by_opencv` 两个功能接口,在测试机上可以通过 IP 和端口进行 RPC 请求;
|
||||
|
||||
客户端代码示意(Client):
|
||||
|
||||
```python
|
||||
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. 动态图像识别
|
||||
|
||||
【背景】
|
||||
|
||||
在桌面壁纸切换,或看图、相册切换图片类的测试场景,由于你的测试资源是不固定的(不同版本的系统壁纸不同、壁纸顺序不同,看图相册在图片资源不一定固定),那么在测试切换壁纸或者切换图片的场景时就会存在一个问题,就是你不知道预期是啥,用例操作动态的,也是极不稳定。
|
||||
|
||||
【原理】
|
||||
|
||||
在切换图片之前截图保存并返回图片的路径,切换图片之后再次识别这张图片,如果不存在,说明图片已经切换了;
|
||||
|
||||
示意图:
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c3661c6c8e54ff486e.png)
|
||||
|
||||
这样,我们截取了当前图片中比较有代表性的位置(一只鸟),在切换图片之后再用这张小图在当前屏幕中进行图像识别:
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c4661c6c8e54ff48ba.png)
|
||||
|
||||
我们再拿着这张小图在当前屏幕中进行图像识别,这样在当前图片中,就不能找到这只鸟了,图像识别的结果是 False,那么也就可以判断图片切换是成功的。
|
||||
|
||||
代码示意:
|
||||
|
||||
```python
|
||||
def save_temporary_picture(_x: int, _y: int, width: int, height: int):
|
||||
"""
|
||||
截取屏幕上指定区域图片,保存临时图片,并返回图片路径
|
||||
:param x: 左上角横坐标
|
||||
:param y: 左上角纵坐标
|
||||
:param width: 宽度
|
||||
:param height: 高度
|
||||
:return: 图片路径
|
||||
"""
|
||||
```
|
||||
|
||||
此函数用于在操作之前截图一张临时图片,返回图片路径,最后在断言的时候再将图片路径作为参数传入断言语句即可;
|
||||
|
||||
代码示例:
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c4661c6c8e54ff48d1.png)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
## 失败录屏
|
||||
|
||||
录屏其实是一种视频形式的日志,因为很多时候我们在查看日志之后仍然无法准确的定位到用例失败的具体原因,因此用例的录屏能让我们看到用例在执行过程;
|
||||
|
||||
【使用方法】
|
||||
|
||||
在 `globalconfig.ini` 里面配置关注 2 个参数;
|
||||
|
||||
```ini
|
||||
;失败用例重跑次数
|
||||
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` 目录下保存失败用例的录屏和断言的那个时间点的截屏,你可以通过视频文件看到失败用例执行过程都发生了什么。
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
## 属性定位
|
||||
|
||||
### 1. 背景
|
||||
|
||||
传统的 UI 自动化大多都是基于浏览器的,核心是在网页的 DOM 树上查找元素,并对其进行定位操作;
|
||||
|
||||
Linux 桌面应用大多是采用 Qt 编写的,在 Qt 中也是从最顶层的 `MainWindow` 开始构建应用,所以逻辑也是一样的,Qt 应用的自动化测试同样可以通过 DOM 树(属性树)进行元素定位,我们称之为**属性定位**。
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c4661c6c8e54ff48ef.png)
|
||||
|
||||
借助开源工具 `dogtail` 我们可以对元素进行获取,从而进行定位操作。`dogtail` 的项目介绍可以[猛戳这里](https://gitlab.com/dogtail/dogtail/)。
|
||||
|
||||
### 2. 使用方法
|
||||
|
||||
#### 2.1. sniff(嗅探器)使用
|
||||
|
||||
在终端输入 sniff 启动 AT-SPI Browser
|
||||
|
||||
```shell
|
||||
mikigo@mikigo-PC:~$ sniff
|
||||
```
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c9661c6c8e54ff4dd8.png)
|
||||
|
||||
**查看应用的标签**
|
||||
|
||||
在 sniff 里面可以看到系统中已启动的应用,点击应用名称前面的三角形图标,可以展开应用的标签,所有标签以 tree 的形式展示,对应应用里面的父窗口和子窗口。
|
||||
|
||||
**获取元素控件的标签名称**
|
||||
|
||||
首先,为了方便查看元素控件对应的位置,建议现在上方工具栏点击 `Actions`,然后勾选 `Hightlight Items`,这样在 sniff 中鼠标选中元素标签的时候,应用中会有相应的光标锁定。
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c9661c6c8e54ff4e0e.png)
|
||||
|
||||
在 sniff 里面点击进入应用的标签 tree 中后,点击相应的元素控件,在工具下方,会展示元素控件的 `Name`,这个就是标签名称。
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c9661c6c8e54ff4e56.png)
|
||||
|
||||
在 tree 中有些地方是空白的或者是 Form,是因为开发人员在添加标签的时候没有添加,或者有些父窗口不需要添加,这种在实际业务中是不影响的,我们只要保证自动化测试用例中,要用到的元素都添加了标签即可。
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054ca661c6c8e54ff4f0b.png)
|
||||
|
||||
#### 2.2. 元素操作
|
||||
|
||||
获取应用对象
|
||||
|
||||
dogtail 获取应用对象的时候,使用的是 tree 库里面的 application() 方法:
|
||||
|
||||
```python
|
||||
from dogtail.tree import root
|
||||
app_obj = root.application('deepin-music')
|
||||
```
|
||||
|
||||
app_obj就是应用的对象。
|
||||
|
||||
- 获取元素对象
|
||||
|
||||
获取元素对象,是应用对象使用child()方法:
|
||||
|
||||
```python
|
||||
element = app_obj.child('element_name')
|
||||
```
|
||||
|
||||
我们可以通过传入元素的 Name,获取到相应元素的对象。Name 可以通过 sniff 查看。
|
||||
|
||||
- 获取元素对象列表:
|
||||
|
||||
```python
|
||||
element_list = element.children
|
||||
```
|
||||
|
||||
获取到这个元素下面所有的元素列表。
|
||||
|
||||
这个方法适用于有些标签没有添加,但是位置是固定的,我们通过索引可以取得元素。
|
||||
|
||||
```python
|
||||
element_list[0]
|
||||
```
|
||||
|
||||
- 对元素的操作
|
||||
|
||||
在获取到元素之后,我们就可以对元素进行相应的操作。
|
||||
|
||||
- 单击
|
||||
|
||||
```python
|
||||
element.click(button=1)
|
||||
```
|
||||
|
||||
button 1 —>左键,2 —>滚轮,3 —>右键,默认为 1
|
||||
|
||||
- 双击
|
||||
|
||||
```python
|
||||
element.doubleClick(button=1)
|
||||
```
|
||||
|
||||
- 鼠标悬停
|
||||
|
||||
```python
|
||||
element.point()
|
||||
```
|
||||
|
||||
鼠标移动到元素中心位置
|
||||
|
||||
- 文本输入
|
||||
|
||||
```python
|
||||
element.typeText(string)
|
||||
```
|
||||
|
||||
向元素对象输入字符串 ,比如输入框
|
||||
|
||||
- 组合键
|
||||
|
||||
```python
|
||||
element.keyCombo(comboString)
|
||||
```
|
||||
|
||||
#### 2.3. 框架封装
|
||||
|
||||
代码示例:
|
||||
|
||||
```python
|
||||
# 详细代码请查看 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
|
||||
```
|
||||
|
||||
框架提供的接口非常简洁,在调用时:
|
||||
|
||||
```python
|
||||
self.dog.element_center("播放")
|
||||
```
|
||||
|
||||
这样就能获取到此元素的中心坐标。
|
|
@ -0,0 +1,265 @@
|
|||
## 执行管理器-manage.py
|
||||
|
||||
`YouQu` 的执行管理器 `manage.py` 提供了丰富的配置和命令行参数,可用于本地用例驱动执行、远程用例驱动执行、`CSV` 文件管理、`PMS` 与本地 `CSV` 文件标签关联管理、脚手架等功能;
|
||||
|
||||
### 1. 如何使用
|
||||
|
||||
**【命令行使用】**
|
||||
|
||||
所有功能的驱动执行都是通过 `manage.py` 进行的,它是全局的入口文件,后面提到的一些命令行参数也都默认是在 `manage.py` 之后添加使用;
|
||||
|
||||
你可以使用 `-h` 或 `--help` 查看它的帮助:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ youqu manage.py -h
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
这样可以查看它支持的子命令;
|
||||
|
||||
然后再通过子命令 `-h` 或 `--help` 查看子命令的帮助,以子命令 `run` 举例:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ youqu manage.py run -h
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
这样可以查看到子命令支持的各项参数及参数使用说明。
|
||||
|
||||
**【配置文件】**
|
||||
|
||||
配置文件在 `setting` 目录下,绝大部分的配置项均在 `globalconfig.ini` 文件中,为了方便描述后面经常提到的“配置文件”、“配置项”几乎都是指的 `setting/globalconfig.ini `。
|
||||
|
||||
你可以在配置文件中每一个配置项上面看到该配置项的使用说明;
|
||||
|
||||
在后面的一些功能描述中有些为了方面描述只提到了配置项的使用说明,并没有再补充其对应的命令行参数,但是几乎所有的命令行参数都对应提供了配置文件配置项;
|
||||
|
||||
???+ Note
|
||||
命令行参数的优先级高于配置文件配置,也就是说通过命令行参数指定了对应的参数,配置文件中不管是否配置均不生效。
|
||||
|
||||
下面介绍两个常用的用例执行的功能:
|
||||
|
||||
### 2. 本地执行
|
||||
|
||||
本地执行子命令为:`run`
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ youqu manage.py run
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
#### 2.1. 命令行参数
|
||||
|
||||
通过命令行参数配置参数
|
||||
|
||||
以下为 `youqu manage.py run` 提供的一些常用的参数选项:
|
||||
|
||||
```coffeescript
|
||||
-h, --help show this help message and exit
|
||||
-a APP, --app APP 应用名称:apps/autotest_deepin_music 或
|
||||
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包的本地路径
|
||||
-u PMS_USER, --pms_user PMS_USER
|
||||
pms 用户名
|
||||
-p PMS_PASSWORD, --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
|
||||
重启类场景开启letmego执行方案
|
||||
```
|
||||
|
||||
在一些 `CI` 环境下使用命令行参数会更加方便:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ youqu manage.py run --app autotest_deepin_music --keywords "xxx" --tags "xxx"
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
`--app` 入参还支持 `apps/autotest_xxx` 写法,方便在输入命令的过程中使用补全,下面的远程执行功能同样支持。
|
||||
|
||||
#### 2.2. 配置文件
|
||||
|
||||
通过配置文件配置参数
|
||||
|
||||
在配置文件 `setting/globalconfig.ini` 里面支持配置对执行的一些参数进行配置,常用的如:
|
||||
|
||||
```ini
|
||||
--8<-- "setting/globalconfig.ini"
|
||||
```
|
||||
|
||||
配置完成之后,直接在命令行执行 `manage.py` 就好了。
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ youqu manage.py run
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
### 3. 远程执行
|
||||
|
||||
远程执行就是用本地作为服务端控制远程机器执行,远程机器执行的用例相同;
|
||||
|
||||
使用 `remote` 命令:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ youqu manage.py remote
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
#### 3.1. 多机器分布式异步执行
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f6d3c0661c6c8e549f8ca5.png)
|
||||
|
||||
多机器分布式异步执行就是由本地 `YouQu` 作为服务端,控制远程 N 台机器执行相同的用例,执行完之后所有测试机的测试结果会返回给服务端 report 目录下;
|
||||
|
||||
远程执行同样通过配置文件 `setting/globalconfig.ini` 进行用例相关配置;
|
||||
|
||||
需要重点说一下远程执行时的测试机信息配置,在配置文件 `setting/remote.ini` 里面配置测试机的用户名、`IP`、密码。
|
||||
|
||||
```ini
|
||||
--8<-- "setting/remote.ini"
|
||||
```
|
||||
|
||||
有多少台机器就像这样参考上面的格式写就行了。
|
||||
|
||||
然后在命令行:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ youqu manage.py remote
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
这样运行是从配置文件去读取相关配置。
|
||||
|
||||
如果你不想通过配置文件,你仍然通过命令行参数进行传参,
|
||||
|
||||
以下为 `python3 manage.py remote` 提供的一些参数选项:
|
||||
|
||||
```coffeescript
|
||||
-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.xx:1
|
||||
或 uos@10.8.13.xx
|
||||
-s, --send_code 发送代码到测试机(不含report目录)
|
||||
-e, --build_env 搭建测试环境,如果为yes,不管send_code是否为yes都会发送代码到测试机.
|
||||
-p CLIENT_PASSWORD, --client_password CLIENT_PASSWORD
|
||||
测试机密码(全局)
|
||||
-y PARALLEL, --parallel PARALLEL
|
||||
yes:表示所有测试机并行跑,执行相同的测试用例;no:表示测试机分布式执行,服务端会根据收集到的测试用例自
|
||||
动分配给各个测试机执行。
|
||||
```
|
||||
|
||||
除了这些特有参数以外,它同样支持本地执行的所有参数;
|
||||
|
||||
在命令行这样运行:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ youqu manage.py remote -a autotest_deepin_music -c uos@10.8.13.xx/uos@10.8.13.xx -k "xxx" -t "xxx"
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
所有用例执行完之后会在 `report` 目录下回收各个测试机执行的测试报告。
|
||||
|
||||
注意,如果远程机器没有搭建自动化测试环境,记得加上参数 `-e` :
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ youqu manage.py remote -a autotest_deepin_music -c uos@10.8.13.xx/uos@10.8.13.xx -k "xxx" -t "xxx" -e
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
执行前确保远程机器已经开启了 ssh 服务,否则会提示无法连接,如果没有开启,请手动开启:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ sudo systemctl restart ssh
|
||||
$ sudo systemctl enable ssh
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
配置文件其他相关配置项详细说明,请查看配置文件中的注释内容。
|
||||
|
||||
#### 3.2. 多机器分布式异步负载均衡执行
|
||||
|
||||
多机器分布式异步负载均衡执行也是用本地作为服务端控制远程机器执行,但远程机器执行的用例不同,而是所有远程机器执行的用例之和,为你想要执行的用例集;
|
||||
|
||||
似乎有点难以理解,我用大白话举例描述下:
|
||||
|
||||
服务端想要执行 10 条用例,现在远程机器有 5 台,然后服务端就先拿着第 1 条用例给远程 1 号机执行,拿第 2 条用例给远程 2 号机执行...,如此循环直到所有用例执行完,这就是负载均衡执行。
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f6d694661c6c8e54a1025b.png)
|
||||
|
||||
使用方法和前面一样,只是需要增加一个参数 `--parallel`:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ youqu manage.py remote -a autotest_deepin_music -c uos@10.8.13.xx/uos@10.8.13.xx -k "xxx" -t "xxx" --parallel no
|
||||
```
|
||||
|
||||
</div>
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
## 日志系统
|
||||
|
||||
### 1. 背景
|
||||
|
||||
基于 `YouQu` 自动化测试框架操作方法封装写法,通常是这样的:类里面一个函数只包含一个操作或多次调用的一系列可合并的操作;
|
||||
|
||||
**传统的**日志输出方式,需要在每个函数里面主动编写日志输出代码 ,例如:
|
||||
|
||||
```python
|
||||
class XXXWidget:
|
||||
|
||||
def click_xxx(self):
|
||||
"""点击某个元素"""
|
||||
logger.info("点击某个元素")
|
||||
...
|
||||
# 省略代码部分
|
||||
```
|
||||
|
||||
一个函数里面至少包含一条日志信息,如果比较复杂操作步骤,可能包含多条日志信息;
|
||||
|
||||
一个应用含有大量的操作函数,**也就是说我们需要写大量的日志代码;**
|
||||
|
||||
通过经验观察,我们发现,函数说明以及函数操作日志,具有较高的重复度(从上面的例子也可以看出来),因此我们大胆的设想,能不能基于框架执行时,自动的将函数说明作为日志打印出来,从而减少大量日志代码量和重复编写,那真是妙啊~。
|
||||
|
||||
基于此“天真”的想法,我们设计出了 `YouQu` 的日志系统。
|
||||
|
||||
### 2. 实现原理
|
||||
|
||||
核心原理:
|
||||
|
||||
通过给类加上一个装饰器 `@log`,动态给该类下所有的函数及父类函数添加日志装饰器 `@_trace`,在此装饰器中我们能动态的捕获所调用函数的功能说明(`func.__doc__`),通过日志输出器将其输出即可。
|
||||
|
||||
实现步骤:
|
||||
|
||||
1. 通过 ```inspect.getmembers``` 获取被装饰的类下所有函数,包含静态方法,类方法,实例方法;
|
||||
2. 通过 ```setattr(类, 方法, _trace)``` 的方式,给符合条件的函数,动态添加日志装饰器;
|
||||
|
||||
### 3. 日志配置
|
||||
|
||||
```ini
|
||||
[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` 方法库里面,只需要在出口文件加上**类装饰器**,就可以实现自动输出日志了;
|
||||
|
||||
```python
|
||||
# dfm_widget.py
|
||||
from src import log
|
||||
|
||||
@log
|
||||
class DfmWidget():
|
||||
"""文管方法库出口"""
|
||||
|
||||
def click_xxx(self):
|
||||
"""点击某个元素"""
|
||||
```
|
||||
|
||||
注意,是类装饰器。
|
||||
|
||||
实例:
|
||||
|
||||
```python
|
||||
# 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)
|
||||
```
|
||||
|
||||
调用方法时:
|
||||
|
||||
```python
|
||||
DfmWidget.find_dfm_image("dfm_001")
|
||||
```
|
||||
|
||||
自动输出的日志:
|
||||
|
||||
```shell
|
||||
>> 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}}` 作为变量输出出来了;
|
||||
|
||||
没错,这就是我们参考`Django` 和 `jinja2` 的模板语法设计出的**日志模板语法**,使用方法很简单,**用两对大括号把函数的参数括起来**,这样在日志输出的时候就能把调用函数时参数的值输出出来。
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
## 标签化管理
|
||||
|
||||
### 1. 标签说明
|
||||
|
||||
根据现有业务需要,用例需要添加的标签有:
|
||||
|
||||
- ==脚本 ID== :自动化用例脚本/函数 ID;
|
||||
- ==PMS用例ID== :`PMS` 上对应的用例 ID(用例库 ID);默认使用用例库 ID,对于暂时没有使用用例库管理用例的项目,可以使用产品库用例 ID;
|
||||
- ==用例级别== :对应 `PMS` 上用例级别,分别用 `L1、L2、L3、L4` 表示;
|
||||
- ==用例类型== :`FUNC`(功能)、`PERF`(性能)、`STR`(压力)、`SEC`(安全)、`CTS`(兼容性)、`API`(接口)、`BASELINE`(基线-预留)
|
||||
- ==设备类型== :`PPL`(依赖外设的用例)、`COL`(依赖主控机的用例)
|
||||
- ==一二级bug自动化== :`BUG`(由 `Bug` 转的用例)
|
||||
- ==上线对象== :`CICD`,表示上线到 `CICD` 流水线的用例,后续可一键生成 `case_list.csv` 文件,用于导入到明道云 AT 用例列表中控制 `CICD` 跑测范围;
|
||||
- ==跳过原因== :`skip-XXX`,用于控制用例是否执行;
|
||||
- ==确认修复== :`fixed-XXX`,用于标记用例的修复状态(后面详细讲解用法);
|
||||
- ==废弃用例== :`removed-已废弃`,用于标记已经废弃的用例,此用例标签不会被添加,也不会被执行;
|
||||
|
||||
示例:
|
||||
|
||||
| 脚本ID | PMS用例ID | 用例级别 | 用例类型 | 设备类型 | 一二级bug自动化 | 上线对象 | 跳过原因 | 确认修复 | 废弃用例 | ... |
|
||||
| :----: | :-------: | :------: | :------: | :------: | :-------------: | :------: | :------: | :-------: | :------------: | ---- |
|
||||
| 679537 | 679537 | L1 | FUNC | PPL | BUG | CICD | skip-XXX | fixed-XXX | removed-已废弃 | ... |
|
||||
|
||||
### 2. 操作步骤
|
||||
|
||||
2.1、在子项目目录下新建 `csv` 文件,用于保存用例标签,以 ==用例脚本的 py 文件去掉首字符串 "test_" ,去掉用例序号后的字符串,取中间的名称作为 csv 文件的文件名== 。
|
||||
|
||||
例如:
|
||||
|
||||
- ==相册的用例文件为 `test_album_xxx.py`,xxx 表示用例的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`,所有条件判断跳过的函数写在此文件中。
|
||||
|
||||
```python
|
||||
--8<-- "setting/skipif.py"
|
||||
```
|
||||
|
||||
方法编写规范:
|
||||
|
||||
- 方法名必须以 `skipif` 开头;
|
||||
- 方法必须有返回结果并且为布尔值(`True` 代表跳过,`False` 代表不跳过);
|
||||
- 方法只能有一个入参;
|
||||
|
||||
在 `csv` 文件跳过原因一栏中填写为 “`{函数名}-{参数}`”,例如:`skipif_platform-aarch64`;在用例收集阶段会以第一个 “`-`” 进行分割,截取的左侧字符串作为函数名,在 `skipif.py` 文件中查找是否有同名函数,并将截取的右侧作为参数传递给该函数,通过获取该函数返回的布尔值,返回 `True`,则用例不执行,返回 `False`,则执行该用例。
|
||||
|
||||
【重要】
|
||||
|
||||
- 若函数需要多个参数,可自定义多个参数之间的连接符,连接符号不可使用下划线和逗号,推荐统一使用 `&` 符号;
|
||||
|
||||
- 若需要多个 skipif 条件判断组合,使用 `&&` 符号将两个方法分开,比如:skipif_platform-aarch64&&skipif_xdg_type-wayland ;
|
||||
|
||||
#### 3.2. 确认修复
|
||||
|
||||
针对于某些用例修复后,但不能立即删除跳过原因(`skip-XXX`)的用例,新增一列标签名为 “`确认修复`”,作为标记该用例是否已经修复,固定填入字段为 “`fixed-已修复`”。这样这条用例即使同时标记了 `skip-XXX` 也会正常执行。
|
||||
|
||||
示例:
|
||||
|
||||
| 用例ID | ...(各种用例标签) | 跳过原因 | 确认修复 |
|
||||
| :----: | :-----------------: | :-------------------: | :----------: |
|
||||
| 679537 | ... | skip-受到某新需求影响 | fixed-已修复 |
|
||||
|
||||
【同时标记了`skip` 和 `fixed`,但仍然想要跳过用例】
|
||||
|
||||
当 “跳过原因” 和 “确认修复” 中同时填入后,命令行传递参数 `--ifixed yes`,则代码不会执行该条用例。
|
||||
|
||||
```shell
|
||||
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-已废弃 |
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054ca661c6c8e54ff4f70.png)
|
||||
|
||||
|
||||
|
||||
### 4. 设计思路
|
||||
|
||||
上面介绍 `Pytest` 框架提供的标签功能 mark,使用时需要为每一个用例添加标签装饰器,则操作复杂,可维护性差,其根本问题就是标签分散在每一条用例的装饰器上,难以集中维护;于是乎将所有标签使用 `csv` 文件进行集中管理,并通过 `Pytest` 的钩子函数,读取 `csv` 文件,动态添加标签到用例中。
|
||||
|
||||
### 5. CSV文件格式
|
||||
|
||||
此配置文件需要维护大量的标签数据,且要方便能使用 `Excel` 打开进行编辑查看,更重要的是我们不想引入三方依赖,`CSV` 文件几乎是唯一能满足所有的要求的文件格式。
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# 导出CSV文件
|
||||
|
||||
**【使用方法一】**
|
||||
|
||||
框架提供导出指定标签用例的功能:
|
||||
|
||||
```shell
|
||||
youqu manage.py csvctl -a apps/autotest_deepin_album -t CICD -ec case_list.csv
|
||||
```
|
||||
|
||||
表示导出 `deepin-album` 的用例中标记了 `CICD` 标签的用例,导出 `CSV` 文件的字段格式已经适配了 `CICD` 的要求。
|
||||
|
||||
**【使用方法二】**
|
||||
|
||||
也可以使用配置文件选项:
|
||||
|
||||
```ini
|
||||
;导出的csv文件名称,比如:case_list.csv
|
||||
EXPORT_CSV_FILE =
|
||||
```
|
||||
|
||||
命令行执行:
|
||||
|
||||
```shell
|
||||
youqu manage.py csvctl -a apps/autotest_deepin_album -t CICD
|
||||
```
|
||||
|
||||
--------------
|
||||
|
||||
参数说明:
|
||||
|
||||
```ini
|
||||
-a 如果不给就是apps目录下所有的;
|
||||
-t 要导出用例的标签;
|
||||
case_list.csv 文件名称可以自定义,在report目录下会自动生成;
|
||||
```
|
|
@ -0,0 +1,84 @@
|
|||
## 标签自动同步
|
||||
|
||||
### 1. 自动同步脚本ID到CSV文件
|
||||
|
||||
支持自动同步脚本 `ID`(用例 `py` 文件的 `ID`)到 `CSV` 文件;
|
||||
|
||||
**【使用方法一】**
|
||||
|
||||
配置文件方式,通过一下几个配置来控制:
|
||||
|
||||
```ini
|
||||
[csvctl]
|
||||
;将py文件的case id同步到csv文件
|
||||
;yes, 开启同步
|
||||
;no, 关闭同步
|
||||
PY_ID_TO_CSV = yes
|
||||
```
|
||||
|
||||
如果不存在 `CSV` 文件会直接创建一个并写入用例脚本的 `ID`;
|
||||
|
||||
此功能默认会将 `CSV` 文件中多余的 `ID` 行删掉,以处理人工删除了用例脚本文件,但 `CSV` 文件里面对应的 `ID` 行未删除的问题;
|
||||
|
||||
```sh
|
||||
youqu manage.py csvctl
|
||||
```
|
||||
|
||||
**【使用方法二】**
|
||||
|
||||
命令行参数的方式:
|
||||
|
||||
```shell
|
||||
youqu manage.py csvctl -p2c
|
||||
```
|
||||
|
||||
不管配置文件是否配置,通过命令行参数的方式执行优先级总是最高的;
|
||||
|
||||
每次操作会将 `CSV` 文件先备份到 `report/pyid2csv_back` 目录下;
|
||||
|
||||
### 2. 从PMS自动同步标签到CSV
|
||||
|
||||
用于自动同步 `PMS` 用例标签数据至本地 `CSV` 文件;
|
||||
|
||||
**【使用方法一】**
|
||||
|
||||
配置文件方式,通过以下几个配置来控制:
|
||||
|
||||
```ini
|
||||
APP_NAME = # 这个参数可填可不填,但是填了可以提高用例的执行速度,因为在用例收集阶段可以指定到具体的应用库。(下同)
|
||||
PMS_USER = # PMS的用户名
|
||||
PMS_PASSWORD = # PMS的密码
|
||||
```
|
||||
|
||||
在 `[pmsctl-pms_link_csv]` 节点下指定 `CSV` 文件名与 `PMS` 用例模块的对应关系,比如:
|
||||
|
||||
```ini
|
||||
[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
|
||||
```
|
||||
|
||||
将以上信息配置好之后,在命令行执行:
|
||||
|
||||
```shell
|
||||
youqu manage.py pmsctl -p2c
|
||||
```
|
||||
|
||||
每次执行时原 `CSV` 文件会自动备份在 `report` 目录下,因此你不用担心脚本执行导致你的数据丢失。
|
||||
|
||||
**【使用方法二】**
|
||||
|
||||
按照我们一贯的风格,你也可以不去管配置文件,完全通过命令行参数传入:
|
||||
|
||||
```
|
||||
youqu manage.py pmsctl -p2c -u ut00xxxx -p you_password -plc music:81
|
||||
```
|
||||
|
||||
每次操作会将 `CSV` 文件先备份到 `report/pms2csv_back` 目录下;
|
|
@ -0,0 +1,66 @@
|
|||
### 1. 目录结构
|
||||
|
||||
执行时会在根目录下动态生成 `report` 目录,所有的报告相关的文件会统一存放在里面,示例:
|
||||
|
||||
```shell
|
||||
/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` 地址、系统信息、镜像版本等;
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054c2661c6c8e54ff478a.png)
|
||||
|
||||
报告 `UI` 效果会持续优化;
|
||||
|
||||
### 3. 查看报告
|
||||
|
||||
- **本地执行**
|
||||
|
||||
在 `report/allure` 目录下会生成一堆文本文件,这些是 `allure` 插件生成的报告源数据,我们在 `report/allure_html` 目录下给你生成了 `html` 文件,但是你不能直接通过浏览器打开 `index.html` 文件,因为 `allure` 的报告都是基于在线的服务。
|
||||
|
||||
可以直接在 `Pycharm` 里面找到 `index.html` 文件,然后右键选择浏览器打开;
|
||||
|
||||
或者你可以用一个 http 服务打开。
|
||||
|
||||
```shell
|
||||
cd report/allure_html
|
||||
python3 -m http.server
|
||||
```
|
||||
|
||||
然后在浏览器访问:127.0.0.1:8000
|
||||
|
||||
- **远程执行**
|
||||
|
||||
远程执行结束之后,会将所有远程测试机的测试报告都收集到 `report/allure` 目录下,分别按照机器的 `IP` 等建了不同的目录,你可以在这些目录里面去查看对应的测试报告,查看方法和前面本地执行查看的方法一样。
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
## 环境部署-env.sh
|
||||
|
||||
### 1. 安装
|
||||
|
||||
项目根目录下运行 `env.sh` 即可。
|
||||
|
||||
```shell
|
||||
$ bash env.sh
|
||||
# 如果你的测试机密码不是 `1` ,那需要在全局配置文件 `globalconfig.ini` 里面将 `PASSWORD` 配置项修改为当前测试机的密码。
|
||||
```
|
||||
|
||||
|
||||
### 2. 定制依赖
|
||||
|
||||
#### 2.1. 新增依赖
|
||||
|
||||
如果应用库还需要其他 `Python` 依赖库,只需要在应用库根目录下保存一个 `requirement.txt` 文件;
|
||||
|
||||
```shell
|
||||
autotest_xxx
|
||||
├── requirement.txt
|
||||
├── case
|
||||
...
|
||||
```
|
||||
|
||||
里面写入需要安装的三方依赖,比如像这样:
|
||||
|
||||
```shell
|
||||
PyYAML==6.0 # 指定安装某个版本
|
||||
requests # 未指定版本则安装最新版
|
||||
```
|
||||
|
||||
在执行 `bash env.sh` 时会一并将其安装。
|
||||
|
||||
如果多个应用库都存在 `requirement.txt` 文件,执行 `env.sh` 时会将多个 `requirement.txt` 文件一并加载;那么一定要注意多个 `requirement.txt` 文件可能存在相同的依赖被指定安装不同版本等等兼容性问题。
|
||||
|
||||
**【deb 形式 Python 包】**
|
||||
|
||||
在应用库根目录下保存一个 `requirement_deb.txt` 文件;
|
||||
|
||||
```shell
|
||||
autotest_xxx
|
||||
├── requirement_deb.txt
|
||||
├── case
|
||||
...
|
||||
```
|
||||
|
||||
里面写入需要安装的三方依赖,比如像这样:
|
||||
|
||||
```
|
||||
python3-pyaudio
|
||||
```
|
||||
|
||||
#### 2.2. 裁剪依赖
|
||||
|
||||
在某些情况下,可能你只需要安装一些最最基础的依赖,其他的都不需要,比如纯接口自动化的项目,它不需要 `UI` 自动化相关的依赖。
|
||||
|
||||
你只需要在应用库根目录下,存放一个 `BASICENV` 的普通文件,里面不需要写任何内容,这样执行 `env.sh` 时,只会安装最基础的依赖。
|
||||
|
||||
```shell
|
||||
autotest_xxx
|
||||
├── BASICENV # 可以使用:touch BASICENV 创建文件
|
||||
├── case
|
||||
...
|
||||
```
|
||||
|
||||
基础依赖仅安装最基础的几个包;
|
||||
|
||||
`裁剪依赖` 和 `新增依赖` 是不冲突的,可以同时使用。
|
||||
|
||||
### 3. 开发环境部署-env_dev.sh
|
||||
|
||||
在开发过程中,如果你想直接部署在本机上:
|
||||
|
||||
```shell
|
||||
$ bash env_dev.sh
|
||||
```
|
||||
|
||||
这种方式安装的环境不会有 `youqu` 这个命令,用例执行都使用 `python3` 驱动即可,比如:
|
||||
|
||||
```shell
|
||||
$ python3 manage.py run
|
||||
```
|
||||
|
||||
### 4. 虚拟环境解释器
|
||||
|
||||
YouQu 默认采用虚拟化部署,虚拟环境实际安装的位置是在 `$HOME/.local/share/virtualenvs/youqu-oHTM7l7G` 目录下;其中,
|
||||
|
||||
`youqu-oHTM7l7G` 此目录名称前面部分是你的代码根目录的名称,后面部分是生成的随机字符串,同学们在部署的时候随机字符串肯定和我这里的例子不一样;
|
||||
|
||||
在远程机器上定位问题的时候,如果使用 `Pycharm` 调试执行,就将解释器指定到这个目录的就行了;
|
||||
|
||||
### 5. 激活虚拟环境
|
||||
|
||||
在开发过程中有可能需要在终端激活虚拟环境,以便进行一些开发调试;
|
||||
|
||||
在框架根目录下命令行输入:
|
||||
|
||||
```shell
|
||||
$ youqu-shell
|
||||
```
|
||||
|
||||
即可在终端激活当前虚拟环境。
|
||||
|
||||
### 6. 原则
|
||||
|
||||
`YouQu` 的环境依赖一直坚持 2 个原则:
|
||||
|
||||
- **最小环境依赖原则**
|
||||
|
||||
有些同学写一些功能,首先想到的就是去搜索引擎搜一下,看有没有现成的工具或者代码直接能用,美其名曰不要重复造轮子,别人已经造好了轮子咱们就不要重复去造了,然后一切似乎都变得那么的理所应当。
|
||||
|
||||
当然这样做无可厚非,我相信有这样想法并且一直践行这样做法的同学不在少数,但这样做有一个很严重的问题,就是当你写一个大型项目时,你会引入非常非常非常多的依赖,比如要做个接口请求要用 `requests`、读写个数据用 `pandas`、写个表格用 `pyopenxl`、解析个文本要用这个那个三方库 ... 。
|
||||
|
||||
我认为这是非常不好的习惯,在实现一个功能的时候,首先我们应该去想自己怎么样去实现,尽量使用标准库去做;
|
||||
|
||||
如果你不会,你可以去学习三方库的实现思路,别人三方库也是用标准库实现的,为什么咱们不行呢;如果别人的你实在是看不懂,想尽一切办法确实做不出来,那再考虑引入这个三方库,我认为这样是没问题的,毕竟咱们段位还不够,但希望将来有一天我们可以。
|
||||
|
||||
这样的做法才应该是“不要重复造轮子”正确的操作。不会没关系,看看别人是怎么实现的,自己再摸索着写出来,这样自己才能有所提高,不然你就只会用别人的东西;那些嘲笑、批评我们重复造轮子的人,先想想自己会造轮子吗。
|
||||
|
||||
本着**自己实现能实现的一切**的原则,`YouQu` 框架做到了非常少的环境依赖,而且我们仍在不断努力,减少环境部署的依赖。
|
||||
|
||||
- **最小仓库体积原则**
|
||||
|
||||
大文件不能上传到工程里面,保持整个工程的轻量化,这样在克隆代码的时候才能非常快速方便使用,我们见过一个 AT 工程 clone 大小达到好几个 G,简直不可思议。
|
||||
|
||||
其实代码文件的大小是很小的,也就是说,纯写代码随便写工程也不会太大,而文档(包含插图)等资源却是很占空间。
|
||||
|
||||
因此我们要将 `YouQu` 的文档工程涉及到的图片资源都采用外链加载;
|
|
@ -0,0 +1,101 @@
|
|||
## 相对坐标定位
|
||||
|
||||
### 1. 背景
|
||||
|
||||
相对坐标定位方案是是一种基于 UI 的元素定位方案,是我们自研的一个使用简单,且效率极高、稳定性好的元素定位方案,基于元素按钮在应用中的相对位置,动态获取元素在当前屏幕中的位置,适用于各种屏幕分辨率(包括高分屏、宽屏、带鱼屏),当元素按钮位置相对于应用界面位置发生修改之后,只需要根据 UI 设计图上的源数据修改对应坐标数据就好,维护非常的方便。
|
||||
|
||||
此类元素定位方案适用于一些元素位置相对与应用界面比较固定的应用,比如音乐(99% 的元素定位采用这种,效果非常好),不适用于界面不固定的应用,比如截图录屏,很明显不适用于这类元素定位方案。这种全新的元素定位方案有它的适用条件,如果你发现使用常规的(属性定位、图像定位)不好做时,不妨考虑使用这种,其效果一定能惊讶到你,并且迅速爱上他。
|
||||
|
||||
### 2. 实现原理
|
||||
|
||||
在 UI 设计图中我们是可以获取到元素按钮相对于应用边框的距离的,然后我们可以通过技术手段获取到应用界面在当前屏幕中的位置及应用窗口的大小,示意图如下:
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054ca661c6c8e54ff4fcb.png)
|
||||
|
||||
`(x1, y1)`为应用左上角相对于屏幕左上角(0, 0)的位置;`(x2, y2)`是按钮【播放所有】的左上角相对于应用窗口左上角`(x1, y1)`的坐标,那么实际上【播放所有】左上角相对于屏幕左上角(0, 0)的位置为:`( x1 + x2, y1 + y2 )`;
|
||||
|
||||
是按钮【播放所有】的大小(w2, h2),可以从 UI 设计图上获取;
|
||||
|
||||
![](https://pic.imgdb.cn/item/64f054cb661c6c8e54ff5001.png)
|
||||
|
||||
在 UI 设计稿上点击【播放所有】图标,然后移动鼠标就就可以看到上图的参考线及数据;
|
||||
|
||||
所以,我们可以轻松的获取到按钮【播放所有】的中心坐标为:`( x1 + x2 + (w2 / 2), y1 + y2 + ( h2 / 2) )`
|
||||
|
||||
详细源代码请查看 AT 基础框架: `src/button_center.py`
|
||||
|
||||
- 获取应用窗口的信息
|
||||
|
||||
使用 `xdotool` 和 `xwininfo` 获取到应用窗口在当前屏幕中的位置(左上角)及大小。
|
||||
|
||||
代码示例:
|
||||
|
||||
```python
|
||||
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` 配置文件,获取元素按钮相对与应用窗口边界的位置及大小。
|
||||
|
||||
代码示例:
|
||||
|
||||
```python
|
||||
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` 配置文件:
|
||||
|
||||
```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 图上没有提供你想要的元素数据,你可以直接在系统中使用截图录屏进行尺量,这是一种不推荐但能用的方法。
|
||||
|
||||
【调用方法】
|
||||
|
||||
在应用库方法层这样写:
|
||||
|
||||
```python
|
||||
def click_add_music_list_btn_in_music_by_ui(self):
|
||||
"""点击新建歌单按钮"""
|
||||
self.click(*self.ui.btn_center("新建歌单+"))
|
||||
```
|
||||
|
||||
`self.ui.btn_center()` 是固定写法,参数就是配置里面的 `section` 。
|
|
@ -0,0 +1,74 @@
|
|||
## 重启类场景
|
||||
|
||||
对于重启类场景的用例需要解决的核心问题是,重启之后如何让用例能继续重启前的步骤继续执行,`YouQu` 集成了自研的 [letmego](https://linuxdeepin.github.io/letmego/) 技术方案;
|
||||
|
||||
详细技术方案、实现细节、Demo可以看 [letmego](https://linuxdeepin.github.io/letmego/) 官方在线文档;
|
||||
|
||||
### 1. 使用方法
|
||||
|
||||
使用方法很简单,只需要给应用方法层的唯一出口类加一个装饰器(`@letmego.mark`)即可:
|
||||
|
||||
```python
|
||||
import letmego
|
||||
|
||||
@letmego.mark
|
||||
class DeepinMusicWidget(WindowWidget, TitleWidget, PopWidget):
|
||||
"""音乐业务层"""
|
||||
```
|
||||
|
||||
### 2. 用例注意事项
|
||||
|
||||
这类用例相对特殊,这里主要介绍写用例的时候注意事项:
|
||||
|
||||
(1)用例的前置和后置要写在同一个用例文件里面;这点如果了解方案实现原理很容易理解;
|
||||
|
||||
(2)重启步骤前面的步骤,如果有对象实例化的,需要处理实例化存在异常;因为 `YouQu` 的对象实例化默认会检测应用是否启动,重启之后虽然重启步骤前面的步骤函数不会执行,但是方法类同样会进行实例化,所以需要处理这个问题;
|
||||
|
||||
```python
|
||||
# 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()
|
||||
music = DeepinMusicWidget()
|
||||
music.recovery_music_by_cmd()
|
||||
music.first_add_music_by_ui()
|
||||
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;
|
||||
|
||||
```python
|
||||
@letmego.mark
|
||||
class DeepinMusicWidget(WindowWidget, TitleWidget, PopWidget):
|
||||
|
||||
@staticmethod
|
||||
def reboot():
|
||||
"""letmego reboot"""
|
||||
os.system("echo '1' | sudo -S reboot")
|
||||
```
|
||||
|
||||
### 3. 驱动执行
|
||||
|
||||
因为重启类场景需要注册自启服务以及对用例执行过程的处理,驱动执行的时候加 `--autostart yes` :
|
||||
|
||||
```shell
|
||||
youqu manage.py run --autostart yes
|
||||
```
|
||||
|
||||
### 4. 执行环境
|
||||
|
||||
默认使用虚拟环境执行,也就是说如果您是部署自动化环境是用的 `env_dev.sh` 是无法使用此技术方案的,解决方法也很简单,执行一下:`bash env.sh`,以此激活虚拟环境即可。
|
|
@ -0,0 +1,53 @@
|
|||
## 键鼠操作
|
||||
|
||||
`YouQu` 键鼠操作模块集成了多个键鼠操作的方案:`PyAutoGUI`、`Xdotool`、`wayland_autotool`;
|
||||
|
||||
有同学肯定要问,我之前就只用 `PyAutoGUI` 也都挺好的,好像用不到这么多键鼠操作的东西吧~;
|
||||
|
||||
任何模块当然是希望越简洁通用越好,但问题是没有一种方案是通用的,它们都有自己存在的问题或者说不适用的场景,如果你还没有遇到,只能说使用的场景还不够多。
|
||||
|
||||
### 1. 常规键鼠操作
|
||||
|
||||
`YouQu` 的键鼠操作模块主要有两个:`MouseKey`、`ShortCut`
|
||||
|
||||
分别是 **键鼠的基本操作模块** 和 **快捷键模块**,基本上你能用到的键鼠操作,这里都包含了,在用例里面只需要通过方法层的类对象直接调用即可,比如:
|
||||
|
||||
```python
|
||||
from apps.autotest_deepin_music.widget.deepin_music_widget import DeepinMusicWidget
|
||||
|
||||
DeepinMusicWidget.click()
|
||||
DeepinMusicWidget.ctrl_c()
|
||||
```
|
||||
|
||||
当然你也可以直接通过 `src` 导入使用:
|
||||
|
||||
```python
|
||||
from src import Src
|
||||
|
||||
Src.click()
|
||||
Src.ctrl_c()
|
||||
```
|
||||
|
||||
再或者直接导入键鼠模块使用:
|
||||
|
||||
```python
|
||||
from src.mouse_key import MouseKey
|
||||
from src.shortcut import ShortCut
|
||||
|
||||
MouseKey.click()
|
||||
ShortCut.ctrl_c()
|
||||
```
|
||||
|
||||
我们**推荐第一种**使用方法,因为你写用例层肯定是会导入方法层出口类的,你不需要有额外的导入代码即可使用到所有的方法。
|
||||
|
||||
### 2. 特殊场景键鼠操作
|
||||
|
||||
一些特殊场景下,无法使用上述的键鼠工具,比如在注销登录界面(没有进入系统),调用上述方法会报错,我们提供了另外一种解决方案:`ydotool`
|
||||
|
||||
```python
|
||||
from src.mouse_key import MouseKey
|
||||
|
||||
MouseKey.press_key("enter", _ydotool=True)
|
||||
```
|
||||
|
||||
此方案用到的场景不多,目前仅对 `press_key` 方法加入了此功能,如果还需要其他的操作方法,请提 `issues` 或 `PR`;
|
|
@ -0,0 +1,123 @@
|
|||
静态代码扫描
|
||||
--------------
|
||||
|
||||
### 1. 提前解决代码问题
|
||||
|
||||
为了帮助开发者统一代码风格,`Python` 社区提出了 `PEP8` 代码编码风格,`Python` 官方同时推出了一个检查代码风格是否符合 `PEP8` 的工具,名字也叫 `PEP8`。
|
||||
|
||||
但是,`Pycharm` 里面的 `PEP8` 插件实际上并不能发现很多代码问题,这些问题并不会在运行时报错,因为从 `Python` 语言角度并不关心这些问题,在 `Pycharm` 编辑器里面使用快捷键 `ctrl + alt + L`,有一点点格式化的效果,能解决一些空格、换行等小问题。
|
||||
|
||||
这里推荐一个 `Python` 社区流行的代码格式化工具:
|
||||
|
||||
`Black`,号称不妥协的代码格式化工具,它检测到不符合规范的代码风格直接就帮你全部格式化好,就是这么刚!
|
||||
|
||||
安装:
|
||||
|
||||
```python
|
||||
sudo pip3 install black
|
||||
```
|
||||
|
||||
使用方法:
|
||||
|
||||
```
|
||||
black ${CheckPath}
|
||||
```
|
||||
|
||||
使用这个工具格式化之后,代码会被自动调整,刚开始你可能会觉得调整得很夸张,没关系坚持看,习惯之后,你会觉得很优雅,没错,这就是 `Pythonic Code` 的核心,请保持优雅~。
|
||||
|
||||
### 2. 代码扫描工具
|
||||
|
||||
使用根目录下 `pylint.sh` 扫描代码,在 `report` 目录下查看代码扫描报告,如果有代码问题请提前解决之后再提交。
|
||||
|
||||
此脚本已经使用 `Python` 社区最流行的代码扫描工具 `Pylint` 进行代码扫描。
|
||||
|
||||
使用方法:
|
||||
|
||||
```shell
|
||||
bash pylint.sh
|
||||
```
|
||||
|
||||
运行之后会提示你要扫描的目录,比如输入 `apps` ,则会扫描 `apps` 下的所有 `Python` 代码。
|
||||
|
||||
在 `setting/pylintrc.cfg` 配置文件里面进行相关配置,扫描完成之后在 `report/pylints` 目录下会生成扫描报告。
|
||||
|
||||
代码提交需通过 `git review` 提交到 `gerrit` ,人工 `Code Review` 通过之后合入代码。
|
||||
|
||||
### 3. 安装依赖
|
||||
|
||||
```shell
|
||||
sudo apt install git-review
|
||||
```
|
||||
|
||||
### 4. 提交模板
|
||||
|
||||
在 `~` 目录下新建文件,并命名为 `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:
|
||||
```
|
||||
|
||||
命令行执行:
|
||||
|
||||
```shell
|
||||
git config --global commit.template ~/gitcommit_template
|
||||
```
|
||||
|
||||
此命令将模板加入到 `git` 的提交模板中。
|
||||
|
||||
后续提交的时候需要关注一下几点:
|
||||
|
||||
- `commit type` 对应不同的修改类型:`fix`(问题修复)、`feat`(功能开发)、`style` (风格修改)、`refactor` (重构)、`docs`(文档)、`chore`(其他)、`test` (测试)
|
||||
- `commit type` 冒号后面加**空格**。
|
||||
- `Description` 必要的情况下需要进行详细说明,比如对功能进行大改等。
|
||||
|
||||
### 5. 推送代码
|
||||
|
||||
首先添加 `commit` 信息
|
||||
|
||||
```shell
|
||||
git add 后面加文件名称
|
||||
```
|
||||
|
||||
```shell
|
||||
git add . # 表示添加所有文件
|
||||
```
|
||||
|
||||
使用这条命令注意有些临时文件不要提交到仓库了。
|
||||
|
||||
```shell
|
||||
git commit -a
|
||||
```
|
||||
|
||||
之后在 `fix: ` 后面(注意冒号后面必须加空格,不然直接-1)写本地提交的 `commit` 信息,然后就可以提交代码了
|
||||
|
||||
```shell
|
||||
git review branch(当前分支) -r origin
|
||||
```
|
||||
|
||||
最好一次 `commit`,一次 `git review` ,经常有同学在本地疯狂 `commit` 最后 `git review` 报错不知道咋回事儿。
|
||||
若不想每次提交都加上 `-r` 选项,执行以下命令:
|
||||
|
||||
```shell
|
||||
git config --global gitreview.remote origin
|
||||
```
|
||||
|
||||
提交代码,直接使用 `git review branch<目标分支>`,例如 `git review master`
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
hide:
|
||||
- navigation
|
||||
---
|
||||
# 框架生态
|
||||
|
||||
## :fontawesome-brands-github: 插件生态
|
||||
|
||||
---
|
||||
|
||||
<div class="grid cards hvr-grow-shadow hvr-overline-from-center" markdown style="width: 850px">
|
||||
|
||||
:fontawesome-brands-github:{ .lg .middle } __funnylog__ [:octicons-arrow-right-24: 详情](https://linuxdeepin.github.io/funnylog){target="_blank"}
|
||||
|
||||
一个简单易用、功能强大的日志工具,只需要加一个装饰器,就能自动日志输出类里面所有的方法的功能说明。
|
||||
|
||||
</div>
|
||||
---
|
||||
<div class="grid cards hvr-grow-shadow hvr-overline-from-center" markdown style="width: 850px">
|
||||
|
||||
:fontawesome-brands-github:{ .lg .middle } __letmego__ [:octicons-arrow-right-24: 详情](https://linuxdeepin.github.io/letmego){target="_blank"}
|
||||
|
||||
一个控制 Python 函数执行的技术方案,目前主要应用场景是在自动化测试程序遇到不得不中断的场景下,如重启场景,需要实现自动化用例步骤执行过程中重启机器,机器重新启动之后,能再次继续紧接着重启前的用例步骤执行的功能。
|
||||
|
||||
</div>
|
||||
---
|
||||
<div class="grid cards hvr-grow-shadow hvr-overline-from-center" markdown style="width: 850px">
|
||||
|
||||
:fontawesome-brands-github:{ .lg .middle } __pdocr-rpc__ [:octicons-arrow-right-24: 详情](https://linuxdeepin.github.io/pdocr-rpc/){target="_blank"}
|
||||
|
||||
基于 PaddleOCR 封装的 RPC 服务,包含客户端和服务端,客户端提供了一个简单易用的函数 ocr,通过不同的参数控制返回不同的值。
|
||||
|
||||
</div>
|
||||
---
|
||||
<div class="grid cards hvr-grow-shadow hvr-overline-from-center" markdown style="width: 850px">
|
||||
|
||||
:fontawesome-brands-github:{ .lg .middle } __image-center__ [:octicons-arrow-right-24: 详情](https://linuxdeepin.github.io/image-center/){target="_blank"}
|
||||
|
||||
图像识别定位某个元素在当前屏幕中的坐标,在自动化测试中获取到元素坐标之后,可以传入到键鼠工具,从而实现对目标元素的操作。
|
||||
|
||||
</div>
|
||||
|
||||
## :file_folder: 衍生工具
|
||||
|
||||
---
|
||||
<div class="grid cards hvr-grow-shadow hvr-overline-from-center" markdown style="width: 850px;">
|
||||
|
||||
:file_folder:{ .lg .middle } __工具库__ [:octicons-arrow-right-24: 详情](http://youqu.uniontech.com/tool/){target="_blank"}
|
||||
|
||||
各种工具(Bug定级工具、备注模板、常用网址、内网激活等等)
|
||||
|
||||
</div>
|
||||
---
|
||||
<div class="grid cards hvr-grow-shadow hvr-overline-from-center" markdown style="width: 850px;">
|
||||
|
||||
:file_folder:{ .lg .middle } __知识库__ [:octicons-arrow-right-24: 详情](http://youqu.uniontech.com/docs/){target="_blank"}
|
||||
|
||||
知识库网站
|
||||
|
||||
</div>
|
|
@ -0,0 +1,328 @@
|
|||
# 自动化测试架构设计规划
|
||||
|
||||
```shell
|
||||
# ====================================
|
||||
# Author : mikigo
|
||||
# ====================================
|
||||
```
|
||||
|
||||
[comment]: <> (- 分类:/)
|
||||
|
||||
[comment]: <> (- 架构师:/)
|
||||
- 目标:
|
||||
- 应用 AT 架构工程化,参考性能自动化工程完成工程化改造。
|
||||
- 应用间用例解耦,解除所有交叉调用的方法,各应用能跟随自身迭代周期独立维护 AT 用例。
|
||||
- 完成公共方法的抽取整合,形成一套独立于应用间的公共方法库,各应用方法里面不存在被多个应用调用的情况。
|
||||
- 用例实现标签化管理,为将来适配更多的 AT 运用场景提供支撑,原则上可实现无限扩展,在每日构建和持续集成流程落地使用。
|
||||
- 代码规范问题清零,符合 `Shell Check`、`Pylint`、系统部相关编码规范要求。
|
||||
- 意义:
|
||||
- 统一成研 AT 架构设计思路,消除 AT 代码实现和维护上可能出现的分歧,改善历史 AT 无工程化设计的缺陷,提高架构专业性。
|
||||
- 各应用 AT 代码相互独立,契合应用独立发布特性,可支持迭代期间独立新增、维护和执行。
|
||||
- 方法调用逻辑得到简化,编写和维护更高效;公共库抽取,减少方法重复编写,提高代码利用率;平均编写一条用例从 40 分钟降低至 30 分钟,日产出用例从 12 条/天提升至 16 条/天。
|
||||
- 可灵活支撑不同的自动化运用场景,如 CI、冒烟测试、集成测试、回归测试、专项测试等。
|
||||
|
||||
## 一、背景介绍
|
||||
|
||||
!!! note "提示"
|
||||
这里介绍以前的AT框架情况以及存在的一些问题;
|
||||
|
||||
### 1、原有架构介绍
|
||||
|
||||
??? note "原AT框架架构图"
|
||||
![](https://pic.imgdb.cn/item/64f054c5661c6c8e54ff498d.png)
|
||||
|
||||
原有自动化测试架构整体分为三层:用例层(业务逻辑层)、中间层(元素定位和操作方法层)、核心层(底层功能库层)。
|
||||
|
||||
- 用例层:即应用的用例,专注于业务功能逻辑,不关心元素的定位和操作;
|
||||
- 中间层:元素的定位和操作方法层,每个方法均对应一个元素的一个具体操作,也可以是多个操作的组合操作,中间层主要服务于用例层,具有可扩展性和复用性;
|
||||
- 核心层:主要封装的底层功能实现,此层在框架中提供一些通用的接口能力,功能模块相对独立,核心层主要服务于操作层,比如:通过图像识别的元素定位模块、通过 `UI` 坐标的元素定位模块、通过属性定位的元素定位模块、键鼠操作的基础方操作模块、`Dbus` 接口操作模块、文件的增删改查操作模块等。
|
||||
|
||||
### 2、自动化的应用
|
||||
|
||||
- CI 流程
|
||||
|
||||
- 每日构建流水线:是对研发每日提交的代码进行测试,AT 的大致流程:每日下班之后会将各应用进行打包,然后在测试机上安装更新 deb 包,最后进行自动化测试。
|
||||
|
||||
- 持续集成流水线:是对应用提交集成的版本进行测试,AT 的大致流程:每日下载最新的 ISO 进行 `PXE` 部署,然后测试机安装最新的镜像,最后进行自动化测试。
|
||||
|
||||
- 验收测试
|
||||
|
||||
- 在各验收节点进行自动化验收,目前的策略是全用例覆盖全架构。
|
||||
|
||||
- 回归测试
|
||||
|
||||
- 回归测试今年规划建设中,旨在回归测试时执行自动化测试用例,减少功能测试的重复劳动力。
|
||||
|
||||
### 3、存在的问题
|
||||
|
||||
- 各应用之间存在耦合
|
||||
|
||||
在自动化测试项目初期,所有应用是整体发布,我们是将所有应用看成是一个整体,各应用作为其中的一个模块,所以存在应用间方法交叉调用的问题,这样从最初的设计来讲确实能够减少重复代码的编写。
|
||||
|
||||
但是,现在应用走独立发布,各应用都有自己的迭代节奏,在新需求快速变化的过程中,自动化维护变得异常困难,原因就是自动化项目里面各个应用的有比较多的耦合关系,因此我们需要进行解耦,以适应应用不同的迭代周期。
|
||||
|
||||
- 无法精准的划分用例范围
|
||||
|
||||
用例执行的范围不够精准,目前自动化用例执行时,主要通过用例的关键词 core(核心)来区分用例是否为核心用例,但是这样的区分太宽泛了,不能适应自动化测试在多场景下的应用。很多场景下我们还需要根据用例的等级、用例的类型、用例来源等等,不同的维度来挑选要执行的用例。在用例脚本中添加关键字需要人工一条条的改,费时费力,而且不好维护。
|
||||
|
||||
- 受新需求影响跳过的用例不好维护
|
||||
|
||||
目前需要跳过的用例都需要在对应的用例脚本里面,添加跳过用例的代码,后续解除跳过的时候又需要找到这条脚本,删掉跳过用例的代码。在跳过用例较多的情况下,维护起来有难度。
|
||||
|
||||
- 编写用例时逻辑比较复杂,需要调用多个应用的方法模块。
|
||||
|
||||
- 框架扩展性不足,无法整合性能自动化、压测自动化、安全自动化等专项测试。
|
||||
|
||||
|
||||
## 二、方案设计
|
||||
|
||||
为解决以上问题,适应不断丰富的测试场景,更好的发挥自动化测试的作用,需要对自动化架构及各功能模块进行重新设计规划。
|
||||
|
||||
### 1、架构设计
|
||||
|
||||
???+ note "现AT框架架构图"
|
||||
![](https://pic.imgdb.cn/item/64f054c4661c6c8e54ff4948.png)
|
||||
|
||||
### 2、==设计思路==
|
||||
|
||||
框架的运行逻辑:
|
||||
|
||||
通过核心层提供一个基础能力,业务层根据实际业务需求(测试用例)动态加载核心层,执行入口加载相应的用例集并控制执行,应用层根据实际测试需求,通过相应的配置项进行配置,从而触发自动化测试任务。
|
||||
|
||||
- ==核心层==:
|
||||
|
||||
基本保持不变,部分模块会涉及到新功能开发,核心层各功能模块保持独立性,提供通用的接口能力,供上层调用;底层核心模块包括:
|
||||
|
||||
- 图像识别模块
|
||||
- `UI` 定位模块
|
||||
- `Dbus` 接口操作模块
|
||||
- 属性定位模块
|
||||
- 日志模块
|
||||
- 键鼠操作模块
|
||||
- 文件操作模块
|
||||
- 录屏模块
|
||||
- 自定义断言模块
|
||||
- 用例执行模块
|
||||
- `PXE` 装机模块
|
||||
- 键鼠信号模拟模块
|
||||
- `OCR` 模块
|
||||
|
||||
- ==业务层==:
|
||||
|
||||
以应用为维度划分,应用内包含多个测试类型,如功能测试、性能测试、漏洞扫描等,后续可以根据需要嫁接进来。其中功能测试设计思路:
|
||||
|
||||
- 以应用为维度划分,并将测试数据和测试资源整合进来,增加用例标签 `csv` 文件,用于给每条用例打标签。
|
||||
|
||||
各标签所使用对应的字段名称,使用 `csv` 文件维护用例与标签的对应关系,对用例实现标签化管理,可以组合其中的标签而从驱动对应的自动化用例执行,兼容现有用例标签,且支持用例标签可扩展;
|
||||
|
||||
!!! note "为什么使用csv格式"
|
||||
使用 `csv` 格式文件可以方便的使用 Excel 表格打开进行编辑,同时由于 `csv` 文件实际是以都好分隔的文本文件,代码中可以在不依赖三方库的情况下方便快速的解析它,可操作性和可维护性较高。
|
||||
|
||||
- 各个应用之间,用例、方法、标签和资源都是相互独立的,编写和维护用例时只需要自己应用下的方法和公共库即可。
|
||||
|
||||
- 结构举例:
|
||||
|
||||
```shell
|
||||
.
|
||||
├── apps
|
||||
│ ├── deepin_album # 应用名 (用下划线连接是 Python 编码规范)
|
||||
│ │ ├── album_assert # 断言库
|
||||
│ │ ├── album_function_tag.csv # 用例标签
|
||||
│ │ ├── asan_cases # 漏洞扫描用例
|
||||
│ │ ├── function_cases # 功能测试用例
|
||||
│ │ ├── res # 测试资源
|
||||
│ │ ├── config # 应用内局部配置模块
|
||||
│ │ └── widget # 方法库
|
||||
│ │ ├── album_widget # 应用自己的方法库
|
||||
│ │ ├── base_widget # 方法基类
|
||||
│ │ └── other_widget # 调用其他应用的方法库
|
||||
│ ├── deepin_camera
|
||||
│ │ ├── function_cases
|
||||
│ │ ...
|
||||
│ └── public_widget # 公共方法库
|
||||
├── globalconfig # 全局配置模块
|
||||
...
|
||||
```
|
||||
|
||||
- ==globalconfig 配置模块==:
|
||||
|
||||
可以根据需要进行相应配置,测试同学可以根据自己的测试计划,在 `globalconfig` 里面进行配置。
|
||||
|
||||
全局配置项:
|
||||
|
||||
- 执行一个或多个应用的用例:在 `pattern` 里面写入应用包名,多个应用之间用 `or` 连接,如 `deepin-music or deepin-movie`;
|
||||
|
||||
- 冒烟测试:在 `tags` 里面配置为 `smoke`;
|
||||
|
||||
- 集成测试:在 `tags` 里面配置为 `core` ;
|
||||
|
||||
- 全量测试:在 `tags` 里面为空即可;
|
||||
|
||||
!!! note ""
|
||||
通过 `tags` 的配置比较灵活,后面标签化管理章节会讲到,支持标签的逻辑组合,可以根据需要进行灵活配置。
|
||||
|
||||
- 指定某台机器在指定镜像版本上执行用例:在 `IP` 里面配置测试机 `IP`,并在 `URL` 里面填入镜像的下载地址,框架会调用 `PXE` 进行自动装机,装机完之后自动开始执行配置的测试用例。
|
||||
|
||||
==应用内局部配置==:
|
||||
|
||||
每个应用内部会有一个单独的配置模块,会包含一些本应用的测试资源的路径、执行用例的标签配置等等,如果在局部配置里面配置了用例执行标签,而外层执行器没有指定执行标签,则在执行时只会执行局部配置已配置的,若外层执行器也配置了执行标签,则会按照全局配置执行用例。
|
||||
|
||||
不同测试类型的配置都在同一个配置文件里面,`py` 文件里面分不同的类,`ini` 文件里面分不同的 `option`。
|
||||
|
||||
全局配置和局部配置的策略如下:
|
||||
|
||||
- 全局配置了执行的用例标签,局部配置未配置,则按照全局配置执行。
|
||||
- 全局配置未配置,局部配置了执行的用例标签,则按照局部配置执行。
|
||||
- 全局配置了执行的用例标签,局部配置了执行的用例标签,则按照全局配置执行。
|
||||
|
||||
- ==应用层==:
|
||||
|
||||
`runner` 是测试执行的入口,它会根据配置里面的配置项,进行用例的加载和执行。它提供接口给自动化测试平台,平台上的指令实际上都是通过下发给执行器,然后由执行器来执行相应的测试。
|
||||
|
||||
- ==自动化测试平台==
|
||||
|
||||
是一个前端系统,可以进行测试机管理、自动安装镜像、自动安装指定应用版本、进行测试用例范围选择、用例触发执行控制、结果展示输出等。
|
||||
|
||||
- 一个应用的功能测试、性能测试、漏洞扫描用例都可以单独触发。
|
||||
|
||||
- 兼容性测试主要通过 `PXE` 服务器对测试机进行装机,然后配合 AT 进行测试,可以实现不同系统版本、不同应用版本环境上都可以进行自动化的 AT 执行,提高兼容性测试效率。
|
||||
|
||||
## 三、详细方案
|
||||
|
||||
### 1、用例解耦
|
||||
|
||||
存在耦合关系的方法:这个方法存在被外部应用调用的情况,则这个方法存在耦合关系。
|
||||
|
||||
1.1、通过编辑器 `Find Usages` 查看每个方法被调用的路径和被调用次数;
|
||||
|
||||
1.2、如果某个方法被 1 个外部应用调用,则在外部应用下新建一个 `widget` 的 Python 文件,在文件中写一个 `Widget` 类,在类中重写此方法;
|
||||
|
||||
1.3、操作层文件名均以 `widget` 结尾,类名以 `Widget` 结尾,如:文件名 `music_widget.py` :
|
||||
|
||||
```python title="music_widget.py"
|
||||
class MusicWidget:
|
||||
"""音乐的操作方法类"""
|
||||
|
||||
def click_xxx_by_attr(self):
|
||||
"""通过属性定位的方式,点击某个元素"""
|
||||
...
|
||||
def ...
|
||||
```
|
||||
|
||||
### 2、公共库建设
|
||||
|
||||
2.1、如果某个方法被 2 个及以上的外部应用调用,则在 ` public` 目录下新建一个 `widget` 的 Python 文件,在文件中写一个 `Widget` 类,在类中重写此方法;`public` 即为公共方法库;
|
||||
|
||||
2.2、在用例层修改用例中类的导入路径,属于公共方法的则调用 `public` 中的类,外部应用的操作方法,则调用本应用目录下重写的外部应用方法。
|
||||
|
||||
比如:几乎所有多媒体应用都需要通过文管加载资源,调起的文管对话框实际为 `dde-desktop`,因此将 `dde_desktop_public_widget.py` 放到 `public` 里面:
|
||||
|
||||
```python title="dde_desktop_public_widget.py"
|
||||
class DdeDesktopPublicWidget:
|
||||
"""公共-桌面的操作方法"""
|
||||
|
||||
def click_xxx_by_attr(self):
|
||||
"""通过属性定位的方式,点击某个元素"""
|
||||
...
|
||||
def ...
|
||||
```
|
||||
|
||||
### 3、用例标签化管理
|
||||
|
||||
3.1、根据业务需要,用例可以添加对应的标签,比如:
|
||||
|
||||
- 用例级别:对应 `PMS` 上用例级别,分别用 `L1、L2、L3、L4` 表示;
|
||||
- 用例类型:对应 `core`、`smoke`,或为空;
|
||||
- 用例来源:对应 `PMS` 用例来源;
|
||||
|
||||
举例:
|
||||
|
||||
| 用例ID | 用例级别 | 用例类型 | 用例来源 |
|
||||
| :----: | :------: | :------: | :------: |
|
||||
| 001 | `L1` | `core` | `xxx` |
|
||||
|
||||
==标签支持扩展;==
|
||||
|
||||
3.2、在每个应用目录下新建 `csv` 文件,用于保存用例标签,第一列为用例的 ID,从第二列开始及之后的列,每一列都是一个用例标签;后续需要新增用例标签,可以直接在 `csv` 文件里面添加对应的列即可;
|
||||
|
||||
对于用例规模比较大的应用,比如文件管理器,建议分模块,每个模块建立一个 `csv` 文件,用于管理模块内的用例标签。是否分模块维护 `csv` 取决于应用的用例复杂度,同时我们应该充分考虑后期的可维护性,`csv` 文件太多了也是一个很糟糕的事情。
|
||||
|
||||
3.3、对照 `PMS` 上用例等级、用例类型和用例来源,标记所有已实现的用例标签,后续编写新增自动化用例时,每写一条都需要在对应的 `csv` 文件里面标记此条用例的标签。
|
||||
|
||||
3.4、跳过用例标签化
|
||||
|
||||
现有跳过用例的方式是在用例脚本里面给用例添加装饰器,解除跳过时将装饰器代码删掉,这种方式需要修改用例代码,而通过 `csv` 文件来管理跳过用例则会方便很多:
|
||||
|
||||
举例:
|
||||
|
||||
| 用例ID | ...(各种用例标签) | 跳过原因 |
|
||||
| :----: | :-----------------: | :-------------------: |
|
||||
| 001 | ... | skip-受到某新需求影响 |
|
||||
|
||||
- 将跳过用例操作也整合进入用例标签,在 `csv` 文件中新增一列为“跳过原因”;
|
||||
- 如果应用受到新需求影响需要跳过,则在此列备注具体的跳过原因。跳过的原因统一标记为 “`skip-跳过原因`”;
|
||||
- 用例执行时判断 `csv` 文件里面跳过原因列是否存在跳过标记,如果已经标记了跳过原因,最终的用例状态会被标记为 `SKIPED`,用例也不会被执行。
|
||||
|
||||
### 4、用例执行
|
||||
|
||||
4.1、标签化管理的驱动执行逻辑功能实现
|
||||
|
||||
- 开发根据用例标签文件里面的用例标签执行对应用例的功能,能支持多个标签的逻辑组合,执行入口能随意通过用例标签指定要执行的用例。
|
||||
- ==用例标签的驱动方式必须能支持标签的扩展,未来随着业务的变化可能需要增加各种各样的标签。==
|
||||
|
||||
4.2、不同测试类型的用例执行
|
||||
|
||||
功能测试、漏洞扫描、性能测试等不同测试类型的用例是分开执行的,也就是在一次执行中,只能执行其中一种测试类型,具体要执行哪一种同样通过参数来控制。
|
||||
|
||||
4.3、分布式轮换执行
|
||||
|
||||
由于测试机资源有限,随着自动化用例数量的增加,CI 执行时间会越来越长。
|
||||
|
||||
分布式轮换执行的功能:
|
||||
|
||||
- 同一个应用的用例分散到不同架构的测试机上执行,缩短执行总时间;
|
||||
- 第二天跑的时候同一个机器上会执行昨天没有执行到的用例,后续执行同理;
|
||||
- 可以实现在全架构测试机上轮流执行用例,既能保证执行了所有的用例,又能在资源有限的情况下覆盖了所有的架构。
|
||||
|
||||
### 5、自动化测试平台
|
||||
|
||||
5.1、自动化测试平台作为前端界面系统,通过页面上的一些功能选项进行对应测试任务的管理或触发,业内比较流行的实现方案是使用 `Vue + DRF` 实现一个前后端分离的系统。
|
||||
|
||||
测试平台可能会涉及到的模块有:测试机资源管理模块、用例执行控制模块、结果展示模块、`PXE` 镜像安装模块等。
|
||||
|
||||
??? note "想要的预期"
|
||||
用户(测试、研发同学等)可以配置自己的测试计划,如执行哪个应用、执行用例的范围、在哪台机器上执行、镜像版本及下载地址、应用版本及下载地址、执行时间。
|
||||
|
||||
5.2、执行入口提供给测试平台的接口包括:用例执行接口、结果返回接口、镜像下载接口、测试机镜像安装接口、应用下载接口、测试机应用安装更新接口等。
|
||||
|
||||
前端平台目前还没有太多详细的方案,本次设计主要集中在后端这部分架构的设计上。
|
||||
|
||||
## 四、实施计划
|
||||
|
||||
近期任务计划
|
||||
|
||||
| 阶段目标 | 计划开始时间 | 计划结束时间 |
|
||||
| :----------------------------------------------------------- | :----------: | :----------: |
|
||||
| 应用解耦、公共方法库抽离和方法文档整理:文管代码解耦 | 2022/3/28 | 2022/4/8 |
|
||||
| 应用解耦、公共方法库抽离和方法文档整理:图形图像应用代码解耦 | 2022/4/11 | 2022/4/22 |
|
||||
| 应用解耦、公共方法库抽离和方法文档整理:音视频应用代码解耦 | 2022/4/11 | 2022/4/22 |
|
||||
| 应用解耦、公共方法库抽离和方法文档整理:全局搜索代码解耦 | 2022/4/25 | 2022/4/26 |
|
||||
| 应用解耦、公共方法库抽离和方法文档整理:公共库建设 | 2022/4/27 | 2022/5/5 |
|
||||
| 应用解耦、公共方法库抽离和方法文档整理:封装的方法整理成一个表 | 2022/5/6 | 2022/5/10 |
|
||||
| 完成标签化管理和执行:用例标签分类评估 | 2022/5/11 | 2022/5/11 |
|
||||
| 完成标签化管理和执行:标签化执行驱动程序编写 | 2022/5/12 | 2022/5/23 |
|
||||
| 完成标签化管理和执行:需实现自动解析 csv 表格中的用例编号 | 2022/5/12 | 2022/5/23 |
|
||||
| 完成标签化管理和执行:编写爬虫脚本,爬取 pms 上用例的标签 | 2022/5/11 | 2022/5/13 |
|
||||
| 完成标签化管理和执行:编写定时自动维护本地 csv 文件脚本 | 2022/5/11 | 2022/5/20 |
|
||||
|
||||
中期任务计划
|
||||
|
||||
| 阶段目标 | 计划开始时间 | 计划结束时间 |
|
||||
| -------------------------------------- | :----------: | :----------: |
|
||||
| 目录结构调整 | 2022/5/20 | 2022/6/2 |
|
||||
| 完成全局和局部配置的具体方案和逻辑实现 | 2022/6/6 | 2022/6/17 |
|
||||
| 静态扫描问题清零 | 2022/6/20 | 2022/7/1 |
|
||||
| 完成新框架使用培训 | 2022/7/4 | 2022/7/11 |
|
||||
| 落地运行 | 2022/7/12 | 2022/7/20 |
|
||||
| 底层库用途说明文档编写 | 2022/7/21 | 2022/7/28 |
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
"@mermaid-js/mermaid-mindmap": "9.3.0",
|
||||
"medium-zoom": "^1.1.0",
|
||||
"mermaid": "^9.1.0",
|
||||
"vite-plugin-vitepress-auto-sidebar": "^1.6.2",
|
||||
"vitepress-plugin-back-to-top": "^1.0.1",
|
||||
"vitepress-plugin-comment-with-giscus": "^1.1.11",
|
||||
"vitepress-plugin-mermaid": "^2.0.10"
|
||||
|
|
Loading…
Reference in New Issue