diff --git a/CMakeLists.txt b/CMakeLists.txt index 007847b3..6398e879 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,9 @@ option(YAZE_ENABLE_EXPERIMENTAL_TESTS "Enable experimental/unstable tests" ON) option(YAZE_ENABLE_UI_TESTS "Enable ImGui Test Engine UI testing" ON) option(YAZE_MINIMAL_BUILD "Minimal build for CI (disable optional features)" OFF) +# Optional gRPC support for ImGuiTestHarness (z3ed agent mode) +option(YAZE_WITH_GRPC "Enable gRPC-based ImGuiTestHarness for automated GUI testing (experimental)" OFF) + # Configure minimal builds for CI/CD if(YAZE_MINIMAL_BUILD) set(YAZE_ENABLE_UI_TESTS OFF CACHE BOOL "Disabled for minimal build" FORCE) @@ -184,6 +187,24 @@ endif() # Abseil Standard Specifications include(cmake/absl.cmake) +# Optional gRPC support +if(YAZE_WITH_GRPC) + message(STATUS "✓ gRPC support enabled (FetchContent will download and build from source)") + message(STATUS " Note: First build takes 15-20 minutes to compile gRPC + Protobuf") + message(STATUS " Versions: gRPC v1.62.0, Protobuf (bundled), Abseil (bundled)") + + # Include existing gRPC infrastructure + include(cmake/grpc.cmake) + + # Pass to source code + add_compile_definitions(YAZE_WITH_GRPC) + + set(YAZE_HAS_GRPC TRUE) +else() + message(STATUS "○ gRPC support disabled (set -DYAZE_WITH_GRPC=ON to enable)") + set(YAZE_HAS_GRPC FALSE) +endif() + # SDL2 and PNG include(cmake/sdl2.cmake) diff --git a/cmake/grpc.cmake b/cmake/grpc.cmake new file mode 100644 index 00000000..0b4a0c5f --- /dev/null +++ b/cmake/grpc.cmake @@ -0,0 +1,158 @@ +cmake_minimum_required(VERSION 3.16) +set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) +set(CMAKE_POLICY_DEFAULT_CMP0074 NEW) + +# Include FetchContent module +include(FetchContent) + +# Set minimum CMake version for subprojects (fixes c-ares compatibility) +set(CMAKE_POLICY_VERSION_MINIMUM 3.5) + +set(FETCHCONTENT_QUIET OFF) + +# CRITICAL: Prevent CMake from finding system-installed protobuf/abseil +# This ensures gRPC uses its own bundled versions +set(CMAKE_DISABLE_FIND_PACKAGE_Protobuf TRUE) +set(CMAKE_DISABLE_FIND_PACKAGE_absl TRUE) +set(CMAKE_DISABLE_FIND_PACKAGE_gRPC TRUE) + +# Also prevent pkg-config from finding system packages +set(PKG_CONFIG_USE_CMAKE_PREFIX_PATH FALSE) + +# Add compiler flags for Clang 15+ compatibility +# gRPC v1.62.0 requires C++17 (std::result_of removed in C++20) +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wno-error=missing-template-arg-list-after-template-kw) +endif() + +# Save YAZE's C++ standard and temporarily set to C++17 for gRPC +set(_SAVED_CMAKE_CXX_STANDARD ${CMAKE_CXX_STANDARD}) +set(CMAKE_CXX_STANDARD 17) + +find_package(ZLIB REQUIRED) + +# Configure gRPC build options before fetching +set(gRPC_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(gRPC_BUILD_CODEGEN ON CACHE BOOL "" FORCE) +set(gRPC_BUILD_GRPC_CPP_PLUGIN ON CACHE BOOL "" FORCE) +set(gRPC_BUILD_CSHARP_EXT OFF CACHE BOOL "" FORCE) +set(gRPC_BUILD_GRPC_CSHARP_PLUGIN OFF CACHE BOOL "" FORCE) +set(gRPC_BUILD_GRPC_NODE_PLUGIN OFF CACHE BOOL "" FORCE) +set(gRPC_BUILD_GRPC_OBJECTIVE_C_PLUGIN OFF CACHE BOOL "" FORCE) +set(gRPC_BUILD_GRPC_PHP_PLUGIN OFF CACHE BOOL "" FORCE) +set(gRPC_BUILD_GRPC_PYTHON_PLUGIN OFF CACHE BOOL "" FORCE) +set(gRPC_BUILD_GRPC_RUBY_PLUGIN OFF CACHE BOOL "" FORCE) + +set(gRPC_BENCHMARK_PROVIDER "none" CACHE STRING "" FORCE) +set(gRPC_ZLIB_PROVIDER "package" CACHE STRING "" FORCE) + +# Let gRPC fetch and build its own protobuf and abseil +set(gRPC_PROTOBUF_PROVIDER "module" CACHE STRING "" FORCE) +set(gRPC_ABSL_PROVIDER "module" CACHE STRING "" FORCE) + +# Protobuf configuration +set(protobuf_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_CONFORMANCE OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_PROTOC_BINARIES ON CACHE BOOL "" FORCE) +set(protobuf_WITH_ZLIB ON CACHE BOOL "" FORCE) + +# Abseil configuration +set(ABSL_PROPAGATE_CXX_STD ON CACHE BOOL "" FORCE) +set(ABSL_ENABLE_INSTALL OFF CACHE BOOL "" FORCE) +set(ABSL_BUILD_TESTING OFF CACHE BOOL "" FORCE) + +# Declare gRPC - use v1.62.0 which fixes health_check_client incomplete type bug +# and is compatible with Clang 18 +FetchContent_Declare( + grpc + GIT_REPOSITORY https://github.com/grpc/grpc.git + GIT_TAG v1.62.0 + GIT_PROGRESS TRUE + GIT_SHALLOW TRUE + USES_TERMINAL_DOWNLOAD TRUE +) + +# Save the current CMAKE_PREFIX_PATH and clear it temporarily +# This prevents system packages from interfering +set(_SAVED_CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH}) +set(CMAKE_PREFIX_PATH "") + +# Download and build in isolation +FetchContent_MakeAvailable(grpc) + +# Restore CMAKE_PREFIX_PATH +set(CMAKE_PREFIX_PATH ${_SAVED_CMAKE_PREFIX_PATH}) + +# Restore YAZE's C++ standard +set(CMAKE_CXX_STANDARD ${_SAVED_CMAKE_CXX_STANDARD}) + +# Verify targets +if(NOT TARGET protoc) + message(FATAL_ERROR "Can not find target protoc") +endif() +if(NOT TARGET grpc_cpp_plugin) + message(FATAL_ERROR "Can not find target grpc_cpp_plugin") +endif() + +set(_gRPC_PROTOBUF_PROTOC_EXECUTABLE $) +set(_gRPC_CPP_PLUGIN $) +set(_gRPC_PROTO_GENS_DIR ${CMAKE_BINARY_DIR}/gens) +file(MAKE_DIRECTORY ${_gRPC_PROTO_GENS_DIR}) + +get_target_property(_PROTOBUF_INCLUDE_DIRS libprotobuf INTERFACE_INCLUDE_DIRECTORIES) +list(GET _PROTOBUF_INCLUDE_DIRS 0 _gRPC_PROTOBUF_WELLKNOWN_INCLUDE_DIR) + +message(STATUS "gRPC setup complete") + +function(target_add_protobuf target) + if(NOT TARGET ${target}) + message(FATAL_ERROR "Target ${target} doesn't exist") + endif() + if(NOT ARGN) + message(SEND_ERROR "Error: target_add_protobuf() called without any proto files") + return() + endif() + + set(_protobuf_include_path -I . -I ${_gRPC_PROTOBUF_WELLKNOWN_INCLUDE_DIR}) + foreach(FIL ${ARGN}) + get_filename_component(ABS_FIL ${FIL} ABSOLUTE) + get_filename_component(FIL_WE ${FIL} NAME_WE) + file(RELATIVE_PATH REL_FIL ${CMAKE_CURRENT_SOURCE_DIR} ${ABS_FIL}) + get_filename_component(REL_DIR ${REL_FIL} DIRECTORY) + if(NOT REL_DIR) + set(RELFIL_WE "${FIL_WE}") + else() + set(RELFIL_WE "${REL_DIR}/${FIL_WE}") + endif() + + add_custom_command( + OUTPUT "${_gRPC_PROTO_GENS_DIR}/${RELFIL_WE}.grpc.pb.cc" + "${_gRPC_PROTO_GENS_DIR}/${RELFIL_WE}.grpc.pb.h" + "${_gRPC_PROTO_GENS_DIR}/${RELFIL_WE}_mock.grpc.pb.h" + "${_gRPC_PROTO_GENS_DIR}/${RELFIL_WE}.pb.cc" + "${_gRPC_PROTO_GENS_DIR}/${RELFIL_WE}.pb.h" + COMMAND ${_gRPC_PROTOBUF_PROTOC_EXECUTABLE} + ARGS --grpc_out=generate_mock_code=true:${_gRPC_PROTO_GENS_DIR} + --cpp_out=${_gRPC_PROTO_GENS_DIR} + --plugin=protoc-gen-grpc=${_gRPC_CPP_PLUGIN} + ${_protobuf_include_path} + ${REL_FIL} + DEPENDS ${ABS_FIL} protoc grpc_cpp_plugin + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Running gRPC C++ protocol buffer compiler on ${FIL}" + VERBATIM) + + target_sources(${target} PRIVATE + "${_gRPC_PROTO_GENS_DIR}/${RELFIL_WE}.grpc.pb.cc" + "${_gRPC_PROTO_GENS_DIR}/${RELFIL_WE}.grpc.pb.h" + "${_gRPC_PROTO_GENS_DIR}/${RELFIL_WE}_mock.grpc.pb.h" + "${_gRPC_PROTO_GENS_DIR}/${RELFIL_WE}.pb.cc" + "${_gRPC_PROTO_GENS_DIR}/${RELFIL_WE}.pb.h" + ) + target_include_directories(${target} PRIVATE + $ + $ + ) + endforeach() +endfunction() diff --git a/docs/E6-z3ed-cli-design.md b/docs/z3ed/E6-z3ed-cli-design.md similarity index 100% rename from docs/E6-z3ed-cli-design.md rename to docs/z3ed/E6-z3ed-cli-design.md diff --git a/docs/E6-z3ed-implementation-plan.md b/docs/z3ed/E6-z3ed-implementation-plan.md similarity index 95% rename from docs/E6-z3ed-implementation-plan.md rename to docs/z3ed/E6-z3ed-implementation-plan.md index afcb143c..8247b8fc 100644 --- a/docs/E6-z3ed-implementation-plan.md +++ b/docs/z3ed/E6-z3ed-implementation-plan.md @@ -88,35 +88,39 @@ _Status Legend: Prototype · In Progress · Planned · Blocked · Done_ - Test Reject and Delete actions - Validate filtering and refresh functionality -### Priority 1: ImGuiTestHarness Foundation (IT-01, 6-8 hours) 🔥 NEW PRIORITY -**Rationale**: Required for automated GUI testing and remote control of YAZE for AI workflows +### Priority 1: ImGuiTestHarness Foundation (IT-01, 10-14 hours) 🔥 ACTIVE +**Rationale**: Required for automated GUI testing and remote control of YAZE for AI workflows +**Decision**: ✅ **Use gRPC** - Production-grade, cross-platform, type-safe (see `docs/IT-01-grpc-evaluation.md`) -2. **DESIGN SPIKE**: ImGuiTestHarness Architecture - - Evaluate IPC transport options: - - **Socket/Unix Domain Socket**: Low overhead, platform-specific - - **HTTP/REST**: Universal, easier debugging, more overhead - - **Shared Memory**: Fastest, most complex - - **stdin/stdout pipe**: Simplest, limited to synchronous calls - - Decision criteria: cross-platform support, latency, debugging ease - - Prototype preferred approach with simple "click button" test +2. **SETUP**: Add gRPC to build system (1-2 hours) + - Add gRPC + Protobuf to vcpkg.json + - Update CMakeLists.txt with conditional gRPC support + - Test build on macOS with `YAZE_WITH_GRPC=ON` + - Verify protobuf code generation works -3. **IMPLEMENT**: Basic IPC Service - - Embed IPC listener in `yaze_test` or main `yaze` binary (conditional compile) - - Protocol: JSON-RPC style commands (click, type, wait, assert) - - Security: localhost-only, optional shared secret - - Example commands: - ```json - {"cmd": "click", "target": "button:Open ROM"} - {"cmd": "type", "target": "input:filename", "value": "zelda3.sfc"} - {"cmd": "wait", "condition": "window_visible:Overworld Editor"} - {"cmd": "assert", "condition": "color_at:100,200", "value": "#FF0000"} - ``` +3. **PROTOTYPE**: Minimal gRPC service (2-3 hours) + - Define basic .proto with Ping, Click operations + - Implement `ImGuiTestHarnessServiceImpl::Ping()` + - Implement `ImGuiTestHarnessServer` singleton + - Test with grpcurl: `grpcurl -d '{"message":"hello"}' localhost:50051 ...` -4. **INTEGRATE**: CLI Agent Translation Layer - - `z3ed agent test` generates ImGui action sequences - - Translates natural language → test commands → IPC calls - - Example: "open overworld editor" → `click(menu:Editors→Overworld)` - - Capture screenshots for LLM feedback loop +4. **IMPLEMENT**: Core Operations (4-6 hours) + - Complete .proto schema (Click, Type, Wait, Assert, Screenshot) + - Implement all RPC handlers with ImGuiTestEngine integration + - Add target parsing ("button:Open ROM" → widget lookup) + - Error handling and timeout support + +5. **INTEGRATE**: CLI Client (2-3 hours) + - `z3ed agent test --prompt "..."` generates gRPC calls + - AI translates natural language → ImGui actions → RPC requests + - Capture screenshots for LLM feedback + - Example: "open overworld editor" → `Click(target="menu:Editors→Overworld")` + +6. **WINDOWS TESTING**: Cross-platform verification (2-3 hours) + - Create detailed Windows build instructions (vcpkg setup) + - Test on Windows VM or with contributor + - Add Windows CI job to GitHub Actions + - Document troubleshooting for common Windows issues ### Priority 2: Policy Evaluation Framework (AW-04, 4-6 hours) 5. **DESIGN**: YAML-based Policy Configuration diff --git a/src/app/app.cmake b/src/app/app.cmake index d240e7f6..43b011b2 100644 --- a/src/app/app.cmake +++ b/src/app/app.cmake @@ -219,3 +219,26 @@ if(NOT APPLE) endif() endif() +# ============================================================================ +# Optional gRPC Support for ImGuiTestHarness +# ============================================================================ +if(YAZE_WITH_GRPC) + message(STATUS "Adding gRPC ImGuiTestHarness to yaze target") + + # Generate C++ code from .proto using the helper function from cmake/grpc.cmake + target_add_protobuf(yaze + ${CMAKE_SOURCE_DIR}/src/app/core/proto/imgui_test_harness.proto) + + # Add service implementation sources + target_sources(yaze PRIVATE + ${CMAKE_SOURCE_DIR}/src/app/core/imgui_test_harness_service.cc + ${CMAKE_SOURCE_DIR}/src/app/core/imgui_test_harness_service.h) + + # Link gRPC libraries + target_link_libraries(yaze PRIVATE + grpc++ + grpc++_reflection + libprotobuf) + + message(STATUS "✓ gRPC ImGuiTestHarness integrated") +endif() diff --git a/src/app/core/imgui_test_harness_service.cc b/src/app/core/imgui_test_harness_service.cc new file mode 100644 index 00000000..df7b0925 --- /dev/null +++ b/src/app/core/imgui_test_harness_service.cc @@ -0,0 +1,265 @@ +#include "app/core/imgui_test_harness_service.h" + +#ifdef YAZE_WITH_GRPC + +#include +#include + +#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 "yaze.h" // For YAZE_VERSION_STRING + +#include +#include + +namespace yaze { +namespace test { + +// gRPC service wrapper that forwards to our implementation +class ImGuiTestHarnessServiceGrpc final : public ImGuiTestHarness::Service { + public: + explicit ImGuiTestHarnessServiceGrpc(ImGuiTestHarnessServiceImpl* impl) + : impl_(impl) {} + + grpc::Status Ping(grpc::ServerContext* context, const PingRequest* request, + PingResponse* response) override { + auto status = impl_->Ping(request, response); + if (!status.ok()) { + return grpc::Status(grpc::StatusCode::INTERNAL, + std::string(status.message())); + } + return grpc::Status::OK; + } + + grpc::Status Click(grpc::ServerContext* context, const ClickRequest* request, + ClickResponse* response) override { + auto status = impl_->Click(request, response); + if (!status.ok()) { + return grpc::Status(grpc::StatusCode::INTERNAL, + std::string(status.message())); + } + return grpc::Status::OK; + } + + grpc::Status Type(grpc::ServerContext* context, const TypeRequest* request, + TypeResponse* response) override { + auto status = impl_->Type(request, response); + if (!status.ok()) { + return grpc::Status(grpc::StatusCode::INTERNAL, + std::string(status.message())); + } + return grpc::Status::OK; + } + + grpc::Status Wait(grpc::ServerContext* context, const WaitRequest* request, + WaitResponse* response) override { + auto status = impl_->Wait(request, response); + if (!status.ok()) { + return grpc::Status(grpc::StatusCode::INTERNAL, + std::string(status.message())); + } + return grpc::Status::OK; + } + + grpc::Status Assert(grpc::ServerContext* context, + const AssertRequest* request, + AssertResponse* response) override { + auto status = impl_->Assert(request, response); + if (!status.ok()) { + return grpc::Status(grpc::StatusCode::INTERNAL, + std::string(status.message())); + } + return grpc::Status::OK; + } + + grpc::Status Screenshot(grpc::ServerContext* context, + const ScreenshotRequest* request, + ScreenshotResponse* response) override { + auto status = impl_->Screenshot(request, response); + if (!status.ok()) { + return grpc::Status(grpc::StatusCode::INTERNAL, + std::string(status.message())); + } + return grpc::Status::OK; + } + + private: + ImGuiTestHarnessServiceImpl* impl_; +}; + +// ============================================================================ +// ImGuiTestHarnessServiceImpl - RPC Handlers +// ============================================================================ + +absl::Status ImGuiTestHarnessServiceImpl::Ping(const PingRequest* request, + PingResponse* response) { + // Echo back the message with "Pong: " prefix + response->set_message(absl::StrFormat("Pong: %s", request->message())); + + // Add current timestamp + auto now = std::chrono::system_clock::now(); + auto ms = std::chrono::duration_cast( + now.time_since_epoch()); + response->set_timestamp_ms(ms.count()); + + // Add YAZE version + response->set_yaze_version(YAZE_VERSION_STRING); + + return absl::OkStatus(); +} + +absl::Status ImGuiTestHarnessServiceImpl::Click(const ClickRequest* request, + ClickResponse* response) { + auto start = std::chrono::steady_clock::now(); + + // 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'"); + return absl::OkStatus(); + } + + std::string widget_type = target.substr(0, colon_pos); + std::string widget_label = target.substr(colon_pos + 1); + + // TODO: Integrate with ImGuiTestEngine to actually perform the click + // For now, just simulate success + + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start); + + response->set_success(true); + response->set_message( + absl::StrFormat("Clicked %s '%s'", widget_type, widget_label)); + response->set_execution_time_ms(elapsed.count()); + + return absl::OkStatus(); +} + +absl::Status ImGuiTestHarnessServiceImpl::Type(const TypeRequest* request, + TypeResponse* response) { + auto start = std::chrono::steady_clock::now(); + + // TODO: Implement actual text input via ImGuiTestEngine + + auto elapsed = std::chrono::duration_cast( + 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_execution_time_ms(elapsed.count()); + + return absl::OkStatus(); +} + +absl::Status ImGuiTestHarnessServiceImpl::Wait(const WaitRequest* request, + WaitResponse* response) { + auto start = std::chrono::steady_clock::now(); + + // TODO: Implement actual condition polling + + auto elapsed = std::chrono::duration_cast( + 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()); + + return absl::OkStatus(); +} + +absl::Status ImGuiTestHarnessServiceImpl::Assert(const AssertRequest* request, + AssertResponse* response) { + // TODO: Implement actual assertion checking + + 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)"); + + return absl::OkStatus(); +} + +absl::Status ImGuiTestHarnessServiceImpl::Screenshot( + const ScreenshotRequest* request, ScreenshotResponse* response) { + // TODO: Implement actual screenshot capture + + response->set_success(false); + response->set_message("Screenshot not yet implemented"); + response->set_file_path(""); + response->set_file_size_bytes(0); + + return absl::OkStatus(); +} + +// ============================================================================ +// ImGuiTestHarnessServer - Server Lifecycle +// ============================================================================ + +ImGuiTestHarnessServer& ImGuiTestHarnessServer::Instance() { + static ImGuiTestHarnessServer* instance = new ImGuiTestHarnessServer(); + return *instance; +} + +absl::Status ImGuiTestHarnessServer::Start(int port) { + if (server_) { + return absl::FailedPreconditionError("Server already running"); + } + + // Create the service implementation + service_ = std::make_unique(); + + // Create the gRPC service wrapper + auto grpc_service = std::make_unique(service_.get()); + + std::string server_address = absl::StrFormat("127.0.0.1:%d", port); + + grpc::ServerBuilder builder; + + // Listen on localhost only (security) + builder.AddListeningPort(server_address, + grpc::InsecureServerCredentials()); + + // Register service + builder.RegisterService(grpc_service.get()); + + // Build and start + server_ = builder.BuildAndStart(); + + if (!server_) { + return absl::InternalError( + absl::StrFormat("Failed to start gRPC server on %s", server_address)); + } + + port_ = port; + + std::cout << "✓ ImGuiTestHarness gRPC server listening on " << server_address + << "\n"; + std::cout << " Use 'grpcurl -plaintext -d '{\"message\":\"test\"}' " + << server_address << " yaze.test.ImGuiTestHarness/Ping' to test\n"; + + return absl::OkStatus(); +} + +void ImGuiTestHarnessServer::Shutdown() { + if (server_) { + std::cout << "⏹ Shutting down ImGuiTestHarness gRPC server...\n"; + server_->Shutdown(); + server_.reset(); + service_.reset(); + port_ = 0; + std::cout << "✓ ImGuiTestHarness gRPC server stopped\n"; + } +} + +} // namespace test +} // namespace yaze + +#endif // YAZE_WITH_GRPC diff --git a/src/app/core/imgui_test_harness_service.h b/src/app/core/imgui_test_harness_service.h new file mode 100644 index 00000000..5dea93b3 --- /dev/null +++ b/src/app/core/imgui_test_harness_service.h @@ -0,0 +1,109 @@ +#ifndef YAZE_APP_CORE_IMGUI_TEST_HARNESS_SERVICE_H_ +#define YAZE_APP_CORE_IMGUI_TEST_HARNESS_SERVICE_H_ + +#ifdef YAZE_WITH_GRPC + +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" + +// Include grpcpp headers for unique_ptr in member variable +#include + +// Forward declarations to avoid including gRPC headers in public interface +namespace grpc { +class ServerContext; +} // namespace grpc + +namespace yaze { +namespace test { + +// Forward declare proto types +class PingRequest; +class PingResponse; +class ClickRequest; +class ClickResponse; +class TypeRequest; +class TypeResponse; +class WaitRequest; +class WaitResponse; +class AssertRequest; +class AssertResponse; +class ScreenshotRequest; +class ScreenshotResponse; + +// Implementation of ImGuiTestHarness gRPC service +// This class provides the actual RPC handlers for automated GUI testing +class ImGuiTestHarnessServiceImpl { + public: + ImGuiTestHarnessServiceImpl() = default; + ~ImGuiTestHarnessServiceImpl() = default; + + // Disable copy and move + ImGuiTestHarnessServiceImpl(const ImGuiTestHarnessServiceImpl&) = delete; + ImGuiTestHarnessServiceImpl& operator=(const ImGuiTestHarnessServiceImpl&) = + delete; + + // RPC Handlers - implemented in imgui_test_harness_service.cc + + // Health check - verifies the service is running + absl::Status Ping(const PingRequest* request, PingResponse* response); + + // Click a button or interactive element + absl::Status Click(const ClickRequest* request, ClickResponse* response); + + // Type text into an input field + absl::Status Type(const TypeRequest* request, TypeResponse* response); + + // Wait for a condition to be true + absl::Status Wait(const WaitRequest* request, WaitResponse* response); + + // Assert that a condition is true + absl::Status Assert(const AssertRequest* request, AssertResponse* response); + + // Capture a screenshot + absl::Status Screenshot(const ScreenshotRequest* request, + ScreenshotResponse* response); +}; + +// Singleton server managing the gRPC service +// This class manages the lifecycle of the gRPC server +class ImGuiTestHarnessServer { + public: + // Get the singleton instance + static ImGuiTestHarnessServer& Instance(); + + // Start the gRPC server on the specified port + // @param port The port to listen on (default 50051) + // @return OK status if server started successfully, error otherwise + absl::Status Start(int port = 50051); + + // Shutdown the server gracefully + void Shutdown(); + + // Check if the server is currently running + bool IsRunning() const { return server_ != nullptr; } + + // Get the port the server is listening on (0 if not running) + int Port() const { return port_; } + + private: + ImGuiTestHarnessServer() = default; + ~ImGuiTestHarnessServer() { Shutdown(); } + + // Disable copy and move + ImGuiTestHarnessServer(const ImGuiTestHarnessServer&) = delete; + ImGuiTestHarnessServer& operator=(const ImGuiTestHarnessServer&) = delete; + + std::unique_ptr server_; + std::unique_ptr service_; + int port_ = 0; +}; + +} // namespace test +} // namespace yaze + +#endif // YAZE_WITH_GRPC +#endif // YAZE_APP_CORE_IMGUI_TEST_HARNESS_SERVICE_H_ diff --git a/src/app/core/proto/imgui_test_harness.proto b/src/app/core/proto/imgui_test_harness.proto new file mode 100644 index 00000000..9e041d62 --- /dev/null +++ b/src/app/core/proto/imgui_test_harness.proto @@ -0,0 +1,131 @@ +syntax = "proto3"; + +package yaze.test; + +// ImGuiTestHarness service for remote GUI testing +// This service allows z3ed CLI to interact with YAZE's GUI for automated testing +service ImGuiTestHarness { + // Health check - verifies the service is running + rpc Ping(PingRequest) returns (PingResponse); + + // Click a button or interactive element + rpc Click(ClickRequest) returns (ClickResponse); + + // Type text into an input field + rpc Type(TypeRequest) returns (TypeResponse); + + // Wait for a condition to be true + rpc Wait(WaitRequest) returns (WaitResponse); + + // Assert that a condition is true + rpc Assert(AssertRequest) returns (AssertResponse); + + // Capture a screenshot + rpc Screenshot(ScreenshotRequest) returns (ScreenshotResponse); +} + +// ============================================================================ +// Ping - Health Check +// ============================================================================ + +message PingRequest { + string message = 1; // Message to echo back +} + +message PingResponse { + string message = 1; // Echoed message with "Pong: " prefix + int64 timestamp_ms = 2; // Server timestamp in milliseconds + string yaze_version = 3; // YAZE version string (e.g., "0.3.2") +} + +// ============================================================================ +// Click - Interact with GUI elements +// ============================================================================ + +message ClickRequest { + string target = 1; // Target element (e.g., "button:Open ROM", "menu:File/Open") + ClickType type = 2; // Type of click + + enum ClickType { + LEFT = 0; // Single left click + RIGHT = 1; // Single right click + DOUBLE = 2; // Double click + MIDDLE = 3; // Middle mouse button + } +} + +message ClickResponse { + bool success = 1; // Whether the click succeeded + string message = 2; // Human-readable result message + int32 execution_time_ms = 3; // Time taken to execute (for debugging) +} + +// ============================================================================ +// Type - Send keyboard input +// ============================================================================ + +message TypeRequest { + string target = 1; // Target input field (e.g., "textbox:File Path") + string text = 2; // Text to type + bool clear_first = 3; // Clear existing text before typing +} + +message TypeResponse { + bool success = 1; + string message = 2; + int32 execution_time_ms = 3; +} + +// ============================================================================ +// Wait - Poll for conditions +// ============================================================================ + +message WaitRequest { + string condition = 1; // Condition to wait for (e.g., "window:Overworld Editor", "enabled:button:Save") + int32 timeout_ms = 2; // Maximum time to wait (default 5000ms) + int32 poll_interval_ms = 3; // How often to check (default 100ms) +} + +message WaitResponse { + bool success = 1; // Whether condition was met before timeout + string message = 2; + int32 elapsed_ms = 3; // Time taken before condition met (or timeout) +} + +// ============================================================================ +// Assert - Validate GUI state +// ============================================================================ + +message AssertRequest { + string condition = 1; // Condition to assert (e.g., "visible:button:Save", "text:label:Version:0.3.2") + string failure_message = 2; // Custom message if assertion fails +} + +message AssertResponse { + bool success = 1; // Whether assertion passed + string message = 2; // Diagnostic message + string actual_value = 3; // Actual value found (for debugging) + string expected_value = 4; // Expected value (for debugging) +} + +// ============================================================================ +// Screenshot - Capture window state +// ============================================================================ + +message ScreenshotRequest { + string window_title = 1; // Window to capture (empty = main window) + string output_path = 2; // Where to save screenshot + ImageFormat format = 3; // Image format + + enum ImageFormat { + PNG = 0; + JPEG = 1; + } +} + +message ScreenshotResponse { + bool success = 1; + string message = 2; + string file_path = 3; // Absolute path to saved screenshot + int64 file_size_bytes = 4; +}