레이블이 adbutils인 게시물을 표시합니다. 모든 게시물 표시
레이블이 adbutils인 게시물을 표시합니다. 모든 게시물 표시

2022년 6월 5일 일요일

android monkey test with python adbutils(파이썬을 이용한 몽키 테스트)

Monkey Test

Monkey는 애플리케이션을 반복 랜덤 방식으로 스트레스 테스트할 수 있는 도구 입니다.

이번에는 python을 이용해서 monkey test를 수행하고 에러가 발생하면 로그를 획득하는 코드입니다. 90% 이상은 이전에 작성한 코드를 재사용 하였습니다.


도움말 링크

https://developer.android.com/studio/test/monkey?hl=ko


기본 구문

$ adb shell monkey [options] <event-count>

예제

$ adb shell monkey -p your.package.name -v 500

명령어 옵션

카테고리옵션설명
일반--help간단한 사용 가이드를 인쇄합니다.
-v명령줄의 각 -v는 상세 레벨을 증가합니다. 레벨 0(기본값)에서는 시작 알림, 테스트 완료, 최종 결과 이외의 정보를 거의 제공하지 않습니다. 레벨 1에서는 실행되는 테스트에 관한 세부정보를 제공합니다(예: 활동에 전송되는 개별 이벤트). 레벨 2에서는 테스트에 선택되거나 선택되지 않은 활동과 같은 더 자세한 설정 정보를 제공합니다.
이벤트-s <seed>의사 랜덤 숫자 생성기의 시드값입니다. 같은 시드값으로 Monkey를 다시 실행하면 동일한 이벤트 시퀀스가 생성됩니다.
--throttle <milliseconds>이벤트 사이에 고정 지연을 삽입합니다. 이 옵션을 사용하여 Monkey의 속도를 늦출 수 있습니다. 지정하지 않으면 지체 없이 최대한 빠르게 이벤트가 생성됩니다.
--pct-touch <percent>터치 이벤트 비율을 조정합니다 (터치 이벤트는 화면의 단일 위치에서의 다운/업 이벤트입니다).
--pct-motion <percent>모션 이벤트 비율을 조정합니다 (모션 이벤트는 화면 어딘가의 다운 이벤트와 일련의 의사 랜덤 이동, 업 이벤트로 구성됩니다).
--pct-trackball <percent>트랙볼 이벤트 비율을 조정합니다 (트랙볼 이벤트는 하나 이상의 랜덤 이동으로 구성되며 때로는 클릭이 뒤따릅니다).
--pct-nav <percent>'기본' 탐색 이벤트 비율을 조정합니다 (탐색 이벤트는 방향 입력 기기의 입력처럼 위/아래/왼쪽/오른쪽으로 구성됩니다).
--pct-majornav <percent>'주요' 탐색 이벤트 비율을 조정합니다 (5방향 패드의 가운데 버튼 또는 메뉴 키와 같이 일반적으로 UI 내에 작업을 발생시키는 탐색 이벤트입니다).
--pct-syskeys <percent>'시스템' 키 이벤트 비율을 조정합니다 (이러한 키는 홈, 돌아가기, 통화 시작, 통화 종료, 볼륨 조절과 같이 일반적으로 시스템에서 사용하도록 예약됩니다).
--pct-appswitch <percent>활동 실행 비율을 조정합니다. 랜덤 간격으로 Monkey는 startActivity() 호출을 실행하며 이는 패키지 내의 모든 활동의 적용 범위를 최대화하는 방법입니다.
--pct-anyevent <percent>다른 유형의 이벤트 비율을 조정합니다. 이것은 다른 모든 유형의 이벤트를 포괄합니다(예: 키 누르기, 기기에서 덜 사용되는 다른 버튼 등).
제약 조건-p <allowed-package-name>이런 식으로 하나 이상의 패키지를 지정하면 Monkey는 시스템이 지정된 패키지 내의 활동 방문하도록 허용합니다. 애플리케이션이 다른 패키지(예: 연락처 선택)의 활동에 액세스해야 한다면 그 패키지도 지정해야 합니다. 어떤 패키지도 지정하지 않으면 Monkey는 시스템이 모든 패키지에서 활동을 시작하도록 허용합니다. 여러 패키지를 지정하려면 -p 옵션을 여러 번 사용하세요(패키지당 -p 옵션 하나).
-c <main-category>이 방법으로 하나 이상의 카테고리를 지정하면 Monkey는 시스템이 지정된 카테고리 중 하나와 함께 나열된 활동 방문하도록 허용합니다. 어떤 카테고리도 지정하지 않으면 Monkey는 Intent.CATEGORY_LAUNCHER 또는 Intent.CATEGORY_MONKEY 카테고리와 함께 나열된 활동을 선택합니다. 여러 카테고리를 지정하려면 -c 옵션을 여러 번 사용하세요(카테고리당 -c 옵션 하나).
디버깅--dbg-no-events지정된 경우 Monkey는 테스트 활동으로 초기 실행을 이행하지만 더 이상의 이벤트를 생성하지는 않습니다. 최상의 결과를 얻으려면 -v와 하나 이상의 패키지 제약 조건, 0이 아닌 스로틀을 결합하여 Monkey가 30초 이상 계속 실행되도록 합니다. 이는 애플리케이션에서 호출한 패키지 전환을 모니터링할 수 있는 환경을 제공합니다.
--hprof설정된 경우 이 옵션은 Monkey 이벤트 시퀀스 직전 및 직후에 프로파일링 보고서를 생성합니다. 이 경우 데이터/misc에 대용량(~5Mb) 파일이 생성되므로 주의해서 사용하세요. 프로파일링 보고서 분석에 관한 자세한 내용은 앱 성능 프로파일링을 참조하세요.
--ignore-crashes일반적으로 Monkey는 애플리케이션이 비정상 종료되거나 처리되지 않은 예외 유형이 발생하면 중지됩니다. 이 옵션을 지정하면 Monkey는 계산이 완료될 때까지 시스템에 이벤트를 계속 전송합니다.
--ignore-timeouts일반적으로 Monkey는 애플리케이션에 모든 유형의 시간 제한 오류가 발생하면 중지됩니다(예: '애플리케이션 응답 없음' 대화상자). 이 옵션을 지정하면 Monkey는 계산이 완료될 때까지 시스템에 이벤트를 계속 전송합니다.
--ignore-security-exceptions일반적으로 Monkey는 애플리케이션에 모든 유형의 권한 오류가 발생하면 중지됩니다(예: 특정 권한이 필요한 활동을 시작하려고 하는 경우). 이 옵션을 지정하면 Monkey는 계산이 완료될 때까지 시스템에 이벤트를 계속 전송합니다.
--kill-process-after-error일반적으로 Monkey가 오류로 인해 중지되면 실패한 애플리케이션은 계속 실행됩니다. 이 옵션을 설정하면 시스템에 오류가 발생한 프로세스를 중지하라는 신호를 보냅니다. 참고: 정상적(성공적)으로 완료가 되면 실행된 프로세스는 중지되지 않으며 기기는 최종 이벤트 후 마지막 상태로 유지됩니다.
--monitor-native-crashesAndroid 시스템 네이티브 코드에서 발생하는 비정상 종료를 감시하고 보고합니다. --kill-process-after-error를 설정하면 시스템은 중지됩니다.
--wait-dbg디버거가 연결될 때까지 Monkey 실행을 중지합니다.


!!주의!!

이벤트 갯수는 제일 마지막에 넣어야 합니다. 

틀린 예제
./adb shell monkey -p your.company.name -v 50 --throttle 1000

맞는 예제
./adb shell monkey -p your.company.name --throttle 1000 -v 50


소스 코드

from adbutils import adb
import re

class adb_utils_rp():
	def adb_connect(self, serial=None):
		self.d = adb.device(serial=serial)
		if self.d.serial==None :
			return None
		print("adb conneced",self.d.serial)
		return self.d

	def get_prop(self, keyname=None):
		ret_dict = {}
		# 전체 poperty 는 getprop 명령으로 획득 가능합니다.
		ret = self.d.shell("getprop")
		#print(ret)
		retlines = ret.replace("]\n","]\0")
		retlines = retlines.split("\0")
		
		for one_line in retlines:
			one_line = one_line.replace("]: [","]"+"\0"+"[")
			dat = one_line.split("\0")
			#print(dat)
			dat1 = dat[1].strip()
			ret_dict[dat[0][1:len(dat[0])-1]] = dat1[1:len(dat1)-1]
			#print(dat[0][1:len(dat[0])-1],ret_dict[dat[0][1:len(dat[0])-1]])
		
		if keyname==None:
			return ret_dict
		
		return ret_dict.get(keyname)

	def make_bugreportz(self, log_zip_filename="log.zip"):
		# bugreportz 는 /data/user_de/0/com.android.shell/files/bugreports/ 에 로그를 생성하며 생성시 이전로그가 삭제 됩니다.
		#            리턴값은 OK:로 시작하며 뒤에는 경로명이 넘어 옵니다.
		#            예) OK:/data/user_de/0/com.android.shell/files/bugreports/dumpstate-2022-05-14-17-33-03.zip
		#
		# 동작중 다시 호출하면 아래와 같은 string이 리턴됩니다.
		# Previous sys dump or full dump is running, so skip this one

		ret = self.d.shell("bugreportz")
		print(ret)

		isok = ret.split(":")
		if len(isok)!=2 or isok[0]!='OK':
			return -1
		
		# 로그 뜬뒤 로그 버퍼를 초기화한다.
		self.d.shell("logcat -b all -c")
		
		# return 은 int size가 넘어옵니다.
		# 파일이 없으면 exception 발생합니다.
		#    adbutils.errors.AdbError: open failed: No such file or directory
		ret=self.d.sync.pull(isok[1], log_zip_filename)
		if ret==0:
			return -2
		
		# int size 가 리턴됩니다.
		return ret
		
	#EVENT LOG (logcat -b events -v threadtime -v printable -v uid -d *:v)
	def get_event_log(self):
		ret = self.d.shell("logcat -b events -v threadtime -v printable -v uid -d *:v")
		#print(ret)
		return ret
		
	# @return
	# None : 오류 없음
	# else : 오류 있음
	def check_event_log(self):
		find_str = "(am_crash)|(am_anr)"
		ret = self.get_event_log()
		#ret = " \n\n\n\n am_cras \n  am_anr "
		finded = re.search(find_str,ret)
		return finded
		
	def monkey(self,package,count,seed=0,throttle=300,extra=""):
		return self.d.shell(f"monkey -p {package} -s {seed} --throttle {throttle} {extra} -v {count}")
		
if __name__ == "__main__":
	adbrp = adb_utils_rp()
	adbrp.adb_connect()
	print(adbrp.get_prop("ro.serialno"))
	print(adbrp.get_prop("ro.build.fingerprint"))
	#print(adbrp.make_bugreportz())
	
	# test가 종료될때까지 기다리게 됩니다.
	print(adbrp.monkey("com.android.chrome",500))
	
	#print(adbrp.get_event_log())
	#print(adbrp.check_event_log())
	if adbrp.check_event_log()!=None:
		adbrp.make_bugreportz()


예제 설명

기존 예제 재활용입니다. monkey를 실행시키고 앱이 중지했거나 anr이 발생하면 dump를 생성하도록 만들었습니다.

앱에서 에러가 발생했는지는 event log를 통해 확인합니다. monkey의 리턴값을 이용해도 될듯 한데 아직 crash 되었을때 monkey의 리턴값이 어떻게 넘어오는지 확인을 하지 못한 상태입니다.

monkey test에서 중요한 인자는 다음과 같습니다.

-p 테스트 하고자 하는 특정 패키지를 실행시킵니다.

--throttle 인자는 테스트 하는 앱으로 키를 보내게 되는데 ms 단위로 보내게됩니다. 즉 너무 짧으면 순식간에 휙 지나가 버리고 테스트도 제대로 안되는 경우가 많습니다. 그래서 시간을 넉넉히 여유를 주도록 합니다.

adbrp.monkey("com.android.chrome",500)

여기에서는 크롬앱을 500개 이벤트를 보내서 테스트 하도록 합니다. 이벤트라는것은 키를 누르거나 스크롤을 하거나 특정 좌표를 클릭하는것도 하나의 이벤트가 됩니다.

기본적으로 throttle은 300ms으로 해두었습니다. 너무 짧으면 순식간에 테스트가 끝나버리고 동작도 제대로 안되는 경우가 많습니다.




2022년 5월 15일 일요일

python adb adbutils 로그 획득하기 bugreportz 사용법

 python으로 adb를 이용하여 자동화 test를 구현하고 있습니다.

adbutils 구현중에 로그 획득하는 부분이 없어서 adbutils 을 래핑해서 구현해봤습니다.


1. property 구해서 dict type에 리턴받기

이번 시간에 구현할 함수는 프로퍼티 얻는 함수 입니다. 이미 adbutils 에 있긴하지만 전체 목록을 얻어서 dict type으로 저장하는 함수를 구현하였습니다.

1.1 adbuilts로 property 가져오기


ret = self.d.shell("getprop")


1.2 dict type으로 저장하기

획득한 property 형태는 [xxxx]: [yyyy] 형태로 되어있습니다. 

그런데 이것이 yyyy에 개행이 있는 경우도 있고 내부에 : , [ , ] 등의 문자가 포함되어 있을 수도 있습니다. 그래서 이부분을 정규식이나 단순히 [ ] 검색해서 뽑아내기가 복잡합니다.

여기에서는 약간의 편법을 사용하였는데 replace("]\n","]\0") 이용하여 하나의 property에 대해서 \0으로 치환하는 방법을 사용하였습니다. ([xxxx1]: [yyyy1]\n [xxxx2]: [yyyy2] 를 [xxxx1]: [yyyy1]\0 [xxxx2]: [yyyy2] 으로 치환함)

또한 하나의 property 안에서는 replace("]: [","]"+"\0"+"[") 두개의 값을 \0로 치환하였습니다. ([xxxx1]: [yyyy1] => [xxxx1]\0[yyyy1] 치환함)

이것을 구현한 내용입니다.


	def get_prop(self, keyname=None):
		ret_dict = {}
		# 전체 poperty 는 getprop 명령으로 획득 가능합니다.
		ret = self.d.shell("getprop")
		#print(ret)
		retlines = ret.replace("]\n","]\0")
		retlines = retlines.split("\0")
		
		for one_line in retlines:
			one_line = one_line.replace("]: [","]"+"\0"+"[")
			dat = one_line.split("\0")
			#print(dat)
			dat1 = dat[1].strip()
			ret_dict[dat[0][1:len(dat[0])-1]] = dat1[1:len(dat1)-1]
			#print(dat[0][1:len(dat[0])-1],ret_dict[dat[0][1:len(dat[0])-1]])
		
		if keyname==None:
			return ret_dict
		
		return ret_dict.get(keyname)


2. event log

android 단말기에서 오류가 발생하였다는것은 event log로 쉽게 알 수 있습니다. event log는 로그가 많이 로깅되지는 않기 때문에 비교적 오랜 시간 저장되어 있음을 알 수 있습니다.

먼저 event log를 획득하는 방법입니다.

2.1 event log를 획득하기


	#EVENT LOG (logcat -b events -v threadtime -v printable -v uid -d *:v)
	def get_event_log(self):
		ret = self.d.shell("logcat -b events -v threadtime -v printable -v uid -d *:v")
		#print(ret)
		return ret


2.1 event log에서 오류가 발생했는지 확인 하는 방법

android 단말에서는 두가지 형태의 오류가 있습니다. 첫번째는 단말 기본 동작 오류 일명 앱 강제 종료라고 불리는 Forced Close 입니다. 이런 동작은 메소드를 실행하지 못할때 일반적으로 발생합니다. 이때 이벤트 로그에서는 am_crash 로그가 찍히게 됩니다.

두번째로는 앱이 응답 없는 경우입니다. 앱들은 여러 프로세서와 통신을 하기 때문에 한쪽 시스템이 늦어지면 이렇현상이 빠질 수 있는데 이때 ANR이라고 불리는 am_anr 이 이벤트 로그에 찍힙니다.

그래서 이것을 획득한 event log에서 정규식을 이용해서 찾는 방법을 알아보겠습니다.


	# @return
	# None : 오류 없음
	# else : 오류 있음
	def check_event_log(self):
		find_str = "(am_crash)|(am_anr)"
		ret = self.get_event_log()
		ret = " \n\n\n\n am_cras \n  am_anr "
		finded = re.search(find_str,ret)
		return finded


3. bugreportz 

예전 adb에서는 adb bugreport가 동작했겠지만, 지금은 bugreportz (압축형태) 로 대체되었습니다. 그런데 adb에서는 abd bugreportz 라고 실행하면 되지만, adbutils 에서는 해당 커멘드를 지원하지 않습니다. 따라서 shell 에서 실행해야하는데 즉, adb shell bugreportz 라고 실행하면 됩니다. 

3.1 bugreportz 특징

bugreportz 는 /data/user_de/0/com.android.shell/files/bugreports/ 에 로그를 생성하며 생성시 이전 로그가 삭제 됩니다. (로그가 쌓이는것으로 걱정하지 않아도 됩니다.)

리턴값은 OK:로 시작하며 뒤에는 경로명이 넘어 옵니다.

예) OK:/data/user_de/0/com.android.shell/files/bugreports/dumpstate-2022-05-14-17-33-03.zip

동작중 다시 호출하면 아래와 같은 오류가 발생합니다.

Previous sys dump or full dump is running, so skip this one

로그가 생성된 다음 adb pull 로 해당 로그를 가져 오면 됩니다. 아래 함수는 로그 생성뒤 특정 파일이름으로 로그를 가져오는 소스입니다.

	def make_bugreportz(self, log_zip_filename="log.zip"):
		# bugreportz 는 /data/user_de/0/com.android.shell/files/bugreports/ 에 로그를 생성하며 생성시 이전로그가 삭제 됩니다.
		#            리턴값은 OK:로 시작하며 뒤에는 경로명이 넘어 옵니다.
		#            예) OK:/data/user_de/0/com.android.shell/files/bugreports/dumpstate-2022-05-14-17-33-03.zip
		#
		# 동작중 다시 호출하면 아래와 같은 string이 리턴됩니다.
		# Previous sys dump or full dump is running, so skip this one

		ret = self.d.shell("bugreportz")
		print(ret)

		isok = ret.split(":")
		if len(isok)!=2 or isok[0]!='OK':
			return -1
		
		# return 은 int size가 넘어옵니다.
		# 파일이 없으면 exception 발생합니다.
		#    adbutils.errors.AdbError: open failed: No such file or directory
		ret=self.d.sync.pull(isok[1], log_zip_filename)
		if ret==0:
			return -2

		# int size 가 리턴됩니다.
		return ret


4. 전체 소스

지금까지 내용을 소스로 만들어 봤습니다.

make_bugreportz 함수는 직접호출해도 되긴하지만 event log에 뭔가 이상한점이 발견되면 호출하도록 만들어 봤습니다.


from adbutils import adb
import re

class adb_utils_rp():
	def adb_connect(self, serial=None):
		self.d = adb.device(serial=serial)
		if self.d.serial==None :
			return None
		print("adb conneced",self.d.serial)
		return self.d

	def get_prop(self, keyname=None):
		ret_dict = {}
		# 전체 poperty 는 getprop 명령으로 획득 가능합니다.
		ret = self.d.shell("getprop")
		#print(ret)
		retlines = ret.replace("]\n","]\0")
		retlines = retlines.split("\0")
		
		for one_line in retlines:
			one_line = one_line.replace("]: [","]"+"\0"+"[")
			dat = one_line.split("\0")
			#print(dat)
			dat1 = dat[1].strip()
			ret_dict[dat[0][1:len(dat[0])-1]] = dat1[1:len(dat1)-1]
			#print(dat[0][1:len(dat[0])-1],ret_dict[dat[0][1:len(dat[0])-1]])
		
		if keyname==None:
			return ret_dict
		
		return ret_dict.get(keyname)

	def make_bugreportz(self, log_zip_filename="log.zip"):
		# bugreportz 는 /data/user_de/0/com.android.shell/files/bugreports/ 에 로그를 생성하며 생성시 이전로그가 삭제 됩니다.
		#            리턴값은 OK:로 시작하며 뒤에는 경로명이 넘어 옵니다.
		#            예) OK:/data/user_de/0/com.android.shell/files/bugreports/dumpstate-2022-05-14-17-33-03.zip
		#
		# 동작중 다시 호출하면 아래와 같은 string이 리턴됩니다.
		# Previous sys dump or full dump is running, so skip this one

		ret = self.d.shell("bugreportz")
		print(ret)

		isok = ret.split(":")
		if len(isok)!=2 or isok[0]!='OK':
			return -1
		
		# return 은 int size가 넘어옵니다.
		# 파일이 없으면 exception 발생합니다.
		#    adbutils.errors.AdbError: open failed: No such file or directory
		ret=self.d.sync.pull(isok[1], log_zip_filename)
		if ret==0:
			return -2

		# int size 가 리턴됩니다.
		return ret
		
	#EVENT LOG (logcat -b events -v threadtime -v printable -v uid -d *:v)
	def get_event_log(self):
		ret = self.d.shell("logcat -b events -v threadtime -v printable -v uid -d *:v")
		#print(ret)
		return ret
		
	# @return
	# None : 오류 없음
	# else : 오류 있음
	def check_event_log(self):
		find_str = "(am_crash)|(am_anr)"
		ret = self.get_event_log()
		finded = re.search(find_str,ret)
		return finded
		
if __name__ == "__main__":
	adbrp = adb_utils_rp()
	adbrp.adb_connect()
	print(adbrp.get_prop("ro.serialno"))
	print(adbrp.get_prop("ro.build.fingerprint"))
	#print(adbrp.make_bugreportz())
	print(adbrp.get_event_log())
	print(adbrp.check_event_log())
	if adbrp.check_event_log()!=None:
		adbrp.make_bugreportz()