Skip to content

Commit de6929d

Browse files
TimPansinoa-feldkat-starumaannamalaikkaiyang94
authored
Sanic Updates (#251)
* Fix sanic response header parsing. * Call twice since underlying function mutates the headers. * Fix request middleware duplication counts. * Add app argument to request. * Fix return count change. * Convert middleware patches to deque. * Fix custom router. * Fix binding. * Expand sanic testing in toxfile. Co-authored-by: Katherine Kelly <[email protected]> Co-authored-by: Uma Annamalai <[email protected]> Co-authored-by: Kevin K. Yang <[email protected]> * Update fixture signature for attribute checking * Fix all sanic tests Fix header tests Working on sanic testing Converting sanic to AsgiTest Asgi Testing Co-authored-by: Uma Annamalai <[email protected]> Patching for latest sanic versions Co-authored-by: Uma Annamalai <[email protected]> Reverting ill conceived changes Finish fixing sanic tests * Fix forgone param fixture * Fix tox.ini typo * Remove unused package Co-authored-by: Allan Feldman <[email protected]> Co-authored-by: Katherine Kelly <[email protected]> Co-authored-by: Uma Annamalai <[email protected]> Co-authored-by: Kevin K. Yang <[email protected]>
1 parent 2b98478 commit de6929d

File tree

7 files changed

+254
-109
lines changed

7 files changed

+254
-109
lines changed

newrelic/hooks/framework_sanic.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import sys
16+
from inspect import isawaitable, iscoroutinefunction
1617

1718
from newrelic.api.web_transaction import web_transaction
1819
from newrelic.api.transaction import current_transaction
@@ -157,6 +158,44 @@ def _sanic_app_init(wrapped, instance, args, kwargs):
157158
return result
158159

159160

161+
def _nr_sanic_response_get_headers(wrapped, instance, args, kwargs):
162+
# We call the wrapped function twice to allow the function to mutate the headers
163+
result = wrapped(*args, **kwargs)
164+
transaction = current_transaction()
165+
166+
if transaction is None:
167+
return result
168+
169+
# instance is the response object
170+
cat_headers = transaction.process_response(str(instance.status),
171+
instance.headers.items())
172+
173+
for header_name, header_value in cat_headers:
174+
if header_name not in instance.headers:
175+
instance.headers[header_name] = header_value
176+
177+
return wrapped(*args, **kwargs)
178+
179+
180+
async def _nr_sanic_response_send(wrapped, instance, args, kwargs):
181+
transaction = current_transaction()
182+
result = wrapped(*args, **kwargs)
183+
if isawaitable(result):
184+
await result
185+
186+
if transaction is None:
187+
return wrapped(*args, **kwargs)
188+
189+
# instance is the response object
190+
cat_headers = transaction.process_response(str(instance.status),
191+
instance.headers.items())
192+
193+
for header_name, header_value in cat_headers:
194+
if header_name not in instance.headers:
195+
instance.headers[header_name] = header_value
196+
197+
return result
198+
160199
def _nr_sanic_response_parse_headers(wrapped, instance, args, kwargs):
161200
transaction = current_transaction()
162201

@@ -235,5 +274,13 @@ def instrument_sanic_app(module):
235274

236275

237276
def instrument_sanic_response(module):
238-
wrap_function_wrapper(module, 'BaseHTTPResponse._parse_headers',
239-
_nr_sanic_response_parse_headers)
277+
if hasattr(module.BaseHTTPResponse, 'send'):
278+
wrap_function_wrapper(module, 'BaseHTTPResponse.send',
279+
_nr_sanic_response_send)
280+
else:
281+
if hasattr(module.BaseHTTPResponse, 'get_headers'):
282+
wrap_function_wrapper(module, 'BaseHTTPResponse.get_headers',
283+
_nr_sanic_response_get_headers)
284+
if hasattr(module.BaseHTTPResponse, '_parse_headers'):
285+
wrap_function_wrapper(module, 'BaseHTTPResponse._parse_headers',
286+
_nr_sanic_response_parse_headers)

tests/framework_sanic/_target_application.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,24 +50,39 @@ def add(self, exception, handler, *args, **kwargs):
5050

5151

5252
class CustomRouter(Router):
53+
def __init__(self):
54+
try:
55+
super().__init__(app=None)
56+
except TypeError:
57+
super().__init__()
58+
5359
def add(self, *args, **kwargs):
5460
base_add = Router.add
5561
if hasattr(base_add, '__wrapped__'):
5662
base_add = base_add.__wrapped__
57-
return base_add(self, *args, **kwargs)
63+
return base_add.__get__(self, Router)(*args, **kwargs)
5864

59-
def get(self, request):
65+
def get(self, *args):
6066
base_get = Router.get
6167
if hasattr(base_get, '__wrapped__'):
6268
base_get = base_get.__wrapped__
6369

64-
handler, args, kwargs, uri = base_get(self, request)
65-
if request.path == '/server-error':
66-
handler = None
67-
return handler, args, kwargs, uri
70+
if len(args) == 1:
71+
path = args[0].path
72+
else:
73+
path = args[0]
74+
75+
bound_get = base_get.__get__(self, Router)
76+
get_results = list(bound_get(*args))
77+
if path == '/server-error':
78+
from sanic.exceptions import ServerError
79+
raise ServerError("Server Error")
80+
return get_results
6881

6982

70-
app = Sanic(error_handler=CustomErrorHandler(), router=CustomRouter())
83+
router = CustomRouter()
84+
app = Sanic(name="test app", error_handler=CustomErrorHandler(), router=router)
85+
router.app = app
7186

7287

7388
@app.route('/')
@@ -165,5 +180,9 @@ async def async_error(request):
165180

166181

167182
app.add_route(MethodView.as_view(), '/method_view')
183+
184+
if not getattr(router, "finalized", True):
185+
router.finalize()
186+
168187
if __name__ == '__main__':
169188
app.run(host='127.0.0.1', port=8000)

tests/framework_sanic/conftest.py

Lines changed: 84 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414

1515
import pytest
1616

17+
from newrelic.common.object_wrapper import transient_function_wrapper
18+
1719
from testing_support.fixtures import (code_coverage_fixture,
1820
collector_agent_registration_fixture, collector_available_fixture)
1921

2022
import asyncio
21-
from sanic.request import Request
2223

2324
_coverage_source = [
2425
'newrelic.hooks.framework_sanic',
@@ -39,47 +40,82 @@
3940
default_settings=_default_settings)
4041

4142

42-
def create_request_class(method, url, headers=None):
43-
_request = Request(
44-
method=method.upper(),
45-
url_bytes=url.encode('utf-8'),
46-
headers=headers,
47-
version='1.0',
48-
transport=None,
49-
)
50-
return _request
51-
52-
53-
def create_request_coroutine(app, method, url, headers=None, responses=None):
54-
if responses is None:
55-
responses = []
56-
57-
def write_callback(response):
58-
response.raw_headers = response.output()
59-
if b'write_response_error' in response.raw_headers:
60-
raise ValueError()
43+
RESPONSES = []
44+
45+
def create_request_class(app, method, url, headers=None, loop=None):
46+
from sanic.request import Request
47+
try:
48+
_request = Request(
49+
method=method.upper(),
50+
url_bytes=url.encode('utf-8'),
51+
headers=headers,
52+
version='1.0',
53+
transport=None,
54+
)
55+
except TypeError:
56+
_request = Request(
57+
app=app,
58+
method=method.upper(),
59+
url_bytes=url.encode('utf-8'),
60+
headers=headers,
61+
version='1.0',
62+
transport=None,
63+
)
64+
65+
try:
66+
# Manually initialize HTTP protocol
67+
from sanic.http import Http, Stage
68+
from sanic.server import HttpProtocol
69+
70+
class MockProtocol(HttpProtocol):
71+
async def send(*args, **kwargs):
72+
return
73+
74+
proto = MockProtocol(loop=loop, app=app)
75+
proto.recv_buffer = bytearray()
76+
http = Http(proto)
77+
http.stage = Stage.HANDLER
78+
http.response_func = http.http1_response_header
79+
_request.stream = http
80+
pass
81+
except ImportError:
82+
pass
6183

62-
responses.append(response)
84+
return _request
6385

64-
async def stream_callback(response):
65-
response.raw_headers = response.get_headers()
66-
responses.append(response)
6786

87+
def create_request_coroutine(app, method, url, headers=None, loop=None):
6888
headers = headers or {}
69-
coro = app.handle_request(
70-
create_request_class(method, url, headers),
71-
write_callback,
72-
stream_callback,
73-
)
89+
try:
90+
coro = app.handle_request(create_request_class(app, method, url, headers, loop=loop))
91+
except TypeError:
92+
def write_callback(response):
93+
response.raw_headers = response.output()
94+
if b'write_response_error' in response.raw_headers:
95+
raise ValueError("write_response_error")
96+
97+
if response not in RESPONSES:
98+
RESPONSES.append(response)
99+
100+
async def stream_callback(response):
101+
response.raw_headers = response.get_headers()
102+
if response not in RESPONSES:
103+
RESPONSES.append(response)
104+
105+
coro = app.handle_request(
106+
create_request_class(app, method, url, headers, loop=loop),
107+
write_callback,
108+
stream_callback,
109+
)
110+
74111
return coro
75112

76113

77114
def request(app, method, url, headers=None):
78-
responses = []
79-
coro = create_request_coroutine(app, method, url, headers, responses)
80115
loop = asyncio.get_event_loop()
116+
coro = create_request_coroutine(app, method, url, headers, loop)
81117
loop.run_until_complete(coro)
82-
return responses[0]
118+
return RESPONSES.pop()
83119

84120

85121
class TestApplication(object):
@@ -94,3 +130,19 @@ def fetch(self, method, url, headers=None):
94130
def app():
95131
from _target_application import app
96132
return TestApplication(app)
133+
134+
@pytest.fixture(autouse=True)
135+
def capture_requests(monkeypatch):
136+
from sanic.response import BaseHTTPResponse
137+
original = BaseHTTPResponse.__init__
138+
139+
def capture(*args, **kwargs):
140+
original(*args, **kwargs)
141+
response = args[0]
142+
if getattr(response, "status", None) is None:
143+
response.status = 200
144+
145+
if response not in RESPONSES:
146+
RESPONSES.append(response)
147+
148+
monkeypatch.setattr(BaseHTTPResponse, "__init__", capture)

tests/framework_sanic/test_application.py

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,12 @@
2929
function_not_called)
3030

3131

32+
sanic_21 = int(sanic.__version__.split('.', 1)[0]) >= 21
33+
34+
3235
BASE_METRICS = [
3336
('Function/_target_application:index', 1),
34-
('Function/_target_application:request_middleware', 2),
37+
('Function/_target_application:request_middleware', 1 if int(sanic.__version__.split('.', 1)[0]) > 18 else 2),
3538
]
3639
FRAMEWORK_METRICS = [
3740
('Python/Framework/Sanic/%s' % sanic.__version__, 1),
@@ -108,8 +111,10 @@ def test_inbound_distributed_trace(app):
108111
response = app.fetch('get', '/', headers=dict(dt_headers))
109112
assert response.status == 200
110113

111-
112-
@pytest.mark.parametrize('endpoint', ('error', 'write_response_error'))
114+
_params = ["error"]
115+
if not sanic_21:
116+
_params.append('write_response_error')
117+
@pytest.mark.parametrize('endpoint', _params)
113118
def test_recorded_error(app, endpoint):
114119
ERROR_METRICS = [
115120
('Function/_target_application:%s' % endpoint, 1),
@@ -304,45 +309,56 @@ def _test():
304309
app.app.response_middleware = original_response_middleware
305310

306311

307-
ERROR_HANDLER_METRICS = [
308-
('Function/_target_application:handle_server_error', 1),
309-
]
312+
def error_middleware(*args, **kwargs):
313+
raise ValueError("1 != 0")
310314

311315

312-
@validate_transaction_metrics(
313-
'_target_application:handle_server_error',
314-
scoped_metrics=ERROR_HANDLER_METRICS,
315-
rollup_metrics=ERROR_HANDLER_METRICS,
316-
)
317-
@validate_base_transaction_event_attr
318-
@validate_transaction_errors(errors=['sanic.exceptions:ServerError'])
319-
def test_error_handler_transaction_naming(app):
316+
def test_errors_in_middleware(app):
317+
metrics = [('Function/test_application:error_middleware', 1)]
318+
319+
@validate_transaction_metrics(
320+
'test_application:error_middleware',
321+
scoped_metrics=metrics,
322+
rollup_metrics=metrics,
323+
)
324+
@validate_base_transaction_event_attr
325+
@validate_transaction_errors(errors=['builtins:ValueError'])
326+
def _test():
327+
response = app.fetch('get', '/')
328+
assert response.status == 500
329+
320330
original_request_middleware = deque(app.app.request_middleware)
321331
original_response_middleware = deque(app.app.response_middleware)
322-
app.app.request_middleware = []
323-
app.app.response_middleware = []
332+
app.app.register_middleware(error_middleware, "request")
324333

325334
try:
326-
response = app.fetch('get', '/server-error')
327-
assert response.status == 500
335+
_test()
328336
finally:
329337
app.app.request_middleware = original_request_middleware
330338
app.app.response_middleware = original_response_middleware
331339

332340

333-
@validate_transaction_metrics(
334-
'_target_application:CustomRouter.get'
335-
)
336341
def test_unknown_route(app):
337-
response = app.fetch('get', '/what-route')
338-
assert response.status == 404
339-
342+
import sanic
343+
sanic_version = [int(x) for x in sanic.__version__.split(".")]
344+
_tx_name = "_target_application:CustomRouter.get" if sanic_version[0] < 21 else "_target_application:request_middleware"
345+
346+
@validate_transaction_metrics(_tx_name)
347+
def _test():
348+
response = app.fetch('get', '/what-route')
349+
assert response.status == 404
350+
351+
_test()
340352

341-
@validate_transaction_metrics(
342-
'_target_application:CustomRouter.get'
343-
)
344-
@override_ignore_status_codes([405])
345-
@validate_transaction_errors(errors=[])
346353
def test_bad_method(app):
347-
response = app.fetch('post', '/')
348-
assert response.status == 405
354+
import sanic
355+
sanic_version = [int(x) for x in sanic.__version__.split(".")]
356+
_tx_name = "_target_application:CustomRouter.get" if sanic_version[0] < 21 else "_target_application:request_middleware"
357+
358+
@validate_transaction_metrics(_tx_name)
359+
@override_ignore_status_codes([405])
360+
@validate_transaction_errors(errors=[])
361+
def _test():
362+
response = app.fetch('post', '/')
363+
assert response.status == 405
364+
_test()

0 commit comments

Comments
 (0)