Tests: refactored sikuli based test wxPython4
authorEdouard Tisserant <edouard.tisserant@gmail.com>
Tue, 29 Mar 2022 08:50:01 +0200
branchwxPython4
changeset 3446 de8cc85b688a
parent 3445 83545348403e
child 3447 65c5f66e9298
Tests: refactored sikuli based test
tests/ide_tests/edit_project.sikuli/edit_project.py
tests/ide_tests/new_project.sikuli/new_project.py
tests/ide_tests/run_python_exemple.sikuli/run_python_exemple.py
tests/ide_tests/sikuliberemiz.py
--- a/tests/ide_tests/edit_project.sikuli/edit_project.py	Tue Mar 29 08:30:03 2022 +0200
+++ b/tests/ide_tests/edit_project.sikuli/edit_project.py	Tue Mar 29 08:50:01 2022 +0200
@@ -12,65 +12,54 @@
 from sikuliberemiz import *
 
 # Start the app
-proc,app = StartBeremizApp(exemple="python")
+app = BeremizApp(exemple="python")
 
-# To detect when actions did finish because IDE content isn't changing
-idle = IDEIdleObserver(app)
+app.doubleClick("1646062660770.png")
 
-doubleClick("1646062660770.png")
+app.WaitIdleUI()
 
-idle.Wait(1,15)
+app.click("example")
 
-click("example")
+app.WaitIdleUI()
 
-idle.Wait(1,15)
+app.type(Key.DOWN * 10, Key.CTRL)
 
-type(Key.DOWN * 10, Key.CTRL)
+app.WaitIdleUI()
 
-idle.Wait(1,15)
+app.doubleClick("1646066996620.png")
 
-doubleClick("1646066996620.png")
+app.WaitIdleUI()
 
-idle.Wait(1,15)
+app.type(Key.TAB*3)  # select text content
 
-type(Key.TAB*3)  # select text content
+app.type("'sys.stdout.write(\"EDIT TEST OK\\n\")'")
 
-type("'sys.stdout.write(\"EDIT TEST OK\\n\")'")
+app.type(Key.ENTER)
 
-type(Key.ENTER)
+app.WaitIdleUI()
 
-idle.Wait(1,15)
+app.k.Save()
 
-k = KBDShortcut(app)
+app.k.Clean()
 
-k.Save()
+app.waitForChangeAndIdleStdout()
 
-del idle
+app.k.Build()
 
-stdoutIdle = stdoutIdleObserver(proc)
+app.waitForChangeAndIdleStdout()
 
-k.Clean()
+app.k.Connect()
 
-stdoutIdle.WaitForChangeAndIdle(2,15)
+app.waitForChangeAndIdleStdout()
 
-k.Build()
+app.k.Transfer()
 
-stdoutIdle.WaitForChangeAndIdle(2,15)
+app.waitForChangeAndIdleStdout()
 
-k.Connect()
+app.k.Run()
 
-stdoutIdle.WaitForChangeAndIdle(2,15)
-
-k.Transfer()
-
-stdoutIdle.WaitForChangeAndIdle(2,15)
-
-del stdoutIdle
-
-k.Run()
-
-# wait 10 seconds for 10 Grumpfs
-found = waitPatternInStdout(proc, "EDIT TEST OK", 10)
+# wait 10 seconds for 10 patterns
+found = app.waitPatternInStdout("EDIT TEST OK", 10)
 
 app.close()
 
--- a/tests/ide_tests/new_project.sikuli/new_project.py	Tue Mar 29 08:30:03 2022 +0200
+++ b/tests/ide_tests/new_project.sikuli/new_project.py	Tue Mar 29 08:50:01 2022 +0200
@@ -12,133 +12,122 @@
 from sikuliberemiz import *
 
 # Start the app without any project given
-proc,app = StartBeremizApp()
+app = BeremizApp()
 
 new_project_path = os.path.join(os.path.abspath(os.path.curdir), "new_test_project")
 
 # New project path must exist (usually created in directory selection dialog)
 os.mkdir(new_project_path)
 
-# To detect when actions did finish because IDE content isn't changing
-idle = IDEIdleObserver(app)
-
-# To send keyboard shortuts
-k = KBDShortcut(app)
-
-idle.Wait(1,15)
+app.WaitIdleUI()
 
 # Create new project (opens new project directory selection dialog)
-k.New()
+app.k.New()
 
-idle.Wait(1,15)
+app.WaitIdleUI()
 
 # Move to "Home" section of file selecor, otherwise address is 
 # "file ignored" at first run
-type("f", Key.CTRL)
-type(Key.ESC)
-type(Key.TAB)
+app.type("f", Key.CTRL)
+app.type(Key.ESC)
+app.type(Key.TAB)
 
 # Enter directory by name
-k.Address()
+app.k.Address()
 
 # Fill address bar
-type(new_project_path + Key.ENTER)
+app.type(new_project_path + Key.ENTER)
 
-idle.Wait(1,15)
+app.WaitIdleUI()
 
 # When prompted for creating first program select type ST
-type(Key.TAB*4)  # go to lang dropdown
-type(Key.DOWN*2) # change selected language
-type(Key.ENTER)  # validate
+app.type(Key.TAB*4)  # go to lang dropdown
+app.type(Key.DOWN*2) # change selected language
+app.type(Key.ENTER)  # validate
 
-idle.Wait(1,15)
+app.WaitIdleUI()
 
 # Name created program
-type("Test program")
+app.type("Test program")
 
-idle.Wait(1,15)
+app.WaitIdleUI()
 
 # Focus on Variable grid
-type(Key.TAB*4)
+app.type(Key.TAB*4)
 
 # Add 2 variables
-type(Key.ADD*2)
+app.type(Key.ADD*2)
 
 # Focus on ST text
-idle.Wait(1,15)
+app.WaitIdleUI()
 
-type(Key.TAB*8)
+app.type(Key.TAB*8)
 
-type("""\
+app.type("""\
 LocalVar0 := LocalVar1;
 {printf("Test OK\\n");fflush(stdout);}
 """)
 
-k.Save()
+app.k.Save()
 
 # Close ST POU
-type("w", Key.CTRL)
+app.type("w", Key.CTRL)
 
-idle.Wait(1,15)
+app.WaitIdleUI()
 
 # Focus project tree and select root item
-type(Key.TAB)
+app.type(Key.TAB)
 
-type(Key.LEFT)
+app.type(Key.LEFT)
 
-type(Key.UP)
+app.type(Key.UP)
 
 # Edit root item
-type(Key.ENTER)
+app.type(Key.ENTER)
 
-idle.Wait(1,15)
+app.WaitIdleUI()
 
 # Switch to config tab
-type(Key.RIGHT*2)
+app.type(Key.RIGHT*2)
 
 # Focus on URI
-type(Key.TAB)
+app.type(Key.TAB)
 
 # Set URI
-type("LOCAL://")
+app.type("LOCAL://")
 
 # FIXME: Select other field to ensure URI is validated
-type(Key.TAB)
+app.type(Key.TAB)
 
-k.Save()
+app.k.Save()
 
 # Close project config editor
-type("w", Key.CTRL)
+app.type("w", Key.CTRL)
 
-idle.Wait(1,15)
+app.WaitIdleUI()
 
 # Focus seems undefined at that time (FIXME)
 # Force focussing on "something" so that next shortcut is taken
-type(Key.TAB)
+app.type(Key.TAB)
 
-del idle
+app.waitIdleStdout()
 
-stdoutIdle = stdoutIdleObserver(proc)
-stdoutIdle.Wait(2,15)
+app.k.Build()
 
-k.Build()
+app.waitIdleStdout(5,30)
 
-stdoutIdle.Wait(5,15)
+app.k.Connect()
 
-k.Connect()
+app.waitIdleStdout()
 
-stdoutIdle.Wait(2,15)
+app.k.Transfer()
 
-k.Transfer()
+app.waitIdleStdout()
 
-stdoutIdle.Wait(2,15)
-
-del stdoutIdle
-
-k.Run()
+app.k.Run()
 
 # wait 10 seconds
-found = waitPatternInStdout(proc, "Test OK", 10)
+found = app.waitPatternInStdout("Test OK", 10)
 
 app.close()
 
--- a/tests/ide_tests/run_python_exemple.sikuli/run_python_exemple.py	Tue Mar 29 08:30:03 2022 +0200
+++ b/tests/ide_tests/run_python_exemple.sikuli/run_python_exemple.py	Tue Mar 29 08:50:01 2022 +0200
@@ -12,41 +12,28 @@
 from sikuliberemiz import *
 
 # Start the app
-proc,app = StartBeremizApp(exemple="python")
+app = BeremizApp(exemple="python")
 
-# To detect when actions did finish because IDE content isn't changing
-# idle = IDEIdleObserver(app)
-# screencap based idle detection was making many false positive. Test is more stable with stdout based idle detection
+app.k.Clean()
 
-stdoutIdle = stdoutIdleObserver(proc)
+app.waitForChangeAndIdleStdout()
 
-# To send keyboard shortuts
-k = KBDShortcut(app)
+app.k.Build()
 
-k.Clean()
+app.waitForChangeAndIdleStdout()
 
-stdoutIdle.Wait(2,15)
+app.k.Connect()
 
-k.Build()
+app.waitForChangeAndIdleStdout()
 
-stdoutIdle.Wait(2,15)
+app.k.Transfer()
 
-k.Connect()
+app.waitForChangeAndIdleStdout()
 
-stdoutIdle.Wait(2,15)
-
-k.Transfer()
-
-stdoutIdle.Wait(2,15)
-
-#del idle
-
-del stdoutIdle
-
-k.Run()
+app.k.Run()
 
 # wait 10 seconds for 10 Grumpfs
-found = waitPatternInStdout(proc, "Grumpf", 10, 10)
+found = app.waitPatternInStdout("Grumpf", 10, 10)
 
 app.close()
 
--- a/tests/ide_tests/sikuliberemiz.py	Tue Mar 29 08:30:03 2022 +0200
+++ b/tests/ide_tests/sikuliberemiz.py	Tue Mar 29 08:50:01 2022 +0200
@@ -3,112 +3,47 @@
 import os
 import sys
 import subprocess
-from threading import Thread, Event
+from threading import Thread, Event, Lock
 from time import time as timesec
 
-typeof=type
-
-from sikuli import *
+import sikuli
 
 beremiz_path = os.environ["BEREMIZPATH"]
 python_bin = os.environ.get("BEREMIZPYTHONPATH", "/usr/bin/python")
 
 opj = os.path.join
 
-def StartBeremizApp(projectpath=None, exemple=None):
-    """
-    Starts Beremiz IDE, waits for main window to appear, maximize it.
-
-        Parameters: 
-            projectpath (str): path to project to open
-            exemple (str): path relative to exemples directory
-
-        Returns:
-            Sikuli App class instance
-    """
-
-    command = [python_bin, opj(beremiz_path,"Beremiz.py"), "--log=/dev/stdout"]
-
-    if exemple is not None:
-        command.append(opj(beremiz_path,"exemples",exemple))
-    elif projectpath is not None:
-        command.append(projectpath)
-
-    # App class is broken in Sikuli 2.0.5: can't start process with arguments.
-    # 
-    # Workaround : - use subprocess module to spawn IDE process,
-    #              - use wmctrl to find IDE window details and maximize it
-    #              - pass exact window title to App class constructor
-
-    proc = subprocess.Popen(command, stdout=subprocess.PIPE, bufsize=0)
-
-    # Window are macthed against process' PID
-    ppid = proc.pid
-
-    # Timeout 5s
-    c = 50
-    while c > 0:
-        # equiv to "wmctrl -l -p | grep $pid"
-        try:
-            wlist = filter(lambda l:(len(l)>2 and l[2]==str(ppid)), map(lambda s:s.split(None,4), subprocess.check_output(["wmctrl", "-l", "-p"]).splitlines()))
-        except subprocess.CalledProcessError:
-            wlist = []
-
-        # window with no title only has 4 fields do describe it
-        # beremiz splashcreen has no title
-        # wait until main window is visible
-        if len(wlist) == 1 and len(wlist[0]) == 5:
-            windowID,_zero,wpid,_XID,wtitle = wlist[0] 
-            break
-
-        wait(0.1)
-        c = c - 1
-
-    if c == 0:
-        raise Exception("Couldn't find Beremiz window")
-
-    # Maximize window x and y
-    subprocess.check_call(["wmctrl", "-i", "-r", windowID, "-b", "add,maximized_vert,maximized_horz"])
-
-    # switchApp creates an App object by finding window by title, is not supposed to spawn a process
-    return proc, switchApp(wtitle)
 
 class KBDShortcut:
-    """Send shortut to app by calling corresponding methods:
-          Stop   
-          Run    
-          Transfer
-          Connect
-          Clean  
-          Build  
+    """Send shortut to app by calling corresponding methods.
 
     example:
-        k = KBDShortcut(app)
+        k = KBDShortcut()
         k.Clean()
     """
 
-    fkeys = {"Stop":     Key.F4,
-             "Run":      Key.F5,
-             "Transfer": Key.F6,
-             "Connect":  Key.F7,
-             "Clean":    Key.F9,
-             "Build":    Key.F11,
-             "Save":     ("s",Key.CTRL),
-             "New":      ("n",Key.CTRL),
-             "Address":  ("l",Key.CTRL)}  # to reach address bar in GTK's file selector
+    fkeys = {"Stop":     sikuli.Key.F4,
+             "Run":      sikuli.Key.F5,
+             "Transfer": sikuli.Key.F6,
+             "Connect":  sikuli.Key.F7,
+             "Clean":    sikuli.Key.F9,
+             "Build":    sikuli.Key.F11,
+             "Save":     ("s",sikuli.Key.CTRL),
+             "New":      ("n",sikuli.Key.CTRL),
+             "Address":  ("l",sikuli.Key.CTRL)}  # to reach address bar in GTK's file selector
 
     def __init__(self, app):
-        self.app = app
+        self.app = app.sikuliapp
     
     def __getattr__(self, name):
         fkey = self.fkeys[name]
-        if typeof(fkey) != tuple:
+        if type(fkey) != tuple:
             fkey = (fkey,)
         app = self.app
 
         def PressShortCut():
             app.focus()
-            type(*fkey)
+            sikuli.type(*fkey)
 
         return PressShortCut
 
@@ -116,12 +51,12 @@
 class IDEIdleObserver:
     "Detects when IDE is idle. This is particularly handy when staring an operation and witing for the en of it."
 
-    def __init__(self, app):
-        """
-        Parameters: 
-            app (class App): Sikuli app given by StartBeremizApp
-        """
-        self.r = Region(app.window())
+    def __init__(self):
+        """
+        Parameters: 
+            app (class BeremizApp)
+        """
+        self.r = sikuli.Region(self.sikuliapp.window())
 
         self.idechanged = False
         
@@ -136,7 +71,7 @@
     def _OnIDEWindowChange(self, event):
         self.idechanged = True
 
-    def Wait(self, period, timeout):
+    def WaitIdleUI(self, period=1, timeout=15):
         """
         Wait for IDE to stop changing
         Parameters: 
@@ -146,7 +81,7 @@
         c = max(timeout/period,1)
         while c > 0:
             self.idechanged = False
-            wait(period)
+            sikuli.wait(period)
             if not self.idechanged:
                 break
             c = c - 1
@@ -158,19 +93,18 @@
 class stdoutIdleObserver:
     "Detects when IDE's stdout is idle. Can be more reliable than pixel based version (false changes ?)"
 
-    def __init__(self, proc):
-        """
-        Parameters: 
-            proc (subprocess.Popen): Beremiz process, given by StartBeremizApp
-        """
-        self.proc = proc
+    def __init__(self):
+        """
+        Parameters: 
+            app (class BeremizApp)
+        """
         self.stdoutchanged = False
 
-        self.changes = 0
-        self.last_change_count = 0
-
         self.event = Event()
 
+        self.pattern = None
+        self.success_event = Event()
+
         self.thread = Thread(target = self._waitStdoutProc).start()
 
     def _waitStdoutProc(self):
@@ -178,72 +112,140 @@
             a = self.proc.stdout.readline()
             if len(a) == 0 or a is None: 
                 break
-            # sys.stdout.write(a)
-            self.changes = self.changes + 1
+            sys.stdout.write(a)
             self.event.set()
-
-    def WaitForChangeAndIdle(self, period, timeout):
+            if self.pattern is not None and a.find(self.pattern) >= 0:
+                sys.stdout.write("found pattern in '" + a +"'")
+                self.success_event.set()
+
+    def waitForChangeAndIdleStdout(self, period=2, timeout=15):
         """
         Wait for IDE'stdout to start changing
         Parameters: 
             timeout (int): how long to wait for change, in seconds
         """
         start_time = timesec()
-        if self.changes == self.last_change_count:
-            if self.event.wait(timeout):
-                self.event.clear()
-                self.last_change_count = self.changes
-            else:
-                raise Exception("Stdout didn't become active before timeout")
-
-        self.Wait(period, timeout - (timesec() - start_time))
-
-    def Wait(self, period, timeout):
+
+        if self.event.wait(timeout):
+            self.event.clear()
+        else:
+            raise Exception("Stdout didn't become active before timeout")
+
+        self.waitIdleStdout(period, timeout - (timesec() - start_time))
+
+    def waitIdleStdout(self, period=2, timeout=15):
         """
         Wait for IDE'stdout to stop changing
         Parameters: 
             period (int): how many seconds with no change to consider idle
             timeout (int): how long to wait for idle, in seconds
         """
-        c = max(timeout/period, 1)
-        while c > 0:
-            changes = self.changes
-            wait(period)
-            if self.changes == changes:
-                self.last_change_count = self.changes
-                break
-            c = c - 1
-
-        if c == 0:
-            raise Exception("Stdout did not idle before timeout")
-
-
-def waitPatternInStdout(proc, pattern, timeout, count=1):
-    
-    success_event = Event()
-
-    def waitPatternInStdoutProc():
+        end_time = timesec() + timeout
+        self.event.clear()
+        while timesec() < end_time:
+            if self.event.wait(period):
+                # no timeout -> got event -> not idle -> loop again
+                self.event.clear()
+            else:
+                # timeout -> no event -> idle -> exit
+                return True
+
+        raise Exception("Stdout did not idle before timeout")
+
+    def waitPatternInStdout(self, pattern, timeout, count=1):
         found = 0
+        self.pattern = pattern
+        end_time = timesec() + timeout
+        self.event.clear()
         while True:
-            a = proc.stdout.readline()
-            if len(a) == 0 or a is None: 
-                raise Exception("App finished before producing expected stdout pattern")
-            # sys.stdout.write(a)
-            if a.find(pattern) >= 0:
-                sys.stdout.write("found pattern in '" + a +"'")
+            remain = end_time - timesec()
+            if remain <= 0 :
+                res = False
+                break
+
+            res = self.success_event.wait(remain)
+            if res:
+                self.success_event.clear()
                 found = found + 1
                 if found >= count:
-                    success_event.set()
                     break
-
-
-    Thread(target = waitPatternInStdoutProc).start()
-
-    if not success_event.wait(timeout):
-        # test timed out
-        return False
-    else:
-        return True
-
-
-
+        self.pattern = None
+        return res
+
+class BeremizApp(IDEIdleObserver, stdoutIdleObserver):
+    def __init__(self, projectpath=None, exemple=None):
+        """
+        Starts Beremiz IDE, waits for main window to appear, maximize it.
+
+            Parameters: 
+                projectpath (str): path to project to open
+                exemple (str): path relative to exemples directory
+
+            Returns:
+                Sikuli App class instance
+        """
+
+        command = [python_bin, opj(beremiz_path,"Beremiz.py"), "--log=/dev/stdout"]
+
+        if exemple is not None:
+            command.append(opj(beremiz_path,"exemples",exemple))
+        elif projectpath is not None:
+            command.append(projectpath)
+
+        # App class is broken in Sikuli 2.0.5: can't start process with arguments.
+        # 
+        # Workaround : - use subprocess module to spawn IDE process,
+        #              - use wmctrl to find IDE window details and maximize it
+        #              - pass exact window title to App class constructor
+
+        self.proc = subprocess.Popen(command, stdout=subprocess.PIPE, bufsize=0)
+
+        # Window are macthed against process' PID
+        ppid = self.proc.pid
+
+        # Timeout 5s
+        c = 50
+        while c > 0:
+            # equiv to "wmctrl -l -p | grep $pid"
+            try:
+                wlist = filter(lambda l:(len(l)>2 and l[2]==str(ppid)), map(lambda s:s.split(None,4), subprocess.check_output(["wmctrl", "-l", "-p"]).splitlines()))
+            except subprocess.CalledProcessError:
+                wlist = []
+
+            # window with no title only has 4 fields do describe it
+            # beremiz splashcreen has no title
+            # wait until main window is visible
+            if len(wlist) == 1 and len(wlist[0]) == 5:
+                windowID,_zero,wpid,_XID,wtitle = wlist[0] 
+                break
+
+            sikuli.wait(0.1)
+            c = c - 1
+
+        if c == 0:
+            raise Exception("Couldn't find Beremiz window")
+
+        # Maximize window x and y
+        subprocess.check_call(["wmctrl", "-i", "-r", windowID, "-b", "add,maximized_vert,maximized_horz"])
+
+        # switchApp creates an App object by finding window by title, is not supposed to spawn a process
+        self.sikuliapp = sikuli.switchApp(wtitle)
+        self.k = KBDShortcut(self)
+
+        IDEIdleObserver.__init__(self)
+        stdoutIdleObserver.__init__(self)
+
+        # stubs for common sikuli calls to allow adding hooks later
+        for n in ["click","doubleClick","type"]:
+            setattr(self, n, getattr(sikuli, n))
+
+    def close(self):
+        self.sikuliapp.close()
+        self.sikuliapp = None
+
+    def __del__(self):
+        if self.sikuliapp is not None:
+            self.sikuliapp.close()
+        IDEIdleObserver.__del__(self)
+        stdoutIdleObserver.__del__(self)
+