feat: Integrate ImGuiTestEngine with gRPC service for dynamic test execution and improve initialization flow

This commit is contained in:
scawful
2025-10-02 00:24:15 -04:00
parent ead85c87b5
commit 4320b67da1
7 changed files with 388 additions and 83 deletions

View File

@@ -4,12 +4,33 @@
#include <chrono>
#include <iostream>
#include <thread>
#include "absl/strings/str_format.h"
#include "app/core/proto/imgui_test_harness.grpc.pb.h"
#include "app/core/proto/imgui_test_harness.pb.h"
#include "app/test/test_manager.h"
#include "yaze.h" // For YAZE_VERSION_STRING
#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
#include "imgui_test_engine/imgui_te_engine.h"
#include "imgui_test_engine/imgui_te_context.h"
// Helper to register and run a test dynamically
namespace {
struct DynamicTestData {
std::function<void(ImGuiTestContext*)> test_func;
};
void RunDynamicTest(ImGuiTestContext* ctx) {
auto* data = (DynamicTestData*)ctx->Test->UserData;
if (data && data->test_func) {
data->test_func(ctx);
}
}
} // namespace
#endif
#include <grpcpp/grpcpp.h>
#include <grpcpp/server_builder.h>
@@ -113,10 +134,113 @@ absl::Status ImGuiTestHarnessServiceImpl::Click(const ClickRequest* request,
ClickResponse* response) {
auto start = std::chrono::steady_clock::now();
#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
// Validate test manager
if (!test_manager_) {
response->set_success(false);
response->set_message("TestManager not available");
return absl::OkStatus();
}
// Get ImGuiTestEngine
ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
if (!engine) {
response->set_success(false);
response->set_message("ImGuiTestEngine not initialized");
return absl::OkStatus();
}
// Parse target: "button:Open ROM" -> type=button, label="Open ROM"
std::string target = request->target();
size_t colon_pos = target.find(':');
if (colon_pos == std::string::npos) {
response->set_success(false);
response->set_message("Invalid target format. Use 'type:label' (e.g. 'button:Open ROM')");
return absl::OkStatus();
}
std::string widget_type = target.substr(0, colon_pos);
std::string widget_label = target.substr(colon_pos + 1);
// Convert click type
ImGuiMouseButton mouse_button = ImGuiMouseButton_Left;
switch (request->type()) {
case ClickRequest::LEFT:
mouse_button = ImGuiMouseButton_Left;
break;
case ClickRequest::RIGHT:
mouse_button = ImGuiMouseButton_Right;
break;
case ClickRequest::MIDDLE:
mouse_button = ImGuiMouseButton_Middle;
break;
case ClickRequest::DOUBLE:
// Double click handled below
break;
default:
break;
}
// Create a dynamic test to perform the click
bool success = false;
std::string message;
auto test_data = std::make_shared<DynamicTestData>();
test_data->test_func = [=, &success, &message](ImGuiTestContext* ctx) {
try {
if (request->type() == ClickRequest::DOUBLE) {
ctx->ItemDoubleClick(widget_label.c_str());
} else {
ctx->ItemClick(widget_label.c_str(), mouse_button);
}
success = true;
message = absl::StrFormat("Clicked %s '%s'", widget_type, widget_label);
} catch (const std::exception& e) {
success = false;
message = absl::StrFormat("Click failed: %s", e.what());
}
};
// Register and queue the test
std::string test_name = absl::StrFormat("grpc_click_%lld",
std::chrono::system_clock::now().time_since_epoch().count());
ImGuiTest* test = IM_REGISTER_TEST(engine, "grpc", test_name.c_str());
test->TestFunc = RunDynamicTest;
test->UserData = test_data.get();
ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
// Wait for test to complete (with timeout)
auto timeout = std::chrono::seconds(5);
auto wait_start = std::chrono::steady_clock::now();
while (test->Output.Status == ImGuiTestStatus_Queued || test->Output.Status == ImGuiTestStatus_Running) {
if (std::chrono::steady_clock::now() - wait_start > timeout) {
success = false;
message = "Test timeout";
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
if (test->Output.Status == ImGuiTestStatus_Success) {
success = true;
} else if (test->Output.Status != ImGuiTestStatus_Unknown) {
success = false;
if (message.empty()) {
message = "Test failed";
}
}
// Cleanup
ImGuiTestEngine_UnregisterTest(engine, test);
#else
// ImGuiTestEngine not available - stub implementation
std::string target = request->target();
size_t colon_pos = target.find(':');
if (colon_pos == std::string::npos) {
response->set_success(false);
response->set_message("Invalid target format. Use 'type:label'");
@@ -125,16 +249,17 @@ absl::Status ImGuiTestHarnessServiceImpl::Click(const ClickRequest* request,
std::string widget_type = target.substr(0, colon_pos);
std::string widget_label = target.substr(colon_pos + 1);
bool success = true;
std::string message = absl::StrFormat("[STUB] Clicked %s '%s' (ImGuiTestEngine not available)",
widget_type, widget_label);
#endif
// TODO: Integrate with ImGuiTestEngine to actually perform the click
// For now, just simulate success
// Calculate execution time
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start);
response->set_success(true);
response->set_message(
absl::StrFormat("Clicked %s '%s'", widget_type, widget_label));
response->set_success(success);
response->set_message(message);
response->set_execution_time_ms(elapsed.count());
return absl::OkStatus();
@@ -144,14 +269,16 @@ absl::Status ImGuiTestHarnessServiceImpl::Type(const TypeRequest* request,
TypeResponse* response) {
auto start = std::chrono::steady_clock::now();
// TODO: Implement actual text input via ImGuiTestEngine
// TODO: Implement with ImGuiTestEngine dynamic tests like Click handler
bool success = true;
std::string message = absl::StrFormat("Typed '%s' into %s (implementation pending)",
request->text(), request->target());
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start);
response->set_success(true);
response->set_message(
absl::StrFormat("Typed '%s' into %s", request->text(), request->target()));
response->set_success(success);
response->set_message(message);
response->set_execution_time_ms(elapsed.count());
return absl::OkStatus();
@@ -161,28 +288,27 @@ absl::Status ImGuiTestHarnessServiceImpl::Wait(const WaitRequest* request,
WaitResponse* response) {
auto start = std::chrono::steady_clock::now();
// TODO: Implement actual condition polling
// TODO: Implement with ImGuiTestEngine dynamic tests
bool condition_met = true;
std::string message = absl::StrFormat("Condition '%s' met (implementation pending)",
request->condition());
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start);
response->set_success(true);
response->set_message(
absl::StrFormat("Condition '%s' met", request->condition()));
response->set_elapsed_ms(elapsed.count());
response->set_success(condition_met);
response->set_message(message);
response->set_elapsed_ms(0);
return absl::OkStatus();
}
absl::Status ImGuiTestHarnessServiceImpl::Assert(const AssertRequest* request,
AssertResponse* response) {
// TODO: Implement actual assertion checking
// TODO: Implement with ImGuiTestEngine dynamic tests
response->set_success(true);
response->set_message(
absl::StrFormat("Assertion '%s' passed", request->condition()));
response->set_actual_value("(not implemented)");
response->set_expected_value("(not implemented)");
absl::StrFormat("Assertion '%s' passed (implementation pending)",
request->condition()));
response->set_actual_value("(pending)");
response->set_expected_value(""); // Set empty string instead of accessing non-existent field
return absl::OkStatus();
}
@@ -212,13 +338,17 @@ ImGuiTestHarnessServer::~ImGuiTestHarnessServer() {
Shutdown();
}
absl::Status ImGuiTestHarnessServer::Start(int port) {
absl::Status ImGuiTestHarnessServer::Start(int port, TestManager* test_manager) {
if (server_) {
return absl::FailedPreconditionError("Server already running");
}
// Create the service implementation
service_ = std::make_unique<ImGuiTestHarnessServiceImpl>();
if (!test_manager) {
return absl::InvalidArgumentError("TestManager cannot be null");
}
// Create the service implementation with TestManager reference
service_ = std::make_unique<ImGuiTestHarnessServiceImpl>(test_manager);
// Create the gRPC service wrapper (store as member to prevent it from going out of scope)
grpc_service_ = std::make_unique<ImGuiTestHarnessServiceGrpc>(service_.get());
@@ -245,7 +375,7 @@ absl::Status ImGuiTestHarnessServer::Start(int port) {
port_ = port;
std::cout << "✓ ImGuiTestHarness gRPC server listening on " << server_address
<< "\n";
<< " (with TestManager integration)\n";
std::cout << " Use 'grpcurl -plaintext -d '{\"message\":\"test\"}' "
<< server_address << " yaze.test.ImGuiTestHarness/Ping' to test\n";

View File

@@ -20,6 +20,9 @@ class ServerContext;
namespace yaze {
namespace test {
// Forward declare TestManager
class TestManager;
// Forward declare proto types
class PingRequest;
class PingResponse;
@@ -38,7 +41,9 @@ class ScreenshotResponse;
// This class provides the actual RPC handlers for automated GUI testing
class ImGuiTestHarnessServiceImpl {
public:
ImGuiTestHarnessServiceImpl() = default;
// Constructor now takes TestManager reference for ImGuiTestEngine access
explicit ImGuiTestHarnessServiceImpl(TestManager* test_manager)
: test_manager_(test_manager) {}
~ImGuiTestHarnessServiceImpl() = default;
// Disable copy and move
@@ -66,6 +71,9 @@ class ImGuiTestHarnessServiceImpl {
// Capture a screenshot
absl::Status Screenshot(const ScreenshotRequest* request,
ScreenshotResponse* response);
private:
TestManager* test_manager_; // Non-owning pointer to access ImGuiTestEngine
};
// Forward declaration of the gRPC service wrapper
@@ -80,8 +88,9 @@ class ImGuiTestHarnessServer {
// Start the gRPC server on the specified port
// @param port The port to listen on (default 50051)
// @param test_manager Pointer to TestManager for ImGuiTestEngine access
// @return OK status if server started successfully, error otherwise
absl::Status Start(int port = 50051);
absl::Status Start(int port, TestManager* test_manager);
// Shutdown the server gracefully
void Shutdown();

View File

@@ -45,6 +45,11 @@ absl::Status CreateWindow(Window& window, int flags) {
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
// Initialize ImGuiTestEngine after ImGui context is created
#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
test::TestManager::Get().InitializeUITesting();
#endif
ImGui_ImplSDL2_InitForSDLRenderer(window.window_.get(),
Renderer::Get().renderer());
ImGui_ImplSDLRenderer2_Init(Renderer::Get().renderer());

View File

@@ -2,6 +2,7 @@
#include "app/core/platform/app_delegate.h"
#endif
#define IMGUI_DEFINE_MATH_OPERATORS
#include "absl/debugging/failure_signal_handler.h"
#include "absl/debugging/symbolize.h"
#include "app/core/controller.h"
@@ -11,6 +12,7 @@
#ifdef YAZE_WITH_GRPC
#include "app/core/imgui_test_harness_service.h"
#include "app/test/test_manager.h"
#endif
/**
@@ -71,11 +73,14 @@ int main(int argc, char **argv) {
#ifdef YAZE_WITH_GRPC
// Start gRPC test harness server if requested
if (FLAGS_enable_test_harness->Get()) {
// Get TestManager instance (initializes UI testing if available)
auto& test_manager = yaze::test::TestManager::Get();
auto& server = yaze::test::ImGuiTestHarnessServer::Instance();
int port = FLAGS_test_harness_port->Get();
std::cout << "\n🚀 Starting ImGui Test Harness on port " << port << "..." << std::endl;
auto status = server.Start(port);
auto status = server.Start(port, &test_manager);
if (!status.ok()) {
std::cerr << "❌ ERROR: Failed to start test harness server on port " << port << std::endl;
std::cerr << " " << status.message() << std::endl;

View File

@@ -64,10 +64,8 @@ TestManager& TestManager::Get() {
}
TestManager::TestManager() {
// Initialize UI test engine
#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
InitializeUITesting();
#endif
// Note: UI test engine initialization is deferred until ImGui context is ready
// Call InitializeUITesting() explicitly after ImGui::CreateContext()
}
TestManager::~TestManager() {
@@ -79,6 +77,12 @@ TestManager::~TestManager() {
#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
void TestManager::InitializeUITesting() {
if (!ui_test_engine_) {
// Check if ImGui context is ready
if (ImGui::GetCurrentContext() == nullptr) {
util::logf("[TestManager] Warning: ImGui context not ready, deferring test engine initialization");
return;
}
ui_test_engine_ = ImGuiTestEngine_CreateContext();
if (ui_test_engine_) {
ImGuiTestEngineIO& test_io = ImGuiTestEngine_GetIO(ui_test_engine_);
@@ -88,6 +92,7 @@ void TestManager::InitializeUITesting() {
// Start the test engine
ImGuiTestEngine_Start(ui_test_engine_, ImGui::GetCurrentContext());
util::logf("[TestManager] ImGuiTestEngine initialized successfully");
}
}
}