From 4be2ca742569652a4ff9638e8cb23dadb62ccb9a Mon Sep 17 00:00:00 2001 From: darius BERNARD Date: Fri, 3 Feb 2017 15:13:16 +0100 Subject: [PATCH] added all tests to start with a full coverage. --- dynamic_logging/admin.py | 14 +++++ dynamic_logging/apps.py | 2 +- dynamic_logging/handlers.py | 19 ++++++- dynamic_logging/migrations/0001_initial.py | 12 ++--- dynamic_logging/models.py | 11 ---- dynamic_logging/scheduler.py | 21 ++++++-- dynamic_logging/tests.py | 60 +++++++++++++--------- setup.cfg | 4 +- testproject/testapp/tests.py | 47 +++++++++++++++++ testproject/testapp/views.py | 4 ++ testproject/wsgi.py | 16 ------ 11 files changed, 144 insertions(+), 66 deletions(-) delete mode 100644 testproject/wsgi.py diff --git a/dynamic_logging/admin.py b/dynamic_logging/admin.py index 40a96af..6d31204 100644 --- a/dynamic_logging/admin.py +++ b/dynamic_logging/admin.py @@ -1 +1,15 @@ # -*- coding: utf-8 -*- + + +from django.contrib import admin +from .models import Config, Trigger +import dynamic_logging.views + +@admin.register(Config) +class ConfigAdmin(admin.ModelAdmin): + pass + + +@admin.register(Trigger) +class TriggerAdmin(admin.ModelAdmin): + pass diff --git a/dynamic_logging/apps.py b/dynamic_logging/apps.py index dec14e1..f1eae1c 100644 --- a/dynamic_logging/apps.py +++ b/dynamic_logging/apps.py @@ -18,7 +18,7 @@ def ready(self): from dynamic_logging.scheduler import main_scheduler try: main_scheduler.reload() - except OperationalError: + except OperationalError: # pragma: nocover pass # no trigger table exists atm. we don't care since there is no Trigger to pull. # setup signals for Trigger changes. it will reload the current trigger and next one from .signals import reload_timers_on_trigger_change diff --git a/dynamic_logging/handlers.py b/dynamic_logging/handlers.py index 4b7a301..3faa988 100644 --- a/dynamic_logging/handlers.py +++ b/dynamic_logging/handlers.py @@ -2,14 +2,29 @@ from __future__ import absolute_import, print_function, unicode_literals import logging +import threading from collections import defaultdict +from contextlib import contextmanager +from functools import partial logger = logging.getLogger(__name__) class MockHandler(logging.Handler): """Mock logging handler to check for expected logs.""" - messages = defaultdict(list) + + _messages_by_thread = defaultdict(list) + + @classmethod + @contextmanager + def capture(cls): + current = defaultdict(list) + cls._messages_by_thread[threading.current_thread()].append(current) + try: + yield current + finally: + cls._messages_by_thread[threading.current_thread()].remove(current) def emit(self, record): - self.messages[record.levelname.lower()].append(record.getMessage()) + for messages_list in self._messages_by_thread[threading.current_thread()]: + messages_list[record.levelname.lower()].append(record.getMessage()) diff --git a/dynamic_logging/migrations/0001_initial.py b/dynamic_logging/migrations/0001_initial.py index 39c8acd..95b9b5c 100644 --- a/dynamic_logging/migrations/0001_initial.py +++ b/dynamic_logging/migrations/0001_initial.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -import django.utils.timezone from django.db import migrations, models - import dynamic_logging.models +import django.utils.timezone class Migration(migrations.Migration): @@ -16,7 +15,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Config', fields=[ - ('id', models.AutoField(verbose_name='ID', auto_created=True, primary_key=True, serialize=False)), + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(max_length=255)), ('config_json', models.TextField()), ], @@ -24,10 +23,11 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Trigger', fields=[ - ('id', models.AutoField(verbose_name='ID', auto_created=True, primary_key=True, serialize=False)), + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(max_length=255)), - ('start_date', models.DateTimeField(null=True, default=django.utils.timezone.now)), - ('end_date', models.DateTimeField(null=True, default=dynamic_logging.models.now_plus_2hours)), + ('is_active', models.BooleanField(default=True)), + ('start_date', models.DateTimeField(default=django.utils.timezone.now, null=True)), + ('end_date', models.DateTimeField(default=dynamic_logging.models.now_plus_2hours, null=True)), ('config', models.ForeignKey(to='dynamic_logging.Config', related_name='triggers')), ], options={ diff --git a/dynamic_logging/models.py b/dynamic_logging/models.py index b009bc2..e9ca65b 100644 --- a/dynamic_logging/models.py +++ b/dynamic_logging/models.py @@ -63,17 +63,6 @@ def default(cls): def __str__(self): return 'trigger %s from %s to %s for config %s' % (self.name, self.start_date, self.end_date, self.config.name) - def is_active(self, date=None): - """ - return True if the current trigger is active at the given date. if no date is given, current date is used - :param date: the date to check or None for now() - :return: - :rtype: bool - """ - date = date or timezone.now() - start_date, end_date = self.start_date, self.end_date - return (start_date <= date or end_date is None) and (end_date >= date or end_date is None) - def apply(self): self.config.apply(self) diff --git a/dynamic_logging/scheduler.py b/dynamic_logging/scheduler.py index 88333e7..7bda6bf 100644 --- a/dynamic_logging/scheduler.py +++ b/dynamic_logging/scheduler.py @@ -115,7 +115,7 @@ def get_next_wake(current=None, after=None): def set_next_wake(self, trigger, at): logger.debug("next trigger to enable : %s at %s", trigger, at, extra={'next_date': at}) with self._lock: - self.reset() + self.reset_timer() interval = (at - timezone.now()).total_seconds() self.next_timer = threading.Timer(interval, functools.partial(self.wake, trigger=trigger, date=at)) @@ -127,11 +127,23 @@ def set_next_wake(self, trigger, at): self.next_timer.start() def reset(self): + """ + reset the logging to the default settings. disable the timer to change it + :return: + """ + with self._lock: + self.reset_timer() + self.current_trigger = Trigger.default() + + def reset_timer(self): + """ + reset the timer + :return: + """ with self._lock: if self.next_timer is not None: self.next_timer.cancel() self.next_timer = None - self.current_trigger = Trigger.default() def activate_current(self): """ @@ -154,7 +166,7 @@ def reload(self): """ if self._enabled: with self._lock: - self.reset() + self.reset_timer() current = self.activate_current() trigger, at = self.get_next_wake(current=current) if at: @@ -178,7 +190,8 @@ def wake(self, trigger, date): # get the next trigger valid at the current expected date # we don't use timezone.now() to prevent the case where threading.Timer wakeup some ms befor the expected # date - self.set_next_wake(next_trigger, at) + if at: + self.set_next_wake(next_trigger, at) def apply(self, trigger): if self.current_trigger != trigger: diff --git a/dynamic_logging/tests.py b/dynamic_logging/tests.py index 426c00c..94c792a 100644 --- a/dynamic_logging/tests.py +++ b/dynamic_logging/tests.py @@ -31,6 +31,7 @@ def get_tz_date(dmy): class SchedulerTest(TestCase): def setUp(self): main_scheduler.disable() + assert not main_scheduler.is_enabled() c = Config.objects.create(name='nothing logged') dates = [ # # # | jan | fevrier @@ -182,15 +183,22 @@ def test_auto_reload_on_trigger_changes(self): t.delete() self.assertIsNone(main_scheduler.next_timer) + def test_wake(self): + now_plus_2h = timezone.now() + datetime.timedelta(hours=2) + now_plus_4h = timezone.now() + datetime.timedelta(hours=4) + # no trigger in fixtures + self.assertIsNone(main_scheduler.next_timer) + t = Trigger.objects.create(name='fake', config=self.config, start_date=now_plus_2h, end_date=now_plus_4h) + + self.assertEqual(main_scheduler.current_trigger, Trigger.default()) + main_scheduler.wake(t, now_plus_2h) + self.assertEqual(main_scheduler.current_trigger, t) -class ConfigApplyTest(TestCase): - def setUp(self): - MockHandler.messages.clear() +class ConfigApplyTest(TestCase): def tearDown(self): # reset the default config Config.default().apply() - MockHandler.messages.clear() def test_default_config_by_default(self): loggers = get_loggers() @@ -242,11 +250,12 @@ def test_level_config(self): self.assertEqual(logger.handlers, []) def test_messages_passed(self): - self.assertEqual(MockHandler.messages['debug'], []) - logger = logging.getLogger('testproject.testapp') - logger.debug("couocu") - # handler not attached to this logger - self.assertEqual(MockHandler.messages['debug'], []) + with MockHandler.capture() as messages: + self.assertEqual(messages['debug'], []) + logger = logging.getLogger('testproject.testapp') + logger.debug("couocu") + # handler not attached to this logger + self.assertEqual(messages['debug'], []) # setup new config cfg = Config(name='empty') cfg.config = {"loggers": { @@ -262,11 +271,12 @@ def test_messages_passed(self): cfg.apply() # log debug ineficient logger.debug("couocu") - self.assertEqual(MockHandler.messages['debug'], []) - self.assertEqual(MockHandler.messages['warning'], []) - logger.warn("hey") - self.assertEqual(MockHandler.messages['debug'], []) - self.assertEqual(MockHandler.messages['warning'], ['hey']) + with MockHandler.capture() as messages: + self.assertEqual(messages['debug'], []) + self.assertEqual(messages['warning'], []) + logger.warn("hey") + self.assertEqual(messages['debug'], []) + self.assertEqual(messages['warning'], ['hey']) def test_config_reversed(self): logger = logging.getLogger('testproject.testapp') @@ -285,14 +295,14 @@ def test_config_reversed(self): }} cfg.apply() # log debug ineficient - self.assertEqual(MockHandler.messages['warning'], []) - - logger.warn("hey") - self.assertEqual(MockHandler.messages['warning'], ['hey']) - logger.warn("hey") - self.assertEqual(MockHandler.messages['warning'], ['hey', 'hey']) - - # default config does not add to mockhandler - Config.default().apply() - logger.warn("hey") - self.assertEqual(MockHandler.messages['warning'], ['hey', 'hey']) + with MockHandler.capture() as messages: + logger.warn("hey") + self.assertEqual(messages['warning'], ['hey']) + logger.warn("hey") + self.assertEqual(messages['warning'], ['hey', 'hey']) + + with MockHandler.capture() as messages: + # default config does not add to mockhandler + Config.default().apply() + logger.warn("hey") + self.assertEqual(messages['warning'], []) diff --git a/setup.cfg b/setup.cfg index 22d3956..7beb3e4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,9 @@ description-file = README.rst universal = 1 [coverage:run] -omit = docs +include = + dynamic_logging + testproject [isort] line_length=119 diff --git a/testproject/testapp/tests.py b/testproject/testapp/tests.py index fb38d61..c30b812 100644 --- a/testproject/testapp/tests.py +++ b/testproject/testapp/tests.py @@ -1,10 +1,15 @@ +from django.contrib.auth import get_user_model from django.test import TestCase # Create your tests here. +from dynamic_logging.handlers import MockHandler +from dynamic_logging.models import Config class TestPages(TestCase): + def setUp(self): + pass def test_200(self): res = self.client.get('/testapp/ok/') @@ -13,3 +18,45 @@ def test_200(self): def test_401(self): res = self.client.get('/testapp/error401/') self.assertEqual(res.status_code, 401) + + def test_raise(self): + self.assertRaises(Exception, self.client.get, '/testapp/error500/') + + def test_log_no_cfg(self): + with MockHandler.capture() as messages: + res = self.client.get('/testapp/log/DEBUG/testproject.testapp/') + self.assertEqual(res.status_code, 200) + self.assertEqual(messages['debug'], []) + + def test_log_with_cfg(self): + cfg = Config(name='mocklog') + cfg.config = {"loggers": { + "testproject.testapp": { + "handlers": ["mock"], + "level": "WARN", + } + }} + cfg.apply() + with MockHandler.capture() as messages: + res = self.client.get('/testapp/log/DEBUG/testproject.testapp/') + self.assertEqual(res.status_code, 200) + self.assertEqual(messages['debug'], []) + + def test_log_bad_level(self): + self.assertRaises(Exception, self.client.get, '/testapp/log/OOPS/testproject.testapp/') + + +class TestAdminContent(TestCase): + + def setUp(self): + super(TestAdminContent, self).setUp() + u = get_user_model().objects.create(username='admin', is_staff=True, is_superuser=True) + """:type: django.contrib.auth.models.User""" + u.set_password('password') + u.save() + self.client.login(username='admin', password='password') + + def test_logging_in_admin(self): + response = self.client.get('/admin/') + self.assertContains(response, 'Trigger') + diff --git a/testproject/testapp/views.py b/testproject/testapp/views.py index f36e84c..be3b9ab 100644 --- a/testproject/testapp/views.py +++ b/testproject/testapp/views.py @@ -1,10 +1,13 @@ import logging +from django.forms.forms import Form from django.http.response import HttpResponse from django.shortcuts import render # Create your views here. +from django.views.generic.edit import FormView + def error401(request): return HttpResponse(status=401) @@ -26,3 +29,4 @@ def log_somthing(request, level='debug', loggername=__name__): logger = logging.getLogger(loggername) logger.log(level, "message from view", extra={'level': level, 'loggername': loggername}) return HttpResponse("ok. logged to %s" % loggername) + diff --git a/testproject/wsgi.py b/testproject/wsgi.py deleted file mode 100644 index de2f4fc..0000000 --- a/testproject/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for testproject project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings") - -application = get_wsgi_application()