Commit d881067f by Utkarsh

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
parents df76994e 387303d3
......@@ -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
"""
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.
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
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
"""
class CallStackMixin(object):
""" 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)
"""
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
......
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