@@ -741,6 +741,128 @@ def test_gecko_collector_basic(self):
741741 self .assertGreater (stack_table ["length" ], 0 )
742742 self .assertGreater (len (stack_table ["frame" ]), 0 )
743743
744+ def test_gecko_collector_resource_module_names (self ):
745+ """Test that GeckoCollector resolves resources to module names and
746+ distinguishes files with the same basename in different packages."""
747+ collector = GeckoCollector (1000 )
748+
749+ # 1. Package __init__.py files
750+ file_pkg_a = "pkg_a/__init__.py"
751+ file_pkg_b = "pkg_b/__init__.py"
752+
753+ # 2. Submodule names
754+ file_submod = "pkg_a/utils.py"
755+
756+ # 3. Standard library file (using collector._path_info)
757+ stdlib_dir = collector ._path_info .get ("stdlib" )
758+ file_stdlib = (
759+ str (stdlib_dir / "os.py" )
760+ if stdlib_dir
761+ else "/usr/lib/python3.15/os.py"
762+ )
763+
764+ # 4. Synthetic files
765+ file_stdin = "<stdin>"
766+ file_string = "<string>"
767+
768+ # 5. Unresolved external paths (absolute paths outside sys.path)
769+ file_ext_pkg = "/opt/external/deep/pkg_x/__init__.py"
770+ file_ext_tool = "/opt/external/deep/pkg_x/tool.py"
771+ file_root_tool = "/root_tool.py"
772+
773+ # 6. Windows-style backslash paths (relative and absolute)
774+ file_win_pkg = r"pkg_w\__init__.py"
775+ file_win_tool = r"pkg_w\utils.py"
776+ file_win_ext_pkg = r"C:\external\pkg_x\__init__.py"
777+ file_win_ext_tool = r"C:\external\pkg_x\tool.py"
778+ file_win_root_tool = r"C:\tool.py"
779+
780+ test_cases = [
781+ (file_pkg_a , 10 , "func_pkg_a" ),
782+ (file_pkg_b , 20 , "func_pkg_b" ),
783+ (file_submod , 30 , "func_submod" ),
784+ (file_stdlib , 40 , "func_stdlib" ),
785+ (file_stdin , 1 , "func_stdin" ),
786+ (file_string , 2 , "func_string" ),
787+ (file_ext_pkg , 10 , "func_ext_pkg" ),
788+ (file_ext_tool , 20 , "func_ext_tool" ),
789+ (file_root_tool , 30 , "func_root_tool" ),
790+ (file_win_pkg , 10 , "func_win_pkg" ),
791+ (file_win_tool , 20 , "func_win_tool" ),
792+ (file_win_ext_pkg , 10 , "func_win_ext_pkg" ),
793+ (file_win_ext_tool , 20 , "func_win_ext_tool" ),
794+ (file_win_root_tool , 30 , "func_win_root_tool" ),
795+ ]
796+
797+ frames = [MockFrameInfo (fn , line , name ) for fn , line , name in test_cases ]
798+ test_frames = [
799+ MockInterpreterInfo (
800+ 0 ,
801+ [
802+ MockThreadInfo (
803+ 1 ,
804+ frames ,
805+ status = THREAD_STATUS_MAIN_THREAD ,
806+ )
807+ ],
808+ )
809+ ]
810+ collector .collect (test_frames )
811+ profile_data = collector ._build_profile ()
812+
813+ string_array = profile_data ["shared" ]["stringArray" ]
814+ thread_data = profile_data ["threads" ][0 ]
815+ func_table = thread_data ["funcTable" ]
816+ resource_table = thread_data ["resourceTable" ]
817+
818+ self .assertEqual (resource_table ["length" ], len (resource_table ["name" ]))
819+ for r_type in resource_table ["type" ]:
820+ self .assertEqual (r_type , gecko_collector .RESOURCE_TYPE_LIBRARY )
821+
822+ funcs_by_name = {
823+ string_array [func_table ["name" ][i ]]: (
824+ func_table ["resource" ][i ],
825+ string_array [
826+ resource_table ["name" ][func_table ["resource" ][i ]]
827+ ],
828+ )
829+ for i in range (func_table ["length" ])
830+ }
831+
832+ # 1. Distinct resource table entries and names for duplicate packages
833+ res_idx_a , res_name_a = funcs_by_name ["func_pkg_a" ]
834+ res_idx_b , res_name_b = funcs_by_name ["func_pkg_b" ]
835+ self .assertNotEqual (res_idx_a , res_idx_b )
836+ self .assertEqual (res_name_a , "pkg_a" )
837+ self .assertEqual (res_name_b , "pkg_b" )
838+ name_idx_a = resource_table ["name" ][res_idx_a ]
839+ name_idx_b = resource_table ["name" ][res_idx_b ]
840+ self .assertNotEqual (name_idx_a , name_idx_b )
841+ self .assertEqual (string_array [name_idx_a ], "pkg_a" )
842+ self .assertEqual (string_array [name_idx_b ], "pkg_b" )
843+
844+ # 2. Submodule names resolve to dotted names
845+ self .assertEqual (funcs_by_name ["func_submod" ][1 ], "pkg_a.utils" )
846+
847+ # 3. Standard library resolves to exact module name
848+ self .assertEqual (funcs_by_name ["func_stdlib" ][1 ], "os" )
849+
850+ # 4. Synthetic files preserve sensible names
851+ self .assertEqual (funcs_by_name ["func_stdin" ][1 ], "<stdin>" )
852+ self .assertEqual (funcs_by_name ["func_string" ][1 ], "<string>" )
853+
854+ # 5. Unresolved external paths provide readable fallbacks
855+ self .assertEqual (funcs_by_name ["func_ext_pkg" ][1 ], "pkg_x" )
856+ self .assertEqual (funcs_by_name ["func_ext_tool" ][1 ], "pkg_x/tool.py" )
857+ self .assertEqual (funcs_by_name ["func_root_tool" ][1 ], "root_tool.py" )
858+
859+ # 6. Windows-style paths resolve platform-independently
860+ self .assertEqual (funcs_by_name ["func_win_pkg" ][1 ], "pkg_w" )
861+ self .assertEqual (funcs_by_name ["func_win_tool" ][1 ], "pkg_w.utils" )
862+ self .assertEqual (funcs_by_name ["func_win_ext_pkg" ][1 ], "pkg_x" )
863+ self .assertEqual (funcs_by_name ["func_win_ext_tool" ][1 ], "pkg_x/tool.py" )
864+ self .assertEqual (funcs_by_name ["func_win_root_tool" ][1 ], "tool.py" )
865+
744866 def test_gecko_collector_async_aware (self ):
745867 collector = GeckoCollector (1000 )
746868
0 commit comments