From 1ac8d222830217b8f5b656ba6cce6325324e6b13 Mon Sep 17 00:00:00 2001 From: Tim Pansino Date: Wed, 7 Dec 2022 10:50:42 -0800 Subject: [PATCH 1/8] Fix CLM exception catching --- newrelic/core/code_level_metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newrelic/core/code_level_metrics.py b/newrelic/core/code_level_metrics.py index 652715eab..ba00d93af 100644 --- a/newrelic/core/code_level_metrics.py +++ b/newrelic/core/code_level_metrics.py @@ -89,7 +89,7 @@ def extract_code_from_callable(func): # Use inspect to get file and line number file_path = inspect.getsourcefile(func) line_number = inspect.getsourcelines(func)[1] - except TypeError: + except Exception: pass # Split function path to extract class name From 1b67aabc1f172086aecdfc831febf22a2d3f5e84 Mon Sep 17 00:00:00 2001 From: Tim Pansino Date: Wed, 7 Dec 2022 10:53:12 -0800 Subject: [PATCH 2/8] Reorganize CLM Tests --- .../_test_code_level_metrics.py | 5 +- .../agent_features/test_code_level_metrics.py | 327 +++++++++++------- 2 files changed, 201 insertions(+), 131 deletions(-) diff --git a/tests/agent_features/_test_code_level_metrics.py b/tests/agent_features/_test_code_level_metrics.py index 90529320d..8297d9c0c 100644 --- a/tests/agent_features/_test_code_level_metrics.py +++ b/tests/agent_features/_test_code_level_metrics.py @@ -13,11 +13,12 @@ # limitations under the License. import functools + def exercise_function(): return -class ExerciseClass(): +class ExerciseClass(object): def exercise_method(self): return @@ -30,7 +31,7 @@ def exercise_class_method(cls): return -class ExerciseClassCallable(): +class ExerciseClassCallable(object): def __call__(self): return diff --git a/tests/agent_features/test_code_level_metrics.py b/tests/agent_features/test_code_level_metrics.py index 1d2bd6c3a..c00b595fa 100644 --- a/tests/agent_features/test_code_level_metrics.py +++ b/tests/agent_features/test_code_level_metrics.py @@ -23,7 +23,8 @@ from newrelic.api.background_task import background_task from newrelic.api.function_trace import FunctionTrace, FunctionTraceWrapper -from _test_code_level_metrics import exercise_function, CLASS_INSTANCE, CLASS_INSTANCE_CALLABLE, exercise_lambda, exercise_partial, ExerciseClass, ExerciseClassCallable, __file__ as FILE_PATH +from _test_code_level_metrics import * +from _test_code_level_metrics import __file__ as FILE_PATH is_pypy = hasattr(sys, "pypy_version_info") @@ -39,115 +40,162 @@ BUILTIN_ATTRS = {"code.filepath": "", "code.lineno": None} if not is_pypy else {} + def merge_dicts(A, B): d = {} d.update(A) d.update(B) return d -@pytest.mark.parametrize( - "func,args,agents", - ( - ( # Function - exercise_function, - (), - { - "code.filepath": FILE_PATH, - "code.function": "exercise_function", - "code.lineno": 16, - "code.namespace": NAMESPACE, - }, - ), - ( # Method - CLASS_INSTANCE.exercise_method, - (), - { - "code.filepath": FILE_PATH, - "code.function": "exercise_method", - "code.lineno": 21, - "code.namespace": CLASS_NAMESPACE, - }, - ), - ( # Static Method - CLASS_INSTANCE.exercise_static_method, - (), - { - "code.filepath": FILE_PATH, - "code.function": "exercise_static_method", - "code.lineno": 24, - "code.namespace": FUZZY_NAMESPACE, - }, - ), - ( # Class Method - ExerciseClass.exercise_class_method, - (), - { - "code.filepath": FILE_PATH, - "code.function": "exercise_class_method", - "code.lineno": 28, - "code.namespace": CLASS_NAMESPACE, - }, - ), - ( # Callable object - CLASS_INSTANCE_CALLABLE, - (), - { - "code.filepath": FILE_PATH, - "code.function": "__call__", - "code.lineno": 34, - "code.namespace": CALLABLE_CLASS_NAMESPACE, - }, - ), - ( # Lambda - exercise_lambda, - (), - { - "code.filepath": FILE_PATH, - "code.function": "", - "code.lineno": 40, - "code.namespace": NAMESPACE, - }, - ), - ( # Functools Partials - exercise_partial, - (), + +def extract(obj): + with FunctionTrace("_test", source=obj): + pass + + +_TEST_BASIC_CALLABLES = { + "function": ( + exercise_function, + (), + { + "code.filepath": FILE_PATH, + "code.function": "exercise_function", + "code.lineno": 17, + "code.namespace": NAMESPACE, + }, + ), + "lambda": ( + exercise_lambda, + (), + { + "code.filepath": FILE_PATH, + "code.function": "", + "code.lineno": 75, + "code.namespace": NAMESPACE, + }, + ), + "partial": ( + exercise_partial, + (), + { + "code.filepath": FILE_PATH, + "code.function": "exercise_function", + "code.lineno": 17, + "code.namespace": NAMESPACE, + }, + ), + "builtin_function": ( + max, + (1, 2), + merge_dicts( { - "code.filepath": FILE_PATH, - "code.function": "exercise_function", - "code.lineno": 16, - "code.namespace": NAMESPACE, - }, - ), - ( # Top Level Builtin - max, - (1, 2), - merge_dicts({ "code.function": "max", "code.namespace": "builtins" if six.PY3 else "__builtin__", - }, BUILTIN_ATTRS), + }, + BUILTIN_ATTRS, ), - ( # Module Level Builtin - sqlite3.connect, - (":memory:",), - merge_dicts({ + ), + "builtin_module_function": ( + sqlite3.connect, + (":memory:",), + merge_dicts( + { "code.function": "connect", "code.namespace": "_sqlite3", - }, BUILTIN_ATTRS), + }, + BUILTIN_ATTRS, ), - ( # Builtin Method - SQLITE_CONNECTION.__enter__, - (), - merge_dicts({ + ), +} + + +@pytest.mark.parametrize( + "func,args,agents", + [pytest.param(*args, id=id_) for id_, args in six.iteritems(_TEST_BASIC_CALLABLES)], +) +def test_code_level_metrics_basic_callables(func, args, agents): + @override_application_settings( + { + "code_level_metrics.enabled": True, + } + ) + @dt_enabled + @validate_span_events( + count=1, + exact_agents=agents, + ) + @background_task() + def _test(): + extract(func) + + _test() + + +_TEST_METHODS = { + "method": ( + CLASS_INSTANCE.exercise_method, + (), + { + "code.filepath": FILE_PATH, + "code.function": "exercise_method", + "code.lineno": 22, + "code.namespace": CLASS_NAMESPACE, + }, + ), + "static_method": ( + CLASS_INSTANCE.exercise_static_method, + (), + { + "code.filepath": FILE_PATH, + "code.function": "exercise_static_method", + "code.lineno": 25, + "code.namespace": FUZZY_NAMESPACE, + }, + ), + "class_method": ( + ExerciseClass.exercise_class_method, + (), + { + "code.filepath": FILE_PATH, + "code.function": "exercise_class_method", + "code.lineno": 29, + "code.namespace": CLASS_NAMESPACE, + }, + ), + "call_method": ( + CLASS_INSTANCE_CALLABLE, + (), + { + "code.filepath": FILE_PATH, + "code.function": "__call__", + "code.lineno": 35, + "code.namespace": CALLABLE_CLASS_NAMESPACE, + }, + ), + "builtin_method": ( + SQLITE_CONNECTION.__enter__, + (), + merge_dicts( + { "code.function": "__enter__", "code.namespace": "sqlite3.Connection" if not is_pypy else "_sqlite3.Connection", - }, BUILTIN_ATTRS), + }, + BUILTIN_ATTRS, ), ), +} + + +@pytest.mark.parametrize( + "func,args,agents", + [pytest.param(*args, id=id_) for id_, args in six.iteritems(_TEST_METHODS)], ) -def test_code_level_metrics_callables(func, args, agents): - @override_application_settings({ - "code_level_metrics.enabled": True, - }) +def test_code_level_metrics_methods(func, args, agents): + @override_application_settings( + { + "code_level_metrics.enabled": True, + } + ) @dt_enabled @validate_span_events( count=1, @@ -155,47 +203,69 @@ def test_code_level_metrics_callables(func, args, agents): ) @background_task() def _test(): - FunctionTraceWrapper(func)(*args) + extract(func) _test() @pytest.mark.parametrize( - "obj,agents", - ( - ( # Class with __call__ - ExerciseClassCallable, - { - "code.filepath": FILE_PATH, - "code.function": "ExerciseClassCallable", - "code.lineno": 33, - "code.namespace":NAMESPACE, - }, - ), - ( # Class without __call__ - ExerciseClass, - { - "code.filepath": FILE_PATH, - "code.function": "ExerciseClass", - "code.lineno": 20, - "code.namespace": NAMESPACE, - }, - ), - ( # Non-callable Object instance - CLASS_INSTANCE, - { - "code.filepath": FILE_PATH, - "code.function": "ExerciseClass", - "code.lineno": 20, - "code.namespace": NAMESPACE, - }, - ), +_TEST_OBJECTS = { + "class": ( + ExerciseClass, + { + "code.filepath": FILE_PATH, + "code.function": "ExerciseClass", + "code.lineno": 21, + "code.namespace": NAMESPACE, + }, + ), + "callable_class": ( + ExerciseClassCallable, + { + "code.filepath": FILE_PATH, + "code.function": "ExerciseClassCallable", + "code.lineno": 34, + "code.namespace": NAMESPACE, + }, ), + "type_constructor_class": ( + ExerciseTypeConstructor, + { + "code.filepath": FILE_PATH, + "code.function": "ExerciseTypeConstructor", + "code.namespace": NAMESPACE, + }, + ), + "type_constructor_class_callable_class": ( + ExerciseTypeConstructorCallable, + { + "code.filepath": FILE_PATH, + "code.function": "ExerciseTypeConstructorCallable", + "code.namespace": NAMESPACE, + }, + ), + "non_callable_object": ( + CLASS_INSTANCE, + { + "code.filepath": FILE_PATH, + "code.function": "ExerciseClass", + "code.lineno": 20, + "code.namespace": NAMESPACE, + }, + ), +} + + +@pytest.mark.parametrize( + "obj,agents", + [pytest.param(*args, id=id_) for id_, args in six.iteritems(_TEST_OBJECTS)], ) def test_code_level_metrics_objects(obj, agents): - @override_application_settings({ - "code_level_metrics.enabled": True, - }) + @override_application_settings( + { + "code_level_metrics.enabled": True, + } + ) @dt_enabled @validate_span_events( count=1, @@ -203,7 +273,6 @@ def test_code_level_metrics_objects(obj, agents): ) @background_task() def _test(): - with FunctionTrace("_test", source=obj): - pass - - _test() \ No newline at end of file + extract(obj) + + _test() From 79957157457e0af015b23e6355d0c0d7fdaa54d2 Mon Sep 17 00:00:00 2001 From: Tim Pansino Date: Wed, 7 Dec 2022 10:53:33 -0800 Subject: [PATCH 3/8] Add type constructor tests to CLM --- .../_test_code_level_metrics.py | 34 ++++++++ .../agent_features/test_code_level_metrics.py | 77 +++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/tests/agent_features/_test_code_level_metrics.py b/tests/agent_features/_test_code_level_metrics.py index 8297d9c0c..5fea199b8 100644 --- a/tests/agent_features/_test_code_level_metrics.py +++ b/tests/agent_features/_test_code_level_metrics.py @@ -35,8 +35,42 @@ class ExerciseClassCallable(object): def __call__(self): return + +def exercise_method(self): + return + + +@staticmethod +def exercise_static_method(): + return + + +@classmethod +def exercise_class_method(cls): + return + + +def __call__(self): + return + + +type_dict = { + "exercise_method": exercise_method, + "exercise_static_method": exercise_static_method, + "exercise_class_method": exercise_class_method, + "exercise_lambda": lambda: None, +} +callable_type_dict = type_dict.copy() +callable_type_dict["__call__"] = __call__ + +ExerciseTypeConstructor = type("ExerciseTypeConstructor", (object,), type_dict) +ExerciseTypeConstructorCallable = type("ExerciseTypeConstructorCallable", (object,), callable_type_dict) + + CLASS_INSTANCE = ExerciseClass() CLASS_INSTANCE_CALLABLE = ExerciseClassCallable() +TYPE_CONSTRUCTOR_CLASS_INSTANCE = ExerciseTypeConstructor() +TYPE_CONSTRUCTOR_CALLABLE_CLASS_INSTANCE = ExerciseTypeConstructorCallable() exercise_lambda = lambda: None exercise_partial = functools.partial(exercise_function) diff --git a/tests/agent_features/test_code_level_metrics.py b/tests/agent_features/test_code_level_metrics.py index c00b595fa..46703e5d7 100644 --- a/tests/agent_features/test_code_level_metrics.py +++ b/tests/agent_features/test_code_level_metrics.py @@ -32,6 +32,8 @@ NAMESPACE = "_test_code_level_metrics" CLASS_NAMESPACE = ".".join((NAMESPACE, "ExerciseClass")) CALLABLE_CLASS_NAMESPACE = ".".join((NAMESPACE, "ExerciseClassCallable")) +TYPE_CONSTRUCTOR_NAMESPACE = ".".join((NAMESPACE, "ExerciseTypeConstructor")) +TYPE_CONSTRUCTOR_CALLABLE_NAMESPACE = ".".join((NAMESPACE, "ExerciseTypeConstructorCallable")) FUZZY_NAMESPACE = CLASS_NAMESPACE if six.PY3 else NAMESPACE if FILE_PATH.endswith(".pyc"): FILE_PATH = FILE_PATH[:-1] @@ -208,7 +210,82 @@ def _test(): _test() +_TEST_TYPE_CONSTRUCTOR_METHODS = { + "method": ( + TYPE_CONSTRUCTOR_CLASS_INSTANCE.exercise_method, + (), + { + "code.filepath": FILE_PATH, + "code.function": "exercise_method", + "code.lineno": 39, + "code.namespace": TYPE_CONSTRUCTOR_NAMESPACE, + }, + ), + "static_method": ( + TYPE_CONSTRUCTOR_CLASS_INSTANCE.exercise_static_method, + (), + { + "code.filepath": FILE_PATH, + "code.function": "exercise_static_method", + "code.lineno": 43, + "code.namespace": NAMESPACE, + }, + ), + "class_method": ( + ExerciseTypeConstructor.exercise_class_method, + (), + { + "code.filepath": FILE_PATH, + "code.function": "exercise_class_method", + "code.lineno": 48, + "code.namespace": TYPE_CONSTRUCTOR_NAMESPACE, + }, + ), + "lambda_method": ( + ExerciseTypeConstructor.exercise_lambda, + (), + { + "code.filepath": FILE_PATH, + "code.function": "", + "code.lineno": 61, + "code.namespace": NAMESPACE, + }, + ), + "call_method": ( + TYPE_CONSTRUCTOR_CALLABLE_CLASS_INSTANCE, + (), + { + "code.filepath": FILE_PATH, + "code.function": "__call__", + "code.lineno": 53, + "code.namespace": TYPE_CONSTRUCTOR_CALLABLE_NAMESPACE, + }, + ), +} + + @pytest.mark.parametrize( + "func,args,agents", + [pytest.param(*args, id=id_) for id_, args in six.iteritems(_TEST_TYPE_CONSTRUCTOR_METHODS)], +) +def test_code_level_metrics_type_constructor_methods(func, args, agents): + @override_application_settings( + { + "code_level_metrics.enabled": True, + } + ) + @dt_enabled + @validate_span_events( + count=1, + exact_agents=agents, + ) + @background_task() + def _test(): + extract(func) + + _test() + + _TEST_OBJECTS = { "class": ( ExerciseClass, From 1f65b549f78e7c4ee10cc16e9e3d00cda7044480 Mon Sep 17 00:00:00 2001 From: Tim Pansino Date: Wed, 7 Dec 2022 11:00:34 -0800 Subject: [PATCH 4/8] Fix line number --- tests/agent_features/test_code_level_metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/agent_features/test_code_level_metrics.py b/tests/agent_features/test_code_level_metrics.py index 46703e5d7..84744b2b9 100644 --- a/tests/agent_features/test_code_level_metrics.py +++ b/tests/agent_features/test_code_level_metrics.py @@ -326,7 +326,7 @@ def _test(): { "code.filepath": FILE_PATH, "code.function": "ExerciseClass", - "code.lineno": 20, + "code.lineno": 21, "code.namespace": NAMESPACE, }, ), From a51d97df2dbd18aceeec73ebfcaf44e60782247f Mon Sep 17 00:00:00 2001 From: Tim Pansino Date: Wed, 7 Dec 2022 11:14:11 -0800 Subject: [PATCH 5/8] Pin tox version --- .github/actions/setup-python-matrix/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-python-matrix/action.yml b/.github/actions/setup-python-matrix/action.yml index 3654f7eb2..bcb5cbc78 100644 --- a/.github/actions/setup-python-matrix/action.yml +++ b/.github/actions/setup-python-matrix/action.yml @@ -47,4 +47,4 @@ runs: shell: bash run: | python3.10 -m pip install -U pip - python3.10 -m pip install -U wheel setuptools tox virtualenv!=20.0.24 + python3.10 -m pip install -U wheel setuptools 'tox<4' virtualenv!=20.0.24 From b4c176b916e3d74f256da2174c42e92ef7f75ef2 Mon Sep 17 00:00:00 2001 From: Tim Pansino Date: Wed, 7 Dec 2022 12:31:50 -0800 Subject: [PATCH 6/8] Fix lambda tests in CLM --- tests/agent_features/test_code_level_metrics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/agent_features/test_code_level_metrics.py b/tests/agent_features/test_code_level_metrics.py index 84744b2b9..0c8ea12d3 100644 --- a/tests/agent_features/test_code_level_metrics.py +++ b/tests/agent_features/test_code_level_metrics.py @@ -248,7 +248,8 @@ def _test(): "code.filepath": FILE_PATH, "code.function": "", "code.lineno": 61, - "code.namespace": NAMESPACE, + # Lambdas behave strangely in type constructors on Python 2 and use the class namespace. + "code.namespace": NAMESPACE if six.PY3 else TYPE_CONSTRUCTOR_NAMESPACE, }, ), "call_method": ( From 56352bff6ccf1020ddcdbf9eba59853bcaf851d0 Mon Sep 17 00:00:00 2001 From: Hannah Stepanek Date: Wed, 7 Dec 2022 13:13:44 -0800 Subject: [PATCH 7/8] Fix lint issues --- .../_test_code_level_metrics.py | 2 +- .../agent_features/test_code_level_metrics.py | 30 +++++++++++++------ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/tests/agent_features/_test_code_level_metrics.py b/tests/agent_features/_test_code_level_metrics.py index 5fea199b8..bbe3363f4 100644 --- a/tests/agent_features/_test_code_level_metrics.py +++ b/tests/agent_features/_test_code_level_metrics.py @@ -72,5 +72,5 @@ def __call__(self): TYPE_CONSTRUCTOR_CLASS_INSTANCE = ExerciseTypeConstructor() TYPE_CONSTRUCTOR_CALLABLE_CLASS_INSTANCE = ExerciseTypeConstructorCallable() -exercise_lambda = lambda: None +exercise_lambda = lambda: None # noqa: E731 exercise_partial = functools.partial(exercise_function) diff --git a/tests/agent_features/test_code_level_metrics.py b/tests/agent_features/test_code_level_metrics.py index 0c8ea12d3..18312718e 100644 --- a/tests/agent_features/test_code_level_metrics.py +++ b/tests/agent_features/test_code_level_metrics.py @@ -12,20 +12,32 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys import sqlite3 -import newrelic.packages.six as six -import pytest +import sys -from testing_support.fixtures import override_application_settings, dt_enabled +import pytest +from _test_code_level_metrics import ( + CLASS_INSTANCE, + CLASS_INSTANCE_CALLABLE, + TYPE_CONSTRUCTOR_CALLABLE_CLASS_INSTANCE, + TYPE_CONSTRUCTOR_CLASS_INSTANCE, + ExerciseClass, + ExerciseClassCallable, + ExerciseTypeConstructor, + ExerciseTypeConstructorCallable, +) +from _test_code_level_metrics import __file__ as FILE_PATH +from _test_code_level_metrics import ( + exercise_function, + exercise_lambda, + exercise_partial, +) +from testing_support.fixtures import dt_enabled, override_application_settings from testing_support.validators.validate_span_events import validate_span_events +import newrelic.packages.six as six from newrelic.api.background_task import background_task -from newrelic.api.function_trace import FunctionTrace, FunctionTraceWrapper - -from _test_code_level_metrics import * -from _test_code_level_metrics import __file__ as FILE_PATH - +from newrelic.api.function_trace import FunctionTrace is_pypy = hasattr(sys, "pypy_version_info") From ccc65671590daf398aa5a3e3e2e4675aef01074f Mon Sep 17 00:00:00 2001 From: Hannah Stepanek Date: Wed, 7 Dec 2022 13:28:45 -0800 Subject: [PATCH 8/8] Turn helper func into pytest fixture --- .../agent_features/test_code_level_metrics.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/agent_features/test_code_level_metrics.py b/tests/agent_features/test_code_level_metrics.py index 18312718e..a7aeaa39a 100644 --- a/tests/agent_features/test_code_level_metrics.py +++ b/tests/agent_features/test_code_level_metrics.py @@ -62,9 +62,13 @@ def merge_dicts(A, B): return d -def extract(obj): - with FunctionTrace("_test", source=obj): - pass +@pytest.fixture +def extract(): + def _extract(obj): + with FunctionTrace("_test", source=obj): + pass + + return _extract _TEST_BASIC_CALLABLES = { @@ -127,7 +131,7 @@ def extract(obj): "func,args,agents", [pytest.param(*args, id=id_) for id_, args in six.iteritems(_TEST_BASIC_CALLABLES)], ) -def test_code_level_metrics_basic_callables(func, args, agents): +def test_code_level_metrics_basic_callables(func, args, agents, extract): @override_application_settings( { "code_level_metrics.enabled": True, @@ -204,7 +208,7 @@ def _test(): "func,args,agents", [pytest.param(*args, id=id_) for id_, args in six.iteritems(_TEST_METHODS)], ) -def test_code_level_metrics_methods(func, args, agents): +def test_code_level_metrics_methods(func, args, agents, extract): @override_application_settings( { "code_level_metrics.enabled": True, @@ -281,7 +285,7 @@ def _test(): "func,args,agents", [pytest.param(*args, id=id_) for id_, args in six.iteritems(_TEST_TYPE_CONSTRUCTOR_METHODS)], ) -def test_code_level_metrics_type_constructor_methods(func, args, agents): +def test_code_level_metrics_type_constructor_methods(func, args, agents, extract): @override_application_settings( { "code_level_metrics.enabled": True, @@ -350,7 +354,7 @@ def _test(): "obj,agents", [pytest.param(*args, id=id_) for id_, args in six.iteritems(_TEST_OBJECTS)], ) -def test_code_level_metrics_objects(obj, agents): +def test_code_level_metrics_objects(obj, agents, extract): @override_application_settings( { "code_level_metrics.enabled": True,