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