8 lipca 2010

Automatyzacja obsługi GUI przy użyciu Sikuli

Coraz większe lenistwo, a czasem zwykły brak czasu sprawiają, że staramy się coraz więcej pracy wykonać przy pomocy jakichś narzędzi automatyzujących różne czynności. Różnego rodzaju skrypty, programy pomagają w wysyłaniu poczty, zarządzaniu plikami, czy obróbce zdjęć. Najczęściej narzędzia te są pewnego rodzaju wtyczkami, czy nakładkami na istniejące już aplikacje. Ostatnio spotkałem się jednak z nieco innym pomysłem na zautomatyzowanie nudnych zadań.
Projekt Sikuli jest idealnym sposobem na ułatwienie sobie życia.
Pomysł jest realizowany przez grupę zapaleńców z MIT (Massachusetts Institute of Technology). Prace programistyczne przyniosły w rezultacie dość wygodne IDE oraz ciekawe graficzne API zrealizowane przy pomocy Jythona. Na pierwszy rzut oka brzmi to dość poważnie, jednak z czystym sumieniem pozwolę sobie na stwierdzenie, że proste automaty może konstruować nawet laik. Całość sprowadza się w sumie do zaznaczania fragmentów obrazu wyświetlanego na monitorze komputera, na które ma reagować nasz skrypt i sposób reakcji, a to wszystko dostępne przez proste klikanie i zaznaczanie obszarów na ekranie przy pomocy myszy. Nie trzeba znać ani Pythona, ani żadnego innego języka programowania. Wystarczy trochę pomyśleć :)
Jak wygląda "wyklikany" kod? Tak jak poniżej

Co można zrobić korzystając z Sikuli? Najlepszym sposobem na zapoznanie się z możliwościami tego projektu, jest obejrzenie kilku krótkich filmików instruktażowych.
Dla zainteresowanych podrzucam jedno ze wspomnianych nagrań


Co więcej, jeżeli przyjdzie nam do głowy skorzystać z mechanizmu Sikuli we własnym programie, w przypadku języka Java prawie wprost dostajemy gotowe narzędzie. Przykładową klasę, która działa jako wykonawca skryptów przedstawiłem poniżej.
/**
* SikuliRunner.java
* in pl.com.uhc
* 2010-07-08 12:01:20
* 
*     This class creates PythonInterpreter and runs
*     Sikuli-python scripts
* 
*/

package pl.com.uhc;

import java.io.IOException;
import edu.mit.csail.uid.ScriptRunner;

/**
* 
* @author Kamil Michalak
*
*/
public class SikuliRunner {
    private String scriptPath = null;    // path to the Sikuli script

    /**
    * Public constructor
    */
        public SikuliRunner() {
        // do nothing
    }

    /**
    * Public constructor with parameters
    * @param scriptPath the script path
    */
    public SikuliRunner(String scriptPath) {
        this.scriptPath = scriptPath;
    }

    /* ******************************************
    * Setters and getters section
    ****************************************** */

    /**
    * @return the scriptPath
    */
    public String getScriptPath() {
        return scriptPath;
    }

    /**
    * @param scriptPath the scriptPath to set
    */
    public void setScriptPath(String scriptPath) {
        this.scriptPath = scriptPath;
    }


    /* ******************************************
    * Other public methods
    ****************************************** */

    /**
    * Runs script in internal script interpreter
    * @throws NullPathException 
    */
    public void runScript() {        
        /*
         * Script interpreter for Sikuli scripts
         * imported from package 
         * edu.mit.csail.uid.ScriptRunner in sikuli-script.jar
         */
        ScriptRunner runner = new ScriptRunner(null);     
        try {            
            runner.runPython(this.scriptPath); // absolute path to script file
        } catch (IOException e) {
            e.printStackTrace();            
        } catch (Exception e) {
            /*
             * This exception will be thrown if method can't find
             * a field to operate at (eg. can not find a button to click)
             */
            System.out.println("Execution aborted!");
            System.out.println("Proces stopped by exception:\n\t");
            e.printStackTrace();
        }
    }
}
Jak widać kod nie jest jakiś straszny. Dodatkowo do naszej zmiennej classpath musimy dołączyć dwa pliki jar: sikuli-script.jar i jython.jar. Tak przygotowani możemy już korzystać z gotowych metod
SikuliRunner sr = new SikuliRunner();
    sr.setScriptPath("skrypt.sikuli");
    sr.runScript();
Schematy postępowania można stworzyć przy wykorzystaniu z IDE Sikuli (muszę przyznać, że całkiem dobrze spełnia swoje zadanie). Jedyne co trochę razi w oczy, to nazwy plików PNG z screenami, ale w sumie nie ma chyba lepszego sposobu na ich automatyczne przydzielanie.
Prosty skrypt (może nie do końca taki prosty, ale napisany szybciorem gdzieś w kącie podczas pracy) dla Sikuli przedstawiam poniżej.
##
#    script.sikuli
#    Writen in Sikuli IDE
##

from random import choice
import string 
import random

# random string generator
def textGen():
    newpasswd = ''
    chars = string.ascii_letters + string.digits
    for i in range(12):
        newpasswd = newpasswd + choice(chars)
    return newpasswd

# click "Kontynuuj" when error dialog appears
def checkFlashError():
    if exists("1278483208812.png"):
        click("1278483208812.png")

# click "No" button in dialog
def checkUnsaved():
    if exists("1278486952937.png"):
        click("1278486968281.png")

#choosing option for "Service category"
def chooseOption():
    opt = random.randint(0, 4)
    if opt==0:
        click("1278491620203.png")
    if opt==1:
        click("1278491658265.png")
    if opt==2:
        click("1278492034656.png")
    if opt==3:
        click("1278492058062.png")
    if opt==4:
        click("1278492086859.png")


def saveOrDiscard():
    opt = random.randint(0, 1)
    if opt==0:
        click("1278416730625.png")
    if opt==1:
        click("1278496136906.png")

#----------------------------
# main 
#----------------------------
def testRun():
    i=0
    checkUnsaved()
    while not exists("1278485774781.png"):
        sleep(1)
        wait("1278411595609.png")
        hover("1278410703906.png")
        wait("1278409800890.png")
        click("1278411311421.png")
        checkFlashError()
        while not exists("1278411542484.png"):
            sleep(1)

    checkUnsaved()
    while not exists(Pattern("1278411542484.png").similar(0.80)):
        sleep(1)
        i = i+1
        if i>5:
            wait("1278411595609.png")
    hover("1278410703906.png")
    wait("1278409800890.png")
    click("1278411311421.png")

    hover("1278411542484.png")
    click(Pattern("1278411542484.png").similar(0.75))
    onAppear("1278412139593.png", click("1278486672359.png"))
    checkFlashError()
    checkUnsaved()
    click("1278491803406.png")
    onAppear(Pattern("1278417987296.png").similar(0.86), doubleClick("1278417987296.png"))
    type(" ")
    doubleClick("1278417987296.png")
    type(" ")
    type(textGen()+"\t")
    type(textGen()+"\t")
    type(textGen()+"\t")
    if exists("1278417243156.png"):
        click("1278417243156.png")
    chooseOption()
    #click("1278417376453.png")
    doubleClick("1278417500250.png")
    type(textGen())
    for x in findAll("1278493541484.png"):
        click(x)
    click("1278494028062.png")
    if exists("1278416683453.png"):
        click(getLastMatch())
    #saveOrDiscard()

if __name__ == '__main__':
    testRun()
W przypadku, kiedy nie podoba nam się tego typu podejście, mamy również możliwość pisania z wykorzystaniem czystej Javy, bez konieczności zaszywania własnego interpretera i podpinania do niego pythonowych skryptów. Sposób pisania takiego kodu został przedstawiony przez programistów na stronie projektu, na której możemy również znaleźć kompletną dokumentację (oczywiście w formacie JavaDoc).
Uważam, że takie narzędzie jest dość obiecującym pomysłem na realizację nie tyle zabawek dla leniwych użytkowników, co prawdziwych automatów do przeprowadzania testów aplikacji, czy zautomatyzowaniu operacji, które wymagają działań na GUI.

0 komentarze:

Prześlij komentarz