游戏测试 自建 GAutomator 的框架 使用解读 (二)

陈子昂 · 2019年10月02日 · 2662 次阅读

国庆节有时间写就写了一篇。

wetest 文件夹

这个文件夹下面是主要的 API,框架进行实例化的时候,API 源头就在这里

#一段封装的例子
import wpyscripts.manager as manager
#省略
    def __init__(self):
       self.device = manager.get_device()
        self.engine = manager.get_engine()
        self.logger = manager.get_logger()
        self.reporter = manager.get_reporter()

[跳转 wetest 文件夹] device.py 上个文章讲过,来看下实例化的 engine 的文件

engine.py

#核心导入的类库
import threading
from wpyscripts.common.adb_process import *
from wpyscripts.wetest.element import Element
from wpyscripts.common.protocol import Commands, TouchEvent
from wpyscripts.common.socket_client import SocketClient
from wpyscripts.common.rpc_thread import RPCReceiveThread
from wpyscripts.common.wetest_exceptions import *

上面类库按字面意思,大概可以明白线程模式,通过 adb,封装了一个 Element 类在 element.py 里面,下面的 3 个类库是通信相关的,这个后面在讲,最后一个包装的异常模块。
这里核心类是 GameEngine(),在使用前依然需要初始化

#官方例子
engine=manager.get_engine()
logger=manager.get_logger()
version=engine.get_sdk_version()
logger.debug("Version Information : {0}".format(version))

# 省略
def get_sdk_version(self): #line 121
        """ 获取引擎集成的SDK的版本信息
        """
        ret = self.send_command_with_retry(Commands.GET_VERSION, 1)  #核心函数
        engine = ret.get("engine", None)
        sdk_version = ret.get("sdkVersion", None)
        engine_version = ret.get("engineVersion", None)
        ui_type = ret.get("sdkUIType", None)
        version = VersionInfo(engine_version, engine, sdk_version, ui_type)
        return version

get_sdk_version 是实例化后通过这个看反馈集成 sdk 版本信息,做为框架前置开发必备的。
send_command_with_retry() 是核心函数,使用了 adb forward 通过命令序列进行转发

def send_command_with_retry(self,command, param=None , timeout=20):  #self.send_command_with_retry(Commands.GET_VERSION, 1)
        for i in range(0,2):
            try:
                ret = self.socket.send_command(command, param,timeout)
                return ret
            except Exception as e:
                ret = excute_adb_process("forward --list")  #核心数据
                logger.info("adb forward list : " + str(ret))
                ret = forward(self.port, unity_sdk_port)# with retry...
                logger.info("after reforward list : " + str(ret))
                try:
                    self.socket = SocketClient(self.address, self.port)
                except Exception as e :
                    logger.exception(e)
                time.sleep(5)

Commands.GET_VERSION 是一个类似枚举类。


class Commands(object):
    GET_VERSION = 100  # 获取版本号
    FIND_ELEMENTS = 101  # 查找节点
    FIND_ELEMENT_PATH = 102  # 模糊查找
    GET_ELEMENTS_BOUND = 103  # 获取节点的位置信息
    GET_ELEMENT_WORLD_BOUND = 104  # 获取节点的世界坐标
    GET_UI_INTERACT_STATUS = 105  # 获取游戏的可点击信息,包括scene、可点击节点,及位置信息
    GET_CURRENT_SCENE = 106  # 获取Unity的Scene名称

跳转 Commands
对通信熟悉的了解,会发现这个是一个内建的协议,用于通信处理的内容。

下面其他函数是包装 Ga 功能的 APi 返回 engine.py

def find_element(self, name):
        """
            通过GameObject.Find查找对应的GameObject
        :param name:
            GameObject.Find的参数
        :Usage:
            >>>import wpyscripts.manager as manager
            >>>engine=manager.get_engine()
            >>>button=engine.find_element('/Canvas/Panel/Button')
        :return:
            a instance of Element if find the GameObject,else return  None
            example:
            {"object_name":"/Canvas/Panel/Button",
            "instance":4257741}
        :rtype: Element
        :raise:
        """
        ret = self.send_command_with_retry(Commands.FIND_ELEMENTS, [name])  #通知查找节点 [name]?未知
        if ret:
            ret = ret[0]
            if ret["instance"] == -1:
                return None
            else:
                return Element(ret["name"], ret["instance"])
        else:#这2句可以省略,如果ret不为真,返回就是None.
            return None  

根据注释里面的 Usage 部分就能看到基础使用方式,'/Canvas/Panel/Button‘是通过 GAutomatorView 工具中间的空间数那边复制拿到的,如果在封装框架时可以用 PO 模式或者配置模式

#配置模式 通过任意配置.py文件  然后type创建占用内存很小的类,等同class LoginMain:PanelMiddleButton是类变量,类内部属性也是在字典内。
LoginMain =type('LoginMain',(object,),dict(PanelMiddleButton='/Canvas/Panel/Button'))

第二种是使用字典缓存,通过 txt 写入内容在缓存到字典内,推荐是第一种。

ret = self.send_command_with_retry(Commands.FIND_ELEMENTS, [name])  #这个[name发现不理解]

Commands.FIND_ELEMENTS 对应的协议号,我们继续深究 send_command_with_retry 函数。ret 本质从哪里来的

ret = self.socket.send_command(command, param,timeout)
#往上翻  line 113
self.socket = SocketClient(self.address, self.port)

跳转 from wpyscripts.common.socket_client import SocketClient

socket_client.py

class SocketClient(object):
    def __init__(self, _host='localhost', _port=27018):
        self.host = _host
        self.port = _port
        self.connect()

    def connect(self):
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)   #Tcp
        self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) #开启Nagle算法
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) #保持长链接
        self.socket.connect((self.host, self.port))

socket 后面跟着那段是从 C 继承过来的,可以通过配置来决定功能。然看下 line 101 行。

def send_command(self, cmd, params=None, timeout=20):
        # if params != None and not isinstance(params, dict):
        #     raise Exception('Params should be dict')
        if not params:
            params = ""
        command = {}
        command["cmd"] = cmd
        command["value"] = params
        for retry in range(0, 2):
            try:
                self.socket.settimeout(timeout)
                self._send_data(command)
                ret = self._recv_data()
                return ret
            except WeTestRuntimeError as e:
                raise e
            except socket.timeout:
                self.socket.close()
                self.connect()
                raise WeTestSDKError("Recv Data From SDK timeout")
            except socket.error as e:
                time.sleep(1)
                print("Retry...{0}".format(e.errno))
                self.socket.close()
                self.connect()
                continue
        raise Exception('Socket Error')

params 没有内容就是一个字符串,然后下面缓存一个 command 字典,字典里面 cmd 为了传入上文的 Commands 里面的协议号,params 传入字符串.python 允许这样套入 [字符串],等于 key 对应的 value 在这里是一个列表,所以使用时可以把 [name] 的长度和内容加个打印看看。

 self.socket.settimeout(timeout) #每个socket设置超时
self._send_data(command)
 ret = self._recv_data()

开闭了 2 个函数_send_data 和 _recv_data 函数。
函数前面带一个下划线代表在当前类内部使用,但可以被实例化后调用。注意函数前面带二个下划线的话就不能被实例化后直接调用,会抛错,这是 python 一种内部设定。

def _send_data(self, data): 
        try:
            serialized = json.dumps(data)
        except (TypeError, ValueError) as e: #出现这里 一般是指转不了json对象内存的。
            raise WeTestInvaildArg('You can only send JSON-serializable data')
        length = len(serialized)
        buff = struct.pack("i", length)
        self.socket.send(buff)
        if six.PY3:
            self.socket.sendall(bytes(serialized, encoding='utf-8'))
        else:
            self.socket.sendall(serialized)

客户端-->服务端,这里讲的是基础的 socket 发送数据和带结构体,dumps 是正序列化数据,json 库可以自己替换成 ujson,但框架的原则是不用安装其他类库就能使用,可以自己修改。结构体是 int32 通过 fmt 压入到 struct.pack 压入缓存区,消息体是上面那段 json.dumps 内存压成二进制,在通过 sendall()
bytes() 估计是做保护转换写法吧,发现框架中有很多保护转换写法。

def _recv_data(self):
        deserialized = self.recv_package()
        if deserialized['status'] != 0:
            message = "Error code: " + str(deserialized['status']) + " msg: " + deserialized['data']
            raise WeTestSDKError(message)
        return deserialized['data']

这里等于是服务端-->本地缓存-->客户端处理,这里可以知道 ga 后端的数据结构,这里面又包裹了一个函数 recv_package().
说明客户端接收的数据 status==0,就是正确的,直接返回回包里面的 data,如果!=0 会 raise 错误条件,说明正确回包里面最少 items 里面有 2 个 key,一个是 status,一个是 data。改造和写得时候具体还是打印个输出看看。这时不用运行也可以基本判断出来 tcp -socket 传输什么样的数据包,回包是什么。根据导入库,双端对象之间协议类型是 Tcp-Rpc,用于自动化框架是用 adb 端口转发。

下文在继续介绍其他的。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册