yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
imgui_test_harness_service.cc
Go to the documentation of this file.
2#include "app/application.h"
4
5#ifdef YAZE_WITH_GRPC
6
8
9#include <algorithm>
10#include <chrono>
11#include <cstdlib>
12#include <deque>
13#include <filesystem>
14#include <fstream>
15#include <iostream>
16#include <limits>
17#include <optional>
18#include <thread>
19
20#include "absl/base/thread_annotations.h"
21#include "absl/container/flat_hash_map.h"
22#include "absl/strings/ascii.h"
23#include "absl/strings/numbers.h"
24#include "absl/strings/str_cat.h"
25#include "absl/strings/str_format.h"
26#include "absl/strings/str_replace.h"
27#include "absl/synchronization/mutex.h"
28#include "absl/time/clock.h"
29#include "absl/time/time.h"
36#include "imgui/imgui_internal.h"
37#include "protos/imgui_test_harness.grpc.pb.h"
38#include "protos/imgui_test_harness.pb.h"
39#include "util/file_util.h"
41#include "yaze.h" // For YAZE_VERSION_STRING
42
43// The Windows SDK defines Yield as a function-like macro, which collides with
44// ImGuiTestContext::Yield when this translation unit is built with clang-cl.
45#ifdef Yield
46#undef Yield
47#endif
48
49#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
50#include "imgui_test_engine/imgui_te_context.h"
51#include "imgui_test_engine/imgui_te_engine.h"
52
53// Helper to register and run a test dynamically
54namespace {
55thread_local int g_remote_replay_depth = 0;
56
57class ScopedRemoteReplaySubmission {
58 public:
59 ScopedRemoteReplaySubmission() { ++g_remote_replay_depth; }
60 ~ScopedRemoteReplaySubmission() { --g_remote_replay_depth; }
61};
62
63struct DynamicTestData {
64 bool remote_replay = g_remote_replay_depth != 0;
65 yaze::test::TestManager* manager = nullptr;
66 std::string harness_id;
67 std::function<void(ImGuiTestContext*)> test_func;
68};
69
70absl::Mutex g_dynamic_tests_mutex;
71std::deque<std::shared_ptr<DynamicTestData>> g_dynamic_tests
72 ABSL_GUARDED_BY(g_dynamic_tests_mutex);
73
74void KeepDynamicTestData(const std::shared_ptr<DynamicTestData>& data) {
75 absl::MutexLock lock(&g_dynamic_tests_mutex);
76 // ImGuiTest stores UserData as a raw pointer and registered tests remain
77 // available for replay for the lifetime of the engine. Retain every backing
78 // closure for that same lifetime; evicting an older entry can otherwise
79 // leave a queued or re-runnable test with dangling UserData.
80 g_dynamic_tests.push_back(data);
81}
82
83void RunDynamicTest(ImGuiTestContext* ctx) {
84 auto* data = (DynamicTestData*)ctx->Test->UserData;
85 if (data && data->test_func) {
86 if (data->remote_replay) {
87 const auto admitted = data->manager->BeginRemoteReplayFocus(ctx->Test);
88 if (!admitted.ok()) {
89 data->manager->MarkHarnessTestCompleted(
90 data->harness_id, yaze::test::HarnessTestStatus::kFailed,
91 std::string(admitted.message()));
92 return;
93 }
94 }
95 data->test_func(ctx);
96 }
97}
98
99// Registration, replay ownership and queue admission are one UI-thread task.
100// In particular, OnPostSwap cannot clear ownership in between these steps.
101absl::Status QueueDynamicTest(ImGuiTestEngine* engine,
104 const std::string& name,
105 const std::string& harness_id,
106 std::shared_ptr<DynamicTestData> data) {
107 return ui_queue->RunOnUiThread(
108 [engine, manager, name, harness_id, data = std::move(data)] {
109 ImGuiTest* test = IM_REGISTER_TEST(engine, "grpc", name.c_str());
110 test->SetOwnedName(name.c_str());
111 test->TestFunc = RunDynamicTest;
112 test->UserData = data.get();
113 data->manager = manager;
114 data->harness_id = harness_id;
115 if (data->remote_replay)
116 manager->RegisterRemoteReplayTest(test, harness_id);
117 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
118 KeepDynamicTestData(data);
119 return absl::OkStatus();
120 });
121}
122
123// Helper to check if a test has completed (not queued or running)
124bool IsTestCompleted(ImGuiTest* test) {
125 return test->Output.Status != ImGuiTestStatus_Queued &&
126 test->Output.Status != ImGuiTestStatus_Running;
127}
128
129// Thread-safe state for RPC communication
130template <typename T>
131struct RPCState {
132 std::atomic<bool> completed{false};
133 std::mutex data_mutex;
134 T result;
135 std::string message;
136
137 void SetResult(const T& res, const std::string& msg) {
138 std::lock_guard<std::mutex> lock(data_mutex);
139 result = res;
140 message = msg;
141 completed.store(true);
142 }
143
144 void GetResult(T& res, std::string& msg) {
145 std::lock_guard<std::mutex> lock(data_mutex);
146 res = result;
147 msg = message;
148 }
149};
150
151} // namespace
152#endif
153
154namespace {
155
156::yaze::test::GetTestStatusResponse_TestStatus ConvertHarnessStatus(
157 ::yaze::test::HarnessTestStatus status) {
158 switch (status) {
159 case ::yaze::test::HarnessTestStatus::kQueued:
160 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_QUEUED;
161 case ::yaze::test::HarnessTestStatus::kRunning:
162 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_RUNNING;
163 case ::yaze::test::HarnessTestStatus::kPassed:
164 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_PASSED;
165 case ::yaze::test::HarnessTestStatus::kFailed:
166 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_FAILED;
167 case ::yaze::test::HarnessTestStatus::kTimeout:
168 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_TIMEOUT;
169 case ::yaze::test::HarnessTestStatus::kUnspecified:
170 default:
171 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_UNSPECIFIED;
172 }
173}
174
175int64_t ToUnixMillisSafe(absl::Time timestamp) {
176 if (timestamp == absl::InfinitePast()) {
177 return 0;
178 }
179 return absl::ToUnixMillis(timestamp);
180}
181
182int32_t ClampDurationToInt32(absl::Duration duration) {
183 int64_t millis = absl::ToInt64Milliseconds(duration);
184 if (millis > std::numeric_limits<int32_t>::max()) {
185 return std::numeric_limits<int32_t>::max();
186 }
187 if (millis < std::numeric_limits<int32_t>::min()) {
188 return std::numeric_limits<int32_t>::min();
189 }
190 return static_cast<int32_t>(millis);
191}
192
193} // namespace
194
195#include <grpcpp/grpcpp.h>
196#include <grpcpp/server_builder.h>
197
198namespace yaze {
199namespace test {
200
201namespace {
202
203std::string ClickTypeToString(ClickRequest::ClickType type) {
204 switch (type) {
205 case ClickRequest::CLICK_TYPE_RIGHT:
206 return "right";
207 case ClickRequest::CLICK_TYPE_MIDDLE:
208 return "middle";
209 case ClickRequest::CLICK_TYPE_DOUBLE:
210 return "double";
211 case ClickRequest::CLICK_TYPE_LEFT:
212 case ClickRequest::CLICK_TYPE_UNSPECIFIED:
213 default:
214 return "left";
215 }
216}
217
218ClickRequest::ClickType ClickTypeFromString(absl::string_view type) {
219 const std::string lower = absl::AsciiStrToLower(std::string(type));
220 if (lower == "right") {
221 return ClickRequest::CLICK_TYPE_RIGHT;
222 }
223 if (lower == "middle") {
224 return ClickRequest::CLICK_TYPE_MIDDLE;
225 }
226 if (lower == "double" || lower == "double_click" || lower == "dbl") {
227 return ClickRequest::CLICK_TYPE_DOUBLE;
228 }
229 return ClickRequest::CLICK_TYPE_LEFT;
230}
231
232HarnessTestStatus HarnessStatusFromString(absl::string_view status) {
233 const std::string lower = absl::AsciiStrToLower(std::string(status));
234 if (lower == "passed" || lower == "success") {
235 return HarnessTestStatus::kPassed;
236 }
237 if (lower == "failed" || lower == "fail") {
238 return HarnessTestStatus::kFailed;
239 }
240 if (lower == "timeout") {
241 return HarnessTestStatus::kTimeout;
242 }
243 if (lower == "queued") {
244 return HarnessTestStatus::kQueued;
245 }
246 if (lower == "running") {
247 return HarnessTestStatus::kRunning;
248 }
249 return HarnessTestStatus::kUnspecified;
250}
251
252const char* HarnessStatusToString(HarnessTestStatus status) {
253 switch (status) {
254 case HarnessTestStatus::kPassed:
255 return "passed";
256 case HarnessTestStatus::kFailed:
257 return "failed";
258 case HarnessTestStatus::kTimeout:
259 return "timeout";
260 case HarnessTestStatus::kQueued:
261 return "queued";
262 case HarnessTestStatus::kRunning:
263 return "running";
264 case HarnessTestStatus::kUnspecified:
265 default:
266 return "unknown";
267 }
268}
269
270std::string ApplyOverrides(
271 const std::string& value,
272 const absl::flat_hash_map<std::string, std::string>& overrides) {
273 if (overrides.empty() || value.empty()) {
274 return value;
275 }
276 std::string result = value;
277 for (const auto& [key, replacement] : overrides) {
278 const std::string placeholder = absl::StrCat("{{", key, "}}");
279 result = absl::StrReplaceAll(result, {{placeholder, replacement}});
280 }
281 return result;
282}
283
284void MaybeRecordStep(TestRecorder* recorder, TestRecorder::RecordedStep step) {
285 if (!recorder || !recorder->IsRecording()) {
286 return;
287 }
288 if (step.captured_at == absl::InfinitePast()) {
289 step.captured_at = absl::Now();
290 }
291 recorder->RecordStep(step);
292}
293
294absl::Status WaitForHarnessTestCompletion(TestManager* manager,
295 const std::string& test_id,
296 HarnessTestExecution* execution) {
297 if (!manager) {
298 return absl::FailedPreconditionError("TestManager unavailable");
299 }
300 if (test_id.empty()) {
301 return absl::InvalidArgumentError("Missing harness test identifier");
302 }
303
304 const absl::Time deadline = absl::Now() + absl::Seconds(20);
305 while (absl::Now() < deadline) {
306 absl::StatusOr<HarnessTestExecution> current =
307 manager->GetHarnessTestExecution(test_id);
308 if (!current.ok()) {
309 absl::SleepFor(absl::Milliseconds(75));
310 continue;
311 }
312
313 if (execution) {
314 *execution = std::move(current.value());
315 }
316
317 if (current->status == HarnessTestStatus::kQueued ||
318 current->status == HarnessTestStatus::kRunning) {
319 absl::SleepFor(absl::Milliseconds(75));
320 continue;
321 }
322 return absl::OkStatus();
323 }
324
325 return absl::DeadlineExceededError(absl::StrFormat(
326 "Harness test %s did not reach a terminal state", test_id));
327}
328
329struct ParsedCondition {
330 std::string type;
331 std::string target;
332};
333
334std::string ExtractLabelFromPath(absl::string_view path) {
335 size_t colon = path.rfind(':');
336 if (colon != absl::string_view::npos && colon + 1 < path.size()) {
337 return std::string(path.substr(colon + 1));
338 }
339 size_t slash = path.rfind('/');
340 if (slash != absl::string_view::npos && slash + 1 < path.size()) {
341 return std::string(path.substr(slash + 1));
342 }
343 return std::string(path);
344}
345
346std::string ExtractTypeFromPath(absl::string_view path) {
347 size_t slash = path.rfind('/');
348 absl::string_view segment =
349 slash == absl::string_view::npos ? path : path.substr(slash + 1);
350 size_t colon = segment.find(':');
351 if (colon == absl::string_view::npos || colon == 0) {
352 return "widget";
353 }
354 return std::string(segment.substr(0, colon));
355}
356
357absl::StatusOr<internal::ParsedTarget> ParseTargetString(
358 absl::string_view target) {
359 if (target.empty()) {
360 return absl::InvalidArgumentError(
361 "Missing target. Provide 'type:label' or widget_key.");
362 }
363 size_t colon_pos = target.find(':');
364 if (colon_pos == absl::string_view::npos || colon_pos == 0 ||
365 colon_pos + 1 >= target.size()) {
366 return absl::InvalidArgumentError(
367 "Invalid target format. Use 'type:label' (e.g. 'button:Open ROM').");
368 }
369
370 internal::ParsedTarget parsed;
371 parsed.type = std::string(target.substr(0, colon_pos));
372 parsed.label = std::string(target.substr(colon_pos + 1));
373 return parsed;
374}
375
376} // namespace
377
378namespace internal {
379
380absl::StatusOr<std::string> FocusHarnessWindow(editor::WindowHost* host,
381 size_t session_id,
382 absl::string_view window_id) {
383 if (window_id.empty() || window_id.find('\0') != absl::string_view::npos) {
384 return absl::InvalidArgumentError(
385 "Window target requires a nonempty base ID");
386 }
387 if (!host || !host->window_manager() || !ImGui::GetCurrentContext()) {
388 return absl::FailedPreconditionError(
389 "Window host/UI context is unavailable");
390 }
391 auto* windows = host->window_manager();
392 if (windows->GetActiveSessionId() != session_id) {
393 return absl::FailedPreconditionError("Window target session is not active");
394 }
395 const std::string id = host->ResolveWindowId(std::string(window_id));
396 const auto session_windows = windows->GetWindowsInSession(session_id);
397 if (std::find(session_windows.begin(), session_windows.end(), id) ==
398 session_windows.end() ||
399 !windows->GetWindowDescriptor(session_id, id)) {
400 return absl::NotFoundError("Unknown window ID in the active session: " +
401 id);
402 }
403 const std::string name = host->GetWorkspaceWindowName(session_id, id);
404 if (name.empty() || !ImGui::FindWindowByName(name.c_str())) {
405 return absl::FailedPreconditionError(
406 "Registered window has not been drawn");
407 }
408 if (!host->OpenAndFocusWindow(session_id, id)) {
409 return absl::FailedPreconditionError("Window host refused to open/focus: " +
410 id);
411 }
412 return name;
413}
414
415absl::Status VerifyHarnessWindowFocus(absl::string_view window_name) {
416 ImGuiContext* context = ImGui::GetCurrentContext();
417 ImGuiWindow* window =
418 context ? ImGui::FindWindowByName(std::string(window_name).c_str())
419 : nullptr;
420 if (!window || !window->Active || window->Hidden ||
421 (window->DockNode && !window->DockTabIsVisible) || !context->NavWindow ||
422 context->NavWindow->RootWindow != window->RootWindow) {
423 return absl::FailedPreconditionError(
424 "Requested window/tab is not focused and visible");
425 }
426 return absl::OkStatus();
427}
428
429absl::Status ValidateProjectSaveAsDestination(const char* destination) {
430 namespace fs = std::filesystem;
431 if (!destination ||
432 (*destination && (!fs::path(destination).is_absolute() ||
433 fs::path(destination).extension() != ".yaze"))) {
434 return absl::InvalidArgumentError(
435 "Set YAZE_TEST_PROJECT_SAVE_AS to an absolute NEW copied-project "
436 ".yaze destination, or an empty string for Cancel");
437 }
438 if (!*destination) {
439 return absl::OkStatus();
440 }
441 std::error_code error;
442 const auto entry = fs::symlink_status(destination, error);
443 if (error && error != std::errc::no_such_file_or_directory) {
444 return absl::FailedPreconditionError(absl::StrCat(
445 "Cannot inspect Project Save As destination: ", error.message()));
446 }
447 if (entry.type() != fs::file_type::not_found) {
448 return absl::AlreadyExistsError(
449 "Project Save As replay destination already exists");
450 }
451 // Windows reports a path below a regular file as not found rather than
452 // ENOTDIR, so the parent is checked explicitly.
453 const auto parent = fs::path(destination).parent_path();
454 const auto parent_entry = fs::status(parent, error);
455 if (error || parent_entry.type() != fs::file_type::directory) {
456 return absl::FailedPreconditionError(
457 absl::StrCat("Cannot inspect Project Save As destination: parent ",
458 parent.string(), " is not an existing directory"));
459 }
460 return absl::OkStatus();
461}
462
463absl::StatusOr<ResolvedWidgetSelector> ResolveWidgetSelector(
464 absl::string_view target, absl::string_view widget_key) {
465 ResolvedWidgetSelector resolved;
466 if (!widget_key.empty()) {
467 const std::string key(widget_key);
468 std::optional<gui::WidgetIdRegistry::WidgetInfo> info =
469 gui::WidgetIdRegistry::Instance().GetWidgetInfo(key);
470 if (!info) {
471 return absl::NotFoundError(absl::StrFormat(
472 "widget_key '%s' was not found in WidgetIdRegistry", key));
473 }
474
475 resolved.resolved_widget_key = key;
476 resolved.resolved_path =
477 info->full_path.empty() ? key : std::string(info->full_path);
478 resolved.imgui_id = info->imgui_id;
479 if (resolved.imgui_id == 0) {
480 return absl::FailedPreconditionError(absl::StrFormat(
481 "widget_key '%s' resolved without a valid ImGui ID", key));
482 }
483 resolved.target.type = info->type.empty()
484 ? ExtractTypeFromPath(resolved.resolved_path)
485 : std::string(info->type);
486 resolved.target.label = info->label.empty()
487 ? ExtractLabelFromPath(resolved.resolved_path)
488 : std::string(info->label);
489 if (resolved.target.label.empty()) {
490 return absl::FailedPreconditionError(absl::StrFormat(
491 "widget_key '%s' resolved without a usable label", key));
492 }
493 if (resolved.target.type.empty()) {
494 resolved.target.type = "widget";
495 }
496 return resolved;
497 }
498
499 absl::StatusOr<ParsedTarget> parsed = ParseTargetString(target);
500 if (!parsed.ok()) {
501 return parsed.status();
502 }
503 resolved.target = *parsed;
504 return resolved;
505}
506
507void ApplyTerminalHarnessExecution(const HarnessTestExecution& execution,
508 bool* step_success,
509 std::string* step_message) {
510 if (step_success) {
511 *step_success = execution.status == HarnessTestStatus::kPassed;
512 }
513 if (step_message && !execution.error_message.empty()) {
514 *step_message = execution.error_message;
515 }
516}
517
518} // namespace internal
519
520absl::StatusOr<ScreenshotArtifact> CaptureScreenshotOnRenderThread(
521 const std::string& preferred_path, const std::string& window_title,
522 ScreenshotFormat format, std::chrono::milliseconds timeout) {
523 auto* controller = Application::Instance().GetController();
524 if (!controller) {
525 return absl::FailedPreconditionError(
526 "Application controller not available");
527 }
528
529 // Shared with the callback so a capture that lands after the timeout still
530 // has somewhere to write.
531 struct State {
532 std::atomic<bool> done{false};
533 absl::StatusOr<ScreenshotArtifact> result =
534 absl::UnknownError("Not captured");
535 };
536 auto state = std::make_shared<State>();
537
538 controller->RequestScreenshot(
539 {.preferred_path = preferred_path,
540 .window_title = window_title,
541 .format = format,
542 .reveal_to_user = false,
543 .callback = [state](absl::StatusOr<ScreenshotArtifact> result) {
544 state->result = std::move(result);
545 state->done.store(true);
546 }});
547
548 const auto deadline = std::chrono::steady_clock::now() + timeout;
549 while (!state->done.load()) {
550 if (std::chrono::steady_clock::now() > deadline) {
551 return absl::DeadlineExceededError(
552 "Timed out waiting for screenshot capture on main thread");
553 }
554 std::this_thread::sleep_for(std::chrono::milliseconds(10));
555 }
556 return state->result;
557}
558
559namespace {
560
561absl::StatusOr<ParsedCondition> ParseConditionString(absl::string_view value) {
562 if (value.empty()) {
563 return absl::InvalidArgumentError(
564 "Missing condition. Provide 'type:target' or widget_key.");
565 }
566
567 size_t colon_pos = value.find(':');
568 if (colon_pos == absl::string_view::npos || colon_pos == 0) {
569 return absl::InvalidArgumentError(
570 "Invalid condition format. Use 'type:target'.");
571 }
572
573 ParsedCondition parsed;
574 parsed.type = std::string(value.substr(0, colon_pos));
575 parsed.target = std::string(value.substr(colon_pos + 1));
576 return parsed;
577}
578
579absl::StatusOr<ParsedCondition> ResolveCondition(
580 absl::string_view condition, absl::string_view widget_key,
581 absl::string_view default_type_for_widget,
582 internal::ResolvedWidgetSelector* resolved_selector) {
583 std::optional<internal::ResolvedWidgetSelector> widget_selector;
584 if (!widget_key.empty()) {
585 absl::StatusOr<internal::ResolvedWidgetSelector> resolved =
586 internal::ResolveWidgetSelector("", widget_key);
587 if (!resolved.ok()) {
588 return resolved.status();
589 }
590 widget_selector = *resolved;
591 if (resolved_selector) {
592 *resolved_selector = *resolved;
593 }
594 } else if (resolved_selector) {
595 *resolved_selector = internal::ResolvedWidgetSelector{};
596 }
597
598 if (condition.empty()) {
599 if (!widget_selector.has_value()) {
600 return absl::InvalidArgumentError(
601 "Missing condition. Provide 'type:target' or widget_key.");
602 }
603 ParsedCondition parsed;
604 parsed.type = std::string(default_type_for_widget);
605 parsed.target = widget_selector->target.label;
606 return parsed;
607 }
608
609 absl::StatusOr<ParsedCondition> parsed = ParseConditionString(condition);
610 if (!parsed.ok()) {
611 return parsed.status();
612 }
613
614 if (widget_selector.has_value() && parsed->target.empty()) {
615 parsed->target = widget_selector->target.label;
616 }
617
618 if (parsed->target.empty()) {
619 return absl::InvalidArgumentError(
620 "Condition target is empty. Provide 'type:target'.");
621 }
622
623 return parsed;
624}
625
626struct UiSyncSnapshot {
627 int64_t frame_id = 0;
628 int32_t pending_editor_actions = 0;
629 int32_t pending_layout_actions = 0;
630 bool layout_rebuild_pending = false;
631 int32_t pending_harness_tests = 0;
632 int32_t queued_harness_tests = 0;
633 int32_t running_harness_tests = 0;
634};
635
636editor::EditorManager* GetEditorManagerForSync() {
637 Controller* controller = Application::Instance().GetController();
638 if (!controller) {
639 return nullptr;
640 }
641 return controller->editor_manager();
642}
643
644UiSyncSnapshot CaptureUiSyncSnapshot(TestManager* test_manager) {
645 UiSyncSnapshot snapshot;
646 if (editor::EditorManager* editor_manager = GetEditorManagerForSync()) {
647 const editor::EditorManager::UiSyncState state =
648 editor_manager->GetUiSyncStateSnapshot();
649 snapshot.frame_id = static_cast<int64_t>(state.frame_id);
650 snapshot.pending_editor_actions = state.pending_editor_actions;
651 snapshot.pending_layout_actions = state.pending_layout_actions;
652 snapshot.layout_rebuild_pending = state.layout_rebuild_pending;
653 }
654
655 if (test_manager) {
656 const std::vector<HarnessTestSummary> summaries =
657 test_manager->ListHarnessTestSummaries();
658 for (const HarnessTestSummary& summary : summaries) {
659 switch (summary.latest_execution.status) {
660 case HarnessTestStatus::kQueued:
661 snapshot.queued_harness_tests++;
662 break;
663 case HarnessTestStatus::kRunning:
664 snapshot.running_harness_tests++;
665 break;
666 default:
667 break;
668 }
669 }
670 }
671
672 snapshot.pending_harness_tests =
673 snapshot.queued_harness_tests + snapshot.running_harness_tests;
674 return snapshot;
675}
676
677bool IsUiIdle(const UiSyncSnapshot& snapshot) {
678 return snapshot.pending_editor_actions == 0 &&
679 snapshot.pending_layout_actions == 0 &&
680 !snapshot.layout_rebuild_pending &&
681 snapshot.pending_harness_tests == 0;
682}
683
684std::string BuildIdleTimeoutReason(const UiSyncSnapshot& snapshot) {
685 return absl::StrFormat(
686 "pending_editor=%d pending_layout=%d layout_rebuild=%s queued_tests=%d "
687 "running_tests=%d",
688 snapshot.pending_editor_actions, snapshot.pending_layout_actions,
689 snapshot.layout_rebuild_pending ? "true" : "false",
690 snapshot.queued_harness_tests, snapshot.running_harness_tests);
691}
692
693int32_t NormalizeTimeoutMs(int32_t timeout_ms, int32_t fallback_ms) {
694 return timeout_ms <= 0 ? fallback_ms : timeout_ms;
695}
696
697int32_t NormalizePollIntervalMs(int32_t poll_interval_ms) {
698 if (poll_interval_ms <= 0) {
699 return 25;
700 }
701 return std::clamp(poll_interval_ms, 5, 1000);
702}
703
704} // namespace
705
706// gRPC service wrapper that forwards to our implementation
707class ImGuiTestHarnessServiceGrpc final : public ImGuiTestHarness::Service {
708 public:
709 explicit ImGuiTestHarnessServiceGrpc(ImGuiTestHarnessServiceImpl* impl)
710 : impl_(impl) {}
711
712 ::grpc::Status Ping(::grpc::ServerContext* context,
713 const PingRequest* request,
714 PingResponse* response) override {
715 return ConvertStatus(impl_->Ping(request, response));
716 }
717
718 ::grpc::Status Click(::grpc::ServerContext* context,
719 const ClickRequest* request,
720 ClickResponse* response) override {
721 return ConvertStatus(impl_->Click(request, response));
722 }
723
724 ::grpc::Status Type(::grpc::ServerContext* context,
725 const TypeRequest* request,
726 TypeResponse* response) override {
727 return ConvertStatus(impl_->Type(request, response));
728 }
729
730 ::grpc::Status Wait(::grpc::ServerContext* context,
731 const WaitRequest* request,
732 WaitResponse* response) override {
733 return ConvertStatus(impl_->Wait(request, response));
734 }
735
736 ::grpc::Status Assert(::grpc::ServerContext* context,
737 const AssertRequest* request,
738 AssertResponse* response) override {
739 return ConvertStatus(impl_->Assert(request, response));
740 }
741
742 ::grpc::Status FlushUiActions(::grpc::ServerContext* context,
743 const FlushUiActionsRequest* request,
744 FlushUiActionsResponse* response) override {
745 return ConvertStatus(impl_->FlushUiActions(request, response));
746 }
747
748 ::grpc::Status WaitForIdle(::grpc::ServerContext* context,
749 const WaitForIdleRequest* request,
750 WaitForIdleResponse* response) override {
751 return ConvertStatus(impl_->WaitForIdle(request, response));
752 }
753
754 ::grpc::Status GetUiSyncState(::grpc::ServerContext* context,
755 const GetUiSyncStateRequest* request,
756 GetUiSyncStateResponse* response) override {
757 return ConvertStatus(impl_->GetUiSyncState(request, response));
758 }
759
760 ::grpc::Status Screenshot(::grpc::ServerContext* context,
761 const ScreenshotRequest* request,
762 ScreenshotResponse* response) override {
763 return ConvertStatus(impl_->Screenshot(request, response));
764 }
765
766 ::grpc::Status GetTestStatus(::grpc::ServerContext* context,
767 const GetTestStatusRequest* request,
768 GetTestStatusResponse* response) override {
769 return ConvertStatus(impl_->GetTestStatus(request, response));
770 }
771
772 ::grpc::Status ListTests(::grpc::ServerContext* context,
773 const ListTestsRequest* request,
774 ListTestsResponse* response) override {
775 return ConvertStatus(impl_->ListTests(request, response));
776 }
777
778 ::grpc::Status GetTestResults(::grpc::ServerContext* context,
779 const GetTestResultsRequest* request,
780 GetTestResultsResponse* response) override {
781 return ConvertStatus(impl_->GetTestResults(request, response));
782 }
783
784 ::grpc::Status DiscoverWidgets(::grpc::ServerContext* context,
785 const DiscoverWidgetsRequest* request,
786 DiscoverWidgetsResponse* response) override {
787 return ConvertStatus(impl_->DiscoverWidgets(request, response));
788 }
789
790 ::grpc::Status StartRecording(::grpc::ServerContext* context,
791 const StartRecordingRequest* request,
792 StartRecordingResponse* response) override {
793 return ConvertStatus(impl_->StartRecording(request, response));
794 }
795
796 ::grpc::Status StopRecording(::grpc::ServerContext* context,
797 const StopRecordingRequest* request,
798 StopRecordingResponse* response) override {
799 return ConvertStatus(impl_->StopRecording(request, response));
800 }
801
802 ::grpc::Status ReplayTest(::grpc::ServerContext* context,
803 const ReplayTestRequest* request,
804 ReplayTestResponse* response) override {
805 return ConvertStatus(impl_->ReplayTest(request, response));
806 }
807
808 private:
809 static ::grpc::Status ConvertStatus(const absl::Status& status) {
810 if (status.ok()) {
811 return ::grpc::Status::OK;
812 }
813
814 ::grpc::StatusCode code = ::grpc::StatusCode::UNKNOWN;
815 switch (status.code()) {
816 case absl::StatusCode::kCancelled:
817 code = ::grpc::StatusCode::CANCELLED;
818 break;
819 case absl::StatusCode::kUnknown:
820 code = ::grpc::StatusCode::UNKNOWN;
821 break;
822 case absl::StatusCode::kInvalidArgument:
823 code = ::grpc::StatusCode::INVALID_ARGUMENT;
824 break;
825 case absl::StatusCode::kDeadlineExceeded:
826 code = ::grpc::StatusCode::DEADLINE_EXCEEDED;
827 break;
828 case absl::StatusCode::kNotFound:
829 code = ::grpc::StatusCode::NOT_FOUND;
830 break;
831 case absl::StatusCode::kAlreadyExists:
832 code = ::grpc::StatusCode::ALREADY_EXISTS;
833 break;
834 case absl::StatusCode::kPermissionDenied:
835 code = ::grpc::StatusCode::PERMISSION_DENIED;
836 break;
837 case absl::StatusCode::kResourceExhausted:
838 code = ::grpc::StatusCode::RESOURCE_EXHAUSTED;
839 break;
840 case absl::StatusCode::kFailedPrecondition:
841 code = ::grpc::StatusCode::FAILED_PRECONDITION;
842 break;
843 case absl::StatusCode::kAborted:
844 code = ::grpc::StatusCode::ABORTED;
845 break;
846 case absl::StatusCode::kOutOfRange:
847 code = ::grpc::StatusCode::OUT_OF_RANGE;
848 break;
849 case absl::StatusCode::kUnimplemented:
850 code = ::grpc::StatusCode::UNIMPLEMENTED;
851 break;
852 case absl::StatusCode::kInternal:
853 code = ::grpc::StatusCode::INTERNAL;
854 break;
855 case absl::StatusCode::kUnavailable:
856 code = ::grpc::StatusCode::UNAVAILABLE;
857 break;
858 case absl::StatusCode::kDataLoss:
859 code = ::grpc::StatusCode::DATA_LOSS;
860 break;
861 case absl::StatusCode::kUnauthenticated:
862 code = ::grpc::StatusCode::UNAUTHENTICATED;
863 break;
864 default:
865 code = ::grpc::StatusCode::UNKNOWN;
866 break;
867 }
868
869 return ::grpc::Status(
870 code, std::string(status.message().data(), status.message().size()));
871 }
872
873 ImGuiTestHarnessServiceImpl* impl_;
874};
875
876std::unique_ptr<::grpc::Service> CreateImGuiTestHarnessServiceGrpc(
877 ImGuiTestHarnessServiceImpl* impl) {
878 return std::make_unique<ImGuiTestHarnessServiceGrpc>(impl);
879}
880
881// ============================================================================
882// ImGuiTestHarnessServiceImpl - RPC Handlers
883// ============================================================================
884
885ImGuiTestHarnessServiceImpl::ImGuiTestHarnessServiceImpl(
886 TestManager* test_manager, util::UiThreadQueue* ui_thread_queue)
887 : test_manager_(test_manager),
888 ui_thread_queue_(ui_thread_queue ? ui_thread_queue
889 : &util::UiThreadQueue::Get()),
890 test_recorder_(test_manager) {}
891
892absl::Status ImGuiTestHarnessServiceImpl::Ping(const PingRequest* request,
893 PingResponse* response) {
894 // Echo back the message with "Pong: " prefix
895 response->set_message(absl::StrFormat("Pong: %s", request->message()));
896
897 // Add current timestamp
898 auto now = std::chrono::system_clock::now();
899 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
900 now.time_since_epoch());
901 response->set_timestamp_ms(ms.count());
902
903 // Add YAZE version
904 response->set_yaze_version(YAZE_VERSION_STRING);
905
906 return absl::OkStatus();
907}
908
909absl::Status ImGuiTestHarnessServiceImpl::Click(const ClickRequest* request,
910 ClickResponse* response) {
911 auto start = std::chrono::steady_clock::now();
912
913 TestRecorder::RecordedStep recorded_step;
914 recorded_step.type = TestRecorder::ActionType::kClick;
915 if (request) {
916 recorded_step.widget_key = request->widget_key();
917 if (!request->widget_key().empty()) {
918 recorded_step.target = absl::StrCat("widget_key:", request->widget_key());
919 } else {
920 recorded_step.target = request->target();
921 }
922 recorded_step.click_type = ClickTypeToString(request->type());
923 }
924
925 auto finalize = [&](const absl::Status& status) {
926 recorded_step.success = response->success();
927 recorded_step.message = response->message();
928 recorded_step.execution_time_ms = response->execution_time_ms();
929 recorded_step.test_id = response->test_id();
930 MaybeRecordStep(&test_recorder_, recorded_step);
931 return status;
932 };
933
934 if (!test_manager_) {
935 response->set_success(false);
936 response->set_message("TestManager not available");
937 response->set_execution_time_ms(0);
938 return finalize(absl::FailedPreconditionError("TestManager not available"));
939 }
940
941 const std::string requested_target = request ? request->target() : "";
942 const std::string requested_widget_key = request ? request->widget_key() : "";
943 const std::string request_summary =
944 !requested_widget_key.empty()
945 ? absl::StrFormat("widget_key:%s", requested_widget_key)
946 : requested_target;
947
948 const std::string test_id = test_manager_->RegisterHarnessTest(
949 absl::StrFormat("Click: %s", request_summary), "grpc");
950 response->set_test_id(test_id);
951 recorded_step.test_id = test_id;
952 test_manager_->AppendHarnessTestLog(
953 test_id, absl::StrCat("Queued click request: ", request_summary));
954
955 absl::StatusOr<internal::ResolvedWidgetSelector> resolved_selector =
956 internal::ResolveWidgetSelector(requested_target, requested_widget_key);
957 if (!resolved_selector.ok()) {
958 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
959 std::chrono::steady_clock::now() - start);
960 std::string message = std::string(resolved_selector.status().message());
961 response->set_success(false);
962 response->set_message(message);
963 response->set_execution_time_ms(elapsed.count());
964 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
965 message);
966 test_manager_->AppendHarnessTestLog(test_id, message);
967 return finalize(absl::OkStatus());
968 }
969
970 if (!resolved_selector->resolved_widget_key.empty()) {
971 response->set_resolved_widget_key(resolved_selector->resolved_widget_key);
972 }
973 if (!resolved_selector->resolved_path.empty()) {
974 response->set_resolved_path(resolved_selector->resolved_path);
975 }
976
977 const std::string widget_type = resolved_selector->target.type;
978 const std::string widget_label = resolved_selector->target.label;
979 const ImGuiID widget_id = resolved_selector->imgui_id;
980
981#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
982 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
983 if (!engine) {
984 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
985 std::chrono::steady_clock::now() - start);
986 std::string message = "ImGuiTestEngine not initialized";
987 response->set_success(false);
988 response->set_message(message);
989 response->set_execution_time_ms(elapsed.count());
990 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
991 message);
992 return finalize(absl::OkStatus());
993 }
994
995 const ClickRequest::ClickType click_type =
996 request ? request->type() : ClickRequest::CLICK_TYPE_LEFT;
997
998 ImGuiMouseButton mouse_button = ImGuiMouseButton_Left;
999 switch (click_type) {
1000 case ClickRequest::CLICK_TYPE_UNSPECIFIED:
1001 case ClickRequest::CLICK_TYPE_LEFT:
1002 mouse_button = ImGuiMouseButton_Left;
1003 break;
1004 case ClickRequest::CLICK_TYPE_RIGHT:
1005 mouse_button = ImGuiMouseButton_Right;
1006 break;
1007 case ClickRequest::CLICK_TYPE_MIDDLE:
1008 mouse_button = ImGuiMouseButton_Middle;
1009 break;
1010 case ClickRequest::CLICK_TYPE_DOUBLE:
1011 // handled below
1012 break;
1013 }
1014
1015 auto test_data = std::make_shared<DynamicTestData>();
1016 TestManager* manager = test_manager_;
1017 test_data->test_func = [manager, captured_id = test_id, widget_type,
1018 widget_label, widget_id, click_type,
1019 mouse_button](ImGuiTestContext* ctx) {
1020 manager->MarkHarnessTestRunning(captured_id);
1021 try {
1022 if (widget_type == "window") {
1023 auto* controller = Application::Instance().GetController();
1024 auto* editor = controller ? controller->editor_manager() : nullptr;
1025 if (!editor || !editor->GetCurrentEditorSet()) {
1026 manager->MarkHarnessTestCompleted(
1027 captured_id, HarnessTestStatus::kFailed,
1028 "Window focus requires an active editor session");
1029 return;
1030 }
1031 const auto focused = internal::FocusHarnessWindow(
1032 editor->window_host(), editor->GetCurrentSessionId(), widget_label);
1033 if (!focused.ok()) {
1034 manager->MarkHarnessTestCompleted(
1035 captured_id, HarnessTestStatus::kFailed,
1036 std::string(focused.status().message()));
1037 return;
1038 }
1039 ctx->Yield(2);
1040 const auto verified = internal::VerifyHarnessWindowFocus(*focused);
1041 if (!verified.ok() || ctx->IsError()) {
1042 manager->MarkHarnessTestCompleted(
1043 captured_id, HarnessTestStatus::kFailed,
1044 verified.ok() ? "Window focus action failed"
1045 : std::string(verified.message()));
1046 return;
1047 }
1048 manager->MarkHarnessTestCompleted(captured_id,
1049 HarnessTestStatus::kPassed,
1050 "Focused window: " + widget_label);
1051 return;
1052 }
1053 if (widget_type == "shortcut") {
1054 ImGuiKeyChord chord = 0;
1055 std::optional<util::ScopedSaveFileDialogResultForTesting> save_picker;
1056 if (widget_label == "Save ROM") {
1057 chord = ImGuiMod_Shortcut | ImGuiKey_S;
1058 } else if (widget_label == "Undo") {
1059 chord = ImGuiMod_Shortcut | ImGuiKey_Z;
1060 } else if (widget_label == "Redo") {
1061 chord = ImGuiMod_Shortcut | ImGuiMod_Shift | ImGuiKey_Z;
1062 } else if (widget_label == "Save As") {
1063 const char* destination = std::getenv("YAZE_TEST_PROJECT_SAVE_AS");
1064 const auto destination_status =
1065 internal::ValidateProjectSaveAsDestination(destination);
1066 if (!destination_status.ok()) {
1067 manager->MarkHarnessTestCompleted(
1068 captured_id, HarnessTestStatus::kFailed,
1069 std::string(destination_status.message()));
1070 return;
1071 }
1072 auto* controller = Application::Instance().GetController();
1073 auto* editor = controller ? controller->editor_manager() : nullptr;
1074 if (!editor || !editor->GetCurrentProject()->project_opened()) {
1075 manager->MarkHarnessTestCompleted(
1076 captured_id, HarnessTestStatus::kFailed,
1077 "Project Save As replay requires an open copied project");
1078 return;
1079 }
1080 save_picker.emplace(destination);
1081 chord = ImGuiMod_Shortcut | ImGuiMod_Shift | ImGuiKey_S;
1082 } else if (widget_label == "Settings") {
1083 chord = ImGuiMod_Shortcut | ImGuiKey_Comma;
1084 } else if (widget_label == "Quit") {
1085 // A successful quit destroys the harness before a simulated key
1086 // press can finish and report its result. Invoke the same guarded
1087 // application action directly, fail if unsaved work blocks it, and
1088 // let the caller prove process and listener teardown.
1089 auto* controller = Application::Instance().GetController();
1090 auto* editor_manager =
1091 controller != nullptr ? controller->editor_manager() : nullptr;
1092 if (editor_manager == nullptr) {
1093 const std::string error_message =
1094 "Application quit is unavailable before editor startup";
1095 manager->AppendHarnessTestLog(captured_id, error_message);
1096 manager->MarkHarnessTestCompleted(
1097 captured_id, HarnessTestStatus::kFailed, error_message);
1098 return;
1099 }
1100
1101 editor_manager->Quit();
1102 if (!editor_manager->quit()) {
1103 const std::string error_message = absl::StrCat(
1104 "Application quit was blocked: ",
1105 editor_manager->GetPendingUnsavedSessionActionPrompt());
1106 manager->AppendHarnessTestLog(captured_id, error_message);
1107 manager->MarkHarnessTestCompleted(
1108 captured_id, HarnessTestStatus::kFailed, error_message);
1109 return;
1110 }
1111
1112 const std::string success_message = "Requested application quit";
1113 manager->AppendHarnessTestLog(captured_id, success_message);
1114 manager->MarkHarnessTestCompleted(
1115 captured_id, HarnessTestStatus::kPassed, success_message);
1116 return;
1117 } else {
1118 const std::string error_message = absl::StrFormat(
1119 "Unsupported harness shortcut '%s'", widget_label);
1120 manager->AppendHarnessTestLog(captured_id, error_message);
1121 manager->MarkHarnessTestCompleted(
1122 captured_id, HarnessTestStatus::kFailed, error_message);
1123 return;
1124 }
1125
1126 ctx->KeyPress(chord);
1127 ctx->Yield(2);
1128 if (save_picker && !save_picker->consumed()) {
1129 manager->MarkHarnessTestCompleted(
1130 captured_id, HarnessTestStatus::kFailed,
1131 "Project Save As shortcut did not reach the destination picker");
1132 return;
1133 }
1134 if (ctx->IsError()) {
1135 const std::string error_message =
1136 absl::StrFormat("Shortcut failed for '%s'", widget_label);
1137 manager->AppendHarnessTestLog(captured_id, error_message);
1138 manager->MarkHarnessTestCompleted(
1139 captured_id, HarnessTestStatus::kFailed, error_message);
1140 return;
1141 }
1142 const std::string success_message =
1143 absl::StrFormat("Pressed shortcut '%s'", widget_label);
1144 manager->AppendHarnessTestLog(captured_id, success_message);
1145 manager->MarkHarnessTestCompleted(
1146 captured_id, HarnessTestStatus::kPassed, success_message);
1147 return;
1148 }
1149
1150 const ImGuiTestRef widget_ref = widget_id != 0
1151 ? ImGuiTestRef(widget_id)
1152 : ImGuiTestRef(widget_label.c_str());
1153 const ImGuiTestItemInfo item =
1154 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1155 if (item.ID == 0) {
1156 const std::string error_message =
1157 absl::StrFormat("Click target '%s' not found", widget_label);
1158 manager->AppendHarnessTestLog(captured_id, error_message);
1159 manager->MarkHarnessTestCompleted(
1160 captured_id, HarnessTestStatus::kFailed, error_message);
1161 return;
1162 }
1163
1164 if (click_type == ClickRequest::CLICK_TYPE_DOUBLE) {
1165 ctx->ItemDoubleClick(ImGuiTestRef(item.ID));
1166 } else {
1167 ctx->ItemClick(ImGuiTestRef(item.ID), mouse_button);
1168 }
1169 ctx->Yield();
1170 if (ctx->IsError()) {
1171 const std::string error_message =
1172 absl::StrFormat("Click failed for '%s'", widget_label);
1173 manager->AppendHarnessTestLog(captured_id, error_message);
1174 manager->MarkHarnessTestCompleted(
1175 captured_id, HarnessTestStatus::kFailed, error_message);
1176 return;
1177 }
1178 const std::string success_message =
1179 absl::StrFormat("Clicked %s '%s'", widget_type, widget_label);
1180 manager->AppendHarnessTestLog(captured_id, success_message);
1181 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kPassed,
1182 success_message);
1183 } catch (const std::exception& e) {
1184 const std::string error_message =
1185 absl::StrFormat("Click failed: %s", e.what());
1186 manager->AppendHarnessTestLog(captured_id, error_message);
1187 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
1188 error_message);
1189 }
1190 };
1191
1192 std::string test_name = absl::StrFormat(
1193 "grpc_click_%lld",
1194 static_cast<long long>(
1195 std::chrono::system_clock::now().time_since_epoch().count()));
1196
1197 const auto queued = QueueDynamicTest(engine, test_manager_, ui_thread_queue_,
1198 test_name, test_id, test_data);
1199 if (!queued.ok()) {
1200 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1201 std::string(queued.message()));
1202 return queued;
1203 }
1204
1205 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1206 std::chrono::steady_clock::now() - start);
1207 std::string message =
1208 absl::StrFormat("Queued click on %s '%s'", widget_type, widget_label);
1209 response->set_success(true);
1210 response->set_message(message);
1211 response->set_execution_time_ms(elapsed.count());
1212 test_manager_->AppendHarnessTestLog(test_id, message);
1213
1214#else
1215 const std::string message = "ImGuiTestEngine is not available in this build";
1216 test_manager_->MarkHarnessTestRunning(test_id);
1217 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1218 message);
1219 test_manager_->AppendHarnessTestLog(test_id, message);
1220
1221 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1222 std::chrono::steady_clock::now() - start);
1223 response->set_success(false);
1224 response->set_message(message);
1225 response->set_execution_time_ms(elapsed.count());
1226#endif
1227
1228 return finalize(absl::OkStatus());
1229}
1230
1231absl::Status ImGuiTestHarnessServiceImpl::Type(const TypeRequest* request,
1232 TypeResponse* response) {
1233 auto start = std::chrono::steady_clock::now();
1234
1235 TestRecorder::RecordedStep recorded_step;
1236 recorded_step.type = TestRecorder::ActionType::kType;
1237 if (request) {
1238 recorded_step.widget_key = request->widget_key();
1239 if (!request->widget_key().empty()) {
1240 recorded_step.target = absl::StrCat("widget_key:", request->widget_key());
1241 } else {
1242 recorded_step.target = request->target();
1243 }
1244 recorded_step.text = request->text();
1245 recorded_step.clear_first = request->clear_first();
1246 }
1247
1248 auto finalize = [&](const absl::Status& status) {
1249 recorded_step.success = response->success();
1250 recorded_step.message = response->message();
1251 recorded_step.execution_time_ms = response->execution_time_ms();
1252 recorded_step.test_id = response->test_id();
1253 MaybeRecordStep(&test_recorder_, recorded_step);
1254 return status;
1255 };
1256
1257 if (!test_manager_) {
1258 response->set_success(false);
1259 response->set_message("TestManager not available");
1260 response->set_execution_time_ms(0);
1261 return finalize(absl::FailedPreconditionError("TestManager not available"));
1262 }
1263
1264 const std::string requested_target = request ? request->target() : "";
1265 const std::string requested_widget_key = request ? request->widget_key() : "";
1266 const std::string request_summary =
1267 !requested_widget_key.empty()
1268 ? absl::StrFormat("widget_key:%s", requested_widget_key)
1269 : requested_target;
1270
1271 const std::string test_id = test_manager_->RegisterHarnessTest(
1272 absl::StrFormat("Type: %s", request_summary), "grpc");
1273 response->set_test_id(test_id);
1274 recorded_step.test_id = test_id;
1275 test_manager_->AppendHarnessTestLog(
1276 test_id, absl::StrFormat("Queued type request: %s", request_summary));
1277
1278 absl::StatusOr<internal::ResolvedWidgetSelector> resolved_selector =
1279 internal::ResolveWidgetSelector(requested_target, requested_widget_key);
1280 if (!resolved_selector.ok()) {
1281 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1282 std::chrono::steady_clock::now() - start);
1283 std::string message = std::string(resolved_selector.status().message());
1284 response->set_success(false);
1285 response->set_message(message);
1286 response->set_execution_time_ms(elapsed.count());
1287 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1288 message);
1289 test_manager_->AppendHarnessTestLog(test_id, message);
1290 return finalize(absl::OkStatus());
1291 }
1292
1293 if (!resolved_selector->resolved_widget_key.empty()) {
1294 response->set_resolved_widget_key(resolved_selector->resolved_widget_key);
1295 }
1296 if (!resolved_selector->resolved_path.empty()) {
1297 response->set_resolved_path(resolved_selector->resolved_path);
1298 }
1299
1300 const std::string widget_type = resolved_selector->target.type;
1301 const std::string widget_label = resolved_selector->target.label;
1302 const ImGuiID widget_id = resolved_selector->imgui_id;
1303
1304#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1305 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
1306 if (!engine) {
1307 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1308 std::chrono::steady_clock::now() - start);
1309 std::string message = "ImGuiTestEngine not initialized";
1310 response->set_success(false);
1311 response->set_message(message);
1312 response->set_execution_time_ms(elapsed.count());
1313 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1314 message);
1315 return finalize(absl::OkStatus());
1316 }
1317 std::string text = request->text();
1318 bool clear_first = request->clear_first();
1319
1320 auto rpc_state = std::make_shared<RPCState<bool>>();
1321 auto test_data = std::make_shared<DynamicTestData>();
1322 TestManager* manager = test_manager_;
1323 test_data->test_func = [manager, captured_id = test_id, widget_type,
1324 widget_label, widget_id, clear_first, text,
1325 rpc_state](ImGuiTestContext* ctx) {
1326 manager->MarkHarnessTestRunning(captured_id);
1327 try {
1328 const ImGuiTestRef widget_ref = widget_id != 0
1329 ? ImGuiTestRef(widget_id)
1330 : ImGuiTestRef(widget_label.c_str());
1331 ImGuiTestItemInfo item =
1332 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1333 if (item.ID == 0) {
1334 std::string error_message =
1335 absl::StrFormat("Input field '%s' not found", widget_label);
1336 manager->AppendHarnessTestLog(captured_id, error_message);
1337 manager->MarkHarnessTestCompleted(
1338 captured_id, HarnessTestStatus::kFailed, error_message);
1339 rpc_state->SetResult(false, error_message);
1340 return;
1341 }
1342
1343 ctx->ItemClick(ImGuiTestRef(item.ID));
1344 if (clear_first) {
1345 ctx->KeyPress(ImGuiMod_Shortcut | ImGuiKey_A);
1346 ctx->KeyPress(ImGuiKey_Delete);
1347 }
1348
1349 ctx->ItemInputValue(ImGuiTestRef(item.ID), text.c_str());
1350 ctx->Yield();
1351 if (ctx->IsError()) {
1352 const std::string error_message =
1353 absl::StrFormat("Type failed for '%s'", widget_label);
1354 manager->AppendHarnessTestLog(captured_id, error_message);
1355 manager->MarkHarnessTestCompleted(
1356 captured_id, HarnessTestStatus::kFailed, error_message);
1357 rpc_state->SetResult(false, error_message);
1358 return;
1359 }
1360
1361 std::string success_message =
1362 absl::StrFormat("Typed '%s' into %s '%s'%s", text, widget_type,
1363 widget_label, clear_first ? " (cleared first)" : "");
1364 manager->AppendHarnessTestLog(captured_id, success_message);
1365 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kPassed,
1366 success_message);
1367 rpc_state->SetResult(true, success_message);
1368 } catch (const std::exception& e) {
1369 std::string error_message = absl::StrFormat("Type failed: %s", e.what());
1370 manager->AppendHarnessTestLog(captured_id, error_message);
1371 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
1372 error_message);
1373 rpc_state->SetResult(false, error_message);
1374 }
1375 };
1376
1377 std::string test_name = absl::StrFormat(
1378 "grpc_type_%lld",
1379 static_cast<long long>(
1380 std::chrono::system_clock::now().time_since_epoch().count()));
1381
1382 const auto queued = QueueDynamicTest(engine, test_manager_, ui_thread_queue_,
1383 test_name, test_id, test_data);
1384 if (!queued.ok()) {
1385 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1386 std::string(queued.message()));
1387 return queued;
1388 }
1389
1390 auto timeout = std::chrono::seconds(5);
1391 auto wait_start = std::chrono::steady_clock::now();
1392 while (!rpc_state->completed.load()) {
1393 if (std::chrono::steady_clock::now() - wait_start > timeout) {
1394 std::string error_message =
1395 "Test timeout - input field not found or unresponsive";
1396 manager->AppendHarnessTestLog(test_id, error_message);
1397 manager->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kTimeout,
1398 error_message);
1399 rpc_state->SetResult(false, error_message);
1400 break;
1401 }
1402 std::this_thread::sleep_for(std::chrono::milliseconds(100));
1403 }
1404
1405 bool success = false;
1406 std::string message;
1407 rpc_state->GetResult(success, message);
1408 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1409 std::chrono::steady_clock::now() - start);
1410
1411 response->set_success(success);
1412 response->set_message(message);
1413 response->set_execution_time_ms(elapsed.count());
1414 if (!message.empty()) {
1415 test_manager_->AppendHarnessTestLog(test_id, message);
1416 }
1417
1418#else
1419 test_manager_->MarkHarnessTestRunning(test_id);
1420 const std::string message = "ImGuiTestEngine is not available in this build";
1421 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1422 message);
1423 test_manager_->AppendHarnessTestLog(test_id, message);
1424
1425 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1426 std::chrono::steady_clock::now() - start);
1427 response->set_success(false);
1428 response->set_message(message);
1429 response->set_execution_time_ms(elapsed.count());
1430#endif
1431
1432 return finalize(absl::OkStatus());
1433}
1434
1435absl::Status ImGuiTestHarnessServiceImpl::Wait(const WaitRequest* request,
1436 WaitResponse* response) {
1437 auto start = std::chrono::steady_clock::now();
1438
1439 TestRecorder::RecordedStep recorded_step;
1440 recorded_step.type = TestRecorder::ActionType::kWait;
1441 if (request) {
1442 recorded_step.widget_key = request->widget_key();
1443 if (!request->widget_key().empty() && request->condition().empty()) {
1444 recorded_step.condition =
1445 absl::StrCat("widget_key:", request->widget_key());
1446 } else {
1447 recorded_step.condition = request->condition();
1448 }
1449 recorded_step.timeout_ms = request->timeout_ms();
1450 }
1451
1452 auto finalize = [&](const absl::Status& status) {
1453 recorded_step.success = response->success();
1454 recorded_step.message = response->message();
1455 recorded_step.execution_time_ms = response->elapsed_ms();
1456 recorded_step.test_id = response->test_id();
1457 MaybeRecordStep(&test_recorder_, recorded_step);
1458 return status;
1459 };
1460
1461 if (!test_manager_) {
1462 response->set_success(false);
1463 response->set_message("TestManager not available");
1464 response->set_elapsed_ms(0);
1465 return finalize(absl::FailedPreconditionError("TestManager not available"));
1466 }
1467
1468 const std::string requested_condition = request ? request->condition() : "";
1469 const std::string requested_widget_key = request ? request->widget_key() : "";
1470 const std::string request_summary =
1471 !requested_widget_key.empty()
1472 ? absl::StrFormat("%s [widget_key:%s]",
1473 requested_condition.empty()
1474 ? "element_visible:<auto>"
1475 : requested_condition,
1476 requested_widget_key)
1477 : requested_condition;
1478
1479 const std::string test_id = test_manager_->RegisterHarnessTest(
1480 absl::StrFormat("Wait: %s", request_summary), "grpc");
1481 response->set_test_id(test_id);
1482 recorded_step.test_id = test_id;
1483 test_manager_->AppendHarnessTestLog(
1484 test_id, absl::StrFormat("Queued wait condition: %s", request_summary));
1485
1486 internal::ResolvedWidgetSelector resolved_selector;
1487 absl::StatusOr<ParsedCondition> parsed_condition = ResolveCondition(
1488 requested_condition, requested_widget_key,
1489 /*default_type_for_widget=*/"element_visible", &resolved_selector);
1490 if (!parsed_condition.ok()) {
1491 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1492 std::chrono::steady_clock::now() - start);
1493 std::string message = std::string(parsed_condition.status().message());
1494 response->set_success(false);
1495 response->set_message(message);
1496 response->set_elapsed_ms(elapsed.count());
1497 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1498 message);
1499 test_manager_->AppendHarnessTestLog(test_id, message);
1500 return finalize(absl::OkStatus());
1501 }
1502
1503 if (!resolved_selector.resolved_widget_key.empty()) {
1504 response->set_resolved_widget_key(resolved_selector.resolved_widget_key);
1505 }
1506 if (!resolved_selector.resolved_path.empty()) {
1507 response->set_resolved_path(resolved_selector.resolved_path);
1508 }
1509
1510 const std::string condition_type = parsed_condition->type;
1511 const std::string condition_target = parsed_condition->target;
1512 const ImGuiID widget_id = resolved_selector.imgui_id;
1513 const std::string resolved_condition =
1514 absl::StrFormat("%s:%s", condition_type, condition_target);
1515
1516#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1517 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
1518 if (!engine) {
1519 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1520 std::chrono::steady_clock::now() - start);
1521 std::string message = "ImGuiTestEngine not initialized";
1522 response->set_success(false);
1523 response->set_message(message);
1524 response->set_elapsed_ms(elapsed.count());
1525 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1526 message);
1527 return finalize(absl::OkStatus());
1528 }
1529 int timeout_ms = request->timeout_ms() > 0 ? request->timeout_ms() : 5000;
1530 int poll_interval_ms =
1531 request->poll_interval_ms() > 0 ? request->poll_interval_ms() : 100;
1532
1533 auto test_data = std::make_shared<DynamicTestData>();
1534 TestManager* manager = test_manager_;
1535 test_data->test_func = [manager, captured_id = test_id, condition_type,
1536 condition_target, widget_id, timeout_ms,
1537 poll_interval_ms](ImGuiTestContext* ctx) {
1538 manager->MarkHarnessTestRunning(captured_id);
1539 auto poll_start = std::chrono::steady_clock::now();
1540 auto timeout = std::chrono::milliseconds(timeout_ms);
1541
1542 for (int i = 0; i < 10; ++i) {
1543 ctx->Yield();
1544 }
1545
1546 try {
1547 while (std::chrono::steady_clock::now() - poll_start < timeout) {
1548 bool current_state = false;
1549 const ImGuiTestRef widget_ref =
1550 widget_id != 0 ? ImGuiTestRef(widget_id)
1551 : ImGuiTestRef(condition_target.c_str());
1552
1553 if (condition_type == "window_visible") {
1554 ImGuiTestItemInfo window_info =
1555 ctx->WindowInfo(widget_ref, ImGuiTestOpFlags_NoError);
1556 current_state = (window_info.ID != 0);
1557 } else if (condition_type == "element_visible") {
1558 ImGuiTestItemInfo item =
1559 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1560 current_state = (item.ID != 0 && item.RectClipped.GetWidth() > 0 &&
1561 item.RectClipped.GetHeight() > 0);
1562 } else if (condition_type == "element_enabled") {
1563 ImGuiTestItemInfo item =
1564 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1565 current_state =
1566 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
1567 } else {
1568 std::string error_message =
1569 absl::StrFormat("Unknown condition type: %s", condition_type);
1570 manager->AppendHarnessTestLog(captured_id, error_message);
1571 manager->MarkHarnessTestCompleted(
1572 captured_id, HarnessTestStatus::kFailed, error_message);
1573 return;
1574 }
1575
1576 if (current_state) {
1577 auto elapsed_ms =
1578 std::chrono::duration_cast<std::chrono::milliseconds>(
1579 std::chrono::steady_clock::now() - poll_start);
1580 std::string success_message = absl::StrFormat(
1581 "Condition '%s:%s' met after %lld ms", condition_type,
1582 condition_target, static_cast<long long>(elapsed_ms.count()));
1583 manager->AppendHarnessTestLog(captured_id, success_message);
1584 manager->MarkHarnessTestCompleted(
1585 captured_id, HarnessTestStatus::kPassed, success_message);
1586 return;
1587 }
1588
1589 std::this_thread::sleep_for(
1590 std::chrono::milliseconds(poll_interval_ms));
1591 ctx->Yield();
1592 }
1593
1594 std::string timeout_message =
1595 absl::StrFormat("Condition '%s:%s' not met after %d ms timeout",
1596 condition_type, condition_target, timeout_ms);
1597 manager->AppendHarnessTestLog(captured_id, timeout_message);
1598 manager->MarkHarnessTestCompleted(
1599 captured_id, HarnessTestStatus::kTimeout, timeout_message);
1600 } catch (const std::exception& e) {
1601 std::string error_message = absl::StrFormat("Wait failed: %s", e.what());
1602 manager->AppendHarnessTestLog(captured_id, error_message);
1603 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
1604 error_message);
1605 }
1606 };
1607
1608 std::string test_name = absl::StrFormat(
1609 "grpc_wait_%lld",
1610 static_cast<long long>(
1611 std::chrono::system_clock::now().time_since_epoch().count()));
1612
1613 const auto queued = QueueDynamicTest(engine, test_manager_, ui_thread_queue_,
1614 test_name, test_id, test_data);
1615 if (!queued.ok()) {
1616 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1617 std::string(queued.message()));
1618 return queued;
1619 }
1620
1621 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1622 std::chrono::steady_clock::now() - start);
1623 std::string message =
1624 absl::StrFormat("Queued wait for '%s'", resolved_condition);
1625 response->set_success(true);
1626 response->set_message(message);
1627 response->set_elapsed_ms(elapsed.count());
1628 test_manager_->AppendHarnessTestLog(test_id, message);
1629
1630#else
1631 test_manager_->MarkHarnessTestRunning(test_id);
1632 const std::string message = "ImGuiTestEngine is not available in this build";
1633 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1634 message);
1635 test_manager_->AppendHarnessTestLog(test_id, message);
1636
1637 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1638 std::chrono::steady_clock::now() - start);
1639 response->set_success(false);
1640 response->set_message(message);
1641 response->set_elapsed_ms(elapsed.count());
1642#endif
1643
1644 return finalize(absl::OkStatus());
1645}
1646
1647absl::Status ImGuiTestHarnessServiceImpl::Assert(const AssertRequest* request,
1648 AssertResponse* response) {
1649 TestRecorder::RecordedStep recorded_step;
1650 recorded_step.type = TestRecorder::ActionType::kAssert;
1651 if (request) {
1652 recorded_step.widget_key = request->widget_key();
1653 if (!request->widget_key().empty() && request->condition().empty()) {
1654 recorded_step.condition =
1655 absl::StrCat("widget_key:", request->widget_key());
1656 } else {
1657 recorded_step.condition = request->condition();
1658 }
1659 }
1660
1661 auto finalize = [&](const absl::Status& status) {
1662 recorded_step.success = response->success();
1663 recorded_step.message = response->message();
1664 recorded_step.expected_value = response->expected_value();
1665 recorded_step.actual_value = response->actual_value();
1666 recorded_step.test_id = response->test_id();
1667 MaybeRecordStep(&test_recorder_, recorded_step);
1668 return status;
1669 };
1670
1671 if (!test_manager_) {
1672 response->set_success(false);
1673 response->set_message("TestManager not available");
1674 response->set_actual_value("N/A");
1675 response->set_expected_value("N/A");
1676 return finalize(absl::FailedPreconditionError("TestManager not available"));
1677 }
1678
1679 const std::string requested_condition = request ? request->condition() : "";
1680 const std::string requested_widget_key = request ? request->widget_key() : "";
1681 const std::string request_summary =
1682 !requested_widget_key.empty()
1683 ? absl::StrFormat("%s [widget_key:%s]",
1684 requested_condition.empty() ? "exists:<auto>"
1685 : requested_condition,
1686 requested_widget_key)
1687 : requested_condition;
1688
1689 const std::string test_id = test_manager_->RegisterHarnessTest(
1690 absl::StrFormat("Assert: %s", request_summary), "grpc");
1691 response->set_test_id(test_id);
1692 recorded_step.test_id = test_id;
1693 test_manager_->AppendHarnessTestLog(
1694 test_id, absl::StrFormat("Queued assertion: %s", request_summary));
1695
1696 internal::ResolvedWidgetSelector resolved_selector;
1697 absl::StatusOr<ParsedCondition> parsed_condition = ResolveCondition(
1698 requested_condition, requested_widget_key,
1699 /*default_type_for_widget=*/"exists", &resolved_selector);
1700 if (!parsed_condition.ok()) {
1701 std::string message = std::string(parsed_condition.status().message());
1702 response->set_success(false);
1703 response->set_message(message);
1704 response->set_actual_value("N/A");
1705 response->set_expected_value("N/A");
1706 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1707 message);
1708 test_manager_->AppendHarnessTestLog(test_id, message);
1709 return finalize(absl::OkStatus());
1710 }
1711
1712 if (!resolved_selector.resolved_widget_key.empty()) {
1713 response->set_resolved_widget_key(resolved_selector.resolved_widget_key);
1714 }
1715 if (!resolved_selector.resolved_path.empty()) {
1716 response->set_resolved_path(resolved_selector.resolved_path);
1717 }
1718
1719 const std::string assertion_type = parsed_condition->type;
1720 const std::string assertion_target = parsed_condition->target;
1721 const ImGuiID widget_id = resolved_selector.imgui_id;
1722 const std::string widget_label = resolved_selector.target.label;
1723
1724 if (assertion_type == "value_equals" && widget_id == 0) {
1725 const std::string message =
1726 "value_equals requires widget_key so the input is unambiguous";
1727 response->set_success(false);
1728 response->set_message(message);
1729 response->set_actual_value("N/A");
1730 response->set_expected_value(assertion_target);
1731 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1732 message);
1733 test_manager_->AppendHarnessTestLog(test_id, message);
1734 return finalize(absl::OkStatus());
1735 }
1736
1737#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1738 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
1739 if (!engine) {
1740 std::string message = "ImGuiTestEngine not initialized";
1741 response->set_success(false);
1742 response->set_message(message);
1743 response->set_actual_value("N/A");
1744 response->set_expected_value("N/A");
1745 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1746 message);
1747 return finalize(absl::OkStatus());
1748 }
1749
1750 auto test_data = std::make_shared<DynamicTestData>();
1751 TestManager* manager = test_manager_;
1752 test_data->test_func = [manager, captured_id = test_id, assertion_type,
1753 assertion_target, widget_id,
1754 widget_label](ImGuiTestContext* ctx) {
1755 manager->MarkHarnessTestRunning(captured_id);
1756
1757 auto complete_with = [manager, captured_id](bool passed,
1758 const std::string& message,
1759 const std::string& actual,
1760 const std::string& expected,
1761 HarnessTestStatus status) {
1762 manager->AppendHarnessTestLog(captured_id, message);
1763 if (!actual.empty() || !expected.empty()) {
1764 manager->AppendHarnessTestLog(
1765 captured_id,
1766 absl::StrFormat("Actual: %s | Expected: %s", actual, expected));
1767 }
1768 manager->MarkHarnessTestCompleted(captured_id, status,
1769 passed ? "" : message);
1770 };
1771
1772 try {
1773 bool passed = false;
1774 std::string actual_value;
1775 std::string expected_value;
1776 std::string message;
1777 const ImGuiTestRef widget_ref =
1778 widget_id != 0 ? ImGuiTestRef(widget_id)
1779 : ImGuiTestRef(assertion_target.c_str());
1780
1781 if (assertion_type == "visible") {
1782 const ImGuiTestItemInfo item =
1783 widget_id != 0
1784 ? ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError)
1785 : ctx->WindowInfo(widget_ref, ImGuiTestOpFlags_NoError);
1786 const bool is_visible =
1787 item.ID != 0 &&
1788 (widget_id == 0 || (item.RectClipped.GetWidth() > 0 &&
1789 item.RectClipped.GetHeight() > 0));
1790 passed = is_visible;
1791 actual_value = is_visible ? "visible" : "hidden";
1792 expected_value = "visible";
1793 const std::string target_description =
1794 widget_id != 0 ? widget_label : assertion_target;
1795 message =
1796 passed ? absl::StrFormat("'%s' is visible", target_description)
1797 : absl::StrFormat("'%s' is not visible", target_description);
1798 } else if (assertion_type == "enabled") {
1799 ImGuiTestItemInfo item =
1800 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1801 bool is_enabled =
1802 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
1803 passed = is_enabled;
1804 actual_value = is_enabled ? "enabled" : "disabled";
1805 expected_value = "enabled";
1806 message =
1807 passed ? absl::StrFormat("'%s' is enabled", assertion_target)
1808 : absl::StrFormat("'%s' is not enabled", assertion_target);
1809 } else if (assertion_type == "exists") {
1810 ImGuiTestItemInfo item =
1811 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1812 bool exists = (item.ID != 0);
1813 passed = exists;
1814 actual_value = exists ? "exists" : "not found";
1815 expected_value = "exists";
1816 message = passed ? absl::StrFormat("'%s' exists", assertion_target)
1817 : absl::StrFormat("'%s' not found", assertion_target);
1818 } else if (assertion_type == "value_equals") {
1819 const ImGuiTestItemInfo item =
1820 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1821 if (item.ID == 0) {
1822 passed = false;
1823 actual_value = "not found";
1824 expected_value = assertion_target;
1825 message = absl::StrFormat("Input '%s' not found", widget_label);
1826 } else {
1827 const char* value = ctx->ItemReadAsString(ImGuiTestRef(item.ID));
1828 actual_value = value ? value : "";
1829 expected_value = assertion_target;
1830 passed = !ctx->IsError() && actual_value == expected_value;
1831 message = passed ? absl::StrFormat("'%s' equals '%s'", widget_label,
1832 expected_value)
1833 : absl::StrFormat(
1834 "'%s' does not equal '%s' (actual: '%s')",
1835 widget_label, expected_value, actual_value);
1836 }
1837 } else if (assertion_type == "text_contains") {
1838 size_t second_colon = assertion_target.find(':');
1839 if (widget_id == 0 && second_colon == std::string::npos) {
1840 std::string error_message =
1841 "text_contains requires format "
1842 "'text_contains:target:expected_text'";
1843 complete_with(false, error_message, "N/A", "N/A",
1844 HarnessTestStatus::kFailed);
1845 return;
1846 }
1847
1848 const std::string input_target =
1849 widget_id != 0 ? widget_label
1850 : assertion_target.substr(0, second_colon);
1851 const std::string expected_text =
1852 widget_id != 0 ? assertion_target
1853 : assertion_target.substr(second_colon + 1);
1854 const ImGuiTestRef input_ref = widget_id != 0
1855 ? ImGuiTestRef(widget_id)
1856 : ImGuiTestRef(input_target.c_str());
1857
1858 ImGuiTestItemInfo item =
1859 ctx->ItemInfo(input_ref, ImGuiTestOpFlags_NoError);
1860 if (item.ID != 0) {
1861 const char* value = ctx->ItemReadAsString(ImGuiTestRef(item.ID));
1862 const std::string actual_text = value ? value : "";
1863 passed = actual_text.find(expected_text) != std::string::npos;
1864 actual_value = actual_text;
1865 expected_value = absl::StrFormat("contains '%s'", expected_text);
1866 message = passed ? absl::StrFormat("'%s' contains '%s'", input_target,
1867 expected_text)
1868 : absl::StrFormat(
1869 "'%s' does not contain '%s' (actual: '%s')",
1870 input_target, expected_text, actual_text);
1871 } else {
1872 passed = false;
1873 actual_value = "not found";
1874 expected_value = expected_text;
1875 message = absl::StrFormat("Input '%s' not found", input_target);
1876 }
1877 } else {
1878 std::string error_message =
1879 absl::StrFormat("Unknown assertion type: %s", assertion_type);
1880 complete_with(false, error_message, "N/A", "N/A",
1881 HarnessTestStatus::kFailed);
1882 return;
1883 }
1884
1885 complete_with(
1886 passed, message, actual_value, expected_value,
1887 passed ? HarnessTestStatus::kPassed : HarnessTestStatus::kFailed);
1888 } catch (const std::exception& e) {
1889 std::string error_message =
1890 absl::StrFormat("Assertion failed: %s", e.what());
1891 manager->AppendHarnessTestLog(captured_id, error_message);
1892 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
1893 error_message);
1894 }
1895 };
1896
1897 std::string test_name = absl::StrFormat(
1898 "grpc_assert_%lld",
1899 static_cast<long long>(
1900 std::chrono::system_clock::now().time_since_epoch().count()));
1901
1902 const auto queued = QueueDynamicTest(engine, test_manager_, ui_thread_queue_,
1903 test_name, test_id, test_data);
1904 if (!queued.ok()) {
1905 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1906 std::string(queued.message()));
1907 return queued;
1908 }
1909
1910 response->set_success(true);
1911 std::string message = absl::StrFormat("Queued assertion for '%s:%s'",
1912 assertion_type, assertion_target);
1913 response->set_message(message);
1914 response->set_actual_value("(async)");
1915 response->set_expected_value("(async)");
1916 test_manager_->AppendHarnessTestLog(test_id, message);
1917
1918#else
1919 test_manager_->MarkHarnessTestRunning(test_id);
1920 const std::string message = "ImGuiTestEngine is not available in this build";
1921 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1922 message);
1923 test_manager_->AppendHarnessTestLog(test_id, message);
1924
1925 response->set_success(false);
1926 response->set_message(message);
1927 response->set_actual_value("(stub)");
1928 response->set_expected_value("(stub)");
1929#endif
1930
1931 return finalize(absl::OkStatus());
1932}
1933
1934absl::Status ImGuiTestHarnessServiceImpl::FlushUiActions(
1935 const FlushUiActionsRequest* request, FlushUiActionsResponse* response) {
1936 if (!response) {
1937 return absl::InvalidArgumentError("response cannot be null");
1938 }
1939
1940 const int32_t timeout_ms =
1941 NormalizeTimeoutMs(request ? request->timeout_ms() : 0, 2000);
1942 const absl::Duration timeout = absl::Milliseconds(timeout_ms);
1943 const absl::Time deadline = absl::Now() + timeout;
1944 const absl::Time started_at = absl::Now();
1945
1946 UiSyncSnapshot initial = CaptureUiSyncSnapshot(test_manager_);
1947 UiSyncSnapshot snapshot = initial;
1948
1949 if (initial.pending_editor_actions == 0 &&
1950 initial.pending_layout_actions == 0 && !initial.layout_rebuild_pending) {
1951 response->set_success(true);
1952 response->set_message("UI queues already idle");
1953 response->set_elapsed_ms(0);
1954 response->set_frame_id(initial.frame_id);
1955 response->set_pending_editor_actions(initial.pending_editor_actions);
1956 response->set_pending_layout_actions(initial.pending_layout_actions);
1957 response->set_layout_rebuild_pending(initial.layout_rebuild_pending);
1958 response->set_pending_harness_tests(initial.pending_harness_tests);
1959 response->set_queued_harness_tests(initial.queued_harness_tests);
1960 response->set_running_harness_tests(initial.running_harness_tests);
1961 return absl::OkStatus();
1962 }
1963
1964 while (absl::Now() <= deadline) {
1965 snapshot = CaptureUiSyncSnapshot(test_manager_);
1966 const bool frame_advanced = snapshot.frame_id > initial.frame_id;
1967 const bool queues_flushed = snapshot.pending_editor_actions == 0 &&
1968 snapshot.pending_layout_actions == 0 &&
1969 !snapshot.layout_rebuild_pending;
1970 if (frame_advanced && queues_flushed) {
1971 break;
1972 }
1973 absl::SleepFor(absl::Milliseconds(10));
1974 }
1975
1976 const int32_t elapsed_ms = ClampDurationToInt32(absl::Now() - started_at);
1977 const bool success = snapshot.pending_editor_actions == 0 &&
1978 snapshot.pending_layout_actions == 0 &&
1979 !snapshot.layout_rebuild_pending;
1980 response->set_success(success);
1981 response->set_message(success ? "Deferred UI actions flushed"
1982 : "Timed out waiting for UI action flush");
1983 response->set_elapsed_ms(elapsed_ms);
1984 response->set_frame_id(snapshot.frame_id);
1985 response->set_pending_editor_actions(snapshot.pending_editor_actions);
1986 response->set_pending_layout_actions(snapshot.pending_layout_actions);
1987 response->set_layout_rebuild_pending(snapshot.layout_rebuild_pending);
1988 response->set_pending_harness_tests(snapshot.pending_harness_tests);
1989 response->set_queued_harness_tests(snapshot.queued_harness_tests);
1990 response->set_running_harness_tests(snapshot.running_harness_tests);
1991 return absl::OkStatus();
1992}
1993
1994absl::Status ImGuiTestHarnessServiceImpl::WaitForIdle(
1995 const WaitForIdleRequest* request, WaitForIdleResponse* response) {
1996 if (!response) {
1997 return absl::InvalidArgumentError("response cannot be null");
1998 }
1999
2000 const int32_t timeout_ms =
2001 NormalizeTimeoutMs(request ? request->timeout_ms() : 0, 5000);
2002 const int32_t required_stable_frames =
2003 std::max(1, request ? request->stable_frames() : 2);
2004 const int32_t poll_interval_ms =
2005 NormalizePollIntervalMs(request ? request->poll_interval_ms() : 25);
2006 const bool flush_first = request ? request->flush_first() : false;
2007
2008 const absl::Time started_at = absl::Now();
2009 const absl::Time deadline = started_at + absl::Milliseconds(timeout_ms);
2010
2011 if (flush_first) {
2012 FlushUiActionsRequest flush_request;
2013 flush_request.set_timeout_ms(std::min(timeout_ms, 2000));
2014 FlushUiActionsResponse flush_response;
2015 absl::Status flush_status = FlushUiActions(&flush_request, &flush_response);
2016 if (!flush_status.ok()) {
2017 return flush_status;
2018 }
2019 }
2020
2021 UiSyncSnapshot snapshot = CaptureUiSyncSnapshot(test_manager_);
2022 int32_t stable_frames_observed = 0;
2023 int64_t last_frame_id = -1;
2024
2025 while (absl::Now() <= deadline) {
2026 snapshot = CaptureUiSyncSnapshot(test_manager_);
2027
2028 const bool is_idle_now = IsUiIdle(snapshot);
2029 if (snapshot.frame_id != last_frame_id) {
2030 last_frame_id = snapshot.frame_id;
2031 stable_frames_observed = is_idle_now ? (stable_frames_observed + 1) : 0;
2032 } else if (!is_idle_now) {
2033 stable_frames_observed = 0;
2034 }
2035
2036 if (stable_frames_observed >= required_stable_frames) {
2037 response->set_success(true);
2038 response->set_message(absl::StrFormat(
2039 "UI idle for %d consecutive frame(s)", stable_frames_observed));
2040 response->set_elapsed_ms(ClampDurationToInt32(absl::Now() - started_at));
2041 response->set_last_frame_id(snapshot.frame_id);
2042 response->set_stable_frames_observed(stable_frames_observed);
2043 response->set_pending_editor_actions(snapshot.pending_editor_actions);
2044 response->set_pending_layout_actions(snapshot.pending_layout_actions);
2045 response->set_layout_rebuild_pending(snapshot.layout_rebuild_pending);
2046 response->set_pending_harness_tests(snapshot.pending_harness_tests);
2047 response->set_queued_harness_tests(snapshot.queued_harness_tests);
2048 response->set_running_harness_tests(snapshot.running_harness_tests);
2049 response->set_timeout_reason("");
2050 return absl::OkStatus();
2051 }
2052
2053 absl::SleepFor(absl::Milliseconds(poll_interval_ms));
2054 }
2055
2056 const std::string timeout_reason = BuildIdleTimeoutReason(snapshot);
2057 response->set_success(false);
2058 response->set_message("Timed out waiting for deterministic UI idle");
2059 response->set_elapsed_ms(ClampDurationToInt32(absl::Now() - started_at));
2060 response->set_last_frame_id(snapshot.frame_id);
2061 response->set_stable_frames_observed(stable_frames_observed);
2062 response->set_pending_editor_actions(snapshot.pending_editor_actions);
2063 response->set_pending_layout_actions(snapshot.pending_layout_actions);
2064 response->set_layout_rebuild_pending(snapshot.layout_rebuild_pending);
2065 response->set_pending_harness_tests(snapshot.pending_harness_tests);
2066 response->set_queued_harness_tests(snapshot.queued_harness_tests);
2067 response->set_running_harness_tests(snapshot.running_harness_tests);
2068 response->set_timeout_reason(timeout_reason);
2069 return absl::OkStatus();
2070}
2071
2072absl::Status ImGuiTestHarnessServiceImpl::GetUiSyncState(
2073 const GetUiSyncStateRequest* /*request*/,
2074 GetUiSyncStateResponse* response) {
2075 if (!response) {
2076 return absl::InvalidArgumentError("response cannot be null");
2077 }
2078
2079 const UiSyncSnapshot snapshot = CaptureUiSyncSnapshot(test_manager_);
2080 response->set_frame_id(snapshot.frame_id);
2081 response->set_pending_editor_actions(snapshot.pending_editor_actions);
2082 response->set_pending_layout_actions(snapshot.pending_layout_actions);
2083 response->set_layout_rebuild_pending(snapshot.layout_rebuild_pending);
2084 response->set_pending_harness_tests(snapshot.pending_harness_tests);
2085 response->set_queued_harness_tests(snapshot.queued_harness_tests);
2086 response->set_running_harness_tests(snapshot.running_harness_tests);
2087 response->set_sampled_at_ms(absl::ToUnixMillis(absl::Now()));
2088 response->set_idle(IsUiIdle(snapshot));
2089 return absl::OkStatus();
2090}
2091
2092absl::Status ImGuiTestHarnessServiceImpl::Screenshot(
2093 const ScreenshotRequest* request, ScreenshotResponse* response) {
2094 if (!response) {
2095 return absl::InvalidArgumentError("response cannot be null");
2096 }
2097 response->Clear();
2098
2099 const std::string requested_path =
2100 request ? request->output_path() : std::string();
2101 ScreenshotFormat format = ScreenshotFormat::kAuto;
2102 if (request) {
2103 switch (request->format()) {
2104 case ScreenshotRequest::IMAGE_FORMAT_UNSPECIFIED:
2105 break;
2106 case ScreenshotRequest::IMAGE_FORMAT_PNG:
2107 format = ScreenshotFormat::kPng;
2108 break;
2109 case ScreenshotRequest::IMAGE_FORMAT_BMP:
2110 format = ScreenshotFormat::kBmp;
2111 break;
2112 case ScreenshotRequest::IMAGE_FORMAT_JPEG:
2113 return absl::UnimplementedError(
2114 "JPEG screenshots are not supported; request PNG or BMP");
2115 default:
2116 return absl::InvalidArgumentError("Unknown screenshot image format");
2117 }
2118 }
2119 auto format_or = ResolveScreenshotFormat(requested_path, format);
2120 if (!format_or.ok()) {
2121 return format_or.status();
2122 }
2123 auto result = CaptureScreenshotOnRenderThread(
2124 requested_path, request ? request->window_title() : std::string(),
2125 *format_or);
2126 if (absl::IsDeadlineExceeded(result.status())) {
2127 return result.status();
2128 }
2129 if (!result.ok()) {
2130 response->set_success(false);
2131 response->set_message(std::string(result.status().message()));
2132 return result.status();
2133 }
2134
2135 const ScreenshotArtifact& artifact = *result;
2136 response->set_success(true);
2137 response->set_message(absl::StrFormat("Screenshot saved to %s (%dx%d)",
2138 artifact.file_path, artifact.width,
2139 artifact.height));
2140 response->set_file_path(artifact.file_path);
2141 response->set_file_size_bytes(artifact.file_size_bytes);
2142
2143 return absl::OkStatus();
2144}
2145
2146absl::Status ImGuiTestHarnessServiceImpl::GetTestStatus(
2147 const GetTestStatusRequest* request, GetTestStatusResponse* response) {
2148 if (!test_manager_) {
2149 return absl::FailedPreconditionError("TestManager not available");
2150 }
2151
2152 if (request->test_id().empty()) {
2153 return absl::InvalidArgumentError("test_id must be provided");
2154 }
2155
2156 auto execution_or =
2157 test_manager_->GetHarnessTestExecution(request->test_id());
2158 if (!execution_or.ok()) {
2159 response->set_status(GetTestStatusResponse::TEST_STATUS_UNSPECIFIED);
2160 response->set_error_message(std::string(execution_or.status().message()));
2161 return absl::OkStatus();
2162 }
2163
2164 const auto& execution = execution_or.value();
2165 response->set_status(ConvertHarnessStatus(execution.status));
2166 response->set_queued_at_ms(ToUnixMillisSafe(execution.queued_at));
2167 response->set_started_at_ms(ToUnixMillisSafe(execution.started_at));
2168 response->set_completed_at_ms(ToUnixMillisSafe(execution.completed_at));
2169 response->set_execution_time_ms(ClampDurationToInt32(execution.duration));
2170 if (!execution.error_message.empty()) {
2171 response->set_error_message(execution.error_message);
2172 } else {
2173 response->clear_error_message();
2174 }
2175
2176 response->clear_assertion_failures();
2177 for (const auto& failure : execution.assertion_failures) {
2178 response->add_assertion_failures(failure);
2179 }
2180
2181 return absl::OkStatus();
2182}
2183
2184absl::Status ImGuiTestHarnessServiceImpl::ListTests(
2185 const ListTestsRequest* request, ListTestsResponse* response) {
2186 if (!test_manager_) {
2187 return absl::FailedPreconditionError("TestManager not available");
2188 }
2189
2190 if (request->page_size() < 0) {
2191 return absl::InvalidArgumentError("page_size cannot be negative");
2192 }
2193
2194 int page_size = request->page_size() > 0 ? request->page_size() : 100;
2195 constexpr int kMaxPageSize = 500;
2196 if (page_size > kMaxPageSize) {
2197 page_size = kMaxPageSize;
2198 }
2199
2200 size_t start_index = 0;
2201 if (!request->page_token().empty()) {
2202 int64_t token_value = 0;
2203 if (!absl::SimpleAtoi(request->page_token(), &token_value) ||
2204 token_value < 0) {
2205 return absl::InvalidArgumentError("Invalid page_token");
2206 }
2207 start_index = static_cast<size_t>(token_value);
2208 }
2209
2210 auto summaries =
2211 test_manager_->ListHarnessTestSummaries(request->category_filter());
2212
2213 response->set_total_count(static_cast<int32_t>(summaries.size()));
2214
2215 if (start_index >= summaries.size()) {
2216 response->clear_tests();
2217 response->clear_next_page_token();
2218 return absl::OkStatus();
2219 }
2220
2221 size_t end_index =
2222 std::min(start_index + static_cast<size_t>(page_size), summaries.size());
2223
2224 for (size_t i = start_index; i < end_index; ++i) {
2225 const auto& summary = summaries[i];
2226 auto* test_info = response->add_tests();
2227 const auto& exec = summary.latest_execution;
2228
2229 test_info->set_test_id(exec.test_id);
2230 test_info->set_name(exec.name);
2231 test_info->set_category(exec.category);
2232
2233 int64_t last_run_ms = ToUnixMillisSafe(exec.completed_at);
2234 if (last_run_ms == 0) {
2235 last_run_ms = ToUnixMillisSafe(exec.started_at);
2236 }
2237 if (last_run_ms == 0) {
2238 last_run_ms = ToUnixMillisSafe(exec.queued_at);
2239 }
2240 test_info->set_last_run_timestamp_ms(last_run_ms);
2241
2242 test_info->set_total_runs(summary.total_runs);
2243 test_info->set_pass_count(summary.pass_count);
2244 test_info->set_fail_count(summary.fail_count);
2245
2246 int32_t average_duration_ms = 0;
2247 if (summary.total_runs > 0) {
2248 absl::Duration average_duration =
2249 summary.total_duration / summary.total_runs;
2250 average_duration_ms = ClampDurationToInt32(average_duration);
2251 }
2252 test_info->set_average_duration_ms(average_duration_ms);
2253 }
2254
2255 if (end_index < summaries.size()) {
2256 response->set_next_page_token(absl::StrCat(end_index));
2257 } else {
2258 response->clear_next_page_token();
2259 }
2260
2261 return absl::OkStatus();
2262}
2263
2264absl::Status ImGuiTestHarnessServiceImpl::GetTestResults(
2265 const GetTestResultsRequest* request, GetTestResultsResponse* response) {
2266 if (!test_manager_) {
2267 return absl::FailedPreconditionError("TestManager not available");
2268 }
2269
2270 if (request->test_id().empty()) {
2271 return absl::InvalidArgumentError("test_id must be provided");
2272 }
2273
2274 auto execution_or =
2275 test_manager_->GetHarnessTestExecution(request->test_id());
2276 if (!execution_or.ok()) {
2277 return execution_or.status();
2278 }
2279
2280 const auto& execution = execution_or.value();
2281 response->set_success(execution.status == HarnessTestStatus::kPassed);
2282 response->set_test_name(execution.name);
2283 response->set_category(execution.category);
2284
2285 int64_t executed_at_ms = ToUnixMillisSafe(execution.completed_at);
2286 if (executed_at_ms == 0) {
2287 executed_at_ms = ToUnixMillisSafe(execution.started_at);
2288 }
2289 if (executed_at_ms == 0) {
2290 executed_at_ms = ToUnixMillisSafe(execution.queued_at);
2291 }
2292 response->set_executed_at_ms(executed_at_ms);
2293 response->set_duration_ms(ClampDurationToInt32(execution.duration));
2294
2295 response->clear_assertions();
2296 if (!execution.assertion_failures.empty()) {
2297 for (const auto& failure : execution.assertion_failures) {
2298 auto* assertion = response->add_assertions();
2299 assertion->set_description(failure);
2300 assertion->set_passed(false);
2301 assertion->set_error_message(failure);
2302 }
2303 } else if (!execution.error_message.empty()) {
2304 auto* assertion = response->add_assertions();
2305 assertion->set_description("Execution error");
2306 assertion->set_passed(false);
2307 assertion->set_error_message(execution.error_message);
2308 }
2309
2310 if (request->include_logs()) {
2311 for (const auto& log_entry : execution.logs) {
2312 response->add_logs(log_entry);
2313 }
2314 }
2315
2316 auto* metrics_map = response->mutable_metrics();
2317 for (const auto& [key, value] : execution.metrics) {
2318 (*metrics_map)[key] = value;
2319 }
2320
2321 // IT-08b: Include failure diagnostics if available
2322 if (!execution.screenshot_path.empty()) {
2323 response->set_screenshot_path(execution.screenshot_path);
2324 response->set_screenshot_size_bytes(execution.screenshot_size_bytes);
2325 }
2326 if (!execution.failure_context.empty()) {
2327 response->set_failure_context(execution.failure_context);
2328 }
2329 if (!execution.widget_state.empty()) {
2330 response->set_widget_state(execution.widget_state);
2331 }
2332
2333 return absl::OkStatus();
2334}
2335
2336absl::Status ImGuiTestHarnessServiceImpl::DiscoverWidgets(
2337 const DiscoverWidgetsRequest* request, DiscoverWidgetsResponse* response) {
2338 if (!request) {
2339 return absl::InvalidArgumentError("request cannot be null");
2340 }
2341 if (!response) {
2342 return absl::InvalidArgumentError("response cannot be null");
2343 }
2344
2345 if (!test_manager_) {
2346 return absl::FailedPreconditionError("TestManager not available");
2347 }
2348
2349 widget_discovery_service_.CollectWidgets(/*ctx=*/nullptr, *request, response);
2350 return absl::OkStatus();
2351}
2352
2353absl::Status ImGuiTestHarnessServiceImpl::StartRecording(
2354 const StartRecordingRequest* request, StartRecordingResponse* response) {
2355 if (!request) {
2356 return absl::InvalidArgumentError("request cannot be null");
2357 }
2358 if (!response) {
2359 return absl::InvalidArgumentError("response cannot be null");
2360 }
2361
2362 TestRecorder::RecordingOptions options;
2363 options.output_path = request->output_path();
2364 options.session_name = request->session_name();
2365 options.description = request->description();
2366
2367 if (options.output_path.empty()) {
2368 response->set_success(false);
2369 response->set_message("output_path is required to start recording");
2370 return absl::InvalidArgumentError("output_path cannot be empty");
2371 }
2372
2373 absl::StatusOr<std::string> recording_id = test_recorder_.Start(options);
2374 if (!recording_id.ok()) {
2375 response->set_success(false);
2376 response->set_message(std::string(recording_id.status().message()));
2377 return recording_id.status();
2378 }
2379
2380 response->set_success(true);
2381 response->set_message("Recording started");
2382 response->set_recording_id(*recording_id);
2383 response->set_started_at_ms(absl::ToUnixMillis(absl::Now()));
2384 return absl::OkStatus();
2385}
2386
2387absl::Status ImGuiTestHarnessServiceImpl::StopRecording(
2388 const StopRecordingRequest* request, StopRecordingResponse* response) {
2389 if (!request) {
2390 return absl::InvalidArgumentError("request cannot be null");
2391 }
2392 if (!response) {
2393 return absl::InvalidArgumentError("response cannot be null");
2394 }
2395
2396 absl::StatusOr<TestRecorder::StopRecordingSummary> summary =
2397 test_recorder_.Stop(request->recording_id(), request->discard());
2398 if (!summary.ok()) {
2399 response->set_success(false);
2400 response->set_message(std::string(summary.status().message()));
2401 return summary.status();
2402 }
2403
2404 response->set_success(true);
2405 if (summary->saved) {
2406 response->set_message("Recording saved");
2407 } else {
2408 response->set_message("Recording discarded");
2409 }
2410 response->set_output_path(summary->output_path);
2411 response->set_step_count(summary->step_count);
2412 response->set_duration_ms(absl::ToInt64Milliseconds(summary->duration));
2413 return absl::OkStatus();
2414}
2415
2416absl::Status ImGuiTestHarnessServiceImpl::ReplayTest(
2417 const ReplayTestRequest* request, ReplayTestResponse* response) {
2418 if (!request) {
2419 return absl::InvalidArgumentError("request cannot be null");
2420 }
2421 if (!response) {
2422 return absl::InvalidArgumentError("response cannot be null");
2423 }
2424
2425 response->clear_logs();
2426 response->clear_assertions();
2427
2428 if (request->script_path().empty()) {
2429 response->set_success(false);
2430 response->set_message("script_path is required");
2431 return absl::InvalidArgumentError("script_path cannot be empty");
2432 }
2433
2434 absl::StatusOr<TestScript> script_or =
2435 TestScriptParser::ParseFromFile(request->script_path());
2436 if (!script_or.ok()) {
2437 response->set_success(false);
2438 response->set_message(std::string(script_or.status().message()));
2439 return script_or.status();
2440 }
2441 TestScript script = std::move(*script_or);
2442
2443 absl::flat_hash_map<std::string, std::string> overrides;
2444 for (const auto& entry : request->parameter_overrides()) {
2445 overrides[entry.first] = entry.second;
2446 }
2447
2448 response->set_replay_session_id(absl::StrFormat(
2449 "replay_%s",
2450 absl::FormatTime("%Y%m%dT%H%M%S", absl::Now(), absl::UTCTimeZone())));
2451
2452 auto suspension = test_recorder_.Suspend();
2453#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
2454 ScopedRemoteReplaySubmission remote_submission;
2455#endif
2456
2457 std::vector<std::string> logs;
2458 logs.reserve(script.steps.size() * 2 + 4);
2459
2460 bool overall_success = true;
2461 std::string overall_message = "Replay completed successfully";
2462 int steps_executed = 0;
2463 absl::Status overall_rpc_status = absl::OkStatus();
2464
2465 for (const auto& step : script.steps) {
2466 ++steps_executed;
2467 std::string action_label =
2468 absl::StrFormat("Step %d: %s", steps_executed, step.action);
2469 logs.push_back(action_label);
2470
2471 absl::Status status = absl::OkStatus();
2472 bool step_success = false;
2473 std::string step_message;
2474 HarnessTestExecution execution;
2475 bool have_execution = false;
2476
2477 if (step.action == "click") {
2478 ClickRequest sub_request;
2479 sub_request.set_target(ApplyOverrides(step.target, overrides));
2480 sub_request.set_widget_key(ApplyOverrides(step.widget_key, overrides));
2481 sub_request.set_type(ClickTypeFromString(step.click_type));
2482 ClickResponse sub_response;
2483 status = Click(&sub_request, &sub_response);
2484 step_success = sub_response.success();
2485 step_message = sub_response.message();
2486 if (status.ok() && !sub_response.test_id().empty()) {
2487 absl::Status wait_status = WaitForHarnessTestCompletion(
2488 test_manager_, sub_response.test_id(), &execution);
2489 if (wait_status.ok()) {
2490 have_execution = true;
2491 internal::ApplyTerminalHarnessExecution(execution, &step_success,
2492 &step_message);
2493 } else {
2494 status = wait_status;
2495 step_success = false;
2496 step_message = std::string(wait_status.message());
2497 }
2498 }
2499 } else if (step.action == "type") {
2500 TypeRequest sub_request;
2501 sub_request.set_target(ApplyOverrides(step.target, overrides));
2502 sub_request.set_widget_key(ApplyOverrides(step.widget_key, overrides));
2503 sub_request.set_text(ApplyOverrides(step.text, overrides));
2504 sub_request.set_clear_first(step.clear_first);
2505 TypeResponse sub_response;
2506 status = Type(&sub_request, &sub_response);
2507 step_success = sub_response.success();
2508 step_message = sub_response.message();
2509 if (status.ok() && !sub_response.test_id().empty()) {
2510 absl::Status wait_status = WaitForHarnessTestCompletion(
2511 test_manager_, sub_response.test_id(), &execution);
2512 if (wait_status.ok()) {
2513 have_execution = true;
2514 internal::ApplyTerminalHarnessExecution(execution, &step_success,
2515 &step_message);
2516 } else {
2517 status = wait_status;
2518 step_success = false;
2519 step_message = std::string(wait_status.message());
2520 }
2521 }
2522 } else if (step.action == "wait") {
2523 WaitRequest sub_request;
2524 sub_request.set_condition(ApplyOverrides(step.condition, overrides));
2525 sub_request.set_widget_key(ApplyOverrides(step.widget_key, overrides));
2526 if (step.timeout_ms > 0) {
2527 sub_request.set_timeout_ms(step.timeout_ms);
2528 }
2529 WaitResponse sub_response;
2530 status = Wait(&sub_request, &sub_response);
2531 step_success = sub_response.success();
2532 step_message = sub_response.message();
2533 if (status.ok() && !sub_response.test_id().empty()) {
2534 absl::Status wait_status = WaitForHarnessTestCompletion(
2535 test_manager_, sub_response.test_id(), &execution);
2536 if (wait_status.ok()) {
2537 have_execution = true;
2538 internal::ApplyTerminalHarnessExecution(execution, &step_success,
2539 &step_message);
2540 } else {
2541 status = wait_status;
2542 step_success = false;
2543 step_message = std::string(wait_status.message());
2544 }
2545 }
2546 } else if (step.action == "assert") {
2547 AssertRequest sub_request;
2548 sub_request.set_condition(ApplyOverrides(step.condition, overrides));
2549 sub_request.set_widget_key(ApplyOverrides(step.widget_key, overrides));
2550 AssertResponse sub_response;
2551 status = Assert(&sub_request, &sub_response);
2552 step_success = sub_response.success();
2553 step_message = sub_response.message();
2554 if (status.ok() && !sub_response.test_id().empty()) {
2555 absl::Status wait_status = WaitForHarnessTestCompletion(
2556 test_manager_, sub_response.test_id(), &execution);
2557 if (wait_status.ok()) {
2558 have_execution = true;
2559 internal::ApplyTerminalHarnessExecution(execution, &step_success,
2560 &step_message);
2561 } else {
2562 status = wait_status;
2563 step_success = false;
2564 step_message = std::string(wait_status.message());
2565 }
2566 }
2567 } else if (step.action == "screenshot") {
2568 ScreenshotRequest sub_request;
2569 sub_request.set_window_title(ApplyOverrides(step.target, overrides));
2570 if (!step.region.empty()) {
2571 sub_request.set_output_path(ApplyOverrides(step.region, overrides));
2572 }
2573 ScreenshotResponse sub_response;
2574 status = Screenshot(&sub_request, &sub_response);
2575 step_success = sub_response.success();
2576 step_message = sub_response.message();
2577 } else {
2578 status = absl::InvalidArgumentError(
2579 absl::StrFormat("Unsupported action '%s'", step.action));
2580 step_message =
2581 std::string(status.message().data(), status.message().size());
2582 }
2583
2584 auto* assertion = response->add_assertions();
2585 assertion->set_description(
2586 absl::StrFormat("Step %d (%s)", steps_executed, step.action));
2587
2588 if (!status.ok()) {
2589 assertion->set_passed(false);
2590 assertion->set_error_message(
2591 std::string(status.message().data(), status.message().size()));
2592 overall_success = false;
2593 overall_message = step_message;
2594 logs.push_back(absl::StrFormat(" Error: %s", status.message()));
2595 overall_rpc_status = status;
2596 break;
2597 }
2598
2599 // Preserve the terminal action evidence before a CI expectation failure
2600 // stops the replay and replaces its summary with the mismatch message.
2601 logs.push_back(absl::StrFormat(" Result: %s", step_message));
2602 if (have_execution) {
2603 logs.push_back(absl::StrFormat(" Test ID: %s", execution.test_id));
2604 logs.push_back(absl::StrFormat(
2605 " Status: %s",
2606 ::yaze::test::HarnessStatusToString(execution.status)));
2607 for (const auto& failure : execution.assertion_failures) {
2608 logs.push_back(absl::StrFormat(" Assertion failure: %s", failure));
2609 }
2610 }
2611
2612 bool expectations_met = (step_success == step.expect_success);
2613 std::string expectation_error;
2614
2615 if (!expectations_met) {
2616 expectation_error =
2617 absl::StrFormat("Expected success=%s but got %s",
2618 step.expect_success ? "true" : "false",
2619 step_success ? "true" : "false");
2620 }
2621
2622 if (!step.expect_status.empty()) {
2623 HarnessTestStatus expected_status =
2624 ::yaze::test::HarnessStatusFromString(step.expect_status);
2625 if (!have_execution) {
2626 expectations_met = false;
2627 if (!expectation_error.empty()) {
2628 expectation_error.append("; ");
2629 }
2630 expectation_error.append("No execution details available");
2631 } else if (expected_status != HarnessTestStatus::kUnspecified &&
2632 execution.status != expected_status) {
2633 expectations_met = false;
2634 if (!expectation_error.empty()) {
2635 expectation_error.append("; ");
2636 }
2637 expectation_error.append(absl::StrFormat(
2638 "Expected status %s but observed %s", step.expect_status,
2639 ::yaze::test::HarnessStatusToString(execution.status)));
2640 }
2641 if (have_execution) {
2642 assertion->set_actual_value(
2643 ::yaze::test::HarnessStatusToString(execution.status));
2644 assertion->set_expected_value(step.expect_status);
2645 }
2646 }
2647
2648 if (!step.expect_message.empty()) {
2649 std::string actual_message = step_message;
2650 if (have_execution && !execution.error_message.empty()) {
2651 actual_message = execution.error_message;
2652 }
2653 if (actual_message.find(step.expect_message) == std::string::npos) {
2654 expectations_met = false;
2655 if (!expectation_error.empty()) {
2656 expectation_error.append("; ");
2657 }
2658 expectation_error.append(
2659 absl::StrFormat("Expected message containing '%s' but got '%s'",
2660 step.expect_message, actual_message));
2661 }
2662 }
2663
2664 if (!expectations_met) {
2665 assertion->set_passed(false);
2666 assertion->set_error_message(expectation_error);
2667 overall_success = false;
2668 overall_message = expectation_error;
2669 logs.push_back(
2670 absl::StrFormat(" Failed expectations: %s", expectation_error));
2671 if (request->ci_mode()) {
2672 break;
2673 }
2674 } else {
2675 assertion->set_passed(true);
2676 }
2677
2678 if (!overall_success && request->ci_mode()) {
2679 break;
2680 }
2681 }
2682
2683 response->set_steps_executed(steps_executed);
2684 response->set_success(overall_success);
2685 response->set_message(overall_message);
2686 for (const auto& log_entry : logs) {
2687 response->add_logs(log_entry);
2688 }
2689
2690 return overall_rpc_status;
2691}
2692
2693// ============================================================================
2694// ImGuiTestHarnessServer - Server Lifecycle
2695// ============================================================================
2696
2697ImGuiTestHarnessServer& ImGuiTestHarnessServer::Instance() {
2698 static ImGuiTestHarnessServer* instance = new ImGuiTestHarnessServer();
2699 return *instance;
2700}
2701
2702ImGuiTestHarnessServer::~ImGuiTestHarnessServer() {
2703 Shutdown();
2704}
2705
2706absl::Status ImGuiTestHarnessServer::Start(int port,
2707 TestManager* test_manager) {
2708 if (server_) {
2709 return absl::FailedPreconditionError("Server already running");
2710 }
2711
2712 if (!test_manager) {
2713 return absl::InvalidArgumentError("TestManager cannot be null");
2714 }
2715
2716 // Create the service implementation with TestManager reference
2717 service_ = std::make_unique<ImGuiTestHarnessServiceImpl>(test_manager);
2718
2719 // Create the gRPC service wrapper (store as member to prevent it from going
2720 // out of scope)
2721 grpc_service_ = std::make_unique<ImGuiTestHarnessServiceGrpc>(service_.get());
2722
2723 std::string server_address = absl::StrFormat("0.0.0.0:%d", port);
2724
2725 grpc::ServerBuilder builder;
2726
2727 // Listen on all interfaces (use 0.0.0.0 to avoid IPv6/IPv4 binding conflicts)
2728 builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
2729
2730 // Register service
2731 builder.RegisterService(grpc_service_.get());
2732
2733 // Build and start
2734 server_ = builder.BuildAndStart();
2735
2736 if (!server_) {
2737 return absl::InternalError(
2738 absl::StrFormat("Failed to start gRPC server on %s", server_address));
2739 }
2740
2741 port_ = port;
2742
2743 std::cout << "✓ ImGuiTestHarness gRPC server listening on " << server_address
2744 << " (with TestManager integration)\n";
2745 std::cout << " Use 'grpcurl -plaintext -d '{\"message\":\"test\"}' "
2746 << server_address << " yaze.test.ImGuiTestHarness/Ping' to test\n";
2747
2748 return absl::OkStatus();
2749}
2750
2751void ImGuiTestHarnessServer::Shutdown() {
2752 if (server_) {
2753 std::cout << "⏹ Shutting down ImGuiTestHarness gRPC server...\n";
2754 server_->Shutdown();
2755 server_.reset();
2756 service_.reset();
2757 port_ = 0;
2758 std::cout << "✓ ImGuiTestHarness gRPC server stopped\n";
2759 }
2760}
2761
2762} // namespace test
2763} // namespace yaze
2764
2765#endif // YAZE_WITH_GRPC
Controller * GetController()
Definition application.h:83
static Application & Instance()
editor::EditorManager * editor_manager()
Definition controller.h:85
UiSyncState GetUiSyncStateSnapshot() const
Moves work from service threads (gRPC, HTTP) onto the UI thread.
R RunOnUiThread(Fn fn, std::chrono::milliseconds timeout=kDefaultTimeout)
Runs fn on the UI thread and returns its result.
#define YAZE_VERSION_STRING
WindowContent * Get(const std::string &id)
Get a specific panel by its ID.
std::string ExtractLabelFromPath(absl::string_view path)
SDL2/SDL3 compatibility layer.
Room transition destination.
Definition zelda.h:448
Public YAZE API umbrella header.