feat: Add gRPC support for ImGuiTestHarness
- Integrated gRPC service for automated GUI testing in ImGuiTestHarness. - Implemented service methods: Ping, Click, Type, Wait, Assert, and Screenshot. - Created proto definitions for the gRPC service and messages. - Added server lifecycle management for the gRPC server. - Included necessary dependencies and build configurations in CMake.
This commit is contained in:
@@ -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_ENABLE_UI_TESTS "Enable ImGui Test Engine UI testing" ON)
|
||||||
option(YAZE_MINIMAL_BUILD "Minimal build for CI (disable optional features)" OFF)
|
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
|
# Configure minimal builds for CI/CD
|
||||||
if(YAZE_MINIMAL_BUILD)
|
if(YAZE_MINIMAL_BUILD)
|
||||||
set(YAZE_ENABLE_UI_TESTS OFF CACHE BOOL "Disabled for minimal build" FORCE)
|
set(YAZE_ENABLE_UI_TESTS OFF CACHE BOOL "Disabled for minimal build" FORCE)
|
||||||
@@ -184,6 +187,24 @@ endif()
|
|||||||
# Abseil Standard Specifications
|
# Abseil Standard Specifications
|
||||||
include(cmake/absl.cmake)
|
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
|
# SDL2 and PNG
|
||||||
include(cmake/sdl2.cmake)
|
include(cmake/sdl2.cmake)
|
||||||
|
|
||||||
|
|||||||
158
cmake/grpc.cmake
Normal file
158
cmake/grpc.cmake
Normal file
@@ -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 $<TARGET_FILE:protoc>)
|
||||||
|
set(_gRPC_CPP_PLUGIN $<TARGET_FILE: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
|
||||||
|
$<BUILD_INTERFACE:${_gRPC_PROTO_GENS_DIR}>
|
||||||
|
$<BUILD_INTERFACE:${_gRPC_PROTOBUF_WELLKNOWN_INCLUDE_DIR}>
|
||||||
|
)
|
||||||
|
endforeach()
|
||||||
|
endfunction()
|
||||||
@@ -88,35 +88,39 @@ _Status Legend: Prototype · In Progress · Planned · Blocked · Done_
|
|||||||
- Test Reject and Delete actions
|
- Test Reject and Delete actions
|
||||||
- Validate filtering and refresh functionality
|
- Validate filtering and refresh functionality
|
||||||
|
|
||||||
### Priority 1: ImGuiTestHarness Foundation (IT-01, 6-8 hours) 🔥 NEW PRIORITY
|
### Priority 1: ImGuiTestHarness Foundation (IT-01, 10-14 hours) 🔥 ACTIVE
|
||||||
**Rationale**: Required for automated GUI testing and remote control of YAZE for AI workflows
|
**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
|
2. **SETUP**: Add gRPC to build system (1-2 hours)
|
||||||
- Evaluate IPC transport options:
|
- Add gRPC + Protobuf to vcpkg.json
|
||||||
- **Socket/Unix Domain Socket**: Low overhead, platform-specific
|
- Update CMakeLists.txt with conditional gRPC support
|
||||||
- **HTTP/REST**: Universal, easier debugging, more overhead
|
- Test build on macOS with `YAZE_WITH_GRPC=ON`
|
||||||
- **Shared Memory**: Fastest, most complex
|
- Verify protobuf code generation works
|
||||||
- **stdin/stdout pipe**: Simplest, limited to synchronous calls
|
|
||||||
- Decision criteria: cross-platform support, latency, debugging ease
|
|
||||||
- Prototype preferred approach with simple "click button" test
|
|
||||||
|
|
||||||
3. **IMPLEMENT**: Basic IPC Service
|
3. **PROTOTYPE**: Minimal gRPC service (2-3 hours)
|
||||||
- Embed IPC listener in `yaze_test` or main `yaze` binary (conditional compile)
|
- Define basic .proto with Ping, Click operations
|
||||||
- Protocol: JSON-RPC style commands (click, type, wait, assert)
|
- Implement `ImGuiTestHarnessServiceImpl::Ping()`
|
||||||
- Security: localhost-only, optional shared secret
|
- Implement `ImGuiTestHarnessServer` singleton
|
||||||
- Example commands:
|
- Test with grpcurl: `grpcurl -d '{"message":"hello"}' localhost:50051 ...`
|
||||||
```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"}
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **INTEGRATE**: CLI Agent Translation Layer
|
4. **IMPLEMENT**: Core Operations (4-6 hours)
|
||||||
- `z3ed agent test` generates ImGui action sequences
|
- Complete .proto schema (Click, Type, Wait, Assert, Screenshot)
|
||||||
- Translates natural language → test commands → IPC calls
|
- Implement all RPC handlers with ImGuiTestEngine integration
|
||||||
- Example: "open overworld editor" → `click(menu:Editors→Overworld)`
|
- Add target parsing ("button:Open ROM" → widget lookup)
|
||||||
- Capture screenshots for LLM feedback loop
|
- 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)
|
### Priority 2: Policy Evaluation Framework (AW-04, 4-6 hours)
|
||||||
5. **DESIGN**: YAML-based Policy Configuration
|
5. **DESIGN**: YAML-based Policy Configuration
|
||||||
@@ -219,3 +219,26 @@ if(NOT APPLE)
|
|||||||
endif()
|
endif()
|
||||||
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()
|
||||||
|
|||||||
265
src/app/core/imgui_test_harness_service.cc
Normal file
265
src/app/core/imgui_test_harness_service.cc
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
#include "app/core/imgui_test_harness_service.h"
|
||||||
|
|
||||||
|
#ifdef YAZE_WITH_GRPC
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#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 <grpcpp/grpcpp.h>
|
||||||
|
#include <grpcpp/server_builder.h>
|
||||||
|
|
||||||
|
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<std::chrono::milliseconds>(
|
||||||
|
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::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_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::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_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::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());
|
||||||
|
|
||||||
|
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<ImGuiTestHarnessServiceImpl>();
|
||||||
|
|
||||||
|
// Create the gRPC service wrapper
|
||||||
|
auto grpc_service = std::make_unique<ImGuiTestHarnessServiceGrpc>(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
|
||||||
109
src/app/core/imgui_test_harness_service.h
Normal file
109
src/app/core/imgui_test_harness_service.h
Normal file
@@ -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 <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "absl/status/status.h"
|
||||||
|
#include "absl/status/statusor.h"
|
||||||
|
|
||||||
|
// Include grpcpp headers for unique_ptr<Server> in member variable
|
||||||
|
#include <grpcpp/server.h>
|
||||||
|
|
||||||
|
// 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<grpc::Server> server_;
|
||||||
|
std::unique_ptr<ImGuiTestHarnessServiceImpl> service_;
|
||||||
|
int port_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace test
|
||||||
|
} // namespace yaze
|
||||||
|
|
||||||
|
#endif // YAZE_WITH_GRPC
|
||||||
|
#endif // YAZE_APP_CORE_IMGUI_TEST_HARNESS_SERVICE_H_
|
||||||
131
src/app/core/proto/imgui_test_harness.proto
Normal file
131
src/app/core/proto/imgui_test_harness.proto
Normal file
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user