]> git.lyx.org Git - lyx.git/blobdiff - development/autotests/keytest.py
Adding binary path for Homebrew on MacOS-arm64 (bug 12619).
[lyx.git] / development / autotests / keytest.py
index a785709e94f4f824804698f715904a4c8d91ac53..ce3b2456534a7572748eac54b072ef8752efbb8d 100755 (executable)
@@ -14,6 +14,9 @@ import os
 import re
 import sys
 import time
+import tempfile
+import shutil
+
 #from subprocess import call
 import subprocess
 
@@ -164,48 +167,70 @@ class CommandSourceFromFile(CommandSource):
 class ControlFile:
 
     def __init__(self):
-        self.control = re.compile(r'^(C[ONPRC]):\s*(.*)$')
-        self.cntrname = None
-        self.cntrfile = None
-
-    def open(self, filename):
-        self.cntrname = filename
-        self.cntrfile = open(filename, 'w')
+        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 close(self):
-        if not self.cntrfile is None:
-            self.cntrfile.close()
-            self.cntrfile = None
-            self.cntrname = None
+    def closeall(self):
+        handles = self.cntrfile.keys()
+        for handle in handles:
+            self.__close(handle)
 
-    def addline(self, pat):
-        self.cntrfile.writelines(pat + "\n")
+    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 + ")")
 
-    def getfname(self):
-        return self.cntrname
+    # 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)
-        text = m.group(2)
+        handle = m.group(2)
+        if handle is None:
+            handle = ""
+        text = m.group(3)
         if command == "CO":
-            self.open(text);
+            self.__open(handle, text);
         elif command == "CC":
-            self.close()
+            self.__close(handle)
         else:
-            if self.cntrfile is None:
-                print("Controlfile not initialized")
-            else:
-                if command == "CN":
-                    self.addline("Comment: " + text)
-                elif command == "CP":
-                    self.addline("Simple: " + text)
-                elif command == "CR":
-                    self.addline("Regex: " + text)
+            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
 
 
@@ -257,7 +282,7 @@ def printresstatus():
 
 def lyx_status_retry(pid):
     resstatus = []
-    if lyx_pid is None:
+    if pid is None:
         print('Pid is None')
         return "dead"
     fname = '/proc/' + pid + '/status'
@@ -361,80 +386,83 @@ def sendKeystringLocal(keystr, LYX_PID):
     subprocess.call(xvpar, stdout = FNULL, stderr = FNULL)
     sys.stdout.flush()
 
-Axreg = re.compile(r'^(.*)\\Ax([^\\]*)(.*)$')
-returnreg = re.compile(r'(\\\[[A-Z][a-z]+\])(.*)$')
-
-# recursive wrapper around sendKeystringLocal()
-# handling \Ax-entries
-def sendKeystringAx(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
-            sendKeystringAx(prefix, LYX_PID)
-        sendKeystringLocal('\Ax', LYX_PID)
-        time.sleep(0.1)
-        m2 = returnreg.match(rest)
-        if m2:
-            line = m2.group(2)
-            ctrlk = m2.group(1)
-            key_delay = "1"
-            sendKeystringLocal(content + ctrlk, LYX_PID)
-            key_delay = saved_delay
-            time.sleep(controlkey_delay)
-            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)
-
-specialkeyreg = re.compile(r'(.+)(\\[AC]([a-zA-Z]|\\\[[A-Z][a-z]+\]).*)$')
-# Split line at start of each meta or controll char
-
-def sendKeystringAC(line, LYX_PID):
-    m = specialkeyreg.match(line)
+        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:
-        first = m.group(1)
-        second = m.group(2)
-        sendKeystringAC(first, LYX_PID)
-        sendKeystringAC(second, LYX_PID)
+        res[0] = False
+        res[1] = m.group(1)
+        res[2] = m.group(2)
     else:
-        sendKeystringAx(line, LYX_PID)
+        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
 
-controlkeyreg = re.compile(r'^(.*\\\[[A-Z][a-z]+\])(.*\\\[[A-Z][a-z]+\])(.*)$')
-# Make sure, only one of \[Return], \[Tab], \[Down], \[Home] etc are in one sent line
-# e.g. split the input line on each keysym
-def sendKeystringRT(line, LYX_PID):
-    m = controlkeyreg.match(line)
-    if m:
-        first = m.group(1)
-        second = m.group(2)
-        third = m.group(3)
-        sendKeystringRT(first, LYX_PID)
-        time.sleep(controlkey_delay)
-        sendKeystringRT(second, LYX_PID)
+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)
-        if third != "":
-            sendKeystringRT(third, LYX_PID)
-    else:
-        sendKeystringAC(line, LYX_PID)
+    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)
@@ -448,6 +476,95 @@ 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.unbindings = {}
+        self.bind = re.compile(r'^\s*\\(un)?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 __UnuseShortcut(self, c):
+        m = self.shortcut_entry.match(c)
+        if m:
+            sh = m.group(1)
+            fkt = m.group(2)
+            self.unbindings[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(2)
+                        if val in self.bindings:
+                            if self.bindings[val] != "":
+                                tmp.write("\\bind \"" + val + "\" \"" + self.bindings[val] + "\"\n")
+                                self.bindings[val] = ""
+                        elif val in self.unbindings:
+                            if self.unbindings[val] != "":
+                                tmp.write("\\unbind \"" + val + "\" \"" + self.unbindings[val] + "\"\n")
+                                self.unbindings[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] = ""
+            for val in self.unbindings:
+                if not self.unbindings[val] is None:
+                    if self.unbindings[val] != "":
+                        tmp.write("\\unbind \"" + val + "\" \"" + self.unbindings[val] + "\"\n")
+                        self.unbindings[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[0:14] == 'UnuseShortcut ':
+            self.__UnuseShortcut(c[14:])
+        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')
@@ -458,6 +575,9 @@ 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:
@@ -491,9 +611,10 @@ if xvkbd_exe is None:
 
 qt_frontend = os.environ.get('QT_FRONTEND')
 if qt_frontend is None:
-    qt_frontend = 'QT4'
+    qt_frontend = 'QT5'
 if qt_frontend == 'QT5':
-    controlkey_delay = 0.01
+    # Some tests sometimes failed with value 0.01 on Qt5.8
+    controlkey_delay = 0.4
 else:
     controlkey_delay = 0.4
 
@@ -541,12 +662,13 @@ outfile = open(outfilename, 'w')
 if not lyx_pid is None:
     RaiseWindow()
     # Next command is language dependent
-    #sendKeystringRT("\Afn", lyx_pid)
+    #sendKeystring("\Afn", lyx_pid)
 
 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()
@@ -560,6 +682,8 @@ while not failed:
     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':
@@ -603,8 +727,7 @@ while not failed:
             print('lyx_pid: ' + lyx_pid)
             print('lyx_win: ' + lyx_window_name)
             dead_expected = False
-            sendKeystringLocal("\C\[Home]", lyx_pid)
-            time.sleep(controlkey_delay)
+            sendKeystring("\C\[Home]", lyx_pid)
     elif c[0:5] == 'Sleep':
         print("Sleeping for " + c[6:] + " seconds")
         time.sleep(float(c[6:]))
@@ -620,7 +743,7 @@ while not failed:
         RaiseWindow()
     elif c[0:4] == 'KK: ':
         if lyx_exists():
-            sendKeystringRT(c[4:], lyx_pid)
+            sendKeystring(c[4:], lyx_pid)
         else:
             ##intr_system('killall lyx; sleep 2 ; killall -9 lyx')
             if lyx_pid is None:
@@ -632,14 +755,14 @@ while not failed:
         print('Setting DELAY to ' + key_delay)
     elif c == 'Loop':
         RaiseWindow()
-        sendKeystringRT(ResetCommand, lyx_pid)
+        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.close()
+        marked.closeall()
         cmd = c[16:].rstrip()
         if lyx_dead(lyx_pid):
             print("LyX instance not found because of crash or assert !\n")
@@ -647,33 +770,33 @@ while not failed:
         else:
             print("    ------------    Forcing kill of lyx instance: " + str(lyx_pid) + "    ------------")
             # This line below is there only to allow lyx to update its log-file
-            sendKeystringLocal("\[Escape]", lyx_pid)
+            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)
+                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':
-        marked.close()
-        #lyx_other_window_name = None
+         #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) + "    ------------")
-            # \Ax Enter command line is sometimes blocked
-            # \[Escape] works after this
-            sendKeystringAx("\Ax\[Escape]", lyx_pid)
-            time.sleep(controlkey_delay)
+            # \[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
-            sendKeystringLocal("\Cq", lyx_pid)
+            sendKeystring("\Cq", lyx_pid)
+            marked.dispatch('CP: action=lyx-quit')
+            marked.dispatch('CC:')
             time.sleep(0.5)
             dead_expected = True
             is_sleeping = wait_until_lyx_sleeping(lyx_pid)
@@ -684,7 +807,7 @@ while not failed:
                 # causing a 'beep'
                 time.sleep(0.5)
                 # probably waiting for Save/Discard/Abort, we select 'Discard'
-                sendKeystringRT("\[Tab]\[Return]", lyx_pid)
+                sendKeystring("\[Tab]\[Return]", lyx_pid)
                 lcount = 0
             else:
                 lcount = 1
@@ -697,7 +820,7 @@ while not failed:
         cmd = c[8:].rstrip()
         if cmd != "":
             print("Executing " + cmd)
-            result = intr_system(cmd)
+            result = intr_system(cmd, True)
             failed = failed or (result != 0)
             print("result=" + str(result) + ", failed=" + str(failed))
         else:
@@ -735,7 +858,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:
@@ -750,7 +873,7 @@ while not failed:
         print("Unrecognised Command '" + c + "'\n")
         failed = True
 
-print("Test case terminated: ")
+print("Test case terminated: ", end = '')
 if failed:
     die(1,"FAIL")
 else: