From 1bdf3b1e683a885be7fd2468587221d907d2223f Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Thu, 24 Mar 2022 14:38:59 -0700 Subject: [PATCH 01/26] Add GraphQL Server Sanic Instrumentation --- newrelic/config.py | 6 ++ newrelic/hooks/component_graphqlserver.py | 49 ++++++++++ .../component_graphqlserver/_test_graphql.py | 38 ++++++++ tests/component_graphqlserver/conftest.py | 38 ++++++++ tests/component_graphqlserver/test_graphql.py | 90 +++++++++++++++++++ tests/framework_starlette/test_graphql.py | 2 +- tox.ini | 7 +- 7 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 newrelic/hooks/component_graphqlserver.py create mode 100644 tests/component_graphqlserver/_test_graphql.py create mode 100644 tests/component_graphqlserver/conftest.py create mode 100644 tests/component_graphqlserver/test_graphql.py diff --git a/newrelic/config.py b/newrelic/config.py index 1a90ae0a3..ca2e19657 100644 --- a/newrelic/config.py +++ b/newrelic/config.py @@ -2153,6 +2153,12 @@ def _process_module_builtin_defaults(): "instrument_flask_rest", ) + _process_module_definition( + "graphql_server", + "newrelic.hooks.component_graphqlserver", + "instrument_graphqlserver", + ) + # _process_module_definition('web.application', # 'newrelic.hooks.framework_webpy') # _process_module_definition('web.template', diff --git a/newrelic/hooks/component_graphqlserver.py b/newrelic/hooks/component_graphqlserver.py new file mode 100644 index 000000000..29004c11f --- /dev/null +++ b/newrelic/hooks/component_graphqlserver.py @@ -0,0 +1,49 @@ +from newrelic.api.asgi_application import wrap_asgi_application +from newrelic.api.error_trace import ErrorTrace +from newrelic.api.graphql_trace import GraphQLOperationTrace +from newrelic.api.transaction import current_transaction +from newrelic.api.transaction_name import TransactionNameWrapper +from newrelic.common.object_names import callable_name +from newrelic.common.object_wrapper import wrap_function_wrapper +from newrelic.core.graphql_utils import graphql_statement +from newrelic.hooks.framework_graphql import ( + framework_version as graphql_framework_version, +) +from newrelic.hooks.framework_graphql import ignore_graphql_duplicate_exception + +def framework_details(): + import graphql_server + return ("GraphQLServer", getattr(graphql_server, "__version__", None)) + +def bind_query(schema, params, *args, **kwargs): + return getattr(params, "query", None) + + +def wrap_get_response(wrapped, instance, args, kwargs): + transaction = current_transaction() + + if not transaction: + return wrapped(*args, **kwargs) + + try: + query = bind_query(*args, **kwargs) + except TypeError: + return wrapped(*args, **kwargs) + + framework = framework_details() + transaction.add_framework_info(name=framework[0], version=framework[1]) + transaction.add_framework_info(name="GraphQL", version=graphql_framework_version()) + + if hasattr(query, "body"): + query = query.body + + transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=10) + + with GraphQLOperationTrace() as trace: + trace.product = "GraphQLServer" + trace.statement = graphql_statement(query) + with ErrorTrace(ignore=ignore_graphql_duplicate_exception): + return wrapped(*args, **kwargs) + +def instrument_graphqlserver(module): + wrap_function_wrapper(module, "get_response", wrap_get_response) diff --git a/tests/component_graphqlserver/_test_graphql.py b/tests/component_graphqlserver/_test_graphql.py new file mode 100644 index 000000000..b478cf83e --- /dev/null +++ b/tests/component_graphqlserver/_test_graphql.py @@ -0,0 +1,38 @@ +# 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. + +from sanic import Sanic +from graphql_server.sanic import GraphQLView +from testing_support.asgi_testing import AsgiTest + +from graphql import GraphQLObjectType, GraphQLString, GraphQLSchema, GraphQLField + + +def resolve_hello(root, info): + return "Hello!" + +hello_field = GraphQLField(GraphQLString, resolve=resolve_hello) +query = GraphQLObjectType( + name="Query", + fields={ + "hello": hello_field, + }, +) + +app = Sanic(name="SanicGraphQL") +routes = [ + app.add_route(GraphQLView.as_view(schema=GraphQLSchema(query=query)), "/graphql"), +] + +target_application = AsgiTest(app) diff --git a/tests/component_graphqlserver/conftest.py b/tests/component_graphqlserver/conftest.py new file mode 100644 index 000000000..c2b5f7d92 --- /dev/null +++ b/tests/component_graphqlserver/conftest.py @@ -0,0 +1,38 @@ +# 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. + +from testing_support.fixtures import ( + code_coverage_fixture, + collector_agent_registration_fixture, + collector_available_fixture, +) + +_coverage_source = [ + "newrelic.hooks.component_graphqlserver", +] + +code_coverage = code_coverage_fixture(source=_coverage_source) + +_default_settings = { + "transaction_tracer.explain_threshold": 0.0, + "transaction_tracer.transaction_threshold": 0.0, + "transaction_tracer.stack_trace_threshold": 0.0, + "debug.log_data_collector_payloads": True, + "debug.record_transaction_failure": True, +} + +collector_agent_registration = collector_agent_registration_fixture( + app_name="Python Agent Test (component_graphqlserver)", + default_settings=_default_settings, +) diff --git a/tests/component_graphqlserver/test_graphql.py b/tests/component_graphqlserver/test_graphql.py new file mode 100644 index 000000000..ee3c5445b --- /dev/null +++ b/tests/component_graphqlserver/test_graphql.py @@ -0,0 +1,90 @@ +# 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 json + +import pytest +from testing_support.fixtures import dt_enabled, validate_transaction_metrics +from testing_support.validators.validate_span_events import validate_span_events +from testing_support.validators.validate_transaction_count import ( + validate_transaction_count, +) + + +@pytest.fixture(scope="session") +def target_application(): + import _test_graphql + + return _test_graphql.target_application + + +@dt_enabled +def test_graphql_metrics_and_attrs(target_application): + from graphql import __version__ as graphql_version + from graphql_server import __version__ as graphql_server_version + from sanic import __version__ as sanic_version + + FRAMEWORK_METRICS = [ + ("Python/Framework/GraphQL/%s" % graphql_version, 1), + ("Python/Framework/GraphQLServer/%s" % graphql_server_version, 1), + ("Python/Framework/Sanic/%s" % sanic_version, 1), + ] + _test_scoped_metrics = [ + ("GraphQL/resolve/GraphQLServer/hello", 1), + ("GraphQL/operation/GraphQLServer/query//hello", 1), + ("Function/graphql_server.sanic.graphqlview:GraphQLView.post", 1), + ] + _test_unscoped_metrics = [ + ("GraphQL/all", 1), + ("GraphQL/GraphQLServer/all", 1), + ("GraphQL/allWeb", 1), + ("GraphQL/GraphQLServer/allWeb", 1), + ] + _test_scoped_metrics + + _expected_query_operation_attributes = { + "graphql.operation.type": "query", + "graphql.operation.name": "", + "graphql.operation.query": "{ hello }", + } + _expected_query_resolver_attributes = { + "graphql.field.name": "hello", + "graphql.field.parentType": "Query", + "graphql.field.path": "hello", + "graphql.field.returnType": "String", + } + + @validate_span_events(exact_agents=_expected_query_operation_attributes) + @validate_span_events(exact_agents=_expected_query_resolver_attributes) + @validate_transaction_metrics( + "query//hello", + "GraphQL", + scoped_metrics=_test_scoped_metrics, + rollup_metrics=_test_unscoped_metrics + FRAMEWORK_METRICS, + ) + def _test(): + response = target_application.make_request( + "POST", "/graphql", body=json.dumps({"query": "{ hello }"}), headers={"Content-Type": "application/json"} + ) + assert response.status == 200 + assert "Hello!" in response.body.decode("utf-8") + + _test() + + +@validate_transaction_count(0) +def test_ignored_introspection_transactions(target_application): + response = target_application.make_request( + "POST", "/graphql", body=json.dumps({"query": "{ __schema { types { name } } }"}), headers={"Content-Type": "application/json"} + ) + assert response.status == 200 diff --git a/tests/framework_starlette/test_graphql.py b/tests/framework_starlette/test_graphql.py index 241371eb1..f9122d48d 100644 --- a/tests/framework_starlette/test_graphql.py +++ b/tests/framework_starlette/test_graphql.py @@ -1,4 +1,4 @@ -# Copyright 2010 New Relic, Inc. + 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. diff --git a/tox.ini b/tox.ini index 62970b6c7..81d38e173 100644 --- a/tox.ini +++ b/tox.ini @@ -60,6 +60,7 @@ envlist = python-component_djangorestframework-py27-djangorestframework0300, python-component_djangorestframework-{py36,py37,py38,py39,py310}-djangorestframeworklatest, python-component_flask_rest-{py27,py36,py37,py38,py39,pypy,pypy3}, + python-component_graphqlserver-{py36,py37,py38,py39,py310}, python-component_tastypie-{py27,pypy}-tastypie0143, python-component_tastypie-{py36,py37,py38,py39,pypy3}-tastypie{0143,latest}, python-coroutines_asyncio-{py36,py37,py38,py39,py310,pypy3}, @@ -147,7 +148,7 @@ envlist = usefixtures = collector_available_fixture collector_agent_registration - code_coverage + ; code_coverage [testenv] deps = @@ -182,6 +183,9 @@ deps = component_flask_rest: flask-restful component_flask_rest: flask-restplus component_flask_rest: flask-restx + component_graphqlserver: graphql-server[sanic]==3.0.0b5 + component_graphqlserver: sanic>20 + component_graphqlserver: jinja2 component_tastypie-tastypie0143: django-tastypie<0.14.4 component_tastypie-{py27,pypy}-tastypie0143: django<1.12 component_tastypie-{py36,py37,py38,py39,pypy3}-tastypie0143: django<3.0.1 @@ -359,6 +363,7 @@ changedir = application_gearman: tests/application_gearman component_djangorestframework: tests/component_djangorestframework component_flask_rest: tests/component_flask_rest + component_graphqlserver: tests/component_graphqlserver component_tastypie: tests/component_tastypie coroutines_asyncio: tests/coroutines_asyncio cross_agent: tests/cross_agent From 9e526d10560e6f4bb61787dba46536bed9e6fcaf Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Thu, 24 Mar 2022 14:56:09 -0700 Subject: [PATCH 02/26] Co-authored-by: Timothy Pansino Co-authored-by: Uma Annamalai From 9f8856b212136704a983bd8678c0988e96208269 Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Thu, 24 Mar 2022 15:20:30 -0700 Subject: [PATCH 03/26] Add co-authors Co-authored-by: Timothy Pansino Co-authored-by: Uma Annamalai From 375bda5cca1b6fbae1f2b5451403f40f7db70055 Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Thu, 24 Mar 2022 15:26:07 -0700 Subject: [PATCH 04/26] Comment out Copyright notice message Co-authored-by: Timothy Pansino Co-authored-by: Uma Annamalai --- tests/framework_starlette/test_graphql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/framework_starlette/test_graphql.py b/tests/framework_starlette/test_graphql.py index f9122d48d..241371eb1 100644 --- a/tests/framework_starlette/test_graphql.py +++ b/tests/framework_starlette/test_graphql.py @@ -1,4 +1,4 @@ - Copyright 2010 New Relic, Inc. +# 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. From 07ec74f7140089a4dbe37a09116bbd0f4c3dca32 Mon Sep 17 00:00:00 2001 From: Tim Pansino Date: Mon, 28 Mar 2022 11:50:31 -0700 Subject: [PATCH 05/26] Finalize Sanic testing --- .../component_graphqlserver/_test_graphql.py | 24 +- tests/component_graphqlserver/test_graphql.py | 390 +++++++++++++++++- 2 files changed, 374 insertions(+), 40 deletions(-) diff --git a/tests/component_graphqlserver/_test_graphql.py b/tests/component_graphqlserver/_test_graphql.py index b478cf83e..c98d587c1 100644 --- a/tests/component_graphqlserver/_test_graphql.py +++ b/tests/component_graphqlserver/_test_graphql.py @@ -13,26 +13,14 @@ # limitations under the License. from sanic import Sanic -from graphql_server.sanic import GraphQLView -from testing_support.asgi_testing import AsgiTest - -from graphql import GraphQLObjectType, GraphQLString, GraphQLSchema, GraphQLField - -def resolve_hello(root, info): - return "Hello!" +from testing_support.asgi_testing import AsgiTest +from framework_graphql._target_application import _target_application as schema +from graphql_server.sanic import GraphQLView -hello_field = GraphQLField(GraphQLString, resolve=resolve_hello) -query = GraphQLObjectType( - name="Query", - fields={ - "hello": hello_field, - }, -) -app = Sanic(name="SanicGraphQL") +sanic_app = Sanic(name="SanicGraphQL") routes = [ - app.add_route(GraphQLView.as_view(schema=GraphQLSchema(query=query)), "/graphql"), + sanic_app.add_route(GraphQLView.as_view(schema=schema), "/graphql"), ] - -target_application = AsgiTest(app) +target_application = AsgiTest(sanic_app) diff --git a/tests/component_graphqlserver/test_graphql.py b/tests/component_graphqlserver/test_graphql.py index ee3c5445b..a10f1ab18 100644 --- a/tests/component_graphqlserver/test_graphql.py +++ b/tests/component_graphqlserver/test_graphql.py @@ -13,14 +13,28 @@ # limitations under the License. import json - import pytest -from testing_support.fixtures import dt_enabled, validate_transaction_metrics +from testing_support.fixtures import ( + dt_enabled, + validate_transaction_errors, + validate_transaction_metrics, +) from testing_support.validators.validate_span_events import validate_span_events from testing_support.validators.validate_transaction_count import ( validate_transaction_count, ) +from newrelic.api.background_task import background_task +from newrelic.common.object_names import callable_name + + +@pytest.fixture(scope="session") +def is_graphql_2(): + from graphql import __version__ as version + + major_version = int(version.split(".")[0]) + return major_version == 2 + @pytest.fixture(scope="session") def target_application(): @@ -29,8 +43,66 @@ def target_application(): return _test_graphql.target_application +@pytest.fixture(scope="session") +def graphql_run(): + """Wrapper function to simulate framework_graphql test behavior.""" + + def execute(target_application, query): + response = target_application.make_request( + "POST", "/graphql", body=json.dumps({"query": query}), headers={"Content-Type": "application/json"} + ) + + if not isinstance(query, str) or "error" not in query: + assert response.status == 200 + + return response + + return execute + + +def example_middleware(next, root, info, **args): #pylint: disable=W0622 + return_value = next(root, info, **args) + return return_value + + +def error_middleware(next, root, info, **args): #pylint: disable=W0622 + raise RuntimeError("Runtime Error!") + + +_runtime_error_name = callable_name(RuntimeError) +_test_runtime_error = [(_runtime_error_name, "Runtime Error!")] +_graphql_base_rollup_metrics = [ + ("GraphQL/all", 1), + ("GraphQL/allWeb", 1), + ("GraphQL/GraphQLServer/all", 1), + ("GraphQL/GraphQLServer/allWeb", 1), +] + + +def test_basic(target_application, graphql_run): + from graphql import __version__ as graphql_version + from graphql_server import __version__ as graphql_server_version + from sanic import __version__ as sanic_version + + FRAMEWORK_METRICS = [ + ("Python/Framework/GraphQL/%s" % graphql_version, 1), + ("Python/Framework/GraphQLServer/%s" % graphql_server_version, 1), + ("Python/Framework/Sanic/%s" % sanic_version, 1), + ] + + @validate_transaction_metrics( + "query//hello", + "GraphQL", + rollup_metrics=_graphql_base_rollup_metrics + FRAMEWORK_METRICS, + ) + def _test(): + response = graphql_run(target_application, "{ hello }") + + _test() + + @dt_enabled -def test_graphql_metrics_and_attrs(target_application): +def test_query_and_mutation(target_application, graphql_run): from graphql import __version__ as graphql_version from graphql_server import __version__ as graphql_server_version from sanic import __version__ as sanic_version @@ -40,51 +112,325 @@ def test_graphql_metrics_and_attrs(target_application): ("Python/Framework/GraphQLServer/%s" % graphql_server_version, 1), ("Python/Framework/Sanic/%s" % sanic_version, 1), ] - _test_scoped_metrics = [ - ("GraphQL/resolve/GraphQLServer/hello", 1), - ("GraphQL/operation/GraphQLServer/query//hello", 1), + _test_query_scoped_metrics = [ + ("GraphQL/resolve/GraphQLServer/storage", 1), + ("GraphQL/operation/GraphQLServer/query//storage", 1), ("Function/graphql_server.sanic.graphqlview:GraphQLView.post", 1), ] - _test_unscoped_metrics = [ + _test_query_unscoped_metrics = [ ("GraphQL/all", 1), ("GraphQL/GraphQLServer/all", 1), ("GraphQL/allWeb", 1), ("GraphQL/GraphQLServer/allWeb", 1), - ] + _test_scoped_metrics + ] + _test_query_scoped_metrics + _test_mutation_scoped_metrics = [ + ("GraphQL/resolve/GraphQLServer/storage_add", 1), + ("GraphQL/operation/GraphQLServer/mutation//storage_add", 1), + ("Function/graphql_server.sanic.graphqlview:GraphQLView.post", 1), + ] + _test_mutation_unscoped_metrics = [ + ("GraphQL/all", 1), + ("GraphQL/GraphQLServer/all", 1), + ("GraphQL/allWeb", 1), + ("GraphQL/GraphQLServer/allWeb", 1), + ] + _test_mutation_scoped_metrics + + _expected_mutation_operation_attributes = { + "graphql.operation.type": "mutation", + "graphql.operation.name": "", + "graphql.operation.query": 'mutation { storage_add(string: ?) }', + } + _expected_mutation_resolver_attributes = { + "graphql.field.name": "storage_add", + "graphql.field.parentType": "Mutation", + "graphql.field.path": "storage_add", + "graphql.field.returnType": "String", + } _expected_query_operation_attributes = { "graphql.operation.type": "query", "graphql.operation.name": "", - "graphql.operation.query": "{ hello }", + "graphql.operation.query": "query { storage }", } _expected_query_resolver_attributes = { + "graphql.field.name": "storage", + "graphql.field.parentType": "Query", + "graphql.field.path": "storage", + "graphql.field.returnType": "[String]", + } + + def _test(): + @validate_transaction_metrics( + "mutation//storage_add", + "GraphQL", + scoped_metrics=_test_mutation_scoped_metrics, + rollup_metrics=_test_mutation_unscoped_metrics + FRAMEWORK_METRICS, + ) + @validate_span_events(exact_agents=_expected_mutation_operation_attributes) + @validate_span_events(exact_agents=_expected_mutation_resolver_attributes) + def _mutation(): + return graphql_run(target_application, 'mutation { storage_add(string: "abc") }') + + @validate_transaction_metrics( + "query//storage", + "GraphQL", + scoped_metrics=_test_query_scoped_metrics, + rollup_metrics=_test_query_unscoped_metrics + FRAMEWORK_METRICS, + ) + @validate_span_events(exact_agents=_expected_query_operation_attributes) + @validate_span_events(exact_agents=_expected_query_resolver_attributes) + def _query(): + return graphql_run(target_application, "query { storage }") + + response = _mutation() + response = _query() + + # These are separate assertions because pypy stores 'abc' as a unicode string while other Python versions do not + assert "storage" in str(response.body.decode("utf-8")) + assert "abc" in str(response.body.decode("utf-8")) + + _test() + + +@pytest.mark.parametrize("field", ("error", "error_non_null")) +@dt_enabled +def test_exception_in_resolver(target_application, graphql_run, field): + query = "query MyQuery { %s }" % field + + txn_name = "framework_graphql._target_application:resolve_error" + + # Metrics + _test_exception_scoped_metrics = [ + ("GraphQL/operation/GraphQLServer/query/MyQuery/%s" % field, 1), + ("GraphQL/resolve/GraphQLServer/%s" % field, 1), + ] + _test_exception_rollup_metrics = [ + ("Errors/all", 1), + ("Errors/allWeb", 1), + ("Errors/WebTransaction/GraphQL/%s" % txn_name, 1), + ] + _test_exception_scoped_metrics + + # Attributes + _expected_exception_resolver_attributes = { + "graphql.field.name": field, + "graphql.field.parentType": "Query", + "graphql.field.path": field, + "graphql.field.returnType": "String!" if "non_null" in field else "String", + } + _expected_exception_operation_attributes = { + "graphql.operation.type": "query", + "graphql.operation.name": "MyQuery", + "graphql.operation.query": query, + } + + @validate_transaction_metrics( + txn_name, + "GraphQL", + scoped_metrics=_test_exception_scoped_metrics, + rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics, + ) + @validate_span_events(exact_agents=_expected_exception_operation_attributes) + @validate_span_events(exact_agents=_expected_exception_resolver_attributes) + @validate_transaction_errors(errors=_test_runtime_error) + def _test(): + response = graphql_run(target_application, query) + + _test() + + +@dt_enabled +@pytest.mark.parametrize( + "query,exc_class", + [ + ("query MyQuery { error_missing_field }", "GraphQLError"), + ("{ syntax_error ", "graphql.error.syntax_error:GraphQLSyntaxError"), + ], +) +def test_exception_in_validation(target_application, graphql_run, is_graphql_2, query, exc_class): + if "syntax" in query: + txn_name = "graphql.language.parser:parse" + else: + if is_graphql_2: + txn_name = "graphql.validation.validation:validate" + else: + txn_name = "graphql.validation.validate:validate" + + # Import path differs between versions + if exc_class == "GraphQLError": + from graphql.error import GraphQLError + + exc_class = callable_name(GraphQLError) + + _test_exception_scoped_metrics = [ + ('GraphQL/operation/GraphQLServer///', 1), + ] + _test_exception_rollup_metrics = [ + ("Errors/all", 1), + ("Errors/allWeb", 1), + ("Errors/WebTransaction/GraphQL/%s" % txn_name, 1), + ] + _test_exception_scoped_metrics + + # Attributes + _expected_exception_operation_attributes = { + "graphql.operation.type": "", + "graphql.operation.name": "", + "graphql.operation.query": query, + } + + @validate_transaction_metrics( + txn_name, + "GraphQL", + scoped_metrics=_test_exception_scoped_metrics, + rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics, + ) + @validate_span_events(exact_agents=_expected_exception_operation_attributes) + @validate_transaction_errors(errors=[exc_class]) + def _test(): + response = graphql_run(target_application, query) + + _test() + + +@dt_enabled +def test_operation_metrics_and_attrs(target_application, graphql_run): + operation_metrics = [("GraphQL/operation/GraphQLServer/query/MyQuery/library", 1)] + operation_attrs = { + "graphql.operation.type": "query", + "graphql.operation.name": "MyQuery", + } + + @validate_transaction_metrics( + "query/MyQuery/library", + "GraphQL", + scoped_metrics=operation_metrics, + rollup_metrics=operation_metrics + _graphql_base_rollup_metrics, + ) + # Span count 10: Transaction, View, Operation, and 7 Resolvers + # library, library.name, library.book + # library.book.name and library.book.id for each book resolved (in this case 2) + @validate_span_events(count=10) + @validate_span_events(exact_agents=operation_attrs) + def _test(): + response = graphql_run(target_application, "query MyQuery { library(index: 0) { branch, book { id, name } } }") + + _test() + + +@dt_enabled +def test_field_resolver_metrics_and_attrs(target_application, graphql_run): + field_resolver_metrics = [("GraphQL/resolve/GraphQLServer/hello", 1)] + graphql_attrs = { "graphql.field.name": "hello", "graphql.field.parentType": "Query", "graphql.field.path": "hello", "graphql.field.returnType": "String", } - @validate_span_events(exact_agents=_expected_query_operation_attributes) - @validate_span_events(exact_agents=_expected_query_resolver_attributes) @validate_transaction_metrics( "query//hello", "GraphQL", - scoped_metrics=_test_scoped_metrics, - rollup_metrics=_test_unscoped_metrics + FRAMEWORK_METRICS, + scoped_metrics=field_resolver_metrics, + rollup_metrics=field_resolver_metrics + _graphql_base_rollup_metrics, ) + # Span count 4: Transaction, View, Operation, and 1 Resolver + @validate_span_events(count=4) + @validate_span_events(exact_agents=graphql_attrs) def _test(): - response = target_application.make_request( - "POST", "/graphql", body=json.dumps({"query": "{ hello }"}), headers={"Content-Type": "application/json"} - ) - assert response.status == 200 + response = graphql_run(target_application, "{ hello }") assert "Hello!" in response.body.decode("utf-8") _test() -@validate_transaction_count(0) -def test_ignored_introspection_transactions(target_application): - response = target_application.make_request( - "POST", "/graphql", body=json.dumps({"query": "{ __schema { types { name } } }"}), headers={"Content-Type": "application/json"} +_test_queries = [ + ("{ hello }", "{ hello }"), # Basic query extraction + ("{ error }", "{ error }"), # Extract query on field error + ( + "{ library(index: 0) { branch } }", + "{ library(index: ?) { branch } }", + ), # Integers + ('{ echo(echo: "123") }', "{ echo(echo: ?) }"), # Strings with numerics + ('{ echo(echo: "test") }', "{ echo(echo: ?) }"), # Strings + ('{ TestEcho: echo(echo: "test") }', "{ TestEcho: echo(echo: ?) }"), # Aliases + ('{ TestEcho: echo(echo: "test") }', "{ TestEcho: echo(echo: ?) }"), # Variables + ( # Fragments + '{ ...MyFragment } fragment MyFragment on Query { echo(echo: "test") }', + "{ ...MyFragment } fragment MyFragment on Query { echo(echo: ?) }", + ), +] + + +@dt_enabled +@pytest.mark.parametrize("query,obfuscated", _test_queries) +def test_query_obfuscation(target_application, graphql_run, query, obfuscated): + graphql_attrs = {"graphql.operation.query": obfuscated} + + if callable(query): + query = query() + + @validate_span_events(exact_agents=graphql_attrs) + def _test(): + response = graphql_run(target_application, query) + + _test() + + +_test_queries = [ + ("{ hello }", "/hello"), # Basic query + ("{ error }", "/error"), # Extract deepest path on field error + ('{ echo(echo: "test") }', "/echo"), # Fields with arguments + ( + "{ library(index: 0) { branch, book { isbn branch } } }", + "/library", + ), # Complex Example, 1 level + ( + "{ library(index: 0) { book { author { first_name }} } }", + "/library.book.author.first_name", + ), # Complex Example, 2 levels + ("{ library(index: 0) { id, book { name } } }", "/library.book.name"), # Filtering + ('{ TestEcho: echo(echo: "test") }', "/echo"), # Aliases + ( + '{ search(contains: "A") { __typename ... on Book { name } } }', + "/search.name", + ), # InlineFragment + ( + '{ hello echo(echo: "test") }', + "", + ), # Multiple root selections. (need to decide on final behavior) + # FragmentSpread + ( + "{ library(index: 0) { book { ...MyFragment } } } fragment MyFragment on Book { name id }", # Fragment filtering + "/library.book.name", + ), + ( + "{ library(index: 0) { book { ...MyFragment } } } fragment MyFragment on Book { author { first_name } }", + "/library.book.author.first_name", + ), + ( + "{ library(index: 0) { book { ...MyFragment } magazine { ...MagFragment } } } fragment MyFragment on Book { author { first_name } } fragment MagFragment on Magazine { name }", + "/library", + ), +] + + +@dt_enabled +@pytest.mark.parametrize("query,expected_path", _test_queries) +def test_deepest_unique_path(target_application, graphql_run, query, expected_path): + if expected_path == "/error": + txn_name = "framework_graphql._target_application:resolve_error" + else: + txn_name = "query/%s" % expected_path + + @validate_transaction_metrics( + txn_name, + "GraphQL", ) - assert response.status == 200 + def _test(): + response = graphql_run(target_application, query) + + _test() + + +@validate_transaction_count(0) +def test_ignored_introspection_transactions(target_application, graphql_run): + response = graphql_run(target_application, "{ __schema { types { name } } }") From f78142398fa7cd7700d0a77fbc1e0509575efd9e Mon Sep 17 00:00:00 2001 From: Tim Pansino Date: Mon, 28 Mar 2022 12:25:22 -0700 Subject: [PATCH 06/26] Fix flask framework details with callable --- newrelic/api/wsgi_application.py | 17 +++++++++++++++-- newrelic/hooks/framework_flask.py | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/newrelic/api/wsgi_application.py b/newrelic/api/wsgi_application.py index 840c094a4..7f010aae7 100644 --- a/newrelic/api/wsgi_application.py +++ b/newrelic/api/wsgi_application.py @@ -521,8 +521,20 @@ def __iter__(self): def WSGIApplicationWrapper(wrapped, application=None, name=None, group=None, framework=None): - if framework is not None and not isinstance(framework, tuple): - framework = (framework, None) + def get_framework(): + """Used to delay imports by passing framework as a callable.""" + nonlocal framework + + if isinstance(framework, tuple) or framework is None: + return framework + + if callable(framework): + framework = framework() + + if framework is not None and not isinstance(framework, tuple): + framework = (framework, None) + + return framework def _nr_wsgi_application_wrapper_(wrapped, instance, args, kwargs): # Check to see if any transaction is present, even an inactive @@ -530,6 +542,7 @@ def _nr_wsgi_application_wrapper_(wrapped, instance, args, kwargs): # stopped already. transaction = current_transaction(active_only=False) + framework = get_framework() if transaction: # If there is any active transaction we will return without diff --git a/newrelic/hooks/framework_flask.py b/newrelic/hooks/framework_flask.py index 4535b3289..1b863f9a7 100644 --- a/newrelic/hooks/framework_flask.py +++ b/newrelic/hooks/framework_flask.py @@ -266,7 +266,7 @@ def instrument_flask_views(module): def instrument_flask_app(module): - wrap_wsgi_application(module, "Flask.wsgi_app", framework=framework_details()) + wrap_wsgi_application(module, "Flask.wsgi_app", framework=framework_details) wrap_function_wrapper( module, "Flask.add_url_rule", _nr_wrapper_Flask_add_url_rule_input_ From 58418ee8c03fb7ec596bd28d7e042da9cfc9fb23 Mon Sep 17 00:00:00 2001 From: Tim Pansino Date: Mon, 28 Mar 2022 13:15:42 -0700 Subject: [PATCH 07/26] Parametrized testing for graphql-server --- .../component_graphqlserver/_test_graphql.py | 57 +++++++++- tests/component_graphqlserver/test_graphql.py | 105 +++++++++--------- tox.ini | 3 +- 3 files changed, 107 insertions(+), 58 deletions(-) diff --git a/tests/component_graphqlserver/_test_graphql.py b/tests/component_graphqlserver/_test_graphql.py index c98d587c1..af952b6fe 100644 --- a/tests/component_graphqlserver/_test_graphql.py +++ b/tests/component_graphqlserver/_test_graphql.py @@ -12,15 +12,66 @@ # See the License for the specific language governing permissions and # limitations under the License. +from flask import Flask from sanic import Sanic +import json +import webtest from testing_support.asgi_testing import AsgiTest from framework_graphql._target_application import _target_application as schema -from graphql_server.sanic import GraphQLView +from graphql_server.flask import GraphQLView as FlaskView +from graphql_server.sanic import GraphQLView as SanicView +# Sanic +target_application = dict() sanic_app = Sanic(name="SanicGraphQL") routes = [ - sanic_app.add_route(GraphQLView.as_view(schema=schema), "/graphql"), + sanic_app.add_route(SanicView.as_view(schema=schema), "/graphql"), ] -target_application = AsgiTest(sanic_app) +sanic_app = AsgiTest(sanic_app) + +def sanic_execute(query): + response = sanic_app.make_request( + "POST", "/graphql", body=json.dumps({"query": query}), headers={"Content-Type": "application/json"} + ) + body = json.loads(response.body.decode("utf-8")) + + if not isinstance(query, str) or "error" in query: + try: + assert response.status != 200 + except AssertionError: + assert body["errors"] + else: + assert response.status == 200 + assert "errors" not in body or not body["errors"] + + return response + +target_application["Sanic"] = sanic_execute + +# Flask + +flask_app = Flask("FlaskGraphQL") +flask_app.add_url_rule("/graphql", view_func=FlaskView.as_view("graphql", schema=schema)) +flask_app = webtest.TestApp(flask_app) + +def flask_execute(query): + if not isinstance(query, str) or "error" in query: + expect_errors = True + else: + expect_errors = False + + response = flask_app.post( + "/graphql", json.dumps({"query": query}), headers={"Content-Type": "application/json"}, expect_errors=expect_errors + ) + + body = json.loads(response.body.decode("utf-8")) + if expect_errors: + assert body["errors"] + else: + assert "errors" not in body or not body["errors"] + + return response + +target_application["Flask"] = flask_execute diff --git a/tests/component_graphqlserver/test_graphql.py b/tests/component_graphqlserver/test_graphql.py index a10f1ab18..1faf55abc 100644 --- a/tests/component_graphqlserver/test_graphql.py +++ b/tests/component_graphqlserver/test_graphql.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json +import importlib import pytest from testing_support.fixtures import ( dt_enabled, @@ -24,7 +24,6 @@ validate_transaction_count, ) -from newrelic.api.background_task import background_task from newrelic.common.object_names import callable_name @@ -35,29 +34,13 @@ def is_graphql_2(): major_version = int(version.split(".")[0]) return major_version == 2 - -@pytest.fixture(scope="session") -def target_application(): +@pytest.fixture(scope="session", params=("Sanic", "Flask")) +def target_application(request): import _test_graphql + framework = request.param + version = importlib.import_module(framework.lower()).__version__ - return _test_graphql.target_application - - -@pytest.fixture(scope="session") -def graphql_run(): - """Wrapper function to simulate framework_graphql test behavior.""" - - def execute(target_application, query): - response = target_application.make_request( - "POST", "/graphql", body=json.dumps({"query": query}), headers={"Content-Type": "application/json"} - ) - - if not isinstance(query, str) or "error" not in query: - assert response.status == 200 - - return response - - return execute + return framework, version, _test_graphql.target_application[framework] def example_middleware(next, root, info, **args): #pylint: disable=W0622 @@ -77,17 +60,18 @@ def error_middleware(next, root, info, **args): #pylint: disable=W0622 ("GraphQL/GraphQLServer/all", 1), ("GraphQL/GraphQLServer/allWeb", 1), ] +_view_metrics = {"Sanic": "Function/graphql_server.sanic.graphqlview:GraphQLView.post", "Flask": "Function/graphql_server.flask.graphqlview:graphql"} -def test_basic(target_application, graphql_run): +def test_basic(target_application): + framework, version, target_application = target_application from graphql import __version__ as graphql_version from graphql_server import __version__ as graphql_server_version - from sanic import __version__ as sanic_version FRAMEWORK_METRICS = [ ("Python/Framework/GraphQL/%s" % graphql_version, 1), ("Python/Framework/GraphQLServer/%s" % graphql_server_version, 1), - ("Python/Framework/Sanic/%s" % sanic_version, 1), + ("Python/Framework/%s/%s" % (framework, version), 1), ] @validate_transaction_metrics( @@ -96,26 +80,26 @@ def test_basic(target_application, graphql_run): rollup_metrics=_graphql_base_rollup_metrics + FRAMEWORK_METRICS, ) def _test(): - response = graphql_run(target_application, "{ hello }") + response = target_application("{ hello }") _test() @dt_enabled -def test_query_and_mutation(target_application, graphql_run): +def test_query_and_mutation(target_application): + framework, version, target_application = target_application from graphql import __version__ as graphql_version from graphql_server import __version__ as graphql_server_version - from sanic import __version__ as sanic_version FRAMEWORK_METRICS = [ ("Python/Framework/GraphQL/%s" % graphql_version, 1), ("Python/Framework/GraphQLServer/%s" % graphql_server_version, 1), - ("Python/Framework/Sanic/%s" % sanic_version, 1), + ("Python/Framework/%s/%s" % (framework, version), 1), ] _test_query_scoped_metrics = [ ("GraphQL/resolve/GraphQLServer/storage", 1), ("GraphQL/operation/GraphQLServer/query//storage", 1), - ("Function/graphql_server.sanic.graphqlview:GraphQLView.post", 1), + (_view_metrics[framework], 1), ] _test_query_unscoped_metrics = [ ("GraphQL/all", 1), @@ -127,7 +111,7 @@ def test_query_and_mutation(target_application, graphql_run): _test_mutation_scoped_metrics = [ ("GraphQL/resolve/GraphQLServer/storage_add", 1), ("GraphQL/operation/GraphQLServer/mutation//storage_add", 1), - ("Function/graphql_server.sanic.graphqlview:GraphQLView.post", 1), + (_view_metrics[framework], 1), ] _test_mutation_unscoped_metrics = [ ("GraphQL/all", 1), @@ -169,7 +153,7 @@ def _test(): @validate_span_events(exact_agents=_expected_mutation_operation_attributes) @validate_span_events(exact_agents=_expected_mutation_resolver_attributes) def _mutation(): - return graphql_run(target_application, 'mutation { storage_add(string: "abc") }') + return target_application('mutation { storage_add(string: "abc") }') @validate_transaction_metrics( "query//storage", @@ -180,7 +164,7 @@ def _mutation(): @validate_span_events(exact_agents=_expected_query_operation_attributes) @validate_span_events(exact_agents=_expected_query_resolver_attributes) def _query(): - return graphql_run(target_application, "query { storage }") + return target_application("query { storage }") response = _mutation() response = _query() @@ -194,7 +178,8 @@ def _query(): @pytest.mark.parametrize("field", ("error", "error_non_null")) @dt_enabled -def test_exception_in_resolver(target_application, graphql_run, field): +def test_exception_in_resolver(target_application, field): + framework, version, target_application = target_application query = "query MyQuery { %s }" % field txn_name = "framework_graphql._target_application:resolve_error" @@ -233,7 +218,7 @@ def test_exception_in_resolver(target_application, graphql_run, field): @validate_span_events(exact_agents=_expected_exception_resolver_attributes) @validate_transaction_errors(errors=_test_runtime_error) def _test(): - response = graphql_run(target_application, query) + response = target_application(query) _test() @@ -246,7 +231,8 @@ def _test(): ("{ syntax_error ", "graphql.error.syntax_error:GraphQLSyntaxError"), ], ) -def test_exception_in_validation(target_application, graphql_run, is_graphql_2, query, exc_class): +def test_exception_in_validation(target_application, is_graphql_2, query, exc_class): + framework, version, target_application = target_application if "syntax" in query: txn_name = "graphql.language.parser:parse" else: @@ -286,38 +272,43 @@ def test_exception_in_validation(target_application, graphql_run, is_graphql_2, @validate_span_events(exact_agents=_expected_exception_operation_attributes) @validate_transaction_errors(errors=[exc_class]) def _test(): - response = graphql_run(target_application, query) + response = target_application(query) _test() @dt_enabled -def test_operation_metrics_and_attrs(target_application, graphql_run): +def test_operation_metrics_and_attrs(target_application): + framework, version, target_application = target_application operation_metrics = [("GraphQL/operation/GraphQLServer/query/MyQuery/library", 1)] operation_attrs = { "graphql.operation.type": "query", "graphql.operation.name": "MyQuery", } + # Base span count 10: Transaction, View, Operation, and 7 Resolvers + # library, library.name, library.book + # library.book.name and library.book.id for each book resolved (in this case 2) + # For Flask, add 9 more for WSGI and framework related spans + span_count = {"Flask": 19, "Sanic": 10} + @validate_transaction_metrics( "query/MyQuery/library", "GraphQL", scoped_metrics=operation_metrics, rollup_metrics=operation_metrics + _graphql_base_rollup_metrics, ) - # Span count 10: Transaction, View, Operation, and 7 Resolvers - # library, library.name, library.book - # library.book.name and library.book.id for each book resolved (in this case 2) - @validate_span_events(count=10) + @validate_span_events(count=span_count[framework]) @validate_span_events(exact_agents=operation_attrs) def _test(): - response = graphql_run(target_application, "query MyQuery { library(index: 0) { branch, book { id, name } } }") + response = target_application("query MyQuery { library(index: 0) { branch, book { id, name } } }") _test() @dt_enabled -def test_field_resolver_metrics_and_attrs(target_application, graphql_run): +def test_field_resolver_metrics_and_attrs(target_application): + framework, version, target_application = target_application field_resolver_metrics = [("GraphQL/resolve/GraphQLServer/hello", 1)] graphql_attrs = { "graphql.field.name": "hello", @@ -326,17 +317,20 @@ def test_field_resolver_metrics_and_attrs(target_application, graphql_run): "graphql.field.returnType": "String", } + # Base span count 4: Transaction, View, Operation, and 1 Resolver + # For Flask, add 9 more for WSGI and framework related spans + span_count = {"Flask": 13, "Sanic": 4} + @validate_transaction_metrics( "query//hello", "GraphQL", scoped_metrics=field_resolver_metrics, rollup_metrics=field_resolver_metrics + _graphql_base_rollup_metrics, ) - # Span count 4: Transaction, View, Operation, and 1 Resolver - @validate_span_events(count=4) + @validate_span_events(count=span_count[framework]) @validate_span_events(exact_agents=graphql_attrs) def _test(): - response = graphql_run(target_application, "{ hello }") + response = target_application("{ hello }") assert "Hello!" in response.body.decode("utf-8") _test() @@ -362,7 +356,8 @@ def _test(): @dt_enabled @pytest.mark.parametrize("query,obfuscated", _test_queries) -def test_query_obfuscation(target_application, graphql_run, query, obfuscated): +def test_query_obfuscation(target_application, query, obfuscated): + framework, version, target_application = target_application graphql_attrs = {"graphql.operation.query": obfuscated} if callable(query): @@ -370,7 +365,7 @@ def test_query_obfuscation(target_application, graphql_run, query, obfuscated): @validate_span_events(exact_agents=graphql_attrs) def _test(): - response = graphql_run(target_application, query) + response = target_application(query) _test() @@ -415,7 +410,8 @@ def _test(): @dt_enabled @pytest.mark.parametrize("query,expected_path", _test_queries) -def test_deepest_unique_path(target_application, graphql_run, query, expected_path): +def test_deepest_unique_path(target_application, query, expected_path): + framework, version, target_application = target_application if expected_path == "/error": txn_name = "framework_graphql._target_application:resolve_error" else: @@ -426,11 +422,12 @@ def test_deepest_unique_path(target_application, graphql_run, query, expected_pa "GraphQL", ) def _test(): - response = graphql_run(target_application, query) + response = target_application(query) _test() @validate_transaction_count(0) -def test_ignored_introspection_transactions(target_application, graphql_run): - response = graphql_run(target_application, "{ __schema { types { name } } }") +def test_ignored_introspection_transactions(target_application): + framework, version, target_application = target_application + response = target_application("{ __schema { types { name } } }") diff --git a/tox.ini b/tox.ini index 81d38e173..8806bf543 100644 --- a/tox.ini +++ b/tox.ini @@ -183,8 +183,9 @@ deps = component_flask_rest: flask-restful component_flask_rest: flask-restplus component_flask_rest: flask-restx - component_graphqlserver: graphql-server[sanic]==3.0.0b5 + component_graphqlserver: graphql-server[sanic,flask]==3.0.0b5 component_graphqlserver: sanic>20 + component_graphqlserver: Flask component_graphqlserver: jinja2 component_tastypie-tastypie0143: django-tastypie<0.14.4 component_tastypie-{py27,pypy}-tastypie0143: django<1.12 From 215538b505ccd90fe2ad11f5390b3687558e54fd Mon Sep 17 00:00:00 2001 From: Tim Pansino Date: Mon, 28 Mar 2022 15:07:50 -0700 Subject: [PATCH 08/26] GraphQL Async Resolvers Co-authored-by: Lalleh Rafeei Co-authored-by: Uma Annamalai --- newrelic/api/graphql_trace.py | 8 ++- newrelic/hooks/framework_graphql.py | 53 +++++++++++++++---- .../framework_strawberry/test_application.py | 1 + .../test_application_async.py | 3 +- 4 files changed, 52 insertions(+), 13 deletions(-) diff --git a/newrelic/api/graphql_trace.py b/newrelic/api/graphql_trace.py index e14e87f86..72a0492f3 100644 --- a/newrelic/api/graphql_trace.py +++ b/newrelic/api/graphql_trace.py @@ -134,9 +134,12 @@ def wrap_graphql_operation_trace(module, object_path): class GraphQLResolverTrace(TimeTrace): - def __init__(self, field_name=None, **kwargs): + def __init__(self, field_name=None, field_parent_type=None, field_return_type=None, field_path=None, **kwargs): super(GraphQLResolverTrace, self).__init__(**kwargs) self.field_name = field_name + self.field_parent_type = field_parent_type + self.field_return_type = field_return_type + self.field_path = field_path self._product = None def __repr__(self): @@ -164,6 +167,9 @@ def product(self): def finalize_data(self, *args, **kwargs): self._add_agent_attribute("graphql.field.name", self.field_name) + self._add_agent_attribute("graphql.field.parentType", self.field_parent_type) + self._add_agent_attribute("graphql.field.returnType", self.field_return_type) + self._add_agent_attribute("graphql.field.path", self.field_path) return super(GraphQLResolverTrace, self).finalize_data(*args, **kwargs) diff --git a/newrelic/hooks/framework_graphql.py b/newrelic/hooks/framework_graphql.py index 83e1fd65b..222063d9f 100644 --- a/newrelic/hooks/framework_graphql.py +++ b/newrelic/hooks/framework_graphql.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import time +from inspect import isawaitable import logging from collections import deque @@ -323,7 +325,16 @@ def wrap_resolver(wrapped, instance, args, kwargs): transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=13) with ErrorTrace(ignore=ignore_graphql_duplicate_exception): - return wrapped(*args, **kwargs) + result = wrapped(*args, **kwargs) + + if isawaitable(result): + # Grab any async resolvers and wrap with error traces + async def _nr_coro_resolver_error_wrapper(): + with ErrorTrace(ignore=ignore_graphql_duplicate_exception): + return await result + return _nr_coro_resolver_error_wrapper() + + return result def wrap_error_handler(wrapped, instance, args, kwargs): @@ -387,18 +398,38 @@ def wrap_resolve_field(wrapped, instance, args, kwargs): field_name = field_asts[0].name.value field_def = parent_type.fields.get(field_name) field_return_type = str(field_def.type) if field_def else "" + if isinstance(field_path, list): + field_path = field_path[0] + else: + field_path = field_path.key - with GraphQLResolverTrace(field_name) as trace: - with ErrorTrace(ignore=ignore_graphql_duplicate_exception): - trace._add_agent_attribute("graphql.field.parentType", parent_type.name) - trace._add_agent_attribute("graphql.field.returnType", field_return_type) + start_time = time.time() - if isinstance(field_path, list): - trace._add_agent_attribute("graphql.field.path", field_path[0]) - else: - trace._add_agent_attribute("graphql.field.path", field_path.key) - - return wrapped(*args, **kwargs) + try: + result = wrapped(*args, **kwargs) + except Exception: + # Synchonous resolver with exception raised + with GraphQLResolverTrace(field_name, field_parent_type=parent_type.name, field_return_type=field_return_type, field_path=field_path) as trace: + trace._start_time = start_time + notice_error(ignore=ignore_graphql_duplicate_exception) + raise + + if isawaitable(result): + # Asynchronous resolvers (returned coroutines from non-coroutine functions) + async def _nr_coro_resolver_wrapper(): + with GraphQLResolverTrace(field_name, field_parent_type=parent_type.name, field_return_type=field_return_type, field_path=field_path) as trace: + with ErrorTrace(ignore=ignore_graphql_duplicate_exception): + trace._start_time = start_time + return await result + + # Return a coroutine that handles wrapping in a resolver trace + return _nr_coro_resolver_wrapper() + else: + # Synchonous resolver with no exception raised + with GraphQLResolverTrace(field_name, field_parent_type=parent_type.name, field_return_type=field_return_type, field_path=field_path) as trace: + with ErrorTrace(ignore=ignore_graphql_duplicate_exception): + trace._start_time = start_time + return result def bind_graphql_impl_query(schema, source, *args, **kwargs): diff --git a/tests/framework_strawberry/test_application.py b/tests/framework_strawberry/test_application.py index b68b825ff..9f23c1396 100644 --- a/tests/framework_strawberry/test_application.py +++ b/tests/framework_strawberry/test_application.py @@ -105,6 +105,7 @@ def test_basic(app, graphql_run): def _test(): response = graphql_run(app, "{ hello }") assert not response.errors + assert response.data["hello"] == "Hello!" _test() diff --git a/tests/framework_strawberry/test_application_async.py b/tests/framework_strawberry/test_application_async.py index 8174eb36e..85d109955 100644 --- a/tests/framework_strawberry/test_application_async.py +++ b/tests/framework_strawberry/test_application_async.py @@ -43,7 +43,7 @@ def execute(schema, *args, **kwargs): loop = asyncio.new_event_loop() -def test_basic(app, graphql_run_async): +def test_basic_async(app, graphql_run_async): from graphql import __version__ as version from newrelic.hooks.framework_strawberry import framework_details @@ -64,6 +64,7 @@ def _test(): async def coro(): response = await graphql_run_async(app, "{ hello_async }") assert not response.errors + assert response.data["hello_async"] == "Hello!" loop.run_until_complete(coro()) From 8cda4f76a86824b3166ac886e552717919f66953 Mon Sep 17 00:00:00 2001 From: Timothy Pansino <11214426+TimPansino@users.noreply.github.com> Date: Wed, 20 Apr 2022 14:48:07 -0700 Subject: [PATCH 09/26] GraphQL Proper Coro and Promise Support (#508) * Fix GraphQL async issues * Fix nonlocal binding issues in python 2 * Fix promises with async graphql * Issues with promises * Fix promises in graphql2 * Fixed all graphql async issues * Fix Py27 quirks * Update tox * Fix importing paths of graphqlserver * Fix broken import path * Unpin pypy37 * Fix weird import issues --- newrelic/api/graphql_trace.py | 2 +- newrelic/api/wsgi_application.py | 5 + newrelic/hooks/framework_graphql.py | 180 +++++++--- newrelic/hooks/framework_graphql_py3.py | 51 +++ .../component_graphqlserver/_test_graphql.py | 11 +- tests/component_graphqlserver/test_graphql.py | 4 +- .../framework_graphql/_target_application.py | 313 +++++------------ .../framework_graphql/_target_schema_async.py | 187 ++++++++++ .../_target_schema_promise.py | 194 +++++++++++ .../framework_graphql/_target_schema_sync.py | 239 +++++++++++++ tests/framework_graphql/conftest.py | 18 +- tests/framework_graphql/test_application.py | 324 ++++++++++-------- .../test_application_async.py | 100 +----- tox.ini | 4 +- 14 files changed, 1121 insertions(+), 511 deletions(-) create mode 100644 newrelic/hooks/framework_graphql_py3.py create mode 100644 tests/framework_graphql/_target_schema_async.py create mode 100644 tests/framework_graphql/_target_schema_promise.py create mode 100644 tests/framework_graphql/_target_schema_sync.py diff --git a/newrelic/api/graphql_trace.py b/newrelic/api/graphql_trace.py index 9b240a052..6863bd73d 100644 --- a/newrelic/api/graphql_trace.py +++ b/newrelic/api/graphql_trace.py @@ -134,7 +134,7 @@ def wrap_graphql_operation_trace(module, object_path): class GraphQLResolverTrace(TimeTrace): - def __init__(self, field_name=None, **kwargs): + def __init__(self, field_name=None, field_parent_type=None, field_return_type=None, field_path=None, **kwargs): parent = kwargs.pop("parent", None) source = kwargs.pop("source", None) if kwargs: diff --git a/newrelic/api/wsgi_application.py b/newrelic/api/wsgi_application.py index 15a21c551..0f4d30454 100644 --- a/newrelic/api/wsgi_application.py +++ b/newrelic/api/wsgi_application.py @@ -526,9 +526,14 @@ def get_framework(): framework = framework() _framework[0] = framework + if framework is not None and not isinstance(framework, tuple): + framework = (framework, None) + _framework[0] = framework + return framework def _nr_wsgi_application_wrapper_(wrapped, instance, args, kwargs): + # Check to see if any transaction is present, even an inactive # one which has been marked to be ignored or which has been # stopped already. diff --git a/newrelic/hooks/framework_graphql.py b/newrelic/hooks/framework_graphql.py index b64042792..dd2c31f70 100644 --- a/newrelic/hooks/framework_graphql.py +++ b/newrelic/hooks/framework_graphql.py @@ -12,9 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import time -from inspect import isawaitable +import functools import logging +import sys +import time from collections import deque from newrelic.api.error_trace import ErrorTrace @@ -25,6 +26,40 @@ from newrelic.common.object_names import callable_name, parse_exc_info from newrelic.common.object_wrapper import function_wrapper, wrap_function_wrapper from newrelic.core.graphql_utils import graphql_statement +from newrelic.packages import six + +try: + from inspect import isawaitable +except ImportError: + + def isawaitable(f): + return False + + +try: + # from promise import is_thenable as is_promise + from promise import Promise + + def is_promise(obj): + return isinstance(obj, Promise) + + def as_promise(f): + return Promise.resolve(None).then(f) + +except ImportError: + # If promises is not installed, prevent crashes by bypassing logic + def is_promise(obj): + return False + + def as_promise(f): + return f + +if six.PY3: + from newrelic.hooks.framework_graphql_py3 import ( + nr_coro_execute_name_wrapper, + nr_coro_resolver_error_wrapper, + nr_coro_resolver_wrapper, + ) _logger = logging.getLogger(__name__) @@ -79,6 +114,20 @@ def ignore_graphql_duplicate_exception(exc, val, tb): return None # Follow original exception matching rules +def catch_promise_error(e): + if hasattr(e, "__traceback__"): + notice_error(error=(e.__class__, e, e.__traceback__), ignore=ignore_graphql_duplicate_exception) + else: + # Python 2 does not retain a reference to the traceback and is irretrievable from a promise. + # As a workaround, raise the error and report it despite having an incorrect traceback. + try: + raise e + except Exception: + notice_error(ignore=ignore_graphql_duplicate_exception) + + return None + + def wrap_executor_context_init(wrapped, instance, args, kwargs): result = wrapped(*args, **kwargs) @@ -146,12 +195,20 @@ def wrap_execute_operation(wrapped, instance, args, kwargs): transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=11) result = wrapped(*args, **kwargs) - if not execution_context.errors: - if hasattr(trace, "set_transaction_name"): + + def set_name(value=None): + if not execution_context.errors and hasattr(trace, "set_transaction_name"): # Operation trace sets transaction name trace.set_transaction_name(priority=14) - - return result + return value + + if is_promise(result) and result.is_pending and graphql_version() < (3, 0): + return result.then(set_name) + elif isawaitable(result) and not is_promise(result): + return nr_coro_execute_name_wrapper(wrapped, result, set_name) + else: + set_name() + return result def get_node_value(field, attr, subattr="value"): @@ -274,7 +331,8 @@ def wrap_middleware(wrapped, instance, args, kwargs): transaction.set_transaction_name(name, "GraphQL", priority=12) with FunctionTrace(name, source=wrapped): with ErrorTrace(ignore=ignore_graphql_duplicate_exception): - return wrapped(*args, **kwargs) + result = wrapped(*args, **kwargs) + return result def bind_get_field_resolver(field_resolver): @@ -326,16 +384,32 @@ def wrap_resolver(wrapped, instance, args, kwargs): transaction.set_transaction_name(name, "GraphQL", priority=13) with ErrorTrace(ignore=ignore_graphql_duplicate_exception): + sync_start_time = time.time() result = wrapped(*args, **kwargs) - - if isawaitable(result): - # Grab any async resolvers and wrap with error traces - async def _nr_coro_resolver_error_wrapper(): - with ErrorTrace(ignore=ignore_graphql_duplicate_exception): - return await result - return _nr_coro_resolver_error_wrapper() - - return result + + if is_promise(result) and result.is_pending and graphql_version() < (3, 0): + @functools.wraps(wrapped) + def nr_promise_resolver_error_wrapper(v): + with FunctionTrace(name, source=wrapped): + with ErrorTrace(ignore=ignore_graphql_duplicate_exception): + try: + return result.get() + except Exception: + transaction.set_transaction_name(name, "GraphQL", priority=15) + raise + return as_promise(nr_promise_resolver_error_wrapper) + elif isawaitable(result) and not is_promise(result): + # Grab any async resolvers and wrap with traces + return nr_coro_resolver_error_wrapper( + wrapped, name, ignore_graphql_duplicate_exception, result, transaction + ) + else: + with FunctionTrace(name, source=wrapped) as trace: + trace.start_time = sync_start_time + if is_promise(result) and result.is_rejected: + result.catch(catch_promise_error).get() + transaction.set_transaction_name(name, "GraphQL", priority=15) + return result def wrap_error_handler(wrapped, instance, args, kwargs): @@ -404,33 +478,36 @@ def wrap_resolve_field(wrapped, instance, args, kwargs): else: field_path = field_path.key + trace = GraphQLResolverTrace( + field_name, field_parent_type=parent_type.name, field_return_type=field_return_type, field_path=field_path + ) start_time = time.time() try: result = wrapped(*args, **kwargs) except Exception: # Synchonous resolver with exception raised - with GraphQLResolverTrace(field_name, field_parent_type=parent_type.name, field_return_type=field_return_type, field_path=field_path) as trace: - trace._start_time = start_time + with trace: + trace.start_time = start_time notice_error(ignore=ignore_graphql_duplicate_exception) raise - if isawaitable(result): - # Asynchronous resolvers (returned coroutines from non-coroutine functions) - async def _nr_coro_resolver_wrapper(): - with GraphQLResolverTrace(field_name, field_parent_type=parent_type.name, field_return_type=field_return_type, field_path=field_path) as trace: + if is_promise(result) and result.is_pending and graphql_version() < (3, 0): + @functools.wraps(wrapped) + def nr_promise_resolver_wrapper(v): + with trace: with ErrorTrace(ignore=ignore_graphql_duplicate_exception): - trace._start_time = start_time - return await result - + return result.get() + return as_promise(nr_promise_resolver_wrapper) + elif isawaitable(result) and not is_promise(result): + # Asynchronous resolvers (returned coroutines from non-coroutine functions) # Return a coroutine that handles wrapping in a resolver trace - return _nr_coro_resolver_wrapper() + return nr_coro_resolver_wrapper(wrapped, trace, ignore_graphql_duplicate_exception, result) else: # Synchonous resolver with no exception raised - with GraphQLResolverTrace(field_name, field_parent_type=parent_type.name, field_return_type=field_return_type, field_path=field_path) as trace: - with ErrorTrace(ignore=ignore_graphql_duplicate_exception): - trace._start_time = start_time - return result + with trace: + trace.start_time = start_time + return result def bind_graphql_impl_query(schema, source, *args, **kwargs): @@ -473,17 +550,42 @@ def wrap_graphql_impl(wrapped, instance, args, kwargs): transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=10) - with GraphQLOperationTrace() as trace: - trace.statement = graphql_statement(query) + trace = GraphQLOperationTrace() - # Handle Schemas created from frameworks - if hasattr(schema, "_nr_framework"): - framework = schema._nr_framework - trace.product = framework[0] - transaction.add_framework_info(name=framework[0], version=framework[1]) + trace.statement = graphql_statement(query) - with ErrorTrace(ignore=ignore_graphql_duplicate_exception): - result = wrapped(*args, **kwargs) + # Handle Schemas created from frameworks + if hasattr(schema, "_nr_framework"): + framework = schema._nr_framework + trace.product = framework[0] + transaction.add_framework_info(name=framework[0], version=framework[1]) + + # Trace must be manually started and stopped to ensure it exists prior to and during the entire duration of the query. + # Otherwise subsequent instrumentation will not be able to find an operation trace and will have issues. + trace.__enter__() + try: + result = wrapped(*args, **kwargs) + except Exception as e: + # Execution finished synchronously, exit immediately. + notice_error(ignore=ignore_graphql_duplicate_exception) + trace.__exit__(*sys.exc_info()) + raise + else: + if is_promise(result) and result.is_pending: + # Execution promise, append callbacks to exit trace. + def on_resolve(v): + trace.__exit__(None, None, None) + return v + + def on_reject(e): + catch_promise_error(e) + trace.__exit__(e.__class__, e, e.__traceback__) + return e + + return result.then(on_resolve, on_reject) + else: + # Execution finished synchronously, exit immediately. + trace.__exit__(None, None, None) return result diff --git a/newrelic/hooks/framework_graphql_py3.py b/newrelic/hooks/framework_graphql_py3.py new file mode 100644 index 000000000..d46fb5988 --- /dev/null +++ b/newrelic/hooks/framework_graphql_py3.py @@ -0,0 +1,51 @@ +# 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 + +from newrelic.api.error_trace import ErrorTrace +from newrelic.api.function_trace import FunctionTrace + + +def nr_coro_execute_name_wrapper(wrapped, result, set_name): + @functools.wraps(wrapped) + async def _nr_coro_execute_name_wrapper(): + result_ = await result + set_name() + return result_ + + return _nr_coro_execute_name_wrapper() + + +def nr_coro_resolver_error_wrapper(wrapped, name, ignore, result, transaction): + @functools.wraps(wrapped) + async def _nr_coro_resolver_error_wrapper(): + with FunctionTrace(name, source=wrapped): + with ErrorTrace(ignore=ignore): + try: + return await result + except Exception: + transaction.set_transaction_name(name, "GraphQL", priority=15) + raise + + return _nr_coro_resolver_error_wrapper() + + +def nr_coro_resolver_wrapper(wrapped, trace, ignore, result): + @functools.wraps(wrapped) + async def _nr_coro_resolver_wrapper(): + with trace: + with ErrorTrace(ignore=ignore): + return await result + + return _nr_coro_resolver_wrapper() diff --git a/tests/component_graphqlserver/_test_graphql.py b/tests/component_graphqlserver/_test_graphql.py index db2bc696c..7a29b3a8f 100644 --- a/tests/component_graphqlserver/_test_graphql.py +++ b/tests/component_graphqlserver/_test_graphql.py @@ -12,16 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json - -import webtest from flask import Flask -from framework_graphql._target_application import _target_application as schema -from graphql_server.flask import GraphQLView as FlaskView -from graphql_server.sanic import GraphQLView as SanicView from sanic import Sanic +import json +import webtest + from testing_support.asgi_testing import AsgiTest -from framework_graphql._target_application import _target_application as schema +from framework_graphql._target_schema_sync import target_schema as schema from graphql_server.flask import GraphQLView as FlaskView from graphql_server.sanic import GraphQLView as SanicView diff --git a/tests/component_graphqlserver/test_graphql.py b/tests/component_graphqlserver/test_graphql.py index 62d96794b..f361e2193 100644 --- a/tests/component_graphqlserver/test_graphql.py +++ b/tests/component_graphqlserver/test_graphql.py @@ -287,7 +287,7 @@ def test_exception_in_resolver(target_application, field): framework, version, target_application = target_application query = "query MyQuery { %s }" % field - txn_name = "framework_graphql._target_application:resolve_error" + txn_name = "framework_graphql._target_schema_sync:resolve_error" # Metrics _test_exception_scoped_metrics = [ @@ -518,7 +518,7 @@ def _test(): def test_deepest_unique_path(target_application, query, expected_path): framework, version, target_application = target_application if expected_path == "/error": - txn_name = "framework_graphql._target_application:resolve_error" + txn_name = "framework_graphql._target_schema_sync:resolve_error" else: txn_name = "query/%s" % expected_path diff --git a/tests/framework_graphql/_target_application.py b/tests/framework_graphql/_target_application.py index 7bef5e975..903c72137 100644 --- a/tests/framework_graphql/_target_application.py +++ b/tests/framework_graphql/_target_application.py @@ -12,228 +12,91 @@ # See the License for the specific language governing permissions and # limitations under the License. -from graphql import ( - GraphQLArgument, - GraphQLField, - GraphQLInt, - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLSchema, - GraphQLString, - GraphQLUnionType, -) - -authors = [ - { - "first_name": "New", - "last_name": "Relic", - }, - { - "first_name": "Bob", - "last_name": "Smith", - }, - { - "first_name": "Leslie", - "last_name": "Jones", - }, -] - -books = [ - { - "id": 1, - "name": "Python Agent: The Book", - "isbn": "a-fake-isbn", - "author": authors[0], - "branch": "riverside", - }, - { - "id": 2, - "name": "Ollies for O11y: A Sk8er's Guide to Observability", - "isbn": "a-second-fake-isbn", - "author": authors[1], - "branch": "downtown", - }, - { - "id": 3, - "name": "[Redacted]", - "isbn": "a-third-fake-isbn", - "author": authors[2], - "branch": "riverside", - }, -] - -magazines = [ - {"id": 1, "name": "Reli Updates Weekly", "issue": 1, "branch": "riverside"}, - {"id": 2, "name": "Reli Updates Weekly", "issue": 2, "branch": "downtown"}, - {"id": 3, "name": "Node Weekly", "issue": 1, "branch": "riverside"}, -] - - -libraries = ["riverside", "downtown"] -libraries = [ - { - "id": i + 1, - "branch": branch, - "magazine": [m for m in magazines if m["branch"] == branch], - "book": [b for b in books if b["branch"] == branch], - } - for i, branch in enumerate(libraries) -] - -storage = [] - - -def resolve_library(parent, info, index): - return libraries[index] - - -def resolve_storage_add(parent, info, string): - storage.append(string) - return string - - -def resolve_storage(parent, info): - return storage - - -def resolve_search(parent, info, contains): - search_books = [b for b in books if contains in b["name"]] - search_magazines = [m for m in magazines if contains in m["name"]] - return search_books + search_magazines - - -Author = GraphQLObjectType( - "Author", - { - "first_name": GraphQLField(GraphQLString), - "last_name": GraphQLField(GraphQLString), - }, -) - -Book = GraphQLObjectType( - "Book", - { - "id": GraphQLField(GraphQLInt), - "name": GraphQLField(GraphQLString), - "isbn": GraphQLField(GraphQLString), - "author": GraphQLField(Author), - "branch": GraphQLField(GraphQLString), - }, -) - -Magazine = GraphQLObjectType( - "Magazine", - { - "id": GraphQLField(GraphQLInt), - "name": GraphQLField(GraphQLString), - "issue": GraphQLField(GraphQLInt), - "branch": GraphQLField(GraphQLString), - }, -) - - -Library = GraphQLObjectType( - "Library", - { - "id": GraphQLField(GraphQLInt), - "branch": GraphQLField(GraphQLString), - "book": GraphQLField(GraphQLList(Book)), - "magazine": GraphQLField(GraphQLList(Magazine)), - }, -) - -Storage = GraphQLList(GraphQLString) - - -def resolve_hello(root, info): - return "Hello!" - - -def resolve_echo(root, info, echo): - return echo - - -def resolve_error(root, info): - raise RuntimeError("Runtime Error!") - - -try: - hello_field = GraphQLField(GraphQLString, resolver=resolve_hello) - library_field = GraphQLField( - Library, - resolver=resolve_library, - args={"index": GraphQLArgument(GraphQLNonNull(GraphQLInt))}, - ) - search_field = GraphQLField( - GraphQLList(GraphQLUnionType("Item", (Book, Magazine), resolve_type=resolve_search)), - args={"contains": GraphQLArgument(GraphQLNonNull(GraphQLString))}, - ) - echo_field = GraphQLField( - GraphQLString, - resolver=resolve_echo, - args={"echo": GraphQLArgument(GraphQLNonNull(GraphQLString))}, - ) - storage_field = GraphQLField( - Storage, - resolver=resolve_storage, - ) - storage_add_field = GraphQLField( - Storage, - resolver=resolve_storage_add, - args={"string": GraphQLArgument(GraphQLNonNull(GraphQLString))}, - ) - error_field = GraphQLField(GraphQLString, resolver=resolve_error) - error_non_null_field = GraphQLField(GraphQLNonNull(GraphQLString), resolver=resolve_error) - error_middleware_field = GraphQLField(GraphQLString, resolver=resolve_hello) -except TypeError: - hello_field = GraphQLField(GraphQLString, resolve=resolve_hello) - library_field = GraphQLField( - Library, - resolve=resolve_library, - args={"index": GraphQLArgument(GraphQLNonNull(GraphQLInt))}, - ) - search_field = GraphQLField( - GraphQLList(GraphQLUnionType("Item", (Book, Magazine), resolve_type=resolve_search)), - args={"contains": GraphQLArgument(GraphQLNonNull(GraphQLString))}, - ) - echo_field = GraphQLField( - GraphQLString, - resolve=resolve_echo, - args={"echo": GraphQLArgument(GraphQLNonNull(GraphQLString))}, - ) - storage_field = GraphQLField( - Storage, - resolve=resolve_storage, - ) - storage_add_field = GraphQLField( - GraphQLString, - resolve=resolve_storage_add, - args={"string": GraphQLArgument(GraphQLNonNull(GraphQLString))}, - ) - error_field = GraphQLField(GraphQLString, resolve=resolve_error) - error_non_null_field = GraphQLField(GraphQLNonNull(GraphQLString), resolve=resolve_error) - error_middleware_field = GraphQLField(GraphQLString, resolve=resolve_hello) - -query = GraphQLObjectType( - name="Query", - fields={ - "hello": hello_field, - "library": library_field, - "search": search_field, - "echo": echo_field, - "storage": storage_field, - "error": error_field, - "error_non_null": error_non_null_field, - "error_middleware": error_middleware_field, - }, -) - -mutation = GraphQLObjectType( - name="Mutation", - fields={ - "storage_add": storage_add_field, - }, -) - -_target_application = GraphQLSchema(query=query, mutation=mutation) +from graphql import __version__ as version +from graphql.language.source import Source + +from newrelic.packages import six +from newrelic.hooks.framework_graphql import is_promise + +from _target_schema_sync import target_schema as target_schema_sync + + +is_graphql_2 = int(version.split(".")[0]) == 2 + + +def check_response(query, response): + if isinstance(query, str) and "error" not in query or isinstance(query, Source) and "error" not in query.body: + assert not response.errors, response.errors + assert response.data + else: + assert response.errors + + +def run_sync(schema): + def _run_sync(query, middleware=None): + try: + from graphql import graphql_sync as graphql + except ImportError: + from graphql import graphql + + response = graphql(schema, query, middleware=middleware) + + check_response(query, response) + + return response.data + + return _run_sync + + +def run_async(schema): + import asyncio + from graphql import graphql + + def _run_async(query, middleware=None): + coro = graphql(schema, query, middleware=middleware) + loop = asyncio.get_event_loop() + response = loop.run_until_complete(coro) + + check_response(query, response) + + return response.data + + return _run_async + + +def run_promise(schema, scheduler): + from graphql import graphql + from promise import set_default_scheduler + + def _run_promise(query, middleware=None): + set_default_scheduler(scheduler) + + promise = graphql(schema, query, middleware=middleware, return_promise=True) + response = promise.get() + + check_response(query, response) + + return response.data + + return _run_promise + + +target_application = { + "sync-sync": run_sync(target_schema_sync), +} + +if is_graphql_2: + from _target_schema_promise import target_schema as target_schema_promise + from promise.schedulers.immediate import ImmediateScheduler + + if six.PY3: + from promise.schedulers.asyncio import AsyncioScheduler as AsyncScheduler + else: + from promise.schedulers.thread import ThreadScheduler as AsyncScheduler + + target_application["sync-promise"] = run_promise(target_schema_promise, ImmediateScheduler()) + target_application["async-promise"] = run_promise(target_schema_promise, AsyncScheduler()) +elif six.PY3: + from _target_schema_async import target_schema as target_schema_async + target_application["async-sync"] = run_async(target_schema_sync) + target_application["async-async"] = run_async(target_schema_async) diff --git a/tests/framework_graphql/_target_schema_async.py b/tests/framework_graphql/_target_schema_async.py new file mode 100644 index 000000000..b57c36bff --- /dev/null +++ b/tests/framework_graphql/_target_schema_async.py @@ -0,0 +1,187 @@ +# 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. + +from graphql import ( + GraphQLArgument, + GraphQLField, + GraphQLInt, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, + GraphQLUnionType, +) + +try: + from _target_schema_sync import books, libraries, magazines +except ImportError: + from framework_graphql._target_schema_sync import books, libraries, magazines + +storage = [] + + +async def resolve_library(parent, info, index): + return libraries[index] + + +async def resolve_storage_add(parent, info, string): + storage.append(string) + return string + + +async def resolve_storage(parent, info): + return [storage.pop()] + + +async def resolve_search(parent, info, contains): + search_books = [b for b in books if contains in b["name"]] + search_magazines = [m for m in magazines if contains in m["name"]] + return search_books + search_magazines + + +Author = GraphQLObjectType( + "Author", + { + "first_name": GraphQLField(GraphQLString), + "last_name": GraphQLField(GraphQLString), + }, +) + +Book = GraphQLObjectType( + "Book", + { + "id": GraphQLField(GraphQLInt), + "name": GraphQLField(GraphQLString), + "isbn": GraphQLField(GraphQLString), + "author": GraphQLField(Author), + "branch": GraphQLField(GraphQLString), + }, +) + +Magazine = GraphQLObjectType( + "Magazine", + { + "id": GraphQLField(GraphQLInt), + "name": GraphQLField(GraphQLString), + "issue": GraphQLField(GraphQLInt), + "branch": GraphQLField(GraphQLString), + }, +) + + +Library = GraphQLObjectType( + "Library", + { + "id": GraphQLField(GraphQLInt), + "branch": GraphQLField(GraphQLString), + "book": GraphQLField(GraphQLList(Book)), + "magazine": GraphQLField(GraphQLList(Magazine)), + }, +) + +Storage = GraphQLList(GraphQLString) + + +async def resolve_hello(root, info): + return "Hello!" + + +async def resolve_echo(root, info, echo): + return echo + + +async def resolve_error(root, info): + raise RuntimeError("Runtime Error!") + + +try: + hello_field = GraphQLField(GraphQLString, resolver=resolve_hello) + library_field = GraphQLField( + Library, + resolver=resolve_library, + args={"index": GraphQLArgument(GraphQLNonNull(GraphQLInt))}, + ) + search_field = GraphQLField( + GraphQLList(GraphQLUnionType("Item", (Book, Magazine), resolve_type=resolve_search)), + args={"contains": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + echo_field = GraphQLField( + GraphQLString, + resolver=resolve_echo, + args={"echo": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + storage_field = GraphQLField( + Storage, + resolver=resolve_storage, + ) + storage_add_field = GraphQLField( + GraphQLString, + resolver=resolve_storage_add, + args={"string": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + error_field = GraphQLField(GraphQLString, resolver=resolve_error) + error_non_null_field = GraphQLField(GraphQLNonNull(GraphQLString), resolver=resolve_error) + error_middleware_field = GraphQLField(GraphQLString, resolver=resolve_hello) +except TypeError: + hello_field = GraphQLField(GraphQLString, resolve=resolve_hello) + library_field = GraphQLField( + Library, + resolve=resolve_library, + args={"index": GraphQLArgument(GraphQLNonNull(GraphQLInt))}, + ) + search_field = GraphQLField( + GraphQLList(GraphQLUnionType("Item", (Book, Magazine), resolve_type=resolve_search)), + args={"contains": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + echo_field = GraphQLField( + GraphQLString, + resolve=resolve_echo, + args={"echo": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + storage_field = GraphQLField( + Storage, + resolve=resolve_storage, + ) + storage_add_field = GraphQLField( + GraphQLString, + resolve=resolve_storage_add, + args={"string": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + error_field = GraphQLField(GraphQLString, resolve=resolve_error) + error_non_null_field = GraphQLField(GraphQLNonNull(GraphQLString), resolve=resolve_error) + error_middleware_field = GraphQLField(GraphQLString, resolve=resolve_hello) + +query = GraphQLObjectType( + name="Query", + fields={ + "hello": hello_field, + "library": library_field, + "search": search_field, + "echo": echo_field, + "storage": storage_field, + "error": error_field, + "error_non_null": error_non_null_field, + "error_middleware": error_middleware_field, + }, +) + +mutation = GraphQLObjectType( + name="Mutation", + fields={ + "storage_add": storage_add_field, + }, +) + +target_schema = GraphQLSchema(query=query, mutation=mutation) diff --git a/tests/framework_graphql/_target_schema_promise.py b/tests/framework_graphql/_target_schema_promise.py new file mode 100644 index 000000000..ea08639e1 --- /dev/null +++ b/tests/framework_graphql/_target_schema_promise.py @@ -0,0 +1,194 @@ +# 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. + +from graphql import ( + GraphQLArgument, + GraphQLField, + GraphQLInt, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, + GraphQLUnionType, +) +from promise import Promise, promisify + +try: + from _target_schema_sync import books, libraries, magazines +except ImportError: + from framework_graphql._target_schema_sync import books, libraries, magazines + +storage = [] + + +@promisify +def resolve_library(parent, info, index): + return libraries[index] + + +@promisify +def resolve_storage_add(parent, info, string): + storage.append(string) + return string + + +@promisify +def resolve_storage(parent, info): + return [storage.pop()] + + +def resolve_search(parent, info, contains): + search_books = [b for b in books if contains in b["name"]] + search_magazines = [m for m in magazines if contains in m["name"]] + return search_books + search_magazines + + +Author = GraphQLObjectType( + "Author", + { + "first_name": GraphQLField(GraphQLString), + "last_name": GraphQLField(GraphQLString), + }, +) + +Book = GraphQLObjectType( + "Book", + { + "id": GraphQLField(GraphQLInt), + "name": GraphQLField(GraphQLString), + "isbn": GraphQLField(GraphQLString), + "author": GraphQLField(Author), + "branch": GraphQLField(GraphQLString), + }, +) + +Magazine = GraphQLObjectType( + "Magazine", + { + "id": GraphQLField(GraphQLInt), + "name": GraphQLField(GraphQLString), + "issue": GraphQLField(GraphQLInt), + "branch": GraphQLField(GraphQLString), + }, +) + + +Library = GraphQLObjectType( + "Library", + { + "id": GraphQLField(GraphQLInt), + "branch": GraphQLField(GraphQLString), + "book": GraphQLField(GraphQLList(Book)), + "magazine": GraphQLField(GraphQLList(Magazine)), + }, +) + +Storage = GraphQLList(GraphQLString) + + +@promisify +def resolve_hello(root, info): + return "Hello!" + + +@promisify +def resolve_echo(root, info, echo): + return echo + + +@promisify +def resolve_error(root, info): + raise RuntimeError("Runtime Error!") + + +try: + hello_field = GraphQLField(GraphQLString, resolver=resolve_hello) + library_field = GraphQLField( + Library, + resolver=resolve_library, + args={"index": GraphQLArgument(GraphQLNonNull(GraphQLInt))}, + ) + search_field = GraphQLField( + GraphQLList(GraphQLUnionType("Item", (Book, Magazine), resolve_type=resolve_search)), + args={"contains": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + echo_field = GraphQLField( + GraphQLString, + resolver=resolve_echo, + args={"echo": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + storage_field = GraphQLField( + Storage, + resolver=resolve_storage, + ) + storage_add_field = GraphQLField( + GraphQLString, + resolver=resolve_storage_add, + args={"string": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + error_field = GraphQLField(GraphQLString, resolver=resolve_error) + error_non_null_field = GraphQLField(GraphQLNonNull(GraphQLString), resolver=resolve_error) + error_middleware_field = GraphQLField(GraphQLString, resolver=resolve_hello) +except TypeError: + hello_field = GraphQLField(GraphQLString, resolve=resolve_hello) + library_field = GraphQLField( + Library, + resolve=resolve_library, + args={"index": GraphQLArgument(GraphQLNonNull(GraphQLInt))}, + ) + search_field = GraphQLField( + GraphQLList(GraphQLUnionType("Item", (Book, Magazine), resolve_type=resolve_search)), + args={"contains": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + echo_field = GraphQLField( + GraphQLString, + resolve=resolve_echo, + args={"echo": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + storage_field = GraphQLField( + Storage, + resolve=resolve_storage, + ) + storage_add_field = GraphQLField( + GraphQLString, + resolve=resolve_storage_add, + args={"string": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + error_field = GraphQLField(GraphQLString, resolve=resolve_error) + error_non_null_field = GraphQLField(GraphQLNonNull(GraphQLString), resolve=resolve_error) + error_middleware_field = GraphQLField(GraphQLString, resolve=resolve_hello) + +query = GraphQLObjectType( + name="Query", + fields={ + "hello": hello_field, + "library": library_field, + "search": search_field, + "echo": echo_field, + "storage": storage_field, + "error": error_field, + "error_non_null": error_non_null_field, + "error_middleware": error_middleware_field, + }, +) + +mutation = GraphQLObjectType( + name="Mutation", + fields={ + "storage_add": storage_add_field, + }, +) + +target_schema = GraphQLSchema(query=query, mutation=mutation) diff --git a/tests/framework_graphql/_target_schema_sync.py b/tests/framework_graphql/_target_schema_sync.py new file mode 100644 index 000000000..ddfd8d190 --- /dev/null +++ b/tests/framework_graphql/_target_schema_sync.py @@ -0,0 +1,239 @@ +# 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 applic`ab`le 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. + +from graphql import ( + GraphQLArgument, + GraphQLField, + GraphQLInt, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, + GraphQLUnionType, +) + +authors = [ + { + "first_name": "New", + "last_name": "Relic", + }, + { + "first_name": "Bob", + "last_name": "Smith", + }, + { + "first_name": "Leslie", + "last_name": "Jones", + }, +] + +books = [ + { + "id": 1, + "name": "Python Agent: The Book", + "isbn": "a-fake-isbn", + "author": authors[0], + "branch": "riverside", + }, + { + "id": 2, + "name": "Ollies for O11y: A Sk8er's Guide to Observability", + "isbn": "a-second-fake-isbn", + "author": authors[1], + "branch": "downtown", + }, + { + "id": 3, + "name": "[Redacted]", + "isbn": "a-third-fake-isbn", + "author": authors[2], + "branch": "riverside", + }, +] + +magazines = [ + {"id": 1, "name": "Reli Updates Weekly", "issue": 1, "branch": "riverside"}, + {"id": 2, "name": "Reli Updates Weekly", "issue": 2, "branch": "downtown"}, + {"id": 3, "name": "Node Weekly", "issue": 1, "branch": "riverside"}, +] + + +libraries = ["riverside", "downtown"] +libraries = [ + { + "id": i + 1, + "branch": branch, + "magazine": [m for m in magazines if m["branch"] == branch], + "book": [b for b in books if b["branch"] == branch], + } + for i, branch in enumerate(libraries) +] + +storage = [] + + +def resolve_library(parent, info, index): + return libraries[index] + + +def resolve_storage_add(parent, info, string): + storage.append(string) + return string + + +def resolve_storage(parent, info): + return [storage.pop()] + + +def resolve_search(parent, info, contains): + search_books = [b for b in books if contains in b["name"]] + search_magazines = [m for m in magazines if contains in m["name"]] + return search_books + search_magazines + + +Author = GraphQLObjectType( + "Author", + { + "first_name": GraphQLField(GraphQLString), + "last_name": GraphQLField(GraphQLString), + }, +) + +Book = GraphQLObjectType( + "Book", + { + "id": GraphQLField(GraphQLInt), + "name": GraphQLField(GraphQLString), + "isbn": GraphQLField(GraphQLString), + "author": GraphQLField(Author), + "branch": GraphQLField(GraphQLString), + }, +) + +Magazine = GraphQLObjectType( + "Magazine", + { + "id": GraphQLField(GraphQLInt), + "name": GraphQLField(GraphQLString), + "issue": GraphQLField(GraphQLInt), + "branch": GraphQLField(GraphQLString), + }, +) + + +Library = GraphQLObjectType( + "Library", + { + "id": GraphQLField(GraphQLInt), + "branch": GraphQLField(GraphQLString), + "book": GraphQLField(GraphQLList(Book)), + "magazine": GraphQLField(GraphQLList(Magazine)), + }, +) + +Storage = GraphQLList(GraphQLString) + + +def resolve_hello(root, info): + return "Hello!" + + +def resolve_echo(root, info, echo): + return echo + + +def resolve_error(root, info): + raise RuntimeError("Runtime Error!") + + +try: + hello_field = GraphQLField(GraphQLString, resolver=resolve_hello) + library_field = GraphQLField( + Library, + resolver=resolve_library, + args={"index": GraphQLArgument(GraphQLNonNull(GraphQLInt))}, + ) + search_field = GraphQLField( + GraphQLList(GraphQLUnionType("Item", (Book, Magazine), resolve_type=resolve_search)), + args={"contains": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + echo_field = GraphQLField( + GraphQLString, + resolver=resolve_echo, + args={"echo": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + storage_field = GraphQLField( + Storage, + resolver=resolve_storage, + ) + storage_add_field = GraphQLField( + GraphQLString, + resolver=resolve_storage_add, + args={"string": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + error_field = GraphQLField(GraphQLString, resolver=resolve_error) + error_non_null_field = GraphQLField(GraphQLNonNull(GraphQLString), resolver=resolve_error) + error_middleware_field = GraphQLField(GraphQLString, resolver=resolve_hello) +except TypeError: + hello_field = GraphQLField(GraphQLString, resolve=resolve_hello) + library_field = GraphQLField( + Library, + resolve=resolve_library, + args={"index": GraphQLArgument(GraphQLNonNull(GraphQLInt))}, + ) + search_field = GraphQLField( + GraphQLList(GraphQLUnionType("Item", (Book, Magazine), resolve_type=resolve_search)), + args={"contains": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + echo_field = GraphQLField( + GraphQLString, + resolve=resolve_echo, + args={"echo": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + storage_field = GraphQLField( + Storage, + resolve=resolve_storage, + ) + storage_add_field = GraphQLField( + GraphQLString, + resolve=resolve_storage_add, + args={"string": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + error_field = GraphQLField(GraphQLString, resolve=resolve_error) + error_non_null_field = GraphQLField(GraphQLNonNull(GraphQLString), resolve=resolve_error) + error_middleware_field = GraphQLField(GraphQLString, resolve=resolve_hello) + +query = GraphQLObjectType( + name="Query", + fields={ + "hello": hello_field, + "library": library_field, + "search": search_field, + "echo": echo_field, + "storage": storage_field, + "error": error_field, + "error_non_null": error_non_null_field, + "error_middleware": error_middleware_field, + }, +) + +mutation = GraphQLObjectType( + name="Mutation", + fields={ + "storage_add": storage_add_field, + }, +) + +target_schema = GraphQLSchema(query=query, mutation=mutation) diff --git a/tests/framework_graphql/conftest.py b/tests/framework_graphql/conftest.py index 2084a5bb4..9aa8b6540 100644 --- a/tests/framework_graphql/conftest.py +++ b/tests/framework_graphql/conftest.py @@ -13,13 +13,14 @@ # limitations under the License. import pytest -import six from testing_support.fixtures import ( code_coverage_fixture, collector_agent_registration_fixture, collector_available_fixture, ) +from newrelic.packages import six + _coverage_source = [ "newrelic.hooks.framework_graphql", ] @@ -39,12 +40,19 @@ default_settings=_default_settings, ) +apps = ["sync-sync", "async-sync", "async-async", "sync-promise", "async-promise"] + + +@pytest.fixture(scope="session", params=apps) +def target_application(request): + from _target_application import target_application -@pytest.fixture(scope="session") -def app(): - from _target_application import _target_application + app = target_application.get(request.param, None) + if app is None: + pytest.skip("Unsupported combination.") + return - return _target_application + return "GraphQL", None, app, True, request.param.split("-")[1] if six.PY2: diff --git a/tests/framework_graphql/test_application.py b/tests/framework_graphql/test_application.py index 770c6f6e1..3f765bece 100644 --- a/tests/framework_graphql/test_application.py +++ b/tests/framework_graphql/test_application.py @@ -18,14 +18,26 @@ validate_transaction_errors, validate_transaction_metrics, ) +from testing_support.validators.validate_code_level_metrics import ( + validate_code_level_metrics, +) from testing_support.validators.validate_span_events import validate_span_events from testing_support.validators.validate_transaction_count import ( validate_transaction_count, ) -from testing_support.validators.validate_code_level_metrics import validate_code_level_metrics from newrelic.api.background_task import background_task from newrelic.common.object_names import callable_name +from newrelic.packages import six + + +def conditional_decorator(decorator, condition): + def _conditional_decorator(func): + if not condition: + return func + return decorator(func) + + return _conditional_decorator @pytest.fixture(scope="session") @@ -36,16 +48,6 @@ def is_graphql_2(): return major_version == 2 -@pytest.fixture(scope="session") -def graphql_run(): - try: - from graphql import graphql_sync as graphql - except ImportError: - from graphql import graphql - - return graphql - - def to_graphql_source(query): def delay_import(): try: @@ -75,58 +77,78 @@ def error_middleware(next, root, info, **args): raise RuntimeError("Runtime Error!") +example_middleware = [example_middleware] +error_middleware = [error_middleware] + +if six.PY3: + from test_application_async import error_middleware_async, example_middleware_async + + example_middleware.append(example_middleware_async) + error_middleware.append(error_middleware_async) + + _runtime_error_name = callable_name(RuntimeError) _test_runtime_error = [(_runtime_error_name, "Runtime Error!")] -_graphql_base_rollup_metrics = [ - ("OtherTransaction/all", 1), - ("GraphQL/all", 1), - ("GraphQL/allOther", 1), - ("GraphQL/GraphQL/all", 1), - ("GraphQL/GraphQL/allOther", 1), -] -def test_basic(app, graphql_run): - from graphql import __version__ as version +def _graphql_base_rollup_metrics(framework, version, background_task=True): + from graphql import __version__ as graphql_version - FRAMEWORK_METRICS = [ - ("Python/Framework/GraphQL/%s" % version, 1), + metrics = [ + ("Python/Framework/GraphQL/%s" % graphql_version, 1), + ("GraphQL/all", 1), + ("GraphQL/%s/all" % framework, 1), ] + if background_task: + metrics.extend( + [ + ("GraphQL/allOther", 1), + ("GraphQL/%s/allOther" % framework, 1), + ] + ) + else: + metrics.extend( + [ + ("GraphQL/allWeb", 1), + ("GraphQL/%s/allWeb" % framework, 1), + ] + ) + + if framework != "GraphQL": + metrics.append(("Python/Framework/%s/%s" % (framework, version), 1)) + + return metrics + + +def test_basic(target_application): + framework, version, target_application, is_bg, schema_type = target_application @validate_transaction_metrics( "query//hello", "GraphQL", - rollup_metrics=_graphql_base_rollup_metrics + FRAMEWORK_METRICS, - background_task=True, + rollup_metrics=_graphql_base_rollup_metrics(framework, version, is_bg), + background_task=is_bg, ) - @background_task() + @conditional_decorator(background_task(), is_bg) def _test(): - response = graphql_run(app, '{ hello }') - assert not response.errors - + response = target_application("{ hello }") + assert response["hello"] == "Hello!" + _test() @dt_enabled -def test_query_and_mutation(app, graphql_run, is_graphql_2): - from graphql import __version__ as version +def test_query_and_mutation(target_application, is_graphql_2): + framework, version, target_application, is_bg, schema_type = target_application - FRAMEWORK_METRICS = [ - ("Python/Framework/GraphQL/%s" % version, 1), - ] _test_mutation_scoped_metrics = [ - ("GraphQL/resolve/GraphQL/storage", 1), - ("GraphQL/resolve/GraphQL/storage_add", 1), - ("GraphQL/operation/GraphQL/query//storage", 1), - ("GraphQL/operation/GraphQL/mutation//storage_add", 1), + ("GraphQL/resolve/%s/storage_add" % framework, 1), + ("GraphQL/operation/%s/mutation//storage_add" % framework, 1), + ] + _test_query_scoped_metrics = [ + ("GraphQL/resolve/%s/storage" % framework, 1), + ("GraphQL/operation/%s/query//storage" % framework, 1), ] - _test_mutation_unscoped_metrics = [ - ("OtherTransaction/all", 1), - ("GraphQL/all", 2), - ("GraphQL/GraphQL/all", 2), - ("GraphQL/allOther", 2), - ("GraphQL/GraphQL/allOther", 2), - ] + _test_mutation_scoped_metrics _expected_mutation_operation_attributes = { "graphql.operation.type": "mutation", @@ -136,7 +158,7 @@ def test_query_and_mutation(app, graphql_run, is_graphql_2): "graphql.field.name": "storage_add", "graphql.field.parentType": "Mutation", "graphql.field.path": "storage_add", - "graphql.field.returnType": "[String]" if is_graphql_2 else "String", + "graphql.field.returnType": "String", } _expected_query_operation_attributes = { "graphql.operation.type": "query", @@ -149,75 +171,97 @@ def test_query_and_mutation(app, graphql_run, is_graphql_2): "graphql.field.returnType": "[String]", } - @validate_code_level_metrics("_target_application", "resolve_storage") - @validate_code_level_metrics("_target_application", "resolve_storage_add") + @validate_code_level_metrics("_target_schema_%s" % schema_type, "resolve_storage_add") @validate_transaction_metrics( - "query//storage", + "mutation//storage_add", "GraphQL", scoped_metrics=_test_mutation_scoped_metrics, - rollup_metrics=_test_mutation_unscoped_metrics + FRAMEWORK_METRICS, - background_task=True, + rollup_metrics=_test_mutation_scoped_metrics + _graphql_base_rollup_metrics(framework, version, is_bg), + background_task=is_bg, ) @validate_span_events(exact_agents=_expected_mutation_operation_attributes) @validate_span_events(exact_agents=_expected_mutation_resolver_attributes) + @conditional_decorator(background_task(), is_bg) + def _mutation(): + response = target_application('mutation { storage_add(string: "abc") }') + assert response["storage_add"] == "abc" + + @validate_code_level_metrics("_target_schema_%s" % schema_type, "resolve_storage") + @validate_transaction_metrics( + "query//storage", + "GraphQL", + scoped_metrics=_test_query_scoped_metrics, + rollup_metrics=_test_query_scoped_metrics + _graphql_base_rollup_metrics(framework, version, is_bg), + background_task=is_bg, + ) @validate_span_events(exact_agents=_expected_query_operation_attributes) @validate_span_events(exact_agents=_expected_query_resolver_attributes) - @background_task() - def _test(): - response = graphql_run(app, 'mutation { storage_add(string: "abc") }') - assert not response.errors - response = graphql_run(app, "query { storage }") - assert not response.errors + @conditional_decorator(background_task(), is_bg) + def _query(): + response = target_application("query { storage }") + assert response["storage"] == ["abc"] - # These are separate assertions because pypy stores 'abc' as a unicode string while other Python versions do not - assert "storage" in str(response.data) - assert "abc" in str(response.data) - - _test() + _mutation() + _query() +@pytest.mark.parametrize("middleware", example_middleware) @dt_enabled -def test_middleware(app, graphql_run, is_graphql_2): +def test_middleware(target_application, middleware): + framework, version, target_application, is_bg, schema_type = target_application + + name = "%s:%s" % (middleware.__module__, middleware.__name__) + if "async" in name: + if schema_type != "async": + pytest.skip("Async middleware not supported in sync applications.") + _test_middleware_metrics = [ ("GraphQL/operation/GraphQL/query//hello", 1), ("GraphQL/resolve/GraphQL/hello", 1), - ("Function/test_application:example_middleware", 1), + ("Function/%s" % name, 1), ] - @validate_code_level_metrics("test_application", "example_middleware") - @validate_code_level_metrics("_target_application", "resolve_hello") + @validate_code_level_metrics(*name.split(":")) + @validate_code_level_metrics("_target_schema_%s" % schema_type, "resolve_hello") @validate_transaction_metrics( "query//hello", "GraphQL", scoped_metrics=_test_middleware_metrics, - rollup_metrics=_test_middleware_metrics + _graphql_base_rollup_metrics, - background_task=True, + rollup_metrics=_test_middleware_metrics + _graphql_base_rollup_metrics(framework, version, is_bg), + background_task=is_bg, ) # Span count 5: Transaction, Operation, Middleware, and 1 Resolver and Resolver Function @validate_span_events(count=5) - @background_task() + @conditional_decorator(background_task(), is_bg) def _test(): - response = graphql_run(app, "{ hello }", middleware=[example_middleware]) - assert not response.errors - assert "Hello!" in str(response.data) + response = target_application("{ hello }", middleware=[middleware]) + assert response["hello"] == "Hello!" _test() +@pytest.mark.parametrize("middleware", error_middleware) @dt_enabled -def test_exception_in_middleware(app, graphql_run): - query = "query MyQuery { hello }" - field = "hello" +def test_exception_in_middleware(target_application, middleware): + framework, version, target_application, is_bg, schema_type = target_application + query = "query MyQuery { error_middleware }" + field = "error_middleware" + + name = "%s:%s" % (middleware.__module__, middleware.__name__) + if "async" in name: + if schema_type != "async": + pytest.skip("Async middleware not supported in sync applications.") # Metrics _test_exception_scoped_metrics = [ - ("GraphQL/operation/GraphQL/query/MyQuery/%s" % field, 1), - ("GraphQL/resolve/GraphQL/%s" % field, 1), + ("GraphQL/operation/%s/query/MyQuery/%s" % (framework, field), 1), + ("GraphQL/resolve/%s/%s" % (framework, field), 1), + ("Function/%s" % name, 1), ] _test_exception_rollup_metrics = [ ("Errors/all", 1), - ("Errors/allOther", 1), - ("Errors/OtherTransaction/GraphQL/test_application:error_middleware", 1), + ("Errors/all%s" % ("Other" if is_bg else "Web"), 1), + ("Errors/%sTransaction/GraphQL/%s" % ("Other" if is_bg else "Web", name), 1), ] + _test_exception_scoped_metrics # Attributes @@ -234,39 +278,39 @@ def test_exception_in_middleware(app, graphql_run): } @validate_transaction_metrics( - "test_application:error_middleware", + name, "GraphQL", scoped_metrics=_test_exception_scoped_metrics, - rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics, - background_task=True, + rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics(framework, version, is_bg), + background_task=is_bg, ) @validate_span_events(exact_agents=_expected_exception_operation_attributes) @validate_span_events(exact_agents=_expected_exception_resolver_attributes) @validate_transaction_errors(errors=_test_runtime_error) - @background_task() + @conditional_decorator(background_task(), is_bg) def _test(): - response = graphql_run(app, query, middleware=[error_middleware]) - assert response.errors + response = target_application(query, middleware=[middleware]) _test() @pytest.mark.parametrize("field", ("error", "error_non_null")) @dt_enabled -def test_exception_in_resolver(app, graphql_run, field): +def test_exception_in_resolver(target_application, field): + framework, version, target_application, is_bg, schema_type = target_application query = "query MyQuery { %s }" % field - txn_name = "_target_application:resolve_error" + txn_name = "_target_schema_%s:resolve_error" % schema_type # Metrics _test_exception_scoped_metrics = [ - ("GraphQL/operation/GraphQL/query/MyQuery/%s" % field, 1), - ("GraphQL/resolve/GraphQL/%s" % field, 1), + ("GraphQL/operation/%s/query/MyQuery/%s" % (framework, field), 1), + ("GraphQL/resolve/%s/%s" % (framework, field), 1), ] _test_exception_rollup_metrics = [ ("Errors/all", 1), - ("Errors/allOther", 1), - ("Errors/OtherTransaction/GraphQL/%s" % txn_name, 1), + ("Errors/all%s" % ("Other" if is_bg else "Web"), 1), + ("Errors/%sTransaction/GraphQL/%s" % ("Other" if is_bg else "Web", txn_name), 1), ] + _test_exception_scoped_metrics # Attributes @@ -286,16 +330,15 @@ def test_exception_in_resolver(app, graphql_run, field): txn_name, "GraphQL", scoped_metrics=_test_exception_scoped_metrics, - rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics, - background_task=True, + rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics(framework, version, is_bg), + background_task=is_bg, ) @validate_span_events(exact_agents=_expected_exception_operation_attributes) @validate_span_events(exact_agents=_expected_exception_resolver_attributes) @validate_transaction_errors(errors=_test_runtime_error) - @background_task() + @conditional_decorator(background_task(), is_bg) def _test(): - response = graphql_run(app, query) - assert response.errors + response = target_application(query) _test() @@ -304,11 +347,12 @@ def _test(): @pytest.mark.parametrize( "query,exc_class", [ - ("query MyQuery { missing_field }", "GraphQLError"), + ("query MyQuery { error_missing_field }", "GraphQLError"), ("{ syntax_error ", "graphql.error.syntax_error:GraphQLSyntaxError"), ], ) -def test_exception_in_validation(app, graphql_run, is_graphql_2, query, exc_class): +def test_exception_in_validation(target_application, is_graphql_2, query, exc_class): + framework, version, target_application, is_bg, schema_type = target_application if "syntax" in query: txn_name = "graphql.language.parser:parse" else: @@ -324,12 +368,12 @@ def test_exception_in_validation(app, graphql_run, is_graphql_2, query, exc_clas exc_class = callable_name(GraphQLError) _test_exception_scoped_metrics = [ - # ('GraphQL/operation/GraphQL///', 1), + ("GraphQL/operation/%s///" % framework, 1), ] _test_exception_rollup_metrics = [ ("Errors/all", 1), - ("Errors/allOther", 1), - ("Errors/OtherTransaction/GraphQL/%s" % txn_name, 1), + ("Errors/all%s" % ("Other" if is_bg else "Web"), 1), + ("Errors/%sTransaction/GraphQL/%s" % ("Other" if is_bg else "Web", txn_name), 1), ] + _test_exception_scoped_metrics # Attributes @@ -343,22 +387,22 @@ def test_exception_in_validation(app, graphql_run, is_graphql_2, query, exc_clas txn_name, "GraphQL", scoped_metrics=_test_exception_scoped_metrics, - rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics, - background_task=True, + rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics(framework, version, is_bg), + background_task=is_bg, ) @validate_span_events(exact_agents=_expected_exception_operation_attributes) @validate_transaction_errors(errors=[exc_class]) - @background_task() + @conditional_decorator(background_task(), is_bg) def _test(): - response = graphql_run(app, query) - assert response.errors + response = target_application(query) _test() @dt_enabled -def test_operation_metrics_and_attrs(app, graphql_run): - operation_metrics = [("GraphQL/operation/GraphQL/query/MyQuery/library", 1)] +def test_operation_metrics_and_attrs(target_application): + framework, version, target_application, is_bg, schema_type = target_application + operation_metrics = [("GraphQL/operation/%s/query/MyQuery/library" % framework, 1)] operation_attrs = { "graphql.operation.type": "query", "graphql.operation.name": "MyQuery", @@ -368,27 +412,26 @@ def test_operation_metrics_and_attrs(app, graphql_run): "query/MyQuery/library", "GraphQL", scoped_metrics=operation_metrics, - rollup_metrics=operation_metrics + _graphql_base_rollup_metrics, - background_task=True, + rollup_metrics=operation_metrics + _graphql_base_rollup_metrics(framework, version, is_bg), + background_task=is_bg, ) # Span count 16: Transaction, Operation, and 7 Resolvers and Resolver functions # library, library.name, library.book # library.book.name and library.book.id for each book resolved (in this case 2) @validate_span_events(count=16) @validate_span_events(exact_agents=operation_attrs) - @background_task() + @conditional_decorator(background_task(), is_bg) def _test(): - response = graphql_run( - app, "query MyQuery { library(index: 0) { branch, book { id, name } } }" - ) - assert not response.errors + response = target_application("query MyQuery { library(index: 0) { branch, book { id, name } } }") _test() @dt_enabled -def test_field_resolver_metrics_and_attrs(app, graphql_run): - field_resolver_metrics = [("GraphQL/resolve/GraphQL/hello", 1)] +def test_field_resolver_metrics_and_attrs(target_application): + framework, version, target_application, is_bg, schema_type = target_application + field_resolver_metrics = [("GraphQL/resolve/%s/hello" % framework, 1)] + graphql_attrs = { "graphql.field.name": "hello", "graphql.field.parentType": "Query", @@ -400,17 +443,16 @@ def test_field_resolver_metrics_and_attrs(app, graphql_run): "query//hello", "GraphQL", scoped_metrics=field_resolver_metrics, - rollup_metrics=field_resolver_metrics + _graphql_base_rollup_metrics, - background_task=True, + rollup_metrics=field_resolver_metrics + _graphql_base_rollup_metrics(framework, version, is_bg), + background_task=is_bg, ) # Span count 4: Transaction, Operation, and 1 Resolver and Resolver function @validate_span_events(count=4) @validate_span_events(exact_agents=graphql_attrs) - @background_task() + @conditional_decorator(background_task(), is_bg) def _test(): - response = graphql_run(app, "{ hello }") - assert not response.errors - assert "Hello!" in str(response.data) + response = target_application("{ hello }") + assert response["hello"] == "Hello!" _test() @@ -433,18 +475,19 @@ def _test(): @dt_enabled @pytest.mark.parametrize("query,obfuscated", _test_queries) -def test_query_obfuscation(app, graphql_run, query, obfuscated): +def test_query_obfuscation(target_application, query, obfuscated): + framework, version, target_application, is_bg, schema_type = target_application graphql_attrs = {"graphql.operation.query": obfuscated} if callable(query): + if framework != "GraphQL": + pytest.skip("Source query objects not tested outside of graphql-core") query = query() @validate_span_events(exact_agents=graphql_attrs) - @background_task() + @conditional_decorator(background_task(), is_bg) def _test(): - response = graphql_run(app, query) - if not isinstance(query, str) or "error" not in query: - assert not response.errors + response = target_application(query) _test() @@ -489,28 +532,31 @@ def _test(): @dt_enabled @pytest.mark.parametrize("query,expected_path", _test_queries) -def test_deepest_unique_path(app, graphql_run, query, expected_path): +def test_deepest_unique_path(target_application, query, expected_path): + framework, version, target_application, is_bg, schema_type = target_application if expected_path == "/error": - txn_name = "_target_application:resolve_error" + txn_name = "_target_schema_%s:resolve_error" % schema_type else: txn_name = "query/%s" % expected_path @validate_transaction_metrics( txn_name, "GraphQL", - background_task=True, + background_task=is_bg, ) - @background_task() + @conditional_decorator(background_task(), is_bg) def _test(): - response = graphql_run(app, query) - if "error" not in query: - assert not response.errors + response = target_application(query) _test() -@validate_transaction_count(0) -@background_task() -def test_ignored_introspection_transactions(app, graphql_run): - response = graphql_run(app, "{ __schema { types { name } } }") - assert not response.errors +def test_ignored_introspection_transactions(target_application): + framework, version, target_application, is_bg, schema_type = target_application + + @validate_transaction_count(0) + @background_task() + def _test(): + response = target_application("{ __schema { types { name } } }") + + _test() diff --git a/tests/framework_graphql/test_application_async.py b/tests/framework_graphql/test_application_async.py index 19b8b1493..39c1871ef 100644 --- a/tests/framework_graphql/test_application_async.py +++ b/tests/framework_graphql/test_application_async.py @@ -12,98 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -import asyncio +from inspect import isawaitable -import pytest -from test_application import is_graphql_2 -from testing_support.fixtures import dt_enabled, validate_transaction_metrics -from testing_support.validators.validate_span_events import validate_span_events -from newrelic.api.background_task import background_task +# Async Functions not allowed in Py2 +async def example_middleware_async(next, root, info, **args): + return_value = next(root, info, **args) + if isawaitable(return_value): + return await return_value + return return_value -@pytest.fixture(scope="session") -def graphql_run_async(): - from graphql import __version__ as version - from graphql import graphql - - major_version = int(version.split(".")[0]) - if major_version == 2: - - def graphql_run(*args, **kwargs): - return graphql(*args, return_promise=True, **kwargs) - - return graphql_run - else: - return graphql - - -@dt_enabled -def test_query_and_mutation_async(app, graphql_run_async, is_graphql_2): - from graphql import __version__ as version - - FRAMEWORK_METRICS = [ - ("Python/Framework/GraphQL/%s" % version, 1), - ] - _test_mutation_scoped_metrics = [ - ("GraphQL/resolve/GraphQL/storage", 1), - ("GraphQL/resolve/GraphQL/storage_add", 1), - ("GraphQL/operation/GraphQL/query//storage", 1), - ("GraphQL/operation/GraphQL/mutation//storage_add", 1), - ] - _test_mutation_unscoped_metrics = [ - ("OtherTransaction/all", 1), - ("GraphQL/all", 2), - ("GraphQL/GraphQL/all", 2), - ("GraphQL/allOther", 2), - ("GraphQL/GraphQL/allOther", 2), - ] + _test_mutation_scoped_metrics - - _expected_mutation_operation_attributes = { - "graphql.operation.type": "mutation", - "graphql.operation.name": "", - } - _expected_mutation_resolver_attributes = { - "graphql.field.name": "storage_add", - "graphql.field.parentType": "Mutation", - "graphql.field.path": "storage_add", - "graphql.field.returnType": "[String]" if is_graphql_2 else "String", - } - _expected_query_operation_attributes = { - "graphql.operation.type": "query", - "graphql.operation.name": "", - } - _expected_query_resolver_attributes = { - "graphql.field.name": "storage", - "graphql.field.parentType": "Query", - "graphql.field.path": "storage", - "graphql.field.returnType": "[String]", - } - - @validate_transaction_metrics( - "query//storage", - "GraphQL", - scoped_metrics=_test_mutation_scoped_metrics, - rollup_metrics=_test_mutation_unscoped_metrics + FRAMEWORK_METRICS, - background_task=True, - ) - @validate_span_events(exact_agents=_expected_mutation_operation_attributes) - @validate_span_events(exact_agents=_expected_mutation_resolver_attributes) - @validate_span_events(exact_agents=_expected_query_operation_attributes) - @validate_span_events(exact_agents=_expected_query_resolver_attributes) - @background_task() - def _test(): - async def coro(): - response = await graphql_run_async(app, 'mutation { storage_add(string: "abc") }') - assert not response.errors - response = await graphql_run_async(app, "query { storage }") - assert not response.errors - - # These are separate assertions because pypy stores 'abc' as a unicode string while other Python versions do not - assert "storage" in str(response.data) - assert "abc" in str(response.data) - - loop = asyncio.new_event_loop() - loop.run_until_complete(coro()) - - _test() +async def error_middleware_async(next, root, info, **args): + raise RuntimeError("Runtime Error!") diff --git a/tox.ini b/tox.ini index 074d44d18..164fe6ced 100644 --- a/tox.ini +++ b/tox.ini @@ -125,8 +125,8 @@ envlist = python-framework_graphene-{py36,py37,py38,py39,py310}-graphenelatest, python-framework_graphene-{py27,py36,py37,py38,py39,pypy,pypy3}-graphene{0200,0201}, python-framework_graphene-py310-graphene0201, - python-framework_graphql-{py27,py36,py37,py38,py39,py310,pypy,pypy36}-graphql02, - python-framework_graphql-{py36,py37,py38,py39,py310,pypy36}-graphql03, + python-framework_graphql-{py27,py36,py37,py38,py39,py310,pypy,pypy3}-graphql02, + python-framework_graphql-{py36,py37,py38,py39,py310,pypy3}-graphql03, python-framework_graphql-py37-graphql{0202,0203,0300,0301,0302,master}, grpc-framework_grpc-{py27,py36}-grpc0125, grpc-framework_grpc-{py36,py37,py38,py39,py310}-grpclatest, From 56cab4b1eca2b25185cd89e390ddc7a7a6024736 Mon Sep 17 00:00:00 2001 From: Timothy Pansino <11214426+TimPansino@users.noreply.github.com> Date: Wed, 27 Apr 2022 16:21:25 -0700 Subject: [PATCH 10/26] Fix graphql impl coros (#522) --- newrelic/hooks/framework_graphql.py | 9 +++++++-- newrelic/hooks/framework_graphql_py3.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/newrelic/hooks/framework_graphql.py b/newrelic/hooks/framework_graphql.py index dd2c31f70..dc5575d4b 100644 --- a/newrelic/hooks/framework_graphql.py +++ b/newrelic/hooks/framework_graphql.py @@ -59,6 +59,7 @@ def as_promise(f): nr_coro_execute_name_wrapper, nr_coro_resolver_error_wrapper, nr_coro_resolver_wrapper, + nr_coro_graphql_impl_wrapper, ) _logger = logging.getLogger(__name__) @@ -564,10 +565,10 @@ def wrap_graphql_impl(wrapped, instance, args, kwargs): # Otherwise subsequent instrumentation will not be able to find an operation trace and will have issues. trace.__enter__() try: - result = wrapped(*args, **kwargs) + with ErrorTrace(ignore=ignore_graphql_duplicate_exception): + result = wrapped(*args, **kwargs) except Exception as e: # Execution finished synchronously, exit immediately. - notice_error(ignore=ignore_graphql_duplicate_exception) trace.__exit__(*sys.exc_info()) raise else: @@ -583,6 +584,10 @@ def on_reject(e): return e return result.then(on_resolve, on_reject) + elif isawaitable(result) and not is_promise(result): + # Asynchronous implementations + # Return a coroutine that handles closing the operation trace + return nr_coro_graphql_impl_wrapper(wrapped, trace, ignore_graphql_duplicate_exception, result) else: # Execution finished synchronously, exit immediately. trace.__exit__(None, None, None) diff --git a/newrelic/hooks/framework_graphql_py3.py b/newrelic/hooks/framework_graphql_py3.py index d46fb5988..c335ae5dd 100644 --- a/newrelic/hooks/framework_graphql_py3.py +++ b/newrelic/hooks/framework_graphql_py3.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import functools +import sys from newrelic.api.error_trace import ErrorTrace from newrelic.api.function_trace import FunctionTrace @@ -49,3 +50,19 @@ async def _nr_coro_resolver_wrapper(): return await result return _nr_coro_resolver_wrapper() + +def nr_coro_graphql_impl_wrapper(wrapped, trace, ignore, result): + @functools.wraps(wrapped) + async def _nr_coro_graphql_impl_wrapper(): + try: + with ErrorTrace(ignore=ignore): + result_ = await result + except: + trace.__exit__(*sys.exc_info()) + raise + else: + trace.__exit__(None, None, None) + return result_ + + + return _nr_coro_graphql_impl_wrapper() \ No newline at end of file From fd57192ea440e34e06ce4ef07b18b065755510b6 Mon Sep 17 00:00:00 2001 From: Timothy Pansino <11214426+TimPansino@users.noreply.github.com> Date: Tue, 3 May 2022 15:28:25 -0700 Subject: [PATCH 11/26] Strawberry Async Updates (#521) * Parameterize strawberry tests * Remove duplicate functions * Fix strawberry version testing * Updates * Finalize strawberry updates * Clean out code --- newrelic/hooks/framework_graphql.py | 11 +- newrelic/hooks/framework_graphql_py3.py | 4 +- newrelic/hooks/framework_strawberry.py | 14 +- tests/framework_graphql/test_application.py | 14 +- .../_target_application.py | 216 +++------ .../_target_schema_async.py | 81 ++++ .../_target_schema_sync.py | 169 +++++++ tests/framework_strawberry/conftest.py | 7 - .../framework_strawberry/test_application.py | 441 +----------------- .../test_application_async.py | 144 ------ tests/framework_strawberry/test_asgi.py | 122 ----- 11 files changed, 350 insertions(+), 873 deletions(-) create mode 100644 tests/framework_strawberry/_target_schema_async.py create mode 100644 tests/framework_strawberry/_target_schema_sync.py delete mode 100644 tests/framework_strawberry/test_application_async.py delete mode 100644 tests/framework_strawberry/test_asgi.py diff --git a/newrelic/hooks/framework_graphql.py b/newrelic/hooks/framework_graphql.py index dc5575d4b..b5af068f0 100644 --- a/newrelic/hooks/framework_graphql.py +++ b/newrelic/hooks/framework_graphql.py @@ -381,8 +381,11 @@ def wrap_resolver(wrapped, instance, args, kwargs): if transaction is None: return wrapped(*args, **kwargs) - name = callable_name(wrapped) + base_resolver = getattr(wrapped, "_nr_base_resolver", wrapped) + + name = callable_name(base_resolver) transaction.set_transaction_name(name, "GraphQL", priority=13) + trace = FunctionTrace(name, source=base_resolver) with ErrorTrace(ignore=ignore_graphql_duplicate_exception): sync_start_time = time.time() @@ -391,7 +394,7 @@ def wrap_resolver(wrapped, instance, args, kwargs): if is_promise(result) and result.is_pending and graphql_version() < (3, 0): @functools.wraps(wrapped) def nr_promise_resolver_error_wrapper(v): - with FunctionTrace(name, source=wrapped): + with trace: with ErrorTrace(ignore=ignore_graphql_duplicate_exception): try: return result.get() @@ -402,10 +405,10 @@ def nr_promise_resolver_error_wrapper(v): elif isawaitable(result) and not is_promise(result): # Grab any async resolvers and wrap with traces return nr_coro_resolver_error_wrapper( - wrapped, name, ignore_graphql_duplicate_exception, result, transaction + wrapped, name, trace, ignore_graphql_duplicate_exception, result, transaction ) else: - with FunctionTrace(name, source=wrapped) as trace: + with trace: trace.start_time = sync_start_time if is_promise(result) and result.is_rejected: result.catch(catch_promise_error).get() diff --git a/newrelic/hooks/framework_graphql_py3.py b/newrelic/hooks/framework_graphql_py3.py index c335ae5dd..3931aa6ed 100644 --- a/newrelic/hooks/framework_graphql_py3.py +++ b/newrelic/hooks/framework_graphql_py3.py @@ -28,10 +28,10 @@ async def _nr_coro_execute_name_wrapper(): return _nr_coro_execute_name_wrapper() -def nr_coro_resolver_error_wrapper(wrapped, name, ignore, result, transaction): +def nr_coro_resolver_error_wrapper(wrapped, name, trace, ignore, result, transaction): @functools.wraps(wrapped) async def _nr_coro_resolver_error_wrapper(): - with FunctionTrace(name, source=wrapped): + with trace: with ErrorTrace(ignore=ignore): try: return await result diff --git a/newrelic/hooks/framework_strawberry.py b/newrelic/hooks/framework_strawberry.py index 92a0ea8b4..cfbe450d6 100644 --- a/newrelic/hooks/framework_strawberry.py +++ b/newrelic/hooks/framework_strawberry.py @@ -29,7 +29,16 @@ def framework_details(): import strawberry - return ("Strawberry", getattr(strawberry, "__version__", None)) + try: + version = strawberry.__version__ + except Exception: + try: + import pkg_resources + version = pkg_resources.get_distribution("strawberry-graphql").version + except Exception: + version = None + + return ("Strawberry", version) def bind_execute(query, *args, **kwargs): @@ -104,8 +113,7 @@ def wrap_from_resolver(wrapped, instance, args, kwargs): else: if hasattr(field, "base_resolver"): if hasattr(field.base_resolver, "wrapped_func"): - resolver_name = callable_name(field.base_resolver.wrapped_func) - result = TransactionNameWrapper(result, resolver_name, "GraphQL", priority=13) + result._nr_base_resolver = field.base_resolver.wrapped_func return result diff --git a/tests/framework_graphql/test_application.py b/tests/framework_graphql/test_application.py index 3f765bece..b620af267 100644 --- a/tests/framework_graphql/test_application.py +++ b/tests/framework_graphql/test_application.py @@ -81,7 +81,10 @@ def error_middleware(next, root, info, **args): error_middleware = [error_middleware] if six.PY3: - from test_application_async import error_middleware_async, example_middleware_async + try: + from test_application_async import error_middleware_async, example_middleware_async + except ImportError: + from framework_graphql.test_application_async import error_middleware_async, example_middleware_async example_middleware.append(example_middleware_async) error_middleware.append(error_middleware_async) @@ -141,6 +144,8 @@ def _test(): def test_query_and_mutation(target_application, is_graphql_2): framework, version, target_application, is_bg, schema_type = target_application + type_annotation = "!" if framework == "Strawberry" else "" + _test_mutation_scoped_metrics = [ ("GraphQL/resolve/%s/storage_add" % framework, 1), ("GraphQL/operation/%s/mutation//storage_add" % framework, 1), @@ -158,7 +163,7 @@ def test_query_and_mutation(target_application, is_graphql_2): "graphql.field.name": "storage_add", "graphql.field.parentType": "Mutation", "graphql.field.path": "storage_add", - "graphql.field.returnType": "String", + "graphql.field.returnType": "String" + type_annotation, } _expected_query_operation_attributes = { "graphql.operation.type": "query", @@ -168,7 +173,7 @@ def test_query_and_mutation(target_application, is_graphql_2): "graphql.field.name": "storage", "graphql.field.parentType": "Query", "graphql.field.path": "storage", - "graphql.field.returnType": "[String]", + "graphql.field.returnType": "[String%s]%s" % (type_annotation, type_annotation), } @validate_code_level_metrics("_target_schema_%s" % schema_type, "resolve_storage_add") @@ -432,11 +437,12 @@ def test_field_resolver_metrics_and_attrs(target_application): framework, version, target_application, is_bg, schema_type = target_application field_resolver_metrics = [("GraphQL/resolve/%s/hello" % framework, 1)] + type_annotation = "!" if framework == "Strawberry" else "" graphql_attrs = { "graphql.field.name": "hello", "graphql.field.parentType": "Query", "graphql.field.path": "hello", - "graphql.field.returnType": "String", + "graphql.field.returnType": "String" + type_annotation, } @validate_transaction_metrics( diff --git a/tests/framework_strawberry/_target_application.py b/tests/framework_strawberry/_target_application.py index e032fc27a..29737db97 100644 --- a/tests/framework_strawberry/_target_application.py +++ b/tests/framework_strawberry/_target_application.py @@ -12,185 +12,79 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List, Union - -import strawberry.mutation -import strawberry.type -from strawberry import Schema, field -from strawberry.asgi import GraphQL -from strawberry.schema.config import StrawberryConfig -from strawberry.types.types import Optional - - -@strawberry.type -class Author: - first_name: str - last_name: str - - -@strawberry.type -class Book: - id: int - name: str - isbn: str - author: Author - branch: str - - -@strawberry.type -class Magazine: - id: int - name: str - issue: int - branch: str - -@strawberry.type -class Library: - id: int - branch: str - magazine: List[Magazine] - book: List[Book] +import asyncio +import json +import pytest +from _target_schema_sync import target_schema as target_schema_sync, target_asgi_application as target_asgi_application_sync +from _target_schema_async import target_schema as target_schema_async, target_asgi_application as target_asgi_application_async -Item = Union[Book, Magazine] -Storage = List[str] +def run_sync(schema): + def _run_sync(query, middleware=None): + from graphql.language.source import Source -authors = [ - Author( - first_name="New", - last_name="Relic", - ), - Author( - first_name="Bob", - last_name="Smith", - ), - Author( - first_name="Leslie", - last_name="Jones", - ), -] - -books = [ - Book( - id=1, - name="Python Agent: The Book", - isbn="a-fake-isbn", - author=authors[0], - branch="riverside", - ), - Book( - id=2, - name="Ollies for O11y: A Sk8er's Guide to Observability", - isbn="a-second-fake-isbn", - author=authors[1], - branch="downtown", - ), - Book( - id=3, - name="[Redacted]", - isbn="a-third-fake-isbn", - author=authors[2], - branch="riverside", - ), -] + if middleware is not None: + pytest.skip("Middleware not supported in Strawberry.") -magazines = [ - Magazine(id=1, name="Reli Updates Weekly", issue=1, branch="riverside"), - Magazine(id=2, name="Reli: The Forgotten Years", issue=2, branch="downtown"), - Magazine(id=3, name="Node Weekly", issue=1, branch="riverside"), -] + response = schema.execute_sync(query) + if isinstance(query, str) and "error" not in query or isinstance(query, Source) and "error" not in query.body: + assert not response.errors + else: + assert response.errors -libraries = ["riverside", "downtown"] -libraries = [ - Library( - id=i + 1, - branch=branch, - magazine=[m for m in magazines if m.branch == branch], - book=[b for b in books if b.branch == branch], - ) - for i, branch in enumerate(libraries) -] + return response.data + return _run_sync -storage = [] +def run_async(schema): + def _run_async(query, middleware=None): + from graphql.language.source import Source -def resolve_hello(): - return "Hello!" + if middleware is not None: + pytest.skip("Middleware not supported in Strawberry.") + loop = asyncio.get_event_loop() + response = loop.run_until_complete(schema.execute(query)) -async def resolve_hello_async(): - return "Hello!" + if isinstance(query, str) and "error" not in query or isinstance(query, Source) and "error" not in query.body: + assert not response.errors + else: + assert response.errors + return response.data + return _run_async -def resolve_echo(echo: str): - return echo +def run_asgi(app): + def _run_asgi(query, middleware=None): + if middleware is not None: + pytest.skip("Middleware not supported in Strawberry.") -def resolve_library(index: int): - return libraries[index] + response = app.make_request( + "POST", "/", body=json.dumps({"query": query}), headers={"Content-Type": "application/json"} + ) + body = json.loads(response.body.decode("utf-8")) + if not isinstance(query, str) or "error" in query: + try: + assert response.status != 200 + except AssertionError: + assert body["errors"] + else: + assert response.status == 200 + assert "errors" not in body or not body["errors"] -def resolve_storage_add(string: str): - storage.add(string) - return storage + return body["data"] + return _run_asgi -def resolve_storage(): - return storage - - -def resolve_error(): - raise RuntimeError("Runtime Error!") - - -def resolve_search(contains: str): - search_books = [b for b in books if contains in b.name] - search_magazines = [m for m in magazines if contains in m.name] - return search_books + search_magazines - - -@strawberry.type -class Query: - library: Library = field(resolver=resolve_library) - hello: str = field(resolver=resolve_hello) - hello_async: str = field(resolver=resolve_hello_async) - search: List[Item] = field(resolver=resolve_search) - echo: str = field(resolver=resolve_echo) - storage: Storage = field(resolver=resolve_storage) - error: Optional[str] = field(resolver=resolve_error) - error_non_null: str = field(resolver=resolve_error) - - def resolve_library(self, info, index): - return libraries[index] - - def resolve_storage(self, info): - return storage - - def resolve_search(self, info, contains): - search_books = [b for b in books if contains in b.name] - search_magazines = [m for m in magazines if contains in m.name] - return search_books + search_magazines - - def resolve_hello(self, info): - return "Hello!" - - def resolve_echo(self, info, echo): - return echo - - def resolve_error(self, info) -> str: - raise RuntimeError("Runtime Error!") - - -@strawberry.type -class Mutation: - @strawberry.mutation - def storage_add(self, string: str) -> str: - storage.append(string) - return str(string) - - -_target_application = Schema(query=Query, mutation=Mutation, config=StrawberryConfig(auto_camel_case=False)) -_target_asgi_application = GraphQL(_target_application) +target_application = { + "sync-sync": run_sync(target_schema_sync), + "async-sync": run_async(target_schema_sync), + "asgi-sync": run_asgi(target_asgi_application_sync), + "async-async": run_async(target_schema_async), + "asgi-async": run_asgi(target_asgi_application_async), +} diff --git a/tests/framework_strawberry/_target_schema_async.py b/tests/framework_strawberry/_target_schema_async.py new file mode 100644 index 000000000..0b4953eb7 --- /dev/null +++ b/tests/framework_strawberry/_target_schema_async.py @@ -0,0 +1,81 @@ +# 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. + +from typing import List +import strawberry.mutation +import strawberry.type +from strawberry import Schema, field +from strawberry.asgi import GraphQL +from strawberry.schema.config import StrawberryConfig +from strawberry.types.types import Optional +from testing_support.asgi_testing import AsgiTest + +try: + from _target_schema_sync import Library, Item, Storage, books, magazines, libraries +except ImportError: + from framework_strawberry._target_schema_sync import Library, Item, Storage, books, magazines, libraries + + +storage = [] + + +async def resolve_hello(): + return "Hello!" + + +async def resolve_echo(echo: str): + return echo + + +async def resolve_library(index: int): + return libraries[index] + + +async def resolve_storage_add(string: str): + storage.append(string) + return string + + +async def resolve_storage(): + return [storage.pop()] + + +async def resolve_error(): + raise RuntimeError("Runtime Error!") + + +async def resolve_search(contains: str): + search_books = [b for b in books if contains in b.name] + search_magazines = [m for m in magazines if contains in m.name] + return search_books + search_magazines + + +@strawberry.type +class Query: + library: Library = field(resolver=resolve_library) + hello: str = field(resolver=resolve_hello) + search: List[Item] = field(resolver=resolve_search) + echo: str = field(resolver=resolve_echo) + storage: Storage = field(resolver=resolve_storage) + error: Optional[str] = field(resolver=resolve_error) + error_non_null: str = field(resolver=resolve_error) + + +@strawberry.type +class Mutation: + storage_add: str = strawberry.mutation(resolver=resolve_storage_add) + + +target_schema = Schema(query=Query, mutation=Mutation, config=StrawberryConfig(auto_camel_case=False)) +target_asgi_application = AsgiTest(GraphQL(target_schema)) diff --git a/tests/framework_strawberry/_target_schema_sync.py b/tests/framework_strawberry/_target_schema_sync.py new file mode 100644 index 000000000..34bff75b9 --- /dev/null +++ b/tests/framework_strawberry/_target_schema_sync.py @@ -0,0 +1,169 @@ +# 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. + +from typing import List, Union + +import strawberry.mutation +import strawberry.type +from strawberry import Schema, field +from strawberry.asgi import GraphQL +from strawberry.schema.config import StrawberryConfig +from strawberry.types.types import Optional +from testing_support.asgi_testing import AsgiTest + + +@strawberry.type +class Author: + first_name: str + last_name: str + + +@strawberry.type +class Book: + id: int + name: str + isbn: str + author: Author + branch: str + + +@strawberry.type +class Magazine: + id: int + name: str + issue: int + branch: str + + +@strawberry.type +class Library: + id: int + branch: str + magazine: List[Magazine] + book: List[Book] + + +Item = Union[Book, Magazine] +Storage = List[str] + + +authors = [ + Author( + first_name="New", + last_name="Relic", + ), + Author( + first_name="Bob", + last_name="Smith", + ), + Author( + first_name="Leslie", + last_name="Jones", + ), +] + +books = [ + Book( + id=1, + name="Python Agent: The Book", + isbn="a-fake-isbn", + author=authors[0], + branch="riverside", + ), + Book( + id=2, + name="Ollies for O11y: A Sk8er's Guide to Observability", + isbn="a-second-fake-isbn", + author=authors[1], + branch="downtown", + ), + Book( + id=3, + name="[Redacted]", + isbn="a-third-fake-isbn", + author=authors[2], + branch="riverside", + ), +] + +magazines = [ + Magazine(id=1, name="Reli Updates Weekly", issue=1, branch="riverside"), + Magazine(id=2, name="Reli: The Forgotten Years", issue=2, branch="downtown"), + Magazine(id=3, name="Node Weekly", issue=1, branch="riverside"), +] + + +libraries = ["riverside", "downtown"] +libraries = [ + Library( + id=i + 1, + branch=branch, + magazine=[m for m in magazines if m.branch == branch], + book=[b for b in books if b.branch == branch], + ) + for i, branch in enumerate(libraries) +] + +storage = [] + + +def resolve_hello(): + return "Hello!" + + +def resolve_echo(echo: str): + return echo + + +def resolve_library(index: int): + return libraries[index] + + +def resolve_storage_add(string: str): + storage.append(string) + return string + + +def resolve_storage(): + return [storage.pop()] + + +def resolve_error(): + raise RuntimeError("Runtime Error!") + + +def resolve_search(contains: str): + search_books = [b for b in books if contains in b.name] + search_magazines = [m for m in magazines if contains in m.name] + return search_books + search_magazines + + +@strawberry.type +class Query: + library: Library = field(resolver=resolve_library) + hello: str = field(resolver=resolve_hello) + search: List[Item] = field(resolver=resolve_search) + echo: str = field(resolver=resolve_echo) + storage: Storage = field(resolver=resolve_storage) + error: Optional[str] = field(resolver=resolve_error) + error_non_null: str = field(resolver=resolve_error) + + +@strawberry.type +class Mutation: + storage_add: str = strawberry.mutation(resolver=resolve_storage_add) + + +target_schema = Schema(query=Query, mutation=Mutation, config=StrawberryConfig(auto_camel_case=False)) +target_asgi_application = AsgiTest(GraphQL(target_schema)) diff --git a/tests/framework_strawberry/conftest.py b/tests/framework_strawberry/conftest.py index 6cbf75b87..10c659b8b 100644 --- a/tests/framework_strawberry/conftest.py +++ b/tests/framework_strawberry/conftest.py @@ -40,12 +40,5 @@ ) -@pytest.fixture(scope="session") -def app(): - from _target_application import _target_application - - return _target_application - - if six.PY2: collect_ignore = ["test_application_async.py"] diff --git a/tests/framework_strawberry/test_application.py b/tests/framework_strawberry/test_application.py index 945a613d4..be1746516 100644 --- a/tests/framework_strawberry/test_application.py +++ b/tests/framework_strawberry/test_application.py @@ -12,435 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pytest -from testing_support.fixtures import ( - dt_enabled, - validate_transaction_errors, - validate_transaction_metrics, -) -from testing_support.validators.validate_span_events import validate_span_events -from testing_support.validators.validate_transaction_count import ( - validate_transaction_count, -) +from framework_graphql.test_application import * -from newrelic.api.background_task import background_task -from newrelic.common.object_names import callable_name +@pytest.fixture(scope="session", params=["sync-sync", "async-sync", "async-async", "asgi-sync", "asgi-async"]) +def target_application(request): + from _target_application import target_application + target_application = target_application[request.param] -@pytest.fixture(scope="session") -def is_graphql_2(): - from graphql import __version__ as version + try: + import strawberry + version = strawberry.__version__ + except Exception: + import pkg_resources + version = pkg_resources.get_distribution("strawberry-graphql").version - major_version = int(version.split(".")[0]) - return major_version == 2 + is_asgi = "asgi" in request.param + schema_type = request.param.split("-")[1] + assert version is not None + return "Strawberry", version, target_application, not is_asgi, schema_type -@pytest.fixture(scope="session") -def graphql_run(): - """Wrapper function to simulate framework_graphql test behavior.""" - - def execute(schema, *args, **kwargs): - return schema.execute_sync(*args, **kwargs) - - return execute - - -def to_graphql_source(query): - def delay_import(): - try: - from graphql import Source - except ImportError: - # Fallback if Source is not implemented - return query - - from graphql import __version__ as version - - # For graphql2, Source objects aren't acceptable input - major_version = int(version.split(".")[0]) - if major_version == 2: - return query - - return Source(query) - - return delay_import - - -def example_middleware(next, root, info, **args): #pylint: disable=W0622 - return_value = next(root, info, **args) - return return_value - - -def error_middleware(next, root, info, **args): #pylint: disable=W0622 - raise RuntimeError("Runtime Error!") - - -_runtime_error_name = callable_name(RuntimeError) -_test_runtime_error = [(_runtime_error_name, "Runtime Error!")] -_graphql_base_rollup_metrics = [ - ("OtherTransaction/all", 1), - ("GraphQL/all", 1), - ("GraphQL/allOther", 1), - ("GraphQL/Strawberry/all", 1), - ("GraphQL/Strawberry/allOther", 1), -] - - -def test_basic(app, graphql_run): - from graphql import __version__ as version - - from newrelic.hooks.framework_strawberry import framework_details - - FRAMEWORK_METRICS = [ - ("Python/Framework/Strawberry/%s" % framework_details()[1], 1), - ("Python/Framework/GraphQL/%s" % version, 1), - ] - - @validate_transaction_metrics( - "query//hello", - "GraphQL", - rollup_metrics=_graphql_base_rollup_metrics + FRAMEWORK_METRICS, - background_task=True, - ) - @background_task() - def _test(): - response = graphql_run(app, "{ hello }") - assert not response.errors - assert response.data["hello"] == "Hello!" - - _test() - - -@dt_enabled -def test_query_and_mutation(app, graphql_run): - from graphql import __version__ as version - - from newrelic.hooks.framework_strawberry import framework_details - - FRAMEWORK_METRICS = [ - ("Python/Framework/Strawberry/%s" % framework_details()[1], 1), - ("Python/Framework/GraphQL/%s" % version, 1), - ] - _test_mutation_scoped_metrics = [ - ("GraphQL/resolve/Strawberry/storage", 1), - ("GraphQL/resolve/Strawberry/storage_add", 1), - ("GraphQL/operation/Strawberry/query//storage", 1), - ("GraphQL/operation/Strawberry/mutation//storage_add", 1), - ] - _test_mutation_unscoped_metrics = [ - ("OtherTransaction/all", 1), - ("GraphQL/all", 2), - ("GraphQL/Strawberry/all", 2), - ("GraphQL/allOther", 2), - ("GraphQL/Strawberry/allOther", 2), - ] + _test_mutation_scoped_metrics - - _expected_mutation_operation_attributes = { - "graphql.operation.type": "mutation", - "graphql.operation.name": "", - } - _expected_mutation_resolver_attributes = { - "graphql.field.name": "storage_add", - "graphql.field.parentType": "Mutation", - "graphql.field.path": "storage_add", - "graphql.field.returnType": "String!", - } - _expected_query_operation_attributes = { - "graphql.operation.type": "query", - "graphql.operation.name": "", - } - _expected_query_resolver_attributes = { - "graphql.field.name": "storage", - "graphql.field.parentType": "Query", - "graphql.field.path": "storage", - "graphql.field.returnType": "[String!]!", - } - - @validate_transaction_metrics( - "query//storage", - "GraphQL", - scoped_metrics=_test_mutation_scoped_metrics, - rollup_metrics=_test_mutation_unscoped_metrics + FRAMEWORK_METRICS, - background_task=True, - ) - @validate_span_events(exact_agents=_expected_mutation_operation_attributes) - @validate_span_events(exact_agents=_expected_mutation_resolver_attributes) - @validate_span_events(exact_agents=_expected_query_operation_attributes) - @validate_span_events(exact_agents=_expected_query_resolver_attributes) - @background_task() - def _test(): - response = graphql_run(app, 'mutation { storage_add(string: "abc") }') - assert not response.errors - response = graphql_run(app, "query { storage }") - assert not response.errors - - # These are separate assertions because pypy stores 'abc' as a unicode string while other Python versions do not - assert "storage" in str(response.data) - assert "abc" in str(response.data) - - _test() - - -@pytest.mark.parametrize("field", ("error", "error_non_null")) -@dt_enabled -def test_exception_in_resolver(app, graphql_run, field): - query = "query MyQuery { %s }" % field - - txn_name = "_target_application:resolve_error" - - # Metrics - _test_exception_scoped_metrics = [ - ("GraphQL/operation/Strawberry/query/MyQuery/%s" % field, 1), - ("GraphQL/resolve/Strawberry/%s" % field, 1), - ] - _test_exception_rollup_metrics = [ - ("Errors/all", 1), - ("Errors/allOther", 1), - ("Errors/OtherTransaction/GraphQL/%s" % txn_name, 1), - ] + _test_exception_scoped_metrics - - # Attributes - _expected_exception_resolver_attributes = { - "graphql.field.name": field, - "graphql.field.parentType": "Query", - "graphql.field.path": field, - "graphql.field.returnType": "String!" if "non_null" in field else "String", - } - _expected_exception_operation_attributes = { - "graphql.operation.type": "query", - "graphql.operation.name": "MyQuery", - "graphql.operation.query": query, - } - - @validate_transaction_metrics( - txn_name, - "GraphQL", - scoped_metrics=_test_exception_scoped_metrics, - rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics, - background_task=True, - ) - @validate_span_events(exact_agents=_expected_exception_operation_attributes) - @validate_span_events(exact_agents=_expected_exception_resolver_attributes) - @validate_transaction_errors(errors=_test_runtime_error) - @background_task() - def _test(): - response = graphql_run(app, query) - assert response.errors - - _test() - - -@dt_enabled -@pytest.mark.parametrize( - "query,exc_class", - [ - ("query MyQuery { missing_field }", "GraphQLError"), - ("{ syntax_error ", "graphql.error.syntax_error:GraphQLSyntaxError"), - ], -) -def test_exception_in_validation(app, graphql_run, is_graphql_2, query, exc_class): - if "syntax" in query: - txn_name = "graphql.language.parser:parse" - else: - if is_graphql_2: - txn_name = "graphql.validation.validation:validate" - else: - txn_name = "graphql.validation.validate:validate" - - # Import path differs between versions - if exc_class == "GraphQLError": - from graphql.error import GraphQLError - - exc_class = callable_name(GraphQLError) - - _test_exception_scoped_metrics = [ - ('GraphQL/operation/Strawberry///', 1), - ] - _test_exception_rollup_metrics = [ - ("Errors/all", 1), - ("Errors/allOther", 1), - ("Errors/OtherTransaction/GraphQL/%s" % txn_name, 1), - ] + _test_exception_scoped_metrics - - # Attributes - _expected_exception_operation_attributes = { - "graphql.operation.type": "", - "graphql.operation.name": "", - "graphql.operation.query": query, - } - - @validate_transaction_metrics( - txn_name, - "GraphQL", - scoped_metrics=_test_exception_scoped_metrics, - rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics, - background_task=True, - ) - @validate_span_events(exact_agents=_expected_exception_operation_attributes) - @validate_transaction_errors(errors=[exc_class]) - @background_task() - def _test(): - response = graphql_run(app, query) - assert response.errors - - _test() - - -@dt_enabled -def test_operation_metrics_and_attrs(app, graphql_run): - operation_metrics = [("GraphQL/operation/Strawberry/query/MyQuery/library", 1)] - operation_attrs = { - "graphql.operation.type": "query", - "graphql.operation.name": "MyQuery", - } - - @validate_transaction_metrics( - "query/MyQuery/library", - "GraphQL", - scoped_metrics=operation_metrics, - rollup_metrics=operation_metrics + _graphql_base_rollup_metrics, - background_task=True, - ) - # Span count 16: Transaction, Operation, and 7 Resolvers and Resolver functions - # library, library.name, library.book - # library.book.name and library.book.id for each book resolved (in this case 2) - @validate_span_events(count=16) - @validate_span_events(exact_agents=operation_attrs) - @background_task() - def _test(): - response = graphql_run(app, "query MyQuery { library(index: 0) { branch, book { id, name } } }") - assert not response.errors - - _test() - - -@dt_enabled -def test_field_resolver_metrics_and_attrs(app, graphql_run): - field_resolver_metrics = [("GraphQL/resolve/Strawberry/hello", 1)] - graphql_attrs = { - "graphql.field.name": "hello", - "graphql.field.parentType": "Query", - "graphql.field.path": "hello", - "graphql.field.returnType": "String!", - } - - @validate_transaction_metrics( - "query//hello", - "GraphQL", - scoped_metrics=field_resolver_metrics, - rollup_metrics=field_resolver_metrics + _graphql_base_rollup_metrics, - background_task=True, - ) - # Span count 4: Transaction, Operation, and 1 Resolver and Resolver function - @validate_span_events(count=4) - @validate_span_events(exact_agents=graphql_attrs) - @background_task() - def _test(): - response = graphql_run(app, "{ hello }") - assert not response.errors - assert "Hello!" in str(response.data) - - _test() - - -_test_queries = [ - ("{ hello }", "{ hello }"), # Basic query extraction - ("{ error }", "{ error }"), # Extract query on field error - (to_graphql_source("{ hello }"), "{ hello }"), # Extract query from Source objects - ( - "{ library(index: 0) { branch } }", - "{ library(index: ?) { branch } }", - ), # Integers - ('{ echo(echo: "123") }', "{ echo(echo: ?) }"), # Strings with numerics - ('{ echo(echo: "test") }', "{ echo(echo: ?) }"), # Strings - ('{ TestEcho: echo(echo: "test") }', "{ TestEcho: echo(echo: ?) }"), # Aliases - ('{ TestEcho: echo(echo: "test") }', "{ TestEcho: echo(echo: ?) }"), # Variables - ( # Fragments - '{ ...MyFragment } fragment MyFragment on Query { echo(echo: "test") }', - "{ ...MyFragment } fragment MyFragment on Query { echo(echo: ?) }", - ), -] - - -@dt_enabled -@pytest.mark.parametrize("query,obfuscated", _test_queries) -def test_query_obfuscation(app, graphql_run, query, obfuscated): - graphql_attrs = {"graphql.operation.query": obfuscated} - - if callable(query): - query = query() - - @validate_span_events(exact_agents=graphql_attrs) - @background_task() - def _test(): - response = graphql_run(app, query) - if not isinstance(query, str) or "error" not in query: - assert not response.errors - - _test() - - -_test_queries = [ - ("{ hello }", "/hello"), # Basic query - ("{ error }", "/error"), # Extract deepest path on field error - ('{ echo(echo: "test") }', "/echo"), # Fields with arguments - ( - "{ library(index: 0) { branch, book { isbn branch } } }", - "/library", - ), # Complex Example, 1 level - ( - "{ library(index: 0) { book { author { first_name }} } }", - "/library.book.author.first_name", - ), # Complex Example, 2 levels - ("{ library(index: 0) { id, book { name } } }", "/library.book.name"), # Filtering - ('{ TestEcho: echo(echo: "test") }', "/echo"), # Aliases - ( - '{ search(contains: "A") { __typename ... on Book { name } } }', - "/search.name", - ), # InlineFragment - ( - '{ hello echo(echo: "test") }', - "", - ), # Multiple root selections. (need to decide on final behavior) - # FragmentSpread - ( - "{ library(index: 0) { book { ...MyFragment } } } fragment MyFragment on Book { name id }", # Fragment filtering - "/library.book.name", - ), - ( - "{ library(index: 0) { book { ...MyFragment } } } fragment MyFragment on Book { author { first_name } }", - "/library.book.author.first_name", - ), - ( - "{ library(index: 0) { book { ...MyFragment } magazine { ...MagFragment } } } fragment MyFragment on Book { author { first_name } } fragment MagFragment on Magazine { name }", - "/library", - ), -] - - -@dt_enabled -@pytest.mark.parametrize("query,expected_path", _test_queries) -def test_deepest_unique_path(app, graphql_run, query, expected_path): - if expected_path == "/error": - txn_name = "_target_application:resolve_error" - else: - txn_name = "query/%s" % expected_path - - @validate_transaction_metrics( - txn_name, - "GraphQL", - background_task=True, - ) - @background_task() - def _test(): - response = graphql_run(app, query) - if "error" not in query: - assert not response.errors - - _test() - - -@validate_transaction_count(0) -@background_task() -def test_ignored_introspection_transactions(app, graphql_run): - response = graphql_run(app, "{ __schema { types { name } } }") - assert not response.errors diff --git a/tests/framework_strawberry/test_application_async.py b/tests/framework_strawberry/test_application_async.py deleted file mode 100644 index 85d109955..000000000 --- a/tests/framework_strawberry/test_application_async.py +++ /dev/null @@ -1,144 +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 asyncio - -import pytest -from testing_support.fixtures import dt_enabled, validate_transaction_metrics -from testing_support.validators.validate_span_events import validate_span_events - -from newrelic.api.background_task import background_task - - -@pytest.fixture(scope="session") -def graphql_run_async(): - """Wrapper function to simulate framework_graphql test behavior.""" - - def execute(schema, *args, **kwargs): - return schema.execute(*args, **kwargs) - - return execute - - -_graphql_base_rollup_metrics = [ - ("OtherTransaction/all", 1), - ("GraphQL/all", 1), - ("GraphQL/allOther", 1), - ("GraphQL/Strawberry/all", 1), - ("GraphQL/Strawberry/allOther", 1), -] - - -loop = asyncio.new_event_loop() - - -def test_basic_async(app, graphql_run_async): - from graphql import __version__ as version - - from newrelic.hooks.framework_strawberry import framework_details - - FRAMEWORK_METRICS = [ - ("Python/Framework/Strawberry/%s" % framework_details()[1], 1), - ("Python/Framework/GraphQL/%s" % version, 1), - ] - - @validate_transaction_metrics( - "query//hello_async", - "GraphQL", - rollup_metrics=_graphql_base_rollup_metrics + FRAMEWORK_METRICS, - background_task=True, - ) - @background_task() - def _test(): - async def coro(): - response = await graphql_run_async(app, "{ hello_async }") - assert not response.errors - assert response.data["hello_async"] == "Hello!" - - loop.run_until_complete(coro()) - - _test() - - -@dt_enabled -def test_query_and_mutation_async(app, graphql_run_async): - from graphql import __version__ as version - - from newrelic.hooks.framework_strawberry import framework_details - - FRAMEWORK_METRICS = [ - ("Python/Framework/Strawberry/%s" % framework_details()[1], 1), - ("Python/Framework/GraphQL/%s" % version, 1), - ] - _test_mutation_scoped_metrics = [ - ("GraphQL/resolve/Strawberry/storage", 1), - ("GraphQL/resolve/Strawberry/storage_add", 1), - ("GraphQL/operation/Strawberry/query//storage", 1), - ("GraphQL/operation/Strawberry/mutation//storage_add", 1), - ] - _test_mutation_unscoped_metrics = [ - ("OtherTransaction/all", 1), - ("GraphQL/all", 2), - ("GraphQL/Strawberry/all", 2), - ("GraphQL/allOther", 2), - ("GraphQL/Strawberry/allOther", 2), - ] + _test_mutation_scoped_metrics - - _expected_mutation_operation_attributes = { - "graphql.operation.type": "mutation", - "graphql.operation.name": "", - } - _expected_mutation_resolver_attributes = { - "graphql.field.name": "storage_add", - "graphql.field.parentType": "Mutation", - "graphql.field.path": "storage_add", - "graphql.field.returnType": "String!", - } - _expected_query_operation_attributes = { - "graphql.operation.type": "query", - "graphql.operation.name": "", - } - _expected_query_resolver_attributes = { - "graphql.field.name": "storage", - "graphql.field.parentType": "Query", - "graphql.field.path": "storage", - "graphql.field.returnType": "[String!]!", - } - - @validate_transaction_metrics( - "query//storage", - "GraphQL", - scoped_metrics=_test_mutation_scoped_metrics, - rollup_metrics=_test_mutation_unscoped_metrics + FRAMEWORK_METRICS, - background_task=True, - ) - @validate_span_events(exact_agents=_expected_mutation_operation_attributes) - @validate_span_events(exact_agents=_expected_mutation_resolver_attributes) - @validate_span_events(exact_agents=_expected_query_operation_attributes) - @validate_span_events(exact_agents=_expected_query_resolver_attributes) - @background_task() - def _test(): - async def coro(): - response = await graphql_run_async(app, 'mutation { storage_add(string: "abc") }') - assert not response.errors - response = await graphql_run_async(app, "query { storage }") - assert not response.errors - - # These are separate assertions because pypy stores 'abc' as a unicode string while other Python versions do not - assert "storage" in str(response.data) - assert "abc" in str(response.data) - - loop.run_until_complete(coro()) - - _test() diff --git a/tests/framework_strawberry/test_asgi.py b/tests/framework_strawberry/test_asgi.py deleted file mode 100644 index 0db1e8a58..000000000 --- a/tests/framework_strawberry/test_asgi.py +++ /dev/null @@ -1,122 +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 json - -import pytest -from testing_support.asgi_testing import AsgiTest -from testing_support.fixtures import dt_enabled, validate_transaction_metrics -from testing_support.validators.validate_span_events import validate_span_events - - -@pytest.fixture(scope="session") -def graphql_asgi_run(): - """Wrapper function to simulate framework_graphql test behavior.""" - from _target_application import _target_asgi_application - - app = AsgiTest(_target_asgi_application) - - def execute(query): - return app.make_request( - "POST", - "/", - headers={"Content-Type": "application/json"}, - body=json.dumps({"query": query}), - ) - - return execute - - -@dt_enabled -def test_query_and_mutation_asgi(graphql_asgi_run): - from graphql import __version__ as version - - from newrelic.hooks.framework_strawberry import framework_details - - FRAMEWORK_METRICS = [ - ("Python/Framework/Strawberry/%s" % framework_details()[1], 1), - ("Python/Framework/GraphQL/%s" % version, 1), - ] - _test_mutation_scoped_metrics = [ - ("GraphQL/resolve/Strawberry/storage_add", 1), - ("GraphQL/operation/Strawberry/mutation//storage_add", 1), - ] - _test_query_scoped_metrics = [ - ("GraphQL/resolve/Strawberry/storage", 1), - ("GraphQL/operation/Strawberry/query//storage", 1), - ] - _test_unscoped_metrics = [ - ("WebTransaction", 1), - ("GraphQL/all", 1), - ("GraphQL/Strawberry/all", 1), - ("GraphQL/allWeb", 1), - ("GraphQL/Strawberry/allWeb", 1), - ] - _test_mutation_unscoped_metrics = _test_unscoped_metrics + _test_mutation_scoped_metrics - _test_query_unscoped_metrics = _test_unscoped_metrics + _test_query_scoped_metrics - - _expected_mutation_operation_attributes = { - "graphql.operation.type": "mutation", - "graphql.operation.name": "", - } - _expected_mutation_resolver_attributes = { - "graphql.field.name": "storage_add", - "graphql.field.parentType": "Mutation", - "graphql.field.path": "storage_add", - "graphql.field.returnType": "String!", - } - _expected_query_operation_attributes = { - "graphql.operation.type": "query", - "graphql.operation.name": "", - } - _expected_query_resolver_attributes = { - "graphql.field.name": "storage", - "graphql.field.parentType": "Query", - "graphql.field.path": "storage", - "graphql.field.returnType": "[String!]!", - } - - @validate_transaction_metrics( - "query//storage", - "GraphQL", - scoped_metrics=_test_query_scoped_metrics, - rollup_metrics=_test_query_unscoped_metrics + FRAMEWORK_METRICS, - ) - @validate_transaction_metrics( - "mutation//storage_add", - "GraphQL", - scoped_metrics=_test_mutation_scoped_metrics, - rollup_metrics=_test_mutation_unscoped_metrics + FRAMEWORK_METRICS, - index=-2, - ) - @validate_span_events(exact_agents=_expected_mutation_operation_attributes, index=-2) - @validate_span_events(exact_agents=_expected_mutation_resolver_attributes, index=-2) - @validate_span_events(exact_agents=_expected_query_operation_attributes) - @validate_span_events(exact_agents=_expected_query_resolver_attributes) - def _test(): - response = graphql_asgi_run('mutation { storage_add(string: "abc") }') - assert response.status == 200 - response = json.loads(response.body.decode("utf-8")) - assert not response.get("errors") - - response = graphql_asgi_run("query { storage }") - assert response.status == 200 - response = json.loads(response.body.decode("utf-8")) - assert not response.get("errors") - - # These are separate assertions because pypy stores 'abc' as a unicode string while other Python versions do not - assert "storage" in str(response.get("data")) - assert "abc" in str(response.get("data")) - - _test() From ff9b1b43103e7f0ca6dc5965fb1f0cae6818217f Mon Sep 17 00:00:00 2001 From: Timothy Pansino <11214426+TimPansino@users.noreply.github.com> Date: Mon, 9 May 2022 10:40:03 -0700 Subject: [PATCH 12/26] Ariadne Async Testing (#523) * Parameterize ariadne tests * Fixing ariadne tests * Fixing ariadne middleware * Set 0 extra spans for graphql core tests * Add spans attr to strawberry tests --- newrelic/hooks/framework_ariadne.py | 14 +- .../framework_ariadne/_target_application.py | 184 +++---- .../framework_ariadne/_target_schema_async.py | 94 ++++ .../framework_ariadne/_target_schema_sync.py | 97 ++++ tests/framework_ariadne/conftest.py | 7 - tests/framework_ariadne/schema.graphql | 7 +- tests/framework_ariadne/test_application.py | 510 +----------------- .../test_application_async.py | 105 ---- tests/framework_ariadne/test_asgi.py | 117 ---- tests/framework_ariadne/test_wsgi.py | 114 ---- tests/framework_graphql/conftest.py | 2 +- tests/framework_graphql/test_application.py | 48 +- .../framework_strawberry/test_application.py | 2 +- 13 files changed, 327 insertions(+), 974 deletions(-) create mode 100644 tests/framework_ariadne/_target_schema_async.py create mode 100644 tests/framework_ariadne/_target_schema_sync.py delete mode 100644 tests/framework_ariadne/test_application_async.py delete mode 100644 tests/framework_ariadne/test_asgi.py delete mode 100644 tests/framework_ariadne/test_wsgi.py diff --git a/newrelic/hooks/framework_ariadne.py b/newrelic/hooks/framework_ariadne.py index 498c662c4..7d55a89b8 100644 --- a/newrelic/hooks/framework_ariadne.py +++ b/newrelic/hooks/framework_ariadne.py @@ -29,9 +29,17 @@ def framework_details(): - import ariadne - - return ("Ariadne", getattr(ariadne, "__version__", None)) + try: + import ariadne + version = ariadne.__version__ + except Exception: + try: + import pkg_resources + version = pkg_resources.get_distribution("ariadne").version + except Exception: + version = None + + return ("Ariadne", version) def bind_graphql(schema, data, *args, **kwargs): diff --git a/tests/framework_ariadne/_target_application.py b/tests/framework_ariadne/_target_application.py index 94bc0710f..bf6ef75c8 100644 --- a/tests/framework_ariadne/_target_application.py +++ b/tests/framework_ariadne/_target_application.py @@ -12,140 +12,110 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os - -from ariadne import ( - MutationType, - QueryType, - UnionType, - load_schema_from_path, - make_executable_schema, -) -from ariadne.asgi import GraphQL as GraphQLASGI -from ariadne.wsgi import GraphQL as GraphQLWSGI - -schema_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "schema.graphql") -type_defs = load_schema_from_path(schema_file) - - -authors = [ - { - "first_name": "New", - "last_name": "Relic", - }, - { - "first_name": "Bob", - "last_name": "Smith", - }, - { - "first_name": "Leslie", - "last_name": "Jones", - }, -] -books = [ - { - "id": 1, - "name": "Python Agent: The Book", - "isbn": "a-fake-isbn", - "author": authors[0], - "branch": "riverside", - }, - { - "id": 2, - "name": "Ollies for O11y: A Sk8er's Guide to Observability", - "isbn": "a-second-fake-isbn", - "author": authors[1], - "branch": "downtown", - }, - { - "id": 3, - "name": "[Redacted]", - "isbn": "a-third-fake-isbn", - "author": authors[2], - "branch": "riverside", - }, -] +import asyncio +import json +import pytest -magazines = [ - {"id": 1, "name": "Reli Updates Weekly", "issue": 1, "branch": "riverside"}, - {"id": 2, "name": "Reli Updates Weekly", "issue": 2, "branch": "downtown"}, - {"id": 3, "name": "Node Weekly", "issue": 1, "branch": "riverside"}, -] +from _target_schema_sync import target_schema as target_schema_sync, target_asgi_application as target_asgi_application_sync, target_wsgi_application as target_wsgi_application_sync +from _target_schema_async import target_schema as target_schema_async, target_asgi_application as target_asgi_application_async +from graphql import MiddlewareManager -libraries = ["riverside", "downtown"] -libraries = [ - { - "id": i + 1, - "branch": branch, - "magazine": [m for m in magazines if m["branch"] == branch], - "book": [b for b in books if b["branch"] == branch], - } - for i, branch in enumerate(libraries) -] -storage = [] +def check_response(query, success, response): + if isinstance(query, str) and "error" not in query: + assert success and not "errors" in response, response["errors"] + assert response["data"] + else: + assert "errors" in response, response -mutation = MutationType() +def run_sync(schema): + def _run_sync(query, middleware=None): + from ariadne import graphql_sync -@mutation.field("storage_add") -def mutate(self, info, string): - storage.append(string) - return {"string": string} + if middleware: + middleware = MiddlewareManager(*middleware) + else: + middleware = None + success, response = graphql_sync(schema, {"query": query}, middleware=middleware) + check_response(query, success, response) -item = UnionType("Item") + return response.get("data", {}) + return _run_sync -@item.type_resolver -def resolve_type(obj, *args): - if "isbn" in obj: - return "Book" - elif "issue" in obj: # pylint: disable=R1705 - return "Magazine" +def run_async(schema): + def _run_async(query, middleware=None): + from ariadne import graphql - return None + if middleware: + middleware = MiddlewareManager(*middleware) + else: + middleware = None + loop = asyncio.get_event_loop() + success, response = loop.run_until_complete(graphql(schema, {"query": query}, middleware=middleware)) + check_response(query, success, response) -query = QueryType() + return response.get("data", {}) + return _run_async -@query.field("library") -def resolve_library(self, info, index): - return libraries[index] +def run_wsgi(app): + def _run_asgi(query, middleware=None): + if not isinstance(query, str) or "error" in query: + expect_errors = True + else: + expect_errors = False + app.app.middleware = middleware -@query.field("storage") -def resolve_storage(self, info): - return storage + response = app.post( + "/", json.dumps({"query": query}), headers={"Content-Type": "application/json"}, expect_errors=expect_errors + ) + body = json.loads(response.body.decode("utf-8")) + if expect_errors: + assert body["errors"] + else: + assert "errors" not in body or not body["errors"] -@query.field("search") -def resolve_search(self, info, contains): - search_books = [b for b in books if contains in b["name"]] - search_magazines = [m for m in magazines if contains in m["name"]] - return search_books + search_magazines + return body.get("data", {}) + return _run_asgi -@query.field("hello") -def resolve_hello(self, info): - return "Hello!" +def run_asgi(app): + def _run_asgi(query, middleware=None): + app.asgi_application.middleware = middleware -@query.field("echo") -def resolve_echo(self, info, echo): - return echo + response = app.make_request( + "POST", "/", body=json.dumps({"query": query}), headers={"Content-Type": "application/json"} + ) + body = json.loads(response.body.decode("utf-8")) + if not isinstance(query, str) or "error" in query: + try: + assert response.status != 200 + except AssertionError: + assert body["errors"] + else: + assert response.status == 200 + assert "errors" not in body or not body["errors"] -@query.field("error_non_null") -@query.field("error") -def resolve_error(self, info): - raise RuntimeError("Runtime Error!") + return body.get("data", {}) + return _run_asgi -_target_application = make_executable_schema(type_defs, query, mutation, item) -_target_asgi_application = GraphQLASGI(_target_application) -_target_wsgi_application = GraphQLWSGI(_target_application) +target_application = { + "sync-sync": run_sync(target_schema_sync), + "async-sync": run_async(target_schema_sync), + "async-async": run_async(target_schema_async), + "wsgi-sync": run_wsgi(target_wsgi_application_sync), + "asgi-sync": run_asgi(target_asgi_application_sync), + "asgi-async": run_asgi(target_asgi_application_async), +} diff --git a/tests/framework_ariadne/_target_schema_async.py b/tests/framework_ariadne/_target_schema_async.py new file mode 100644 index 000000000..076475628 --- /dev/null +++ b/tests/framework_ariadne/_target_schema_async.py @@ -0,0 +1,94 @@ +# 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 os + +from ariadne import ( + MutationType, + QueryType, + UnionType, + load_schema_from_path, + make_executable_schema, +) +from ariadne.asgi import GraphQL as GraphQLASGI +from framework_graphql._target_schema_sync import books, magazines, libraries + +from testing_support.asgi_testing import AsgiTest + +schema_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "schema.graphql") +type_defs = load_schema_from_path(schema_file) + +storage = [] + +mutation = MutationType() + + +@mutation.field("storage_add") +async def resolve_storage_add(self, info, string): + storage.append(string) + return string + + +item = UnionType("Item") + + +@item.type_resolver +async def resolve_type(obj, *args): + if "isbn" in obj: + return "Book" + elif "issue" in obj: # pylint: disable=R1705 + return "Magazine" + + return None + + +query = QueryType() + + +@query.field("library") +async def resolve_library(self, info, index): + return libraries[index] + + +@query.field("storage") +async def resolve_storage(self, info): + return [storage.pop()] + + +@query.field("search") +async def resolve_search(self, info, contains): + search_books = [b for b in books if contains in b["name"]] + search_magazines = [m for m in magazines if contains in m["name"]] + return search_books + search_magazines + + +@query.field("hello") +@query.field("error_middleware") +async def resolve_hello(self, info): + return "Hello!" + + +@query.field("echo") +async def resolve_echo(self, info, echo): + return echo + + +@query.field("error_non_null") +@query.field("error") +async def resolve_error(self, info): + raise RuntimeError("Runtime Error!") + + +target_schema = make_executable_schema(type_defs, query, mutation, item) +target_asgi_application = AsgiTest(GraphQLASGI(target_schema)) diff --git a/tests/framework_ariadne/_target_schema_sync.py b/tests/framework_ariadne/_target_schema_sync.py new file mode 100644 index 000000000..e42ee0bc1 --- /dev/null +++ b/tests/framework_ariadne/_target_schema_sync.py @@ -0,0 +1,97 @@ +# 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 os +import webtest + +from ariadne import ( + MutationType, + QueryType, + UnionType, + load_schema_from_path, + make_executable_schema, +) +from ariadne.asgi import GraphQL as GraphQLASGI +from ariadne.wsgi import GraphQL as GraphQLWSGI +from framework_graphql._target_schema_sync import books, magazines, libraries + +from testing_support.asgi_testing import AsgiTest + +schema_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "schema.graphql") +type_defs = load_schema_from_path(schema_file) + +storage = [] + +mutation = MutationType() + + +@mutation.field("storage_add") +def resolve_storage_add(self, info, string): + storage.append(string) + return string + + +item = UnionType("Item") + + +@item.type_resolver +def resolve_type(obj, *args): + if "isbn" in obj: + return "Book" + elif "issue" in obj: # pylint: disable=R1705 + return "Magazine" + + return None + + +query = QueryType() + + +@query.field("library") +def resolve_library(self, info, index): + return libraries[index] + + +@query.field("storage") +def resolve_storage(self, info): + return [storage.pop()] + + +@query.field("search") +def resolve_search(self, info, contains): + search_books = [b for b in books if contains in b["name"]] + search_magazines = [m for m in magazines if contains in m["name"]] + return search_books + search_magazines + + +@query.field("hello") +@query.field("error_middleware") +def resolve_hello(self, info): + return "Hello!" + + +@query.field("echo") +def resolve_echo(self, info, echo): + return echo + + +@query.field("error_non_null") +@query.field("error") +def resolve_error(self, info): + raise RuntimeError("Runtime Error!") + + +target_schema = make_executable_schema(type_defs, query, mutation, item) +target_asgi_application = AsgiTest(GraphQLASGI(target_schema)) +target_wsgi_application = webtest.TestApp(GraphQLWSGI(target_schema)) diff --git a/tests/framework_ariadne/conftest.py b/tests/framework_ariadne/conftest.py index f7c94ed26..31a19f5a6 100644 --- a/tests/framework_ariadne/conftest.py +++ b/tests/framework_ariadne/conftest.py @@ -40,12 +40,5 @@ ) -@pytest.fixture(scope="session") -def app(): - from _target_application import _target_application - - return _target_application - - if six.PY2: collect_ignore = ["test_application_async.py"] diff --git a/tests/framework_ariadne/schema.graphql b/tests/framework_ariadne/schema.graphql index 4c76e0b88..8bf64af51 100644 --- a/tests/framework_ariadne/schema.graphql +++ b/tests/framework_ariadne/schema.graphql @@ -33,7 +33,7 @@ type Magazine { } type Mutation { - storage_add(string: String!): StorageAdd + storage_add(string: String!): String } type Query { @@ -44,8 +44,5 @@ type Query { echo(echo: String!): String error: String error_non_null: String! -} - -type StorageAdd { - string: String + error_middleware: String } diff --git a/tests/framework_ariadne/test_application.py b/tests/framework_ariadne/test_application.py index f0b3587b8..40c63bf76 100644 --- a/tests/framework_ariadne/test_application.py +++ b/tests/framework_ariadne/test_application.py @@ -12,501 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pytest -from testing_support.fixtures import ( - dt_enabled, - validate_transaction_errors, - validate_transaction_metrics, -) -from testing_support.validators.validate_span_events import validate_span_events -from testing_support.validators.validate_transaction_count import ( - validate_transaction_count, -) +from framework_graphql.test_application import * -from newrelic.api.background_task import background_task -from newrelic.common.object_names import callable_name +@pytest.fixture(scope="session", params=["sync-sync", "async-sync", "async-async", "wsgi-sync", "asgi-sync", "asgi-async"]) +def target_application(request): + from _target_application import target_application + target_application = target_application[request.param] -@pytest.fixture(scope="session") -def is_graphql_2(): - from graphql import __version__ as version + try: + import ariadne + version = ariadne.__version__ + except Exception: + import pkg_resources + version = pkg_resources.get_distribution("ariadne").version - major_version = int(version.split(".")[0]) - return major_version == 2 + param = request.param.split("-") + is_background = param[0] not in {"wsgi", "asgi"} + schema_type = param[1] + extra_spans = 4 if param[0] == "wsgi" else 0 - -@pytest.fixture(scope="session") -def graphql_run(): - """Wrapper function to simulate framework_graphql test behavior.""" - - def execute(schema, query, *args, **kwargs): - from ariadne import graphql_sync - - return graphql_sync(schema, {"query": query}, *args, **kwargs) - - return execute - - -def to_graphql_source(query): - def delay_import(): - try: - from graphql import Source - except ImportError: - # Fallback if Source is not implemented - return query - - from graphql import __version__ as version - - # For graphql2, Source objects aren't acceptable input - major_version = int(version.split(".")[0]) - if major_version == 2: - return query - - return Source(query) - - return delay_import - - -def example_middleware(next, root, info, **args): # pylint: disable=W0622 - return_value = next(root, info, **args) - return return_value - - -def error_middleware(next, root, info, **args): # pylint: disable=W0622 - raise RuntimeError("Runtime Error!") - - -_runtime_error_name = callable_name(RuntimeError) -_test_runtime_error = [(_runtime_error_name, "Runtime Error!")] -_graphql_base_rollup_metrics = [ - ("OtherTransaction/all", 1), - ("GraphQL/all", 1), - ("GraphQL/allOther", 1), - ("GraphQL/Ariadne/all", 1), - ("GraphQL/Ariadne/allOther", 1), -] - - -def test_basic(app, graphql_run): - from graphql import __version__ as version - - FRAMEWORK_METRICS = [ - ("Python/Framework/Ariadne/None", 1), - ("Python/Framework/GraphQL/%s" % version, 1), - ] - - @validate_transaction_metrics( - "query//hello", - "GraphQL", - rollup_metrics=_graphql_base_rollup_metrics + FRAMEWORK_METRICS, - background_task=True, - ) - @background_task() - def _test(): - ok, response = graphql_run(app, "{ hello }") - assert ok and not response.get("errors") - - _test() - - -@dt_enabled -def test_query_and_mutation(app, graphql_run): - from graphql import __version__ as version - - FRAMEWORK_METRICS = [ - ("Python/Framework/Ariadne/None", 1), - ("Python/Framework/GraphQL/%s" % version, 1), - ] - _test_mutation_scoped_metrics = [ - ("GraphQL/resolve/Ariadne/storage", 1), - ("GraphQL/resolve/Ariadne/storage_add", 1), - ("GraphQL/operation/Ariadne/query//storage", 1), - ("GraphQL/operation/Ariadne/mutation//storage_add.string", 1), - ] - _test_mutation_unscoped_metrics = [ - ("OtherTransaction/all", 1), - ("GraphQL/all", 2), - ("GraphQL/Ariadne/all", 2), - ("GraphQL/allOther", 2), - ("GraphQL/Ariadne/allOther", 2), - ] + _test_mutation_scoped_metrics - - _expected_mutation_operation_attributes = { - "graphql.operation.type": "mutation", - "graphql.operation.name": "", - } - _expected_mutation_resolver_attributes = { - "graphql.field.name": "storage_add", - "graphql.field.parentType": "Mutation", - "graphql.field.path": "storage_add", - "graphql.field.returnType": "StorageAdd", - } - _expected_query_operation_attributes = { - "graphql.operation.type": "query", - "graphql.operation.name": "", - } - _expected_query_resolver_attributes = { - "graphql.field.name": "storage", - "graphql.field.parentType": "Query", - "graphql.field.path": "storage", - "graphql.field.returnType": "[String]", - } - - @validate_transaction_metrics( - "query//storage", - "GraphQL", - scoped_metrics=_test_mutation_scoped_metrics, - rollup_metrics=_test_mutation_unscoped_metrics + FRAMEWORK_METRICS, - background_task=True, - ) - @validate_span_events(exact_agents=_expected_mutation_operation_attributes) - @validate_span_events(exact_agents=_expected_mutation_resolver_attributes) - @validate_span_events(exact_agents=_expected_query_operation_attributes) - @validate_span_events(exact_agents=_expected_query_resolver_attributes) - @background_task() - def _test(): - ok, response = graphql_run(app, 'mutation { storage_add(string: "abc") { string } }') - assert ok and not response.get("errors") - ok, response = graphql_run(app, "query { storage }") - assert ok and not response.get("errors") - - # These are separate assertions because pypy stores 'abc' as a unicode string while other Python versions do not - assert "storage" in str(response["data"]) - assert "abc" in str(response["data"]) - - _test() - - -@dt_enabled -def test_middleware(app, graphql_run, is_graphql_2): - _test_middleware_metrics = [ - ("GraphQL/operation/Ariadne/query//hello", 1), - ("GraphQL/resolve/Ariadne/hello", 1), - ("Function/test_application:example_middleware", 1), - ] - - @validate_transaction_metrics( - "query//hello", - "GraphQL", - scoped_metrics=_test_middleware_metrics, - rollup_metrics=_test_middleware_metrics + _graphql_base_rollup_metrics, - background_task=True, - ) - # Span count 5: Transaction, Operation, Middleware, and 1 Resolver and Resolver function - @validate_span_events(count=5) - @background_task() - def _test(): - from graphql import MiddlewareManager - - ok, response = graphql_run(app, "{ hello }", middleware=MiddlewareManager(example_middleware)) - assert ok and not response.get("errors") - assert "Hello!" in str(response["data"]) - - _test() - - -@dt_enabled -def test_exception_in_middleware(app, graphql_run): - query = "query MyQuery { hello }" - field = "hello" - - # Metrics - _test_exception_scoped_metrics = [ - ("GraphQL/operation/Ariadne/query/MyQuery/%s" % field, 1), - ("GraphQL/resolve/Ariadne/%s" % field, 1), - ] - _test_exception_rollup_metrics = [ - ("Errors/all", 1), - ("Errors/allOther", 1), - ("Errors/OtherTransaction/GraphQL/test_application:error_middleware", 1), - ] + _test_exception_scoped_metrics - - # Attributes - _expected_exception_resolver_attributes = { - "graphql.field.name": field, - "graphql.field.parentType": "Query", - "graphql.field.path": field, - "graphql.field.returnType": "String", - } - _expected_exception_operation_attributes = { - "graphql.operation.type": "query", - "graphql.operation.name": "MyQuery", - "graphql.operation.query": query, - } - - @validate_transaction_metrics( - "test_application:error_middleware", - "GraphQL", - scoped_metrics=_test_exception_scoped_metrics, - rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics, - background_task=True, - ) - @validate_span_events(exact_agents=_expected_exception_operation_attributes) - @validate_span_events(exact_agents=_expected_exception_resolver_attributes) - @validate_transaction_errors(errors=_test_runtime_error) - @background_task() - def _test(): - from graphql import MiddlewareManager - - _, response = graphql_run(app, query, middleware=MiddlewareManager(error_middleware)) - assert response["errors"] - - _test() - - -@pytest.mark.parametrize("field", ("error", "error_non_null")) -@dt_enabled -def test_exception_in_resolver(app, graphql_run, field): - query = "query MyQuery { %s }" % field - txn_name = "_target_application:resolve_error" - - # Metrics - _test_exception_scoped_metrics = [ - ("GraphQL/operation/Ariadne/query/MyQuery/%s" % field, 1), - ("GraphQL/resolve/Ariadne/%s" % field, 1), - ] - _test_exception_rollup_metrics = [ - ("Errors/all", 1), - ("Errors/allOther", 1), - ("Errors/OtherTransaction/GraphQL/%s" % txn_name, 1), - ] + _test_exception_scoped_metrics - - # Attributes - _expected_exception_resolver_attributes = { - "graphql.field.name": field, - "graphql.field.parentType": "Query", - "graphql.field.path": field, - "graphql.field.returnType": "String!" if "non_null" in field else "String", - } - _expected_exception_operation_attributes = { - "graphql.operation.type": "query", - "graphql.operation.name": "MyQuery", - "graphql.operation.query": query, - } - - @validate_transaction_metrics( - txn_name, - "GraphQL", - scoped_metrics=_test_exception_scoped_metrics, - rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics, - background_task=True, - ) - @validate_span_events(exact_agents=_expected_exception_operation_attributes) - @validate_span_events(exact_agents=_expected_exception_resolver_attributes) - @validate_transaction_errors(errors=_test_runtime_error) - @background_task() - def _test(): - _, response = graphql_run(app, query) - assert response["errors"] - - _test() - - -@dt_enabled -@pytest.mark.parametrize( - "query,exc_class", - [ - ("query MyQuery { missing_field }", "GraphQLError"), - ("{ syntax_error ", "graphql.error.syntax_error:GraphQLSyntaxError"), - ], -) -def test_exception_in_validation(app, graphql_run, is_graphql_2, query, exc_class): - if "syntax" in query: - txn_name = "graphql.language.parser:parse" - else: - if is_graphql_2: - txn_name = "graphql.validation.validation:validate" - else: - txn_name = "graphql.validation.validate:validate" - - # Import path differs between versions - if exc_class == "GraphQLError": - from graphql.error import GraphQLError - - exc_class = callable_name(GraphQLError) - - _test_exception_scoped_metrics = [ - ('GraphQL/operation/Ariadne///', 1), - ] - _test_exception_rollup_metrics = [ - ("Errors/all", 1), - ("Errors/allOther", 1), - ("Errors/OtherTransaction/GraphQL/%s" % txn_name, 1), - ] + _test_exception_scoped_metrics - - # Attributes - _expected_exception_operation_attributes = { - "graphql.operation.type": "", - "graphql.operation.name": "", - "graphql.operation.query": query, - } - - @validate_transaction_metrics( - txn_name, - "GraphQL", - scoped_metrics=_test_exception_scoped_metrics, - rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics, - background_task=True, - ) - @validate_span_events(exact_agents=_expected_exception_operation_attributes) - @validate_transaction_errors(errors=[exc_class]) - @background_task() - def _test(): - _, response = graphql_run(app, query) - assert response["errors"] - - _test() - - -@dt_enabled -def test_operation_metrics_and_attrs(app, graphql_run): - operation_metrics = [("GraphQL/operation/Ariadne/query/MyQuery/library", 1)] - operation_attrs = { - "graphql.operation.type": "query", - "graphql.operation.name": "MyQuery", - } - - @validate_transaction_metrics( - "query/MyQuery/library", - "GraphQL", - scoped_metrics=operation_metrics, - rollup_metrics=operation_metrics + _graphql_base_rollup_metrics, - background_task=True, - ) - # Span count 16: Transaction, Operation, and 7 Resolvers and Resolver functions - # library, library.name, library.book - # library.book.name and library.book.id for each book resolved (in this case 2) - @validate_span_events(count=16) - @validate_span_events(exact_agents=operation_attrs) - @background_task() - def _test(): - ok, response = graphql_run(app, "query MyQuery { library(index: 0) { branch, book { id, name } } }") - assert ok and not response.get("errors") - - _test() - - -@dt_enabled -def test_field_resolver_metrics_and_attrs(app, graphql_run): - field_resolver_metrics = [("GraphQL/resolve/Ariadne/hello", 1)] - graphql_attrs = { - "graphql.field.name": "hello", - "graphql.field.parentType": "Query", - "graphql.field.path": "hello", - "graphql.field.returnType": "String", - } - - @validate_transaction_metrics( - "query//hello", - "GraphQL", - scoped_metrics=field_resolver_metrics, - rollup_metrics=field_resolver_metrics + _graphql_base_rollup_metrics, - background_task=True, - ) - # Span count 4: Transaction, Operation, and 1 Resolver and Resolver function - @validate_span_events(count=4) - @validate_span_events(exact_agents=graphql_attrs) - @background_task() - def _test(): - ok, response = graphql_run(app, "{ hello }") - assert ok and not response.get("errors") - assert "Hello!" in str(response["data"]) - - _test() - - -_test_queries = [ - ("{ hello }", "{ hello }"), # Basic query extraction - ("{ error }", "{ error }"), # Extract query on field error - ("{ library(index: 0) { branch } }", "{ library(index: ?) { branch } }"), # Integers - ('{ echo(echo: "123") }', "{ echo(echo: ?) }"), # Strings with numerics - ('{ echo(echo: "test") }', "{ echo(echo: ?) }"), # Strings - ('{ TestEcho: echo(echo: "test") }', "{ TestEcho: echo(echo: ?) }"), # Aliases - ('{ TestEcho: echo(echo: "test") }', "{ TestEcho: echo(echo: ?) }"), # Variables - ( # Fragments - '{ ...MyFragment } fragment MyFragment on Query { echo(echo: "test") }', - "{ ...MyFragment } fragment MyFragment on Query { echo(echo: ?) }", - ), -] - - -@dt_enabled -@pytest.mark.parametrize("query,obfuscated", _test_queries) -def test_query_obfuscation(app, graphql_run, query, obfuscated): - graphql_attrs = {"graphql.operation.query": obfuscated} - - @validate_span_events(exact_agents=graphql_attrs) - @background_task() - def _test(): - ok, response = graphql_run(app, query) - if not isinstance(query, str) or "error" not in query: - assert ok and not response.get("errors") - - _test() - - -_test_queries = [ - ("{ hello }", "/hello"), # Basic query - ("{ error }", "/error"), # Extract deepest path on field error - ('{ echo(echo: "test") }', "/echo"), # Fields with arguments - ( - "{ library(index: 0) { branch, book { isbn branch } } }", - "/library", - ), # Complex Example, 1 level - ( - "{ library(index: 0) { book { author { first_name }} } }", - "/library.book.author.first_name", - ), # Complex Example, 2 levels - ("{ library(index: 0) { id, book { name } } }", "/library.book.name"), # Filtering - ('{ TestEcho: echo(echo: "test") }', "/echo"), # Aliases - ( - '{ search(contains: "A") { __typename ... on Book { name } } }', - "/search.name", - ), # InlineFragment - ( - '{ hello echo(echo: "test") }', - "", - ), # Multiple root selections. (need to decide on final behavior) - # FragmentSpread - ( - "{ library(index: 0) { book { ...MyFragment } } } fragment MyFragment on Book { name id }", # Fragment filtering - "/library.book.name", - ), - ( - "{ library(index: 0) { book { ...MyFragment } } } fragment MyFragment on Book { author { first_name } }", - "/library.book.author.first_name", - ), - ( - "{ library(index: 0) { book { ...MyFragment } magazine { ...MagFragment } } } fragment MyFragment on Book { author { first_name } } fragment MagFragment on Magazine { name }", - "/library", - ), -] - - -@dt_enabled -@pytest.mark.parametrize("query,expected_path", _test_queries) -def test_deepest_unique_path(app, graphql_run, query, expected_path): - if expected_path == "/error": - txn_name = "_target_application:resolve_error" - else: - txn_name = "query/%s" % expected_path - - @validate_transaction_metrics( - txn_name, - "GraphQL", - background_task=True, - ) - @background_task() - def _test(): - ok, response = graphql_run(app, query) - if "error" not in query: - assert ok and not response.get("errors") - - _test() - - -@validate_transaction_count(0) -@background_task() -def test_ignored_introspection_transactions(app, graphql_run): - ok, response = graphql_run(app, "{ __schema { types { name } } }") - assert ok and not response.get("errors") + assert version is not None + return "Ariadne", version, target_application, is_background, schema_type, extra_spans diff --git a/tests/framework_ariadne/test_application_async.py b/tests/framework_ariadne/test_application_async.py deleted file mode 100644 index 8e46752f2..000000000 --- a/tests/framework_ariadne/test_application_async.py +++ /dev/null @@ -1,105 +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 asyncio - -import pytest -from testing_support.fixtures import dt_enabled, validate_transaction_metrics -from testing_support.validators.validate_span_events import validate_span_events - -from newrelic.api.background_task import background_task - - -@pytest.fixture(scope="session") -def graphql_run_async(): - """Wrapper function to simulate framework_graphql test behavior.""" - - def execute(schema, query, *args, **kwargs): - from ariadne import graphql - - return graphql(schema, {"query": query}, *args, **kwargs) - - return execute - - -@dt_enabled -def test_query_and_mutation_async(app, graphql_run_async): - from graphql import __version__ as version - - FRAMEWORK_METRICS = [ - ("Python/Framework/Ariadne/None", 1), - ("Python/Framework/GraphQL/%s" % version, 1), - ] - _test_mutation_scoped_metrics = [ - ("GraphQL/resolve/Ariadne/storage", 1), - ("GraphQL/resolve/Ariadne/storage_add", 1), - ("GraphQL/operation/Ariadne/query//storage", 1), - ("GraphQL/operation/Ariadne/mutation//storage_add.string", 1), - ] - _test_mutation_unscoped_metrics = [ - ("OtherTransaction/all", 1), - ("GraphQL/all", 2), - ("GraphQL/Ariadne/all", 2), - ("GraphQL/allOther", 2), - ("GraphQL/Ariadne/allOther", 2), - ] + _test_mutation_scoped_metrics - - _expected_mutation_operation_attributes = { - "graphql.operation.type": "mutation", - "graphql.operation.name": "", - } - _expected_mutation_resolver_attributes = { - "graphql.field.name": "storage_add", - "graphql.field.parentType": "Mutation", - "graphql.field.path": "storage_add", - "graphql.field.returnType": "StorageAdd", - } - _expected_query_operation_attributes = { - "graphql.operation.type": "query", - "graphql.operation.name": "", - } - _expected_query_resolver_attributes = { - "graphql.field.name": "storage", - "graphql.field.parentType": "Query", - "graphql.field.path": "storage", - "graphql.field.returnType": "[String]", - } - - @validate_transaction_metrics( - "query//storage", - "GraphQL", - scoped_metrics=_test_mutation_scoped_metrics, - rollup_metrics=_test_mutation_unscoped_metrics + FRAMEWORK_METRICS, - background_task=True, - ) - @validate_span_events(exact_agents=_expected_mutation_operation_attributes) - @validate_span_events(exact_agents=_expected_mutation_resolver_attributes) - @validate_span_events(exact_agents=_expected_query_operation_attributes) - @validate_span_events(exact_agents=_expected_query_resolver_attributes) - @background_task() - def _test(): - async def coro(): - ok, response = await graphql_run_async(app, 'mutation { storage_add(string: "abc") { string } }') - assert ok and not response.get("errors") - ok, response = await graphql_run_async(app, "query { storage }") - assert ok and not response.get("errors") - - # These are separate assertions because pypy stores 'abc' as a unicode string while other Python versions do not - assert "storage" in str(response.get("data")) - assert "abc" in str(response.get("data")) - - loop = asyncio.new_event_loop() - loop.run_until_complete(coro()) - - _test() diff --git a/tests/framework_ariadne/test_asgi.py b/tests/framework_ariadne/test_asgi.py deleted file mode 100644 index 6275e781f..000000000 --- a/tests/framework_ariadne/test_asgi.py +++ /dev/null @@ -1,117 +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 json - -import pytest -from testing_support.asgi_testing import AsgiTest -from testing_support.fixtures import dt_enabled, validate_transaction_metrics -from testing_support.validators.validate_span_events import validate_span_events - - -@pytest.fixture(scope="session") -def graphql_asgi_run(): - """Wrapper function to simulate framework_graphql test behavior.""" - from _target_application import _target_asgi_application - - app = AsgiTest(_target_asgi_application) - - def execute(query): - return app.make_request( - "POST", "/", headers={"Content-Type": "application/json"}, body=json.dumps({"query": query}) - ) - - return execute - - -@dt_enabled -def test_query_and_mutation_asgi(graphql_asgi_run): - from graphql import __version__ as version - - FRAMEWORK_METRICS = [ - ("Python/Framework/Ariadne/None", 1), - ("Python/Framework/GraphQL/%s" % version, 1), - ] - _test_mutation_scoped_metrics = [ - ("GraphQL/resolve/Ariadne/storage_add", 1), - ("GraphQL/operation/Ariadne/mutation//storage_add.string", 1), - ] - _test_query_scoped_metrics = [ - ("GraphQL/resolve/Ariadne/storage", 1), - ("GraphQL/operation/Ariadne/query//storage", 1), - ] - _test_unscoped_metrics = [ - ("WebTransaction", 1), - ("GraphQL/all", 1), - ("GraphQL/Ariadne/all", 1), - ("GraphQL/allWeb", 1), - ("GraphQL/Ariadne/allWeb", 1), - ] - _test_mutation_unscoped_metrics = _test_unscoped_metrics + _test_mutation_scoped_metrics - _test_query_unscoped_metrics = _test_unscoped_metrics + _test_query_scoped_metrics - - _expected_mutation_operation_attributes = { - "graphql.operation.type": "mutation", - "graphql.operation.name": "", - } - _expected_mutation_resolver_attributes = { - "graphql.field.name": "storage_add", - "graphql.field.parentType": "Mutation", - "graphql.field.path": "storage_add", - "graphql.field.returnType": "StorageAdd", - } - _expected_query_operation_attributes = { - "graphql.operation.type": "query", - "graphql.operation.name": "", - } - _expected_query_resolver_attributes = { - "graphql.field.name": "storage", - "graphql.field.parentType": "Query", - "graphql.field.path": "storage", - "graphql.field.returnType": "[String]", - } - - @validate_transaction_metrics( - "query//storage", - "GraphQL", - scoped_metrics=_test_query_scoped_metrics, - rollup_metrics=_test_query_unscoped_metrics + FRAMEWORK_METRICS, - ) - @validate_transaction_metrics( - "mutation//storage_add.string", - "GraphQL", - scoped_metrics=_test_mutation_scoped_metrics, - rollup_metrics=_test_mutation_unscoped_metrics + FRAMEWORK_METRICS, - index=-2, - ) - @validate_span_events(exact_agents=_expected_mutation_operation_attributes, index=-2) - @validate_span_events(exact_agents=_expected_mutation_resolver_attributes, index=-2) - @validate_span_events(exact_agents=_expected_query_operation_attributes) - @validate_span_events(exact_agents=_expected_query_resolver_attributes) - def _test(): - response = graphql_asgi_run('mutation { storage_add(string: "abc") { string } }') - assert response.status == 200 - response = json.loads(response.body.decode("utf-8")) - assert not response.get("errors") - - response = graphql_asgi_run("query { storage }") - assert response.status == 200 - response = json.loads(response.body.decode("utf-8")) - assert not response.get("errors") - - # These are separate assertions because pypy stores 'abc' as a unicode string while other Python versions do not - assert "storage" in str(response.get("data")) - assert "abc" in str(response.get("data")) - - _test() diff --git a/tests/framework_ariadne/test_wsgi.py b/tests/framework_ariadne/test_wsgi.py deleted file mode 100644 index 2c11276ed..000000000 --- a/tests/framework_ariadne/test_wsgi.py +++ /dev/null @@ -1,114 +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 pytest -import webtest -from testing_support.fixtures import dt_enabled, validate_transaction_metrics -from testing_support.validators.validate_span_events import validate_span_events - - -@pytest.fixture(scope="session") -def graphql_wsgi_run(): - """Wrapper function to simulate framework_graphql test behavior.""" - from _target_application import _target_wsgi_application - - app = webtest.TestApp(_target_wsgi_application) - - def execute(query): - return app.post_json("/", {"query": query}) - - return execute - - -@dt_enabled -def test_query_and_mutation_wsgi(graphql_wsgi_run): - from graphql import __version__ as version - - FRAMEWORK_METRICS = [ - ("Python/Framework/Ariadne/None", 1), - ("Python/Framework/GraphQL/%s" % version, 1), - ] - _test_mutation_scoped_metrics = [ - ("GraphQL/resolve/Ariadne/storage_add", 1), - ("GraphQL/operation/Ariadne/mutation//storage_add.string", 1), - ] - _test_query_scoped_metrics = [ - ("GraphQL/resolve/Ariadne/storage", 1), - ("GraphQL/operation/Ariadne/query//storage", 1), - ] - _test_unscoped_metrics = [ - ("WebTransaction", 1), - ("Python/WSGI/Response", 1), - ("GraphQL/all", 1), - ("GraphQL/Ariadne/all", 1), - ("GraphQL/allWeb", 1), - ("GraphQL/Ariadne/allWeb", 1), - ] - _test_mutation_unscoped_metrics = _test_unscoped_metrics + _test_mutation_scoped_metrics - _test_query_unscoped_metrics = _test_unscoped_metrics + _test_query_scoped_metrics - - _expected_mutation_operation_attributes = { - "graphql.operation.type": "mutation", - "graphql.operation.name": "", - } - _expected_mutation_resolver_attributes = { - "graphql.field.name": "storage_add", - "graphql.field.parentType": "Mutation", - "graphql.field.path": "storage_add", - "graphql.field.returnType": "StorageAdd", - } - _expected_query_operation_attributes = { - "graphql.operation.type": "query", - "graphql.operation.name": "", - } - _expected_query_resolver_attributes = { - "graphql.field.name": "storage", - "graphql.field.parentType": "Query", - "graphql.field.path": "storage", - "graphql.field.returnType": "[String]", - } - - @validate_transaction_metrics( - "query//storage", - "GraphQL", - scoped_metrics=_test_query_scoped_metrics, - rollup_metrics=_test_query_unscoped_metrics + FRAMEWORK_METRICS, - ) - @validate_transaction_metrics( - "mutation//storage_add.string", - "GraphQL", - scoped_metrics=_test_mutation_scoped_metrics, - rollup_metrics=_test_mutation_unscoped_metrics + FRAMEWORK_METRICS, - index=-2, - ) - @validate_span_events(exact_agents=_expected_mutation_operation_attributes, index=-2) - @validate_span_events(exact_agents=_expected_mutation_resolver_attributes, index=-2) - @validate_span_events(exact_agents=_expected_query_operation_attributes) - @validate_span_events(exact_agents=_expected_query_resolver_attributes) - def _test(): - response = graphql_wsgi_run('mutation { storage_add(string: "abc") { string } }') - assert response.status_code == 200 - response = response.json_body - assert not response.get("errors") - - response = graphql_wsgi_run("query { storage }") - assert response.status_code == 200 - response = response.json_body - assert not response.get("errors") - - # These are separate assertions because pypy stores 'abc' as a unicode string while other Python versions do not - assert "storage" in str(response.get("data")) - assert "abc" in str(response.get("data")) - - _test() diff --git a/tests/framework_graphql/conftest.py b/tests/framework_graphql/conftest.py index 9aa8b6540..1b04348ea 100644 --- a/tests/framework_graphql/conftest.py +++ b/tests/framework_graphql/conftest.py @@ -52,7 +52,7 @@ def target_application(request): pytest.skip("Unsupported combination.") return - return "GraphQL", None, app, True, request.param.split("-")[1] + return "GraphQL", None, app, True, request.param.split("-")[1], 0 if six.PY2: diff --git a/tests/framework_graphql/test_application.py b/tests/framework_graphql/test_application.py index b620af267..6566ff0f0 100644 --- a/tests/framework_graphql/test_application.py +++ b/tests/framework_graphql/test_application.py @@ -124,7 +124,7 @@ def _graphql_base_rollup_metrics(framework, version, background_task=True): def test_basic(target_application): - framework, version, target_application, is_bg, schema_type = target_application + framework, version, target_application, is_bg, schema_type, extra_spans = target_application @validate_transaction_metrics( "query//hello", @@ -142,7 +142,7 @@ def _test(): @dt_enabled def test_query_and_mutation(target_application, is_graphql_2): - framework, version, target_application, is_bg, schema_type = target_application + framework, version, target_application, is_bg, schema_type, extra_spans = target_application type_annotation = "!" if framework == "Strawberry" else "" @@ -213,7 +213,7 @@ def _query(): @pytest.mark.parametrize("middleware", example_middleware) @dt_enabled def test_middleware(target_application, middleware): - framework, version, target_application, is_bg, schema_type = target_application + framework, version, target_application, is_bg, schema_type, extra_spans = target_application name = "%s:%s" % (middleware.__module__, middleware.__name__) if "async" in name: @@ -221,13 +221,17 @@ def test_middleware(target_application, middleware): pytest.skip("Async middleware not supported in sync applications.") _test_middleware_metrics = [ - ("GraphQL/operation/GraphQL/query//hello", 1), - ("GraphQL/resolve/GraphQL/hello", 1), + ("GraphQL/operation/%s/query//hello" % framework, 1), + ("GraphQL/resolve/%s/hello" % framework, 1), ("Function/%s" % name, 1), ] + # Span count 5: Transaction, Operation, Middleware, and 1 Resolver and Resolver Function + span_count = 5 + extra_spans + @validate_code_level_metrics(*name.split(":")) @validate_code_level_metrics("_target_schema_%s" % schema_type, "resolve_hello") + @validate_span_events(count=span_count) @validate_transaction_metrics( "query//hello", "GraphQL", @@ -235,8 +239,6 @@ def test_middleware(target_application, middleware): rollup_metrics=_test_middleware_metrics + _graphql_base_rollup_metrics(framework, version, is_bg), background_task=is_bg, ) - # Span count 5: Transaction, Operation, Middleware, and 1 Resolver and Resolver Function - @validate_span_events(count=5) @conditional_decorator(background_task(), is_bg) def _test(): response = target_application("{ hello }", middleware=[middleware]) @@ -248,7 +250,7 @@ def _test(): @pytest.mark.parametrize("middleware", error_middleware) @dt_enabled def test_exception_in_middleware(target_application, middleware): - framework, version, target_application, is_bg, schema_type = target_application + framework, version, target_application, is_bg, schema_type, extra_spans = target_application query = "query MyQuery { error_middleware }" field = "error_middleware" @@ -302,7 +304,7 @@ def _test(): @pytest.mark.parametrize("field", ("error", "error_non_null")) @dt_enabled def test_exception_in_resolver(target_application, field): - framework, version, target_application, is_bg, schema_type = target_application + framework, version, target_application, is_bg, schema_type, extra_spans = target_application query = "query MyQuery { %s }" % field txn_name = "_target_schema_%s:resolve_error" % schema_type @@ -357,7 +359,7 @@ def _test(): ], ) def test_exception_in_validation(target_application, is_graphql_2, query, exc_class): - framework, version, target_application, is_bg, schema_type = target_application + framework, version, target_application, is_bg, schema_type, extra_spans = target_application if "syntax" in query: txn_name = "graphql.language.parser:parse" else: @@ -406,13 +408,18 @@ def _test(): @dt_enabled def test_operation_metrics_and_attrs(target_application): - framework, version, target_application, is_bg, schema_type = target_application + framework, version, target_application, is_bg, schema_type, extra_spans = target_application operation_metrics = [("GraphQL/operation/%s/query/MyQuery/library" % framework, 1)] operation_attrs = { "graphql.operation.type": "query", "graphql.operation.name": "MyQuery", } + # Span count 16: Transaction, Operation, and 7 Resolvers and Resolver functions + # library, library.name, library.book + # library.book.name and library.book.id for each book resolved (in this case 2) + span_count = 16 + extra_spans # WSGI may add 4 spans, other frameworks may add other amounts + @validate_transaction_metrics( "query/MyQuery/library", "GraphQL", @@ -420,10 +427,7 @@ def test_operation_metrics_and_attrs(target_application): rollup_metrics=operation_metrics + _graphql_base_rollup_metrics(framework, version, is_bg), background_task=is_bg, ) - # Span count 16: Transaction, Operation, and 7 Resolvers and Resolver functions - # library, library.name, library.book - # library.book.name and library.book.id for each book resolved (in this case 2) - @validate_span_events(count=16) + @validate_span_events(count=span_count) @validate_span_events(exact_agents=operation_attrs) @conditional_decorator(background_task(), is_bg) def _test(): @@ -434,7 +438,7 @@ def _test(): @dt_enabled def test_field_resolver_metrics_and_attrs(target_application): - framework, version, target_application, is_bg, schema_type = target_application + framework, version, target_application, is_bg, schema_type, extra_spans = target_application field_resolver_metrics = [("GraphQL/resolve/%s/hello" % framework, 1)] type_annotation = "!" if framework == "Strawberry" else "" @@ -445,6 +449,9 @@ def test_field_resolver_metrics_and_attrs(target_application): "graphql.field.returnType": "String" + type_annotation, } + # Span count 4: Transaction, Operation, and 1 Resolver and Resolver function + span_count = 4 + extra_spans # WSGI may add 4 spans, other frameworks may add other amounts + @validate_transaction_metrics( "query//hello", "GraphQL", @@ -452,8 +459,7 @@ def test_field_resolver_metrics_and_attrs(target_application): rollup_metrics=field_resolver_metrics + _graphql_base_rollup_metrics(framework, version, is_bg), background_task=is_bg, ) - # Span count 4: Transaction, Operation, and 1 Resolver and Resolver function - @validate_span_events(count=4) + @validate_span_events(count=span_count) @validate_span_events(exact_agents=graphql_attrs) @conditional_decorator(background_task(), is_bg) def _test(): @@ -482,7 +488,7 @@ def _test(): @dt_enabled @pytest.mark.parametrize("query,obfuscated", _test_queries) def test_query_obfuscation(target_application, query, obfuscated): - framework, version, target_application, is_bg, schema_type = target_application + framework, version, target_application, is_bg, schema_type, extra_spans = target_application graphql_attrs = {"graphql.operation.query": obfuscated} if callable(query): @@ -539,7 +545,7 @@ def _test(): @dt_enabled @pytest.mark.parametrize("query,expected_path", _test_queries) def test_deepest_unique_path(target_application, query, expected_path): - framework, version, target_application, is_bg, schema_type = target_application + framework, version, target_application, is_bg, schema_type, extra_spans = target_application if expected_path == "/error": txn_name = "_target_schema_%s:resolve_error" % schema_type else: @@ -558,7 +564,7 @@ def _test(): def test_ignored_introspection_transactions(target_application): - framework, version, target_application, is_bg, schema_type = target_application + framework, version, target_application, is_bg, schema_type, extra_spans = target_application @validate_transaction_count(0) @background_task() diff --git a/tests/framework_strawberry/test_application.py b/tests/framework_strawberry/test_application.py index be1746516..c705423e0 100644 --- a/tests/framework_strawberry/test_application.py +++ b/tests/framework_strawberry/test_application.py @@ -31,5 +31,5 @@ def target_application(request): schema_type = request.param.split("-")[1] assert version is not None - return "Strawberry", version, target_application, not is_asgi, schema_type + return "Strawberry", version, target_application, not is_asgi, schema_type, 0 From a2889a6926d84dd744f3fdb44c4013b6f176b01e Mon Sep 17 00:00:00 2001 From: Timothy Pansino <11214426+TimPansino@users.noreply.github.com> Date: Tue, 10 May 2022 12:58:41 -0700 Subject: [PATCH 13/26] Graphene Async Testing (#524) * Graphene Async Testing * Fix missing extra spans numbers * Graphene promise tests * Fix py2 imports * Removed unused __init__ * Update code level metrics validator for py2 * Unify graphql testing imports * Fix ariadne imports * Fix other imports * Fix import issues --- .../test_application.py | 2 +- .../component_flask_rest/test_application.py | 4 +- tests/component_graphqlserver/__init__.py | 13 + .../_target_schema_async.py | 184 +++++++ tests/component_graphqlserver/test_graphql.py | 39 +- tests/framework_ariadne/__init__.py | 13 + .../framework_ariadne/_target_application.py | 4 +- tests/framework_ariadne/test_application.py | 2 +- tests/framework_graphene/__init__.py | 13 + .../framework_graphene/_target_application.py | 212 +++---- .../_target_schema_async.py | 72 +++ .../_target_schema_promise.py | 80 +++ .../framework_graphene/_target_schema_sync.py | 162 ++++++ tests/framework_graphene/test_application.py | 517 +----------------- tests/framework_graphql/__init__.py | 13 + .../framework_graphql/_target_application.py | 6 +- .../framework_graphql/_target_schema_async.py | 2 +- .../_target_schema_promise.py | 8 +- tests/framework_graphql/conftest.py | 7 +- tests/framework_graphql/test_application.py | 33 +- tests/framework_strawberry/__init__.py | 13 + .../_target_application.py | 4 +- .../_target_schema_async.py | 5 +- .../framework_strawberry/test_application.py | 2 +- .../test_pika_async_connection_consume.py | 10 +- .../test_pika_blocking_connection_consume.py | 4 +- .../validators/validate_code_level_metrics.py | 6 +- 27 files changed, 710 insertions(+), 720 deletions(-) create mode 100644 tests/component_graphqlserver/__init__.py create mode 100644 tests/component_graphqlserver/_target_schema_async.py create mode 100644 tests/framework_ariadne/__init__.py create mode 100644 tests/framework_graphene/__init__.py create mode 100644 tests/framework_graphene/_target_schema_async.py create mode 100644 tests/framework_graphene/_target_schema_promise.py create mode 100644 tests/framework_graphene/_target_schema_sync.py create mode 100644 tests/framework_graphql/__init__.py create mode 100644 tests/framework_strawberry/__init__.py diff --git a/tests/component_djangorestframework/test_application.py b/tests/component_djangorestframework/test_application.py index 2951e0401..0d0b98d82 100644 --- a/tests/component_djangorestframework/test_application.py +++ b/tests/component_djangorestframework/test_application.py @@ -165,7 +165,7 @@ def _test(): @validate_transaction_errors(errors=[]) @validate_transaction_metrics(_test_api_view_view_name_get, scoped_metrics=_test_api_view_scoped_metrics_get) -@validate_code_level_metrics("urls.WrappedAPIView" if six.PY3 else "urls", "wrapped_view") +@validate_code_level_metrics("urls.WrappedAPIView", "wrapped_view", py2_namespace="urls") def test_api_view_get(target_application): response = target_application.get('/api_view/') response.mustcontain('wrapped_view response') diff --git a/tests/component_flask_rest/test_application.py b/tests/component_flask_rest/test_application.py index 94b6fbc5c..d0eb41795 100644 --- a/tests/component_flask_rest/test_application.py +++ b/tests/component_flask_rest/test_application.py @@ -53,7 +53,7 @@ def application(request): ] -@validate_code_level_metrics("_test_application.create_app." if six.PY3 else "_test_application", "IndexResource") +@validate_code_level_metrics("_test_application.create_app.", "IndexResource", py2_namespace="_test_application") @validate_transaction_errors(errors=[]) @validate_transaction_metrics('_test_application:index', scoped_metrics=_test_application_index_scoped_metrics) @@ -80,7 +80,7 @@ def test_application_index(application): def test_application_raises(exception, status_code, ignore_status_code, propagate_exceptions, application): - @validate_code_level_metrics("_test_application.create_app." if six.PY3 else "_test_application", "ExceptionResource") + @validate_code_level_metrics("_test_application.create_app.", "ExceptionResource", py2_namespace="_test_application") @validate_transaction_metrics('_test_application:exception', scoped_metrics=_test_application_raises_scoped_metrics) def _test(): diff --git a/tests/component_graphqlserver/__init__.py b/tests/component_graphqlserver/__init__.py new file mode 100644 index 000000000..8030baccf --- /dev/null +++ b/tests/component_graphqlserver/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/tests/component_graphqlserver/_target_schema_async.py b/tests/component_graphqlserver/_target_schema_async.py new file mode 100644 index 000000000..c48be2126 --- /dev/null +++ b/tests/component_graphqlserver/_target_schema_async.py @@ -0,0 +1,184 @@ +# 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. + +from graphql import ( + GraphQLArgument, + GraphQLField, + GraphQLInt, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, + GraphQLUnionType, +) + +from ._target_schema_sync import books, libraries, magazines + +storage = [] + + +async def resolve_library(parent, info, index): + return libraries[index] + + +async def resolve_storage_add(parent, info, string): + storage.append(string) + return string + + +async def resolve_storage(parent, info): + return [storage.pop()] + + +async def resolve_search(parent, info, contains): + search_books = [b for b in books if contains in b["name"]] + search_magazines = [m for m in magazines if contains in m["name"]] + return search_books + search_magazines + + +Author = GraphQLObjectType( + "Author", + { + "first_name": GraphQLField(GraphQLString), + "last_name": GraphQLField(GraphQLString), + }, +) + +Book = GraphQLObjectType( + "Book", + { + "id": GraphQLField(GraphQLInt), + "name": GraphQLField(GraphQLString), + "isbn": GraphQLField(GraphQLString), + "author": GraphQLField(Author), + "branch": GraphQLField(GraphQLString), + }, +) + +Magazine = GraphQLObjectType( + "Magazine", + { + "id": GraphQLField(GraphQLInt), + "name": GraphQLField(GraphQLString), + "issue": GraphQLField(GraphQLInt), + "branch": GraphQLField(GraphQLString), + }, +) + + +Library = GraphQLObjectType( + "Library", + { + "id": GraphQLField(GraphQLInt), + "branch": GraphQLField(GraphQLString), + "book": GraphQLField(GraphQLList(Book)), + "magazine": GraphQLField(GraphQLList(Magazine)), + }, +) + +Storage = GraphQLList(GraphQLString) + + +async def resolve_hello(root, info): + return "Hello!" + + +async def resolve_echo(root, info, echo): + return echo + + +async def resolve_error(root, info): + raise RuntimeError("Runtime Error!") + + +try: + hello_field = GraphQLField(GraphQLString, resolver=resolve_hello) + library_field = GraphQLField( + Library, + resolver=resolve_library, + args={"index": GraphQLArgument(GraphQLNonNull(GraphQLInt))}, + ) + search_field = GraphQLField( + GraphQLList(GraphQLUnionType("Item", (Book, Magazine), resolve_type=resolve_search)), + args={"contains": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + echo_field = GraphQLField( + GraphQLString, + resolver=resolve_echo, + args={"echo": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + storage_field = GraphQLField( + Storage, + resolver=resolve_storage, + ) + storage_add_field = GraphQLField( + GraphQLString, + resolver=resolve_storage_add, + args={"string": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + error_field = GraphQLField(GraphQLString, resolver=resolve_error) + error_non_null_field = GraphQLField(GraphQLNonNull(GraphQLString), resolver=resolve_error) + error_middleware_field = GraphQLField(GraphQLString, resolver=resolve_hello) +except TypeError: + hello_field = GraphQLField(GraphQLString, resolve=resolve_hello) + library_field = GraphQLField( + Library, + resolve=resolve_library, + args={"index": GraphQLArgument(GraphQLNonNull(GraphQLInt))}, + ) + search_field = GraphQLField( + GraphQLList(GraphQLUnionType("Item", (Book, Magazine), resolve_type=resolve_search)), + args={"contains": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + echo_field = GraphQLField( + GraphQLString, + resolve=resolve_echo, + args={"echo": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + storage_field = GraphQLField( + Storage, + resolve=resolve_storage, + ) + storage_add_field = GraphQLField( + GraphQLString, + resolve=resolve_storage_add, + args={"string": GraphQLArgument(GraphQLNonNull(GraphQLString))}, + ) + error_field = GraphQLField(GraphQLString, resolve=resolve_error) + error_non_null_field = GraphQLField(GraphQLNonNull(GraphQLString), resolve=resolve_error) + error_middleware_field = GraphQLField(GraphQLString, resolve=resolve_hello) + +query = GraphQLObjectType( + name="Query", + fields={ + "hello": hello_field, + "library": library_field, + "search": search_field, + "echo": echo_field, + "storage": storage_field, + "error": error_field, + "error_non_null": error_non_null_field, + "error_middleware": error_middleware_field, + }, +) + +mutation = GraphQLObjectType( + name="Mutation", + fields={ + "storage_add": storage_add_field, + }, +) + +target_schema = GraphQLSchema(query=query, mutation=mutation) diff --git a/tests/component_graphqlserver/test_graphql.py b/tests/component_graphqlserver/test_graphql.py index f361e2193..f22245d84 100644 --- a/tests/component_graphqlserver/test_graphql.py +++ b/tests/component_graphqlserver/test_graphql.py @@ -36,38 +36,7 @@ def is_graphql_2(): @pytest.fixture(scope="session", params=("Sanic", "Flask")) def target_application(request): - import _test_graphql - framework = request.param - version = importlib.import_module(framework.lower()).__version__ - - return framework, version, _test_graphql.target_application[framework] - - -def example_middleware(next, root, info, **args): #pylint: disable=W0622 - return_value = next(root, info, **args) - return return_value - - -def error_middleware(next, root, info, **args): #pylint: disable=W0622 - raise RuntimeError("Runtime Error!") - - -_runtime_error_name = callable_name(RuntimeError) -_test_runtime_error = [(_runtime_error_name, "Runtime Error!")] -_graphql_base_rollup_metrics = [ - ("GraphQL/all", 1), - ("GraphQL/allWeb", 1), - ("GraphQL/GraphQLServer/all", 1), - ("GraphQL/GraphQLServer/allWeb", 1), -] -_view_metrics = {"Sanic": "Function/graphql_server.sanic.graphqlview:GraphQLView.post", "Flask": "Function/graphql_server.flask.graphqlview:graphql"} - - -def test_basic(target_application): - framework, version, target_application = target_application - from graphql import __version__ as graphql_version - from graphql_server import __version__ as graphql_server_version - + from . import _test_graphql framework = request.param version = importlib.import_module(framework.lower()).__version__ @@ -216,7 +185,7 @@ def test_middleware(target_application): _test_middleware_metrics = [ ("GraphQL/operation/GraphQLServer/query//hello", 1), ("GraphQL/resolve/GraphQLServer/hello", 1), - ("Function/test_graphql:example_middleware", 1), + ("Function/component_graphqlserver.test_graphql:example_middleware", 1), ] # Base span count 6: Transaction, View, Operation, Middleware, and 1 Resolver and Resolver function @@ -250,7 +219,7 @@ def test_exception_in_middleware(target_application): _test_exception_rollup_metrics = [ ("Errors/all", 1), ("Errors/allWeb", 1), - ("Errors/WebTransaction/GraphQL/test_graphql:error_middleware", 1), + ("Errors/WebTransaction/GraphQL/component_graphqlserver.test_graphql:error_middleware", 1), ] + _test_exception_scoped_metrics # Attributes @@ -267,7 +236,7 @@ def test_exception_in_middleware(target_application): } @validate_transaction_metrics( - "test_graphql:error_middleware", + "component_graphqlserver.test_graphql:error_middleware", "GraphQL", scoped_metrics=_test_exception_scoped_metrics, rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics, diff --git a/tests/framework_ariadne/__init__.py b/tests/framework_ariadne/__init__.py new file mode 100644 index 000000000..8030baccf --- /dev/null +++ b/tests/framework_ariadne/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/tests/framework_ariadne/_target_application.py b/tests/framework_ariadne/_target_application.py index bf6ef75c8..b0e19ac0d 100644 --- a/tests/framework_ariadne/_target_application.py +++ b/tests/framework_ariadne/_target_application.py @@ -17,8 +17,8 @@ import json import pytest -from _target_schema_sync import target_schema as target_schema_sync, target_asgi_application as target_asgi_application_sync, target_wsgi_application as target_wsgi_application_sync -from _target_schema_async import target_schema as target_schema_async, target_asgi_application as target_asgi_application_async +from ._target_schema_sync import target_schema as target_schema_sync, target_asgi_application as target_asgi_application_sync, target_wsgi_application as target_wsgi_application_sync +from ._target_schema_async import target_schema as target_schema_async, target_asgi_application as target_asgi_application_async from graphql import MiddlewareManager diff --git a/tests/framework_ariadne/test_application.py b/tests/framework_ariadne/test_application.py index 40c63bf76..fd294ac92 100644 --- a/tests/framework_ariadne/test_application.py +++ b/tests/framework_ariadne/test_application.py @@ -17,7 +17,7 @@ @pytest.fixture(scope="session", params=["sync-sync", "async-sync", "async-async", "wsgi-sync", "asgi-sync", "asgi-async"]) def target_application(request): - from _target_application import target_application + from ._target_application import target_application target_application = target_application[request.param] try: diff --git a/tests/framework_graphene/__init__.py b/tests/framework_graphene/__init__.py new file mode 100644 index 000000000..8030baccf --- /dev/null +++ b/tests/framework_graphene/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/tests/framework_graphene/_target_application.py b/tests/framework_graphene/_target_application.py index 50acc776f..22d18897a 100644 --- a/tests/framework_graphene/_target_application.py +++ b/tests/framework_graphene/_target_application.py @@ -11,150 +11,84 @@ # 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. -from graphene import Field, Int, List -from graphene import Mutation as GrapheneMutation -from graphene import NonNull, ObjectType, Schema, String, Union - - -class Author(ObjectType): - first_name = String() - last_name = String() - - -class Book(ObjectType): - id = Int() - name = String() - isbn = String() - author = Field(Author) - branch = String() - - -class Magazine(ObjectType): - id = Int() - name = String() - issue = Int() - branch = String() - - -class Item(Union): - class Meta: - types = (Book, Magazine) - - -class Library(ObjectType): - id = Int() - branch = String() - magazine = Field(List(Magazine)) - book = Field(List(Book)) - - -Storage = List(String) - - -authors = [ - Author( - first_name="New", - last_name="Relic", - ), - Author( - first_name="Bob", - last_name="Smith", - ), - Author( - first_name="Leslie", - last_name="Jones", - ), -] +from graphql import __version__ as version +from newrelic.packages import six -books = [ - Book( - id=1, - name="Python Agent: The Book", - isbn="a-fake-isbn", - author=authors[0], - branch="riverside", - ), - Book( - id=2, - name="Ollies for O11y: A Sk8er's Guide to Observability", - isbn="a-second-fake-isbn", - author=authors[1], - branch="downtown", - ), - Book( - id=3, - name="[Redacted]", - isbn="a-third-fake-isbn", - author=authors[2], - branch="riverside", - ), -] +from ._target_schema_sync import target_schema as target_schema_sync -magazines = [ - Magazine(id=1, name="Reli Updates Weekly", issue=1, branch="riverside"), - Magazine(id=2, name="Reli Updates Weekly", issue=2, branch="downtown"), - Magazine(id=3, name="Node Weekly", issue=1, branch="riverside"), -] +is_graphql_2 = int(version.split(".")[0]) == 2 -libraries = ["riverside", "downtown"] -libraries = [ - Library( - id=i + 1, - branch=branch, - magazine=[m for m in magazines if m.branch == branch], - book=[b for b in books if b.branch == branch], - ) - for i, branch in enumerate(libraries) -] -storage = [] +def check_response(query, response): + if isinstance(query, str) and "error" not in query: + assert not response.errors, response + assert response.data + else: + assert response.errors, response -class StorageAdd(GrapheneMutation): - class Arguments: - string = String(required=True) +def run_sync(schema): + def _run_sync(query, middleware=None): + response = schema.execute(query, middleware=middleware) + check_response(query, response) - string = String() - - def mutate(self, info, string): - storage.append(string) - return String(string=string) - - -class Query(ObjectType): - library = Field(Library, index=Int(required=True)) - hello = String() - search = Field(List(Item), contains=String(required=True)) - echo = Field(String, echo=String(required=True)) - storage = Storage - error = String() - - def resolve_library(self, info, index): - return libraries[index] - - def resolve_storage(self, info): - return storage - - def resolve_search(self, info, contains): - search_books = [b for b in books if contains in b.name] - search_magazines = [m for m in magazines if contains in m.name] - return search_books + search_magazines - - def resolve_hello(self, info): - return "Hello!" - - def resolve_echo(self, info, echo): - return echo - - def resolve_error(self, info): - raise RuntimeError("Runtime Error!") - - error_non_null = Field(NonNull(String), resolver=resolve_error) - - -class Mutation(ObjectType): - storage_add = StorageAdd.Field() - - -_target_application = Schema(query=Query, mutation=Mutation, auto_camelcase=False) + return response.data + return _run_sync + + +def run_async(schema): + import asyncio + def _run_async(query, middleware=None): + loop = asyncio.get_event_loop() + response = loop.run_until_complete(schema.execute_async(query, middleware=middleware)) + check_response(query, response) + + return response.data + return _run_async + +def run_promise(schema): + def _run_promise(query, middleware=None): + response = schema.execute(query, middleware=middleware, return_promise=True).get() + check_response(query, response) + + return response.data + return _run_promise + + +def run_promise(schema, scheduler): + from graphql import graphql + from promise import set_default_scheduler + + def _run_promise(query, middleware=None): + set_default_scheduler(scheduler) + + promise = graphql(schema, query, middleware=middleware, return_promise=True) + response = promise.get() + + check_response(query, response) + + return response.data + + return _run_promise + + +target_application = { + "sync-sync": run_sync(target_schema_sync), +} + +if is_graphql_2: + from ._target_schema_promise import target_schema as target_schema_promise + from promise.schedulers.immediate import ImmediateScheduler + + if six.PY3: + from promise.schedulers.asyncio import AsyncioScheduler as AsyncScheduler + else: + from promise.schedulers.thread import ThreadScheduler as AsyncScheduler + + target_application["sync-promise"] = run_promise(target_schema_promise, ImmediateScheduler()) + target_application["async-promise"] = run_promise(target_schema_promise, AsyncScheduler()) +elif six.PY3: + from ._target_schema_async import target_schema as target_schema_async + target_application["async-sync"] = run_async(target_schema_sync) + target_application["async-async"] = run_async(target_schema_async) diff --git a/tests/framework_graphene/_target_schema_async.py b/tests/framework_graphene/_target_schema_async.py new file mode 100644 index 000000000..39905f2f9 --- /dev/null +++ b/tests/framework_graphene/_target_schema_async.py @@ -0,0 +1,72 @@ +# 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. +from graphene import Field, Int, List +from graphene import Mutation as GrapheneMutation +from graphene import NonNull, ObjectType, Schema, String, Union + +from ._target_schema_sync import Author, Book, Magazine, Item, Library, Storage, authors, books, magazines, libraries + + +storage = [] + + +async def resolve_library(self, info, index): + return libraries[index] + +async def resolve_storage(self, info): + return [storage.pop()] + +async def resolve_search(self, info, contains): + search_books = [b for b in books if contains in b.name] + search_magazines = [m for m in magazines if contains in m.name] + return search_books + search_magazines + +async def resolve_hello(self, info): + return "Hello!" + +async def resolve_echo(self, info, echo): + return echo + +async def resolve_error(self, info): + raise RuntimeError("Runtime Error!") + +async def resolve_storage_add(self, info, string): + storage.append(string) + return StorageAdd(string=string) + + +class StorageAdd(GrapheneMutation): + class Arguments: + string = String(required=True) + + string = String() + mutate = resolve_storage_add + + +class Query(ObjectType): + library = Field(Library, index=Int(required=True), resolver=resolve_library) + hello = String(resolver=resolve_hello) + search = Field(List(Item), contains=String(required=True), resolver=resolve_search) + echo = Field(String, echo=String(required=True), resolver=resolve_echo) + storage = Field(Storage, resolver=resolve_storage) + error = String(resolver=resolve_error) + error_non_null = Field(NonNull(String), resolver=resolve_error) + error_middleware = String(resolver=resolve_hello) + + +class Mutation(ObjectType): + storage_add = StorageAdd.Field() + + +target_schema = Schema(query=Query, mutation=Mutation, auto_camelcase=False) diff --git a/tests/framework_graphene/_target_schema_promise.py b/tests/framework_graphene/_target_schema_promise.py new file mode 100644 index 000000000..905f47a0b --- /dev/null +++ b/tests/framework_graphene/_target_schema_promise.py @@ -0,0 +1,80 @@ +# 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. +from graphene import Field, Int, List +from graphene import Mutation as GrapheneMutation +from graphene import NonNull, ObjectType, Schema, String, Union +from promise import promisify + +from ._target_schema_sync import Author, Book, Magazine, Item, Library, Storage, authors, books, magazines, libraries + + +storage = [] + + +@promisify +def resolve_library(self, info, index): + return libraries[index] + +@promisify +def resolve_storage(self, info): + return [storage.pop()] + +@promisify +def resolve_search(self, info, contains): + search_books = [b for b in books if contains in b.name] + search_magazines = [m for m in magazines if contains in m.name] + return search_books + search_magazines + +@promisify +def resolve_hello(self, info): + return "Hello!" + +@promisify +def resolve_echo(self, info, echo): + return echo + +@promisify +def resolve_error(self, info): + raise RuntimeError("Runtime Error!") + +@promisify +def resolve_storage_add(self, info, string): + storage.append(string) + return StorageAdd(string=string) + + +class StorageAdd(GrapheneMutation): + class Arguments: + string = String(required=True) + + string = String() + mutate = resolve_storage_add + + +class Query(ObjectType): + library = Field(Library, index=Int(required=True), resolver=resolve_library) + hello = String(resolver=resolve_hello) + search = Field(List(Item), contains=String(required=True), resolver=resolve_search) + echo = Field(String, echo=String(required=True), resolver=resolve_echo) + storage = Field(Storage, resolver=resolve_storage) + error = String(resolver=resolve_error) + error_non_null = Field(NonNull(String), resolver=resolve_error) + error_middleware = String(resolver=resolve_hello) + + +class Mutation(ObjectType): + storage_add = StorageAdd.Field() + + +target_schema = Schema(query=Query, mutation=Mutation, auto_camelcase=False) diff --git a/tests/framework_graphene/_target_schema_sync.py b/tests/framework_graphene/_target_schema_sync.py new file mode 100644 index 000000000..b59179065 --- /dev/null +++ b/tests/framework_graphene/_target_schema_sync.py @@ -0,0 +1,162 @@ +# 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. +from graphene import Field, Int, List +from graphene import Mutation as GrapheneMutation +from graphene import NonNull, ObjectType, Schema, String, Union + + +class Author(ObjectType): + first_name = String() + last_name = String() + + +class Book(ObjectType): + id = Int() + name = String() + isbn = String() + author = Field(Author) + branch = String() + + +class Magazine(ObjectType): + id = Int() + name = String() + issue = Int() + branch = String() + + +class Item(Union): + class Meta: + types = (Book, Magazine) + + +class Library(ObjectType): + id = Int() + branch = String() + magazine = Field(List(Magazine)) + book = Field(List(Book)) + + +Storage = List(String) + + +authors = [ + Author( + first_name="New", + last_name="Relic", + ), + Author( + first_name="Bob", + last_name="Smith", + ), + Author( + first_name="Leslie", + last_name="Jones", + ), +] + +books = [ + Book( + id=1, + name="Python Agent: The Book", + isbn="a-fake-isbn", + author=authors[0], + branch="riverside", + ), + Book( + id=2, + name="Ollies for O11y: A Sk8er's Guide to Observability", + isbn="a-second-fake-isbn", + author=authors[1], + branch="downtown", + ), + Book( + id=3, + name="[Redacted]", + isbn="a-third-fake-isbn", + author=authors[2], + branch="riverside", + ), +] + +magazines = [ + Magazine(id=1, name="Reli Updates Weekly", issue=1, branch="riverside"), + Magazine(id=2, name="Reli Updates Weekly", issue=2, branch="downtown"), + Magazine(id=3, name="Node Weekly", issue=1, branch="riverside"), +] + + +libraries = ["riverside", "downtown"] +libraries = [ + Library( + id=i + 1, + branch=branch, + magazine=[m for m in magazines if m.branch == branch], + book=[b for b in books if b.branch == branch], + ) + for i, branch in enumerate(libraries) +] + +storage = [] + + +def resolve_library(self, info, index): + return libraries[index] + +def resolve_storage(self, info): + return [storage.pop()] + +def resolve_search(self, info, contains): + search_books = [b for b in books if contains in b.name] + search_magazines = [m for m in magazines if contains in m.name] + return search_books + search_magazines + +def resolve_hello(self, info): + return "Hello!" + +def resolve_echo(self, info, echo): + return echo + +def resolve_error(self, info): + raise RuntimeError("Runtime Error!") + +def resolve_storage_add(self, info, string): + storage.append(string) + return StorageAdd(string=string) + + +class StorageAdd(GrapheneMutation): + class Arguments: + string = String(required=True) + + string = String() + mutate = resolve_storage_add + + +class Query(ObjectType): + library = Field(Library, index=Int(required=True), resolver=resolve_library) + hello = String(resolver=resolve_hello) + search = Field(List(Item), contains=String(required=True), resolver=resolve_search) + echo = Field(String, echo=String(required=True), resolver=resolve_echo) + storage = Field(Storage, resolver=resolve_storage) + error = String(resolver=resolve_error) + error_non_null = Field(NonNull(String), resolver=resolve_error) + error_middleware = String(resolver=resolve_hello) + + +class Mutation(ObjectType): + storage_add = StorageAdd.Field() + + +target_schema = Schema(query=Query, mutation=Mutation, auto_camelcase=False) diff --git a/tests/framework_graphene/test_application.py b/tests/framework_graphene/test_application.py index b4e8e0739..c4d1f15d6 100644 --- a/tests/framework_graphene/test_application.py +++ b/tests/framework_graphene/test_application.py @@ -13,507 +13,30 @@ # limitations under the License. import pytest -import six -from testing_support.fixtures import ( - dt_enabled, - validate_transaction_errors, - validate_transaction_metrics, -) -from testing_support.validators.validate_span_events import validate_span_events -from testing_support.validators.validate_transaction_count import ( - validate_transaction_count, -) -from newrelic.api.background_task import background_task -from newrelic.common.object_names import callable_name +from framework_graphql.test_application import * -@pytest.fixture(scope="session") -def is_graphql_2(): - from graphql import __version__ as version +@pytest.fixture(scope="session", params=["sync-sync", "async-sync", "async-async", "sync-promise", "async-promise"]) +def target_application(request): + from ._target_application import target_application - major_version = int(version.split(".")[0]) - return major_version == 2 + target_application = target_application.get(request.param, None) + if target_application is None: + pytest.skip("Unsupported combination.") + return + try: + import graphene + version = graphene.__version__ + except Exception: + import pkg_resources + version = pkg_resources.get_distribution("graphene").version -@pytest.fixture(scope="session") -def graphql_run(): - """Wrapper function to simulate framework_graphql test behavior.""" + param = request.param.split("-") + is_background = param[0] not in {"wsgi", "asgi"} + schema_type = param[1] + extra_spans = 4 if param[0] == "wsgi" else 0 - def execute(schema, *args, **kwargs): - return schema.execute(*args, **kwargs) - - return execute - - -def to_graphql_source(query): - def delay_import(): - try: - from graphql import Source - except ImportError: - # Fallback if Source is not implemented - return query - - from graphql import __version__ as version - - # For graphql2, Source objects aren't acceptable input - major_version = int(version.split(".")[0]) - if major_version == 2: - return query - - return Source(query) - - return delay_import - - -def example_middleware(next, root, info, **args): # pylint: disable=W0622 - return_value = next(root, info, **args) - return return_value - - -def error_middleware(next, root, info, **args): # pylint: disable=W0622 - raise RuntimeError("Runtime Error!") - - -_runtime_error_name = callable_name(RuntimeError) -_test_runtime_error = [(_runtime_error_name, "Runtime Error!")] -_graphql_base_rollup_metrics = [ - ("OtherTransaction/all", 1), - ("GraphQL/all", 1), - ("GraphQL/allOther", 1), - ("GraphQL/Graphene/all", 1), - ("GraphQL/Graphene/allOther", 1), -] - - -def test_basic(app, graphql_run): - from graphql import __version__ as version - - from newrelic.hooks.framework_graphene import framework_details - - FRAMEWORK_METRICS = [ - ("Python/Framework/Graphene/%s" % framework_details()[1], 1), - ("Python/Framework/GraphQL/%s" % version, 1), - ] - - @validate_transaction_metrics( - "query//hello", - "GraphQL", - rollup_metrics=_graphql_base_rollup_metrics + FRAMEWORK_METRICS, - background_task=True, - ) - @background_task() - def _test(): - response = graphql_run(app, "{ hello }") - assert not response.errors - - _test() - - -@dt_enabled -def test_query_and_mutation(app, graphql_run): - from graphql import __version__ as version - - FRAMEWORK_METRICS = [ - ("Python/Framework/GraphQL/%s" % version, 1), - ] - _test_mutation_scoped_metrics = [ - ("GraphQL/resolve/Graphene/storage", 1), - ("GraphQL/resolve/Graphene/storage_add", 1), - ("GraphQL/operation/Graphene/query//storage", 1), - ("GraphQL/operation/Graphene/mutation//storage_add.string", 1), - ] - _test_mutation_unscoped_metrics = [ - ("OtherTransaction/all", 1), - ("GraphQL/all", 2), - ("GraphQL/Graphene/all", 2), - ("GraphQL/allOther", 2), - ("GraphQL/Graphene/allOther", 2), - ] + _test_mutation_scoped_metrics - - _expected_mutation_operation_attributes = { - "graphql.operation.type": "mutation", - "graphql.operation.name": "", - } - _expected_mutation_resolver_attributes = { - "graphql.field.name": "storage_add", - "graphql.field.parentType": "Mutation", - "graphql.field.path": "storage_add", - "graphql.field.returnType": "StorageAdd", - } - _expected_query_operation_attributes = { - "graphql.operation.type": "query", - "graphql.operation.name": "", - } - _expected_query_resolver_attributes = { - "graphql.field.name": "storage", - "graphql.field.parentType": "Query", - "graphql.field.path": "storage", - "graphql.field.returnType": "[String]", - } - - @validate_transaction_metrics( - "query//storage", - "GraphQL", - scoped_metrics=_test_mutation_scoped_metrics, - rollup_metrics=_test_mutation_unscoped_metrics + FRAMEWORK_METRICS, - background_task=True, - ) - @validate_span_events(exact_agents=_expected_mutation_operation_attributes) - @validate_span_events(exact_agents=_expected_mutation_resolver_attributes) - @validate_span_events(exact_agents=_expected_query_operation_attributes) - @validate_span_events(exact_agents=_expected_query_resolver_attributes) - @background_task() - def _test(): - response = graphql_run(app, 'mutation { storage_add(string: "abc") { string } }') - assert not response.errors - response = graphql_run(app, "query { storage }") - assert not response.errors - - # These are separate assertions because pypy stores 'abc' as a unicode string while other Python versions do not - assert "storage" in str(response.data) - assert "abc" in str(response.data) - - _test() - - -@dt_enabled -def test_middleware(app, graphql_run, is_graphql_2): - _test_middleware_metrics = [ - ("GraphQL/operation/Graphene/query//hello", 1), - ("GraphQL/resolve/Graphene/hello", 1), - ("Function/test_application:example_middleware", 1), - ] - - @validate_transaction_metrics( - "query//hello", - "GraphQL", - scoped_metrics=_test_middleware_metrics, - rollup_metrics=_test_middleware_metrics + _graphql_base_rollup_metrics, - background_task=True, - ) - # Span count 5: Transaction, Operation, Middleware, and 1 Resolver and 1 Resolver Function - @validate_span_events(count=5) - @background_task() - def _test(): - response = graphql_run(app, "{ hello }", middleware=[example_middleware]) - assert not response.errors - assert "Hello!" in str(response.data) - - _test() - - -@dt_enabled -def test_exception_in_middleware(app, graphql_run): - query = "query MyQuery { hello }" - field = "hello" - - # Metrics - _test_exception_scoped_metrics = [ - ("GraphQL/operation/Graphene/query/MyQuery/%s" % field, 1), - ("GraphQL/resolve/Graphene/%s" % field, 1), - ] - _test_exception_rollup_metrics = [ - ("Errors/all", 1), - ("Errors/allOther", 1), - ("Errors/OtherTransaction/GraphQL/test_application:error_middleware", 1), - ] + _test_exception_scoped_metrics - - # Attributes - _expected_exception_resolver_attributes = { - "graphql.field.name": field, - "graphql.field.parentType": "Query", - "graphql.field.path": field, - "graphql.field.returnType": "String", - } - _expected_exception_operation_attributes = { - "graphql.operation.type": "query", - "graphql.operation.name": "MyQuery", - "graphql.operation.query": query, - } - - @validate_transaction_metrics( - "test_application:error_middleware", - "GraphQL", - scoped_metrics=_test_exception_scoped_metrics, - rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics, - background_task=True, - ) - @validate_span_events(exact_agents=_expected_exception_operation_attributes) - @validate_span_events(exact_agents=_expected_exception_resolver_attributes) - @validate_transaction_errors(errors=_test_runtime_error) - @background_task() - def _test(): - response = graphql_run(app, query, middleware=[error_middleware]) - assert response.errors - - _test() - - -@pytest.mark.parametrize("field", ("error", "error_non_null")) -@dt_enabled -def test_exception_in_resolver(app, graphql_run, field): - query = "query MyQuery { %s }" % field - - if six.PY2: - txn_name = "_target_application:resolve_error" - else: - txn_name = "_target_application:Query.resolve_error" - - # Metrics - _test_exception_scoped_metrics = [ - ("GraphQL/operation/Graphene/query/MyQuery/%s" % field, 1), - ("GraphQL/resolve/Graphene/%s" % field, 1), - ] - _test_exception_rollup_metrics = [ - ("Errors/all", 1), - ("Errors/allOther", 1), - ("Errors/OtherTransaction/GraphQL/%s" % txn_name, 1), - ] + _test_exception_scoped_metrics - - # Attributes - _expected_exception_resolver_attributes = { - "graphql.field.name": field, - "graphql.field.parentType": "Query", - "graphql.field.path": field, - "graphql.field.returnType": "String!" if "non_null" in field else "String", - } - _expected_exception_operation_attributes = { - "graphql.operation.type": "query", - "graphql.operation.name": "MyQuery", - "graphql.operation.query": query, - } - - @validate_transaction_metrics( - txn_name, - "GraphQL", - scoped_metrics=_test_exception_scoped_metrics, - rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics, - background_task=True, - ) - @validate_span_events(exact_agents=_expected_exception_operation_attributes) - @validate_span_events(exact_agents=_expected_exception_resolver_attributes) - @validate_transaction_errors(errors=_test_runtime_error) - @background_task() - def _test(): - response = graphql_run(app, query) - assert response.errors - - _test() - - -@dt_enabled -@pytest.mark.parametrize( - "query,exc_class", - [ - ("query MyQuery { missing_field }", "GraphQLError"), - ("{ syntax_error ", "graphql.error.syntax_error:GraphQLSyntaxError"), - ], -) -def test_exception_in_validation(app, graphql_run, is_graphql_2, query, exc_class): - if "syntax" in query: - txn_name = "graphql.language.parser:parse" - else: - if is_graphql_2: - txn_name = "graphql.validation.validation:validate" - else: - txn_name = "graphql.validation.validate:validate" - - # Import path differs between versions - if exc_class == "GraphQLError": - from graphql.error import GraphQLError - - exc_class = callable_name(GraphQLError) - - _test_exception_scoped_metrics = [ - ("GraphQL/operation/Graphene///", 1), - ] - _test_exception_rollup_metrics = [ - ("Errors/all", 1), - ("Errors/allOther", 1), - ("Errors/OtherTransaction/GraphQL/%s" % txn_name, 1), - ] + _test_exception_scoped_metrics - - # Attributes - _expected_exception_operation_attributes = { - "graphql.operation.type": "", - "graphql.operation.name": "", - "graphql.operation.query": query, - } - - @validate_transaction_metrics( - txn_name, - "GraphQL", - scoped_metrics=_test_exception_scoped_metrics, - rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics, - background_task=True, - ) - @validate_span_events(exact_agents=_expected_exception_operation_attributes) - @validate_transaction_errors(errors=[exc_class]) - @background_task() - def _test(): - response = graphql_run(app, query) - assert response.errors - - _test() - - -@dt_enabled -def test_operation_metrics_and_attrs(app, graphql_run): - operation_metrics = [("GraphQL/operation/Graphene/query/MyQuery/library", 1)] - operation_attrs = { - "graphql.operation.type": "query", - "graphql.operation.name": "MyQuery", - } - - @validate_transaction_metrics( - "query/MyQuery/library", - "GraphQL", - scoped_metrics=operation_metrics, - rollup_metrics=operation_metrics + _graphql_base_rollup_metrics, - background_task=True, - ) - # Span count 16: Transaction, Operation, and 7 Resolvers and Resolver functions - # library, library.name, library.book - # library.book.name and library.book.id for each book resolved (in this case 2) - @validate_span_events(count=16) - @validate_span_events(exact_agents=operation_attrs) - @background_task() - def _test(): - response = graphql_run(app, "query MyQuery { library(index: 0) { branch, book { id, name } } }") - assert not response.errors - - _test() - - -@dt_enabled -def test_field_resolver_metrics_and_attrs(app, graphql_run): - field_resolver_metrics = [("GraphQL/resolve/Graphene/hello", 1)] - graphql_attrs = { - "graphql.field.name": "hello", - "graphql.field.parentType": "Query", - "graphql.field.path": "hello", - "graphql.field.returnType": "String", - } - - @validate_transaction_metrics( - "query//hello", - "GraphQL", - scoped_metrics=field_resolver_metrics, - rollup_metrics=field_resolver_metrics + _graphql_base_rollup_metrics, - background_task=True, - ) - # Span count 4: Transaction, Operation, and 1 Resolver and Resolver function - @validate_span_events(count=4) - @validate_span_events(exact_agents=graphql_attrs) - @background_task() - def _test(): - response = graphql_run(app, "{ hello }") - assert not response.errors - assert "Hello!" in str(response.data) - - _test() - - -_test_queries = [ - ("{ hello }", "{ hello }"), # Basic query extraction - ("{ error }", "{ error }"), # Extract query on field error - (to_graphql_source("{ hello }"), "{ hello }"), # Extract query from Source objects - ("{ library(index: 0) { branch } }", "{ library(index: ?) { branch } }"), # Integers - ('{ echo(echo: "123") }', "{ echo(echo: ?) }"), # Strings with numerics - ('{ echo(echo: "test") }', "{ echo(echo: ?) }"), # Strings - ('{ TestEcho: echo(echo: "test") }', "{ TestEcho: echo(echo: ?) }"), # Aliases - ('{ TestEcho: echo(echo: "test") }', "{ TestEcho: echo(echo: ?) }"), # Variables - ( # Fragments - '{ ...MyFragment } fragment MyFragment on Query { echo(echo: "test") }', - "{ ...MyFragment } fragment MyFragment on Query { echo(echo: ?) }", - ), -] - - -@dt_enabled -@pytest.mark.parametrize("query,obfuscated", _test_queries) -def test_query_obfuscation(app, graphql_run, query, obfuscated): - graphql_attrs = {"graphql.operation.query": obfuscated} - - if callable(query): - query = query() - - @validate_span_events(exact_agents=graphql_attrs) - @background_task() - def _test(): - response = graphql_run(app, query) - if not isinstance(query, str) or "error" not in query: - assert not response.errors - - _test() - - -_test_queries = [ - ("{ hello }", "/hello"), # Basic query - ("{ error }", "/error"), # Extract deepest path on field error - ('{ echo(echo: "test") }', "/echo"), # Fields with arguments - ( - "{ library(index: 0) { branch, book { isbn branch } } }", - "/library", - ), # Complex Example, 1 level - ( - "{ library(index: 0) { book { author { first_name }} } }", - "/library.book.author.first_name", - ), # Complex Example, 2 levels - ("{ library(index: 0) { id, book { name } } }", "/library.book.name"), # Filtering - ('{ TestEcho: echo(echo: "test") }', "/echo"), # Aliases - ( - '{ search(contains: "A") { __typename ... on Book { name } } }', - "/search.name", - ), # InlineFragment - ( - '{ hello echo(echo: "test") }', - "", - ), # Multiple root selections. (need to decide on final behavior) - # FragmentSpread - ( - "{ library(index: 0) { book { ...MyFragment } } } fragment MyFragment on Book { name id }", # Fragment filtering - "/library.book.name", - ), - ( - "{ library(index: 0) { book { ...MyFragment } } } fragment MyFragment on Book { author { first_name } }", - "/library.book.author.first_name", - ), - ( - "{ library(index: 0) { book { ...MyFragment } magazine { ...MagFragment } } } fragment MyFragment on Book { author { first_name } } fragment MagFragment on Magazine { name }", - "/library", - ), -] - - -@dt_enabled -@pytest.mark.parametrize("query,expected_path", _test_queries) -def test_deepest_unique_path(app, graphql_run, query, expected_path): - if expected_path == "/error": - if six.PY2: - txn_name = "_target_application:resolve_error" - else: - txn_name = "_target_application:Query.resolve_error" - else: - txn_name = "query/%s" % expected_path - - @validate_transaction_metrics( - txn_name, - "GraphQL", - background_task=True, - ) - @background_task() - def _test(): - response = graphql_run(app, query) - if "error" not in query: - assert not response.errors - - _test() - - -@validate_transaction_count(0) -@background_task() -def test_ignored_introspection_transactions(app, graphql_run): - response = graphql_run(app, "{ __schema { types { name } } }") - assert not response.errors + assert version is not None + return "Graphene", version, target_application, is_background, schema_type, extra_spans diff --git a/tests/framework_graphql/__init__.py b/tests/framework_graphql/__init__.py new file mode 100644 index 000000000..8030baccf --- /dev/null +++ b/tests/framework_graphql/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/tests/framework_graphql/_target_application.py b/tests/framework_graphql/_target_application.py index 903c72137..5ed7d9edd 100644 --- a/tests/framework_graphql/_target_application.py +++ b/tests/framework_graphql/_target_application.py @@ -18,7 +18,7 @@ from newrelic.packages import six from newrelic.hooks.framework_graphql import is_promise -from _target_schema_sync import target_schema as target_schema_sync +from ._target_schema_sync import target_schema as target_schema_sync is_graphql_2 = int(version.split(".")[0]) == 2 @@ -86,7 +86,7 @@ def _run_promise(query, middleware=None): } if is_graphql_2: - from _target_schema_promise import target_schema as target_schema_promise + from ._target_schema_promise import target_schema as target_schema_promise from promise.schedulers.immediate import ImmediateScheduler if six.PY3: @@ -97,6 +97,6 @@ def _run_promise(query, middleware=None): target_application["sync-promise"] = run_promise(target_schema_promise, ImmediateScheduler()) target_application["async-promise"] = run_promise(target_schema_promise, AsyncScheduler()) elif six.PY3: - from _target_schema_async import target_schema as target_schema_async + from ._target_schema_async import target_schema as target_schema_async target_application["async-sync"] = run_async(target_schema_sync) target_application["async-async"] = run_async(target_schema_async) diff --git a/tests/framework_graphql/_target_schema_async.py b/tests/framework_graphql/_target_schema_async.py index b57c36bff..1ea417c10 100644 --- a/tests/framework_graphql/_target_schema_async.py +++ b/tests/framework_graphql/_target_schema_async.py @@ -25,7 +25,7 @@ ) try: - from _target_schema_sync import books, libraries, magazines + from ._target_schema_sync import books, libraries, magazines except ImportError: from framework_graphql._target_schema_sync import books, libraries, magazines diff --git a/tests/framework_graphql/_target_schema_promise.py b/tests/framework_graphql/_target_schema_promise.py index ea08639e1..b0bf8cef7 100644 --- a/tests/framework_graphql/_target_schema_promise.py +++ b/tests/framework_graphql/_target_schema_promise.py @@ -23,12 +23,9 @@ GraphQLString, GraphQLUnionType, ) -from promise import Promise, promisify +from promise import promisify -try: - from _target_schema_sync import books, libraries, magazines -except ImportError: - from framework_graphql._target_schema_sync import books, libraries, magazines +from ._target_schema_sync import books, libraries, magazines storage = [] @@ -49,6 +46,7 @@ def resolve_storage(parent, info): return [storage.pop()] +@promisify def resolve_search(parent, info, contains): search_books = [b for b in books if contains in b["name"]] search_magazines = [m for m in magazines if contains in m["name"]] diff --git a/tests/framework_graphql/conftest.py b/tests/framework_graphql/conftest.py index 1b04348ea..e46f70f91 100644 --- a/tests/framework_graphql/conftest.py +++ b/tests/framework_graphql/conftest.py @@ -40,12 +40,9 @@ default_settings=_default_settings, ) -apps = ["sync-sync", "async-sync", "async-async", "sync-promise", "async-promise"] - - -@pytest.fixture(scope="session", params=apps) +@pytest.fixture(scope="session", params=["sync-sync", "async-sync", "async-async", "sync-promise", "async-promise"]) def target_application(request): - from _target_application import target_application + from ._target_application import target_application app = target_application.get(request.param, None) if app is None: diff --git a/tests/framework_graphql/test_application.py b/tests/framework_graphql/test_application.py index 6566ff0f0..8ac499273 100644 --- a/tests/framework_graphql/test_application.py +++ b/tests/framework_graphql/test_application.py @@ -144,11 +144,12 @@ def _test(): def test_query_and_mutation(target_application, is_graphql_2): framework, version, target_application, is_bg, schema_type, extra_spans = target_application + mutation_path = "storage_add" if framework != "Graphene" else "storage_add.string" type_annotation = "!" if framework == "Strawberry" else "" _test_mutation_scoped_metrics = [ ("GraphQL/resolve/%s/storage_add" % framework, 1), - ("GraphQL/operation/%s/mutation//storage_add" % framework, 1), + ("GraphQL/operation/%s/mutation//%s" % (framework, mutation_path), 1), ] _test_query_scoped_metrics = [ ("GraphQL/resolve/%s/storage" % framework, 1), @@ -163,7 +164,7 @@ def test_query_and_mutation(target_application, is_graphql_2): "graphql.field.name": "storage_add", "graphql.field.parentType": "Mutation", "graphql.field.path": "storage_add", - "graphql.field.returnType": "String" + type_annotation, + "graphql.field.returnType": ("String" if framework != "Graphene" else "StorageAdd") + type_annotation, } _expected_query_operation_attributes = { "graphql.operation.type": "query", @@ -176,22 +177,28 @@ def test_query_and_mutation(target_application, is_graphql_2): "graphql.field.returnType": "[String%s]%s" % (type_annotation, type_annotation), } - @validate_code_level_metrics("_target_schema_%s" % schema_type, "resolve_storage_add") + @validate_code_level_metrics("framework_%s._target_schema_%s" % (framework.lower(), schema_type), "resolve_storage_add") + @validate_span_events(exact_agents=_expected_mutation_operation_attributes) + @validate_span_events(exact_agents=_expected_mutation_resolver_attributes) @validate_transaction_metrics( - "mutation//storage_add", + "mutation//%s" % mutation_path, "GraphQL", scoped_metrics=_test_mutation_scoped_metrics, rollup_metrics=_test_mutation_scoped_metrics + _graphql_base_rollup_metrics(framework, version, is_bg), background_task=is_bg, ) - @validate_span_events(exact_agents=_expected_mutation_operation_attributes) - @validate_span_events(exact_agents=_expected_mutation_resolver_attributes) @conditional_decorator(background_task(), is_bg) def _mutation(): - response = target_application('mutation { storage_add(string: "abc") }') - assert response["storage_add"] == "abc" + if framework == "Graphene": + query = 'mutation { storage_add(string: "abc") { string } }' + else: + query = 'mutation { storage_add(string: "abc") }' + response = target_application(query) + assert response["storage_add"] == "abc" or response["storage_add"]["string"] == "abc" - @validate_code_level_metrics("_target_schema_%s" % schema_type, "resolve_storage") + @validate_code_level_metrics("framework_%s._target_schema_%s" % (framework.lower(), schema_type), "resolve_storage") + @validate_span_events(exact_agents=_expected_query_operation_attributes) + @validate_span_events(exact_agents=_expected_query_resolver_attributes) @validate_transaction_metrics( "query//storage", "GraphQL", @@ -199,8 +206,6 @@ def _mutation(): rollup_metrics=_test_query_scoped_metrics + _graphql_base_rollup_metrics(framework, version, is_bg), background_task=is_bg, ) - @validate_span_events(exact_agents=_expected_query_operation_attributes) - @validate_span_events(exact_agents=_expected_query_resolver_attributes) @conditional_decorator(background_task(), is_bg) def _query(): response = target_application("query { storage }") @@ -230,7 +235,7 @@ def test_middleware(target_application, middleware): span_count = 5 + extra_spans @validate_code_level_metrics(*name.split(":")) - @validate_code_level_metrics("_target_schema_%s" % schema_type, "resolve_hello") + @validate_code_level_metrics("framework_%s._target_schema_%s" % (framework.lower(), schema_type), "resolve_hello") @validate_span_events(count=span_count) @validate_transaction_metrics( "query//hello", @@ -307,7 +312,7 @@ def test_exception_in_resolver(target_application, field): framework, version, target_application, is_bg, schema_type, extra_spans = target_application query = "query MyQuery { %s }" % field - txn_name = "_target_schema_%s:resolve_error" % schema_type + txn_name = "framework_%s._target_schema_%s:resolve_error" % (framework.lower(), schema_type) # Metrics _test_exception_scoped_metrics = [ @@ -547,7 +552,7 @@ def _test(): def test_deepest_unique_path(target_application, query, expected_path): framework, version, target_application, is_bg, schema_type, extra_spans = target_application if expected_path == "/error": - txn_name = "_target_schema_%s:resolve_error" % schema_type + txn_name = "framework_%s._target_schema_%s:resolve_error" % (framework.lower(), schema_type) else: txn_name = "query/%s" % expected_path diff --git a/tests/framework_strawberry/__init__.py b/tests/framework_strawberry/__init__.py new file mode 100644 index 000000000..8030baccf --- /dev/null +++ b/tests/framework_strawberry/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/tests/framework_strawberry/_target_application.py b/tests/framework_strawberry/_target_application.py index 29737db97..ec618be5c 100644 --- a/tests/framework_strawberry/_target_application.py +++ b/tests/framework_strawberry/_target_application.py @@ -17,8 +17,8 @@ import json import pytest -from _target_schema_sync import target_schema as target_schema_sync, target_asgi_application as target_asgi_application_sync -from _target_schema_async import target_schema as target_schema_async, target_asgi_application as target_asgi_application_async +from ._target_schema_sync import target_schema as target_schema_sync, target_asgi_application as target_asgi_application_sync +from ._target_schema_async import target_schema as target_schema_async, target_asgi_application as target_asgi_application_async def run_sync(schema): diff --git a/tests/framework_strawberry/_target_schema_async.py b/tests/framework_strawberry/_target_schema_async.py index 0b4953eb7..397166d4d 100644 --- a/tests/framework_strawberry/_target_schema_async.py +++ b/tests/framework_strawberry/_target_schema_async.py @@ -21,10 +21,7 @@ from strawberry.types.types import Optional from testing_support.asgi_testing import AsgiTest -try: - from _target_schema_sync import Library, Item, Storage, books, magazines, libraries -except ImportError: - from framework_strawberry._target_schema_sync import Library, Item, Storage, books, magazines, libraries +from ._target_schema_sync import Library, Item, Storage, books, magazines, libraries storage = [] diff --git a/tests/framework_strawberry/test_application.py b/tests/framework_strawberry/test_application.py index c705423e0..76082dee9 100644 --- a/tests/framework_strawberry/test_application.py +++ b/tests/framework_strawberry/test_application.py @@ -17,7 +17,7 @@ @pytest.fixture(scope="session", params=["sync-sync", "async-sync", "async-async", "asgi-sync", "asgi-async"]) def target_application(request): - from _target_application import target_application + from ._target_application import target_application target_application = target_application[request.param] try: diff --git a/tests/messagebroker_pika/test_pika_async_connection_consume.py b/tests/messagebroker_pika/test_pika_async_connection_consume.py index 0ed76503f..18c999845 100644 --- a/tests/messagebroker_pika/test_pika_async_connection_consume.py +++ b/tests/messagebroker_pika/test_pika_async_connection_consume.py @@ -76,7 +76,7 @@ def handle_callback_exception(self, *args, **kwargs): @parametrized_connection @pytest.mark.parametrize('callback_as_partial', [True, False]) -@validate_code_level_metrics("test_pika_async_connection_consume" + (".test_async_connection_basic_get_inside_txn." if six.PY3 else ""), "on_message") +@validate_code_level_metrics("test_pika_async_connection_consume.test_async_connection_basic_get_inside_txn.", "on_message", py2_namespace="test_pika_async_connection_consume") @validate_transaction_metrics( ('test_pika_async_connection_consume:' 'test_async_connection_basic_get_inside_txn'), @@ -269,7 +269,7 @@ def on_open_connection(connection): scoped_metrics=_test_select_conn_basic_consume_in_txn_metrics, rollup_metrics=_test_select_conn_basic_consume_in_txn_metrics, background_task=True) -@validate_code_level_metrics("test_pika_async_connection_consume" + (".test_async_connection_basic_consume_inside_txn." if six.PY3 else ""), "on_message") +@validate_code_level_metrics("test_pika_async_connection_consume.test_async_connection_basic_consume_inside_txn.", "on_message", py2_namespace="test_pika_async_connection_consume") @validate_tt_collector_json(message_broker_params=_message_broker_tt_params) @background_task() def test_async_connection_basic_consume_inside_txn(producer, ConnectionClass): @@ -329,8 +329,8 @@ def on_open_connection(connection): scoped_metrics=_test_select_conn_basic_consume_two_exchanges, rollup_metrics=_test_select_conn_basic_consume_two_exchanges, background_task=True) -@validate_code_level_metrics("test_pika_async_connection_consume" + (".test_async_connection_basic_consume_two_exchanges." if six.PY3 else ""), "on_message_1") -@validate_code_level_metrics("test_pika_async_connection_consume" + (".test_async_connection_basic_consume_two_exchanges." if six.PY3 else ""), "on_message_2") +@validate_code_level_metrics("test_pika_async_connection_consume.test_async_connection_basic_consume_two_exchanges.", "on_message_1", py2_namespace="test_pika_async_connection_consume") +@validate_code_level_metrics("test_pika_async_connection_consume.test_async_connection_basic_consume_two_exchanges.", "on_message_2", py2_namespace="test_pika_async_connection_consume") @background_task() def test_async_connection_basic_consume_two_exchanges(producer, producer_2, ConnectionClass): @@ -435,7 +435,7 @@ def on_open_connection(connection): rollup_metrics=_test_select_connection_consume_outside_txn_metrics, background_task=True, group='Message/RabbitMQ/Exchange/%s' % EXCHANGE) -@validate_code_level_metrics("test_pika_async_connection_consume" + (".test_select_connection_basic_consume_outside_transaction." if six.PY3 else ""), "on_message") +@validate_code_level_metrics("test_pika_async_connection_consume.test_select_connection_basic_consume_outside_transaction.", "on_message", py2_namespace="test_pika_async_connection_consume") def test_select_connection_basic_consume_outside_transaction(producer): def on_message(channel, method_frame, header_frame, body): assert hasattr(method_frame, '_nr_start_time') diff --git a/tests/messagebroker_pika/test_pika_blocking_connection_consume.py b/tests/messagebroker_pika/test_pika_blocking_connection_consume.py index 417055bfc..d52fce95a 100644 --- a/tests/messagebroker_pika/test_pika_blocking_connection_consume.py +++ b/tests/messagebroker_pika/test_pika_blocking_connection_consume.py @@ -133,7 +133,7 @@ def test_basic_get(): @pytest.mark.parametrize('as_partial', [True, False]) -@validate_code_level_metrics("test_pika_blocking_connection_consume" + (".test_blocking_connection_basic_consume_outside_transaction." if six.PY3 else ""), "on_message") +@validate_code_level_metrics("test_pika_blocking_connection_consume.test_blocking_connection_basic_consume_outside_transaction.", "on_message", py2_namespace="test_pika_blocking_connection_consume") @validate_transaction_metrics( _txn_name, scoped_metrics=_test_blocking_conn_basic_consume_no_txn_metrics, @@ -179,7 +179,7 @@ def on_message(channel, method_frame, header_frame, body): @pytest.mark.parametrize('as_partial', [True, False]) -@validate_code_level_metrics("test_pika_blocking_connection_consume" + (".test_blocking_connection_basic_consume_inside_txn." if six.PY3 else ""), "on_message") +@validate_code_level_metrics("test_pika_blocking_connection_consume.test_blocking_connection_basic_consume_inside_txn.", "on_message", py2_namespace="test_pika_blocking_connection_consume") @validate_transaction_metrics( ('test_pika_blocking_connection_consume:' 'test_blocking_connection_basic_consume_inside_txn'), diff --git a/tests/testing_support/validators/validate_code_level_metrics.py b/tests/testing_support/validators/validate_code_level_metrics.py index d5c4b5648..1f99d9d52 100644 --- a/tests/testing_support/validators/validate_code_level_metrics.py +++ b/tests/testing_support/validators/validate_code_level_metrics.py @@ -12,13 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +from newrelic.packages import six from testing_support.validators.validate_span_events import validate_span_events from testing_support.fixtures import dt_enabled from newrelic.common.object_wrapper import function_wrapper -def validate_code_level_metrics(namespace, function, builtin=False, count=1, index=-1): +def validate_code_level_metrics(namespace, function, py2_namespace=None, builtin=False, count=1, index=-1): """Verify that code level metrics are generated for a callable.""" + if six.PY2 and py2_namespace is not None: + namespace = py2_namespace + if builtin: validator = validate_span_events( exact_agents={"code.function": function, "code.namespace": namespace, "code.filepath": ""}, From 5b3693ed7d754e6dcb13a2c3d24eb462925c08e0 Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Mon, 10 Oct 2022 15:44:39 -0700 Subject: [PATCH 14/26] Catch GraphiQL reload to introspection --- newrelic/hooks/framework_graphql.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/newrelic/hooks/framework_graphql.py b/newrelic/hooks/framework_graphql.py index b5af068f0..b58601db6 100644 --- a/newrelic/hooks/framework_graphql.py +++ b/newrelic/hooks/framework_graphql.py @@ -54,12 +54,13 @@ def is_promise(obj): def as_promise(f): return f + if six.PY3: from newrelic.hooks.framework_graphql_py3 import ( nr_coro_execute_name_wrapper, + nr_coro_graphql_impl_wrapper, nr_coro_resolver_error_wrapper, nr_coro_resolver_wrapper, - nr_coro_graphql_impl_wrapper, ) _logger = logging.getLogger(__name__) @@ -202,7 +203,7 @@ def set_name(value=None): # Operation trace sets transaction name trace.set_transaction_name(priority=14) return value - + if is_promise(result) and result.is_pending and graphql_version() < (3, 0): return result.then(set_name) elif isawaitable(result) and not is_promise(result): @@ -390,8 +391,9 @@ def wrap_resolver(wrapped, instance, args, kwargs): with ErrorTrace(ignore=ignore_graphql_duplicate_exception): sync_start_time = time.time() result = wrapped(*args, **kwargs) - + if is_promise(result) and result.is_pending and graphql_version() < (3, 0): + @functools.wraps(wrapped) def nr_promise_resolver_error_wrapper(v): with trace: @@ -401,6 +403,7 @@ def nr_promise_resolver_error_wrapper(v): except Exception: transaction.set_transaction_name(name, "GraphQL", priority=15) raise + return as_promise(nr_promise_resolver_error_wrapper) elif isawaitable(result) and not is_promise(result): # Grab any async resolvers and wrap with traces @@ -482,6 +485,13 @@ def wrap_resolve_field(wrapped, instance, args, kwargs): else: field_path = field_path.key + # This is to catch the specific case of the user loading/reloading + # a GraphiQL page after running the program (which in turn will + # result in the field_path being a __schema, an introspection field) + if field_path in GRAPHQL_INTROSPECTION_FIELDS: + ignore_transaction() + return wrapped(*args, **kwargs) + trace = GraphQLResolverTrace( field_name, field_parent_type=parent_type.name, field_return_type=field_return_type, field_path=field_path ) @@ -497,11 +507,13 @@ def wrap_resolve_field(wrapped, instance, args, kwargs): raise if is_promise(result) and result.is_pending and graphql_version() < (3, 0): + @functools.wraps(wrapped) def nr_promise_resolver_wrapper(v): with trace: with ErrorTrace(ignore=ignore_graphql_duplicate_exception): return result.get() + return as_promise(nr_promise_resolver_wrapper) elif isawaitable(result) and not is_promise(result): # Asynchronous resolvers (returned coroutines from non-coroutine functions) @@ -563,7 +575,7 @@ def wrap_graphql_impl(wrapped, instance, args, kwargs): framework = schema._nr_framework trace.product = framework[0] transaction.add_framework_info(name=framework[0], version=framework[1]) - + # Trace must be manually started and stopped to ensure it exists prior to and during the entire duration of the query. # Otherwise subsequent instrumentation will not be able to find an operation trace and will have issues. trace.__enter__() From 34209bc6f8f3f0d2c5ed13adddaeb44de3d89f55 Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Tue, 18 Oct 2022 12:38:26 -0700 Subject: [PATCH 15/26] Instrument graphene_django framework --- newrelic/config.py | 4 ++ newrelic/hooks/framework_graphene_django.py | 67 +++++++++++++++++++++ newrelic/hooks/framework_graphql.py | 4 +- 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 newrelic/hooks/framework_graphene_django.py diff --git a/newrelic/config.py b/newrelic/config.py index 4e0912db8..d41805704 100644 --- a/newrelic/config.py +++ b/newrelic/config.py @@ -2210,6 +2210,10 @@ def _process_module_builtin_defaults(): "instrument_graphene_types_schema", ) + _process_module_definition( + "graphene_django.views", "newrelic.hooks.framework_graphene_django", "instrument_graphene_django_views" + ) + _process_module_definition( "graphql.graphql", "newrelic.hooks.framework_graphql", diff --git a/newrelic/hooks/framework_graphene_django.py b/newrelic/hooks/framework_graphene_django.py new file mode 100644 index 000000000..792b2d104 --- /dev/null +++ b/newrelic/hooks/framework_graphene_django.py @@ -0,0 +1,67 @@ +# 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. + +from newrelic.api.error_trace import ErrorTrace +from newrelic.api.graphql_trace import GraphQLOperationTrace +from newrelic.api.transaction import current_transaction +from newrelic.common.object_names import callable_name +from newrelic.common.object_wrapper import wrap_function_wrapper +from newrelic.core.graphql_utils import graphql_statement +from newrelic.hooks.framework_graphql import ( + framework_version as graphql_framework_version, +) +from newrelic.hooks.framework_graphql import ignore_graphql_duplicate_exception + + +def framework_details(): + import graphene_django + + return ("Graphene-Django", getattr(graphene_django, "__version__", None)) + + +def bind_execute(query, *args, **kwargs): + return query + + +# @promisify +def wrap_execute_graphql_request(wrapped, instance, args, kwargs): + transaction = current_transaction() + + if not transaction: + return wrapped(*args, **kwargs) + + try: + query = bind_execute(*args, **kwargs) + except TypeError: + return wrapped(*args, **kwargs) + + framework = framework_details() + transaction.add_framework_info(name=framework[0], version=framework[1]) + transaction.add_framework_info(name="GraphQL", version=graphql_framework_version()) + + if hasattr(query, "body"): + query = query.body + + transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=10) + + with GraphQLOperationTrace(source=wrapped) as trace: + trace.product = "Graphene-Django" + trace.statement = graphql_statement(query) + with ErrorTrace(ignore=ignore_graphql_duplicate_exception): + return wrapped(*args, **kwargs) + + +def instrument_graphene_django_views(module): + if hasattr(module, "GraphQLView"): + wrap_function_wrapper(module, "GraphQLView.execute_graphql_request", wrap_execute_graphql_request) diff --git a/newrelic/hooks/framework_graphql.py b/newrelic/hooks/framework_graphql.py index b58601db6..0e691090f 100644 --- a/newrelic/hooks/framework_graphql.py +++ b/newrelic/hooks/framework_graphql.py @@ -652,9 +652,9 @@ def instrument_graphql_validate(module): def instrument_graphql(module): - if hasattr(module, "graphql_impl"): + if hasattr(module, "graphql_impl"): # for graphql 3 wrap_function_wrapper(module, "graphql_impl", wrap_graphql_impl) - if hasattr(module, "execute_graphql"): + if hasattr(module, "execute_graphql"): # for graphql 2 wrap_function_wrapper(module, "execute_graphql", wrap_graphql_impl) From 6dfbc0270a24976705e8ddf6c84ad6d50db8fd29 Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Tue, 18 Oct 2022 12:39:11 -0700 Subject: [PATCH 16/26] Add tests in .tox file --- tox.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tox.ini b/tox.ini index c50a1b75b..162e14ba0 100644 --- a/tox.ini +++ b/tox.ini @@ -129,6 +129,7 @@ envlist = python-framework_graphene-{py37,py38,py39,py310}-graphenelatest, python-framework_graphene-{py27,py37,py38,py39,pypy,pypy37}-graphene{0200,0201}, python-framework_graphene-py310-graphene0201, + python-framework_graphene_django-{py37,py38,py39,py310,pypy37} python-framework_graphql-{py27,py37,py38,py39,py310,pypy,pypy37}-graphql02, python-framework_graphql-{py37,py38,py39,py310,pypy37}-graphql03, ; temporarily disabling graphqlmaster tests @@ -312,6 +313,8 @@ deps = framework_graphene-graphenelatest: graphene framework_graphene-graphene0200: graphene<2.1 framework_graphene-graphene0201: graphene<2.2 + framework_graphene_django: graphene<3 + framework_graphene_django: django framework_graphql-graphql02: graphql-core<3 framework_graphql-graphql03: graphql-core<4 framework_graphql-graphql0202: graphql-core<2.3 @@ -463,6 +466,7 @@ changedir = framework_fastapi: tests/framework_fastapi framework_flask: tests/framework_flask framework_graphene: tests/framework_graphene + framework_graphene_django: tests/framework_graphene_django framework_graphql: tests/framework_graphql framework_grpc: tests/framework_grpc framework_pyramid: tests/framework_pyramid From 1bf207198ec7e79ffcd92cd9d3a94115f4bb4966 Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Tue, 18 Oct 2022 17:25:37 -0700 Subject: [PATCH 17/26] Temporarily remove graphene_django tests from tox.ini --- tox.ini | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 162e14ba0..e1e3ff367 100644 --- a/tox.ini +++ b/tox.ini @@ -129,7 +129,7 @@ envlist = python-framework_graphene-{py37,py38,py39,py310}-graphenelatest, python-framework_graphene-{py27,py37,py38,py39,pypy,pypy37}-graphene{0200,0201}, python-framework_graphene-py310-graphene0201, - python-framework_graphene_django-{py37,py38,py39,py310,pypy37} + ; python-framework_graphene_django-{py37,py38,py39,py310,pypy37} python-framework_graphql-{py27,py37,py38,py39,py310,pypy,pypy37}-graphql02, python-framework_graphql-{py37,py38,py39,py310,pypy37}-graphql03, ; temporarily disabling graphqlmaster tests @@ -313,8 +313,9 @@ deps = framework_graphene-graphenelatest: graphene framework_graphene-graphene0200: graphene<2.1 framework_graphene-graphene0201: graphene<2.2 - framework_graphene_django: graphene<3 - framework_graphene_django: django + ; framework_graphene_django: graphene-django + ; framework_graphene_django: graphene + ; framework_graphene_django: django framework_graphql-graphql02: graphql-core<3 framework_graphql-graphql03: graphql-core<4 framework_graphql-graphql0202: graphql-core<2.3 @@ -466,7 +467,7 @@ changedir = framework_fastapi: tests/framework_fastapi framework_flask: tests/framework_flask framework_graphene: tests/framework_graphene - framework_graphene_django: tests/framework_graphene_django + ; framework_graphene_django: tests/framework_graphene_django framework_graphql: tests/framework_graphql framework_grpc: tests/framework_grpc framework_pyramid: tests/framework_pyramid From 5b67b3a8018c15c5fa1a6209efaed3192ecb228c Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Sun, 23 Oct 2022 08:46:33 -0700 Subject: [PATCH 18/26] Add initial test setup for graphene-django --- .../_target_application.py | 18 + tests/framework_graphene_django/conftest.py | 45 ++ tests/framework_graphene_django/models.py | 17 + tests/framework_graphene_django/schema.py | 47 ++ tests/framework_graphene_django/settings.py | 68 ++ .../test_application.py | 607 ++++++++++++++++++ tests/framework_graphene_django/urls.py | 59 ++ tests/framework_graphene_django/views.py | 131 ++++ tests/framework_graphene_django/wsgi.py | 27 + tox.ini | 10 +- 10 files changed, 1024 insertions(+), 5 deletions(-) create mode 100644 tests/framework_graphene_django/_target_application.py create mode 100644 tests/framework_graphene_django/conftest.py create mode 100644 tests/framework_graphene_django/models.py create mode 100644 tests/framework_graphene_django/schema.py create mode 100644 tests/framework_graphene_django/settings.py create mode 100644 tests/framework_graphene_django/test_application.py create mode 100644 tests/framework_graphene_django/urls.py create mode 100644 tests/framework_graphene_django/views.py create mode 100644 tests/framework_graphene_django/wsgi.py diff --git a/tests/framework_graphene_django/_target_application.py b/tests/framework_graphene_django/_target_application.py new file mode 100644 index 000000000..f80fe709f --- /dev/null +++ b/tests/framework_graphene_django/_target_application.py @@ -0,0 +1,18 @@ +# 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 webtest +from wsgi import application + +_target_application = webtest.TestApp(application) diff --git a/tests/framework_graphene_django/conftest.py b/tests/framework_graphene_django/conftest.py new file mode 100644 index 000000000..48ecec7ae --- /dev/null +++ b/tests/framework_graphene_django/conftest.py @@ -0,0 +1,45 @@ +# 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 os + +# import pytest +from testing_support.fixtures import ( # noqa: F401; pylint: disable=W0611 + code_coverage_fixture, + collector_agent_registration_fixture, + collector_available_fixture, +) + +# from newrelic.packages import six + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") + +_coverage_source = [ + "newrelic.hooks.framework_graphql", +] + +code_coverage = code_coverage_fixture(source=_coverage_source) + +_default_settings = { + "transaction_tracer.explain_threshold": 0.0, + "transaction_tracer.transaction_threshold": 0.0, + "transaction_tracer.stack_trace_threshold": 0.0, + "debug.log_data_collector_payloads": True, + "debug.record_transaction_failure": True, +} + +collector_agent_registration = collector_agent_registration_fixture( + app_name="Python Agent Test (framework_graphene_django)", + default_settings=_default_settings, +) diff --git a/tests/framework_graphene_django/models.py b/tests/framework_graphene_django/models.py new file mode 100644 index 000000000..55b15817b --- /dev/null +++ b/tests/framework_graphene_django/models.py @@ -0,0 +1,17 @@ +from django.db import models + + +class Category(models.Model): + name = models.CharField(max_length=100) + + def __str__(self): + return self.name + + +class Ingredient(models.Model): + name = models.CharField(max_length=100) + notes = models.TextField(null=True, blank=True) + category = models.ForeignKey(Category, related_name="ingredients", on_delete=models.CASCADE) + + def __str__(self): + return self.name diff --git a/tests/framework_graphene_django/schema.py b/tests/framework_graphene_django/schema.py new file mode 100644 index 000000000..bd4e158c5 --- /dev/null +++ b/tests/framework_graphene_django/schema.py @@ -0,0 +1,47 @@ +# 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 graphene +from graphene_django import DjangoObjectType +from models import Category, Ingredient + + +class CategoryType(DjangoObjectType): + class Meta: + model = Category + fields = ("id", "name", "ingredients") + + +class IngredientType(DjangoObjectType): + class Meta: + model = Ingredient + fields = ("id", "name", "notes", "category") + + +class Query(graphene.ObjectType): + all_ingredients = graphene.List(IngredientType) + category_by_name = graphene.Field(CategoryType, name=graphene.String(required=True)) + + def resolve_all_ingredients(root, info): + # We can easily optimize query count in the resolve method + return Ingredient.objects.select_related("category").all() + + def resolve_category_by_name(root, info, name): + try: + return Category.objects.get(name=name) + except Category.DoesNotExist: + return None + + +schema = graphene.Schema(query=Query) diff --git a/tests/framework_graphene_django/settings.py b/tests/framework_graphene_django/settings.py new file mode 100644 index 000000000..d1d8d3f24 --- /dev/null +++ b/tests/framework_graphene_django/settings.py @@ -0,0 +1,68 @@ +# 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 os + +import django + +BASE_DIR = os.path.dirname(__file__) +DEBUG = True + +django_version = django.VERSION + +# Make this unique, and don't share it with anybody. +SECRET_KEY = "OMGsecrets" # nosec: B105 + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", +) + +# middleware = ( +# 'django.middleware.common.CommonMiddleware', +# 'django.contrib.sessions.middleware.SessionMiddleware', +# 'django.middleware.csrf.CsrfViewMiddleware', +# 'django.contrib.auth.middleware.AuthenticationMiddleware', +# 'django.contrib.messages.middleware.MessageMiddleware', +# 'django.middleware.gzip.GZipMiddleware', +# 'middleware.ExceptionTo410Middleware', +# ) +# if django_version[:2] >= (1, 10): +# MIDDLEWARE = middleware +# else: +# MIDDLEWARE_CLASSES = middleware + +ROOT_URLCONF = "urls" + +TEMPLATE_DIRS = [os.path.join(BASE_DIR, "templates")] + +# For Django 1.10 compatibility because TEMPLATE_DIRS is deprecated +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": TEMPLATE_DIRS, + } +] + +INSTALLED_APPS = ( + "django.contrib.auth", + "django.contrib.contenttypes", + "dummy_app", + "newrelic.extras.framework_graphene_django", +) + +GRAPHENE = {"SCHEMA": "schema.schema"} + +WSGI_APPLICATION = "wsgi.application" diff --git a/tests/framework_graphene_django/test_application.py b/tests/framework_graphene_django/test_application.py new file mode 100644 index 000000000..a5501c8b4 --- /dev/null +++ b/tests/framework_graphene_django/test_application.py @@ -0,0 +1,607 @@ +# 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 os + +import django + +# from testing_support.fixtures import ( +# override_application_settings, +# override_generic_settings, +# override_ignore_status_codes, +# validate_transaction_errors, +# validate_transaction_metrics, +# ) +# from testing_support.validators.validate_code_level_metrics import ( +# validate_code_level_metrics, +# ) + +# from newrelic.hooks.framework_django import django_settings + + +DJANGO_VERSION = tuple(map(int, django.get_version().split(".")[:2])) +DJANGO_SETTINGS_MODULE = os.environ.get("DJANGO_SETTINGS_MODULE", None) + + +def target_application(): + from _target_application import _target_application + + return _target_application + + +# The middleware scoped metrics are dependent on the MIDDLEWARE_CLASSES or +# MIDDLEWARE defined in the version-specific Django settings.py file. + +# _test_django_pre_1_10_middleware_scoped_metrics = [ +# (('Function/django.middleware.common:' +# 'CommonMiddleware.process_request'), 1), +# (('Function/django.contrib.sessions.middleware:' +# 'SessionMiddleware.process_request'), 1), +# (('Function/django.contrib.auth.middleware:' +# 'AuthenticationMiddleware.process_request'), 1), +# (('Function/django.contrib.messages.middleware:' +# 'MessageMiddleware.process_request'), 1), +# (('Function/django.middleware.csrf:' +# 'CsrfViewMiddleware.process_view'), 1), +# (('Function/django.contrib.messages.middleware:' +# 'MessageMiddleware.process_response'), 1), +# (('Function/django.middleware.csrf:' +# 'CsrfViewMiddleware.process_response'), 1), +# (('Function/django.contrib.sessions.middleware:' +# 'SessionMiddleware.process_response'), 1), +# (('Function/django.middleware.common:' +# 'CommonMiddleware.process_response'), 1), +# (('Function/django.middleware.gzip:' +# 'GZipMiddleware.process_response'), 1), +# (('Function/newrelic.hooks.framework_django:' +# 'browser_timing_insertion'), 1), +# ] + +# _test_django_post_1_10_middleware_scoped_metrics = [ +# ('Function/django.middleware.security:SecurityMiddleware', 1), +# ('Function/django.contrib.sessions.middleware:SessionMiddleware', 1), +# ('Function/django.middleware.common:CommonMiddleware', 1), +# ('Function/django.middleware.csrf:CsrfViewMiddleware', 1), +# ('Function/django.contrib.auth.middleware:AuthenticationMiddleware', 1), +# ('Function/django.contrib.messages.middleware:MessageMiddleware', 1), +# ('Function/django.middleware.clickjacking:XFrameOptionsMiddleware', 1), +# ('Function/django.middleware.gzip:GZipMiddleware', 1), +# ] + +# _test_django_pre_1_10_url_resolver_scoped_metrics = [ +# ('Function/django.core.urlresolvers:RegexURLResolver.resolve', 'present'), +# ] + +# _test_django_post_1_10_url_resolver_scoped_metrics = [ +# ('Function/django.urls.resolvers:RegexURLResolver.resolve', 'present'), +# ] + +# _test_django_post_2_0_url_resolver_scoped_metrics = [ +# ('Function/django.urls.resolvers:URLResolver.resolve', 'present'), +# ] + +# _test_application_index_scoped_metrics = [ +# ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), +# ('Python/WSGI/Application', 1), +# ('Python/WSGI/Response', 1), +# ('Python/WSGI/Finalize', 1), +# ('Function/views:index', 1), +# ] + +# if DJANGO_VERSION >= (1, 5): +# _test_application_index_scoped_metrics.extend([ +# ('Function/django.http.response:HttpResponse.close', 1)]) + +# if DJANGO_VERSION < (1, 10): +# _test_application_index_scoped_metrics.extend( +# _test_django_pre_1_10_url_resolver_scoped_metrics) +# elif DJANGO_VERSION >= (2, 0): +# _test_application_index_scoped_metrics.extend( +# _test_django_post_2_0_url_resolver_scoped_metrics) +# else: +# _test_application_index_scoped_metrics.extend( +# _test_django_post_1_10_url_resolver_scoped_metrics) + +# if DJANGO_SETTINGS_MODULE == 'settings_0110_old': +# _test_application_index_scoped_metrics.extend( +# _test_django_pre_1_10_middleware_scoped_metrics) +# elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': +# _test_application_index_scoped_metrics.extend( +# _test_django_post_1_10_middleware_scoped_metrics) +# elif DJANGO_VERSION < (1, 10): +# _test_application_index_scoped_metrics.extend( +# _test_django_pre_1_10_middleware_scoped_metrics) + + +# @validate_transaction_errors(errors=[]) +# @validate_transaction_metrics('views:index', +# scoped_metrics=_test_application_index_scoped_metrics) +# @validate_code_level_metrics("views", "index") +def test_application_index(): + test_application = target_application() + response = test_application.get("") + response.mustcontain("INDEX RESPONSE") + + +# @validate_transaction_metrics('views:exception') +# @validate_code_level_metrics("views", "exception") +# def test_application_exception(): +# test_application = target_application() +# test_application.get('/exception', status=500) + + +# _test_application_not_found_scoped_metrics = [ +# ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), +# ('Python/WSGI/Application', 1), +# ('Python/WSGI/Response', 1), +# ('Python/WSGI/Finalize', 1), +# ] + +# if DJANGO_VERSION >= (1, 5): +# _test_application_not_found_scoped_metrics.extend([ +# ('Function/django.http.response:HttpResponseNotFound.close', 1)]) + +# if DJANGO_VERSION < (1, 10): +# _test_application_not_found_scoped_metrics.extend( +# _test_django_pre_1_10_url_resolver_scoped_metrics) +# elif DJANGO_VERSION >= (2, 0): +# _test_application_not_found_scoped_metrics.extend( +# _test_django_post_2_0_url_resolver_scoped_metrics) +# else: +# _test_application_not_found_scoped_metrics.extend( +# _test_django_post_1_10_url_resolver_scoped_metrics) + +# if DJANGO_SETTINGS_MODULE == 'settings_0110_old': +# _test_application_not_found_scoped_metrics.extend( +# _test_django_pre_1_10_middleware_scoped_metrics) +# # The `CsrfViewMiddleware.process_view` isn't called for 404 Not Found. +# _test_application_not_found_scoped_metrics.remove( +# ('Function/django.middleware.csrf:CsrfViewMiddleware.process_view', 1)) +# elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': +# _test_application_not_found_scoped_metrics.extend( +# _test_django_post_1_10_middleware_scoped_metrics) +# elif DJANGO_VERSION < (1, 10): +# _test_application_not_found_scoped_metrics.extend( +# _test_django_pre_1_10_middleware_scoped_metrics) +# # The `CsrfViewMiddleware.process_view` isn't called for 404 Not Found. +# _test_application_not_found_scoped_metrics.remove( +# ('Function/django.middleware.csrf:CsrfViewMiddleware.process_view', 1)) + + +# @validate_transaction_errors(errors=[]) +# @validate_transaction_metrics('django.views.debug:technical_404_response', +# scoped_metrics=_test_application_not_found_scoped_metrics) +# def test_application_not_found(): +# test_application = target_application() +# test_application.get('/not_found', status=404) + + +# @override_ignore_status_codes([403]) +# @validate_transaction_errors(errors=[]) +# @validate_transaction_metrics('views:permission_denied') +# @validate_code_level_metrics("views", "permission_denied") +# def test_ignored_status_code(): +# test_application = target_application() +# test_application.get('/permission_denied', status=403) + + +# @override_ignore_status_codes([410]) +# @validate_transaction_errors(errors=[]) +# @validate_transaction_metrics('views:middleware_410') +# @validate_code_level_metrics("views", "middleware_410") +# def test_middleware_ignore_status_codes(): +# test_application = target_application() +# test_application.get('/middleware_410', status=410) + + +# _test_application_cbv_scoped_metrics = [ +# ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), +# ('Python/WSGI/Application', 1), +# ('Python/WSGI/Response', 1), +# ('Python/WSGI/Finalize', 1), +# ('Function/views:MyView', 1), +# ('Function/views:MyView.get', 1), +# ] + +# if DJANGO_VERSION >= (1, 5): +# _test_application_cbv_scoped_metrics.extend([ +# ('Function/django.http.response:HttpResponse.close', 1)]) + +# if DJANGO_VERSION < (1, 10): +# _test_application_cbv_scoped_metrics.extend( +# _test_django_pre_1_10_url_resolver_scoped_metrics) +# elif DJANGO_VERSION >= (2, 0): +# _test_application_cbv_scoped_metrics.extend( +# _test_django_post_2_0_url_resolver_scoped_metrics) +# else: +# _test_application_cbv_scoped_metrics.extend( +# _test_django_post_1_10_url_resolver_scoped_metrics) + +# if DJANGO_SETTINGS_MODULE == 'settings_0110_old': +# _test_application_cbv_scoped_metrics.extend( +# _test_django_pre_1_10_middleware_scoped_metrics) +# elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': +# _test_application_cbv_scoped_metrics.extend( +# _test_django_post_1_10_middleware_scoped_metrics) +# elif DJANGO_VERSION < (1, 10): +# _test_application_cbv_scoped_metrics.extend( +# _test_django_pre_1_10_middleware_scoped_metrics) + + +# @validate_transaction_errors(errors=[]) +# @validate_transaction_metrics('views:MyView.get', +# scoped_metrics=_test_application_cbv_scoped_metrics) +# @validate_code_level_metrics("views.MyView", "get") +# def test_application_cbv(): +# test_application = target_application() +# response = test_application.get('/cbv') +# response.mustcontain('CBV RESPONSE') + + +# _test_application_deferred_cbv_scoped_metrics = [ +# ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), +# ('Python/WSGI/Application', 1), +# ('Python/WSGI/Response', 1), +# ('Python/WSGI/Finalize', 1), +# ('Function/views:deferred_cbv', 1), +# ('Function/views:MyView.get', 1), +# ] + +# if DJANGO_VERSION >= (1, 5): +# _test_application_deferred_cbv_scoped_metrics.extend([ +# ('Function/django.http.response:HttpResponse.close', 1)]) + +# if DJANGO_VERSION < (1, 10): +# _test_application_deferred_cbv_scoped_metrics.extend( +# _test_django_pre_1_10_url_resolver_scoped_metrics) +# elif DJANGO_VERSION >= (2, 0): +# _test_application_deferred_cbv_scoped_metrics.extend( +# _test_django_post_2_0_url_resolver_scoped_metrics) +# else: +# _test_application_deferred_cbv_scoped_metrics.extend( +# _test_django_post_1_10_url_resolver_scoped_metrics) + +# if DJANGO_SETTINGS_MODULE == 'settings_0110_old': +# _test_application_deferred_cbv_scoped_metrics.extend( +# _test_django_pre_1_10_middleware_scoped_metrics) +# elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': +# _test_application_deferred_cbv_scoped_metrics.extend( +# _test_django_post_1_10_middleware_scoped_metrics) +# elif DJANGO_VERSION < (1, 10): +# _test_application_deferred_cbv_scoped_metrics.extend( +# _test_django_pre_1_10_middleware_scoped_metrics) + + +# @validate_transaction_errors(errors=[]) +# @validate_transaction_metrics('views:deferred_cbv', +# scoped_metrics=_test_application_deferred_cbv_scoped_metrics) +# @validate_code_level_metrics("views", "deferred_cbv") +# def test_application_deferred_cbv(): +# test_application = target_application() +# response = test_application.get('/deferred_cbv') +# response.mustcontain('CBV RESPONSE') + + +# _test_html_insertion_settings = { +# 'browser_monitoring.enabled': True, +# 'browser_monitoring.auto_instrument': True, +# 'js_agent_loader': u'', +# } + + +# @override_application_settings(_test_html_insertion_settings) +# def test_html_insertion_django_middleware(): +# test_application = target_application() +# response = test_application.get('/html_insertion', status=200) + +# # The 'NREUM HEADER' value comes from our override for the header. +# # The 'NREUM.info' value comes from the programmatically generated +# # footer added by the agent. + +# response.mustcontain('NREUM HEADER', 'NREUM.info') + + +# @override_application_settings(_test_html_insertion_settings) +# def test_html_insertion_django_gzip_middleware_enabled(): +# test_application = target_application() + +# # GZipMiddleware only fires if given the following header. + +# gzip_header = {'Accept-Encoding': 'gzip'} +# response = test_application.get('/gzip_html_insertion', status=200, +# headers=gzip_header) + +# # The 'NREUM HEADER' value comes from our override for the header. +# # The 'NREUM.info' value comes from the programmatically generated +# # footer added by the agent. + +# # The response.text will already be gunzipped + +# response.mustcontain('NREUM HEADER', 'NREUM.info') + + +# _test_html_insertion_settings_disabled = { +# 'browser_monitoring.enabled': False, +# 'browser_monitoring.auto_instrument': False, +# 'js_agent_loader': u'', +# } + + +# @override_application_settings(_test_html_insertion_settings_disabled) +# def test_html_insertion_django_gzip_middleware_disabled(): +# test_application = target_application() + +# # GZipMiddleware only fires if given the following header. + +# gzip_header = {'Accept-Encoding': 'gzip'} +# response = test_application.get('/gzip_html_insertion', status=200, +# headers=gzip_header) + +# # The 'NREUM HEADER' value comes from our override for the header. +# # The 'NREUM.info' value comes from the programmatically generated +# # footer added by the agent. + +# # The response.text will already be gunzipped + +# response.mustcontain(no=['NREUM HEADER', 'NREUM.info']) + + +# _test_html_insertion_manual_settings = { +# 'browser_monitoring.enabled': True, +# 'browser_monitoring.auto_instrument': True, +# 'js_agent_loader': u'', +# } + + +# @override_application_settings(_test_html_insertion_manual_settings) +# def test_html_insertion_manual_django_middleware(): +# test_application = target_application() +# response = test_application.get('/html_insertion_manual', status=200) + +# # The 'NREUM HEADER' value comes from our override for the header. +# # The 'NREUM.info' value comes from the programmatically generated +# # footer added by the agent. + +# response.mustcontain(no=['NREUM HEADER', 'NREUM.info']) + + +# @override_application_settings(_test_html_insertion_settings) +# def test_html_insertion_unnamed_attachment_header_django_middleware(): +# test_application = target_application() +# response = test_application.get( +# '/html_insertion_unnamed_attachment_header', status=200) + +# # The 'NREUM HEADER' value comes from our override for the header. +# # The 'NREUM.info' value comes from the programmatically generated +# # footer added by the agent. + +# response.mustcontain(no=['NREUM HEADER', 'NREUM.info']) + + +# @override_application_settings(_test_html_insertion_settings) +# def test_html_insertion_named_attachment_header_django_middleware(): +# test_application = target_application() +# response = test_application.get( +# '/html_insertion_named_attachment_header', status=200) + +# # The 'NREUM HEADER' value comes from our override for the header. +# # The 'NREUM.info' value comes from the programmatically generated +# # footer added by the agent. + +# response.mustcontain(no=['NREUM HEADER', 'NREUM.info']) + + +# _test_html_insertion_settings = { +# 'browser_monitoring.enabled': True, +# 'browser_monitoring.auto_instrument': False, +# 'js_agent_loader': u'', +# } + + +# @override_application_settings(_test_html_insertion_settings) +# def test_html_insertion_manual_tag_instrumentation(): +# test_application = target_application() +# response = test_application.get('/template_tags') + +# # Assert that the instrumentation is not inappropriately escaped + +# response.mustcontain('', +# no=['<!-- NREUM HEADER -->']) + + +# _test_application_inclusion_tag_scoped_metrics = [ +# ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), +# ('Python/WSGI/Application', 1), +# ('Python/WSGI/Response', 1), +# ('Python/WSGI/Finalize', 1), +# ('Function/views:inclusion_tag', 1), +# ('Template/Render/main.html', 1), +# ] + +# if DJANGO_VERSION < (1, 9): +# _test_application_inclusion_tag_scoped_metrics.extend([ +# ('Template/Include/results.html', 1)]) + +# if DJANGO_VERSION < (1, 10): +# _test_application_inclusion_tag_scoped_metrics.extend( +# _test_django_pre_1_10_url_resolver_scoped_metrics) +# elif DJANGO_VERSION >= (2, 0): +# _test_application_inclusion_tag_scoped_metrics.extend( +# _test_django_post_2_0_url_resolver_scoped_metrics) +# else: +# _test_application_inclusion_tag_scoped_metrics.extend( +# _test_django_post_1_10_url_resolver_scoped_metrics) + +# if DJANGO_SETTINGS_MODULE == 'settings_0110_old': +# _test_application_inclusion_tag_scoped_metrics.extend( +# _test_django_pre_1_10_middleware_scoped_metrics) +# elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': +# _test_application_inclusion_tag_scoped_metrics.extend( +# _test_django_post_1_10_middleware_scoped_metrics) +# elif DJANGO_VERSION < (1, 10): +# _test_application_inclusion_tag_scoped_metrics.extend( +# _test_django_pre_1_10_middleware_scoped_metrics) + +# try: +# _test_application_inclusion_tag_scoped_metrics.remove( +# (('Function/newrelic.hooks.framework_django:' +# 'browser_timing_insertion'), 1) +# ) +# except ValueError: +# pass + + +# @validate_transaction_errors(errors=[]) +# @validate_transaction_metrics('views:inclusion_tag', +# scoped_metrics=_test_application_inclusion_tag_scoped_metrics) +# @validate_code_level_metrics("views", "inclusion_tag") +# def test_application_inclusion_tag(): +# test_application = target_application() +# response = test_application.get('/inclusion_tag') +# response.mustcontain('Inclusion tag') + + +# _test_inclusion_tag_template_tags_scoped_metrics = [ +# ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), +# ('Python/WSGI/Application', 1), +# ('Python/WSGI/Response', 1), +# ('Python/WSGI/Finalize', 1), +# ('Function/views:inclusion_tag', 1), +# ('Template/Render/main.html', 1), +# ] + +# if DJANGO_VERSION < (1, 9): +# _test_inclusion_tag_template_tags_scoped_metrics.extend([ +# ('Template/Include/results.html', 1), +# ('Template/Tag/show_results', 1)]) + +# _test_inclusion_tag_settings = { +# 'instrumentation.templates.inclusion_tag': '*' +# } + +# if DJANGO_VERSION < (1, 10): +# _test_inclusion_tag_template_tags_scoped_metrics.extend( +# _test_django_pre_1_10_url_resolver_scoped_metrics) +# elif DJANGO_VERSION >= (2, 0): +# _test_inclusion_tag_template_tags_scoped_metrics.extend( +# _test_django_post_2_0_url_resolver_scoped_metrics) +# else: +# _test_inclusion_tag_template_tags_scoped_metrics.extend( +# _test_django_post_1_10_url_resolver_scoped_metrics) + +# if DJANGO_SETTINGS_MODULE == 'settings_0110_old': +# _test_inclusion_tag_template_tags_scoped_metrics.extend( +# _test_django_pre_1_10_middleware_scoped_metrics) +# elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': +# _test_inclusion_tag_template_tags_scoped_metrics.extend( +# _test_django_post_1_10_middleware_scoped_metrics) +# elif DJANGO_VERSION < (1, 10): +# _test_inclusion_tag_template_tags_scoped_metrics.extend( +# _test_django_pre_1_10_middleware_scoped_metrics) + +# try: +# _test_inclusion_tag_template_tags_scoped_metrics.remove( +# (('Function/newrelic.hooks.framework_django:' +# 'browser_timing_insertion'), 1) +# ) +# except ValueError: +# pass + + +# @validate_transaction_errors(errors=[]) +# @validate_transaction_metrics('views:inclusion_tag', +# scoped_metrics=_test_inclusion_tag_template_tags_scoped_metrics) +# @override_generic_settings(django_settings, _test_inclusion_tag_settings) +# @validate_code_level_metrics("views", "inclusion_tag") +# def test_inclusion_tag_template_tag_metric(): +# test_application = target_application() +# response = test_application.get('/inclusion_tag') +# response.mustcontain('Inclusion tag') + + +# _test_template_render_exception_scoped_metrics_base = [ +# ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), +# ('Python/WSGI/Application', 1), +# ('Python/WSGI/Response', 1), +# ('Python/WSGI/Finalize', 1), +# ] + +# if DJANGO_VERSION < (1, 5): +# _test_template_render_exception_scoped_metrics_base.append( +# ('Function/django.http:HttpResponseServerError.close', 1)) +# elif DJANGO_VERSION < (1, 8): +# _test_template_render_exception_scoped_metrics_base.append( +# ('Function/django.http.response:HttpResponseServerError.close', 1)) +# else: +# _test_template_render_exception_scoped_metrics_base.append( +# ('Function/django.http.response:HttpResponse.close', 1)) + +# if DJANGO_VERSION < (1, 10): +# _test_template_render_exception_scoped_metrics_base.extend( +# _test_django_pre_1_10_url_resolver_scoped_metrics) +# elif DJANGO_VERSION >= (2, 0): +# _test_template_render_exception_scoped_metrics_base.extend( +# _test_django_post_2_0_url_resolver_scoped_metrics) +# else: +# _test_template_render_exception_scoped_metrics_base.extend( +# _test_django_post_1_10_url_resolver_scoped_metrics) + +# if DJANGO_SETTINGS_MODULE == 'settings_0110_old': +# _test_template_render_exception_scoped_metrics_base.extend( +# _test_django_pre_1_10_middleware_scoped_metrics) +# elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': +# _test_template_render_exception_scoped_metrics_base.extend( +# _test_django_post_1_10_middleware_scoped_metrics) +# elif DJANGO_VERSION < (1, 10): +# _test_template_render_exception_scoped_metrics_base.extend( +# _test_django_pre_1_10_middleware_scoped_metrics) + +# if DJANGO_VERSION < (1, 9): +# _test_template_render_exception_errors = [ +# 'django.template.base:TemplateSyntaxError'] +# else: +# _test_template_render_exception_errors = [ +# 'django.template.exceptions:TemplateSyntaxError'] + +# _test_template_render_exception_function_scoped_metrics = list( +# _test_template_render_exception_scoped_metrics_base) +# _test_template_render_exception_function_scoped_metrics.extend([ +# ('Function/views:render_exception_function', 1), +# ]) + + +# @validate_transaction_errors(errors=_test_template_render_exception_errors) +# @validate_transaction_metrics('views:render_exception_function', +# scoped_metrics=_test_template_render_exception_function_scoped_metrics) +# @validate_code_level_metrics("views", "render_exception_function") +# def test_template_render_exception_function(): +# test_application = target_application() +# test_application.get('/render_exception_function', status=500) + + +# _test_template_render_exception_class_scoped_metrics = list( +# _test_template_render_exception_scoped_metrics_base) +# _test_template_render_exception_class_scoped_metrics.extend([ +# ('Function/views:RenderExceptionClass', 1), +# ('Function/views:RenderExceptionClass.get', 1), +# ]) + + +# @validate_transaction_errors(errors=_test_template_render_exception_errors) +# @validate_transaction_metrics('views:RenderExceptionClass.get', +# scoped_metrics=_test_template_render_exception_class_scoped_metrics) +# @validate_code_level_metrics("views.RenderExceptionClass", "get") +# def test_template_render_exception_class(): +# test_application = target_application() +# test_application.get('/render_exception_class', status=500) diff --git a/tests/framework_graphene_django/urls.py b/tests/framework_graphene_django/urls.py new file mode 100644 index 000000000..870996913 --- /dev/null +++ b/tests/framework_graphene_django/urls.py @@ -0,0 +1,59 @@ +# 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. + +from django.views.decorators.csrf import csrf_exempt + +try: + from django.conf.urls.defaults import path +except ImportError: + try: + from django.conf.urls import path + except ImportError: + from django.urls import path + +import views +from graphene_django.views import GraphQLView +from schema import schema + +urlpatterns = [ + path("", views.index, name="index"), + path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))), + # url(r'^$', views.index, name='index'), + # url(r'^exception$', views.exception, name='exception'), + # url(r'^middleware_410$', views.middleware_410, name='middleware_410'), + # url(r'^permission_denied$', views.permission_denied, + # name='permission_denied'), + # url(r'^cbv$', views.MyView.as_view()), + # url(r'^deferred_cbv$', views.deferred_cbv), + # url(r'^html_insertion$', views.html_insertion, name='html_insertion'), + # url(r'^html_insertion_content_length$', + # views.html_insertion_content_length, + # name='html_insertion_content_length'), + # url(r'^html_insertion_manual$', views.html_insertion_manual, + # name='html_insertion_manual'), + # url(r'^html_insertion_unnamed_attachment_header$', + # views.html_insertion_unnamed_attachment_header, + # name='html_insertion_unnamed_attachment_header'), + # url(r'^html_insertion_named_attachment_header$', + # views.html_insertion_named_attachment_header, + # name='html_insertion_named_attachment_header'), + # url(r'^inclusion_tag$', views.inclusion_tag, name='inclusion_tag'), + # url(r'^template_tags$', views.template_tags, name='template_tags'), + # url(r'^render_exception_function', views.render_exception_function, + # name='render_exception_function'), + # url(r'^render_exception_class', views.RenderExceptionClass.as_view(), + # name='render_exception_class'), + # url(r'^gzip_html_insertion', views.gzip_html_insertion, + # name='gzip_html_insertion'), +] diff --git a/tests/framework_graphene_django/views.py b/tests/framework_graphene_django/views.py new file mode 100644 index 000000000..ea696cdbc --- /dev/null +++ b/tests/framework_graphene_django/views.py @@ -0,0 +1,131 @@ +# 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. + +from django.core.exceptions import PermissionDenied +from django.http import HttpResponse +from django.shortcuts import render +from django.views.generic.base import TemplateView, View + +from newrelic.api.transaction import ( + get_browser_timing_footer, + get_browser_timing_header, +) + +# from middleware import Custom410 + + +def index(request): + return HttpResponse("INDEX RESPONSE") + + +def exception(request): + raise RuntimeError("exception") + + +def permission_denied(request): + raise PermissionDenied() + + +# def middleware_410(request): +# raise Custom410() + + +class MyView(View): + def get(self, request): + return HttpResponse("CBV RESPONSE") + + +def deferred_cbv(request): + return MyView.as_view()(request) + + +def html_insertion(request): + return HttpResponse( + "Some header" + "

My First Heading

My first paragraph.

" + "" + ) + + +def html_insertion_content_length(request): + content = ( + "Some header" + "

My First Heading

My first paragraph.

" + "" + ) + response = HttpResponse(content) + response["Content-Length"] = len(content) + return response + + +def html_insertion_manual(request): + header = get_browser_timing_header() + footer = get_browser_timing_footer() + + header = get_browser_timing_header() + footer = get_browser_timing_footer() + + assert header == "" + assert footer == "" + + return HttpResponse( + "Some header" + "

My First Heading

My first paragraph.

" + "" + ) + + +def html_insertion_unnamed_attachment_header(request): + response = HttpResponse( + "Some header" + "

My First Heading

My first paragraph.

" + "" + ) + response["Content-Disposition"] = "attachment" + return response + + +def html_insertion_named_attachment_header(request): + response = HttpResponse( + "Some header" + "

My First Heading

My first paragraph.

" + "" + ) + response["Content-Disposition"] = 'Attachment; filename="X"' + return response + + +def inclusion_tag(request): + return render(request, "main.html", {}, content_type="text/html") + + +def template_tags(request): + return render(request, "main.html", {}, content_type="text/html") + + +def render_exception_function(request): + return render(request, "render_exception.html") + + +class RenderExceptionClass(TemplateView): + template_name = "render_exception.html" + + +def gzip_html_insertion(request): + # contents must be at least 200 bytes for gzip middleware to work + contents = "*" * 200 + return HttpResponse( + "Some header" + "

My First Heading

%s

" % contents + ) diff --git a/tests/framework_graphene_django/wsgi.py b/tests/framework_graphene_django/wsgi.py new file mode 100644 index 000000000..72b1f2110 --- /dev/null +++ b/tests/framework_graphene_django/wsgi.py @@ -0,0 +1,27 @@ +# 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 os + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") + +try: + from django.core.wsgi import get_wsgi_application + + application = get_wsgi_application() + +except ImportError: + import django.core.handlers.wsgi + + application = django.core.handlers.wsgi.WSGIHandler() diff --git a/tox.ini b/tox.ini index e1e3ff367..56ccba0ae 100644 --- a/tox.ini +++ b/tox.ini @@ -129,7 +129,7 @@ envlist = python-framework_graphene-{py37,py38,py39,py310}-graphenelatest, python-framework_graphene-{py27,py37,py38,py39,pypy,pypy37}-graphene{0200,0201}, python-framework_graphene-py310-graphene0201, - ; python-framework_graphene_django-{py37,py38,py39,py310,pypy37} + python-framework_graphene_django-{py37,py38,py39,py310,pypy37} python-framework_graphql-{py27,py37,py38,py39,py310,pypy,pypy37}-graphql02, python-framework_graphql-{py37,py38,py39,py310,pypy37}-graphql03, ; temporarily disabling graphqlmaster tests @@ -313,9 +313,9 @@ deps = framework_graphene-graphenelatest: graphene framework_graphene-graphene0200: graphene<2.1 framework_graphene-graphene0201: graphene<2.2 - ; framework_graphene_django: graphene-django - ; framework_graphene_django: graphene - ; framework_graphene_django: django + framework_graphene_django: graphene-django + framework_graphene_django: graphene + framework_graphene_django: django framework_graphql-graphql02: graphql-core<3 framework_graphql-graphql03: graphql-core<4 framework_graphql-graphql0202: graphql-core<2.3 @@ -467,7 +467,7 @@ changedir = framework_fastapi: tests/framework_fastapi framework_flask: tests/framework_flask framework_graphene: tests/framework_graphene - ; framework_graphene_django: tests/framework_graphene_django + framework_graphene_django: tests/framework_graphene_django framework_graphql: tests/framework_graphql framework_grpc: tests/framework_grpc framework_pyramid: tests/framework_pyramid From 7c2c396846399e0a27a1fe9ceaad6e4002b02bf1 Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Tue, 1 Nov 2022 14:41:56 -0700 Subject: [PATCH 19/26] Initial test commit for graphene-django Co-authored-by: Timothy Pansino --- tests/framework_graphene_django/__init__.py | 17 + .../_target_application.py | 43 +- .../_target_schema_sync.py | 173 +++++ tests/framework_graphene_django/conftest.py | 4 +- tests/framework_graphene_django/models.py | 46 +- tests/framework_graphene_django/schema.py | 47 -- tests/framework_graphene_django/settings.py | 53 +- .../test_application.py | 607 +----------------- tests/framework_graphene_django/urls.py | 37 +- tests/framework_graphene_django/views.py | 131 ---- tests/framework_graphene_django/wsgi.py | 4 - 11 files changed, 318 insertions(+), 844 deletions(-) create mode 100644 tests/framework_graphene_django/__init__.py create mode 100644 tests/framework_graphene_django/_target_schema_sync.py delete mode 100644 tests/framework_graphene_django/schema.py delete mode 100644 tests/framework_graphene_django/views.py diff --git a/tests/framework_graphene_django/__init__.py b/tests/framework_graphene_django/__init__.py new file mode 100644 index 000000000..5abc40f73 --- /dev/null +++ b/tests/framework_graphene_django/__init__.py @@ -0,0 +1,17 @@ +# 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 os + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "framework_graphene_django.settings") diff --git a/tests/framework_graphene_django/_target_application.py b/tests/framework_graphene_django/_target_application.py index f80fe709f..5a4118938 100644 --- a/tests/framework_graphene_django/_target_application.py +++ b/tests/framework_graphene_django/_target_application.py @@ -12,7 +12,46 @@ # See the License for the specific language governing permissions and # limitations under the License. + +import json + import webtest -from wsgi import application -_target_application = webtest.TestApp(application) +from .wsgi import application + + +def check_response(query, success, response): + if isinstance(query, str) and "error" not in query: + assert success and "errors" not in response, response["errors"] + assert response["data"] + else: + assert "errors" in response, response + + +def run_wsgi(app): + def _run_wsgi(query, middleware=None): + if not isinstance(query, str) or "error" in query: + expect_errors = True + else: + expect_errors = False + + app.app.middleware = middleware + + response = app.post( + "/", json.dumps({"query": query}), headers={"Content-Type": "application/json"}, expect_errors=expect_errors + ) + + body = json.loads(response.body.decode("utf-8")) + if expect_errors: + assert body["errors"] + else: + assert "errors" not in body or not body["errors"] + + return body.get("data", {}) + + return _run_wsgi + + +target_application = { + "wsgi-sync": run_wsgi(webtest.TestApp(application)), +} diff --git a/tests/framework_graphene_django/_target_schema_sync.py b/tests/framework_graphene_django/_target_schema_sync.py new file mode 100644 index 000000000..aa7039a64 --- /dev/null +++ b/tests/framework_graphene_django/_target_schema_sync.py @@ -0,0 +1,173 @@ +# 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. +from graphene import Field, Int, List +from graphene import Mutation as GrapheneMutation +from graphene import NonNull +from graphene import ObjectType as GrapheneObjectType +from graphene import Schema, String, Union +from graphene_django import DjangoObjectType + +from .models import Author as _Author +from .models import Book as _Book +from .models import Library as _Library +from .models import Magazine as _Magazine + + +class Author(DjangoObjectType): + class Meta: + model = _Author + fields = ("first_name", "last_name") + + +class Book(DjangoObjectType): + class Meta: + model = _Book + fields = ("id", "name", "isbn", "author", "branch") + + +class Magazine(DjangoObjectType): + class Meta: + model = _Magazine + fields = ("id", "name", "issue", "branch") + + +class Item(Union): + class Meta: + types = (Book, Magazine) + + +class Library(DjangoObjectType): + class Meta: + model = _Library + fields = ("id", "branch", "magazine", "book") + + +Storage = List(String) + + +authors = [ + Author( + first_name="New", + last_name="Relic", + ), + Author( + first_name="Bob", + last_name="Smith", + ), + Author( + first_name="Leslie", + last_name="Jones", + ), +] + +books = [ + Book( + id=1, + name="Python Agent: The Book", + isbn="a-fake-isbn", + author=authors[0], + branch="riverside", + ), + Book( + id=2, + name="Ollies for O11y: A Sk8er's Guide to Observability", + isbn="a-second-fake-isbn", + author=authors[1], + branch="downtown", + ), + Book( + id=3, + name="[Redacted]", + isbn="a-third-fake-isbn", + author=authors[2], + branch="riverside", + ), +] + +magazines = [ + Magazine(id=1, name="Reli Updates Weekly", issue=1, branch="riverside"), + Magazine(id=2, name="Reli Updates Weekly", issue=2, branch="downtown"), + Magazine(id=3, name="Node Weekly", issue=1, branch="riverside"), +] + + +libraries = ["riverside", "downtown"] +libraries = [ + Library( + id=i + 1, + branch=branch, + magazine=[m for m in magazines if m.branch == branch], + book=[b for b in books if b.branch == branch], + ) + for i, branch in enumerate(libraries) +] + +storage = [] + + +def resolve_library(self, info, index): + return libraries[index] + + +def resolve_storage(self, info): + return [storage.pop()] + + +def resolve_search(self, info, contains): + search_books = [b for b in books if contains in b.name] + search_magazines = [m for m in magazines if contains in m.name] + return search_books + search_magazines + + +def resolve_hello(self, info): + return "Hello!" + + +def resolve_echo(self, info, echo): + return echo + + +def resolve_error(self, info): + raise RuntimeError("Runtime Error!") + + +def resolve_storage_add(self, info, string): + storage.append(string) + return StorageAdd(string=string) + + +class StorageAdd(GrapheneMutation): + class Arguments: + string = String(required=True) + + string = String() + mutate = resolve_storage_add + + +class Query(GrapheneObjectType): + library = Field(Library, index=Int(required=True), resolver=resolve_library) + hello = String(resolver=resolve_hello) + search = Field(List(Item), contains=String(required=True), resolver=resolve_search) + echo = Field(String, echo=String(required=True), resolver=resolve_echo) + storage = Field(Storage, resolver=resolve_storage) + error = String(resolver=resolve_error) + error_non_null = Field(NonNull(String), resolver=resolve_error) + error_middleware = String(resolver=resolve_hello) + + +class Mutation(GrapheneObjectType): + storage_add = StorageAdd.Field() + + +target_schema = Schema(query=Query, mutation=Mutation, auto_camelcase=False) diff --git a/tests/framework_graphene_django/conftest.py b/tests/framework_graphene_django/conftest.py index 48ecec7ae..43886d5a7 100644 --- a/tests/framework_graphene_django/conftest.py +++ b/tests/framework_graphene_django/conftest.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os +# import os # import pytest from testing_support.fixtures import ( # noqa: F401; pylint: disable=W0611 @@ -23,7 +23,7 @@ # from newrelic.packages import six -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") +# os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") _coverage_source = [ "newrelic.hooks.framework_graphql", diff --git a/tests/framework_graphene_django/models.py b/tests/framework_graphene_django/models.py index 55b15817b..7e3f42a87 100644 --- a/tests/framework_graphene_django/models.py +++ b/tests/framework_graphene_django/models.py @@ -1,17 +1,41 @@ -from django.db import models +# 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. +from django.db.models import CASCADE, ForeignKey, IntegerField, Model, TextField -class Category(models.Model): - name = models.CharField(max_length=100) +class Author(Model): + first_name = TextField() + last_name = TextField() - def __str__(self): - return self.name +class Book(Model): + id = IntegerField() + name = TextField() + isbn = TextField() + author = ForeignKey(Author, on_delete=CASCADE) + branch = TextField() -class Ingredient(models.Model): - name = models.CharField(max_length=100) - notes = models.TextField(null=True, blank=True) - category = models.ForeignKey(Category, related_name="ingredients", on_delete=models.CASCADE) - def __str__(self): - return self.name +class Magazine(Model): + id = IntegerField() + name = TextField() + issue = IntegerField() + branch = TextField() + + +class Library(Model): + id = IntegerField() + branch = TextField() + magazine = ForeignKey(Magazine, on_delete=CASCADE) + book = ForeignKey(Book, on_delete=CASCADE) diff --git a/tests/framework_graphene_django/schema.py b/tests/framework_graphene_django/schema.py deleted file mode 100644 index bd4e158c5..000000000 --- a/tests/framework_graphene_django/schema.py +++ /dev/null @@ -1,47 +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 graphene -from graphene_django import DjangoObjectType -from models import Category, Ingredient - - -class CategoryType(DjangoObjectType): - class Meta: - model = Category - fields = ("id", "name", "ingredients") - - -class IngredientType(DjangoObjectType): - class Meta: - model = Ingredient - fields = ("id", "name", "notes", "category") - - -class Query(graphene.ObjectType): - all_ingredients = graphene.List(IngredientType) - category_by_name = graphene.Field(CategoryType, name=graphene.String(required=True)) - - def resolve_all_ingredients(root, info): - # We can easily optimize query count in the resolve method - return Ingredient.objects.select_related("category").all() - - def resolve_category_by_name(root, info, name): - try: - return Category.objects.get(name=name) - except Category.DoesNotExist: - return None - - -schema = graphene.Schema(query=Query) diff --git a/tests/framework_graphene_django/settings.py b/tests/framework_graphene_django/settings.py index d1d8d3f24..57b000176 100644 --- a/tests/framework_graphene_django/settings.py +++ b/tests/framework_graphene_django/settings.py @@ -21,48 +21,37 @@ django_version = django.VERSION -# Make this unique, and don't share it with anybody. -SECRET_KEY = "OMGsecrets" # nosec: B105 +SECRET_KEY = "NotASecret" # nosec: B105 # List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - "django.template.loaders.filesystem.Loader", - "django.template.loaders.app_directories.Loader", -) - -# middleware = ( -# 'django.middleware.common.CommonMiddleware', -# 'django.contrib.sessions.middleware.SessionMiddleware', -# 'django.middleware.csrf.CsrfViewMiddleware', -# 'django.contrib.auth.middleware.AuthenticationMiddleware', -# 'django.contrib.messages.middleware.MessageMiddleware', -# 'django.middleware.gzip.GZipMiddleware', -# 'middleware.ExceptionTo410Middleware', +# TEMPLATE_LOADERS = ( +# "django.template.loaders.filesystem.Loader", +# "django.template.loaders.app_directories.Loader", # ) -# if django_version[:2] >= (1, 10): -# MIDDLEWARE = middleware -# else: -# MIDDLEWARE_CLASSES = middleware -ROOT_URLCONF = "urls" +ROOT_URLCONF = "framework_graphene_django.urls" -TEMPLATE_DIRS = [os.path.join(BASE_DIR, "templates")] +# TEMPLATE_DIRS = [os.path.join(BASE_DIR, "templates")] # For Django 1.10 compatibility because TEMPLATE_DIRS is deprecated -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": TEMPLATE_DIRS, - } -] +# TEMPLATES = [ +# { +# # "BACKEND": "django.template.backends.django.DjangoTemplates", +# # "DIRS": TEMPLATE_DIRS, +# "DIRS": [] +# } +# ] INSTALLED_APPS = ( - "django.contrib.auth", - "django.contrib.contenttypes", - "dummy_app", - "newrelic.extras.framework_graphene_django", + # "django.contrib.auth", + # "django.contrib.contenttypes", + # "django.contrib.sessions", + # "django.contrib.messagess", + # "django.contrib.staticfiles", + "graphene_django", + "framework_graphene_django", ) -GRAPHENE = {"SCHEMA": "schema.schema"} +# GRAPHENE = {"SCHEMA": "_target_schema_sync.schema"} WSGI_APPLICATION = "wsgi.application" diff --git a/tests/framework_graphene_django/test_application.py b/tests/framework_graphene_django/test_application.py index a5501c8b4..444ae4c6a 100644 --- a/tests/framework_graphene_django/test_application.py +++ b/tests/framework_graphene_django/test_application.py @@ -12,596 +12,39 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os +import pytest +from framework_graphql.test_application import * # noqa -import django -# from testing_support.fixtures import ( -# override_application_settings, -# override_generic_settings, -# override_ignore_status_codes, -# validate_transaction_errors, -# validate_transaction_metrics, -# ) -# from testing_support.validators.validate_code_level_metrics import ( -# validate_code_level_metrics, -# ) +@pytest.fixture(scope="session", params=["wsgi-sync"]) +def target_application(request): + from ._target_application import target_application -# from newrelic.hooks.framework_django import django_settings + target_application = target_application.get(request.param, None) + if target_application is None: + pytest.skip("Unsupported combination.") + return + try: + import graphene_django -DJANGO_VERSION = tuple(map(int, django.get_version().split(".")[:2])) -DJANGO_SETTINGS_MODULE = os.environ.get("DJANGO_SETTINGS_MODULE", None) + version = graphene_django.__version__ + except Exception: + import pkg_resources + version = pkg_resources.get_distribution("graphene_django").version -def target_application(): - from _target_application import _target_application + param = request.param.split("-") + is_background = param[0] not in {"wsgi", "asgi"} + schema_type = param[1] + extra_spans = 4 if param[0] == "wsgi" else 0 - return _target_application + assert version is not None + return "GrapheneDjango", version, target_application, is_background, schema_type, extra_spans -# The middleware scoped metrics are dependent on the MIDDLEWARE_CLASSES or -# MIDDLEWARE defined in the version-specific Django settings.py file. - -# _test_django_pre_1_10_middleware_scoped_metrics = [ -# (('Function/django.middleware.common:' -# 'CommonMiddleware.process_request'), 1), -# (('Function/django.contrib.sessions.middleware:' -# 'SessionMiddleware.process_request'), 1), -# (('Function/django.contrib.auth.middleware:' -# 'AuthenticationMiddleware.process_request'), 1), -# (('Function/django.contrib.messages.middleware:' -# 'MessageMiddleware.process_request'), 1), -# (('Function/django.middleware.csrf:' -# 'CsrfViewMiddleware.process_view'), 1), -# (('Function/django.contrib.messages.middleware:' -# 'MessageMiddleware.process_response'), 1), -# (('Function/django.middleware.csrf:' -# 'CsrfViewMiddleware.process_response'), 1), -# (('Function/django.contrib.sessions.middleware:' -# 'SessionMiddleware.process_response'), 1), -# (('Function/django.middleware.common:' -# 'CommonMiddleware.process_response'), 1), -# (('Function/django.middleware.gzip:' -# 'GZipMiddleware.process_response'), 1), -# (('Function/newrelic.hooks.framework_django:' -# 'browser_timing_insertion'), 1), -# ] - -# _test_django_post_1_10_middleware_scoped_metrics = [ -# ('Function/django.middleware.security:SecurityMiddleware', 1), -# ('Function/django.contrib.sessions.middleware:SessionMiddleware', 1), -# ('Function/django.middleware.common:CommonMiddleware', 1), -# ('Function/django.middleware.csrf:CsrfViewMiddleware', 1), -# ('Function/django.contrib.auth.middleware:AuthenticationMiddleware', 1), -# ('Function/django.contrib.messages.middleware:MessageMiddleware', 1), -# ('Function/django.middleware.clickjacking:XFrameOptionsMiddleware', 1), -# ('Function/django.middleware.gzip:GZipMiddleware', 1), -# ] - -# _test_django_pre_1_10_url_resolver_scoped_metrics = [ -# ('Function/django.core.urlresolvers:RegexURLResolver.resolve', 'present'), -# ] - -# _test_django_post_1_10_url_resolver_scoped_metrics = [ -# ('Function/django.urls.resolvers:RegexURLResolver.resolve', 'present'), -# ] - -# _test_django_post_2_0_url_resolver_scoped_metrics = [ -# ('Function/django.urls.resolvers:URLResolver.resolve', 'present'), -# ] - -# _test_application_index_scoped_metrics = [ -# ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), -# ('Python/WSGI/Application', 1), -# ('Python/WSGI/Response', 1), -# ('Python/WSGI/Finalize', 1), -# ('Function/views:index', 1), -# ] - -# if DJANGO_VERSION >= (1, 5): -# _test_application_index_scoped_metrics.extend([ -# ('Function/django.http.response:HttpResponse.close', 1)]) - -# if DJANGO_VERSION < (1, 10): -# _test_application_index_scoped_metrics.extend( -# _test_django_pre_1_10_url_resolver_scoped_metrics) -# elif DJANGO_VERSION >= (2, 0): -# _test_application_index_scoped_metrics.extend( -# _test_django_post_2_0_url_resolver_scoped_metrics) -# else: -# _test_application_index_scoped_metrics.extend( -# _test_django_post_1_10_url_resolver_scoped_metrics) - -# if DJANGO_SETTINGS_MODULE == 'settings_0110_old': -# _test_application_index_scoped_metrics.extend( -# _test_django_pre_1_10_middleware_scoped_metrics) -# elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': -# _test_application_index_scoped_metrics.extend( -# _test_django_post_1_10_middleware_scoped_metrics) -# elif DJANGO_VERSION < (1, 10): -# _test_application_index_scoped_metrics.extend( -# _test_django_pre_1_10_middleware_scoped_metrics) - - -# @validate_transaction_errors(errors=[]) -# @validate_transaction_metrics('views:index', -# scoped_metrics=_test_application_index_scoped_metrics) -# @validate_code_level_metrics("views", "index") -def test_application_index(): - test_application = target_application() - response = test_application.get("") - response.mustcontain("INDEX RESPONSE") - - -# @validate_transaction_metrics('views:exception') -# @validate_code_level_metrics("views", "exception") -# def test_application_exception(): -# test_application = target_application() -# test_application.get('/exception', status=500) - - -# _test_application_not_found_scoped_metrics = [ -# ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), -# ('Python/WSGI/Application', 1), -# ('Python/WSGI/Response', 1), -# ('Python/WSGI/Finalize', 1), -# ] - -# if DJANGO_VERSION >= (1, 5): -# _test_application_not_found_scoped_metrics.extend([ -# ('Function/django.http.response:HttpResponseNotFound.close', 1)]) - -# if DJANGO_VERSION < (1, 10): -# _test_application_not_found_scoped_metrics.extend( -# _test_django_pre_1_10_url_resolver_scoped_metrics) -# elif DJANGO_VERSION >= (2, 0): -# _test_application_not_found_scoped_metrics.extend( -# _test_django_post_2_0_url_resolver_scoped_metrics) -# else: -# _test_application_not_found_scoped_metrics.extend( -# _test_django_post_1_10_url_resolver_scoped_metrics) - -# if DJANGO_SETTINGS_MODULE == 'settings_0110_old': -# _test_application_not_found_scoped_metrics.extend( -# _test_django_pre_1_10_middleware_scoped_metrics) -# # The `CsrfViewMiddleware.process_view` isn't called for 404 Not Found. -# _test_application_not_found_scoped_metrics.remove( -# ('Function/django.middleware.csrf:CsrfViewMiddleware.process_view', 1)) -# elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': -# _test_application_not_found_scoped_metrics.extend( -# _test_django_post_1_10_middleware_scoped_metrics) -# elif DJANGO_VERSION < (1, 10): -# _test_application_not_found_scoped_metrics.extend( -# _test_django_pre_1_10_middleware_scoped_metrics) -# # The `CsrfViewMiddleware.process_view` isn't called for 404 Not Found. -# _test_application_not_found_scoped_metrics.remove( -# ('Function/django.middleware.csrf:CsrfViewMiddleware.process_view', 1)) - - -# @validate_transaction_errors(errors=[]) -# @validate_transaction_metrics('django.views.debug:technical_404_response', -# scoped_metrics=_test_application_not_found_scoped_metrics) -# def test_application_not_found(): -# test_application = target_application() -# test_application.get('/not_found', status=404) - - -# @override_ignore_status_codes([403]) -# @validate_transaction_errors(errors=[]) -# @validate_transaction_metrics('views:permission_denied') -# @validate_code_level_metrics("views", "permission_denied") -# def test_ignored_status_code(): -# test_application = target_application() -# test_application.get('/permission_denied', status=403) - - -# @override_ignore_status_codes([410]) -# @validate_transaction_errors(errors=[]) -# @validate_transaction_metrics('views:middleware_410') -# @validate_code_level_metrics("views", "middleware_410") -# def test_middleware_ignore_status_codes(): -# test_application = target_application() -# test_application.get('/middleware_410', status=410) - - -# _test_application_cbv_scoped_metrics = [ -# ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), -# ('Python/WSGI/Application', 1), -# ('Python/WSGI/Response', 1), -# ('Python/WSGI/Finalize', 1), -# ('Function/views:MyView', 1), -# ('Function/views:MyView.get', 1), -# ] - -# if DJANGO_VERSION >= (1, 5): -# _test_application_cbv_scoped_metrics.extend([ -# ('Function/django.http.response:HttpResponse.close', 1)]) - -# if DJANGO_VERSION < (1, 10): -# _test_application_cbv_scoped_metrics.extend( -# _test_django_pre_1_10_url_resolver_scoped_metrics) -# elif DJANGO_VERSION >= (2, 0): -# _test_application_cbv_scoped_metrics.extend( -# _test_django_post_2_0_url_resolver_scoped_metrics) -# else: -# _test_application_cbv_scoped_metrics.extend( -# _test_django_post_1_10_url_resolver_scoped_metrics) - -# if DJANGO_SETTINGS_MODULE == 'settings_0110_old': -# _test_application_cbv_scoped_metrics.extend( -# _test_django_pre_1_10_middleware_scoped_metrics) -# elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': -# _test_application_cbv_scoped_metrics.extend( -# _test_django_post_1_10_middleware_scoped_metrics) -# elif DJANGO_VERSION < (1, 10): -# _test_application_cbv_scoped_metrics.extend( -# _test_django_pre_1_10_middleware_scoped_metrics) - - -# @validate_transaction_errors(errors=[]) -# @validate_transaction_metrics('views:MyView.get', -# scoped_metrics=_test_application_cbv_scoped_metrics) -# @validate_code_level_metrics("views.MyView", "get") -# def test_application_cbv(): -# test_application = target_application() -# response = test_application.get('/cbv') -# response.mustcontain('CBV RESPONSE') - - -# _test_application_deferred_cbv_scoped_metrics = [ -# ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), -# ('Python/WSGI/Application', 1), -# ('Python/WSGI/Response', 1), -# ('Python/WSGI/Finalize', 1), -# ('Function/views:deferred_cbv', 1), -# ('Function/views:MyView.get', 1), -# ] - -# if DJANGO_VERSION >= (1, 5): -# _test_application_deferred_cbv_scoped_metrics.extend([ -# ('Function/django.http.response:HttpResponse.close', 1)]) - -# if DJANGO_VERSION < (1, 10): -# _test_application_deferred_cbv_scoped_metrics.extend( -# _test_django_pre_1_10_url_resolver_scoped_metrics) -# elif DJANGO_VERSION >= (2, 0): -# _test_application_deferred_cbv_scoped_metrics.extend( -# _test_django_post_2_0_url_resolver_scoped_metrics) -# else: -# _test_application_deferred_cbv_scoped_metrics.extend( -# _test_django_post_1_10_url_resolver_scoped_metrics) - -# if DJANGO_SETTINGS_MODULE == 'settings_0110_old': -# _test_application_deferred_cbv_scoped_metrics.extend( -# _test_django_pre_1_10_middleware_scoped_metrics) -# elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': -# _test_application_deferred_cbv_scoped_metrics.extend( -# _test_django_post_1_10_middleware_scoped_metrics) -# elif DJANGO_VERSION < (1, 10): -# _test_application_deferred_cbv_scoped_metrics.extend( -# _test_django_pre_1_10_middleware_scoped_metrics) - - -# @validate_transaction_errors(errors=[]) -# @validate_transaction_metrics('views:deferred_cbv', -# scoped_metrics=_test_application_deferred_cbv_scoped_metrics) -# @validate_code_level_metrics("views", "deferred_cbv") -# def test_application_deferred_cbv(): -# test_application = target_application() -# response = test_application.get('/deferred_cbv') -# response.mustcontain('CBV RESPONSE') - - -# _test_html_insertion_settings = { -# 'browser_monitoring.enabled': True, -# 'browser_monitoring.auto_instrument': True, -# 'js_agent_loader': u'', -# } - - -# @override_application_settings(_test_html_insertion_settings) -# def test_html_insertion_django_middleware(): -# test_application = target_application() -# response = test_application.get('/html_insertion', status=200) - -# # The 'NREUM HEADER' value comes from our override for the header. -# # The 'NREUM.info' value comes from the programmatically generated -# # footer added by the agent. - -# response.mustcontain('NREUM HEADER', 'NREUM.info') - - -# @override_application_settings(_test_html_insertion_settings) -# def test_html_insertion_django_gzip_middleware_enabled(): -# test_application = target_application() - -# # GZipMiddleware only fires if given the following header. - -# gzip_header = {'Accept-Encoding': 'gzip'} -# response = test_application.get('/gzip_html_insertion', status=200, -# headers=gzip_header) - -# # The 'NREUM HEADER' value comes from our override for the header. -# # The 'NREUM.info' value comes from the programmatically generated -# # footer added by the agent. - -# # The response.text will already be gunzipped - -# response.mustcontain('NREUM HEADER', 'NREUM.info') - - -# _test_html_insertion_settings_disabled = { -# 'browser_monitoring.enabled': False, -# 'browser_monitoring.auto_instrument': False, -# 'js_agent_loader': u'', -# } - - -# @override_application_settings(_test_html_insertion_settings_disabled) -# def test_html_insertion_django_gzip_middleware_disabled(): -# test_application = target_application() - -# # GZipMiddleware only fires if given the following header. - -# gzip_header = {'Accept-Encoding': 'gzip'} -# response = test_application.get('/gzip_html_insertion', status=200, -# headers=gzip_header) - -# # The 'NREUM HEADER' value comes from our override for the header. -# # The 'NREUM.info' value comes from the programmatically generated -# # footer added by the agent. - -# # The response.text will already be gunzipped - -# response.mustcontain(no=['NREUM HEADER', 'NREUM.info']) - - -# _test_html_insertion_manual_settings = { -# 'browser_monitoring.enabled': True, -# 'browser_monitoring.auto_instrument': True, -# 'js_agent_loader': u'', -# } - - -# @override_application_settings(_test_html_insertion_manual_settings) -# def test_html_insertion_manual_django_middleware(): -# test_application = target_application() -# response = test_application.get('/html_insertion_manual', status=200) - -# # The 'NREUM HEADER' value comes from our override for the header. -# # The 'NREUM.info' value comes from the programmatically generated -# # footer added by the agent. - -# response.mustcontain(no=['NREUM HEADER', 'NREUM.info']) - - -# @override_application_settings(_test_html_insertion_settings) -# def test_html_insertion_unnamed_attachment_header_django_middleware(): -# test_application = target_application() -# response = test_application.get( -# '/html_insertion_unnamed_attachment_header', status=200) - -# # The 'NREUM HEADER' value comes from our override for the header. -# # The 'NREUM.info' value comes from the programmatically generated -# # footer added by the agent. - -# response.mustcontain(no=['NREUM HEADER', 'NREUM.info']) - - -# @override_application_settings(_test_html_insertion_settings) -# def test_html_insertion_named_attachment_header_django_middleware(): -# test_application = target_application() -# response = test_application.get( -# '/html_insertion_named_attachment_header', status=200) - -# # The 'NREUM HEADER' value comes from our override for the header. -# # The 'NREUM.info' value comes from the programmatically generated -# # footer added by the agent. - -# response.mustcontain(no=['NREUM HEADER', 'NREUM.info']) - - -# _test_html_insertion_settings = { -# 'browser_monitoring.enabled': True, -# 'browser_monitoring.auto_instrument': False, -# 'js_agent_loader': u'', -# } - - -# @override_application_settings(_test_html_insertion_settings) -# def test_html_insertion_manual_tag_instrumentation(): -# test_application = target_application() -# response = test_application.get('/template_tags') - -# # Assert that the instrumentation is not inappropriately escaped - -# response.mustcontain('', -# no=['<!-- NREUM HEADER -->']) - - -# _test_application_inclusion_tag_scoped_metrics = [ -# ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), -# ('Python/WSGI/Application', 1), -# ('Python/WSGI/Response', 1), -# ('Python/WSGI/Finalize', 1), -# ('Function/views:inclusion_tag', 1), -# ('Template/Render/main.html', 1), -# ] - -# if DJANGO_VERSION < (1, 9): -# _test_application_inclusion_tag_scoped_metrics.extend([ -# ('Template/Include/results.html', 1)]) - -# if DJANGO_VERSION < (1, 10): -# _test_application_inclusion_tag_scoped_metrics.extend( -# _test_django_pre_1_10_url_resolver_scoped_metrics) -# elif DJANGO_VERSION >= (2, 0): -# _test_application_inclusion_tag_scoped_metrics.extend( -# _test_django_post_2_0_url_resolver_scoped_metrics) -# else: -# _test_application_inclusion_tag_scoped_metrics.extend( -# _test_django_post_1_10_url_resolver_scoped_metrics) - -# if DJANGO_SETTINGS_MODULE == 'settings_0110_old': -# _test_application_inclusion_tag_scoped_metrics.extend( -# _test_django_pre_1_10_middleware_scoped_metrics) -# elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': -# _test_application_inclusion_tag_scoped_metrics.extend( -# _test_django_post_1_10_middleware_scoped_metrics) -# elif DJANGO_VERSION < (1, 10): -# _test_application_inclusion_tag_scoped_metrics.extend( -# _test_django_pre_1_10_middleware_scoped_metrics) - -# try: -# _test_application_inclusion_tag_scoped_metrics.remove( -# (('Function/newrelic.hooks.framework_django:' -# 'browser_timing_insertion'), 1) -# ) -# except ValueError: -# pass - - -# @validate_transaction_errors(errors=[]) -# @validate_transaction_metrics('views:inclusion_tag', -# scoped_metrics=_test_application_inclusion_tag_scoped_metrics) -# @validate_code_level_metrics("views", "inclusion_tag") -# def test_application_inclusion_tag(): -# test_application = target_application() -# response = test_application.get('/inclusion_tag') -# response.mustcontain('Inclusion tag') - - -# _test_inclusion_tag_template_tags_scoped_metrics = [ -# ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), -# ('Python/WSGI/Application', 1), -# ('Python/WSGI/Response', 1), -# ('Python/WSGI/Finalize', 1), -# ('Function/views:inclusion_tag', 1), -# ('Template/Render/main.html', 1), -# ] - -# if DJANGO_VERSION < (1, 9): -# _test_inclusion_tag_template_tags_scoped_metrics.extend([ -# ('Template/Include/results.html', 1), -# ('Template/Tag/show_results', 1)]) - -# _test_inclusion_tag_settings = { -# 'instrumentation.templates.inclusion_tag': '*' -# } - -# if DJANGO_VERSION < (1, 10): -# _test_inclusion_tag_template_tags_scoped_metrics.extend( -# _test_django_pre_1_10_url_resolver_scoped_metrics) -# elif DJANGO_VERSION >= (2, 0): -# _test_inclusion_tag_template_tags_scoped_metrics.extend( -# _test_django_post_2_0_url_resolver_scoped_metrics) -# else: -# _test_inclusion_tag_template_tags_scoped_metrics.extend( -# _test_django_post_1_10_url_resolver_scoped_metrics) - -# if DJANGO_SETTINGS_MODULE == 'settings_0110_old': -# _test_inclusion_tag_template_tags_scoped_metrics.extend( -# _test_django_pre_1_10_middleware_scoped_metrics) -# elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': -# _test_inclusion_tag_template_tags_scoped_metrics.extend( -# _test_django_post_1_10_middleware_scoped_metrics) -# elif DJANGO_VERSION < (1, 10): -# _test_inclusion_tag_template_tags_scoped_metrics.extend( -# _test_django_pre_1_10_middleware_scoped_metrics) - -# try: -# _test_inclusion_tag_template_tags_scoped_metrics.remove( -# (('Function/newrelic.hooks.framework_django:' -# 'browser_timing_insertion'), 1) -# ) -# except ValueError: +# def test_no_harm(target_application): +# framework, version, target_application, is_bg, schema_type, extra_spans = target_application +# r = target_application("{ hello }") +# breakpoint() # pass - - -# @validate_transaction_errors(errors=[]) -# @validate_transaction_metrics('views:inclusion_tag', -# scoped_metrics=_test_inclusion_tag_template_tags_scoped_metrics) -# @override_generic_settings(django_settings, _test_inclusion_tag_settings) -# @validate_code_level_metrics("views", "inclusion_tag") -# def test_inclusion_tag_template_tag_metric(): -# test_application = target_application() -# response = test_application.get('/inclusion_tag') -# response.mustcontain('Inclusion tag') - - -# _test_template_render_exception_scoped_metrics_base = [ -# ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), -# ('Python/WSGI/Application', 1), -# ('Python/WSGI/Response', 1), -# ('Python/WSGI/Finalize', 1), -# ] - -# if DJANGO_VERSION < (1, 5): -# _test_template_render_exception_scoped_metrics_base.append( -# ('Function/django.http:HttpResponseServerError.close', 1)) -# elif DJANGO_VERSION < (1, 8): -# _test_template_render_exception_scoped_metrics_base.append( -# ('Function/django.http.response:HttpResponseServerError.close', 1)) -# else: -# _test_template_render_exception_scoped_metrics_base.append( -# ('Function/django.http.response:HttpResponse.close', 1)) - -# if DJANGO_VERSION < (1, 10): -# _test_template_render_exception_scoped_metrics_base.extend( -# _test_django_pre_1_10_url_resolver_scoped_metrics) -# elif DJANGO_VERSION >= (2, 0): -# _test_template_render_exception_scoped_metrics_base.extend( -# _test_django_post_2_0_url_resolver_scoped_metrics) -# else: -# _test_template_render_exception_scoped_metrics_base.extend( -# _test_django_post_1_10_url_resolver_scoped_metrics) - -# if DJANGO_SETTINGS_MODULE == 'settings_0110_old': -# _test_template_render_exception_scoped_metrics_base.extend( -# _test_django_pre_1_10_middleware_scoped_metrics) -# elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': -# _test_template_render_exception_scoped_metrics_base.extend( -# _test_django_post_1_10_middleware_scoped_metrics) -# elif DJANGO_VERSION < (1, 10): -# _test_template_render_exception_scoped_metrics_base.extend( -# _test_django_pre_1_10_middleware_scoped_metrics) - -# if DJANGO_VERSION < (1, 9): -# _test_template_render_exception_errors = [ -# 'django.template.base:TemplateSyntaxError'] -# else: -# _test_template_render_exception_errors = [ -# 'django.template.exceptions:TemplateSyntaxError'] - -# _test_template_render_exception_function_scoped_metrics = list( -# _test_template_render_exception_scoped_metrics_base) -# _test_template_render_exception_function_scoped_metrics.extend([ -# ('Function/views:render_exception_function', 1), -# ]) - - -# @validate_transaction_errors(errors=_test_template_render_exception_errors) -# @validate_transaction_metrics('views:render_exception_function', -# scoped_metrics=_test_template_render_exception_function_scoped_metrics) -# @validate_code_level_metrics("views", "render_exception_function") -# def test_template_render_exception_function(): -# test_application = target_application() -# test_application.get('/render_exception_function', status=500) - - -# _test_template_render_exception_class_scoped_metrics = list( -# _test_template_render_exception_scoped_metrics_base) -# _test_template_render_exception_class_scoped_metrics.extend([ -# ('Function/views:RenderExceptionClass', 1), -# ('Function/views:RenderExceptionClass.get', 1), -# ]) - - -# @validate_transaction_errors(errors=_test_template_render_exception_errors) -# @validate_transaction_metrics('views:RenderExceptionClass.get', -# scoped_metrics=_test_template_render_exception_class_scoped_metrics) -# @validate_code_level_metrics("views.RenderExceptionClass", "get") -# def test_template_render_exception_class(): -# test_application = target_application() -# test_application.get('/render_exception_class', status=500) diff --git a/tests/framework_graphene_django/urls.py b/tests/framework_graphene_django/urls.py index 870996913..f6317f533 100644 --- a/tests/framework_graphene_django/urls.py +++ b/tests/framework_graphene_django/urls.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from django.views.decorators.csrf import csrf_exempt - try: from django.conf.urls.defaults import path except ImportError: @@ -22,38 +20,11 @@ except ImportError: from django.urls import path -import views +from django.views.decorators.csrf import csrf_exempt from graphene_django.views import GraphQLView -from schema import schema + +from ._target_schema_sync import target_schema urlpatterns = [ - path("", views.index, name="index"), - path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))), - # url(r'^$', views.index, name='index'), - # url(r'^exception$', views.exception, name='exception'), - # url(r'^middleware_410$', views.middleware_410, name='middleware_410'), - # url(r'^permission_denied$', views.permission_denied, - # name='permission_denied'), - # url(r'^cbv$', views.MyView.as_view()), - # url(r'^deferred_cbv$', views.deferred_cbv), - # url(r'^html_insertion$', views.html_insertion, name='html_insertion'), - # url(r'^html_insertion_content_length$', - # views.html_insertion_content_length, - # name='html_insertion_content_length'), - # url(r'^html_insertion_manual$', views.html_insertion_manual, - # name='html_insertion_manual'), - # url(r'^html_insertion_unnamed_attachment_header$', - # views.html_insertion_unnamed_attachment_header, - # name='html_insertion_unnamed_attachment_header'), - # url(r'^html_insertion_named_attachment_header$', - # views.html_insertion_named_attachment_header, - # name='html_insertion_named_attachment_header'), - # url(r'^inclusion_tag$', views.inclusion_tag, name='inclusion_tag'), - # url(r'^template_tags$', views.template_tags, name='template_tags'), - # url(r'^render_exception_function', views.render_exception_function, - # name='render_exception_function'), - # url(r'^render_exception_class', views.RenderExceptionClass.as_view(), - # name='render_exception_class'), - # url(r'^gzip_html_insertion', views.gzip_html_insertion, - # name='gzip_html_insertion'), + path("", csrf_exempt(GraphQLView.as_view(graphiql=True, schema=target_schema)), name="graphql"), ] diff --git a/tests/framework_graphene_django/views.py b/tests/framework_graphene_django/views.py deleted file mode 100644 index ea696cdbc..000000000 --- a/tests/framework_graphene_django/views.py +++ /dev/null @@ -1,131 +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. - -from django.core.exceptions import PermissionDenied -from django.http import HttpResponse -from django.shortcuts import render -from django.views.generic.base import TemplateView, View - -from newrelic.api.transaction import ( - get_browser_timing_footer, - get_browser_timing_header, -) - -# from middleware import Custom410 - - -def index(request): - return HttpResponse("INDEX RESPONSE") - - -def exception(request): - raise RuntimeError("exception") - - -def permission_denied(request): - raise PermissionDenied() - - -# def middleware_410(request): -# raise Custom410() - - -class MyView(View): - def get(self, request): - return HttpResponse("CBV RESPONSE") - - -def deferred_cbv(request): - return MyView.as_view()(request) - - -def html_insertion(request): - return HttpResponse( - "Some header" - "

My First Heading

My first paragraph.

" - "" - ) - - -def html_insertion_content_length(request): - content = ( - "Some header" - "

My First Heading

My first paragraph.

" - "" - ) - response = HttpResponse(content) - response["Content-Length"] = len(content) - return response - - -def html_insertion_manual(request): - header = get_browser_timing_header() - footer = get_browser_timing_footer() - - header = get_browser_timing_header() - footer = get_browser_timing_footer() - - assert header == "" - assert footer == "" - - return HttpResponse( - "Some header" - "

My First Heading

My first paragraph.

" - "" - ) - - -def html_insertion_unnamed_attachment_header(request): - response = HttpResponse( - "Some header" - "

My First Heading

My first paragraph.

" - "" - ) - response["Content-Disposition"] = "attachment" - return response - - -def html_insertion_named_attachment_header(request): - response = HttpResponse( - "Some header" - "

My First Heading

My first paragraph.

" - "" - ) - response["Content-Disposition"] = 'Attachment; filename="X"' - return response - - -def inclusion_tag(request): - return render(request, "main.html", {}, content_type="text/html") - - -def template_tags(request): - return render(request, "main.html", {}, content_type="text/html") - - -def render_exception_function(request): - return render(request, "render_exception.html") - - -class RenderExceptionClass(TemplateView): - template_name = "render_exception.html" - - -def gzip_html_insertion(request): - # contents must be at least 200 bytes for gzip middleware to work - contents = "*" * 200 - return HttpResponse( - "Some header" - "

My First Heading

%s

" % contents - ) diff --git a/tests/framework_graphene_django/wsgi.py b/tests/framework_graphene_django/wsgi.py index 72b1f2110..fdf8da6dc 100644 --- a/tests/framework_graphene_django/wsgi.py +++ b/tests/framework_graphene_django/wsgi.py @@ -12,10 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") - try: from django.core.wsgi import get_wsgi_application From 8aa9de8fb68283fb03cceffd95a5e475a38137ab Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Wed, 2 Nov 2022 16:40:26 -0700 Subject: [PATCH 20/26] Imported graphene tests into graphene-django tests Co-authored-by: Timothy Pansino --- newrelic/hooks/framework_graphene_django.py | 28 ++++++++++++++----- .../_target_application.py | 4 ++- tests/framework_graphene_django/conftest.py | 5 ---- .../test_application.py | 17 ++++------- tests/framework_graphene_django/urls.py | 3 +- tox.ini | 5 +++- 6 files changed, 34 insertions(+), 28 deletions(-) diff --git a/newrelic/hooks/framework_graphene_django.py b/newrelic/hooks/framework_graphene_django.py index 792b2d104..e4d66fece 100644 --- a/newrelic/hooks/framework_graphene_django.py +++ b/newrelic/hooks/framework_graphene_django.py @@ -12,41 +12,54 @@ # See the License for the specific language governing permissions and # limitations under the License. +# import sys + from newrelic.api.error_trace import ErrorTrace from newrelic.api.graphql_trace import GraphQLOperationTrace from newrelic.api.transaction import current_transaction from newrelic.common.object_names import callable_name from newrelic.common.object_wrapper import wrap_function_wrapper -from newrelic.core.graphql_utils import graphql_statement + +# from newrelic.core.graphql_utils import graphql_statement +from newrelic.hooks.framework_graphene import ( + framework_details as graphene_framework_details, +) from newrelic.hooks.framework_graphql import ( framework_version as graphql_framework_version, ) from newrelic.hooks.framework_graphql import ignore_graphql_duplicate_exception -def framework_details(): +def graphene_django_version(): import graphene_django - return ("Graphene-Django", getattr(graphene_django, "__version__", None)) + try: + return tuple(int(x) for x in graphene_django.__version__.split(".")) + except Exception: + return (0, 0, 0) def bind_execute(query, *args, **kwargs): return query -# @promisify def wrap_execute_graphql_request(wrapped, instance, args, kwargs): transaction = current_transaction() if not transaction: return wrapped(*args, **kwargs) + # Return early for versions where this wrapper is unnecessary + version = graphene_django_version() + if version >= (3,) or not version: + return wrapped(*args, **kwargs) + try: query = bind_execute(*args, **kwargs) except TypeError: return wrapped(*args, **kwargs) - framework = framework_details() + framework = graphene_framework_details() transaction.add_framework_info(name=framework[0], version=framework[1]) transaction.add_framework_info(name="GraphQL", version=graphql_framework_version()) @@ -56,8 +69,9 @@ def wrap_execute_graphql_request(wrapped, instance, args, kwargs): transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=10) with GraphQLOperationTrace(source=wrapped) as trace: - trace.product = "Graphene-Django" - trace.statement = graphql_statement(query) + trace.product = "Graphene" + # breakpoint() + # trace.statement = graphql_statement(query) with ErrorTrace(ignore=ignore_graphql_duplicate_exception): return wrapped(*args, **kwargs) diff --git a/tests/framework_graphene_django/_target_application.py b/tests/framework_graphene_django/_target_application.py index 5a4118938..c972b2a62 100644 --- a/tests/framework_graphene_django/_target_application.py +++ b/tests/framework_graphene_django/_target_application.py @@ -15,6 +15,7 @@ import json +import pytest import webtest from .wsgi import application @@ -35,7 +36,8 @@ def _run_wsgi(query, middleware=None): else: expect_errors = False - app.app.middleware = middleware + if middleware is not None: + pytest.skip("Middleware not supported.") response = app.post( "/", json.dumps({"query": query}), headers={"Content-Type": "application/json"}, expect_errors=expect_errors diff --git a/tests/framework_graphene_django/conftest.py b/tests/framework_graphene_django/conftest.py index 43886d5a7..aaf959548 100644 --- a/tests/framework_graphene_django/conftest.py +++ b/tests/framework_graphene_django/conftest.py @@ -12,9 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# import os - -# import pytest from testing_support.fixtures import ( # noqa: F401; pylint: disable=W0611 code_coverage_fixture, collector_agent_registration_fixture, @@ -23,8 +20,6 @@ # from newrelic.packages import six -# os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") - _coverage_source = [ "newrelic.hooks.framework_graphql", ] diff --git a/tests/framework_graphene_django/test_application.py b/tests/framework_graphene_django/test_application.py index 444ae4c6a..dd86c663e 100644 --- a/tests/framework_graphene_django/test_application.py +++ b/tests/framework_graphene_django/test_application.py @@ -26,25 +26,18 @@ def target_application(request): return try: - import graphene_django + import graphene - version = graphene_django.__version__ + version = graphene.__version__ except Exception: import pkg_resources - version = pkg_resources.get_distribution("graphene_django").version + version = pkg_resources.get_distribution("graphene").version param = request.param.split("-") - is_background = param[0] not in {"wsgi", "asgi"} + is_background = False schema_type = param[1] extra_spans = 4 if param[0] == "wsgi" else 0 assert version is not None - return "GrapheneDjango", version, target_application, is_background, schema_type, extra_spans - - -# def test_no_harm(target_application): -# framework, version, target_application, is_bg, schema_type, extra_spans = target_application -# r = target_application("{ hello }") -# breakpoint() -# pass + return "Graphene", version, target_application, is_background, schema_type, extra_spans diff --git a/tests/framework_graphene_django/urls.py b/tests/framework_graphene_django/urls.py index f6317f533..06c64987d 100644 --- a/tests/framework_graphene_django/urls.py +++ b/tests/framework_graphene_django/urls.py @@ -21,10 +21,9 @@ from django.urls import path from django.views.decorators.csrf import csrf_exempt +from framework_graphene._target_schema_sync import target_schema from graphene_django.views import GraphQLView -from ._target_schema_sync import target_schema - urlpatterns = [ path("", csrf_exempt(GraphQLView.as_view(graphiql=True, schema=target_schema)), name="graphql"), ] diff --git a/tox.ini b/tox.ini index 56ccba0ae..3b3313a6e 100644 --- a/tox.ini +++ b/tox.ini @@ -129,7 +129,7 @@ envlist = python-framework_graphene-{py37,py38,py39,py310}-graphenelatest, python-framework_graphene-{py27,py37,py38,py39,pypy,pypy37}-graphene{0200,0201}, python-framework_graphene-py310-graphene0201, - python-framework_graphene_django-{py37,py38,py39,py310,pypy37} + python-framework_graphene_django-{py37,py38,py39,py310,pypy37}-framework_graphene_django{0215,latest} python-framework_graphql-{py27,py37,py38,py39,py310,pypy,pypy37}-graphql02, python-framework_graphql-{py37,py38,py39,py310,pypy37}-graphql03, ; temporarily disabling graphqlmaster tests @@ -313,6 +313,9 @@ deps = framework_graphene-graphenelatest: graphene framework_graphene-graphene0200: graphene<2.1 framework_graphene-graphene0201: graphene<2.2 + framework_graphene_django0215: graphene-django==2.15.0 + framework_graphene_django0215: graphene<3 + framework_graphene_django0215: django<4 framework_graphene_django: graphene-django framework_graphene_django: graphene framework_graphene_django: django From 1b52d492b30aff855b21c23bc6641a4efa2a2a46 Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Thu, 3 Nov 2022 15:02:39 -0700 Subject: [PATCH 21/26] Fix testing/account for existing GraphQLOperationTrace creation Co-authored-by: Timothy Pansino Co-authored-by: Hannah Stepanek Co-authored-by: Uma Annamalai --- newrelic/hooks/framework_graphene_django.py | 30 +++++-------------- newrelic/hooks/framework_graphql.py | 8 +++++ tests/framework_graphene_django/settings.py | 26 ++-------------- .../test_application.py | 2 +- 4 files changed, 18 insertions(+), 48 deletions(-) diff --git a/newrelic/hooks/framework_graphene_django.py b/newrelic/hooks/framework_graphene_django.py index e4d66fece..999d3ce15 100644 --- a/newrelic/hooks/framework_graphene_django.py +++ b/newrelic/hooks/framework_graphene_django.py @@ -12,15 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -# import sys - from newrelic.api.error_trace import ErrorTrace from newrelic.api.graphql_trace import GraphQLOperationTrace from newrelic.api.transaction import current_transaction from newrelic.common.object_names import callable_name from newrelic.common.object_wrapper import wrap_function_wrapper - -# from newrelic.core.graphql_utils import graphql_statement +from newrelic.core.graphql_utils import graphql_statement from newrelic.hooks.framework_graphene import ( framework_details as graphene_framework_details, ) @@ -30,16 +27,7 @@ from newrelic.hooks.framework_graphql import ignore_graphql_duplicate_exception -def graphene_django_version(): - import graphene_django - - try: - return tuple(int(x) for x in graphene_django.__version__.split(".")) - except Exception: - return (0, 0, 0) - - -def bind_execute(query, *args, **kwargs): +def bind_execute_graphql_request(request, data, query, variables, operation_name, show_graphiql=False): return query @@ -49,15 +37,10 @@ def wrap_execute_graphql_request(wrapped, instance, args, kwargs): if not transaction: return wrapped(*args, **kwargs) - # Return early for versions where this wrapper is unnecessary - version = graphene_django_version() - if version >= (3,) or not version: - return wrapped(*args, **kwargs) - try: - query = bind_execute(*args, **kwargs) + query = bind_execute_graphql_request(*args, **kwargs) except TypeError: - return wrapped(*args, **kwargs) + query = None framework = graphene_framework_details() transaction.add_framework_info(name=framework[0], version=framework[1]) @@ -70,8 +53,9 @@ def wrap_execute_graphql_request(wrapped, instance, args, kwargs): with GraphQLOperationTrace(source=wrapped) as trace: trace.product = "Graphene" - # breakpoint() - # trace.statement = graphql_statement(query) + if query: + trace.statement = graphql_statement(query) + with ErrorTrace(ignore=ignore_graphql_duplicate_exception): return wrapped(*args, **kwargs) diff --git a/newrelic/hooks/framework_graphql.py b/newrelic/hooks/framework_graphql.py index 0e691090f..aecfde94c 100644 --- a/newrelic/hooks/framework_graphql.py +++ b/newrelic/hooks/framework_graphql.py @@ -550,7 +550,15 @@ def wrap_graphql_impl(wrapped, instance, args, kwargs): if not transaction: return wrapped(*args, **kwargs) + # Inspect trace stack to see if there is an active GraphQLOperationTrace and return early if so + trace = current_trace() + while trace is not None: + if isinstance(trace, GraphQLOperationTrace): + return wrapped(*args, **kwargs) + trace = getattr(trace, "parent", None) + transaction.add_framework_info(name="GraphQL", version=framework_version()) + if graphql_version() < (3, 0): bind_query = bind_execute_graphql_query else: diff --git a/tests/framework_graphene_django/settings.py b/tests/framework_graphene_django/settings.py index 57b000176..511fd93f4 100644 --- a/tests/framework_graphene_django/settings.py +++ b/tests/framework_graphene_django/settings.py @@ -17,41 +17,19 @@ import django BASE_DIR = os.path.dirname(__file__) -DEBUG = True +DEBUG = False django_version = django.VERSION SECRET_KEY = "NotASecret" # nosec: B105 -# List of callables that know how to import templates from various sources. -# TEMPLATE_LOADERS = ( -# "django.template.loaders.filesystem.Loader", -# "django.template.loaders.app_directories.Loader", -# ) - ROOT_URLCONF = "framework_graphene_django.urls" -# TEMPLATE_DIRS = [os.path.join(BASE_DIR, "templates")] - -# For Django 1.10 compatibility because TEMPLATE_DIRS is deprecated -# TEMPLATES = [ -# { -# # "BACKEND": "django.template.backends.django.DjangoTemplates", -# # "DIRS": TEMPLATE_DIRS, -# "DIRS": [] -# } -# ] - INSTALLED_APPS = ( - # "django.contrib.auth", - # "django.contrib.contenttypes", - # "django.contrib.sessions", - # "django.contrib.messagess", - # "django.contrib.staticfiles", "graphene_django", "framework_graphene_django", ) -# GRAPHENE = {"SCHEMA": "_target_schema_sync.schema"} +MIDDLEWARE = [] WSGI_APPLICATION = "wsgi.application" diff --git a/tests/framework_graphene_django/test_application.py b/tests/framework_graphene_django/test_application.py index dd86c663e..6c5e96929 100644 --- a/tests/framework_graphene_django/test_application.py +++ b/tests/framework_graphene_django/test_application.py @@ -37,7 +37,7 @@ def target_application(request): param = request.param.split("-") is_background = False schema_type = param[1] - extra_spans = 4 if param[0] == "wsgi" else 0 + extra_spans = 8 if param[0] == "wsgi" else 0 assert version is not None return "Graphene", version, target_application, is_background, schema_type, extra_spans From 9288e440bc56a771a32d994513ff5f025a0e6145 Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Thu, 3 Nov 2022 16:00:23 -0700 Subject: [PATCH 22/26] Change framework to component --- newrelic/config.py | 2 +- ...e_django.py => component_graphenedjango.py} | 0 .../__init__.py | 2 +- .../_target_application.py | 0 .../_target_schema_sync.py | 0 .../conftest.py | 0 .../models.py | 0 .../settings.py | 4 ++-- .../test_application.py | 0 .../urls.py | 0 .../wsgi.py | 0 tox.ini | 18 +++++++++--------- 12 files changed, 13 insertions(+), 13 deletions(-) rename newrelic/hooks/{framework_graphene_django.py => component_graphenedjango.py} (100%) rename tests/{framework_graphene_django => component_graphenedjango}/__init__.py (87%) rename tests/{framework_graphene_django => component_graphenedjango}/_target_application.py (100%) rename tests/{framework_graphene_django => component_graphenedjango}/_target_schema_sync.py (100%) rename tests/{framework_graphene_django => component_graphenedjango}/conftest.py (100%) rename tests/{framework_graphene_django => component_graphenedjango}/models.py (100%) rename tests/{framework_graphene_django => component_graphenedjango}/settings.py (91%) rename tests/{framework_graphene_django => component_graphenedjango}/test_application.py (100%) rename tests/{framework_graphene_django => component_graphenedjango}/urls.py (100%) rename tests/{framework_graphene_django => component_graphenedjango}/wsgi.py (100%) diff --git a/newrelic/config.py b/newrelic/config.py index d41805704..109f820d9 100644 --- a/newrelic/config.py +++ b/newrelic/config.py @@ -2211,7 +2211,7 @@ def _process_module_builtin_defaults(): ) _process_module_definition( - "graphene_django.views", "newrelic.hooks.framework_graphene_django", "instrument_graphene_django_views" + "graphene_django.views", "newrelic.hooks.component_graphenedjango", "instrument_graphene_django_views" ) _process_module_definition( diff --git a/newrelic/hooks/framework_graphene_django.py b/newrelic/hooks/component_graphenedjango.py similarity index 100% rename from newrelic/hooks/framework_graphene_django.py rename to newrelic/hooks/component_graphenedjango.py diff --git a/tests/framework_graphene_django/__init__.py b/tests/component_graphenedjango/__init__.py similarity index 87% rename from tests/framework_graphene_django/__init__.py rename to tests/component_graphenedjango/__init__.py index 5abc40f73..4ef33f6e6 100644 --- a/tests/framework_graphene_django/__init__.py +++ b/tests/component_graphenedjango/__init__.py @@ -14,4 +14,4 @@ import os -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "framework_graphene_django.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "component_graphenedjango.settings") diff --git a/tests/framework_graphene_django/_target_application.py b/tests/component_graphenedjango/_target_application.py similarity index 100% rename from tests/framework_graphene_django/_target_application.py rename to tests/component_graphenedjango/_target_application.py diff --git a/tests/framework_graphene_django/_target_schema_sync.py b/tests/component_graphenedjango/_target_schema_sync.py similarity index 100% rename from tests/framework_graphene_django/_target_schema_sync.py rename to tests/component_graphenedjango/_target_schema_sync.py diff --git a/tests/framework_graphene_django/conftest.py b/tests/component_graphenedjango/conftest.py similarity index 100% rename from tests/framework_graphene_django/conftest.py rename to tests/component_graphenedjango/conftest.py diff --git a/tests/framework_graphene_django/models.py b/tests/component_graphenedjango/models.py similarity index 100% rename from tests/framework_graphene_django/models.py rename to tests/component_graphenedjango/models.py diff --git a/tests/framework_graphene_django/settings.py b/tests/component_graphenedjango/settings.py similarity index 91% rename from tests/framework_graphene_django/settings.py rename to tests/component_graphenedjango/settings.py index 511fd93f4..2d164bc65 100644 --- a/tests/framework_graphene_django/settings.py +++ b/tests/component_graphenedjango/settings.py @@ -23,11 +23,11 @@ SECRET_KEY = "NotASecret" # nosec: B105 -ROOT_URLCONF = "framework_graphene_django.urls" +ROOT_URLCONF = "component_graphenedjango.urls" INSTALLED_APPS = ( "graphene_django", - "framework_graphene_django", + "component_graphenedjango", ) MIDDLEWARE = [] diff --git a/tests/framework_graphene_django/test_application.py b/tests/component_graphenedjango/test_application.py similarity index 100% rename from tests/framework_graphene_django/test_application.py rename to tests/component_graphenedjango/test_application.py diff --git a/tests/framework_graphene_django/urls.py b/tests/component_graphenedjango/urls.py similarity index 100% rename from tests/framework_graphene_django/urls.py rename to tests/component_graphenedjango/urls.py diff --git a/tests/framework_graphene_django/wsgi.py b/tests/component_graphenedjango/wsgi.py similarity index 100% rename from tests/framework_graphene_django/wsgi.py rename to tests/component_graphenedjango/wsgi.py diff --git a/tox.ini b/tox.ini index 3b3313a6e..577c17cf0 100644 --- a/tox.ini +++ b/tox.ini @@ -129,7 +129,7 @@ envlist = python-framework_graphene-{py37,py38,py39,py310}-graphenelatest, python-framework_graphene-{py27,py37,py38,py39,pypy,pypy37}-graphene{0200,0201}, python-framework_graphene-py310-graphene0201, - python-framework_graphene_django-{py37,py38,py39,py310,pypy37}-framework_graphene_django{0215,latest} + python-component_graphenedjango-{py37,py38,py39,py310,pypy37}-component_graphenedjango{0215,latest} python-framework_graphql-{py27,py37,py38,py39,py310,pypy,pypy37}-graphql02, python-framework_graphql-{py37,py38,py39,py310,pypy37}-graphql03, ; temporarily disabling graphqlmaster tests @@ -209,6 +209,12 @@ deps = component_flask_rest: flask-restx component_flask_rest: jinja2<3.1 component_flask_rest: itsdangerous<2.1 + component_graphenedjango0215: graphene-django==2.15.0 + component_graphenedjango0215: graphene<3 + component_graphenedjango0215: django<4 + component_graphenedjango: graphene-django + component_graphenedjango: graphene + component_graphenedjango: django component_graphqlserver: graphql-server[sanic,flask]==3.0.0b5 component_graphqlserver: sanic>20 component_graphqlserver: Flask @@ -313,12 +319,6 @@ deps = framework_graphene-graphenelatest: graphene framework_graphene-graphene0200: graphene<2.1 framework_graphene-graphene0201: graphene<2.2 - framework_graphene_django0215: graphene-django==2.15.0 - framework_graphene_django0215: graphene<3 - framework_graphene_django0215: django<4 - framework_graphene_django: graphene-django - framework_graphene_django: graphene - framework_graphene_django: django framework_graphql-graphql02: graphql-core<3 framework_graphql-graphql03: graphql-core<4 framework_graphql-graphql0202: graphql-core<2.3 @@ -428,6 +428,7 @@ changedir = application_gearman: tests/application_gearman component_djangorestframework: tests/component_djangorestframework component_flask_rest: tests/component_flask_rest + component_graphenedjango: tests/component_graphenedjango component_graphqlserver: tests/component_graphqlserver component_tastypie: tests/component_tastypie coroutines_asyncio: tests/coroutines_asyncio @@ -469,8 +470,7 @@ changedir = framework_falcon: tests/framework_falcon framework_fastapi: tests/framework_fastapi framework_flask: tests/framework_flask - framework_graphene: tests/framework_graphene - framework_graphene_django: tests/framework_graphene_django + framework_graphene: tests/framework_graphene framework_graphql: tests/framework_graphql framework_grpc: tests/framework_grpc framework_pyramid: tests/framework_pyramid From 5ffd1af2c5e359016fafabd6c8e8a475f746abcf Mon Sep 17 00:00:00 2001 From: Tim Pansino Date: Fri, 4 Nov 2022 15:41:53 -0700 Subject: [PATCH 23/26] Add django ASGI testing --- .../_target_application.py | 42 +++++++++++++++---- tests/component_graphenedjango/asgi.py | 16 +++++++ .../test_application.py | 4 +- tests/component_graphenedjango/urls.py | 10 +++-- 4 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 tests/component_graphenedjango/asgi.py diff --git a/tests/component_graphenedjango/_target_application.py b/tests/component_graphenedjango/_target_application.py index c972b2a62..1d2d660f0 100644 --- a/tests/component_graphenedjango/_target_application.py +++ b/tests/component_graphenedjango/_target_application.py @@ -15,11 +15,15 @@ import json -import pytest import webtest -from .wsgi import application +from .asgi import application as asgi_application +from .wsgi import application as wsgi_application +from .urls import set_schema_and_middleware +from framework_graphene._target_schema_sync import target_schema as target_schema_sync + +from testing_support.asgi_testing import AsgiTest def check_response(query, success, response): if isinstance(query, str) and "error" not in query: @@ -29,16 +33,37 @@ def check_response(query, success, response): assert "errors" in response, response -def run_wsgi(app): +def run_asgi(app, schema): + def _run_asgi(query, middleware=None): + set_schema_and_middleware(schema, middleware) + + response = app.make_request( + "POST", "/", body=json.dumps({"query": query}), headers={"Content-Type": "application/json"} + ) + body = json.loads(response.body.decode("utf-8")) + + if not isinstance(query, str) or "error" in query: + try: + assert response.status != 200 + except AssertionError: + assert body["errors"] + else: + assert response.status == 200 + assert "errors" not in body or not body["errors"], body["errors"] + + return body.get("data", {}) + return _run_asgi + + +def run_wsgi(app, schema): def _run_wsgi(query, middleware=None): + set_schema_and_middleware(schema, middleware) + if not isinstance(query, str) or "error" in query: expect_errors = True else: expect_errors = False - if middleware is not None: - pytest.skip("Middleware not supported.") - response = app.post( "/", json.dumps({"query": query}), headers={"Content-Type": "application/json"}, expect_errors=expect_errors ) @@ -47,7 +72,7 @@ def _run_wsgi(query, middleware=None): if expect_errors: assert body["errors"] else: - assert "errors" not in body or not body["errors"] + assert "errors" not in body or not body["errors"], body["errors"] return body.get("data", {}) @@ -55,5 +80,6 @@ def _run_wsgi(query, middleware=None): target_application = { - "wsgi-sync": run_wsgi(webtest.TestApp(application)), + "wsgi-sync": run_wsgi(webtest.TestApp(wsgi_application), target_schema_sync), + "asgi-sync": run_asgi(AsgiTest(asgi_application), target_schema_sync), } diff --git a/tests/component_graphenedjango/asgi.py b/tests/component_graphenedjango/asgi.py new file mode 100644 index 000000000..2b3305bd1 --- /dev/null +++ b/tests/component_graphenedjango/asgi.py @@ -0,0 +1,16 @@ +# 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. + +from django.core.asgi import get_asgi_application +application = get_asgi_application() \ No newline at end of file diff --git a/tests/component_graphenedjango/test_application.py b/tests/component_graphenedjango/test_application.py index 6c5e96929..33f5ab59e 100644 --- a/tests/component_graphenedjango/test_application.py +++ b/tests/component_graphenedjango/test_application.py @@ -16,7 +16,7 @@ from framework_graphql.test_application import * # noqa -@pytest.fixture(scope="session", params=["wsgi-sync"]) +@pytest.fixture(scope="session", params=["wsgi-sync", "asgi-sync"]) def target_application(request): from ._target_application import target_application @@ -37,7 +37,7 @@ def target_application(request): param = request.param.split("-") is_background = False schema_type = param[1] - extra_spans = 8 if param[0] == "wsgi" else 0 + extra_spans = 8 if param[0] == "wsgi" else 3 assert version is not None return "Graphene", version, target_application, is_background, schema_type, extra_spans diff --git a/tests/component_graphenedjango/urls.py b/tests/component_graphenedjango/urls.py index 06c64987d..520ca1401 100644 --- a/tests/component_graphenedjango/urls.py +++ b/tests/component_graphenedjango/urls.py @@ -20,10 +20,14 @@ except ImportError: from django.urls import path -from django.views.decorators.csrf import csrf_exempt -from framework_graphene._target_schema_sync import target_schema from graphene_django.views import GraphQLView + +graphql_view = GraphQLView.as_view(graphiql=True) + urlpatterns = [ - path("", csrf_exempt(GraphQLView.as_view(graphiql=True, schema=target_schema)), name="graphql"), + path("", graphql_view, name="graphql"), ] + +def set_schema_and_middleware(schema=None, middleware=None): + graphql_view.view_initkwargs.update({"schema": schema, "middleware": middleware}) From ba20f03895dc51f2d3be810ab028f8dab26ab585 Mon Sep 17 00:00:00 2001 From: TimPansino Date: Fri, 4 Nov 2022 23:05:18 +0000 Subject: [PATCH 24/26] [Mega-Linter] Apply linters fixes --- tests/component_graphenedjango/_target_application.py | 8 ++++---- tests/component_graphenedjango/asgi.py | 3 ++- tests/component_graphenedjango/urls.py | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/component_graphenedjango/_target_application.py b/tests/component_graphenedjango/_target_application.py index 1d2d660f0..c7c8cd9aa 100644 --- a/tests/component_graphenedjango/_target_application.py +++ b/tests/component_graphenedjango/_target_application.py @@ -16,14 +16,13 @@ import json import webtest +from framework_graphene._target_schema_sync import target_schema as target_schema_sync +from testing_support.asgi_testing import AsgiTest from .asgi import application as asgi_application -from .wsgi import application as wsgi_application from .urls import set_schema_and_middleware +from .wsgi import application as wsgi_application -from framework_graphene._target_schema_sync import target_schema as target_schema_sync - -from testing_support.asgi_testing import AsgiTest def check_response(query, success, response): if isinstance(query, str) and "error" not in query: @@ -52,6 +51,7 @@ def _run_asgi(query, middleware=None): assert "errors" not in body or not body["errors"], body["errors"] return body.get("data", {}) + return _run_asgi diff --git a/tests/component_graphenedjango/asgi.py b/tests/component_graphenedjango/asgi.py index 2b3305bd1..2ced71680 100644 --- a/tests/component_graphenedjango/asgi.py +++ b/tests/component_graphenedjango/asgi.py @@ -13,4 +13,5 @@ # limitations under the License. from django.core.asgi import get_asgi_application -application = get_asgi_application() \ No newline at end of file + +application = get_asgi_application() diff --git a/tests/component_graphenedjango/urls.py b/tests/component_graphenedjango/urls.py index 520ca1401..4c27c5319 100644 --- a/tests/component_graphenedjango/urls.py +++ b/tests/component_graphenedjango/urls.py @@ -22,12 +22,12 @@ from graphene_django.views import GraphQLView - graphql_view = GraphQLView.as_view(graphiql=True) urlpatterns = [ path("", graphql_view, name="graphql"), ] + def set_schema_and_middleware(schema=None, middleware=None): graphql_view.view_initkwargs.update({"schema": schema, "middleware": middleware}) From 6ade93ebce6062f23a9fdf83c566e52beb36b196 Mon Sep 17 00:00:00 2001 From: Hannah Stepanek Date: Mon, 7 Nov 2022 11:17:15 -0800 Subject: [PATCH 25/26] Trigger tests From 85021605ec2e737a8437782ffc711471f92d85ea Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Mon, 7 Nov 2022 12:25:49 -0800 Subject: [PATCH 26/26] Add comment to explain import exception --- tests/component_graphenedjango/wsgi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/component_graphenedjango/wsgi.py b/tests/component_graphenedjango/wsgi.py index fdf8da6dc..89a4001e4 100644 --- a/tests/component_graphenedjango/wsgi.py +++ b/tests/component_graphenedjango/wsgi.py @@ -17,6 +17,7 @@ application = get_wsgi_application() +# This exception is to account for any users using Django v1.4 or earlier except ImportError: import django.core.handlers.wsgi