Skip to content

Commit 8634d73

Browse files
committed
add tests for src directory equals test dir
1 parent 6ab7448 commit 8634d73

1 file changed

Lines changed: 346 additions & 0 deletions

File tree

tests/test_function_discovery.py

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)