debian-django-statsd-mozilla/tests/test_django_statsd.py

547 lines
20 KiB
Python

import json
import logging.config
import sys
import unittest
from django.conf import settings
from nose.exc import SkipTest
from nose import tools as nose_tools
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseForbidden
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils.http import urlencode
import mock
from nose.tools import eq_
from django_statsd.clients import get_client, statsd
from django_statsd.patches import utils
from django_statsd.patches.db import (
patched_callproc,
patched_execute,
patched_executemany,
)
from django_statsd import middleware
cfg = {
'version': 1,
'formatters': {},
'handlers': {
'test_statsd_handler': {
'class': 'django_statsd.loggers.errors.StatsdHandler',
},
},
'loggers': {
'test.logging': {
'handlers': ['test_statsd_handler'],
},
},
}
@mock.patch.object(middleware.statsd, 'incr')
class TestIncr(TestCase):
def setUp(self):
self.req = RequestFactory().get('/')
self.res = HttpResponse()
def test_graphite_response(self, incr):
gmw = middleware.GraphiteMiddleware()
gmw.process_response(self.req, self.res)
assert incr.called
def test_graphite_response_authenticated(self, incr):
self.req.user = mock.Mock()
self.req.user.is_authenticated.return_value = True
gmw = middleware.GraphiteMiddleware()
gmw.process_response(self.req, self.res)
eq_(incr.call_count, 2)
def test_graphite_exception(self, incr):
gmw = middleware.GraphiteMiddleware()
gmw.process_exception(self.req, None)
assert incr.called
def test_graphite_exception_authenticated(self, incr):
self.req.user = mock.Mock()
self.req.user.is_authenticated.return_value = True
gmw = middleware.GraphiteMiddleware()
gmw.process_exception(self.req, None)
eq_(incr.call_count, 2)
@mock.patch.object(middleware.statsd, 'timing')
class TestTiming(unittest.TestCase):
def setUp(self):
self.req = RequestFactory().get('/')
self.res = HttpResponse()
def test_request_timing(self, timing):
func = lambda x: x
gmw = middleware.GraphiteRequestTimingMiddleware()
gmw.process_view(self.req, func, tuple(), dict())
gmw.process_response(self.req, self.res)
eq_(timing.call_count, 3)
names = ['view.%s.%s.GET' % (func.__module__, func.__name__),
'view.%s.GET' % func.__module__,
'view.GET']
for expected, (args, kwargs) in zip(names, timing.call_args_list):
eq_(expected, args[0])
def test_request_timing_exception(self, timing):
func = lambda x: x
gmw = middleware.GraphiteRequestTimingMiddleware()
gmw.process_view(self.req, func, tuple(), dict())
gmw.process_exception(self.req, self.res)
eq_(timing.call_count, 3)
names = ['view.%s.%s.GET' % (func.__module__, func.__name__),
'view.%s.GET' % func.__module__,
'view.GET']
for expected, (args, kwargs) in zip(names, timing.call_args_list):
eq_(expected, args[0])
def test_request_timing_tastypie(self, timing):
func = lambda x: x
gmw = middleware.TastyPieRequestTimingMiddleware()
gmw.process_view(self.req, func, tuple(), {
'api_name': 'my_api_name',
'resource_name': 'my_resource_name'
})
gmw.process_response(self.req, self.res)
eq_(timing.call_count, 3)
names = ['view.my_api_name.my_resource_name.GET',
'view.my_api_name.GET',
'view.GET']
for expected, (args, kwargs) in zip(names, timing.call_args_list):
eq_(expected, args[0])
def test_request_timing_tastypie_fallback(self, timing):
func = lambda x: x
gmw = middleware.TastyPieRequestTimingMiddleware()
gmw.process_view(self.req, func, tuple(), dict())
gmw.process_response(self.req, self.res)
eq_(timing.call_count, 3)
names = ['view.%s.%s.GET' % (func.__module__, func.__name__),
'view.%s.GET' % func.__module__,
'view.GET']
for expected, (args, kwargs) in zip(names, timing.call_args_list):
eq_(expected, args[0])
class TestClient(unittest.TestCase):
@mock.patch.object(settings, 'STATSD_CLIENT', 'statsd.client')
def test_normal(self):
eq_(get_client().__module__, 'statsd.client')
@mock.patch.object(settings, 'STATSD_CLIENT',
'django_statsd.clients.null')
def test_null(self):
eq_(get_client().__module__, 'django_statsd.clients.null')
@mock.patch.object(settings, 'STATSD_CLIENT',
'django_statsd.clients.toolbar')
def test_toolbar(self):
eq_(get_client().__module__, 'django_statsd.clients.toolbar')
@mock.patch.object(settings, 'STATSD_CLIENT',
'django_statsd.clients.toolbar')
def test_toolbar_send(self):
client = get_client()
eq_(client.cache, {})
client.incr('testing')
eq_(client.cache, {'testing|count': [[1, 1]]})
class TestMetlogClient(TestCase):
def check_metlog(self):
try:
from metlog.config import client_from_dict_config
return client_from_dict_config
except ImportError:
raise SkipTest("Metlog is not installed")
@nose_tools.raises(AttributeError)
def test_no_metlog(self):
with self.settings(STATSD_PREFIX='moz_metlog',
STATSD_CLIENT='django_statsd.clients.moz_metlog'):
get_client()
def _create_client(self):
client_from_dict_config = self.check_metlog()
# Need to load within the test in case metlog is not installed
from metlog.config import client_from_dict_config
METLOG_CONF = {
'logger': 'django-statsd',
'sender': {
'class': 'metlog.senders.DebugCaptureSender',
},
}
return client_from_dict_config(METLOG_CONF)
def test_get_client(self):
metlog = self._create_client()
with self.settings(METLOG=metlog,
STATSD_PREFIX='moz_metlog',
STATSD_CLIENT='django_statsd.clients.moz_metlog'):
client = get_client()
eq_(client.__module__, 'django_statsd.clients.moz_metlog')
def test_metlog_incr(self):
metlog = self._create_client()
with self.settings(METLOG=metlog,
STATSD_PREFIX='moz_metlog',
STATSD_CLIENT='django_statsd.clients.moz_metlog'):
client = get_client()
eq_(len(client.metlog.sender.msgs), 0)
client.incr('testing')
eq_(len(client.metlog.sender.msgs), 1)
msg = json.loads(client.metlog.sender.msgs[0])
eq_(msg['severity'], 6)
eq_(msg['payload'], '1')
eq_(msg['fields']['rate'], 1)
eq_(msg['fields']['name'], 'moz_metlog.testing')
eq_(msg['type'], 'counter')
def test_metlog_decr(self):
metlog = self._create_client()
with self.settings(METLOG=metlog,
STATSD_PREFIX='moz_metlog',
STATSD_CLIENT='django_statsd.clients.moz_metlog'):
client = get_client()
eq_(len(client.metlog.sender.msgs), 0)
client.decr('testing')
eq_(len(client.metlog.sender.msgs), 1)
msg = json.loads(client.metlog.sender.msgs[0])
eq_(msg['severity'], 6)
eq_(msg['payload'], '-1')
eq_(msg['fields']['rate'], 1)
eq_(msg['fields']['name'], 'moz_metlog.testing')
eq_(msg['type'], 'counter')
def test_metlog_timing(self):
metlog = self._create_client()
with self.settings(METLOG=metlog,
STATSD_PREFIX='moz_metlog',
STATSD_CLIENT='django_statsd.clients.moz_metlog'):
client = get_client()
eq_(len(client.metlog.sender.msgs), 0)
client.timing('testing', 512, rate=2)
eq_(len(client.metlog.sender.msgs), 1)
msg = json.loads(client.metlog.sender.msgs[0])
eq_(msg['severity'], 6)
eq_(msg['payload'], '512')
eq_(msg['fields']['rate'], 2)
eq_(msg['fields']['name'], 'moz_metlog.testing')
eq_(msg['type'], 'timer')
@nose_tools.raises(AttributeError)
def test_metlog_no_prefixes(self):
metlog = self._create_client()
with self.settings(METLOG=metlog,
STATSD_CLIENT='django_statsd.clients.moz_metlog'):
client = get_client()
client.incr('foo', 2)
def test_metlog_prefixes(self):
metlog = self._create_client()
with self.settings(METLOG=metlog,
STATSD_PREFIX='some_prefix',
STATSD_CLIENT='django_statsd.clients.moz_metlog'):
client = get_client()
eq_(len(client.metlog.sender.msgs), 0)
client.timing('testing', 512, rate=2)
client.incr('foo', 2)
client.decr('bar', 5)
eq_(len(client.metlog.sender.msgs), 3)
msg = json.loads(client.metlog.sender.msgs[0])
eq_(msg['severity'], 6)
eq_(msg['payload'], '512')
eq_(msg['fields']['rate'], 2)
eq_(msg['fields']['name'], 'some_prefix.testing')
eq_(msg['type'], 'timer')
msg = json.loads(client.metlog.sender.msgs[1])
eq_(msg['severity'], 6)
eq_(msg['payload'], '2')
eq_(msg['fields']['rate'], 1)
eq_(msg['fields']['name'], 'some_prefix.foo')
eq_(msg['type'], 'counter')
msg = json.loads(client.metlog.sender.msgs[2])
eq_(msg['severity'], 6)
eq_(msg['payload'], '-5')
eq_(msg['fields']['rate'], 1)
eq_(msg['fields']['name'], 'some_prefix.bar')
eq_(msg['type'], 'counter')
# This is primarily for Zamboni, which loads in the custom middleware
# classes, one of which, breaks posts to our url. Let's stop that.
@mock.patch.object(settings, 'MIDDLEWARE_CLASSES', [])
class TestRecord(TestCase):
urls = 'django_statsd.urls'
def setUp(self):
super(TestRecord, self).setUp()
self.url = reverse('django_statsd.record')
settings.STATSD_RECORD_GUARD = None
self.good = {'client': 'boomerang', 'nt_nav_st': 1,
'nt_domcomp': 3}
self.stick = {'client': 'stick',
'window.performance.timing.domComplete': 123,
'window.performance.timing.domInteractive': 456,
'window.performance.timing.domLoading': 789,
'window.performance.timing.navigationStart': 0,
'window.performance.navigation.redirectCount': 3,
'window.performance.navigation.type': 1}
def test_no_client(self):
response = self.client.get(self.url)
assert response.status_code == 400
def test_no_valid_client(self):
response = self.client.get(self.url, {'client': 'no'})
assert response.status_code == 400
def test_boomerang_almost(self):
response = self.client.get(self.url, {'client': 'boomerang'})
assert response.status_code == 400
def test_boomerang_minimum(self):
content = self.client.get(
self.url, {
'client': 'boomerang',
'nt_nav_st': 1,
}).content.decode()
assert content == 'recorded'
@mock.patch('django_statsd.views.process_key')
def test_boomerang_something(self, process_key):
content = self.client.get(self.url, self.good).content.decode()
assert content == 'recorded'
assert process_key.called
def test_boomerang_post(self):
assert self.client.post(self.url + '?' + urlencode(self.good), self.good).status_code == 405
def test_good_guard(self):
settings.STATSD_RECORD_GUARD = lambda r: None
response = self.client.get(self.url, self.good)
assert response.status_code == 200
def test_bad_guard(self):
settings.STATSD_RECORD_GUARD = lambda r: HttpResponseForbidden()
response = self.client.get(self.url, self.good)
assert response.status_code == 403
def test_stick_get(self):
assert self.client.get(self.url, self.stick).status_code == 405
@mock.patch('django_statsd.views.process_key')
def test_stick(self, process_key):
assert self.client.post(self.url, self.stick).status_code == 200
assert process_key.called
def test_stick_start(self):
data = self.stick.copy()
del data['window.performance.timing.navigationStart']
assert self.client.post(self.url, data).status_code == 400
@mock.patch('django_statsd.views.process_key')
def test_stick_missing(self, process_key):
data = self.stick.copy()
del data['window.performance.timing.domInteractive']
assert self.client.post(self.url, data).status_code == 200
assert process_key.called
def test_stick_garbage(self):
data = self.stick.copy()
data['window.performance.timing.domInteractive'] = '<alert>'
assert self.client.post(self.url, data).status_code == 400
def test_stick_some_garbage(self):
data = self.stick.copy()
data['window.performance.navigation.redirectCount'] = '<alert>'
assert self.client.post(self.url, data).status_code == 400
def test_stick_more_garbage(self):
data = self.stick.copy()
data['window.performance.navigation.type'] = '<alert>'
assert self.client.post(self.url, data).status_code == 400
@mock.patch.object(middleware.statsd, 'incr')
class TestErrorLog(TestCase):
def setUp(self):
logging.config.dictConfig(cfg)
self.log = logging.getLogger('test.logging')
def division_error(self):
try:
1 / 0
except:
return sys.exc_info()
def test_emit(self, incr):
self.log.error('blargh!', exc_info=self.division_error())
assert incr.call_args[0][0] == 'error.zerodivisionerror'
def test_not_emit(self, incr):
self.log.error('blargh!')
assert not incr.called
class TestPatchMethod(TestCase):
def setUp(self):
super(TestPatchMethod, self).setUp()
class DummyClass(object):
def sumargs(self, a, b, c=3, d=4):
return a + b + c + d
def badfn(self, a, b=2):
raise ValueError
self.cls = DummyClass
def test_late_patching(self):
"""
Objects created before patching should get patched as well.
"""
def patch_fn(original_fn, self, *args, **kwargs):
return original_fn(self, *args, **kwargs) + 10
obj = self.cls()
self.assertEqual(obj.sumargs(1, 2, 3, 4), 10)
utils.patch_method(self.cls, 'sumargs')(patch_fn)
self.assertEqual(obj.sumargs(1, 2, 3, 4), 20)
def test_doesnt_call_original_implicitly(self):
"""
Original fn must be called explicitly from patched to be
executed.
"""
def patch_fn(original_fn, self, *args, **kwargs):
return 10
with self.assertRaises(ValueError):
obj = self.cls()
obj.badfn(1, 2)
utils.patch_method(self.cls, 'badfn')(patch_fn)
self.assertEqual(obj.badfn(1, 2), 10)
def test_args_kwargs_are_honored(self):
"""
Args and kwargs must be honored between calls from the patched to
the original version.
"""
def patch_fn(original_fn, self, *args, **kwargs):
return original_fn(self, *args, **kwargs)
utils.patch_method(self.cls, 'sumargs')(patch_fn)
obj = self.cls()
self.assertEqual(obj.sumargs(1, 2), 10)
self.assertEqual(obj.sumargs(1, 1, d=1), 6)
self.assertEqual(obj.sumargs(1, 1, 1, 1), 4)
def test_patched_fn_can_receive_arbitrary_arguments(self):
"""
Args and kwargs can be received arbitrarily with no contraints on
the patched fn, even if the original_fn had a fixed set of
allowed args and kwargs.
"""
def patch_fn(original_fn, self, *args, **kwargs):
return args, kwargs
utils.patch_method(self.cls, 'badfn')(patch_fn)
obj = self.cls()
self.assertEqual(obj.badfn(1, d=2), ((1,), {'d': 2}))
self.assertEqual(obj.badfn(1, d=2), ((1,), {'d': 2}))
self.assertEqual(obj.badfn(1, 2, c=1, d=2), ((1, 2), {'c': 1, 'd': 2}))
class TestCursorWrapperPatching(TestCase):
example_queries = {
'select': 'select * from something;',
'insert': 'insert (1, 2) into something;',
'update': 'update something set a=1;',
}
def test_patched_callproc_calls_timer(self):
for operation, query in list(self.example_queries.items()):
with mock.patch.object(statsd, 'timer') as timer:
client = mock.Mock(executable_name='client_executable_name')
db = mock.Mock(executable_name='name', alias='alias', client=client)
instance = mock.Mock(db=db)
patched_callproc(lambda *args, **kwargs: None, instance, query)
self.assertEqual(timer.call_count, 1)
self.assertEqual(timer.call_args[0][0], 'db.client_executable_name.alias.callproc.%s' % operation)
def test_patched_execute_calls_timer(self):
for operation, query in list(self.example_queries.items()):
with mock.patch.object(statsd, 'timer') as timer:
client = mock.Mock(executable_name='client_executable_name')
db = mock.Mock(executable_name='name', alias='alias', client=client)
instance = mock.Mock(db=db)
patched_execute(lambda *args, **kwargs: None, instance, query)
self.assertEqual(timer.call_count, 1)
self.assertEqual(timer.call_args[0][0], 'db.client_executable_name.alias.execute.%s' % operation)
def test_patched_executemany_calls_timer(self):
for operation, query in list(self.example_queries.items()):
with mock.patch.object(statsd, 'timer') as timer:
client = mock.Mock(executable_name='client_executable_name')
db = mock.Mock(executable_name='name', alias='alias', client=client)
instance = mock.Mock(db=db)
patched_executemany(lambda *args, **kwargs: None, instance, query)
self.assertEqual(timer.call_count, 1)
self.assertEqual(timer.call_args[0][0], 'db.client_executable_name.alias.executemany.%s' % operation)
@mock.patch('django_statsd.patches.db.patched_callproc')
@mock.patch('django_statsd.patches.db.patched_executemany')
@mock.patch('django_statsd.patches.db.patched_execute')
@mock.patch('django.db.backends.utils.CursorWrapper')
def test_cursorwrapper_patching(self, CursorWrapper, execute, executemany, callproc):
from django_statsd.patches.db import patch
execute.__name__ = 'execute'
executemany.__name__ = 'executemany'
callproc.__name__ = 'callproc'
execute.return_value = 'execute'
executemany.return_value = 'executemany'
callproc.return_value = 'callproc'
patch()
self.assertEqual(CursorWrapper.execute(), 'execute')
self.assertEqual(CursorWrapper.executemany(), 'executemany')
self.assertEqual(CursorWrapper.callproc(), 'callproc')