]> git.lyx.org Git - lyx.git/blob - development/autotests/keytest.py
keytests: Print proc-info in case lyx_status() signals "dead"
[lyx.git] / development / autotests / keytest.py
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3 # This script generates hundreds of random keypresses per second,
4 #  and sends them to the lyx window
5 # It requires xvkbd and wmctrl
6 # It generates a log of the KEYCODES it sends as development/keystest/out/KEYCODES
7 #
8 # Adapted by Tommaso Cucinotta from the original MonKey Test by
9 # John McCabe-Dansted.
10
11 from __future__ import print_function
12 import random
13 import os
14 import re
15 import sys
16 import time
17 #from subprocess import call
18 import subprocess
19
20 print('Beginning keytest.py')
21
22 FNULL = open('/dev/null', 'w')
23
24 key_delay = ''
25
26 class CommandSource:
27
28     def __init__(self):
29         keycode = [
30             "\[Left]",
31             '\[Right]',
32             '\[Down]',
33             '\[Up]',
34             '\[BackSpace]',
35             '\[Delete]',
36             '\[Escape]',
37             ]
38         keycode[:0] = keycode
39         keycode[:0] = keycode
40
41         keycode[:0] = ['\\']
42
43         for k in range(97, 123):
44             keycode[:0] = chr(k)
45
46         for k in range(97, 123):
47             keycode[:0] = ["\A" + chr(k)]
48
49         for k in range(97, 123):
50             keycode[:0] = ["\A" + chr(k)]
51
52         for k in range(97, 123):
53             keycode[:0] = ["\C" + chr(k)]
54
55         self.keycode = keycode
56         self.count = 0
57         self.count_max = 1999
58
59     def getCommand(self):
60         self.count = self.count + 1
61         if self.count % 200 == 0:
62             return 'RaiseLyx'
63         elif self.count > self.count_max:
64             os._exit(0)
65         else:
66             keystr = ''
67             for k in range(1, 2):
68                 keystr = keystr + self.keycode[random.randint(1,
69                         len(self.keycode)) - 1]
70             return 'KK: ' + keystr
71
72
73 class CommandSourceFromFile(CommandSource):
74
75     def __init__(self, filename, p):
76
77         self.infile = open(filename, 'r')
78         self.lines = self.infile.readlines()
79         self.infile.close()
80         linesbak = self.lines
81         self.p = p
82         print(p, self.p, 'self.p')
83         self.i = 0
84         self.count = 0
85         self.loops = 0
86
87         # Now we start randomly dropping lines, which we hope are redundant
88         # p is the probability that any given line will be removed
89
90         if p > 0.001:
91             if random.uniform(0, 1) < 0.5:
92                 print('randomdrop_independant\n')
93                 self.randomdrop_independant()
94             else:
95                 print('randomdrop_slice\n')
96                 self.randomdrop_slice()
97         if screenshot_out is None:
98             count_atleast = 100
99         else:
100             count_atleast = 1
101         self.max_count = max(len(self.lines) + 20, count_atleast)
102         if len(self.lines) < 1:
103             self.lines = linesbak
104
105     def randomdrop_independant(self):
106         p = self.p
107
108         # The next couple of lines are to ensure that at least one line is dropped
109
110         drop = random.randint(0, len(self.lines) - 1)
111         del self.lines[drop]
112         #p = p - 1 / len(self.lines)
113         origlines = self.lines
114         self.lines = []
115         for l in origlines:
116             if random.uniform(0, 1) < self.p:
117                 print('Randomly dropping line ' + l + '\n')
118             else:
119                 self.lines.append(l)
120         print('LINES\n')
121         print(self.lines)
122         sys.stdout.flush()
123
124     def randomdrop_slice(self):
125         lines = self.lines
126         if random.uniform(0, 1) < 0.4:
127             lines.append(lines[0])
128             del lines[0]
129         num_lines = len(lines)
130         max_drop = max(5, num_lines / 5)
131         num_drop = random.randint(1, 5)
132         drop_mid = random.randint(0, num_lines)
133         drop_start = max(drop_mid - num_drop / 2, 0)
134         drop_end = min(drop_start + num_drop, num_lines)
135         print(drop_start, drop_mid, drop_end)
136         print(lines)
137         del lines[drop_start:drop_end]
138         print(lines)
139         self.lines = lines
140
141     def getCommand(self):
142         if self.count >= self.max_count:
143             os._exit(0)
144         if self.i >= len(self.lines):
145             self.loops = self.loops + 1
146             if self.loops >= int(max_loops):
147                 return None
148             self.i = 0
149             return 'Loop'
150         line = self.lines[self.i].rstrip('\n')
151         self.count = self.count + 1
152         self.i = self.i + 1
153         return line
154
155 def get_proc_pid(proc_name):
156     pid=os.popen("pidof " + proc_name).read().rstrip()
157     return pid
158
159 wlistreg = re.compile(r'^(0x[0-9a-f]{5,9})\s+[^\s]+\s+([0-9]+)\s.*$')
160 def get_proc_win_id(pid, ignoreid):
161     nlist = os.popen("wmctrl -l -p").read()
162     wlist = nlist.split("\n")
163     for item in wlist:
164         m = wlistreg.match(item)
165         if m:
166             win_id = m.group(1)
167             win_pid = m.group(2)
168             if win_pid == pid:
169                 if win_id != ignoreid:
170                     return win_id
171     return None
172
173 def lyx_exists():
174     if lyx_pid is None:
175         return False
176     fname = '/proc/' + lyx_pid + '/status'
177     return os.path.exists(fname)
178
179
180 # Interruptible os.system()
181 def intr_system(cmd, ignore_err = False):
182     print("Executing " + cmd)
183     ret = os.system(cmd)
184     if os.WIFSIGNALED(ret):
185         raise KeyboardInterrupt
186     if ret != 0 and not ignore_err:
187         raise BaseException("command failed:" + cmd)
188     return ret
189
190 statreg = re.compile(r'^State:.*\(([a-z]+)\)')
191
192 resstatus = []
193 def printresstatus():
194     for line in resstatus:
195         line = line.rstrip()
196         print("    " + line.rstrip())
197     print('End of /proc-lines')
198
199 def lyx_status(pid):
200     resstatus = []
201     if lyx_pid is None:
202         return "dead"
203     fname = '/proc/' + pid + '/status'
204     status = "dead"
205     try:
206         f = open(fname)
207         found = False
208         for line in f:
209             resstatus.extend([line])
210             m = statreg.match(line)
211             if m:
212                 status = m.group(1)
213                 found = True
214         f.close()
215         return status
216     except IOError as e:
217         print("I/O error({0}): {1}".format(e.errno, e.strerror))
218         return "dead"
219     except:
220         print("Unexpected error:", sys.exc_info()[0])
221         return "dead"
222     return status
223
224 # Return true if LyX (identified via lyx_pid) is sleeping
225 def lyx_sleeping():
226     return lyx_status(lyx_pid) == "sleeping"
227
228 # Return true if LyX (identified via lyx_pid) is zombie
229 def lyx_zombie():
230     return lyx_status(lyx_pid) == "zombie"
231
232 def lyx_dead():
233     status = lyx_status(lyx_pid)
234     return (status == "dead") or (status == "zombie")
235
236 def wait_until_lyx_sleeping():
237     before_secs = time.time()
238     while True:
239         status = lyx_status(lyx_pid)
240         if status == "sleeping":
241             return
242         if (status == "dead") or (status == "zombie"):
243             print('Lyx is dead, exiting')
244             printresstatus()
245             sys.stdout.flush()
246             os._exit(1)
247         if time.time() - before_secs > 180:
248             print('Killing due to freeze (KILL_FREEZE)')
249
250             # Do profiling, but sysprof has no command line interface?
251             # intr_system("killall -KILL lyx")
252             printresstatus()
253             sys.stdout.flush()
254             os._exit(1)
255         time.sleep(0.02)
256
257 def sendKeystringLocal(keystr, LYX_PID):
258     wait_until_lyx_sleeping()
259     if not screenshot_out is None:
260         print('Making Screenshot: ' + screenshot_out + ' OF ' + infilename)
261         time.sleep(0.2)
262         intr_system('import -window root '+screenshot_out+str(x.count)+".png")
263         time.sleep(0.1)
264     actual_delay = key_delay
265     if actual_delay == '':
266         actual_delay = def_delay
267     xvpar = [xvkbd_exe]
268     if qt_frontend == 'QT5':
269         xvpar.extend(["-jump-pointer", "-no-back-pointer"])
270     else:
271         xvpar.extend(["-xsendevent"])
272     if lyx_other_window_name is None:
273         xvpar.extend(["-window", lyx_window_name])
274     else:
275         xvpar.extend(["-window", lyx_other_window_name])
276     xvpar.extend(["-delay", actual_delay, "-text", keystr])
277     print("Sending \"" + keystr + "\"")
278     subprocess.call(xvpar, stdout = FNULL, stderr = FNULL)
279     sys.stdout.flush()
280
281 Axreg = re.compile(r'^(.*)\\Ax([^\\]*)(.*)$')
282 returnreg = re.compile(r'(\\\[[A-Z][a-z]+\])(.*)$')
283
284 # recursive wrapper around sendKeystringLocal()
285 # handling \Ax-entries
286 def sendKeystringAx(line, LYX_PID):
287     global key_delay
288     saved_delay = key_delay
289     m = Axreg.match(line)
290     if m:
291         prefix = m.group(1)
292         content = m.group(2)
293         rest = m.group(3);
294         if prefix != "":
295             # since (.*) is greedy, check prefix for '\Ax' again
296             sendKeystringAx(prefix, LYX_PID)
297         sendKeystringLocal('\Ax', LYX_PID)
298         time.sleep(0.1)
299         m2 = returnreg.match(rest)
300         if m2:
301             line = m2.group(2)
302             ctrlk = m2.group(1)
303             key_delay = "1"
304             sendKeystringLocal(content + ctrlk, LYX_PID)
305             key_delay = saved_delay
306             time.sleep(controlkey_delay)
307             if line != "":
308                 sendKeystringLocal(line, LYX_PID)
309         else:
310             if content != "":
311                 sendKeystringLocal(content, LYX_PID)
312             if rest != "":
313                 sendKeystringLocal(rest, LYX_PID)
314     else:
315         if line != "":
316             sendKeystringLocal(line, LYX_PID)
317
318 specialkeyreg = re.compile(r'(.+)(\\[AC]([a-zA-Z]|\\\[[A-Z][a-z]+\]).*)$')
319 # Split line at start of each meta or controll char
320
321 def sendKeystringAC(line, LYX_PID):
322     m = specialkeyreg.match(line)
323     if m:
324         first = m.group(1)
325         second = m.group(2)
326         sendKeystringAC(first, LYX_PID)
327         sendKeystringAC(second, LYX_PID)
328     else:
329         sendKeystringAx(line, LYX_PID)
330
331 controlkeyreg = re.compile(r'^(.*\\\[[A-Z][a-z]+\])(.*\\\[[A-Z][a-z]+\])(.*)$')
332 # Make sure, only one of \[Return], \[Tab], \[Down], \[Home] etc are in one sent line
333 # e.g. split the input line on each keysym
334 def sendKeystringRT(line, LYX_PID):
335     m = controlkeyreg.match(line)
336     if m:
337         first = m.group(1)
338         second = m.group(2)
339         third = m.group(3)
340         sendKeystringRT(first, LYX_PID)
341         time.sleep(controlkey_delay)
342         sendKeystringRT(second, LYX_PID)
343         time.sleep(controlkey_delay)
344         if third != "":
345             sendKeystringRT(third, LYX_PID)
346     else:
347         sendKeystringAC(line, LYX_PID)
348
349 def system_retry(num_retry, cmd):
350     i = 0
351     rtn = intr_system(cmd)
352     while ( ( i < num_retry ) and ( rtn != 0) ):
353         i = i + 1
354         rtn = intr_system(cmd)
355         time.sleep(1)
356     if ( rtn != 0 ):
357         print("Command Failed: "+cmd)
358         print(" EXITING!\n")
359         os._exit(1)
360
361 def RaiseWindow():
362     #intr_system("echo x-session-manager PID: $X_PID.")
363     #intr_system("echo x-session-manager open files: `lsof -p $X_PID | grep ICE-unix | wc -l`")
364     ####intr_system("wmctrl -l | ( grep '"+lyx_window_name+"' || ( killall lyx ; sleep 1 ; killall -9 lyx ))")
365     print("lyx_window_name = " + lyx_window_name + "\n")
366     intr_system("wmctrl -R '"+lyx_window_name+"' ;sleep 0.1")
367     system_retry(30, "wmctrl -i -a '"+lyx_window_name+"'")
368
369
370 lyx_pid = os.environ.get('LYX_PID')
371 print('lyx_pid: ' + str(lyx_pid) + '\n')
372 infilename = os.environ.get('KEYTEST_INFILE')
373 outfilename = os.environ.get('KEYTEST_OUTFILE')
374 max_drop = os.environ.get('MAX_DROP')
375 lyx_window_name = os.environ.get('LYX_WINDOW_NAME')
376 lyx_other_window_name = None
377 screenshot_out = os.environ.get('SCREENSHOT_OUT')
378 lyx_userdir = os.environ.get('LYX_USERDIR')
379
380 max_loops = os.environ.get('MAX_LOOPS')
381 if max_loops is None:
382     max_loops = 3
383
384 PACKAGE = os.environ.get('PACKAGE')
385 if not PACKAGE is None:
386   print("PACKAGE = " + PACKAGE + "\n")
387
388 PO_BUILD_DIR = os.environ.get('PO_BUILD_DIR')
389 if not PO_BUILD_DIR is None:
390   print("PO_BUILD_DIR = " + PO_BUILD_DIR + "\n")
391
392 lyx = os.environ.get('LYX')
393 if lyx is None:
394     lyx = "lyx"
395
396 lyx_exe = os.environ.get('LYX_EXE')
397 if lyx_exe is None:
398     lyx_exe = lyx
399
400 xvkbd_exe = os.environ.get('XVKBD_EXE')
401 if xvkbd_exe is None:
402     xvkbd_exe = "xvkbd"
403
404 qt_frontend = os.environ.get('QT_FRONTEND')
405 if qt_frontend is None:
406     qt_frontend = 'QT4'
407 if qt_frontend == 'QT5':
408     controlkey_delay = 0.01
409 else:
410     controlkey_delay = 0.4
411
412 locale_dir = os.environ.get('LOCALE_DIR')
413 if locale_dir is None:
414     locale_dir = '.'
415
416 def_delay = os.environ.get('XVKBD_DELAY')
417 if def_delay is None:
418     if qt_frontend == 'QT5':
419         def_delay = '5'
420     else:
421         def_delay = '1'
422
423 file_new_command = os.environ.get('FILE_NEW_COMMAND')
424 if file_new_command is None:
425     file_new_command = "\Afn"
426
427 ResetCommand = os.environ.get('RESET_COMMAND')
428 if ResetCommand is None:
429     ResetCommand = "\[Escape]\[Escape]\[Escape]\[Escape]" + file_new_command
430     #ResetCommand="\[Escape]\[Escape]\[Escape]\[Escape]\Cw\Cw\Cw\Cw\Cw\Afn"
431
432 if lyx_window_name is None:
433     lyx_window_name = 'LyX'
434
435 print('outfilename: ' + outfilename + '\n')
436 print('max_drop: ' + max_drop + '\n')
437
438 if infilename is None:
439     print('infilename is None\n')
440     x = CommandSource()
441     print('Using x=CommandSource\n')
442 else:
443     print('infilename: ' + infilename + '\n')
444     probability_we_drop_a_command = random.uniform(0, float(max_drop))
445     print('probability_we_drop_a_command: ')
446     print('%s' % probability_we_drop_a_command)
447     print('\n')
448     x = CommandSourceFromFile(infilename, probability_we_drop_a_command)
449     print('Using x=CommandSourceFromFile\n')
450
451 outfile = open(outfilename, 'w')
452
453 if not lyx_pid is None:
454     RaiseWindow()
455     # Next command is language dependent
456     #sendKeystringRT("\Afn", lyx_pid)
457
458 write_commands = True
459 failed = False
460 lineempty = re.compile(r'^\s*$')
461
462 while not failed:
463     #intr_system('echo -n LOADAVG:; cat /proc/loadavg')
464     c = x.getCommand()
465     if c is None:
466         break
467
468     # Do not strip trailing spaces, only check for 'empty' lines
469     if lineempty.match(c):
470         continue
471     outfile.writelines(c + '\n')
472     outfile.flush()
473     if c[0] == '#':
474         print("Ignoring comment line: " + c)
475     elif c[0:9] == 'TestBegin':
476         print("\n")
477         lyx_pid=get_proc_pid(lyx)
478         if lyx_pid != "":
479             print("Found running instance(s) of LyX: " + lyx_pid + ": killing them all\n")
480             intr_system("killall " + lyx, True)
481             time.sleep(0.5)
482             intr_system("killall -KILL " + lyx, True)
483         time.sleep(0.2)
484         print("Starting LyX . . .")
485         if lyx_userdir is None:
486             intr_system(lyx_exe + c[9:] + "&")
487         else:
488             intr_system(lyx_exe + " -userdir " + lyx_userdir + " " + c[9:] + "&")
489         count = 5
490         old_lyx_pid = "-7"
491         old_lyx_window_name = None
492         print("Waiting for LyX to show up . . .")
493         while count > 0:
494             lyx_pid=get_proc_pid(lyx)
495             if lyx_pid != old_lyx_pid:
496                 print('lyx_pid=' + lyx_pid)
497                 old_lyx_pid = lyx_pid
498             if lyx_pid != "":
499                 lyx_window_name=get_proc_win_id(lyx_pid, "")
500                 if not lyx_window_name is None:
501                     if old_lyx_window_name != lyx_window_name:
502                         print('lyx_win=' + lyx_window_name, '\n')
503                         old_lyx_window_name = lyx_window_name
504                     break
505             else:
506                 count = count - 1
507             time.sleep(1)
508         if count <= 0:
509             print('Timeout: could not start ' + lyx_exe, '\n')
510             sys.stdout.flush()
511             failed = True
512         else:
513             print('lyx_pid: ' + lyx_pid)
514             print('lyx_win: ' + lyx_window_name)
515             sendKeystringLocal("\C\[Home]", lyx_pid)
516             time.sleep(controlkey_delay)
517     elif c[0:5] == 'Sleep':
518         print("Sleeping for " + c[6:] + " seconds")
519         time.sleep(float(c[6:]))
520     elif c[0:4] == 'Exec':
521         cmd = c[5:].rstrip()
522         intr_system(cmd)
523     elif c == 'Loop':
524         outfile.close()
525         outfile = open(outfilename + '+', 'w')
526         print('Now Looping')
527     elif c == 'RaiseLyx':
528         print('Raising Lyx')
529         RaiseWindow()
530     elif c[0:4] == 'KK: ':
531         if lyx_exists():
532             sendKeystringRT(c[4:], lyx_pid)
533         else:
534             ##intr_system('killall lyx; sleep 2 ; killall -9 lyx')
535             if lyx_pid is None:
536               print('No path /proc/xxxx/status, exiting')
537             else:
538               print('No path /proc/' + lyx_pid + '/status, exiting')
539             os._exit(1)
540     elif c[0:4] == 'KD: ':
541         key_delay = c[4:].rstrip('\n')
542         print('Setting DELAY to ' + key_delay)
543     elif c == 'Loop':
544         RaiseWindow()
545         sendKeystringRT(ResetCommand, lyx_pid)
546     elif c[0:6] == 'Assert':
547         cmd = c[7:].rstrip()
548         result = intr_system(cmd)
549         failed = failed or (result != 0)
550         print("result=" + str(result) + ", failed=" + str(failed))
551     elif c[0:15] == 'TestEndWithKill':
552         cmd = c[16:].rstrip()
553         if lyx_dead():
554             print("LyX instance not found because of crash or assert !\n")
555             failed = True
556         else:
557             print("    ------------    Forcing kill of lyx instance: " + str(lyx_pid) + "    ------------")
558             # This line below is there only to allow lyx to update its log-file
559             sendKeystringLocal("\[Escape]", lyx_pid)
560             while not lyx_dead():
561                 intr_system("kill -9 " + str(lyx_pid), True);
562                 time.sleep(0.5)
563             if cmd != "":
564                 print("Executing " + cmd)
565                 result = intr_system(cmd)
566                 failed = failed or (result != 0)
567                 print("result=" + str(result) + ", failed=" + str(failed))
568             else:
569                 print("failed=" + str(failed))
570     elif c[0:7] == 'TestEnd':
571         #lyx_other_window_name = None
572         if lyx_dead():
573             print("LyX instance not found because of crash or assert !\n")
574             failed = True
575         else:
576             print("    ------------    Forcing quit of lyx instance: " + str(lyx_pid) + "    ------------")
577             # \Ax Enter command line is sometimes blocked
578             # \[Escape] works after this
579             sendKeystringAx("\Ax\[Escape]", lyx_pid)
580             time.sleep(controlkey_delay)
581             # now we should be outside any dialog
582             # and so the function lyx-quit should work
583             sendKeystringLocal("\Cq", lyx_pid)
584             time.sleep(0.5)
585             if lyx_sleeping():
586                 # probably waiting for Save/Discard/Abort, we select 'Discard'
587                 sendKeystringRT("\[Tab]\[Return]", lyx_pid)
588                 lcount = 0
589             else:
590                 lcount = 1
591             while not lyx_dead():
592                 lcount = lcount + 1
593                 if lcount > 20:
594                     print("LyX still up, killing process and waiting for it to die...\n")
595                     intr_system("kill -9 " + str(lyx_pid), True);
596                 time.sleep(0.5)
597         cmd = c[8:].rstrip()
598         if cmd != "":
599             print("Executing " + cmd)
600             result = intr_system(cmd)
601             failed = failed or (result != 0)
602             print("result=" + str(result) + ", failed=" + str(failed))
603         else:
604             print("failed=" + str(failed))
605     elif c[0:4] == 'Lang':
606         lang = c[5:].rstrip()
607         print("Setting LANG=" + lang)
608         os.environ['LANG'] = lang
609         os.environ['LC_ALL'] = lang
610 # If it doesn't exist, create a link <locale_dir>/<country-code>/LC_MESSAGES/lyx<version-suffix>.mo
611 # pointing to the corresponding .gmo file. Needed to let lyx find the right translation files.
612 # See http://www.mail-archive.com/lyx-devel@lists.lyx.org/msg165613.html
613         idx = lang.rfind(".")
614         if idx != -1:
615             ccode = lang[0:idx]
616         else:
617             ccode = lang
618
619         print("Setting LANGUAGE=" + ccode)
620         os.environ['LANGUAGE'] = ccode
621
622         idx = lang.find("_")
623         if idx != -1:
624             short_code = lang[0:idx]
625         else:
626             short_code = ccode
627         lyx_dir = os.popen("dirname \"" + lyx_exe + "\"").read().rstrip()
628         if PACKAGE is None:
629           # on cmake-build there is no Makefile in this directory
630           # so PACKAGE has to be provided
631           if os.path.exists(lyx_dir + "/Makefile"):
632             print("Executing: grep 'PACKAGE =' " + lyx_dir + "/Makefile | sed -e 's/PACKAGE = \(.*\)/\\1/'")
633             lyx_name = os.popen("grep 'PACKAGE =' " + lyx_dir + "/Makefile | sed -e 's/PACKAGE = \(.*\)/\\1/'").read().rstrip()
634           else:
635             print('Could not determine PACKAGE name needed for translations\n')
636             failed = True
637         else:
638           lyx_name = PACKAGE
639         intr_system("mkdir -p " + locale_dir + "/" + ccode + "/LC_MESSAGES")
640         intr_system("rm -f " + locale_dir + "/" + ccode + "/LC_MESSAGES/" + lyx_name + ".mo")
641         if PO_BUILD_DIR is None:
642             if lyx_dir[0:3] == "../":
643                 rel_dir = "../../" + lyx_dir
644             else:
645                 rel_dir = lyx_dir
646             intr_system("ln -s " + rel_dir + "/../po/" + short_code + ".gmo " + locale_dir + "/" + ccode + "/LC_MESSAGES/" + lyx_name + ".mo")
647         else:
648             intr_system("ln -s " + PO_BUILD_DIR + "/" + short_code + ".gmo " + locale_dir + "/" + ccode + "/LC_MESSAGES/" + lyx_name + ".mo")
649     else:
650         print("Unrecognised Command '" + c + "'\n")
651         failed = True
652
653 print("Test case terminated: ")
654 if failed:
655     print("FAIL\n")
656     os._exit(1)
657 else:
658     print("Ok\n")
659     os._exit(0)