1+ # Tests for discover_tests_candidates
2+ # Framework: pytest (preferred) with standard assertions and parametrization.
3+ # If your project uses unittest, these tests can be adapted to TestCase easily.
4+
5+ import os
6+ import sys
7+ from pathlib import Path
8+
9+ import pytest
10+
11+ try :
12+ # Try common import paths; adjust as needed if your module path differs.
13+ from discover import discover_tests_candidates
14+ except ImportError :
15+ try :
16+ from discovery import discover_tests_candidates
17+ except ImportError as err :
18+ # Fallback to project-local packages; this keeps tests resilient across refactors.
19+ from importlib import import_module as _imp
20+ _candidates = None
21+ for mod in (
22+ "src.discover" ,
23+ "src.discovery" ,
24+ "project.discover" ,
25+ "project.discovery" ,
26+ "tools.discover" ,
27+ ):
28+ try :
29+ _m = _imp (mod )
30+ _candidates = getattr (_m , "discover_tests_candidates" , None )
31+ if _candidates :
32+ discover_tests_candidates = _candidates # type: ignore
33+ break
34+ except ImportError :
35+ continue
36+ if not _candidates :
37+ raise ImportError () from err
38+
39+
40+ def _touch (p : Path , text : str = "" ) -> None :
41+ p .parent .mkdir (parents = True , exist_ok = True )
42+ p .write_text (text , encoding = "utf-8" )
43+
44+
45+ @pytest .fixture
46+ def tmp_tree (tmp_path : Path ):
47+ # Create a representative test tree with various files and patterns
48+ files = {
49+ "tests/test_alpha.py" : "def test_a(): pass\n " ,
50+ "tests/test_beta.py" : "def test_b(): pass\n " ,
51+ "tests/helpers/util.py" : "def helper(): pass\n " ,
52+ "package/module_test.py" : "def test_c(): pass\n " ,
53+ "package/not_a_test.txt" : "content\n " ,
54+ "nested/deep/test_gamma.py" : "def test_g(): pass\n " ,
55+ "nested/deep/_private_test.py" : "def test_p(): pass\n " ,
56+ "nested/deep/test_🌟.py" : "def test_unicode(): pass\n " ,
57+ "nested/deep/__init__.py" : "" ,
58+ "README.md" : "# docs\n " ,
59+ }
60+ for rel , content in files .items ():
61+ _touch (tmp_path / rel , content )
62+ return tmp_path
63+
64+
65+ def norm (p : Path ) -> str :
66+ return str (p ).replace ("\\ " , "/" )
67+
68+
69+ @pytest .mark .parametrize (
70+ "inputs,expected_contains,expected_absent" ,
71+ [
72+ # Happy path: standard tests/ directory with test_*.py
73+ (["tests/" ], ["tests/test_alpha.py" , "tests/test_beta.py" ], ["tests/helpers/util.py" ]),
74+ # Glob patterns
75+ (["**/test_*.py" ], ["tests/test_alpha.py" , "nested/deep/test_gamma.py" ], ["package/not_a_test.txt" ]),
76+ # Mixed files and dirs
77+ (["tests/test_beta.py" , "nested/" ], ["tests/test_beta.py" , "nested/deep/test_gamma.py" ], ["package/module_test.py" ]),
78+ # File with unicode name
79+ (["nested/deep/" ], ["nested/deep/test_🌟.py" ], []),
80+ ],
81+ )
82+ def test_discover_candidates_basic_patterns (tmp_tree : Path , inputs , expected_contains , expected_absent ):
83+ cwd = os .getcwd ()
84+ try :
85+ os .chdir (tmp_tree )
86+ results = list (discover_tests_candidates (inputs ))
87+ results_n = sorted (norm (Path (p )) for p in results )
88+ for exp in expected_contains :
89+ assert any (r .endswith (exp ) for r in results_n ), f"Expected { exp } in { results_n } "
90+ for bad in expected_absent :
91+ assert all (not r .endswith (bad ) for r in results_n ), f"Did not expect { bad } in { results_n } "
92+ finally :
93+ os .chdir (cwd )
94+
95+ def test_discover_candidates_ignores_non_python_files (tmp_tree : Path ):
96+ cwd = os .getcwd ()
97+ try :
98+ os .chdir (tmp_tree )
99+ results = list (discover_tests_candidates (["." ]))
100+ assert all (r .endswith (".py" ) for r in results ), f"Non-Python files leaked into results: { results } "
101+ finally :
102+ os .chdir (cwd )
103+
104+ @pytest .mark .parametrize (
105+ "inputs" ,
106+ [
107+ ([]),
108+ (["nonexistent_dir/" ]),
109+ (["README.md" ]),
110+ ],
111+ )
112+ def test_discover_candidates_empty_or_invalid_inputs (tmp_tree : Path , inputs ):
113+ cwd = os .getcwd ()
114+ try :
115+ os .chdir (tmp_tree )
116+ results = list (discover_tests_candidates (inputs ))
117+ # Either empty or gracefully handles invalid paths without raising
118+ assert isinstance (results , list ) or hasattr (results , "__iter__" )
119+ # Count should be zero for no valid candidates
120+ assert len (list (results )) == 0
121+ finally :
122+ os .chdir (cwd )
123+
124+ def test_discover_candidates_deduplicates_and_sorts (tmp_tree : Path ):
125+ cwd = os .getcwd ()
126+ try :
127+ os .chdir (tmp_tree )
128+ res = list (discover_tests_candidates (["tests/test_alpha.py" , "tests/" , "tests/test_alpha.py" ]))
129+ # Expect no duplicates
130+ normed = [norm (Path (p )) for p in res ]
131+ assert len (normed ) == len (set (normed )), f"Duplicates found: { normed } "
132+ # Many implementations return deterministic order; if not, we at least check that sorting is stable
133+ assert sorted (normed ) == sorted (set (normed ))
134+ finally :
135+ os .chdir (cwd )
136+
137+ def test_discover_candidates_recurses_directories_respecting_test_naming (tmp_tree : Path ):
138+ cwd = os .getcwd ()
139+ try :
140+ os .chdir (tmp_tree )
141+ res = list (discover_tests_candidates (["package/" ]))
142+ normed = [norm (Path (p )) for p in res ]
143+ assert any (p .endswith ("package/module_test.py" ) for p in normed ), f"Expected module_test.py, got { normed } "
144+ assert all (not p .endswith ("package/not_a_test.txt" ) for p in normed )
145+ finally :
146+ os .chdir (cwd )
0 commit comments