Logo AND Algorithmique Numérique Distribuée

Public GIT Repository
tesh 'expect signal' can now accept more than one potential signal
[simgrid.git] / tools / tesh / tesh.py
index 8972bbc..2e0d94a 100755 (executable)
@@ -33,6 +33,7 @@ import re
 import difflib
 import signal
 import argparse
+import time
 
 if sys.version_info[0] == 3:
     import subprocess
@@ -115,27 +116,47 @@ except NameError:
 #
 
 # Global variable. Stores which process group should be killed (or None otherwise)
-running_pgids = list()
+running_pids = list()
 
-def kill_process_group(pgid):
-    if pgid is None:  # Nobody to kill. We don't know who to kill on windows, or we don't have anyone to kill on signal handler
+# Tests whether the process is dead already
+def process_is_dead(pid):
+    try:
+        os.kill(pid, 0)
+    except ProcessLookupError:
+        return True
+    except OSError as err:
+        if err.errno == errno.ESRCH: # ESRCH == No such process. The process is now dead
+            return True
+    return False
+
+# This function send TERM signal + KILL signal after 0.2s to the group of the specified process
+def kill_process_group(pid):
+    if pid is None:  # Nobody to kill. We don't know who to kill on windows, or we don't have anyone to kill on signal handler
         return
 
-    # print("Kill process group {}".format(pgid))
+    try:
+        pgid = os.getpgid(pid)
+    except:
+        # os.getpgid failed. Ok, don't cleanup.
+        return
+    
     try:
         os.killpg(pgid, signal.SIGTERM)
+        if process_is_dead(pid):
+            return
+        time.sleep(0.2)
+        os.killpg(pgid, signal.SIGKILL)
     except OSError:
         # os.killpg failed. OK. Some subprocesses may still be running.
         pass
 
-
 def signal_handler(signal, frame):
     print("Caught signal {}".format(SIGNALS_TO_NAMES_DICT[signal]))
-    global running_pgids
-    running_pgids_copy = running_pgids # Just in case of interthread conflicts.
-    for pgid in running_pgids_copy:
-        kill_process_group(pgid)
-    running_pgids.clear()
+    global running_pids
+    running_pids_copy = running_pids # Just in case of interthread conflicts.
+    for pid in running_pids_copy:
+        kill_process_group(pid)
+    running_pids.clear()
     tesh_exit(5)
 
 
@@ -217,7 +238,7 @@ class Cmd(object):
         self.cwd = os.getcwd()
 
         self.ignore_output = False
-        self.expect_return = 0
+        self.expect_return = [0]
 
         self.output_display = False
 
@@ -322,13 +343,14 @@ class Cmd(object):
 
         self.args += TeshState().args_suffix
 
-        print("[" + FileReader().filename + ":" + str(self.linenumber) + "] " + self.args)
+        logs = list()
+        logs.append("[{file}:{number}] {args}".format(file=FileReader().filename,
+            number=self.linenumber, args=self.args))
 
         args = shlex.split(self.args)
-        #print (args)
 
-        global running_pgids
-        local_pgid = None
+        global running_pids
+        local_pid = None
         global return_code
 
         try:
@@ -343,29 +365,30 @@ class Cmd(object):
                 stderr=subprocess.STDOUT,
                 universal_newlines=True,
                 preexec_fn=preexec_function)
-            try:
-                if not isWindows():
-                    local_pgid = os.getpgid(proc.pid)
-                    running_pgids.append(local_pgid)
-            except OSError:
-                # os.getpgid failed. OK. No cleanup.
-                pass
+            if not isWindows():
+                local_pid = proc.pid
+                running_pids.append(local_pid)
         except PermissionError:
-            print("[" + FileReader().filename + ":" + str(self.linenumber) +
-                  "] Cannot start '" + args[0] + "': The binary is not executable.")
-            print("[" + FileReader().filename + ":" + str(self.linenumber) + "] Current dir: " + os.getcwd())
+            logs.append("[{file}:{number}] Cannot start '{cmd}': The binary is not executable.".format(
+                file=FileReader().filename, number=self.linenumber, cmd=args[0]))
+            logs.append("[{file}:{number}] Current dir: {dir}".format(file=FileReader().filename,
+                number=self.linenumber, dir=os.getcwd()))
             return_code = max(3, return_code)
+            print('\n'.join(logs))
             return
         except NotADirectoryError:
-            print("[" + FileReader().filename + ":" + str(self.linenumber) + "] Cannot start '" +
-                  args[0] + "': The path to binary does not exist.")
-            print("[" + FileReader().filename + ":" + str(self.linenumber) + "] Current dir: " + os.getcwd())
+            logs.append("[{file}:{number}] Cannot start '{cmd}': The path to binary does not exist.".format(
+                file=FileReader().filename, number=self.linenumber, cmd=args[0]))
+            logs.append("[{file}:{number}] Current dir: {dir}".format(file=FileReader().filename,
+                number=self.linenumber, dir=os.getcwd()))
             return_code = max(3, return_code)
+            print('\n'.join(logs))
             return
         except FileNotFoundError:
-            print("[" + FileReader().filename + ":" + str(self.linenumber) +
-                  "] Cannot start '" + args[0] + "': File not found")
+            logs.append("[{file}:{number}] Cannot start '{cmd}': File not found.".format(
+                file=FileReader().filename, number=self.linenumber, cmd=args[0]))
             return_code = max(3, return_code)
+            print('\n'.join(logs))
             return
         except OSError as osE:
             if osE.errno == 8:
@@ -375,34 +398,33 @@ class Cmd(object):
         cmdName = FileReader().filename + ":" + str(self.linenumber)
         try:
             (stdout_data, stderr_data) = proc.communicate("\n".join(self.input_pipe), self.timeout)
-            local_pgid = None
+            local_pid = None
             timeout_reached = False
         except subprocess.TimeoutExpired:
             timeout_reached = True
-            print("Test suite `" + FileReader().filename + "': NOK (<" +
-                  cmdName + "> timeout after " + str(self.timeout) + " sec)")
-            running_pgids.remove(local_pgid)
-            kill_process_group(local_pgid)
+            logs.append("Test suite `{file}': NOK (<{cmd}> timeout after {timeout} sec)".format(
+                file=FileReader().filename, cmd=cmdName, timeout=self.timeout))
+            running_pids.remove(local_pid)
+            kill_process_group(local_pid)
             # Try to get the output of the timeout process, to help in debugging.
             try:
                 (stdout_data, stderr_data) = proc.communicate(timeout=1)
             except subprocess.TimeoutExpired:
-                print("[{file}:{number}] Could not retrieve output. Killing the process group failed?".format(
+                logs.append("[{file}:{number}] Could not retrieve output. Killing the process group failed?".format(
                     file=FileReader().filename, number=self.linenumber))
                 return_code = max(3, return_code)
+                print('\n'.join(logs))
                 return
 
         if self.output_display:
-            print(stdout_data)
+            logs.append(str(stdout_data))
 
         # remove text colors
         ansi_escape = re.compile(r'\x1b[^m]*m')
         stdout_data = ansi_escape.sub('', stdout_data)
 
-        #print ((stdout_data, stderr_data))
-
         if self.ignore_output:
-            print("(ignoring the output of <" + cmdName + "> as requested)")
+            logs.append("(ignoring the output of <{cmd}> as requested)".format(cmd=cmdName))
         else:
             stdouta = stdout_data.split("\n")
             while len(stdouta) > 0 and stdouta[-1] == "":
@@ -426,7 +448,7 @@ class Cmd(object):
                     fromfile='expected',
                     tofile='obtained'))
             if len(diff) > 0:
-                print("Output of <" + cmdName + "> mismatch:")
+                logs.append("Output of <{cmd}> mismatch:".format(cmd=cmdName))
                 if self.sort >= 0:  # If sorted, truncate the diff output and show the unsorted version
                     difflen = 0
                     for line in diff:
@@ -434,15 +456,16 @@ class Cmd(object):
                             print(line)
                         difflen += 1
                     if difflen > 50:
-                        print("(diff truncated after 50 lines)")
-                    print("Unsorted observed output:\n")
+                        logs.append("(diff truncated after 50 lines)")
+                    logs.append("Unsorted observed output:\n")
                     for line in stdcpy:
-                        print(line)
+                        logs.append(line)
                 else:  # If not sorted, just display the diff
                     for line in diff:
-                        print(line)
+                        logs.append(line)
 
-                print("Test suite `" + FileReader().filename + "': NOK (<" + cmdName + "> output mismatch)")
+                logs.append("Test suite `{file}': NOK (<{cmd}> output mismatch)".format(
+                    file=FileReader().filename, cmd=cmdName))
                 if lock is not None:
                     lock.release()
                 if TeshState().keep:
@@ -454,35 +477,40 @@ class Cmd(object):
                     for line in obtained:
                         f.write("> " + line + "\n")
                     f.close()
-                    print("Obtained output kept as requested: " + os.path.abspath("obtained"))
+                    logs.append("Obtained output kept as requested: {path}".format(path=os.path.abspath("obtained")))
                 return_code = max(2, return_code)
+                print('\n'.join(logs))
                 return
 
         if timeout_reached:
             return_code = max(3, return_code)
+            print('\n'.join(logs))
             return
 
-        #print ((proc.returncode, self.expect_return))
-
-        if proc.returncode != self.expect_return:
+        if not proc.returncode in self.expect_return:
             if proc.returncode >= 0:
-                print("Test suite `" + FileReader().filename + "': NOK (<" +
-                      cmdName + "> returned code " + str(proc.returncode) + ")")
+                logs.append("Test suite `{file}': NOK (<{cmd}> returned code {code})".format(
+                    file=FileReader().filename, cmd=cmdName, code=proc.returncode))
                 if lock is not None:
                     lock.release()
                 return_code = max(2, return_code)
+                print('\n'.join(logs))
                 return
             else:
-                print("Test suite `" + FileReader().filename + "': NOK (<" + cmdName +
-                      "> got signal " + SIGNALS_TO_NAMES_DICT[-proc.returncode] + ")")
+                logs.append("Test suite `{file}': NOK (<{cmd}> got signal {sig})".format(
+                    file=FileReader().filename, cmd=cmdName,
+                    sig=SIGNALS_TO_NAMES_DICT[-proc.returncode]))
                 if lock is not None:
                     lock.release()
                 return_code = max(max(-proc.returncode, 1), return_code)
+                print('\n'.join(logs))
                 return
 
         if lock is not None:
             lock.release()
 
+        print('\n'.join(logs))
+
     def can_run(self):
         return self.args is not None
 
@@ -620,16 +648,17 @@ if __name__ == '__main__':
             cmd.output_display = True
             cmd.ignore_output = True
         elif line[0:15] == "! expect return":
-            cmd.expect_return = int(line[16:])
+            cmd.expect_return = [int(line[16:])]
             #print("expect return "+str(int(line[16:])))
         elif line[0:15] == "! expect signal":
-            sig = line[16:]
-            # get the signal integer value from the signal module
-            if sig not in signal.__dict__:
-                fatal_error("unrecognized signal '" + sig + "'")
-            sig = int(signal.__dict__[sig])
-            # popen return -signal when a process ends with a signal
-            cmd.expect_return = -sig
+            cmd.expect_return = []
+            for sig in (line[16:]).split("|"):
+                # get the signal integer value from the signal module
+                if sig not in signal.__dict__:
+                    fatal_error("unrecognized signal '" + sig + "'")
+                sig = int(signal.__dict__[sig])
+                # popen return -signal when a process ends with a signal
+                cmd.expect_return.append(-sig)
         elif line[0:len("! timeout ")] == "! timeout ":
             if "no" in line[len("! timeout "):]:
                 cmd.timeout = None