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()