88
99Options:
1010 -u, --unit [DIR] Run unit tests (optionally specify subdirectory)
11- -i, --integrated Run integrated tests
11+ -i, --integrated Run integration tests (tests/integration)
1212 -a, --all Run all tests (default)
1313 -c, --coverage Generate coverage report
1414 -p, --parallel Run tests in parallel
1818 python scripts/run_tests.py # Run all tests
1919 python scripts/run_tests.py -u # Run all unit tests
2020 python scripts/run_tests.py -u providers # Run unit tests in providers
21- python scripts/run_tests.py -i # Run integrated tests
21+ python scripts/run_tests.py -i # Run integration tests
2222 python scripts/run_tests.py -a -c # Run all tests with coverage
2323 python scripts/run_tests.py -p # Run tests in parallel
24+
25+ Notes:
26+ * The default ``-a`` run executes the complete ``tests/unit`` tree
27+ (root-level files included), ``tests/contract`` and
28+ ``tests/integration`` — the same tiers GitHub Actions runs.
29+ * A missing test suite directory is reported as an error with a
30+ nonzero exit status instead of being silently skipped.
31+ * Output is safe on terminals with limited encodings (for example
32+ CP936/GBK on Windows): unencodable status symbols are replaced
33+ instead of raising ``UnicodeEncodeError``.
2434"""
2535
2636import argparse
3040from typing import Optional
3141
3242
33- class Colors :
43+ def _make_output_safe (stream ) -> None :
44+ """Keep printing from crashing on limited terminal encodings.
45+
46+ Windows consoles frequently use CP936/GBK; the Unicode status
47+ symbols below used to raise ``UnicodeEncodeError`` before the test
48+ outcome was reported. Replacing unencodable characters keeps the
49+ runner usable there.
50+ """
51+ if hasattr (stream , "reconfigure" ):
52+ try :
53+ stream .reconfigure (errors = "replace" )
54+ except (ValueError , OSError ):
55+ pass
56+
57+
58+ _make_output_safe (sys .stdout )
59+ _make_output_safe (sys .stderr )
60+
61+
62+ class Colors : # pylint: disable=too-few-public-methods
3463 """ANSI color codes for terminal output."""
3564
3665 RED = "\033 [0;31m"
@@ -61,10 +90,10 @@ def print_warning(message: str) -> None:
6190
6291
6392def check_pytest () -> bool :
64- """Check if pytest is installed ."""
93+ """Check that pytest is usable by the active interpreter ."""
6594 try :
6695 subprocess .run (
67- ["pytest" , "--version" ],
96+ [sys . executable , "-m" , "pytest" , "--version" ],
6897 capture_output = True ,
6998 check = True ,
7099 )
@@ -73,103 +102,121 @@ def check_pytest() -> bool:
73102 return False
74103
75104
105+ def run_pytest (
106+ project_root : Path ,
107+ test_path : Path ,
108+ coverage : bool = False ,
109+ parallel : bool = False ,
110+ ) -> int :
111+ """Run pytest for ``test_path`` from the repository root.
112+
113+ Running from the root lets pytest pick up the repository
114+ configuration (markers, timeouts, ...), and invoking pytest through
115+ ``sys.executable`` targets the active interpreter even when the
116+ ``pytest`` entry point is not on PATH.
117+ """
118+ cmd = [sys .executable , "-m" , "pytest" , "-v" , str (test_path )]
119+
120+ if coverage :
121+ cmd .extend (
122+ [
123+ "--cov=src/qwenpaw" ,
124+ "--cov-report=html" ,
125+ "--cov-report=term-missing" ,
126+ ],
127+ )
128+
129+ if parallel :
130+ cmd .extend (["-n" , "auto" ])
131+
132+ try :
133+ result = subprocess .run (cmd , cwd = project_root , check = True )
134+ return result .returncode
135+ except subprocess .CalledProcessError as e :
136+ return e .returncode
137+
138+
76139def run_unit_tests (
77140 project_root : Path ,
78141 subdir : Optional [str ] = None ,
79142 coverage : bool = False ,
80143 parallel : bool = False ,
81144) -> int :
82- """Run unit tests."""
145+ """Run unit tests.
146+
147+ Without ``subdir`` the complete ``tests/unit`` tree is executed in
148+ one pytest invocation, so files placed directly in ``tests/unit``
149+ are included as well.
150+ """
83151 if subdir :
84- # Run specific subdirectory
85152 test_path = project_root / "tests" / "unit" / subdir
86153 if not test_path .is_dir ():
87154 print_error (f"Unit test directory not found: { test_path } " )
88155 return 1
89156
90157 print_info (f"Running unit tests in: { subdir } " )
91- return_code = run_pytest (test_path , coverage , parallel )
158+ return_code = run_pytest (project_root , test_path , coverage , parallel )
92159 if return_code == 0 :
93160 print_success (f"Unit tests in { subdir } completed" )
94161 return return_code
95- else :
96- # Run all unit test subdirectories
97- print_info ("Running all unit tests..." )
98- unit_dir = project_root / "tests" / "unit"
99162
100- if not unit_dir .is_dir ():
101- print_warning ("Unit test directory not found: tests/unit" )
102- return 0
103-
104- subdirs = [d for d in unit_dir .iterdir () if d .is_dir ()]
105- if not subdirs :
106- print_warning ("No unit test subdirectories found" )
107- return 0
108-
109- overall_return_code = 0
110- for test_dir in subdirs :
111- dirname = test_dir .name
112- print_info (f"Running unit tests in: { dirname } " )
113- return_code = run_pytest (test_dir , coverage , parallel )
114- if return_code == 0 :
115- print_success (f"Unit tests in { dirname } completed" )
116- else :
117- overall_return_code = return_code
118- print ()
163+ unit_dir = project_root / "tests" / "unit"
164+ if not unit_dir .is_dir ():
165+ print_error ("Unit test directory not found: tests/unit" )
166+ return 1
119167
120- return overall_return_code
168+ print_info ("Running all unit tests..." )
169+ return_code = run_pytest (project_root , unit_dir , coverage , parallel )
170+ if return_code == 0 :
171+ print_success ("Unit tests completed" )
172+ return return_code
121173
122174
123- def run_integrated_tests (
175+ def run_contract_tests (
124176 project_root : Path ,
125177 coverage : bool = False ,
126178 parallel : bool = False ,
127179) -> int :
128- """Run integrated tests."""
129- print_info ("Running integrated tests..." )
130- integrated_dir = project_root / "tests" / "integrated"
131-
132- if not integrated_dir .is_dir ():
133- print_warning ("Integrated test directory not found: tests/integrated" )
134- return 0
135-
136- # Check if there are any Python test files
137- test_files = list (integrated_dir .glob ("*.py" ))
138- if not test_files :
139- print_warning ("No integrated test files found in tests/integrated" )
140- return 0
180+ """Run contract tests (tests/contract)."""
181+ print_info ("Running contract tests..." )
182+ contract_dir = project_root / "tests" / "contract"
183+ if not contract_dir .is_dir ():
184+ print_error ("Contract test directory not found: tests/contract" )
185+ return 1
141186
142- return_code = run_pytest (integrated_dir , coverage , parallel )
187+ return_code = run_pytest (project_root , contract_dir , coverage , parallel )
143188 if return_code == 0 :
144- print_success ("Integrated tests completed" )
189+ print_success ("Contract tests completed" )
145190 return return_code
146191
147192
148- def run_pytest (
149- test_path : Path ,
193+ def run_integrated_tests (
194+ project_root : Path ,
150195 coverage : bool = False ,
151196 parallel : bool = False ,
152197) -> int :
153- """Run pytest with specified options."""
154- cmd = [ "pytest" , "-v" , str ( test_path )]
155-
156- if coverage :
157- cmd . extend (
158- [
159- "--cov=src/qwenpaw" ,
160- "--cov-report=html" ,
161- "--cov-report=term-missing" ,
162- ] ,
198+ """Run integration tests (tests/integration).
199+
200+ A missing directory is an error: silently returning success here
201+ used to mask the fact that no integration test ran at all.
202+ """
203+ print_info ( "Running integration tests..." )
204+ integration_dir = project_root / "tests" / "integration"
205+ if not integration_dir . is_dir ():
206+ print_error (
207+ "Integration test directory not found: tests/integration" ,
163208 )
209+ return 1
164210
165- if parallel :
166- cmd .extend (["-n" , "auto" ])
167-
168- try :
169- result = subprocess .run (cmd , cwd = test_path .parents [2 ], check = True )
170- return result .returncode
171- except subprocess .CalledProcessError as e :
172- return e .returncode
211+ return_code = run_pytest (
212+ project_root ,
213+ integration_dir ,
214+ coverage ,
215+ parallel ,
216+ )
217+ if return_code == 0 :
218+ print_success ("Integration tests completed" )
219+ return return_code
173220
174221
175222def main () -> int :
@@ -191,7 +238,7 @@ def main() -> int:
191238 "-i" ,
192239 "--integrated" ,
193240 action = "store_true" ,
194- help = "Run integrated tests" ,
241+ help = "Run integration tests (tests/integration) " ,
195242 )
196243 parser .add_argument (
197244 "-a" ,
@@ -245,12 +292,18 @@ def main() -> int:
245292 parallel = args .parallel ,
246293 )
247294 print ()
295+ contract_code = run_contract_tests (
296+ project_root ,
297+ coverage = args .coverage ,
298+ parallel = args .parallel ,
299+ )
300+ print ()
248301 integrated_code = run_integrated_tests (
249302 project_root ,
250303 coverage = args .coverage ,
251304 parallel = args .parallel ,
252305 )
253- return_code = unit_code or integrated_code
306+ return_code = unit_code or contract_code or integrated_code
254307 elif args .unit is not None :
255308 return_code = run_unit_tests (
256309 project_root ,
@@ -266,12 +319,18 @@ def main() -> int:
266319 )
267320
268321 print ()
269- if args .coverage :
270- print_success (
271- "Test run completed! Coverage report generated in htmlcov/index.html" ,
272- )
322+ if return_code == 0 :
323+ if args .coverage :
324+ print_success (
325+ "Test run completed! Coverage report generated in "
326+ "htmlcov/index.html" ,
327+ )
328+ else :
329+ print_success ("All test suites completed successfully!" )
273330 else :
274- print_success ("Test run completed!" )
331+ print_error (
332+ f"Test run finished with failures (exit code { return_code } )" ,
333+ )
275334 print ()
276335
277336 return return_code
0 commit comments