"Commons definitions for sikuli based beremiz IDE GUI tests"
from threading import Thread, Event, Lock
from time import time as timesec
from xml.sax.saxutils import escape as escape_xml
beremiz_path = os.environ["BEREMIZPATH"]
python_bin = os.environ.get("BEREMIZPYTHONPATH", "/usr/bin/python")
tessdata_path = os.environ["TESSDATAPATH"]
ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
return ansi_escape.sub('', line)
return escape_xml(escape_ansi(txt))
"""Send shortut to app by calling corresponding methods.
fkeys = {"Stop": sikuli.Key.F4,
"Transfer": sikuli.Key.F6,
"Connect": sikuli.Key.F7,
"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 __getattr__(self, name):
self.app.sikuliapp.focus()
self.app.ReportText("Sending " + name + " shortcut")
"Detects when IDE is idle. This is particularly handy when staring an operation and witing for the en of it."
self.r = sikuli.Region(self.sikuliapp.window())
self.targetOffset = self.r.getTopLeft()
# 200 was selected because default 50 was still catching cursor blinking in console
# FIXME : remove blinking cursor in console
self.r.onChange(200,self._OnIDEWindowChange)
self.r.observeInBackground()
def _OnIDEWindowChange(self, event):
def WaitIdleUI(self, period=1, timeout=15):
Wait for IDE to stop changing
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)
self.ReportScreenShot("UI is idle" if c != 0 else "UI is not idle")
raise Exception("Window did not idle before timeout")
class stdoutIdleObserver:
"Detects when IDE's stdout is idle. Can be more reliable than pixel based version (false changes ?)"
self.stdoutchanged = False
self.success_event = Event()
if self.proc is not None:
self.thread = Thread(target = self._waitStdoutProc).start()
pass # self.thread.join() ?
def _waitStdoutProc(self):
a = self.proc.stdout.readline()
if len(a) == 0 or a is None:
if self.pattern is not None and a.find(self.pattern) >= 0:
sys.stdout.write("found pattern in '" + a +"'")
def waitForChangeAndIdleStdout(self, period=2, timeout=15):
Wait for IDE'stdout to start changing
timeout (int): how long to wait for change, in seconds
wait_result = self.event.wait(timeout)
self.ReportScreenShot("stdout changed" if wait_result else "stdout didn't change")
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
period (int): how many seconds with no change to consider idle
timeout (int): how long to wait for idle, in seconds
end_time = timesec() + timeout
while timesec() < end_time:
if self.event.wait(period):
# no timeout -> got event -> not idle -> loop again
# timeout -> no event -> idle -> exit
self.ReportScreenShot("stdout is idle")
self.ReportScreenShot("stdout did not idle")
raise Exception("Stdout did not idle before timeout")
def waitPatternInStdout(self, pattern, timeout, count=1):
end_time = timesec() + timeout
remain = end_time - timesec()
res = self.success_event.wait(remain)
self.success_event.clear()
self.ReportScreenShot("found pattern" if res else "pattern not found")
class BeremizApp(IDEIdleObserver, stdoutIdleObserver):
def __init__(self, projectpath=None, exemple=None, testproject=None):
Starts Beremiz IDE, waits for main window to appear, maximize it.
projectpath (str): path to project to open
exemple (str): path relative to exemples directory
Sikuli App class instance
self.ocropts = sikuli.OCR.globalOptions()
self.ocropts.dataPath(tessdata_path)
# 0 use legacy Tesseract (not so good, but repeatable)
# 1 use RNN Tesseract (better but non-repeatable)
self.starttime = timesec()
self.screen = sikuli.Screen()
self.report = open("report.xhtml", "w")
<html xmlns="http://www.w3.org/1999/xhtml">
<meta name="color-scheme" content="light dark"/>
<title>Test report</title>
command = ["setsid", python_bin, opj(beremiz_path,"Beremiz.py"), "--log=/dev/stdout"]
command.append(opj(beremiz_path,"exemples",exemple))
elif projectpath is not None:
command.append(projectpath)
elif testproject is not None:
command.append(opj(beremiz_path,"tests","projects",testproject))
# 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.ReportText("Launching " + repr(command))
self.proc = subprocess.Popen(command, stdout=subprocess.PIPE, bufsize=0)
# Window are macthed against process' PID
# equiv to "wmctrl -l -p | grep $pid"
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:
# 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]
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 name, takes_matches in [
def myMeth(*args, **kwargs):
self.ReportScreenShot("Begin: " + n + "(" + repr(args) + "," + repr(kwargs) + ")")
args = map(self.handle_PFRML_arg, args)
kwargs = dict(map(lambda k,v:(k,self.handle_PFRML_arg(v)), kwargs.items()))
getattr(sikuli, n)(*args, **kwargs)
self.ReportScreenShot("end: " + n + "(" + repr(args) + "," + repr(kwargs) + ")")
setattr(self, name, makeMyMeth(name,takes_matches))
def handle_PFRML_arg(self, arg):
return self.findBest(*arg)
if type(arg)==str and not arg.endswith(".png"):
return self.findBest(arg)
def findBest(self, *args):
#match = self.r.findBest(*args)
matches = sikuli.OCR.readWords(self.r) + sikuli.OCR.readLines(self.r)
mText = m.getText().encode('ascii', 'ignore')
if arg in mText or arg in mText.translate(None, "\"`'|-. "):
self.ReportText("Not found: " + repr(args) + " OCR content: ")
self.ReportText(repr(m) + ": " + m.getText().encode('ascii', 'ignore'))
raise Exception("Not Found: " + repr(args))
# translate match to screen ref
#match.setTargetOffset(self.targetOffset)
match.setTopLeft(match.getTopLeft().offset(self.targetOffset))
self.ReportTextImage("Found for " + repr(args) + ": " +
" ".join([repr(match), repr(match.getTarget()), repr(match.getTargetOffset())]),
self.screen.capture(match))
def dragNdrop(self, src, dst):
self.ReportScreenShot("Drag: (" + repr(src) + ")")
sikuli.drag(self.handle_PFRML_arg(src))
sikuli.dropAt(self.handle_PFRML_arg(dst))
self.ReportScreenShot("Drop: (" + repr(dst) + ")")
self.ReportScreenShot("Close app")
os.kill(self.proc.pid, signal.SIGKILL)
if self.sikuliapp is not None:
IDEIdleObserver.__del__(self)
stdoutIdleObserver.__del__(self)
def ReportScreenShot(self, msg):
cap = self.screen.capture(self.r)
self.ReportTextImage(msg, cap)
def ReportTextImage(self, msg, img):
elapsed = "%.3fs: "%(timesec() - self.starttime)
fname = "capture"+str(self.imgnum)+".png"
sys.stdout.write(elapsed + " [" + fname + "] " + msg + "\n")
self.imgnum = self.imgnum + 1
self.report.write( "<p>" + escape(elapsed + msg) + "<br/><img src=\""+ fname + "\"/>" + "</p>")
def ReportText(self, text):
elapsed = "%.3fs: "%(timesec() - self.starttime)
#res = u"<p><![CDATA[" + elapsed + text + "]]></p>"
sys.stdout.write(elapsed + text + "\n")
res = u"<p>" + escape(elapsed + text) + "</p>"
def ReportOutput(self, text):
elapsed = "%.3fs: "%(timesec() - self.starttime)
sys.stdout.write(elapsed + text)
self.report.write("<pre>" + escape(elapsed + text) + "</pre>")
class AuxiliaryProcess(stdoutIdleObserver):
def __init__(self, beremiz_app, command):
self.app.ReportText("Launching process " + repr(command))
self.proc = subprocess.Popen(command, stdout=subprocess.PIPE, bufsize=0)
self.app.ReportText("Launched process " + repr(command) + " PID: " + str(self.proc.pid))
stdoutIdleObserver.__init__(self)
if self.proc is not None:
# self.proc.stdout.close()
self.app.ReportText("Kill process PID: " + str(proc.pid))
os.kill(proc.pid, signal.SIGTERM)
def ReportOutput(self, text):
self.app.ReportOutput("Aux: "+text)
def ReportScreenShot(self, msg):
self.app.ReportOutput("Aux: "+msg)
def run_test(func, *args, **kwargs):
app = BeremizApp(*args, **kwargs)
# sadly, sys.excepthook is broken in sikuli/jython
# purpose of this run_test function is to work around it.
# and catch exception cleanly anyhow
e_type, e_value, e_traceback = sys.exc_info()
err_msg = "\n".join(traceback.format_exception(e_type, e_value, e_traceback))
app.ReportOutput(err_msg)