Index: Misc/NEWS =================================================================== --- Misc/NEWS (revision 83287) +++ Misc/NEWS (working copy) @@ -18,6 +18,9 @@ Library ------- +- Issue #9315: Fix for the trace module to record correct class name + when tracing methods. Patch by Eli Bendersky. + - Issue #6213: Implement getstate() and setstate() methods of utf-8-sig and utf-16 incremental encoders. Index: Lib/trace.py =================================================================== --- Lib/trace.py (revision 83287) +++ Lib/trace.py (working copy) @@ -56,7 +56,7 @@ import time import token import tokenize -import types +import inspect import gc try: @@ -396,7 +396,7 @@ # and check the constants for references to other code objects for c in code.co_consts: - if isinstance(c, types.CodeType): + if inspect.iscode(c): # find another code object, so recurse into it linenos.update(find_lines(c, strs)) return linenos @@ -543,7 +543,7 @@ ## use of gc.get_referrers() was suggested by Michael Hudson # all functions which refer to this code object funcs = [f for f in gc.get_referrers(code) - if hasattr(f, "func_doc")] + if inspect.isfunction(f)] # require len(func) == 1 to avoid ambiguity caused by calls to # new.function(): "In the face of ambiguity, refuse the # temptation to guess." @@ -555,17 +555,13 @@ if hasattr(c, "__bases__")] if len(classes) == 1: # ditto for new.classobj() - clsname = str(classes[0]) + clsname = classes[0].__name__ # cache the result - assumption is that new.* is # not called later to disturb this relationship # _caller_cache could be flushed if functions in # the new module get called. self._caller_cache[code] = clsname if clsname is not None: - # final hack - module name shows up in str(cls), but we've already - # computed module name, so remove it - clsname = clsname.split(".")[1:] - clsname = ".".join(clsname) funcname = "%s.%s" % (clsname, funcname) return filename, modulename, funcname Index: Lib/test/test_trace.py =================================================================== --- Lib/test/test_trace.py (revision 0) +++ Lib/test/test_trace.py (revision 0) @@ -0,0 +1,301 @@ +import imp +import os +import sys +from test.test_support import run_unittest +import unittest + +import trace + +#------------------------------- Utilities -----------------------------------# + +def make_fake_module(): + """Creates a fake module named 'fakemodule'. + + The new module has a single function named 'func', and it's placed in + sys.modules + The file this fake module "comes from" is fakefile.py + + """ + + # Prepare the function to import from the fake module + # + fake_func_src = ''' + def func(a): + b = a + 1 + return b + 2 + '''.lstrip() + + fake_func = compile(fake_func_src, 'fakefile.py', 'exec') + + # Create a new module, place the function into it and add it to sys.modules + # + fakemodule = imp.new_module('fakemodule') + exec fake_func in fakemodule.__dict__ + fakemodule.__file__ = 'fakefile.py' + sys.modules['fakemodule'] = fakemodule + + +def modname(filename): + """Infer a module name from a containing file name""" + base = os.path.basename(filename) + return os.path.splitext(base)[0] + +def my_file_and_modname(): + """The file and module name of this file (__file__)""" + return __file__, modname(__file__) + + +#-------------------- Target functions for tracing ---------------------------# + +def traced_func_linear(x, y): + a = x + b = y + c = a + b + return c + +def traced_func_loop(x, y): + c = x + for i in range(5): + c += y + return c + +# Expects the 'fakemodule' module to exist and have a 'func' function in it +# +def traced_func_importing(x, y): + from fakemodule import func + return x + y + func(1) + +def traced_func_simple_caller(x): + c = traced_func_linear(x, x) + return c + x + +def traced_func_importing_caller(x): + k = traced_func_simple_caller(x) + k += traced_func_importing(k, x) + return k + +def traced_func_generator(num): + c = 5 # executed once + for i in range(num): + yield i + c + +def traced_func_calling_generator(): + k = 0 + for i in traced_func_generator(10): + k += i + +def traced_doubler(num): + return num * 2 + +def traced_caller_list_comprehension(): + k = 10 + mylist = [traced_doubler(i) for i in range(k)] + return mylist + + +class TracedClass(object): + def __init__(self, x): + self.a = x + + def inst_method_linear(self, y): + return self.a + y + + def inst_method_calling(self, x): + c = self.inst_method_linear(x) + return c + traced_func_linear(x, c) + + @classmethod + def class_method_linear(cls, y): + return y * 2 + + @staticmethod + def static_method_linear(y): + return y * 2 + + +#------------------------------ Test cases -----------------------------------# + + +class TestLineCounts(unittest.TestCase): + """White-box testing of line-counting, via runfunc""" + def setUp(self): + self.tracer = trace.Trace(count=1, trace=0, countfuncs=0, countcallers=0) + + def test_traced_func_linear(self): + result = self.tracer.runfunc(traced_func_linear, 2, 5) + self.assertEqual(result, 7) + + # all lines are executed once + expected = {} + firstlineno = traced_func_linear.__code__.co_firstlineno + for i in range(1, 5): + expected[(__file__, firstlineno + i)] = 1 + + self.assertEqual(self.tracer.results().counts, expected) + + def test_traced_func_loop(self): + self.tracer.runfunc(traced_func_loop, 2, 3) + + firstlineno = traced_func_loop.__code__.co_firstlineno + expected = { + (__file__, firstlineno + 1): 1, + (__file__, firstlineno + 2): 6, + (__file__, firstlineno + 3): 5, + (__file__, firstlineno + 4): 1, + } + self.assertEqual(self.tracer.results().counts, expected) + + def test_traced_func_importing(self): + make_fake_module() + self.tracer.runfunc(traced_func_importing, 2, 5) + + firstlineno = traced_func_importing.__code__.co_firstlineno + expected = { + (__file__, firstlineno + 1): 1, + (__file__, firstlineno + 2): 1, + ('fakefile.py', 2): 1, + ('fakefile.py', 3): 1, + } + self.assertEqual(self.tracer.results().counts, expected) + + def test_trace_func_generator(self): + self.tracer.runfunc(traced_func_calling_generator) + + firstlineno_calling = traced_func_calling_generator.__code__.co_firstlineno + firstlineno_gen = traced_func_generator.__code__.co_firstlineno + expected = { + (__file__, firstlineno_calling + 1): 1, + (__file__, firstlineno_calling + 2): 11, + (__file__, firstlineno_calling + 3): 10, + (__file__, firstlineno_gen + 1): 1, + (__file__, firstlineno_gen + 2): 11, + (__file__, firstlineno_gen + 3): 10, + } + self.assertEqual(self.tracer.results().counts, expected) + + def test_trace_list_comprehension(self): + self.tracer.runfunc(traced_caller_list_comprehension) + + firstlineno_calling = traced_caller_list_comprehension.__code__.co_firstlineno + firstlineno_called = traced_doubler.__code__.co_firstlineno + expected = { + (__file__, firstlineno_calling + 1): 1, + (__file__, firstlineno_calling + 2): 11, + (__file__, firstlineno_calling + 3): 1, + (__file__, firstlineno_called + 1): 10, + } + self.assertEqual(self.tracer.results().counts, expected) + + + def test_linear_methods(self): + # ZZZ todo: later add 'static_method_linear' and 'class_method_linear' + # here, once the line numbers of those are resolved + # + for methname in ['inst_method_linear',]: + tracer = trace.Trace(count=1, trace=0, countfuncs=0, countcallers=0) + traced_obj = TracedClass(25) + method = getattr(traced_obj, methname) + tracer.runfunc(method, 20) + + firstlineno = method.__code__.co_firstlineno + expected = { + (__file__, firstlineno + 1): 1, + } + self.assertEqual(tracer.results().counts, expected) + + +class TestRunExecCounts(unittest.TestCase): + """A simple sanity test of line-counting, via run (exec)""" + def test_exec_counts(self): + self.tracer = trace.Trace(count=1, trace=0, countfuncs=0, countcallers=0) + code = r'''traced_func_loop(2, 5)''' + code = compile(code, __file__, 'exec') + self.tracer.run(code) + + firstlineno = traced_func_loop.__code__.co_firstlineno + expected = { + (__file__, firstlineno + 1): 1, + (__file__, firstlineno + 2): 6, + (__file__, firstlineno + 3): 5, + (__file__, firstlineno + 4): 1, + } + + # When used through 'run', some other spurios counts are produced, like + # the settrace of threading, which we ignore, just making sure that the + # counts fo traced_func_loop were right. + for k in expected: + self.assertEqual(self.tracer.results().counts[k], expected[k]) + +class TestFuncs(unittest.TestCase): + """White-box testing of funcs tracing""" + def setUp(self): + self.tracer = trace.Trace(count=0, trace=0, countfuncs=1) + self.filemod = my_file_and_modname() + + def test_simple_caller(self): + self.tracer.runfunc(traced_func_simple_caller, 1) + + expected = { + self.filemod + ('traced_func_simple_caller',): 1, + self.filemod + ('traced_func_linear',): 1, + } + self.assertEqual(self.tracer.results().calledfuncs, expected) + + def test_loop_caller_importing(self): + make_fake_module() + self.tracer.runfunc(traced_func_importing_caller, 1) + + expected = { + self.filemod + ('traced_func_simple_caller',): 1, + self.filemod + ('traced_func_linear',): 1, + self.filemod + ('traced_func_importing_caller',): 1, + self.filemod + ('traced_func_importing',): 1, + ('fakefile.py', 'fakefile', 'func'): 1, + } + self.assertEqual(self.tracer.results().calledfuncs, expected) + + def test_inst_method_calling(self): + obj = TracedClass(20) + self.tracer.runfunc(obj.inst_method_calling, 1) + + expected = { + self.filemod + ('TracedClass.inst_method_calling',): 1, + self.filemod + ('TracedClass.inst_method_linear',): 1, + self.filemod + ('traced_func_linear',): 1, + } + self.assertEqual(self.tracer.results().calledfuncs, expected) + + +class TestCallers(unittest.TestCase): + """White-box testing of callers tracing""" + def setUp(self): + self.tracer = trace.Trace(count=0, trace=0, countcallers=1) + self.filemod = my_file_and_modname() + + def test_loop_caller_importing(self): + make_fake_module() + self.tracer.runfunc(traced_func_importing_caller, 1) + + expected = { + ((os.path.splitext(trace.__file__)[0] + '.py', 'trace', 'Trace.runfunc'), + (self.filemod + ('traced_func_importing_caller',))): 1, + ((self.filemod + ('traced_func_simple_caller',)), + (self.filemod + ('traced_func_linear',))): 1, + ((self.filemod + ('traced_func_importing_caller',)), + (self.filemod + ('traced_func_simple_caller',))): 1, + ((self.filemod + ('traced_func_importing_caller',)), + (self.filemod + ('traced_func_importing',))): 1, + ((self.filemod + ('traced_func_importing',)), + ('fakefile.py', 'fakefile', 'func')): 1, + } + self.assertEqual(self.tracer.results().callers, expected) + + +#------------------------------ Driver ---------------------------------------# + +def test_main(): + run_unittest(__name__) + + +if __name__ == '__main__': + test_main()