Issue #9315: Fix for the trace module to record correct class name
when tracing methods. Unit tests. Patch by Eli Bendersky.
This commit is contained in:
parent
dc69e7217a
commit
4d7701729c
|
@ -1,10 +1,286 @@
|
|||
# Testing the trace module
|
||||
|
||||
from test.support import run_unittest, TESTFN, rmtree, unlink, captured_stdout
|
||||
import os
|
||||
import sys
|
||||
from test.support import (run_unittest, TESTFN, rmtree, unlink,
|
||||
captured_stdout)
|
||||
import unittest
|
||||
import trace
|
||||
import os, sys
|
||||
|
||||
import trace
|
||||
from trace import CoverageResults, Trace
|
||||
|
||||
from test.tracedmodules import testmod
|
||||
|
||||
|
||||
#------------------------------- Utilities -----------------------------------#
|
||||
|
||||
def fix_ext_py(filename):
|
||||
"""Given a .pyc/.pyo filename converts it to the appropriate .py"""
|
||||
if filename.endswith(('.pyc', '.pyo')):
|
||||
filename = filename[:-1]
|
||||
return filename
|
||||
|
||||
def my_file_and_modname():
|
||||
"""The .py file and module name of this file (__file__)"""
|
||||
modname = os.path.splitext(os.path.basename(__file__))[0]
|
||||
return fix_ext_py(__file__), modname
|
||||
|
||||
def get_firstlineno(func):
|
||||
return func.__code__.co_firstlineno
|
||||
|
||||
#-------------------- Target functions for tracing ---------------------------#
|
||||
#
|
||||
# The relative line numbers of lines in these functions matter for verifying
|
||||
# tracing. Please modify the appropriate tests if you change one of the
|
||||
# functions. Absolute line numbers don't matter.
|
||||
#
|
||||
|
||||
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
|
||||
|
||||
def traced_func_importing(x, y):
|
||||
return x + y + testmod.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(count=1, trace=0, countfuncs=0, countcallers=0)
|
||||
self.my_py_filename = fix_ext_py(__file__)
|
||||
self.maxDiff = None
|
||||
|
||||
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 = get_firstlineno(traced_func_linear)
|
||||
for i in range(1, 5):
|
||||
expected[(self.my_py_filename, 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 = get_firstlineno(traced_func_loop)
|
||||
expected = {
|
||||
(self.my_py_filename, firstlineno + 1): 1,
|
||||
(self.my_py_filename, firstlineno + 2): 6,
|
||||
(self.my_py_filename, firstlineno + 3): 5,
|
||||
(self.my_py_filename, firstlineno + 4): 1,
|
||||
}
|
||||
self.assertEqual(self.tracer.results().counts, expected)
|
||||
|
||||
def test_traced_func_importing(self):
|
||||
self.tracer.runfunc(traced_func_importing, 2, 5)
|
||||
|
||||
firstlineno = get_firstlineno(traced_func_importing)
|
||||
expected = {
|
||||
(self.my_py_filename, firstlineno + 1): 1,
|
||||
(fix_ext_py(testmod.__file__), 2): 1,
|
||||
(fix_ext_py(testmod.__file__), 3): 1,
|
||||
}
|
||||
|
||||
self.assertEqual(self.tracer.results().counts, expected)
|
||||
|
||||
def test_trace_func_generator(self):
|
||||
self.tracer.runfunc(traced_func_calling_generator)
|
||||
|
||||
firstlineno_calling = get_firstlineno(traced_func_calling_generator)
|
||||
firstlineno_gen = get_firstlineno(traced_func_generator)
|
||||
expected = {
|
||||
(self.my_py_filename, firstlineno_calling + 1): 1,
|
||||
(self.my_py_filename, firstlineno_calling + 2): 11,
|
||||
(self.my_py_filename, firstlineno_calling + 3): 10,
|
||||
(self.my_py_filename, firstlineno_gen + 1): 1,
|
||||
(self.my_py_filename, firstlineno_gen + 2): 11,
|
||||
(self.my_py_filename, 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 = get_firstlineno(traced_caller_list_comprehension)
|
||||
firstlineno_called = get_firstlineno(traced_doubler)
|
||||
expected = {
|
||||
(self.my_py_filename, firstlineno_calling + 1): 1,
|
||||
# List compehentions work differently in 3.x, so the count
|
||||
# below changed compared to 2.x.
|
||||
(self.my_py_filename, firstlineno_calling + 2): 12,
|
||||
(self.my_py_filename, firstlineno_calling + 3): 1,
|
||||
(self.my_py_filename, firstlineno_called + 1): 10,
|
||||
}
|
||||
self.assertEqual(self.tracer.results().counts, expected)
|
||||
|
||||
|
||||
def test_linear_methods(self):
|
||||
# XXX todo: later add 'static_method_linear' and 'class_method_linear'
|
||||
# here, once issue1764286 is resolved
|
||||
#
|
||||
for methname in ['inst_method_linear',]:
|
||||
tracer = Trace(count=1, trace=0, countfuncs=0, countcallers=0)
|
||||
traced_obj = TracedClass(25)
|
||||
method = getattr(traced_obj, methname)
|
||||
tracer.runfunc(method, 20)
|
||||
|
||||
firstlineno = get_firstlineno(method)
|
||||
expected = {
|
||||
(self.my_py_filename, firstlineno + 1): 1,
|
||||
}
|
||||
self.assertEqual(tracer.results().counts, expected)
|
||||
|
||||
|
||||
class TestRunExecCounts(unittest.TestCase):
|
||||
"""A simple sanity test of line-counting, via runctx (exec)"""
|
||||
def setUp(self):
|
||||
self.my_py_filename = fix_ext_py(__file__)
|
||||
|
||||
def test_exec_counts(self):
|
||||
self.tracer = Trace(count=1, trace=0, countfuncs=0, countcallers=0)
|
||||
code = r'''traced_func_loop(2, 5)'''
|
||||
code = compile(code, __file__, 'exec')
|
||||
self.tracer.runctx(code, globals(), vars())
|
||||
|
||||
firstlineno = get_firstlineno(traced_func_loop)
|
||||
expected = {
|
||||
(self.my_py_filename, firstlineno + 1): 1,
|
||||
(self.my_py_filename, firstlineno + 2): 6,
|
||||
(self.my_py_filename, firstlineno + 3): 5,
|
||||
(self.my_py_filename, 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.keys():
|
||||
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(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):
|
||||
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,
|
||||
(fix_ext_py(testmod.__file__), 'testmod', '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(count=0, trace=0, countcallers=1)
|
||||
self.filemod = my_file_and_modname()
|
||||
|
||||
def test_loop_caller_importing(self):
|
||||
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',)),
|
||||
(fix_ext_py(testmod.__file__), 'testmod', 'func')): 1,
|
||||
}
|
||||
self.assertEqual(self.tracer.results().callers, expected)
|
||||
|
||||
|
||||
# Created separately for issue #3821
|
||||
class TestCoverage(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
rmtree(TESTFN)
|
||||
|
@ -34,7 +310,6 @@ class TestCoverage(unittest.TestCase):
|
|||
trace=0, count=1)
|
||||
with captured_stdout() as stdout:
|
||||
self._coverage(tracer)
|
||||
self.assertEquals(stdout.getvalue(), "")
|
||||
if os.path.exists(TESTFN):
|
||||
files = os.listdir(TESTFN)
|
||||
self.assertEquals(files, [])
|
||||
|
@ -43,5 +318,6 @@ class TestCoverage(unittest.TestCase):
|
|||
def test_main():
|
||||
run_unittest(__name__)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_main()
|
||||
|
|
12
Lib/trace.py
12
Lib/trace.py
|
@ -57,7 +57,7 @@ import threading
|
|||
import time
|
||||
import token
|
||||
import tokenize
|
||||
import types
|
||||
import inspect
|
||||
import gc
|
||||
|
||||
import pickle
|
||||
|
@ -395,7 +395,7 @@ def find_lines(code, strs):
|
|||
|
||||
# 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
|
||||
|
@ -544,7 +544,7 @@ class Trace:
|
|||
## 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, "__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."
|
||||
|
@ -556,17 +556,13 @@ class Trace:
|
|||
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
|
||||
|
|
|
@ -145,6 +145,8 @@ Tools/Demos
|
|||
Tests
|
||||
-----
|
||||
|
||||
- Issue #9315: Added tests for the trace module. Patch by Eli Bendersky.
|
||||
|
||||
- Issue #9323: Make test.regrtest.__file__ absolute, this was not always the
|
||||
case when running profile or trace, for example.
|
||||
|
||||
|
@ -2070,6 +2072,9 @@ Library
|
|||
- Issue #8235: _socket: Add the constant ``SO_SETFIB``. SO_SETFIB is a socket
|
||||
option available on FreeBSD 7.1 and newer.
|
||||
|
||||
- Issue #9315: Fix for the trace module to record correct class name
|
||||
for tracing methods.
|
||||
|
||||
Extension Modules
|
||||
-----------------
|
||||
|
||||
|
|
Loading…
Reference in New Issue