Skip to content

Commit 703dd97

Browse files
committed
Test for decoration by multiple profilers
tests/test_line_profiler.py test_multiple_profilers_identical_bytecode() New test for applying multiple profilers to identical functions in arbitrary order (one subtest currently failing)
1 parent cdf0c2e commit 703dd97

1 file changed

Lines changed: 152 additions & 0 deletions

File tree

tests/test_line_profiler.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,3 +815,155 @@ def func(n):
815815
# (Entries are represented as tuples `(lineno, nhits, time)`)
816816
entries, = profile.get_stats().timings.values()
817817
assert entries[-2][1] == 10 + 20
818+
819+
820+
@pytest.mark.parametrize('force_same_line_numbers', [True, False])
821+
@pytest.mark.parametrize(
822+
'ops',
823+
[
824+
# Replication of the problematic case in issue #350
825+
'func1:prof_all'
826+
'-func2:prof_some:prof_all'
827+
'-func3:prof_all'
828+
'-func4:prof_some:prof_all',
829+
# Invert the order of decoration
830+
'func1:prof_all'
831+
'-func2:prof_all:prof_some'
832+
'-func3:prof_all'
833+
'-func4:prof_all:prof_some',
834+
# More profiler stacks
835+
'func1:p1:p2'
836+
'-func2:p2:p3'
837+
'-func3:p3:p4'
838+
'-func4:p4:p1',
839+
'func1:p1:p2:p3'
840+
'-func2:p2:p3:p4'
841+
'-func3:p3:p4:p1'
842+
'-func4:p4:p1:p2',
843+
'func1:p1:p2:p3'
844+
'-func2:p4:p3:p2'
845+
'-func3:p3:p4:p1'
846+
'-func4:p2:p1:p4',
847+
# Misc. edge cases
848+
# - Note: while the following results in `func1()` and `func2()`
849+
# sharing the same bytecodes, the profiler `p3` is nonetheless
850+
# able to distinguish between the two (when the functions have
851+
# distinct line numbers), because they are defined on
852+
# different lines and thus hashed to different line hashes
853+
'func1:p1:p2' # `func1()` padded once
854+
'-func2:p3' # `func2()` padded twice
855+
'-func1:p4:p3', # `func1()` padded once (again)
856+
])
857+
def test_multiple_profilers_identical_bytecode(
858+
tmp_path, ops, force_same_line_numbers):
859+
"""
860+
Test that functions compiling down to the same bytecode are
861+
correctly handled between multiple profilers.
862+
863+
Notes
864+
-----
865+
`ops` should consist of chunks joined by hyphens, where each chunk
866+
has the format `<func_id>:<prof_name>[:<prof_name>[...]]`,
867+
indicating that the profilers are to be used in order to decorate
868+
the specified function.
869+
"""
870+
def check_seen(name, output, func_id, expected):
871+
lines = [line for line in output.splitlines()
872+
if line.startswith('Function: ')]
873+
if any(func_id in line for line in lines) == expected:
874+
return
875+
if expected:
876+
raise AssertionError(
877+
f'profiler `@{name}` didn\'t see `{func_id}()`')
878+
raise AssertionError(
879+
f'profiler `@{name}` saw `{func_id}()`')
880+
881+
def check_has_profiling_data(name, output, func_id, expected):
882+
assert func_id.startswith('func')
883+
nloops = func_id[len('func'):]
884+
try:
885+
line = next(line for line in output.splitlines()
886+
if line.endswith(f'result.append({nloops})'))
887+
except StopIteration:
888+
if expected:
889+
raise AssertionError(
890+
f'profiler `@{name}` didn\'t see `{func_id}()`')
891+
else:
892+
return
893+
if (line.split()[1] == nloops) == expected:
894+
return
895+
if expected:
896+
raise AssertionError(
897+
f'profiler `@{name}` didn\'t get data from `{func_id}()`')
898+
raise AssertionError(
899+
f'profiler `@{name}` got data from `{func_id}()`')
900+
901+
if force_same_line_numbers:
902+
funcs = {}
903+
pattern = textwrap.dedent("""
904+
def func{0}():
905+
result = []
906+
for _ in range({0}):
907+
result.append({0})
908+
return result
909+
""").strip('\n')
910+
for i in [1, 2, 3, 4]:
911+
tempfile = tmp_path / f'func{i}.py'
912+
source = pattern.format(i)
913+
tempfile.write_text(source)
914+
exec(compile(source, str(tempfile), 'exec'), funcs)
915+
else:
916+
def func1():
917+
result = []
918+
for _ in range(1):
919+
result.append(1)
920+
return result
921+
922+
def func2():
923+
result = []
924+
for _ in range(2):
925+
result.append(2)
926+
return result
927+
928+
def func3():
929+
result = []
930+
for _ in range(3):
931+
result.append(3)
932+
return result
933+
934+
def func4():
935+
result = []
936+
for _ in range(4):
937+
result.append(4)
938+
return result
939+
940+
funcs = {'func1': func1, 'func2': func2,
941+
'func3': func3, 'func4': func4}
942+
943+
# Apply the decorators in order
944+
all_dec_names = {}
945+
all_profs = {}
946+
for op in ops.split('-'):
947+
func_id, *profs = op.split(':')
948+
all_dec_names.setdefault(func_id, set()).update(profs)
949+
for name in profs:
950+
try:
951+
prof = all_profs[name]
952+
except KeyError:
953+
prof = all_profs[name] = LineProfiler()
954+
funcs[func_id] = prof(funcs[func_id])
955+
# Call each function once
956+
assert funcs['func1']() == [1]
957+
assert funcs['func2']() == [2, 2]
958+
assert funcs['func3']() == [3, 3, 3]
959+
assert funcs['func4']() == [4, 4, 4, 4]
960+
# Check the profiling results
961+
for name, prof in sorted(all_profs.items()):
962+
with io.StringIO() as sio:
963+
prof.print_stats(sio, summarize=True)
964+
output = sio.getvalue()
965+
print(f'@{name}:', textwrap.indent(output, ' '), sep='\n\n')
966+
for func_id, decs in all_dec_names.items():
967+
profiled = name in decs
968+
check_seen(name, output, func_id, profiled)
969+
check_has_profiling_data(name, output, func_id, profiled)

0 commit comments

Comments
 (0)