diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..af309da --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: +- repo: git://github.com/pre-commit/pre-commit-hooks + rev: v1.3.0 + hooks: + - id: trailing-whitespace + - id: check-ast + - id: check-merge-conflict + - id: flake8 +- repo: https://github.com/asottile/seed-isort-config + rev: v1.8.0 + hooks: + - id: seed-isort-config +- repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.17 + hooks: + - id: isort +- repo: https://github.com/ambv/black + rev: 19.3b0 + hooks: + - id: black diff --git a/.travis.yml b/.travis.yml index b60528c..76977b5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ before_install: - docker run -d --rm -p 15672:15672 -p 5672:5672 -p 5671:5671 --name nameko-rabbitmq nameko/nameko-rabbitmq:3.6.6 stages: + - static - test jobs: @@ -29,6 +30,11 @@ jobs: env: DEPS="nameko>=2.12.0" - python: 3.5 env: DEPS="nameko>=2.12.0" + - python: 3.6 + stage: static + install: pip install pre-commit + script: make static + env: matrix: allow_failures: @@ -41,7 +47,7 @@ install: - pip install -U $DEPS script: - - make test + - make pytest deploy: - provider: pypi diff --git a/Makefile b/Makefile index f9e5c62..c4ae0f6 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,7 @@ -test: flake8 pylint pytest +test: static pytest -flake8: - flake8 nameko_tracer tests - -pylint: - pylint nameko_tracer -E +static: + pre-commit run --all-files pytest: coverage run --concurrency=eventlet --source nameko_tracer --branch -m pytest tests diff --git a/nameko_tracer/adapters.py b/nameko_tracer/adapters.py index 444f4c4..c38243d 100644 --- a/nameko_tracer/adapters.py +++ b/nameko_tracer/adapters.py @@ -2,9 +2,9 @@ import logging from traceback import format_exception +import six from nameko.exceptions import get_module_path from nameko.utils import get_redacted_args -import six from werkzeug.wrappers import Response from nameko_tracer import constants, utils @@ -14,7 +14,6 @@ class DefaultAdapter(logging.LoggerAdapter): - def process(self, message, kwargs): """ Extract useful entrypoint processing information @@ -26,15 +25,15 @@ def process(self, message, kwargs): """ - hostname = self.extra['hostname'] + hostname = self.extra["hostname"] - stage = kwargs['extra']['stage'] - worker_ctx = kwargs['extra']['worker_ctx'] - timestamp = kwargs['extra']['timestamp'] + stage = kwargs["extra"]["stage"] + worker_ctx = kwargs["extra"]["worker_ctx"] + timestamp = kwargs["extra"]["timestamp"] entrypoint = worker_ctx.entrypoint - data = kwargs['extra'].get(constants.TRACE_KEY, {}) + data = kwargs["extra"].get(constants.TRACE_KEY, {}) data[constants.TIMESTAMP_KEY] = timestamp data[constants.HOSTNAME_KEY] = hostname @@ -42,8 +41,7 @@ def process(self, message, kwargs): data[constants.ENTRYPOINT_TYPE_KEY] = type(entrypoint).__name__ data[constants.ENTRYPOINT_NAME_KEY] = entrypoint.method_name - data[constants.CONTEXT_DATA_KEY] = utils.safe_for_serialisation( - worker_ctx.data) + data[constants.CONTEXT_DATA_KEY] = utils.safe_for_serialisation(worker_ctx.data) data[constants.CALL_ID_KEY] = worker_ctx.call_id data[constants.CALL_ID_STACK_KEY] = worker_ctx.call_id_stack @@ -57,22 +55,19 @@ def process(self, message, kwargs): if stage == constants.Stage.response: - exc_info = kwargs['extra']['exc_info_'] + exc_info = kwargs["extra"]["exc_info_"] if exc_info: - data[constants.RESPONSE_STATUS_KEY] = ( - constants.Status.error.value) + data[constants.RESPONSE_STATUS_KEY] = constants.Status.error.value self.set_exception(data, worker_ctx, exc_info) else: - data[constants.RESPONSE_STATUS_KEY] = ( - constants.Status.success.value) - result = kwargs['extra']['result'] + data[constants.RESPONSE_STATUS_KEY] = constants.Status.success.value + result = kwargs["extra"]["result"] data[constants.RESPONSE_KEY] = self.get_result(result) - data[constants.RESPONSE_TIME_KEY] = ( - kwargs['extra']['response_time']) + data[constants.RESPONSE_TIME_KEY] = kwargs["extra"]["response_time"] - kwargs['extra'][constants.TRACE_KEY] = data + kwargs["extra"][constants.TRACE_KEY] = data return message, kwargs @@ -82,20 +77,21 @@ def get_call_args(self, worker_ctx): entrypoint = worker_ctx.entrypoint - if getattr(entrypoint, 'sensitive_variables', None): + if getattr(entrypoint, "sensitive_variables", None): # backwards compatibility with nameko < 2.7.0 entrypoint.sensitive_arguments = entrypoint.sensitive_variables - if getattr(entrypoint, 'sensitive_arguments', None): + if getattr(entrypoint, "sensitive_arguments", None): call_args = get_redacted_args( - entrypoint, *worker_ctx.args, **worker_ctx.kwargs) + entrypoint, *worker_ctx.args, **worker_ctx.kwargs + ) redacted = True else: - method = getattr( - entrypoint.container.service_cls, entrypoint.method_name) + method = getattr(entrypoint.container.service_cls, entrypoint.method_name) call_args = inspect.getcallargs( - method, None, *worker_ctx.args, **worker_ctx.kwargs) - del call_args['self'] + method, None, *worker_ctx.args, **worker_ctx.kwargs + ) + del call_args["self"] redacted = False return call_args, redacted @@ -112,14 +108,15 @@ def set_exception(self, data, worker_ctx, exc_info): exc_type, exc, _ = exc_info expected_exceptions = getattr( - worker_ctx.entrypoint, 'expected_exceptions', None) + worker_ctx.entrypoint, "expected_exceptions", None + ) expected_exceptions = expected_exceptions or tuple() is_expected = isinstance(exc, expected_exceptions) try: - exc_traceback = ''.join(format_exception(*exc_info)) + exc_traceback = "".join(format_exception(*exc_info)) except Exception: - exc_traceback = 'traceback serialisation failed' + exc_traceback = "traceback serialisation failed" exc_args = utils.safe_for_serialisation(exc.args) @@ -132,27 +129,26 @@ def set_exception(self, data, worker_ctx, exc_info): class HttpRequestHandlerAdapter(DefaultAdapter): - def get_call_args(self, worker_ctx): """ Transform request object to serialized dictionary """ entrypoint = worker_ctx.entrypoint - method = getattr( - entrypoint.container.service_cls, entrypoint.method_name) + method = getattr(entrypoint.container.service_cls, entrypoint.method_name) call_args = inspect.getcallargs( - method, None, *worker_ctx.args, **worker_ctx.kwargs) - del call_args['self'] + method, None, *worker_ctx.args, **worker_ctx.kwargs + ) + del call_args["self"] - request = call_args.pop('request') + request = call_args.pop("request") data = request.data or request.form - call_args['request'] = { - 'url': request.url, - 'method': request.method, - 'data': utils.safe_for_serialisation(data), - 'headers': dict(self.get_headers(request.environ)), - 'env': dict(self.get_environ(request.environ)), + call_args["request"] = { + "url": request.url, + "method": request.method, + "data": utils.safe_for_serialisation(data), + "headers": dict(self.get_headers(request.environ)), + "env": dict(self.get_environ(request.environ)), } return call_args, False @@ -170,16 +166,13 @@ def get_result(self, result): payload = result status = 200 - result = Response( - payload, - status=status, - ) + result = Response(payload, status=status) return { - 'content_type': result.content_type, - 'data': result.get_data(), - 'status_code': result.status_code, - 'content_length': result.content_length, + "content_type": result.content_type, + "data": result.get_data(), + "status_code": result.status_code, + "content_length": result.content_length, } def get_headers(self, environ): @@ -187,15 +180,17 @@ def get_headers(self, environ): """ for key, value in six.iteritems(environ): key = str(key) - if key.startswith('HTTP_') and key not in \ - ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'): + if key.startswith("HTTP_") and key not in ( + "HTTP_CONTENT_TYPE", + "HTTP_CONTENT_LENGTH", + ): yield key[5:].lower(), str(value) - elif key in ('CONTENT_TYPE', 'CONTENT_LENGTH'): + elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"): yield key.lower(), str(value) def get_environ(self, environ): """ Return white-listed environment variables """ - for key in ('REMOTE_ADDR', 'SERVER_NAME', 'SERVER_PORT'): + for key in ("REMOTE_ADDR", "SERVER_NAME", "SERVER_PORT"): if key in environ: yield key.lower(), str(environ[key]) diff --git a/nameko_tracer/constants.py b/nameko_tracer/constants.py index b9a8703..a4e0f2d 100644 --- a/nameko_tracer/constants.py +++ b/nameko_tracer/constants.py @@ -4,18 +4,20 @@ class Stage(Enum): """ Entrypoint stage """ - request = 'request' - response = 'response' + + request = "request" + response = "response" class Status(Enum): """ Entrypoint result status """ - success = 'success' - error = 'error' + + success = "success" + error = "error" -LOGGER_NAME = 'nameko_tracer' +LOGGER_NAME = "nameko_tracer" """ Name of the logger used for entrypoint logging Use this name to configure entrypoint logging in ``LOGGING`` setting @@ -23,7 +25,7 @@ class Status(Enum): """ -TRACE_KEY = 'nameko_trace' +TRACE_KEY = "nameko_trace" """ Name of the log record attribute holding the serialisable details Contains gathered entrypoint call and result details in serialisable @@ -31,60 +33,60 @@ class Status(Enum): """ -TIMESTAMP_KEY = 'timestamp' +TIMESTAMP_KEY = "timestamp" """ A key holding the entrypoint stage timestamp """ -STAGE_KEY = 'stage' +STAGE_KEY = "stage" """ A key holding the lifecycle stage (a value of one of ``Stage`` options) """ -HOSTNAME_KEY = 'hostname' +HOSTNAME_KEY = "hostname" """ A key holding the service host name """ -REQUEST_KEY = 'call_args' +REQUEST_KEY = "call_args" """ A key holding a dictionary of arguments passed to the entrypoint call """ -REQUEST_REDACTED_KEY = 'call_args_redacted' +REQUEST_REDACTED_KEY = "call_args_redacted" """ A key holding a boolean value saying whether sensitive values of the entrypoint call arguments were redacted. """ -RESPONSE_KEY = 'response' +RESPONSE_KEY = "response" """ A key holding serialisable return value of the entrypoint. """ -REQUEST_TRUNCATED_KEY = 'call_args_truncated' +REQUEST_TRUNCATED_KEY = "call_args_truncated" """ A key holding a boolean value saying whether the call args data were truncated. Set by ``TruncateRequestFilter``. """ -RESPONSE_TRUNCATED_KEY = 'response_truncated' +RESPONSE_TRUNCATED_KEY = "response_truncated" """ A key holding a boolean value saying whether the result data were truncated. Set by ``TruncateResponseFilter``. """ -REQUEST_LENGTH_KEY = 'call_args_length' +REQUEST_LENGTH_KEY = "call_args_length" """ A key holding the original call args data length Set by ``TruncateRequestFilter`` to the original length of data in ``REQUEST_KEY``. """ -RESPONSE_LENGTH_KEY = 'response_length' +RESPONSE_LENGTH_KEY = "response_length" """ A key holding the original result data length Set by ``TruncateResponseFilter`` to the original length of data in @@ -92,92 +94,93 @@ class Status(Enum): """ -RESPONSE_STATUS_KEY = 'response_status' +RESPONSE_STATUS_KEY = "response_status" """ A key holding the result status (a value of one of ``Status`` options) """ -RESPONSE_TIME_KEY = 'response_time' +RESPONSE_TIME_KEY = "response_time" """ A key holding the amount of time taken between the two stages """ -EXCEPTION_TYPE_KEY = 'exception_type' +EXCEPTION_TYPE_KEY = "exception_type" """ A key holding exception type name Set if the entrypoint resulted into an error """ -EXCEPTION_PATH_KEY = 'exception_path' +EXCEPTION_PATH_KEY = "exception_path" """ A key holding exception path e.g. ``some.module.SomeError`` Set if the entrypoint resulted into an error """ -EXCEPTION_VALUE_KEY = 'exception_value' +EXCEPTION_VALUE_KEY = "exception_value" """ A key holding string representation of exception raised Set if the entrypoint resulted into an error """ -EXCEPTION_ARGS_KEY = 'exception_args' +EXCEPTION_ARGS_KEY = "exception_args" """ A key holding a list of exception arguments Set if the entrypoint resulted into an error """ -EXCEPTION_TRACEBACK_KEY = 'exception_traceback' +EXCEPTION_TRACEBACK_KEY = "exception_traceback" """ A key holding exception traceback string Set if the entrypoint resulted into an error """ -EXCEPTION_EXPECTED_KEY = 'exception_expected' +EXCEPTION_EXPECTED_KEY = "exception_expected" """ A key holding a boolean saying whether the exception raised was one of errors expected by the entrypoint """ -SERVICE_NAME_KEY = 'service' +SERVICE_NAME_KEY = "service" """ A key holding the name of the service """ -ENTRYPOINT_NAME_KEY = 'entrypoint_name' +ENTRYPOINT_NAME_KEY = "entrypoint_name" """ A key holding the entrypoint service method name e.g. ``'get_user'`` """ -ENTRYPOINT_TYPE_KEY = 'entrypoint_type' +ENTRYPOINT_TYPE_KEY = "entrypoint_type" """ A key holding the entrypoint type name e.g. ``'Rpc'``. """ -CALL_ID_KEY = 'call_id' +CALL_ID_KEY = "call_id" """ A key holding the unique ID of the entrypoint call """ -CALL_ID_STACK_KEY = 'call_id_stack' +CALL_ID_STACK_KEY = "call_id_stack" """ A key holding the call ID stack ... """ -ORIGIN_CALL_ID_KEY = 'origin_call_id' +ORIGIN_CALL_ID_KEY = "origin_call_id" """ A key holding the Source of all stack calls """ -CONTEXT_DATA_KEY = 'context_data' +CONTEXT_DATA_KEY = "context_data" """ A key holding the worker context data dictionary """ DEFAULT_ADAPTERS = { - 'nameko.web.handlers.HttpRequestHandler': ( - 'nameko_tracer.adapters.HttpRequestHandlerAdapter'), + "nameko.web.handlers.HttpRequestHandler": ( + "nameko_tracer.adapters.HttpRequestHandlerAdapter" + ) } """ Default adapter overrides setup @@ -187,12 +190,12 @@ class Status(Enum): """ -CONFIG_KEY = 'TRACER' +CONFIG_KEY = "TRACER" """ Nameko config key holding tracer configuration """ -ADAPTERS_CONFIG_KEY = 'ADAPTERS' +ADAPTERS_CONFIG_KEY = "ADAPTERS" """ A key holding adapters configuration in Nameko config (under ``CONFIG_KEY``) """ diff --git a/nameko_tracer/dependency.py b/nameko_tracer/dependency.py index 36cc217..d083d9d 100644 --- a/nameko_tracer/dependency.py +++ b/nameko_tracer/dependency.py @@ -1,7 +1,7 @@ -from collections import defaultdict -from datetime import datetime import logging import socket +from collections import defaultdict +from datetime import datetime from weakref import WeakKeyDictionary from nameko.extensions import DependencyProvider @@ -28,8 +28,7 @@ def setup(self): config = self.container.config.get(constants.CONFIG_KEY, {}) self.configure_adapter_types(constants.DEFAULT_ADAPTERS) - self.configure_adapter_types( - config.get(constants.ADAPTERS_CONFIG_KEY, {})) + self.configure_adapter_types(config.get(constants.ADAPTERS_CONFIG_KEY, {})) self.logger = logging.getLogger(constants.LOGGER_NAME) @@ -41,7 +40,7 @@ def configure_adapter_types(self, adapters_config): def adapter_factory(self, worker_ctx): adapter_class = self.adapter_types[type(worker_ctx.entrypoint)] - extra = {'hostname': socket.gethostname()} + extra = {"hostname": socket.gethostname()} return adapter_class(self.logger, extra=extra) def worker_setup(self, worker_ctx): @@ -53,17 +52,14 @@ def worker_setup(self, worker_ctx): try: extra = { - 'stage': constants.Stage.request, - 'worker_ctx': worker_ctx, - 'timestamp': timestamp, + "stage": constants.Stage.request, + "worker_ctx": worker_ctx, + "timestamp": timestamp, } adapter = self.adapter_factory(worker_ctx) - adapter.info( - '[%s] entrypoint call trace', - worker_ctx.call_id, - extra=extra) + adapter.info("[%s] entrypoint call trace", worker_ctx.call_id, extra=extra) except Exception: - logger.warning('Failed to log entrypoint trace', exc_info=True) + logger.warning("Failed to log entrypoint trace", exc_info=True) def worker_result(self, worker_ctx, result=None, exc_info=None): """ Log entrypoint result details @@ -75,23 +71,21 @@ def worker_result(self, worker_ctx, result=None, exc_info=None): try: extra = { - 'stage': constants.Stage.response, - 'worker_ctx': worker_ctx, - 'result': result, - 'exc_info_': exc_info, - 'timestamp': timestamp, - 'response_time': response_time, + "stage": constants.Stage.response, + "worker_ctx": worker_ctx, + "result": result, + "exc_info_": exc_info, + "timestamp": timestamp, + "response_time": response_time, } adapter = self.adapter_factory(worker_ctx) if exc_info: adapter.warning( - '[%s] entrypoint result trace', - worker_ctx.call_id, - extra=extra) + "[%s] entrypoint result trace", worker_ctx.call_id, extra=extra + ) else: adapter.info( - '[%s] entrypoint result trace', - worker_ctx.call_id, - extra=extra) + "[%s] entrypoint result trace", worker_ctx.call_id, extra=extra + ) except Exception: - logger.warning('Failed to log entrypoint trace', exc_info=True) + logger.warning("Failed to log entrypoint trace", exc_info=True) diff --git a/nameko_tracer/filters.py b/nameko_tracer/filters.py index 9604587..5a56a08 100644 --- a/nameko_tracer/filters.py +++ b/nameko_tracer/filters.py @@ -2,7 +2,6 @@ import logging import re - from nameko_tracer import constants, utils @@ -54,7 +53,7 @@ def truncate(self, data): call_args = utils.serialise_to_string(data[constants.REQUEST_KEY]) length = len(call_args) if length > self.max_len: - data[constants.REQUEST_KEY] = call_args[:self.max_len] + data[constants.REQUEST_KEY] = call_args[: self.max_len] truncated = True else: truncated = False @@ -77,7 +76,7 @@ class TruncateResponseFilter(BaseTruncateFilter): """ - default_entrypoints = ['^get_|^list_|^query_'] + default_entrypoints = ["^get_|^list_|^query_"] def truncate(self, data): @@ -87,7 +86,7 @@ def truncate(self, data): result = utils.serialise_to_string(data[constants.RESPONSE_KEY]) length = len(result) if length > self.max_len: - data[constants.RESPONSE_KEY] = result[:self.max_len] + data[constants.RESPONSE_KEY] = result[: self.max_len] truncated = True else: truncated = False diff --git a/nameko_tracer/formatters.py b/nameko_tracer/formatters.py index 77e4132..7764299 100644 --- a/nameko_tracer/formatters.py +++ b/nameko_tracer/formatters.py @@ -32,7 +32,8 @@ class ElasticsearchDocumentFormatter(JSONFormatter): constants.CONTEXT_DATA_KEY, constants.REQUEST_KEY, constants.RESPONSE_KEY, - constants.EXCEPTION_ARGS_KEY) + constants.EXCEPTION_ARGS_KEY, + ) def format(self, record): diff --git a/nameko_tracer/utils.py b/nameko_tracer/utils.py index 2d2a96b..e8cc16b 100644 --- a/nameko_tracer/utils.py +++ b/nameko_tracer/utils.py @@ -1,7 +1,7 @@ import collections -from importlib import import_module import json import logging +from importlib import import_module import six @@ -26,19 +26,19 @@ def safe_for_serialisation(value): if isinstance(value, no_op_types): return value if isinstance(value, bytes): - return value.decode('utf-8', 'ignore') + return value.decode("utf-8", "ignore") if isinstance(value, dict): return { safe_for_serialisation(key): safe_for_serialisation(val) - for key, val in six.iteritems(value)} + for key, val in six.iteritems(value) + } if isinstance(value, collections.Iterable): return list(map(safe_for_serialisation, value)) try: return six.text_type(value) except Exception: - logger.warning( - 'failed to get string representation', exc_info=True) - return 'failed to get string representation' + logger.warning("failed to get string representation", exc_info=True) + return "failed to get string representation" def import_by_path(dotted_path): @@ -50,10 +50,9 @@ def import_by_path(dotted_path): """ try: - module_path, class_name = dotted_path.rsplit('.', 1) + module_path, class_name = dotted_path.rsplit(".", 1) except ValueError: - raise ImportError( - "{} doesn't look like a module path".format(dotted_path)) + raise ImportError("{} doesn't look like a module path".format(dotted_path)) module = import_module(module_path) @@ -61,5 +60,7 @@ def import_by_path(dotted_path): return getattr(module, class_name) except AttributeError: raise ImportError( - 'Module "{}" does not define a "{}" attribute/class' - .format(module_path, class_name)) + 'Module "{}" does not define a "{}" attribute/class'.format( + module_path, class_name + ) + ) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..0900450 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,17 @@ +[bdist_wheel] +universal = 1 + +[isort] +line_length=88 +known_first_party=nameko_tracer +known_third_party = kombu,mock,nameko,pytest,setuptools,six,werkzeug +multi_line_output=3 +indent=' ' +include_trailing_comma=true +forced_separate=test +default_section=THIRDPARTY +lines_after_imports=2 +skip=.tox,.git + +[flake8] +max-line-length = 88 diff --git a/setup.py b/setup.py index 88a445b..c338e63 100644 --- a/setup.py +++ b/setup.py @@ -1,26 +1,18 @@ #!/usr/bin/env python from setuptools import find_packages, setup + setup( - name='nameko-tracer', - version='1.2.0', - description='Nameko extension logging entrypoint processing metrics', - author='student.com', - author_email='wearehiring@student.com', - url='https://github.com/nameko/nameko-tracer', - packages=find_packages(exclude=['test', 'test.*']), - install_requires=[ - "nameko>=2.8.5", - ], - extras_require={ - 'dev': [ - "coverage", - "flake8", - "pylint", - "pytest", - ] - }, + name="nameko-tracer", + version="1.2.0", + description="Nameko extension logging entrypoint processing metrics", + author="student.com", + author_email="wearehiring@student.com", + url="https://github.com/nameko/nameko-tracer", + packages=find_packages(exclude=["test", "test.*"]), + install_requires=["nameko>=2.8.5"], + extras_require={"dev": ["coverage", "pytest", "pre-commit"]}, dependency_links=[], zip_safe=True, - license='Apache License, Version 2.0' + license="Apache License, Version 2.0", ) diff --git a/tests/test_adapters.py b/tests/test_adapters.py index 6c446bf..f0da634 100644 --- a/tests/test_adapters.py +++ b/tests/test_adapters.py @@ -1,18 +1,18 @@ -from datetime import datetime import json import logging import logging.handlers +from datetime import datetime +import pytest from kombu import Exchange, Queue from mock import patch from nameko.containers import WorkerContext from nameko.events import EventHandler, event_handler from nameko.messaging import Consumer, consume -from nameko.rpc import rpc, Rpc -from nameko.web.handlers import http, HttpRequestHandler -from nameko.testing.services import dummy, Entrypoint +from nameko.rpc import Rpc, rpc +from nameko.testing.services import Entrypoint, dummy from nameko.testing.utils import get_extension -import pytest +from nameko.web.handlers import HttpRequestHandler, http from werkzeug.test import create_environ from werkzeug.wrappers import Request, Response @@ -21,9 +21,7 @@ @pytest.fixture def tracker(): - class Tracker(logging.Handler): - def __init__(self, *args, **kwargs): self.log_records = [] super(Tracker, self).__init__(*args, **kwargs) @@ -45,14 +43,12 @@ def logger(tracker): class TestDefaultAdapter: - @pytest.fixture def container(self, container_factory, rabbit_config, service_class): return container_factory(service_class, rabbit_config) @pytest.fixture def service_class(self): - class Service(object): name = "some-service" @@ -65,205 +61,187 @@ def some_method(self, spam): @pytest.fixture def worker_ctx(self, container, service_class): - entrypoint = get_extension( - container, Entrypoint, method_name='some_method') - return WorkerContext( - container, service_class, entrypoint, args=('some-arg',)) + entrypoint = get_extension(container, Entrypoint, method_name="some_method") + return WorkerContext(container, service_class, entrypoint, args=("some-arg",)) @pytest.fixture def adapter(self, logger): - adapter = adapters.DefaultAdapter( - logger, extra={'hostname': 'some.host'}) + adapter = adapters.DefaultAdapter(logger, extra={"hostname": "some.host"}) return adapter @pytest.mark.parametrize( - 'stage', - (constants.Stage.request, constants.Stage.response), + "stage", (constants.Stage.request, constants.Stage.response) ) - def test_common_worker_data( - self, adapter, tracker, worker_ctx, stage - ): + def test_common_worker_data(self, adapter, tracker, worker_ctx, stage): extra = { - 'stage': stage, - 'worker_ctx': worker_ctx, - 'result': None, - 'exc_info_': None, - 'timestamp': datetime(2017, 7, 7, 12, 0, 0), - 'response_time': 60.0, + "stage": stage, + "worker_ctx": worker_ctx, + "result": None, + "exc_info_": None, + "timestamp": datetime(2017, 7, 7, 12, 0, 0), + "response_time": 60.0, } - adapter.info('spam', extra=extra) + adapter.info("spam", extra=extra) log_record = tracker.log_records[-1] data = getattr(log_record, constants.TRACE_KEY) - assert data['hostname'] == 'some.host' - assert data['service'] == 'some-service' - assert data['entrypoint_type'] == 'Entrypoint' - assert data['entrypoint_name'] == 'some_method' - assert data['call_id'] == worker_ctx.call_id - assert data['call_id_stack'] == worker_ctx.call_id_stack - assert data['stage'] == stage.value + assert data["hostname"] == "some.host" + assert data["service"] == "some-service" + assert data["entrypoint_type"] == "Entrypoint" + assert data["entrypoint_name"] == "some_method" + assert data["call_id"] == worker_ctx.call_id + assert data["call_id_stack"] == worker_ctx.call_id_stack + assert data["stage"] == stage.value @pytest.mark.parametrize( - 'stage', - (constants.Stage.request, constants.Stage.response), + "stage", (constants.Stage.request, constants.Stage.response) ) - def test_worker_ctx_data( - self, adapter, tracker, worker_ctx, stage - ): + def test_worker_ctx_data(self, adapter, tracker, worker_ctx, stage): worker_ctx.data = { - 'some-key': 'simple-data', - 'some-other-key': {'a bit more': ['complex data', 1, None]}, + "some-key": "simple-data", + "some-other-key": {"a bit more": ["complex data", 1, None]}, } extra = { - 'stage': stage, - 'worker_ctx': worker_ctx, - 'result': None, - 'exc_info_': None, - 'timestamp': None, - 'response_time': None, + "stage": stage, + "worker_ctx": worker_ctx, + "result": None, + "exc_info_": None, + "timestamp": None, + "response_time": None, } - adapter.info('spam', extra=extra) + adapter.info("spam", extra=extra) log_record = tracker.log_records[-1] data = getattr(log_record, constants.TRACE_KEY) - assert data['context_data']['some-key'] == 'simple-data' - assert ( - data['context_data']['some-other-key'] == - {'a bit more': ['complex data', 1, None]}) + assert data["context_data"]["some-key"] == "simple-data" + assert data["context_data"]["some-other-key"] == { + "a bit more": ["complex data", 1, None] + } @pytest.mark.parametrize( - 'stage', - (constants.Stage.request, constants.Stage.response), + "stage", (constants.Stage.request, constants.Stage.response) ) - def test_call_args_data( - self, adapter, tracker, worker_ctx, stage - ): + def test_call_args_data(self, adapter, tracker, worker_ctx, stage): extra = { - 'stage': stage, - 'worker_ctx': worker_ctx, - 'result': None, - 'exc_info_': None, - 'timestamp': None, - 'response_time': None, + "stage": stage, + "worker_ctx": worker_ctx, + "result": None, + "exc_info_": None, + "timestamp": None, + "response_time": None, } - adapter.info('spam', extra=extra) + adapter.info("spam", extra=extra) log_record = tracker.log_records[-1] data = getattr(log_record, constants.TRACE_KEY) - assert data['call_args'] == {'spam': 'some-arg'} - assert data['call_args_redacted'] is False + assert data["call_args"] == {"spam": "some-arg"} + assert data["call_args_redacted"] is False - @pytest.fixture(params=['sensitive_variables', 'sensitive_arguments']) + @pytest.fixture(params=["sensitive_variables", "sensitive_arguments"]) def compatibility_shim(self, request): return request.param @pytest.mark.parametrize( - 'stage', - (constants.Stage.request, constants.Stage.response), + "stage", (constants.Stage.request, constants.Stage.response) ) def test_sensitive_call_args_data( self, adapter, tracker, worker_ctx, stage, compatibility_shim ): - setattr(worker_ctx.entrypoint, compatibility_shim, ('spam')) + setattr(worker_ctx.entrypoint, compatibility_shim, ("spam")) extra = { - 'stage': stage, - 'worker_ctx': worker_ctx, - 'result': None, - 'exc_info_': None, - 'timestamp': None, - 'response_time': None, + "stage": stage, + "worker_ctx": worker_ctx, + "result": None, + "exc_info_": None, + "timestamp": None, + "response_time": None, } - adapter.info('spam', extra=extra) + adapter.info("spam", extra=extra) log_record = tracker.log_records[-1] data = getattr(log_record, constants.TRACE_KEY) - assert data['call_args'] == {'spam': '********'} - assert data['call_args_redacted'] is True + assert data["call_args"] == {"spam": "********"} + assert data["call_args_redacted"] is True @pytest.mark.parametrize( - ('result_in', 'expected_result_out'), - ( - (None, None), - ('spam', 'spam'), - ({'spam': 'ham'}, {'spam': 'ham'}), - ), + ("result_in", "expected_result_out"), + ((None, None), ("spam", "spam"), ({"spam": "ham"}, {"spam": "ham"})), ) def test_result_data( self, adapter, tracker, worker_ctx, result_in, expected_result_out ): extra = { - 'stage': constants.Stage.response, - 'worker_ctx': worker_ctx, - 'result': result_in, - 'exc_info_': None, - 'timestamp': None, - 'response_time': None, + "stage": constants.Stage.response, + "worker_ctx": worker_ctx, + "result": result_in, + "exc_info_": None, + "timestamp": None, + "response_time": None, } - adapter.info('spam', extra=extra) + adapter.info("spam", extra=extra) log_record = tracker.log_records[-1] data = getattr(log_record, constants.TRACE_KEY) - assert data['response'] == expected_result_out - assert data['response_status'] == constants.Status.success.value + assert data["response"] == expected_result_out + assert data["response_status"] == constants.Status.success.value def test_exception_data(self, adapter, tracker, worker_ctx): - class Error(Exception): pass - exception = Error('Yo!') + exception = Error("Yo!") exc_info = (Error, exception, exception.__traceback__) extra = { - 'stage': constants.Stage.response, - 'worker_ctx': worker_ctx, - 'result': None, - 'exc_info_': exc_info, - 'timestamp': None, - 'response_time': None, + "stage": constants.Stage.response, + "worker_ctx": worker_ctx, + "result": None, + "exc_info_": exc_info, + "timestamp": None, + "response_time": None, } - adapter.info('spam', extra=extra) + adapter.info("spam", extra=extra) log_record = tracker.log_records[-1] data = getattr(log_record, constants.TRACE_KEY) - assert 'response' not in data + assert "response" not in data - assert data['exception_type'] == 'Error' - assert data['exception_path'] == 'test_adapters.Error' - assert data['exception_args'] == ["Yo!"] - assert data['exception_value'] == 'Yo!' + assert data["exception_type"] == "Error" + assert data["exception_path"] == "test_adapters.Error" + assert data["exception_args"] == ["Yo!"] + assert data["exception_value"] == "Yo!" - assert 'Error: Yo!' in data['exception_traceback'] + assert "Error: Yo!" in data["exception_traceback"] - assert data['exception_expected'] is False + assert data["exception_expected"] is False - assert data['response_status'] == constants.Status.error.value + assert data["response_status"] == constants.Status.error.value - @pytest.mark.parametrize('expected_exceptions', (None, (), (ValueError))) + @pytest.mark.parametrize("expected_exceptions", (None, (), (ValueError))) def test_exception_data_unexpected_exception( self, adapter, tracker, worker_ctx, expected_exceptions ): @@ -273,56 +251,53 @@ def test_exception_data_unexpected_exception( class Error(Exception): pass - exception = Error('Yo!') + exception = Error("Yo!") exc_info = (Error, exception, exception.__traceback__) extra = { - 'stage': constants.Stage.response, - 'worker_ctx': worker_ctx, - 'result': None, - 'exc_info_': exc_info, - 'timestamp': None, - 'response_time': None, + "stage": constants.Stage.response, + "worker_ctx": worker_ctx, + "result": None, + "exc_info_": exc_info, + "timestamp": None, + "response_time": None, } - adapter.info('spam', extra=extra) + adapter.info("spam", extra=extra) log_record = tracker.log_records[-1] data = getattr(log_record, constants.TRACE_KEY) - assert data['exception_expected'] is False - - def test_exception_data_expected_exception( - self, adapter, tracker, worker_ctx - ): + assert data["exception_expected"] is False + def test_exception_data_expected_exception(self, adapter, tracker, worker_ctx): class Error(Exception): pass - worker_ctx.entrypoint.expected_exceptions = (Error) + worker_ctx.entrypoint.expected_exceptions = Error - exception = Error('Yo!') + exception = Error("Yo!") exc_info = (Error, exception, exception.__traceback__) extra = { - 'stage': constants.Stage.response, - 'worker_ctx': worker_ctx, - 'result': None, - 'exc_info_': exc_info, - 'timestamp': None, - 'response_time': None, + "stage": constants.Stage.response, + "worker_ctx": worker_ctx, + "result": None, + "exc_info_": exc_info, + "timestamp": None, + "response_time": None, } - adapter.info('spam', extra=extra) + adapter.info("spam", extra=extra) log_record = tracker.log_records[-1] data = getattr(log_record, constants.TRACE_KEY) - assert data['exception_expected'] is True + assert data["exception_expected"] is True - @patch('nameko_tracer.adapters.format_exception') + @patch("nameko_tracer.adapters.format_exception") def test_exception_data_deals_with_failing_exception_serialisation( self, format_exception, adapter, tracker, worker_ctx ): @@ -332,39 +307,37 @@ def test_exception_data_deals_with_failing_exception_serialisation( class Error(Exception): pass - exception = Error('Yo!') + exception = Error("Yo!") exc_info = (Error, exception, exception.__traceback__) extra = { - 'stage': constants.Stage.response, - 'worker_ctx': worker_ctx, - 'result': None, - 'exc_info_': exc_info, - 'timestamp': None, - 'response_time': None, + "stage": constants.Stage.response, + "worker_ctx": worker_ctx, + "result": None, + "exc_info_": exc_info, + "timestamp": None, + "response_time": None, } - adapter.info('spam', extra=extra) + adapter.info("spam", extra=extra) log_record = tracker.log_records[-1] data = getattr(log_record, constants.TRACE_KEY) - assert 'response' not in data + assert "response" not in data - assert data['exception_type'] == 'Error' - assert data['exception_path'] == 'test_adapters.Error' - assert data['exception_args'] == ["Yo!"] - assert data['exception_type'] == 'Error' - assert data['exception_value'] == 'Yo!' + assert data["exception_type"] == "Error" + assert data["exception_path"] == "test_adapters.Error" + assert data["exception_args"] == ["Yo!"] + assert data["exception_type"] == "Error" + assert data["exception_value"] == "Yo!" - assert ( - data['exception_traceback'] == - 'traceback serialisation failed') + assert data["exception_traceback"] == "traceback serialisation failed" - assert data['exception_expected'] is False + assert data["exception_expected"] is False - assert data['response_status'] == constants.Status.error.value + assert data["response_status"] == constants.Status.error.value @pytest.fixture(params=[Rpc, EventHandler, Consumer]) def entrypoint(self, request, container_factory, rabbit_config): @@ -385,8 +358,7 @@ def rpc(self, payload): def event_handler(self, payload): pass - @consume(queue=Queue( - 'service', exchange=exchange, routing_key=ROUTING_KEY)) + @consume(queue=Queue("service", exchange=exchange, routing_key=ROUTING_KEY)) def consume(self, payload): pass @@ -394,13 +366,12 @@ def consume(self, payload): extension_class = request.param - methods = { - Rpc: 'rpc', EventHandler: 'event_handler', Consumer: 'consume'} + methods = {Rpc: "rpc", EventHandler: "event_handler", Consumer: "consume"} entrypoint = get_extension( - container, extension_class, method_name=methods[extension_class]) - worker_context = WorkerContext( - container, Service, entrypoint, args=('spam',)) + container, extension_class, method_name=methods[extension_class] + ) + worker_context = WorkerContext(container, Service, entrypoint, args=("spam",)) return entrypoint, worker_context @@ -409,42 +380,40 @@ def test_amqp_entrypoints(self, adapter, entrypoint, tracker): entrypoint, worker_ctx = entrypoint extra = { - 'stage': constants.Stage.response, - 'worker_ctx': worker_ctx, - 'result': {'some': 'data'}, - 'exc_info_': None, - 'timestamp': None, - 'response_time': None, + "stage": constants.Stage.response, + "worker_ctx": worker_ctx, + "result": {"some": "data"}, + "exc_info_": None, + "timestamp": None, + "response_time": None, } - adapter.info('spam', extra=extra) + adapter.info("spam", extra=extra) log_record = tracker.log_records[-1] data = getattr(log_record, constants.TRACE_KEY) - assert data['response'] == {'some': 'data'} - assert data['response_status'] == constants.Status.success.value - assert data['entrypoint_type'] == entrypoint.__class__.__name__ - assert data['entrypoint_name'] == entrypoint.method_name + assert data["response"] == {"some": "data"} + assert data["response_status"] == constants.Status.success.value + assert data["entrypoint_type"] == entrypoint.__class__.__name__ + assert data["entrypoint_name"] == entrypoint.method_name class TestHttpRequestHandlerAdapter: - @pytest.fixture def container(self, container_factory, rabbit_config, service_class): return container_factory(service_class, rabbit_config) @pytest.fixture def service_class(self): - class Service(object): name = "some-service" - @http('GET', '/spam/') + @http("GET", "/spam/") def some_method(self, request, value): - payload = {'value': value} + payload = {"value": value} return json.dumps(payload) return Service @@ -453,148 +422,129 @@ def some_method(self, request, value): def worker_ctx(self, container, service_class): environ = create_environ( - '/spam/1?test=123', - 'http://localhost:8080/', - data=json.dumps({'foo': 'bar'}), - content_type='application/json' + "/spam/1?test=123", + "http://localhost:8080/", + data=json.dumps({"foo": "bar"}), + content_type="application/json", ) request = Request(environ) entrypoint = get_extension( - container, HttpRequestHandler, method_name='some_method') - return WorkerContext( - container, service_class, entrypoint, args=(request, 1)) + container, HttpRequestHandler, method_name="some_method" + ) + return WorkerContext(container, service_class, entrypoint, args=(request, 1)) @pytest.fixture def adapter(self, logger): adapter = adapters.HttpRequestHandlerAdapter( - logger, extra={'hostname': 'some.host'}) + logger, extra={"hostname": "some.host"} + ) return adapter @pytest.mark.parametrize( - 'stage', - (constants.Stage.request, constants.Stage.response), + "stage", (constants.Stage.request, constants.Stage.response) ) - def test_call_args_data( - self, adapter, tracker, worker_ctx, stage - ): + def test_call_args_data(self, adapter, tracker, worker_ctx, stage): extra = { - 'stage': stage, - 'worker_ctx': worker_ctx, - 'result': Response( - json.dumps({"value": 1}), mimetype='application/json'), - 'exc_info_': None, - 'timestamp': None, - 'response_time': None, + "stage": stage, + "worker_ctx": worker_ctx, + "result": Response(json.dumps({"value": 1}), mimetype="application/json"), + "exc_info_": None, + "timestamp": None, + "response_time": None, } - adapter.info('spam', extra=extra) + adapter.info("spam", extra=extra) log_record = tracker.log_records[-1] data = getattr(log_record, constants.TRACE_KEY) - call_args = data['call_args'] + call_args = data["call_args"] - assert call_args['value'] == 1 + assert call_args["value"] == 1 - request = call_args['request'] + request = call_args["request"] - assert request['method'] == 'GET' - assert request['url'] == 'http://localhost:8080/spam/1?test=123' - assert request['env']['server_port'] == '8080' - assert request['env']['server_name'] == 'localhost' - assert request['headers']['host'] == 'localhost:8080' - assert request['headers']['content_type'] == 'application/json' - assert request['headers']['content_length'] == '14' - assert request['data'] == '{"foo": "bar"}' + assert request["method"] == "GET" + assert request["url"] == "http://localhost:8080/spam/1?test=123" + assert request["env"]["server_port"] == "8080" + assert request["env"]["server_name"] == "localhost" + assert request["headers"]["host"] == "localhost:8080" + assert request["headers"]["content_type"] == "application/json" + assert request["headers"]["content_length"] == "14" + assert request["data"] == '{"foo": "bar"}' @pytest.mark.parametrize( - ('data_in', 'content_type', 'expected_data_out'), + ("data_in", "content_type", "expected_data_out"), ( - ( - json.dumps({'foo': 'bar'}), - 'application/json', - '{"foo": "bar"}', - ), - ( - 'foo=bar', - 'application/x-www-form-urlencoded', - {'foo': 'bar'}, - ), - ( - 'foo=bar', - 'text/plain', - 'foo=bar', - ), - ) + (json.dumps({"foo": "bar"}), "application/json", '{"foo": "bar"}'), + ("foo=bar", "application/x-www-form-urlencoded", {"foo": "bar"}), + ("foo=bar", "text/plain", "foo=bar"), + ), ) def test_can_get_request_data( - self, adapter, container, service_class, data_in, content_type, - expected_data_out + self, + adapter, + container, + service_class, + data_in, + content_type, + expected_data_out, ): environ = create_environ( - '/get/1?test=123', - 'http://localhost:8080/', + "/get/1?test=123", + "http://localhost:8080/", data=data_in, - content_type=content_type + content_type=content_type, ) request = Request(environ) entrypoint = get_extension( - container, HttpRequestHandler, method_name='some_method') + container, HttpRequestHandler, method_name="some_method" + ) worker_ctx = WorkerContext( - container, service_class, entrypoint, args=(request, 1)) + container, service_class, entrypoint, args=(request, 1) + ) call_args, redacted = adapter.get_call_args(worker_ctx) assert redacted is False - assert call_args['request']['data'] == expected_data_out - assert call_args['request']['headers']['content_type'] == content_type + assert call_args["request"]["data"] == expected_data_out + assert call_args["request"]["headers"]["content_type"] == content_type @pytest.mark.parametrize( - ('response', 'data', 'status_code', 'content_type'), + ("response", "data", "status_code", "content_type"), ( - (Response( + ( + Response('{"value": 1}', status=200, mimetype="application/json"), '{"value": 1}', - status=200, - mimetype='application/json', - ), '{"value": 1}', 200, 'application/json', - ), - (Response( - 'foo', - status=202, - mimetype='text/plain', - ), 'foo', 202, 'text/plain', - ), - (Response( - 'some error', - status=400, - mimetype='text/plain', - ), 'some error', 400, 'text/plain', + 200, + "application/json", ), ( - (200, {}, 'foo'), - 'foo', 200, 'text/plain', + Response("foo", status=202, mimetype="text/plain"), + "foo", + 202, + "text/plain", ), ( - (200, 'foo'), - 'foo', 200, 'text/plain', + Response("some error", status=400, mimetype="text/plain"), + "some error", + 400, + "text/plain", ), - ( - 'foo', - 'foo', 200, 'text/plain', - ) - ) + ((200, {}, "foo"), "foo", 200, "text/plain"), + ((200, "foo"), "foo", 200, "text/plain"), + ("foo", "foo", 200, "text/plain"), + ), ) - def test_result_data( - self, adapter, response, data, status_code, content_type - ): + def test_result_data(self, adapter, response, data, status_code, content_type): result = adapter.get_result(response) - assert result['data'] == data.encode('utf-8') - assert result['status_code'] == status_code - assert result['content_type'].startswith(content_type) + assert result["data"] == data.encode("utf-8") + assert result["status_code"] == status_code + assert result["content_type"].startswith(content_type) diff --git a/tests/test_dependency.py b/tests/test_dependency.py index 3f57381..25e2d9b 100644 --- a/tests/test_dependency.py +++ b/tests/test_dependency.py @@ -1,21 +1,19 @@ -from datetime import datetime import logging +from datetime import datetime -from mock import call, patch, Mock +import pytest +from mock import Mock, call, patch from nameko.containers import WorkerContext -from nameko.web.handlers import HttpRequestHandler from nameko.testing.services import dummy, entrypoint_hook from nameko.testing.utils import DummyProvider -import pytest +from nameko.web.handlers import HttpRequestHandler -from nameko_tracer import adapters, constants, Tracer +from nameko_tracer import Tracer, adapters, constants @pytest.fixture def tracker(): - class Tracker(logging.Handler): - def __init__(self, *args, **kwargs): self.log_records = [] super(Tracker, self).__init__(*args, **kwargs) @@ -34,14 +32,14 @@ def emit(self, log_record): @pytest.yield_fixture def mocked_datetime(): - with patch('nameko_tracer.dependency.datetime') as dt: + with patch("nameko_tracer.dependency.datetime") as dt: yield dt @pytest.yield_fixture(autouse=True) def mocked_hostname(): - with patch('nameko_tracer.dependency.socket.gethostname') as gethostname: - gethostname.return_value = 'some.host' + with patch("nameko_tracer.dependency.socket.gethostname") as gethostname: + gethostname.return_value = "some.host" yield gethostname @@ -49,8 +47,7 @@ def test_successful_result(container_factory, mocked_datetime, tracker): request_timestamp = datetime(2017, 7, 7, 12, 0, 0) response_timestamp = datetime(2017, 7, 7, 12, 1, 0) - mocked_datetime.utcnow.side_effect = [ - request_timestamp, response_timestamp] + mocked_datetime.utcnow.side_effect = [request_timestamp, response_timestamp] class Service(object): @@ -65,49 +62,44 @@ def some_method(self, spam): container = container_factory(Service, {}) container.start() - with entrypoint_hook(container, 'some_method') as some_method: - some_method('ham') + with entrypoint_hook(container, "some_method") as some_method: + some_method("ham") assert len(tracker.log_records) == 2 setup_record, result_record = tracker.log_records - assert setup_record.msg == '[%s] entrypoint call trace' + assert setup_record.msg == "[%s] entrypoint call trace" assert setup_record.levelno == logging.INFO - assert result_record.msg == '[%s] entrypoint result trace' + assert result_record.msg == "[%s] entrypoint result trace" assert result_record.levelno == logging.INFO setup_details = getattr(setup_record, constants.TRACE_KEY) - assert setup_record.args == (setup_details['call_id'],) + assert setup_record.args == (setup_details["call_id"],) assert setup_details[constants.TIMESTAMP_KEY] == request_timestamp - assert ( - setup_details[constants.STAGE_KEY] == - constants.Stage.request.value) - assert setup_details[constants.HOSTNAME_KEY] == 'some.host' + assert setup_details[constants.STAGE_KEY] == constants.Stage.request.value + assert setup_details[constants.HOSTNAME_KEY] == "some.host" result_details = getattr(result_record, constants.TRACE_KEY) - assert result_record.args == (result_details['call_id'],) + assert result_record.args == (result_details["call_id"],) assert result_details[constants.TIMESTAMP_KEY] == response_timestamp assert result_details[constants.RESPONSE_TIME_KEY] == 60.0 + assert result_details[constants.STAGE_KEY] == constants.Stage.response.value + assert result_details[constants.HOSTNAME_KEY] == "some.host" assert ( - result_details[constants.STAGE_KEY] == - constants.Stage.response.value) - assert result_details[constants.HOSTNAME_KEY] == 'some.host' - assert ( - result_details[constants.RESPONSE_STATUS_KEY] == - constants.Status.success.value) + result_details[constants.RESPONSE_STATUS_KEY] == constants.Status.success.value + ) def test_failing_result(container_factory, mocked_datetime, tracker): request_timestamp = datetime(2017, 7, 7, 12, 0, 0) response_timestamp = datetime(2017, 7, 7, 12, 1, 0) - mocked_datetime.utcnow.side_effect = [ - request_timestamp, response_timestamp] + mocked_datetime.utcnow.side_effect = [request_timestamp, response_timestamp] class SomeError(Exception): pass @@ -120,51 +112,44 @@ class Service(object): @dummy def some_method(self, spam): - raise SomeError('Yo!') + raise SomeError("Yo!") container = container_factory(Service, {}) container.start() with pytest.raises(SomeError): - with entrypoint_hook(container, 'some_method') as some_method: - some_method('ham') + with entrypoint_hook(container, "some_method") as some_method: + some_method("ham") assert len(tracker.log_records) == 2 setup_record, result_record = tracker.log_records - assert setup_record.msg == '[%s] entrypoint call trace' + assert setup_record.msg == "[%s] entrypoint call trace" assert setup_record.levelno == logging.INFO - assert result_record.msg == '[%s] entrypoint result trace' + assert result_record.msg == "[%s] entrypoint result trace" assert result_record.levelno == logging.WARNING setup_details = getattr(setup_record, constants.TRACE_KEY) - assert setup_record.args == (setup_details['call_id'],) + assert setup_record.args == (setup_details["call_id"],) assert setup_details[constants.TIMESTAMP_KEY] == request_timestamp - assert ( - setup_details[constants.STAGE_KEY] == - constants.Stage.request.value) + assert setup_details[constants.STAGE_KEY] == constants.Stage.request.value result_details = getattr(result_record, constants.TRACE_KEY) - assert result_record.args == (result_details['call_id'],) + assert result_record.args == (result_details["call_id"],) assert result_details[constants.TIMESTAMP_KEY] == response_timestamp assert result_details[constants.RESPONSE_TIME_KEY] == 60.0 - assert ( - result_details[constants.STAGE_KEY] == - constants.Stage.response.value) - assert ( - result_details[constants.RESPONSE_STATUS_KEY] == - constants.Status.error.value) + assert result_details[constants.STAGE_KEY] == constants.Stage.response.value + assert result_details[constants.RESPONSE_STATUS_KEY] == constants.Status.error.value -@patch('nameko_tracer.adapters.DefaultAdapter.info') -@patch('nameko_tracer.dependency.logger') +@patch("nameko_tracer.adapters.DefaultAdapter.info") +@patch("nameko_tracer.dependency.logger") def test_erroring_setup_adapter(logger, info, container_factory, tracker): - class SomeError(Exception): pass @@ -181,25 +166,22 @@ def some_method(self, spam): container = container_factory(Service, {}) container.start() - info.side_effect = [ - SomeError('Yo!'), - None - ] - with entrypoint_hook(container, 'some_method') as some_method: - some_method('ham') + info.side_effect = [SomeError("Yo!"), None] + with entrypoint_hook(container, "some_method") as some_method: + some_method("ham") # nothing logged by entrypoint logger assert len(tracker.log_records) == 0 # warning logged by module logger assert logger.warning.call_args == call( - 'Failed to log entrypoint trace', exc_info=True) + "Failed to log entrypoint trace", exc_info=True + ) -@patch('nameko_tracer.adapters.DefaultAdapter.info') -@patch('nameko_tracer.dependency.logger') +@patch("nameko_tracer.adapters.DefaultAdapter.info") +@patch("nameko_tracer.dependency.logger") def test_erroring_result_adapter(logger, info, container_factory, tracker): - class SomeError(Exception): pass @@ -216,39 +198,38 @@ def some_method(self, spam): container = container_factory(Service, {}) container.start() - info.side_effect = [ - Mock(return_value=(Mock(), Mock())), - SomeError('Yo!') - ] - with entrypoint_hook(container, 'some_method') as some_method: - some_method('ham') + info.side_effect = [Mock(return_value=(Mock(), Mock())), SomeError("Yo!")] + with entrypoint_hook(container, "some_method") as some_method: + some_method("ham") # nothing logged by entrypoint logger assert len(tracker.log_records) == 0 # warning logged by module logger assert logger.warning.call_args == call( - 'Failed to log entrypoint trace', exc_info=True) + "Failed to log entrypoint trace", exc_info=True + ) -@patch('nameko_tracer.adapters.HttpRequestHandlerAdapter.info') -@patch('nameko_tracer.adapters.DefaultAdapter.info') +@patch("nameko_tracer.adapters.HttpRequestHandlerAdapter.info") +@patch("nameko_tracer.adapters.DefaultAdapter.info") def test_default_adapters(default_info, http_info, mock_container): - mock_container.service_name = 'dummy' + mock_container.service_name = "dummy" mock_container.config = {} - tracer = Tracer().bind(mock_container, 'logger') + tracer = Tracer().bind(mock_container, "logger") tracer.setup() default_worker_ctx = WorkerContext(mock_container, None, DummyProvider()) http_worker_ctx = WorkerContext( - mock_container, None, HttpRequestHandler('GET', 'http://yo')) + mock_container, None, HttpRequestHandler("GET", "http://yo") + ) calls = [ tracer.worker_setup, tracer.worker_result, tracer.worker_setup, - tracer.worker_result + tracer.worker_result, ] for call_ in calls: @@ -263,31 +244,33 @@ class CustomAdapter(adapters.DefaultAdapter): pass -@patch('nameko_tracer.adapters.DefaultAdapter.info') -@patch.object(CustomAdapter, 'info') +@patch("nameko_tracer.adapters.DefaultAdapter.info") +@patch.object(CustomAdapter, "info") def test_config_adapters(default_info, custom_info, mock_container): - mock_container.service_name = 'dummy' + mock_container.service_name = "dummy" mock_container.config = { constants.CONFIG_KEY: { constants.ADAPTERS_CONFIG_KEY: { - 'nameko.web.handlers.HttpRequestHandler': - 'test_dependency.CustomAdapter', + "nameko.web.handlers.HttpRequestHandler": ( + "test_dependency.CustomAdapter" + ) } } } - tracer = Tracer().bind(mock_container, 'logger') + tracer = Tracer().bind(mock_container, "logger") tracer.setup() default_worker_ctx = WorkerContext(mock_container, None, DummyProvider()) http_worker_ctx = WorkerContext( - mock_container, None, HttpRequestHandler('GET', 'http://yo')) + mock_container, None, HttpRequestHandler("GET", "http://yo") + ) calls = [ tracer.worker_setup, tracer.worker_result, tracer.worker_setup, - tracer.worker_result + tracer.worker_result, ] for call_ in calls: diff --git a/tests/test_filters.py b/tests/test_filters.py index aff0907..d6fdf5f 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -7,9 +7,7 @@ @pytest.fixture def handler(): - class LRUTracker(logging.Handler): - def __init__(self, *args, **kwargs): self.log_record = None super(LRUTracker, self).__init__(*args, **kwargs) @@ -22,7 +20,7 @@ def emit(self, log_record): @pytest.yield_fixture def logger(handler): - logger = logging.getLogger('test') + logger = logging.getLogger("test") logger.setLevel(logging.INFO) logger.addHandler(handler) yield logger @@ -34,44 +32,53 @@ def logger(handler): @pytest.mark.parametrize( ( - 'entrypoints', 'max_len', 'expected_request', - 'expected_request_length', 'truncated', 'stage' + "entrypoints", + "max_len", + "expected_request", + "expected_request_length", + "truncated", + "stage", ), ( # should truncate call args of 'spam' entrypoint - (['spam'], 5, '12345', 9, True, constants.Stage.request), - (['^ham|spam'], 5, '12345', 9, True, constants.Stage.request), - (['^spam'], 5, '12345', 9, True, constants.Stage.request), - (['^spam'], 5, '12345', 9, True, constants.Stage.response), + (["spam"], 5, "12345", 9, True, constants.Stage.request), + (["^ham|spam"], 5, "12345", 9, True, constants.Stage.request), + (["^spam"], 5, "12345", 9, True, constants.Stage.request), + (["^spam"], 5, "12345", 9, True, constants.Stage.response), # call args of 'spam' entrypoint shorter than max len - (['^spam'], 10, '123456789', 9, False, constants.Stage.request), + (["^spam"], 10, "123456789", 9, False, constants.Stage.request), # 'spam' entrypoint does not match the regexp - (['^ham'], 5, '123456789', None, False, constants.Stage.request), + (["^ham"], 5, "123456789", None, False, constants.Stage.request), # no entrypoint should be truncated - (None, 5, '123456789', None, False, constants.Stage.request), - ([], 5, '123456789', None, False, constants.Stage.request), - ('', 5, '123456789', None, False, constants.Stage.request), - ) + (None, 5, "123456789", None, False, constants.Stage.request), + ([], 5, "123456789", None, False, constants.Stage.request), + ("", 5, "123456789", None, False, constants.Stage.request), + ), ) def test_truncate_call_args( - handler, logger, entrypoints, max_len, expected_request, - expected_request_length, truncated, stage + handler, + logger, + entrypoints, + max_len, + expected_request, + expected_request_length, + truncated, + stage, ): - filter_ = filters.TruncateCallArgsFilter( - entrypoints=entrypoints, max_len=max_len) + filter_ = filters.TruncateCallArgsFilter(entrypoints=entrypoints, max_len=max_len) logger.addFilter(filter_) extra = { constants.TRACE_KEY: { constants.STAGE_KEY: stage.value, - constants.ENTRYPOINT_NAME_KEY: 'spam', - constants.REQUEST_KEY: '123456789', - }, + constants.ENTRYPOINT_NAME_KEY: "spam", + constants.REQUEST_KEY: "123456789", + } } - logger.info('request', extra=extra) + logger.info("request", extra=extra) data = getattr(handler.log_record, constants.TRACE_KEY) @@ -82,60 +89,54 @@ def test_truncate_call_args( def test_truncate_no_call_args(handler, logger): - filter_ = filters.TruncateCallArgsFilter(entrypoints=['spam'], max_len=5) + filter_ = filters.TruncateCallArgsFilter(entrypoints=["spam"], max_len=5) logger.addFilter(filter_) extra = { constants.TRACE_KEY: { constants.STAGE_KEY: constants.Stage.request, - constants.ENTRYPOINT_NAME_KEY: 'spam', - }, + constants.ENTRYPOINT_NAME_KEY: "spam", + } } - logger.info('request', extra=extra) + logger.info("request", extra=extra) data = getattr(handler.log_record, constants.TRACE_KEY) - assert data[constants.ENTRYPOINT_NAME_KEY] == 'spam' + assert data[constants.ENTRYPOINT_NAME_KEY] == "spam" assert constants.REQUEST_KEY not in data assert constants.REQUEST_TRUNCATED_KEY not in data assert constants.REQUEST_LENGTH_KEY not in data @pytest.mark.parametrize( - ('request_in', 'expected_request_out'), + ("request_in", "expected_request_out"), ( + (["too", "short"], ["too", "short"]), # untouched + ("a long string should stay a string", "a long string should sta"), ( - ['too', 'short'], - ['too', 'short'], # untouched - ), - ( - 'a long string should stay a string', - 'a long string should sta', - ), - ( - {'a': ('more', 'complex', 'data', 'structure')}, + {"a": ("more", "complex", "data", "structure")}, "{'a': ['more', 'complex'", # turned to string - ) - ) + ), + ), ) def test_truncate_call_args_to_string_casting( handler, logger, request_in, expected_request_out ): - filter_ = filters.TruncateCallArgsFilter(entrypoints=['spam'], max_len=24) + filter_ = filters.TruncateCallArgsFilter(entrypoints=["spam"], max_len=24) logger.addFilter(filter_) extra = { constants.TRACE_KEY: { constants.STAGE_KEY: constants.Stage.request.value, - constants.ENTRYPOINT_NAME_KEY: 'spam', + constants.ENTRYPOINT_NAME_KEY: "spam", constants.REQUEST_KEY: request_in, - }, + } } - logger.info('request', extra=extra) + logger.info("request", extra=extra) data = getattr(handler.log_record, constants.TRACE_KEY) @@ -144,43 +145,50 @@ def test_truncate_call_args_to_string_casting( @pytest.mark.parametrize( ( - 'entrypoints', 'max_len', 'expected_response', - 'expected_response_length', 'truncated' + "entrypoints", + "max_len", + "expected_response", + "expected_response_length", + "truncated", ), ( # should truncate return value of 'spam' entrypoint - (['spam'], 5, '12345', 9, True), - (['^ham|spam'], 5, '12345', 9, True), - (['^spam'], 5, '12345', 9, True), + (["spam"], 5, "12345", 9, True), + (["^ham|spam"], 5, "12345", 9, True), + (["^spam"], 5, "12345", 9, True), # return value of 'spam' entrypoint shorter than max len - (['^spam'], 10, '123456789', 9, False), + (["^spam"], 10, "123456789", 9, False), # 'spam' entrypoint does not match the regexp - (['^ham'], 5, '123456789', None, False), + (["^ham"], 5, "123456789", None, False), # no entrypoint should be truncated - (None, 5, '123456789', None, False), - ([], 5, '123456789', None, False), - ('', 5, '123456789', None, False), - ) + (None, 5, "123456789", None, False), + ([], 5, "123456789", None, False), + ("", 5, "123456789", None, False), + ), ) def test_truncate_response( - handler, logger, entrypoints, max_len, expected_response, - expected_response_length, truncated + handler, + logger, + entrypoints, + max_len, + expected_response, + expected_response_length, + truncated, ): - filter_ = filters.TruncateResponseFilter( - entrypoints=entrypoints, max_len=max_len) + filter_ = filters.TruncateResponseFilter(entrypoints=entrypoints, max_len=max_len) logger.addFilter(filter_) extra = { constants.TRACE_KEY: { constants.STAGE_KEY: constants.Stage.response.value, - constants.ENTRYPOINT_NAME_KEY: 'spam', - constants.RESPONSE_KEY: '123456789', - }, + constants.ENTRYPOINT_NAME_KEY: "spam", + constants.RESPONSE_KEY: "123456789", + } } - logger.info('response', extra=extra) + logger.info("response", extra=extra) data = getattr(handler.log_record, constants.TRACE_KEY) @@ -190,39 +198,33 @@ def test_truncate_response( @pytest.mark.parametrize( - ('response_in', 'expected_response_out'), + ("response_in", "expected_response_out"), ( + (["too", "short"], ["too", "short"]), # untouched + ("a long string should stay a string", "a long string should sta"), ( - ['too', 'short'], - ['too', 'short'], # untouched - ), - ( - 'a long string should stay a string', - 'a long string should sta', - ), - ( - {'a': ('more', 'complex', 'data', 'structure')}, + {"a": ("more", "complex", "data", "structure")}, "{'a': ['more', 'complex'", # turned to string - ) - ) + ), + ), ) def test_truncate_response_to_string_casting( handler, logger, response_in, expected_response_out ): - filter_ = filters.TruncateResponseFilter(entrypoints=['spam'], max_len=24) + filter_ = filters.TruncateResponseFilter(entrypoints=["spam"], max_len=24) logger.addFilter(filter_) extra = { constants.TRACE_KEY: { constants.STAGE_KEY: constants.Stage.response.value, - constants.ENTRYPOINT_NAME_KEY: 'spam', + constants.ENTRYPOINT_NAME_KEY: "spam", constants.RESPONSE_KEY: response_in, - }, + } } - logger.info('response', extra=extra) + logger.info("response", extra=extra) data = getattr(handler.log_record, constants.TRACE_KEY) @@ -231,19 +233,19 @@ def test_truncate_response_to_string_casting( def test_truncate_response_ignores_error_response(handler, logger): - filter_ = filters.TruncateResponseFilter(entrypoints=['^spam'], max_len=5) + filter_ = filters.TruncateResponseFilter(entrypoints=["^spam"], max_len=5) logger.addFilter(filter_) extra = { constants.TRACE_KEY: { constants.STAGE_KEY: constants.Stage.response.value, - constants.ENTRYPOINT_NAME_KEY: 'spam', + constants.ENTRYPOINT_NAME_KEY: "spam", constants.RESPONSE_STATUS_KEY: constants.Status.error.value, - }, + } } - logger.info('response', extra=extra) + logger.info("response", extra=extra) data = getattr(handler.log_record, constants.TRACE_KEY) @@ -255,23 +257,23 @@ def test_truncate_response_ignores_error_response(handler, logger): def test_truncate_request_ignores_response_data(handler, logger): - filter_ = filters.TruncateCallArgsFilter(entrypoints=['^spam'], max_len=5) + filter_ = filters.TruncateCallArgsFilter(entrypoints=["^spam"], max_len=5) logger.addFilter(filter_) extra = { constants.TRACE_KEY: { constants.STAGE_KEY: constants.Stage.response.value, - constants.ENTRYPOINT_NAME_KEY: 'spam', - constants.RESPONSE_KEY: '123456789', - }, + constants.ENTRYPOINT_NAME_KEY: "spam", + constants.RESPONSE_KEY: "123456789", + } } - logger.info('response', extra=extra) + logger.info("response", extra=extra) data = getattr(handler.log_record, constants.TRACE_KEY) - assert data[constants.RESPONSE_KEY] == '123456789' + assert data[constants.RESPONSE_KEY] == "123456789" assert constants.REQUEST_TRUNCATED_KEY not in data assert constants.REQUEST_LENGTH_KEY not in data assert constants.RESPONSE_TRUNCATED_KEY not in data @@ -280,23 +282,23 @@ def test_truncate_request_ignores_response_data(handler, logger): def test_truncate_response_ignores_request_data(handler, logger): - filter_ = filters.TruncateResponseFilter(entrypoints=['^spam'], max_len=5) + filter_ = filters.TruncateResponseFilter(entrypoints=["^spam"], max_len=5) logger.addFilter(filter_) extra = { constants.TRACE_KEY: { constants.STAGE_KEY: constants.Stage.request.value, - constants.ENTRYPOINT_NAME_KEY: 'spam', - constants.REQUEST_KEY: '123456789', - }, + constants.ENTRYPOINT_NAME_KEY: "spam", + constants.REQUEST_KEY: "123456789", + } } - logger.info('request', extra=extra) + logger.info("request", extra=extra) data = getattr(handler.log_record, constants.TRACE_KEY) - assert data[constants.REQUEST_KEY] == '123456789' + assert data[constants.REQUEST_KEY] == "123456789" assert constants.RESPONSE_TRUNCATED_KEY not in data assert constants.RESPONSE_LENGTH_KEY not in data assert constants.REQUEST_TRUNCATED_KEY not in data @@ -305,4 +307,4 @@ def test_truncate_response_ignores_request_data(handler, logger): def test_base_truncate_filter_cannot_be_used(handler, logger): with pytest.raises(TypeError): - filters.BaseTruncateFilter(entrypoints=['^spam'], max_len=5) + filters.BaseTruncateFilter(entrypoints=["^spam"], max_len=5) diff --git a/tests/test_formatters.py b/tests/test_formatters.py index f3226dc..3e6923e 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -1,60 +1,59 @@ -from datetime import datetime import json +from datetime import datetime -from mock import Mock import pytest +from mock import Mock from nameko_tracer import constants, formatters @pytest.mark.parametrize( - ('input_', 'expected_output'), + ("input_", "expected_output"), ( ( - {'datetime': datetime(2017, 7, 7, 12, 0)}, - '{"datetime": "2017-07-07 12:00:00"}' + {"datetime": datetime(2017, 7, 7, 12, 0)}, + '{"datetime": "2017-07-07 12:00:00"}', ), ({None}, '"{None}"'), - ) + ), ) def test_json_serialiser_will_deal_with_datetime(input_, expected_output): log_record = Mock() setattr(log_record, constants.TRACE_KEY, input_) - assert ( - formatters.JSONFormatter().format(log_record) == expected_output) + assert formatters.JSONFormatter().format(log_record) == expected_output @pytest.mark.parametrize( - ('key', 'value_in', 'expected_value_out'), + ("key", "value_in", "expected_value_out"), ( ( constants.CONTEXT_DATA_KEY, - {'should': ('be', 'serialised')}, + {"should": ("be", "serialised")}, '{"should": ["be", "serialised"]}', ), ( constants.REQUEST_KEY, - ('should', 'be', 'serialised'), + ("should", "be", "serialised"), '["should", "be", "serialised"]', ), ( constants.RESPONSE_KEY, - {'should': ('be', 'serialised')}, + {"should": ("be", "serialised")}, '{"should": ["be", "serialised"]}', ), ( constants.EXCEPTION_ARGS_KEY, - {'should': ('be', 'serialised')}, + {"should": ("be", "serialised")}, '{"should": ["be", "serialised"]}', ), ( - 'some-other-key', - {'should': ['NOT', 'be', 'serialised']}, - {'should': ['NOT', 'be', 'serialised']}, + "some-other-key", + {"should": ["NOT", "be", "serialised"]}, + {"should": ["NOT", "be", "serialised"]}, ), - ) + ), ) def test_elasticsearch_document_serialiser(key, value_in, expected_value_out): diff --git a/tests/test_utils.py b/tests/test_utils.py index 08f7761..2f4f3d2 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,56 +4,59 @@ @pytest.mark.parametrize( - ('input_', 'expected_output'), + ("input_", "expected_output"), ( - ('string', 'string'), - ([1, 0.5, 'string'], [1, 0.5, 'string']), - ((1, 0.5, 'string'), [1, 0.5, 'string']), + ("string", "string"), + ([1, 0.5, "string"], [1, 0.5, "string"]), + ((1, 0.5, "string"), [1, 0.5, "string"]), ({1: 2, 0.5: 0.5}, {1: 2, 0.5: 0.5}), - ({'string': 'string'}, {'string': 'string'}), + ({"string": "string"}, {"string": "string"}), ({1}, [1]), (object, repr(object)), (None, None), ( - ((1, 0.5, 'string', object), [], {}, set()), - [[1, 0.5, 'string', repr(object)], [], {}, []], + ((1, 0.5, "string", object), [], {}, set()), + [[1, 0.5, "string", repr(object)], [], {}, []], ), ( - ((), [1, 0.5, 'string', object], {}), - [[], [1, 0.5, 'string', repr(object)], {}], + ((), [1, 0.5, "string", object], {}), + [[], [1, 0.5, "string", repr(object)], {}], ), ( - ((), [], {1: {0.5: ('string', object)}}), - [[], [], {1: {0.5: ['string', repr(object)]}}], + ((), [], {1: {0.5: ("string", object)}}), + [[], [], {1: {0.5: ["string", repr(object)]}}], ), - (b'bytes to decode', 'bytes to decode'), - ) + (b"bytes to decode", "bytes to decode"), + ), ) def test_safe_for_serialisation(input_, expected_output): assert utils.safe_for_serialisation(input_) == expected_output def test_safe_for_serialisation_repr_failure(): - class CannotRepr(object): def __repr__(self): - raise Exception('boom') + raise Exception("boom") - assert ( - utils.safe_for_serialisation([1, CannotRepr(), 2]) == - [1, 'failed to get string representation', 2]) + assert utils.safe_for_serialisation([1, CannotRepr(), 2]) == [ + 1, + "failed to get string representation", + 2, + ] def test_serialise_to_json(): assert ( - utils.serialise_to_json(((), [], {1: {0.5: ('string', object)}})) == - '[[], [], {"1": {"0.5": ["string", ""]}}]') + utils.serialise_to_json(((), [], {1: {0.5: ("string", object)}})) + == '[[], [], {"1": {"0.5": ["string", ""]}}]' + ) def test_serialise_to_string(): assert ( - utils.serialise_to_string(((), [], {1: {0.5: ('string', object)}})) == - '[[], [], {1: {0.5: [\'string\', ""]}}]') + utils.serialise_to_string(((), [], {1: {0.5: ("string", object)}})) + == "[[], [], {1: {0.5: ['string', \"\"]}}]" + ) class SomeClass: @@ -61,19 +64,19 @@ class SomeClass: def test_import_by_path(): - cls = utils.import_by_path('test_utils.SomeClass') + cls = utils.import_by_path("test_utils.SomeClass") assert cls == SomeClass def test_import_by_path_error_not_a_path(): with pytest.raises(ImportError) as exc: - utils.import_by_path('no_dots_in_path') - assert str(exc.value) == ( - "no_dots_in_path doesn't look like a module path") + utils.import_by_path("no_dots_in_path") + assert str(exc.value) == ("no_dots_in_path doesn't look like a module path") def test_import_by_path_not_found_error(): with pytest.raises(ImportError) as exc: - utils.import_by_path('test_utils.Nothing') + utils.import_by_path("test_utils.Nothing") assert str(exc.value) == ( - 'Module "test_utils" does not define a "Nothing" attribute/class') + 'Module "test_utils" does not define a "Nothing" attribute/class' + )