Skip to content

Commit f9d73ab

Browse files
committed
Add ut test
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
1 parent ad03ede commit f9d73ab

3 files changed

Lines changed: 476 additions & 6 deletions

File tree

pyproject.toml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ test = [
163163
"openpyxl>=3.1.5",
164164
"pillow>=10.4.0",
165165
"pytest>=8.3.5",
166+
"pytest-asyncio>=1.3.0",
167+
"pytest-xdist>=3.8.0",
168+
"pytest-cov>=7.0.0",
166169
"python-docx>=1.1.2",
167170
"python-pptx>=1.0.2",
168171
"reportlab>=4.4.1",
@@ -195,8 +198,83 @@ extend-select = ["ASYNC", "ASYNC1"]
195198
ignore = ["E402"]
196199

197200
[tool.pytest.ini_options]
201+
pythonpath = [
202+
"."
203+
]
204+
205+
testpaths = ["test"]
206+
python_files = ["test_*.py"]
207+
python_classes = ["Test*"]
208+
python_functions = ["test_*"]
209+
198210
markers = [
199211
"p1: high priority test cases",
200212
"p2: medium priority test cases",
201213
"p3: low priority test cases",
202214
]
215+
216+
# Test collection and runtime configuration
217+
filterwarnings = [
218+
"error", # Treat warnings as errors
219+
"ignore::DeprecationWarning", # Ignore specific warnings
220+
]
221+
222+
# Command line options
223+
addopts = [
224+
"-v", # Verbose output
225+
"--strict-markers", # Enforce marker definitions
226+
"--tb=short", # Simplified traceback
227+
"--disable-warnings", # Disable warnings
228+
"--color=yes" # Colored output
229+
]
230+
231+
232+
# Coverage configuration
233+
[tool.coverage.run]
234+
# Source paths - adjust according to your project structure
235+
source = [
236+
# "../../api/db/services",
237+
# Add more directories if needed:
238+
"../../common",
239+
# "../../utils",
240+
]
241+
242+
# Files/directories to exclude
243+
omit = [
244+
"*/tests/*",
245+
"*/test_*",
246+
"*/__pycache__/*",
247+
"*/.pytest_cache/*",
248+
"*/venv/*",
249+
"*/.venv/*",
250+
"*/env/*",
251+
"*/site-packages/*",
252+
"*/dist/*",
253+
"*/build/*",
254+
"*/migrations/*",
255+
"setup.py"
256+
]
257+
258+
[tool.coverage.report]
259+
# Report configuration
260+
precision = 2
261+
show_missing = true
262+
skip_covered = false
263+
fail_under = 0 # Minimum coverage requirement (0-100)
264+
265+
# Lines to exclude (optional)
266+
exclude_lines = [
267+
# "pragma: no cover",
268+
# "def __repr__",
269+
# "raise AssertionError",
270+
# "raise NotImplementedError",
271+
# "if __name__ == .__main__.:",
272+
# "if TYPE_CHECKING:",
273+
"pass"
274+
]
275+
276+
[tool.coverage.html]
277+
# HTML report configuration
278+
directory = "htmlcov"
279+
title = "Test Coverage Report"
280+
# extra_css = "custom.css" # Optional custom CSS

run_tests.py

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
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"\nOpen 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

Comments
 (0)