From 2d06bd95eb038fe5d7f883a08d144f3c1ed0b461 Mon Sep 17 00:00:00 2001 From: "wolfgang.ziegler" Date: Wed, 27 Jan 2021 17:11:34 +0100 Subject: [PATCH 1/2] Prepare 1.4 Release --- README.md | 51 +- constraints.txt | 1 + samples/basic-sdk-sample/basic_sdk_sample.py | 9 +- samples/basic-sdk-sample/setup.py | 2 +- samples/fork-sdk-sample/fork_sdk_sample.py | 11 + samples/fork-sdk-sample/setup.py | 4 +- src/oneagent/__init__.py | 43 +- src/oneagent/_impl/native/sdkctypesiface.py | 64 ++- src/oneagent/_impl/native/sdkmockiface.py | 544 ------------------- src/oneagent/_impl/native/sdknulliface.py | 15 +- src/oneagent/common.py | 46 ++ src/oneagent/sdk/__init__.py | 73 ++- src/oneagent/version.py | 6 +- test-util-src/sdkmockiface.py | 29 +- test-util-src/testconfig.py | 9 +- test/conftest.py | 16 - test/test_import_all.py | 5 +- test/test_public_sdk.py | 16 +- test/test_pyversion.py | 19 + tox.ini | 1 + 20 files changed, 357 insertions(+), 607 deletions(-) create mode 100644 constraints.txt delete mode 100644 src/oneagent/_impl/native/sdkmockiface.py delete mode 100644 test/conftest.py create mode 100644 test/test_pyversion.py diff --git a/README.md b/README.md index 3a869c0..752e656 100644 --- a/README.md +++ b/README.md @@ -58,11 +58,16 @@ Dynatrace OneAgent version (it is the same as |OneAgent SDK for Python|OneAgent SDK for C/C++|Dynatrace OneAgent|Support status | |:----------------------|:---------------------|:-----------------|:------------------| +|1.4 |1.6.1 |≥1.179 |Supported | |1.3 |1.5.1 |≥1.179 |Supported | -|1.2 |1.4.1 |≥1.161 |Supported | -|1.1 |1.3.1 |≥1.151 |Supported | +|1.2 |1.4.1 |≥1.161 |Deprecated¹ | +|1.1 |1.3.1 |≥1.151 |Deprecated¹ | |1.0 |1.1.0 |≥1.141 |EAP (not supported)| +> 1. *Deprecated* releases of the OneAgent SDK for Python are still supported but this +> might change with a future release. Applications using those deprecated versions should +> be upgraded to the latest release. + ## Using the OneAgent SDK for Python in your application @@ -157,11 +162,15 @@ Unusual events that prevent an operation from completing successfully include: > **NOTE**: Use this as a development and debugging aid only. Your application should not rely on a calling sequence or any message content being set or passed to the callback. +During development, it is additionally recommended to use the "verbose callback" which also informs about other events that may be benign +but can be very helpful in debugging, e.g. a PurePath that was not created because a Tracer is disabled by configuration, etc. + ```python def _diag_callback(unicode_message): print(unicode_message) sdk.set_diagnostic_callback(_diag_callback) +sdk.set_verbose_callback(_diag_callback) # Do not use this callback in production ``` @@ -569,7 +578,18 @@ For more information on forked child processes, take a look at those resources: To debug your OneAgent SDK for Python installation, execute the following Python code: ```python +import logging +import time import oneagent + +log_handler = logging.StreamHandler() +log_formatter = logging.Formatter( + '%(asctime)s.%(msecs)03d UTC [%(thread)08x]' + ' %(levelname)-7s [%(name)-6s] %(message)s', + '%Y-%m-%d %H:%M:%S') +log_formatter.converter = time.gmtime +log_handler.setFormatter(log_formatter) +oneagent.logger.addHandler(log_handler) oneagent.logger.setLevel(1) init_result = oneagent.initialize(['loglevelsdk=finest', 'loglevel=finest']) print('InitResult=' + repr(init_result)) @@ -613,7 +633,6 @@ Known gotchas: directory, your platform is not supported. Otherwise, regardless if it works with that method or not, please report an issue as described in [Let us help you](#let-us-help-you). - ### Extended SDK State @@ -633,7 +652,12 @@ print('Agent is compatible:', oneagent.get_sdk().agent_is_compatible) # OneAgent SDK for C/C++ version separated by a '/'. print('Agent version:', oneagent.get_sdk().agent_version_string) ``` + +### Shutdown crashes +If your are experiencing crashes when your application exits, make +sure the you uninitialized the SDK properly by calling its `shutdown` +function. ## Repository contents @@ -695,13 +719,28 @@ SLAs don't apply for GitHub tickets. SLAs apply according to the customer's support level. - ## Release notes -Please see the [GitHub releases page](https://github.com/Dynatrace/OneAgent-SDK-for-Python/releases), -and the [PyPI release history](https://pypi.org/project/oneagent-sdk/#history). +### Version 1.4.0 +* Don't look for agent module in `PATH/LD_LIBRARY_PATH/..`. and +disallow relative a `DT_HOME` directory on Windows to prevent DLL hijacking issues. + +* Fixed a bug that might lead to crashes in the SDK's shutdown phase + +* Support for **Python versions < 3.5** is deprecated. The OneAgent SDK for Python +will still work with this release, but this might change in the future. + +* Following versions of the **OneAgent SDK for Python** are considered deprecated +and might not be supported in the future. Applications using it should be upgraded to the latest release. + * [Version 1.1.0](https://github.com/Dynatrace/OneAgent-SDK-for-Python/releases/tag/v1.1.0) + * [Version 1.2.0](https://github.com/Dynatrace/OneAgent-SDK-for-Python/releases/tag/v1.2.0) + * [Version 1.2.1](https://github.com/Dynatrace/OneAgent-SDK-for-Python/releases/tag/v1.2.1) + +> For older versions of the OneAgent SDK for Python, please see +the [GitHub releases page](https://github.com/Dynatrace/OneAgent-SDK-for-Python/releases), +and the [PyPI release history](https://pypi.org/project/oneagent-sdk/#history). ## License diff --git a/constraints.txt b/constraints.txt new file mode 100644 index 0000000..9bd2928 --- /dev/null +++ b/constraints.txt @@ -0,0 +1 @@ +pylint<2.5 diff --git a/samples/basic-sdk-sample/basic_sdk_sample.py b/samples/basic-sdk-sample/basic_sdk_sample.py index 0aa5951..99906c6 100644 --- a/samples/basic-sdk-sample/basic_sdk_sample.py +++ b/samples/basic-sdk-sample/basic_sdk_sample.py @@ -32,6 +32,8 @@ except NameError: pass +IN_DEV_ENVIRONMENT = True # Let's assume we are *not* in production here... + getsdk = oneagent.get_sdk # Just to make the code shorter. def traced_db_operation(dbinfo, sql): @@ -269,9 +271,14 @@ def main(): # oneagent.get_sdk() instead of calling the function multiple times. sdk = getsdk() - # Set the diagnostic callback. + # Set the diagnostic callback. Strongly recommended. sdk.set_diagnostic_callback(_diag_callback) + # Set the verbose callback. + # Not recommended in production as lots of messages can be emitted. + if IN_DEV_ENVIRONMENT: + sdk.set_verbose_callback(_diag_callback) + # The agent state is one of the integers in oneagent.sdk.AgentState. print('Agent state:', sdk.agent_state) diff --git a/samples/basic-sdk-sample/setup.py b/samples/basic-sdk-sample/setup.py index 7da368f..48fb48e 100644 --- a/samples/basic-sdk-sample/setup.py +++ b/samples/basic-sdk-sample/setup.py @@ -28,7 +28,7 @@ name='oneagent-sdk-basic-sample', version='0.0', # This sample is not separately versioned - install_requires=['oneagent-sdk==1.*,>=1.2'], + install_requires=['oneagent-sdk==1.*,>=1.4'], description='OneAgent SDK for Python: Basic sample application', long_description=long_description, diff --git a/samples/fork-sdk-sample/fork_sdk_sample.py b/samples/fork-sdk-sample/fork_sdk_sample.py index b804828..d983c30 100644 --- a/samples/fork-sdk-sample/fork_sdk_sample.py +++ b/samples/fork-sdk-sample/fork_sdk_sample.py @@ -39,9 +39,17 @@ def do_some_fancy_stuff(proc_number): sdk = getsdk() + # Initially, the state in the child will be PRE_INITIALIZED (2). + print('Agent fork state (child process before SDK call):', sdk.agent_fork_state) + # The agent state in the child process should be ACTIVE (0). print('Agent state (child process #{}): {}'.format(proc_number, sdk.agent_state), flush=True) + # After calling any SDK function but agent_fork_state, + # the state in the child will be FULLY_INITIALIZED (3). + # In this case the SDK function called was the agent_state property accessed above. + print('Agent fork state (child process after SDK call):', sdk.agent_fork_state) + print('Agent found:', sdk.agent_found) print('Agent is compatible:', sdk.agent_is_compatible) print('Agent version:', sdk.agent_version_string) @@ -105,6 +113,9 @@ def main(): # Since we're using the 'forkable' mode the state will be TEMPORARILY_INACTIVE (1) on Linux. print('Agent state (parent process):', sdk.agent_state) + # In the parent, the state will be PARENT_INITIALIZED (1). + print('Agent fork state (parent process):', sdk.agent_fork_state) + # The instance attribute 'agent_found' indicates whether an agent could be found or not. print('Agent found:', sdk.agent_found) diff --git a/samples/fork-sdk-sample/setup.py b/samples/fork-sdk-sample/setup.py index 6932a28..1de2fbf 100644 --- a/samples/fork-sdk-sample/setup.py +++ b/samples/fork-sdk-sample/setup.py @@ -28,7 +28,7 @@ name='oneagent-sdk-fork-sample', version='0.0', # This sample is not separately versioned - install_requires=['oneagent-sdk==1.*,>=1.3'], + install_requires=['oneagent-sdk==1.*,>=1.4'], description='OneAgent SDK for Python: Fork sample application', long_description=long_description, @@ -38,7 +38,7 @@ maintainer_email='dynatrace.oneagent.sdk@dynatrace.com', license='Apache License 2.0', entry_points={ - 'console_scripts': ['oneagent-sdk-basic-sample=fork_sdk_sample:main'], + 'console_scripts': ['oneagent-sdk-fork-sample=fork_sdk_sample:main'], }, classifiers=[ 'Intended Audience :: Developers', diff --git a/src/oneagent/__init__.py b/src/oneagent/__init__.py index 9c36a68..327d7fa 100644 --- a/src/oneagent/__init__.py +++ b/src/oneagent/__init__.py @@ -84,15 +84,35 @@ from oneagent._impl.six.moves import range #pylint:disable=import-error from oneagent.version import __version__ -from .common import SDKError, SDKInitializationError, ErrorCode, _ONESDK_INIT_FLAG_FORKABLE +from .common import ( + SDKError, SDKInitializationError, ErrorCode, + _ONESDK_INIT_FLAG_FORKABLE, _add_enum_helpers) from ._impl.native import nativeagent from ._impl.native.nativeagent import try_get_sdk from ._impl.native.sdknulliface import SDKNullInterface from ._impl.native.sdkdllinfo import WIN32 +if hasattr(sys, 'implementation'): + def _get_py_edition(): + return sys.implementation.name # pylint:disable=no-member +else: + import platform + + def _get_py_edition(): + return platform.python_implementation() + logger = logging.getLogger('py_sdk') logger.setLevel(logging.CRITICAL + 1) # Disabled by default +_PROCESS_TECH_PYTHON = 28 +_PROCESS_TECH_ONEAGENT_SDK = 118 + +def _get_py_version(): + return '.'.join(map(str, sys.version_info[:3])) + ( + '' if sys.version_info.releaselevel == "final" + else sys.version_info.releaselevel + str(sys.version_info.serial)) + +@_add_enum_helpers class InitResult(namedtuple('InitResult', 'status error')): __slots__ = () @@ -104,6 +124,10 @@ class InitResult(namedtuple('InitResult', 'status error')): __nonzero__ = __bool__ = lambda self: self.status >= 0 + def __repr__(self): + return "InitResult(status={}, error={!r})".format( + self._value_name(self.status), self.error) #pylint:disable=no-member + _sdk_ref_lk = Lock() _sdk_ref_count = 0 @@ -186,8 +210,9 @@ def initialize(sdkopts=(), sdklibname=None, forkable=False): * :meth:`oneagent.sdk.SDK.agent_version_string` works as expected. * :meth:`oneagent.sdk.SDK.agent_state` will return :data:`oneagent.common.AgentState.TEMPORARILY_INACTIVE` - but see the note below. - * :meth:`oneagent.sdk.SDK.set_diagnostic_callback` works as expected, the callback will be - carried over to forked child processes. + * :meth:`oneagent.sdk.SDK.set_diagnostic_callback` and + :meth:`oneagent.sdk.SDK.set_verbose_callback` work as expected, + the callback will be carried over to forked child processes. * It is recommended you call :func:`shutdown` when the original process will not fork any more children that want to use the SDK. @@ -248,8 +273,8 @@ def _try_init_noref(sdkopts=(), sdklibname=None, forkable=False): try: logger.info( - 'Initializing SDK with options=%s, libname=%s.', - sdkopts, sdklibname) + 'Initializing SDK on Python=%s with options=%s, libname=%s.', + (sys.version or '?').replace('\n', ' ').replace('\r', ''), sdkopts, sdklibname) sdk = nativeagent.initialize(sdklibname) have_warning = False @@ -270,7 +295,11 @@ def _try_init_noref(sdkopts=(), sdklibname=None, forkable=False): nativeagent.checkresult(sdk, sdk.initialize(flags), 'onesdk_initialize_2') _should_shutdown = True - logger.debug("initialize successful") + logger.debug('initialize successful, adding tech types...') + sdk.ex_agent_add_process_technology(_PROCESS_TECH_ONEAGENT_SDK, 'Python', __version__) + sdk.ex_agent_add_process_technology( + _PROCESS_TECH_PYTHON, _get_py_edition(), _get_py_version()) + logger.debug('tech type reporting complete') return InitResult( (InitResult.STATUS_INITIALIZED_WITH_WARNING if have_warning else InitResult.STATUS_INITIALIZED), @@ -280,7 +309,7 @@ def _try_init_noref(sdkopts=(), sdklibname=None, forkable=False): #pylint:disable=no-member if isinstance(e, SDKError) and e.code == ErrorCode.AGENT_NOT_ACTIVE: #pylint:enable=no-member - logger.debug("initialized, but agent not active") + logger.debug('initialized, but agent not active') return InitResult(InitResult.STATUS_INITIALIZED_WITH_WARNING, e) logger.exception('Failed initializing agent.') sdk = nativeagent.try_get_sdk() diff --git a/src/oneagent/_impl/native/sdkctypesiface.py b/src/oneagent/_impl/native/sdkctypesiface.py index 7cd96ab..aa62d5c 100644 --- a/src/oneagent/_impl/native/sdkctypesiface.py +++ b/src/oneagent/_impl/native/sdkctypesiface.py @@ -275,12 +275,16 @@ def __init__(self, libname): (), result_t) - # Missing: agent_internl_dispatch(int32, void*); probably useless + initfn( + 'agent_set_warning_callback', + (agent_logging_callback_t,), + result_t, + public=False) initfn( - 'agent_set_logging_callback', + 'agent_set_verbose_callback', (agent_logging_callback_t,), - None, + result_t, public=False) initfn( @@ -289,6 +293,22 @@ def __init__(self, libname): xchar_p, public=False) + + initfn( + 'agent_get_fork_state', + (), + ctypes.c_int32) + + initfn( + 'ex_api_enable_techtype', + (), + result_t, + public=False) + initfn( + 'ex_agent_add_process_technology_p', + (ctypes.c_int32, CCStringPInArg, CCStringPInArg), + None).__doc__ = '''(tech_type, tech_edition, tech_version)''' + # Specific nodes ## Database @@ -559,6 +579,12 @@ def initialize(self, init_flags=0): self._agent_found = found.value != 0 self._agent_is_compatible = compatible.value != 0 + enable_result = self._ex_api_enable_techtype() + if self._agent_is_compatible and enable_result != ErrorCode.SUCCESS: + logger.warning( + "Tech type reporting API could not be enabled: %d %s", + enable_result, self.strerror(enable_result)) + return result def agent_found(self): @@ -678,14 +704,16 @@ def cb_wrapper(level, msg): def agent_get_version_string(self): return self._agent_version - def agent_set_logging_callback(self, callback): + def _invoke_agent_log_cb_setter(self, callback, setter, update_stored_cb): + result = None + store_c_cb = lambda c_cb: setattr(self, "_diag_cb_" + setter.__name__, c_cb) if callback is None: - self._agent_set_logging_callback( - ctypes.cast(None, agent_logging_callback_t)) - self._diag_cb = None - self._py_diag_cb = None + result = setter(ctypes.cast(None, agent_logging_callback_t)) + if result == ErrorCode.SUCCESS: + store_c_cb(None) + if update_stored_cb: + self._py_diag_cb = None else: - @wraps(callback) def cb_wrapper(msg): if isinstance(msg, six.binary_type): @@ -693,9 +721,21 @@ def cb_wrapper(msg): return callback(msg) c_cb = agent_logging_callback_t(cb_wrapper) - self._agent_set_logging_callback(c_cb) - self._diag_cb = c_cb - self._py_diag_cb = cb_wrapper + result = setter(c_cb) + if result == ErrorCode.SUCCESS: + store_c_cb(c_cb) + if update_stored_cb: + self._py_diag_cb = cb_wrapper + return result + + + def agent_set_warning_callback(self, callback): + return self._invoke_agent_log_cb_setter( + callback, self._agent_set_warning_callback, update_stored_cb=True) + + def agent_set_verbose_callback(self, callback): + return self._invoke_agent_log_cb_setter( + callback, self._agent_set_verbose_callback, update_stored_cb=False) def __del__(self): # __del__ is also called when __init__ fails, so safeguard against that diff --git a/src/oneagent/_impl/native/sdkmockiface.py b/src/oneagent/_impl/native/sdkmockiface.py deleted file mode 100644 index d6a0d80..0000000 --- a/src/oneagent/_impl/native/sdkmockiface.py +++ /dev/null @@ -1,544 +0,0 @@ -# -*- coding: utf-8 -*- -'''SDK interface with mock implementation, for testing etc. Uses strict error -handling, i.e. does not support broken paths.''' - -from __future__ import print_function - -import sys -import warnings -from functools import wraps -import threading -import base64 -import struct -from itertools import chain - - -from oneagent._impl.six.moves import _thread, range #pylint:disable=import-error - -from oneagent._impl import six -from oneagent.common import AgentState, ErrorCode, MessageSeverity - -from .sdknulliface import SDKNullInterface - -class SDKLeakWarning(RuntimeWarning): - '''Warning that is emitted when a SDK resource was not properly disposed - of.''' - -class _Handle(object): - def __str__(self): - return '{}@0x{:X}'.format(type(self).__name__, id(self)) - - def __repr__(self): - return '{}{!r}@0x{:X}'.format(type(self).__name__, self.vals, id(self)) - - def __init__(self, *vals): - self.vals = vals - self.is_live = True - - def close(self): - self.is_live = False - - def __del__(self): - if self.is_live: - warnings.warn('Leaked handle {}'.format(self), SDKLeakWarning) - - - -class DbInfoHandle(_Handle): - pass - -class WsAppHandle(_Handle): - pass - -class ThreadBoundObject(object): - def __init__(self): - self.tid = _thread.get_ident() - - def check_thread(self): - if self.tid != _thread.get_ident(): - raise ValueError( - '{} was created on T{}, but T{} attempted an access'.format( - self, self.tid, _thread.get_ident())) - -class TracerHandle(_Handle, ThreadBoundObject): - CREATED = 0 - STARTED = 1 - ENDED = 2 - - LINK_CHILD = 0 - LINK_TAG = 1 - - _TAG_STRUCT = struct.Struct('>Q') # Big/network-endian unsigned long long - - is_in_taggable = False - has_out_tag = False - is_entrypoint = False - - def __init__(self, _nsdk, *vals): - assert isinstance(_nsdk, SDKMockInterface) - _Handle.__init__(self, *vals) - ThreadBoundObject.__init__(self) - self.path = None - self.state = self.CREATED - self.err_info = None - self.children = [] #[(link_kind: int, child: TracerHandle)] - self.linked_parent = None - self.in_tag = None - self.is_in_tag_resolved = False - - def close(self): - self.check_thread() - self.state = self.ENDED - if any(lnk == self.LINK_CHILD and c.state == self.STARTED - for lnk, c in self.children): - raise ValueError( - 'Ending tracer {} that has un-ended children: {}'.format( - self, self.children)) - _Handle.close(self) - - def all_original_children(self): - '''Yields all (direct and indirect) children with LINK_CHILD.''' - return chain.from_iterable( - c.all_nodes_in_subtree() - for lnk, c in self.children - if lnk == self.LINK_CHILD) - - def all_nodes_in_subtree(self): - '''Yields self and all (incl indirect) children with LINK_CHILD.''' - return chain((self,), self.all_original_children()) - - @property - def out_tag(self): - if not self.has_out_tag: - raise ValueError( - '{} tracer is not OutgoingTaggable'.format(type(self))) - if self.state != self.STARTED: - raise ValueError('Can only obtain tag when started!') - return self._TAG_STRUCT.pack(id(self)) - - def set_in_tag(self, tag): - if not self.is_in_taggable: - raise ValueError( - '{} tracer is not IncomingTaggable'.format(type(self))) - if self.state != self.CREATED: - raise ValueError('Cannot set tags after starting.') - - self.in_tag = tag - - @property - def in_tag_as_id(self): - if self.in_tag is None: - return None - (result,) = self._TAG_STRUCT.unpack(self.in_tag) - return result - - def dump(self, indent=''): - result = '{}{}(S={}'.format(indent, str(self), self.state) - intag = self.in_tag_as_id - if intag is not None: - result += ',I={}0x{:x}'.format( - '' if self.is_in_tag_resolved else '!', intag) - result += ')' - valstr = ', '.join(map(repr, self.vals)) - if valstr: - result += '\n{} V=({})'.format(indent, valstr) - for lnk, child in self.children: - result += '\n{} {}\n{}'.format( - indent, lnk, child.dump(indent + ' ')) - return result - - -class RemoteCallHandleBase(TracerHandle): - def __init__(self, *args, **kwargs): - TracerHandle.__init__(self, *args, **kwargs) - self.protocol_name = None -class InRemoteCallHandle(RemoteCallHandleBase): - is_entrypoint = True - is_in_taggable = True -class OutRemoteCallHandle(RemoteCallHandleBase): - has_out_tag = True - -class DbRequestHandle(TracerHandle): - def __init__(self, *args, **kwargs): - TracerHandle.__init__(self, *args, **kwargs) - self.returned_row_count = None - self.round_trip_count = None - -class InWebReqHandle(TracerHandle): - is_entrypoint = True - is_in_taggable = True - - def __init__(self, *args, **kwargs): - TracerHandle.__init__(self, *args, **kwargs) - self.req_hdrs = [] - self.resp_hdrs = [] - self.params = [] - self.resp_code = None - self.remote_addr = None - -class Path(ThreadBoundObject): - def __init__(self): - ThreadBoundObject.__init__(self) - self.nodestack = [] - - def start(self, tracer): - assert tracer.tid == self.tid - if tracer.state != TracerHandle.CREATED: - raise ValueError( - 'Tracer state {} is != CREATED'.format(tracer.state)) - if self.nodestack: - self.nodestack[-1].children.append( - (TracerHandle.LINK_CHILD, tracer)) - self.nodestack.append(tracer) - tracer.state = TracerHandle.STARTED - - def end(self, tracer): - tracer.close() - if self.nodestack[-1] is not tracer: - raise ValueError('Attempt to end {} while {} was active'.format( - tracer, self.nodestack[-1])) - else: - self.nodestack.pop() - -def _typecheck(val, expected_ty): - if not isinstance(val, expected_ty): - raise TypeError('Expected type {} but got {}({})'.format( - expected_ty, type(val), val)) - if isinstance(val, ThreadBoundObject): - val.check_thread() - -def _livecheck(val, expected_ty, state=None): - _typecheck(val, expected_ty) - if not val.is_live: - raise ValueError('Handle already closed: {}'.format(val)) - if state is not None and val.state != state: - raise ValueError('Handle {} has state {}, but needs {}.'.format( - val, val.state, state)) - -def _checkstate(state, maxstate=None, failure_r=ErrorCode.GENERIC): - def checkstate_impl(func): - @wraps(func) - def state_checked(self, *args, **kwargs): - #pylint:disable=protected-access - if maxstate is not None: - if not state <= self._state <= maxstate: - return failure_r - elif state != self._state: - return failure_r - return func(self, *args, **kwargs) - - state_checked.__wrapped__ = func - return state_checked - - return checkstate_impl - -def _entry_field(func): - - @wraps(func) - def checked(self, tracer_h, *args, **kwargs): - if tracer_h.state != TracerHandle.CREATED: - raise ValueError( - 'Attempt to set entry field too late: ' + func.__name__) - return func(self, tracer_h, *args, **kwargs) - - checked.__wrapped__ = func - return checked - - -def _strcheck(val, optional=False): - if optional and val is None: - return - if not isinstance(val, six.string_types): - raise TypeError('Expected a string type but got {}({})'.format( - type(val), val)) - if not optional and not val.strip(): - raise ValueError('Expected non-empty string, but got {!r}'.format(val)) - -def _mk_add_kvs_fn(adder): - - def add_kvs(self, tracer_h, keys, vals, count): - _livecheck(tracer_h, InWebReqHandle) - _typecheck(count, int) - for _, key, val in zip(range(count), keys, vals): - adder(self, tracer_h, key, val) - return add_kvs - -class SDKMockInterface(object): #pylint:disable=too-many-public-methods - def __init__(self): - self._diag_cb = self.stub_default_logging_function - self._log_cb = self.stub_default_logging_function - self._state = AgentState.NOT_INITIALIZED - self._log_level = MessageSeverity.FINEST - self._path_tls = threading.local() - self.finished_paths = [] - self.finished_paths_lk = threading.RLock() - - def all_finished_nodes(self): - with self.finished_paths_lk: - for node in self.finished_paths: - for subnode in node.all_nodes_in_subtree(): - yield subnode - - - def get_finished_node_by_id(self, id_tag): - with self.finished_paths_lk: - for node in self.all_finished_nodes(): - if id(node) == id_tag: - return node - return None - - - def process_finished_paths_tags(self): - unresolved = [] - with self.finished_paths_lk: - for node in self.all_finished_nodes(): - in_id = node.in_tag_as_id - if in_id is None: - continue - linked = self.get_finished_node_by_id(in_id) - if not linked: - unresolved.append(node) - continue - linked.children.append((TracerHandle.LINK_TAG, node)) - node.is_in_tag_resolved = True - node.linked_parent = linked - return unresolved - - def get_path(self, create=False): - path = getattr(self._path_tls, 'path', None) - if not path and create: - path = Path() - self.set_path(path) - return path - - def set_path(self, path): - self._path_tls.path = path - - #pylint:disable=no-self-use,unused-argument - - def stub_is_sdk_cmdline_arg(self, arg): - return arg.startswith('--dt_') - - def stub_process_cmdline_arg(self, arg, replace): - _strcheck(arg) - _typecheck(replace, bool) - if self._state != AgentState.NOT_INITIALIZED: - return ErrorCode.GENERIC - return ErrorCode.SUCCESS - - def stub_set_variable(self, assignment, replace): - _strcheck(assignment) - _typecheck(replace, bool) - if self._state != AgentState.NOT_INITIALIZED: - return ErrorCode.GENERIC - return ErrorCode.SUCCESS - - def stub_set_logging_level(self, level): - _typecheck(level, int) - if level < MessageSeverity.FINEST or level > MessageSeverity.DEBUG: - warnings.warn('Bad message severity level.', RuntimeWarning) - - def stub_default_logging_function(self, level, msg): - print('[OneSDK:Mock]', level, msg, file=sys.stderr) - - stub_set_logging_callback = SDKNullInterface.stub_set_logging_callback - - @_checkstate(AgentState.NOT_INITIALIZED) - def stub_free_variables(self): - pass - - def agent_get_version_string(self): - return u'0.000.0.00000000-{}'.format(type(self).__name__) - - @_checkstate(AgentState.NOT_INITIALIZED) - def initialize(self): - self._state = AgentState.ACTIVE - return ErrorCode.SUCCESS - - @_checkstate(AgentState.ACTIVE, AgentState.TEMPORARILY_INACTIVE) - def shutdown(self): - self._state = AgentState.NOT_INITIALIZED - return ErrorCode.SUCCESS - - def agent_get_current_state(self): - return self._state - - agent_set_logging_callback = SDKNullInterface.agent_set_logging_callback - agent_get_logging_callback = SDKNullInterface.agent_get_logging_callback - - def strerror(self, error_code): - if error_code == ErrorCode.SUCCESS: - return u'Success.' - elif error_code == ErrorCode.GENERIC: - return u'Generic error.' - return u'Unknown error #' + str(error_code) - - def webapplicationinfo_create(self, vhost, appid, ctxroot): - _strcheck(vhost) - _strcheck(appid) - _strcheck(ctxroot) - return WsAppHandle(vhost, appid, ctxroot) - - def webapplicationinfo_delete(self, wapp_h): - _typecheck(wapp_h, WsAppHandle) - wapp_h.close() - - def incomingwebrequesttracer_create(self, wapp_h, uri, http_method): - _livecheck(wapp_h, WsAppHandle) - _strcheck(uri) - _strcheck(http_method) - return InWebReqHandle(self, wapp_h, uri, http_method) - - #pylint:disable=invalid-name - - - @_entry_field - def incomingwebrequesttracer_add_request_header(self, tracer_h, key, val): - _livecheck(tracer_h, InWebReqHandle) - _strcheck(key) - _strcheck(val) - tracer_h.req_hdrs.append((key, val)) - - incomingwebrequesttracer_add_request_headers = _mk_add_kvs_fn( - incomingwebrequesttracer_add_request_header) - - def incomingwebrequesttracer_add_response_header(self, tracer_h, key, val): - _livecheck(tracer_h, InWebReqHandle) - _strcheck(key) - _strcheck(val) - tracer_h.resp_hdrs.append((key, val)) - - incomingwebrequesttracer_add_response_headers = _mk_add_kvs_fn( - incomingwebrequesttracer_add_response_header) - - def incomingwebrequesttracer_add_parameter(self, tracer_h, key, val): - _livecheck(tracer_h, InWebReqHandle) - _strcheck(key) - _strcheck(val) - tracer_h.params.append((key, val)) - - incomingwebrequesttracer_add_parameters = _mk_add_kvs_fn( - incomingwebrequesttracer_add_parameter) - - @_entry_field - def incomingwebrequesttracer_set_remote_address(self, tracer_h, addr): - _livecheck(tracer_h, InWebReqHandle) - _strcheck(addr, optional=True) - tracer_h.remote_addr = addr - - def incomingwebrequesttracer_set_status_code(self, tracer_h, code): - _livecheck(tracer_h, InWebReqHandle) - _typecheck(code, int) - tracer_h.resp_code = code - - #pylint:enable=invalid-name - - def databaseinfo_create(self, dbname, dbvendor, chan_ty, chan_ep): - _strcheck(dbname) - _strcheck(dbvendor) - _typecheck(chan_ty, int) - _strcheck(chan_ep, optional=True) - return DbInfoHandle(dbname, dbvendor, chan_ty, chan_ep) - - def databaseinfo_delete(self, dbh): - _typecheck(dbh, DbInfoHandle) - dbh.close() - - #pylint:disable=invalid-name - - def databaserequesttracer_create_sql( - self, dbh, sql): - _livecheck(dbh, DbInfoHandle) - _strcheck(sql) - return DbRequestHandle(self, dbh, sql) - - def databaserequesttracer_set_returned_row_count(self, tracer_h, count): - _livecheck(tracer_h, DbRequestHandle) - _typecheck(count, int) - assert count >= 0, 'Invalid count' - tracer_h.returned_row_count = count - - - def databaserequesttracer_set_round_trip_count(self, tracer_h, count): - _livecheck(tracer_h, DbRequestHandle) - _typecheck(count, int) - assert count >= 0, 'Invalid count' - tracer_h.round_trip_count = count - - #pylint:enable=invalid-name - - def outgoingremotecalltracer_create( #pylint:disable=too-many-arguments - self, svc_method, svc_name, svc_endpoint, chan_ty, chan_ep): - _strcheck(svc_method) - _strcheck(svc_name) - _strcheck(svc_endpoint) - _typecheck(chan_ty, int) - _strcheck(chan_ep, optional=True) - return OutRemoteCallHandle( - self, svc_method, svc_name, svc_endpoint, chan_ty, chan_ep) - - @_entry_field - def outgoingremotecalltracer_set_protocol_name( #pylint:disable=invalid-name - self, tracer_h, protocol_name): - _livecheck(tracer_h, OutRemoteCallHandle, TracerHandle.CREATED) - _strcheck(protocol_name, optional=True) - tracer_h.protocol_name = protocol_name - - def incomingremotecalltracer_create( - self, svc_method, svc_name, svc_endpoint): - _strcheck(svc_method) - _strcheck(svc_name) - _strcheck(svc_endpoint) - return InRemoteCallHandle(self, svc_method, svc_name, svc_endpoint) - - @_entry_field - def incomingremotecalltracer_set_protocol_name( #pylint:disable=invalid-name - self, tracer_h, protocol_name): - _livecheck(tracer_h, InRemoteCallHandle, TracerHandle.CREATED) - _strcheck(protocol_name, optional=True) - tracer_h.protocol_name = protocol_name - - def tracer_start(self, tracer_h): - _livecheck(tracer_h, TracerHandle, TracerHandle.CREATED) - path = self.get_path(create=tracer_h.is_entrypoint) - if path: - path.start(tracer_h) - - - def tracer_end(self, tracer_h): - _typecheck(tracer_h, TracerHandle) - path = self.get_path() - if not path: - assert tracer_h.state in (TracerHandle.ENDED, TracerHandle.CREATED) - tracer_h.close() - return - path.end(tracer_h) - if not path.nodestack: - with self.finished_paths_lk: - self.finished_paths.append(tracer_h) # tracer_h is the root node - - def tracer_error(self, tracer_h, error_class, error_message): - _livecheck(tracer_h, TracerHandle, TracerHandle.STARTED) - _strcheck(error_class, optional=True) - _strcheck(error_message, optional=True) - tracer_h.err_info = (error_class, error_message) - - def tracer_get_outgoing_tag(self, tracer_h, use_byte_tag=False): - _livecheck(tracer_h, TracerHandle, TracerHandle.STARTED) - if use_byte_tag: - return tracer_h.out_tag - return base64.b64encode(tracer_h.out_tag).decode('ASCII') - - @_entry_field - def tracer_set_incoming_string_tag(self, tracer_h, tag): - _livecheck(tracer_h, TracerHandle, TracerHandle.CREATED) - if tag is None: - tracer_h.set_in_tag(None) - _strcheck(tag, optional=True) - tracer_h.set_in_tag(base64.b64decode(tag)) - - @_entry_field - def tracer_set_incoming_byte_tag(self, tracer_h, tag): - _livecheck(tracer_h, TracerHandle, TracerHandle.CREATED) - _typecheck(tag, (six.binary_type, type(None))) - tracer_h.set_in_tag(tag) diff --git a/src/oneagent/_impl/native/sdknulliface.py b/src/oneagent/_impl/native/sdknulliface.py index 8b1898f..d0e70cc 100644 --- a/src/oneagent/_impl/native/sdknulliface.py +++ b/src/oneagent/_impl/native/sdknulliface.py @@ -22,7 +22,7 @@ from oneagent._impl import six -from oneagent.common import ErrorCode, AgentState +from oneagent.common import ErrorCode, AgentState, AgentForkState NULL_HANDLE = 0 @@ -75,8 +75,19 @@ def agent_get_current_state(self): # is what the stub also returns if it cannot find the agent. return AgentState.NOT_INITIALIZED - def agent_set_logging_callback(self, callback): + def agent_get_fork_state(self): + return AgentForkState.ERROR + + def ex_agent_add_process_technology(self, tech_type, tech_edition, tech_version): + pass + + + def agent_set_warning_callback(self, callback): self._diag_cb = callback + return ErrorCode.SUCCESS + + def agent_set_verbose_callback(self, callback): + return ErrorCode.SUCCESS def agent_get_logging_callback(self): return self._diag_cb diff --git a/src/oneagent/common.py b/src/oneagent/common.py index 5527611..f5ba75d 100644 --- a/src/oneagent/common.py +++ b/src/oneagent/common.py @@ -51,6 +51,24 @@ class _Uninstantiable(object): def __new__(cls): raise ValueError('Attempt to instantiate') +def _add_enum_helpers(decorated_cls): + # pylint:disable=protected-access + decorated_cls._enum_name_by_val = dict() + for key in dir(decorated_cls): + val = getattr(decorated_cls, key) + if isinstance(val, int): + decorated_cls._enum_name_by_val.setdefault(val, key) + + @classmethod + def _value_name(cls, val): + result = cls._enum_name_by_val.get(val) # pylint:disable=no-member + if result is None: + return "" + return cls.__name__ + "." + result + decorated_cls._value_name = _value_name + return decorated_cls + + class AgentState(_Uninstantiable): '''Constants for the agent's state. See :attr:`oneagent.sdk.SDK.agent_state`.''' @@ -125,6 +143,34 @@ class ErrorCode(_Uninstantiable): #: occurred while the SDK was initialized. FORK_CHILD = _ERROR_BASE + 13 +class AgentForkState(_Uninstantiable): + '''Constants for the agent's fork state. See + :attr:`oneagent.sdk.SDK.agent_fork_state`.''' + + #: SDK cannot be used in this process, but forked processes may use the SDK. + #: This is the state of the process + #: that called :func:`oneagent.initialize` with :code:`forkable=True` + PARENT_INITIALIZED = 1 + + #: Forked processes can use the SDK. + #: Using the SDK in this process is allowed but + #: changes the state to :attr:`.FULLY_INITIALIZED` + #: This is the state of all child processes + #: of a process that is :attr:`.PARENT_INITIALIZED`. + PRE_INITIALIZED = 2 + + #: SDK can be used, forked processes may not use the SDK. + #: This is the state of a process that was previously :attr:`.PRE_INITIALIZED` + #: and then called an SDK function. + FULLY_INITIALIZED = 3 + + #: SDK can be used, forked processes may not use the SDK, + #: :func:`oneagent.initialize` was called without :code:`forkable=True`. + NOT_FORKABLE = 4 + + #: Some error occurred while trying to determine the agent fork state. + ERROR = -1 + class MessageSeverity(_Uninstantiable): # Private '''Constants for the severity of log messages. diff --git a/src/oneagent/sdk/__init__.py b/src/oneagent/sdk/__init__.py index b617da9..67feebd 100644 --- a/src/oneagent/sdk/__init__.py +++ b/src/oneagent/sdk/__init__.py @@ -71,7 +71,7 @@ def _get_kvc(kv_arg): kv_arg[1], kv_arg[2] if len(kv_arg) == 3 else len(kv_arg[0])) -class SDK(object): +class SDK(object): # pylint:disable=too-many-public-methods '''The main entry point to the Dynatrace SDK.''' def _applytag(self, tracer, str_tag, byte_tag): @@ -378,30 +378,91 @@ def trace_in_process_link(self, link_bytes): self._nsdk.trace_in_process_link(link_bytes)) def set_diagnostic_callback(self, callback): - '''Sets a callback to be informed of unusual events. + '''Sets the agent warning callback function. - Unusual events include: + The agent warning callback is called whenever one of the following + happens while executing an SDK function: - - API usage errors. - - Other unexpected events (like out of memory situations) that prevented + - An SDK usage error is detected or + - An unexpected or unusual event (e.g. out of memory) prevented an operation from completing successfully. + The application must not call any SDK functions from the callback. + + There is no default warning callback. + It is recommended you set one for development and debugging, + as this is the main way the SDK reports errors. + .. warning:: Use this as a development and debugging aid only. Your application should not rely on a calling sequence or any message content being set or passed to the callback. + .. seealso:: :meth:`set_verbose_callback` is a method you + can call additionally to get more messages. + :param callable callback: The callback function. Receives the (unicode) error message as its only argument. ''' - self._nsdk.agent_set_logging_callback(callback) + result = self._nsdk.agent_set_warning_callback(callback) + if result != ErrorCode.SUCCESS: + logger.error( + "Could not set warning callback: Error %d: %s", + result, self._nsdk.strerror(result)) + + def set_verbose_callback(self, callback): + '''Sets the verbose agent logging callback function. + + Similar to :meth:`set_diagnostic_callback` but the callback supplied + here will not be called with warning messages but with additional + messages that may e.g. explain why a PurePath was not created even if + the reason is (usually) benign. + + .. note:: It usually does not make sense to set this callback without also using + :meth:`set_diagnostic_callback` in addition. + + + .. warning:: This callback can receive lots and lots of messages. + You should not usually use it in production. + + :param callable callback: The callback function. Receives the (unicode) + error message as its only argument. + + .. versionadded:: 1.4.0 + ''' + result = self._nsdk.agent_set_verbose_callback(callback) + if result != ErrorCode.SUCCESS: + logger.error( + "Could not set verbose callback: Error %d: %s", + result, self._nsdk.strerror(result)) + @property def agent_state(self): '''Returns the current agent state (one of the constants in :class:`oneagent.common.AgentState`). + .. warning:: Even though this is a property, + accessing it may modify the agent's fork state + (see :class:`~oneagent.common.AgentForkState` and :meth:`.agent_fork_state`) + when :func:`oneagent.initialize` was called with :code:`forkable=True`. + :rtype: int''' return self._nsdk.agent_get_current_state() + @property + def agent_fork_state(self): + '''Returns the current agent fork state (one of the constants in + :class:`oneagent.common.AgentForkState`). + + This is only relevant if :func:`oneagent.initialize` was called with :code:`forkable=True`. + + Calling this function only has a defined result when :meth:`.agent_state` is + equal to :attr:`oneagent.common.AgentState.ACTIVE` + or :attr:`oneagent.common.AgentState.TEMPORARILY_INACTIVE`. + + :rtype: int + ''' + return self._nsdk.agent_get_fork_state() + @property def agent_version_string(self): '''Returns the version string of the loaded SDK agent module. diff --git a/src/oneagent/version.py b/src/oneagent/version.py index 62738fd..cd0da17 100644 --- a/src/oneagent/version.py +++ b/src/oneagent/version.py @@ -21,12 +21,12 @@ # That's the OneAgent SDK for Python version. # See https://www.python.org/dev/peps/pep-0440/ "Version Identification and # Dependency Specification" -__version__ = '1.3.0' +__version__ = '1.4.0' # Define the OneAgent SDK for C/C++ version which should be shipped with this # Python SDK version. -shipped_c_stub_version = '1.5.1' +shipped_c_stub_version = '1.6.1' # Below are the minimum and maximum required/supported OneAgent SDK for C/C++ versions. -min_stub_version = OnesdkStubVersion(1, 5, 1) +min_stub_version = OnesdkStubVersion(1, 6, 1) max_stub_version = OnesdkStubVersion(2, 0, 0) diff --git a/test-util-src/sdkmockiface.py b/test-util-src/sdkmockiface.py index 9752e13..18a0d65 100644 --- a/test-util-src/sdkmockiface.py +++ b/test-util-src/sdkmockiface.py @@ -26,12 +26,13 @@ import base64 import struct from itertools import chain +from collections import namedtuple from oneagent._impl.six.moves import _thread, range #pylint:disable=import-error from oneagent._impl import six -from oneagent.common import AgentState, ErrorCode, MessageSeverity +from oneagent.common import AgentState, AgentForkState, ErrorCode, MessageSeverity class SDKLeakWarning(RuntimeWarning): '''Warning that is emitted when a SDK resource was not properly disposed @@ -308,14 +309,17 @@ def add_kvs(self, tracer_h, keys, vals, count): adder(self, tracer_h, key, val) return add_kvs +ProcessTech = namedtuple('ProcessTech', 'type edition version') + class SDKMockInterface(object): #pylint:disable=too-many-public-methods def __init__(self): - self._diag_cb = self.stub_default_logging_function + self._diag_cb = lambda text: self.stub_default_logging_function(text, 0) self._log_cb = self.stub_default_logging_function self._state = AgentState.NOT_INITIALIZED self._log_level = MessageSeverity.FINEST self._path_tls = threading.local() self.finished_paths = [] + self.techs = [] self.finished_paths_lk = threading.RLock() def all_finished_nodes(self): @@ -389,6 +393,7 @@ def stub_default_logging_function(self, level, msg): def stub_set_logging_callback(self, sink): self._log_cb = sink + @_checkstate(AgentState.NOT_INITIALIZED) def stub_free_variables(self): pass @@ -418,6 +423,17 @@ def agent_get_current_state(self): def agent_set_logging_callback(self, callback): self._diag_cb = callback + def agent_set_warning_callback(self, callback): + if self._state == AgentState.NOT_INITIALIZED: + return ErrorCode.GENERIC + self._diag_cb = callback + return ErrorCode.SUCCESS + + def agent_set_verbose_callback(self, callback): + if self._state != AgentState.NOT_INITIALIZED: + return ErrorCode.GENERIC + return ErrorCode.SUCCESS + def agent_get_logging_callback(self): return self._diag_cb @@ -428,6 +444,15 @@ def strerror(self, error_code): return u'Generic error.' return u'Unknown error #' + str(error_code) + def agent_get_fork_state(self): + return AgentForkState.ERROR + + def ex_agent_add_process_technology(self, tech_type, tech_edition, tech_version): + _typecheck(tech_type, int) + _strcheck(tech_edition) + _strcheck(tech_version) + self.techs.append(ProcessTech(type=tech_type, edition=tech_edition, version=tech_version)) + def webapplicationinfo_create(self, vhost, appid, ctxroot): _strcheck(vhost) _strcheck(appid) diff --git a/test-util-src/testconfig.py b/test-util-src/testconfig.py index 39cca44..727b9de 100644 --- a/test-util-src/testconfig.py +++ b/test-util-src/testconfig.py @@ -27,9 +27,8 @@ def setup_logging(): logger.setLevel(logging.DEBUG) @pytest.fixture -def native_sdk(): +def native_sdk_noinit(): nsdk = sdkmockiface.SDKMockInterface() - nsdk.initialize() yield nsdk for i, root in enumerate(nsdk.finished_paths): itag = root.in_tag_as_id @@ -39,6 +38,12 @@ def native_sdk(): rootstr = root.dump() print('root#{:2}: {}'.format(i, rootstr)) +@pytest.fixture +def native_sdk(native_sdk_noinit): + native_sdk_noinit.initialize() + yield native_sdk_noinit + + @pytest.fixture def sdk(native_sdk): diff --git a/test/conftest.py b/test/conftest.py deleted file mode 100644 index ecc183c..0000000 --- a/test/conftest.py +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2018 Dynatrace LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -pytest_plugins = 'testconfig' diff --git a/test/test_import_all.py b/test/test_import_all.py index 2e4089d..e88c235 100644 --- a/test/test_import_all.py +++ b/test/test_import_all.py @@ -18,6 +18,7 @@ import os from os import path import inspect +import warnings import pytest import oneagent @@ -100,7 +101,9 @@ def argstr(func): if spec.args and spec.args[0] == 'self': #pylint:disable=no-member,protected-access spec = spec._replace(args=spec.args[1:]) - return inspect.formatargspec(*spec, formatarg=lambda arg: 'arg') + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + return inspect.formatargspec(*spec, formatarg=lambda arg: 'arg') def check_sdk_iface(csdkinst, actual): cnames = pubnames(csdkinst) diff --git a/test/test_public_sdk.py b/test/test_public_sdk.py index cf783ac..4fa79bc 100644 --- a/test/test_public_sdk.py +++ b/test/test_public_sdk.py @@ -17,6 +17,7 @@ import pytest +import oneagent from oneagent import sdk as onesdk from oneagent._impl import six from oneagent._impl.native import nativeagent @@ -253,10 +254,21 @@ def chk_dbcall(dbchild): chk_seq(node.children, [check_linked_remote_thread]) -def test_public_sdk_sample(native_sdk): - nativeagent._force_initialize(native_sdk) #pylint:disable=protected-access +def test_public_sdk_sample(native_sdk_noinit, monkeypatch): + # pylint:disable=protected-access + monkeypatch.setattr( + nativeagent, + "initialize", + lambda libname: nativeagent._force_initialize(native_sdk_noinit)) from . import onesdksamplepy onesdksamplepy.main() + native_sdk = native_sdk_noinit + + pysdk_tech = sdkmockiface.ProcessTech( + oneagent._PROCESS_TECH_ONEAGENT_SDK, 'Python', oneagent.__version__) + assert pysdk_tech in native_sdk.techs + assert any(tt for tt in native_sdk.techs if tt.type == oneagent._PROCESS_TECH_PYTHON) + assert_resolve_all(native_sdk) diff --git a/test/test_pyversion.py b/test/test_pyversion.py new file mode 100644 index 0000000..a1dced0 --- /dev/null +++ b/test/test_pyversion.py @@ -0,0 +1,19 @@ +import sys +from collections import namedtuple + +import oneagent + +# pylint:disable=protected-access + +MockVerInfo = namedtuple("MockVerInfo", "major minor patch releaselevel serial") + +def test_pyversion_beta(monkeypatch): + monkeypatch.setattr(sys, 'version_info', MockVerInfo(5, 4, 3, 'beta', 123)) + assert oneagent._get_py_version() == '5.4.3beta123' + +def test_pyversion_final(monkeypatch): + monkeypatch.setattr(sys, 'version_info', MockVerInfo(5, 4, 3, 'final', 0)) + assert oneagent._get_py_version() == '5.4.3' + +def test_py_edition(): + assert oneagent._get_py_edition() diff --git a/tox.ini b/tox.ini index a5d47f0..9a4620c 100644 --- a/tox.ini +++ b/tox.ini @@ -17,6 +17,7 @@ commands = lint-!py27-!pypy: pylint test --disable=redefined-outer-name,unidiomatic-typecheck lint-py27,lint-pypy: pylint test --disable=redefined-outer-name,unidiomatic-typecheck --ignore-patterns=.*py3.* deps = + -c constraints.txt py27-lint: pylint~=1.9 lint-!py27-!pypy: pylint~=2.4 pytest From abf71f3f4996a6536484f7f27e298d8d586d79c7 Mon Sep 17 00:00:00 2001 From: Wolfgang Ziegler Date: Thu, 28 Jan 2021 21:07:10 +0100 Subject: [PATCH 2/2] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Christian Neumüller --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 752e656..0038619 100644 --- a/README.md +++ b/README.md @@ -725,7 +725,7 @@ SLAs apply according to the customer's support level. ### Version 1.4.0 * Don't look for agent module in `PATH/LD_LIBRARY_PATH/..`. and -disallow relative a `DT_HOME` directory on Windows to prevent DLL hijacking issues. +disallow a relative path in the `DT_HOME` directory on Windows to prevent DLL hijacking issues. * Fixed a bug that might lead to crashes in the SDK's shutdown phase