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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
@echo off
|
||||
echo Running XCEngine Tests via Python runner...
|
||||
echo.
|
||||
|
||||
python "%~dp0..\scripts\run_tests.py" --ci
|
||||
|
||||
echo.
|
||||
pause
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user