tidevice 助你在非Mac环境执行iOS自动化
2021-05-16 08:49 zouhui 阅读(2305) 评论(1) 编辑 收藏 举报前言
一直以来,iOS自动化的实现&执行都依赖 Mac 系统,其主要原因是因为需要通过 xcodebuild 编译&安装 WDA (WebDriverAgent) 到 iOS 设备中,通过WDA实现对被测应用进行操作。
导致想要做iOS自动化 就必须拥有 Mac 设备的现象
常用电脑非 Mac 的同学 想要做 iOS 自动化的时候, 就需要再申请一台Mac设备 ,可能会出现资源利用不充分的情况
云测试平台要搭建 IOS 自动化 服务环境时,也只能批量申请 Mac 设备,经费在燃烧
一个月前,阿里团队开源了一个内部使用的 iOS自动化工具 :tidevice (https://github.com/alibaba/taobao-iphone-device
) ,让我们可以更方便、简单的脱离Mac的限制。
本文会和大家分享下 tidevice 调研记录 以及 如何集成到现有方案中
tidevice 能做什么
设备信息获取
应用安装、卸载、启动、停止、查看应用信息、已安装应用列表
启动 WebDriverAgent (不依赖 xcodebuild , 跨平台)
运行 XcTest UITest
性能数据采集
设备截图、设备日志 ...
tidevice 核心原理
usbmux通信协议:
实现 Mac/Windows/Linux 与 iOS设备服务间的通信
Mac
usbmuxd 是苹果的一个服务,这个服务主要用于在USB协议上实现多路TCP连接,将USB通信抽象为TCP通信。苹果的iTunes、Xcode,都直接或间接地用到了这个服务。
Linux / Windows
本身是没有 usbmux的,不过都有开源项目的实现,可以直接使用/参考
Windows 另外依赖 AppleApplicationSupport和AppleMobileDeviceSupport 两个服务,安装Itunes 环境即可安装对应服务
usbmux 本身是socket 套接字,通过截获、破解等手段,结合开源界的成果,用python 进行模拟,从而实现了当前工具已有的所有功能
tidevice 环境安装
必须已有 Python 3.6+ 环境
tidevice 安装
pip3 install -U "tidevice[openssl]” (推荐)
pip3 install -U tidevice (缺少设备配对功能)
usbmuxd
Mac 自带:/var/run/usbmux
Linux/Windows: 参考官方建议的
https://github.com/alibaba/taobao-iphone-device/issues/7
tidevice 现支持cmd 及 Python 模拟实现
因为转转客户端UI自动化框架 是Python工程,所以当发现tidevice 也是Python工程后,对tidevice功能的具体实现很感兴趣,希望可以直接调用其内部实现进行集成,而不是封装 cmd ,在调研的同时,根据转转侧需要,进行了部分功能的重新封装。
tidevice 支持的所有cmd 都在
tidevice.__main__
中定义实现以下 cmd 的Python实现,会直接参考/复用 main 中实现 ,大家也可以根据自己的封装习惯,重新封装
设备管理
查看已连接设备列表
cmd:
tidevice list
Python:
from tidevice import Usbmux
print(Usbmux().device_list())
监听设备连接状态
当有设备连接、断开连接时,都会实时返回内容
cmd:
tidevice watch
Python:
from tidevice import Usbmux
for info in Usbmux().watch_device():
print(info)
Usbmux().watch_device() 返回 generator , 利用for 循环调用 ,但这里会阻塞后续逻辑,所以最好用Process 单独监听处理
等待任意设备连接,连接一台就结束等待
cmd:
tidevice wait-for-device
Python:
from tidevice import Usbmux
for info in Usbmux().watch_device():
if info["MessageType"] == "Attached":
break
等待指定设备连接
cmd:
tidevice -u $UDID wait-for-device
Python:
from tidevice import Usbmux
for info in Usbmux().watch_device():
if info["MessageType"] != "Attached":
continue
udid = info["Properties"]["SerialNumber"]
if udid == "$UDID":
break
打印设备日志
cmd:
tidevice -u $UDID syslog
Python:
from tidevice import Device
d = Device("udid")
d.start_service("com.apple.syslog_relay")
while True:
print(s.recv().decode("utf-8"))
应用管理
安装应用
cmd:
tidevice --udid $UDID install example.ipa
Python:
from tidevice import Device
Device("udid").app_install(ipa_url_or_path)
卸载应用
cmd:
tidevice --udid $UDID uninstall com.example.demo
Python:
from tidevice import Device
Device("udid").app_uninstall("com.example.demo")
启动应用
cmd:
tidevice --udid $UDID launch com.example.demo
Python:
from tidevice import Device
# 如果是已启动&最上层应用,则不变;如果已启动&退到后台,拉起,如果未启动,启动
pid = Device("udid").app_start("com.example.demo")
# 强制重新启动
pid = Device("udid").app_start("com.example.demo", kill_running=True)
停止应用
cmd:
tidevice --udid &UDID kill com.example.demo
Python:
from tidevice import Device
# 如果传 app_start生成的 pid ,可以直接杀掉,如果传 com.example.demo 也可以杀掉
Device("udid").app_stop(pid_or_name)
查看已安装应用
cmd:
tidevice —udid &UDID applist
Python:
from tidevice import Device
Instruments = Device("udid").connect_instruments()
# 设备上全部App信息列表 包含 系统应用和插件,通过 Type 可以区分App
apps = instruments.app_list()
# 只筛选用户安装的App列表
user_app_list = [app for app in apps if app["Type"] == "User"]
查看应用信息
名称、版本、权限、依赖、插件 等包信息
cmd:
tidevice appinfo com.example.demo
Python:
from tidevice import Device
Device("udid").installation.lookup("com.example.demo")
通过ipa下载链接分析ipa信息
cmd:
tidevice parse ipa_url
Python:
import httpio
from tidevice._ipautil import IPAReader
fp = httpio.open(ork, block_size=1)
ir = IPAReader(fp)
ir.get_infoplist()
通过ipa文件分析ipa信息
cmd:
tidevice parse ipa_path
Python:
from tidevice._ipautil import IPAReader
fp = open(ipa_path, "rb")
ir = IPAReader(fp)
ir.get_infoplist()
执行自动化
执行XCTest (需要先确保手机上已经安装有WebDriverAgent,并且知道bundleId)
cmd:
tidevice xctest -B com.facebook.wda.WebDriverAgent.Runner
Python:
from tidevice import Device
Device("udid").xctest("com.facebook.wda.WebDriverAgent.Runner")
无论是在代码中通过cmd 执行,还是通过python库执行xctest,都会阻塞后续操作,所以建议使用 Process
执行XCTest 修改监听端口为8200,并显示调试日志
cmd:
idevice XCTestCase -B com,facebook.wda.WebDriverAgent.Runner -e USB_PORT:8200 —debug
Python:
from tidevice import Device
Import logging
logger = logging.getLogger("tidevice.xctest")
Device("udid").xctest("com.facebook.wda.WebDriverAgent.Runner", log=logger, evn={"USB_PORT": 8200})
Relay 转发请求到手机,类似于iproxy
cmd:
tidevice relay 8100 8100
Python:
from tidevice import Device
from tidevice._relay import relay
d = Device("udid")
relay(d, 8100, 8100)
同执行xctest ,无论是在代码中通过cmd执行,还是通过python库执行relay, 都会阻塞后续操作,所以建议使用Process
Relay 转发请求并把传输的内容用hexdump的方法打印出来
cmd:
tidevice relay -x 8100 8100
Python:
from tidevice import Device
from tidevice._relay import relay
d = Device("udid")
relay(d, 8100, 8100, debug=True)
运行XCTest 并在PC上监听8200端口转发到手机8100服务
cmd:
tidevice wdaproxy -B com.facebook.wda.WebDriverAgent.Runner —port 8200
Python:
方式1:
tidevice.__main__
中cmd_wdaproxy方式
from tidevice._wdaproxy import WDAService
from tidevice import Device
d = Device("udid")
serv = WDAService(d, "com.facebook.wda.WebDriverAgent.Runner")
cmd = [sys,executable, "-m" , "tidevice" , "-u" d.udid, "relay", "8200", "8100"]
p = subprocess.Popen(cmd, stdout=sys.stfout, stderr=sys.stderr)
serv.start()
while serv._service.running:
time.sleep(1)
p and p.terminate()
serv.stop()
方式2:通过 xctest relay 结合
from tidevice import Device
from tidevice._relay import relay
from multiprocessing import Process
d = Device("udid")
p1 = Process(target=device.xctest, args=("com.facebook.wda.WebDriverAgent.Runner", None, None, {"USB_PORT":8200}))
p2 = Process(target=relay, args=(d, 8200, 8100, False))
p1.start()
p2.start()
..
p1.teminate()
p2.terminate()
运行后,可以通过 http://127.0.0.1:8200/status
判断
运行XCTest UITest
cmd:
tidevice xctest —bundle-id philhuang.testXCTestUITests.xcrunner —target-bundle-id philhuang.testXCTest
Python:
from tidevice import Device
d = Device("udid")
d.xctest("philhuang.testXCTestUITests.xcrunner", target_bundle_id="philhuang.testXCTest")
获取设备信息
基础信息
型号、设备名、系统版本、手机号、序列号、时区、蓝牙地址、Wifi地址等
cmd:
tidevice info
Python:
from tidevice import Device
# 内容会比cmd 全很多, 但是需要理解每个字段的含义
Device("udid").device_info()
查看设备电源信息
cmd:
tidevice info --domain com.apple.mobile.battery --json
Python:
from tidevice import Device
import json
domain_info = Device("udid").device_info("com.apple.mobile.battery")
print(json.dumps(domain_info))
主要通过 --domain 参数 查看指定domain的信息
装有libimobiledevice 的电脑,可以执行 ideviceinfo -h 查看都有哪些domain
具体的domain信息 ,可以根据需求进行封装
系统信息
cmd:
tidevice sysinfo
Python:
from tidevice import Device
Device("udid").instruments.system_info()
电池信息
cmd:
tidevice battery
Python:
from tidevice import Device
Device("udid").get_io_power()
这里比 Device("udid").device_info("com.apple.mobile.battery")
获取的信息更全面,后者只是基础的当前电量和当前充电状态信息
fps 数据采集
cmd:
tidevice dumpsfps
Python:
from tidevice import Device
d = Device("udid")
for data in d.instruments.iter_opengl_data():
if is instance(data, str):
continue
print(data["CoreAnimationFranesPerSecond"])
在 Device().instruments
中已经封装好了部分性能数据采集方法,可以二次封装使用
设备操作
截屏保存在当前目录
cmd:
tidevice screenshot
Python:
from tidevice import Device
import time
filename = "screenshot_" + str(time.time()) + ".jpg"
Device("udid").screenshot().conver("RGB").save(filename)
截屏保存在指定目录
cmd:
tidevice screenshot /xxxx/xxx/screenshot.jpg
Python:
from tidevice import Device
file_path = "/xxxx/xxx/screenshot.jpg"
Device("udid").screenshot().conver("RGB").save(file_path)
关机
cmd:
tidevice shutdown
Python:
from tidevice import Device
Device("udid").shutdown()
重启设备
cmd:
tidevice reboot
Python:
from tidevice import Device
Device("udid").reboot()
设备文件管理
cmd:
tidevice -u $UDID fsync
Python:
from tidevice import Device
Device("udid").sync
应用文件管理
cmd:
tidevice -u $UDID fsync -B com.example.app
Python:
from tidevice import Device
Device("udid").app_sync("com.example.app")
配对设备
如果已信任的设备不建议使用, 会重新信任,如果是远程设备,则不能自动操作信任窗口
cmd:
tidevice -u $UDID pair
Python:
from tidevice import Device
Device("udid").pair()
tidevice 集成
转转方案当前底层使用的是Appium框架,同类方案可以参考
集成思路
首先要提前将 WDA 安装到 iOS设备中 并在设置中信任开发者,确保WDA可以正常启动
将 WDA 的bundle_id (Bundle Identifier) 作为 一个配置/常量,保存在 框架工程中, tidevice wdaproxy 需要
在 通过 webdriver 启动driver 前,通过 tidevice 的 cmd 或者 自己封装实现的 wdaproxy 启动 WDA & 端口转发 ,这时候 local_port(本地端口)需要 记录
在 通过 webdriver 启动driver 前,修改 driver 启动需要的配置
添加 webDriverAgentUrl : "http://127.0.0.1:" + str(local_port)
如果有设置 useNewWDA 为 True 的话,需要 改为 False
wdaLocalPort 也可以不设置了
使用新的配置 启动 driver ,就可以执行测试逻辑了
所有任务执行完成后,最好主动检查&回收 tidevice 进程
建议
如果Mac环境 建议先保留原有的流程
在集成后实际测试使用过程中发现,tidevice wdaproxy 方式,wda不是很稳定,偶尔会出现通信异常,重启wda的现象,具体原因还没有分析出来,如果后续多次验证其稳定后,可以再根据实际需求决定是否全都使用tidevice wdaproxy
如果Mac环境保留原有的流程 , 别忘记增加 是Linux/Windows 还是 Mac 环境的判断
总结
tidevice工具,使iOS自动化摆脱了Mac的限制,给iOS自动化方案建设更多的可能
可以在Windows 环境 通过 tidevice wdaproxy 启动WDA, 使用Appium Client 分析页面 & 编写Case
可以在Linux 服务器上 通过 tidevice 搭建 iOS设备自动化 远程执行环境
可以在Mac/Windows/Linux 通过 tidevice 采集性能数据,集成到自动化测试流程中
在看了项目源码后,发现 tidevice 提供的 cmd 只使用到部分底层代码实现,可以根据自己的需求,立刻封装出更多功能,也可以参考源码通过 usbmux 实现更多功能
最后,感谢阿里团队开源tidevice工具,希望本篇可以帮助到大家
还在等什么,快去试用、集成、看源码吖~
如果喜欢这篇分享的话,辛苦关注、转发、在看、点赞、评论、赞赏吖~
end