From fa50892150398f3035e4860e4d6c664c119884f5 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 22 Mar 2026 21:41:56 +0800 Subject: [PATCH] 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 --- tests/RHI/D3D12/TEST_SPEC.md | 328 ++---------- .../D3D12/integration/minimal/CMakeLists.txt | 2 +- .../RHI/D3D12/integration/quad/CMakeLists.txt | 2 +- .../D3D12/integration/sphere/CMakeLists.txt | 7 +- .../D3D12/integration/triangle/CMakeLists.txt | 2 +- tests/run_tests.bat | 8 - tests/run_tests.py | 485 ------------------ 7 files changed, 65 insertions(+), 769 deletions(-) delete mode 100644 tests/run_tests.bat delete mode 100644 tests/run_tests.py diff --git a/tests/RHI/D3D12/TEST_SPEC.md b/tests/RHI/D3D12/TEST_SPEC.md index 96270904..6900ee41 100644 --- a/tests/RHI/D3D12/TEST_SPEC.md +++ b/tests/RHI/D3D12/TEST_SPEC.md @@ -1,223 +1,65 @@ # D3D12 测试专项规范 -本文档是 XCEngine 测试规范的 D3D12 专项补充。 +## 1. 构建命令 -**前置阅读**: [tests/TEST_SPEC.md](../TEST_SPEC.md) - 通用测试规范 +```bash +# 构建所有 +cmake --build --config Debug ---- +# 只构建单元测试 +cmake --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 --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 +ctest -C Debug --output-on-failure -### 2.3 当前测试统计 +# 只运行单元测试 +cd /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 /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} $/run_integration_test.py - $ - minimal.ppm - ${CMAKE_CURRENT_SOURCE_DIR}/minimal/GT.ppm - 5 - WORKING_DIRECTORY $ +add_test(NAME D3D12_Sphere_Integration + COMMAND ${Python3_EXECUTABLE} $/run_integration_test.py + $ + sphere.ppm + ${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm + 0 + WORKING_DIRECTORY $ ) ``` -### 3.4 Golden Image 规范 +`run_integration_test.py` 负责: +1. 启动 exe +2. 等待完成 +3. 调用 `compare_ppm.py` 比对图像 +4. 返回 0(通过)/ 1(失败) -| 属性 | 值 | -|------|-----| -| 格式 | PPM (P6) | -| 命名 | `GT_.ppm` | -| 阈值 | 默认 5% | -| 存储位置 | `tests/RHI/D3D12/integration//` | - -### 3.5 Golden Image 生成流程 - -1. 在干净硬件环境运行集成测试 -2. 截图保存为 `GT_.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 diff --git a/tests/RHI/D3D12/integration/minimal/CMakeLists.txt b/tests/RHI/D3D12/integration/minimal/CMakeLists.txt index 8dc90278..2d4256fa 100644 --- a/tests/RHI/D3D12/integration/minimal/CMakeLists.txt +++ b/tests/RHI/D3D12/integration/minimal/CMakeLists.txt @@ -46,6 +46,6 @@ add_test(NAME D3D12_Minimal_Integration $ minimal.ppm ${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm - 5 + 0 WORKING_DIRECTORY $ ) diff --git a/tests/RHI/D3D12/integration/quad/CMakeLists.txt b/tests/RHI/D3D12/integration/quad/CMakeLists.txt index a8442b3e..4aeedbe7 100644 --- a/tests/RHI/D3D12/integration/quad/CMakeLists.txt +++ b/tests/RHI/D3D12/integration/quad/CMakeLists.txt @@ -55,6 +55,6 @@ add_test(NAME D3D12_Quad_Integration $ quad.ppm ${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm - 5 + 0 WORKING_DIRECTORY $ ) \ No newline at end of file diff --git a/tests/RHI/D3D12/integration/sphere/CMakeLists.txt b/tests/RHI/D3D12/integration/sphere/CMakeLists.txt index 0f30ae5b..7a96d4a9 100644 --- a/tests/RHI/D3D12/integration/sphere/CMakeLists.txt +++ b/tests/RHI/D3D12/integration/sphere/CMakeLists.txt @@ -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 $/ + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm + $/ ) add_test(NAME D3D12_Sphere_Integration COMMAND ${Python3_EXECUTABLE} $/run_integration_test.py $ - screenshot.ppm + sphere.ppm ${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm - 5 + 0 WORKING_DIRECTORY $ ) diff --git a/tests/RHI/D3D12/integration/triangle/CMakeLists.txt b/tests/RHI/D3D12/integration/triangle/CMakeLists.txt index 55116b69..07332289 100644 --- a/tests/RHI/D3D12/integration/triangle/CMakeLists.txt +++ b/tests/RHI/D3D12/integration/triangle/CMakeLists.txt @@ -52,6 +52,6 @@ add_test(NAME D3D12_Triangle_Integration $ triangle.ppm ${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm - 5 + 0 WORKING_DIRECTORY $ ) diff --git a/tests/run_tests.bat b/tests/run_tests.bat deleted file mode 100644 index b819e2b4..00000000 --- a/tests/run_tests.bat +++ /dev/null @@ -1,8 +0,0 @@ -@echo off -echo Running XCEngine Tests via Python runner... -echo. - -python "%~dp0..\scripts\run_tests.py" --ci - -echo. -pause \ No newline at end of file diff --git a/tests/run_tests.py b/tests/run_tests.py deleted file mode 100644 index 837719a1..00000000 --- a/tests/run_tests.py +++ /dev/null @@ -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 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)