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