Files
yaze/docs/z3ed/archive/IT-01-getting-started-grpc.md
scawful 286efdec6a Enhance ImGuiTestHarness with dynamic test integration and end-to-end validation
- Updated README.md to reflect the completion of IT-01 and the transition to end-to-end validation phase.
- Introduced a new end-to-end test script (scripts/test_harness_e2e.sh) for validating all RPC methods of the ImGuiTestHarness gRPC service.
- Implemented dynamic test functionality in ImGuiTestHarnessService for Type, Wait, and Assert methods, utilizing ImGuiTestEngine.
- Enhanced error handling and response messages for better clarity during test execution.
- Updated existing methods to support dynamic test registration and execution, ensuring robust interaction with the GUI elements.
2025-10-02 00:49:28 -04:00

16 KiB

IT-01 Getting Started: gRPC Implementation

Goal: Add gRPC-based ImGuiTestHarness to YAZE for automated GUI testing
Timeline: 10-14 hours over 2-3 days
Current Phase: Setup & Prototype

Quick Start Checklist

  • Step 1: Add gRPC to vcpkg.json (15 min)
  • Step 2: Update CMakeLists.txt (30 min)
  • Step 3: Create minimal .proto (15 min)
  • Step 4: Build and verify (30 min)
  • Step 5: Implement Ping service (1 hour)
  • Step 6: Test with grpcurl (15 min)
  • Step 7: Implement Click handler (2 hours)
  • Step 8: Add remaining operations (3-4 hours)
  • Step 9: CLI client integration (2 hours)
  • Step 10: Windows testing (2-3 hours)

Step-by-Step Implementation

Step 0: Read Dependency Management Guide (5 min)

⚠️ IMPORTANT: Before adding gRPC, understand the existing infrastructure:

Good News: YAZE already has gRPC support via CMake FetchContent!

  • cmake/grpc.cmake exists with gRPC v1.70.1 + Protobuf v29.3
  • Builds from source (no vcpkg needed for gRPC)
  • Works identically on macOS, Linux, Windows
  • Statically linked (no DLL issues)

What We Need To Do:

  1. Test that existing gRPC infrastructure works
  2. Add CMake option to enable gRPC (currently always-off)
  3. Create .proto schema for ImGuiTestHarness
  4. Implement gRPC service using existing target_add_protobuf() helper

Step 1: Verify Existing gRPC Infrastructure (30 min)

Goal: Confirm cmake/grpc.cmake works on your system

cd /Users/scawful/Code/yaze

# Check existing gRPC infrastructure
cat cmake/grpc.cmake | head -20
# Should show FetchContent_Declare for grpc v1.70.1

# Create isolated test build
cmake -B build-grpc-test -DYAZE_WITH_GRPC=ON

# This will:
# 1. Download gRPC v1.70.1 from GitHub (~100MB, 2-3 min)
# 2. Download Protobuf v29.3 from GitHub (~50MB, 1-2 min)  
# 3. Build both from source (~15-20 min first time)
# 4. Cache everything in build-grpc-test/ for reuse

# Watch for:
#   -- Fetching grpc...
#   -- Fetching protobuf...
#   -- Building CXX object (lots of output)...

# If this fails, gRPC infrastructure needs fixing first

Expected Output:

-- Fetching grpc...
-- Populating grpc
-- Fetching protobuf...
-- Populating protobuf
-- Building grpc (this will take 15-20 minutes)...
[lots of compilation output]
-- Configuring done
-- Generating done

Success Criteria:

  • FetchContent downloads gRPC + Protobuf successfully
  • Both build without errors
  • No need to install anything (all in build directory)

If This Fails: Stop here, investigate errors in build-grpc-test/CMakeFiles/CMakeError.log

Step 2: Add CMake Option for gRPC (15 min)

Goal: Make gRPC opt-in via CMake flag

The existing cmake/grpc.cmake is always included, but we need to make it optional.

Add to CMakeLists.txt (after project(yaze VERSION ...), around line 50):

# Optional gRPC support for ImGuiTestHarness
option(YAZE_WITH_GRPC "Enable gRPC-based ImGuiTestHarness (experimental)" OFF)

if(YAZE_WITH_GRPC)
  message(STATUS "✓ gRPC support enabled (FetchContent will download source)")
  message(STATUS "  Note: First build takes 15-20 minutes to compile gRPC")
  
  # 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 YAZE_WITH_GRPC=ON to enable)")
  set(YAZE_HAS_GRPC FALSE)
endif()

Why This Works:

  • cmake/grpc.cmake already uses FetchContent to download + build gRPC
  • It provides target_add_protobuf(target proto_file) helper
  • We just need to make it conditional

Test the CMake change:

# Reconfigure with gRPC enabled
cmake -B build-grpc-test -DYAZE_WITH_GRPC=ON

# Should show:
#   -- ✓ gRPC support enabled (FetchContent will download source)
#   -- Note: First build takes 15-20 minutes to compile gRPC
#   -- Fetching grpc...

# Without flag (default):
cmake -B build-default

# Should show:
#   -- ○ gRPC support disabled (set YAZE_WITH_GRPC=ON to enable)

Step 2: Update CMakeLists.txt (30 min)

Add to root CMakeLists.txt:

# Optional gRPC support for ImGuiTestHarness
option(YAZE_WITH_GRPC "Enable gRPC-based ImGuiTestHarness" OFF)

if(YAZE_WITH_GRPC)
  find_package(gRPC CONFIG REQUIRED)
  find_package(Protobuf CONFIG REQUIRED)
  
  message(STATUS "gRPC support enabled")
  message(STATUS "  gRPC version: ${gRPC_VERSION}")
  message(STATUS "  Protobuf version: ${Protobuf_VERSION}")
  
  # Function to generate C++ from .proto
  function(yaze_add_grpc_proto target proto_file)
    get_filename_component(proto_dir ${proto_file} DIRECTORY)
    get_filename_component(proto_name ${proto_file} NAME_WE)
    
    set(proto_srcs "${CMAKE_CURRENT_BINARY_DIR}/${proto_name}.pb.cc")
    set(proto_hdrs "${CMAKE_CURRENT_BINARY_DIR}/${proto_name}.pb.h")
    set(grpc_srcs "${CMAKE_CURRENT_BINARY_DIR}/${proto_name}.grpc.pb.cc")
    set(grpc_hdrs "${CMAKE_CURRENT_BINARY_DIR}/${proto_name}.grpc.pb.h")
    
    add_custom_command(
      OUTPUT ${proto_srcs} ${proto_hdrs} ${grpc_srcs} ${grpc_hdrs}
      COMMAND protobuf::protoc
        --proto_path=${proto_dir}
        --cpp_out=${CMAKE_CURRENT_BINARY_DIR}
        --grpc_out=${CMAKE_CURRENT_BINARY_DIR}
        --plugin=protoc-gen-grpc=$<TARGET_FILE:gRPC::grpc_cpp_plugin>
        ${proto_file}
      DEPENDS ${proto_file}
      COMMENT "Generating C++ from ${proto_file}"
    )
    
    target_sources(${target} PRIVATE ${proto_srcs} ${grpc_srcs})
    target_include_directories(${target} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
  endfunction()
endif()

Step 3: Create minimal .proto (15 min)

mkdir -p src/app/core/proto

Create src/app/core/proto/imgui_test_harness.proto:

syntax = "proto3";

package yaze.test;

// ImGuiTestHarness service for remote GUI testing
service ImGuiTestHarness {
  // Health check
  rpc Ping(PingRequest) returns (PingResponse);
  
  // TODO: Add Click, Type, Wait, Assert
}

message PingRequest {
  string message = 1;
}

message PingResponse {
  string message = 1;
  int64 timestamp_ms = 2;
}

Step 4: Build and verify (20 min)

# Build YAZE with gRPC enabled
cd /Users/scawful/Code/yaze

cmake --build build-grpc-test --target yaze -j8

# First time: 15-20 minutes (compiling gRPC)
# Subsequent: ~30 seconds (using cached gRPC)

# Watch for:
#   [1/500] Building CXX object (gRPC compilation)
#   ...
#   [500/500] Linking CXX executable yaze

Expected Outcomes:

Success:

  • Build completes without errors
  • Binary size: build-grpc-test/bin/yaze.app is ~10-15MB larger
  • YAZE_WITH_GRPC preprocessor flag defined in code
  • target_add_protobuf() function available

⚠️ Common Issues:

  • "error: 'absl::...' not found": gRPC needs abseil. Check cmake/absl.cmake is included.
  • Long compile time: Normal! gRPC is a large library (~500 source files)
  • Out of disk space: gRPC build artifacts ~2GB. Clean old builds: rm -rf build/

Verify gRPC is linked:

# macOS: Check for gRPC symbols
nm build-grpc-test/bin/yaze.app/Contents/MacOS/yaze | grep grpc | head -5
# Should show symbols like: _grpc_completion_queue_create

# Check binary size
ls -lh build-grpc-test/bin/yaze.app/Contents/MacOS/yaze
# Should be ~60-70MB (vs ~50MB without gRPC)

Rollback: If anything goes wrong:

# Delete test build, use original
rm -rf build-grpc-test
cmake --build build --target yaze -j8  # Still works!

Step 5: Implement Ping service (1 hour)

Create src/app/core/imgui_test_harness_service.h:

#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 <grpcpp/grpcpp.h>
#include "proto/imgui_test_harness.grpc.pb.h"
#include "absl/status/status.h"

namespace yaze {
namespace test {

// Implementation of ImGuiTestHarness gRPC service
class ImGuiTestHarnessServiceImpl final 
    : public ImGuiTestHarness::Service {
 public:
  grpc::Status Ping(
      grpc::ServerContext* context,
      const PingRequest* request,
      PingResponse* response) override;
};

// Singleton server managing the gRPC service
class ImGuiTestHarnessServer {
 public:
  static ImGuiTestHarnessServer& Instance();
  
  // Start server on specified port (default 50051)
  absl::Status Start(int port = 50051);
  
  // Shutdown server gracefully
  void Shutdown();
  
  // Check if server is running
  bool IsRunning() const { return server_ != nullptr; }
  
  int Port() const { return port_; }
  
 private:
  ImGuiTestHarnessServer() = default;
  ~ImGuiTestHarnessServer() { Shutdown(); }
  
  std::unique_ptr<grpc::Server> server_;
  ImGuiTestHarnessServiceImpl service_;
  int port_ = 0;
};

}  // namespace test
}  // namespace yaze

#endif  // YAZE_WITH_GRPC
#endif  // YAZE_APP_CORE_IMGUI_TEST_HARNESS_SERVICE_H_

Create src/app/core/imgui_test_harness_service.cc:

#include "app/core/imgui_test_harness_service.h"

#ifdef YAZE_WITH_GRPC

#include <chrono>
#include "absl/strings/str_format.h"

namespace yaze {
namespace test {

// Implement Ping RPC
grpc::Status ImGuiTestHarnessServiceImpl::Ping(
    grpc::ServerContext* context,
    const PingRequest* request,
    PingResponse* response) {
  
  // Echo back the message
  response->set_message(
      absl::StrFormat("Pong: %s", request->message()));
  
  // Add 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());
  
  return grpc::Status::OK;
}

// Singleton instance
ImGuiTestHarnessServer& ImGuiTestHarnessServer::Instance() {
  static ImGuiTestHarnessServer* instance = new ImGuiTestHarnessServer();
  return *instance;
}

absl::Status ImGuiTestHarnessServer::Start(int port) {
  if (server_) {
    return absl::FailedPreconditionError("Server already running");
  }
  
  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(&service_);
  
  // 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";
  
  return absl::OkStatus();
}

void ImGuiTestHarnessServer::Shutdown() {
  if (server_) {
    server_->Shutdown();
    server_.reset();
    port_ = 0;
    std::cout << "✓ ImGuiTestHarness gRPC server stopped\n";
  }
}

}  // namespace test
}  // namespace yaze

#endif  // YAZE_WITH_GRPC

Update src/CMakeLists.txt or src/app/app.cmake:

if(YAZE_WITH_GRPC)
  # Generate gRPC code from .proto
  yaze_add_grpc_proto(yaze 
    ${CMAKE_CURRENT_SOURCE_DIR}/app/core/proto/imgui_test_harness.proto)
  
  # Add service implementation
  target_sources(yaze PRIVATE
    app/core/imgui_test_harness_service.cc
    app/core/imgui_test_harness_service.h)
  
  # Link gRPC libraries
  target_link_libraries(yaze PRIVATE
    gRPC::grpc++
    gRPC::grpc++_reflection
    protobuf::libprotobuf)
  
  # Add compile definition
  target_compile_definitions(yaze PRIVATE YAZE_WITH_GRPC)
endif()

Step 6: Test with grpcurl (15 min)

# Rebuild with service implementation
cmake --build build --target yaze -j8

# Start YAZE with --enable-test-harness flag
# (You'll need to add this flag handler in main.cc first)
./build/bin/yaze.app/Contents/MacOS/yaze --enable-test-harness &

# Install grpcurl if not already
brew install grpcurl

# Test the Ping RPC
grpcurl -plaintext -d '{"message": "Hello from grpcurl"}' \
  127.0.0.1:50051 yaze.test.ImGuiTestHarness/Ping

# Expected output:
# {
#   "message": "Pong: Hello from grpcurl",
#   "timestamp_ms": "1696204800000"
# }

Success Criteria: You should see the Pong response with a timestamp!

Step 7: Add --enable-test-harness flag (30 min)

Add to src/app/main.cc:

#include "absl/flags/flag.h"

#ifdef YAZE_WITH_GRPC
#include "app/core/imgui_test_harness_service.h"

ABSL_FLAG(bool, enable_test_harness, false,
          "Start gRPC test harness server for automated testing");
ABSL_FLAG(int, test_harness_port, 50051,
          "Port for gRPC test harness (default 50051)");
#endif

// In main() after SDL/ImGui initialization:
#ifdef YAZE_WITH_GRPC
  if (absl::GetFlag(FLAGS_enable_test_harness)) {
    auto& harness = yaze::test::ImGuiTestHarnessServer::Instance();
    auto status = harness.Start(absl::GetFlag(FLAGS_test_harness_port));
    if (!status.ok()) {
      std::cerr << "Failed to start test harness: " 
                << status.message() << "\n";
      return 1;
    }
  }
#endif

Step 8: Implement Click handler (2 hours)

Extend .proto:

service ImGuiTestHarness {
  rpc Ping(PingRequest) returns (PingResponse);
  rpc Click(ClickRequest) returns (ClickResponse);  // NEW
}

message ClickRequest {
  string target = 1;  // e.g. "button:Open ROM"
  ClickType type = 2;
  
  enum ClickType {
    LEFT = 0;
    RIGHT = 1;
    DOUBLE = 2;
  }
}

message ClickResponse {
  bool success = 1;
  string message = 2;
  int32 execution_time_ms = 3;
}

Implement in service:

grpc::Status ImGuiTestHarnessServiceImpl::Click(
    grpc::ServerContext* context,
    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 grpc::Status::OK;
  }
  
  std::string widget_type = target.substr(0, colon_pos);
  std::string widget_label = target.substr(colon_pos + 1);
  
  // TODO: Integrate with ImGuiTestEngine
  // 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 grpc::Status::OK;
}

Next Steps After Prototype

Once you have Ping + Click working:

  1. Add remaining operations (Type, Wait, Assert, Screenshot) - 3-4 hours
  2. CLI integration (z3ed agent test) - 2 hours
  3. Windows testing - 2-3 hours
  4. Documentation - 1 hour

Windows Testing Checklist

For Windows contributors:

# Install vcpkg
git clone https://github.com/Microsoft/vcpkg.git C:\vcpkg
C:\vcpkg\bootstrap-vcpkg.bat
C:\vcpkg\vcpkg integrate install

# Install dependencies
C:\vcpkg\vcpkg install grpc:x64-windows protobuf:x64-windows

# Build YAZE
cmake -B build -DCMAKE_TOOLCHAIN_FILE=C:\vcpkg\scripts\buildsystems\vcpkg.cmake ^
  -DYAZE_WITH_GRPC=ON -A x64
cmake --build build --config Release

# Test
.\build\bin\Release\yaze.exe --enable-test-harness

Troubleshooting

"gRPC not found"

vcpkg install grpc:arm64-osx  # or x64-osx, x64-windows
vcpkg integrate install

"protoc not found"

vcpkg install protobuf:arm64-osx
export PATH=$PATH:$(vcpkg list protobuf | grep 'tools' | cut -d: -f1)/tools/protobuf

Build errors on Windows

  • Use Developer Command Prompt for Visual Studio
  • Ensure CMake 3.20+
  • Try clean build: rmdir /s /q build

Success Metrics

Phase 1 Complete When:

  • gRPC builds without errors
  • Ping RPC responds via grpcurl
  • YAZE starts with --enable-test-harness flag

Phase 2 Complete When:

  • Click RPC simulates button click
  • Type RPC sends text input
  • Wait RPC polls for conditions
  • Assert RPC validates state

Phase 3 Complete When:

  • Windows build succeeds
  • Windows contributor can test
  • CI job runs on Windows

Estimated Total: 10-14 hours
Current Status: Ready to start Step 1
Next Session: Add gRPC to vcpkg.json and build