有幸能参与到实验室学长的一个项目当中,简单记录一下整个项目和一些收获

概述

UniRLTestUniversal Reinforcement Learning Test,是基于图像识别、深度强化学习等实现的跨平台软件测试工具

前端

前端部分主要负责与后端和设备进行通讯,接收后端传来的操作指令并在设备上执行,获取设备上应用的屏幕截图并传给后端进行分析

文件大致结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
../urt/urt-frontend
│ client.py
│ launcher.py
│ logger.py
│ plt.py
│ urt_android.py

├─android
│ │ action_executor.py
│ │ plt_android.py
│ │ screen_capture.py
│ │ tool_base.py
│ │ utils.py

├─pc
│ │ action_executor.py
│ │ plt_pc.py
│ │ screen_capture.py
│ │ tool_base.py

├─web
│ │ action_executor.py
│ │ plt_web.py
│ │ screen_capture.py
│ │ tool_base.py

├─configs
│ │ configure.py
│ │ ssh_config.json
│ │
│ ├─android
│ │
│ ├─web

├─coverages

├─logs

前端的运行需要一个配置文件作为额外参数(e.g. configs/android/anymemo.json),配置文件结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"platform": "", // e.g."android"/"web"/"pc"
"serial_no": "", // 测试使用的设备
"screen_size": [0, 0], // 设备的屏幕尺寸(屏幕截图的分辨率)
"server_proj_dir": "", // 服务器上部署的后端项目的路径,e.g."/root/urt"
"server_screenshot_dir": "", // 后端保存屏幕截图的文件夹路径,e.g."screenshots"
"local_screenshot_path": "", // 本地保存屏幕截图的文件路径,e.g."anymemo.png"
"window_keys": "",
"total_time": "", // 测试的总时长,单位为秒
"banner_height": "",
"remote_port": "", // 在服务器上使用的端口号,e.g."5000"
"app": { // 被测试应用的基本信息
"name": "", //e.g."budgetwatch"
"pkg": "", //e.g."protect.budgetwatch"
"main_activity": "" //e.g."protect.budgetwatch.test.InstrumentedActivity"
}
}

后端

此部分内容根据论文初稿整理

image-20220805174903049

后端的功能主要分为两大模块:

图像处理
  • 提取组件和页面布局特征

    • 使用 Canny Edge Detection 技术从屏幕截图中提取组件
    • 根据提取到的组件构建一棵组件树来表示页面布局特征
  • 组件 Embedding

    • 组件图像经过卷积神经网络处理得到一个 4096 维向量,表示组件的图像信息
    • 将屏幕截图的组件位置变为全白而其余地方变为全黑而得到的图片,经过卷积神经网络处理得到一个 4096 维向量,表示组件的位置信息(不使用坐标是为了相对图像信息有足够的权重)
    • 组件图像经过卷积神经网络处理得到一个 14 维独热向量,表示组件的类型
    • 将上述结果拼接而成一个 8206 维的向量,即为 embedding 的结果
    • 以上所说的卷积神经网络,是一个预训练好的用于判断组件类型的 VGG-16 模型,4096 维的向量是其倒数第二层的输出,14 维的向量是其最后一层的输出
  • 页面布局 Embedding

    • 将树状结构的页面布局转化为字符串
    • 将该字符串使用预训练好的 LSTM 模型处理,输出 512 维的向量,即为 embedding 的结果
  • 页面(app 状态)表示

    • 计算所有组件的 embedding 向量的平均值,即用一个 8206 维的向量表示所有组件
    • 将组件和页面布局的 embedding 结果连接成 8718 维的向量,表示整个页面(app 状态)
强化学习

这部分在此没有详细展开说明

  • 动作生成
    • 用 17 维的独热向量表示动作类型
    • 用 8026 维的向量表示实施动作的组件(没有则做 zero-padding)
    • 用一维向量表示 window size 参数(没有则做 zero-padding)
    • 表示一个动作的向量由上述向量连接而成
  • Q-Network 模型训练
    • Q-Network 使用含有四个隐藏层的全连接神经网络
    • replay-memory 中保存了每次状态转换以及其对应的奖励值,也是训练模型的数据来源
    • 随着测试的进行,模型需要被不断训练和更新
  • 动作决定
    • 通过 Q-Network 的输出可以得到当前页面下,所有可能动作被执行的优先级
    • 使用 Boltzmann Strategy 决定下一个被执行的动作(避免被困在局部最优解)

测试

Web 应用的测试和 PC 端应用的测试本人没有参与,所以以下内容只涉及安卓应用测试

安卓应用插桩测试方法

环境准备:
  • Java:jdk8(较老的 gradle 无法用 jdk11 运行)和 jdk11(较新的 gradle 需要至少 jdk11 运行)
  • Gradle:不用额外安装,项目中自带的 gradlew 脚本能在当前自动安装对应版本的 gradle
  • Android SDK:使用 Android Studio 统一管理,环境变量中正确设置ANDROID_HOME的值
构建原项目:
  • 在项目目录下执行如下命令进行构建:

    1
    ./gradlew --no-daemon --stacktrace assembleDebug
  • –no-daemon 表示 Gradle 守护进程会在命令执行完成后自动退出,不会常驻后台(因为不同应用依赖不同版本的gradle,守护进程没法很好的被复用,无需常驻后台)

  • –stacktrace 表示若报错会打印堆栈信息

  • assembleDebug 表示构建 debug 包

  • 这一步的目的是保证应用本身可正常构建(其构建产物不会被后续使用)

添加插桩代码:
  • 使用 python 脚本一键插桩:
    1
    python instrument.py {x}
  • 注:脚本完成了如下操作

  1. 备份 app 目录下的 build.gradle,并添加一个 apply 语句以应用 jacoco 插件

  2. app 目录下添加 jacoco-multi.gradlejacoco-multi-kt.gradle,包含了 jacoco 配置,其中后者是应对 kotlin 项目的配置(该配置文件是一个骨架,后面需要进行修改)

  3. 备份并修改 AndroidManifest.xml 配置文件,注册 InstrumentedActiviyJacocoInstrumentation

  4. 向源码中加入三个文件:FinishListenerInstrumentedActiviy 和J acocoInstrumentation

(上述 app 目录为源码所在目录,一般为 app,也有可能为其它名称,该脚本若未检测到 app 目录,则会提示输入该目录的名称,需要用户自行判别目录并输入,有时源码所在目录就是项目目录,则输入一个点.表示项目目录即可;AndroidManifest.xml 文件默认路径为 app/src/main 下,若检测不到,会要求用户输入其完整路径;源码 java 目录默认为 app/src/main/java,若检测不到,会要求用户输入其完整路径)

  • 修改 build 和 jacoco 配置:
  1. 检查源码语言是纯 java 还是使用了 kotlin,若使用了 kotlin,检查是否在 app/build.gradleapply 了带 “-kt”jacoco 配置,若没有,则需进行替换
  2. 查看 build/intermediates 下的 javacclasses 目录,若没有 javac 目录而有 classes 目录,则将 jacoco配置中的 javac 全部替换成 classes
  3. (假定上一步是 javac 目录)在 javac 目录下查看 app 构建类型,一般是 xxxDebug,选一个作为测试类型,以后只用该类型的 apk(在 build/outputs/apk 目录下有对应的 apk 文件),修改 jacoco 配置中的 variantName 变量为测试类型的名称
构建插桩后的应用:
  • 使用相同的命令构建:

    1
    ./gradlew --no-daemon --stacktrace assembleDebug

这一步若出现报错,可能有以下几种情况:

  • java 版本不对(仔细查看报错信息,调整 jdk 版本)

  • jacoco 配置出错,无法设置 outputLocation 属性。这种情况是由于 gradle 版本较低导致的,将jacoco 配置中的 outputLocation 全部替换成 destination,然后将以下三行改掉:

    1
    2
    3
    4
    5
    6
    // classDirectories.from = files(...)
    // sourceDirectories.from = files(...)
    // executionData.from = ecFile
    classDirectories = files(...)
    sourceDirectories = files(...)
    executionData = files(ecFile) // 注意这里要套一个files
安装并运行插桩后的应用:
  • 检查本机是否已经连上用于测试的安卓设备(真机或虚拟机):

    1
    adb devices
  • 项目目录下,在用于测试的设备上安装插桩后的应用:

    1
    adb [-s {设备名称}] install ./app/build/outputs/xxxDebug.apk
  • 通过源码查看应用的{包名},并在测试设备上运行安装的应用:

    1
    adb [-s {设备名称}] shell am instrument {包名}/.test.JacocoInstrumentation

此时手机会打开插桩后的应用,随便交互几步后,关闭该应用并在后台进程中杀死该应用(若不杀死则会不断生成新的覆盖率数据)

  • 若插桩后的应用存在打开时闪退的问题,可能是由应用的主题导致的:
    • 尝试修改 app/src/main/AndroidManifest.xmlapplicationandroid:theme 属性,将原来的例如 AnyMemo.Theme.Dark 修改为 AnyMemo.Theme.Dark.NoActionBar**,再重新构建和安装
    • 插桩后出现闪退问题的一般定位方法如下:
      1. 打开一个命令行,使用adb logcat *:D命令跟踪日志
      2. 到另一个命令行中启动 instrumentation
      3. 切换到第一步的命令行,ctrl+c 停止打印,自底向上查找相关的报错信息
导出覆盖率数据并生成报告:
  • 覆盖率数据文件被保存在 /data/data/<包名>/files 目录下,通过如下命令导出这些文件:

    1
    2
    3
    4
    5
    6
    7
    8
    adb shell
    # 初次导出需要创建中间目录
    > mkdir /sdcard/coverage
    # 下面run-as后为应用包名
    > run-as org.liberty.android.fantastischmemo
    > mv files/*.ec /sdcard/coverage
    # 目标路径为空则默认为当前目录
    adb pull /sdcard/coverage [目标路径]
  • 下面将 coverage 目录下所有 .ec 文件移动到该应用的项目目录下的 app/build/jacoco 目录下(没有则创建),在项目目录下使用如下命令生成报告(报告生成在 app/build/reports 目录下):

    1
    ./gradlew --no-daemon --stacktrace multiJacocoTestReport

安卓测试实验流程

  • 在安卓设备上安装插桩后的应用
  • 在前端项目目录下的 configs/android/ 目录下添加被测应用对应的配置信息(xxx.json)
  • 在服务器上启动后端(执行命令:sh run.sh
  • 在本地启动前端(执行命令:python urt_android xxx.json
  • 测试设备开始运行被测应用,UniRLTest 测试进行中 …
  • 测试结束,覆盖率数据生成在前端项目目录下的 coverages/xxx/ 目录下
  • 手动生成测试报告(具体步骤已在安卓应用插桩测试中讲过)