summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozprocess/tests
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mozbase/mozprocess/tests')
-rw-r--r--testing/mozbase/mozprocess/tests/manifest.ini12
-rw-r--r--testing/mozbase/mozprocess/tests/process_normal_broad.ini30
-rw-r--r--testing/mozbase/mozprocess/tests/process_normal_deep.ini65
-rw-r--r--testing/mozbase/mozprocess/tests/process_normal_finish.ini17
-rw-r--r--testing/mozbase/mozprocess/tests/process_normal_finish_no_process_group.ini2
-rw-r--r--testing/mozbase/mozprocess/tests/process_waittimeout.ini16
-rw-r--r--testing/mozbase/mozprocess/tests/process_waittimeout_10s.ini16
-rw-r--r--testing/mozbase/mozprocess/tests/proclaunch.py209
-rw-r--r--testing/mozbase/mozprocess/tests/proctest.py62
-rw-r--r--testing/mozbase/mozprocess/tests/scripts/ignore_sigterm.py13
-rw-r--r--testing/mozbase/mozprocess/tests/scripts/infinite_loop.py18
-rw-r--r--testing/mozbase/mozprocess/tests/scripts/proccountfive.py2
-rw-r--r--testing/mozbase/mozprocess/tests/scripts/procnonewline.py4
-rw-r--r--testing/mozbase/mozprocess/tests/test_detached.py62
-rw-r--r--testing/mozbase/mozprocess/tests/test_kill.py144
-rw-r--r--testing/mozbase/mozprocess/tests/test_misc.py63
-rw-r--r--testing/mozbase/mozprocess/tests/test_output.py76
-rw-r--r--testing/mozbase/mozprocess/tests/test_params.py94
-rw-r--r--testing/mozbase/mozprocess/tests/test_pid.py46
-rw-r--r--testing/mozbase/mozprocess/tests/test_poll.py150
-rw-r--r--testing/mozbase/mozprocess/tests/test_process_reader.py114
-rw-r--r--testing/mozbase/mozprocess/tests/test_wait.py144
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()