diff --git a/README.rst b/README.rst index 4ac10af..5d216db 100644 --- a/README.rst +++ b/README.rst @@ -34,18 +34,18 @@ The work of instrumentation libraries generally consists of three steps: Span object in the process. If the request does not contain an active trace, the service starts a new trace and a new *root* Span. 2. The service needs to store the current Span in some request-local storage, - where it can be retrieved from when a child Span must be created, e.g. in case - of the service making an RPC to another service. + (called ``Span`` *activation*) where it can be retrieved from when a child Span must + be created, e.g. in case of the service making an RPC to another service. 3. When making outbound calls to another service, the current Span must be retrieved from request-local storage, a child span must be created (e.g., by using the ``start_child_span()`` helper), and that child span must be embedded into the outbound request (e.g., using HTTP headers) via OpenTracing's inject/extract API. -Below are the code examples for steps 1 and 3. Implementation of request-local -storage needed for step 2 is specific to the service and/or frameworks / -instrumentation libraries it is using (TODO: reference to other OSS projects -with examples of instrumentation). +Below are the code examples for the previously mentioned steps. Implementation +of request-local storage needed for step 2 is specific to the service and/or frameworks / +instrumentation libraries it is using, exposed as a ``ScopeManager`` child contained +as ``Tracer.scope_manager``. See details below. Inbound request ^^^^^^^^^^^^^^^ @@ -56,12 +56,12 @@ Somewhere in your server's request handler code: def handle_request(request): span = before_request(request, opentracing.tracer) - # use span as Context Manager to ensure span.finish() will be called - with span: - # store span in some request-local storage - with RequestContext(span): - # actual business logic - handle_request_for_real(request) + # store span in some request-local storage using Tracer.scope_manager, + # using the returned `Scope` as Context Manager to ensure + # `Span` will be cleared and (in this case) `Span.finish()` be called. + with tracer.scope_manager.activate(span, True) as scope: + # actual business logic + handle_request_for_real(request) def before_request(request, tracer): @@ -141,6 +141,61 @@ Somewhere in your service that's about to make an outgoing call: return outbound_span +Scope and within-process propagation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For getting/setting the current active ``Span`` in the used request-local storage, +OpenTracing requires that every ``Tracer`` contains a ``ScopeManager`` that grants +access to the active ``Span`` through a ``Scope``. Any ``Span`` may be transferred to +another task or thread, but not ``Scope``. + +.. code-block:: python + + # Access to the active span is straightforward. + scope = tracer.scope_manager.active() + if scope is not None: + scope.span.set_tag('...', '...') + +The common case starts a ``Scope`` that's automatically registered for intra-process +propagation via ``ScopeManager``. + +Note that ``start_active_span('...', True)`` finishes the span on ``Scope.close()`` +(``start_active_span('...', False)`` does not finish it, in contrast). + +.. code-block:: python + + # Manual activation of the Span. + span = tracer.start_span(operation_name='someWork') + with tracer.scope_manager.activate(span, True) as scope: + # Do things. + + # Automatic activation of the Span. + # finish_on_close is a required parameter. + with tracer.start_active_span('someWork', finish_on_close=True) as scope: + # Do things. + + # Handling done through a try construct: + span = tracer.start_span(operation_name='someWork') + scope = tracer.scope_manager.activate(span, True) + try: + # Do things. + except Exception as e: + scope.set_tag('error', '...') + finally: + scope.finish() + +**If there is a Scope, it will act as the parent to any newly started Span** unless +the programmer passes ``ignore_active_span=True`` at ``start_span()``/``start_active_span()`` +time or specified parent context explicitly: + +.. code-block:: python + + scope = tracer.start_active_span('someWork', ignore_active_span=True) + +Each service/framework ought to provide a specific ``ScopeManager`` implementation +that relies on their own request-local storage (thread-local storage, or coroutine-based storage +for asynchronous frameworks, for example). + Development ----------- diff --git a/docs/api.rst b/docs/api.rst index c7f9d11..5e1bf6f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -9,6 +9,12 @@ Classes .. autoclass:: opentracing.SpanContext :members: +.. autoclass:: opentracing.Scope + :members: + +.. autoclass:: opentracing.ScopeManager + :members: + .. autoclass:: opentracing.Tracer :members: diff --git a/opentracing/__init__.py b/opentracing/__init__.py index bc8bc26..4faf9de 100644 --- a/opentracing/__init__.py +++ b/opentracing/__init__.py @@ -22,6 +22,8 @@ from __future__ import absolute_import from .span import Span # noqa from .span import SpanContext # noqa +from .scope import Scope # noqa +from .scope_manager import ScopeManager # noqa from .tracer import child_of # noqa from .tracer import follows_from # noqa from .tracer import Reference # noqa diff --git a/opentracing/harness/api_check.py b/opentracing/harness/api_check.py index 36c6fc4..f46d011 100644 --- a/opentracing/harness/api_check.py +++ b/opentracing/harness/api_check.py @@ -18,8 +18,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. from __future__ import absolute_import -import time +import mock +import time import pytest import opentracing @@ -39,11 +40,78 @@ def check_baggage_values(self): """If true, the test will validate Baggage items by storing and retrieving them from the trace context. If false, it will only attempt to store and retrieve the Baggage items to check the API compliance, - but not actually validate stored values. The latter mode is only + but not actually validate stored values. The latter mode is only useful for no-op tracer. """ return True + def check_scope_manager(self): + """If true, the test suite will validate the `ScopeManager` propagation + to ensure correct parenting. If false, it will only use the API without + asserting. The latter mode is only useful for no-op tracer. + """ + return True + + def is_parent(self, parent, span): + """Utility method that must be defined by Tracer implementers to define + how the test suite can check when a `Span` is a parent of another one. + It depends by the underlying implementation that is not part of the + OpenTracing API. + """ + return False + + def test_start_active_span(self): + # the first usage returns a `Scope` that wraps a root `Span` + tracer = self.tracer() + scope = tracer.start_active_span('Fry', False) + + assert scope.span is not None + if self.check_scope_manager(): + assert self.is_parent(None, scope.span) + + def test_start_active_span_parent(self): + # ensure the `ScopeManager` provides the right parenting + tracer = self.tracer() + with tracer.start_active_span('Fry', False) as parent: + with tracer.start_active_span('Farnsworth', False) as child: + if self.check_scope_manager(): + assert self.is_parent(parent.span, child.span) + + def test_start_active_span_ignore_active_span(self): + # ensure the `ScopeManager` ignores the active `Scope` + # if the flag is set + tracer = self.tracer() + with tracer.start_active_span('Fry', False) as parent: + with tracer.start_active_span('Farnsworth', False, + ignore_active_span=True) as child: + if self.check_scope_manager(): + assert not self.is_parent(parent.span, child.span) + + def test_start_active_span_finish_on_close(self): + # ensure a `Span` is finished when the `Scope` close + tracer = self.tracer() + scope = tracer.start_active_span('Fry', False) + with mock.patch.object(scope.span, 'finish') as finish: + scope.close() + + assert finish.call_count == 0 + + def test_start_active_span_not_finish_on_close(self): + # a `Span` is not finished when the flag is set + tracer = self.tracer() + scope = tracer.start_active_span('Fry', True) + with mock.patch.object(scope.span, 'finish') as finish: + scope.close() + + if self.check_scope_manager(): + assert finish.call_count == 1 + + def test_scope_as_context_manager(self): + tracer = self.tracer() + + with tracer.start_active_span('antiquing', False) as scope: + assert scope.span is not None + def test_start_span(self): tracer = self.tracer() span = tracer.start_span(operation_name='Fry') @@ -54,6 +122,24 @@ def test_start_span(self): payload={'hospital': 'Brooklyn Pre-Med Hospital', 'city': 'Old New York'}) + def test_start_span_propagation(self): + # `start_span` must inherit the current active `Scope` span + tracer = self.tracer() + with tracer.start_active_span('Fry', False) as parent: + with tracer.start_span(operation_name='Farnsworth') as child: + if self.check_scope_manager(): + assert self.is_parent(parent.span, child) + + def test_start_span_propagation_ignore_active_span(self): + # `start_span` doesn't inherit the current active `Scope` span + # if the flag is set + tracer = self.tracer() + with tracer.start_active_span('Fry', False) as parent: + with tracer.start_span(operation_name='Farnsworth', + ignore_active_span=True) as child: + if self.check_scope_manager(): + assert not self.is_parent(parent.span, child) + def test_start_span_with_parent(self): tracer = self.tracer() parent_span = tracer.start_span(operation_name='parent') @@ -83,19 +169,20 @@ def test_set_operation_name(self): span.finish() def test_span_as_context_manager(self): + tracer = self.tracer() finish = {'called': False} def mock_finish(*_): finish['called'] = True - with self.tracer().start_span(operation_name='antiquing') as span: + with tracer.start_span(operation_name='antiquing') as span: setattr(span, 'finish', mock_finish) assert finish['called'] is True # now try with exception finish['called'] = False try: - with self.tracer().start_span(operation_name='antiquing') as span: + with tracer.start_span(operation_name='antiquing') as span: setattr(span, 'finish', mock_finish) raise ValueError() except ValueError: @@ -206,3 +293,76 @@ def test_unknown_format(self): span.tracer.inject(span.context, custom_format, {}) with pytest.raises(opentracing.UnsupportedFormatException): span.tracer.extract(custom_format, {}) + + def test_tracer_start_active_span_scope(self): + # the Tracer ScopeManager should store the active Scope + tracer = self.tracer() + scope = tracer.start_active_span('Fry', False) + + if self.check_scope_manager(): + assert tracer.scope_manager.active == scope + + scope.close() + + def test_tracer_start_active_span_nesting(self): + # when a Scope is closed, the previous one must be activated + tracer = self.tracer() + with tracer.start_active_span('Fry', False) as parent: + with tracer.start_active_span('Farnsworth', False): + pass + + if self.check_scope_manager(): + assert tracer.scope_manager.active == parent + + if self.check_scope_manager(): + assert tracer.scope_manager.active is None + + def test_tracer_start_active_span_nesting_finish_on_close(self): + # finish_on_close must be correctly handled + tracer = self.tracer() + parent = tracer.start_active_span('Fry', False) + with mock.patch.object(parent.span, 'finish') as finish: + with tracer.start_active_span('Farnsworth', False): + pass + parent.close() + + assert finish.call_count == 0 + + if self.check_scope_manager(): + assert tracer.scope_manager.active is None + + def test_tracer_start_active_span_wrong_close_order(self): + # only the active `Scope` can be closed + tracer = self.tracer() + parent = tracer.start_active_span('Fry', False) + child = tracer.start_active_span('Farnsworth', False) + parent.close() + + if self.check_scope_manager(): + assert tracer.scope_manager.active == child + + def test_tracer_start_span_scope(self): + # the Tracer ScopeManager should not store the new Span + tracer = self.tracer() + span = tracer.start_span(operation_name='Fry') + + if self.check_scope_manager(): + assert tracer.scope_manager.active is None + + span.finish() + + def test_tracer_scope_manager_active(self): + # a `ScopeManager` has no scopes in its initial state + tracer = self.tracer() + + if self.check_scope_manager(): + assert tracer.scope_manager.active is None + + def test_tracer_scope_manager_activate(self): + # a `ScopeManager` should activate any `Span` + tracer = self.tracer() + span = tracer.start_span(operation_name='Fry') + tracer.scope_manager.activate(span, False) + + if self.check_scope_manager(): + assert tracer.scope_manager.active.span == span diff --git a/opentracing/scope.py b/opentracing/scope.py new file mode 100644 index 0000000..9aa321a --- /dev/null +++ b/opentracing/scope.py @@ -0,0 +1,69 @@ +# Copyright (c) 2017 The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import + + +class Scope(object): + """A `Scope` formalizes the activation and deactivation of a `Span`, + usually from a CPU standpoint. Many times a `Span` will be extant (in that + `Span#finish()` has not been called) despite being in a non-runnable state + from a CPU/scheduler standpoint. For instance, a `Span` representing the + client side of an RPC will be unfinished but blocked on IO while the RPC is + still outstanding. A `Scope` defines when a given `Span` is scheduled + and on the path. + """ + def __init__(self, manager, span): + """Initializes a `Scope` for the given `Span` object. + + :param manager: the `ScopeManager` that created this `Scope` + :param span: the `Span` used for this `Scope` + """ + self._manager = manager + self._span = span + + @property + def span(self): + """Returns the `Span` wrapped by this `Scope`.""" + return self._span + + @property + def manager(self): + """Returns the `ScopeManager` that created this `Scope`.""" + return self._manager + + def close(self): + """Marks the end of the active period for this `Scope`, + updating `ScopeManager#active` in the process. + + NOTE: Calling `close()` more than once on a single `Scope` instance + leads to undefined behavior. + """ + pass + + def __enter__(self): + """Allows `Scope` to be used inside a Python Context Manager.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Calls `close()` when the execution is outside the Python + Context Manager. + """ + self.close() diff --git a/opentracing/scope_manager.py b/opentracing/scope_manager.py new file mode 100644 index 0000000..72b0d10 --- /dev/null +++ b/opentracing/scope_manager.py @@ -0,0 +1,63 @@ +# Copyright (c) 2017 The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import + +from .span import Span, SpanContext +from .scope import Scope + + +class ScopeManager(object): + """The `ScopeManager` interface abstracts both the activation of `Span` + instances (via `ScopeManager#activate(span, finish_on_close)`) and + access to an active `Span` / `Scope` (via `ScopeManager#active`). + """ + def __init__(self): + # TODO: `tracer` should not be None, but we don't have a reference; + # should we move the NOOP SpanContext, Span, Scope to somewhere + # else so that they're globally reachable? + self._noop_span = Span(tracer=None, context=SpanContext()) + self._noop_scope = Scope(self, self._noop_span) + + def activate(self, span, finish_on_close): + """Makes a `Span` instance active. + + :param span: the `Span` that should become active. + :param finish_on_close: whether span should be automatically + finished when `Scope#close()` is called. + + :return: a `Scope` instance to control the end of the active period for + the `Span`. It is a programming error to neglect to call + `Scope#close()` on the returned instance. + """ + return self._noop_scope + + @property + def active(self): + """Returns the currently active `Scope` which can be used to access the + currently active `Scope#span`. + + If there is a non-null `Scope`, its wrapped `Span` becomes an implicit + parent of any newly-created `Span` at `Tracer#start_active_span()` + time. + + :return: the `Scope` that is active, or `None` if not available. + """ + return self._noop_scope diff --git a/opentracing/tracer.py b/opentracing/tracer.py index 57709c3..42ba1e2 100644 --- a/opentracing/tracer.py +++ b/opentracing/tracer.py @@ -24,6 +24,8 @@ from collections import namedtuple from .span import Span from .span import SpanContext +from .scope import Scope +from .scope_manager import ScopeManager from .propagation import Format, UnsupportedFormatException @@ -37,16 +39,73 @@ class Tracer(object): _supported_formats = [Format.TEXT_MAP, Format.BINARY, Format.HTTP_HEADERS] - def __init__(self): + def __init__(self, scope_manager=None): + self._scope_manager = ScopeManager() if scope_manager is None \ + else scope_manager self._noop_span_context = SpanContext() self._noop_span = Span(tracer=self, context=self._noop_span_context) + self._noop_scope = Scope(self._scope_manager, self._noop_span) + + @property + def scope_manager(self): + """ScopeManager accessor""" + return self._scope_manager + + def start_active_span(self, + operation_name, + finish_on_close, + child_of=None, + references=None, + tags=None, + start_time=None, + ignore_active_span=False): + """Returns a newly started and activated `Scope`. + + The returned `Scope` supports with-statement contexts. For example: + + with tracer.start_active_span('...', False) as scope: + scope.span.set_tag('http.method', 'GET') + do_some_work() + # Span is not finished outside the `Scope` `with`. + + It's also possible to finish the `Span` when the `Scope` context + expires: + + with tracer.start_active_span('...', True) as scope: + scope.span.set_tag('http.method', 'GET') + do_some_work() + # Span finishes when the Scope is closed as + # `finish_on_close` is `True` + + :param operation_name: name of the operation represented by the new + span from the perspective of the current service. + :param finish_on_close: whether span should automatically be finished + when `Scope#close()` is called. + :param child_of: (optional) a Span or SpanContext instance representing + the parent in a REFERENCE_CHILD_OF Reference. If specified, the + `references` parameter must be omitted. + :param references: (optional) a list of Reference objects that identify + one or more parent SpanContexts. (See the Reference documentation + for detail). + :param tags: an optional dictionary of Span Tags. The caller gives up + ownership of that dictionary, because the Tracer may use it as-is + to avoid extra data copying. + :param start_time: an explicit Span start time as a unix timestamp per + time.time(). + :param ignore_active_span: (optional) an explicit flag that ignores + the current active `Scope` and creates a root `Span`. + + :return: a `Scope`, already registered via the `ScopeManager`. + """ + return self._noop_scope def start_span(self, operation_name=None, child_of=None, references=None, tags=None, - start_time=None): + start_time=None, + ignore_active_span=False): """Starts and returns a new Span representing a unit of work. @@ -82,6 +141,8 @@ def start_span(self, to avoid extra data copying. :param start_time: an explicit Span start time as a unix timestamp per time.time() + :param ignore_active_span: an explicit flag that ignores the current + active `Scope` and creates a root `Span`. :return: Returns an already-started Span instance. """ diff --git a/tests/test_api.py b/tests/test_api.py index 7e800e2..f32df62 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -33,3 +33,6 @@ def tracer(self): def check_baggage_values(self): return False + + def check_scope_manager(self): + return False diff --git a/tests/test_api_check_mixin.py b/tests/test_api_check_mixin.py index a1c0cad..03463aa 100644 --- a/tests/test_api_check_mixin.py +++ b/tests/test_api_check_mixin.py @@ -33,6 +33,10 @@ def test_default_baggage_check_mode(self): api_check = APICompatibilityCheckMixin() assert api_check.check_baggage_values() is True + def test_default_scope_manager_check_mode(self): + api_check = APICompatibilityCheckMixin() + assert api_check.check_scope_manager() is True + def test_baggage_check_works(self): api_check = APICompatibilityCheckMixin() setattr(api_check, 'tracer', lambda: Tracer()) @@ -45,3 +49,46 @@ def test_baggage_check_works(self): # second check that assert on empty baggage will fail too with self.assertRaises(AssertionError): api_check.test_context_baggage() + + def test_scope_manager_check_works(self): + api_check = APICompatibilityCheckMixin() + setattr(api_check, 'tracer', lambda: Tracer()) + + # these tests are expected to succeed + api_check.test_start_active_span_ignore_active_span() + api_check.test_start_span_propagation_ignore_active_span() + + # no-op tracer doesn't have a ScopeManager implementation + # so these tests are expected to work, but asserts to fail + with self.assertRaises(AssertionError): + api_check.test_start_active_span() + + with self.assertRaises(AssertionError): + api_check.test_start_active_span_parent() + + with self.assertRaises(AssertionError): + api_check.test_start_span_propagation() + + with self.assertRaises(AssertionError): + api_check.test_tracer_start_active_span_scope() + + with self.assertRaises(AssertionError): + api_check.test_tracer_start_active_span_nesting() + + with self.assertRaises(AssertionError): + api_check.test_tracer_start_active_span_nesting_finish_on_close() + + with self.assertRaises(AssertionError): + api_check.test_tracer_start_active_span_wrong_close_order() + + with self.assertRaises(AssertionError): + api_check.test_tracer_start_span_scope() + + with self.assertRaises(AssertionError): + api_check.test_tracer_scope_manager_active() + + with self.assertRaises(AssertionError): + api_check.test_tracer_scope_manager_activate() + + with self.assertRaises(AssertionError): + api_check.test_start_active_span_not_finish_on_close() diff --git a/tests/test_scope.py b/tests/test_scope.py new file mode 100644 index 0000000..f4a536b --- /dev/null +++ b/tests/test_scope.py @@ -0,0 +1,46 @@ +# Copyright (c) 2017 The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import + +import mock + +from opentracing.scope_manager import ScopeManager +from opentracing.tracer import Tracer +from opentracing.scope import Scope +from opentracing.span import Span, SpanContext + + +def test_scope_wrapper(): + # ensure `Scope` wraps the `Span` argument + span = Span(tracer=Tracer(), context=SpanContext()) + scope = Scope(ScopeManager, span) + assert scope.span == span + + +def test_scope_context_manager(): + # ensure `Scope` can be used in a Context Manager that + # calls the `close()` method + span = Span(tracer=Tracer(), context=SpanContext()) + scope = Scope(ScopeManager(), span) + with mock.patch.object(scope, 'close') as close: + with scope: + pass + assert close.call_count == 1 diff --git a/tests/test_scope_manager.py b/tests/test_scope_manager.py new file mode 100644 index 0000000..01ae0cd --- /dev/null +++ b/tests/test_scope_manager.py @@ -0,0 +1,34 @@ +# Copyright (c) 2017 The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import + +from opentracing.scope_manager import ScopeManager +from opentracing.tracer import Tracer +from opentracing.span import Span, SpanContext + + +def test_scope_manager(): + # ensure the activation returns the noop `Scope` that is always active + scope_manager = ScopeManager() + span = Span(tracer=Tracer(), context=SpanContext()) + scope = scope_manager.activate(span, False) + assert scope == scope_manager._noop_scope + assert scope == scope_manager.active