From 21074f6445ad890b25470d5f01391edc90248ef8 Mon Sep 17 00:00:00 2001 From: scawful Date: Thu, 2 Oct 2025 21:44:15 -0400 Subject: [PATCH] feat: Enhance widget state capture with fallback diagnostics and update test harness interactions --- scripts/test_harness_e2e.sh | 4 +- src/app/core/widget_state_capture.cc | 16 ++- src/app/core/widget_state_capture.h | 7 ++ src/app/test/test_manager.cc | 143 ++++++++++++++++----------- 4 files changed, 110 insertions(+), 60 deletions(-) diff --git a/scripts/test_harness_e2e.sh b/scripts/test_harness_e2e.sh index 329c0796..6d4b657c 100755 --- a/scripts/test_harness_e2e.sh +++ b/scripts/test_harness_e2e.sh @@ -112,7 +112,7 @@ run_test "Ping (Health Check)" "Ping" '{"message":"test"}' # 2. Click - Menu Item (Open Overworld Editor) # Note: Menu items in YAZE use format "menuitem: Name" -run_test "Click (Open Overworld Editor)" "Click" '{"target":"menuitem: Overworld Editor","type":"LEFT"}' +run_test "Click (Open Overworld Editor)" "Click" '{"target":"menuitem: Overworld Editor","type":"CLICK_TYPE_LEFT"}' # 3. Wait - Window Visible (Overworld Editor should open) run_test "Wait (Overworld Editor Window)" "Wait" '{"condition":"window_visible:Overworld","timeout_ms":15000,"poll_interval_ms":100}' @@ -121,7 +121,7 @@ run_test "Wait (Overworld Editor Window)" "Wait" '{"condition":"window_visible:O run_test "Assert (Overworld Editor Visible)" "Assert" '{"condition":"visible:Overworld"}' # 5. Click - Another menu item (Dungeon Editor) -run_test "Click (Open Dungeon Editor)" "Click" '{"target":"menuitem: Dungeon Editor","type":"LEFT"}' +run_test "Click (Open Dungeon Editor)" "Click" '{"target":"menuitem: Dungeon Editor","type":"CLICK_TYPE_LEFT"}' # 6. Screenshot - Not Implemented (stub) echo -e "${YELLOW}Test 6: Screenshot (Not Implemented - Stub)${NC}" diff --git a/src/app/core/widget_state_capture.cc b/src/app/core/widget_state_capture.cc index 6c560072..ef5d4a56 100644 --- a/src/app/core/widget_state_capture.cc +++ b/src/app/core/widget_state_capture.cc @@ -1,6 +1,12 @@ #include "app/core/widget_state_capture.h" #include "absl/strings/str_format.h" +#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE +#include "imgui.h" +#include "imgui_internal.h" +#else +#include "imgui/imgui.h" +#endif #include "nlohmann/json.hpp" namespace yaze { @@ -9,12 +15,13 @@ namespace core { std::string CaptureWidgetState() { WidgetState state; +#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE // Check if ImGui context is available ImGuiContext* ctx = ImGui::GetCurrentContext(); if (!ctx) { return R"({"error": "ImGui context not available"})"; } - + ImGuiIO& io = ImGui::GetIO(); // Capture frame information @@ -70,6 +77,13 @@ std::string CaptureWidgetState() { state.shift_pressed = io.KeyShift; state.alt_pressed = io.KeyAlt; +#else + // When UI test engine / ImGui internals aren't available, provide a minimal + // payload so downstream systems still receive structured JSON. This keeps + // builds that exclude the UI test engine (e.g., Windows release) working. + return R"({"warning": "Widget state capture unavailable (UI test engine disabled)"})"; +#endif + return SerializeWidgetStateToJson(state); } diff --git a/src/app/core/widget_state_capture.h b/src/app/core/widget_state_capture.h index 0757e18a..19fb2ac7 100644 --- a/src/app/core/widget_state_capture.h +++ b/src/app/core/widget_state_capture.h @@ -4,7 +4,12 @@ #include #include +#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE +#include "imgui.h" +#include "imgui_internal.h" +#else #include "imgui/imgui.h" +#endif namespace yaze { namespace core { @@ -36,6 +41,8 @@ struct WidgetState { // Capture current ImGui widget state for debugging // Returns JSON-formatted string representing the widget hierarchy and state +// When ImGui internals are unavailable (UI test engine disabled), returns a +// short diagnostic JSON payload. std::string CaptureWidgetState(); // Serialize widget state to JSON format diff --git a/src/app/test/test_manager.cc b/src/app/test/test_manager.cc index 86226cae..cc73b490 100644 --- a/src/app/test/test_manager.cc +++ b/src/app/test/test_manager.cc @@ -14,7 +14,12 @@ #include "app/core/platform/file_dialog.h" #include "app/gfx/arena.h" #include "app/gui/icons.h" +#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE #include "imgui.h" +#include "imgui_internal.h" +#else +#include "imgui.h" +#endif #include "util/log.h" // Forward declaration to avoid circular dependency @@ -1479,55 +1484,55 @@ void TestManager::MarkHarnessTestCompleted( const std::vector& assertion_failures, const std::vector& logs, const std::map& metrics) { - absl::MutexLock lock(&harness_history_mutex_); + bool capture_failure_context = + status == HarnessTestStatus::kFailed || + status == HarnessTestStatus::kTimeout; - auto it = harness_history_.find(test_id); - if (it == harness_history_.end()) { - return; + { + absl::MutexLock lock(&harness_history_mutex_); + + auto it = harness_history_.find(test_id); + if (it == harness_history_.end()) { + return; + } + + HarnessTestExecution& execution = it->second; + execution.status = status; + if (execution.started_at == absl::InfinitePast()) { + execution.started_at = execution.queued_at; + } + execution.completed_at = absl::Now(); + execution.duration = execution.completed_at - execution.started_at; + execution.error_message = error_message; + if (!assertion_failures.empty()) { + execution.assertion_failures = assertion_failures; + } + if (!logs.empty()) { + execution.logs.insert(execution.logs.end(), logs.begin(), logs.end()); + } + if (!metrics.empty()) { + execution.metrics.insert(metrics.begin(), metrics.end()); + } + + HarnessAggregate& aggregate = harness_aggregates_[execution.name]; + if (aggregate.category.empty()) { + aggregate.category = execution.category; + } + aggregate.total_runs += 1; + if (status == HarnessTestStatus::kPassed) { + aggregate.pass_count += 1; + } else if (status == HarnessTestStatus::kFailed || + status == HarnessTestStatus::kTimeout) { + aggregate.fail_count += 1; + } + aggregate.total_duration += execution.duration; + aggregate.last_run = execution.completed_at; + aggregate.latest_execution = execution; } - HarnessTestExecution& execution = it->second; - execution.status = status; - if (execution.started_at == absl::InfinitePast()) { - execution.started_at = execution.queued_at; - } - execution.completed_at = absl::Now(); - execution.duration = execution.completed_at - execution.started_at; - execution.error_message = error_message; - if (!assertion_failures.empty()) { - execution.assertion_failures = assertion_failures; - } - if (!logs.empty()) { - execution.logs.insert(execution.logs.end(), logs.begin(), logs.end()); - } - if (!metrics.empty()) { - execution.metrics.insert(metrics.begin(), metrics.end()); - } - - // IT-08b: Auto-capture failure context for failed/timeout tests - if (status == HarnessTestStatus::kFailed || - status == HarnessTestStatus::kTimeout) { - // Release lock before calling CaptureFailureContext to avoid deadlock - // TODO: FIXME - // lock.Release(); + if (capture_failure_context) { CaptureFailureContext(test_id); - // lock.Acquire(); } - - HarnessAggregate& aggregate = harness_aggregates_[execution.name]; - if (aggregate.category.empty()) { - aggregate.category = execution.category; - } - aggregate.total_runs += 1; - if (status == HarnessTestStatus::kPassed) { - aggregate.pass_count += 1; - } else if (status == HarnessTestStatus::kFailed || - status == HarnessTestStatus::kTimeout) { - aggregate.fail_count += 1; - } - aggregate.total_duration += execution.duration; - aggregate.last_run = execution.completed_at; - aggregate.latest_execution = execution; } void TestManager::AppendHarnessTestLog(const std::string& test_id, @@ -1642,29 +1647,53 @@ void TestManager::CaptureFailureContext(const std::string& test_id) { HarnessTestExecution& execution = it->second; // 1. Capture execution context (frame count, active window, etc.) - if (ImGui::GetCurrentContext() != nullptr) { - // TODO: FIXME - // ImGuiWindow* current_window = ImGui::GetCurrentWindow(); - // const char* window_name = current_window ? current_window->Name : "none"; - // ImGuiID active_id = ImGui::GetActiveID(); + ImGuiContext* ctx = ImGui::GetCurrentContext(); + if (ctx != nullptr) { +#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE + ImGuiWindow* current_window = ctx->CurrentWindow; + ImGuiWindow* nav_window = ctx->NavWindow; + ImGuiWindow* hovered_window = ctx->HoveredWindow; - // execution.failure_context = - // absl::StrFormat("Frame: %d, Active Window: %s, Focused Widget: 0x%08X", - // ImGui::GetFrameCount(), window_name, active_id); + const char* current_name = + (current_window && current_window->Name) ? current_window->Name : "none"; + const char* nav_name = + (nav_window && nav_window->Name) ? nav_window->Name : "none"; + const char* hovered_name = + (hovered_window && hovered_window->Name) ? hovered_window->Name : "none"; + + ImGuiID active_id = ImGui::GetActiveID(); + ImGuiID hovered_id = ImGui::GetHoveredID(); + execution.failure_context = + absl::StrFormat( + "frame=%d current_window=%s nav_window=%s hovered_window=%s active_id=0x%08X hovered_id=0x%08X", + ImGui::GetFrameCount(), current_name, nav_name, hovered_name, + active_id, hovered_id); +#else + execution.failure_context = + absl::StrFormat("frame=%d", ImGui::GetFrameCount()); +#endif } else { - execution.failure_context = "ImGui context not available"; + execution.failure_context = "ImGui context not available"; } // 2. Screenshot capture would happen here via gRPC call // Note: Screenshot RPC implementation is in ImGuiTestHarnessServiceImpl // The screenshot_path will be set by the RPC handler when it completes // For now, we just set a placeholder path to indicate where it should be saved - execution.screenshot_path = - absl::StrFormat("/tmp/yaze_test_%s_failure.bmp", test_id); + if (execution.screenshot_path.empty()) { + execution.screenshot_path = + absl::StrFormat("/tmp/yaze_test_%s_failure.bmp", test_id); + execution.screenshot_size_bytes = 0; + } // 3. Widget state capture (IT-08c) - // TODO: FINISHME - // execution.widget_state = core::CaptureWidgetState(); + execution.widget_state = core::CaptureWidgetState(); + + // Keep aggregate cache in sync with the latest execution snapshot. + auto aggregate_it = harness_aggregates_.find(execution.name); + if (aggregate_it != harness_aggregates_.end()) { + aggregate_it->second.latest_execution = execution; + } util::logf("[TestManager] Captured failure context for test %s: %s", test_id.c_str(), execution.failure_context.c_str());