edouard@3424: "Commons definitions for sikuli based beremiz IDE GUI tests" edouard@3424: edouard@3424: import os edouard@3424: import sys edouard@3424: import subprocess edouard@3446: from threading import Thread, Event, Lock edouard@3437: from time import time as timesec edouard@3432: edouard@3446: import sikuli edouard@3424: edouard@3424: beremiz_path = os.environ["BEREMIZPATH"] edouard@3431: python_bin = os.environ.get("BEREMIZPYTHONPATH", "/usr/bin/python") edouard@3424: edouard@3424: opj = os.path.join edouard@3424: edouard@3424: edouard@3424: class KBDShortcut: edouard@3446: """Send shortut to app by calling corresponding methods. edouard@3424: edouard@3424: example: edouard@3446: k = KBDShortcut() edouard@3424: k.Clean() edouard@3424: """ edouard@3424: edouard@3446: fkeys = {"Stop": sikuli.Key.F4, edouard@3446: "Run": sikuli.Key.F5, edouard@3446: "Transfer": sikuli.Key.F6, edouard@3446: "Connect": sikuli.Key.F7, edouard@3446: "Clean": sikuli.Key.F9, edouard@3446: "Build": sikuli.Key.F11, edouard@3446: "Save": ("s",sikuli.Key.CTRL), edouard@3446: "New": ("n",sikuli.Key.CTRL), edouard@3446: "Address": ("l",sikuli.Key.CTRL)} # to reach address bar in GTK's file selector edouard@3424: edouard@3424: def __init__(self, app): edouard@3446: self.app = app.sikuliapp edouard@3424: edouard@3424: def __getattr__(self, name): edouard@3424: fkey = self.fkeys[name] edouard@3446: if type(fkey) != tuple: edouard@3432: fkey = (fkey,) edouard@3424: app = self.app edouard@3424: edouard@3424: def PressShortCut(): edouard@3424: app.focus() edouard@3446: sikuli.type(*fkey) edouard@3424: edouard@3424: return PressShortCut edouard@3424: edouard@3424: edouard@3424: class IDEIdleObserver: edouard@3424: "Detects when IDE is idle. This is particularly handy when staring an operation and witing for the en of it." edouard@3424: edouard@3446: def __init__(self): edouard@3446: """ edouard@3446: Parameters: edouard@3446: app (class BeremizApp) edouard@3446: """ edouard@3446: self.r = sikuli.Region(self.sikuliapp.window()) edouard@3424: edouard@3424: self.idechanged = False edouard@3424: edouard@3424: # 200 was selected because default 50 was still catching cursor blinking in console edouard@3424: # FIXME : remove blinking cursor in console edouard@3424: self.r.onChange(200,self._OnIDEWindowChange) edouard@3424: self.r.observeInBackground() edouard@3424: edouard@3424: def __del__(self): edouard@3424: self.r.stopObserver() edouard@3424: edouard@3424: def _OnIDEWindowChange(self, event): edouard@3424: self.idechanged = True edouard@3424: edouard@3446: def WaitIdleUI(self, period=1, timeout=15): edouard@3424: """ edouard@3424: Wait for IDE to stop changing edouard@3424: Parameters: edouard@3424: period (int): how many seconds with no change to consider idle edouard@3424: timeout (int): how long to wait for idle, in seconds edouard@3424: """ edouard@3437: c = max(timeout/period,1) edouard@3424: while c > 0: edouard@3424: self.idechanged = False edouard@3446: sikuli.wait(period) edouard@3424: if not self.idechanged: edouard@3424: break edouard@3424: c = c - 1 edouard@3424: edouard@3424: if c == 0: edouard@3424: raise Exception("Window did not idle before timeout") edouard@3424: edouard@3424: edouard@3424: class stdoutIdleObserver: edouard@3424: "Detects when IDE's stdout is idle. Can be more reliable than pixel based version (false changes ?)" edouard@3424: edouard@3446: def __init__(self): edouard@3446: """ edouard@3446: Parameters: edouard@3446: app (class BeremizApp) edouard@3446: """ edouard@3424: self.stdoutchanged = False edouard@3424: edouard@3437: self.event = Event() edouard@3437: edouard@3446: self.pattern = None edouard@3446: self.success_event = Event() edouard@3446: edouard@3424: self.thread = Thread(target = self._waitStdoutProc).start() edouard@3424: edouard@3432: def _waitStdoutProc(self): edouard@3424: while True: edouard@3437: a = self.proc.stdout.readline() edouard@3424: if len(a) == 0 or a is None: edouard@3424: break edouard@3446: sys.stdout.write(a) edouard@3437: self.event.set() edouard@3446: if self.pattern is not None and a.find(self.pattern) >= 0: edouard@3446: sys.stdout.write("found pattern in '" + a +"'") edouard@3446: self.success_event.set() edouard@3446: edouard@3446: def waitForChangeAndIdleStdout(self, period=2, timeout=15): edouard@3437: """ edouard@3437: Wait for IDE'stdout to start changing edouard@3437: Parameters: edouard@3437: timeout (int): how long to wait for change, in seconds edouard@3437: """ edouard@3437: start_time = timesec() edouard@3446: edouard@3446: if self.event.wait(timeout): edouard@3446: self.event.clear() edouard@3446: else: edouard@3446: raise Exception("Stdout didn't become active before timeout") edouard@3446: edouard@3446: self.waitIdleStdout(period, timeout - (timesec() - start_time)) edouard@3446: edouard@3446: def waitIdleStdout(self, period=2, timeout=15): edouard@3424: """ edouard@3424: Wait for IDE'stdout to stop changing edouard@3424: Parameters: edouard@3424: period (int): how many seconds with no change to consider idle edouard@3424: timeout (int): how long to wait for idle, in seconds edouard@3424: """ edouard@3446: end_time = timesec() + timeout edouard@3446: self.event.clear() edouard@3446: while timesec() < end_time: edouard@3446: if self.event.wait(period): edouard@3446: # no timeout -> got event -> not idle -> loop again edouard@3446: self.event.clear() edouard@3446: else: edouard@3446: # timeout -> no event -> idle -> exit edouard@3446: return True edouard@3446: edouard@3446: raise Exception("Stdout did not idle before timeout") edouard@3446: edouard@3446: def waitPatternInStdout(self, pattern, timeout, count=1): edouard@3424: found = 0 edouard@3446: self.pattern = pattern edouard@3446: end_time = timesec() + timeout edouard@3446: self.event.clear() edouard@3424: while True: edouard@3446: remain = end_time - timesec() edouard@3446: if remain <= 0 : edouard@3446: res = False edouard@3446: break edouard@3446: edouard@3446: res = self.success_event.wait(remain) edouard@3446: if res: edouard@3446: self.success_event.clear() edouard@3424: found = found + 1 edouard@3424: if found >= count: edouard@3424: break edouard@3446: self.pattern = None edouard@3446: return res edouard@3446: edouard@3446: class BeremizApp(IDEIdleObserver, stdoutIdleObserver): edouard@3446: def __init__(self, projectpath=None, exemple=None): edouard@3446: """ edouard@3446: Starts Beremiz IDE, waits for main window to appear, maximize it. edouard@3446: edouard@3446: Parameters: edouard@3446: projectpath (str): path to project to open edouard@3446: exemple (str): path relative to exemples directory edouard@3446: edouard@3446: Returns: edouard@3446: Sikuli App class instance edouard@3446: """ edouard@3446: edouard@3446: command = [python_bin, opj(beremiz_path,"Beremiz.py"), "--log=/dev/stdout"] edouard@3446: edouard@3446: if exemple is not None: edouard@3446: command.append(opj(beremiz_path,"exemples",exemple)) edouard@3446: elif projectpath is not None: edouard@3446: command.append(projectpath) edouard@3446: edouard@3446: # App class is broken in Sikuli 2.0.5: can't start process with arguments. edouard@3446: # edouard@3446: # Workaround : - use subprocess module to spawn IDE process, edouard@3446: # - use wmctrl to find IDE window details and maximize it edouard@3446: # - pass exact window title to App class constructor edouard@3446: edouard@3446: self.proc = subprocess.Popen(command, stdout=subprocess.PIPE, bufsize=0) edouard@3446: edouard@3446: # Window are macthed against process' PID edouard@3446: ppid = self.proc.pid edouard@3446: edouard@3446: # Timeout 5s edouard@3446: c = 50 edouard@3446: while c > 0: edouard@3446: # equiv to "wmctrl -l -p | grep $pid" edouard@3446: try: edouard@3446: 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())) edouard@3446: except subprocess.CalledProcessError: edouard@3446: wlist = [] edouard@3446: edouard@3446: # window with no title only has 4 fields do describe it edouard@3446: # beremiz splashcreen has no title edouard@3446: # wait until main window is visible edouard@3446: if len(wlist) == 1 and len(wlist[0]) == 5: edouard@3446: windowID,_zero,wpid,_XID,wtitle = wlist[0] edouard@3446: break edouard@3446: edouard@3446: sikuli.wait(0.1) edouard@3446: c = c - 1 edouard@3446: edouard@3446: if c == 0: edouard@3446: raise Exception("Couldn't find Beremiz window") edouard@3446: edouard@3446: # Maximize window x and y edouard@3446: subprocess.check_call(["wmctrl", "-i", "-r", windowID, "-b", "add,maximized_vert,maximized_horz"]) edouard@3446: edouard@3446: # switchApp creates an App object by finding window by title, is not supposed to spawn a process edouard@3446: self.sikuliapp = sikuli.switchApp(wtitle) edouard@3446: self.k = KBDShortcut(self) edouard@3446: edouard@3446: IDEIdleObserver.__init__(self) edouard@3446: stdoutIdleObserver.__init__(self) edouard@3446: edouard@3446: # stubs for common sikuli calls to allow adding hooks later edouard@3446: for n in ["click","doubleClick","type"]: edouard@3446: setattr(self, n, getattr(sikuli, n)) edouard@3446: edouard@3446: def close(self): edouard@3446: self.sikuliapp.close() edouard@3446: self.sikuliapp = None edouard@3446: edouard@3446: def __del__(self): edouard@3446: if self.sikuliapp is not None: edouard@3446: self.sikuliapp.close() edouard@3446: IDEIdleObserver.__del__(self) edouard@3446: stdoutIdleObserver.__del__(self) edouard@3446: