Skip to content

Commit 367e57b

Browse files
authored
GraphQL Server Testing (#506)
* Finalize Sanic testing * Fix flask framework details with callable * Parametrized testing for graphql-server * Add middleware tests to graphqlserver * Reenable code coverage * [Mega-Linter] Apply linters fixes * Bump Tests * Fix nonlocal binding issues in python 2 Co-authored-by: TimPansino <[email protected]>
1 parent 191f2e8 commit 367e57b

File tree

6 files changed

+624
-164
lines changed

6 files changed

+624
-164
lines changed

newrelic/api/wsgi_application.py

+81-83
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,25 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import functools
16+
import logging
1517
import sys
1618
import time
17-
import logging
18-
import functools
1919

2020
from newrelic.api.application import application_instance
21-
from newrelic.api.transaction import current_transaction
22-
from newrelic.api.time_trace import notice_error
23-
from newrelic.api.web_transaction import WSGIWebTransaction
2421
from newrelic.api.function_trace import FunctionTrace
2522
from newrelic.api.html_insertion import insert_html_snippet, verify_body_exists
26-
23+
from newrelic.api.time_trace import notice_error
24+
from newrelic.api.transaction import current_transaction
25+
from newrelic.api.web_transaction import WSGIWebTransaction
2726
from newrelic.common.object_names import callable_name
28-
from newrelic.common.object_wrapper import wrap_object, FunctionWrapper
29-
27+
from newrelic.common.object_wrapper import FunctionWrapper, wrap_object
3028
from newrelic.packages import six
3129

3230
_logger = logging.getLogger(__name__)
3331

3432

3533
class _WSGIApplicationIterable(object):
36-
3734
def __init__(self, transaction, generator):
3835
self.transaction = transaction
3936
self.generator = generator
@@ -68,8 +65,7 @@ def start_trace(self):
6865
self.transaction._sent_start = time.time()
6966

7067
if not self.response_trace:
71-
self.response_trace = FunctionTrace(
72-
name='Response', group='Python/WSGI')
68+
self.response_trace = FunctionTrace(name="Response", group="Python/WSGI")
7369
self.response_trace.__enter__()
7470

7571
def close(self):
@@ -81,13 +77,12 @@ def close(self):
8177
self.response_trace = None
8278

8379
try:
84-
with FunctionTrace(
85-
name='Finalize', group='Python/WSGI'):
80+
with FunctionTrace(name="Finalize", group="Python/WSGI"):
8681

8782
if isinstance(self.generator, _WSGIApplicationMiddleware):
8883
self.generator.close()
8984

90-
elif hasattr(self.generator, 'close'):
85+
elif hasattr(self.generator, "close"):
9186
name = callable_name(self.generator.close)
9287
with FunctionTrace(name):
9388
self.generator.close()
@@ -105,7 +100,6 @@ def close(self):
105100

106101

107102
class _WSGIInputWrapper(object):
108-
109103
def __init__(self, transaction, input):
110104
self.__transaction = transaction
111105
self.__input = input
@@ -114,7 +108,7 @@ def __getattr__(self, name):
114108
return getattr(self.__input, name)
115109

116110
def close(self):
117-
if hasattr(self.__input, 'close'):
111+
if hasattr(self.__input, "close"):
118112
self.__input.close()
119113

120114
def read(self, *args, **kwargs):
@@ -204,8 +198,7 @@ def __init__(self, application, environ, start_response, transaction):
204198

205199
# Grab the iterable returned by the wrapped WSGI
206200
# application.
207-
self.iterable = self.application(self.request_environ,
208-
self.start_response)
201+
self.iterable = self.application(self.request_environ, self.start_response)
209202

210203
def process_data(self, data):
211204
# If this is the first data block, then immediately try
@@ -217,7 +210,7 @@ def html_to_be_inserted():
217210
header = self.transaction.browser_timing_header()
218211

219212
if not header:
220-
return b''
213+
return b""
221214

222215
footer = self.transaction.browser_timing_footer()
223216

@@ -228,10 +221,12 @@ def html_to_be_inserted():
228221

229222
if modified is not None:
230223
if self.debug:
231-
_logger.debug('RUM insertion from WSGI middleware '
232-
'triggered on first yielded string from '
233-
'response. Bytes added was %r.',
234-
len(modified) - len(data))
224+
_logger.debug(
225+
"RUM insertion from WSGI middleware "
226+
"triggered on first yielded string from "
227+
"response. Bytes added was %r.",
228+
len(modified) - len(data),
229+
)
235230

236231
if self.content_length is not None:
237232
length = len(modified) - len(data)
@@ -264,7 +259,7 @@ def html_to_be_inserted():
264259

265260
if self.response_data:
266261
self.response_data.append(data)
267-
data = b''.join(self.response_data)
262+
data = b"".join(self.response_data)
268263
self.response_data = []
269264

270265
# Perform the insertion of the HTML. This should always
@@ -276,10 +271,12 @@ def html_to_be_inserted():
276271

277272
if modified is not None:
278273
if self.debug:
279-
_logger.debug('RUM insertion from WSGI middleware '
280-
'triggered on subsequent string yielded from '
281-
'response. Bytes added was %r.',
282-
len(modified) - len(data))
274+
_logger.debug(
275+
"RUM insertion from WSGI middleware "
276+
"triggered on subsequent string yielded from "
277+
"response. Bytes added was %r.",
278+
len(modified) - len(data),
279+
)
283280

284281
if self.content_length is not None:
285282
length = len(modified) - len(data)
@@ -297,11 +294,10 @@ def flush_headers(self):
297294
# additional data was inserted into the response.
298295

299296
if self.content_length is not None:
300-
header = (('Content-Length', str(self.content_length)))
297+
header = ("Content-Length", str(self.content_length))
301298
self.response_headers.append(header)
302299

303-
self.outer_write = self.outer_start_response(self.response_status,
304-
self.response_headers, *self.response_args)
300+
self.outer_write = self.outer_start_response(self.response_status, self.response_headers, *self.response_args)
305301

306302
def inner_write(self, data):
307303
# If the write() callable is used, we do not attempt to
@@ -345,8 +341,7 @@ def start_response(self, status, response_headers, *args):
345341
# This is because it can be disabled using an API call.
346342
# Also check whether RUM insertion has already occurred.
347343

348-
if (self.transaction.autorum_disabled or
349-
self.transaction.rum_header_generated):
344+
if self.transaction.autorum_disabled or self.transaction.rum_header_generated:
350345

351346
self.flush_headers()
352347
self.pass_through = True
@@ -370,21 +365,21 @@ def start_response(self, status, response_headers, *args):
370365
for (name, value) in response_headers:
371366
_name = name.lower()
372367

373-
if _name == 'content-length':
368+
if _name == "content-length":
374369
try:
375370
content_length = int(value)
376371
continue
377372

378373
except ValueError:
379374
pass_through = True
380375

381-
elif _name == 'content-type':
376+
elif _name == "content-type":
382377
content_type = value
383378

384-
elif _name == 'content-encoding':
379+
elif _name == "content-encoding":
385380
content_encoding = value
386381

387-
elif _name == 'content-disposition':
382+
elif _name == "content-disposition":
388383
content_disposition = value
389384

390385
headers.append((name, value))
@@ -408,9 +403,7 @@ def should_insert_html():
408403

409404
return False
410405

411-
if (content_disposition is not None and
412-
content_disposition.split(';')[0].strip().lower() ==
413-
'attachment'):
406+
if content_disposition is not None and content_disposition.split(";")[0].strip().lower() == "attachment":
414407
return False
415408

416409
if content_type is None:
@@ -419,7 +412,7 @@ def should_insert_html():
419412
settings = self.transaction.settings
420413
allowed_content_type = settings.browser_monitoring.content_type
421414

422-
if content_type.split(';')[0] not in allowed_content_type:
415+
if content_type.split(";")[0] not in allowed_content_type:
423416
return False
424417

425418
return True
@@ -443,7 +436,7 @@ def close(self):
443436
# Call close() on the iterable as required by the
444437
# WSGI specification.
445438

446-
if hasattr(self.iterable, 'close'):
439+
if hasattr(self.iterable, "close"):
447440
name = callable_name(self.iterable.close)
448441
with FunctionTrace(name):
449442
self.iterable.close()
@@ -518,18 +511,35 @@ def __iter__(self):
518511
yield data
519512

520513

521-
def WSGIApplicationWrapper(wrapped, application=None, name=None,
522-
group=None, framework=None):
514+
def WSGIApplicationWrapper(wrapped, application=None, name=None, group=None, framework=None):
515+
516+
# Python 2 does not allow rebinding nonlocal variables, so to fix this
517+
# framework must be stored in list so it can be edited by closure.
518+
_framework = [framework]
519+
520+
def get_framework():
521+
"""Used to delay imports by passing framework as a callable."""
522+
framework = _framework[0]
523+
if isinstance(framework, tuple) or framework is None:
524+
return framework
525+
526+
if callable(framework):
527+
framework = framework()
528+
_framework[0] = framework
529+
530+
if framework is not None and not isinstance(framework, tuple):
531+
framework = (framework, None)
532+
_framework[0] = framework
523533

524-
if framework is not None and not isinstance(framework, tuple):
525-
framework = (framework, None)
534+
return framework
526535

527536
def _nr_wsgi_application_wrapper_(wrapped, instance, args, kwargs):
528537
# Check to see if any transaction is present, even an inactive
529538
# one which has been marked to be ignored or which has been
530539
# stopped already.
531540

532541
transaction = current_transaction(active_only=False)
542+
framework = get_framework()
533543

534544
if transaction:
535545
# If there is any active transaction we will return without
@@ -545,8 +555,7 @@ def _nr_wsgi_application_wrapper_(wrapped, instance, args, kwargs):
545555
# supportability metrics.
546556

547557
if framework:
548-
transaction.add_framework_info(
549-
name=framework[0], version=framework[1])
558+
transaction.add_framework_info(name=framework[0], version=framework[1])
550559

551560
# Also override the web transaction name to be the name of
552561
# the wrapped callable if not explicitly named, and we want
@@ -560,9 +569,8 @@ def _nr_wsgi_application_wrapper_(wrapped, instance, args, kwargs):
560569
if name is None and settings:
561570
if framework is not None:
562571
naming_scheme = settings.transaction_name.naming_scheme
563-
if naming_scheme in (None, 'framework'):
564-
transaction.set_transaction_name(
565-
callable_name(wrapped), priority=1)
572+
if naming_scheme in (None, "framework"):
573+
transaction.set_transaction_name(callable_name(wrapped), priority=1)
566574

567575
elif name:
568576
transaction.set_transaction_name(name, group, priority=1)
@@ -580,11 +588,11 @@ def _args(environ, start_response, *args, **kwargs):
580588

581589
target_application = application
582590

583-
if 'newrelic.app_name' in environ:
584-
app_name = environ['newrelic.app_name']
591+
if "newrelic.app_name" in environ:
592+
app_name = environ["newrelic.app_name"]
585593

586-
if ';' in app_name:
587-
app_names = [n.strip() for n in app_name.split(';')]
594+
if ";" in app_name:
595+
app_names = [n.strip() for n in app_name.split(";")]
588596
app_name = app_names[0]
589597
target_application = application_instance(app_name)
590598
for altname in app_names[1:]:
@@ -598,7 +606,7 @@ def _args(environ, start_response, *args, **kwargs):
598606

599607
# FIXME Should this allow for multiple apps if a string.
600608

601-
if not hasattr(application, 'activate'):
609+
if not hasattr(application, "activate"):
602610
target_application = application_instance(application)
603611

604612
# Now start recording the actual web transaction.
@@ -609,8 +617,7 @@ def _args(environ, start_response, *args, **kwargs):
609617
# reporting as supportability metrics.
610618

611619
if framework:
612-
transaction.add_framework_info(
613-
name=framework[0], version=framework[1])
620+
transaction.add_framework_info(name=framework[0], version=framework[1])
614621

615622
# Override the initial web transaction name to be the supplied
616623
# name, or the name of the wrapped callable if wanting to use
@@ -630,24 +637,20 @@ def _args(environ, start_response, *args, **kwargs):
630637
naming_scheme = settings.transaction_name.naming_scheme
631638

632639
if framework is not None:
633-
if naming_scheme in (None, 'framework'):
634-
transaction.set_transaction_name(
635-
callable_name(wrapped), priority=1)
640+
if naming_scheme in (None, "framework"):
641+
transaction.set_transaction_name(callable_name(wrapped), priority=1)
636642

637-
elif naming_scheme in ('component', 'framework'):
638-
transaction.set_transaction_name(
639-
callable_name(wrapped), priority=1)
643+
elif naming_scheme in ("component", "framework"):
644+
transaction.set_transaction_name(callable_name(wrapped), priority=1)
640645

641646
elif name:
642647
transaction.set_transaction_name(name, group, priority=1)
643648

644649
def _start_response(status, response_headers, *args):
645650

646-
additional_headers = transaction.process_response(
647-
status, response_headers, *args)
651+
additional_headers = transaction.process_response(status, response_headers, *args)
648652

649-
_write = start_response(status,
650-
response_headers + additional_headers, *args)
653+
_write = start_response(status, response_headers + additional_headers, *args)
651654

652655
def write(data):
653656
if not transaction._sent_start:
@@ -667,17 +670,13 @@ def write(data):
667670
# Should always exist, but check as test harnesses may not
668671
# have it.
669672

670-
if 'wsgi.input' in environ:
671-
environ['wsgi.input'] = _WSGIInputWrapper(transaction,
672-
environ['wsgi.input'])
673+
if "wsgi.input" in environ:
674+
environ["wsgi.input"] = _WSGIInputWrapper(transaction, environ["wsgi.input"])
673675

674-
with FunctionTrace(
675-
name='Application', group='Python/WSGI'):
676+
with FunctionTrace(name="Application", group="Python/WSGI"):
676677
with FunctionTrace(name=callable_name(wrapped)):
677-
if (settings and settings.browser_monitoring.enabled and
678-
not transaction.autorum_disabled):
679-
result = _WSGIApplicationMiddleware(wrapped,
680-
environ, _start_response, transaction)
678+
if settings and settings.browser_monitoring.enabled and not transaction.autorum_disabled:
679+
result = _WSGIApplicationMiddleware(wrapped, environ, _start_response, transaction)
681680
else:
682681
result = wrapped(environ, _start_response)
683682

@@ -691,11 +690,10 @@ def write(data):
691690

692691

693692
def wsgi_application(application=None, name=None, group=None, framework=None):
694-
return functools.partial(WSGIApplicationWrapper, application=application,
695-
name=name, group=group, framework=framework)
693+
return functools.partial(
694+
WSGIApplicationWrapper, application=application, name=name, group=group, framework=framework
695+
)
696696

697697

698-
def wrap_wsgi_application(module, object_path, application=None,
699-
name=None, group=None, framework=None):
700-
wrap_object(module, object_path, WSGIApplicationWrapper,
701-
(application, name, group, framework))
698+
def wrap_wsgi_application(module, object_path, application=None, name=None, group=None, framework=None):
699+
wrap_object(module, object_path, WSGIApplicationWrapper, (application, name, group, framework))

0 commit comments

Comments
 (0)