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:
Utkarsh
2015-08-27 13:42:15 -04:00
7 changed files with 321 additions and 129 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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