Browse Source

Various tests and tweaks for Rules

David Cramer 10 years ago
parent
commit
c811bab889

+ 1 - 0
conftest.py

@@ -65,6 +65,7 @@ def pytest_configure(config):
     # Need a predictable key for tests that involve checking signatures
     settings.SENTRY_KEY = 'abc123'
     settings.SENTRY_PUBLIC = False
+
     # This speeds up the tests considerably, pbkdf2 is by design, slow.
     settings.PASSWORD_HASHERS = [
         'django.contrib.auth.hashers.MD5PasswordHasher',

+ 5 - 0
src/sentry/models/rule.py

@@ -9,6 +9,7 @@ from django.db import models
 from django.utils import timezone
 
 from sentry.db.models import Model, GzippedDictField, sane_repr
+from sentry.db.models.manager import BaseManager
 
 
 class Rule(Model):
@@ -17,6 +18,10 @@ class Rule(Model):
     data = GzippedDictField()
     date_added = models.DateTimeField(default=timezone.now)
 
+    objects = BaseManager(cache_fields=(
+        'pk',
+    ))
+
     class Meta:
         db_table = 'sentry_rule'
         app_label = 'sentry'

+ 1 - 0
src/sentry/plugins/bases/notify.py

@@ -89,6 +89,7 @@ class NotificationPlugin(Plugin):
         if not send_to:
             return False
 
+        # TODO(dcramer): remove this in favor of rules
         allowed_tags = project.get_option('notifcation:tags', {})
         if allowed_tags:
             tags = event.data.get('tags', ())

+ 1 - 1
src/sentry/rules/actions/base.py

@@ -14,5 +14,5 @@ from sentry.rules.base import RuleBase
 class EventAction(RuleBase):
     rule_type = 'action/event'
 
-    def after(self, event, is_new, is_regression, **kwargs):
+    def after(self, event, is_new, is_regression, is_sample, **kwargs):
         pass

+ 5 - 9
src/sentry/rules/actions/notify_event.py

@@ -8,19 +8,15 @@ sentry.rules.actions.notify_event
 
 from __future__ import absolute_import
 
+from sentry.plugins import plugins
 from sentry.rules.actions.base import EventAction
+from sentry.utils.safe import safe_execute
 
 
 class NotifyEventAction(EventAction):
     label = 'Send a notification'
 
-    def notify(self, event):
-        # TODO: fire off plugin notifications
-        pass
-
     def after(self, event, **kwargs):
-        if self.should_notify(event):
-            self.notify(event)
-
-    def passes(self, event, **kwargs):
-        raise NotImplementedError
+        for plugin in plugins.for_project(event.project):
+            if hasattr(plugin, 'notify_users'):
+                safe_execute(plugin.notify_users, group=event.group, event=event)

+ 1 - 1
src/sentry/rules/conditions/base.py

@@ -12,5 +12,5 @@ from sentry.rules.base import RuleBase
 class EventCondition(RuleBase):
     rule_type = 'condition/event'
 
-    def passes(self, event, is_new, is_regression, **kwargs):
+    def passes(self, event, is_new, is_regression, is_sample, **kwargs):
         raise NotImplementedError

+ 5 - 0
src/sentry/rules/registry.py

@@ -14,6 +14,7 @@ from collections import defaultdict
 class RuleRegistry(object):
     def __init__(self):
         self._rules = defaultdict(list)
+        self._map = {}
 
     def __iter__(self):
         for rule_type, rule_list in self._rules.iteritems():
@@ -21,4 +22,8 @@ class RuleRegistry(object):
                 yield rule_type, rule
 
     def add(self, rule):
+        self._map[rule.id] = rule
         self._rules[rule.rule_type].append(rule)
+
+    def get(self, rule_id):
+        return self._map.get(rule_id)

+ 82 - 5
src/sentry/tasks/post_process.py

@@ -8,24 +8,54 @@ sentry.tasks.post_process
 
 from __future__ import absolute_import
 
-from hashlib import md5
+import logging
 
 from django.conf import settings
+from hashlib import md5
+
 from sentry.plugins import plugins
+from sentry.rules import rules
 from sentry.tasks.base import instrumented_task
 from sentry.utils.safe import safe_execute
 
 
+rules_logger = logging.getLogger('sentry.errors.rules')
+
+
+def condition_matches(project, condition, **kwargs):
+    condition_cls = rules.get(condition['id'])
+    if condition_cls is None:
+        rules_logger.error('Unregistered condition %r', condition['id'])
+        return
+
+    condition_inst = condition_cls(project)
+    return safe_execute(condition_inst.passes, **kwargs)
+
+
+# TODO(dcramer): cache this
+def get_rules(project):
+    from sentry.models import Rule
+
+    return list(Rule.objects.filter(project=project))
+
+
 @instrumented_task(
     name='sentry.tasks.post_process.post_process_group',
     queue='triggers')
-def post_process_group(group, event, **kwargs):
+def post_process_group(group, event, is_new, is_regression, is_sample, **kwargs):
     """
     Fires post processing hooks for a group.
     """
-    for plugin in plugins.for_project(group.project):
-        plugin_post_process_group.delay(
-            plugin.slug, group=group, event=event, **kwargs)
+    from sentry.models import Project
+
+    project = Project.objects.get_from_cache(id=group.project_id)
+
+    child_kwargs = {
+        'event': event,
+        'is_new': is_new,
+        'is_regression': is_regression,
+        'is_sample': is_sample,
+    }
 
     if settings.SENTRY_ENABLE_EXPLORE_CODE:
         record_affected_code.delay(group=group, event=event)
@@ -33,6 +63,53 @@ def post_process_group(group, event, **kwargs):
     if settings.SENTRY_ENABLE_EXPLORE_USERS:
         record_affected_user.delay(group=group, event=event)
 
+    for plugin in plugins.for_project(project):
+        plugin_post_process_group.delay(
+            plugin.slug, group=group, **child_kwargs)
+
+    for rule in get_rules(project):
+        match = rule.data.get('action_match', 'all')
+        condition_list = rule.data.get('conditions', ())
+        if not condition_list:
+            pass
+        elif match == 'all':
+            if not all(condition_matches(project, c, **child_kwargs) for c in condition_list):
+                continue
+        elif match == 'any':
+            if not any(condition_matches(project, c, **child_kwargs) for c in condition_list):
+                continue
+        else:
+            rules_logger.error('Unsupported action_match %r for rule %d',
+                               match, rule.id)
+            continue
+
+        execute_rule.delay(
+            rule_id=rule.id,
+            **child_kwargs
+        )
+
+
+@instrumented_task(
+    name='sentry.tasks.post_process.execute_rule',
+    queue='triggers')
+def execute_rule(rule_id, event, **kwargs):
+    """
+    Fires post processing hooks for a rule.
+    """
+    from sentry.models import Project, Rule
+
+    rule = Rule.objects.get(id=rule_id)
+    project = Project.objects.get_from_cache(id=event.project_id)
+
+    for action in rule.data.get('actions', ()):
+        action_cls = rules.get(action['id'])
+        if action_cls is None:
+            rules_logger.error('Unregistered action %r', action['id'])
+            continue
+
+        action_inst = action_cls(project)
+        safe_execute(action_inst.after, event=event, **kwargs)
+
 
 @instrumented_task(
     name='sentry.tasks.post_process.plugin_post_process_group',

+ 6 - 6
src/sentry/testutils/fixtures.py

@@ -7,13 +7,11 @@ sentry.testutils.fixtures
 """
 from __future__ import unicode_literals
 
-from uuid import uuid4
-
-from exam import fixture
+import six
 
 from django.utils.text import slugify
-
-import six
+from exam import fixture
+from uuid import uuid4
 
 from sentry.models import Activity, Event, Group, Project, Team, User
 from sentry.utils.compat import pickle
@@ -99,11 +97,13 @@ class Fixtures(object):
 
         return user
 
-    def create_event(self, event_id, **kwargs):
+    def create_event(self, event_id=None, **kwargs):
         if 'group' not in kwargs:
             kwargs['group'] = self.group
         kwargs.setdefault('project', kwargs['group'].project)
         kwargs.setdefault('message', kwargs['group'].message)
+        if event_id is None:
+            event_id = uuid4().hex
         kwargs.setdefault('data', LEGACY_DATA.copy())
         if kwargs.get('tags'):
             tags = kwargs.pop('tags')

+ 144 - 2
tests/sentry/tasks/post_process/tests.py

@@ -4,10 +4,152 @@ from __future__ import absolute_import
 
 import mock
 
-from sentry.models import Group
+from sentry.models import Group, Rule
 from sentry.testutils import TestCase
 from sentry.tasks.post_process import (
-    record_affected_user, record_affected_code)
+    execute_rule, post_process_group, record_affected_user,
+    record_affected_code
+)
+
+
+class PostProcessGroupTest(TestCase):
+    @mock.patch('sentry.tasks.post_process.record_affected_code')
+    def test_record_affected_code(self, mock_record_affected_code):
+        group = self.create_group(project=self.project)
+        event = self.create_event(group=group)
+
+        with self.settings(SENTRY_ENABLE_EXPLORE_CODE=False):
+            post_process_group(
+                group=group,
+                event=event,
+                is_new=True,
+                is_regression=False,
+                is_sample=False,
+            )
+
+        assert not mock_record_affected_code.delay.called
+
+        with self.settings(SENTRY_ENABLE_EXPLORE_CODE=True):
+            post_process_group(
+                group=group,
+                event=event,
+                is_new=True,
+                is_regression=False,
+                is_sample=False,
+            )
+
+        mock_record_affected_code.delay.assert_called_once_with(
+            group=group,
+            event=event,
+        )
+
+    @mock.patch('sentry.tasks.post_process.record_affected_user')
+    def test_record_affected_user(self, mock_record_affected_user):
+        group = self.create_group(project=self.project)
+        event = self.create_event(group=group)
+
+        with self.settings(SENTRY_ENABLE_EXPLORE_USERS=False):
+            post_process_group(
+                group=group,
+                event=event,
+                is_new=True,
+                is_regression=False,
+                is_sample=False,
+            )
+
+        assert not mock_record_affected_user.delay.called
+
+        with self.settings(SENTRY_ENABLE_EXPLORE_USERS=True):
+            post_process_group(
+                group=group,
+                event=event,
+                is_new=True,
+                is_regression=False,
+                is_sample=False,
+            )
+
+        mock_record_affected_user.delay.assert_called_once_with(
+            group=group,
+            event=event,
+        )
+
+    @mock.patch('sentry.tasks.post_process.execute_rule')
+    @mock.patch('sentry.tasks.post_process.get_rules')
+    def test_execute_rule(self, mock_get_rules, mock_execute_rule):
+        action_id = 'sentry.rules.actions.notify_event.NotifyEventAction'
+        condition_id = 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition'
+
+        group = self.create_group(project=self.project)
+        event = self.create_event(group=group)
+
+        mock_get_rules.return_value = [
+            Rule(
+                id=1,
+                data={
+                    'actions': [{'id': action_id}],
+                    'conditions': [{'id': condition_id}],
+                }
+            )
+        ]
+
+        post_process_group(
+            group=group,
+            event=event,
+            is_new=False,
+            is_regression=False,
+            is_sample=False,
+        )
+
+        assert not mock_execute_rule.delay.called
+
+        post_process_group(
+            group=group,
+            event=event,
+            is_new=True,
+            is_regression=False,
+            is_sample=False,
+        )
+
+        mock_execute_rule.delay.assert_called_once_with(
+            rule_id=1,
+            event=event,
+            is_new=True,
+            is_regression=False,
+            is_sample=False,
+        )
+
+
+class ExecuteRuleTest(TestCase):
+    @mock.patch('sentry.tasks.post_process.rules')
+    def test_simple(self, mock_rules):
+        group = self.create_group(project=self.project)
+        event = self.create_event(group=group)
+        rule = Rule.objects.create(
+            project=event.project,
+            data={
+                'actions': [
+                    {'id': 'a.rule.id'},
+                ],
+            }
+        )
+
+        execute_rule(
+            rule_id=rule.id,
+            event=event,
+            is_new=True,
+            is_regression=False,
+            is_sample=True,
+        )
+
+        mock_rules.get.assert_called_once_with('a.rule.id')
+        mock_rule_inst = mock_rules.get.return_value
+        mock_rule_inst.assert_called_once_with(self.project)
+        mock_rule_inst.return_value.after.assert_called_once_with(
+            event=event,
+            is_new=True,
+            is_regression=False,
+            is_sample=True,
+        )
 
 
 class RecordAffectedUserTest(TestCase):