Commit bc822b7f by Sarina Canelake

Merge pull request #10873 from edx/kill-callstackmanager

Remove CallStackManager (PLAT-931)
parents 5ecb9af2 596e93a2
......@@ -173,9 +173,6 @@ CACHES = {
},
}
# Add apps to Installed apps for testing
INSTALLED_APPS += ('openedx.core.djangoapps.call_stack_manager',)
# hide ratelimit warnings while running tests
filterwarnings('ignore', message='No request passed to the backend, unable to rate-limit')
......
......@@ -2,7 +2,6 @@
from __future__ import unicode_literals
from django.db import migrations, models
import openedx.core.djangoapps.call_stack_manager.core
import model_utils.fields
import xmodule_django.models
import django.utils.timezone
......@@ -69,7 +68,6 @@ class Migration(migrations.Migration):
('modified', models.DateTimeField(auto_now=True, db_index=True)),
('student', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
],
bases=(openedx.core.djangoapps.call_stack_manager.core.CallStackMixin, models.Model),
),
migrations.CreateModel(
name='StudentModuleHistory',
......@@ -85,7 +83,6 @@ class Migration(migrations.Migration):
options={
'get_latest_by': 'created',
},
bases=(openedx.core.djangoapps.call_stack_manager.core.CallStackMixin, models.Model),
),
migrations.CreateModel(
name='XModuleStudentInfoField',
......
......@@ -45,7 +45,6 @@ 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__)
......@@ -992,7 +991,6 @@ 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,7 +25,6 @@ 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
log = logging.getLogger(__name__)
......@@ -72,21 +71,10 @@ class ChunkingManager(models.Manager):
return res
class ChunkingCallStackManager(CallStackManager, ChunkingManager):
"""
A derived class of ChunkingManager, and CallStackManager
Class is currently unused but remains as part of the CallStackManger work. To re-enable see comment in StudentModule
"""
pass
class StudentModule(CallStackMixin, models.Model):
class StudentModule(models.Model):
"""
Keeps student state for a particular module in a particular course.
"""
# Changed back to ChunkingManager from ChunkingCallStackManger. To re-enable CallStack Management change the line
# back to: objects = ChunkingCallStackManager() Ticket: PLAT-881
objects = ChunkingManager()
MODEL_TAGS = ['course_id', 'module_type']
......@@ -161,11 +149,11 @@ class StudentModule(CallStackMixin, models.Model):
return unicode(repr(self))
class StudentModuleHistory(CallStackMixin, models.Model):
class StudentModuleHistory(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()
objects = ChunkingManager()
HISTORY_SAVING_TYPES = {'problem'}
class Meta(object):
......@@ -197,6 +185,9 @@ class StudentModuleHistory(CallStackMixin, models.Model):
max_grade=instance.max_grade)
history_entry.save()
def __unicode__(self):
return unicode(repr(self))
class XBlockFieldBase(models.Model):
"""
......
......@@ -18,8 +18,6 @@ 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):
"""
......@@ -71,7 +69,6 @@ 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``.
......@@ -119,7 +116,6 @@ 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.
......@@ -173,7 +169,6 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
self._ddog_histogram(evt_time, 'get_many.blks_out', block_count)
self._ddog_histogram(evt_time, 'get_many.response_time', (finish_time - evt_time) * 1000)
@donottrack(StudentModule, StudentModuleHistory)
def set_many(self, username, block_keys_to_state, scope=Scope.user_state):
"""
Set fields for a particular XBlock.
......@@ -250,7 +245,6 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
self._ddog_histogram(evt_time, 'set_many.blks_updated', len(block_keys_to_state))
self._ddog_histogram(evt_time, 'set_many.response_time', (finish_time - evt_time) * 1000)
@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.
......@@ -291,7 +285,6 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
finish_time = time()
self._ddog_histogram(evt_time, 'delete_many.response_time', (finish_time - evt_time) * 1000)
@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
......@@ -346,7 +339,6 @@ 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
......@@ -357,7 +349,6 @@ 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
......
......@@ -516,9 +516,6 @@ FEATURES['ENABLE_EDXNOTES'] = True
# Enable teams feature for tests.
FEATURES['ENABLE_TEAMS'] = True
# Add apps to Installed apps for testing
INSTALLED_APPS += ('openedx.core.djangoapps.call_stack_manager',)
# Enable courseware search for tests
FEATURES['ENABLE_COURSEWARE_SEARCH'] = True
......
"""
Root Package for getting call stacks of various Model classes being used
"""
from __future__ import absolute_import
from .core import CallStackManager, CallStackMixin, donottrack, trackit
"""
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
Decorators:
@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:
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-
objects = CallStackManager()
3. For tracking Save and Delete events-
Use mixin called "CallStackMixin"
For ex.
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
REGULAR_EXPS = [re.compile(x) for x in ['^.*python2.7.*$', '^.*<exec_function>.*$', '^.*exec_code_object.*$',
'^.*edxapp/src.*$', '^.*call_stack_manager.*$']]
# List keeping track of entities not to be tracked
HALT_TRACKING = []
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(entity_name):
""" Logs customised call stacks in global dictionary STACK_BOOK and logs it.
Arguments:
entity_name - entity
"""
# 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)]
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):
""" 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() """
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() """
capture_call_stack(type(self))
return super(CallStackMixin, self).delete(*args, **kwargs)
class CallStackManager(Manager):
""" Manager class which overrides the default Manager class for getting call stacks """
def get_queryset(self):
""" Override the default queryset API method """
capture_call_stack(self.model)
return super(CallStackManager, self).get_queryset()
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
@wrapt.decorator
def real_donottrack(wrapped, instance, args, kwargs): # pylint: disable=unused-argument
""" Takes function to be decorated and returns wrapped function
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
"""
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:
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)
"""
Dummy models.py file
Note -
django-nose loads models for tests, but only if the django app that the test is contained in has models itself.
This file is empty so that the unit tests can have models.
For call_stack_manager - models specific to tests are defined in tests.py
"""
"""
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, trackit
from openedx.core.djangoapps.call_stack_manager import core
class ModelMixinCallStckMngr(CallStackMixin, models.Model):
""" 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 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 """
id_field = models.IntegerField()
class ModelAnotherCallStckMngr(models.Model):
""" Test Model class that only uses overridden Manager CallStackManager """
objects = CallStackManager()
id_field = models.IntegerField()
class ModelWithCallStackMngr(models.Model):
""" Parent class of ModelWithCallStckMngrChild """
id_field = models.IntegerField()
class ModelWithCallStckMngrChild(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 """
ModelWithCallStackMngr.objects.filter(id_field=1)
ModelWithCallStckMngrChild.objects.filter(child_id_field=1)
def track_without_donottrack():
""" Function calling QuerySetAPI, another function, again QuerySetAPI """
ModelAnotherCallStckMngr.objects.filter(id_field=1)
donottrack_child_func()
ModelAnotherCallStckMngr.objects.filter(id_field=1)
@donottrack(ModelAnotherCallStckMngr)
def donottrack_child_func():
""" decorated child function """
# should not be tracked
ModelAnotherCallStckMngr.objects.filter(id_field=1)
# should be tracked
ModelMixinCallStckMngr.objects.filter(id_field=1)
@donottrack(ModelMixinCallStckMngr)
def donottrack_parent_func():
""" decorated parent function """
# should not be tracked
ModelMixinCallStckMngr.objects.filter(id_field=1)
# should be tracked
ModelAnotherCallStckMngr.objects.filter(id_field=1)
donottrack_child_func()
@donottrack()
def donottrack_func_parent():
""" non-parameterized @donottrack decorated function calling child function """
ModelMixin.objects.all()
donottrack_func_child()
ModelMixin.objects.filter(id_field=1)
@donottrack()
def donottrack_func_child():
""" 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):
"""Tests for call_stack_manager
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()
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()
classes without CallStackMixin should not participate in logging
"""
ModelAnotherCallStckMngr(id_field=1).save()
self.assertEqual(len(log_capt.call_args_list), 0)
def test_queryset(self, log_capt):
""" Tests for Overriding QuerySet API
classes with CallStackManager should get logged.
"""
ModelAnotherCallStckMngr(id_field=1).save()
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
classes without CallStackManager should not get logged
"""
# create and save objects of class not overriding queryset API
ModelNothingCallStckMngr(id_field=1).save()
# class not using Manager, should not get logged
ModelNothingCallStckMngr.objects.all()
self.assertEqual(len(log_capt.call_args_list), 0)
def test_donottrack(self, log_capt):
""" Test for @donottrack
calls in decorated function should not get logged
"""
donottrack_func_parent()
self.assertEqual(len(log_capt.call_args_list), 0)
def test_parameterized_donottrack(self, log_capt):
""" Test for parameterized @donottrack
classes specified in the decorator @donottrack should not get logged
"""
ModelAnotherCallStckMngr(id_field=1).save()
ModelMixinCallStckMngr(id_field=1).save()
donottrack_child_func()
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
should not track call of classes specified in decorated with scope bounded to the respective class
"""
ModelAnotherCallStckMngr(id_field=1).save()
donottrack_parent_func()
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
QuerySetAPI calls after calling function with @donottrack should get logged
"""
donottrack_child_func()
# class with CallStackManager as Manager
ModelAnotherCallStckMngr(id_field=1).save()
# test is this- that this should get called.
ModelAnotherCallStckMngr.objects.filter(id_field=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
functions without @donottrack decorator should log
"""
ModelAnotherCallStckMngr(id_field=1).save()
ModelMixinCallStckMngr(id_field=1).save()
track_without_donottrack()
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
subclass should not be tracked when superclass is called in a @donottrack decorated function
"""
ModelWithCallStackMngr(id_field=1).save()
ModelWithCallStckMngrChild(id_field=1, child_id_field=1).save()
donottrack_subclass()
self.assertEqual(len(log_capt.call_args_list), 0)
def test_duplication(self, log_capt):
""" Test for duplication of call stacks
should not log duplicated call stacks
"""
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)
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