From 387303d36438b08215763af6f419383a7467e3c7 Mon Sep 17 00:00:00 2001 From: utkjad Date: Thu, 9 Jul 2015 19:36:52 +0000 Subject: [PATCH] Injecting call stack manager in CSM/CSMH,introducting @trackit,@wrapt, and refining implementation[PLAT-758] --- lms/djangoapps/courseware/model_data.py | 3 + lms/djangoapps/courseware/models.py | 15 +- .../courseware/user_state_client.py | 9 + .../djangoapps/call_stack_manager/__init__.py | 2 +- .../djangoapps/call_stack_manager/core.py | 241 ++++++++++++------ .../djangoapps/call_stack_manager/tests.py | 179 +++++++++---- requirements/edx/base.txt | 1 + 7 files changed, 321 insertions(+), 129 deletions(-) diff --git a/lms/djangoapps/courseware/model_data.py b/lms/djangoapps/courseware/model_data.py index c7275831c7..e9cb25cb7d 100644 --- a/lms/djangoapps/courseware/model_data.py +++ b/lms/djangoapps/courseware/model_data.py @@ -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. diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index 32a01e9e6e..5aae6fbde8 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -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 diff --git a/lms/djangoapps/courseware/user_state_client.py b/lms/djangoapps/courseware/user_state_client.py index 5827f658e5..ce0de871da 100644 --- a/lms/djangoapps/courseware/user_state_client.py +++ b/lms/djangoapps/courseware/user_state_client.py @@ -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 diff --git a/openedx/core/djangoapps/call_stack_manager/__init__.py b/openedx/core/djangoapps/call_stack_manager/__init__.py index 5682284fa2..4e99dbb28c 100644 --- a/openedx/core/djangoapps/call_stack_manager/__init__.py +++ b/openedx/core/djangoapps/call_stack_manager/__init__.py @@ -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 diff --git a/openedx/core/djangoapps/call_stack_manager/core.py b/openedx/core/djangoapps/call_stack_manager/core.py index 98a63e8dbd..968ea1e440 100644 --- a/openedx/core/djangoapps/call_stack_manager/core.py +++ b/openedx/core/djangoapps/call_stack_manager/core.py @@ -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_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. {"" : [[(file,line number,context),(---,---,---)], -# [(file,line number,context),(---,---,---)]]} STACK_BOOK = collections.defaultdict(list) +# Dictionary which stores call logs +# {'EntityName' : ListOf} +# CallStacks is ListOf +# Frame is a tuple ('FilePath','LineNumber','Function Name', 'Context') +# {"" : [[(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) diff --git a/openedx/core/djangoapps/call_stack_manager/tests.py b/openedx/core/djangoapps/call_stack_manager/tests.py index 7112e01ec5..3b6c9d784c 100644 --- a/openedx/core/djangoapps/call_stack_manager/tests.py +++ b/openedx/core/djangoapps/call_stack_manager/tests.py @@ -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) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index e7cc934d70..fa29a44a45 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -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