From d34884b65e907d516d622188aa238cb698defd4f Mon Sep 17 00:00:00 2001 From: Guillaume Chanel <Guillaume.Chanel@unige.ch> Date: Wed, 16 Nov 2022 16:58:49 +0100 Subject: [PATCH] Add background jobs and SIGTERM/QUI tests --- test/test.py | 108 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 88 insertions(+), 20 deletions(-) mode change 100644 => 100755 test/test.py diff --git a/test/test.py b/test/test.py old mode 100644 new mode 100755 index 3a73484..f15165a --- a/test/test.py +++ b/test/test.py @@ -5,18 +5,22 @@ # # Author: David Gonzalez (HEPIA) <david.dg.gonzalez@hesge.ch> # Author: Guillaume Chanel <guillaume.chanel@unige.ch> +# TODO: There are many sleep functions called to wait for the shell. Would there be a way to be sure that: +# - the shell is up and waiting for an input +# - the shell has processed a command +# - the shell as received and treated / ignored a signal (could be done by launching a command and waiting for it to be processed) -import sys +import filecmp +import logging import os -import tempfile +import signal import subprocess -from pathlib import Path -from colorama import Fore -import logging +import sys +import tempfile import time +from pathlib import Path import psutil -import filecmp - +from colorama import Fore def print_usage(): @@ -96,6 +100,18 @@ class Shell: self.stderr = None + def is_alive(self): + return self.shell_process.poll() == None + + + def get_children(self): + return self.shell_ps.children() + + + def send_signal(self, signal): + self.shell_process.send_signal(signal) + + # TODO: move out of Shell ? def check_ophans(self, cmd: Cmd): """ Check if an orphan process (child of 1) was executed with cmd @@ -138,6 +154,9 @@ class Shell: # as the command exceeded the timeout ? duration = time.time() - start_time if duration > timeout: + # Kill the children and raise timeout error + for c in children: + c.kill() raise psutil.TimeoutExpired('The process took more than the timeout ({}s) to terminate (last command executed: {})'.format(duration, self.last_cmd)) @@ -215,14 +234,19 @@ class Test: raise AssertionError('No exit code found') - def _test_command_results(self, cmd: Cmd, shell: Shell, test_stdout: str, test_stderr: str, test_return: bool): + def _test_command_results(self, cmd: Cmd, shell: Shell, test_stdout: str, test_stderr: str, test_return: bool, test_in_shell: bool = False): """ Test if the results (standard outputs and return code) of a command are the correct ones test_stdout/test_stderr: a string indicating if the standard output should include the normal output ('include'), be empty ('empty'), or not tested ('notest' or any other string) test_return: should the return code be tested (relies on the computation of the return code from the standard output) + test_in_shell: the command is tested in a shell instead of a direct subprocess. This is done to test commands with pipes + easily. """ # get "real" output - real = subprocess.run(cmd, cwd=tempfile.gettempdir(), capture_output=True, encoding='utf-8') + if test_in_shell: + real = subprocess.run(str(cmd), cwd=tempfile.gettempdir(), shell=True, capture_output=True, encoding='utf-8') + else: + real = subprocess.run(cmd, cwd=tempfile.gettempdir(), capture_output=True, encoding='utf-8') # check standard output # TODO combine the two tests (the one below) in one fonction @@ -286,7 +310,6 @@ class Test: def test_foregroundjobs(self): - print('--- TESTING FOREGROUND JOBS ---') self.test_simple_foregroundjob() self.test_wrongcmd() @@ -326,7 +349,7 @@ class Test: shell.exec_command(cmd, timeout=1) time.sleep(0.5) # to be "sure" that the shell command executed if dir.name != shell.get_cwd(): - raise AssertionError('Changing directory failed: the directory shouldbe {} but it is {}'.format(dir, shell.get_cwd())) + raise AssertionError('Changing directory failed: the directory should be {} but it is {}'.format(dir, shell.get_cwd())) shell.exit() # Test non-existing directory @@ -338,7 +361,6 @@ class Test: def test_builtin(self): - print('--- TESTING BUITIN COMMANDS ---') self.test_builtin_exit() self.test_builtin_cd() @@ -357,7 +379,7 @@ class Test: shell = Shell(self.shell_exec) # Test file creation and correct content - cmd = Cmd(['echo', '-e', 'First line\\nSecond line\\nThird line']) + cmd = Cmd(['echo', '-e', '"First line\\nSecond line\\nThird line"']) def run_cmd(): cmd_shell = cmd + ['>', out_file] shell.exec_command(cmd_shell, timeout=1) @@ -380,6 +402,51 @@ class Test: shell.exit() + @test + def test_pipe(self): + # Working commands + cmd = Cmd(['ls', '-alh', '/tmp', '|', 'wc', '-l']) + shell = Shell(self.shell_exec) + shell.exec_commands([cmd]) + self._test_command_results(cmd, shell, test_stdout='include', test_stderr='empty', test_return=False, test_in_shell=True) + + # Unsuccessful command + cmd = Cmd(['cat', 'nonExistingFile', '|', 'grep', 'word']) + shell = Shell(self.shell_exec) + shell.exec_commands([cmd]) + self._test_command_results(cmd, shell, test_stdout='notest', test_stderr='include', test_return=False, test_in_shell=True) + + @test + def test_background_jobs(self): + # Check possibility to have two processes running + cmd_back = Cmd(['sleep', '2', '&']) + cmd_fore = Cmd(['sleep', '1']) + shell = Shell(self.shell_exec) + shell.exec_command(cmd_back, wait_cmd=False) + shell.exec_command(cmd_fore, wait_cmd=False) + time.sleep(0.2) + children = shell.get_children() + assert len(children) == 2, 'After the following commands:\n{}\n{}\n, two processes were expected, the following processes were found: {}'.format( + cmd_back, cmd_fore, children + ) + shell.wait_children() # also test zombies + shell.exit() + + # TODO: test return error message or error value ? + + + @test + def test_SIGTERM_SIGQUIT(self): + shell = Shell(self.shell_exec) + time.sleep(0.1) # To be sure that handlers are configured + shell.send_signal(signal.SIGTERM) + shell.send_signal(signal.SIGQUIT) + time.sleep(0.1) # To be sure that signals are treated + assert shell.is_alive(), "SIGTERM and SIGQUIT sent, the shell should ignore those signals but it died" + shell.exit() + + + if __name__ == "__main__": if len(sys.argv) < 2: @@ -387,24 +454,25 @@ if __name__ == "__main__": exit(1) t = Test(sys.argv[1]) + print('--- TESTING BUITIN COMMANDS ---') t.test_builtin() + print('--- TESTING FOREGROUND JOBS ---') t.test_foregroundjobs() + print('--- TESTING I/O redirection (inc. pipes) ---') t.test_stdout_redirect() + t.test_pipe() + print('--- TESTING background jobs and signals ---') + t.test_background_jobs() + t.test_SIGTERM_SIGQUIT() sys.exit(test_failed) # # Pipe - # execute_commandon_shell(tp_dir, tp_shell_name, b'ls -alh | wc -l') # # Background job where shell exit right after # execute_commandon_shell(tp_dir, tp_shell_name, b'sleep 2 &', 5) - # # Background job where shell wait too - # execute_commandon_shell(tp_dir, tp_shell_name, b'sleep 2 &\nsleep 3', 6) # # Background job exit code # execute_commandon_shell(tp_dir, tp_shell_name, b'ls clkscncqp &') - # # Background job SIGTERM (should be ignored) - # #execute_commandon_shell(tp_dir, tp_shell_name, b'sleep 2 &\nsleep 1\nkill -SIGTERM {pid}', 6) - # # Background job SIGQUIT (should be ignored) - # #execute_commandon_shell(tp_dir, tp_shell_name, b'sleep 2 &\nsleep 1\nkill -SIGQUIT {pid}', 6) + # # Background job SIGHUP # #execute_commandon_shell(tp_dir, tp_shell_name, b'sleep 10 &\nsleep 1\nkill -SIGHUP {pid}', 6) -- GitLab