源码位置:airtest/cli/runner.py

使用:根据 airtest 文档说明,可以通过命令行来启动 air 脚本,需要传入一些参数如设备号,脚本名等,这样就可以不用通过 AirTest IDE 来运行了,可以集成,所以我们也可以写个脚本来控制 air 脚本的运行。

文档链接:https://airtest.readthedocs.io/en/latest/README_MORE.html#running-air-from-cli

Running .air from CLI

Using AirtestIDE, you can easily create and author automated tests as .air directories. Airtest CLI provides the possibility to execute tests on different host machine and target device platforms without using AirtestIDE itself.

Connections to devices are specified by command line arguments, i.e. the test code is platform independent and one code, test cases, scenarios can be used for Android, Windows or iOS devices as well.

Following examples demonstrate the basic usage of airtest framework running from CLI. For a deeper understanding, try running provided test cases: airtest/playground/test_blackjack.air
run test case

run test test cases and scenarios on various devices

airtest run "path to your .air dir" --device Android:///
airtest run "path to your .air dir" --device Android://adbhost:adbport/serialno
airtest run "path to your .air dir" --device Windows:///?title_re=Unity.*
airtest run "path to your .air dir" --device iOS:///
...

show help

airtest run -h
usage: airtest run [-h] [--device [DEVICE]] [--log [LOG]]
[--recording [RECORDING]]
script

positional arguments:
script air path

optional arguments:
-h, --help show this help message and exit
--device [DEVICE] connect dev by uri string, e.g. Android:///
--log [LOG] set log dir, default to be script dir
--recording [RECORDING]
record screen when running

这就是说你用 airtest run 命令,可以指定运行某 air 脚本,指定设备,指定 log 输出地址

翻一下源码,找到 runner.py,可以看得出这是一个 unittest 的子类 AirtestCase,入口是 run_script 接口

1、程序的入口——run_script,传入参数 parsed_args,进来以后传给全局变量 args,给 airtestcase 里调用。

这几行代码,用过 unittest 的朋友应该都很熟悉了

def run_script(parsed_args, testcase_cls=AirtestCase):
    global args  # make it global deliberately to be used in AirtestCase & test scripts
    args = parsed_args
    suite = unittest.TestSuite()#创建一个测试套件
    suite.addTest(testcase_cls())#添加一条AirtestCase类型的case,因为接口入参默认testcase_cls=AirtestCase
    result = unittest.TextTestRunner(verbosity=0).run(suite)#运行它
    if not result.wasSuccessful():
         sys.exit(-1)#退出

2、AirtestCase 类

这里定义好了 setUpClass、setUp、runTest、tearDown、tearDownClass

分别做了什么呢,一个个看一下:

@classmethod
def setUpClass(cls):
    cls.args = args  #runScrip传进来的参数
    setup_by_args(args) #设置参数,设备、log路径、脚本路径
    # setup script exec scope
    cls.scope = copy(globals())
    cls.scope["exec_script"] = cls.exec_other_script

def setUp(self):
    if self.args.log and self.args.recording: #如果参数配置了log路径且recording为Ture
        for dev in G.DEVICE_LIST:
            try:
                dev.start_recording() #开始录制
            except:
                traceback.print_exc()

def tearDown(self):#停止录制
    if self.args.log and self.args.recording:
        for k, dev in enumerate(G.DEVICE_LIST):
            try:
                output = os.path.join(self.args.log, "recording_%d.mp4" % k)
                dev.stop_recording(output)
            except:
                traceback.print_exc()

def runTest(self):#运行脚本
    scriptpath = self.args.script #参数传入的脚本路径
    #分割路径最后的名字,替换.air为.py,也就是传入‘d:/aaa/bbb.air’,pyfilename就为bbb.py
    pyfilename = os.path.basename(scriptpath).replace(self.SCRIPTEXT, ".py")
    #再组装py文件的路径,d:/aaa/bbb.air/bbb.py,看过air脚本文件就知道,这才是脚本代码,其他是图片
    pyfilepath = os.path.join(scriptpath, pyfilename)
    pyfilepath = os.path.abspath(pyfilepath)
    self.scope["__file__"] = pyfilepath
    #读进来
    with open(pyfilepath, 'r', encoding="utf8") as f:
        code = f.read()
    pyfilepath = pyfilepath.encode(sys.getfilesystemencoding())
    #运行读进来的脚本
    try:
        exec(compile(code.encode("utf-8"), pyfilepath, 'exec'), self.scope)
    except Exception as err:
        #出错处理,日志
        tb = traceback.format_exc()
        log("Final Error", tb)
        six.reraise(*sys.exc_info())

def exec_other_script(cls, scriptpath):#这个接口不分析了,因为已经用using代替了。
#这个接口就是在你的air脚本中如果用了exec_script就会调用这里,它会把子脚本的图片文件拷贝过来,并读取py文件运行

#参数设置
def setup_by_args(args):
    # init devices
    if isinstance(args.device, list):#如果传入的设备参数是一个列表,所以命令行可以设置多个设备哦
        devices = args.device
    elif args.device:
        devices = [args.device]#不是列表就给转成列表
    else:
        devices = []
        print("do not connect device")

    # set base dir to find tpl
    args.script = decode_path(args.script)#脚本路径

    # set log dir日志路径
    if args.log is True:
        print("save log in %s/log" % args.script)
        args.log = os.path.join(args.script, "log")
    elif args.log:
        print("save log in '%s'" % args.log)
        args.log = decode_path(args.log)
    else:
        print("do not save log")

    # guess project_root to be basedir of current .air path把air脚本的路径设置为工程根目录
    project_root = os.path.dirname(args.script) if not ST.PROJECT_ROOT else None
    auto_setup(args.script, devices, args.log, project_root)#这个接口很熟悉吧,在IDE里新建一个air脚本就会自动生成这句,里面就设备的初始化连接,设置工程路径,日志路径。

所以,上层的 air 脚本不用什么测试框架,是这个 airtestCase 在支撑,这种设计思路确实降低了脚本的上手门槛,跟那些用 excel 表格、自然语言脚本的框架有点像。

源码全部贴出来方便看

# -*- coding: utf-8 -*-

import unittest
import os
import sys
import six
import re
import shutil
import traceback
import warnings
from io import open
from airtest.core.api import G, auto_setup, log
from airtest.core.settings import Settings as ST
from airtest.utils.compat import decode_path
from copy import copy


class AirtestCase(unittest.TestCase):

    PROJECT_ROOT = "."
    SCRIPTEXT = ".air"
    TPLEXT = ".png"

    @classmethod
    def setUpClass(cls):
        cls.args = args

        setup_by_args(args)

        # setup script exec scope
        cls.scope = copy(globals())
        cls.scope["exec_script"] = cls.exec_other_script

    def setUp(self):
        if self.args.log and self.args.recording:
            for dev in G.DEVICE_LIST:
                try:
                    dev.start_recording()
                except:
                    traceback.print_exc()

    def tearDown(self):
        if self.args.log and self.args.recording:
            for k, dev in enumerate(G.DEVICE_LIST):
                try:
                    output = os.path.join(self.args.log, "recording_%d.mp4" % k)
                    dev.stop_recording(output)
                except:
                    traceback.print_exc()

    def runTest(self):
        scriptpath = self.args.script
        pyfilename = os.path.basename(scriptpath).replace(self.SCRIPTEXT, ".py")
        pyfilepath = os.path.join(scriptpath, pyfilename)
        pyfilepath = os.path.abspath(pyfilepath)
        self.scope["__file__"] = pyfilepath
        with open(pyfilepath, 'r', encoding="utf8") as f:
            code = f.read()
        pyfilepath = pyfilepath.encode(sys.getfilesystemencoding())

        try:
            exec(compile(code.encode("utf-8"), pyfilepath, 'exec'), self.scope)
        except Exception as err:
            tb = traceback.format_exc()
            log("Final Error", tb)
            six.reraise(*sys.exc_info())

    @classmethod
    def exec_other_script(cls, scriptpath):
        """run other script in test script"""

        warnings.simplefilter("always")
        warnings.warn("please use using() api instead.", PendingDeprecationWarning)

        def _sub_dir_name(scriptname):
            dirname = os.path.splitdrive(os.path.normpath(scriptname))[-1]
            dirname = dirname.strip(os.path.sep).replace(os.path.sep, "_").replace(cls.SCRIPTEXT, "_sub")
            return dirname

        def _copy_script(src, dst):
            if os.path.isdir(dst):
                shutil.rmtree(dst, ignore_errors=True)
            os.mkdir(dst)
            for f in os.listdir(src):
                srcfile = os.path.join(src, f)
                if not (os.path.isfile(srcfile) and f.endswith(cls.TPLEXT)):
                    continue
                dstfile = os.path.join(dst, f)
                shutil.copy(srcfile, dstfile)

        # find script in PROJECT_ROOT
        scriptpath = os.path.join(ST.PROJECT_ROOT, scriptpath)
        # copy submodule's images into sub_dir
        sub_dir = _sub_dir_name(scriptpath)
        sub_dirpath = os.path.join(cls.args.script, sub_dir)
        _copy_script(scriptpath, sub_dirpath)
        # read code
        pyfilename = os.path.basename(scriptpath).replace(cls.SCRIPTEXT, ".py")
        pyfilepath = os.path.join(scriptpath, pyfilename)
        pyfilepath = os.path.abspath(pyfilepath)
        with open(pyfilepath, 'r', encoding='utf8') as f:
            code = f.read()
        # replace tpl filepath with filepath in sub_dir
        code = re.sub("[\'\"](\w+.png)[\'\"]", "\"%s/\g<1>\"" % sub_dir, code)
        exec(compile(code.encode("utf8"), pyfilepath, 'exec'), cls.scope)


def setup_by_args(args):
    # init devices
    if isinstance(args.device, list):
        devices = args.device
    elif args.device:
        devices = [args.device]
    else:
        devices = []
        print("do not connect device")

    # set base dir to find tpl
    args.script = decode_path(args.script)

    # set log dir
    if args.log is True:
        print("save log in %s/log" % args.script)
        args.log = os.path.join(args.script, "log")
    elif args.log:
        print("save log in '%s'" % args.log)
        args.log = decode_path(args.log)
    else:
        print("do not save log")

    # guess project_root to be basedir of current .air path
    project_root = os.path.dirname(args.script) if not ST.PROJECT_ROOT else None
    auto_setup(args.script, devices, args.log, project_root)


def run_script(parsed_args, testcase_cls=AirtestCase):
    global args  # make it global deliberately to be used in AirtestCase & test scripts
    args = parsed_args
    suite = unittest.TestSuite()
    suite.addTest(testcase_cls())
    result = unittest.TextTestRunner(verbosity=0).run(suite)
    if not result.wasSuccessful():
        sys.exit(-1)


↙↙↙阅读原文可查看相关链接,并与作者交流