@@ -604,3 +604,349 @@ def test_function_in_tests_dir():
604604 assert "vanilla_function" not in remaining_functions
605605 files_and_funcs = get_all_files_and_functions (module_root_path = temp_dir , ignore_paths = [])
606606 assert len (files_and_funcs ) == 6
607+
608+
609+ def test_filter_functions_tests_root_overlaps_source ():
610+ """Test that source files are not filtered when tests_root equals module_root or project_root.
611+
612+ This is a critical test for monorepo structures where tests live alongside source code
613+ (e.g., TypeScript projects with .test.ts files in the same directories as source).
614+ """
615+ with tempfile .TemporaryDirectory () as temp_dir_str :
616+ temp_dir = Path (temp_dir_str )
617+
618+ # Create a source file (NOT a test file)
619+ source_file = temp_dir / "utils.py"
620+ with source_file .open ("w" ) as f :
621+ f .write ("""
622+ def process_data(items):
623+ return [item * 2 for item in items]
624+
625+ def calculate_sum(numbers):
626+ return sum(numbers)
627+ """ )
628+
629+ # Create a test file with standard naming pattern
630+ test_file = temp_dir / "utils.test.py"
631+ with test_file .open ("w" ) as f :
632+ f .write ("""
633+ def test_process_data():
634+ return "test"
635+ """ )
636+
637+ # Create a test file with _test suffix pattern
638+ test_file_underscore = temp_dir / "utils_test.py"
639+ with test_file_underscore .open ("w" ) as f :
640+ f .write ("""
641+ def test_calculate_sum():
642+ return "test"
643+ """ )
644+
645+ # Create a spec file
646+ spec_file = temp_dir / "utils.spec.py"
647+ with spec_file .open ("w" ) as f :
648+ f .write ("""
649+ def spec_function():
650+ return "spec"
651+ """ )
652+
653+ # Create a file in a tests subdirectory
654+ tests_subdir = temp_dir / "tests"
655+ tests_subdir .mkdir ()
656+ tests_subdir_file = tests_subdir / "test_main.py"
657+ with tests_subdir_file .open ("w" ) as f :
658+ f .write ("""
659+ def test_in_tests_dir():
660+ return "test"
661+ """ )
662+
663+ # Create a file in __tests__ subdirectory (common in JS/TS projects)
664+ dunder_tests_subdir = temp_dir / "__tests__"
665+ dunder_tests_subdir .mkdir ()
666+ dunder_tests_file = dunder_tests_subdir / "main.py"
667+ with dunder_tests_file .open ("w" ) as f :
668+ f .write ("""
669+ def test_in_dunder_tests():
670+ return "test"
671+ """ )
672+
673+ # Discover all functions
674+ discovered_source = find_all_functions_in_file (source_file )
675+ discovered_test = find_all_functions_in_file (test_file )
676+ discovered_test_underscore = find_all_functions_in_file (test_file_underscore )
677+ discovered_spec = find_all_functions_in_file (spec_file )
678+ discovered_tests_dir = find_all_functions_in_file (tests_subdir_file )
679+ discovered_dunder_tests = find_all_functions_in_file (dunder_tests_file )
680+
681+ # Combine all discovered functions
682+ all_functions = {}
683+ for discovered in [discovered_source , discovered_test , discovered_test_underscore ,
684+ discovered_spec , discovered_tests_dir , discovered_dunder_tests ]:
685+ all_functions .update (discovered )
686+
687+ # Test Case 1: tests_root == module_root (overlapping case)
688+ # This is the bug scenario where all functions were being filtered
689+ with unittest .mock .patch (
690+ "codeflash.discovery.functions_to_optimize.get_blocklisted_functions" , return_value = {}
691+ ):
692+ filtered , count = filter_functions (
693+ all_functions ,
694+ tests_root = temp_dir , # Same as module_root
695+ ignore_paths = [],
696+ project_root = temp_dir ,
697+ module_root = temp_dir , # Same as tests_root
698+ )
699+
700+ # Strict check: only source_file should remain in filtered results
701+ assert set (filtered .keys ()) == {source_file }, (
702+ f"Expected only source file in filtered results, got: { set (filtered .keys ())} "
703+ )
704+
705+ # Strict check: exactly these two functions should be present
706+ source_functions = sorted ([fn .function_name for fn in filtered .get (source_file , [])])
707+ assert source_functions == ["calculate_sum" , "process_data" ], (
708+ f"Expected ['calculate_sum', 'process_data'], got { source_functions } "
709+ )
710+
711+ # Strict check: exactly 2 functions remaining
712+ assert count == 2 , f"Expected exactly 2 functions, got { count } "
713+
714+ # Test Case 2: tests_root == project_root (another overlapping case)
715+ with unittest .mock .patch (
716+ "codeflash.discovery.functions_to_optimize.get_blocklisted_functions" , return_value = {}
717+ ):
718+ filtered2 , count2 = filter_functions (
719+ {source_file : discovered_source [source_file ]},
720+ tests_root = temp_dir , # Same as project_root
721+ ignore_paths = [],
722+ project_root = temp_dir ,
723+ module_root = temp_dir ,
724+ )
725+
726+ # Strict check: only source_file should remain
727+ assert set (filtered2 .keys ()) == {source_file }, (
728+ f"Expected only source file when tests_root == project_root, got: { set (filtered2 .keys ())} "
729+ )
730+ assert count2 == 2 , f"Expected exactly 2 functions, got { count2 } "
731+
732+
733+ def test_filter_functions_strict_string_matching ():
734+ """Test that test file pattern matching uses strict string matching.
735+
736+ Ensures patterns like '.test.' only match actual test files and don't
737+ accidentally match files with similar names like 'contest.py' or 'latest.py'.
738+ """
739+ with tempfile .TemporaryDirectory () as temp_dir_str :
740+ temp_dir = Path (temp_dir_str )
741+
742+ # Files that should NOT be filtered (contain 'test' as substring but not as pattern)
743+ contest_file = temp_dir / "contest.py"
744+ with contest_file .open ("w" ) as f :
745+ f .write ("def run_contest(): return 1" )
746+
747+ latest_file = temp_dir / "latest.py"
748+ with latest_file .open ("w" ) as f :
749+ f .write ("def get_latest(): return 1" )
750+
751+ attestation_file = temp_dir / "attestation.py"
752+ with attestation_file .open ("w" ) as f :
753+ f .write ("def verify_attestation(): return 1" )
754+
755+ # File that SHOULD be filtered (matches .test. pattern)
756+ actual_test_file = temp_dir / "utils.test.py"
757+ with actual_test_file .open ("w" ) as f :
758+ f .write ("def test_utils(): return 1" )
759+
760+ # File that SHOULD be filtered (matches _test. pattern)
761+ underscore_test_file = temp_dir / "utils_test.py"
762+ with underscore_test_file .open ("w" ) as f :
763+ f .write ("def test_stuff(): return 1" )
764+
765+ # Discover all functions
766+ all_functions = {}
767+ for file_path in [contest_file , latest_file , attestation_file , actual_test_file , underscore_test_file ]:
768+ discovered = find_all_functions_in_file (file_path )
769+ all_functions .update (discovered )
770+
771+ with unittest .mock .patch (
772+ "codeflash.discovery.functions_to_optimize.get_blocklisted_functions" , return_value = {}
773+ ):
774+ filtered , count = filter_functions (
775+ all_functions ,
776+ tests_root = temp_dir , # Overlapping case to trigger pattern matching
777+ ignore_paths = [],
778+ project_root = temp_dir ,
779+ module_root = temp_dir ,
780+ )
781+
782+ # Strict check: exactly these 3 files should remain (those with 'test' as substring only)
783+ expected_files = {contest_file , latest_file , attestation_file }
784+ assert set (filtered .keys ()) == expected_files , (
785+ f"Expected files { expected_files } , got { set (filtered .keys ())} "
786+ )
787+
788+ # Strict check: each file should have exactly 1 function with the expected name
789+ assert [fn .function_name for fn in filtered [contest_file ]] == ["run_contest" ], (
790+ f"Expected ['run_contest'], got { [fn .function_name for fn in filtered [contest_file ]]} "
791+ )
792+ assert [fn .function_name for fn in filtered [latest_file ]] == ["get_latest" ], (
793+ f"Expected ['get_latest'], got { [fn .function_name for fn in filtered [latest_file ]]} "
794+ )
795+ assert [fn .function_name for fn in filtered [attestation_file ]] == ["verify_attestation" ], (
796+ f"Expected ['verify_attestation'], got { [fn .function_name for fn in filtered [attestation_file ]]} "
797+ )
798+
799+ # Strict check: exactly 3 functions remaining
800+ assert count == 3 , f"Expected exactly 3 functions, got { count } "
801+
802+
803+ def test_filter_functions_test_directory_patterns ():
804+ """Test that test directory patterns work correctly with strict matching.
805+
806+ Ensures that /test/, /tests/, and /__tests__/ patterns only match actual
807+ test directories and not directories that happen to contain 'test' in name.
808+ """
809+ with tempfile .TemporaryDirectory () as temp_dir_str :
810+ temp_dir = Path (temp_dir_str )
811+
812+ # Directory that should NOT be filtered (contains 'test' but not as /test/ pattern)
813+ contest_dir = temp_dir / "contest_results"
814+ contest_dir .mkdir ()
815+ contest_file = contest_dir / "scores.py"
816+ with contest_file .open ("w" ) as f :
817+ f .write ("def get_scores(): return [1, 2, 3]" )
818+
819+ latest_dir = temp_dir / "latest_data"
820+ latest_dir .mkdir ()
821+ latest_file = latest_dir / "data.py"
822+ with latest_file .open ("w" ) as f :
823+ f .write ("def load_data(): return {}" )
824+
825+ # Directory that SHOULD be filtered (matches /tests/ pattern)
826+ tests_dir = temp_dir / "tests"
827+ tests_dir .mkdir ()
828+ tests_file = tests_dir / "test_main.py"
829+ with tests_file .open ("w" ) as f :
830+ f .write ("def test_main(): return True" )
831+
832+ # Directory that SHOULD be filtered (matches /test/ pattern - singular)
833+ test_dir = temp_dir / "test"
834+ test_dir .mkdir ()
835+ test_file = test_dir / "test_utils.py"
836+ with test_file .open ("w" ) as f :
837+ f .write ("def test_utils(): return True" )
838+
839+ # Directory that SHOULD be filtered (matches /__tests__/ pattern)
840+ dunder_tests_dir = temp_dir / "__tests__"
841+ dunder_tests_dir .mkdir ()
842+ dunder_file = dunder_tests_dir / "component.py"
843+ with dunder_file .open ("w" ) as f :
844+ f .write ("def test_component(): return True" )
845+
846+ # Nested test directory
847+ src_dir = temp_dir / "src"
848+ src_dir .mkdir ()
849+ nested_tests_dir = src_dir / "tests"
850+ nested_tests_dir .mkdir ()
851+ nested_test_file = nested_tests_dir / "test_nested.py"
852+ with nested_test_file .open ("w" ) as f :
853+ f .write ("def test_nested(): return True" )
854+
855+ # Discover all functions
856+ all_functions = {}
857+ for file_path in [contest_file , latest_file , tests_file , test_file , dunder_file , nested_test_file ]:
858+ discovered = find_all_functions_in_file (file_path )
859+ all_functions .update (discovered )
860+
861+ with unittest .mock .patch (
862+ "codeflash.discovery.functions_to_optimize.get_blocklisted_functions" , return_value = {}
863+ ):
864+ filtered , count = filter_functions (
865+ all_functions ,
866+ tests_root = temp_dir , # Overlapping case
867+ ignore_paths = [],
868+ project_root = temp_dir ,
869+ module_root = temp_dir ,
870+ )
871+
872+ # Strict check: exactly these 2 files should remain (those in non-test directories)
873+ expected_files = {contest_file , latest_file }
874+ assert set (filtered .keys ()) == expected_files , (
875+ f"Expected files { expected_files } , got { set (filtered .keys ())} "
876+ )
877+
878+ # Strict check: each file should have exactly 1 function with the expected name
879+ assert [fn .function_name for fn in filtered [contest_file ]] == ["get_scores" ], (
880+ f"Expected ['get_scores'], got { [fn .function_name for fn in filtered [contest_file ]]} "
881+ )
882+ assert [fn .function_name for fn in filtered [latest_file ]] == ["load_data" ], (
883+ f"Expected ['load_data'], got { [fn .function_name for fn in filtered [latest_file ]]} "
884+ )
885+
886+ # Strict check: exactly 2 functions remaining
887+ assert count == 2 , f"Expected exactly 2 functions, got { count } "
888+
889+
890+ def test_filter_functions_non_overlapping_tests_root ():
891+ """Test that the original directory-based filtering still works when tests_root is separate.
892+
893+ When tests_root is a distinct directory (e.g., 'tests/'), the original behavior
894+ of filtering files that start with tests_root should still work.
895+ """
896+ with tempfile .TemporaryDirectory () as temp_dir_str :
897+ temp_dir = Path (temp_dir_str )
898+
899+ # Create source directory structure
900+ src_dir = temp_dir / "src"
901+ src_dir .mkdir ()
902+ source_file = src_dir / "utils.py"
903+ with source_file .open ("w" ) as f :
904+ f .write ("def process(): return 1" )
905+
906+ # Create a file with .test. pattern in source (should NOT be filtered in non-overlapping mode)
907+ # because directory-based filtering takes precedence
908+ test_in_src = src_dir / "helper.test.py"
909+ with test_in_src .open ("w" ) as f :
910+ f .write ("def helper_test(): return 1" )
911+
912+ # Create separate tests directory
913+ tests_dir = temp_dir / "tests"
914+ tests_dir .mkdir ()
915+ test_file = tests_dir / "test_utils.py"
916+ with test_file .open ("w" ) as f :
917+ f .write ("def test_process(): return 1" )
918+
919+ # Discover functions
920+ all_functions = {}
921+ for file_path in [source_file , test_in_src , test_file ]:
922+ discovered = find_all_functions_in_file (file_path )
923+ all_functions .update (discovered )
924+
925+ # Non-overlapping case: tests_root is a separate directory
926+ with unittest .mock .patch (
927+ "codeflash.discovery.functions_to_optimize.get_blocklisted_functions" , return_value = {}
928+ ):
929+ filtered , count = filter_functions (
930+ all_functions ,
931+ tests_root = tests_dir , # Separate from module_root
932+ ignore_paths = [],
933+ project_root = temp_dir ,
934+ module_root = src_dir , # Different from tests_root
935+ )
936+
937+ # Strict check: exactly these 2 files should remain (both in src/, not in tests/)
938+ expected_files = {source_file , test_in_src }
939+ assert set (filtered .keys ()) == expected_files , (
940+ f"Expected files { expected_files } , got { set (filtered .keys ())} "
941+ )
942+
943+ # Strict check: each file should have exactly 1 function with the expected name
944+ assert [fn .function_name for fn in filtered [source_file ]] == ["process" ], (
945+ f"Expected ['process'], got { [fn .function_name for fn in filtered [source_file ]]} "
946+ )
947+ assert [fn .function_name for fn in filtered [test_in_src ]] == ["helper_test" ], (
948+ f"Expected ['helper_test'], got { [fn .function_name for fn in filtered [test_in_src ]]} "
949+ )
950+
951+ # Strict check: exactly 2 functions remaining
952+ assert count == 2 , f"Expected exactly 2 functions, got { count } "
0 commit comments