Skip to content

Commit 654121f

Browse files
committed
Test for profiling data preservation
tests/test_line_profiler.py get_prof_stats() New helper function for formatting and retrieving `LineProfiler.print_stats()` output test_*_decorator() test_profile_generated_code() test_multiple_profilers_identical_bytecode() Refactored to use `get_prof_stats()` test_aggregate_profiling_data_between_code_versions() New test checking that profiling data gathered before and after a function's code object is rewritten are correctly aggregated
1 parent d118805 commit 654121f

1 file changed

Lines changed: 52 additions & 37 deletions

File tree

tests/test_line_profiler.py

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ def strip(s):
4141
return textwrap.dedent(s).strip('\n')
4242

4343

44+
def get_prof_stats(prof, name='prof', **kwargs):
45+
with io.StringIO() as sio:
46+
prof.print_stats(sio, **kwargs)
47+
output = sio.getvalue()
48+
print(f'@{name}:', textwrap.indent(output, ' '), sep='\n\n')
49+
return output
50+
51+
4452
class check_timings:
4553
"""
4654
Verify that the profiler starts without timing data and ends with
@@ -252,10 +260,7 @@ def foo(cls) -> str:
252260
assert profile.enable_count == 0
253261
assert len(profile.functions) == 1
254262
assert Object.foo() == Object().foo() == 'ObjectObject'
255-
with io.StringIO() as sio:
256-
profile.print_stats(stream=sio, summarize=True)
257-
output = strip(sio.getvalue())
258-
print(output)
263+
output = strip(get_prof_stats(profile, name='profile', summarize=True))
259264
# Check that we have profiled `Object.foo()`
260265
assert output.endswith('foo')
261266
line, = (line for line in output.splitlines() if line.endswith('* 2'))
@@ -286,10 +291,7 @@ def foo(x: int) -> int:
286291
assert profile.enable_count == 0
287292
assert len(profile.functions) == 1
288293
assert Object.foo(3) == Object().foo(3) == 6
289-
with io.StringIO() as sio:
290-
profile.print_stats(stream=sio, summarize=True)
291-
output = strip(sio.getvalue())
292-
print(output)
294+
output = strip(get_prof_stats(profile, name='profile', summarize=True))
293295
# Check that we have profiled `Object.foo()`
294296
assert output.endswith('foo')
295297
line, = (line for line in output.splitlines() if line.endswith('* 2'))
@@ -327,10 +329,7 @@ def foo(self, x: int) -> int:
327329
== profiled_foo_2(2)
328330
== obj.foo(2)
329331
== id(obj) * 2)
330-
with io.StringIO() as sio:
331-
profile.print_stats(stream=sio, summarize=True)
332-
output = strip(sio.getvalue())
333-
print(output)
332+
output = strip(get_prof_stats(profile, name='profile', summarize=True))
334333
# Check that we have profiled `Object.foo()`
335334
assert output.endswith('foo')
336335
line, = (line for line in output.splitlines() if line.endswith('* x'))
@@ -363,10 +362,7 @@ def foo(self, x: int) -> int:
363362
assert profile.enable_count == 0
364363
assert profile.functions == [Object.foo]
365364
assert obj.foo(1) == obj.bar() == id(obj)
366-
with io.StringIO() as sio:
367-
profile.print_stats(stream=sio, summarize=True)
368-
output = strip(sio.getvalue())
369-
print(output)
365+
output = strip(get_prof_stats(profile, name='profile', summarize=True))
370366
# Check that we have profiled `Object.foo()` (via `.bar()`)
371367
assert output.endswith('foo')
372368
line, = (line for line in output.splitlines() if line.endswith('* x'))
@@ -403,10 +399,7 @@ def foo(x: int, y: int) -> int:
403399
== bar(3)
404400
== foo(2, 3)
405401
== 5)
406-
with io.StringIO() as sio:
407-
profile.print_stats(stream=sio, summarize=True)
408-
output = strip(sio.getvalue())
409-
print(output)
402+
output = strip(get_prof_stats(profile, name='profile', summarize=True))
410403
# Check that we have profiled `foo()`
411404
assert output.endswith('foo')
412405
line, = (line for line in output.splitlines() if line.endswith('x + y'))
@@ -452,10 +445,7 @@ def foo(self, foo) -> None:
452445
obj.foo = 10 # Use setter
453446
assert obj.x == 5
454447
assert obj.foo == 10 # Use getter
455-
with io.StringIO() as sio:
456-
profile.print_stats(stream=sio, summarize=True)
457-
output = strip(sio.getvalue())
458-
print(output)
448+
output = strip(get_prof_stats(profile, name='profile', summarize=True))
459449
# Check that we have profiled `Object.foo`
460450
assert output.endswith('foo')
461451
getter_line, = (line for line in output.splitlines()
@@ -495,9 +485,7 @@ def foo(self) -> int:
495485
obj = Object(3)
496486
assert obj.foo == 6 # Use getter
497487
assert obj.foo == 6 # Getter not called because it's cached
498-
with io.StringIO() as sio:
499-
profile.print_stats(stream=sio, summarize=True)
500-
output = strip(sio.getvalue())
488+
output = strip(get_prof_stats(profile, name='profile', summarize=True))
501489
# Check that we have profiled `Object.foo`
502490
assert output.endswith('foo')
503491
line, = (line for line in output.splitlines() if line.endswith('* 2'))
@@ -713,10 +701,7 @@ def test_profile_generated_code():
713701
profiled_fn = profiler(fn)
714702
profiled_fn()
715703

716-
import io
717-
s = io.StringIO()
718-
profiler.print_stats(stream=s)
719-
output = s.getvalue()
704+
output = get_prof_stats(profiler, 'profiler')
720705

721706
# Check that the output contains the generated code's source lines
722707
for line in code_lines:
@@ -900,13 +885,13 @@ def check_has_profiling_data(name, output, func_id, expected):
900885

901886
if force_same_line_numbers:
902887
funcs = {}
903-
pattern = textwrap.dedent("""
888+
pattern = strip("""
904889
def func{0}():
905890
result = []
906891
for _ in range({0}):
907892
result.append({0})
908893
return result
909-
""").strip('\n')
894+
""")
910895
for i in [1, 2, 3, 4]:
911896
tempfile = tmp_path / f'func{i}.py'
912897
source = pattern.format(i)
@@ -964,11 +949,41 @@ def func4():
964949
for func in profiled_funcs}) == len(profiled_funcs)
965950
# Check the profiling results
966951
for name, prof in sorted(all_profs.items()):
967-
with io.StringIO() as sio:
968-
prof.print_stats(sio, summarize=True)
969-
output = sio.getvalue()
970-
print(f'@{name}:', textwrap.indent(output, ' '), sep='\n\n')
952+
output = get_prof_stats(prof, name=name, summarize=True)
971953
for func_id, decs in all_dec_names.items():
972954
profiled = name in decs
973955
check_seen(name, output, func_id, profiled)
974956
check_has_profiling_data(name, output, func_id, profiled)
957+
958+
959+
def test_aggregate_profiling_data_between_code_versions():
960+
"""
961+
Test that profiling data from previous versions of the code object
962+
are preserved when another profiler causes the code object of a
963+
function to be overwritten.
964+
"""
965+
def func(n):
966+
x = 0
967+
for n in range(1, n + 1):
968+
x += n
969+
return x
970+
971+
prof1 = LineProfiler()
972+
prof2 = LineProfiler()
973+
974+
# Gather data with `@prof1`
975+
wrapper1 = prof1(func)
976+
assert wrapper1(10) == 10 * 11 // 2
977+
code = func.__code__
978+
# Gather data with `@prof2`; the code object is overwritten here
979+
wrapper2 = prof2(wrapper1)
980+
assert func.__code__ != code
981+
assert wrapper2(15) == 15 * 16 // 2
982+
# Despite the overwrite of the code object, the old data should
983+
# still remain, and be aggregated with the new data when calling
984+
# `prof1.get_stats()`
985+
for prof, name, count in (prof1, 'prof1', 25), (prof2, 'prof2', 15):
986+
result = get_prof_stats(prof, name)
987+
loop_body = next(line for line in result.splitlines()
988+
if line.endswith('x += n'))
989+
assert loop_body.split()[1] == str(count)

0 commit comments

Comments
 (0)