]> git.lyx.org Git - lyx.git/blobdiff - development/autotests/keytest.py
Update tests and documentation for supported languages.
[lyx.git] / development / autotests / keytest.py
index 7dd86a6357777d9fa5f671f955abb396e726f15e..dab92a8ca07ed184cf14e79807a8f5914098cde8 100755 (executable)
@@ -14,6 +14,9 @@ import os
 import re
 import sys
 import time
+import tempfile
+import shutil
+
 #from subprocess import call
 import subprocess
 
@@ -23,6 +26,15 @@ FNULL = open('/dev/null', 'w')
 
 key_delay = ''
 
+# Ignore status == "dead" if this is set. Used at the last commands after "\Cq"
+dead_expected = False
+
+def die(excode, text):
+    if text != "":
+        print(text)
+    sys.stdout.flush()
+    os._exit(excode)
+
 class CommandSource:
 
     def __init__(self):
@@ -61,7 +73,7 @@ class CommandSource:
         if self.count % 200 == 0:
             return 'RaiseLyx'
         elif self.count > self.count_max:
-            os._exit(0)
+            die(0, "")
         else:
             keystr = ''
             for k in range(1, 2):
@@ -140,7 +152,7 @@ class CommandSourceFromFile(CommandSource):
 
     def getCommand(self):
         if self.count >= self.max_count:
-            os._exit(0)
+            die(0, "")
         if self.i >= len(self.lines):
             self.loops = self.loops + 1
             if self.loops >= int(max_loops):
@@ -152,6 +164,93 @@ class CommandSourceFromFile(CommandSource):
         self.i = self.i + 1
         return line
 
+class ControlFile:
+
+    def __init__(self):
+        self.control = re.compile(r'^(C[ONPpRrC])([A-Za-z0-9]*):\s*(.*)$')
+        self.fileformat = re.compile(r'^((\>\>?)[,\s]\s*)?([^\s]+)\s*$')
+        self.cntrfile = dict()
+        # Map keytest marker to pattern-file-marker for searchPatterns.pl
+        self.convertSearchMark = { 'CN': 'Comment: ',
+                                   'CP': 'Simple: ', 'Cp': 'ErrSimple: ',
+                                   'CR': 'Regex: ',  'Cr': 'ErrRegex: '}
+
+    def __open(self, handle, filename):
+        if handle in self.cntrfile:
+            self.cntrfile[handle].close()
+            del self.cntrfile[handle]
+        m = self.fileformat.match(filename)
+        if m:
+            type = m.group(2)
+            filename = m.group(3)
+            if type == '>>':
+                append = True
+            else:
+                append = False
+        else:
+            append = False
+        if append:
+            self.cntrfile[handle] = open(filename, 'a')
+        else:
+            self.cntrfile[handle] = open(filename, 'w')
+
+    def closeall(self):
+        handles = self.cntrfile.keys()
+        for handle in handles:
+            self.__close(handle)
+
+    def __close(self, handle):
+        if handle in self.cntrfile:
+            name = self.cntrfile[handle].name
+            self.cntrfile[handle].close()
+            del self.cntrfile[handle]
+            print("Closed ctrl " + handle + " (" + name + ")")
+
+    # make the method below 'private'
+    def __addline(self, handle, pat):
+        self.cntrfile[handle].writelines(pat + "\n")
+
+    def dispatch(self, c):
+        m = self.control.match(c)
+        if not m:
+            return False
+        command = m.group(1)
+        handle = m.group(2)
+        if handle is None:
+            handle = ""
+        text = m.group(3)
+        if command == "CO":
+            self.__open(handle, text);
+        elif command == "CC":
+            self.__close(handle)
+        else:
+            if handle in self.cntrfile:
+                if command in self.convertSearchMark:
+                    self.__addline(handle, self.convertSearchMark[command] + text)
+                else:
+                    die(1,"Error, Unrecognised Command '" + command + "'")
+            elif handle != "":
+                die(1, "Ctrl-file " + handle + " not in use")
+        return True
+
+
+def get_proc_pid(proc_name):
+    pid=os.popen("pidof " + proc_name).read().rstrip()
+    return pid
+
+wlistreg = re.compile(r'^(0x[0-9a-f]{5,9})\s+[^\s]+\s+([0-9]+)\s.*$')
+def get_proc_win_id(pid, ignoreid):
+    nlist = os.popen("wmctrl -l -p").read()
+    wlist = nlist.split("\n")
+    for item in wlist:
+        m = wlistreg.match(item)
+        if m:
+            win_id = m.group(1)
+            win_pid = m.group(2)
+            if win_pid == pid:
+                if win_id != ignoreid:
+                    return win_id
+    return None
 
 def lyx_exists():
     if lyx_pid is None:
@@ -162,7 +261,9 @@ def lyx_exists():
 
 # Interruptible os.system()
 def intr_system(cmd, ignore_err = False):
-    print("Executing " + cmd + "\n")
+    print("Executing " + cmd)
+    # Assure the output of cmd does not overhaul
+    sys.stdout.flush()
     ret = os.system(cmd)
     if os.WIFSIGNALED(ret):
         raise KeyboardInterrupt
@@ -172,124 +273,200 @@ def intr_system(cmd, ignore_err = False):
 
 statreg = re.compile(r'^State:.*\(([a-z]+)\)')
 
-def lyx_status(pid):
-  if lyx_pid is None:
-    return "dead"
-  fname = '/proc/' + pid + '/status'
-  try:
-    f = open(fname)
-    for line in f:
-      m = statreg.match(line)
-      if m:
-        status = m.group(1)
+resstatus = []
+def printresstatus():
+    for line in resstatus:
+        line = line.rstrip()
+        print("    " + line.rstrip())
+    print('End of /proc-lines')
+
+def lyx_status_retry(pid):
+    resstatus = []
+    if pid is None:
+        print('Pid is None')
+        return "dead"
+    fname = '/proc/' + pid + '/status'
+    status = "dead"
+    try:
+        f = open(fname)
+        found = False
+        for line in f:
+            resstatus.extend([line])
+            m = statreg.match(line)
+            if m:
+                status = m.group(1)
+                found = True
         f.close()
+        if not found:
+            return "retry"
         return status
-    f.close()
-  except IOError as e:
-     print("I/O error({0}): {1}".format(e.errno, e.strerror))
-     return "dead"
-  except:
-    print("Unexpected error:", sys.exc_info()[0])
-  return "dead"
+    except IOError as e:
+        print("I/O error({0}): {1}".format(e.errno, e.strerror))
+        return "dead"
+    except:
+        print("Unexpected error:", sys.exc_info()[0])
+        return "dead"
+    print('This should not happen')
+    return status
+
+def lyx_status(pid):
+    count = 0
+    while 1:
+        status = lyx_status_retry(pid)
+        if status != "retry":
+            break
+        if count == 0:
+            print('Retrying check for status')
+        count += 1
+        time.sleep(0.01)
+    if count > 1:
+        print('Retried to read status ' + str(count) + ' times')
+    #print('lys_status() returning ' + status)
+    return status
 
 # Return true if LyX (identified via lyx_pid) is sleeping
-def lyx_sleeping():
-    return lyx_status(lyx_pid) == "sleeping"
+def lyx_sleeping(LYX_PID):
+    return lyx_status(LYX_PID) == "sleeping"
 
 # Return true if LyX (identified via lyx_pid) is zombie
-def lyx_zombie():
-    return lyx_status(lyx_pid) == "zombie"
+def lyx_zombie(LYX_PID):
+    return lyx_status(LYX_PID) == "zombie"
 
-def lyx_dead():
-    status = lyx_status(lyx_pid)
+def lyx_dead(LYX_PID):
+    status = lyx_status(LYX_PID)
     return (status == "dead") or (status == "zombie")
 
-def sendKeystringLocal(keystr, LYX_PID):
-
-    # print "sending keystring "+keystr+"\n"
-    if not re.match(".*\w.*", keystr):
-        print('print .' + keystr + '.\n')
-        keystr = 'a'
+def wait_until_lyx_sleeping(LYX_PID):
     before_secs = time.time()
-    while lyx_exists() and not lyx_sleeping():
-        time.sleep(0.02)
-        sys.stdout.flush()
+    while True:
+        status = lyx_status(LYX_PID)
+        if status == "sleeping":
+            return True
+        if (status == "dead") or (status == "zombie"):
+            printresstatus()
+            if dead_expected:
+                print('Lyx died while waiting for status == sleeping')
+                return False
+            else:
+                die(1,"Lyx is dead, exiting")
         if time.time() - before_secs > 180:
-            print('Killing due to freeze (KILL_FREEZE)')
-
             # Do profiling, but sysprof has no command line interface?
             # intr_system("killall -KILL lyx")
+            printresstatus()
+            die(1,"Killing due to freeze (KILL_FREEZE)")
+        time.sleep(0.02)
+    # Should be never reached
+    print('Wait for sleeping ends unexpectedly')
+    return False
 
-            os._exit(1)
+def sendKeystringLocal(keystr, LYX_PID):
+    is_sleeping = wait_until_lyx_sleeping(LYX_PID)
+    if not is_sleeping:
+        print("Not sending \"" + keystr + "\"")
+        return
     if not screenshot_out is None:
-        while lyx_exists() and not lyx_sleeping():
-            time.sleep(0.02)
-            sys.stdout.flush()
         print('Making Screenshot: ' + screenshot_out + ' OF ' + infilename)
         time.sleep(0.2)
         intr_system('import -window root '+screenshot_out+str(x.count)+".png")
         time.sleep(0.1)
-    sys.stdout.flush()
     actual_delay = key_delay
     if actual_delay == '':
         actual_delay = def_delay
     xvpar = [xvkbd_exe]
     if qt_frontend == 'QT5':
-        xvpar.extend(["-no-jump-pointer"])
+        xvpar.extend(["-jump-pointer", "-no-back-pointer"])
     else:
         xvpar.extend(["-xsendevent"])
-    xvpar.extend(["-window", lyx_window_name, "-delay", actual_delay, "-text", keystr])
-
-    print("Sending \"" + keystr + "\"\n")
+    if lyx_other_window_name is None:
+        xvpar.extend(["-window", lyx_window_name])
+    else:
+        xvpar.extend(["-window", lyx_other_window_name])
+    xvpar.extend(["-delay", actual_delay, "-text", keystr])
+    print("Sending \"" + keystr + "\"")
     subprocess.call(xvpar, stdout = FNULL, stderr = FNULL)
+    sys.stdout.flush()
 
-Axreg = re.compile(r'^(.*)\\Ax([^\\]*)(.*)$')
-returnreg = re.compile(r'\\\[Return\](.*)$')
-
-# recursive wrapper around sendKeystringLocal()
-def sendKeystring(line, LYX_PID):
-    global key_delay
-    saved_delay = key_delay
-    m = Axreg.match(line)
+def extractmultiple(line, regex):
+    #print("extractmultiple " + line)
+    res = ["", ""]
+    m = regex.match(line)
     if m:
-        prefix = m.group(1)
-        content = m.group(2)
-        rest = m.group(3);
-        if prefix != "":
-            # since (.*) is greedy, check prefix for '\Ax' again
-            sendKeystring(prefix, LYX_PID)
-        sendKeystringLocal('\Ax', LYX_PID)
-        time.sleep(0.1)
-        m2 = returnreg.match(rest)
-        if m2:
-            line = m2.group(1)
-            key_delay = "1"
-            sendKeystringLocal(content + '\[Return]', LYX_PID)
-            key_delay = saved_delay
-            time.sleep(0.1)
-            if line != "":
-                sendKeystringLocal(line, LYX_PID)
+        chr = m.group(1)
+        if m.group(2) == "":
+            res[0] = chr
+            res[1] = ""
         else:
-            if content != "":
-                sendKeystringLocal(content, LYX_PID)
-            if rest != "":
-                sendKeystringLocal(rest, LYX_PID)
+            norm = extractmultiple(m.group(2), regex)
+            res[0] = chr + norm[0]
+            res[1] = norm[1]
     else:
-        if line != "":
-            sendKeystringLocal(line, LYX_PID)
+        res[0] = ""
+        res[1] = line
+    return res
+
+normal_re = re.compile(r'^([^\\]|\\\\)(.*)$')
+def extractnormal(line):
+    # collect non-special chars from start of line
+    return extractmultiple(line, normal_re)
+
+modifier_re = re.compile(r'^(\\[CAS])(.+)$')
+def extractmodifiers(line):
+    # collect modifiers like '\\A' at start of line
+    return extractmultiple(line, modifier_re)
+
+special_re = re.compile(r'^(\\\[[A-Z][a-z0-9]+\])(.*)$')
+def extractsingle(line):
+    # check for single key following a modifier
+    # either ascii like 'a'
+    # or special like '\[Return]'
+    res = [False, "", ""]
+    m = normal_re.match(line)
+    if m:
+        res[0] = False
+        res[1] = m.group(1)
+        res[2] = m.group(2)
+    else:
+        m = special_re.match(line)
+        if m:
+            res[0] = True
+            res[1] = m.group(1)
+            res[2] = m.group(2)
+        else:
+            die(1, "Undecodable key for line \'" + line + "\"")
+    return res
 
+def sendKeystring(line, LYX_PID):
+    if line == "":
+        return
+    normalchars = extractnormal(line)
+    line = normalchars[1]
+    if normalchars[0] != "":
+        sendKeystringLocal(normalchars[0], LYX_PID)
+    if line == "":
+        return
+    modchars = extractmodifiers(line)
+    line = modchars[1]
+    if line == "":
+        die(1, "Missing modified key")
+    modifiedchar = extractsingle(line)
+    line = modifiedchar[2]
+    special = modchars[0] != "" or modifiedchar[0]
+    sendKeystringLocal(modchars[0] + modifiedchar[1], LYX_PID)
+    if special:
+        # give the os time to update the status info (in /proc)
+        time.sleep(controlkey_delay)
+    sendKeystring(line, LYX_PID)
 
 def system_retry(num_retry, cmd):
     i = 0
-    rtn = intr_system(cmd)
+    rtn = intr_system(cmd, True)
     while ( ( i < num_retry ) and ( rtn != 0) ):
         i = i + 1
-        rtn = intr_system(cmd)
+        rtn = intr_system(cmd, True)
         time.sleep(1)
     if ( rtn != 0 ):
         print("Command Failed: "+cmd)
-        print(" EXITING!\n")
-        os._exit(1)
+        die(1," EXITING!")
 
 def RaiseWindow():
     #intr_system("echo x-session-manager PID: $X_PID.")
@@ -299,6 +476,74 @@ def RaiseWindow():
     intr_system("wmctrl -R '"+lyx_window_name+"' ;sleep 0.1")
     system_retry(30, "wmctrl -i -a '"+lyx_window_name+"'")
 
+class Shortcuts:
+
+    def __init__(self):
+        self.shortcut_entry = re.compile(r'^\s*"([^"]+)"\s*\"([^"]+)\"')
+        self.bindings = {}
+        self.bind = re.compile(r'^\s*\\bind\s+"([^"]+)"')
+        if lyx_userdir_ver is None:
+            self.dir = lyx_userdir
+        else:
+            self.dir = lyx_userdir_ver
+
+    def __UseShortcut(self, c):
+        m = self.shortcut_entry.match(c)
+        if m:
+            sh = m.group(1)
+            fkt = m.group(2)
+            self.bindings[sh] = fkt
+        else:
+            die(1, "cad shortcut spec(" + c + ")")
+
+    def __PrepareShortcuts(self):
+        if not self.dir is None:
+            tmp = tempfile.NamedTemporaryFile(suffix='.bind', delete=False)
+            try:
+                old = open(self.dir + '/bind/user.bind', 'r')
+            except IOError as e:
+                old = None
+            if not old is None:
+                lines = old.read().split("\n")
+                old.close()
+                bindfound = False
+                for line in lines:
+                    m = self.bind.match(line)
+                    if m:
+                        bindfound = True
+                        val = m.group(1)
+                        if val in self.bindings:
+                            if self.bindings[val] != "":
+                                tmp.write("\\bind \"" + val + "\" \"" + self.bindings[val] + "\"\n")
+                                self.bindings[val] = ""
+                        else:
+                            tmp.write(line + '\n')
+                    elif not bindfound:
+                        tmp.write(line + '\n')
+            else:
+                tmp.writelines(
+                    '## This file is used for keytests only\n\n' +
+                    'Format 4\n\n'
+                )
+            for val in self.bindings:
+                if not self.bindings[val] is None:
+                    if  self.bindings[val] != "":
+                        tmp.write("\\bind \"" + val + "\" \"" + self.bindings[val] + "\"\n")
+                        self.bindings[val] = ""
+            tmp.close()
+            shutil.move(tmp.name, self.dir + '/bind/user.bind')
+        else:
+            print("User dir not specified")
+
+    def dispatch(self, c):
+        if c[0:12] == 'UseShortcut ':
+            self.__UseShortcut(c[12:])
+        elif c == 'PrepareShortcuts':
+            print('Preparing usefull sortcuts for tests')
+            self.__PrepareShortcuts()
+        else:
+            return False
+        return True
 
 lyx_pid = os.environ.get('LYX_PID')
 print('lyx_pid: ' + str(lyx_pid) + '\n')
@@ -306,13 +551,23 @@ infilename = os.environ.get('KEYTEST_INFILE')
 outfilename = os.environ.get('KEYTEST_OUTFILE')
 max_drop = os.environ.get('MAX_DROP')
 lyx_window_name = os.environ.get('LYX_WINDOW_NAME')
+lyx_other_window_name = None
 screenshot_out = os.environ.get('SCREENSHOT_OUT')
 lyx_userdir = os.environ.get('LYX_USERDIR')
+lyx_userdir_ver = os.environ.get('LYX_USERDIR_24x')
+if lyx_userdir is None:
+    lyx_userdir = lyx_userdir_ver
 
 max_loops = os.environ.get('MAX_LOOPS')
 if max_loops is None:
     max_loops = 3
 
+extra_path = os.environ.get('EXTRA_PATH')
+if not extra_path is None:
+  os.environ['PATH'] = extra_path + os.pathsep + os.environ['PATH']
+  print("Added " + extra_path + " to path")
+  print(os.environ['PATH'])
+
 PACKAGE = os.environ.get('PACKAGE')
 if not PACKAGE is None:
   print("PACKAGE = " + PACKAGE + "\n")
@@ -336,6 +591,11 @@ if xvkbd_exe is None:
 qt_frontend = os.environ.get('QT_FRONTEND')
 if qt_frontend is None:
     qt_frontend = 'QT4'
+if qt_frontend == 'QT5':
+    # Some tests sometimes failed with value 0.01 on Qt5.8
+    controlkey_delay = 0.4
+else:
+    controlkey_delay = 0.4
 
 locale_dir = os.environ.get('LOCALE_DIR')
 if locale_dir is None:
@@ -343,7 +603,10 @@ if locale_dir is None:
 
 def_delay = os.environ.get('XVKBD_DELAY')
 if def_delay is None:
-    def_delay = '100'
+    if qt_frontend == 'QT5':
+        def_delay = '1'
+    else:
+        def_delay = '1'
 
 file_new_command = os.environ.get('FILE_NEW_COMMAND')
 if file_new_command is None:
@@ -382,55 +645,70 @@ if not lyx_pid is None:
 
 write_commands = True
 failed = False
-
+lineempty = re.compile(r'^\s*$')
+marked = ControlFile()
+shortcuts = Shortcuts()
 while not failed:
     #intr_system('echo -n LOADAVG:; cat /proc/loadavg')
     c = x.getCommand()
     if c is None:
         break
-    if c.strip() == "":
+
+    # Do not strip trailing spaces, only check for 'empty' lines
+    if lineempty.match(c):
         continue
     outfile.writelines(c + '\n')
     outfile.flush()
+    if marked.dispatch(c):
+        continue
+    elif shortcuts.dispatch(c):
+        continue
     if c[0] == '#':
         print("Ignoring comment line: " + c)
     elif c[0:9] == 'TestBegin':
         print("\n")
-        lyx_pid=os.popen("pidof " + lyx).read()
+        lyx_pid=get_proc_pid(lyx)
         if lyx_pid != "":
             print("Found running instance(s) of LyX: " + lyx_pid + ": killing them all\n")
             intr_system("killall " + lyx, True)
             time.sleep(0.5)
             intr_system("killall -KILL " + lyx, True)
-        time.sleep(0.2)
+            time.sleep(0.2)
         print("Starting LyX . . .")
         if lyx_userdir is None:
             intr_system(lyx_exe + c[9:] + "&")
         else:
             intr_system(lyx_exe + " -userdir " + lyx_userdir + " " + c[9:] + "&")
-        count = 5
+        count = 10
+        old_lyx_pid = "-7"
+        old_lyx_window_name = None
+        print("Waiting for LyX to show up . . .")
         while count > 0:
-            lyx_pid=os.popen("pidof " + lyx).read().rstrip()
-            print('lyx_pid=' + lyx_pid, '\n')
+            lyx_pid=get_proc_pid(lyx)
+            if lyx_pid != old_lyx_pid:
+                print('lyx_pid=' + lyx_pid)
+                old_lyx_pid = lyx_pid
             if lyx_pid != "":
-                lyx_window_name=os.popen("wmctrl -l -p | grep ' " + str(lyx_pid) +  " ' | cut -d ' ' -f 1").read().rstrip()
-                print('lyx_win=' + lyx_window_name, '\n')
-                if lyx_window_name != "":
+                lyx_window_name=get_proc_win_id(lyx_pid, "")
+                if not lyx_window_name is None:
+                    if old_lyx_window_name != lyx_window_name:
+                        print('lyx_win=' + lyx_window_name, '\n')
+                        old_lyx_window_name = lyx_window_name
                     break
             else:
                 count = count - 1
-            print('lyx_win: ' + lyx_window_name + '\n')
-            print("Waiting for LyX to show up . . .")
-            time.sleep(1)
+            time.sleep(0.5)
         if count <= 0:
             print('Timeout: could not start ' + lyx_exe, '\n')
             sys.stdout.flush()
             failed = True
-        print('lyx_pid: ' + lyx_pid + '\n')
-        print('lyx_win: ' + lyx_window_name + '\n')
-        sendKeystringLocal("\C\[Home]", lyx_pid)
+        else:
+            print('lyx_pid: ' + lyx_pid)
+            print('lyx_win: ' + lyx_window_name)
+            dead_expected = False
+            sendKeystring("\C\[Home]", lyx_pid)
     elif c[0:5] == 'Sleep':
-        print("Sleeping for " + c[6:] + " seconds\n")
+        print("Sleeping for " + c[6:] + " seconds")
         time.sleep(float(c[6:]))
     elif c[0:4] == 'Exec':
         cmd = c[5:].rstrip()
@@ -448,55 +726,87 @@ while not failed:
         else:
             ##intr_system('killall lyx; sleep 2 ; killall -9 lyx')
             if lyx_pid is None:
-              print('No path /proc/xxxx/status, exiting')
+              die(1, 'No path /proc/xxxx/status, exiting')
             else:
-              print('No path /proc/' + lyx_pid + '/status, exiting')
-            os._exit(1)
+              die(1, 'No path /proc/' + lyx_pid + '/status, exiting')
     elif c[0:4] == 'KD: ':
         key_delay = c[4:].rstrip('\n')
-        print('Setting DELAY to ' + key_delay + '.\n')
+        print('Setting DELAY to ' + key_delay)
     elif c == 'Loop':
         RaiseWindow()
         sendKeystring(ResetCommand, lyx_pid)
     elif c[0:6] == 'Assert':
         cmd = c[7:].rstrip()
-        result = intr_system(cmd)
+        result = intr_system(cmd, True)
         failed = failed or (result != 0)
         print("result=" + str(result) + ", failed=" + str(failed))
+    elif c[0:15] == 'TestEndWithKill':
+        marked.closeall()
+        cmd = c[16:].rstrip()
+        if lyx_dead(lyx_pid):
+            print("LyX instance not found because of crash or assert !\n")
+            failed = True
+        else:
+            print("    ------------    Forcing kill of lyx instance: " + str(lyx_pid) + "    ------------")
+            # This line below is there only to allow lyx to update its log-file
+            sendKeystring("\[Escape]", lyx_pid)
+            dead_expected = True
+            while not lyx_dead(lyx_pid):
+                intr_system("kill -9 " + str(lyx_pid), True);
+                time.sleep(0.5)
+            if cmd != "":
+                print("Executing " + cmd)
+                result = intr_system(cmd, True)
+                failed = failed or (result != 0)
+                print("result=" + str(result) + ", failed=" + str(failed))
+            else:
+                print("failed=" + str(failed))
     elif c[0:7] == 'TestEnd':
-#        time.sleep(0.5)
-        if lyx_dead():
+         #lyx_other_window_name = None
+        if lyx_dead(lyx_pid):
             print("LyX instance not found because of crash or assert !\n")
+            marked.closeall()
             failed = True
         else:
-            print("Forcing quit of lyx instance: " + str(lyx_pid) + "...\n")
-            # \Ax Enter command line is sometimes blocked
-           # \[Escape] works after this
-           sendKeystring("\Ax\[Escape]", lyx_pid)
-           # now we should be outside any dialog
-           # and so the function lyx-quit should work
+            print("    ------------    Forcing quit of lyx instance: " + str(lyx_pid) + "    ------------")
+            # \[Escape]+ should work as RESET focus to main window
+            sendKeystring("\[Escape]\[Escape]\[Escape]\[Escape]", lyx_pid)
+            # now we should be outside any dialog
+            # and so the function lyx-quit should work
             sendKeystring("\Cq", lyx_pid)
+            marked.dispatch('CP: action=lyx-quit')
+            marked.dispatch('CC:')
             time.sleep(0.5)
-            if lyx_sleeping():
+            dead_expected = True
+            is_sleeping = wait_until_lyx_sleeping(lyx_pid)
+            if is_sleeping:
+                print('wait_until_lyx_sleeping() indicated "sleeping"')
+                # For a short time lyx-status is 'sleeping', even if it is nearly dead.
+                # Without the wait below, the \[Tab]-char is sent to nirvana
+                # causing a 'beep'
+                time.sleep(0.5)
                 # probably waiting for Save/Discard/Abort, we select 'Discard'
                 sendKeystring("\[Tab]\[Return]", lyx_pid)
                 lcount = 0
             else:
                 lcount = 1
-            while not lyx_dead():
+            while not lyx_dead(lyx_pid):
                 lcount = lcount + 1
                 if lcount > 20:
                     print("LyX still up, killing process and waiting for it to die...\n")
                     intr_system("kill -9 " + str(lyx_pid), True);
                 time.sleep(0.5)
         cmd = c[8:].rstrip()
-        print("Executing " + cmd)
-        result = intr_system(cmd)
-        failed = failed or (result != 0)
-        print("result=" + str(result) + ", failed=" + str(failed))
+        if cmd != "":
+            print("Executing " + cmd)
+            result = intr_system(cmd, True)
+            failed = failed or (result != 0)
+            print("result=" + str(result) + ", failed=" + str(failed))
+        else:
+            print("failed=" + str(failed))
     elif c[0:4] == 'Lang':
         lang = c[5:].rstrip()
-        print("Setting LANG=" + lang + "\n")
+        print("Setting LANG=" + lang)
         os.environ['LANG'] = lang
         os.environ['LC_ALL'] = lang
 # If it doesn't exist, create a link <locale_dir>/<country-code>/LC_MESSAGES/lyx<version-suffix>.mo
@@ -508,7 +818,7 @@ while not failed:
         else:
             ccode = lang
 
-        print("Setting LANGUAGE=" + ccode + "\n")
+        print("Setting LANGUAGE=" + ccode)
         os.environ['LANGUAGE'] = ccode
 
         idx = lang.find("_")
@@ -527,7 +837,7 @@ while not failed:
             print('Could not determine PACKAGE name needed for translations\n')
             failed = True
         else:
-          lyx_name = PACKAGE
+            lyx_name = PACKAGE
         intr_system("mkdir -p " + locale_dir + "/" + ccode + "/LC_MESSAGES")
         intr_system("rm -f " + locale_dir + "/" + ccode + "/LC_MESSAGES/" + lyx_name + ".mo")
         if PO_BUILD_DIR is None:
@@ -542,10 +852,8 @@ while not failed:
         print("Unrecognised Command '" + c + "'\n")
         failed = True
 
-print("Test case terminated: ")
+print("Test case terminated: ", end = '')
 if failed:
-    print("FAIL\n")
-    os._exit(1)
+    die(1,"FAIL")
 else:
-    print("Ok\n")
-    os._exit(0)
+    die(0, "Ok")