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

646 lines
25 KiB
Python

import functools
import importlib
import sys
import unittest
from ddtrace.vendor import wrapt
from tests.subprocesstest import SubprocessTestCase, run_in_subprocess
class PatchMixin(unittest.TestCase):
"""
TestCase for testing the patch logic of an integration.
"""
def module_imported(self, modname):
"""
Returns whether a module is imported or not.
"""
return modname in sys.modules
def assert_module_imported(self, modname):
"""
Asserts that the module, given its name is imported.
"""
assert self.module_imported(modname), '{} module not imported'.format(modname)
def assert_not_module_imported(self, modname):
"""
Asserts that the module, given its name is not imported.
"""
assert not self.module_imported(modname), '{} module is imported'.format(modname)
def is_wrapped(self, obj):
return isinstance(obj, wrapt.ObjectProxy)
def assert_wrapped(self, obj):
"""
Helper to assert that a given object is properly wrapped by wrapt.
"""
self.assertTrue(self.is_wrapped(obj), '{} is not wrapped'.format(obj))
def assert_not_wrapped(self, obj):
"""
Helper to assert that a given object is not wrapped by wrapt.
"""
self.assertFalse(self.is_wrapped(obj), '{} is wrapped'.format(obj))
def assert_not_double_wrapped(self, obj):
"""
Helper to assert that a given already wrapped object is not wrapped twice.
This is useful for asserting idempotence.
"""
self.assert_wrapped(obj)
self.assert_not_wrapped(obj.__wrapped__)
def raise_if_no_attrs(f):
"""
A helper for PatchTestCase test methods that will check if there are any
modules to use else raise a NotImplementedError.
:param f: method to wrap with a check
"""
required_attrs = [
'__module_name__',
'__integration_name__',
'__unpatch_func__',
]
@functools.wraps(f)
def checked_method(self, *args, **kwargs):
for attr in required_attrs:
if not getattr(self, attr):
raise NotImplementedError(f.__doc__)
return f(self, *args, **kwargs)
return checked_method
class PatchTestCase(object):
"""
unittest or other test runners will pick up the base test case as a testcase
since it inherits from unittest.TestCase unless we wrap it with this empty
parent class.
"""
@run_in_subprocess
class Base(SubprocessTestCase, PatchMixin):
"""Provides default test methods to be used for testing common integration patching logic.
Each test method provides a default implementation which will use the
provided attributes (described below). If the attributes are not
provided a NotImplementedError will be raised for each method that is
not overridden.
Attributes:
__integration_name__ the name of the integration.
__module_name__ module which the integration patches.
__unpatch_func__ unpatch function from the integration.
Example:
A simple implementation inheriting this TestCase looks like::
from ddtrace.contrib.redis import unpatch
class RedisPatchTestCase(PatchTestCase.Base):
__integration_name__ = 'redis'
__module_name__ 'redis'
__unpatch_func__ = unpatch
def assert_module_patched(self, redis):
# assert patching logic
# self.assert_wrapped(...)
def assert_not_module_patched(self, redis):
# assert patching logic
# self.assert_not_wrapped(...)
def assert_not_module_double_patched(self, redis):
# assert patching logic
# self.assert_not_double_wrapped(...)
# override this particular test case
def test_patch_import(self):
# custom patch before import check
# optionally override other test methods...
"""
__integration_name__ = None
__module_name__ = None
__unpatch_func__ = None
def __init__(self, *args, **kwargs):
# DEV: Python will wrap a function when assigning to a class as an
# attribute. So we cannot call self.__unpatch_func__() as the `self`
# reference will be passed as an argument.
# So we need to unwrap the function and then wrap it in a function
# that will absorb the unpatch function.
if self.__unpatch_func__:
unpatch_func = self.__unpatch_func__.__func__
def unpatch():
unpatch_func()
self.__unpatch_func__ = unpatch
super(PatchTestCase.Base, self).__init__(*args, **kwargs)
def patch(self, *args, **kwargs):
from ddtrace import patch
return patch(*args, **kwargs)
def _gen_test_attrs(self, ops):
"""
A helper to return test names for tests given a list of different
operations.
:return:
"""
from itertools import permutations
return [
'test_{}'.format('_'.join(c)) for c in permutations(ops, len(ops))
]
def test_verify_test_coverage(self):
"""
This TestCase should cover a variety of combinations of importing,
patching and unpatching.
"""
tests = []
tests += self._gen_test_attrs(['import', 'patch'])
tests += self._gen_test_attrs(['import', 'patch', 'patch'])
tests += self._gen_test_attrs(['import', 'patch', 'unpatch'])
tests += self._gen_test_attrs(['import', 'patch', 'unpatch', 'unpatch'])
# TODO: it may be possible to generate test cases dynamically. For
# now focus on the important ones.
test_ignore = set([
'test_unpatch_import_patch',
'test_import_unpatch_patch_unpatch',
'test_import_unpatch_unpatch_patch',
'test_patch_import_unpatch_unpatch',
'test_unpatch_import_patch_unpatch',
'test_unpatch_import_unpatch_patch',
'test_unpatch_patch_import_unpatch',
'test_unpatch_patch_unpatch_import',
'test_unpatch_unpatch_import_patch',
'test_unpatch_unpatch_patch_import',
])
for test_attr in tests:
if test_attr in test_ignore:
continue
assert hasattr(self, test_attr), '{} not found in expected test attrs'.format(test_attr)
def assert_module_patched(self, module):
"""
Asserts that the given module is patched.
For example, the redis integration patches the following methods:
- redis.StrictRedis.execute_command
- redis.StrictRedis.pipeline
- redis.Redis.pipeline
- redis.client.BasePipeline.execute
- redis.client.BasePipeline.immediate_execute_command
So an appropriate assert_module_patched would look like::
def assert_module_patched(self, redis):
self.assert_wrapped(redis.StrictRedis.execute_command)
self.assert_wrapped(redis.StrictRedis.pipeline)
self.assert_wrapped(redis.Redis.pipeline)
self.assert_wrapped(redis.client.BasePipeline.execute)
self.assert_wrapped(redis.client.BasePipeline.immediate_execute_command)
:param module: module to check
:return: None
"""
raise NotImplementedError(self.assert_module_patched.__doc__)
def assert_not_module_patched(self, module):
"""
Asserts that the given module is not patched.
For example, the redis integration patches the following methods:
- redis.StrictRedis.execute_command
- redis.StrictRedis.pipeline
- redis.Redis.pipeline
- redis.client.BasePipeline.execute
- redis.client.BasePipeline.immediate_execute_command
So an appropriate assert_not_module_patched would look like::
def assert_not_module_patched(self, redis):
self.assert_not_wrapped(redis.StrictRedis.execute_command)
self.assert_not_wrapped(redis.StrictRedis.pipeline)
self.assert_not_wrapped(redis.Redis.pipeline)
self.assert_not_wrapped(redis.client.BasePipeline.execute)
self.assert_not_wrapped(redis.client.BasePipeline.immediate_execute_command)
:param module:
:return: None
"""
raise NotImplementedError(self.assert_not_module_patched.__doc__)
def assert_not_module_double_patched(self, module):
"""
Asserts that the given module is not patched twice.
For example, the redis integration patches the following methods:
- redis.StrictRedis.execute_command
- redis.StrictRedis.pipeline
- redis.Redis.pipeline
- redis.client.BasePipeline.execute
- redis.client.BasePipeline.immediate_execute_command
So an appropriate assert_not_module_double_patched would look like::
def assert_not_module_double_patched(self, redis):
self.assert_not_double_wrapped(redis.StrictRedis.execute_command)
self.assert_not_double_wrapped(redis.StrictRedis.pipeline)
self.assert_not_double_wrapped(redis.Redis.pipeline)
self.assert_not_double_wrapped(redis.client.BasePipeline.execute)
self.assert_not_double_wrapped(redis.client.BasePipeline.immediate_execute_command)
:param module: module to check
:return: None
"""
raise NotImplementedError(self.assert_not_module_double_patched.__doc__)
@raise_if_no_attrs
def test_import_patch(self):
"""
The integration should test that each class, method or function that
is to be patched is in fact done so when ddtrace.patch() is called
before the module is imported.
For example:
an appropriate ``test_patch_import`` would be::
import redis
ddtrace.patch(redis=True)
self.assert_module_patched(redis)
"""
self.assert_not_module_imported(self.__module_name__)
module = importlib.import_module(self.__module_name__)
self.assert_not_module_patched(module)
self.patch(**{self.__integration_name__: True})
self.assert_module_patched(module)
@raise_if_no_attrs
def test_patch_import(self):
"""
The integration should test that each class, method or function that
is to be patched is in fact done so when ddtrace.patch() is called
after the module is imported.
an appropriate ``test_patch_import`` would be::
import redis
ddtrace.patch(redis=True)
self.assert_module_patched(redis)
"""
self.assert_not_module_imported(self.__module_name__)
module = importlib.import_module(self.__module_name__)
self.patch(**{self.__integration_name__: True})
self.assert_module_patched(module)
@raise_if_no_attrs
def test_import_patch_patch(self):
"""
Proper testing should be done to ensure that multiple calls to the
integration.patch() method are idempotent. That is, that the
integration does not patch its library more than once.
An example for what this might look like for the redis integration::
import redis
ddtrace.patch(redis=True)
self.assert_module_patched(redis)
ddtrace.patch(redis=True)
self.assert_not_module_double_patched(redis)
"""
self.assert_not_module_imported(self.__module_name__)
self.patch(**{self.__module_name__: True})
module = importlib.import_module(self.__module_name__)
self.assert_module_patched(module)
self.patch(**{self.__module_name__: True})
self.assert_not_module_double_patched(module)
@raise_if_no_attrs
def test_patch_import_patch(self):
"""
Proper testing should be done to ensure that multiple calls to the
integration.patch() method are idempotent. That is, that the
integration does not patch its library more than once.
An example for what this might look like for the redis integration::
ddtrace.patch(redis=True)
import redis
self.assert_module_patched(redis)
ddtrace.patch(redis=True)
self.assert_not_module_double_patched(redis)
"""
self.assert_not_module_imported(self.__module_name__)
self.patch(**{self.__module_name__: True})
module = importlib.import_module(self.__module_name__)
self.assert_module_patched(module)
self.patch(**{self.__module_name__: True})
self.assert_not_module_double_patched(module)
@raise_if_no_attrs
def test_patch_patch_import(self):
"""
Proper testing should be done to ensure that multiple calls to the
integration.patch() method are idempotent. That is, that the
integration does not patch its library more than once.
An example for what this might look like for the redis integration::
ddtrace.patch(redis=True)
ddtrace.patch(redis=True)
import redis
self.assert_not_double_wrapped(redis.StrictRedis.execute_command)
"""
self.assert_not_module_imported(self.__module_name__)
self.patch(**{self.__module_name__: True})
self.patch(**{self.__module_name__: True})
module = importlib.import_module(self.__module_name__)
self.assert_module_patched(module)
self.assert_not_module_double_patched(module)
@raise_if_no_attrs
def test_import_patch_unpatch_patch(self):
"""
To ensure that we can thoroughly test the installation/patching of
an integration we must be able to unpatch it and then subsequently
patch it again.
For example::
import redis
from ddtrace.contrib.redis import unpatch
ddtrace.patch(redis=True)
unpatch()
ddtrace.patch(redis=True)
self.assert_module_patched(redis)
"""
self.assert_not_module_imported(self.__module_name__)
module = importlib.import_module(self.__module_name__)
self.patch(**{self.__integration_name__: True})
self.assert_module_patched(module)
self.__unpatch_func__()
self.assert_not_module_patched(module)
self.patch(**{self.__integration_name__: True})
self.assert_module_patched(module)
@raise_if_no_attrs
def test_patch_import_unpatch_patch(self):
"""
To ensure that we can thoroughly test the installation/patching of
an integration we must be able to unpatch it and then subsequently
patch it again.
For example::
from ddtrace.contrib.redis import unpatch
ddtrace.patch(redis=True)
import redis
unpatch()
ddtrace.patch(redis=True)
self.assert_module_patched(redis)
"""
self.assert_not_module_imported(self.__module_name__)
self.patch(**{self.__integration_name__: True})
module = importlib.import_module(self.__module_name__)
self.assert_module_patched(module)
self.__unpatch_func__()
self.assert_not_module_patched(module)
self.patch(**{self.__integration_name__: True})
self.assert_module_patched(module)
@raise_if_no_attrs
def test_patch_unpatch_import_patch(self):
"""
To ensure that we can thoroughly test the installation/patching of
an integration we must be able to unpatch it and then subsequently
patch it again.
For example::
from ddtrace.contrib.redis import unpatch
ddtrace.patch(redis=True)
import redis
unpatch()
ddtrace.patch(redis=True)
self.assert_module_patched(redis)
"""
self.assert_not_module_imported(self.__module_name__)
self.patch(**{self.__integration_name__: True})
self.__unpatch_func__()
module = importlib.import_module(self.__module_name__)
self.assert_not_module_patched(module)
self.patch(**{self.__integration_name__: True})
self.assert_module_patched(module)
@raise_if_no_attrs
def test_patch_unpatch_patch_import(self):
"""
To ensure that we can thoroughly test the installation/patching of
an integration we must be able to unpatch it and then subsequently
patch it again.
For example::
from ddtrace.contrib.redis import unpatch
ddtrace.patch(redis=True)
unpatch()
ddtrace.patch(redis=True)
import redis
self.assert_module_patched(redis)
"""
self.assert_not_module_imported(self.__module_name__)
self.patch(**{self.__integration_name__: True})
self.__unpatch_func__()
self.patch(**{self.__integration_name__: True})
module = importlib.import_module(self.__module_name__)
self.assert_module_patched(module)
@raise_if_no_attrs
def test_unpatch_patch_import(self):
"""
Make sure unpatching before patch does not break patching.
For example::
from ddtrace.contrib.redis import unpatch
unpatch()
ddtrace.patch(redis=True)
import redis
self.assert_not_module_patched(redis)
"""
self.assert_not_module_imported(self.__module_name__)
self.__unpatch_func__()
self.patch(**{self.__integration_name__: True})
module = importlib.import_module(self.__module_name__)
self.assert_module_patched(module)
@raise_if_no_attrs
def test_patch_unpatch_import(self):
"""
To ensure that we can thoroughly test the installation/patching of
an integration we must be able to unpatch it before importing the
library.
For example::
ddtrace.patch(redis=True)
from ddtrace.contrib.redis import unpatch
unpatch()
import redis
self.assert_not_module_patched(redis)
"""
self.assert_not_module_imported(self.__module_name__)
self.patch(**{self.__integration_name__: True})
self.__unpatch_func__()
module = importlib.import_module(self.__module_name__)
self.assert_not_module_patched(module)
@raise_if_no_attrs
def test_import_unpatch_patch(self):
"""
To ensure that we can thoroughly test the installation/patching of
an integration we must be able to unpatch it before patching.
For example::
import redis
from ddtrace.contrib.redis import unpatch
ddtrace.patch(redis=True)
unpatch()
self.assert_not_module_patched(redis)
"""
self.assert_not_module_imported(self.__module_name__)
module = importlib.import_module(self.__module_name__)
self.__unpatch_func__()
self.assert_not_module_patched(module)
self.patch(**{self.__integration_name__: True})
self.assert_module_patched(module)
@raise_if_no_attrs
def test_import_patch_unpatch(self):
"""
To ensure that we can thoroughly test the installation/patching of
an integration we must be able to unpatch it after patching.
For example::
import redis
from ddtrace.contrib.redis import unpatch
ddtrace.patch(redis=True)
unpatch()
self.assert_not_module_patched(redis)
"""
self.assert_not_module_imported(self.__module_name__)
module = importlib.import_module(self.__module_name__)
self.assert_not_module_patched(module)
self.patch(**{self.__integration_name__: True})
self.assert_module_patched(module)
self.__unpatch_func__()
self.assert_not_module_patched(module)
@raise_if_no_attrs
def test_patch_import_unpatch(self):
"""
To ensure that we can thoroughly test the installation/patching of
an integration we must be able to unpatch it after patching.
For example::
from ddtrace.contrib.redis import unpatch
ddtrace.patch(redis=True)
import redis
unpatch()
self.assert_not_module_patched(redis)
"""
self.assert_not_module_imported(self.__module_name__)
self.patch(**{self.__integration_name__: True})
module = importlib.import_module(self.__module_name__)
self.assert_module_patched(module)
self.__unpatch_func__()
self.assert_not_module_patched(module)
@raise_if_no_attrs
def test_import_patch_unpatch_unpatch(self):
"""
Unpatching twice should be a no-op.
For example::
import redis
from ddtrace.contrib.redis import unpatch
ddtrace.patch(redis=True)
self.assert_module_patched(redis)
unpatch()
self.assert_not_module_patched(redis)
unpatch()
self.assert_not_module_patched(redis)
"""
self.assert_not_module_imported(self.__module_name__)
module = importlib.import_module(self.__module_name__)
self.patch(**{self.__integration_name__: True})
self.assert_module_patched(module)
self.__unpatch_func__()
self.assert_not_module_patched(module)
self.__unpatch_func__()
self.assert_not_module_patched(module)
@raise_if_no_attrs
def test_patch_unpatch_import_unpatch(self):
"""
Unpatching twice should be a no-op.
For example::
from ddtrace.contrib.redis import unpatch
ddtrace.patch(redis=True)
unpatch()
import redis
self.assert_not_module_patched(redis)
unpatch()
self.assert_not_module_patched(redis)
"""
self.assert_not_module_imported(self.__module_name__)
self.patch(**{self.__integration_name__: True})
self.__unpatch_func__()
module = importlib.import_module(self.__module_name__)
self.assert_not_module_patched(module)
self.__unpatch_func__()
self.assert_not_module_patched(module)
@raise_if_no_attrs
def test_patch_unpatch_unpatch_import(self):
"""
Unpatching twice should be a no-op.
For example::
from ddtrace.contrib.redis import unpatch
ddtrace.patch(redis=True)
unpatch()
unpatch()
import redis
self.assert_not_module_patched(redis)
"""
self.assert_not_module_imported(self.__module_name__)
self.patch(**{self.__integration_name__: True})
self.__unpatch_func__()
self.__unpatch_func__()
module = importlib.import_module(self.__module_name__)
self.assert_not_module_patched(module)