应用于桌面应用压力测试的模拟输入工具调研报告

相关术语

术语 解释
InputDevice 支持 linux 管理不同类型、不同原理、不同的输入信息的输入设备软件体系
MEM MEM 是一个内存显示程序,显示所有内存驻留程序的占用情况
输入子系统 输入子系统核心层(Input Core)、驱动层和事件处理层(Event Handler)

问题

随着文件管理器(以下简称"文管")的使用者越来越多,厂商和社区反馈了越来越多反复操作后出现概率概率性 BUG,为了对 BUG 进行复现,就需要进行压力测试。

在我们日常压力测试过程中,文管测试组需要使用现有的设备进行问题复验和场景压测,设备和人力资源有限,因此,在复现和验证这些问题的过程中,存在着如下问题:

  1. 耗时巨大,追其原因,是因为在测试过程中,测试复现时反复操作的顺序不同、频率不够、操作慢都可能导致Bug无法复现,整个复现周期相对较长,人力和时间耗费较多。

  2. 无可靠性,在遇到概率性 BUG 进行回归的时候,我们无法确定需要测试多少次才能确保问题已修复,而且有可能人力压力测试无法达到复现条件,没有数据支撑测试结论。

因此,探索一种良好的压力测试方案,是迫在眉睫的,可以更准确的验证及回归问题,并且减少人力资源和提升测试效率。

现状

基于上述问题,当前市面上对于应用压测存在如下几个方案:

方案一:使用 Python 脚本进行场景压测

使用 Python 编写压测脚本,需要使用第三方库 PyAutoGUI 和 Dogtail,PyAutoGUI 是一个纯Python 的 GUI 自动化工具,可以用程序自动控制鼠标和键盘操作模拟用户操作行为,多平台支持(Windows,OS X,Linux)

在编写脚本之前,我们首先需要准备环境,即安装 PyAutoGUI 和 Dogtail 库。

开始编写一个简单压测场景脚本:循环一直新建文件夹。

import pyautogui
for i  in range(10):
    # 鼠标移动到x=100,y=100
    pyautogui.rightClick(100,100)
    pyautogui.click(x=155,y=117)

一个场景压测,往往不会这么简单,列举部分文管场景压力测试项如下:

压测场景 压测步骤 压测次数/时长
循环执行创建,删除文件 1.在桌面/文管空白处,右键菜单2.点击“新建文件夹“3.选择新建文件夹右键”删除“ 8H
重复打开 1000 张图片的文件夹 1.在桌面选择包含 1000 张图片的文件夹2.关闭文管 8H
重复搜索目录中的文件 1.打开文件管理器,进入test目录中2.点击搜索按钮3.在输入框输入 "test",进行搜索4.关闭文管 8H
重复同时执行 10 个复制文件任务 1.在 dock 栏目右键打开 2 个文管窗口2.在 A 窗口中,选择文件右键-复制3.在 B 窗口中,右键粘贴4.快速粘贴 10 次5.删除B窗口的文件 8H
重复向桌面复制文件 1.在dock栏目打开文管2.选择一个“音乐”目录 CTRL+C 复制文件3.在桌面粘贴文件4.关闭提示弹窗5.关闭文管 8H

可以很直观的看出,这些测试项的操作都较复杂。

Dogtail 是 Python 的三方库,它使用 Accessbility 技术与桌面应用程序通信。代码通过提前设置好的 Accessbility 标签进行操作,但 Accessbility 标签需要开发人员提前在应用程序中添加,在下图一中看到,1021 的文管中就不存在标签,部分版本基线中该方法并不适用。

1021-文管

通过 Dogtail 和 PyAutogui 来控制键盘、鼠标来进行应用的操作,在 Python 代码遍历寻找应用程序上可用的 Accessbility 节点时候,过程需要 1-2S 左右,之后再执行其他操作,这个过程耗时较长,失去了压力测试本身的意义。

纵观整个方案一,缺点是显而易见的,例如无法测试所有的应用版本、无法满足压力测试暴力操作的要求。

方案二:使用 shell 脚本进行场景压测

这个方案使用 Xdotool 工具模拟键盘、鼠标操作,Xwininfo 工具来获取窗体属性,此方法通过窗体固有属性,找到对应坐标,之后来进行键盘鼠标操作,整个流程如下图所示:

x操作

UOS 系统自带了Xdotool和Xwininfo工具,这个方法其实和方案二比较相似,方案二使用 Dogtail 定位元素位置、PyAutoGUI 来操作键盘、鼠标,使用wininfo 获取窗体属性计算坐标,通过 Xdotool 控制键鼠操作。

编写shell脚本,使用 Xodtool 工具进行简单操作,运行代码,查看图三,能看到与上述 Python 脚本同样的操作,shell脚本运行时间接近快了1s。

#!/bin/bash/
for in  in {0..10}
do
xdotool mousemove 100 100  click 3
xdotool mousemove 155 117  click 1
done

速度

使用 shell 脚本编写:重复向桌面复制文件,并且每次输出桌面和文管的窗口 ID(寻找窗口 ID 来获取坐标) 测试场景,多次运行后,在下图四能看到,同一个场景同一套代码同一个机器,运行结果,极其不稳定,容易报错找不到窗口。

向桌面复制文件.png

纵观整个方案二:运行环境简单、但是运行结果不稳定。

总结: 现目前这两种方案,各有优劣,都是现阶段较主流的方案,优缺点如表三所示:

方案 优点 缺点
1 可自动化、场景覆盖多 无法适用所有版本,环境复杂、键鼠操作比较慢达不到压测效果
2 可自动化、运行环境简单,能适用所有版本 不稳定

在目前的测试环境下,方案一由于 Python 在使用第三方库的时候,程序响应往往都需要 1-2s 时间,运行速度较慢,而且无法满足所有版本的压测需求。方案二使用 shell 脚本编写压测脚本,在运行 Xdotool 工具时候容易出错,Xdotool 工具对键鼠的操作已经算是比较快的工具了,但是在特殊 BUG 上,鼠标操作速度还是有所欠缺,如:异步线程出错,在测试过程中,经常性会遇到这样的小概率问题、同样的操作,有时候能复现问题有时候又不能复现,BUG 复现条件极其苛刻,需要更快的操作增加 复现概率,如下表列举部分的 BUG,在回归 BUG 就需要快速的键鼠操作。

BUG编号 描述 严重程度 优先级
29689 【文件管理器】【5.1.1.63-1】【sp2】桌面空白处右键,选择“壁纸与屏保”,再快速在右键,桌面黑屏 2 2
26972 【文件管理器】【5.1.1.39-1】在列表试图下,快速来回切换排列顺序,再点击上级栏目,文件管理器崩溃 2 2
26100 【文件管理器】【5.1.1.33-1】4K屏幕,2.75缩放,在桌面空白处快速多次点击右键,桌面崩溃 2 2
25591 【文件管理器】【5.1.1.31-1】鼠标左右键,快速连续一起按,桌面黑屏 3 3
24478 【sp1】【文件管理器】【5.1.0.8-1】拖动文件到其他目录下,快速敲击键盘“ctrl+z”出现不正确提示 4 4
21172 【文件管理器】【5.1.0.8-1】左侧栏目快速切换,右键闪退。 2 2

因此,我们需要对方案二进行优化,找到能解决键鼠操作速度不够快容易出错不稳定的方案。

技术方案

在用户使用桌面应用时候,都是通过键盘与鼠标输入设备与软件交互。

我们对现状中的方案二进行优化,也参考了 Xdotool 和 Xwininfo 工具的逻辑,设计一款新的应用程序,程序名称为 app-wininfo-m,计划解决上述键鼠操作速度不够快容易出错不稳定的问题。

对于键鼠操作速度不够快的问题,了解到 Linux 输入子系统,可以通过设备驱动程序写入键鼠事件驱动设备文件来对 USB 键盘输入和鼠标移动进行操作。

一个输入事件,如鼠标移动或者键盘按键事件,到达用户空间传给应用程序,如下图四流程

input

事件处理层为不同类型硬件提供了用户访问及处理接口。在 /dev/input 目录下显示的是已经注册在内核中的设备编程接口,用户通过 open 这些设备文件来打开不同的输入设备进行硬件操作,当打开设备 /dev/input/mice 时,会调用到事件处理层的 Mouse Handler 来处理输入事件,这也使得设备驱动层无需关心设备文件的操作,因为 Mouse Handler 已经有了对应事件处理的方法。

对于容易出错不稳定的问题,是 Xdotool 这个工具在经过长时间运行后获取窗口 ID 值出错,目前我们能接触到的 UOS 系统图形化界面都是基于 X11 协议,Xlib 是 X11 协议的 API 实现,另外用于代替 Xlib 的新型 API 为 XCB。下图五 Xlib 告诉我们每个窗口和像素图都有自己的坐标系,通过坐标系的位置,可以鼠标定位操作应用。

xlib窗口说明

整体设计

app-wininfo-m 自上而下的模块结构呈现如下图七:

app-wininfo-m.png

当按照这种模式设计,每个业务都比较统一有序和模块化,再扩展其他功能更加方便,不会影响到原有的功能。

  • keyboard-event:处理用户传入类型为 EV_KEY 的事件,将事件写入到 “/dev/input” 目录下的键盘设备文件中,上报事件和同步给 input 子系统中。
  • mouse-event:处理用户传入类型为 EV_REL 的事件,将事件写入到 “/dev/input” 目录下的鼠标设备文件中,上报事件和同步给 input 子系统中。
  • mouse-position:基于 xlib 实现获取鼠标的位置,给 app-action 层提供数据支持,监视鼠标的位置变化。
  • app-attribute:提供一个基于 xlib 获桌面应用窗口属性的集合,具体需要其他的接口,可根据实际需要自由扩展。

app-action 的设计理念是一个 wrapper,从功能需求上来看,app-action 提供以下功能:

  • 坐标:主要业务是窗口坐标和鼠标指针位置的获取,根据用户传入的 PID 查到应用的窗口ID,获取窗口属性。
  • 键鼠:主要业务是对设备驱动文件写入键鼠事件,来进行鼠标移动、点击和键盘输入。

app-wininfo-m程序运行流程如下:

sequenceDiagram
	应用 ->> 坐标 :传入应用的进程、窗口 id
	坐标 ->> 键鼠 : 通过进程、窗口 id 获取窗口属性
	键鼠 -->> 应用 :通过坐标信息,控制键盘鼠标操作应用

对应用使用者提供了友好的说明文档,程序已经帮助实现了需要的操作,使用者只需要根据说明文档传入对应的参数,执行需要的操作。以及添加简单崩溃的断言服务,通过对 coredump 日志监控,判断经过压力测试的应用是否出现崩溃现象,并输出结果提供给测试人员。

关键技术

坐标服务

在坐标获取业务中,获取不同应用的坐标位置,在之前我们获取坐标的方式是通过 Xdotool 获取到Window_ID,再通过 Window_ID 使用工具 Xwininfo 获取坐标。需要进行多个步骤操作后才能获取到坐标,在代码编写上步骤复杂,而且获取的结果不够稳定,因此找到了一个获取坐标的新方法,基本操作流程如下:

根据应用的进程寻找应用的坐标位置:

​ 传入的 PID,再调用 app-wininfo-m 程序,如果 PID 不存在,则不会返回窗口的坐标等属性,如果 PID 存在,就返回窗口的坐标等属性。

graph TD
 A(开始)-->B(读取传入的进程)
 B-->C(判断PID是否存在?)
 C-->|是|D[根据进程PID查找窗口属性]
 D-->E(反回窗口的坐标等属性)
 C-->|否|G[不会返回窗口的坐标等属性]
 E-->F(结束)
 G-->F(结束)

在编码过程中,获取窗口的坐标引入 Xlib 的头文件 #include <linux/xlib.h> 获取结构体:XGetWindowAttributes() 函数返回指定窗口到当前属性 XWindowAttributes 结构。

// 获取窗口的属性
extern Status XGetWindowAttributes(
 Display*		/* display */,
 Window		/* w */,
 XWindowAttributes*	/* window_attributes_return */
);
// ...

// 窗口的属性详细信息
typedef struct {
 int x, y;			/* location of window */
 int width, height;		/* width and height of window */
 int border_width;		/* border width of window */
 int depth;          	/* depth of window */
 Visual *visual;		/* the associated visual structure */
 Window root;        	/* root of screen containing window */
#if defined(__cplusplus) || defined(c_plusplus)
 int c_class;		/* C++ InputOutput, InputOnly*/
#else
 int class;			/* InputOutput, InputOnly*/
#endif
 int bit_gravity;		/* one of bit gravity values */
 int win_gravity;		/* one of the window gravity values */
 int backing_store;		/* NotUseful, WhenMapped, Always */
 unsigned long backing_planes;/* planes to be preserved if possible */
 unsigned long backing_pixel;/* value to be used when restoring planes */
 Bool save_under;		/* boolean, should bits under be saved? */
 Colormap colormap;		/* color map to be associated with window */
 Bool map_installed;		/* boolean, is color map currently installed*/
 int map_state;		/* IsUnmapped, IsUnviewable, IsViewable */
 long all_event_masks;	/* set of events all people have interest in*/
 long your_event_mask;	/* my event mask */
 long do_not_propagate_mask; /* set of events that should not propagate */
 Bool override_redirect;	/* boolean value for override-redirect */
 Screen *screen;		/* back pointer to correct screen */
} XWindowAttributes;
// ...

了解到 Xlib 函数,就可以围绕 XGetWindowAttributes() 函数来获取窗口属性,经过以下流程,就完成窗口的属性获取了。

  • 传入进程判断,获取窗口,伪代码如下

    // Get the PID property atom 获取PID属性atom
    _atomPID = XInternAtom(display, "_NET_WM_PID", True);
            if(_atomPID == None)
            {
                cout << "No such atom" << endl;
                return;
            }
    // ...
    
    // Get the PID for the current Window.获取当前所有窗口的PID。
    void searchmywindow(window w){
            Atom           type;
            int            format;
            unsigned long  nItems;
            unsigned long  bytesAfter;
            unsigned char *propPID = 0;
            if(Success == XGetWindowProperty(_display, w, _atomPID, 0, 1, False, XA_CARDINAL,
                                             &type, &format, &nItems, &bytesAfter, &propPID))
            {
                if(propPID != 0)
                {
                // If the PID matches, add this window to the result set.
                //如果PID匹配,则将此窗口添加到result中。
                    if(_pid == *((unsigned long *)propPID))
                        _result.push_back(w);
    
                    XFree(propPID);
                }
            }
        }
  • 对鼠标进行移动点击,程序要知道鼠标的位置,获取鼠标位置的代码如下:

    //获取鼠标坐标 
     Display *display = XOpenDisplay(NULL);
     Window root = XDefaultRootWindow(display);
     Window root_return;
     Window child_return;
     int root_x_return;
     int root_y_return;
     uint32_t mask_return;
    
      if (::XQueryPointer(display, root, &root_return, &child_return, &root_x_return, &root_y_return, &win_x_return, &win_y_return, &mask_return) == True)
         { //输出鼠标的属性
             qWarning()<<"Window_ID:"<<child_return<<"x:"<<win_x_return<<"y:"<<"win_y_return";
         }
     XCloseDisplay(display);
  • 窗口属性的获取,伪代码如下:

Window get_toplevel_parent(Display * display, Window window)//获取父窗口ID
{
     Window parent;
     Window root;
     Window * children;
     unsigned int num_children;

     while (1) {
         if (0 == XQueryTree(display, window, &root,&parent, &children, &num_children)) {
             fprintf(stderr, "XQueryTree error\n");
             abort(); 
         }
         if (children) { 
             XFree(children);
         }
         if (window == root || parent == root) {
             return window;
         }
         else {
             window = parent;
         }
     }
}

void getwindowattribute(Window w){
	//获取窗口的属性
    Display *display;
    Window  focus,toplevel_parent_of_focus;
    XWindowAttributes attr;
    int revert;
    display = XOpenDisplay(NULL);
    XGetInputFocus(display, &w, &revert);
    toplevel_parent_of_focus = get_toplevel_parent(display, w);
    XGetWindowAttributes(display, toplevel_parent_of_focus, &attr);
    qWarning()<<"width:"<<attr.width<<"height:"<<attr.height<<"x:"<<attr.x<<"y:"<<attr.y;
	XCloseDisplay(display);
}

操作服务

在 Linux 系统中,万物皆文件,我们的键盘和鼠标都是设备文件,经过调研,通过对设备文件写入,就可以对键盘和鼠标进行操作,这个操作是硬件操作的最快的方法,其基本流程如下:

graph TD
A(开始)-->B(查找设备文件)
B-->C(打开设备文件)
C-->D(写入设备文件)
D-->E(同步写入事件)
E-->F(事件执行)
F-->G(结束)

在编码过程中,对设备文件写入生效,需要引入子系统头文件 #include <linux/input.h> 获取到结构体:

struct input_event {
#if (__BITS_PER_LONG != 32 || !defined(__USE_TIME_BITS64)) && !defined(__KERNEL__)
	struct timeval time;
#define input_event_sec time.tv_sec
#define input_event_usec time.tv_usec
#else
	__kernel_ulong_t __sec;
#if defined(__sparc__) && defined(__arch64__)
	unsigned int __usec;
	unsigned int __pad;
#else
	__kernel_ulong_t __usec;
#endif
#define input_event_sec  __sec
#define input_event_usec __usec
#endif
	__u16 type;
	__u16 code;
	__s32 value;
};

通过 write() 函数对 /dev/input 目录下设备文件写入,伪代码如下:

//fd:打开的文件设备文件
void Move_mouse(int fd, int rel_x, int rel_y)
{
    struct input_event event;
    //x轴坐标的相对位移
    event.type = EV_REL;
    event.value = rel_x;
    event.code = REL_X;
    write(fd, &event, sizeof(event));
    //y轴坐标的相对位移
    event.type = EV_REL;
    event.value = rel_y;
    event.code = REL_Y;
    write(fd, &event, sizeof(event));
    //同步
    event.type = EV_SYN;
    event.value = 0;
    event.code = SYN_REPORT;
    write(fd, &event, sizeof(event));

}

最终,通过公式移动到鼠标想要的位置,如移动到应用左上角的位置( X1,Y1 ),鼠标位置( X2,Y2 ),鼠标相对位置 rel_x,rel_y 相对移动位置。

rel_x = X2 - X1;
rel_y = Y2 - Y1;

对于键盘的操作,与上面操作类似,就需要修改参数,代码如下:

//fd:打开的文件设备文件 kval:键盘按键
void simulate_key(int fd, int kval)
{
         struct input_event event;
         gettimeofday(&event.time, 0);
         event.type = EV_KEY;
         event.value = 1;
         event.code = kval;
         write(fd, &event, sizeof(event));
       //同步,报告给系统

         event.type = EV_SYN;
         event.value = 0;
         event.code = SYN_REPORT;
         write(fd, &event, sizeof(event));
         memset(&event, 0, sizeof(event));
         gettimeofday(&event.time, 0);
         event.type = EV_KEY;//松开kval键
         event.value = 0;
         event.code = kval;
         write(fd, &event, sizeof(event));

       //同步,报告给系统

       	event.type = EV_SYN;
      	event.value = 0;
       	event.code = SYN_REPORT;
       	write(fd, &event, sizeof(event));

}

实验验证

根据技术方案调研的结果,需要对整体设计的方案和一些技术细节进行验证,为此设计以下实验:

app-wininfo-m 可行性和兼容性验证

app-wininfo-m 的目的是对桌面应用坐标定位,通过输入设备的设备文件操作鼠标键盘,本次实验将按照整体设计中的方式,生成一个可以运行的程序执行文件,以通过进程获取到应用坐标,并且鼠标点击关闭窗口为例进行演示。

app-wininfo-m 应用程序参数文件说明:

参数 说明
-m 鼠标移动位置,参数一:坐标X轴,参数二:坐标Y轴,参数三:1点击鼠标左键,2鼠标中键,3鼠标右键
-p 通过传入的进程号查找应用窗口的属性,查看所有进程下的窗口,并输出窗口属性
-k 键盘输入,如:-k KEY_A,键盘输入A

【实验环境】

  • 系统:UOS 20 1030
  • 架构:X86 、ARM64、MIPS64EL

【实验步骤】

  • 安装环境
  1. app-wininfo-m 代码编写完毕,在 Qt Creator 中构建制执行文件为 demo。
  • 编写测试脚本

    使用 shell 方式来运行 demo 程序,编写的代码如下

#!/bin/bash
file_pid=pidof "dde-file-manager" 	#获取文管进程pid
window_PP=`./demo -p $file_pid` 	#获取输出应用窗口属性
window_W=`echo ${window_PP#*"width:"}|awk -F 'X' '{print $1}'` 	#截取窗口属性宽
window_H=`echo ${window_PP#*"height:"}|awk -F ')' '{print $1}'`	#截取窗口属性高
window_X=`echo ${window_PP#*"x:"}|awk -F 'y' '{print $1}'`	#截取窗口属性x轴坐标
window_Y=`echo ${window_PP#*"y:"}`	#截取窗口属性Y轴坐标
./demo -m $[ window_X + window_W -5 ] $[ window_Y + 5 ] 1	#鼠标移动到关闭按钮位置,并点击关闭按钮
  • 运行结果

查看下图运行结果,看到 通过进程获取到应用坐标,并且鼠标点击关闭窗口 演示操作的例子完美运行成功,app-wininfo-m 程序的整体方案是正确的。

newdemo

以上述例子运行,进行不同系统架构测试,测试结果如下:

系统架构 测试结果
MIPS64EL 正常运行
X86 正常运行
ARM64 正常运行

app-wininfo-m 稳定性验证

压力测试对稳定性要求很高,设计以下测试项,对 app-wininfo-m 程序的关键点进行稳定性验证。

测试项 测试说明 时长 结果
获取10个应用窗口属性 打开多个10个应用,对10个窗口属性获取,demo程序稳定 24H 通过
同一个应用打开10个窗口,获取所有窗口属性 选择文管,打开10个窗口,获取10个窗口属性,demo程序稳定 24H 通过
鼠标移动 使用demo控制在桌面随意移动,demo程序稳定 24H 通过
长时间获取鼠标位置 桌面随机一定,一直获取鼠标位置,demo程序稳定 24H 通过
点击鼠标左键 使用demo程序,鼠标随机一直标右键,demo程序稳定 24H 通过
点击鼠标右键 使用demo程序,鼠标随机一直标左键,demo程序稳定 24H 通过
键盘输入 使用demo程序,控制键盘一直输入,demo程序稳定 24H 通过

经过上面的测试项,demo 程序满足了稳定性

app-wininfo-m 操作性能验证

demo 程序在设计当初考虑了极限的压力操作场景,准备了一下实验,进行验证结果,代码如下:

for i  in {1..10}
do
    ./demo -m 100 100 3
    ./demo -m 155 117 1
done

使用 time 命令运行 shell 脚本,下图就是 demo 程序运行速度。

newdemo

由上图九可知 demo 程序运行速达到 1.844s,对比方案二的 2.315s 速度,明显超过 Xdotool 工具的运行速度。

小结

本文针对压力测试工具提出技术方案,整体设计中通过对设备文件写入操作和使用 xlib 库开发了定位和操作服务,解决了方案二获取坐标不稳定和操作速度不够快的问题,基本上能满足现有压测场景的使用要求,同时提供简单崩溃断言的服务。

app-wininfo-m 工具作为我们自己开发的工具,未来可以根据需求进行功能扩展,如在“进行搜索的压力测试”,目前只能验证搜索出现崩溃这样的问题,无法验证搜索结果是否正确,现有市面上仍然没有这样功能的工具,我们可以在 app-wininfo-m 工具中定制开发。

一个新开发工具,往往要经过无数次的版本迭代和优化,接下来的方向,就是对工具进行以下功能扩展和优化。

扩展功能 优化
模拟拖拽操作 优化鼠标移动计算方式
元素定位 优化 app-wininfo-m 代码
图像定位 优化数据输出方式
增加断言方式(如:搜索) 优化键盘输入传值方式
模拟触摸操作

参考资料