앞선 1편에서는 Monkey Test의 기본 사용법에 대하여 알아보았다.
2편에서는 랜덤 event를 발생시키던 Monkey Test를 Script를 사용하여 원하는 시나리오로 실행하는 방법을 알아보자!
공식 developer 사이트에는 아이러니하게(?) script로 test 하는 법이 나와있지 않다
(내가 못 찾는 거 일 수도 있지만)
그래서 구글 코드를 읽어보면서 작성해보고자 한다 😊
adb 명령어
adb shell monkey -f <fileName> 1
else if (opt.equals("-f")) {
mScriptFileNames.add(nextOptionData());
}
Monkey.java에서 moneky command를 읽는 부분인데 -f 옵션을 사용하면 script file을 지정할 수 있었다.
MonkeySourceScript.java 는 Script 파일을 parsing하는 부분이고, Monkey는 Monkey test를 위한 command에 대한 동작을 지정하는 것을 볼 수 있다.
* 마지막 숫자 1은 반복 횟수이다
Script Format
/**
* monkey event queue. It takes a script to produce events sample script format:
*
* <pre>
* type= raw events
* count= 10
* speed= 1.0
* start data >>
* captureDispatchPointer(5109520,5109520,0,230.75429,458.1814,0.20784314,0.06666667,0,0.0,0.0,65539,0)
* captureDispatchKey(5113146,5113146,0,20,0,0,0,0)
* captureDispatchFlip(true)
* ...
* </pre>
*/
일단 주석으로 sample format이 적혀있다.
저런 형식으로 시나리오나 옵션을 정한 다는 것을 알 수 있었다... 각 내용을 어떻게 파싱하는지 알아보자
Monkey 실행부
private MonkeyEventQueue mQ;
/**
* Gets the next event to be injected from the script. If the event queue is
* empty, reads the next n events from the script into the queue, where n is
* the lesser of the number of remaining events and the value specified by
* MAX_ONE_TIME_READS. If the end of the file is reached, no events are
* added to the queue and null is returned.
*
* @return The first event in the event queue or null if the end of the file
* is reached or if an error is encountered reading the file.
*/
@Override
public MonkeyEvent getNextEvent() {
long recordedEventTime = -1;
MonkeyEvent ev;
if (mQ.isEmpty()) {
try {
readNextBatch();
} catch (IOException e) {
return null;
}
}
try {
ev = mQ.getFirst();
mQ.removeFirst();
} catch (NoSuchElementException e) {
return null;
}
if (ev.getEventType() == MonkeyEvent.EVENT_TYPE_KEY) {
adjustKeyEventTime((MonkeyKeyEvent) ev);
} else if (ev.getEventType() == MonkeyEvent.EVENT_TYPE_TOUCH
|| ev.getEventType() == MonkeyEvent.EVENT_TYPE_TRACKBALL) {
adjustMotionEventTime((MonkeyMotionEvent) ev);
}
return ev;
}
Monkey script에서 event를 읽어오는 부분이다.
mQ는 eventQueue이며 queue가 비어있으면 다음 라인을 읽어오게 되어있다.
Header Parsing
/**
* Read next batch of events from the script file into the event queue.
* Checks if the script is open and then reads the next MAX_ONE_TIME_READS
* events or reads until the end of the file. If no events are read, then
* the script is closed.
*
* @throws IOException If there was an error reading the file.
*/
private void readNextBatch() throws IOException {
int linesRead = 0;
if (THIS_DEBUG) {
Logger.out.println("readNextBatch(): reading next batch of events");
}
if (!mFileOpened) {
resetValue();
readHeader();
}
if (mReadScriptLineByLine) {
linesRead = readOneLine();
} else {
linesRead = readLines();
}
if (linesRead == 0) {
closeFile();
}
}
eventQueue가 비어있으면 script file에서 읽어오는데, file이 open되어 있으면 각 변수들을 reset 시키고 header를 parsing해서 셋팅한다. 그 이후에 line을 읽어온다.
아래에서 보겠지만 여기서 header는 sample format에서 "start data >>" 까지 해당하고, 이후 line은 실제 event를 지정하는 것이라 readLine으로 읽는다.
* mReadScriptLineByLine은 1줄씩 읽게 하는 것이고 script에서는 "linebyline"을 입력하면 된다.
private static final String HEADER_COUNT = "count=";
private static final String HEADER_SPEED = "speed=";
// if this header is present, scripts are read and processed in line-by-line mode
private static final String HEADER_LINE_BY_LINE = "linebyline";
// a line at the end of the header
private static final String STARTING_DATA_LINE = "start data >>";
/**
* Reads the header of the script file.
*
* @return True if the file header could be parsed, and false otherwise.
* @throws IOException If there was an error reading the file.
*/
private boolean readHeader() throws IOException {
mFileOpened = true;
mFStream = new FileInputStream(mScriptFileName);
mInputStream = new DataInputStream(mFStream);
mBufferedReader = new BufferedReader(new InputStreamReader(mInputStream));
String line;
while ((line = mBufferedReader.readLine()) != null) {
line = line.trim();
if (line.indexOf(HEADER_COUNT) >= 0) {
try {
String value = line.substring(HEADER_COUNT.length() + 1).trim();
mEventCountInScript = Integer.parseInt(value);
} catch (NumberFormatException e) {
Logger.err.println("" + e);
return false;
}
} else if (line.indexOf(HEADER_SPEED) >= 0) {
try {
String value = line.substring(HEADER_COUNT.length() + 1).trim();
mSpeed = Double.parseDouble(value);
} catch (NumberFormatException e) {
Logger.err.println("" + e);
return false;
}
} else if (line.indexOf(HEADER_LINE_BY_LINE) >= 0) {
mReadScriptLineByLine = true;
} else if (line.indexOf(STARTING_DATA_LINE) >= 0) {
return true;
}
}
return false;
}
header parsing 하는 부분이다.
4가지의 header를 인식하는데 아래와 같다.
근데.. sample format에는 "type="으로 해서 입력하는게 있는데... 코드를 보면 해당 값을 읽는 부분이 없다. 업데이트가 안된건지... 다른 부분에 있는건지는 모르겠다 😅 일단 없어도 잘 작동할 것 같다(?)
변수명 | script | 내용 |
HEADER_COUNT | count= | 총 event 수 |
HEADER_SPEED | speed= | 이벤트간 딜레이 (적을 수록 딜레이가 짧음) |
HEADER_LINE_BY_LINE | linebyline | line을 1개씩 읽겠다 |
STARTING_DATA_LINE | start data >> | 여기서부터 event가 시작된다 |
Add Queue
/**
* Reads a number of lines and passes the lines to be processed.
*
* @return The number of lines read.
* @throws IOException If there was an error reading the file.
*/
private int readLines() throws IOException {
String line;
for (int i = 0; i < MAX_ONE_TIME_READS; i++) {
line = mBufferedReader.readLine();
if (line == null) {
return i;
}
line = line.trim();
processLine(line);
}
return MAX_ONE_TIME_READS;
}
/**
* Reads one line and processes it.
*
* @return the number of lines read
* @throws IOException If there was an error reading the file.
*/
private int readOneLine() throws IOException {
String line = mBufferedReader.readLine();
if (line == null) {
return 0;
}
line = line.trim();
processLine(line);
return 1;
}
/**
* Extracts an event and a list of arguments from a line. If the line does
* not match the format required, it is ignored.
*
* @param line A string in the form {@code cmd(arg1,arg2,arg3)}.
*/
private void processLine(String line) {
int index1 = line.indexOf('(');
int index2 = line.indexOf(')');
if (index1 < 0 || index2 < 0) {
return;
}
String[] args = line.substring(index1 + 1, index2).split(",");
for (int i = 0; i < args.length; i++) {
args[i] = args[i].trim();
}
handleEvent(line, args);
}
header parsing이 끝난 후 테스트 시나리오를 읽는다. OneLine은 말그대로 1줄만 읽고, Lines는 최대한 모든 라인을 다 읽는 듯하다.
* OneLine은 위에서 적었드시 Script Header에 "linebyline"을 작성하면 된다
line을 읽어와서 processLine함수를 실행하는데 이 함수에서 arguments를 parsing하고 handleEvent 함수를 사용해서 eventQueue에 add를 한다.
Event List
// event key word in the capture log
private static final String EVENT_KEYWORD_POINTER = "DispatchPointer";
private static final String EVENT_KEYWORD_TRACKBALL = "DispatchTrackball";
private static final String EVENT_KEYWORD_ROTATION = "RotateScreen";
private static final String EVENT_KEYWORD_KEY = "DispatchKey";
private static final String EVENT_KEYWORD_FLIP = "DispatchFlip";
private static final String EVENT_KEYWORD_KEYPRESS = "DispatchPress";
private static final String EVENT_KEYWORD_ACTIVITY = "LaunchActivity";
private static final String EVENT_KEYWORD_INSTRUMENTATION = "LaunchInstrumentation";
private static final String EVENT_KEYWORD_WAIT = "UserWait";
private static final String EVENT_KEYWORD_LONGPRESS = "LongPress";
private static final String EVENT_KEYWORD_POWERLOG = "PowerLog";
private static final String EVENT_KEYWORD_WRITEPOWERLOG = "WriteLog";
private static final String EVENT_KEYWORD_RUNCMD = "RunCmd";
private static final String EVENT_KEYWORD_TAP = "Tap";
private static final String EVENT_KEYWORD_PROFILE_WAIT = "ProfileWait";
private static final String EVENT_KEYWORD_DEVICE_WAKEUP = "DeviceWakeUp";
private static final String EVENT_KEYWORD_INPUT_STRING = "DispatchString";
private static final String EVENT_KEYWORD_PRESSANDHOLD = "PressAndHold";
private static final String EVENT_KEYWORD_DRAG = "Drag";
private static final String EVENT_KEYWORD_PINCH_ZOOM = "PinchZoom";
private static final String EVENT_KEYWORD_START_FRAMERATE_CAPTURE = "StartCaptureFramerate";
private static final String EVENT_KEYWORD_END_FRAMERATE_CAPTURE = "EndCaptureFramerate";
private static final String EVENT_KEYWORD_START_APP_FRAMERATE_CAPTURE =
"StartCaptureAppFramerate";
private static final String EVENT_KEYWORD_END_APP_FRAMERATE_CAPTURE = "EndCaptureAppFramerate";
private void handleEvent(String s, String[] args) {
// Handle screen rotation events
if ((s.indexOf(EVENT_KEYWORD_ROTATION) >= 0) && args.length == 2) {
try {
int rotationDegree = Integer.parseInt(args[0]);
int persist = Integer.parseInt(args[1]);
if ((rotationDegree == Surface.ROTATION_0) ||
(rotationDegree == Surface.ROTATION_90) ||
(rotationDegree == Surface.ROTATION_180) ||
(rotationDegree == Surface.ROTATION_270)) {
mQ.addLast(new MonkeyRotationEvent(rotationDegree,
persist != 0));
}
} catch (NumberFormatException e) {
}
return;
}
}
handleEvent는 event 종류만큼 MonkeyEvent 객체를 만드는 로직이 있어서 너무 길었다. 그래서 짧은거 1개만 가져와봤다. 위에서 parsing한 arguments를 가지고 MonkeyEvent 객체를 생성하여 Queue에 넣어주는 것을 알 수 있다.
상수로 정의된 event string이 바로 script의 event 발동 키워드가 되겠다.
* touch event는 pointer이다.
각 argument 별로 정리하고 싶은데.. 너무 많아서 간략하게만 정리 해보았다.
왼쪽부터 args[0]에 해당한다.
* 자세한 사항은 코드를 좀 더 참조해야할 것 같다.
DispatchKey
- downTime, eventTime, action, code, repeat, metaState, device, scancode
DispatchPointer/DispatchTrackball
- downTime, eventTime, action, x, y, pressure, size, metaState, xPrecision, yPrecision, device, edgeFlags, (pointerId)
* pointerId는 있어도 되고 없어도 된다.
RotateScreen
- rotationDegree, persist
Tap
- x, y, tapDuration
PressAndHold
- x, y, pressDuration
Drag
- xStart, yStart, xEnd, yEnd, stepCount
PinchZoom
- pt1xStart, pt1yStart, pt1xEnd, pt1yEnd, pt2xStart, pt2yStart, pt2xEnd, pt2yEnd, stepCount
DispatchFlip
- keyboardOpen
LaunchActivity
- pkg_name, cl_name
LaunchInstrumentation
- test_name, runner_name
UserWait
- sleeptime
DispatchPress
- key_name
PowerLog
- power_log_type
RunCmd
- cmd
DispatchString
- input
EndCaptureFramerate
- input
StartCaptureAppFramerate
- app
EndCaptureAppFramerate
- app, label
DeviceWakeUp, ProfileWait, LongPress, WriteLog,StartCaptureFramerate
- args 없음
Api 문서를 못 찾아서 어떤 식으로 구성되나 호기심으로 한번 찾아보았다.
Api 문서처럼 만들어보고 싶었는데... 조금 내용이 방대해서 실제 사용하게 된다면 코드를 좀 찾아봐야할 것 같다.
위 내용은 참고 정도로만 확인해보고 자세한 사용법은 필요한 요소의 코드를 더 읽어보면 좋을 것 같다.
'Android > 이것저것' 카테고리의 다른 글
[Android] 코드 스캔하기 with Lint (0) | 2023.04.09 |
---|---|
[Android] 터치 영역 넓히기 (0) | 2023.03.26 |
[Android] Monkey Test #1 (0) | 2023.02.26 |
[Android] 안드로이드에 MVVM 적용해보기 (DataBinding, LiveData, Koin) (0) | 2020.12.05 |
[Android] 안드로이드에 Animation 적용하기! (0) | 2020.11.09 |
댓글