diff --git a/newrelic/admin/record_deploy.py b/newrelic/admin/record_deploy.py index 883c54590..7851253c5 100644 --- a/newrelic/admin/record_deploy.py +++ b/newrelic/admin/record_deploy.py @@ -16,7 +16,7 @@ import pwd from newrelic.admin import command, usage -from newrelic.common import agent_http, certs, encoding_utils +from newrelic.common import agent_http, encoding_utils from newrelic.config import initialize from newrelic.core.config import global_settings diff --git a/newrelic/admin/run_python.py b/newrelic/admin/run_python.py index a627c1711..956afdcc1 100644 --- a/newrelic/admin/run_python.py +++ b/newrelic/admin/run_python.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from newrelic.admin import command, usage +from newrelic.admin import command @command( @@ -62,7 +62,6 @@ def log_message(text, *args): log_message("%s = %r", name, os.environ.get(name)) from newrelic import __file__ as root_directory - from newrelic import version root_directory = os.path.dirname(root_directory) boot_directory = os.path.join(root_directory, "bootstrap") diff --git a/newrelic/api/in_function.py b/newrelic/api/in_function.py index bf7edc7a1..88edb084b 100644 --- a/newrelic/api/in_function.py +++ b/newrelic/api/in_function.py @@ -14,4 +14,4 @@ # Use of these from this module will be deprecated. -from newrelic.common.object_wrapper import InFunctionWrapper, in_function, wrap_in_function +from newrelic.common.object_wrapper import InFunctionWrapper, in_function, wrap_in_function # noqa: F401 diff --git a/newrelic/api/object_wrapper.py b/newrelic/api/object_wrapper.py index e742cc17d..3f76d00e5 100644 --- a/newrelic/api/object_wrapper.py +++ b/newrelic/api/object_wrapper.py @@ -12,13 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys - # These have been moved. They are retained here until all references to # them are moved at which point will mark as deprecated to ensure users # weren't using them directly. -from newrelic.common.object_names import callable_name -from newrelic.common.object_wrapper import ObjectWrapper, wrap_object +from newrelic.common.object_names import callable_name # noqa: F401 +from newrelic.common.object_wrapper import ObjectWrapper, wrap_object # noqa: F401 # From Python 3.X. In older Python versions it fails if attributes do # not exist and don't maintain a __wrapped__ attribute. diff --git a/newrelic/api/out_function.py b/newrelic/api/out_function.py index 97950f732..7672cf93d 100644 --- a/newrelic/api/out_function.py +++ b/newrelic/api/out_function.py @@ -14,4 +14,4 @@ # Use of these from this module will be deprecated. -from newrelic.common.object_wrapper import OutFunctionWrapper, out_function, wrap_out_function +from newrelic.common.object_wrapper import OutFunctionWrapper, out_function, wrap_out_function # noqa: F401 diff --git a/newrelic/api/post_function.py b/newrelic/api/post_function.py index 9e2912eb8..436b15f98 100644 --- a/newrelic/api/post_function.py +++ b/newrelic/api/post_function.py @@ -14,4 +14,4 @@ # Use of these from this module will be deprecated. -from newrelic.common.object_wrapper import PostFunctionWrapper, post_function, wrap_post_function +from newrelic.common.object_wrapper import PostFunctionWrapper, post_function, wrap_post_function # noqa: F401 diff --git a/newrelic/api/pre_function.py b/newrelic/api/pre_function.py index 26b67e2b2..e5df8e9b6 100644 --- a/newrelic/api/pre_function.py +++ b/newrelic/api/pre_function.py @@ -14,4 +14,4 @@ # Use of these from this module will be deprecated. -from newrelic.common.object_wrapper import PreFunctionWrapper, pre_function, wrap_pre_function +from newrelic.common.object_wrapper import PreFunctionWrapper, pre_function, wrap_pre_function # noqa: F401 diff --git a/newrelic/common/async_wrapper.py b/newrelic/common/async_wrapper.py index e4b9bf213..d0f3c8245 100644 --- a/newrelic/common/async_wrapper.py +++ b/newrelic/common/async_wrapper.py @@ -64,7 +64,7 @@ async def wrapper(*args, **kwargs): while True: try: sent = yield yielded - except GeneratorExit as e: + except GeneratorExit: await g.aclose() raise except BaseException as e: diff --git a/newrelic/common/object_names.py b/newrelic/common/object_names.py index 19b42a0a1..b6471a090 100644 --- a/newrelic/common/object_names.py +++ b/newrelic/common/object_names.py @@ -18,7 +18,6 @@ import functools import inspect import sys -import types # Object model terminology for quick reference. # diff --git a/newrelic/console.py b/newrelic/console.py index 78f82672d..1bdd6c17b 100644 --- a/newrelic/console.py +++ b/newrelic/console.py @@ -366,8 +366,6 @@ def do_interpreter(self): interactive Python interpreter. Invoke 'exit()' or 'quit()' to escape the interpreter session.""" - enabled = False - _settings = global_settings() if not _settings.console.allow_interpreter_cmd: diff --git a/newrelic/core/agent_control_health.py b/newrelic/core/agent_control_health.py index faeaf40d9..bdaf6225a 100644 --- a/newrelic/core/agent_control_health.py +++ b/newrelic/core/agent_control_health.py @@ -96,7 +96,7 @@ def is_valid_file_delivery_location(file_uri): return True - except Exception as e: + except Exception: _logger.warning( "Configured Agent Control health delivery location is not valid. Health check will not be enabled." ) diff --git a/newrelic/core/internal_metrics.py b/newrelic/core/internal_metrics.py index 5a2517e72..8c22eeabf 100644 --- a/newrelic/core/internal_metrics.py +++ b/newrelic/core/internal_metrics.py @@ -12,11 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import functools -import sys import threading import time -import types import newrelic.common.object_wrapper diff --git a/newrelic/hooks/database_aiomysql.py b/newrelic/hooks/database_aiomysql.py index 1f0ced3ab..9a2f3d1d1 100644 --- a/newrelic/hooks/database_aiomysql.py +++ b/newrelic/hooks/database_aiomysql.py @@ -47,7 +47,7 @@ def __await__(self): while True: try: sent = yield yielded - except GeneratorExit as e: + except GeneratorExit: g.close() raise except BaseException as e: diff --git a/newrelic/hooks/database_psycopg2.py b/newrelic/hooks/database_psycopg2.py index 46b8a7a0e..c5835c940 100644 --- a/newrelic/hooks/database_psycopg2.py +++ b/newrelic/hooks/database_psycopg2.py @@ -63,6 +63,10 @@ class ConnectionSaveParamsWrapper(DBAPI2ConnectionWrapper): def __enter__(self): transaction = current_transaction() + if not transaction: + # Return unwrapped connection if there is no transaction + return self.__wrapped__.__enter__() + name = callable_name(self.__wrapped__.__enter__) with FunctionTrace(name, source=self.__wrapped__.__enter__): self.__wrapped__.__enter__() @@ -77,6 +81,9 @@ def __enter__(self): def __exit__(self, exc, value, tb): transaction = current_transaction() + if not transaction: + return self.__wrapped__.__exit__(exc, value, tb) + name = callable_name(self.__wrapped__.__exit__) with FunctionTrace(name, source=self.__wrapped__.__exit__): if exc is None: diff --git a/newrelic/hooks/external_botocore.py b/newrelic/hooks/external_botocore.py index 2a2254964..5b3bd9786 100644 --- a/newrelic/hooks/external_botocore.py +++ b/newrelic/hooks/external_botocore.py @@ -82,7 +82,7 @@ def extract_sqs_agent_attrs(instance, *args, **kwargs): agent_attrs["cloud.region"] = m.group(1) agent_attrs["cloud.account.id"] = m.group(2) agent_attrs["messaging.destination.name"] = m.group(3) - except Exception as e: + except Exception: _logger.debug("Failed to capture AWS SQS info.", exc_info=True) return agent_attrs @@ -117,7 +117,7 @@ def extract_kinesis_agent_attrs(instance, *args, **kwargs): agent_attrs["cloud.platform"] = "aws_kinesis_data_streams" return agent_attrs - except Exception as e: + except Exception: _logger.debug("Failed to capture AWS Kinesis info.", exc_info=True) return agent_attrs @@ -140,7 +140,7 @@ def extract_firehose_agent_attrs(instance, *args, **kwargs): agent_attrs["cloud.resource_id"] = ( f"arn:aws:firehose:{region}:{account_id}:deliverystream/{stream_name}" ) - except Exception as e: + except Exception: _logger.debug("Failed to capture AWS Kinesis Delivery Stream (Firehose) info.", exc_info=True) return agent_attrs @@ -786,7 +786,7 @@ async def __anext__(self): try: return_val = await self.__wrapped__.__anext__() record_stream_chunk(self, return_val, transaction) - except StopAsyncIteration as e: + except StopAsyncIteration: record_events_on_stop_iteration(self, transaction) raise except Exception as exc: @@ -919,8 +919,6 @@ def handle_chat_completion_event(transaction, bedrock_attrs): response_id = bedrock_attrs.get("response_id", None) model = bedrock_attrs.get("model", None) - settings = transaction.settings if transaction.settings is not None else global_settings() - input_message_list = bedrock_attrs.get("input_message_list", []) output_message_list = bedrock_attrs.get("output_message_list", []) number_of_messages = ( @@ -1055,7 +1053,7 @@ def _nr_dynamodb_datastore_trace_wrapper_(wrapped, instance, args, kwargs): f"arn:{partition}:dynamodb:{region}:{account_id:012d}:table/{_target}" ) - except Exception as e: + except Exception: _logger.debug("Failed to capture AWS DynamoDB info.", exc_info=True) trace.agent_attributes.update(agent_attrs) diff --git a/newrelic/hooks/external_feedparser.py b/newrelic/hooks/external_feedparser.py index 83c54da1d..902bb4ceb 100644 --- a/newrelic/hooks/external_feedparser.py +++ b/newrelic/hooks/external_feedparser.py @@ -13,7 +13,6 @@ # limitations under the License. import sys -import types import newrelic.api.external_trace import newrelic.api.object_wrapper diff --git a/newrelic/hooks/external_httpx.py b/newrelic/hooks/external_httpx.py index eab6f120a..03974df45 100644 --- a/newrelic/hooks/external_httpx.py +++ b/newrelic/hooks/external_httpx.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from collections import abc - from newrelic.api.external_trace import ExternalTrace from newrelic.common.object_wrapper import wrap_function_wrapper diff --git a/newrelic/hooks/framework_graphql.py b/newrelic/hooks/framework_graphql.py index 25922538e..d64784605 100644 --- a/newrelic/hooks/framework_graphql.py +++ b/newrelic/hooks/framework_graphql.py @@ -453,7 +453,7 @@ def wrap_graphql_impl(wrapped, instance, args, kwargs): try: with ErrorTrace(ignore=ignore_graphql_duplicate_exception): result = wrapped(*args, **kwargs) - except Exception as e: + except Exception: # Execution finished synchronously, exit immediately. trace.__exit__(*sys.exc_info()) raise diff --git a/newrelic/hooks/framework_graphql_py3.py b/newrelic/hooks/framework_graphql_py3.py index 92ac14aae..5c79e80be 100644 --- a/newrelic/hooks/framework_graphql_py3.py +++ b/newrelic/hooks/framework_graphql_py3.py @@ -15,7 +15,6 @@ import sys from newrelic.api.error_trace import ErrorTrace -from newrelic.api.function_trace import FunctionTrace def nr_coro_execute_name_wrapper(wrapped, result, set_name): diff --git a/newrelic/hooks/framework_sanic.py b/newrelic/hooks/framework_sanic.py index 7b6543e5e..14077eb6d 100644 --- a/newrelic/hooks/framework_sanic.py +++ b/newrelic/hooks/framework_sanic.py @@ -245,7 +245,7 @@ def _nr_sanic_register_middleware_(wrapped, instance, args, kwargs): callable_name(middleware) middleware_func = middleware if hasattr(middleware, "func"): - name = callable_name(middleware.func) + callable_name(middleware.func) middleware_func = middleware.func wrapped_middleware = _nr_wrapper_middleware_(attach_to)(middleware_func) diff --git a/newrelic/hooks/framework_tornado.py b/newrelic/hooks/framework_tornado.py index 245565fd7..c80a45a58 100644 --- a/newrelic/hooks/framework_tornado.py +++ b/newrelic/hooks/framework_tornado.py @@ -15,7 +15,6 @@ import functools import inspect import sys -import textwrap import time from newrelic.api.application import application_instance @@ -46,7 +45,10 @@ def _store_version_info(): return tornado.version_info -def convert_yielded(*args, **kwargs): +def convert_yielded(*args, **kwargs): # noqa: F811 + # Delays import of convert_yielded until this function is called to avoid + # import ordering issues. + global convert_yielded from tornado.gen import convert_yielded as _convert_yielded diff --git a/newrelic/hooks/logger_loguru.py b/newrelic/hooks/logger_loguru.py index 19b2af3a0..c00e0d460 100644 --- a/newrelic/hooks/logger_loguru.py +++ b/newrelic/hooks/logger_loguru.py @@ -18,7 +18,6 @@ from newrelic.api.application import application_instance from newrelic.api.transaction import current_transaction, record_log_event from newrelic.common.object_wrapper import wrap_function_wrapper -from newrelic.common.package_version_utils import get_package_version_tuple from newrelic.common.signature import bind_args from newrelic.core.config import global_settings from newrelic.hooks.logger_logging import add_nr_linking_metadata diff --git a/newrelic/hooks/messagebroker_confluentkafka.py b/newrelic/hooks/messagebroker_confluentkafka.py index e799e75e5..662e5ba87 100644 --- a/newrelic/hooks/messagebroker_confluentkafka.py +++ b/newrelic/hooks/messagebroker_confluentkafka.py @@ -120,7 +120,7 @@ def wrap_Consumer_poll(wrapped, instance, args, kwargs): # Step 2: Poll for records try: record = wrapped(*args, **kwargs) - except Exception as e: + except Exception: if current_transaction(): notice_error() else: diff --git a/newrelic/hooks/messagebroker_kombu.py b/newrelic/hooks/messagebroker_kombu.py index 48cfaa4e0..73807dba4 100644 --- a/newrelic/hooks/messagebroker_kombu.py +++ b/newrelic/hooks/messagebroker_kombu.py @@ -103,8 +103,6 @@ def wrap_Producer_publish(wrapped, instance, args, kwargs): headers = bound_args["headers"] headers = headers if headers else {} - value = bound_args["body"] - key = bound_args["routing_key"] exchange = getattr(bound_args["exchange"], "name", None) or "Default" transaction.add_messagebroker_info("Kombu", get_package_version("kombu")) @@ -257,7 +255,7 @@ def wrap_serialize(wrapped, instance, args, kwargs): group = "MessageBroker/Kombu/Exchange" name = f"Named/{exchange}/Serialization/Value" - with FunctionTrace(name=name, group=group) as ft: + with FunctionTrace(name=name, group=group): return wrapped(*args, **kwargs) diff --git a/newrelic/hooks/mlmodel_langchain.py b/newrelic/hooks/mlmodel_langchain.py index 67454e383..cfcc031e9 100644 --- a/newrelic/hooks/mlmodel_langchain.py +++ b/newrelic/hooks/mlmodel_langchain.py @@ -188,7 +188,7 @@ async def wrap_asimilarity_search(wrapped, instance, args, kwargs): linking_metadata = get_trace_linking_metadata() try: response = await wrapped(*args, **kwargs) - except Exception as exc: + except Exception: ft.notice_error(attributes={"vector_store_id": search_id}) ft.__exit__(*sys.exc_info()) _create_error_vectorstore_events(transaction, search_id, args, kwargs, linking_metadata, wrapped) @@ -221,7 +221,7 @@ def wrap_similarity_search(wrapped, instance, args, kwargs): linking_metadata = get_trace_linking_metadata() try: response = wrapped(*args, **kwargs) - except Exception as exc: + except Exception: ft.notice_error(attributes={"vector_store_id": search_id}) ft.__exit__(*sys.exc_info()) _create_error_vectorstore_events(transaction, search_id, args, kwargs, linking_metadata, wrapped) @@ -309,7 +309,7 @@ def wrap_tool_sync_run(wrapped, instance, args, kwargs): linking_metadata = get_trace_linking_metadata() try: return_val = wrapped(**run_args) - except Exception as exc: + except Exception: _record_tool_error( instance, transaction, @@ -366,7 +366,7 @@ async def wrap_tool_async_run(wrapped, instance, args, kwargs): linking_metadata = get_trace_linking_metadata() try: return_val = await wrapped(**run_args) - except Exception as exc: + except Exception: _record_tool_error( instance, transaction, @@ -561,7 +561,7 @@ async def wrap_chain_async_run(wrapped, instance, args, kwargs): linking_metadata = get_trace_linking_metadata() try: response = await wrapped(input=run_args["input"], config=run_args["config"], **run_args.get("kwargs", {})) - except Exception as exc: + except Exception: ft.notice_error(attributes={"completion_id": completion_id}) ft.__exit__(*sys.exc_info()) _create_error_chain_run_events( @@ -605,7 +605,7 @@ def wrap_chain_sync_run(wrapped, instance, args, kwargs): linking_metadata = get_trace_linking_metadata() try: response = wrapped(input=run_args["input"], config=run_args["config"], **run_args.get("kwargs", {})) - except Exception as exc: + except Exception: ft.notice_error(attributes={"completion_id": completion_id}) ft.__exit__(*sys.exc_info()) _create_error_chain_run_events( @@ -708,7 +708,7 @@ def _create_successful_chain_run_events( except: try: output_message_list = [str(response)] - except Exception as e: + except Exception: _logger.warning( "Unable to capture response inside langchain chain instrumentation. No response message event will be captured. Report this issue to New Relic Support.\n%s", traceback.format_exception(*sys.exc_info()), diff --git a/newrelic/hooks/mlmodel_openai.py b/newrelic/hooks/mlmodel_openai.py index 8c32a575f..3e47a0560 100644 --- a/newrelic/hooks/mlmodel_openai.py +++ b/newrelic/hooks/mlmodel_openai.py @@ -93,7 +93,6 @@ def wrap_chat_completion_sync(wrapped, instance, args, kwargs): transaction._add_agent_attribute("llm", True) completion_id = str(uuid.uuid4()) - request_message_list = kwargs.get("messages", []) ft = FunctionTrace(name=wrapped.__name__, group="Llm/completion/OpenAI") ft.__enter__() @@ -275,7 +274,6 @@ def _record_embedding_success(transaction, embedding_id, linking_metadata, kwarg request_id = response_headers.get("x-request-id") response_model = attribute_response.get("model") - response_usage = attribute_response.get("usage", {}) or {} organization = ( response_headers.get("openai-organization") if OPENAI_V1 @@ -430,9 +428,6 @@ async def wrap_chat_completion_async(wrapped, instance, args, kwargs): def _handle_completion_success(transaction, linking_metadata, completion_id, kwargs, ft, return_val): settings = transaction.settings if transaction.settings is not None else global_settings() - span_id = linking_metadata.get("span.id") - trace_id = linking_metadata.get("trace.id") - request_message_list = kwargs.get("messages") or [] stream = kwargs.get("stream", False) # Only if streaming and streaming monitoring is enabled and the response is not empty # do we not exit the function trace. @@ -485,7 +480,6 @@ def _record_completion_success(transaction, linking_metadata, completion_id, kwa if response: response_model = response.get("model") response_id = response.get("id") - response_usage = response.get("usage") or {} output_message_list = [] finish_reason = None choices = response.get("choices") or [] @@ -497,7 +491,6 @@ def _record_completion_success(transaction, linking_metadata, completion_id, kwa else: response_model = kwargs.get("response.model") response_id = kwargs.get("id") - response_usage = {} output_message_list = [] finish_reason = None if "content" in kwargs: @@ -732,7 +725,7 @@ def __next__(self): try: return_val = self.__wrapped__.__next__() _record_stream_chunk(self, return_val) - except StopIteration as e: + except StopIteration: _record_events_on_stop_iteration(self, transaction) raise except Exception as exc: @@ -829,7 +822,7 @@ async def __anext__(self): try: return_val = await self._nr_wrapped_iter.__anext__() _record_stream_chunk(self, return_val) - except StopAsyncIteration as e: + except StopAsyncIteration: _record_events_on_stop_iteration(self, transaction) raise except Exception as exc: diff --git a/pyproject.toml b/pyproject.toml index ce6990b3f..6304b8b53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,8 +33,8 @@ pep8-naming.extend-ignore-names = ["X", "y"] select = [ # Enabled linters and rules - # "F", # Pyflakes "E", # pycodestyle + "F", # Pyflakes "W", # pycodestyle "I", # isort # "N", # pep8-naming @@ -137,6 +137,7 @@ ignore = [ "S", # flake8-bandit (security checks are not necessary in tests) "F401", # unused-import "F811", # redefined-while-unused (pytest fixtures trigger this) + "F841", # unused-variable (intentional in tests to document what an unused output is) "E731", # lambda-assignment (acceptable in tests) "PLR2004", # magic-value-comparison (comparing to constant values) "ASYNC251", # blocking-sleep-in-async-function (acceptable in tests) diff --git a/tests/agent_features/_test_async_generator_trace.py b/tests/agent_features/_test_async_generator_trace.py deleted file mode 100644 index c69efbe51..000000000 --- a/tests/agent_features/_test_async_generator_trace.py +++ /dev/null @@ -1,538 +0,0 @@ -# Copyright 2010 New Relic, Inc. -# -# 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. - -import functools -import sys -import time - -import pytest -from testing_support.fixtures import capture_transaction_metrics, validate_tt_parenting -from testing_support.validators.validate_transaction_errors import validate_transaction_errors -from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics - -from newrelic.api.background_task import background_task -from newrelic.api.database_trace import database_trace -from newrelic.api.datastore_trace import datastore_trace -from newrelic.api.external_trace import external_trace -from newrelic.api.function_trace import function_trace -from newrelic.api.graphql_trace import graphql_operation_trace, graphql_resolver_trace -from newrelic.api.memcache_trace import memcache_trace -from newrelic.api.message_trace import message_trace - -asyncio = pytest.importorskip("asyncio") - - -@pytest.mark.parametrize( - "trace,metric", - [ - (functools.partial(function_trace, name="simple_gen"), "Function/simple_gen"), - (functools.partial(external_trace, library="lib", url="http://foo.com"), "External/foo.com/lib/"), - (functools.partial(database_trace, "select * from foo"), "Datastore/statement/None/foo/select"), - (functools.partial(datastore_trace, "lib", "foo", "bar"), "Datastore/statement/lib/foo/bar"), - (functools.partial(message_trace, "lib", "op", "typ", "name"), "MessageBroker/lib/typ/op/Named/name"), - (functools.partial(memcache_trace, "cmd"), "Memcache/cmd"), - (functools.partial(graphql_operation_trace), "GraphQL/operation/GraphQL///"), - (functools.partial(graphql_resolver_trace), "GraphQL/resolve/GraphQL/"), - ], -) -def test_async_generator_timing(event_loop, trace, metric): - @trace() - async def simple_gen(): - time.sleep(0.1) - yield - time.sleep(0.1) - - metrics = [] - full_metrics = {} - - @capture_transaction_metrics(metrics, full_metrics) - @validate_transaction_metrics( - "test_async_generator_timing", background_task=True, scoped_metrics=[(metric, 1)], rollup_metrics=[(metric, 1)] - ) - @background_task(name="test_async_generator_timing") - def _test_async_generator_timing(): - async def _test(): - async for _ in simple_gen(): - pass - - event_loop.run_until_complete(_test()) - - _test_async_generator_timing() - - # Check that coroutines time the total call time (including pauses) - metric_key = (metric, "") - assert full_metrics[metric_key].total_call_time >= 0.2 - - -class MyException(Exception): - pass - - -@validate_transaction_metrics( - "test_async_generator_error", - background_task=True, - scoped_metrics=[("Function/agen", 1)], - rollup_metrics=[("Function/agen", 1)], -) -@validate_transaction_errors(errors=["_test_async_generator_trace:MyException"]) -def test_async_generator_error(event_loop): - @function_trace(name="agen") - async def agen(): - yield - - @background_task(name="test_async_generator_error") - async def _test(): - gen = agen() - await gen.asend(None) - await gen.athrow(MyException) - - with pytest.raises(MyException): - event_loop.run_until_complete(_test()) - - -@validate_transaction_metrics( - "test_async_generator_caught_exception", - background_task=True, - scoped_metrics=[("Function/agen", 1)], - rollup_metrics=[("Function/agen", 1)], -) -@validate_transaction_errors(errors=[]) -def test_async_generator_caught_exception(event_loop): - @function_trace(name="agen") - async def agen(): - for _ in range(2): - time.sleep(0.1) - try: - yield - except ValueError: - pass - - metrics = [] - full_metrics = {} - - @capture_transaction_metrics(metrics, full_metrics) - @background_task(name="test_async_generator_caught_exception") - def _test_async_generator_caught_exception(): - async def _test(): - gen = agen() - # kickstart the generator (the try/except logic is inside the - # generator) - await gen.asend(None) - await gen.athrow(ValueError) - - # consume the generator - async for _ in gen: - pass - - # The ValueError should not be reraised - event_loop.run_until_complete(_test()) - - _test_async_generator_caught_exception() - - assert full_metrics[("Function/agen", "")].total_call_time >= 0.2 - - -@validate_transaction_metrics( - "test_async_generator_handles_terminal_nodes", - background_task=True, - scoped_metrics=[("Function/parent", 1), ("Function/agen", None)], - rollup_metrics=[("Function/parent", 1), ("Function/agen", None)], -) -def test_async_generator_handles_terminal_nodes(event_loop): - # sometimes coroutines can be called underneath terminal nodes - # In this case, the trace shouldn't actually be created and we also - # shouldn't get any errors - - @function_trace(name="agen") - async def agen(): - yield - time.sleep(0.1) - - @function_trace(name="parent", terminal=True) - async def parent(): - # parent calls child - async for _ in agen(): - pass - - metrics = [] - full_metrics = {} - - @capture_transaction_metrics(metrics, full_metrics) - @background_task(name="test_async_generator_handles_terminal_nodes") - def _test_async_generator_handles_terminal_nodes(): - async def _test(): - await parent() - - event_loop.run_until_complete(_test()) - - _test_async_generator_handles_terminal_nodes() - - metric_key = ("Function/parent", "") - assert full_metrics[metric_key].total_exclusive_call_time >= 0.1 - - -@validate_transaction_metrics( - "test_async_generator_close_ends_trace", - background_task=True, - scoped_metrics=[("Function/agen", 1)], - rollup_metrics=[("Function/agen", 1)], -) -def test_async_generator_close_ends_trace(event_loop): - @function_trace(name="agen") - async def agen(): - yield - - @background_task(name="test_async_generator_close_ends_trace") - async def _test(): - gen = agen() - - # kickstart the coroutine - await gen.asend(None) - - # trace should be ended/recorded by close - await gen.aclose() - - # We may call gen.close as many times as we want - await gen.aclose() - - event_loop.run_until_complete(_test()) - - -@validate_tt_parenting(("TransactionNode", [("FunctionNode", [("FunctionNode", [])])])) -@validate_transaction_metrics( - "test_async_generator_parents", - background_task=True, - scoped_metrics=[("Function/child", 1), ("Function/parent", 1)], - rollup_metrics=[("Function/child", 1), ("Function/parent", 1)], -) -def test_async_generator_parents(event_loop): - @function_trace(name="child") - async def child(): - yield - time.sleep(0.1) - yield - - @function_trace(name="parent") - async def parent(): - time.sleep(0.1) - yield - async for _ in child(): - pass - - metrics = [] - full_metrics = {} - - @capture_transaction_metrics(metrics, full_metrics) - @background_task(name="test_async_generator_parents") - def _test_async_generator_parents(): - async def _test(): - async for _ in parent(): - pass - - event_loop.run_until_complete(_test()) - - _test_async_generator_parents() - - # Check that the child time is subtracted from the parent time (parenting - # relationship is correctly established) - key = ("Function/parent", "") - assert full_metrics[key].total_exclusive_call_time < 0.2 - - -@validate_transaction_metrics( - "test_asend_receives_a_value", - background_task=True, - scoped_metrics=[("Function/agen", 1)], - rollup_metrics=[("Function/agen", 1)], -) -def test_asend_receives_a_value(event_loop): - _received = [] - - @function_trace(name="agen") - async def agen(): - value = yield - _received.append(value) - yield value - - @background_task(name="test_asend_receives_a_value") - async def _test(): - gen = agen() - - # kickstart the coroutine - await gen.asend(None) - - assert await gen.asend("foobar") == "foobar" - assert _received and _received[0] == "foobar" - - # finish consumption of the coroutine if necessary - async for _ in gen: - pass - - event_loop.run_until_complete(_test()) - - -@validate_transaction_metrics( - "test_athrow_yields_a_value", - background_task=True, - scoped_metrics=[("Function/agen", 1)], - rollup_metrics=[("Function/agen", 1)], -) -def test_athrow_yields_a_value(event_loop): - @function_trace(name="agen") - async def agen(): - for _ in range(2): - try: - yield - except MyException: - yield "foobar" - - @background_task(name="test_athrow_yields_a_value") - async def _test(): - gen = agen() - - # kickstart the coroutine - await gen.asend(None) - - assert await gen.athrow(MyException) == "foobar" - - # finish consumption of the coroutine if necessary - async for _ in gen: - pass - - event_loop.run_until_complete(_test()) - - -@validate_transaction_metrics( - "test_multiple_throws_yield_a_value", - background_task=True, - scoped_metrics=[("Function/agen", 1)], - rollup_metrics=[("Function/agen", 1)], -) -def test_multiple_throws_yield_a_value(event_loop): - @function_trace(name="agen") - async def agen(): - value = None - for _ in range(4): - try: - yield value - value = "bar" - except MyException: - value = "foo" - - @background_task(name="test_multiple_throws_yield_a_value") - async def _test(): - gen = agen() - - # kickstart the coroutine - assert await gen.asend(None) is None - assert await gen.athrow(MyException) == "foo" - assert await gen.athrow(MyException) == "foo" - assert await gen.asend(None) == "bar" - - # finish consumption of the coroutine if necessary - async for _ in gen: - pass - - event_loop.run_until_complete(_test()) - - -@validate_transaction_metrics( - "test_athrow_does_not_yield_a_value", - background_task=True, - scoped_metrics=[("Function/agen", 1)], - rollup_metrics=[("Function/agen", 1)], -) -def test_athrow_does_not_yield_a_value(event_loop): - @function_trace(name="agen") - async def agen(): - for _ in range(2): - try: - yield - except MyException: - return - - @background_task(name="test_athrow_does_not_yield_a_value") - async def _test(): - gen = agen() - - # kickstart the coroutine - await gen.asend(None) - - # async generator will raise StopAsyncIteration - with pytest.raises(StopAsyncIteration): - await gen.athrow(MyException) - - event_loop.run_until_complete(_test()) - - -@pytest.mark.parametrize( - "trace", - [ - function_trace(name="simple_gen"), - external_trace(library="lib", url="http://foo.com"), - database_trace("select * from foo"), - datastore_trace("lib", "foo", "bar"), - message_trace("lib", "op", "typ", "name"), - memcache_trace("cmd"), - ], -) -def test_async_generator_functions_outside_of_transaction(event_loop, trace): - @trace - async def agen(): - for _ in range(2): - yield "foo" - - async def _test(): - assert [_ async for _ in agen()] == ["foo", "foo"] - - event_loop.run_until_complete(_test()) - - -@validate_transaction_metrics( - "test_catching_generator_exit_causes_runtime_error", - background_task=True, - scoped_metrics=[("Function/agen", 1)], - rollup_metrics=[("Function/agen", 1)], -) -def test_catching_generator_exit_causes_runtime_error(event_loop): - @function_trace(name="agen") - async def agen(): - try: - yield - except GeneratorExit: - yield - - @background_task(name="test_catching_generator_exit_causes_runtime_error") - async def _test(): - gen = agen() - - # kickstart the coroutine (we're inside the try now) - await gen.asend(None) - - # Generators cannot catch generator exit exceptions (which are injected by - # close). This will result in a runtime error. - with pytest.raises(RuntimeError): - await gen.aclose() - - event_loop.run_until_complete(_test()) - - -@validate_transaction_metrics( - "test_async_generator_time_excludes_creation_time", - background_task=True, - scoped_metrics=[("Function/agen", 1)], - rollup_metrics=[("Function/agen", 1)], -) -def test_async_generator_time_excludes_creation_time(event_loop): - @function_trace(name="agen") - async def agen(): - yield - - metrics = [] - full_metrics = {} - - @capture_transaction_metrics(metrics, full_metrics) - @background_task(name="test_async_generator_time_excludes_creation_time") - def _test_async_generator_time_excludes_creation_time(): - async def _test(): - gen = agen() - time.sleep(0.1) - async for _ in gen: - pass - - event_loop.run_until_complete(_test()) - - _test_async_generator_time_excludes_creation_time() - - # check that the trace does not include the time between creation and - # consumption - assert full_metrics[("Function/agen", "")].total_call_time < 0.1 - - -@validate_transaction_metrics( - "test_complete_async_generator", - background_task=True, - scoped_metrics=[("Function/agen", 1)], - rollup_metrics=[("Function/agen", 1)], -) -@background_task(name="test_complete_async_generator") -def test_complete_async_generator(event_loop): - @function_trace(name="agen") - async def agen(): - for i in range(5): - yield i - - async def _test(): - gen = agen() - assert [x async for x in gen] == list(range(5)) - - event_loop.run_until_complete(_test()) - - -@pytest.mark.parametrize("nr_transaction", [True, False]) -def test_incomplete_async_generator(event_loop, nr_transaction): - @function_trace(name="agen") - async def agen(): - for _ in range(5): - yield - - def _test_incomplete_async_generator(): - async def _test(): - c = agen() - - async for _ in c: - break - - if nr_transaction: - _test = background_task(name="test_incomplete_async_generator")(_test) - - event_loop.run_until_complete(_test()) - - if nr_transaction: - _test_incomplete_async_generator = validate_transaction_metrics( - "test_incomplete_async_generator", - background_task=True, - scoped_metrics=[("Function/agen", 1)], - rollup_metrics=[("Function/agen", 1)], - )(_test_incomplete_async_generator) - - _test_incomplete_async_generator() - - -def test_incomplete_async_generator_transaction_exited(event_loop): - @function_trace(name="agen") - async def agen(): - for _ in range(5): - yield - - @validate_transaction_metrics( - "test_incomplete_async_generator", - background_task=True, - scoped_metrics=[("Function/agen", 1)], - rollup_metrics=[("Function/agen", 1)], - ) - def _test_incomplete_async_generator(): - c = agen() - - @background_task(name="test_incomplete_async_generator") - async def _test(): - async for _ in c: - break - - event_loop.run_until_complete(_test()) - - # Remove generator after transaction completes - del c - - _test_incomplete_async_generator() diff --git a/tests/agent_features/test_agent_control_health_check.py b/tests/agent_features/test_agent_control_health_check.py index ffd88a6a3..473125a6f 100644 --- a/tests/agent_features/test_agent_control_health_check.py +++ b/tests/agent_features/test_agent_control_health_check.py @@ -56,7 +56,7 @@ def test_agent_control_not_enabled(monkeypatch, tmp_path): def test_write_to_file_healthy_status(monkeypatch, tmp_path): # Setup expected env vars to run agent control health check - monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", True) + monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", "True") file_path = tmp_path.as_uri() monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_HEALTH_DELIVERY_LOCATION", file_path) @@ -76,7 +76,7 @@ def test_write_to_file_healthy_status(monkeypatch, tmp_path): def test_write_to_file_unhealthy_status(monkeypatch, tmp_path): # Setup expected env vars to run agent control health check - monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", True) + monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", "True") file_path = tmp_path.as_uri() monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_HEALTH_DELIVERY_LOCATION", file_path) @@ -100,7 +100,7 @@ def test_write_to_file_unhealthy_status(monkeypatch, tmp_path): def test_no_override_on_unhealthy_shutdown(monkeypatch, tmp_path): # Setup expected env vars to run agent control health check - monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", True) + monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", "True") file_path = tmp_path.as_uri() monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_HEALTH_DELIVERY_LOCATION", file_path) @@ -127,7 +127,7 @@ def test_health_check_running_threads(monkeypatch, tmp_path): # Only the main thread should be running since not agent control env vars are set assert len(running_threads) == 1 - monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", True) + monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", "True") file_path = tmp_path.as_uri() monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_HEALTH_DELIVERY_LOCATION", file_path) @@ -144,7 +144,7 @@ def test_health_check_running_threads(monkeypatch, tmp_path): def test_proxy_error_status(monkeypatch, tmp_path): # Setup expected env vars to run agent control health check - monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", True) + monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", "True") file_path = tmp_path.as_uri() monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_HEALTH_DELIVERY_LOCATION", file_path) @@ -173,7 +173,7 @@ def test_proxy_error_status(monkeypatch, tmp_path): def test_multiple_activations_running_threads(monkeypatch, tmp_path): # Setup expected env vars to run agent control health check - monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", True) + monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", "True") file_path = tmp_path.as_uri() monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_HEALTH_DELIVERY_LOCATION", file_path) @@ -198,7 +198,7 @@ def test_multiple_activations_running_threads(monkeypatch, tmp_path): def test_update_to_healthy(monkeypatch, tmp_path): # Setup expected env vars to run agent control health check - monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", True) + monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", "True") file_path = tmp_path.as_uri() monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_HEALTH_DELIVERY_LOCATION", file_path) @@ -226,7 +226,7 @@ def test_update_to_healthy(monkeypatch, tmp_path): def test_max_app_name_status(monkeypatch, tmp_path): # Setup expected env vars to run agent control health check - monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", True) + monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", "True") file_path = tmp_path.as_uri() monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_HEALTH_DELIVERY_LOCATION", file_path) diff --git a/tests/agent_features/test_async_generator_trace.py b/tests/agent_features/test_async_generator_trace.py index f4fe78e2d..be1a55646 100644 --- a/tests/agent_features/test_async_generator_trace.py +++ b/tests/agent_features/test_async_generator_trace.py @@ -12,8 +12,527 @@ # See the License for the specific language governing permissions and # limitations under the License. +import functools import sys +import time -# Async Generators were introduced in Python 3.6, but some APIs weren't completely stable until Python 3.7. -if sys.version_info >= (3, 7): - from _test_async_generator_trace import * +import pytest +from testing_support.fixtures import capture_transaction_metrics, validate_tt_parenting +from testing_support.validators.validate_transaction_errors import validate_transaction_errors +from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics + +from newrelic.api.background_task import background_task +from newrelic.api.database_trace import database_trace +from newrelic.api.datastore_trace import datastore_trace +from newrelic.api.external_trace import external_trace +from newrelic.api.function_trace import function_trace +from newrelic.api.graphql_trace import graphql_operation_trace, graphql_resolver_trace +from newrelic.api.memcache_trace import memcache_trace +from newrelic.api.message_trace import message_trace + +asyncio = pytest.importorskip("asyncio") + + +@pytest.mark.parametrize( + "trace,metric", + [ + (functools.partial(function_trace, name="simple_gen"), "Function/simple_gen"), + (functools.partial(external_trace, library="lib", url="http://foo.com"), "External/foo.com/lib/"), + (functools.partial(database_trace, "select * from foo"), "Datastore/statement/None/foo/select"), + (functools.partial(datastore_trace, "lib", "foo", "bar"), "Datastore/statement/lib/foo/bar"), + (functools.partial(message_trace, "lib", "op", "typ", "name"), "MessageBroker/lib/typ/op/Named/name"), + (functools.partial(memcache_trace, "cmd"), "Memcache/cmd"), + (functools.partial(graphql_operation_trace), "GraphQL/operation/GraphQL///"), + (functools.partial(graphql_resolver_trace), "GraphQL/resolve/GraphQL/"), + ], +) +def test_async_generator_timing(event_loop, trace, metric): + @trace() + async def simple_gen(): + time.sleep(0.1) + yield + time.sleep(0.1) + + metrics = [] + full_metrics = {} + + @capture_transaction_metrics(metrics, full_metrics) + @validate_transaction_metrics( + "test_async_generator_timing", background_task=True, scoped_metrics=[(metric, 1)], rollup_metrics=[(metric, 1)] + ) + @background_task(name="test_async_generator_timing") + def _test_async_generator_timing(): + async def _test(): + async for _ in simple_gen(): + pass + + event_loop.run_until_complete(_test()) + + _test_async_generator_timing() + + # Check that coroutines time the total call time (including pauses) + metric_key = (metric, "") + assert full_metrics[metric_key].total_call_time >= 0.2 + + +class MyException(Exception): + pass + + +@validate_transaction_metrics( + "test_async_generator_error", + background_task=True, + scoped_metrics=[("Function/agen", 1)], + rollup_metrics=[("Function/agen", 1)], +) +@validate_transaction_errors(errors=["test_async_generator_trace:MyException"]) +def test_async_generator_error(event_loop): + @function_trace(name="agen") + async def agen(): + yield + + @background_task(name="test_async_generator_error") + async def _test(): + gen = agen() + await gen.asend(None) + await gen.athrow(MyException) + + with pytest.raises(MyException): + event_loop.run_until_complete(_test()) + + +@validate_transaction_metrics( + "test_async_generator_caught_exception", + background_task=True, + scoped_metrics=[("Function/agen", 1)], + rollup_metrics=[("Function/agen", 1)], +) +@validate_transaction_errors(errors=[]) +def test_async_generator_caught_exception(event_loop): + @function_trace(name="agen") + async def agen(): + for _ in range(2): + time.sleep(0.1) + try: + yield + except ValueError: + pass + + metrics = [] + full_metrics = {} + + @capture_transaction_metrics(metrics, full_metrics) + @background_task(name="test_async_generator_caught_exception") + def _test_async_generator_caught_exception(): + async def _test(): + gen = agen() + # kickstart the generator (the try/except logic is inside the + # generator) + await gen.asend(None) + await gen.athrow(ValueError) + + # consume the generator + async for _ in gen: + pass + + # The ValueError should not be reraised + event_loop.run_until_complete(_test()) + + _test_async_generator_caught_exception() + + assert full_metrics[("Function/agen", "")].total_call_time >= 0.2 + + +@validate_transaction_metrics( + "test_async_generator_handles_terminal_nodes", + background_task=True, + scoped_metrics=[("Function/parent", 1), ("Function/agen", None)], + rollup_metrics=[("Function/parent", 1), ("Function/agen", None)], +) +def test_async_generator_handles_terminal_nodes(event_loop): + # sometimes coroutines can be called underneath terminal nodes + # In this case, the trace shouldn't actually be created and we also + # shouldn't get any errors + + @function_trace(name="agen") + async def agen(): + yield + time.sleep(0.1) + + @function_trace(name="parent", terminal=True) + async def parent(): + # parent calls child + async for _ in agen(): + pass + + metrics = [] + full_metrics = {} + + @capture_transaction_metrics(metrics, full_metrics) + @background_task(name="test_async_generator_handles_terminal_nodes") + def _test_async_generator_handles_terminal_nodes(): + async def _test(): + await parent() + + event_loop.run_until_complete(_test()) + + _test_async_generator_handles_terminal_nodes() + + metric_key = ("Function/parent", "") + assert full_metrics[metric_key].total_exclusive_call_time >= 0.1 + + +@validate_transaction_metrics( + "test_async_generator_close_ends_trace", + background_task=True, + scoped_metrics=[("Function/agen", 1)], + rollup_metrics=[("Function/agen", 1)], +) +def test_async_generator_close_ends_trace(event_loop): + @function_trace(name="agen") + async def agen(): + yield + + @background_task(name="test_async_generator_close_ends_trace") + async def _test(): + gen = agen() + + # kickstart the coroutine + await gen.asend(None) + + # trace should be ended/recorded by close + await gen.aclose() + + # We may call gen.close as many times as we want + await gen.aclose() + + event_loop.run_until_complete(_test()) + + +@validate_tt_parenting(("TransactionNode", [("FunctionNode", [("FunctionNode", [])])])) +@validate_transaction_metrics( + "test_async_generator_parents", + background_task=True, + scoped_metrics=[("Function/child", 1), ("Function/parent", 1)], + rollup_metrics=[("Function/child", 1), ("Function/parent", 1)], +) +def test_async_generator_parents(event_loop): + @function_trace(name="child") + async def child(): + yield + time.sleep(0.1) + yield + + @function_trace(name="parent") + async def parent(): + time.sleep(0.1) + yield + async for _ in child(): + pass + + metrics = [] + full_metrics = {} + + @capture_transaction_metrics(metrics, full_metrics) + @background_task(name="test_async_generator_parents") + def _test_async_generator_parents(): + async def _test(): + async for _ in parent(): + pass + + event_loop.run_until_complete(_test()) + + _test_async_generator_parents() + + # Check that the child time is subtracted from the parent time (parenting + # relationship is correctly established) + key = ("Function/parent", "") + assert full_metrics[key].total_exclusive_call_time < 0.2 + + +@validate_transaction_metrics( + "test_asend_receives_a_value", + background_task=True, + scoped_metrics=[("Function/agen", 1)], + rollup_metrics=[("Function/agen", 1)], +) +def test_asend_receives_a_value(event_loop): + _received = [] + + @function_trace(name="agen") + async def agen(): + value = yield + _received.append(value) + yield value + + @background_task(name="test_asend_receives_a_value") + async def _test(): + gen = agen() + + # kickstart the coroutine + await gen.asend(None) + + assert await gen.asend("foobar") == "foobar" + assert _received and _received[0] == "foobar" + + # finish consumption of the coroutine if necessary + async for _ in gen: + pass + + event_loop.run_until_complete(_test()) + + +@validate_transaction_metrics( + "test_athrow_yields_a_value", + background_task=True, + scoped_metrics=[("Function/agen", 1)], + rollup_metrics=[("Function/agen", 1)], +) +def test_athrow_yields_a_value(event_loop): + @function_trace(name="agen") + async def agen(): + for _ in range(2): + try: + yield + except MyException: + yield "foobar" + + @background_task(name="test_athrow_yields_a_value") + async def _test(): + gen = agen() + + # kickstart the coroutine + await gen.asend(None) + + assert await gen.athrow(MyException) == "foobar" + + # finish consumption of the coroutine if necessary + async for _ in gen: + pass + + event_loop.run_until_complete(_test()) + + +@validate_transaction_metrics( + "test_multiple_throws_yield_a_value", + background_task=True, + scoped_metrics=[("Function/agen", 1)], + rollup_metrics=[("Function/agen", 1)], +) +def test_multiple_throws_yield_a_value(event_loop): + @function_trace(name="agen") + async def agen(): + value = None + for _ in range(4): + try: + yield value + value = "bar" + except MyException: + value = "foo" + + @background_task(name="test_multiple_throws_yield_a_value") + async def _test(): + gen = agen() + + # kickstart the coroutine + assert await gen.asend(None) is None + assert await gen.athrow(MyException) == "foo" + assert await gen.athrow(MyException) == "foo" + assert await gen.asend(None) == "bar" + + # finish consumption of the coroutine if necessary + async for _ in gen: + pass + + event_loop.run_until_complete(_test()) + + +@validate_transaction_metrics( + "test_athrow_does_not_yield_a_value", + background_task=True, + scoped_metrics=[("Function/agen", 1)], + rollup_metrics=[("Function/agen", 1)], +) +def test_athrow_does_not_yield_a_value(event_loop): + @function_trace(name="agen") + async def agen(): + for _ in range(2): + try: + yield + except MyException: + return + + @background_task(name="test_athrow_does_not_yield_a_value") + async def _test(): + gen = agen() + + # kickstart the coroutine + await gen.asend(None) + + # async generator will raise StopAsyncIteration + with pytest.raises(StopAsyncIteration): + await gen.athrow(MyException) + + event_loop.run_until_complete(_test()) + + +@pytest.mark.parametrize( + "trace", + [ + function_trace(name="simple_gen"), + external_trace(library="lib", url="http://foo.com"), + database_trace("select * from foo"), + datastore_trace("lib", "foo", "bar"), + message_trace("lib", "op", "typ", "name"), + memcache_trace("cmd"), + ], +) +def test_async_generator_functions_outside_of_transaction(event_loop, trace): + @trace + async def agen(): + for _ in range(2): + yield "foo" + + async def _test(): + assert [_ async for _ in agen()] == ["foo", "foo"] + + event_loop.run_until_complete(_test()) + + +@validate_transaction_metrics( + "test_catching_generator_exit_causes_runtime_error", + background_task=True, + scoped_metrics=[("Function/agen", 1)], + rollup_metrics=[("Function/agen", 1)], +) +def test_catching_generator_exit_causes_runtime_error(event_loop): + @function_trace(name="agen") + async def agen(): + try: + yield + except GeneratorExit: + yield + + @background_task(name="test_catching_generator_exit_causes_runtime_error") + async def _test(): + gen = agen() + + # kickstart the coroutine (we're inside the try now) + await gen.asend(None) + + # Generators cannot catch generator exit exceptions (which are injected by + # close). This will result in a runtime error. + with pytest.raises(RuntimeError): + await gen.aclose() + + event_loop.run_until_complete(_test()) + + +@validate_transaction_metrics( + "test_async_generator_time_excludes_creation_time", + background_task=True, + scoped_metrics=[("Function/agen", 1)], + rollup_metrics=[("Function/agen", 1)], +) +def test_async_generator_time_excludes_creation_time(event_loop): + @function_trace(name="agen") + async def agen(): + yield + + metrics = [] + full_metrics = {} + + @capture_transaction_metrics(metrics, full_metrics) + @background_task(name="test_async_generator_time_excludes_creation_time") + def _test_async_generator_time_excludes_creation_time(): + async def _test(): + gen = agen() + time.sleep(0.1) + async for _ in gen: + pass + + event_loop.run_until_complete(_test()) + + _test_async_generator_time_excludes_creation_time() + + # check that the trace does not include the time between creation and + # consumption + assert full_metrics[("Function/agen", "")].total_call_time < 0.1 + + +@validate_transaction_metrics( + "test_complete_async_generator", + background_task=True, + scoped_metrics=[("Function/agen", 1)], + rollup_metrics=[("Function/agen", 1)], +) +@background_task(name="test_complete_async_generator") +def test_complete_async_generator(event_loop): + @function_trace(name="agen") + async def agen(): + for i in range(5): + yield i + + async def _test(): + gen = agen() + assert [x async for x in gen] == list(range(5)) + + event_loop.run_until_complete(_test()) + + +@pytest.mark.parametrize("nr_transaction", [True, False]) +def test_incomplete_async_generator(event_loop, nr_transaction): + @function_trace(name="agen") + async def agen(): + for _ in range(5): + yield + + def _test_incomplete_async_generator(): + async def _test(): + c = agen() + + async for _ in c: + break + + if nr_transaction: + _test = background_task(name="test_incomplete_async_generator")(_test) + + event_loop.run_until_complete(_test()) + + if nr_transaction: + _test_incomplete_async_generator = validate_transaction_metrics( + "test_incomplete_async_generator", + background_task=True, + scoped_metrics=[("Function/agen", 1)], + rollup_metrics=[("Function/agen", 1)], + )(_test_incomplete_async_generator) + + _test_incomplete_async_generator() + + +def test_incomplete_async_generator_transaction_exited(event_loop): + @function_trace(name="agen") + async def agen(): + for _ in range(5): + yield + + @validate_transaction_metrics( + "test_incomplete_async_generator", + background_task=True, + scoped_metrics=[("Function/agen", 1)], + rollup_metrics=[("Function/agen", 1)], + ) + def _test_incomplete_async_generator(): + c = agen() + + @background_task(name="test_incomplete_async_generator") + async def _test(): + async for _ in c: # noqa: F821 + break + + event_loop.run_until_complete(_test()) + + # Remove generator after transaction completes + del c + + _test_incomplete_async_generator() diff --git a/tests/agent_features/test_coroutine_trace.py b/tests/agent_features/test_coroutine_trace.py index 596a58eaa..dc1058c5a 100644 --- a/tests/agent_features/test_coroutine_trace.py +++ b/tests/agent_features/test_coroutine_trace.py @@ -489,4 +489,4 @@ def _test(): if sys.version_info >= (3, 5): - from _test_async_coroutine_trace import * + from _test_async_coroutine_trace import * # noqa: F403 diff --git a/tests/agent_features/test_coroutine_transaction.py b/tests/agent_features/test_coroutine_transaction.py index 58c499a25..1805f961a 100644 --- a/tests/agent_features/test_coroutine_transaction.py +++ b/tests/agent_features/test_coroutine_transaction.py @@ -51,7 +51,7 @@ async def task(): try: if does_hang: - await loop.create_future() + await event_loop.create_future() else: await asyncio.sleep(0.0) if nr_enabled and txn.enabled: diff --git a/tests/agent_unittests/test_agent_connect.py b/tests/agent_unittests/test_agent_connect.py index a32a839a9..a783faddc 100644 --- a/tests/agent_unittests/test_agent_connect.py +++ b/tests/agent_unittests/test_agent_connect.py @@ -80,7 +80,7 @@ def test_ml_streaming_disabled_supportability_metrics(): @validate_internal_metrics([("Supportability/AgentControl/Health/enabled", 1)]) def test_agent_control_health_supportability_metric(monkeypatch, tmp_path): # Setup expected env vars to run agent control health check - monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", True) + monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", "True") file_path = tmp_path.as_uri() monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_HEALTH_DELIVERY_LOCATION", file_path) diff --git a/tests/component_tastypie/api.py b/tests/component_tastypie/api.py index 6ebe72e02..a806343d9 100644 --- a/tests/component_tastypie/api.py +++ b/tests/component_tastypie/api.py @@ -30,4 +30,4 @@ def obj_get(self, *args, **kwargs): elif pk == "ZeroDivisionError": 1 / 0 # noqa: B018 else: - raise NotImplemented + raise NotImplementedError diff --git a/tests/external_aiobotocore/test_bedrock_chat_completion.py b/tests/external_aiobotocore/test_bedrock_chat_completion.py index 735a020c8..53ac292f4 100644 --- a/tests/external_aiobotocore/test_bedrock_chat_completion.py +++ b/tests/external_aiobotocore/test_bedrock_chat_completion.py @@ -854,7 +854,7 @@ def test_bedrock_chat_completion_functions_marked_as_wrapped_for_sdk_compatibili assert bedrock_server._nr_wrapped -def test_chat_models_instrumented(): +def test_chat_models_instrumented(loop): import aiobotocore SUPPORTED_MODELS = [model for model, _, _, _ in MODEL_EXTRACTORS if "embed" not in model] diff --git a/tests/external_aiobotocore/test_bedrock_embeddings.py b/tests/external_aiobotocore/test_bedrock_embeddings.py index 84ff1f3fa..016098637 100644 --- a/tests/external_aiobotocore/test_bedrock_embeddings.py +++ b/tests/external_aiobotocore/test_bedrock_embeddings.py @@ -411,7 +411,7 @@ async def _test(): loop.run_until_complete(_test()) -def test_embedding_models_instrumented(): +def test_embedding_models_instrumented(loop): import aiobotocore SUPPORTED_MODELS = [model for model, _, _, _ in MODEL_EXTRACTORS if "embed" in model] diff --git a/tests/external_botocore/test_boto3_firehose.py b/tests/external_botocore/test_boto3_firehose.py index c9ec81551..4c8d7c824 100644 --- a/tests/external_botocore/test_boto3_firehose.py +++ b/tests/external_botocore/test_boto3_firehose.py @@ -35,7 +35,7 @@ TEST_STREAM_ARN = f"arn:aws:firehose:us-east-1:123456789012:deliverystream/{TEST_STREAM}" TEST_S3_BUCKET = f"python-agent-test-{uuid.uuid4()}" TEST_S3_BUCKET_ARN = f"arn:aws:s3:::{TEST_S3_BUCKET}" -TEST_S3_ROLE_ARN = f"arn:aws:iam::123456789012:role/test-role" +TEST_S3_ROLE_ARN = "arn:aws:iam::123456789012:role/test-role" EXPECTED_AGENT_ATTRS = { "exact_agents": {"cloud.platform": "aws_kinesis_delivery_streams", "cloud.resource_id": TEST_STREAM_ARN} } @@ -49,7 +49,7 @@ (f"Firehose/put_record/{TEST_STREAM}", 1), (f"Firehose/put_record_batch/{TEST_STREAM}", 1), (f"Firehose/describe_delivery_stream/{TEST_STREAM}", 1), - (f"Firehose/list_delivery_streams", 1), + ("Firehose/list_delivery_streams", 1), (f"Firehose/delete_delivery_stream/{TEST_STREAM}", 1), (f"External/{URL}/botocore/POST", 6), ] @@ -59,7 +59,7 @@ (f"Firehose/put_record/{TEST_STREAM}", 1), (f"Firehose/put_record_batch/{TEST_STREAM}", 1), (f"Firehose/describe_delivery_stream/{TEST_STREAM}", 1), - (f"Firehose/list_delivery_streams", 1), + ("Firehose/list_delivery_streams", 1), (f"Firehose/delete_delivery_stream/{TEST_STREAM}", 1), ("External/all", 7), # Includes creating S3 bucket ("External/allOther", 7), diff --git a/tests/framework_ariadne/test_application.py b/tests/framework_ariadne/test_application.py index 0b7bf2489..da0867faa 100644 --- a/tests/framework_ariadne/test_application.py +++ b/tests/framework_ariadne/test_application.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import pytest -from framework_graphql.test_application import * +from framework_graphql.test_application import * # noqa: F403 from newrelic.common.package_version_utils import get_package_version diff --git a/tests/framework_graphene/test_application.py b/tests/framework_graphene/test_application.py index c3ed89d0b..08807aead 100644 --- a/tests/framework_graphene/test_application.py +++ b/tests/framework_graphene/test_application.py @@ -13,7 +13,7 @@ # limitations under the License. import pytest -from framework_graphql.test_application import * +from framework_graphql.test_application import * # noqa: F403 from newrelic.common.package_version_utils import get_package_version diff --git a/tests/framework_strawberry/test_application.py b/tests/framework_strawberry/test_application.py index 86377d728..403491ab2 100644 --- a/tests/framework_strawberry/test_application.py +++ b/tests/framework_strawberry/test_application.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import pytest -from framework_graphql.test_application import * +from framework_graphql.test_application import * # noqa: F403 from testing_support.fixtures import override_application_settings from testing_support.validators.validate_transaction_count import validate_transaction_count