test: Add RHI integration test framework
Add integration tests for RHI module: - Add tests/RHI/integration/ directory with CMakeLists.txt - Add RHIIntegrationFixture for shared test utilities - Add minimal integration test (window creation, basic rendering) - Add compare_ppm.py for image comparison - Add run_integration_test.py test runner script These integration tests verify the complete rendering pipeline by comparing rendered output against ground truth PPM files.
This commit is contained in:
7
tests/RHI/integration/CMakeLists.txt
Normal file
7
tests/RHI/integration/CMakeLists.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.15)
|
||||||
|
|
||||||
|
find_package(GTest REQUIRED)
|
||||||
|
|
||||||
|
enable_testing()
|
||||||
|
|
||||||
|
add_subdirectory(minimal)
|
||||||
75
tests/RHI/integration/compare_ppm.py
Normal file
75
tests/RHI/integration/compare_ppm.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def read_ppm(filename):
|
||||||
|
with open(filename, "rb") as f:
|
||||||
|
header = f.readline()
|
||||||
|
if header != b"P6\n":
|
||||||
|
raise ValueError(f"Not a P6 PPM file: {filename}")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
line = f.readline()
|
||||||
|
if not line.startswith(b"#"):
|
||||||
|
break
|
||||||
|
|
||||||
|
dims = line.split()
|
||||||
|
width, height = int(dims[0]), int(dims[1])
|
||||||
|
|
||||||
|
line = f.readline()
|
||||||
|
maxval = int(line.strip())
|
||||||
|
|
||||||
|
data = f.read()
|
||||||
|
return width, height, data
|
||||||
|
|
||||||
|
|
||||||
|
def compare_ppm(file1, file2, threshold):
|
||||||
|
w1, h1, d1 = read_ppm(file1)
|
||||||
|
w2, h2, d2 = read_ppm(file2)
|
||||||
|
|
||||||
|
if w1 != w2 or h1 != h2:
|
||||||
|
print(f"ERROR: Size mismatch - {file1}: {w1}x{h1}, {file2}: {w2}x{h2}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
total_pixels = w1 * h1
|
||||||
|
diff_count = 0
|
||||||
|
|
||||||
|
for i in range(len(d1)):
|
||||||
|
diff = abs(d1[i] - d2[i])
|
||||||
|
if diff > threshold:
|
||||||
|
diff_count += 1
|
||||||
|
|
||||||
|
diff_percent = (diff_count / (total_pixels * 3)) * 100
|
||||||
|
|
||||||
|
print(f"Image 1: {file1} ({w1}x{h1})")
|
||||||
|
print(f"Image 2: {file2} ({w2}x{h2})")
|
||||||
|
print(f"Threshold: {threshold}")
|
||||||
|
print(f"Different pixels: {diff_count} / {total_pixels * 3} ({diff_percent:.2f}%)")
|
||||||
|
|
||||||
|
if diff_percent <= 1.0:
|
||||||
|
print("PASS: Images match!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("FAIL: Images differ!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 4:
|
||||||
|
print("Usage: python compare_ppm.py <file1.ppm> <file2.ppm> <threshold>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
file1 = sys.argv[1]
|
||||||
|
file2 = sys.argv[2]
|
||||||
|
threshold = int(sys.argv[3])
|
||||||
|
|
||||||
|
if not os.path.exists(file1):
|
||||||
|
print(f"ERROR: File not found: {file1}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not os.path.exists(file2):
|
||||||
|
print(f"ERROR: File not found: {file2}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
result = compare_ppm(file1, file2, threshold)
|
||||||
|
sys.exit(0 if result else 1)
|
||||||
188
tests/RHI/integration/fixtures/RHIIntegrationFixture.cpp
Normal file
188
tests/RHI/integration/fixtures/RHIIntegrationFixture.cpp
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
#include "RHIIntegrationFixture.h"
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
#include <windows.h>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
#include "XCEngine/RHI/D3D12/D3D12Device.h"
|
||||||
|
#include "XCEngine/RHI/D3D12/D3D12SwapChain.h"
|
||||||
|
#include "XCEngine/RHI/D3D12/D3D12Texture.h"
|
||||||
|
#include "XCEngine/RHI/D3D12/D3D12CommandList.h"
|
||||||
|
#include "XCEngine/RHI/RHIFence.h"
|
||||||
|
#include "XCEngine/RHI/RHIScreenshot.h"
|
||||||
|
#include "XCEngine/RHI/RHIEnums.h"
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace RHI {
|
||||||
|
namespace Integration {
|
||||||
|
|
||||||
|
void RHIIntegrationFixture::SetUp() {
|
||||||
|
WNDCLASSEXW wc = {};
|
||||||
|
wc.cbSize = sizeof(WNDCLASSEXW);
|
||||||
|
wc.style = CS_HREDRAW | CS_VREDRAW;
|
||||||
|
wc.lpfnWndProc = DefWindowProcW;
|
||||||
|
wc.hInstance = GetModuleHandle(nullptr);
|
||||||
|
wc.lpszClassName = L"XCEngine_RHI_Integration_Test";
|
||||||
|
RegisterClassExW(&wc);
|
||||||
|
|
||||||
|
const int width = 1280;
|
||||||
|
const int height = 720;
|
||||||
|
RECT rect = { 0, 0, width, height };
|
||||||
|
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE);
|
||||||
|
|
||||||
|
mWindow = CreateWindowExW(
|
||||||
|
0,
|
||||||
|
L"XCEngine_RHI_Integration_Test",
|
||||||
|
L"RHI Integration Test",
|
||||||
|
WS_OVERLAPPEDWINDOW,
|
||||||
|
CW_USEDEFAULT, CW_USEDEFAULT,
|
||||||
|
rect.right - rect.left, rect.bottom - rect.top,
|
||||||
|
NULL, NULL, GetModuleHandle(nullptr), NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ASSERT_NE(mWindow, nullptr);
|
||||||
|
|
||||||
|
mDevice = RHIFactory::CreateRHIDevice(GetParam());
|
||||||
|
ASSERT_NE(mDevice, nullptr);
|
||||||
|
|
||||||
|
bool initResult = false;
|
||||||
|
if (GetParam() == RHIType::D3D12) {
|
||||||
|
RHIDeviceDesc desc = {};
|
||||||
|
desc.enableDebugLayer = false;
|
||||||
|
desc.enableGPUValidation = false;
|
||||||
|
initResult = mDevice->Initialize(desc);
|
||||||
|
} else if (GetParam() == RHIType::OpenGL) {
|
||||||
|
auto* oglDevice = static_cast<OpenGLDevice*>(mDevice);
|
||||||
|
initResult = oglDevice->InitializeWithExistingWindow(mWindow);
|
||||||
|
}
|
||||||
|
ASSERT_TRUE(initResult);
|
||||||
|
|
||||||
|
SwapChainDesc swapDesc = {};
|
||||||
|
swapDesc.windowHandle = mWindow;
|
||||||
|
swapDesc.width = width;
|
||||||
|
swapDesc.height = height;
|
||||||
|
swapDesc.bufferCount = 2;
|
||||||
|
mSwapChain = mDevice->CreateSwapChain(swapDesc);
|
||||||
|
ASSERT_NE(mSwapChain, nullptr);
|
||||||
|
|
||||||
|
CommandQueueDesc queueDesc = {};
|
||||||
|
queueDesc.queueType = static_cast<uint32_t>(CommandQueueType::Direct);
|
||||||
|
mCommandQueue = mDevice->CreateCommandQueue(queueDesc);
|
||||||
|
ASSERT_NE(mCommandQueue, nullptr);
|
||||||
|
|
||||||
|
CommandListDesc cmdDesc = {};
|
||||||
|
cmdDesc.commandListType = static_cast<uint32_t>(CommandQueueType::Direct);
|
||||||
|
mCommandList = mDevice->CreateCommandList(cmdDesc);
|
||||||
|
ASSERT_NE(mCommandList, nullptr);
|
||||||
|
|
||||||
|
mScreenshot = RHIScreenshot::Create(GetParam());
|
||||||
|
ASSERT_NE(mScreenshot, nullptr);
|
||||||
|
|
||||||
|
ShowWindow(mWindow, SW_SHOW);
|
||||||
|
UpdateWindow(mWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RHIIntegrationFixture::BeginRender() {
|
||||||
|
mCurrentBackBufferIndex = mSwapChain->GetCurrentBackBufferIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RHIIntegrationFixture::EndRender() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void RHIIntegrationFixture::TearDown() {
|
||||||
|
if (mScreenshot) {
|
||||||
|
mScreenshot->Shutdown();
|
||||||
|
delete mScreenshot;
|
||||||
|
mScreenshot = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mCommandList) {
|
||||||
|
mCommandList->Shutdown();
|
||||||
|
delete mCommandList;
|
||||||
|
mCommandList = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mCommandQueue) {
|
||||||
|
mCommandQueue->Shutdown();
|
||||||
|
delete mCommandQueue;
|
||||||
|
mCommandQueue = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mSwapChain) {
|
||||||
|
mSwapChain->Shutdown();
|
||||||
|
delete mSwapChain;
|
||||||
|
mSwapChain = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mDevice) {
|
||||||
|
mDevice->Shutdown();
|
||||||
|
delete mDevice;
|
||||||
|
mDevice = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mWindow) {
|
||||||
|
DestroyWindow(mWindow);
|
||||||
|
mWindow = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RHIIntegrationFixture::WaitForGPU() {
|
||||||
|
if (mDevice == nullptr || mCommandQueue == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GetParam() == RHIType::D3D12) {
|
||||||
|
FenceDesc fenceDesc = {};
|
||||||
|
fenceDesc.initialValue = 0;
|
||||||
|
fenceDesc.flags = 0;
|
||||||
|
auto* fence = mDevice->CreateFence(fenceDesc);
|
||||||
|
if (fence) {
|
||||||
|
mCommandQueue->Signal(fence, 1);
|
||||||
|
fence->Wait(1);
|
||||||
|
|
||||||
|
for (int i = 0; i < 100; i++) {
|
||||||
|
if (fence->GetCompletedValue() >= 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Sleep(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
fence->Shutdown();
|
||||||
|
delete fence;
|
||||||
|
}
|
||||||
|
Sleep(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RHIIntegrationFixture::TakeScreenshot(const char* filename) {
|
||||||
|
if (!mScreenshot || !mDevice || !mSwapChain) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return mScreenshot->Capture(mDevice, mSwapChain, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RHIIntegrationFixture::CompareWithGoldenTemplate(const char* outputPpm, const char* gtPpm, float threshold) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
fs::path exeDir = fs::current_path();
|
||||||
|
fs::path outputPath = exeDir / outputPpm;
|
||||||
|
fs::path gtPath = exeDir / gtPpm;
|
||||||
|
|
||||||
|
if (!fs::exists(outputPath)) {
|
||||||
|
std::cerr << "Output file not found: " << outputPath << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs::exists(gtPath)) {
|
||||||
|
std::cerr << "Golden template not found: " << gtPath << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cmd = "python compare_ppm.py \"" + outputPath.string() + "\" \"" + gtPath.string() + "\" " + std::to_string(static_cast<int>(threshold));
|
||||||
|
int result = system(cmd.c_str());
|
||||||
|
return result == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Integration
|
||||||
|
} // namespace RHI
|
||||||
|
} // namespace XCEngine
|
||||||
55
tests/RHI/integration/fixtures/RHIIntegrationFixture.h
Normal file
55
tests/RHI/integration/fixtures/RHIIntegrationFixture.h
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <string>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "XCEngine/RHI/RHIFactory.h"
|
||||||
|
#include "XCEngine/RHI/RHIDevice.h"
|
||||||
|
#include "XCEngine/RHI/RHICommandQueue.h"
|
||||||
|
#include "XCEngine/RHI/RHICommandList.h"
|
||||||
|
#include "XCEngine/RHI/RHISwapChain.h"
|
||||||
|
#include "XCEngine/RHI/RHIScreenshot.h"
|
||||||
|
#include "XCEngine/RHI/RHIEnums.h"
|
||||||
|
#include "XCEngine/RHI/OpenGL/OpenGLDevice.h"
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace RHI {
|
||||||
|
namespace Integration {
|
||||||
|
|
||||||
|
class RHIIntegrationFixture : public ::testing::TestWithParam<RHIType> {
|
||||||
|
protected:
|
||||||
|
void SetUp() override;
|
||||||
|
void TearDown() override;
|
||||||
|
|
||||||
|
void WaitForGPU();
|
||||||
|
void BeginRender();
|
||||||
|
void EndRender();
|
||||||
|
|
||||||
|
RHIDevice* GetDevice() { return mDevice; }
|
||||||
|
RHISwapChain* GetSwapChain() { return mSwapChain; }
|
||||||
|
RHICommandQueue* GetCommandQueue() { return mCommandQueue; }
|
||||||
|
RHICommandList* GetCommandList() { return mCommandList; }
|
||||||
|
RHIScreenshot* GetScreenshot() { return mScreenshot; }
|
||||||
|
RHIType GetBackendType() const { return GetParam(); }
|
||||||
|
HWND GetWindowHandle() const { return mWindow; }
|
||||||
|
int GetCurrentBackBufferIndex() const { return mCurrentBackBufferIndex; }
|
||||||
|
|
||||||
|
virtual void RenderFrame() {}
|
||||||
|
|
||||||
|
bool TakeScreenshot(const char* filename);
|
||||||
|
bool CompareWithGoldenTemplate(const char* outputPpm, const char* gtPpm, float threshold = 0.0f);
|
||||||
|
|
||||||
|
private:
|
||||||
|
RHIDevice* mDevice = nullptr;
|
||||||
|
RHISwapChain* mSwapChain = nullptr;
|
||||||
|
RHICommandQueue* mCommandQueue = nullptr;
|
||||||
|
RHICommandList* mCommandList = nullptr;
|
||||||
|
RHIScreenshot* mScreenshot = nullptr;
|
||||||
|
HWND mWindow = nullptr;
|
||||||
|
int mCurrentBackBufferIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Integration
|
||||||
|
} // namespace RHI
|
||||||
|
} // namespace XCEngine
|
||||||
58
tests/RHI/integration/minimal/CMakeLists.txt
Normal file
58
tests/RHI/integration/minimal/CMakeLists.txt
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.15)
|
||||||
|
|
||||||
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
|
||||||
|
project(rhi_integration_minimal)
|
||||||
|
|
||||||
|
set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine)
|
||||||
|
set(PACKAGE_DIR ${CMAKE_SOURCE_DIR}/tests/opengl/package)
|
||||||
|
|
||||||
|
get_filename_component(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../.. ABSOLUTE)
|
||||||
|
|
||||||
|
add_executable(rhi_integration_minimal
|
||||||
|
main.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../fixtures/RHIIntegrationFixture.cpp
|
||||||
|
${PACKAGE_DIR}/src/glad.c
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(rhi_integration_minimal PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../fixtures
|
||||||
|
${ENGINE_ROOT_DIR}/include
|
||||||
|
${PACKAGE_DIR}/include
|
||||||
|
${PROJECT_ROOT_DIR}/engine/src
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(rhi_integration_minimal PRIVATE
|
||||||
|
d3d12
|
||||||
|
dxgi
|
||||||
|
d3dcompiler
|
||||||
|
winmm
|
||||||
|
opengl32
|
||||||
|
XCEngine
|
||||||
|
GTest::gtest
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(rhi_integration_minimal PRIVATE
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
XCENGINE_SUPPORT_OPENGL
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_command(TARGET rhi_integration_minimal POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/RHI/integration/compare_ppm.py
|
||||||
|
$<TARGET_FILE_DIR:rhi_integration_minimal>/
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/RHI/D3D12/integration/minimal/GT.ppm
|
||||||
|
$<TARGET_FILE_DIR:rhi_integration_minimal>/GT_D3D12.ppm
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/RHI/OpenGL/integration/minimal/GT.ppm
|
||||||
|
$<TARGET_FILE_DIR:rhi_integration_minimal>/GT_OpenGL.ppm
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
${ENGINE_ROOT_DIR}/third_party/renderdoc/renderdoc.dll
|
||||||
|
$<TARGET_FILE_DIR:rhi_integration_minimal>/
|
||||||
|
)
|
||||||
|
|
||||||
|
include(GoogleTest)
|
||||||
|
gtest_discover_tests(rhi_integration_minimal)
|
||||||
73
tests/RHI/integration/minimal/main.cpp
Normal file
73
tests/RHI/integration/minimal/main.cpp
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#include <windows.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "../fixtures/RHIIntegrationFixture.h"
|
||||||
|
|
||||||
|
using namespace XCEngine::RHI;
|
||||||
|
using namespace XCEngine::RHI::Integration;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class MinimalTest : public RHIIntegrationFixture {
|
||||||
|
protected:
|
||||||
|
void RenderFrame() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
void MinimalTest::RenderFrame() {
|
||||||
|
RHICommandList* cmdList = GetCommandList();
|
||||||
|
RHICommandQueue* cmdQueue = GetCommandQueue();
|
||||||
|
|
||||||
|
cmdList->Reset();
|
||||||
|
|
||||||
|
Viewport viewport = { 0.0f, 0.0f, 1280.0f, 720.0f, 0.0f, 1.0f };
|
||||||
|
Rect scissorRect = { 0, 0, 1280, 720 };
|
||||||
|
cmdList->SetViewport(viewport);
|
||||||
|
cmdList->SetScissorRect(scissorRect);
|
||||||
|
|
||||||
|
float clearColor[] = { 1.0f, 0.0f, 0.0f, 1.0f };
|
||||||
|
cmdList->Clear(clearColor[0], clearColor[1], clearColor[2], clearColor[3], 1);
|
||||||
|
|
||||||
|
cmdList->Close();
|
||||||
|
void* cmdLists[] = { cmdList };
|
||||||
|
cmdQueue->ExecuteCommandLists(1, cmdLists);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_P(MinimalTest, RenderClear) {
|
||||||
|
RHICommandQueue* cmdQueue = GetCommandQueue();
|
||||||
|
RHISwapChain* swapChain = GetSwapChain();
|
||||||
|
const int targetFrameCount = 30;
|
||||||
|
|
||||||
|
for (int frameCount = 0; frameCount <= targetFrameCount; ++frameCount) {
|
||||||
|
if (frameCount > 0) {
|
||||||
|
cmdQueue->WaitForPreviousFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
BeginRender();
|
||||||
|
RenderFrame();
|
||||||
|
EndRender();
|
||||||
|
|
||||||
|
if (frameCount >= targetFrameCount) {
|
||||||
|
cmdQueue->WaitForIdle();
|
||||||
|
ASSERT_TRUE(TakeScreenshot("minimal.ppm"));
|
||||||
|
ASSERT_TRUE(CompareWithGoldenTemplate("minimal.ppm",
|
||||||
|
(GetBackendType() == RHIType::D3D12) ? "GT_D3D12.ppm" : "GT_OpenGL.ppm",
|
||||||
|
(GetBackendType() == RHIType::D3D12) ? 0.0f : 5.0f));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
swapChain->Present(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
INSTANTIATE_TEST_SUITE_P(D3D12, MinimalTest, ::testing::Values(RHIType::D3D12));
|
||||||
|
INSTANTIATE_TEST_SUITE_P(OpenGL, MinimalTest, ::testing::Values(RHIType::OpenGL));
|
||||||
|
|
||||||
|
GTEST_API_ int main(int argc, char** argv) {
|
||||||
|
testing::InitGoogleTest(&argc, argv);
|
||||||
|
return RUN_ALL_TESTS();
|
||||||
|
}
|
||||||
126
tests/RHI/integration/run_integration_test.py
Normal file
126
tests/RHI/integration/run_integration_test.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def run_integration_test(exe_path, output_ppm, gt_ppm, threshold, backend, timeout=120):
|
||||||
|
"""
|
||||||
|
Run a RHI integration test and compare output with golden template.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
exe_path: Path to the test executable
|
||||||
|
output_ppm: Filename of the output screenshot
|
||||||
|
gt_ppm: Path to the golden template PPM file
|
||||||
|
threshold: Pixel difference threshold for comparison
|
||||||
|
backend: Backend type (D3D12 or OpenGL)
|
||||||
|
timeout: Maximum time to wait for test completion (seconds)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
0 on success, non-zero on failure
|
||||||
|
"""
|
||||||
|
exe_dir = os.path.dirname(os.path.abspath(exe_path))
|
||||||
|
output_path = os.path.join(exe_dir, output_ppm)
|
||||||
|
|
||||||
|
print(f"[Integration Test] Starting: {exe_path}")
|
||||||
|
print(f"[Integration Test] Working directory: {exe_dir}")
|
||||||
|
print(f"[Integration Test] Expected output: {output_path}")
|
||||||
|
print(f"[Integration Test] Backend: {backend}")
|
||||||
|
|
||||||
|
if not os.path.exists(exe_path):
|
||||||
|
print(f"[Integration Test] ERROR: Executable not found: {exe_path}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if not os.path.exists(gt_ppm):
|
||||||
|
print(f"[Integration Test] ERROR: Golden template not found: {gt_ppm}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if os.path.exists(output_path):
|
||||||
|
print(f"[Integration Test] Removing old output: {output_path}")
|
||||||
|
os.remove(output_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"[Integration Test] Launching process...")
|
||||||
|
start_time = time.time()
|
||||||
|
process = subprocess.Popen(
|
||||||
|
[exe_path, backend],
|
||||||
|
cwd=exe_dir,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
creationflags=subprocess.CREATE_NEW_CONSOLE if os.name == "nt" else 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
returncode = None
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
returncode = process.poll()
|
||||||
|
if returncode is not None:
|
||||||
|
break
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
if returncode is None:
|
||||||
|
print(f"[Integration Test] ERROR: Process timed out after {timeout}s")
|
||||||
|
process.kill()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
print(
|
||||||
|
f"[Integration Test] Process finished in {elapsed:.1f}s with exit code: {returncode}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if returncode != 0:
|
||||||
|
print(f"[Integration Test] ERROR: Process returned non-zero exit code")
|
||||||
|
stdout, stderr = process.communicate(timeout=5)
|
||||||
|
if stdout:
|
||||||
|
print(
|
||||||
|
f"[Integration Test] STDOUT:\n{stdout.decode('utf-8', errors='replace')}"
|
||||||
|
)
|
||||||
|
if stderr:
|
||||||
|
print(
|
||||||
|
f"[Integration Test] STDERR:\n{stderr.decode('utf-8', errors='replace')}"
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Integration Test] ERROR: Failed to run process: {e}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if not os.path.exists(output_path):
|
||||||
|
print(f"[Integration Test] ERROR: Output file not created: {output_path}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"[Integration Test] Running image comparison...")
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
compare_script = os.path.join(script_dir, "compare_ppm.py")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, compare_script, output_path, gt_ppm, str(threshold)],
|
||||||
|
cwd=exe_dir,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(f"[Integration Test] Comparison STDERR: {result.stderr}")
|
||||||
|
return result.returncode
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Integration Test] ERROR: Failed to run comparison: {e}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 6:
|
||||||
|
print(
|
||||||
|
"Usage: run_integration_test.py <exe_path> <output_ppm> <gt_ppm> <threshold> <backend>"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
exe_path = sys.argv[1]
|
||||||
|
output_ppm = sys.argv[2]
|
||||||
|
gt_ppm = sys.argv[3]
|
||||||
|
threshold = int(sys.argv[4])
|
||||||
|
backend = sys.argv[5]
|
||||||
|
|
||||||
|
exit_code = run_integration_test(exe_path, output_ppm, gt_ppm, threshold, backend)
|
||||||
|
sys.exit(exit_code)
|
||||||
Reference in New Issue
Block a user