自动化工具 让所有人都能用 python 操作设备

williamfzc · 2018年11月01日 · 最后由 蓝蓝 回复于 2018年12月26日 · 3576 次阅读

项目主页:pyatool

这个项目已经想做很久啦。在 android 端测试开发的过程中,个人感觉在不同项目中需要重复开发最多的模块一个是设备监听;另一个是设备操作。

设备监听方面,之前也设计了whenconnect作为设备插拔监听,目前也用它替代了项目中一些重复的设备管理部分。有兴趣的同学可以看看之前的文章

这一次想要解决的是设备操作问题。这里的设备操作指的并非 ui 层级。ui 级别的操作目前 appium、uiautomator2的完成度已经较高了,在此就不多逼逼。

来源

为什么有这个想法,主要还是来源于工作中其他人的反馈与自己的体验。

对于测试开发,这个部分简直是最烦人没有之一。鬼知道在过去的项目里已经重复封装了多少次subprocess.callsubprocess.Popen了,每次开发都要重新造轮子,但特地为它写一个公共库又似乎有些大费周章。结果就是每新开一个项目都要重新捏着鼻子写一遍。(google 官方封装的python-adb的文档简直让人怀疑这个工具就是开发给他们自己人玩的)

对于功能测试,很多同学表示的就是很想写一些简单的脚本用于简化平日工作解放双手,但是苦于不知道从何下手。而 bat 与 shell 脚本能做到的功能又比较有限。

目标

所以吧,针对上面提到的两种情况,还是决定,无论如何还是要把这件破事儿给做了。

  • 简化日常开发中对设备的操作
  • 简洁的方法自定义与增删
  • 无痛融入到现有框架内
  • 减少重复工作,共享开发
  • 降低使用门槛,让所有人都可以快速上手

设计与使用

与 google 官方的版本不同,这个项目并没有包含很多底层的实现,基本上还是通过命令行与 adb 交互。所以你的 adb 一定要先安装好,确保在命令行adb devices是能正常找到设备的。

还是从一个例子开始 8

平时如果我要用 python 获取一下当前设备已安装的包,大概是这么写(单纯举个例):

subprocess.call('adb shell pm list package', shell=True)

看起来还蛮简单的哦,那么如果插着好几台设备,要指定一台:

device_id = '123456f'
subprocess.call('adb -s {} shell pm list package'.format(device_id), shell=True)

也还行。那如果要获取它的结果然后处理:

device_id = '123456f'
proc = subprocess.Popen('adb -s {} shell pm list package'.format(device_id), stdout=subprocess.PIPE, shell=True)
adb_stdout, adb_stderr = proc.communicate()
result = adb_stdout.decode()

好的,越来越复杂了。接下来再提几个需求:

  • 如果设备没连上
  • 如果要同时管理多台设备
  • 如果要重复使用同样的命令
  • ...

虽然说这些东西也不是不能解决,但就是很烦人。而且很容易把你的代码写乱,影响心情。

那么用pyatool怎么写:

from pyatool import PYAToolkit


# 注册函数
PYAToolkit.bind_cmd(func_name='show_package', command='shell pm list package')

# 注册设备
device1 = PYAToolkit('123456f')
device2 = PYAToolkit('234567e')

# 直接调用
result1 = device1.show_package()
result2 = device2.show_package()

再也不用看到那些烦人的ossubprocess。pyatool 也覆盖了多台设备同时连接时的状况,所有烦人的adb -s 123456F shell真的再见~

adb 之外的扩展

当然,我们平时的需求不可能仅仅需要一条 adb 命令。pyatool 也支持了更复杂的定制。例如我们需要一个函数,用于下载 apk 并安装到手机上:

def download_and_install(url, toolkit=None):
    resp = requests.get(url)
    if not resp.ok:
        return False
    with tempfile.NamedTemporaryFile('wb+', suffix='.apk', delete=False) as temp:
        temp.write(resp.content)
        temp.close()
        toolkit.adb.run(['install', '-r', '-d', '-t', temp.name])
        os.remove(temp.name)
    return True

# 注册函数
PYAToolkit.bind_func(real_func=download_and_install)

# 注册设备
device1 = PYAToolkit('123456f')

# 直接调用
result1 = device1.download_and_install()

其中,你的函数必须包含名为 toolkit 的可选参数,它将提供一些方法用于简化开发流程。例如,

  • 通过toolkit.device_id获取设备 id
  • 通过toolkit.adb.run用于执行 adb 命令。

一个更复杂的例子

在实际开发中,我们可能会频繁给设备安装 apk;例如一旦设备连入电脑,自动给该设备安装 apk。而结合whenconnect,只需要几行代码就可以实现:

from pyatool import PYAToolkit
from whenconnect import when_connect, start_detect


VERSION = 'v0.1.4'
BASE_URL = r'https://github.com/williamfzc/simhand2/releases/download/{}/{}'
TEST_APK = r'app-debug-androidTest.apk'
MAIN_APK = r'app-debug.apk'

TEST_DL_URL = BASE_URL.format(VERSION, TEST_APK)
MAIN_DL_URL = BASE_URL.format(VERSION, MAIN_APK)


def install_sh(device_id):
    pya = PYAToolkit(device_id)
    pya.install_from(url=TEST_DL_URL)
    pya.install_from(url=MAIN_DL_URL)
    print('install simhand2 ok in {}'.format(device_id))


when_connect(device='all', do=install_sh)
start_detect()

就完成了。在运行之后,一旦有 android 设备接入,将会自动为其安装 apk。

参与开发

pyatool 如此设计的主要目的除了减少不必要的重复代码,另一考虑是为了方便所有人使用,让所有测试人员只要会使用 adb 命令就能使用 python 进行设备操作。

如果你编写了一些好方法并希望将其合入 pyatool 内置库以方便后续使用,你只需要:

  • 直接在 github 上编辑extras.py
  • 将写好的函数按照格式粘贴到extras.py
  • __all__中加入你的函数名称
  • 描述你的修改,然后点击Propose file change,github 会自动为你发起 pull request

如果确实是懒得去 github 上捣鼓,有想法也可以分享在 issue 或者文章评论里~

内置 API

extras.py内置一些常用方法的实现,可供用户直接调用。
用法强烈推荐直接看代码,word is weak:

安装

# only support python3
pip install pyatool

意见与建议

目前该项目已经在内部几个小项目里用起来了,暂时看起来还是蛮舒服的,确实减少了不少重复的代码。就是内置的 API 少了点哈哈,这个部分还是需要各位的协助~
有任何意见与建议欢迎 issue/评论交流,或者简单粗暴地给我 PR :)
欢迎 star 与 fork,开源项目的目的还是希望大家都能参与进来,把工具打磨得更好吧~

共收到 19 条回复 时间 点赞

在运行时,遇到下面的问题:
RuntimeError: unknown error happened when execute ['adb', '-s', '28c3f649', 'install', '-r', '-d', '-t', '/var/folders/nc/53fjn56d76d6s51q9p1shd480000gp/T/tmptb_wogrc.apk'], view terminal for detail
不知道什么原因,求解

luomanqshijie 回复

目前如果命令执行遇到问题的话就统一会报这个错误的,后面打算改一下吧~
真正的报错内容应该在 Trackback 上面,例如:

error: device '123456F' not found
Traceback (most recent call last):
  File "/Users/admin/pyat/demo.py", line 24, in <module>
    d = PYAToolkit('123456F', mode='remote')
  File "/Users/admin/pyat/pyatool/core.py", line 42, in __init__
    self.adb = ADB(device_id, mode)
  File "/Users/admin/pyat/pyatool/adb.py", line 15, in __init__
    self.device_ip = self._enable_remote_connect()
  File "/Users/admin/pyat/pyatool/adb.py", line 46, in _enable_remote_connect
    ip_address = self._get_ip_address()
  File "/Users/admin/pyat/pyatool/adb.py", line 41, in _get_ip_address
    result = self.run(['shell', 'ifconfig', 'wlan0'])
  File "/Users/admin/pyat/pyatool/adb.py", line 26, in run
    return self._exec(final_command)
  File "/Users/admin/pyat/pyatool/adb.py", line 36, in _exec
    raise RuntimeError(feedback)
RuntimeError: unknown error happened when execute ['adb', '-s', '123456F', 'shell', 'ifconfig', 'wlan0'], view terminal for detail

根据第一行可以看出问题是找不到设备

luomanqshijie 回复

改好啦 用 pip 更新一下就可以了 最新的应该是 0.3.0

2018-11-11 02:24.11 DEVICE                         adb_cmd=['adb', '-s', '123456F'] id=123456F ip=None
Traceback (most recent call last):
  File "/Users/admin/pyat/demo.py", line 27, in <module>
    result = d.test_a()
  File "/Users/admin/pyat/pyatool/core.py", line 88, in <lambda>
    return lambda *args, **kwargs: command(*args, toolkit=self, **kwargs)
  File "/Users/admin/pyat/pyatool/core.py", line 53, in <lambda>
    return binder.add(func_name, lambda toolkit: toolkit.adb.run(command))
  File "/Users/admin/pyat/pyatool/adb.py", line 26, in run
    return self._exec(final_command)
  File "/Users/admin/pyat/pyatool/adb.py", line 36, in _exec
    raise RuntimeError(feedback)
RuntimeError: error: device '123456F' not found

报错信息会正常出现了

脚本中集成了常用的 adb 命令,赞!
学习了不常见的 adb 命令,赞!
不过在此调用封装好的脚本个人感觉,,,,,,
思路还是很好的,,,

Benjamin 回复

😹 😹 老哥给点建议?

思想我学习了!

大佬,result1 = device1.show_package() 这返回的是咋结果呢,我打印了内容为空呢

蓝蓝 回复

我试了一下好像没问题啊?是因为不是最新版本?

from pyatool import PYAToolkit


d = PYAToolkit('4df189487c7b6fef')
result = d.show_package()
print(result)

console:

package:com.sec.android.app.phoneutil
package:com.android.defcontainer
package:com.UCMobile
package:com.sec.android.gallery3d
package:com.android.phone
package:com.sec.android.fotaclient
williamfzc 回复

我试了三星手机 可以,乐视手机 打印的内容为空哦

蓝蓝 回复

这个问题我好像没遇到过 我是测过 oppo、小米

def show_package(toolkit=None):
    """
    展示设备上所有已安装的包
    :param toolkit:
    :return:
    """
    return toolkit.adb.run(['shell', 'pm', 'list', 'package'])

可以点进去看源码实现的,可以先试一下 adb shell pm list package是不是正常的?乐视的手机我很少见到😹

williamfzc 回复

adb shell pm list package 正常的

蓝蓝 回复

那其他的 API 是正常的吗?

williamfzc 回复

不可以,乐视手机 返回都是空

蓝蓝 回复

我手边没有乐视手机,而且没遇到过这种情况..
你可以试一下:

  1. pip install --upgrade pyatool更新一下版本
  2. 在脚本最前面打开日志(默认是关闭的):PYAToolkit.switch_logger(True)
  3. 看一下日志里的输出是什么样的
williamfzc 回复

result1 = 'package:com.github.uiautomator\r\r\npackage:com.letv.android.FuseWire\r\r\n'
两个\r 回车就为空了。

蓝蓝 回复

日志展示的是全的对吗?
然后 result1 是空还是package:com.github.uiautomator\r\r\npackage:com.letv.android.FuseWire\r\r\n呢?
不过我发现乐视手机上是\r\r\n 换行 其他手机都是\r\n

williamfzc 回复

日志是对的,result1='package:com.github.uiautomator\r\r\npackage:com.letv.android.FuseWire\r\r\n'
除了乐视手机,我还有一个手机 oppo r7005 4.4 系统的获取到的也是空。

蓝蓝 回复

去翻了一台 4.4 的手机终于复现这个问题了...
感觉是因为 pycharm 的终端对换行符的错误解析导致的,以前 android 版本的换行符都是\r\r\n

你可以按照如下步骤看看:

  1. 不要用 pycharm 的终端运行,直接用 cmd 或者 powershell 之类的终端运行。我这边在终端上运行是正常的
  2. 如果要用 pycharm,可以把换行符换掉result = result.replace('\r\r\n', '\r\n')。这么做之后我在 pycharm 上运行也正常
williamfzc 回复

恩确实如此,cmd 下正常的,感谢大佬啊👍

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册