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