|
7 | 7 | import io |
8 | 8 | import os |
9 | 9 | import pickle |
| 10 | +import subprocess |
10 | 11 | import sys |
11 | 12 | import textwrap |
12 | 13 | import types |
| 14 | +from pathlib import Path |
13 | 15 | from tempfile import TemporaryDirectory |
14 | 16 | import pytest |
| 17 | +from ubelt import ChDir |
15 | 18 | from line_profiler import _line_profiler, LineProfiler, LineStats |
16 | 19 |
|
17 | 20 |
|
@@ -989,6 +992,94 @@ def func(n): |
989 | 992 | assert entries[-2][1] == 10 + 20 |
990 | 993 |
|
991 | 994 |
|
| 995 | +def test_nonprofiled_clashing_bytecodes(tmp_path_factory): |
| 996 | + """ |
| 997 | + Test that the profiler can distinguish between a profiled function |
| 998 | + and a non-profiled one compiling down to the same bytecode. |
| 999 | + """ |
| 1000 | + # See issue #424 |
| 1001 | + template = textwrap.dedent(""" |
| 1002 | + def {}(n): # Any function using this compiles to the same bytecode |
| 1003 | + x = 0 |
| 1004 | + for n in range(1, n + 1): |
| 1005 | + x += n |
| 1006 | + return x |
| 1007 | + """).strip('\n') |
| 1008 | + module_name = 'my_module' |
| 1009 | + script_name = 'my-script.py' |
| 1010 | + outfile = 'out.lprof' |
| 1011 | + func_p = 'profiled_func' |
| 1012 | + func_no_p = 'nonprofiled_func' |
| 1013 | + |
| 1014 | + # Note: bytecode padding depends on the existence of duplicates, |
| 1015 | + # which are counted throughout the lifetime of the `LineProfiler` |
| 1016 | + # class. To ensure that we start on a clean slate -- that |
| 1017 | + # `LineProfiler` isn't "polluted" by running prior tests -- run the |
| 1018 | + # profliing in a subprocess. |
| 1019 | + with ChDir(tmp_path_factory.mktemp('test_nonprofiled_clashing_bytecodes')): |
| 1020 | + syspath_annex = (Path.cwd() / 'syspath').resolve() |
| 1021 | + syspath_annex.mkdir() |
| 1022 | + with (syspath_annex / (module_name + '.py')).open('w') as fobj: |
| 1023 | + print( |
| 1024 | + textwrap.dedent(""" |
| 1025 | + ''' |
| 1026 | + This docstring is fluff to make the function definitions overlap in |
| 1027 | + line numbers. |
| 1028 | + ''' |
| 1029 | +
|
| 1030 | +
|
| 1031 | + {fp_def} |
| 1032 | + """).strip('\n').format(fp_def=template.format(func_p)), |
| 1033 | + file=fobj, |
| 1034 | + ) |
| 1035 | + with open(script_name, 'w') as fobj: |
| 1036 | + print( |
| 1037 | + textwrap.dedent(""" |
| 1038 | + from line_profiler import LineProfiler |
| 1039 | + from {mod} import {fp_name} |
| 1040 | +
|
| 1041 | +
|
| 1042 | + {fnp_def} |
| 1043 | +
|
| 1044 | +
|
| 1045 | + if __name__ == '__main__': |
| 1046 | + prof = LineProfiler() |
| 1047 | + prof.add_callable({fp_name}) |
| 1048 | + with prof: |
| 1049 | + # The context turns on profiling for the call, but it |
| 1050 | + # shouldn't do anything since the imported and profiled |
| 1051 | + # function is not called |
| 1052 | + {fnp_name}(10) |
| 1053 | + prof.dump_stats({out!r}) |
| 1054 | + """).strip('\n').format( |
| 1055 | + mod=module_name, |
| 1056 | + fp_name=func_p, |
| 1057 | + fnp_name=func_no_p, |
| 1058 | + fnp_def=template.format(func_no_p), |
| 1059 | + out=outfile, |
| 1060 | + ), |
| 1061 | + file=fobj, |
| 1062 | + ) |
| 1063 | + |
| 1064 | + syspath = os.environ.get('PYTHONPATH', '') |
| 1065 | + syspath = ('{}:{}' if syspath else '{}').format(syspath_annex, syspath) |
| 1066 | + subprocess.run( |
| 1067 | + [sys.executable, script_name], |
| 1068 | + check=True, env={**os.environ, 'PYTHONPATH': syspath}, |
| 1069 | + ) |
| 1070 | + |
| 1071 | + assert os.path.exists(outfile) |
| 1072 | + stats = LineStats.from_files(outfile) |
| 1073 | + stats.print() # For debugging purposes |
| 1074 | + |
| 1075 | + # There should only be one function profiled (`profiled_func()`), |
| 1076 | + # which however doesn't have any actual data because it was never |
| 1077 | + # called |
| 1078 | + ((*_, func_name), data), = stats.timings.items() |
| 1079 | + assert (func_name == func_p), stats |
| 1080 | + assert (not data), stats |
| 1081 | + |
| 1082 | + |
992 | 1083 | @pytest.mark.parametrize('force_same_line_numbers', [True, False]) |
993 | 1084 | @pytest.mark.parametrize( |
994 | 1085 | 'ops', |
|
0 commit comments