20#include "absl/base/thread_annotations.h"
21#include "absl/container/flat_hash_map.h"
22#include "absl/strings/ascii.h"
23#include "absl/strings/numbers.h"
24#include "absl/strings/str_cat.h"
25#include "absl/strings/str_format.h"
26#include "absl/strings/str_replace.h"
27#include "absl/synchronization/mutex.h"
28#include "absl/time/clock.h"
29#include "absl/time/time.h"
36#include "imgui/imgui_internal.h"
37#include "protos/imgui_test_harness.grpc.pb.h"
38#include "protos/imgui_test_harness.pb.h"
49#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
50#include "imgui_test_engine/imgui_te_context.h"
51#include "imgui_test_engine/imgui_te_engine.h"
55thread_local int g_remote_replay_depth = 0;
57class ScopedRemoteReplaySubmission {
59 ScopedRemoteReplaySubmission() { ++g_remote_replay_depth; }
60 ~ScopedRemoteReplaySubmission() { --g_remote_replay_depth; }
63struct DynamicTestData {
64 bool remote_replay = g_remote_replay_depth != 0;
66 std::string harness_id;
67 std::function<void(ImGuiTestContext*)> test_func;
70absl::Mutex g_dynamic_tests_mutex;
71std::deque<std::shared_ptr<DynamicTestData>> g_dynamic_tests
72 ABSL_GUARDED_BY(g_dynamic_tests_mutex);
74void KeepDynamicTestData(
const std::shared_ptr<DynamicTestData>& data) {
75 absl::MutexLock lock(&g_dynamic_tests_mutex);
80 g_dynamic_tests.push_back(data);
83void RunDynamicTest(ImGuiTestContext* ctx) {
84 auto* data = (DynamicTestData*)ctx->Test->UserData;
85 if (data && data->test_func) {
86 if (data->remote_replay) {
87 const auto admitted = data->manager->BeginRemoteReplayFocus(ctx->Test);
89 data->manager->MarkHarnessTestCompleted(
90 data->harness_id, yaze::test::HarnessTestStatus::kFailed,
91 std::string(admitted.message()));
101absl::Status QueueDynamicTest(ImGuiTestEngine* engine,
104 const std::string& name,
105 const std::string& harness_id,
106 std::shared_ptr<DynamicTestData> data) {
108 [engine, manager, name, harness_id, data = std::move(data)] {
109 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc",
name.c_str());
110 test->SetOwnedName(
name.c_str());
111 test->TestFunc = RunDynamicTest;
112 test->UserData = data.get();
113 data->manager = manager;
114 data->harness_id = harness_id;
115 if (data->remote_replay)
116 manager->RegisterRemoteReplayTest(test, harness_id);
117 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
118 KeepDynamicTestData(data);
119 return absl::OkStatus();
124bool IsTestCompleted(ImGuiTest* test) {
125 return test->Output.Status != ImGuiTestStatus_Queued &&
126 test->Output.Status != ImGuiTestStatus_Running;
132 std::atomic<bool> completed{
false};
133 std::mutex data_mutex;
137 void SetResult(
const T& res,
const std::string& msg) {
138 std::lock_guard<std::mutex> lock(data_mutex);
141 completed.store(
true);
144 void GetResult(T& res, std::string& msg) {
145 std::lock_guard<std::mutex> lock(data_mutex);
156::yaze::test::GetTestStatusResponse_TestStatus ConvertHarnessStatus(
157 ::yaze::test::HarnessTestStatus status) {
159 case ::yaze::test::HarnessTestStatus::kQueued:
160 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_QUEUED;
161 case ::yaze::test::HarnessTestStatus::kRunning:
162 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_RUNNING;
163 case ::yaze::test::HarnessTestStatus::kPassed:
164 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_PASSED;
165 case ::yaze::test::HarnessTestStatus::kFailed:
166 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_FAILED;
167 case ::yaze::test::HarnessTestStatus::kTimeout:
168 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_TIMEOUT;
169 case ::yaze::test::HarnessTestStatus::kUnspecified:
171 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_UNSPECIFIED;
175int64_t ToUnixMillisSafe(absl::Time timestamp) {
176 if (timestamp == absl::InfinitePast()) {
179 return absl::ToUnixMillis(timestamp);
182int32_t ClampDurationToInt32(absl::Duration duration) {
183 int64_t millis = absl::ToInt64Milliseconds(duration);
184 if (millis > std::numeric_limits<int32_t>::max()) {
185 return std::numeric_limits<int32_t>::max();
187 if (millis < std::numeric_limits<int32_t>::min()) {
188 return std::numeric_limits<int32_t>::min();
190 return static_cast<int32_t
>(millis);
195#include <grpcpp/grpcpp.h>
196#include <grpcpp/server_builder.h>
203std::string ClickTypeToString(ClickRequest::ClickType type) {
205 case ClickRequest::CLICK_TYPE_RIGHT:
207 case ClickRequest::CLICK_TYPE_MIDDLE:
209 case ClickRequest::CLICK_TYPE_DOUBLE:
211 case ClickRequest::CLICK_TYPE_LEFT:
212 case ClickRequest::CLICK_TYPE_UNSPECIFIED:
218ClickRequest::ClickType ClickTypeFromString(absl::string_view type) {
219 const std::string lower = absl::AsciiStrToLower(std::string(type));
220 if (lower ==
"right") {
221 return ClickRequest::CLICK_TYPE_RIGHT;
223 if (lower ==
"middle") {
224 return ClickRequest::CLICK_TYPE_MIDDLE;
226 if (lower ==
"double" || lower ==
"double_click" || lower ==
"dbl") {
227 return ClickRequest::CLICK_TYPE_DOUBLE;
229 return ClickRequest::CLICK_TYPE_LEFT;
232HarnessTestStatus HarnessStatusFromString(absl::string_view status) {
233 const std::string lower = absl::AsciiStrToLower(std::string(status));
234 if (lower ==
"passed" || lower ==
"success") {
235 return HarnessTestStatus::kPassed;
237 if (lower ==
"failed" || lower ==
"fail") {
238 return HarnessTestStatus::kFailed;
240 if (lower ==
"timeout") {
241 return HarnessTestStatus::kTimeout;
243 if (lower ==
"queued") {
244 return HarnessTestStatus::kQueued;
246 if (lower ==
"running") {
247 return HarnessTestStatus::kRunning;
249 return HarnessTestStatus::kUnspecified;
252const char* HarnessStatusToString(HarnessTestStatus status) {
254 case HarnessTestStatus::kPassed:
256 case HarnessTestStatus::kFailed:
258 case HarnessTestStatus::kTimeout:
260 case HarnessTestStatus::kQueued:
262 case HarnessTestStatus::kRunning:
264 case HarnessTestStatus::kUnspecified:
270std::string ApplyOverrides(
271 const std::string& value,
272 const absl::flat_hash_map<std::string, std::string>& overrides) {
273 if (overrides.empty() || value.empty()) {
276 std::string result = value;
277 for (
const auto& [key, replacement] : overrides) {
278 const std::string placeholder = absl::StrCat(
"{{", key,
"}}");
279 result = absl::StrReplaceAll(result, {{placeholder, replacement}});
284void MaybeRecordStep(TestRecorder* recorder, TestRecorder::RecordedStep step) {
285 if (!recorder || !recorder->IsRecording()) {
288 if (step.captured_at == absl::InfinitePast()) {
289 step.captured_at = absl::Now();
291 recorder->RecordStep(step);
294absl::Status WaitForHarnessTestCompletion(TestManager* manager,
295 const std::string& test_id,
296 HarnessTestExecution* execution) {
298 return absl::FailedPreconditionError(
"TestManager unavailable");
300 if (test_id.empty()) {
301 return absl::InvalidArgumentError(
"Missing harness test identifier");
304 const absl::Time deadline = absl::Now() + absl::Seconds(20);
305 while (absl::Now() < deadline) {
306 absl::StatusOr<HarnessTestExecution> current =
307 manager->GetHarnessTestExecution(test_id);
309 absl::SleepFor(absl::Milliseconds(75));
314 *execution = std::move(current.value());
317 if (current->status == HarnessTestStatus::kQueued ||
318 current->status == HarnessTestStatus::kRunning) {
319 absl::SleepFor(absl::Milliseconds(75));
322 return absl::OkStatus();
325 return absl::DeadlineExceededError(absl::StrFormat(
326 "Harness test %s did not reach a terminal state", test_id));
329struct ParsedCondition {
335 size_t colon = path.rfind(
':');
336 if (colon != absl::string_view::npos && colon + 1 < path.size()) {
337 return std::string(path.substr(colon + 1));
339 size_t slash = path.rfind(
'/');
340 if (slash != absl::string_view::npos && slash + 1 < path.size()) {
341 return std::string(path.substr(slash + 1));
343 return std::string(path);
346std::string ExtractTypeFromPath(absl::string_view path) {
347 size_t slash = path.rfind(
'/');
348 absl::string_view segment =
349 slash == absl::string_view::npos ? path : path.substr(slash + 1);
350 size_t colon = segment.find(
':');
351 if (colon == absl::string_view::npos || colon == 0) {
354 return std::string(segment.substr(0, colon));
357absl::StatusOr<internal::ParsedTarget> ParseTargetString(
358 absl::string_view target) {
359 if (target.empty()) {
360 return absl::InvalidArgumentError(
361 "Missing target. Provide 'type:label' or widget_key.");
363 size_t colon_pos = target.find(
':');
364 if (colon_pos == absl::string_view::npos || colon_pos == 0 ||
365 colon_pos + 1 >= target.size()) {
366 return absl::InvalidArgumentError(
367 "Invalid target format. Use 'type:label' (e.g. 'button:Open ROM').");
370 internal::ParsedTarget parsed;
371 parsed.type = std::string(target.substr(0, colon_pos));
372 parsed.label = std::string(target.substr(colon_pos + 1));
380absl::StatusOr<std::string> FocusHarnessWindow(editor::WindowHost* host,
382 absl::string_view window_id) {
383 if (window_id.empty() || window_id.find(
'\0') != absl::string_view::npos) {
384 return absl::InvalidArgumentError(
385 "Window target requires a nonempty base ID");
387 if (!host || !host->window_manager() || !ImGui::GetCurrentContext()) {
388 return absl::FailedPreconditionError(
389 "Window host/UI context is unavailable");
391 auto* windows = host->window_manager();
392 if (windows->GetActiveSessionId() != session_id) {
393 return absl::FailedPreconditionError(
"Window target session is not active");
395 const std::string
id = host->ResolveWindowId(std::string(window_id));
396 const auto session_windows = windows->GetWindowsInSession(session_id);
397 if (std::find(session_windows.begin(), session_windows.end(),
id) ==
398 session_windows.end() ||
399 !windows->GetWindowDescriptor(session_id,
id)) {
400 return absl::NotFoundError(
"Unknown window ID in the active session: " +
403 const std::string
name = host->GetWorkspaceWindowName(session_id,
id);
404 if (
name.empty() || !ImGui::FindWindowByName(
name.c_str())) {
405 return absl::FailedPreconditionError(
406 "Registered window has not been drawn");
408 if (!host->OpenAndFocusWindow(session_id,
id)) {
409 return absl::FailedPreconditionError(
"Window host refused to open/focus: " +
415absl::Status VerifyHarnessWindowFocus(absl::string_view window_name) {
416 ImGuiContext* context = ImGui::GetCurrentContext();
417 ImGuiWindow* window =
418 context ? ImGui::FindWindowByName(std::string(window_name).c_str())
420 if (!window || !window->Active || window->Hidden ||
421 (window->DockNode && !window->DockTabIsVisible) || !context->NavWindow ||
422 context->NavWindow->RootWindow != window->RootWindow) {
423 return absl::FailedPreconditionError(
424 "Requested window/tab is not focused and visible");
426 return absl::OkStatus();
429absl::Status ValidateProjectSaveAsDestination(
const char*
destination) {
430 namespace fs = std::filesystem;
434 return absl::InvalidArgumentError(
435 "Set YAZE_TEST_PROJECT_SAVE_AS to an absolute NEW copied-project "
436 ".yaze destination, or an empty string for Cancel");
439 return absl::OkStatus();
441 std::error_code error;
442 const auto entry = fs::symlink_status(
destination, error);
443 if (error && error != std::errc::no_such_file_or_directory) {
444 return absl::FailedPreconditionError(absl::StrCat(
445 "Cannot inspect Project Save As destination: ", error.message()));
447 if (entry.type() != fs::file_type::not_found) {
448 return absl::AlreadyExistsError(
449 "Project Save As replay destination already exists");
453 const auto parent = fs::path(
destination).parent_path();
454 const auto parent_entry = fs::status(parent, error);
455 if (error || parent_entry.type() != fs::file_type::directory) {
456 return absl::FailedPreconditionError(
457 absl::StrCat(
"Cannot inspect Project Save As destination: parent ",
458 parent.string(),
" is not an existing directory"));
460 return absl::OkStatus();
463absl::StatusOr<ResolvedWidgetSelector> ResolveWidgetSelector(
464 absl::string_view target, absl::string_view widget_key) {
465 ResolvedWidgetSelector resolved;
466 if (!widget_key.empty()) {
467 const std::string key(widget_key);
468 std::optional<gui::WidgetIdRegistry::WidgetInfo> info =
469 gui::WidgetIdRegistry::Instance().GetWidgetInfo(key);
471 return absl::NotFoundError(absl::StrFormat(
472 "widget_key '%s' was not found in WidgetIdRegistry", key));
475 resolved.resolved_widget_key = key;
476 resolved.resolved_path =
477 info->full_path.empty() ? key : std::string(info->full_path);
478 resolved.imgui_id = info->imgui_id;
479 if (resolved.imgui_id == 0) {
480 return absl::FailedPreconditionError(absl::StrFormat(
481 "widget_key '%s' resolved without a valid ImGui ID", key));
483 resolved.target.type = info->type.empty()
484 ? ExtractTypeFromPath(resolved.resolved_path)
485 : std::string(info->type);
486 resolved.target.label = info->label.empty()
488 : std::string(info->label);
489 if (resolved.target.label.empty()) {
490 return absl::FailedPreconditionError(absl::StrFormat(
491 "widget_key '%s' resolved without a usable label", key));
493 if (resolved.target.type.empty()) {
494 resolved.target.type =
"widget";
499 absl::StatusOr<ParsedTarget> parsed = ParseTargetString(target);
501 return parsed.status();
503 resolved.target = *parsed;
507void ApplyTerminalHarnessExecution(
const HarnessTestExecution& execution,
509 std::string* step_message) {
511 *step_success = execution.status == HarnessTestStatus::kPassed;
513 if (step_message && !execution.error_message.empty()) {
514 *step_message = execution.error_message;
520absl::StatusOr<ScreenshotArtifact> CaptureScreenshotOnRenderThread(
521 const std::string& preferred_path,
const std::string& window_title,
525 return absl::FailedPreconditionError(
526 "Application controller not available");
532 std::atomic<bool> done{
false};
533 absl::StatusOr<ScreenshotArtifact> result =
534 absl::UnknownError(
"Not captured");
536 auto state = std::make_shared<State>();
538 controller->RequestScreenshot(
539 {.preferred_path = preferred_path,
540 .window_title = window_title,
542 .reveal_to_user =
false,
543 .callback = [state](absl::StatusOr<ScreenshotArtifact> result) {
544 state->result = std::move(result);
545 state->done.store(
true);
548 const auto deadline = std::chrono::steady_clock::now() + timeout;
549 while (!state->done.load()) {
550 if (std::chrono::steady_clock::now() > deadline) {
551 return absl::DeadlineExceededError(
552 "Timed out waiting for screenshot capture on main thread");
554 std::this_thread::sleep_for(std::chrono::milliseconds(10));
556 return state->result;
561absl::StatusOr<ParsedCondition> ParseConditionString(absl::string_view value) {
563 return absl::InvalidArgumentError(
564 "Missing condition. Provide 'type:target' or widget_key.");
567 size_t colon_pos = value.find(
':');
568 if (colon_pos == absl::string_view::npos || colon_pos == 0) {
569 return absl::InvalidArgumentError(
570 "Invalid condition format. Use 'type:target'.");
573 ParsedCondition parsed;
574 parsed.type = std::string(value.substr(0, colon_pos));
575 parsed.target = std::string(value.substr(colon_pos + 1));
579absl::StatusOr<ParsedCondition> ResolveCondition(
580 absl::string_view condition, absl::string_view widget_key,
581 absl::string_view default_type_for_widget,
582 internal::ResolvedWidgetSelector* resolved_selector) {
583 std::optional<internal::ResolvedWidgetSelector> widget_selector;
584 if (!widget_key.empty()) {
585 absl::StatusOr<internal::ResolvedWidgetSelector> resolved =
586 internal::ResolveWidgetSelector(
"", widget_key);
587 if (!resolved.ok()) {
588 return resolved.status();
590 widget_selector = *resolved;
591 if (resolved_selector) {
592 *resolved_selector = *resolved;
594 }
else if (resolved_selector) {
595 *resolved_selector = internal::ResolvedWidgetSelector{};
598 if (condition.empty()) {
599 if (!widget_selector.has_value()) {
600 return absl::InvalidArgumentError(
601 "Missing condition. Provide 'type:target' or widget_key.");
603 ParsedCondition parsed;
604 parsed.type = std::string(default_type_for_widget);
605 parsed.target = widget_selector->target.label;
609 absl::StatusOr<ParsedCondition> parsed = ParseConditionString(condition);
611 return parsed.status();
614 if (widget_selector.has_value() && parsed->target.empty()) {
615 parsed->target = widget_selector->target.label;
618 if (parsed->target.empty()) {
619 return absl::InvalidArgumentError(
620 "Condition target is empty. Provide 'type:target'.");
626struct UiSyncSnapshot {
627 int64_t frame_id = 0;
628 int32_t pending_editor_actions = 0;
629 int32_t pending_layout_actions = 0;
630 bool layout_rebuild_pending =
false;
631 int32_t pending_harness_tests = 0;
632 int32_t queued_harness_tests = 0;
633 int32_t running_harness_tests = 0;
636editor::EditorManager* GetEditorManagerForSync() {
644UiSyncSnapshot CaptureUiSyncSnapshot(TestManager* test_manager) {
645 UiSyncSnapshot snapshot;
646 if (editor::EditorManager* editor_manager = GetEditorManagerForSync()) {
647 const editor::EditorManager::UiSyncState state =
648 editor_manager->GetUiSyncStateSnapshot();
649 snapshot.frame_id =
static_cast<int64_t
>(state.frame_id);
650 snapshot.pending_editor_actions = state.pending_editor_actions;
651 snapshot.pending_layout_actions = state.pending_layout_actions;
652 snapshot.layout_rebuild_pending = state.layout_rebuild_pending;
656 const std::vector<HarnessTestSummary> summaries =
657 test_manager->ListHarnessTestSummaries();
658 for (
const HarnessTestSummary& summary : summaries) {
659 switch (summary.latest_execution.status) {
660 case HarnessTestStatus::kQueued:
661 snapshot.queued_harness_tests++;
663 case HarnessTestStatus::kRunning:
664 snapshot.running_harness_tests++;
672 snapshot.pending_harness_tests =
673 snapshot.queued_harness_tests + snapshot.running_harness_tests;
677bool IsUiIdle(
const UiSyncSnapshot& snapshot) {
678 return snapshot.pending_editor_actions == 0 &&
679 snapshot.pending_layout_actions == 0 &&
680 !snapshot.layout_rebuild_pending &&
681 snapshot.pending_harness_tests == 0;
684std::string BuildIdleTimeoutReason(
const UiSyncSnapshot& snapshot) {
685 return absl::StrFormat(
686 "pending_editor=%d pending_layout=%d layout_rebuild=%s queued_tests=%d "
688 snapshot.pending_editor_actions, snapshot.pending_layout_actions,
689 snapshot.layout_rebuild_pending ?
"true" :
"false",
690 snapshot.queued_harness_tests, snapshot.running_harness_tests);
693int32_t NormalizeTimeoutMs(int32_t timeout_ms, int32_t fallback_ms) {
694 return timeout_ms <= 0 ? fallback_ms : timeout_ms;
697int32_t NormalizePollIntervalMs(int32_t poll_interval_ms) {
698 if (poll_interval_ms <= 0) {
701 return std::clamp(poll_interval_ms, 5, 1000);
707class ImGuiTestHarnessServiceGrpc final :
public ImGuiTestHarness::Service {
709 explicit ImGuiTestHarnessServiceGrpc(ImGuiTestHarnessServiceImpl* impl)
712 ::grpc::Status Ping(::grpc::ServerContext* context,
713 const PingRequest* request,
714 PingResponse* response)
override {
715 return ConvertStatus(impl_->Ping(request, response));
718 ::grpc::Status Click(::grpc::ServerContext* context,
719 const ClickRequest* request,
720 ClickResponse* response)
override {
721 return ConvertStatus(impl_->Click(request, response));
724 ::grpc::Status Type(::grpc::ServerContext* context,
725 const TypeRequest* request,
726 TypeResponse* response)
override {
727 return ConvertStatus(impl_->Type(request, response));
730 ::grpc::Status Wait(::grpc::ServerContext* context,
731 const WaitRequest* request,
732 WaitResponse* response)
override {
733 return ConvertStatus(impl_->Wait(request, response));
736 ::grpc::Status Assert(::grpc::ServerContext* context,
737 const AssertRequest* request,
738 AssertResponse* response)
override {
739 return ConvertStatus(impl_->Assert(request, response));
742 ::grpc::Status FlushUiActions(::grpc::ServerContext* context,
743 const FlushUiActionsRequest* request,
744 FlushUiActionsResponse* response)
override {
745 return ConvertStatus(impl_->FlushUiActions(request, response));
748 ::grpc::Status WaitForIdle(::grpc::ServerContext* context,
749 const WaitForIdleRequest* request,
750 WaitForIdleResponse* response)
override {
751 return ConvertStatus(impl_->WaitForIdle(request, response));
754 ::grpc::Status GetUiSyncState(::grpc::ServerContext* context,
755 const GetUiSyncStateRequest* request,
756 GetUiSyncStateResponse* response)
override {
757 return ConvertStatus(impl_->GetUiSyncState(request, response));
760 ::grpc::Status Screenshot(::grpc::ServerContext* context,
761 const ScreenshotRequest* request,
762 ScreenshotResponse* response)
override {
763 return ConvertStatus(impl_->Screenshot(request, response));
766 ::grpc::Status GetTestStatus(::grpc::ServerContext* context,
767 const GetTestStatusRequest* request,
768 GetTestStatusResponse* response)
override {
769 return ConvertStatus(impl_->GetTestStatus(request, response));
772 ::grpc::Status ListTests(::grpc::ServerContext* context,
773 const ListTestsRequest* request,
774 ListTestsResponse* response)
override {
775 return ConvertStatus(impl_->ListTests(request, response));
778 ::grpc::Status GetTestResults(::grpc::ServerContext* context,
779 const GetTestResultsRequest* request,
780 GetTestResultsResponse* response)
override {
781 return ConvertStatus(impl_->GetTestResults(request, response));
784 ::grpc::Status DiscoverWidgets(::grpc::ServerContext* context,
785 const DiscoverWidgetsRequest* request,
786 DiscoverWidgetsResponse* response)
override {
787 return ConvertStatus(impl_->DiscoverWidgets(request, response));
790 ::grpc::Status StartRecording(::grpc::ServerContext* context,
791 const StartRecordingRequest* request,
792 StartRecordingResponse* response)
override {
793 return ConvertStatus(impl_->StartRecording(request, response));
796 ::grpc::Status StopRecording(::grpc::ServerContext* context,
797 const StopRecordingRequest* request,
798 StopRecordingResponse* response)
override {
799 return ConvertStatus(impl_->StopRecording(request, response));
802 ::grpc::Status ReplayTest(::grpc::ServerContext* context,
803 const ReplayTestRequest* request,
804 ReplayTestResponse* response)
override {
805 return ConvertStatus(impl_->ReplayTest(request, response));
809 static ::grpc::Status ConvertStatus(
const absl::Status& status) {
811 return ::grpc::Status::OK;
814 ::grpc::StatusCode code = ::grpc::StatusCode::UNKNOWN;
815 switch (status.code()) {
816 case absl::StatusCode::kCancelled:
817 code = ::grpc::StatusCode::CANCELLED;
819 case absl::StatusCode::kUnknown:
820 code = ::grpc::StatusCode::UNKNOWN;
822 case absl::StatusCode::kInvalidArgument:
823 code = ::grpc::StatusCode::INVALID_ARGUMENT;
825 case absl::StatusCode::kDeadlineExceeded:
826 code = ::grpc::StatusCode::DEADLINE_EXCEEDED;
828 case absl::StatusCode::kNotFound:
829 code = ::grpc::StatusCode::NOT_FOUND;
831 case absl::StatusCode::kAlreadyExists:
832 code = ::grpc::StatusCode::ALREADY_EXISTS;
834 case absl::StatusCode::kPermissionDenied:
835 code = ::grpc::StatusCode::PERMISSION_DENIED;
837 case absl::StatusCode::kResourceExhausted:
838 code = ::grpc::StatusCode::RESOURCE_EXHAUSTED;
840 case absl::StatusCode::kFailedPrecondition:
841 code = ::grpc::StatusCode::FAILED_PRECONDITION;
843 case absl::StatusCode::kAborted:
844 code = ::grpc::StatusCode::ABORTED;
846 case absl::StatusCode::kOutOfRange:
847 code = ::grpc::StatusCode::OUT_OF_RANGE;
849 case absl::StatusCode::kUnimplemented:
850 code = ::grpc::StatusCode::UNIMPLEMENTED;
852 case absl::StatusCode::kInternal:
853 code = ::grpc::StatusCode::INTERNAL;
855 case absl::StatusCode::kUnavailable:
856 code = ::grpc::StatusCode::UNAVAILABLE;
858 case absl::StatusCode::kDataLoss:
859 code = ::grpc::StatusCode::DATA_LOSS;
861 case absl::StatusCode::kUnauthenticated:
862 code = ::grpc::StatusCode::UNAUTHENTICATED;
865 code = ::grpc::StatusCode::UNKNOWN;
869 return ::grpc::Status(
870 code, std::string(status.message().data(), status.message().size()));
873 ImGuiTestHarnessServiceImpl* impl_;
876std::unique_ptr<::grpc::Service> CreateImGuiTestHarnessServiceGrpc(
877 ImGuiTestHarnessServiceImpl* impl) {
878 return std::make_unique<ImGuiTestHarnessServiceGrpc>(impl);
885ImGuiTestHarnessServiceImpl::ImGuiTestHarnessServiceImpl(
886 TestManager* test_manager, util::UiThreadQueue* ui_thread_queue)
887 : test_manager_(test_manager),
888 ui_thread_queue_(ui_thread_queue ? ui_thread_queue
889 : &util::UiThreadQueue::
Get()),
890 test_recorder_(test_manager) {}
892absl::Status ImGuiTestHarnessServiceImpl::Ping(
const PingRequest* request,
893 PingResponse* response) {
895 response->set_message(absl::StrFormat(
"Pong: %s", request->message()));
898 auto now = std::chrono::system_clock::now();
899 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
900 now.time_since_epoch());
901 response->set_timestamp_ms(ms.count());
906 return absl::OkStatus();
909absl::Status ImGuiTestHarnessServiceImpl::Click(
const ClickRequest* request,
910 ClickResponse* response) {
911 auto start = std::chrono::steady_clock::now();
913 TestRecorder::RecordedStep recorded_step;
914 recorded_step.type = TestRecorder::ActionType::kClick;
916 recorded_step.widget_key = request->widget_key();
917 if (!request->widget_key().empty()) {
918 recorded_step.target = absl::StrCat(
"widget_key:", request->widget_key());
920 recorded_step.target = request->target();
922 recorded_step.click_type = ClickTypeToString(request->type());
925 auto finalize = [&](
const absl::Status& status) {
926 recorded_step.success = response->success();
927 recorded_step.message = response->message();
928 recorded_step.execution_time_ms = response->execution_time_ms();
929 recorded_step.test_id = response->test_id();
930 MaybeRecordStep(&test_recorder_, recorded_step);
934 if (!test_manager_) {
935 response->set_success(
false);
936 response->set_message(
"TestManager not available");
937 response->set_execution_time_ms(0);
938 return finalize(absl::FailedPreconditionError(
"TestManager not available"));
941 const std::string requested_target = request ? request->target() :
"";
942 const std::string requested_widget_key = request ? request->widget_key() :
"";
943 const std::string request_summary =
944 !requested_widget_key.empty()
945 ? absl::StrFormat(
"widget_key:%s", requested_widget_key)
948 const std::string test_id = test_manager_->RegisterHarnessTest(
949 absl::StrFormat(
"Click: %s", request_summary),
"grpc");
950 response->set_test_id(test_id);
951 recorded_step.test_id = test_id;
952 test_manager_->AppendHarnessTestLog(
953 test_id, absl::StrCat(
"Queued click request: ", request_summary));
955 absl::StatusOr<internal::ResolvedWidgetSelector> resolved_selector =
956 internal::ResolveWidgetSelector(requested_target, requested_widget_key);
957 if (!resolved_selector.ok()) {
958 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
959 std::chrono::steady_clock::now() - start);
960 std::string message = std::string(resolved_selector.status().message());
961 response->set_success(
false);
962 response->set_message(message);
963 response->set_execution_time_ms(elapsed.count());
964 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
966 test_manager_->AppendHarnessTestLog(test_id, message);
967 return finalize(absl::OkStatus());
970 if (!resolved_selector->resolved_widget_key.empty()) {
971 response->set_resolved_widget_key(resolved_selector->resolved_widget_key);
973 if (!resolved_selector->resolved_path.empty()) {
974 response->set_resolved_path(resolved_selector->resolved_path);
977 const std::string widget_type = resolved_selector->target.type;
978 const std::string widget_label = resolved_selector->target.label;
979 const ImGuiID widget_id = resolved_selector->imgui_id;
981#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
982 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
984 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
985 std::chrono::steady_clock::now() - start);
986 std::string message =
"ImGuiTestEngine not initialized";
987 response->set_success(
false);
988 response->set_message(message);
989 response->set_execution_time_ms(elapsed.count());
990 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
992 return finalize(absl::OkStatus());
995 const ClickRequest::ClickType click_type =
996 request ? request->type() : ClickRequest::CLICK_TYPE_LEFT;
998 ImGuiMouseButton mouse_button = ImGuiMouseButton_Left;
999 switch (click_type) {
1000 case ClickRequest::CLICK_TYPE_UNSPECIFIED:
1001 case ClickRequest::CLICK_TYPE_LEFT:
1002 mouse_button = ImGuiMouseButton_Left;
1004 case ClickRequest::CLICK_TYPE_RIGHT:
1005 mouse_button = ImGuiMouseButton_Right;
1007 case ClickRequest::CLICK_TYPE_MIDDLE:
1008 mouse_button = ImGuiMouseButton_Middle;
1010 case ClickRequest::CLICK_TYPE_DOUBLE:
1015 auto test_data = std::make_shared<DynamicTestData>();
1016 TestManager* manager = test_manager_;
1017 test_data->test_func = [manager, captured_id = test_id, widget_type,
1018 widget_label, widget_id, click_type,
1019 mouse_button](ImGuiTestContext* ctx) {
1020 manager->MarkHarnessTestRunning(captured_id);
1022 if (widget_type ==
"window") {
1023 auto* controller = Application::Instance().GetController();
1024 auto* editor = controller ? controller->editor_manager() :
nullptr;
1025 if (!editor || !editor->GetCurrentEditorSet()) {
1026 manager->MarkHarnessTestCompleted(
1027 captured_id, HarnessTestStatus::kFailed,
1028 "Window focus requires an active editor session");
1031 const auto focused = internal::FocusHarnessWindow(
1032 editor->window_host(), editor->GetCurrentSessionId(), widget_label);
1033 if (!focused.ok()) {
1034 manager->MarkHarnessTestCompleted(
1035 captured_id, HarnessTestStatus::kFailed,
1036 std::string(focused.status().message()));
1040 const auto verified = internal::VerifyHarnessWindowFocus(*focused);
1041 if (!verified.ok() || ctx->IsError()) {
1042 manager->MarkHarnessTestCompleted(
1043 captured_id, HarnessTestStatus::kFailed,
1044 verified.ok() ?
"Window focus action failed"
1045 : std::string(verified.message()));
1048 manager->MarkHarnessTestCompleted(captured_id,
1049 HarnessTestStatus::kPassed,
1050 "Focused window: " + widget_label);
1053 if (widget_type ==
"shortcut") {
1054 ImGuiKeyChord chord = 0;
1055 std::optional<util::ScopedSaveFileDialogResultForTesting> save_picker;
1056 if (widget_label ==
"Save ROM") {
1057 chord = ImGuiMod_Shortcut | ImGuiKey_S;
1058 }
else if (widget_label ==
"Undo") {
1059 chord = ImGuiMod_Shortcut | ImGuiKey_Z;
1060 }
else if (widget_label ==
"Redo") {
1061 chord = ImGuiMod_Shortcut | ImGuiMod_Shift | ImGuiKey_Z;
1062 }
else if (widget_label ==
"Save As") {
1063 const char*
destination = std::getenv(
"YAZE_TEST_PROJECT_SAVE_AS");
1064 const auto destination_status =
1065 internal::ValidateProjectSaveAsDestination(
destination);
1066 if (!destination_status.ok()) {
1067 manager->MarkHarnessTestCompleted(
1068 captured_id, HarnessTestStatus::kFailed,
1069 std::string(destination_status.message()));
1072 auto* controller = Application::Instance().GetController();
1073 auto* editor = controller ? controller->editor_manager() :
nullptr;
1074 if (!editor || !editor->GetCurrentProject()->project_opened()) {
1075 manager->MarkHarnessTestCompleted(
1076 captured_id, HarnessTestStatus::kFailed,
1077 "Project Save As replay requires an open copied project");
1081 chord = ImGuiMod_Shortcut | ImGuiMod_Shift | ImGuiKey_S;
1082 }
else if (widget_label ==
"Settings") {
1083 chord = ImGuiMod_Shortcut | ImGuiKey_Comma;
1084 }
else if (widget_label ==
"Quit") {
1089 auto* controller = Application::Instance().GetController();
1090 auto* editor_manager =
1091 controller !=
nullptr ? controller->editor_manager() :
nullptr;
1092 if (editor_manager ==
nullptr) {
1093 const std::string error_message =
1094 "Application quit is unavailable before editor startup";
1095 manager->AppendHarnessTestLog(captured_id, error_message);
1096 manager->MarkHarnessTestCompleted(
1097 captured_id, HarnessTestStatus::kFailed, error_message);
1101 editor_manager->Quit();
1102 if (!editor_manager->quit()) {
1103 const std::string error_message = absl::StrCat(
1104 "Application quit was blocked: ",
1105 editor_manager->GetPendingUnsavedSessionActionPrompt());
1106 manager->AppendHarnessTestLog(captured_id, error_message);
1107 manager->MarkHarnessTestCompleted(
1108 captured_id, HarnessTestStatus::kFailed, error_message);
1112 const std::string success_message =
"Requested application quit";
1113 manager->AppendHarnessTestLog(captured_id, success_message);
1114 manager->MarkHarnessTestCompleted(
1115 captured_id, HarnessTestStatus::kPassed, success_message);
1118 const std::string error_message = absl::StrFormat(
1119 "Unsupported harness shortcut '%s'", widget_label);
1120 manager->AppendHarnessTestLog(captured_id, error_message);
1121 manager->MarkHarnessTestCompleted(
1122 captured_id, HarnessTestStatus::kFailed, error_message);
1126 ctx->KeyPress(chord);
1128 if (save_picker && !save_picker->consumed()) {
1129 manager->MarkHarnessTestCompleted(
1130 captured_id, HarnessTestStatus::kFailed,
1131 "Project Save As shortcut did not reach the destination picker");
1134 if (ctx->IsError()) {
1135 const std::string error_message =
1136 absl::StrFormat(
"Shortcut failed for '%s'", widget_label);
1137 manager->AppendHarnessTestLog(captured_id, error_message);
1138 manager->MarkHarnessTestCompleted(
1139 captured_id, HarnessTestStatus::kFailed, error_message);
1142 const std::string success_message =
1143 absl::StrFormat(
"Pressed shortcut '%s'", widget_label);
1144 manager->AppendHarnessTestLog(captured_id, success_message);
1145 manager->MarkHarnessTestCompleted(
1146 captured_id, HarnessTestStatus::kPassed, success_message);
1150 const ImGuiTestRef widget_ref = widget_id != 0
1151 ? ImGuiTestRef(widget_id)
1152 : ImGuiTestRef(widget_label.c_str());
1153 const ImGuiTestItemInfo item =
1154 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1156 const std::string error_message =
1157 absl::StrFormat(
"Click target '%s' not found", widget_label);
1158 manager->AppendHarnessTestLog(captured_id, error_message);
1159 manager->MarkHarnessTestCompleted(
1160 captured_id, HarnessTestStatus::kFailed, error_message);
1164 if (click_type == ClickRequest::CLICK_TYPE_DOUBLE) {
1165 ctx->ItemDoubleClick(ImGuiTestRef(item.ID));
1167 ctx->ItemClick(ImGuiTestRef(item.ID), mouse_button);
1170 if (ctx->IsError()) {
1171 const std::string error_message =
1172 absl::StrFormat(
"Click failed for '%s'", widget_label);
1173 manager->AppendHarnessTestLog(captured_id, error_message);
1174 manager->MarkHarnessTestCompleted(
1175 captured_id, HarnessTestStatus::kFailed, error_message);
1178 const std::string success_message =
1179 absl::StrFormat(
"Clicked %s '%s'", widget_type, widget_label);
1180 manager->AppendHarnessTestLog(captured_id, success_message);
1181 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kPassed,
1183 }
catch (
const std::exception& e) {
1184 const std::string error_message =
1185 absl::StrFormat(
"Click failed: %s", e.what());
1186 manager->AppendHarnessTestLog(captured_id, error_message);
1187 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
1192 std::string test_name = absl::StrFormat(
1194 static_cast<long long>(
1195 std::chrono::system_clock::now().time_since_epoch().count()));
1197 const auto queued = QueueDynamicTest(engine, test_manager_, ui_thread_queue_,
1198 test_name, test_id, test_data);
1200 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1201 std::string(queued.message()));
1205 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1206 std::chrono::steady_clock::now() - start);
1207 std::string message =
1208 absl::StrFormat(
"Queued click on %s '%s'", widget_type, widget_label);
1209 response->set_success(
true);
1210 response->set_message(message);
1211 response->set_execution_time_ms(elapsed.count());
1212 test_manager_->AppendHarnessTestLog(test_id, message);
1215 const std::string message =
"ImGuiTestEngine is not available in this build";
1216 test_manager_->MarkHarnessTestRunning(test_id);
1217 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1219 test_manager_->AppendHarnessTestLog(test_id, message);
1221 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1222 std::chrono::steady_clock::now() - start);
1223 response->set_success(
false);
1224 response->set_message(message);
1225 response->set_execution_time_ms(elapsed.count());
1228 return finalize(absl::OkStatus());
1231absl::Status ImGuiTestHarnessServiceImpl::Type(
const TypeRequest* request,
1232 TypeResponse* response) {
1233 auto start = std::chrono::steady_clock::now();
1235 TestRecorder::RecordedStep recorded_step;
1236 recorded_step.type = TestRecorder::ActionType::kType;
1238 recorded_step.widget_key = request->widget_key();
1239 if (!request->widget_key().empty()) {
1240 recorded_step.target = absl::StrCat(
"widget_key:", request->widget_key());
1242 recorded_step.target = request->target();
1244 recorded_step.text = request->text();
1245 recorded_step.clear_first = request->clear_first();
1248 auto finalize = [&](
const absl::Status& status) {
1249 recorded_step.success = response->success();
1250 recorded_step.message = response->message();
1251 recorded_step.execution_time_ms = response->execution_time_ms();
1252 recorded_step.test_id = response->test_id();
1253 MaybeRecordStep(&test_recorder_, recorded_step);
1257 if (!test_manager_) {
1258 response->set_success(
false);
1259 response->set_message(
"TestManager not available");
1260 response->set_execution_time_ms(0);
1261 return finalize(absl::FailedPreconditionError(
"TestManager not available"));
1264 const std::string requested_target = request ? request->target() :
"";
1265 const std::string requested_widget_key = request ? request->widget_key() :
"";
1266 const std::string request_summary =
1267 !requested_widget_key.empty()
1268 ? absl::StrFormat(
"widget_key:%s", requested_widget_key)
1271 const std::string test_id = test_manager_->RegisterHarnessTest(
1272 absl::StrFormat(
"Type: %s", request_summary),
"grpc");
1273 response->set_test_id(test_id);
1274 recorded_step.test_id = test_id;
1275 test_manager_->AppendHarnessTestLog(
1276 test_id, absl::StrFormat(
"Queued type request: %s", request_summary));
1278 absl::StatusOr<internal::ResolvedWidgetSelector> resolved_selector =
1279 internal::ResolveWidgetSelector(requested_target, requested_widget_key);
1280 if (!resolved_selector.ok()) {
1281 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1282 std::chrono::steady_clock::now() - start);
1283 std::string message = std::string(resolved_selector.status().message());
1284 response->set_success(
false);
1285 response->set_message(message);
1286 response->set_execution_time_ms(elapsed.count());
1287 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1289 test_manager_->AppendHarnessTestLog(test_id, message);
1290 return finalize(absl::OkStatus());
1293 if (!resolved_selector->resolved_widget_key.empty()) {
1294 response->set_resolved_widget_key(resolved_selector->resolved_widget_key);
1296 if (!resolved_selector->resolved_path.empty()) {
1297 response->set_resolved_path(resolved_selector->resolved_path);
1300 const std::string widget_type = resolved_selector->target.type;
1301 const std::string widget_label = resolved_selector->target.label;
1302 const ImGuiID widget_id = resolved_selector->imgui_id;
1304#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1305 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
1307 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1308 std::chrono::steady_clock::now() - start);
1309 std::string message =
"ImGuiTestEngine not initialized";
1310 response->set_success(
false);
1311 response->set_message(message);
1312 response->set_execution_time_ms(elapsed.count());
1313 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1315 return finalize(absl::OkStatus());
1317 std::string text = request->text();
1318 bool clear_first = request->clear_first();
1320 auto rpc_state = std::make_shared<RPCState<bool>>();
1321 auto test_data = std::make_shared<DynamicTestData>();
1322 TestManager* manager = test_manager_;
1323 test_data->test_func = [manager, captured_id = test_id, widget_type,
1324 widget_label, widget_id, clear_first, text,
1325 rpc_state](ImGuiTestContext* ctx) {
1326 manager->MarkHarnessTestRunning(captured_id);
1328 const ImGuiTestRef widget_ref = widget_id != 0
1329 ? ImGuiTestRef(widget_id)
1330 : ImGuiTestRef(widget_label.c_str());
1331 ImGuiTestItemInfo item =
1332 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1334 std::string error_message =
1335 absl::StrFormat(
"Input field '%s' not found", widget_label);
1336 manager->AppendHarnessTestLog(captured_id, error_message);
1337 manager->MarkHarnessTestCompleted(
1338 captured_id, HarnessTestStatus::kFailed, error_message);
1339 rpc_state->SetResult(
false, error_message);
1343 ctx->ItemClick(ImGuiTestRef(item.ID));
1345 ctx->KeyPress(ImGuiMod_Shortcut | ImGuiKey_A);
1346 ctx->KeyPress(ImGuiKey_Delete);
1349 ctx->ItemInputValue(ImGuiTestRef(item.ID), text.c_str());
1351 if (ctx->IsError()) {
1352 const std::string error_message =
1353 absl::StrFormat(
"Type failed for '%s'", widget_label);
1354 manager->AppendHarnessTestLog(captured_id, error_message);
1355 manager->MarkHarnessTestCompleted(
1356 captured_id, HarnessTestStatus::kFailed, error_message);
1357 rpc_state->SetResult(
false, error_message);
1361 std::string success_message =
1362 absl::StrFormat(
"Typed '%s' into %s '%s'%s", text, widget_type,
1363 widget_label, clear_first ?
" (cleared first)" :
"");
1364 manager->AppendHarnessTestLog(captured_id, success_message);
1365 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kPassed,
1367 rpc_state->SetResult(
true, success_message);
1368 }
catch (
const std::exception& e) {
1369 std::string error_message = absl::StrFormat(
"Type failed: %s", e.what());
1370 manager->AppendHarnessTestLog(captured_id, error_message);
1371 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
1373 rpc_state->SetResult(
false, error_message);
1377 std::string test_name = absl::StrFormat(
1379 static_cast<long long>(
1380 std::chrono::system_clock::now().time_since_epoch().count()));
1382 const auto queued = QueueDynamicTest(engine, test_manager_, ui_thread_queue_,
1383 test_name, test_id, test_data);
1385 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1386 std::string(queued.message()));
1390 auto timeout = std::chrono::seconds(5);
1391 auto wait_start = std::chrono::steady_clock::now();
1392 while (!rpc_state->completed.load()) {
1393 if (std::chrono::steady_clock::now() - wait_start > timeout) {
1394 std::string error_message =
1395 "Test timeout - input field not found or unresponsive";
1396 manager->AppendHarnessTestLog(test_id, error_message);
1397 manager->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kTimeout,
1399 rpc_state->SetResult(
false, error_message);
1402 std::this_thread::sleep_for(std::chrono::milliseconds(100));
1405 bool success =
false;
1406 std::string message;
1407 rpc_state->GetResult(success, message);
1408 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1409 std::chrono::steady_clock::now() - start);
1411 response->set_success(success);
1412 response->set_message(message);
1413 response->set_execution_time_ms(elapsed.count());
1414 if (!message.empty()) {
1415 test_manager_->AppendHarnessTestLog(test_id, message);
1419 test_manager_->MarkHarnessTestRunning(test_id);
1420 const std::string message =
"ImGuiTestEngine is not available in this build";
1421 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1423 test_manager_->AppendHarnessTestLog(test_id, message);
1425 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1426 std::chrono::steady_clock::now() - start);
1427 response->set_success(
false);
1428 response->set_message(message);
1429 response->set_execution_time_ms(elapsed.count());
1432 return finalize(absl::OkStatus());
1435absl::Status ImGuiTestHarnessServiceImpl::Wait(
const WaitRequest* request,
1436 WaitResponse* response) {
1437 auto start = std::chrono::steady_clock::now();
1439 TestRecorder::RecordedStep recorded_step;
1440 recorded_step.type = TestRecorder::ActionType::kWait;
1442 recorded_step.widget_key = request->widget_key();
1443 if (!request->widget_key().empty() && request->condition().empty()) {
1444 recorded_step.condition =
1445 absl::StrCat(
"widget_key:", request->widget_key());
1447 recorded_step.condition = request->condition();
1449 recorded_step.timeout_ms = request->timeout_ms();
1452 auto finalize = [&](
const absl::Status& status) {
1453 recorded_step.success = response->success();
1454 recorded_step.message = response->message();
1455 recorded_step.execution_time_ms = response->elapsed_ms();
1456 recorded_step.test_id = response->test_id();
1457 MaybeRecordStep(&test_recorder_, recorded_step);
1461 if (!test_manager_) {
1462 response->set_success(
false);
1463 response->set_message(
"TestManager not available");
1464 response->set_elapsed_ms(0);
1465 return finalize(absl::FailedPreconditionError(
"TestManager not available"));
1468 const std::string requested_condition = request ? request->condition() :
"";
1469 const std::string requested_widget_key = request ? request->widget_key() :
"";
1470 const std::string request_summary =
1471 !requested_widget_key.empty()
1472 ? absl::StrFormat(
"%s [widget_key:%s]",
1473 requested_condition.empty()
1474 ?
"element_visible:<auto>"
1475 : requested_condition,
1476 requested_widget_key)
1477 : requested_condition;
1479 const std::string test_id = test_manager_->RegisterHarnessTest(
1480 absl::StrFormat(
"Wait: %s", request_summary),
"grpc");
1481 response->set_test_id(test_id);
1482 recorded_step.test_id = test_id;
1483 test_manager_->AppendHarnessTestLog(
1484 test_id, absl::StrFormat(
"Queued wait condition: %s", request_summary));
1486 internal::ResolvedWidgetSelector resolved_selector;
1487 absl::StatusOr<ParsedCondition> parsed_condition = ResolveCondition(
1488 requested_condition, requested_widget_key,
1489 "element_visible", &resolved_selector);
1490 if (!parsed_condition.ok()) {
1491 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1492 std::chrono::steady_clock::now() - start);
1493 std::string message = std::string(parsed_condition.status().message());
1494 response->set_success(
false);
1495 response->set_message(message);
1496 response->set_elapsed_ms(elapsed.count());
1497 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1499 test_manager_->AppendHarnessTestLog(test_id, message);
1500 return finalize(absl::OkStatus());
1503 if (!resolved_selector.resolved_widget_key.empty()) {
1504 response->set_resolved_widget_key(resolved_selector.resolved_widget_key);
1506 if (!resolved_selector.resolved_path.empty()) {
1507 response->set_resolved_path(resolved_selector.resolved_path);
1510 const std::string condition_type = parsed_condition->type;
1511 const std::string condition_target = parsed_condition->target;
1512 const ImGuiID widget_id = resolved_selector.imgui_id;
1513 const std::string resolved_condition =
1514 absl::StrFormat(
"%s:%s", condition_type, condition_target);
1516#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1517 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
1519 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1520 std::chrono::steady_clock::now() - start);
1521 std::string message =
"ImGuiTestEngine not initialized";
1522 response->set_success(
false);
1523 response->set_message(message);
1524 response->set_elapsed_ms(elapsed.count());
1525 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1527 return finalize(absl::OkStatus());
1529 int timeout_ms = request->timeout_ms() > 0 ? request->timeout_ms() : 5000;
1530 int poll_interval_ms =
1531 request->poll_interval_ms() > 0 ? request->poll_interval_ms() : 100;
1533 auto test_data = std::make_shared<DynamicTestData>();
1534 TestManager* manager = test_manager_;
1535 test_data->test_func = [manager, captured_id = test_id, condition_type,
1536 condition_target, widget_id, timeout_ms,
1537 poll_interval_ms](ImGuiTestContext* ctx) {
1538 manager->MarkHarnessTestRunning(captured_id);
1539 auto poll_start = std::chrono::steady_clock::now();
1540 auto timeout = std::chrono::milliseconds(timeout_ms);
1542 for (
int i = 0; i < 10; ++i) {
1547 while (std::chrono::steady_clock::now() - poll_start < timeout) {
1548 bool current_state =
false;
1549 const ImGuiTestRef widget_ref =
1550 widget_id != 0 ? ImGuiTestRef(widget_id)
1551 : ImGuiTestRef(condition_target.c_str());
1553 if (condition_type ==
"window_visible") {
1554 ImGuiTestItemInfo window_info =
1555 ctx->WindowInfo(widget_ref, ImGuiTestOpFlags_NoError);
1556 current_state = (window_info.ID != 0);
1557 }
else if (condition_type ==
"element_visible") {
1558 ImGuiTestItemInfo item =
1559 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1560 current_state = (item.ID != 0 && item.RectClipped.GetWidth() > 0 &&
1561 item.RectClipped.GetHeight() > 0);
1562 }
else if (condition_type ==
"element_enabled") {
1563 ImGuiTestItemInfo item =
1564 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1566 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
1568 std::string error_message =
1569 absl::StrFormat(
"Unknown condition type: %s", condition_type);
1570 manager->AppendHarnessTestLog(captured_id, error_message);
1571 manager->MarkHarnessTestCompleted(
1572 captured_id, HarnessTestStatus::kFailed, error_message);
1576 if (current_state) {
1578 std::chrono::duration_cast<std::chrono::milliseconds>(
1579 std::chrono::steady_clock::now() - poll_start);
1580 std::string success_message = absl::StrFormat(
1581 "Condition '%s:%s' met after %lld ms", condition_type,
1582 condition_target,
static_cast<long long>(elapsed_ms.count()));
1583 manager->AppendHarnessTestLog(captured_id, success_message);
1584 manager->MarkHarnessTestCompleted(
1585 captured_id, HarnessTestStatus::kPassed, success_message);
1589 std::this_thread::sleep_for(
1590 std::chrono::milliseconds(poll_interval_ms));
1594 std::string timeout_message =
1595 absl::StrFormat(
"Condition '%s:%s' not met after %d ms timeout",
1596 condition_type, condition_target, timeout_ms);
1597 manager->AppendHarnessTestLog(captured_id, timeout_message);
1598 manager->MarkHarnessTestCompleted(
1599 captured_id, HarnessTestStatus::kTimeout, timeout_message);
1600 }
catch (
const std::exception& e) {
1601 std::string error_message = absl::StrFormat(
"Wait failed: %s", e.what());
1602 manager->AppendHarnessTestLog(captured_id, error_message);
1603 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
1608 std::string test_name = absl::StrFormat(
1610 static_cast<long long>(
1611 std::chrono::system_clock::now().time_since_epoch().count()));
1613 const auto queued = QueueDynamicTest(engine, test_manager_, ui_thread_queue_,
1614 test_name, test_id, test_data);
1616 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1617 std::string(queued.message()));
1621 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1622 std::chrono::steady_clock::now() - start);
1623 std::string message =
1624 absl::StrFormat(
"Queued wait for '%s'", resolved_condition);
1625 response->set_success(
true);
1626 response->set_message(message);
1627 response->set_elapsed_ms(elapsed.count());
1628 test_manager_->AppendHarnessTestLog(test_id, message);
1631 test_manager_->MarkHarnessTestRunning(test_id);
1632 const std::string message =
"ImGuiTestEngine is not available in this build";
1633 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1635 test_manager_->AppendHarnessTestLog(test_id, message);
1637 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1638 std::chrono::steady_clock::now() - start);
1639 response->set_success(
false);
1640 response->set_message(message);
1641 response->set_elapsed_ms(elapsed.count());
1644 return finalize(absl::OkStatus());
1647absl::Status ImGuiTestHarnessServiceImpl::Assert(
const AssertRequest* request,
1648 AssertResponse* response) {
1649 TestRecorder::RecordedStep recorded_step;
1650 recorded_step.type = TestRecorder::ActionType::kAssert;
1652 recorded_step.widget_key = request->widget_key();
1653 if (!request->widget_key().empty() && request->condition().empty()) {
1654 recorded_step.condition =
1655 absl::StrCat(
"widget_key:", request->widget_key());
1657 recorded_step.condition = request->condition();
1661 auto finalize = [&](
const absl::Status& status) {
1662 recorded_step.success = response->success();
1663 recorded_step.message = response->message();
1664 recorded_step.expected_value = response->expected_value();
1665 recorded_step.actual_value = response->actual_value();
1666 recorded_step.test_id = response->test_id();
1667 MaybeRecordStep(&test_recorder_, recorded_step);
1671 if (!test_manager_) {
1672 response->set_success(
false);
1673 response->set_message(
"TestManager not available");
1674 response->set_actual_value(
"N/A");
1675 response->set_expected_value(
"N/A");
1676 return finalize(absl::FailedPreconditionError(
"TestManager not available"));
1679 const std::string requested_condition = request ? request->condition() :
"";
1680 const std::string requested_widget_key = request ? request->widget_key() :
"";
1681 const std::string request_summary =
1682 !requested_widget_key.empty()
1683 ? absl::StrFormat(
"%s [widget_key:%s]",
1684 requested_condition.empty() ?
"exists:<auto>"
1685 : requested_condition,
1686 requested_widget_key)
1687 : requested_condition;
1689 const std::string test_id = test_manager_->RegisterHarnessTest(
1690 absl::StrFormat(
"Assert: %s", request_summary),
"grpc");
1691 response->set_test_id(test_id);
1692 recorded_step.test_id = test_id;
1693 test_manager_->AppendHarnessTestLog(
1694 test_id, absl::StrFormat(
"Queued assertion: %s", request_summary));
1696 internal::ResolvedWidgetSelector resolved_selector;
1697 absl::StatusOr<ParsedCondition> parsed_condition = ResolveCondition(
1698 requested_condition, requested_widget_key,
1699 "exists", &resolved_selector);
1700 if (!parsed_condition.ok()) {
1701 std::string message = std::string(parsed_condition.status().message());
1702 response->set_success(
false);
1703 response->set_message(message);
1704 response->set_actual_value(
"N/A");
1705 response->set_expected_value(
"N/A");
1706 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1708 test_manager_->AppendHarnessTestLog(test_id, message);
1709 return finalize(absl::OkStatus());
1712 if (!resolved_selector.resolved_widget_key.empty()) {
1713 response->set_resolved_widget_key(resolved_selector.resolved_widget_key);
1715 if (!resolved_selector.resolved_path.empty()) {
1716 response->set_resolved_path(resolved_selector.resolved_path);
1719 const std::string assertion_type = parsed_condition->type;
1720 const std::string assertion_target = parsed_condition->target;
1721 const ImGuiID widget_id = resolved_selector.imgui_id;
1722 const std::string widget_label = resolved_selector.target.label;
1724 if (assertion_type ==
"value_equals" && widget_id == 0) {
1725 const std::string message =
1726 "value_equals requires widget_key so the input is unambiguous";
1727 response->set_success(
false);
1728 response->set_message(message);
1729 response->set_actual_value(
"N/A");
1730 response->set_expected_value(assertion_target);
1731 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1733 test_manager_->AppendHarnessTestLog(test_id, message);
1734 return finalize(absl::OkStatus());
1737#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1738 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
1740 std::string message =
"ImGuiTestEngine not initialized";
1741 response->set_success(
false);
1742 response->set_message(message);
1743 response->set_actual_value(
"N/A");
1744 response->set_expected_value(
"N/A");
1745 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1747 return finalize(absl::OkStatus());
1750 auto test_data = std::make_shared<DynamicTestData>();
1751 TestManager* manager = test_manager_;
1752 test_data->test_func = [manager, captured_id = test_id, assertion_type,
1753 assertion_target, widget_id,
1754 widget_label](ImGuiTestContext* ctx) {
1755 manager->MarkHarnessTestRunning(captured_id);
1757 auto complete_with = [manager, captured_id](
bool passed,
1758 const std::string& message,
1759 const std::string& actual,
1760 const std::string& expected,
1761 HarnessTestStatus status) {
1762 manager->AppendHarnessTestLog(captured_id, message);
1763 if (!actual.empty() || !expected.empty()) {
1764 manager->AppendHarnessTestLog(
1766 absl::StrFormat(
"Actual: %s | Expected: %s", actual, expected));
1768 manager->MarkHarnessTestCompleted(captured_id, status,
1769 passed ?
"" : message);
1773 bool passed =
false;
1774 std::string actual_value;
1775 std::string expected_value;
1776 std::string message;
1777 const ImGuiTestRef widget_ref =
1778 widget_id != 0 ? ImGuiTestRef(widget_id)
1779 : ImGuiTestRef(assertion_target.c_str());
1781 if (assertion_type ==
"visible") {
1782 const ImGuiTestItemInfo item =
1784 ? ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError)
1785 : ctx->WindowInfo(widget_ref, ImGuiTestOpFlags_NoError);
1786 const bool is_visible =
1788 (widget_id == 0 || (item.RectClipped.GetWidth() > 0 &&
1789 item.RectClipped.GetHeight() > 0));
1790 passed = is_visible;
1791 actual_value = is_visible ?
"visible" :
"hidden";
1792 expected_value =
"visible";
1793 const std::string target_description =
1794 widget_id != 0 ? widget_label : assertion_target;
1796 passed ? absl::StrFormat(
"'%s' is visible", target_description)
1797 : absl::StrFormat(
"'%s' is not visible", target_description);
1798 }
else if (assertion_type ==
"enabled") {
1799 ImGuiTestItemInfo item =
1800 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1802 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
1803 passed = is_enabled;
1804 actual_value = is_enabled ?
"enabled" :
"disabled";
1805 expected_value =
"enabled";
1807 passed ? absl::StrFormat(
"'%s' is enabled", assertion_target)
1808 : absl::StrFormat(
"'%s' is not enabled", assertion_target);
1809 }
else if (assertion_type ==
"exists") {
1810 ImGuiTestItemInfo item =
1811 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1812 bool exists = (item.ID != 0);
1814 actual_value = exists ?
"exists" :
"not found";
1815 expected_value =
"exists";
1816 message = passed ? absl::StrFormat(
"'%s' exists", assertion_target)
1817 : absl::StrFormat(
"'%s' not found", assertion_target);
1818 }
else if (assertion_type ==
"value_equals") {
1819 const ImGuiTestItemInfo item =
1820 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1823 actual_value =
"not found";
1824 expected_value = assertion_target;
1825 message = absl::StrFormat(
"Input '%s' not found", widget_label);
1827 const char* value = ctx->ItemReadAsString(ImGuiTestRef(item.ID));
1828 actual_value = value ? value :
"";
1829 expected_value = assertion_target;
1830 passed = !ctx->IsError() && actual_value == expected_value;
1831 message = passed ? absl::StrFormat(
"'%s' equals '%s'", widget_label,
1834 "'%s' does not equal '%s' (actual: '%s')",
1835 widget_label, expected_value, actual_value);
1837 }
else if (assertion_type ==
"text_contains") {
1838 size_t second_colon = assertion_target.find(
':');
1839 if (widget_id == 0 && second_colon == std::string::npos) {
1840 std::string error_message =
1841 "text_contains requires format "
1842 "'text_contains:target:expected_text'";
1843 complete_with(
false, error_message,
"N/A",
"N/A",
1844 HarnessTestStatus::kFailed);
1848 const std::string input_target =
1849 widget_id != 0 ? widget_label
1850 : assertion_target.substr(0, second_colon);
1851 const std::string expected_text =
1852 widget_id != 0 ? assertion_target
1853 : assertion_target.substr(second_colon + 1);
1854 const ImGuiTestRef input_ref = widget_id != 0
1855 ? ImGuiTestRef(widget_id)
1856 : ImGuiTestRef(input_target.c_str());
1858 ImGuiTestItemInfo item =
1859 ctx->ItemInfo(input_ref, ImGuiTestOpFlags_NoError);
1861 const char* value = ctx->ItemReadAsString(ImGuiTestRef(item.ID));
1862 const std::string actual_text = value ? value :
"";
1863 passed = actual_text.find(expected_text) != std::string::npos;
1864 actual_value = actual_text;
1865 expected_value = absl::StrFormat(
"contains '%s'", expected_text);
1866 message = passed ? absl::StrFormat(
"'%s' contains '%s'", input_target,
1869 "'%s' does not contain '%s' (actual: '%s')",
1870 input_target, expected_text, actual_text);
1873 actual_value =
"not found";
1874 expected_value = expected_text;
1875 message = absl::StrFormat(
"Input '%s' not found", input_target);
1878 std::string error_message =
1879 absl::StrFormat(
"Unknown assertion type: %s", assertion_type);
1880 complete_with(
false, error_message,
"N/A",
"N/A",
1881 HarnessTestStatus::kFailed);
1886 passed, message, actual_value, expected_value,
1887 passed ? HarnessTestStatus::kPassed : HarnessTestStatus::
kFailed);
1888 }
catch (
const std::exception& e) {
1889 std::string error_message =
1890 absl::StrFormat(
"Assertion failed: %s", e.what());
1891 manager->AppendHarnessTestLog(captured_id, error_message);
1892 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
1897 std::string test_name = absl::StrFormat(
1899 static_cast<long long>(
1900 std::chrono::system_clock::now().time_since_epoch().count()));
1902 const auto queued = QueueDynamicTest(engine, test_manager_, ui_thread_queue_,
1903 test_name, test_id, test_data);
1905 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1906 std::string(queued.message()));
1910 response->set_success(
true);
1911 std::string message = absl::StrFormat(
"Queued assertion for '%s:%s'",
1912 assertion_type, assertion_target);
1913 response->set_message(message);
1914 response->set_actual_value(
"(async)");
1915 response->set_expected_value(
"(async)");
1916 test_manager_->AppendHarnessTestLog(test_id, message);
1919 test_manager_->MarkHarnessTestRunning(test_id);
1920 const std::string message =
"ImGuiTestEngine is not available in this build";
1921 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1923 test_manager_->AppendHarnessTestLog(test_id, message);
1925 response->set_success(
false);
1926 response->set_message(message);
1927 response->set_actual_value(
"(stub)");
1928 response->set_expected_value(
"(stub)");
1931 return finalize(absl::OkStatus());
1934absl::Status ImGuiTestHarnessServiceImpl::FlushUiActions(
1935 const FlushUiActionsRequest* request, FlushUiActionsResponse* response) {
1937 return absl::InvalidArgumentError(
"response cannot be null");
1940 const int32_t timeout_ms =
1941 NormalizeTimeoutMs(request ? request->timeout_ms() : 0, 2000);
1942 const absl::Duration timeout = absl::Milliseconds(timeout_ms);
1943 const absl::Time deadline = absl::Now() + timeout;
1944 const absl::Time started_at = absl::Now();
1946 UiSyncSnapshot initial = CaptureUiSyncSnapshot(test_manager_);
1947 UiSyncSnapshot snapshot = initial;
1949 if (initial.pending_editor_actions == 0 &&
1950 initial.pending_layout_actions == 0 && !initial.layout_rebuild_pending) {
1951 response->set_success(
true);
1952 response->set_message(
"UI queues already idle");
1953 response->set_elapsed_ms(0);
1954 response->set_frame_id(initial.frame_id);
1955 response->set_pending_editor_actions(initial.pending_editor_actions);
1956 response->set_pending_layout_actions(initial.pending_layout_actions);
1957 response->set_layout_rebuild_pending(initial.layout_rebuild_pending);
1958 response->set_pending_harness_tests(initial.pending_harness_tests);
1959 response->set_queued_harness_tests(initial.queued_harness_tests);
1960 response->set_running_harness_tests(initial.running_harness_tests);
1961 return absl::OkStatus();
1964 while (absl::Now() <= deadline) {
1965 snapshot = CaptureUiSyncSnapshot(test_manager_);
1966 const bool frame_advanced = snapshot.frame_id > initial.frame_id;
1967 const bool queues_flushed = snapshot.pending_editor_actions == 0 &&
1968 snapshot.pending_layout_actions == 0 &&
1969 !snapshot.layout_rebuild_pending;
1970 if (frame_advanced && queues_flushed) {
1973 absl::SleepFor(absl::Milliseconds(10));
1976 const int32_t elapsed_ms = ClampDurationToInt32(absl::Now() - started_at);
1977 const bool success = snapshot.pending_editor_actions == 0 &&
1978 snapshot.pending_layout_actions == 0 &&
1979 !snapshot.layout_rebuild_pending;
1980 response->set_success(success);
1981 response->set_message(success ?
"Deferred UI actions flushed"
1982 :
"Timed out waiting for UI action flush");
1983 response->set_elapsed_ms(elapsed_ms);
1984 response->set_frame_id(snapshot.frame_id);
1985 response->set_pending_editor_actions(snapshot.pending_editor_actions);
1986 response->set_pending_layout_actions(snapshot.pending_layout_actions);
1987 response->set_layout_rebuild_pending(snapshot.layout_rebuild_pending);
1988 response->set_pending_harness_tests(snapshot.pending_harness_tests);
1989 response->set_queued_harness_tests(snapshot.queued_harness_tests);
1990 response->set_running_harness_tests(snapshot.running_harness_tests);
1991 return absl::OkStatus();
1994absl::Status ImGuiTestHarnessServiceImpl::WaitForIdle(
1995 const WaitForIdleRequest* request, WaitForIdleResponse* response) {
1997 return absl::InvalidArgumentError(
"response cannot be null");
2000 const int32_t timeout_ms =
2001 NormalizeTimeoutMs(request ? request->timeout_ms() : 0, 5000);
2002 const int32_t required_stable_frames =
2003 std::max(1, request ? request->stable_frames() : 2);
2004 const int32_t poll_interval_ms =
2005 NormalizePollIntervalMs(request ? request->poll_interval_ms() : 25);
2006 const bool flush_first = request ? request->flush_first() :
false;
2008 const absl::Time started_at = absl::Now();
2009 const absl::Time deadline = started_at + absl::Milliseconds(timeout_ms);
2012 FlushUiActionsRequest flush_request;
2013 flush_request.set_timeout_ms(std::min(timeout_ms, 2000));
2014 FlushUiActionsResponse flush_response;
2015 absl::Status flush_status = FlushUiActions(&flush_request, &flush_response);
2016 if (!flush_status.ok()) {
2017 return flush_status;
2021 UiSyncSnapshot snapshot = CaptureUiSyncSnapshot(test_manager_);
2022 int32_t stable_frames_observed = 0;
2023 int64_t last_frame_id = -1;
2025 while (absl::Now() <= deadline) {
2026 snapshot = CaptureUiSyncSnapshot(test_manager_);
2028 const bool is_idle_now = IsUiIdle(snapshot);
2029 if (snapshot.frame_id != last_frame_id) {
2030 last_frame_id = snapshot.frame_id;
2031 stable_frames_observed = is_idle_now ? (stable_frames_observed + 1) : 0;
2032 }
else if (!is_idle_now) {
2033 stable_frames_observed = 0;
2036 if (stable_frames_observed >= required_stable_frames) {
2037 response->set_success(
true);
2038 response->set_message(absl::StrFormat(
2039 "UI idle for %d consecutive frame(s)", stable_frames_observed));
2040 response->set_elapsed_ms(ClampDurationToInt32(absl::Now() - started_at));
2041 response->set_last_frame_id(snapshot.frame_id);
2042 response->set_stable_frames_observed(stable_frames_observed);
2043 response->set_pending_editor_actions(snapshot.pending_editor_actions);
2044 response->set_pending_layout_actions(snapshot.pending_layout_actions);
2045 response->set_layout_rebuild_pending(snapshot.layout_rebuild_pending);
2046 response->set_pending_harness_tests(snapshot.pending_harness_tests);
2047 response->set_queued_harness_tests(snapshot.queued_harness_tests);
2048 response->set_running_harness_tests(snapshot.running_harness_tests);
2049 response->set_timeout_reason(
"");
2050 return absl::OkStatus();
2053 absl::SleepFor(absl::Milliseconds(poll_interval_ms));
2056 const std::string timeout_reason = BuildIdleTimeoutReason(snapshot);
2057 response->set_success(
false);
2058 response->set_message(
"Timed out waiting for deterministic UI idle");
2059 response->set_elapsed_ms(ClampDurationToInt32(absl::Now() - started_at));
2060 response->set_last_frame_id(snapshot.frame_id);
2061 response->set_stable_frames_observed(stable_frames_observed);
2062 response->set_pending_editor_actions(snapshot.pending_editor_actions);
2063 response->set_pending_layout_actions(snapshot.pending_layout_actions);
2064 response->set_layout_rebuild_pending(snapshot.layout_rebuild_pending);
2065 response->set_pending_harness_tests(snapshot.pending_harness_tests);
2066 response->set_queued_harness_tests(snapshot.queued_harness_tests);
2067 response->set_running_harness_tests(snapshot.running_harness_tests);
2068 response->set_timeout_reason(timeout_reason);
2069 return absl::OkStatus();
2072absl::Status ImGuiTestHarnessServiceImpl::GetUiSyncState(
2073 const GetUiSyncStateRequest* ,
2074 GetUiSyncStateResponse* response) {
2076 return absl::InvalidArgumentError(
"response cannot be null");
2079 const UiSyncSnapshot snapshot = CaptureUiSyncSnapshot(test_manager_);
2080 response->set_frame_id(snapshot.frame_id);
2081 response->set_pending_editor_actions(snapshot.pending_editor_actions);
2082 response->set_pending_layout_actions(snapshot.pending_layout_actions);
2083 response->set_layout_rebuild_pending(snapshot.layout_rebuild_pending);
2084 response->set_pending_harness_tests(snapshot.pending_harness_tests);
2085 response->set_queued_harness_tests(snapshot.queued_harness_tests);
2086 response->set_running_harness_tests(snapshot.running_harness_tests);
2087 response->set_sampled_at_ms(absl::ToUnixMillis(absl::Now()));
2088 response->set_idle(IsUiIdle(snapshot));
2089 return absl::OkStatus();
2092absl::Status ImGuiTestHarnessServiceImpl::Screenshot(
2093 const ScreenshotRequest* request, ScreenshotResponse* response) {
2095 return absl::InvalidArgumentError(
"response cannot be null");
2099 const std::string requested_path =
2100 request ? request->output_path() : std::string();
2103 switch (request->format()) {
2104 case ScreenshotRequest::IMAGE_FORMAT_UNSPECIFIED:
2106 case ScreenshotRequest::IMAGE_FORMAT_PNG:
2107 format = ScreenshotFormat::kPng;
2109 case ScreenshotRequest::IMAGE_FORMAT_BMP:
2110 format = ScreenshotFormat::kBmp;
2112 case ScreenshotRequest::IMAGE_FORMAT_JPEG:
2113 return absl::UnimplementedError(
2114 "JPEG screenshots are not supported; request PNG or BMP");
2116 return absl::InvalidArgumentError(
"Unknown screenshot image format");
2119 auto format_or = ResolveScreenshotFormat(requested_path, format);
2120 if (!format_or.ok()) {
2121 return format_or.status();
2123 auto result = CaptureScreenshotOnRenderThread(
2124 requested_path, request ? request->window_title() : std::string(),
2126 if (absl::IsDeadlineExceeded(result.status())) {
2127 return result.status();
2130 response->set_success(
false);
2131 response->set_message(std::string(result.status().message()));
2132 return result.status();
2135 const ScreenshotArtifact& artifact = *result;
2136 response->set_success(
true);
2137 response->set_message(absl::StrFormat(
"Screenshot saved to %s (%dx%d)",
2138 artifact.file_path, artifact.width,
2140 response->set_file_path(artifact.file_path);
2141 response->set_file_size_bytes(artifact.file_size_bytes);
2143 return absl::OkStatus();
2146absl::Status ImGuiTestHarnessServiceImpl::GetTestStatus(
2147 const GetTestStatusRequest* request, GetTestStatusResponse* response) {
2148 if (!test_manager_) {
2149 return absl::FailedPreconditionError(
"TestManager not available");
2152 if (request->test_id().empty()) {
2153 return absl::InvalidArgumentError(
"test_id must be provided");
2157 test_manager_->GetHarnessTestExecution(request->test_id());
2158 if (!execution_or.ok()) {
2159 response->set_status(GetTestStatusResponse::TEST_STATUS_UNSPECIFIED);
2160 response->set_error_message(std::string(execution_or.status().message()));
2161 return absl::OkStatus();
2164 const auto& execution = execution_or.value();
2165 response->set_status(ConvertHarnessStatus(execution.status));
2166 response->set_queued_at_ms(ToUnixMillisSafe(execution.queued_at));
2167 response->set_started_at_ms(ToUnixMillisSafe(execution.started_at));
2168 response->set_completed_at_ms(ToUnixMillisSafe(execution.completed_at));
2169 response->set_execution_time_ms(ClampDurationToInt32(execution.duration));
2170 if (!execution.error_message.empty()) {
2171 response->set_error_message(execution.error_message);
2173 response->clear_error_message();
2176 response->clear_assertion_failures();
2177 for (
const auto& failure : execution.assertion_failures) {
2178 response->add_assertion_failures(failure);
2181 return absl::OkStatus();
2184absl::Status ImGuiTestHarnessServiceImpl::ListTests(
2185 const ListTestsRequest* request, ListTestsResponse* response) {
2186 if (!test_manager_) {
2187 return absl::FailedPreconditionError(
"TestManager not available");
2190 if (request->page_size() < 0) {
2191 return absl::InvalidArgumentError(
"page_size cannot be negative");
2194 int page_size = request->page_size() > 0 ? request->page_size() : 100;
2195 constexpr int kMaxPageSize = 500;
2196 if (page_size > kMaxPageSize) {
2197 page_size = kMaxPageSize;
2200 size_t start_index = 0;
2201 if (!request->page_token().empty()) {
2202 int64_t token_value = 0;
2203 if (!absl::SimpleAtoi(request->page_token(), &token_value) ||
2205 return absl::InvalidArgumentError(
"Invalid page_token");
2207 start_index =
static_cast<size_t>(token_value);
2211 test_manager_->ListHarnessTestSummaries(request->category_filter());
2213 response->set_total_count(
static_cast<int32_t
>(summaries.size()));
2215 if (start_index >= summaries.size()) {
2216 response->clear_tests();
2217 response->clear_next_page_token();
2218 return absl::OkStatus();
2222 std::min(start_index +
static_cast<size_t>(page_size), summaries.size());
2224 for (
size_t i = start_index; i < end_index; ++i) {
2225 const auto& summary = summaries[i];
2226 auto* test_info = response->add_tests();
2227 const auto& exec = summary.latest_execution;
2229 test_info->set_test_id(exec.test_id);
2230 test_info->set_name(exec.name);
2231 test_info->set_category(exec.category);
2233 int64_t last_run_ms = ToUnixMillisSafe(exec.completed_at);
2234 if (last_run_ms == 0) {
2235 last_run_ms = ToUnixMillisSafe(exec.started_at);
2237 if (last_run_ms == 0) {
2238 last_run_ms = ToUnixMillisSafe(exec.queued_at);
2240 test_info->set_last_run_timestamp_ms(last_run_ms);
2242 test_info->set_total_runs(summary.total_runs);
2243 test_info->set_pass_count(summary.pass_count);
2244 test_info->set_fail_count(summary.fail_count);
2246 int32_t average_duration_ms = 0;
2247 if (summary.total_runs > 0) {
2248 absl::Duration average_duration =
2249 summary.total_duration / summary.total_runs;
2250 average_duration_ms = ClampDurationToInt32(average_duration);
2252 test_info->set_average_duration_ms(average_duration_ms);
2255 if (end_index < summaries.size()) {
2256 response->set_next_page_token(absl::StrCat(end_index));
2258 response->clear_next_page_token();
2261 return absl::OkStatus();
2264absl::Status ImGuiTestHarnessServiceImpl::GetTestResults(
2265 const GetTestResultsRequest* request, GetTestResultsResponse* response) {
2266 if (!test_manager_) {
2267 return absl::FailedPreconditionError(
"TestManager not available");
2270 if (request->test_id().empty()) {
2271 return absl::InvalidArgumentError(
"test_id must be provided");
2275 test_manager_->GetHarnessTestExecution(request->test_id());
2276 if (!execution_or.ok()) {
2277 return execution_or.status();
2280 const auto& execution = execution_or.value();
2281 response->set_success(execution.status == HarnessTestStatus::kPassed);
2282 response->set_test_name(execution.name);
2283 response->set_category(execution.category);
2285 int64_t executed_at_ms = ToUnixMillisSafe(execution.completed_at);
2286 if (executed_at_ms == 0) {
2287 executed_at_ms = ToUnixMillisSafe(execution.started_at);
2289 if (executed_at_ms == 0) {
2290 executed_at_ms = ToUnixMillisSafe(execution.queued_at);
2292 response->set_executed_at_ms(executed_at_ms);
2293 response->set_duration_ms(ClampDurationToInt32(execution.duration));
2295 response->clear_assertions();
2296 if (!execution.assertion_failures.empty()) {
2297 for (
const auto& failure : execution.assertion_failures) {
2298 auto* assertion = response->add_assertions();
2299 assertion->set_description(failure);
2300 assertion->set_passed(
false);
2301 assertion->set_error_message(failure);
2303 }
else if (!execution.error_message.empty()) {
2304 auto* assertion = response->add_assertions();
2305 assertion->set_description(
"Execution error");
2306 assertion->set_passed(
false);
2307 assertion->set_error_message(execution.error_message);
2310 if (request->include_logs()) {
2311 for (
const auto& log_entry : execution.logs) {
2312 response->add_logs(log_entry);
2316 auto* metrics_map = response->mutable_metrics();
2317 for (
const auto& [key, value] : execution.metrics) {
2318 (*metrics_map)[key] = value;
2322 if (!execution.screenshot_path.empty()) {
2323 response->set_screenshot_path(execution.screenshot_path);
2324 response->set_screenshot_size_bytes(execution.screenshot_size_bytes);
2326 if (!execution.failure_context.empty()) {
2327 response->set_failure_context(execution.failure_context);
2329 if (!execution.widget_state.empty()) {
2330 response->set_widget_state(execution.widget_state);
2333 return absl::OkStatus();
2336absl::Status ImGuiTestHarnessServiceImpl::DiscoverWidgets(
2337 const DiscoverWidgetsRequest* request, DiscoverWidgetsResponse* response) {
2339 return absl::InvalidArgumentError(
"request cannot be null");
2342 return absl::InvalidArgumentError(
"response cannot be null");
2345 if (!test_manager_) {
2346 return absl::FailedPreconditionError(
"TestManager not available");
2349 widget_discovery_service_.CollectWidgets(
nullptr, *request, response);
2350 return absl::OkStatus();
2353absl::Status ImGuiTestHarnessServiceImpl::StartRecording(
2354 const StartRecordingRequest* request, StartRecordingResponse* response) {
2356 return absl::InvalidArgumentError(
"request cannot be null");
2359 return absl::InvalidArgumentError(
"response cannot be null");
2362 TestRecorder::RecordingOptions options;
2363 options.output_path = request->output_path();
2364 options.session_name = request->session_name();
2365 options.description = request->description();
2367 if (options.output_path.empty()) {
2368 response->set_success(
false);
2369 response->set_message(
"output_path is required to start recording");
2370 return absl::InvalidArgumentError(
"output_path cannot be empty");
2373 absl::StatusOr<std::string> recording_id = test_recorder_.Start(options);
2374 if (!recording_id.ok()) {
2375 response->set_success(
false);
2376 response->set_message(std::string(recording_id.status().message()));
2377 return recording_id.status();
2380 response->set_success(
true);
2381 response->set_message(
"Recording started");
2382 response->set_recording_id(*recording_id);
2383 response->set_started_at_ms(absl::ToUnixMillis(absl::Now()));
2384 return absl::OkStatus();
2387absl::Status ImGuiTestHarnessServiceImpl::StopRecording(
2388 const StopRecordingRequest* request, StopRecordingResponse* response) {
2390 return absl::InvalidArgumentError(
"request cannot be null");
2393 return absl::InvalidArgumentError(
"response cannot be null");
2396 absl::StatusOr<TestRecorder::StopRecordingSummary> summary =
2397 test_recorder_.Stop(request->recording_id(), request->discard());
2398 if (!summary.ok()) {
2399 response->set_success(
false);
2400 response->set_message(std::string(summary.status().message()));
2401 return summary.status();
2404 response->set_success(
true);
2405 if (summary->saved) {
2406 response->set_message(
"Recording saved");
2408 response->set_message(
"Recording discarded");
2410 response->set_output_path(summary->output_path);
2411 response->set_step_count(summary->step_count);
2412 response->set_duration_ms(absl::ToInt64Milliseconds(summary->duration));
2413 return absl::OkStatus();
2416absl::Status ImGuiTestHarnessServiceImpl::ReplayTest(
2417 const ReplayTestRequest* request, ReplayTestResponse* response) {
2419 return absl::InvalidArgumentError(
"request cannot be null");
2422 return absl::InvalidArgumentError(
"response cannot be null");
2425 response->clear_logs();
2426 response->clear_assertions();
2428 if (request->script_path().empty()) {
2429 response->set_success(
false);
2430 response->set_message(
"script_path is required");
2431 return absl::InvalidArgumentError(
"script_path cannot be empty");
2434 absl::StatusOr<TestScript> script_or =
2435 TestScriptParser::ParseFromFile(request->script_path());
2436 if (!script_or.ok()) {
2437 response->set_success(
false);
2438 response->set_message(std::string(script_or.status().message()));
2439 return script_or.status();
2441 TestScript script = std::move(*script_or);
2443 absl::flat_hash_map<std::string, std::string> overrides;
2444 for (
const auto& entry : request->parameter_overrides()) {
2445 overrides[entry.first] = entry.second;
2448 response->set_replay_session_id(absl::StrFormat(
2450 absl::FormatTime(
"%Y%m%dT%H%M%S", absl::Now(), absl::UTCTimeZone())));
2452 auto suspension = test_recorder_.Suspend();
2453#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
2454 ScopedRemoteReplaySubmission remote_submission;
2457 std::vector<std::string> logs;
2458 logs.reserve(script.steps.size() * 2 + 4);
2460 bool overall_success =
true;
2461 std::string overall_message =
"Replay completed successfully";
2462 int steps_executed = 0;
2463 absl::Status overall_rpc_status = absl::OkStatus();
2465 for (
const auto& step : script.steps) {
2467 std::string action_label =
2468 absl::StrFormat(
"Step %d: %s", steps_executed, step.action);
2469 logs.push_back(action_label);
2471 absl::Status status = absl::OkStatus();
2472 bool step_success =
false;
2473 std::string step_message;
2474 HarnessTestExecution execution;
2475 bool have_execution =
false;
2477 if (step.action ==
"click") {
2478 ClickRequest sub_request;
2479 sub_request.set_target(ApplyOverrides(step.target, overrides));
2480 sub_request.set_widget_key(ApplyOverrides(step.widget_key, overrides));
2481 sub_request.set_type(ClickTypeFromString(step.click_type));
2482 ClickResponse sub_response;
2483 status = Click(&sub_request, &sub_response);
2484 step_success = sub_response.success();
2485 step_message = sub_response.message();
2486 if (status.ok() && !sub_response.test_id().empty()) {
2487 absl::Status wait_status = WaitForHarnessTestCompletion(
2488 test_manager_, sub_response.test_id(), &execution);
2489 if (wait_status.ok()) {
2490 have_execution =
true;
2491 internal::ApplyTerminalHarnessExecution(execution, &step_success,
2494 status = wait_status;
2495 step_success =
false;
2496 step_message = std::string(wait_status.message());
2499 }
else if (step.action ==
"type") {
2500 TypeRequest sub_request;
2501 sub_request.set_target(ApplyOverrides(step.target, overrides));
2502 sub_request.set_widget_key(ApplyOverrides(step.widget_key, overrides));
2503 sub_request.set_text(ApplyOverrides(step.text, overrides));
2504 sub_request.set_clear_first(step.clear_first);
2505 TypeResponse sub_response;
2506 status = Type(&sub_request, &sub_response);
2507 step_success = sub_response.success();
2508 step_message = sub_response.message();
2509 if (status.ok() && !sub_response.test_id().empty()) {
2510 absl::Status wait_status = WaitForHarnessTestCompletion(
2511 test_manager_, sub_response.test_id(), &execution);
2512 if (wait_status.ok()) {
2513 have_execution =
true;
2514 internal::ApplyTerminalHarnessExecution(execution, &step_success,
2517 status = wait_status;
2518 step_success =
false;
2519 step_message = std::string(wait_status.message());
2522 }
else if (step.action ==
"wait") {
2523 WaitRequest sub_request;
2524 sub_request.set_condition(ApplyOverrides(step.condition, overrides));
2525 sub_request.set_widget_key(ApplyOverrides(step.widget_key, overrides));
2526 if (step.timeout_ms > 0) {
2527 sub_request.set_timeout_ms(step.timeout_ms);
2529 WaitResponse sub_response;
2530 status = Wait(&sub_request, &sub_response);
2531 step_success = sub_response.success();
2532 step_message = sub_response.message();
2533 if (status.ok() && !sub_response.test_id().empty()) {
2534 absl::Status wait_status = WaitForHarnessTestCompletion(
2535 test_manager_, sub_response.test_id(), &execution);
2536 if (wait_status.ok()) {
2537 have_execution =
true;
2538 internal::ApplyTerminalHarnessExecution(execution, &step_success,
2541 status = wait_status;
2542 step_success =
false;
2543 step_message = std::string(wait_status.message());
2546 }
else if (step.action ==
"assert") {
2547 AssertRequest sub_request;
2548 sub_request.set_condition(ApplyOverrides(step.condition, overrides));
2549 sub_request.set_widget_key(ApplyOverrides(step.widget_key, overrides));
2550 AssertResponse sub_response;
2551 status = Assert(&sub_request, &sub_response);
2552 step_success = sub_response.success();
2553 step_message = sub_response.message();
2554 if (status.ok() && !sub_response.test_id().empty()) {
2555 absl::Status wait_status = WaitForHarnessTestCompletion(
2556 test_manager_, sub_response.test_id(), &execution);
2557 if (wait_status.ok()) {
2558 have_execution =
true;
2559 internal::ApplyTerminalHarnessExecution(execution, &step_success,
2562 status = wait_status;
2563 step_success =
false;
2564 step_message = std::string(wait_status.message());
2567 }
else if (step.action ==
"screenshot") {
2568 ScreenshotRequest sub_request;
2569 sub_request.set_window_title(ApplyOverrides(step.target, overrides));
2570 if (!step.region.empty()) {
2571 sub_request.set_output_path(ApplyOverrides(step.region, overrides));
2573 ScreenshotResponse sub_response;
2574 status = Screenshot(&sub_request, &sub_response);
2575 step_success = sub_response.success();
2576 step_message = sub_response.message();
2578 status = absl::InvalidArgumentError(
2579 absl::StrFormat(
"Unsupported action '%s'", step.action));
2581 std::string(status.message().data(), status.message().size());
2584 auto* assertion = response->add_assertions();
2585 assertion->set_description(
2586 absl::StrFormat(
"Step %d (%s)", steps_executed, step.action));
2589 assertion->set_passed(
false);
2590 assertion->set_error_message(
2591 std::string(status.message().data(), status.message().size()));
2592 overall_success =
false;
2593 overall_message = step_message;
2594 logs.push_back(absl::StrFormat(
" Error: %s", status.message()));
2595 overall_rpc_status = status;
2601 logs.push_back(absl::StrFormat(
" Result: %s", step_message));
2602 if (have_execution) {
2603 logs.push_back(absl::StrFormat(
" Test ID: %s", execution.test_id));
2604 logs.push_back(absl::StrFormat(
2606 ::yaze::test::HarnessStatusToString(execution.status)));
2607 for (
const auto& failure : execution.assertion_failures) {
2608 logs.push_back(absl::StrFormat(
" Assertion failure: %s", failure));
2612 bool expectations_met = (step_success == step.expect_success);
2613 std::string expectation_error;
2615 if (!expectations_met) {
2617 absl::StrFormat(
"Expected success=%s but got %s",
2618 step.expect_success ?
"true" :
"false",
2619 step_success ?
"true" :
"false");
2622 if (!step.expect_status.empty()) {
2623 HarnessTestStatus expected_status =
2624 ::yaze::test::HarnessStatusFromString(step.expect_status);
2625 if (!have_execution) {
2626 expectations_met =
false;
2627 if (!expectation_error.empty()) {
2628 expectation_error.append(
"; ");
2630 expectation_error.append(
"No execution details available");
2631 }
else if (expected_status != HarnessTestStatus::kUnspecified &&
2632 execution.status != expected_status) {
2633 expectations_met =
false;
2634 if (!expectation_error.empty()) {
2635 expectation_error.append(
"; ");
2637 expectation_error.append(absl::StrFormat(
2638 "Expected status %s but observed %s", step.expect_status,
2639 ::yaze::test::HarnessStatusToString(execution.status)));
2641 if (have_execution) {
2642 assertion->set_actual_value(
2643 ::yaze::test::HarnessStatusToString(execution.status));
2644 assertion->set_expected_value(step.expect_status);
2648 if (!step.expect_message.empty()) {
2649 std::string actual_message = step_message;
2650 if (have_execution && !execution.error_message.empty()) {
2651 actual_message = execution.error_message;
2653 if (actual_message.find(step.expect_message) == std::string::npos) {
2654 expectations_met =
false;
2655 if (!expectation_error.empty()) {
2656 expectation_error.append(
"; ");
2658 expectation_error.append(
2659 absl::StrFormat(
"Expected message containing '%s' but got '%s'",
2660 step.expect_message, actual_message));
2664 if (!expectations_met) {
2665 assertion->set_passed(
false);
2666 assertion->set_error_message(expectation_error);
2667 overall_success =
false;
2668 overall_message = expectation_error;
2670 absl::StrFormat(
" Failed expectations: %s", expectation_error));
2671 if (request->ci_mode()) {
2675 assertion->set_passed(
true);
2678 if (!overall_success && request->ci_mode()) {
2683 response->set_steps_executed(steps_executed);
2684 response->set_success(overall_success);
2685 response->set_message(overall_message);
2686 for (
const auto& log_entry : logs) {
2687 response->add_logs(log_entry);
2690 return overall_rpc_status;
2697ImGuiTestHarnessServer& ImGuiTestHarnessServer::Instance() {
2698 static ImGuiTestHarnessServer* instance =
new ImGuiTestHarnessServer();
2702ImGuiTestHarnessServer::~ImGuiTestHarnessServer() {
2706absl::Status ImGuiTestHarnessServer::Start(
int port,
2707 TestManager* test_manager) {
2709 return absl::FailedPreconditionError(
"Server already running");
2712 if (!test_manager) {
2713 return absl::InvalidArgumentError(
"TestManager cannot be null");
2717 service_ = std::make_unique<ImGuiTestHarnessServiceImpl>(test_manager);
2721 grpc_service_ = std::make_unique<ImGuiTestHarnessServiceGrpc>(service_.get());
2723 std::string server_address = absl::StrFormat(
"0.0.0.0:%d", port);
2725 grpc::ServerBuilder builder;
2728 builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
2731 builder.RegisterService(grpc_service_.get());
2734 server_ = builder.BuildAndStart();
2737 return absl::InternalError(
2738 absl::StrFormat(
"Failed to start gRPC server on %s", server_address));
2743 std::cout <<
"✓ ImGuiTestHarness gRPC server listening on " << server_address
2744 <<
" (with TestManager integration)\n";
2745 std::cout <<
" Use 'grpcurl -plaintext -d '{\"message\":\"test\"}' "
2746 << server_address <<
" yaze.test.ImGuiTestHarness/Ping' to test\n";
2748 return absl::OkStatus();
2751void ImGuiTestHarnessServer::Shutdown() {
2753 std::cout <<
"⏹ Shutting down ImGuiTestHarness gRPC server...\n";
2754 server_->Shutdown();
2758 std::cout <<
"✓ ImGuiTestHarness gRPC server stopped\n";
Controller * GetController()
static Application & Instance()
editor::EditorManager * editor_manager()
UiSyncState GetUiSyncStateSnapshot() const
Moves work from service threads (gRPC, HTTP) onto the UI thread.
R RunOnUiThread(Fn fn, std::chrono::milliseconds timeout=kDefaultTimeout)
Runs fn on the UI thread and returns its result.
#define YAZE_VERSION_STRING
WindowContent * Get(const std::string &id)
Get a specific panel by its ID.
SDL2/SDL3 compatibility layer.
Room transition destination.
Public YAZE API umbrella header.