Files
2020-04-08 10:39:44 -07:00

132 lines
4.2 KiB
Python

"""
subprocesstest enables unittest test cases and suites to be run in separate
python interpreter instances.
A base class SubprocessTestCase is provided that, when extended, will run test
cases marked with @run_in_subprocess in a separate python interpreter.
"""
import os
import subprocess
import sys
import unittest
SUBPROC_TEST_ATTR = '_subproc_test'
SUBPROC_ENV_VAR = 'SUBPROCESS_TEST'
def run_in_subprocess(obj):
"""
Marks a test case that is to be run in its own 'clean' interpreter instance.
When applied to a TestCase class, each method will be run in a separate
interpreter instance.
Usage on a class::
from tests.subprocesstest import SubprocessTestCase, run_in_subprocess
@run_in_subprocess
class PatchTests(SubprocessTestCase):
# will be run in new interpreter
def test_patch_before_import(self):
patch()
import module
# will be run in new interpreter as well
def test_patch_after_import(self):
import module
patch()
Usage on a test method::
class OtherTests(SubprocessTestCase):
@run_in_subprocess
def test_case(self):
pass
:param obj: method or class to run in a separate python interpreter.
:return:
"""
setattr(obj, SUBPROC_TEST_ATTR, True)
return obj
class SubprocessTestCase(unittest.TestCase):
def _full_method_name(self):
test = getattr(self, self._testMethodName)
# DEV: we have to use the internal self reference of the bound test
# method to pull out the class and module since using a mix of `self`
# and the test attributes will result in inconsistencies when the test
# method is defined on another class.
# A concrete case of this is a parent and child TestCase where the child
# doesn't override a parent test method. The full_method_name we want
# is that of the child test method (even though it exists on the parent)
modpath = test.__self__.__class__.__module__
clsname = test.__self__.__class__.__name__
testname = test.__name__
testcase_name = '{}.{}.{}'.format(modpath, clsname, testname)
return testcase_name
def _run_test_in_subprocess(self, result):
full_testcase_name = self._full_method_name()
# copy the environment and include the special subprocess environment
# variable for the subprocess to detect
sp_test_env = os.environ.copy()
sp_test_env[SUBPROC_ENV_VAR] = 'True'
sp_test_cmd = ['python', '-m', 'unittest', full_testcase_name]
sp = subprocess.Popen(
sp_test_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=sp_test_env,
)
stdout, stderr = sp.communicate()
if sp.returncode:
try:
cmdf = ' '.join(sp_test_cmd)
raise Exception('Subprocess Test "{}" Failed'.format(cmdf))
except Exception:
exc_info = sys.exc_info()
# DEV: stderr, stdout are byte sequences so to print them nicely
# back out they should be decoded.
sys.stderr.write(stderr.decode())
sys.stdout.write(stdout.decode())
result.addFailure(self, exc_info)
else:
result.addSuccess(self)
def _in_subprocess(self):
"""Determines if the test is being run in a subprocess.
This is done by checking for an environment variable that we call the
subprocess test with.
:return: whether the test is a subprocess test
"""
return os.getenv(SUBPROC_ENV_VAR, None) is not None
def _is_subprocess_test(self):
if hasattr(self, SUBPROC_TEST_ATTR):
return True
test = getattr(self, self._testMethodName)
if hasattr(test, SUBPROC_TEST_ATTR):
return True
return False
def run(self, result=None):
if not self._is_subprocess_test():
return super(SubprocessTestCase, self).run(result=result)
if self._in_subprocess():
return super(SubprocessTestCase, self).run(result=result)
else:
self._run_test_in_subprocess(result)