Skip to content

Commit

Permalink
Merge pull request #83 from Tencent/bugfix/logcat-encoding
Browse files Browse the repository at this point in the history
Bugfix/logcat encoding
  • Loading branch information
drunkdream committed Nov 13, 2019
2 parents 82ceeed + 6436ed2 commit 5e9975b
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 34 deletions.
11 changes: 7 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import shlex

import sphinx_rtd_theme

import recommonmark.parser as markdown

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
Expand All @@ -39,8 +39,7 @@

# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
source_suffix = ['.rst', '.md']

# The encoding of source files.
#source_encoding = 'utf-8-sig'
Expand All @@ -50,7 +49,7 @@

# General information about the project.
project = u'QT4A'
copyright = u'2018, QTA'
copyright = u'2018-2019, QTA'
author = u'QTA'

# The version info for the project you're documenting, acts as replacement for
Expand Down Expand Up @@ -287,3 +286,7 @@

# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False

source_parsers = {
'.md': markdown.CommonMarkParser,
}
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ QT4A,即Quick Test for Android,基于QTA提供的面向Android应用的UI测
dev_operation
web_test
advanced_feature
testdriver_plugin
settings
qa

Expand Down
78 changes: 78 additions & 0 deletions docs/testdriver_plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# 测试桩插件

## 什么是测试桩插件

QT4A通过将测试桩注入到被测应用进程中,获取进程中的相关数据,如控件树信息等。但是有些情况下,用户也需要注入自己的代码,以获取自己期望的数据。QT4A测试桩提供了一种将用户指定的`dex`文件注入到被测进程中的能力,同时,借助于QT4A的通信通道,用户可以传输自己的数据,而不需要自己创建信道。

## 如何实现测试桩插件

测试桩插件本质上还是一个`dex`,可以使用`Java`语言编写。但是与普通的dex相比,它还是存在着一些差异。最主要的,就是它需要实现一个入口类,用来作为整个插件的执行入口。

```java
public class MyPlugin {
private int handleRequest(String arg){
// do your job
return 123;
}

public static JSONObject main(JSONObject args) {
JSONObject result = new JSONObject();
try{
String cmd = args.getString("SubCmd");
View view = (View)args.get("Control"); //获取View实例
String arg = args.getString("Arg"); // 获取参数
if("Hello".equals(cmd)){
MyPlugin plugin = new MyPlugin();
try{
result.put("Result", plugin.handleRequest(arg));
}catch(Exception e){
result.put("Error", e.toString());
}
}else{
result.put("Result", -1);
}
}catch(JSONException e){
e.printStackTrace();
}
return result;
}
}
```

`MyPlugin.main`是整个dex的入口函数,会被QT4A测试桩调起,并传入客户端传入的参数。`main`里一般会解析这些参数,并调用相应的方法,返回结果可以通过`Result`字段返回,函数执行的报错信息,可以通过`Error`字段返回,也可以打印到`logcat`中。不过,不捕获异常也没有关系,因为QT4A测试桩在调用插件函数的时候也会主动捕获异常的。


## 如何编译测试桩插件

1. 使用普通编译apk的方式编译,然后提取出apk中的`classes.dex`
2. 使用命令行方式编译,具体命令如下:

```bash
javac -encoding utf-8 -target 1.7 -d bin src/com/test/plugin/MyPlugin.java -bootclasspath $SDK_ROOT/platforms/android-24/android.jar # $SDK_ROOT为Android SDK根路径

cd bin
jar cvf plugin.jar com

$SDK_ROOT/build-tools/25.0.3/dx --dex --output=plugin.jar ../plugin.jar # 25.0.3要改成实际安装的版本
```

这样会在根目录生成目标文件`plugin.jar`,虽然这个文件是`.jar`结尾,但本质上是一个zip格式的`dex`文件。


## 如何使用测试桩插件

先将编译出来的dex/jar文件push到设备某一路径下,如:`/data/local/tmp/plugin.jar`

然后使用以下代码来调用:

```python

result = driver.call_external_method(jar_path, # plugin.jar在设备中的路径
'com.test.plugin.MyPlugin', # 替换为真正的插件入口类路径
Control=hashcode, # 如果需要操作控件可以在这里指定控件的hashcode
SubCmd='Hello', # 子命令
Arg='you param' # 子命令的参数
)
```

`driver``AndroidDriver`实例。建议用户对接口再做一层封装,这样更像是本地方法调用。
6 changes: 3 additions & 3 deletions qt4a/andrcontrols.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
'''定义Android控件
'''

import six
from io import BytesIO
import io
import os
import tempfile
import time

import six
from testbase.util import LazyInit, Timeout
from tuia.exceptions import ControlNotFoundError

Expand Down Expand Up @@ -1830,7 +1830,7 @@ def screenshot(self):
with open(temp_path, 'rb') as fp:
image_data = fp.read()
os.remove(temp_path)
image = Image.open(BytesIO(image_data))
image = Image.open(io.BytesIO(image_data))
image = image.crop(self.rect)
return image

Expand Down
54 changes: 28 additions & 26 deletions qt4a/androiddriver/adb.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

from pkg_resources import iter_entry_points
from qt4a.androiddriver.adbclient import ADBClient
from qt4a.androiddriver.util import Singleton, Deprecated, logger, ThreadEx, TimeoutError, InstallPackageFailedError, PermissionError, is_int, encode_wrap, enforce_utf8_decode
from qt4a.androiddriver.util import Singleton, Deprecated, logger, ThreadEx, TimeoutError, InstallPackageFailedError, PermissionError, is_int, utf8_encode, encode_wrap, enforce_utf8_decode

try:
import _strptime # time.strptime() is not thread-safed, so import _strptime first, otherwise it raises an AttributeError: _strptime_time
Expand Down Expand Up @@ -468,24 +468,22 @@ def save_log(self, save_path):
if not hasattr(self, '_log_list'):
return
log_list = self.get_log()
if six.PY2:
for i in range(len(log_list)):
log = log_list[i]
if not isinstance(log, unicode):
# 先编码为unicode
for code in ['utf8', 'gbk']:
try:
log = log.decode(code)
break
except UnicodeDecodeError as e:
# logger.warn('decode with %s error: %s' % (code, e))
pass
else:
log = repr(log)
log_list[i] = log.encode('utf8') if isinstance(
log, unicode) else log
f = open(save_path, 'w')
f.write('\n'.join(log_list))
for i, log in enumerate(log_list):
if isinstance(log, bytes):
# 先编码为unicode
for code in ['utf8', 'gbk']:
try:
log = log.decode(code)
break
except UnicodeDecodeError as e:
# logger.warn('decode with %s error: %s' % (code, e))
pass
else:
log = repr(log)
log_list[i] = log.encode('utf8') if not isinstance(
log, bytes) else log
f = open(save_path, 'wb')
f.write(b'\n'.join(log_list))
f.close()

def add_logcat_callback(self, callback):
Expand All @@ -501,12 +499,13 @@ def remove_logcat_callback(self, callback):
self._logcat_callbacks.remove(callback)

def insert_logcat(self, process_name, year, month_day, timestamp, level, tag, tid, content):
self._log_list.append('[%s] [%s-%s %s] %s/%s(%s): %s' % (process_name,
year, month_day, timestamp,
level,
tag,
tid,
content))
self._log_list.append(b'[%s] [%d-%s %s] %s/%s(%d): %s' % (utf8_encode(process_name),
int(year), utf8_encode(month_day),
utf8_encode(timestamp),
utf8_encode(level),
utf8_encode(tag),
int(tid),
utf8_encode(content)))
pid = 0
pattern = re.compile(r'^(.+)\((\d+)\)$')
ret = pattern.match(process_name)
Expand Down Expand Up @@ -548,6 +547,9 @@ def _logcat_thread_func(self, process_list, params=''):
else:
continue

if 'beginning of main' in log or 'beginning of system' in log:
continue

ret = pattern.match(log)
if not ret:
logger.info('log: %s not match pattern' % log)
Expand Down Expand Up @@ -579,7 +581,7 @@ def _logcat_thread_func(self, process_list, params=''):

for i in range(len(self._log_list) - 1, -1, -1):
# 修复之前记录的“<pre-initialized>”进程
pre_process_name = '[%s(%d)]' % (
pre_process_name = b'[%s(%d)]' % (
init_process, item['pid'])
if not pre_process_name in self._log_list[i]:
continue
Expand Down
Binary file modified qt4a/androiddriver/tools/QT4AHelper.apk
Binary file not shown.
19 changes: 18 additions & 1 deletion test/test_androiddriver/test_adb.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
from unittest import mock
except:
import mock
import os
import shlex
import tempfile
import unittest

from qt4a.androiddriver.adb import ADB, LocalADBBackend
Expand Down Expand Up @@ -346,6 +348,8 @@ def mock_run_shell_cmd(cmd_line, root=False, **kwds):
u0_a1308 24558 2598 1634068 86392 ffffffff 00000000 S com.tencent.nijigen:picker
u0_a1308 24887 1 9272 448 ffffffff 00000000 S /data/data/com.tencent.nijigen/lib/libxguardian.so
'''
elif args[0] == 'logcat':
return ''
else:
raise NotImplementedError('Not supported command: %s' % cmd_line)

Expand Down Expand Up @@ -420,7 +424,20 @@ def test_list_dir(self):
self.assertEqual(dir_list[0]['name'], 'com.android.apps.tag')
self.assertEqual(dir_list[0]['attr'], 'rwxr-x--x')


def test_save_log(self):
adb_backend = LocalADBBackend('127.0.0.1', '')
adb = ADB(adb_backend)
adb.start_logcat()
adb.insert_logcat('test', 2019, '0101', '10:51:42.899', 'I', 'test', 1, '我们')
adb.insert_logcat('test', 2019, '0101', '10:51:42.899', 'I', 'test', 1, u'中国'.encode('gbk'))
adb.insert_logcat('test', 2019, '0101', '10:51:42.899', 'I', 'test', 1, u'\ub274')
save_path = tempfile.mkstemp('.log')[1]
adb.save_log(save_path)
with open(save_path, 'r') as fp:
text = fp.read()
self.assertIn('我们', text)
self.assertIn('中国', text)


if __name__ == '__main__':
unittest.main()
Expand Down
17 changes: 17 additions & 0 deletions test/test_apktool/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# -*- coding: UTF-8 -*-
#
# Tencent is pleased to support the open source community by making QTA available.
# Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved.
# Licensed under the BSD 3-Clause License (the "License"); you may not use this
# file except in compliance with the License. You may obtain a copy of the License at
#
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
#

'''
'''
49 changes: 49 additions & 0 deletions test/test_apktool/test_repack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# -*- coding: UTF-8 -*-
#
# Tencent is pleased to support the open source community by making QTA available.
# Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved.
# Licensed under the BSD 3-Clause License (the "License"); you may not use this
# file except in compliance with the License. You may obtain a copy of the License at
#
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
#

'''repack.py unittest
'''


try:
from unittest import mock
except:
import mock

import os
import tempfile
import unittest

from qt4a.apktool import apkfile, repack


class TestRepack(unittest.TestCase):

def test_get_apk_signature(self):
apk_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'qt4a', 'androiddriver', 'tools', 'QT4AHelper.apk')
apk_file = apkfile.APKFile(apk_path)
for it in apk_file.list_dir('META-INF'):
if it.lower().endswith('.rsa'):
tmp_rsa_path = tempfile.mkstemp('.rsa')[1]
apk_file.extract_file('META-INF/%s' % it, tmp_rsa_path)
orig_signature = repack.get_apk_signature(tmp_rsa_path).strip()
self.assertEqual(orig_signature, '3082036f30820257a00302010202046d23394e300d06092a864886f70d01010b05003067310b300906035504061302383631123010060355040813094775616e67646f6e673111300f060355040713085368656e7a68656e3110300e060355040a130754656e63656e743110300e060355040b130754656e63656e74310d300b06035504031304515434413020170d3137303731383038303130335a180f32313136303632343038303130335a3067310b300906035504061302383631123010060355040813094775616e67646f6e673111300f060355040713085368656e7a68656e3110300e060355040a130754656e63656e743110300e060355040b130754656e63656e74310d300b060355040313045154344130820122300d06092a864886f70d01010105000382010f003082010a0282010100a05d7ca7768dd6a16098236ee3d670d139abbda479557bee2ce62e0a5ee9f825c986e8ba875decb4dec3fb13a933bbfc9434a70442b6cccc8d6d12db6e510cf915cc25c71bb4670876ddf15de880340a3af3d656e76cef452ccd2192879e4eef67aca9b203124dc5c978f57533c707e49abf0ca3f5691d3de9048587c7aa22ecf703d589236edcf1a0cadb26fbbb126326f200ce9b5573e36dd2d63363ad1c518df2a9550b7aede75bc74e44484fcb177c8c6515e7f2011af1a987c1bc11ddef1303bcaf04f7ea186ce66d96921021e3ebf7141801a7abe09663caae7386785b144a358b3bb877c190ee9ac0a8f313a48794ca2a3fb8c0e7e38afac4f0956cc50203010001a321301f301d0603551d0e041604144d974cc4e8bd5b5116ff0ef2676c4556ca4aa727300d06092a864886f70d01010b050003820101000fd165541ad15e729549fa497eae037893032f565fc55ceea2fbb8e77a283fbf23dab00afe1f6943056cbc62a567400879418abc6a3646bdc7bbf51f84741173d7f8386a07e89d7cd1228e387fbd727af8402231bf5834450799ba79251f3673c45fb523301a3791a279523c78af98c0932e17a365b3a28c59701a123e0b3df49ec9d1ef6203b4b92ce67b100d2f493c4de0376103b4b2f4b1f40ba09e5bc3329f184646af0d046968b3af2af7786fc060f3c0bfd757bf2d4a32d222fb701a7032fd19271bb6cffc06f37cc2921bec1e2f6ff5a58b4010e54b5c8d18a6394dd6ed715800fcc7fc47436345294e6eb791cf585bee38ab6079559d3a40802f9802')
return
else:
raise RuntimeError('No signature file found in QT4AHelper.apk')


if __name__ == '__main__':
unittest.main()

0 comments on commit 5e9975b

Please sign in to comment.