@@ -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