refactor: simplify D3D12 test infrastructure

- Delete run_tests.py and run_tests.bat (use cmake/ctest directly)
- Fix integration test CMakeLists: output_ppm name, threshold=0, GT.ppm copy
- Rewrite TEST_SPEC.md to be concise
- Update integration test CTest registration
This commit is contained in:
2026-03-22 21:41:56 +08:00
parent 50b50d06a0
commit fa50892150
7 changed files with 65 additions and 769 deletions

View File

@@ -1,223 +1,65 @@
# D3D12 测试专项规范
本文档是 XCEngine 测试规范的 D3D12 专项补充。
## 1. 构建命令
**前置阅读**: [tests/TEST_SPEC.md](../TEST_SPEC.md) - 通用测试规范
```bash
# 构建所有
cmake --build <build> --config Debug
---
# 只构建单元测试
cmake --build <build> --target d3d12_engine_tests --config Debug
## 1. 概述
### 1.1 D3D12 测试特点
| 特点 | 说明 |
|------|------|
| 硬件依赖 | 需要支持 D3D12 的显卡 |
| 窗口依赖 | 集成测试需要 GUI 窗口 |
| GPU 状态 | 测试间可能有 GPU 状态污染 |
### 1.2 测试层级
| 层级 | 位置 | 执行方式 | 框架 |
|------|------|----------|------|
| 单元测试 | `tests/RHI/D3D12/unit/` | CTest | Google Test |
| 集成测试 | `tests/RHI/D3D12/integration/` | CTest + Python | Python wrapper |
---
## 2. 单元测试规范
### 2.1 Fixture 设计
每个测试独立创建设备,避免 GPU 状态污染:
```cpp
class D3D12TestFixture : public ::testing::Test {
protected:
void SetUp() override; // 创建设备、命令队列等
void TearDown() override; // 清理资源
ID3D12Device* GetDevice();
ID3D12CommandQueue* GetCommandQueue();
ID3D12CommandAllocator* GetCommandAllocator();
ID3D12GraphicsCommandList* GetCommandList();
void WaitForGPU();
};
# 只构建集成测试
cmake --build <build> --target D3D12_Sphere D3D12_Triangle D3D12_Quad D3D12_Minimal --config Debug
```
### 2.2 测试前缀对应
## 2. 运行测试
| 类名 | 测试前缀 |
|------|---------|
| D3D12Device | Device |
| D3D12CommandQueue | CommandQueue |
| D3D12CommandAllocator | CommandAllocator |
| D3D12CommandList | CommandList |
| D3D12Buffer | Buffer |
| D3D12Texture | Texture |
| D3D12DescriptorHeap | DescriptorHeap |
| D3D12Fence | Fence |
| D3D12PipelineState | PipelineState |
| D3D12RootSignature | RootSignature |
| D3D12Shader | Shader |
| D3D12RenderTargetView | RTV |
| D3D12DepthStencilView | DSV |
```bash
# 运行所有已注册的测试(单元 + 集成)
cd <build>
ctest -C Debug --output-on-failure
### 2.3 当前测试统计
# 只运行单元测试
cd <build>/tests/RHI/D3D12/unit
ctest -C Debug --output-on-failure
| 组件 | 测试数 | 状态 |
|------|--------|------|
| Device | 6 | ✅ 通过 |
| Fence | 5 | ✅ 通过 |
| CommandQueue | 2 | ✅ 通过 |
| CommandAllocator | 3 | ✅ 通过 |
| CommandList | 2 | ✅ 通过 |
| Buffer | 5 | ✅ 通过 |
| Texture | 4 | ✅ 通过 |
| DescriptorHeap | 5 | ✅ 通过 |
| PipelineState | 2 | ✅ 通过 |
| RootSignature | 2 | ✅ 通过 |
| Shader | 3 | ✅ 通过 |
| Views | 4 | ✅ 通过 |
| **总计** | **44** | **全部通过** |
---
## 3. 集成测试规范
### 3.1 目录结构
每个集成测试独占一个子文件夹,资源相互隔离:
```
integration/
├── CMakeLists.txt # 构建配置
├── run_integration_test.py # 公共测试运行脚本
├── compare_ppm.py # PPM 图像比对脚本
├── run.bat # Windows 启动脚本
├── minimal/ # 最小化测试
│ ├── main.cpp
│ ├── GT.ppm
│ └── Res/
│ └── Shader/
│ ├── ndctriangle.hlsl
│ └── gs.hlsl
├── render_model/ # 模型渲染测试
│ ├── main.cpp
│ ├── GT.ppm
│ └── Res/
│ ├── Image/
│ ├── Model/
│ └── Shader/
├── quad/ # 矩形渲染测试
│ ├── main.cpp
│ ├── GT.ppm
│ └── Res/
│ └── Shader/
├── sphere/ # 球体渲染测试
│ ├── main.cpp
│ ├── GT.ppm
│ └── Res/
│ ├── Image/
│ └── Shader/
└── triangle/ # 三角形渲染测试 (待实现)
├── main.cpp
├── GT_triangle.ppm
└── Res/
# 只运行集成测试
cd <build>/tests/RHI/D3D12/integration
ctest -C Debug --output-on-failure
```
### 3.2 Python Wrapper
## 3. 集成测试列表
**位置**: `tests/RHI/D3D12/integration/run_integration_test.py`
| 测试名 | Target | Golden Image |
|--------|--------|-------------|
| D3D12_Minimal_Integration | D3D12_Minimal | `minimal/GT.ppm` |
| D3D12_Quad_Integration | D3D12_Quad | `quad/GT.ppm` |
| D3D12_Sphere_Integration | D3D12_Sphere | `sphere/GT.ppm` |
| D3D12_Triangle_Integration | D3D12_Triangle | `triangle/GT.ppm` |
**职责**:
1. 启动 D3D12 exe
2. 等待进程完成
3. 检查输出文件 (PPM 截图)
4. 调用 `compare_ppm.py` 比对 Golden Image
5. 返回 0(成功)/1(失败)
## 4. CTest 注册机制
### 3.3 CTest 注册格式
集成测试通过 `add_test()` 注册到 CTest
```cmake
add_test(NAME D3D12_Minimal_Integration
COMMAND ${Python3_EXECUTABLE} $<TARGET_FILE_DIR:D3D12_Minimal>/run_integration_test.py
$<TARGET_FILE:D3D12_Minimal>
minimal.ppm
${CMAKE_CURRENT_SOURCE_DIR}/minimal/GT.ppm
5
WORKING_DIRECTORY $<TARGET_FILE_DIR:D3D12_Minimal>
add_test(NAME D3D12_Sphere_Integration
COMMAND ${Python3_EXECUTABLE} $<TARGET_FILE_DIR:D3D12_Sphere>/run_integration_test.py
$<TARGET_FILE:D3D12_Sphere>
sphere.ppm
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
0
WORKING_DIRECTORY $<TARGET_FILE_DIR:D3D12_Sphere>
)
```
### 3.4 Golden Image 规范
`run_integration_test.py` 负责:
1. 启动 exe
2. 等待完成
3. 调用 `compare_ppm.py` 比对图像
4. 返回 0通过/ 1失败
| 属性 | 值 |
|------|-----|
| 格式 | PPM (P6) |
| 命名 | `GT_<test_name>.ppm` |
| 阈值 | 默认 5% |
| 存储位置 | `tests/RHI/D3D12/integration/<test_name>/` |
### 3.5 Golden Image 生成流程
1. 在干净硬件环境运行集成测试
2. 截图保存为 `GT_<name>.ppm`
3. 人工验证截图正确性
4. 提交到版本控制
### 3.6 当前集成测试
| 测试名 | Golden Image | 状态 |
|--------|-------------|------|
| D3D12_Minimal_Integration | `minimal/GT.ppm` | ✅ 通过 |
| D3D12_RenderModel_Integration | `render_model/GT.ppm` | ❌ 待修复 |
| D3D12_Quad_Integration | `quad/GT.ppm` | ✅ 通过 |
| D3D12_Sphere_Integration | `sphere/GT.ppm` | ✅ 通过 |
| D3D12_Triangle_Integration | `triangle/GT_triangle.ppm` | 🔄 待实现 |
---
## 4. 测试执行
### 4.1 单元测试
```bash
# 方式 1: 使用统一脚本
python scripts/run_tests.py --unit-only
# 方式 2: 直接使用 CTest
cd build/tests/RHI/D3D12/unit
ctest -C Debug --output-on-failure
```
### 4.2 集成测试
```bash
# 方式 1: 使用统一脚本
python scripts/run_tests.py --integration
# 方式 2: 直接使用 CTest
cd build/tests/RHI/D3D12/integration
ctest -C Debug --output-on-failure
```
### 4.3 构建和测试
```bash
# 构建
cmake --build . --target D3D12_Minimal D3D12_RenderModel --config Debug
# 运行测试
python scripts/run_tests.py --build
```
---
## 5. CI 集成
### 5.1 GitHub Actions 配置
## 5. CI 配置
```yaml
name: D3D12 Tests
@@ -232,101 +74,45 @@ jobs:
- name: Configure CMake
run: cmake -B build -DCMAKE_BUILD_TYPE=Debug
- name: Build D3D12 Tests
run: cmake --build build --target D3D12_Minimal D3D12_RenderModel --config Debug
- name: Build D3D12
run: cmake --build build --config Debug
- name: Run Unit Tests
run: cd build/tests/RHI/D3D12/unit && ctest -C Debug --output-on-failure
- name: Run Integration Tests
run: python scripts/run_tests.py --integration
- name: Run Tests
run: cd build && ctest -C Debug --output-on-failure
```
### 5.2 CI 模式
`--ci` 模式会跳过需要 GUI 的集成测试:
```bash
python scripts/run_tests.py --ci # 仅运行单元测试
```
---
## 6. 文件结构
## 6. 目录结构
```
tests/RHI/D3D12/
├── CMakeLists.txt
├── TEST_SPEC.md # 本文档 (D3D12 专项)
├── TEST_IMPROVEMENT_PLAN.md # 改进计划
├── unit/
│ ├── CMakeLists.txt
│ ├── fixtures/
│ │ ├── D3D12TestFixture.h
│ │ └── D3D12TestFixture.cpp
│ ├── test_device.cpp
│ ├── test_buffer.cpp
│ ├── test_*.cpp
│ └── ...
└── integration/
├── CMakeLists.txt
├── run_integration_test.py # 公共脚本
├── compare_ppm.py # 公共脚本
├── run.bat # 公共脚本
├── minimal/ # 测试子文件夹
├── run_integration_test.py # 测试 wrapper
├── compare_ppm.py # 图像比对
├── minimal/ # 最小测试
│ ├── main.cpp
── GT.ppm
│ └── Res/
├── render_model/ # 测试子文件夹
│ ├── main.cpp # 有 API 问题,待修复
│ ├── GT.ppm
│ └── Res/
├── quad/ # 测试子文件夹
── GT.ppm
├── quad/
│ ├── main.cpp
── GT.ppm
│ └── Res/
├── sphere/ # 测试子文件夹
── GT.ppm
├── sphere/
│ ├── main.cpp
│ ├── GT.ppm
│ └── Res/
│ ├── Image/
│ └── Shader/
└── triangle/ # 测试子文件夹 (待实现)
└── triangle/
├── main.cpp
── GT_triangle.ppm
└── Res/
engine/
└── third_party/
└── stb/ # stb 图像库
├── stb_image.h
└── stb_image.cpp
── GT.ppm
```
---
## 7. 已知问题
### 7.1 render_model API 不兼容
**问题**: `D3D12Buffer::Initialize``D3D12Screenshot::Capture` API 签名变更
**影响**: `D3D12_RenderModel_Integration` 无法编译
**状态**: 待修复
---
## 8. 规范更新记录
| 版本 | 日期 | 变更 |
|------|------|------|
| 1.0 | 2026-03-20 | 初始版本 |
| 1.1 | 2026-03-20 | 添加 CI 集成章节,补充 Phase 5 内容 |
| 1.2 | 2026-03-20 | 重构集成测试目录结构每个测试独立子文件夹stb 库移至 engine/third_party/stb/ |
| 1.3 | 2026-03-20 | 修复 minimal GetBuffer 原生调用问题:添加 D3D12Texture 所有权语义,删除 GetSwapChain() 暴露方法,移除 gColorRTs 数组 |
| 1.4 | 2026-03-22 | 添加 quad 和 sphere 集成测试到文档sphere 测试包含纹理采样、深度写入等修复 |
---
**规范版本**: 1.4
**最后更新**: 2026-03-22
**前置文档**: [tests/TEST_SPEC.md](../TEST_SPEC.md)
**最后更新**: 2026-03-22

View File

@@ -46,6 +46,6 @@ add_test(NAME D3D12_Minimal_Integration
$<TARGET_FILE:D3D12_Minimal>
minimal.ppm
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
5
0
WORKING_DIRECTORY $<TARGET_FILE_DIR:D3D12_Minimal>
)

View File

@@ -55,6 +55,6 @@ add_test(NAME D3D12_Quad_Integration
$<TARGET_FILE:D3D12_Quad>
quad.ppm
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
5
0
WORKING_DIRECTORY $<TARGET_FILE_DIR:D3D12_Quad>
)

View File

@@ -43,13 +43,16 @@ add_custom_command(TARGET D3D12_Sphere POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/tests/RHI/D3D12/integration/run_integration_test.py
$<TARGET_FILE_DIR:D3D12_Sphere>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
$<TARGET_FILE_DIR:D3D12_Sphere>/
)
add_test(NAME D3D12_Sphere_Integration
COMMAND ${Python3_EXECUTABLE} $<TARGET_FILE_DIR:D3D12_Sphere>/run_integration_test.py
$<TARGET_FILE:D3D12_Sphere>
screenshot.ppm
sphere.ppm
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
5
0
WORKING_DIRECTORY $<TARGET_FILE_DIR:D3D12_Sphere>
)

View File

@@ -52,6 +52,6 @@ add_test(NAME D3D12_Triangle_Integration
$<TARGET_FILE:D3D12_Triangle>
triangle.ppm
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
5
0
WORKING_DIRECTORY $<TARGET_FILE_DIR:D3D12_Triangle>
)

View File

@@ -1,8 +0,0 @@
@echo off
echo Running XCEngine Tests via Python runner...
echo.
python "%~dp0..\scripts\run_tests.py" --ci
echo.
pause

View File

@@ -1,485 +0,0 @@
#!/usr/bin/env python3
"""
XCEngine Test Runner
Unified test execution script for XCEngine test suites.
Supports Windows and Linux platforms with CTest and Python wrapper integration.
Usage:
python run_tests.py [options]
Options:
--build Build tests before running
--unit-only Run only unit tests (skip integration tests)
--integration Run integration tests (includes unit tests)
--ci CI mode (skip GUI-dependent integration tests)
--dir <path> Run tests for specific directory
--verbose,-v Verbose output
--help,-h Show this help message
"""
import argparse
import subprocess
import sys
import os
import json
import time
from datetime import datetime
from pathlib import Path
from typing import Optional, List, Dict, Any
class Colors:
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
BOLD = "\033[1m"
class TestRunner:
PLATFORM_WINDOWS = "Windows"
PLATFORM_LINUX = "Linux"
PLATFORM_MACOS = "Darwin"
def __init__(self, build_dir: Path, source_dir: Path, verbose: bool = False):
self.build_dir = Path(build_dir)
self.source_dir = Path(source_dir)
self.verbose = verbose
self.platform = self._detect_platform()
self.results: List[Dict[str, Any]] = []
self.total_passed = 0
self.total_failed = 0
self.total_time = 0.0
def _detect_platform(self) -> str:
if sys.platform.startswith("win"):
return self.PLATFORM_WINDOWS
elif sys.platform.startswith("linux"):
return self.PLATFORM_LINUX
elif sys.platform.startswith("darwin"):
return self.PLATFORM_MACOS
return "Unknown"
def _log(self, message: str, color: str = ""):
if color:
print(f"{color}{message}{Colors.RESET}")
else:
print(message)
def _log_header(self, message: str):
print()
print(f"{Colors.BOLD}{Colors.BLUE}{'=' * 60}{Colors.RESET}")
print(f"{Colors.BOLD}{Colors.BLUE}{message}{Colors.RESET}")
print(f"{Colors.BOLD}{Colors.BLUE}{'=' * 60}{Colors.RESET}")
def _log_success(self, message: str):
self._log(f"{Colors.GREEN}[PASS] {message}{Colors.RESET}")
def _log_error(self, message: str):
self._log(f"{Colors.RED}[FAIL] {message}{Colors.RESET}")
def _log_warning(self, message: str):
self._log(f"{Colors.YELLOW}[WARN] {message}{Colors.RESET}")
def _log_info(self, message: str):
self._log(f" {message}")
def _run_command(
self, cmd: List[str], cwd: Optional[Path] = None, capture_output: bool = True
) -> subprocess.CompletedProcess:
if self.verbose:
self._log_info(f"Running: {' '.join(str(c) for c in cmd)}")
try:
result = subprocess.run(
cmd, cwd=cwd, capture_output=capture_output, text=True, timeout=300
)
return result
except subprocess.TimeoutExpired:
self._log_error(f"Command timed out: {' '.join(str(c) for c in cmd)}")
return subprocess.CompletedProcess(cmd, 1, "", "Timeout expired")
except Exception as e:
self._log_error(f"Command failed: {e}")
return subprocess.CompletedProcess(cmd, 1, "", str(e))
def build_tests(self, config: str = "Debug") -> bool:
self._log_header("Building Tests")
cmd = ["cmake", "--build", str(self.build_dir), "--config", config]
result = self._run_command(cmd)
if result.returncode == 0:
self._log_success("Build completed successfully")
return True
else:
self._log_error("Build failed")
if self.verbose and result.stdout:
print(result.stdout)
return False
def _get_ctest_executable(self) -> str:
if self.platform == self.PLATFORM_WINDOWS:
return "ctest"
return "ctest"
def _run_ctest_in_dir(self, test_dir: Path, label: str = "") -> Dict[str, Any]:
ctest_exe = self._get_ctest_executable()
cmd = [ctest_exe, "-C", "Debug", "--output-on-failure"]
if self.verbose:
cmd.append("--verbose")
start_time = time.time()
result = self._run_command(cmd, cwd=test_dir)
elapsed = time.time() - start_time
passed = 0
failed = 0
output = result.stdout + result.stderr
if "100% tests passed" in output or "passed" in output.lower():
if "0 tests failed" in output:
passed_match = output.find("tests passed")
if passed_match != -1:
try:
num_str = (
output[max(0, passed_match - 5) : passed_match]
.strip()
.split()[-1]
)
passed = int(num_str)
except:
passed = 1
else:
passed = 1
if "Failed" in output or result.returncode != 0:
failed = 1
return {
"name": label or f"Tests in {test_dir.name}",
"passed": passed,
"failed": failed,
"time": elapsed,
"returncode": result.returncode,
"output": output if self.verbose else "",
}
def run_ctest(self, test_dir: Optional[Path] = None) -> bool:
if test_dir:
test_dirs = [Path(test_dir)]
else:
test_dirs = [
self.build_dir / "tests" / "math",
self.build_dir / "tests" / "core",
self.build_dir / "tests" / "containers",
self.build_dir / "tests" / "memory",
self.build_dir / "tests" / "threading",
self.build_dir / "tests" / "debug",
self.build_dir / "tests" / "Resources",
self.build_dir / "tests" / "RHI" / "D3D12" / "unit",
]
all_passed = True
for td in test_dirs:
if not td.exists():
self._log_warning(f"Test directory not found: {td}")
continue
self._log_header(f"Running Tests: {td.name}")
result = self._run_ctest_in_dir(td, td.name)
self.results.append(result)
if result["failed"] > 0:
self._log_error(f"Failed: {result['name']}")
all_passed = False
else:
self._log_success(f"Passed: {result['name']} ({result['time']:.2f}s)")
if self.verbose and result.get("output"):
print(result["output"])
return all_passed
def _run_integration_test(
self,
test_name: str,
exe_path: Path,
output_ppm: str,
gt_ppm: Path,
threshold: int = 5,
) -> Dict[str, Any]:
wrapper_script = (
self.source_dir
/ "tests"
/ "RHI"
/ "D3D12"
/ "integration"
/ "run_integration_test.py"
)
if not wrapper_script.exists():
return {
"name": test_name,
"passed": 0,
"failed": 1,
"time": 0,
"error": "Wrapper script not found",
}
python_exe = sys.executable
cmd = [
python_exe,
str(wrapper_script),
str(exe_path),
output_ppm,
str(gt_ppm),
str(threshold),
]
start_time = time.time()
result = self._run_command(cmd, cwd=exe_path.parent)
elapsed = time.time() - start_time
passed = 1 if result.returncode == 0 else 0
failed = 0 if passed else 1
return {
"name": test_name,
"passed": passed,
"failed": failed,
"time": elapsed,
"returncode": result.returncode,
"output": result.stdout + result.stderr,
}
def run_integration_tests(self) -> bool:
integration_dir = self.build_dir / "tests" / "RHI" / "D3D12" / "integration"
integration_src_dir = (
self.source_dir / "tests" / "RHI" / "D3D12" / "integration"
)
if not integration_dir.exists():
self._log_warning(
f"Integration test directory not found: {integration_dir}"
)
return True
self._log_header("Running Integration Tests")
integration_tests = [
("D3D12_Minimal_Integration", "minimal", "minimal.ppm"),
("D3D12_Sphere_Integration", "sphere", "sphere.ppm"),
("D3D12_Triangle_Integration", "triangle", "triangle.ppm"),
("D3D12_Quad_Integration", "quad", "quad.ppm"),
]
all_passed = True
for test_name, test_folder, output_ppm in integration_tests:
exe_path = (
integration_dir
/ test_folder
/ "Debug"
/ f"D3D12_{test_folder.title()}.exe"
)
gt_ppm = integration_src_dir / test_folder / "GT.ppm"
if exe_path.exists() and gt_ppm.exists():
result = self._run_integration_test(
test_name, exe_path, output_ppm, gt_ppm, 0
)
self.results.append(result)
if result["failed"] > 0:
self._log_error(f"Failed: {result['name']}")
if self.verbose:
self._log_info(result.get("output", ""))
all_passed = False
else:
self._log_success(
f"Passed: {result['name']} ({result['time']:.2f}s)"
)
else:
self._log_warning(f"{test_name} not found, skipping")
return all_passed
def _summarize_results(self):
self._log_header("Test Summary")
total_passed = 0
total_failed = 0
total_time = 0.0
for result in self.results:
total_passed += result.get("passed", 0)
total_failed += result.get("failed", 0)
total_time += result.get("time", 0)
self.total_passed = total_passed
self.total_failed = total_failed
self.total_time = total_time
print()
print(f" Platform: {self.platform}")
print(f" Total Passed: {Colors.GREEN}{total_passed}{Colors.RESET}")
print(
f" Total Failed: {Colors.RED if total_failed > 0 else Colors.GREEN}{total_failed}{Colors.RESET}"
)
print(f" Total Time: {total_time:.2f}s")
print()
def run(
self,
build: bool = False,
unit_only: bool = False,
integration: bool = False,
ci_mode: bool = False,
test_dir: Optional[Path] = None,
) -> int:
print()
print(f"{Colors.BOLD}XCEngine Test Runner{Colors.RESET}")
print(f" Platform: {self.platform}")
print(f" Build Dir: {self.build_dir}")
print(f" Source Dir: {self.source_dir}")
if build:
if not self.build_tests():
self._log_error("Build failed, aborting tests")
return 1
if test_dir:
self._log_info(f"Running tests for: {test_dir}")
if self._run_ctest_in_dir(Path(test_dir)).get("failed", 0) > 0:
self._summarize_results()
return 1
self._summarize_results()
return 0
if not self.run_ctest():
self._summarize_results()
return 1
if integration or (not unit_only and not ci_mode):
if not self.run_integration_tests():
self._summarize_results()
return 1
self._summarize_results()
if self.total_failed > 0:
self._log_error("Some tests failed")
return 1
else:
self._log_success("All tests passed!")
return 0
def find_build_dir() -> Optional[Path]:
possible_dirs = [
Path(__file__).parent.parent / "build",
Path.cwd() / "build",
Path.cwd(),
]
for d in possible_dirs:
if d.exists() and (d / "CMakeCache.txt").exists():
return d
script_dir = Path(__file__).parent.parent
test_build = script_dir / "tests" / "build"
if test_build.exists():
return test_build
return Path.cwd()
def find_source_dir() -> Path:
return Path(__file__).parent.parent
def main():
parser = argparse.ArgumentParser(
description="XCEngine Test Runner - Unified test execution script",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python run_tests.py Run all tests
python run_tests.py --build Build and run tests
python run_tests.py --unit-only Run only unit tests
python run_tests.py --ci CI mode (skip GUI integration tests)
python run_tests.py --dir tests/math Run specific test directory
python run_tests.py --verbose Verbose output
""",
)
parser.add_argument(
"--build", "-b", action="store_true", help="Build tests before running"
)
parser.add_argument(
"--unit-only",
"-u",
action="store_true",
help="Run only unit tests (skip integration tests)",
)
parser.add_argument(
"--integration",
"-i",
action="store_true",
help="Run integration tests (includes unit tests)",
)
parser.add_argument(
"--ci",
"-c",
action="store_true",
help="CI mode (skip GUI-dependent integration tests)",
)
parser.add_argument(
"--dir", "-d", type=Path, help="Run tests for specific directory"
)
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
parser.add_argument(
"--build-dir", type=Path, help="Build directory (default: auto-detect)"
)
parser.add_argument(
"--source-dir", type=Path, help="Source directory (default: auto-detect)"
)
args = parser.parse_args()
build_dir = args.build_dir if args.build_dir else find_build_dir()
source_dir = args.source_dir if args.source_dir else find_source_dir()
if not build_dir:
print(f"{Colors.RED}Error: Could not find build directory{Colors.RESET}")
print("Please specify --build-dir or run from a directory with CMake cache")
return 1
runner = TestRunner(build_dir, source_dir, verbose=args.verbose)
return runner.run(
build=args.build,
unit_only=args.unit_only,
integration=args.integration,
ci_mode=args.ci,
test_dir=args.dir,
)
if __name__ == "__main__":
sys.exit(main() or 0)