Change RunPythonUnitTests() to run the unit tests in a separate process.

The unit tests could modify global state in hard-to-revert ways and would make the PRESUBMIT.py check flaky.
Having the test running out of process alleviate any potential issue at the cost of speed (more noticeable on Windows).

TEST=unit tests
BUG=none

Review URL: http://codereview.chromium.org/147035

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@19248 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/presubmit_canned_checks.py b/presubmit_canned_checks.py
index 24ae89f..3aaecf8 100755
--- a/presubmit_canned_checks.py
+++ b/presubmit_canned_checks.py
@@ -256,35 +256,53 @@
   return []
 
 
-def _RunPythonUnitTests_LoadTests(input_api, module_name):
-  """Meant to be stubbed out during unit testing."""
-  module = __import__(module_name)
-  for part in module_name.split('.')[1:]:
-    module = getattr(module, part)
-  return input_api.unittest.TestLoader().loadTestsFromModule(module)._tests
-
-
 def RunPythonUnitTests(input_api, output_api, unit_tests):
-  """Imports the unit_tests modules and run them."""
+  """Run the unit tests out of process, capture the output and use the result
+  code to determine success.
+  """
   # We don't want to hinder users from uploading incomplete patches.
   if input_api.is_committing:
     message_type = output_api.PresubmitError
   else:
     message_type = output_api.PresubmitNotifyResult
-  tests_suite = []
   outputs = []
   for unit_test in unit_tests:
-    try:
-      tests_suite.extend(_RunPythonUnitTests_LoadTests(input_api, unit_test))
-    except ImportError:
-      outputs.append(message_type("Failed to load %s" % unit_test,
-                                  long_text=input_api.traceback.format_exc()))
-
-  buffer = input_api.cStringIO.StringIO()
-  results = input_api.unittest.TextTestRunner(stream=buffer, verbosity=0).run(
-      input_api.unittest.TestSuite(tests_suite))
-  if not results.wasSuccessful():
-    outputs.append(message_type("%d unit tests failed." %
-                                (len(results.failures) + len(results.errors)),
-                                long_text=buffer.getvalue()))
-  return outputs
+    # Run the unit tests out of process. This is because some unit tests
+    # stub out base libraries and don't clean up their mess. It's too easy to
+    # get subtle bugs.
+    cwd = None
+    env = None
+    unit_test_name = unit_test
+    # "python -m test.unit_test" doesn't work. We need to change to the right
+    # directory instead.
+    if '.' in unit_test:
+      # Tests imported in submodules (subdirectories) assume that the current
+      # directory is in the PYTHONPATH. Manually fix that.
+      unit_test = unit_test.replace('.', '/')
+      cwd = input_api.os_path.dirname(unit_test)
+      unit_test = input_api.os_path.basename(unit_test)
+      env = input_api.environ.copy()
+      backpath = [';'.join(['..'] * (cwd.count('/') + 1))]
+      if env.get('PYTHONPATH'):
+        backpath.append(env.get('PYTHONPATH'))
+      env['PYTHONPATH'] = ';'.join((backpath))
+    subproc = input_api.subprocess.Popen(
+        [
+          input_api.python_executable,
+          "-m",
+          "%s" % unit_test
+        ],
+        cwd=cwd,
+        env=env,
+        stdin=input_api.subprocess.PIPE,
+        stdout=input_api.subprocess.PIPE,
+        stderr=input_api.subprocess.PIPE)
+    stdoutdata, stderrdata = subproc.communicate()
+    # Discard the output if returncode == 0
+    if subproc.returncode:
+      outputs.append("Test '%s' failed with code %d\n%s\n%s\n" % (
+          unit_test_name, subproc.returncode, stdoutdata, stderrdata))
+  if outputs:
+    return [message_type("%d unit tests failed." % len(outputs),
+                                long_text='\n'.join(outputs))]
+  return []