Commit 59e0d22f by utkjad

Adding call stack manager

parent 5fb2a1fd
......@@ -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
......
"""
Root Package for getting call stacks of various Model classes being used
"""
from __future__ import absolute_import
from .core import CallStackManager, CallStackMixin, donottrack
"""
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
"""
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
"""
"""
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)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment