Merge pull request #8835 from edx/utkjad/injecting_callstackmanager
[PLAT -758] Making Call Stack Manager work in StudentModule and StudentModuleHistory, add @trackit, @wrapt dependency, and refine conditions
This commit is contained in:
@@ -45,6 +45,8 @@ from xmodule.modulestore.django import modulestore
|
||||
from xblock.core import XBlockAside
|
||||
from courseware.user_state_client import DjangoXBlockUserStateClient
|
||||
|
||||
from openedx.core.djangoapps.call_stack_manager import donottrack
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -990,6 +992,7 @@ class ScoresClient(object):
|
||||
|
||||
|
||||
# @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):
|
||||
"""
|
||||
Set the score and max_score for the specified user and xblock usage.
|
||||
|
||||
@@ -25,6 +25,7 @@ from model_utils.models import TimeStampedModel
|
||||
from student.models import user_by_anonymous_id
|
||||
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
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -68,11 +69,18 @@ class ChunkingManager(models.Manager):
|
||||
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.
|
||||
"""
|
||||
objects = ChunkingManager()
|
||||
objects = ChunkingCallStackManager()
|
||||
MODEL_TAGS = ['course_id', 'module_type']
|
||||
|
||||
# For a homework problem, contains a JSON
|
||||
@@ -145,10 +153,11 @@ class StudentModule(models.Model):
|
||||
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
|
||||
Student. Right now, we restrict this to problems so that the table doesn't
|
||||
explode in size."""
|
||||
objects = CallStackManager()
|
||||
HISTORY_SAVING_TYPES = {'problem'}
|
||||
|
||||
class Meta(object): # pylint: disable=missing-docstring
|
||||
|
||||
@@ -18,6 +18,8 @@ from xblock.fields import Scope, ScopeBase
|
||||
from courseware.models import StudentModule, StudentModuleHistory
|
||||
from edx_user_state_client.interface import XBlockUserStateClient, XBlockUserState
|
||||
|
||||
from openedx.core.djangoapps.call_stack_manager import donottrack
|
||||
|
||||
|
||||
class DjangoXBlockUserStateClient(XBlockUserStateClient):
|
||||
"""
|
||||
@@ -69,6 +71,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
|
||||
"""
|
||||
self.user = user
|
||||
|
||||
@donottrack(StudentModule, StudentModuleHistory)
|
||||
def _get_student_modules(self, username, block_keys):
|
||||
"""
|
||||
Retrieve the :class:`~StudentModule`s for the supplied ``username`` and ``block_keys``.
|
||||
@@ -116,6 +119,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
|
||||
sample_rate=self.API_DATADOG_SAMPLE_RATE,
|
||||
)
|
||||
|
||||
@donottrack(StudentModule, StudentModuleHistory)
|
||||
def get_many(self, username, block_keys, scope=Scope.user_state, fields=None):
|
||||
"""
|
||||
Retrieve the stored XBlock state for the specified XBlock usages.
|
||||
@@ -165,6 +169,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
|
||||
# Remove it once we're no longer interested in the data.
|
||||
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):
|
||||
"""
|
||||
Set fields for a particular XBlock.
|
||||
@@ -239,6 +244,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
|
||||
# Event for the entire set_many call.
|
||||
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):
|
||||
"""
|
||||
Delete the stored XBlock state for a many xblock usages.
|
||||
@@ -275,6 +281,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
|
||||
# We just read this object, so we know that we can do an update
|
||||
student_module.save(force_update=True)
|
||||
|
||||
@donottrack(StudentModule, StudentModuleHistory)
|
||||
def get_history(self, username, block_key, scope=Scope.user_state):
|
||||
"""
|
||||
Retrieve history of state changes for a given block for a given
|
||||
@@ -329,6 +336,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
|
||||
|
||||
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):
|
||||
"""
|
||||
You get no ordering guarantees. Fetching will happen in batch_size
|
||||
@@ -339,6 +347,7 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
|
||||
raise ValueError("Only Scope.user_state is supported")
|
||||
raise NotImplementedError()
|
||||
|
||||
@donottrack(StudentModule, StudentModuleHistory)
|
||||
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
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
Root Package for getting call stacks of various Model classes being used
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
from .core import CallStackManager, CallStackMixin, donottrack
|
||||
from .core import CallStackManager, CallStackMixin, donottrack, trackit
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
"""
|
||||
Get call stacks of Model Class
|
||||
in three cases-
|
||||
1. QuerySet API
|
||||
2. save()
|
||||
3. delete()
|
||||
Call Stack Manager deals with tracking call stacks of functions/methods/classes(Django Model Classes)
|
||||
Call Stack Manager logs unique call stacks. The call stacks then can be retrieved via Splunk, or log reads.
|
||||
|
||||
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
|
||||
@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.
|
||||
|
||||
How to use-
|
||||
TRACKING DJANGO MODEL CLASSES -
|
||||
Call stacks of Model Class
|
||||
in three cases-
|
||||
1. QuerySet API
|
||||
2. save()
|
||||
3. delete()
|
||||
|
||||
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-
|
||||
@@ -23,122 +27,197 @@ How to use-
|
||||
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
|
||||
class StudentModule(models.Model, CallStackMixin):
|
||||
|
||||
TRACKING FUNCTIONS, and METHODS-
|
||||
1. Import following-
|
||||
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 traceback
|
||||
import re
|
||||
import collections
|
||||
import wrapt
|
||||
import types
|
||||
import inspect
|
||||
from django.db.models import Manager
|
||||
|
||||
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.*$',
|
||||
'^.*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.
|
||||
# List keeping track of entities not to be tracked
|
||||
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)
|
||||
# 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):
|
||||
""" logs customised call stacks in global dictionary `STACK_BOOK`, and logs it.
|
||||
def capture_call_stack(entity_name):
|
||||
""" Logs customised call stacks in global dictionary STACK_BOOK and logs it.
|
||||
|
||||
Args:
|
||||
current_model - Name of the model class
|
||||
Arguments:
|
||||
entity_name - entity
|
||||
"""
|
||||
# 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()]
|
||||
# Holds temporary callstack
|
||||
# List with each element 4-tuple(filename, line number, function name, text)
|
||||
# and filtered with respect to regular expressions
|
||||
temp_call_stack = [frame for frame in traceback.extract_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)
|
||||
final_call_stack = "".join(traceback.format_list(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
|
||||
"""
|
||||
|
||||
""" 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()
|
||||
"""
|
||||
""" 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()
|
||||
"""
|
||||
""" 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
|
||||
"""
|
||||
""" Manager class which overrides the default Manager class for getting call stacks """
|
||||
def get_query_set(self):
|
||||
"""overriding the default queryset API method
|
||||
"""
|
||||
""" Override 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
|
||||
def donottrack(*entities_not_to_be_tracked):
|
||||
""" Decorator which halts tracking for some entities for specific functions
|
||||
|
||||
Arguments:
|
||||
entities_not_to_be_tracked: entities which are not to be tracked
|
||||
|
||||
Returns:
|
||||
wrapped function
|
||||
"""
|
||||
if not entities_not_to_be_tracked:
|
||||
entities_not_to_be_tracked = None
|
||||
|
||||
def real_donottrack(function):
|
||||
"""takes function to be decorated and returns wrapped function
|
||||
@wrapt.decorator
|
||||
def real_donottrack(wrapped, instance, args, kwargs): # pylint: disable=unused-argument
|
||||
""" Takes function to be decorated and returns wrapped function
|
||||
|
||||
Args:
|
||||
function - wrapped function i.e. real_donottrack
|
||||
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:
|
||||
return of wrapped function
|
||||
"""
|
||||
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
|
||||
global HALT_TRACKING # pylint: disable=global-variable-not-assigned
|
||||
if entities_not_to_be_tracked is None:
|
||||
HALT_TRACKING.append(None)
|
||||
else:
|
||||
if HALT_TRACKING:
|
||||
if HALT_TRACKING[-1] is None: # if @donottrack() calls @donottrack('xyz')
|
||||
pass
|
||||
else:
|
||||
HALT_TRACKING.append(set(HALT_TRACKING[-1].union(set(entities_not_to_be_tracked))))
|
||||
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
|
||||
HALT_TRACKING.append(set(entities_not_to_be_tracked))
|
||||
|
||||
return_value = wrapped(*args, **kwargs)
|
||||
# check if the returning class is a generator
|
||||
if isinstance(return_value, types.GeneratorType):
|
||||
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
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
@@ -1,70 +1,58 @@
|
||||
"""
|
||||
Test cases for Call Stack Manager
|
||||
"""
|
||||
import collections
|
||||
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
|
||||
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):
|
||||
"""
|
||||
Test Model class which uses both CallStackManager, and CallStackMixin
|
||||
"""
|
||||
""" 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
|
||||
"""
|
||||
""" Test Model class 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
|
||||
"""
|
||||
""" 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
|
||||
"""
|
||||
""" 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()
|
||||
""" Parent class of ModelWithCallStckMngrChild """
|
||||
id_field = models.IntegerField()
|
||||
|
||||
|
||||
class ModelWithCallStckMngrChild(ModelWithCallStackMngr):
|
||||
"""child class of 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
|
||||
"""
|
||||
""" 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
|
||||
"""
|
||||
""" Function calling QuerySetAPI, another function, again QuerySetAPI """
|
||||
ModelAnotherCallStckMngr.objects.filter(id_field=1)
|
||||
donottrack_child_func()
|
||||
ModelAnotherCallStckMngr.objects.filter(id_field=1)
|
||||
@@ -72,8 +60,7 @@ def track_without_donottrack():
|
||||
|
||||
@donottrack(ModelAnotherCallStckMngr)
|
||||
def donottrack_child_func():
|
||||
""" decorated child function
|
||||
"""
|
||||
""" decorated child function """
|
||||
# should not be tracked
|
||||
ModelAnotherCallStckMngr.objects.filter(id_field=1)
|
||||
|
||||
@@ -83,8 +70,7 @@ def donottrack_child_func():
|
||||
|
||||
@donottrack(ModelMixinCallStckMngr)
|
||||
def donottrack_parent_func():
|
||||
""" decorated parent function
|
||||
"""
|
||||
""" decorated parent function """
|
||||
# should not be tracked
|
||||
ModelMixinCallStckMngr.objects.filter(id_field=1)
|
||||
# should be tracked
|
||||
@@ -94,8 +80,7 @@ def donottrack_parent_func():
|
||||
|
||||
@donottrack()
|
||||
def donottrack_func_parent():
|
||||
""" non-parameterized @donottrack decorated function calling child function
|
||||
"""
|
||||
""" non-parameterized @donottrack decorated function calling child function """
|
||||
ModelMixin.objects.all()
|
||||
donottrack_func_child()
|
||||
ModelMixin.objects.filter(id_field=1)
|
||||
@@ -103,12 +88,55 @@ def donottrack_func_parent():
|
||||
|
||||
@donottrack()
|
||||
def donottrack_func_child():
|
||||
""" child decorated non-parameterized function
|
||||
"""
|
||||
""" child decorated non-parameterized function """
|
||||
# Should not be tracked
|
||||
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.REGULAR_EXPS', [])
|
||||
class TestingCallStackManager(TestCase):
|
||||
@@ -116,15 +144,22 @@ class TestingCallStackManager(TestCase):
|
||||
1. Tests CallStackManager QuerySetAPI functionality
|
||||
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):
|
||||
""" 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])
|
||||
modelclass_logged = log_capt.call_args[0][2]
|
||||
self.assertEqual(modelclass_logged, ModelMixin)
|
||||
|
||||
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
|
||||
"""
|
||||
ModelAnotherCallStckMngr(id_field=1).save()
|
||||
@@ -135,8 +170,9 @@ class TestingCallStackManager(TestCase):
|
||||
classes with CallStackManager should get logged.
|
||||
"""
|
||||
ModelAnotherCallStckMngr(id_field=1).save()
|
||||
ModelAnotherCallStckMngr.objects.all()
|
||||
self.assertEqual(ModelAnotherCallStckMngr, log_capt.call_args[0][1])
|
||||
ModelAnotherCallStckMngr.objects.filter(id_field=1)
|
||||
modelclass_logged = log_capt.call_args[0][2]
|
||||
self.assertEqual(ModelAnotherCallStckMngr, modelclass_logged)
|
||||
|
||||
def test_withoutqueryset(self, log_capt):
|
||||
""" Tests for Overriding QuerySet API
|
||||
@@ -162,7 +198,8 @@ class TestingCallStackManager(TestCase):
|
||||
ModelAnotherCallStckMngr(id_field=1).save()
|
||||
ModelMixinCallStckMngr(id_field=1).save()
|
||||
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):
|
||||
""" Tests parameterized nested @donottrack
|
||||
@@ -170,8 +207,8 @@ class TestingCallStackManager(TestCase):
|
||||
"""
|
||||
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])
|
||||
modelclass_logged = log_capt.call_args_list[0][0][2]
|
||||
self.assertEqual(ModelAnotherCallStckMngr, modelclass_logged)
|
||||
|
||||
def test_nested_parameterized_donottrack_after(self, log_capt):
|
||||
""" Tests parameterized nested @donottrack
|
||||
@@ -182,8 +219,10 @@ class TestingCallStackManager(TestCase):
|
||||
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])
|
||||
first_in_log = log_capt.call_args_list[0][0][2]
|
||||
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):
|
||||
""" test for function which calls decorated function
|
||||
@@ -192,10 +231,14 @@ class TestingCallStackManager(TestCase):
|
||||
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])
|
||||
first_in_log = log_capt.call_args_list[0][0][2]
|
||||
second_in_log = log_capt.call_args_list[1][0][2]
|
||||
third_in_log = log_capt.call_args_list[2][0][2]
|
||||
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):
|
||||
""" Test for inheritance
|
||||
@@ -213,3 +256,51 @@ class TestingCallStackManager(TestCase):
|
||||
for __ in range(1, 5):
|
||||
ModelMixinCallStckMngr(id_field=1).save()
|
||||
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
|
||||
unicodecsv==0.9.4
|
||||
django-require==1.0.6
|
||||
pyuca==1.1
|
||||
wrapt==1.10.5
|
||||
|
||||
# This needs to be installed *after* Cython, which is in pre.txt
|
||||
lxml==3.4.4
|
||||
|
||||
Reference in New Issue
Block a user