diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
commit | 9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /testing/mozbase/mozprocess/tests | |
parent | Initial commit. (diff) | |
download | thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.tar.xz thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/mozbase/mozprocess/tests')
22 files changed, 1359 insertions, 0 deletions
diff --git a/testing/mozbase/mozprocess/tests/manifest.ini b/testing/mozbase/mozprocess/tests/manifest.ini new file mode 100644 index 0000000000..c63c4684c5 --- /dev/null +++ b/testing/mozbase/mozprocess/tests/manifest.ini @@ -0,0 +1,12 @@ +[DEFAULT] +subsuite = mozbase +[test_detached.py] +skip-if = os == 'win' # Bug 1493796 +[test_kill.py] +[test_misc.py] +[test_pid.py] +[test_poll.py] +[test_wait.py] +[test_output.py] +[test_params.py] +[test_process_reader.py] diff --git a/testing/mozbase/mozprocess/tests/process_normal_broad.ini b/testing/mozbase/mozprocess/tests/process_normal_broad.ini new file mode 100644 index 0000000000..28109cb31e --- /dev/null +++ b/testing/mozbase/mozprocess/tests/process_normal_broad.ini @@ -0,0 +1,30 @@ +; Generate a Broad Process Tree +; This generates a Tree of the form: +; +; main +; \_ c1 +; | \_ c2 +; | \_ c2 +; | \_ c2 +; | \_ c2 +; | \_ c2 +; | +; \_ c1 +; | \_ c2 +; | \_ c2 +; | \_ c2 +; | \_ c2 +; | \_ c2 +; | +; \_ ... 23 more times + +[main] +children=25*c1 +maxtime=10 + +[c1] +children=5*c2 +maxtime=10 + +[c2] +maxtime=5 diff --git a/testing/mozbase/mozprocess/tests/process_normal_deep.ini b/testing/mozbase/mozprocess/tests/process_normal_deep.ini new file mode 100644 index 0000000000..ef9809f6ab --- /dev/null +++ b/testing/mozbase/mozprocess/tests/process_normal_deep.ini @@ -0,0 +1,65 @@ +; Deep Process Tree +; Should generate a process tree of the form: +; +; main +; \_ c2 +; | \_ c5 +; | | \_ c6 +; | | \_ c7 +; | | \_ c8 +; | | \_ c1 +; | | \_ c4 +; | \_ c5 +; | \_ c6 +; | \_ c7 +; | \_ c8 +; | \_ c1 +; | \_ c4 +; \_ c2 +; | \_ c5 +; | | \_ c6 +; | | \_ c7 +; | | \_ c8 +; | | \_ c1 +; | | \_ c4 +; | \_ c5 +; | \_ c6 +; | \_ c7 +; | \_ c8 +; | \_ c1 +; | \_ c4 +; \_ c1 +; | \_ c4 +; \_ c1 +; \_ c4 + +[main] +children=2*c1, 2*c2 +maxtime=20 + +[c1] +children=c4 +maxtime=20 + +[c2] +children=2*c5 +maxtime=20 + +[c4] +maxtime=20 + +[c5] +children=c6 +maxtime=20 + +[c6] +children=c7 +maxtime=20 + +[c7] +children=c8 +maxtime=20 + +[c8] +children=c1 +maxtime=20 diff --git a/testing/mozbase/mozprocess/tests/process_normal_finish.ini b/testing/mozbase/mozprocess/tests/process_normal_finish.ini new file mode 100644 index 0000000000..4519c70830 --- /dev/null +++ b/testing/mozbase/mozprocess/tests/process_normal_finish.ini @@ -0,0 +1,17 @@ +; Generates a normal process tree +; Tree is of the form: +; main +; \_ c1 +; \_ c2 + +[main] +children=c1,c2 +maxtime=10 + +[c1] +children=c2 +maxtime=5 + +[c2] +maxtime=5 + diff --git a/testing/mozbase/mozprocess/tests/process_normal_finish_no_process_group.ini b/testing/mozbase/mozprocess/tests/process_normal_finish_no_process_group.ini new file mode 100644 index 0000000000..2b0f1f9a4f --- /dev/null +++ b/testing/mozbase/mozprocess/tests/process_normal_finish_no_process_group.ini @@ -0,0 +1,2 @@ +[main] +maxtime=10 diff --git a/testing/mozbase/mozprocess/tests/process_waittimeout.ini b/testing/mozbase/mozprocess/tests/process_waittimeout.ini new file mode 100644 index 0000000000..5800267d18 --- /dev/null +++ b/testing/mozbase/mozprocess/tests/process_waittimeout.ini @@ -0,0 +1,16 @@ +; Generates a normal process tree +; Tree is of the form: +; main +; \_ c1 +; \_ c2 + +[main] +children=2*c1 +maxtime=300 + +[c1] +children=2*c2 +maxtime=300 + +[c2] +maxtime=300 diff --git a/testing/mozbase/mozprocess/tests/process_waittimeout_10s.ini b/testing/mozbase/mozprocess/tests/process_waittimeout_10s.ini new file mode 100644 index 0000000000..abf8d6a4ef --- /dev/null +++ b/testing/mozbase/mozprocess/tests/process_waittimeout_10s.ini @@ -0,0 +1,16 @@ +; Generate a normal process tree +; Tree is of the form: +; main +; \_ c1 +; \_ c2 + +[main] +children=c1 +maxtime=10 + +[c1] +children=2*c2 +maxtime=5 + +[c2] +maxtime=5 diff --git a/testing/mozbase/mozprocess/tests/proclaunch.py b/testing/mozbase/mozprocess/tests/proclaunch.py new file mode 100644 index 0000000000..c57e2bb12c --- /dev/null +++ b/testing/mozbase/mozprocess/tests/proclaunch.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python + +import argparse +import collections +import multiprocessing +import time + +from six.moves import configparser + +ProcessNode = collections.namedtuple("ProcessNode", ["maxtime", "children"]) + + +class ProcessLauncher(object): + """Create and Launch process trees specified by a '.ini' file + + Typical .ini file accepted by this class : + + [main] + children=c1, 1*c2, 4*c3 + maxtime=10 + + [c1] + children= 2*c2, c3 + maxtime=20 + + [c2] + children=3*c3 + maxtime=5 + + [c3] + maxtime=3 + + This generates a process tree of the form: + [main] + |---[c1] + | |---[c2] + | | |---[c3] + | | |---[c3] + | | |---[c3] + | | + | |---[c2] + | | |---[c3] + | | |---[c3] + | | |---[c3] + | | + | |---[c3] + | + |---[c2] + | |---[c3] + | |---[c3] + | |---[c3] + | + |---[c3] + |---[c3] + |---[c3] + + Caveat: The section names cannot contain a '*'(asterisk) or a ','(comma) + character as these are used as delimiters for parsing. + """ + + # Unit time for processes in seconds + UNIT_TIME = 1 + + def __init__(self, manifest, verbose=False): + """ + Parses the manifest and stores the information about the process tree + in a format usable by the class. + + Raises IOError if : + - The path does not exist + - The file cannot be read + Raises ConfigParser.*Error if: + - Files does not contain section headers + - File cannot be parsed because of incorrect specification + + :param manifest: Path to the manifest file that contains the + configuration for the process tree to be launched + :verbose: Print the process start and end information. + Genrates a lot of output. Disabled by default. + """ + + self.verbose = verbose + + # Children is a dictionary used to store information from the, + # Configuration file in a more usable format. + # Key : string contain the name of child process + # Value : A Named tuple of the form (max_time, (list of child processes of Key)) + # Where each child process is a list of type: [count to run, name of child] + self.children = {} + + cfgparser = configparser.ConfigParser() + + if not cfgparser.read(manifest): + raise IOError("The manifest %s could not be found/opened", manifest) + + sections = cfgparser.sections() + for section in sections: + # Maxtime is a mandatory option + # ConfigParser.NoOptionError is raised if maxtime does not exist + if "*" in section or "," in section: + raise configparser.ParsingError( + "%s is not a valid section name. " + "Section names cannot contain a '*' or ','." % section + ) + m_time = cfgparser.get(section, "maxtime") + try: + m_time = int(m_time) + except ValueError: + raise ValueError( + "Expected maxtime to be an integer, specified %s" % m_time + ) + + # No children option implies there are no further children + # Leaving the children option blank is an error. + try: + c = cfgparser.get(section, "children") + if not c: + # If children is an empty field, assume no children + children = None + + else: + # Tokenize chilren field, ignore empty strings + children = [ + [y.strip() for y in x.strip().split("*", 1)] + for x in c.split(",") + if x + ] + try: + for i, child in enumerate(children): + # No multiplicate factor infront of a process implies 1 + if len(child) == 1: + children[i] = [1, child[0]] + else: + children[i][0] = int(child[0]) + + if children[i][1] not in sections: + raise configparser.ParsingError( + "No section corresponding to child %s" % child[1] + ) + except ValueError: + raise ValueError( + "Expected process count to be an integer, specified %s" + % child[0] + ) + + except configparser.NoOptionError: + children = None + pn = ProcessNode(maxtime=m_time, children=children) + self.children[section] = pn + + def run(self): + """ + This function launches the process tree. + """ + self._run("main", 0) + + def _run(self, proc_name, level): + """ + Runs the process specified by the section-name `proc_name` in the manifest file. + Then makes calls to launch the child processes of `proc_name` + + :param proc_name: File name of the manifest as a string. + :param level: Depth of the current process in the tree. + """ + if proc_name not in self.children: + raise IOError("%s is not a valid process" % proc_name) + + maxtime = self.children[proc_name].maxtime + if self.verbose: + print( + "%sLaunching %s for %d*%d seconds" + % (" " * level, proc_name, maxtime, self.UNIT_TIME) + ) + + while self.children[proc_name].children: + child = self.children[proc_name].children.pop() + + count, child_proc = child + for i in range(count): + p = multiprocessing.Process( + target=self._run, args=(child[1], level + 1) + ) + p.start() + + self._launch(maxtime) + if self.verbose: + print("%sFinished %s" % (" " * level, proc_name)) + + def _launch(self, running_time): + """ + Create and launch a process and idles for the time specified by + `running_time` + + :param running_time: Running time of the process in seconds. + """ + elapsed_time = 0 + + while elapsed_time < running_time: + time.sleep(self.UNIT_TIME) + elapsed_time += self.UNIT_TIME + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("manifest", help="Specify the configuration .ini file") + args = parser.parse_args() + + proclaunch = ProcessLauncher(args.manifest) + proclaunch.run() diff --git a/testing/mozbase/mozprocess/tests/proctest.py b/testing/mozbase/mozprocess/tests/proctest.py new file mode 100644 index 0000000000..d1e7138a1d --- /dev/null +++ b/testing/mozbase/mozprocess/tests/proctest.py @@ -0,0 +1,62 @@ +import os +import sys +import unittest + +from mozprocess import ProcessHandler + +here = os.path.dirname(os.path.abspath(__file__)) + + +class ProcTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.proclaunch = os.path.join(here, "proclaunch.py") + cls.python = sys.executable + + def determine_status(self, proc, isalive=False, expectedfail=()): + """ + Use to determine if the situation has failed. + Parameters: + proc -- the processhandler instance + isalive -- Use True to indicate we pass if the process exists; however, by default + the test will pass if the process does not exist (isalive == False) + expectedfail -- Defaults to [], used to indicate a list of fields + that are expected to fail + """ + returncode = proc.proc.returncode + didtimeout = proc.didTimeout + detected = ProcessHandler.pid_exists(proc.pid) + output = "" + # ProcessHandler has output when store_output is set to True in the constructor + # (this is the default) + if getattr(proc, "output"): + output = proc.output + + if "returncode" in expectedfail: + self.assertTrue( + returncode, "Detected an unexpected return code of: %s" % returncode + ) + elif isalive: + self.assertEqual( + returncode, None, "Detected not None return code of: %s" % returncode + ) + else: + self.assertNotEqual( + returncode, None, "Detected unexpected None return code of" + ) + + if "didtimeout" in expectedfail: + self.assertTrue(didtimeout, "Detected that process didn't time out") + else: + self.assertTrue(not didtimeout, "Detected that process timed out") + + if isalive: + self.assertTrue( + detected, + "Detected process is not running, " "process output: %s" % output, + ) + else: + self.assertTrue( + not detected, + "Detected process is still running, " "process output: %s" % output, + ) diff --git a/testing/mozbase/mozprocess/tests/scripts/ignore_sigterm.py b/testing/mozbase/mozprocess/tests/scripts/ignore_sigterm.py new file mode 100644 index 0000000000..15870d6267 --- /dev/null +++ b/testing/mozbase/mozprocess/tests/scripts/ignore_sigterm.py @@ -0,0 +1,13 @@ +import signal +import time + +signal.pthread_sigmask(signal.SIG_SETMASK, {signal.SIGTERM}) + + +def main(): + while True: + time.sleep(1) + + +if __name__ == "__main__": + main() diff --git a/testing/mozbase/mozprocess/tests/scripts/infinite_loop.py b/testing/mozbase/mozprocess/tests/scripts/infinite_loop.py new file mode 100644 index 0000000000..9af60b3811 --- /dev/null +++ b/testing/mozbase/mozprocess/tests/scripts/infinite_loop.py @@ -0,0 +1,18 @@ +import signal +import sys +import threading +import time + +if "deadlock" in sys.argv: + lock = threading.Lock() + + def trap(sig, frame): + lock.acquire() + + # get the lock once + lock.acquire() + # and take it again on SIGTERM signal: deadlock. + signal.signal(signal.SIGTERM, trap) + +while 1: + time.sleep(1) diff --git a/testing/mozbase/mozprocess/tests/scripts/proccountfive.py b/testing/mozbase/mozprocess/tests/scripts/proccountfive.py new file mode 100644 index 0000000000..39fabee508 --- /dev/null +++ b/testing/mozbase/mozprocess/tests/scripts/proccountfive.py @@ -0,0 +1,2 @@ +for i in range(0, 5): + print(i) diff --git a/testing/mozbase/mozprocess/tests/scripts/procnonewline.py b/testing/mozbase/mozprocess/tests/scripts/procnonewline.py new file mode 100644 index 0000000000..341a94be0a --- /dev/null +++ b/testing/mozbase/mozprocess/tests/scripts/procnonewline.py @@ -0,0 +1,4 @@ +import sys + +print("this is a newline") +sys.stdout.write("this has NO newline") diff --git a/testing/mozbase/mozprocess/tests/test_detached.py b/testing/mozbase/mozprocess/tests/test_detached.py new file mode 100644 index 0000000000..bc310fa3db --- /dev/null +++ b/testing/mozbase/mozprocess/tests/test_detached.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python + +import os + +import mozunit +import proctest +from mozprocess import processhandler + +here = os.path.dirname(os.path.abspath(__file__)) + + +class ProcTestDetached(proctest.ProcTest): + """Class to test for detached processes.""" + + def test_check_for_detached_before_run(self): + """Process is not started yet when checked for detached.""" + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_finish.ini"], cwd=here + ) + + with self.assertRaises(RuntimeError): + p.check_for_detached(1234) + + def test_check_for_detached_while_running_with_current_pid(self): + """Process is started, and check for detached with original pid.""" + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_finish.ini"], cwd=here + ) + p.run() + + orig_pid = p.pid + p.check_for_detached(p.pid) + + self.assertEqual(p.pid, orig_pid) + self.assertIsNone(p.proc.detached_pid) + + self.determine_status(p, True) + p.kill() + + def test_check_for_detached_after_fork(self): + """Process is started, and check for detached with new pid.""" + pass + + def test_check_for_detached_after_kill(self): + """Process is killed before checking for detached pid.""" + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_finish.ini"], cwd=here + ) + p.run() + p.kill() + + orig_pid = p.pid + p.check_for_detached(p.pid) + + self.assertEqual(p.pid, orig_pid) + self.assertIsNone(p.proc.detached_pid) + + self.determine_status(p) + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprocess/tests/test_kill.py b/testing/mozbase/mozprocess/tests/test_kill.py new file mode 100644 index 0000000000..bba19f6fe8 --- /dev/null +++ b/testing/mozbase/mozprocess/tests/test_kill.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python + +import os +import signal +import sys +import time +import unittest + +import mozunit +import proctest +from mozprocess import processhandler + +here = os.path.dirname(os.path.abspath(__file__)) + + +class ProcTestKill(proctest.ProcTest): + """Class to test various process tree killing scenatios""" + + def test_kill_before_run(self): + """Process is not started, and kill() is called""" + + p = processhandler.ProcessHandler([self.python, "-V"]) + self.assertRaises(RuntimeError, p.kill) + + def test_process_kill(self): + """Process is started, we kill it""" + + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_finish.ini"], cwd=here + ) + p.run() + p.kill() + + self.determine_status(p, expectedfail=("returncode",)) + + def test_process_kill_deep(self): + """Process is started, we kill it, we use a deep process tree""" + + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_deep.ini"], cwd=here + ) + p.run() + p.kill() + + self.determine_status(p, expectedfail=("returncode",)) + + def test_process_kill_deep_wait(self): + """Process is started, we use a deep process tree, we let it spawn + for a bit, we kill it""" + + myenv = None + # On macosx1014, subprocess fails to find `six` when run with python3. + # This ensures that subprocess first looks to sys.path to find `six`. + # See https://bugzilla.mozilla.org/show_bug.cgi?id=1562083 + if sys.platform == "darwin" and sys.version_info[0] > 2: + myenv = os.environ.copy() + myenv["PYTHONPATH"] = ":".join(sys.path) + + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_deep.ini"], + cwd=here, + env=myenv, + ) + p.run() + # Let the tree spawn a bit, before attempting to kill + time.sleep(3) + p.kill() + + self.determine_status(p, expectedfail=("returncode",)) + + def test_process_kill_broad(self): + """Process is started, we kill it, we use a broad process tree""" + + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_broad.ini"], cwd=here + ) + p.run() + p.kill() + + self.determine_status(p, expectedfail=("returncode",)) + + def test_process_kill_broad_delayed(self): + """Process is started, we use a broad process tree, we let it spawn + for a bit, we kill it""" + + myenv = None + # On macosx1014, subprocess fails to find `six` when run with python3. + # This ensures that subprocess first looks to sys.path to find `six`. + # See https://bugzilla.mozilla.org/show_bug.cgi?id=1562083 + if sys.platform == "darwin" and sys.version_info[0] > 2: + myenv = os.environ.copy() + myenv["PYTHONPATH"] = ":".join(sys.path) + + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_broad.ini"], + cwd=here, + env=myenv, + ) + p.run() + # Let the tree spawn a bit, before attempting to kill + time.sleep(3) + p.kill() + + self.determine_status(p, expectedfail=("returncode",)) + + @unittest.skipUnless(processhandler.isPosix, "posix only") + def test_process_kill_with_sigterm(self): + script = os.path.join(here, "scripts", "infinite_loop.py") + p = processhandler.ProcessHandler([self.python, script]) + + p.run() + p.kill() + + self.assertEqual(p.proc.returncode, -signal.SIGTERM) + + @unittest.skipUnless(processhandler.isPosix, "posix only") + def test_process_kill_with_sigint_if_needed(self): + script = os.path.join(here, "scripts", "infinite_loop.py") + p = processhandler.ProcessHandler([self.python, script, "deadlock"]) + + p.run() + time.sleep(1) + p.kill() + + self.assertEqual(p.proc.returncode, -signal.SIGKILL) + + @unittest.skipUnless(processhandler.isPosix, "posix only") + def test_process_kill_with_timeout(self): + script = os.path.join(here, "scripts", "ignore_sigterm.py") + p = processhandler.ProcessHandler([self.python, script]) + + p.run() + time.sleep(1) + t0 = time.time() + p.kill(sig=signal.SIGTERM, timeout=2) + self.assertEqual(p.proc.returncode, None) + self.assertGreaterEqual(time.time(), t0 + 2) + + p.kill(sig=signal.SIGKILL) + self.assertEqual(p.proc.returncode, -signal.SIGKILL) + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprocess/tests/test_misc.py b/testing/mozbase/mozprocess/tests/test_misc.py new file mode 100644 index 0000000000..a4908fe203 --- /dev/null +++ b/testing/mozbase/mozprocess/tests/test_misc.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import sys + +import mozunit +import proctest +from mozprocess import processhandler + +here = os.path.dirname(os.path.abspath(__file__)) + + +class ProcTestMisc(proctest.ProcTest): + """Class to test misc operations""" + + def test_process_timeout_no_kill(self): + """Process is started, runs but we time out waiting on it + to complete. Process should not be killed. + """ + p = None + + def timeout_handler(): + self.assertEqual(p.proc.poll(), None) + p.kill() + + myenv = None + # On macosx1014, subprocess fails to find `six` when run with python3. + # This ensures that subprocess first looks to sys.path to find `six`. + # See https://bugzilla.mozilla.org/show_bug.cgi?id=1562083 + if sys.platform == "darwin" and sys.version_info[0] > 2: + myenv = os.environ.copy() + myenv["PYTHONPATH"] = ":".join(sys.path) + + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_waittimeout.ini"], + cwd=here, + env=myenv, + onTimeout=(timeout_handler,), + kill_on_timeout=False, + ) + p.run(timeout=1) + p.wait() + self.assertTrue(p.didTimeout) + + self.determine_status(p, False, ["returncode", "didtimeout"]) + + def test_unicode_in_environment(self): + env = { + "FOOBAR": "ʘ", + } + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_finish.ini"], + cwd=here, + env=env, + ) + # passes if no exceptions are raised + p.run() + p.wait() + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprocess/tests/test_output.py b/testing/mozbase/mozprocess/tests/test_output.py new file mode 100644 index 0000000000..fb1551eaf4 --- /dev/null +++ b/testing/mozbase/mozprocess/tests/test_output.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python + +import io +import os + +import mozunit +import proctest +from mozprocess import processhandler + +here = os.path.dirname(os.path.abspath(__file__)) + + +class ProcTestOutput(proctest.ProcTest): + """Class to test operations related to output handling""" + + def test_process_output_twice(self): + """ + Process is started, then processOutput is called a second time explicitly + """ + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_waittimeout_10s.ini"], cwd=here + ) + + p.run() + p.processOutput(timeout=5) + p.wait() + + self.determine_status(p, False, ()) + + def test_process_output_nonewline(self): + """ + Process is started, outputs data with no newline + """ + p = processhandler.ProcessHandler( + [self.python, os.path.join("scripts", "procnonewline.py")], cwd=here + ) + + p.run() + p.processOutput(timeout=5) + p.wait() + + self.determine_status(p, False, ()) + + def test_stream_process_output(self): + """ + Process output stream does not buffer + """ + expected = "\n".join([str(n) for n in range(0, 10)]) + + stream = io.BytesIO() + buf = io.BufferedRandom(stream) + + p = processhandler.ProcessHandler( + [self.python, os.path.join("scripts", "proccountfive.py")], + cwd=here, + stream=buf, + ) + + p.run() + p.wait() + for i in range(5, 10): + stream.write(str(i).encode("utf8") + "\n".encode("utf8")) + + buf.flush() + self.assertEqual(stream.getvalue().strip().decode("utf8"), expected) + + # make sure mozprocess doesn't close the stream + # since mozprocess didn't create it + self.assertFalse(buf.closed) + buf.close() + + self.determine_status(p, False, ()) + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprocess/tests/test_params.py b/testing/mozbase/mozprocess/tests/test_params.py new file mode 100644 index 0000000000..4a8e98affd --- /dev/null +++ b/testing/mozbase/mozprocess/tests/test_params.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +import unittest + +import mozunit +from mozprocess import processhandler + + +class ParamTests(unittest.TestCase): + def test_process_outputline_handler(self): + """Parameter processOutputLine is accepted with a single function""" + + def output(line): + print("output " + str(line)) + + err = None + try: + processhandler.ProcessHandler(["ls", "-l"], processOutputLine=output) + except (TypeError, AttributeError) as e: + err = e + self.assertFalse(err) + + def test_process_outputline_handler_list(self): + """Parameter processOutputLine is accepted with a list of functions""" + + def output(line): + print("output " + str(line)) + + err = None + try: + processhandler.ProcessHandler(["ls", "-l"], processOutputLine=[output]) + except (TypeError, AttributeError) as e: + err = e + self.assertFalse(err) + + def test_process_ontimeout_handler(self): + """Parameter onTimeout is accepted with a single function""" + + def timeout(): + print("timeout!") + + err = None + try: + processhandler.ProcessHandler(["sleep", "2"], onTimeout=timeout) + except (TypeError, AttributeError) as e: + err = e + self.assertFalse(err) + + def test_process_ontimeout_handler_list(self): + """Parameter onTimeout is accepted with a list of functions""" + + def timeout(): + print("timeout!") + + err = None + try: + processhandler.ProcessHandler(["sleep", "2"], onTimeout=[timeout]) + except (TypeError, AttributeError) as e: + err = e + self.assertFalse(err) + + def test_process_onfinish_handler(self): + """Parameter onFinish is accepted with a single function""" + + def finish(): + print("finished!") + + err = None + try: + processhandler.ProcessHandler(["sleep", "1"], onFinish=finish) + except (TypeError, AttributeError) as e: + err = e + self.assertFalse(err) + + def test_process_onfinish_handler_list(self): + """Parameter onFinish is accepted with a list of functions""" + + def finish(): + print("finished!") + + err = None + try: + processhandler.ProcessHandler(["sleep", "1"], onFinish=[finish]) + except (TypeError, AttributeError) as e: + err = e + self.assertFalse(err) + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprocess/tests/test_pid.py b/testing/mozbase/mozprocess/tests/test_pid.py new file mode 100644 index 0000000000..ddc352db13 --- /dev/null +++ b/testing/mozbase/mozprocess/tests/test_pid.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python + +import os + +import mozunit +import proctest +from mozprocess import processhandler + +here = os.path.dirname(os.path.abspath(__file__)) + + +class ProcTestPid(proctest.ProcTest): + """Class to test process pid.""" + + def test_pid_before_run(self): + """Process is not started, and pid is checked.""" + p = processhandler.ProcessHandler([self.python]) + with self.assertRaises(RuntimeError): + p.pid + + def test_pid_while_running(self): + """Process is started, and pid is checked.""" + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_finish.ini"], cwd=here + ) + p.run() + + self.assertIsNotNone(p.pid) + + self.determine_status(p, True) + p.kill() + + def test_pid_after_kill(self): + """Process is killed, and pid is checked.""" + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_finish.ini"], cwd=here + ) + p.run() + p.kill() + + self.assertIsNotNone(p.pid) + self.determine_status(p) + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprocess/tests/test_poll.py b/testing/mozbase/mozprocess/tests/test_poll.py new file mode 100644 index 0000000000..475c61576c --- /dev/null +++ b/testing/mozbase/mozprocess/tests/test_poll.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python + +import os +import signal +import sys +import time +import unittest + +import mozinfo +import mozunit +import proctest +from mozprocess import processhandler + +here = os.path.dirname(os.path.abspath(__file__)) + + +class ProcTestPoll(proctest.ProcTest): + """Class to test process poll.""" + + def test_poll_before_run(self): + """Process is not started, and poll() is called.""" + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_finish.ini"], cwd=here + ) + self.assertRaises(RuntimeError, p.poll) + + def test_poll_while_running(self): + """Process is started, and poll() is called.""" + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_finish.ini"], cwd=here + ) + p.run() + returncode = p.poll() + + self.assertEqual(returncode, None) + + self.determine_status(p, True) + p.kill() + + def test_poll_after_kill(self): + """Process is killed, and poll() is called.""" + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_finish.ini"], cwd=here + ) + p.run() + returncode = p.kill() + + # We killed the process, so the returncode should be non-zero + if mozinfo.isWin: + self.assertGreater( + returncode, 0, 'Positive returncode expected, got "%s"' % returncode + ) + else: + self.assertLess( + returncode, 0, 'Negative returncode expected, got "%s"' % returncode + ) + + self.assertEqual(returncode, p.poll()) + + self.determine_status(p) + + def test_poll_after_kill_no_process_group(self): + """Process (no group) is killed, and poll() is called.""" + p = processhandler.ProcessHandler( + [ + self.python, + self.proclaunch, + "process_normal_finish_no_process_group.ini", + ], + cwd=here, + ignore_children=True, + ) + p.run() + returncode = p.kill() + + # We killed the process, so the returncode should be non-zero + if mozinfo.isWin: + self.assertGreater( + returncode, 0, 'Positive returncode expected, got "%s"' % returncode + ) + else: + self.assertLess( + returncode, 0, 'Negative returncode expected, got "%s"' % returncode + ) + + self.assertEqual(returncode, p.poll()) + + self.determine_status(p) + + def test_poll_after_double_kill(self): + """Process is killed twice, and poll() is called.""" + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_finish.ini"], cwd=here + ) + p.run() + p.kill() + returncode = p.kill() + + # We killed the process, so the returncode should be non-zero + if mozinfo.isWin: + self.assertGreater( + returncode, 0, 'Positive returncode expected, got "%s"' % returncode + ) + else: + self.assertLess( + returncode, 0, 'Negative returncode expected, got "%s"' % returncode + ) + + self.assertEqual(returncode, p.poll()) + + self.determine_status(p) + + @unittest.skipIf(sys.platform.startswith("win"), "Bug 1493796") + def test_poll_after_external_kill(self): + """Process is killed externally, and poll() is called.""" + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_finish.ini"], cwd=here + ) + p.run() + + os.kill(p.pid, signal.SIGTERM) + + # Allow the output reader thread to finish processing remaining data + for i in range(0, 100): + time.sleep(processhandler.INTERVAL_PROCESS_ALIVE_CHECK) + returncode = p.poll() + if returncode is not None: + break + + # We killed the process, so the returncode should be non-zero + if mozinfo.isWin: + self.assertEqual( + returncode, + signal.SIGTERM, + 'Positive returncode expected, got "%s"' % returncode, + ) + else: + self.assertEqual( + returncode, + -signal.SIGTERM, + '%s expected, got "%s"' % (-signal.SIGTERM, returncode), + ) + + self.assertEqual(returncode, p.wait()) + + self.determine_status(p) + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprocess/tests/test_process_reader.py b/testing/mozbase/mozprocess/tests/test_process_reader.py new file mode 100644 index 0000000000..36e2188ead --- /dev/null +++ b/testing/mozbase/mozprocess/tests/test_process_reader.py @@ -0,0 +1,114 @@ +import subprocess +import sys +import unittest + +import mozunit +from mozprocess.processhandler import ProcessReader, StoreOutput + + +def run_python(str_code, stdout=subprocess.PIPE, stderr=subprocess.PIPE): + cmd = [sys.executable, "-c", str_code] + return subprocess.Popen(cmd, stdout=stdout, stderr=stderr) + + +class TestProcessReader(unittest.TestCase): + def setUp(self): + self.out = StoreOutput() + self.err = StoreOutput() + self.finished = False + + def on_finished(): + self.finished = True + + self.timeout = False + + def on_timeout(): + self.timeout = True + + self.reader = ProcessReader( + stdout_callback=self.out, + stderr_callback=self.err, + finished_callback=on_finished, + timeout_callback=on_timeout, + ) + + def test_stdout_callback(self): + proc = run_python("print(1); print(2)") + self.reader.start(proc) + self.reader.thread.join() + + self.assertEqual([x.decode("utf8") for x in self.out.output], ["1", "2"]) + self.assertEqual(self.err.output, []) + + def test_stderr_callback(self): + proc = run_python('import sys; sys.stderr.write("hello world\\n")') + self.reader.start(proc) + self.reader.thread.join() + + self.assertEqual(self.out.output, []) + self.assertEqual([x.decode("utf8") for x in self.err.output], ["hello world"]) + + def test_stdout_and_stderr_callbacks(self): + proc = run_python( + 'import sys; sys.stderr.write("hello world\\n"); print(1); print(2)' + ) + self.reader.start(proc) + self.reader.thread.join() + + self.assertEqual([x.decode("utf8") for x in self.out.output], ["1", "2"]) + self.assertEqual([x.decode("utf8") for x in self.err.output], ["hello world"]) + + def test_finished_callback(self): + self.assertFalse(self.finished) + proc = run_python("") + self.reader.start(proc) + self.reader.thread.join() + self.assertTrue(self.finished) + + def test_timeout(self): + self.reader.timeout = 0.05 + self.assertFalse(self.timeout) + proc = run_python("import time; time.sleep(0.1)") + self.reader.start(proc) + self.reader.thread.join() + self.assertTrue(self.timeout) + self.assertFalse(self.finished) + + def test_output_timeout(self): + self.reader.output_timeout = 0.05 + self.assertFalse(self.timeout) + proc = run_python("import time; time.sleep(0.1)") + self.reader.start(proc) + self.reader.thread.join() + self.assertTrue(self.timeout) + self.assertFalse(self.finished) + + def test_read_without_eol(self): + proc = run_python('import sys; sys.stdout.write("1")') + self.reader.start(proc) + self.reader.thread.join() + self.assertEqual([x.decode("utf8") for x in self.out.output], ["1"]) + + def test_read_with_strange_eol(self): + proc = run_python('import sys; sys.stdout.write("1\\r\\r\\r\\n")') + self.reader.start(proc) + self.reader.thread.join() + self.assertEqual([x.decode("utf8") for x in self.out.output], ["1"]) + + def test_mixed_stdout_stderr(self): + proc = run_python( + 'import sys; sys.stderr.write("hello world\\n"); print(1); print(2)', + stderr=subprocess.STDOUT, + ) + self.reader.start(proc) + self.reader.thread.join() + + self.assertEqual( + sorted([x.decode("utf8") for x in self.out.output]), + sorted(["1", "2", "hello world"]), + ) + self.assertEqual(self.err.output, []) + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprocess/tests/test_wait.py b/testing/mozbase/mozprocess/tests/test_wait.py new file mode 100644 index 0000000000..20d1f0ca17 --- /dev/null +++ b/testing/mozbase/mozprocess/tests/test_wait.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python + +import os +import signal +import sys + +import mozinfo +import mozunit +import proctest +from mozprocess import processhandler + +here = os.path.dirname(os.path.abspath(__file__)) + + +class ProcTestWait(proctest.ProcTest): + """Class to test process waits and timeouts""" + + def test_normal_finish(self): + """Process is started, runs to completion while we wait for it""" + + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_finish.ini"], cwd=here + ) + p.run() + p.wait() + + self.determine_status(p) + + def test_wait(self): + """Process is started runs to completion while we wait indefinitely""" + + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_waittimeout_10s.ini"], cwd=here + ) + p.run() + p.wait() + + self.determine_status(p) + + def test_timeout(self): + """Process is started, runs but we time out waiting on it + to complete + """ + myenv = None + # On macosx1014, subprocess fails to find `six` when run with python3. + # This ensures that subprocess first looks to sys.path to find `six`. + # See https://bugzilla.mozilla.org/show_bug.cgi?id=1562083 + if sys.platform == "darwin" and sys.version_info[0] > 2: + myenv = os.environ.copy() + myenv["PYTHONPATH"] = ":".join(sys.path) + + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_waittimeout.ini"], + cwd=here, + env=myenv, + ) + p.run(timeout=10) + p.wait() + + if mozinfo.isUnix: + # process was killed, so returncode should be negative + self.assertLess(p.proc.returncode, 0) + + self.determine_status(p, False, ["returncode", "didtimeout"]) + + def test_waittimeout(self): + """ + Process is started, then wait is called and times out. + Process is still running and didn't timeout + """ + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_waittimeout_10s.ini"], cwd=here + ) + + p.run() + p.wait(timeout=0) + + self.determine_status(p, True, ()) + + def test_waitnotimeout(self): + """Process is started, runs to completion before our wait times out""" + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_waittimeout_10s.ini"], cwd=here + ) + p.run(timeout=30) + p.wait() + + self.determine_status(p) + + def test_wait_twice_after_kill(self): + """Bug 968718: Process is started and stopped. wait() twice afterward.""" + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_waittimeout.ini"], cwd=here + ) + p.run() + p.kill() + returncode1 = p.wait() + returncode2 = p.wait() + + self.determine_status(p) + + # We killed the process, so the returncode should be non-zero + if mozinfo.isWin: + self.assertGreater( + returncode2, 0, 'Positive returncode expected, got "%s"' % returncode2 + ) + else: + self.assertLess( + returncode2, 0, 'Negative returncode expected, got "%s"' % returncode2 + ) + self.assertEqual( + returncode1, returncode2, "Expected both returncodes of wait() to be equal" + ) + + def test_wait_after_external_kill(self): + """Process is killed externally, and poll() is called.""" + p = processhandler.ProcessHandler( + [self.python, self.proclaunch, "process_normal_finish.ini"], cwd=here + ) + p.run() + os.kill(p.pid, signal.SIGTERM) + returncode = p.wait() + + # We killed the process, so the returncode should be non-zero + if mozinfo.isWin: + self.assertEqual( + returncode, + signal.SIGTERM, + 'Positive returncode expected, got "%s"' % returncode, + ) + else: + self.assertEqual( + returncode, + -signal.SIGTERM, + '%s expected, got "%s"' % (-signal.SIGTERM, returncode), + ) + + self.assertEqual(returncode, p.poll()) + + self.determine_status(p) + + +if __name__ == "__main__": + mozunit.main() |