import json import logging import sys from django.conf import settings from nose.exc import SkipTest from nose import tools as nose_tools 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 import dictconfig from django.utils import unittest import mock from nose.tools import eq_ from django_statsd.clients import get_client from django_statsd.patches import utils 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): assert self.client.get(self.url).status_code == 400 def test_no_valid_client(self): assert self.client.get(self.url, {'client': 'no'}).status_code == 400 def test_boomerang_almost(self): assert self.client.get(self.url, {'client': 'boomerang'}).status_code == 400 def test_boomerang_minimum(self): assert self.client.get(self.url, {'client': 'boomerang', 'nt_nav_st': 1}).content == 'recorded' @mock.patch('django_statsd.views.process_key') def test_boomerang_something(self, process_key): assert self.client.get(self.url, self.good).content == 'recorded' assert process_key.called def test_boomerang_post(self): assert self.client.post(self.url, self.good).status_code == 405 def test_good_guard(self): settings.STATSD_RECORD_GUARD = lambda r: None assert self.client.get(self.url, self.good).status_code == 200 def test_bad_guard(self): settings.STATSD_RECORD_GUARD = lambda r: HttpResponseForbidden() assert self.client.get(self.url, self.good).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'] = '' 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'] = '' 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'] = '' assert self.client.post(self.url, data).status_code == 400 @mock.patch.object(middleware.statsd, 'incr') class TestErrorLog(TestCase): def setUp(self): dictconfig.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}))