1010import pathlib
1111import sys
1212import traceback
13- from typing import TYPE_CHECKING , Any , Dict , Generator , Literal , TypedDict
13+ from typing import TYPE_CHECKING , Any , Dict , Generator , Literal , Protocol , TypedDict
1414
1515import pytest
1616
2525 USES_PYTEST_DESCRIBE = True
2626
2727
28+ class HasPathOrFspath (Protocol ):
29+ """Protocol defining objects that have either a path or fspath attribute."""
30+
31+ path : pathlib .Path | None = None
32+ fspath : Any | None = None
33+
34+
2835class TestData (TypedDict ):
2936 """A general class that all test objects inherit from."""
3037
@@ -522,11 +529,131 @@ def pytest_sessionfinish(session, exitstatus):
522529 send_message (payload )
523530
524531
532+ def construct_nested_folders (
533+ file_nodes_dict : dict [str , TestNode ],
534+ session_node : TestNode , # session_node['path'] is a pathlib.Path object
535+ session_children_dict : dict [str , TestNode ]
536+ ) -> dict [str , TestNode ]:
537+ """Iterate through all files and construct them into nested folders.
538+
539+ Keyword arguments:
540+ file_nodes_dict -- Dictionary of all file nodes
541+ session_node -- The session node that will be parent to the folder structure
542+ session_children_dict -- Dictionary of session's children nodes indexed by ID
543+
544+ Returns:
545+ dict[str, TestNode] -- Updated session_children_dict with folder nodes added
546+ """
547+ created_files_folders_dict : dict [str , TestNode ] = {}
548+ for file_node in file_nodes_dict .values ():
549+ # Iterate through all the files that exist and construct them into nested folders.
550+ root_folder_node : TestNode
551+ try :
552+ root_folder_node : TestNode = build_nested_folders (
553+ file_node , created_files_folders_dict , session_node
554+ )
555+ except ValueError :
556+ # This exception is raised when the session node is not a parent of the file node.
557+ print (
558+ "[vscode-pytest]: Session path not a parent of test paths, adjusting session node to common parent."
559+ )
560+ # IMPORTANT: Use session_node["path"] directly as it's already a pathlib.Path object
561+ # Do NOT use get_node_path(session_node["path"]) as get_node_path expects pytest objects,
562+ # not Path objects directly.
563+ common_parent = os .path .commonpath ([file_node ["path" ], session_node ["path" ]])
564+ common_parent_path = pathlib .Path (common_parent )
565+ print ("[vscode-pytest]: Session node now set to: " , common_parent )
566+ session_node ["path" ] = common_parent_path # pathlib.Path
567+ session_node ["id_" ] = common_parent # str
568+ session_node ["name" ] = common_parent_path .name # str
569+ root_folder_node = build_nested_folders (
570+ file_node , created_files_folders_dict , session_node
571+ )
572+ # The final folder we get to is the highest folder in the path
573+ # and therefore we add this as a child to the session.
574+ root_id = root_folder_node .get ("id_" )
575+ if root_id and root_id not in session_children_dict :
576+ session_children_dict [root_id ] = root_folder_node
577+
578+ return session_children_dict
579+
580+
581+ def process_parameterized_test (
582+ test_case : pytest .Item , # Must have callspec attribute (parameterized test)
583+ test_node : TestItem ,
584+ function_nodes_dict : dict [str , TestNode ],
585+ file_nodes_dict : dict [str , TestNode ]
586+ ) -> TestNode :
587+ """Process a parameterized test case and create appropriate function nodes.
588+
589+ Keyword arguments:
590+ test_case -- the parameterized pytest test case
591+ test_node -- the test node created from the test case
592+ function_nodes_dict -- dictionary of function nodes indexed by ID
593+ file_nodes_dict -- dictionary of file nodes indexed by path
594+
595+ Returns:
596+ TestNode -- the node to use for further processing (function node or original test node)
597+ """
598+ function_name : str = ""
599+ # parameterized test cases cut the repetitive part of the name off.
600+ parent_part , parameterized_section = test_node ["name" ].split ("[" , 1 )
601+ test_node ["name" ] = "[" + parameterized_section
602+
603+ first_split = test_case .nodeid .rsplit (
604+ "::" , 1
605+ ) # splits the parameterized test name from the rest of the nodeid
606+ second_split = first_split [0 ].rsplit (
607+ ".py" , 1
608+ ) # splits the file path from the rest of the nodeid
609+
610+ class_and_method = second_split [1 ] + "::" # This has "::" separator at both ends
611+ # construct the parent id, so it is absolute path :: any class and method :: parent_part
612+ parent_id = os .fspath (get_node_path (test_case )) + class_and_method + parent_part
613+
614+ try :
615+ function_name = test_case .originalname # type: ignore
616+ function_test_node = function_nodes_dict [parent_id ]
617+ except AttributeError : # actual error has occurred
618+ ERRORS .append (
619+ f"unable to find original name for { test_case .name } with parameterization detected."
620+ )
621+ raise VSCodePytestError (
622+ "Unable to find original name for parameterized test case"
623+ ) from None
624+ except KeyError :
625+ function_test_node : TestNode = create_parameterized_function_node (
626+ function_name , get_node_path (test_case ), parent_id
627+ )
628+ function_nodes_dict [parent_id ] = function_test_node
629+
630+ if test_node not in function_test_node ["children" ]:
631+ function_test_node ["children" ].append (test_node )
632+
633+ # Check if the parent node of the function is file, if so create/add to this file node.
634+ if isinstance (test_case .parent , pytest .File ):
635+ # calculate the parent path of the test case
636+ parent_path = get_node_path (test_case .parent )
637+ try :
638+ parent_test_case = file_nodes_dict [os .fspath (parent_path )]
639+ except KeyError :
640+ parent_test_case = create_file_node (parent_path )
641+ file_nodes_dict [os .fspath (parent_path )] = parent_test_case
642+ if function_test_node not in parent_test_case ["children" ]:
643+ parent_test_case ["children" ].append (function_test_node )
644+
645+ # Return the function node as the test node to handle subsequent nesting
646+ return function_test_node
647+
648+
525649def build_test_tree (session : pytest .Session ) -> TestNode :
526650 """Builds a tree made up of testing nodes from the pytest session.
527651
528652 Keyword arguments:
529- session -- the pytest session object.
653+ session -- the pytest session object that contains test items.
654+
655+ Returns:
656+ TestNode -- The root node of the constructed test tree.
530657 """
531658 session_node = create_session_node (session )
532659 session_children_dict : dict [str , TestNode ] = {}
@@ -542,54 +669,8 @@ def build_test_tree(session: pytest.Session) -> TestNode:
542669 for test_case in session .items :
543670 test_node = create_test_node (test_case )
544671 if hasattr (test_case , "callspec" ): # This means it is a parameterized test.
545- function_name : str = ""
546- # parameterized test cases cut the repetitive part of the name off.
547- parent_part , parameterized_section = test_node ["name" ].split ("[" , 1 )
548- test_node ["name" ] = "[" + parameterized_section
549-
550- first_split = test_case .nodeid .rsplit (
551- "::" , 1
552- ) # splits the parameterized test name from the rest of the nodeid
553- second_split = first_split [0 ].rsplit (
554- ".py" , 1
555- ) # splits the file path from the rest of the nodeid
556-
557- class_and_method = second_split [1 ] + "::" # This has "::" separator at both ends
558- # construct the parent id, so it is absolute path :: any class and method :: parent_part
559- parent_id = os .fspath (get_node_path (test_case )) + class_and_method + parent_part
560- # file, middle, param = test_case.nodeid.rsplit("::", 2)
561- # parent_id = test_case.nodeid.rsplit("::", 1)[0] + "::" + parent_part
562- # parent_path = os.fspath(get_node_path(test_case)) + "::" + parent_part
563- try :
564- function_name = test_case .originalname # type: ignore
565- function_test_node = function_nodes_dict [parent_id ]
566- except AttributeError : # actual error has occurred
567- ERRORS .append (
568- f"unable to find original name for { test_case .name } with parameterization detected."
569- )
570- raise VSCodePytestError (
571- "Unable to find original name for parameterized test case"
572- ) from None
573- except KeyError :
574- function_test_node : TestNode = create_parameterized_function_node (
575- function_name , get_node_path (test_case ), parent_id
576- )
577- function_nodes_dict [parent_id ] = function_test_node
578- if test_node not in function_test_node ["children" ]:
579- function_test_node ["children" ].append (test_node )
580- # Check if the parent node of the function is file, if so create/add to this file node.
581- if isinstance (test_case .parent , pytest .File ):
582- # calculate the parent path of the test case
583- parent_path = get_node_path (test_case .parent )
584- try :
585- parent_test_case = file_nodes_dict [os .fspath (parent_path )]
586- except KeyError :
587- parent_test_case = create_file_node (parent_path )
588- file_nodes_dict [os .fspath (parent_path )] = parent_test_case
589- if function_test_node not in parent_test_case ["children" ]:
590- parent_test_case ["children" ].append (function_test_node )
591- # If the parent is not a file, it is a class, add the function node as the test node to handle subsequent nesting.
592- test_node = function_test_node
672+ # Process parameterized test and get the function node to use for further processing
673+ test_node = process_parameterized_test (test_case , test_node , function_nodes_dict , file_nodes_dict )
593674 if isinstance (test_case .parent , pytest .Class ) or (
594675 USES_PYTEST_DESCRIBE and isinstance (test_case .parent , DescribeBlock )
595676 ):
@@ -636,41 +717,16 @@ def build_test_tree(session: pytest.Session) -> TestNode:
636717 parent_test_case = create_file_node (parent_path )
637718 file_nodes_dict [os .fspath (parent_path )] = parent_test_case
638719 parent_test_case ["children" ].append (test_node )
639- created_files_folders_dict : dict [str , TestNode ] = {}
640- for file_node in file_nodes_dict .values ():
641- # Iterate through all the files that exist and construct them into nested folders.
642- root_folder_node : TestNode
643- try :
644- root_folder_node : TestNode = build_nested_folders (
645- file_node , created_files_folders_dict , session_node
646- )
647- except ValueError :
648- # This exception is raised when the session node is not a parent of the file node.
649- print (
650- "[vscode-pytest]: Session path not a parent of test paths, adjusting session node to common parent."
651- )
652- common_parent = os .path .commonpath ([file_node ["path" ], get_node_path (session )])
653- common_parent_path = pathlib .Path (common_parent )
654- print ("[vscode-pytest]: Session node now set to: " , common_parent )
655- session_node ["path" ] = common_parent_path # pathlib.Path
656- session_node ["id_" ] = common_parent # str
657- session_node ["name" ] = common_parent_path .name # str
658- root_folder_node = build_nested_folders (
659- file_node , created_files_folders_dict , session_node
660- )
661- # The final folder we get to is the highest folder in the path
662- # and therefore we add this as a child to the session.
663- root_id = root_folder_node .get ("id_" )
664- if root_id and root_id not in session_children_dict :
665- session_children_dict [root_id ] = root_folder_node
720+ # Process all files and construct them into nested folders
721+ session_children_dict = construct_nested_folders (file_nodes_dict , session_node , session_children_dict )
666722 session_node ["children" ] = list (session_children_dict .values ())
667723 return session_node
668724
669725
670726def build_nested_folders (
671- file_node : TestNode ,
672- created_files_folders_dict : dict [str , TestNode ],
673- session_node : TestNode ,
727+ file_node : TestNode , # A file node to build folders for
728+ created_files_folders_dict : dict [str , TestNode ], # Cache of created folders indexed by path
729+ session_node : TestNode , # The session node containing path information
674730) -> TestNode :
675731 """Takes a file or folder and builds the nested folder structure for it.
676732
@@ -851,10 +907,17 @@ class CoveragePayloadDict(Dict):
851907 error : str | None # Currently unused need to check
852908
853909
854- def get_node_path (node : Any ) -> pathlib .Path :
910+ def get_node_path (node : pytest . Session | pytest . Item | pytest . File | pytest . Class | pytest . Module | HasPathOrFspath ) -> pathlib .Path :
855911 """A function that returns the path of a node given the switch to pathlib.Path.
856912
857913 It also evaluates if the node is a symlink and returns the equivalent path.
914+
915+ Parameters:
916+ node: A pytest object or any object that has a path or fspath attribute.
917+ Do NOT pass a pathlib.Path object directly; use it directly instead.
918+
919+ Returns:
920+ pathlib.Path: The resolved path for the node.
858921 """
859922 node_path = getattr (node , "path" , None ) or pathlib .Path (node .fspath )
860923
0 commit comments