Commit 387303d3 by utkjad

Injecting call stack manager in CSM/CSMH,introducting @trackit,@wrapt, and…

Injecting call stack manager in CSM/CSMH,introducting @trackit,@wrapt, and refining implementation[PLAT-758]
parent a3599425
...@@ -45,6 +45,8 @@ from xmodule.modulestore.django import modulestore ...@@ -45,6 +45,8 @@ from xmodule.modulestore.django import modulestore
from xblock.core import XBlockAside from xblock.core import XBlockAside
from courseware.user_state_client import DjangoXBlockUserStateClient from courseware.user_state_client import DjangoXBlockUserStateClient
from openedx.core.djangoapps.call_stack_manager import donottrack
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -990,6 +992,7 @@ class ScoresClient(object): ...@@ -990,6 +992,7 @@ class ScoresClient(object):
# @contract(user_id=int, usage_key=UsageKey, score="number|None", max_score="number|None") # @contract(user_id=int, usage_key=UsageKey, score="number|None", max_score="number|None")
@donottrack(StudentModule)
def set_score(user_id, usage_key, score, max_score): def set_score(user_id, usage_key, score, max_score):
""" """
Set the score and max_score for the specified user and xblock usage. Set the score and max_score for the specified user and xblock usage.
......
...@@ -25,6 +25,7 @@ from model_utils.models import TimeStampedModel ...@@ -25,6 +25,7 @@ from model_utils.models import TimeStampedModel
from student.models import user_by_anonymous_id from student.models import user_by_anonymous_id
from submissions.models import score_set, score_reset from submissions.models import score_set, score_reset
from openedx.core.djangoapps.call_stack_manager import CallStackManager, CallStackMixin
from xmodule_django.models import CourseKeyField, LocationKeyField, BlockTypeKeyField # pylint: disable=import-error from xmodule_django.models import CourseKeyField, LocationKeyField, BlockTypeKeyField # pylint: disable=import-error
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -68,11 +69,18 @@ class ChunkingManager(models.Manager): ...@@ -68,11 +69,18 @@ class ChunkingManager(models.Manager):
return res return res
class StudentModule(models.Model): class ChunkingCallStackManager(CallStackManager, ChunkingManager):
"""
A derived class of ChunkingManager, and CallStackManager
"""
pass
class StudentModule(CallStackMixin, models.Model):
""" """
Keeps student state for a particular module in a particular course. Keeps student state for a particular module in a particular course.
""" """
objects = ChunkingManager() objects = ChunkingCallStackManager()
MODEL_TAGS = ['course_id', 'module_type'] MODEL_TAGS = ['course_id', 'module_type']
# For a homework problem, contains a JSON # For a homework problem, contains a JSON
...@@ -145,10 +153,11 @@ class StudentModule(models.Model): ...@@ -145,10 +153,11 @@ class StudentModule(models.Model):
return unicode(repr(self)) return unicode(repr(self))
class StudentModuleHistory(models.Model): class StudentModuleHistory(CallStackMixin, models.Model):
"""Keeps a complete history of state changes for a given XModule for a given """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 Student. Right now, we restrict this to problems so that the table doesn't
explode in size.""" explode in size."""
objects = CallStackManager()
HISTORY_SAVING_TYPES = {'problem'} HISTORY_SAVING_TYPES = {'problem'}
class Meta(object): # pylint: disable=missing-docstring class Meta(object): # pylint: disable=missing-docstring
......
...@@ -18,6 +18,8 @@ from xblock.fields import Scope, ScopeBase ...@@ -18,6 +18,8 @@ from xblock.fields import Scope, ScopeBase
from courseware.models import StudentModule, StudentModuleHistory from courseware.models import StudentModule, StudentModuleHistory
from edx_user_state_client.interface import XBlockUserStateClient, XBlockUserState from edx_user_state_client.interface import XBlockUserStateClient, XBlockUserState
from openedx.core.djangoapps.call_stack_manager import donottrack
class DjangoXBlockUserStateClient(XBlockUserStateClient): class DjangoXBlockUserStateClient(XBlockUserStateClient):
""" """
...@@ -69,6 +71,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): ...@@ -69,6 +71,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
""" """
self.user = user self.user = user
@donottrack(StudentModule, StudentModuleHistory)
def _get_student_modules(self, username, block_keys): def _get_student_modules(self, username, block_keys):
""" """
Retrieve the :class:`~StudentModule`s for the supplied ``username`` and ``block_keys``. Retrieve the :class:`~StudentModule`s for the supplied ``username`` and ``block_keys``.
...@@ -116,6 +119,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): ...@@ -116,6 +119,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
sample_rate=self.API_DATADOG_SAMPLE_RATE, sample_rate=self.API_DATADOG_SAMPLE_RATE,
) )
@donottrack(StudentModule, StudentModuleHistory)
def get_many(self, username, block_keys, scope=Scope.user_state, fields=None): def get_many(self, username, block_keys, scope=Scope.user_state, fields=None):
""" """
Retrieve the stored XBlock state for the specified XBlock usages. Retrieve the stored XBlock state for the specified XBlock usages.
...@@ -165,6 +169,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): ...@@ -165,6 +169,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
# Remove it once we're no longer interested in the data. # Remove it once we're no longer interested in the data.
self._ddog_histogram(evt_time, 'get_many.blks_out', block_count) self._ddog_histogram(evt_time, 'get_many.blks_out', block_count)
@donottrack(StudentModule, StudentModuleHistory)
def set_many(self, username, block_keys_to_state, scope=Scope.user_state): def set_many(self, username, block_keys_to_state, scope=Scope.user_state):
""" """
Set fields for a particular XBlock. Set fields for a particular XBlock.
...@@ -239,6 +244,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): ...@@ -239,6 +244,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
# Event for the entire set_many call. # Event for the entire set_many call.
self._ddog_histogram(evt_time, 'set_many.blks_updated', len(block_keys_to_state)) self._ddog_histogram(evt_time, 'set_many.blks_updated', len(block_keys_to_state))
@donottrack(StudentModule, StudentModuleHistory)
def delete_many(self, username, block_keys, scope=Scope.user_state, fields=None): def delete_many(self, username, block_keys, scope=Scope.user_state, fields=None):
""" """
Delete the stored XBlock state for a many xblock usages. Delete the stored XBlock state for a many xblock usages.
...@@ -275,6 +281,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): ...@@ -275,6 +281,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
# We just read this object, so we know that we can do an update # We just read this object, so we know that we can do an update
student_module.save(force_update=True) student_module.save(force_update=True)
@donottrack(StudentModule, StudentModuleHistory)
def get_history(self, username, block_key, scope=Scope.user_state): def get_history(self, username, block_key, scope=Scope.user_state):
""" """
Retrieve history of state changes for a given block for a given Retrieve history of state changes for a given block for a given
...@@ -329,6 +336,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): ...@@ -329,6 +336,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
yield XBlockUserState(username, block_key, state, history_entry.created, scope) yield XBlockUserState(username, block_key, state, history_entry.created, scope)
@donottrack(StudentModule, StudentModuleHistory)
def iter_all_for_block(self, block_key, scope=Scope.user_state, batch_size=None): def iter_all_for_block(self, block_key, scope=Scope.user_state, batch_size=None):
""" """
You get no ordering guarantees. Fetching will happen in batch_size You get no ordering guarantees. Fetching will happen in batch_size
...@@ -339,6 +347,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): ...@@ -339,6 +347,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
raise ValueError("Only Scope.user_state is supported") raise ValueError("Only Scope.user_state is supported")
raise NotImplementedError() raise NotImplementedError()
@donottrack(StudentModule, StudentModuleHistory)
def iter_all_for_course(self, course_key, block_type=None, scope=Scope.user_state, batch_size=None): def iter_all_for_course(self, course_key, block_type=None, scope=Scope.user_state, batch_size=None):
""" """
You get no ordering guarantees. Fetching will happen in batch_size You get no ordering guarantees. Fetching will happen in batch_size
......
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
Root Package for getting call stacks of various Model classes being used Root Package for getting call stacks of various Model classes being used
""" """
from __future__ import absolute_import from __future__ import absolute_import
from .core import CallStackManager, CallStackMixin, donottrack from .core import CallStackManager, CallStackMixin, donottrack, trackit
""" """
Get call stacks of Model Class Call Stack Manager deals with tracking call stacks of functions/methods/classes(Django Model Classes)
in three cases- Call Stack Manager logs unique call stacks. The call stacks then can be retrieved via Splunk, or log reads.
1. QuerySet API
2. save()
3. delete()
classes: classes:
CallStackManager - stores all stacks in global dictionary and logs CallStackManager - stores all stacks in global dictionary and logs
CallStackMixin - used for Model save(), and delete() method CallStackMixin - used for Model save(), and delete() method
Functions:
capture_call_stack - global function used to store call stack
Decorators: Decorators:
donottrack - mainly for the places where we know the calls. This decorator will let us not to track in specified cases @donottrack - Decorator that will halt tracking for parameterized entities,
(or halt tracking anything in case of non-parametrized decorator).
@trackit - Decorator that will start tracking decorated entity.
@track_till_now - Will log every unique call stack of parametrized entity/ entities.
TRACKING DJANGO MODEL CLASSES -
Call stacks of Model Class
in three cases-
1. QuerySet API
2. save()
3. delete()
How to use- How to use:
1. Import following in the file where class to be tracked resides 1. Import following in the file where class to be tracked resides
from openedx.core.djangoapps.call_stack_manager import CallStackManager, CallStackMixin 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- 2. Override objects of default manager by writing following in any model class which you want to track-
...@@ -23,122 +27,197 @@ How to use- ...@@ -23,122 +27,197 @@ How to use-
3. For tracking Save and Delete events- 3. For tracking Save and Delete events-
Use mixin called "CallStackMixin" Use mixin called "CallStackMixin"
For ex. For ex.
class StudentModule(CallStackMixin, models.Model): class StudentModule(models.Model, CallStackMixin):
4. Decorator is a parameterized decorator with class name/s as argument
How to use - TRACKING FUNCTIONS, and METHODS-
1. Import following 1. Import following-
import from openedx.core.djangoapps.call_stack_manager import donottrack from openedx.core.djangoapps.call_stack_manager import trackit
NOTE - @trackit is non-parameterized decorator.
FOR DISABLING TRACKING-
1. Import following at appropriate location-
from openedx.core.djangoapps.call_stack_manager import donottrack
NOTE - You need to import function/class you do not want to track.
""" """
import logging import logging
import traceback import traceback
import re import re
import collections import collections
import wrapt
import types
import inspect
from django.db.models import Manager from django.db.models import Manager
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# list of regular expressions acting as filters # List of regular expressions acting as filters
REGULAR_EXPS = [re.compile(x) for x in ['^.*python2.7.*$', '^.*<exec_function>.*$', '^.*exec_code_object.*$', REGULAR_EXPS = [re.compile(x) for x in ['^.*python2.7.*$', '^.*<exec_function>.*$', '^.*exec_code_object.*$',
'^.*edxapp/src.*$', '^.*call_stack_manager.*$']] '^.*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 # List keeping track of entities not to be tracked
# usually cases where we know that the function is calling Model classes.
HALT_TRACKING = [] 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) STACK_BOOK = collections.defaultdict(list)
# Dictionary which stores call logs
# {'EntityName' : ListOf<CallStacks>}
# CallStacks is ListOf<Frame>
# Frame is a tuple ('FilePath','LineNumber','Function Name', 'Context')
# {"<class 'courseware.models.StudentModule'>" : [[(file, line number, function name, context),(---,---,---)],
# [(file, line number, function name, context),(---,---,---)]]}
def capture_call_stack(current_model): def capture_call_stack(entity_name):
""" logs customised call stacks in global dictionary `STACK_BOOK`, and logs it. """ Logs customised call stacks in global dictionary STACK_BOOK and logs it.
Args: Arguments:
current_model - Name of the model class entity_name - entity
""" """
# holds temporary callstack # Holds temporary callstack
# frame[0][6:-1] -> File name along with path # List with each element 4-tuple(filename, line number, function name, text)
# frame[1][6:] -> Line Number # and filtered with respect to regular expressions
# frame[2][3:] -> Context temp_call_stack = [frame for frame in traceback.extract_stack()
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)] if not any(reg.match(frame[0]) for reg in REGULAR_EXPS)]
# avoid duplication. final_call_stack = "".join(traceback.format_list(temp_call_stack))
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)
def _should_get_logged(entity_name): # pylint: disable=
""" Checks if current call stack of current entity should be logged or not.
Arguments:
entity_name - Name of the current entity
Returns:
True if the current call stack is to logged, False otherwise
"""
is_class_in_halt_tracking = bool(HALT_TRACKING and inspect.isclass(entity_name) and
issubclass(entity_name, tuple(HALT_TRACKING[-1])))
is_function_in_halt_tracking = bool(HALT_TRACKING and not inspect.isclass(entity_name) and
any((entity_name.__name__ == x.__name__ and
entity_name.__module__ == x.__module__)
for x in tuple(HALT_TRACKING[-1])))
is_top_none = HALT_TRACKING and HALT_TRACKING[-1] is None
# if top of STACK_BOOK is None
if is_top_none:
return False
# if call stack is empty
if not temp_call_stack:
return False
if HALT_TRACKING:
if is_class_in_halt_tracking or is_function_in_halt_tracking:
return False
else:
return temp_call_stack not in STACK_BOOK[entity_name]
else:
return temp_call_stack not in STACK_BOOK[entity_name]
if _should_get_logged(entity_name):
STACK_BOOK[entity_name].append(temp_call_stack)
if inspect.isclass(entity_name):
log.info("Logging new call stack number %s for %s:\n %s", len(STACK_BOOK[entity_name]),
entity_name, final_call_stack)
else:
log.info("Logging new call stack number %s for %s.%s:\n %s", len(STACK_BOOK[entity_name]),
entity_name.__module__, entity_name.__name__, final_call_stack)
class CallStackMixin(object):
""" A mixin class for getting call stacks when Save() and Delete() methods are called
"""
class CallStackMixin(object):
""" Mixin class for getting call stacks when save() and delete() methods are called """
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" """ Logs before save() and overrides respective model API save() """
Logs before save and overrides respective model API save()
"""
capture_call_stack(type(self)) capture_call_stack(type(self))
return super(CallStackMixin, self).save(*args, **kwargs) return super(CallStackMixin, self).save(*args, **kwargs)
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
""" """ Logs before delete() and overrides respective model API delete() """
Logs before delete and overrides respective model API delete()
"""
capture_call_stack(type(self)) capture_call_stack(type(self))
return super(CallStackMixin, self).delete(*args, **kwargs) return super(CallStackMixin, self).delete(*args, **kwargs)
class CallStackManager(Manager): class CallStackManager(Manager):
""" A Manager class which overrides the default Manager class for getting call stacks """ Manager class which overrides the default Manager class for getting call stacks """
"""
def get_query_set(self): def get_query_set(self):
"""overriding the default queryset API method """ Override the default queryset API method """
"""
capture_call_stack(self.model) capture_call_stack(self.model)
return super(CallStackManager, self).get_query_set() return super(CallStackManager, self).get_query_set()
def donottrack(*classes_not_to_be_tracked): def donottrack(*entities_not_to_be_tracked):
"""function decorator which deals with toggling call stack """ Decorator which halts tracking for some entities for specific functions
Args:
classes_not_to_be_tracked: model classes where tracking is undesirable Arguments:
entities_not_to_be_tracked: entities which are not to be tracked
Returns: Returns:
wrapped function wrapped function
""" """
if not entities_not_to_be_tracked:
entities_not_to_be_tracked = None
def real_donottrack(function): @wrapt.decorator
"""takes function to be decorated and returns wrapped function def real_donottrack(wrapped, instance, args, kwargs): # pylint: disable=unused-argument
""" Takes function to be decorated and returns wrapped function
Args: Arguments:
function - wrapped function i.e. real_donottrack wrapped - The wrapped function which in turns needs to be called by wrapper function
instance - The object to which the wrapped function was bound when it was called.
args - The list of positional arguments supplied when the decorated function was called.
kwargs - The dictionary of keyword arguments supplied when the decorated function was called.
Returns:
return of wrapped function
""" """
def wrapper(*args, **kwargs): global HALT_TRACKING # pylint: disable=global-variable-not-assigned
""" wrapper function for decorated function if entities_not_to_be_tracked is None:
Returns: HALT_TRACKING.append(None)
wrapper function i.e. wrapper else:
""" if HALT_TRACKING:
if len(classes_not_to_be_tracked) == 0: if HALT_TRACKING[-1] is None: # if @donottrack() calls @donottrack('xyz')
global TRACK_FLAG # pylint: disable=W0603 pass
current_flag = TRACK_FLAG else:
TRACK_FLAG = False HALT_TRACKING.append(set(HALT_TRACKING[-1].union(set(entities_not_to_be_tracked))))
function(*args, **kwargs)
TRACK_FLAG = current_flag
else: else:
global HALT_TRACKING # pylint: disable=W0603 HALT_TRACKING.append(set(entities_not_to_be_tracked))
current_halt_track = HALT_TRACKING
HALT_TRACKING = classes_not_to_be_tracked return_value = wrapped(*args, **kwargs)
function(*args, **kwargs) # check if the returning class is a generator
HALT_TRACKING = current_halt_track if isinstance(return_value, types.GeneratorType):
return wrapper def generator_wrapper(wrapped_generator):
""" Function handling wrapped yielding values.
Argument:
wrapped_generator - wrapped function returning generator function
Returns:
Generator Wrapper
"""
try:
while True:
return_value = next(wrapped_generator)
yield return_value
finally:
HALT_TRACKING.pop()
return generator_wrapper(return_value)
else:
HALT_TRACKING.pop()
return return_value
return real_donottrack return real_donottrack
@wrapt.decorator
def trackit(wrapped, instance, args, kwargs): # pylint: disable=unused-argument
""" Decorator which tracks logs call stacks
Arguments:
wrapped - The wrapped function which in turns needs to be called by wrapper function.
instance - The object to which the wrapped function was bound when it was called.
args - The list of positional arguments supplied when the decorated function was called.
kwargs - The dictionary of keyword arguments supplied when the decorated function was called.
Returns:
wrapped function
"""
capture_call_stack(wrapped)
return wrapped(*args, **kwargs)
""" """
Test cases for Call Stack Manager Test cases for Call Stack Manager
""" """
import collections
from mock import patch from mock import patch
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from openedx.core.djangoapps.call_stack_manager import donottrack, CallStackManager, CallStackMixin from openedx.core.djangoapps.call_stack_manager import donottrack, CallStackManager, CallStackMixin, trackit
from openedx.core.djangoapps.call_stack_manager import core
class ModelMixinCallStckMngr(CallStackMixin, models.Model): class ModelMixinCallStckMngr(CallStackMixin, models.Model):
""" """ Test Model class which uses both CallStackManager, and CallStackMixin """
Test Model class which uses both CallStackManager, and CallStackMixin
"""
# override Manager objects # override Manager objects
objects = CallStackManager() objects = CallStackManager()
id_field = models.IntegerField() id_field = models.IntegerField()
class ModelMixin(CallStackMixin, models.Model): class ModelMixin(CallStackMixin, models.Model):
""" """ Test Model class that uses CallStackMixin but does not use CallStackManager """
Test Model that uses CallStackMixin but does not use CallStackManager
"""
id_field = models.IntegerField() id_field = models.IntegerField()
class ModelNothingCallStckMngr(models.Model): class ModelNothingCallStckMngr(models.Model):
""" """ Test Model class that neither uses CallStackMixin nor CallStackManager """
Test Model class that neither uses CallStackMixin nor CallStackManager
"""
id_field = models.IntegerField() id_field = models.IntegerField()
class ModelAnotherCallStckMngr(models.Model): class ModelAnotherCallStckMngr(models.Model):
""" """ Test Model class that only uses overridden Manager CallStackManager """
Test Model class that only uses overridden Manager CallStackManager
"""
objects = CallStackManager() objects = CallStackManager()
id_field = models.IntegerField() id_field = models.IntegerField()
class ModelWithCallStackMngr(models.Model): class ModelWithCallStackMngr(models.Model):
""" """ Parent class of ModelWithCallStckMngrChild """
Test Model Class with overridden CallStackManager
"""
objects = CallStackManager()
id_field = models.IntegerField() id_field = models.IntegerField()
class ModelWithCallStckMngrChild(ModelWithCallStackMngr): class ModelWithCallStckMngrChild(ModelWithCallStackMngr):
"""child class of ModelWithCallStackMngr """ Child class of ModelWithCallStackMngr """
"""
objects = CallStackManager() objects = CallStackManager()
child_id_field = models.IntegerField() child_id_field = models.IntegerField()
@donottrack(ModelWithCallStackMngr) @donottrack(ModelWithCallStackMngr)
def donottrack_subclass(): def donottrack_subclass():
""" function in which subclass and superclass calls QuerySetAPI """ function in which subclass and superclass calls QuerySetAPI """
"""
ModelWithCallStackMngr.objects.filter(id_field=1) ModelWithCallStackMngr.objects.filter(id_field=1)
ModelWithCallStckMngrChild.objects.filter(child_id_field=1) ModelWithCallStckMngrChild.objects.filter(child_id_field=1)
def track_without_donottrack(): def track_without_donottrack():
""" function calling QuerySetAPI, another function, again QuerySetAPI """ Function calling QuerySetAPI, another function, again QuerySetAPI """
"""
ModelAnotherCallStckMngr.objects.filter(id_field=1) ModelAnotherCallStckMngr.objects.filter(id_field=1)
donottrack_child_func() donottrack_child_func()
ModelAnotherCallStckMngr.objects.filter(id_field=1) ModelAnotherCallStckMngr.objects.filter(id_field=1)
...@@ -72,8 +60,7 @@ def track_without_donottrack(): ...@@ -72,8 +60,7 @@ def track_without_donottrack():
@donottrack(ModelAnotherCallStckMngr) @donottrack(ModelAnotherCallStckMngr)
def donottrack_child_func(): def donottrack_child_func():
""" decorated child function """ decorated child function """
"""
# should not be tracked # should not be tracked
ModelAnotherCallStckMngr.objects.filter(id_field=1) ModelAnotherCallStckMngr.objects.filter(id_field=1)
...@@ -83,8 +70,7 @@ def donottrack_child_func(): ...@@ -83,8 +70,7 @@ def donottrack_child_func():
@donottrack(ModelMixinCallStckMngr) @donottrack(ModelMixinCallStckMngr)
def donottrack_parent_func(): def donottrack_parent_func():
""" decorated parent function """ decorated parent function """
"""
# should not be tracked # should not be tracked
ModelMixinCallStckMngr.objects.filter(id_field=1) ModelMixinCallStckMngr.objects.filter(id_field=1)
# should be tracked # should be tracked
...@@ -94,8 +80,7 @@ def donottrack_parent_func(): ...@@ -94,8 +80,7 @@ def donottrack_parent_func():
@donottrack() @donottrack()
def donottrack_func_parent(): def donottrack_func_parent():
""" non-parameterized @donottrack decorated function calling child function """ non-parameterized @donottrack decorated function calling child function """
"""
ModelMixin.objects.all() ModelMixin.objects.all()
donottrack_func_child() donottrack_func_child()
ModelMixin.objects.filter(id_field=1) ModelMixin.objects.filter(id_field=1)
...@@ -103,12 +88,55 @@ def donottrack_func_parent(): ...@@ -103,12 +88,55 @@ def donottrack_func_parent():
@donottrack() @donottrack()
def donottrack_func_child(): def donottrack_func_child():
""" child decorated non-parameterized function """ child decorated non-parameterized function """
"""
# Should not be tracked # Should not be tracked
ModelMixin.objects.all() ModelMixin.objects.all()
@trackit
def trackit_func():
""" Test function for track it function """
return "hi"
class ClassFortrackit(object):
""" Test class for track it """
@trackit
def trackit_method(self):
""" Instance method for testing track it """
return 42
@trackit
@classmethod
def trackit_class_method(cls):
""" Classmethod for testing track it """
return 42
@donottrack(ClassFortrackit.trackit_class_method)
def donottrack_function():
"""Testing function donottrack for a function"""
for __ in range(5):
temp_var = ClassFortrackit.trackit_class_method()
return temp_var
@donottrack()
def donottrack_yield_func():
""" Function testing yield in donottrack """
ModelMixinCallStckMngr(id_field=1).save()
donottrack_function()
yield 48
class ClassReturingValue(object):
""" Test class with a decorated method """
@donottrack()
def donottrack_check_with_return(self, argument=43):
""" Function that returns something i.e. a wrapped function returning some value """
return 42 + argument
@patch('openedx.core.djangoapps.call_stack_manager.core.log.info') @patch('openedx.core.djangoapps.call_stack_manager.core.log.info')
@patch('openedx.core.djangoapps.call_stack_manager.core.REGULAR_EXPS', []) @patch('openedx.core.djangoapps.call_stack_manager.core.REGULAR_EXPS', [])
class TestingCallStackManager(TestCase): class TestingCallStackManager(TestCase):
...@@ -116,15 +144,22 @@ class TestingCallStackManager(TestCase): ...@@ -116,15 +144,22 @@ class TestingCallStackManager(TestCase):
1. Tests CallStackManager QuerySetAPI functionality 1. Tests CallStackManager QuerySetAPI functionality
2. Tests @donottrack decorator 2. Tests @donottrack decorator
""" """
def setUp(self):
core.TRACK_FLAG = True
core.STACK_BOOK = collections.defaultdict(list)
core.HALT_TRACKING = []
super(TestingCallStackManager, self).setUp()
def test_save(self, log_capt): def test_save(self, log_capt):
""" tests save() of CallStackMixin/ applies same for delete() """ tests save() of CallStackMixin/ applies same for delete()
classes with CallStackMixin should participate in logging. classes with CallStackMixin should participate in logging.
""" """
ModelMixin(id_field=1).save() ModelMixin(id_field=1).save()
self.assertEqual(ModelMixin, log_capt.call_args[0][1]) modelclass_logged = log_capt.call_args[0][2]
self.assertEqual(modelclass_logged, ModelMixin)
def test_withoutmixin_save(self, log_capt): def test_withoutmixin_save(self, log_capt):
"""tests save() of CallStackMixin/ applies same for delete() """ Tests save() of CallStackMixin/ applies same for delete()
classes without CallStackMixin should not participate in logging classes without CallStackMixin should not participate in logging
""" """
ModelAnotherCallStckMngr(id_field=1).save() ModelAnotherCallStckMngr(id_field=1).save()
...@@ -135,8 +170,9 @@ class TestingCallStackManager(TestCase): ...@@ -135,8 +170,9 @@ class TestingCallStackManager(TestCase):
classes with CallStackManager should get logged. classes with CallStackManager should get logged.
""" """
ModelAnotherCallStckMngr(id_field=1).save() ModelAnotherCallStckMngr(id_field=1).save()
ModelAnotherCallStckMngr.objects.all() ModelAnotherCallStckMngr.objects.filter(id_field=1)
self.assertEqual(ModelAnotherCallStckMngr, log_capt.call_args[0][1]) modelclass_logged = log_capt.call_args[0][2]
self.assertEqual(ModelAnotherCallStckMngr, modelclass_logged)
def test_withoutqueryset(self, log_capt): def test_withoutqueryset(self, log_capt):
""" Tests for Overriding QuerySet API """ Tests for Overriding QuerySet API
...@@ -162,7 +198,8 @@ class TestingCallStackManager(TestCase): ...@@ -162,7 +198,8 @@ class TestingCallStackManager(TestCase):
ModelAnotherCallStckMngr(id_field=1).save() ModelAnotherCallStckMngr(id_field=1).save()
ModelMixinCallStckMngr(id_field=1).save() ModelMixinCallStckMngr(id_field=1).save()
donottrack_child_func() donottrack_child_func()
self.assertEqual(ModelMixinCallStckMngr, log_capt.call_args[0][1]) modelclass_logged = log_capt.call_args[0][2]
self.assertEqual(ModelMixinCallStckMngr, modelclass_logged)
def test_nested_parameterized_donottrack(self, log_capt): def test_nested_parameterized_donottrack(self, log_capt):
""" Tests parameterized nested @donottrack """ Tests parameterized nested @donottrack
...@@ -170,8 +207,8 @@ class TestingCallStackManager(TestCase): ...@@ -170,8 +207,8 @@ class TestingCallStackManager(TestCase):
""" """
ModelAnotherCallStckMngr(id_field=1).save() ModelAnotherCallStckMngr(id_field=1).save()
donottrack_parent_func() donottrack_parent_func()
self.assertEqual(ModelAnotherCallStckMngr, log_capt.call_args_list[0][0][1]) modelclass_logged = log_capt.call_args_list[0][0][2]
self.assertEqual(ModelMixinCallStckMngr, log_capt.call_args_list[1][0][1]) self.assertEqual(ModelAnotherCallStckMngr, modelclass_logged)
def test_nested_parameterized_donottrack_after(self, log_capt): def test_nested_parameterized_donottrack_after(self, log_capt):
""" Tests parameterized nested @donottrack """ Tests parameterized nested @donottrack
...@@ -182,8 +219,10 @@ class TestingCallStackManager(TestCase): ...@@ -182,8 +219,10 @@ class TestingCallStackManager(TestCase):
ModelAnotherCallStckMngr(id_field=1).save() ModelAnotherCallStckMngr(id_field=1).save()
# test is this- that this should get called. # test is this- that this should get called.
ModelAnotherCallStckMngr.objects.filter(id_field=1) ModelAnotherCallStckMngr.objects.filter(id_field=1)
self.assertEqual(ModelMixinCallStckMngr, log_capt.call_args_list[0][0][1]) first_in_log = log_capt.call_args_list[0][0][2]
self.assertEqual(ModelAnotherCallStckMngr, log_capt.call_args_list[1][0][1]) second_in_log = log_capt.call_args_list[1][0][2]
self.assertEqual(ModelMixinCallStckMngr, first_in_log)
self.assertEqual(ModelAnotherCallStckMngr, second_in_log)
def test_donottrack_called_in_func(self, log_capt): def test_donottrack_called_in_func(self, log_capt):
""" test for function which calls decorated function """ test for function which calls decorated function
...@@ -192,10 +231,14 @@ class TestingCallStackManager(TestCase): ...@@ -192,10 +231,14 @@ class TestingCallStackManager(TestCase):
ModelAnotherCallStckMngr(id_field=1).save() ModelAnotherCallStckMngr(id_field=1).save()
ModelMixinCallStckMngr(id_field=1).save() ModelMixinCallStckMngr(id_field=1).save()
track_without_donottrack() track_without_donottrack()
self.assertEqual(ModelMixinCallStckMngr, log_capt.call_args_list[0][0][1]) first_in_log = log_capt.call_args_list[0][0][2]
self.assertEqual(ModelAnotherCallStckMngr, log_capt.call_args_list[1][0][1]) second_in_log = log_capt.call_args_list[1][0][2]
self.assertEqual(ModelMixinCallStckMngr, log_capt.call_args_list[2][0][1]) third_in_log = log_capt.call_args_list[2][0][2]
self.assertEqual(ModelAnotherCallStckMngr, log_capt.call_args_list[3][0][1]) fourth_in_log = log_capt.call_args_list[3][0][2]
self.assertEqual(ModelMixinCallStckMngr, first_in_log)
self.assertEqual(ModelAnotherCallStckMngr, second_in_log)
self.assertEqual(ModelMixinCallStckMngr, third_in_log)
self.assertEqual(ModelAnotherCallStckMngr, fourth_in_log)
def test_donottrack_child_too(self, log_capt): def test_donottrack_child_too(self, log_capt):
""" Test for inheritance """ Test for inheritance
...@@ -213,3 +256,51 @@ class TestingCallStackManager(TestCase): ...@@ -213,3 +256,51 @@ class TestingCallStackManager(TestCase):
for __ in range(1, 5): for __ in range(1, 5):
ModelMixinCallStckMngr(id_field=1).save() ModelMixinCallStckMngr(id_field=1).save()
self.assertEqual(len(log_capt.call_args_list), 1) self.assertEqual(len(log_capt.call_args_list), 1)
def test_donottrack_with_return(self, log_capt):
""" Test for @donottrack
Checks if wrapper function returns the same value as wrapped function
"""
class_returning_value = ClassReturingValue()
everything = class_returning_value.donottrack_check_with_return(argument=42)
self.assertEqual(everything, 84)
self.assertEqual(len(log_capt.call_args_list), 0)
def test_trackit_func(self, log_capt):
""" Test track it for function """
var = trackit_func()
self.assertEqual("hi", var)
self.assertEqual(len(log_capt.call_args_list), 1)
def test_trackit_instance_method(self, log_capt):
""" Test track it for instance method """
cls = ClassFortrackit()
var = cls.trackit_method()
self.assertEqual(42, var)
logged_function_module = log_capt.call_args_list[0][0][2]
logged_function_name = log_capt.call_args_list[0][0][3]
# check tracking the same function
self.assertEqual(ClassFortrackit.trackit_method.__name__, logged_function_name)
self.assertEqual(ClassFortrackit.trackit_method.__module__, logged_function_module)
def test_trackit_class_method(self, log_capt):
""" Test for class method """
var = ClassFortrackit.trackit_class_method()
self.assertEqual(42, var)
logged_function_module = log_capt.call_args_list[0][0][2]
logged_function_name = log_capt.call_args_list[0][0][3]
# check tracking the same function
self.assertEqual(ClassFortrackit.trackit_class_method.__name__, logged_function_name)
self.assertEqual(ClassFortrackit.trackit_class_method.__module__, logged_function_module)
def test_yield(self, log_capt):
""" Test for yield generator """
donottrack_yield_func()
self.assertEqual(core.HALT_TRACKING[-1], None)
self.assertEqual(len(log_capt.call_args_list), 0)
def test_donottrack_function(self, log_capt):
""" Test donotrack for functions """
temp = donottrack_function()
self.assertEqual(temp, 42)
self.assertEqual(len(log_capt.call_args_list), 0)
...@@ -86,6 +86,7 @@ django-ratelimit-backend==0.6 ...@@ -86,6 +86,7 @@ django-ratelimit-backend==0.6
unicodecsv==0.9.4 unicodecsv==0.9.4
django-require==1.0.6 django-require==1.0.6
pyuca==1.1 pyuca==1.1
wrapt==1.10.5
# This needs to be installed *after* Cython, which is in pre.txt # This needs to be installed *after* Cython, which is in pre.txt
lxml==3.4.4 lxml==3.4.4
......
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