2014年7月9日

UI Test Script for Android - 以Puzzle And Dragons自動轉珠為範例 (中)

Script Language

既然是要寫個auto test script, 當然一樣需要有一個script language, 這邊有兩個選擇, 一是你可以選擇自己寫一個, 或是直接使用現有的語言來使用, 以現在都在講求速率的年代來說, 我們當然不會選擇前者了, 而使用現有的script language, 到底要使用什麼語言比較好咧? 比較常見的腳本語言像是Ruby, Lua, Python, 優缺點的比較有興趣的人可以看看這篇文章: Python、Lua和Ruby——脚本大P.K. , 在這裡我選擇使用了遊戲界常用的輕量化及高效能的Lua來使用。


不過單純使用Lua的話, 一定沒有辦法完全符合我們的要求, 我們需要在Lua中建立新的API讓使用者可以透過它來達到與手機端溝通的功能,例如Touch, Drag, StartActivity …等各種指令, 所以我使用LuaJ這個library, 它可以很輕鬆的在Lua中新增一些API供使用者來呼叫並執行我們想要的功能, 有關LuaJ的設定在此就不多說, 只簡單講解一下在Java中新增API的方法, 假設你要在Lua中建立一個function touch(x, y):
    public class Manager extends TwoArgFunction {
        @Override
        public LuaValue call(LuaValue modname, LuaValue env) {
            LuaValue library = tableOf();
            library.set("Touch", new Touch());
            env.set("Manager", library);
        }

        public static class Touch extends TwoArgFunction {
            @Override
            public LuaValue call(LuaValue lv1, LuaValue lv2) {
                int x = lv1.checkint();
                int y = lv2.checkint();
                // send touch event to device via IChimpDevice
                System.out.println("Send touch event to device");
                boolean ret = ActionExecutor.execute(new TouchAction(x, y));
                return (ret)? LuaBoolean.TRUE:LuaBoolean.FALSE;
            }
        }
    }
首先先建立一個class Manager, 用來管理各個新API, 在Manager中的call裡, 我們將新API-Touch註冊進library中, 並且設進Manager這一個變數,接著實作一個Touch class, TwoArgFunction為LuaJ的class, 代表你這一個Touch需要有兩個參數lv1及lv2, 我們透過.checkint()將他轉換回java中的integer, 並且將Touch event送到手機端。
完成class Manager的實作之後, 我們就可以嘗試著在Lua中呼叫實作完成的function Touch, 程式碼如下:
    require 'com.around35.lua.Manager'
    Manager.Touch(100, 100)
接著試著從java project中讀取上面的檔案, 就可以發現我們成功呼叫了Touch function了:
    // Run lua script from java
    public void runLuaScript(File file) {
        if ( file != null && file.exists() ) {
            Globals globals = JsePlatform.standardGlobals();
            LuaValue chunk = globals.loadfile(file.getAbsolutePath());
            try {
                chunk.call();
            } catch(Exception e) {
                System.out.println(e.toString());
            }
        }
    }
之後我們想要新增API時, 只要建立新的class並且將它加進Manager中的library, 就可以在Lua中使用了。
以下是後來新增的API:
        library.set("Touch", new Touch());
        library.set("Wait", new Wait());
        library.set("Sleep", new Sleep());
        library.set("Click", new PressImage());
        library.set("Drag", new PathDrag());
        library.set("Type", new Type());
        library.set("PressSysKey", new PressSysKey());
        library.set("Find", new ObjectFinder());
        library.set("FindAll", new ObjectsFinder());
        library.set("FindImage", new FindImage());
        library.set("StartActivity", new StartActivity());
        library.set("Capture", new SaveImage());
        library.set("Shell", new Shell());
        library.set("AdbPull", new AdbPull());
        library.set("AdbPush", new AdbPush());
至於API內部的實作方法, 除了需要圖形辨識外的API, 都可以從IChimpDevice的interface中得知如何實作, 在這邊就不多做介紹了。

圖形辨識

在上一段落所列出來的API中, 讓我們在這一個段落為大家講解一下FindImage/Click這些API的實作吧!
如果我們想要點擊下面這一個dialog中的ok button, 我們要怎麼做呢?
confirm mail/friend requset
ok button
有學過影像處理的人應該都很熟悉OpenCV這一個影像處理函式庫, 沒錯!要在一個圖像中尋找裡面的另一個sub-image,我們可以使用OpenCV中所提供的template matching, 詳細底層運作方法可以參考網址內介紹, 講解的相當完整, 下列為實作的source code:
    public boolean findImage(opencv_core.IplImage src, opencv_core.IplImage template) {
        opencv_core.IplImage result = matchTemplate(src, template);
        double[] maxVal = new double[1];
        opencv_core.CvPoint maxLoc = new opencv_core.CvPoint();
        cvMinMaxLoc(result, null, maxVal, null, maxLoc, null);
        cvReleaseImage(result);
        return maxVal[0] >= THRESHOLD;
    }
    public static opencv_core.IplImage matchTemplate(opencv_core.IplImage src, opencv_core.IplImage tmp) {
        opencv_core.IplImage result = cvCreateImage(
            cvSize(src.width()-tmp.width()+1, src.height()-tmp.height()+1), IPL_DEPTH_32F, 1);
        cvMatchTemplate(src, tmp, result, CV_TM_CCORR_NORMED);
        return result;
    }
透過OpenCV的matchTemplate, 我們就可以取到ok button在src image裡最匹配的位置, 然後我們判斷相似度如果大於等於THRESHOLD,那麼我們就可以視為找到了ok button。再配合上一章節開放的FindImage, 我們就可以在Lua Script中判斷ok button是否存在畫面上, 並且針對它做點擊或其他動作。
Java part:
    public static class FindImage extends OneArgFunction {

        @Override
        public LuaValue call(LuaValue arg1) {
            LuaString imageId = arg1.checkstring();

            IplImage screen = OpenCVUtils.createFromBufferedImage(
                    JavaMonkey.getInstance().takeSnapshot().getBufferedImage());
            IplImage template = cvLoadImage(CommonUtils.getFileName(imageId.toString()));
            boolean ret = OpenCVUtils.findImage(screen, template);
            screen.release();
            cvReleaseImage(template);
            return (ret)? LuaBoolean.TRUE:LuaBoolean.FALSE;
        }

    }
Lua part:
    require 'com.around35.lua.Manager'
    if Manager.FindImage('ok_button') then
        print('find ok button')
    end
如果我們想要點擊找到的ok button要怎麼做呢, 眼尖的人會發現在cvMinMaxLoc中有一個maxLoc參數, 是的, 在呼叫cvMinMaxLoc之後我們就可以取得最相似圖片的Position(x, y), 所以我們就可以利用這個位置來做點擊按鈕的動作了。
Java part:
    IplImage screen = OpenCVUtils.createFromBufferedImage(JavaMonkey.getInstance().takeSnapshot().getBufferedImage());
    IplImage template = cvLoadImage(CommonUtils.getFileName(imageId.toString()));
    int offsetX = template.width() / 2;
    int offsetY = template.height() / 2;

    double[] maxVal = new double[1];
    CvPoint maxLoc = new CvPoint();
    IplImage result = OpenCVUtils.matchTemplate(screen, template);
    cvMinMaxLoc(result, null, maxVal, null, maxLoc, null);
    if ( maxVal[0] >= THRESHOLD ) {
        new TouchEvent(maxLoc.x() + offsetX, maxLoc.y() + offsetY).execute(device);
    }
Lua part:
    require 'com.around35.lua.Manager'
    Manager.Click('ok_button')
完成這個段落後, 我們就可以透過一些基本指令來撰寫簡單的auto test程式了, 下面為一個簡單的範例-進到龍族拼圖中並觀看第一封信:
    require 'com.around35.lua.Manager'
    function StartPAD() 
        Shell('am start -n jp.gungho.padHT/jp.gungho.padHT.AppDelegate')
        Sleep(15.0)
    end
    function PressOk() 
        -- wait ok button for 50 seconds
        Wait('ok', 50)
        -- find all ok and press all of them
        for i=0, 10 do
            if FindImage('ok') then
                Click('ok')
                Sleep(5.0)
            else
                break
            end
        end
    end

    StartPAD()
    -- wait the title 'puzzle & dragons' appears for 20 seconds
    Wait('puzzle', 20)
    Sleep(2.0)
    -- click the screen
    Touch(312,462)
    Sleep(5.0)
    PressOk()
    if FindImage('mail') then
        Click('read_mail')
    end
不過大家看到這邊一定有個疑問, 每台手機的解析度不是都不一樣嗎, 那同樣的script如果跑在不一樣的手機上面能不能使用呢? 這個答案是Yes也是No, Script本身當然不需要做修改, 但是圖片辨識的部分如果是利用template matching的話, 受限於他的實作方法, 所以只能在同樣解析度的手機上辨識成功, 大家一定都會想說: 這樣不是很麻煩嗎, 你總不可能每一種解析度都再去抓一次圖吧。對於這個問題, 其實是有解法的, 這一個方法就是SIFT(Scale-invariant feature transform), SIFT可以將圖片中的區域特徵點計算出來, 並且透過這些特徵點來判斷sub-image是否存在於原始圖像中, 即使是旋轉或是縮放過的圖像也可以比對出來, 是個相當強大的演算法, 有興趣的人可以研究看看囉。
待續

沒有留言:

張貼留言