1+ #!/usr/bin/env python3
2+ #
3+ # Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
4+ #
5+ # Licensed under the Apache License, Version 2.0 (the "License");
6+ # you may not use this file except in compliance with the License.
7+ # You may obtain a copy of the License at
8+ #
9+ # http://www.apache.org/licenses/LICENSE-2.0
10+ #
11+ # Unless required by applicable law or agreed to in writing, software
12+ # distributed under the License is distributed on an "AS IS" BASIS,
13+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+ # See the License for the specific language governing permissions and
15+ # limitations under the License.
16+
17+ import sys
18+ import os
19+ import argparse
20+ import subprocess
21+ from pathlib import Path
22+ from typing import List
23+
24+
25+ class Colors :
26+ """ANSI color codes for terminal output"""
27+ RED = '\033 [0;31m'
28+ GREEN = '\033 [0;32m'
29+ YELLOW = '\033 [1;33m'
30+ BLUE = '\033 [0;34m'
31+ NC = '\033 [0m' # No Color
32+
33+
34+ class TestRunner :
35+ """RAGFlow Unit Test Runner"""
36+
37+ def __init__ (self ):
38+ self .project_root = Path (__file__ ).parent .resolve ()
39+ self .ut_dir = Path (self .project_root / 'test' / 'unit_test' )
40+ # Default options
41+ self .coverage = False
42+ self .parallel = False
43+ self .verbose = False
44+ self .markers = ""
45+
46+ # Python interpreter path
47+ self .python = sys .executable
48+
49+ @staticmethod
50+ def print_info (message : str ) -> None :
51+ """Print informational message"""
52+ print (f"{ Colors .BLUE } [INFO]{ Colors .NC } { message } " )
53+
54+ @staticmethod
55+ def print_error (message : str ) -> None :
56+ """Print error message"""
57+ print (f"{ Colors .RED } [ERROR]{ Colors .NC } { message } " )
58+
59+ @staticmethod
60+ def show_usage () -> None :
61+ """Display usage information"""
62+ usage = """
63+ RAGFlow Unit Test Runner
64+ Usage: python run_tests.py [OPTIONS]
65+
66+ OPTIONS:
67+ -h, --help Show this help message
68+ -c, --coverage Run tests with coverage report
69+ -p, --parallel Run tests in parallel (requires pytest-xdist)
70+ -v, --verbose Verbose output
71+ -t, --test FILE Run specific test file or directory
72+ -m, --markers MARKERS Run tests with specific markers (e.g., "unit", "integration")
73+
74+ EXAMPLES:
75+ # Run all tests
76+ python run_tests.py
77+
78+ # Run with coverage
79+ python run_tests.py --coverage
80+
81+ # Run in parallel
82+ python run_tests.py --parallel
83+
84+ # Run specific test file
85+ python run_tests.py --test services/test_dialog_service.py
86+
87+ # Run only unit tests
88+ python run_tests.py --markers "unit"
89+
90+ # Run tests with coverage and parallel execution
91+ python run_tests.py --coverage --parallel
92+
93+ """
94+ print (usage )
95+
96+ def build_pytest_command (self ) -> List [str ]:
97+ """Build the pytest command arguments"""
98+ cmd = ["pytest" , str (self .ut_dir )]
99+
100+ # Add test path
101+
102+ # Add markers
103+ if self .markers :
104+ cmd .extend (["-m" , self .markers ])
105+
106+ # Add verbose flag
107+ if self .verbose :
108+ cmd .extend (["-vv" ])
109+ else :
110+ cmd .append ("-v" )
111+
112+ # Add coverage
113+ if self .coverage :
114+ # Relative path from test directory to source code
115+ source_path = str (self .project_root / "common" )
116+ cmd .extend ([
117+ "--cov" , source_path ,
118+ "--cov-report" , "html" ,
119+ "--cov-report" , "term"
120+ ])
121+
122+ # Add parallel execution
123+ if self .parallel :
124+ # Try to get number of CPU cores
125+ try :
126+ import multiprocessing
127+ cpu_count = multiprocessing .cpu_count ()
128+ cmd .extend (["-n" , str (cpu_count )])
129+ except ImportError :
130+ # Fallback to auto if multiprocessing not available
131+ cmd .extend (["-n" , "auto" ])
132+
133+ # Add default options from pyproject.toml if it exists
134+ pyproject_path = self .project_root / "pyproject.toml"
135+ if pyproject_path .exists ():
136+ cmd .extend (["--config-file" , str (pyproject_path )])
137+
138+ return cmd
139+
140+ def run_tests (self ) -> bool :
141+ """Execute the pytest command"""
142+ # Change to test directory
143+ os .chdir (self .project_root )
144+
145+ # Build command
146+ cmd = self .build_pytest_command ()
147+
148+ # Print test configuration
149+ self .print_info ("Running RAGFlow Unit Tests" )
150+ self .print_info ("=" * 40 )
151+ self .print_info (f"Test Directory: { self .ut_dir } " )
152+ self .print_info (f"Coverage: { self .coverage } " )
153+ self .print_info (f"Parallel: { self .parallel } " )
154+ self .print_info (f"Verbose: { self .verbose } " )
155+
156+ if self .markers :
157+ self .print_info (f"Markers: { self .markers } " )
158+
159+ print (f"\n { Colors .BLUE } [EXECUTING]{ Colors .NC } { ' ' .join (cmd )} \n " )
160+
161+ # Run pytest
162+ try :
163+ result = subprocess .run (cmd , check = False )
164+
165+ if result .returncode == 0 :
166+ print (f"\n { Colors .GREEN } [SUCCESS]{ Colors .NC } All tests passed!" )
167+
168+ if self .coverage :
169+ coverage_dir = self .ut_dir / "htmlcov"
170+ if coverage_dir .exists ():
171+ index_file = coverage_dir / "index.html"
172+ print (f"\n { Colors .BLUE } [INFO]{ Colors .NC } Coverage report generated:" )
173+ print (f" { index_file } " )
174+ print (f"\n Open with:" )
175+ print (f" - Windows: start { index_file } " )
176+ print (f" - macOS: open { index_file } " )
177+ print (f" - Linux: xdg-open { index_file } " )
178+
179+ return True
180+ else :
181+ print (f"\n { Colors .RED } [FAILURE]{ Colors .NC } Some tests failed!" )
182+ return False
183+
184+ except KeyboardInterrupt :
185+ print (f"\n { Colors .YELLOW } [INTERRUPTED]{ Colors .NC } Test execution interrupted by user" )
186+ return False
187+ except Exception as e :
188+ self .print_error (f"Failed to execute tests: { e } " )
189+ return False
190+
191+ def parse_arguments (self ) -> bool :
192+ """Parse command line arguments"""
193+ parser = argparse .ArgumentParser (
194+ description = "RAGFlow Unit Test Runner" ,
195+ formatter_class = argparse .RawDescriptionHelpFormatter ,
196+ epilog = """
197+ Examples:
198+ python run_tests.py # Run all tests
199+ python run_tests.py --coverage # Run with coverage
200+ python run_tests.py --parallel # Run in parallel
201+ python run_tests.py --test services/test_dialog_service.py # Run specific test
202+ python run_tests.py --markers "unit" # Run only unit tests
203+ """
204+ )
205+
206+ parser .add_argument (
207+ "-c" , "--coverage" ,
208+ action = "store_true" ,
209+ help = "Run tests with coverage report"
210+ )
211+
212+ parser .add_argument (
213+ "-p" , "--parallel" ,
214+ action = "store_true" ,
215+ help = "Run tests in parallel (requires pytest-xdist)"
216+ )
217+
218+ parser .add_argument (
219+ "-v" , "--verbose" ,
220+ action = "store_true" ,
221+ help = "Verbose output"
222+ )
223+
224+ parser .add_argument (
225+ "-t" , "--test" ,
226+ type = str ,
227+ default = "" ,
228+ help = "Run specific test file or directory"
229+ )
230+
231+ parser .add_argument (
232+ "-m" , "--markers" ,
233+ type = str ,
234+ default = "" ,
235+ help = "Run tests with specific markers (e.g., 'unit', 'integration')"
236+ )
237+
238+ try :
239+ args = parser .parse_args ()
240+
241+ # Set options
242+ self .coverage = args .coverage
243+ self .parallel = args .parallel
244+ self .verbose = args .verbose
245+ self .markers = args .markers
246+
247+ return True
248+
249+ except SystemExit :
250+ # argparse already printed help, just exit
251+ return False
252+ except Exception as e :
253+ self .print_error (f"Error parsing arguments: { e } " )
254+ return False
255+
256+ def run (self ) -> int :
257+ """Main execution method"""
258+ # Parse command line arguments
259+ if not self .parse_arguments ():
260+ return 1
261+
262+ # Run tests
263+ success = self .run_tests ()
264+
265+ return 0 if success else 1
266+
267+
268+ def main ():
269+ """Entry point"""
270+ runner = TestRunner ()
271+ return runner .run ()
272+
273+
274+ if __name__ == "__main__" :
275+ sys .exit (main ())
0 commit comments