Adding call stack manager
This commit is contained in:
@@ -173,7 +173,7 @@ CACHES = {
|
||||
INSTALLED_APPS += ('external_auth', )
|
||||
|
||||
# Add milestones to Installed apps for testing
|
||||
INSTALLED_APPS += ('milestones', )
|
||||
INSTALLED_APPS += ('milestones', 'openedx.core.djangoapps.call_stack_manager')
|
||||
|
||||
# hide ratelimit warnings while running tests
|
||||
filterwarnings('ignore', message='No request passed to the backend, unable to rate-limit')
|
||||
|
||||
@@ -26,6 +26,7 @@ from student.models import user_by_anonymous_id
|
||||
from submissions.models import score_set, score_reset
|
||||
|
||||
from xmodule_django.models import CourseKeyField, LocationKeyField, BlockTypeKeyField # pylint: disable=import-error
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
log = logging.getLogger("edx.courseware")
|
||||
|
||||
@@ -72,7 +73,6 @@ class StudentModule(models.Model):
|
||||
Keeps student state for a particular module in a particular course.
|
||||
"""
|
||||
objects = ChunkingManager()
|
||||
|
||||
MODEL_TAGS = ['course_id', 'module_type']
|
||||
|
||||
# For a homework problem, contains a JSON
|
||||
@@ -96,10 +96,10 @@ class StudentModule(models.Model):
|
||||
class Meta(object): # pylint: disable=missing-docstring
|
||||
unique_together = (('student', 'module_state_key', 'course_id'),)
|
||||
|
||||
## Internal state of the object
|
||||
# Internal state of the object
|
||||
state = models.TextField(null=True, blank=True)
|
||||
|
||||
## Grade, and are we done?
|
||||
# Grade, and are we done?
|
||||
grade = models.FloatField(null=True, blank=True, db_index=True)
|
||||
max_grade = models.FloatField(null=True, blank=True)
|
||||
DONE_TYPES = (
|
||||
@@ -146,7 +146,6 @@ class StudentModuleHistory(models.Model):
|
||||
"""Keeps a complete history of state changes for a given XModule for a given
|
||||
Student. Right now, we restrict this to problems so that the table doesn't
|
||||
explode in size."""
|
||||
|
||||
HISTORY_SAVING_TYPES = {'problem'}
|
||||
|
||||
class Meta(object): # pylint: disable=missing-docstring
|
||||
@@ -211,7 +210,6 @@ class XModuleUserStateSummaryField(XBlockFieldBase):
|
||||
"""
|
||||
Stores data set in the Scope.user_state_summary scope by an xmodule field
|
||||
"""
|
||||
|
||||
class Meta(object): # pylint: disable=missing-docstring
|
||||
unique_together = (('usage_id', 'field_name'),)
|
||||
|
||||
@@ -223,7 +221,6 @@ class XModuleStudentPrefsField(XBlockFieldBase):
|
||||
"""
|
||||
Stores data set in the Scope.preferences scope by an xmodule field
|
||||
"""
|
||||
|
||||
class Meta(object): # pylint: disable=missing-docstring
|
||||
unique_together = (('student', 'module_type', 'field_name'),)
|
||||
|
||||
@@ -237,10 +234,8 @@ class XModuleStudentInfoField(XBlockFieldBase):
|
||||
"""
|
||||
Stores data set in the Scope.preferences scope by an xmodule field
|
||||
"""
|
||||
|
||||
class Meta(object): # pylint: disable=missing-docstring
|
||||
unique_together = (('student', 'field_name'),)
|
||||
|
||||
student = models.ForeignKey(User, db_index=True)
|
||||
|
||||
|
||||
|
||||
@@ -457,7 +457,7 @@ FEATURES['ENABLE_EDXNOTES'] = True
|
||||
FEATURES['ENABLE_TEAMS'] = True
|
||||
|
||||
# Add milestones to Installed apps for testing
|
||||
INSTALLED_APPS += ('milestones', )
|
||||
INSTALLED_APPS += ('milestones', 'openedx.core.djangoapps.call_stack_manager')
|
||||
|
||||
# Enable courseware search for tests
|
||||
FEATURES['ENABLE_COURSEWARE_SEARCH'] = True
|
||||
|
||||
5
openedx/core/djangoapps/call_stack_manager/__init__.py
Normal file
5
openedx/core/djangoapps/call_stack_manager/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
Root Package for getting call stacks of various Model classes being used
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
from .core import CallStackManager, CallStackMixin, donottrack
|
||||
144
openedx/core/djangoapps/call_stack_manager/core.py
Normal file
144
openedx/core/djangoapps/call_stack_manager/core.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
Get call stacks of Model Class
|
||||
in three cases-
|
||||
1. QuerySet API
|
||||
2. save()
|
||||
3. delete()
|
||||
|
||||
classes:
|
||||
CallStackManager - stores all stacks in global dictionary and logs
|
||||
CallStackMixin - used for Model save(), and delete() method
|
||||
|
||||
Functions:
|
||||
capture_call_stack - global function used to store call stack
|
||||
|
||||
Decorators:
|
||||
donottrack - mainly for the places where we know the calls. This decorator will let us not to track in specified cases
|
||||
|
||||
How to use-
|
||||
1. Import following in the file where class to be tracked resides
|
||||
from openedx.core.djangoapps.call_stack_manager import CallStackManager, CallStackMixin
|
||||
2. Override objects of default manager by writing following in any model class which you want to track-
|
||||
objects = CallStackManager()
|
||||
3. For tracking Save and Delete events-
|
||||
Use mixin called "CallStackMixin"
|
||||
For ex.
|
||||
class StudentModule(CallStackMixin, models.Model):
|
||||
4. Decorator is a parameterized decorator with class name/s as argument
|
||||
How to use -
|
||||
1. Import following
|
||||
import from openedx.core.djangoapps.call_stack_manager import donottrack
|
||||
"""
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
import re
|
||||
import collections
|
||||
from django.db.models import Manager
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# list of regular expressions acting as filters
|
||||
REGULAR_EXPS = [re.compile(x) for x in ['^.*python2.7.*$', '^.*<exec_function>.*$', '^.*exec_code_object.*$',
|
||||
'^.*edxapp/src.*$', '^.*call_stack_manager.*$']]
|
||||
# Variable which decides whether to track calls in the function or not. Do it by default.
|
||||
TRACK_FLAG = True
|
||||
|
||||
# List keeping track of Model classes not be tracked for special cases
|
||||
# usually cases where we know that the function is calling Model classes.
|
||||
HALT_TRACKING = []
|
||||
|
||||
# Module Level variables
|
||||
# dictionary which stores call stacks.
|
||||
# { "ModelClasses" : [ListOfFrames]}
|
||||
# Frames - ('FilePath','LineNumber','Context')
|
||||
# ex. {"<class 'courseware.models.StudentModule'>" : [[(file,line number,context),(---,---,---)],
|
||||
# [(file,line number,context),(---,---,---)]]}
|
||||
STACK_BOOK = collections.defaultdict(list)
|
||||
|
||||
|
||||
def capture_call_stack(current_model):
|
||||
""" logs customised call stacks in global dictionary `STACK_BOOK`, and logs it.
|
||||
|
||||
Args:
|
||||
current_model - Name of the model class
|
||||
"""
|
||||
# holds temporary callstack
|
||||
# frame[0][6:-1] -> File name along with path
|
||||
# frame[1][6:] -> Line Number
|
||||
# frame[2][3:] -> Context
|
||||
temp_call_stack = [(frame[0][6:-1],
|
||||
frame[1][6:],
|
||||
frame[2][3:])
|
||||
for frame in [stack.replace("\n", "").strip().split(',') for stack in traceback.format_stack()]
|
||||
if not any(reg.match(frame[0]) for reg in REGULAR_EXPS)]
|
||||
|
||||
# avoid duplication.
|
||||
if temp_call_stack not in STACK_BOOK[current_model] and TRACK_FLAG \
|
||||
and not issubclass(current_model, tuple(HALT_TRACKING)):
|
||||
STACK_BOOK[current_model].append(temp_call_stack)
|
||||
log.info("logging new call stack for %s:\n %s", current_model, temp_call_stack)
|
||||
|
||||
|
||||
class CallStackMixin(object):
|
||||
""" A mixin class for getting call stacks when Save() and Delete() methods are called
|
||||
"""
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Logs before save and overrides respective model API save()
|
||||
"""
|
||||
capture_call_stack(type(self))
|
||||
return super(CallStackMixin, self).save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""
|
||||
Logs before delete and overrides respective model API delete()
|
||||
"""
|
||||
capture_call_stack(type(self))
|
||||
return super(CallStackMixin, self).delete(*args, **kwargs)
|
||||
|
||||
|
||||
class CallStackManager(Manager):
|
||||
""" A Manager class which overrides the default Manager class for getting call stacks
|
||||
"""
|
||||
def get_query_set(self):
|
||||
"""overriding the default queryset API method
|
||||
"""
|
||||
capture_call_stack(self.model)
|
||||
return super(CallStackManager, self).get_query_set()
|
||||
|
||||
|
||||
def donottrack(*classes_not_to_be_tracked):
|
||||
"""function decorator which deals with toggling call stack
|
||||
Args:
|
||||
classes_not_to_be_tracked: model classes where tracking is undesirable
|
||||
Returns:
|
||||
wrapped function
|
||||
"""
|
||||
|
||||
def real_donottrack(function):
|
||||
"""takes function to be decorated and returns wrapped function
|
||||
|
||||
Args:
|
||||
function - wrapped function i.e. real_donottrack
|
||||
"""
|
||||
def wrapper(*args, **kwargs):
|
||||
""" wrapper function for decorated function
|
||||
Returns:
|
||||
wrapper function i.e. wrapper
|
||||
"""
|
||||
if len(classes_not_to_be_tracked) == 0:
|
||||
global TRACK_FLAG # pylint: disable=W0603
|
||||
current_flag = TRACK_FLAG
|
||||
TRACK_FLAG = False
|
||||
function(*args, **kwargs)
|
||||
TRACK_FLAG = current_flag
|
||||
else:
|
||||
global HALT_TRACKING # pylint: disable=W0603
|
||||
current_halt_track = HALT_TRACKING
|
||||
HALT_TRACKING = classes_not_to_be_tracked
|
||||
function(*args, **kwargs)
|
||||
HALT_TRACKING = current_halt_track
|
||||
return wrapper
|
||||
return real_donottrack
|
||||
8
openedx/core/djangoapps/call_stack_manager/models.py
Normal file
8
openedx/core/djangoapps/call_stack_manager/models.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
Dummy models.py file
|
||||
|
||||
Note -
|
||||
django-nose loads models for tests, but only if the django app that the test is contained in has models itself.
|
||||
This file is empty so that the unit tests can have models.
|
||||
For call_stack_manager - models specific to tests are defined in tests.py
|
||||
"""
|
||||
215
openedx/core/djangoapps/call_stack_manager/tests.py
Normal file
215
openedx/core/djangoapps/call_stack_manager/tests.py
Normal file
@@ -0,0 +1,215 @@
|
||||
"""
|
||||
Test cases for Call Stack Manager
|
||||
"""
|
||||
from mock import patch
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
|
||||
from openedx.core.djangoapps.call_stack_manager import donottrack, CallStackManager, CallStackMixin
|
||||
|
||||
|
||||
class ModelMixinCallStckMngr(CallStackMixin, models.Model):
|
||||
"""
|
||||
Test Model class which uses both CallStackManager, and CallStackMixin
|
||||
"""
|
||||
# override Manager objects
|
||||
objects = CallStackManager()
|
||||
id_field = models.IntegerField()
|
||||
|
||||
|
||||
class ModelMixin(CallStackMixin, models.Model):
|
||||
"""
|
||||
Test Model that uses CallStackMixin but does not use CallStackManager
|
||||
"""
|
||||
id_field = models.IntegerField()
|
||||
|
||||
|
||||
class ModelNothingCallStckMngr(models.Model):
|
||||
"""
|
||||
Test Model class that neither uses CallStackMixin nor CallStackManager
|
||||
"""
|
||||
id_field = models.IntegerField()
|
||||
|
||||
|
||||
class ModelAnotherCallStckMngr(models.Model):
|
||||
"""
|
||||
Test Model class that only uses overridden Manager CallStackManager
|
||||
"""
|
||||
objects = CallStackManager()
|
||||
id_field = models.IntegerField()
|
||||
|
||||
|
||||
class ModelWithCallStackMngr(models.Model):
|
||||
"""
|
||||
Test Model Class with overridden CallStackManager
|
||||
"""
|
||||
objects = CallStackManager()
|
||||
id_field = models.IntegerField()
|
||||
|
||||
|
||||
class ModelWithCallStckMngrChild(ModelWithCallStackMngr):
|
||||
"""child class of ModelWithCallStackMngr
|
||||
"""
|
||||
objects = CallStackManager()
|
||||
child_id_field = models.IntegerField()
|
||||
|
||||
|
||||
@donottrack(ModelWithCallStackMngr)
|
||||
def donottrack_subclass():
|
||||
""" function in which subclass and superclass calls QuerySetAPI
|
||||
"""
|
||||
ModelWithCallStackMngr.objects.filter(id_field=1)
|
||||
ModelWithCallStckMngrChild.objects.filter(child_id_field=1)
|
||||
|
||||
|
||||
def track_without_donottrack():
|
||||
""" function calling QuerySetAPI, another function, again QuerySetAPI
|
||||
"""
|
||||
ModelAnotherCallStckMngr.objects.filter(id_field=1)
|
||||
donottrack_child_func()
|
||||
ModelAnotherCallStckMngr.objects.filter(id_field=1)
|
||||
|
||||
|
||||
@donottrack(ModelAnotherCallStckMngr)
|
||||
def donottrack_child_func():
|
||||
""" decorated child function
|
||||
"""
|
||||
# should not be tracked
|
||||
ModelAnotherCallStckMngr.objects.filter(id_field=1)
|
||||
|
||||
# should be tracked
|
||||
ModelMixinCallStckMngr.objects.filter(id_field=1)
|
||||
|
||||
|
||||
@donottrack(ModelMixinCallStckMngr)
|
||||
def donottrack_parent_func():
|
||||
""" decorated parent function
|
||||
"""
|
||||
# should not be tracked
|
||||
ModelMixinCallStckMngr.objects.filter(id_field=1)
|
||||
# should be tracked
|
||||
ModelAnotherCallStckMngr.objects.filter(id_field=1)
|
||||
donottrack_child_func()
|
||||
|
||||
|
||||
@donottrack()
|
||||
def donottrack_func_parent():
|
||||
""" non-parameterized @donottrack decorated function calling child function
|
||||
"""
|
||||
ModelMixin.objects.all()
|
||||
donottrack_func_child()
|
||||
ModelMixin.objects.filter(id_field=1)
|
||||
|
||||
|
||||
@donottrack()
|
||||
def donottrack_func_child():
|
||||
""" child decorated non-parameterized function
|
||||
"""
|
||||
# Should not be tracked
|
||||
ModelMixin.objects.all()
|
||||
|
||||
|
||||
@patch('openedx.core.djangoapps.call_stack_manager.core.log.info')
|
||||
@patch('openedx.core.djangoapps.call_stack_manager.core.REGULAR_EXPS', [])
|
||||
class TestingCallStackManager(TestCase):
|
||||
"""Tests for call_stack_manager
|
||||
1. Tests CallStackManager QuerySetAPI functionality
|
||||
2. Tests @donottrack decorator
|
||||
"""
|
||||
def test_save(self, log_capt):
|
||||
""" tests save() of CallStackMixin/ applies same for delete()
|
||||
classes with CallStackMixin should participate in logging.
|
||||
"""
|
||||
ModelMixin(id_field=1).save()
|
||||
self.assertEqual(ModelMixin, log_capt.call_args[0][1])
|
||||
|
||||
def test_withoutmixin_save(self, log_capt):
|
||||
"""tests save() of CallStackMixin/ applies same for delete()
|
||||
classes without CallStackMixin should not participate in logging
|
||||
"""
|
||||
ModelAnotherCallStckMngr(id_field=1).save()
|
||||
self.assertEqual(len(log_capt.call_args_list), 0)
|
||||
|
||||
def test_queryset(self, log_capt):
|
||||
""" Tests for Overriding QuerySet API
|
||||
classes with CallStackManager should get logged.
|
||||
"""
|
||||
ModelAnotherCallStckMngr(id_field=1).save()
|
||||
ModelAnotherCallStckMngr.objects.all()
|
||||
self.assertEqual(ModelAnotherCallStckMngr, log_capt.call_args[0][1])
|
||||
|
||||
def test_withoutqueryset(self, log_capt):
|
||||
""" Tests for Overriding QuerySet API
|
||||
classes without CallStackManager should not get logged
|
||||
"""
|
||||
# create and save objects of class not overriding queryset API
|
||||
ModelNothingCallStckMngr(id_field=1).save()
|
||||
# class not using Manager, should not get logged
|
||||
ModelNothingCallStckMngr.objects.all()
|
||||
self.assertEqual(len(log_capt.call_args_list), 0)
|
||||
|
||||
def test_donottrack(self, log_capt):
|
||||
""" Test for @donottrack
|
||||
calls in decorated function should not get logged
|
||||
"""
|
||||
donottrack_func_parent()
|
||||
self.assertEqual(len(log_capt.call_args_list), 0)
|
||||
|
||||
def test_parameterized_donottrack(self, log_capt):
|
||||
""" Test for parameterized @donottrack
|
||||
classes specified in the decorator @donottrack should not get logged
|
||||
"""
|
||||
ModelAnotherCallStckMngr(id_field=1).save()
|
||||
ModelMixinCallStckMngr(id_field=1).save()
|
||||
donottrack_child_func()
|
||||
self.assertEqual(ModelMixinCallStckMngr, log_capt.call_args[0][1])
|
||||
|
||||
def test_nested_parameterized_donottrack(self, log_capt):
|
||||
""" Tests parameterized nested @donottrack
|
||||
should not track call of classes specified in decorated with scope bounded to the respective class
|
||||
"""
|
||||
ModelAnotherCallStckMngr(id_field=1).save()
|
||||
donottrack_parent_func()
|
||||
self.assertEqual(ModelAnotherCallStckMngr, log_capt.call_args_list[0][0][1])
|
||||
self.assertEqual(ModelMixinCallStckMngr, log_capt.call_args_list[1][0][1])
|
||||
|
||||
def test_nested_parameterized_donottrack_after(self, log_capt):
|
||||
""" Tests parameterized nested @donottrack
|
||||
QuerySetAPI calls after calling function with @donottrack should get logged
|
||||
"""
|
||||
donottrack_child_func()
|
||||
# class with CallStackManager as Manager
|
||||
ModelAnotherCallStckMngr(id_field=1).save()
|
||||
# test is this- that this should get called.
|
||||
ModelAnotherCallStckMngr.objects.filter(id_field=1)
|
||||
self.assertEqual(ModelMixinCallStckMngr, log_capt.call_args_list[0][0][1])
|
||||
self.assertEqual(ModelAnotherCallStckMngr, log_capt.call_args_list[1][0][1])
|
||||
|
||||
def test_donottrack_called_in_func(self, log_capt):
|
||||
""" test for function which calls decorated function
|
||||
functions without @donottrack decorator should log
|
||||
"""
|
||||
ModelAnotherCallStckMngr(id_field=1).save()
|
||||
ModelMixinCallStckMngr(id_field=1).save()
|
||||
track_without_donottrack()
|
||||
self.assertEqual(ModelMixinCallStckMngr, log_capt.call_args_list[0][0][1])
|
||||
self.assertEqual(ModelAnotherCallStckMngr, log_capt.call_args_list[1][0][1])
|
||||
self.assertEqual(ModelMixinCallStckMngr, log_capt.call_args_list[2][0][1])
|
||||
self.assertEqual(ModelAnotherCallStckMngr, log_capt.call_args_list[3][0][1])
|
||||
|
||||
def test_donottrack_child_too(self, log_capt):
|
||||
""" Test for inheritance
|
||||
subclass should not be tracked when superclass is called in a @donottrack decorated function
|
||||
"""
|
||||
ModelWithCallStackMngr(id_field=1).save()
|
||||
ModelWithCallStckMngrChild(id_field=1, child_id_field=1).save()
|
||||
donottrack_subclass()
|
||||
self.assertEqual(len(log_capt.call_args_list), 0)
|
||||
|
||||
def test_duplication(self, log_capt):
|
||||
""" Test for duplication of call stacks
|
||||
should not log duplicated call stacks
|
||||
"""
|
||||
for __ in range(1, 5):
|
||||
ModelMixinCallStckMngr(id_field=1).save()
|
||||
self.assertEqual(len(log_capt.call_args_list), 1)
|
||||
Reference in New Issue
Block a user