mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-07-30 21:56:07 +08:00
646 lines
25 KiB
Python
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)
|