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"
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"
48struct DynamicTestData {
49 std::function<void(ImGuiTestContext*)> test_func;
52absl::Mutex g_dynamic_tests_mutex;
53std::deque<std::shared_ptr<DynamicTestData>> g_dynamic_tests
54 ABSL_GUARDED_BY(g_dynamic_tests_mutex);
56void KeepDynamicTestData(
const std::shared_ptr<DynamicTestData>& data) {
57 absl::MutexLock lock(&g_dynamic_tests_mutex);
62 g_dynamic_tests.push_back(data);
65void RunDynamicTest(ImGuiTestContext* ctx) {
66 auto* data = (DynamicTestData*)ctx->Test->UserData;
67 if (data && data->test_func) {
73bool IsTestCompleted(ImGuiTest* test) {
74 return test->Output.Status != ImGuiTestStatus_Queued &&
75 test->Output.Status != ImGuiTestStatus_Running;
81 std::atomic<bool> completed{
false};
82 std::mutex data_mutex;
86 void SetResult(
const T& res,
const std::string& msg) {
87 std::lock_guard<std::mutex> lock(data_mutex);
90 completed.store(
true);
93 void GetResult(T& res, std::string& msg) {
94 std::lock_guard<std::mutex> lock(data_mutex);
105::yaze::test::GetTestStatusResponse_TestStatus ConvertHarnessStatus(
106 ::yaze::test::HarnessTestStatus 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:
120 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_UNSPECIFIED;
124int64_t ToUnixMillisSafe(absl::Time timestamp) {
125 if (timestamp == absl::InfinitePast()) {
128 return absl::ToUnixMillis(timestamp);
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();
136 if (millis < std::numeric_limits<int32_t>::min()) {
137 return std::numeric_limits<int32_t>::min();
139 return static_cast<int32_t
>(millis);
144#include <grpcpp/grpcpp.h>
145#include <grpcpp/server_builder.h>
152std::string ClickTypeToString(ClickRequest::ClickType type) {
154 case ClickRequest::CLICK_TYPE_RIGHT:
156 case ClickRequest::CLICK_TYPE_MIDDLE:
158 case ClickRequest::CLICK_TYPE_DOUBLE:
160 case ClickRequest::CLICK_TYPE_LEFT:
161 case ClickRequest::CLICK_TYPE_UNSPECIFIED:
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;
172 if (lower ==
"middle") {
173 return ClickRequest::CLICK_TYPE_MIDDLE;
175 if (lower ==
"double" || lower ==
"double_click" || lower ==
"dbl") {
176 return ClickRequest::CLICK_TYPE_DOUBLE;
178 return ClickRequest::CLICK_TYPE_LEFT;
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;
186 if (lower ==
"failed" || lower ==
"fail") {
187 return HarnessTestStatus::kFailed;
189 if (lower ==
"timeout") {
190 return HarnessTestStatus::kTimeout;
192 if (lower ==
"queued") {
193 return HarnessTestStatus::kQueued;
195 if (lower ==
"running") {
196 return HarnessTestStatus::kRunning;
198 return HarnessTestStatus::kUnspecified;
201const char* HarnessStatusToString(HarnessTestStatus status) {
203 case HarnessTestStatus::kPassed:
205 case HarnessTestStatus::kFailed:
207 case HarnessTestStatus::kTimeout:
209 case HarnessTestStatus::kQueued:
211 case HarnessTestStatus::kRunning:
213 case HarnessTestStatus::kUnspecified:
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()) {
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}});
233void MaybeRecordStep(TestRecorder* recorder, TestRecorder::RecordedStep step) {
234 if (!recorder || !recorder->IsRecording()) {
237 if (step.captured_at == absl::InfinitePast()) {
238 step.captured_at = absl::Now();
240 recorder->RecordStep(step);
243absl::Status WaitForHarnessTestCompletion(TestManager* manager,
244 const std::string& test_id,
245 HarnessTestExecution* execution) {
247 return absl::FailedPreconditionError(
"TestManager unavailable");
249 if (test_id.empty()) {
250 return absl::InvalidArgumentError(
"Missing harness test identifier");
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);
258 absl::SleepFor(absl::Milliseconds(75));
263 *execution = std::move(current.value());
266 if (current->status == HarnessTestStatus::kQueued ||
267 current->status == HarnessTestStatus::kRunning) {
268 absl::SleepFor(absl::Milliseconds(75));
271 return absl::OkStatus();
274 return absl::DeadlineExceededError(absl::StrFormat(
275 "Harness test %s did not reach a terminal state", test_id));
278struct ParsedCondition {
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));
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));
292 return std::string(path);
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) {
303 return std::string(segment.substr(0, colon));
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.");
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').");
319 internal::ParsedTarget parsed;
320 parsed.type = std::string(target.substr(0, colon_pos));
321 parsed.label = std::string(target.substr(colon_pos + 1));
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);
337 return absl::NotFoundError(absl::StrFormat(
338 "widget_key '%s' was not found in WidgetIdRegistry", key));
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));
349 resolved.target.type = info->type.empty()
350 ? ExtractTypeFromPath(resolved.resolved_path)
351 : std::string(info->type);
352 resolved.target.label = info->label.empty()
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));
359 if (resolved.target.type.empty()) {
360 resolved.target.type =
"widget";
365 absl::StatusOr<ParsedTarget> parsed = ParseTargetString(target);
367 return parsed.status();
369 resolved.target = *parsed;
373void ApplyTerminalHarnessExecution(
const HarnessTestExecution& execution,
375 std::string* step_message) {
377 *step_success = execution.status == HarnessTestStatus::kPassed;
379 if (step_message && !execution.error_message.empty()) {
380 *step_message = execution.error_message;
388absl::StatusOr<ParsedCondition> ParseConditionString(absl::string_view value) {
390 return absl::InvalidArgumentError(
391 "Missing condition. Provide 'type:target' or widget_key.");
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'.");
400 ParsedCondition parsed;
401 parsed.type = std::string(value.substr(0, colon_pos));
402 parsed.target = std::string(value.substr(colon_pos + 1));
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();
417 widget_selector = *resolved;
418 if (resolved_selector) {
419 *resolved_selector = *resolved;
421 }
else if (resolved_selector) {
422 *resolved_selector = internal::ResolvedWidgetSelector{};
425 if (condition.empty()) {
426 if (!widget_selector.has_value()) {
427 return absl::InvalidArgumentError(
428 "Missing condition. Provide 'type:target' or widget_key.");
430 ParsedCondition parsed;
431 parsed.type = std::string(default_type_for_widget);
432 parsed.target = widget_selector->target.label;
436 absl::StatusOr<ParsedCondition> parsed = ParseConditionString(condition);
438 return parsed.status();
441 if (widget_selector.has_value() && parsed->target.empty()) {
442 parsed->target = widget_selector->target.label;
445 if (parsed->target.empty()) {
446 return absl::InvalidArgumentError(
447 "Condition target is empty. Provide 'type:target'.");
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;
463editor::EditorManager* GetEditorManagerForSync() {
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;
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++;
490 case HarnessTestStatus::kRunning:
491 snapshot.running_harness_tests++;
499 snapshot.pending_harness_tests =
500 snapshot.queued_harness_tests + snapshot.running_harness_tests;
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;
511std::string BuildIdleTimeoutReason(
const UiSyncSnapshot& snapshot) {
512 return absl::StrFormat(
513 "pending_editor=%d pending_layout=%d layout_rebuild=%s queued_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);
520int32_t NormalizeTimeoutMs(int32_t timeout_ms, int32_t fallback_ms) {
521 return timeout_ms <= 0 ? fallback_ms : timeout_ms;
524int32_t NormalizePollIntervalMs(int32_t poll_interval_ms) {
525 if (poll_interval_ms <= 0) {
528 return std::clamp(poll_interval_ms, 5, 1000);
534class ImGuiTestHarnessServiceGrpc final :
public ImGuiTestHarness::Service {
536 explicit ImGuiTestHarnessServiceGrpc(ImGuiTestHarnessServiceImpl* impl)
539 ::grpc::Status Ping(::grpc::ServerContext* context,
540 const PingRequest* request,
541 PingResponse* response)
override {
542 return ConvertStatus(impl_->Ping(request, response));
545 ::grpc::Status Click(::grpc::ServerContext* context,
546 const ClickRequest* request,
547 ClickResponse* response)
override {
548 return ConvertStatus(impl_->Click(request, response));
551 ::grpc::Status Type(::grpc::ServerContext* context,
552 const TypeRequest* request,
553 TypeResponse* response)
override {
554 return ConvertStatus(impl_->Type(request, response));
557 ::grpc::Status Wait(::grpc::ServerContext* context,
558 const WaitRequest* request,
559 WaitResponse* response)
override {
560 return ConvertStatus(impl_->Wait(request, response));
563 ::grpc::Status Assert(::grpc::ServerContext* context,
564 const AssertRequest* request,
565 AssertResponse* response)
override {
566 return ConvertStatus(impl_->Assert(request, response));
569 ::grpc::Status FlushUiActions(::grpc::ServerContext* context,
570 const FlushUiActionsRequest* request,
571 FlushUiActionsResponse* response)
override {
572 return ConvertStatus(impl_->FlushUiActions(request, response));
575 ::grpc::Status WaitForIdle(::grpc::ServerContext* context,
576 const WaitForIdleRequest* request,
577 WaitForIdleResponse* response)
override {
578 return ConvertStatus(impl_->WaitForIdle(request, response));
581 ::grpc::Status GetUiSyncState(::grpc::ServerContext* context,
582 const GetUiSyncStateRequest* request,
583 GetUiSyncStateResponse* response)
override {
584 return ConvertStatus(impl_->GetUiSyncState(request, response));
587 ::grpc::Status Screenshot(::grpc::ServerContext* context,
588 const ScreenshotRequest* request,
589 ScreenshotResponse* response)
override {
590 return ConvertStatus(impl_->Screenshot(request, response));
593 ::grpc::Status GetTestStatus(::grpc::ServerContext* context,
594 const GetTestStatusRequest* request,
595 GetTestStatusResponse* response)
override {
596 return ConvertStatus(impl_->GetTestStatus(request, response));
599 ::grpc::Status ListTests(::grpc::ServerContext* context,
600 const ListTestsRequest* request,
601 ListTestsResponse* response)
override {
602 return ConvertStatus(impl_->ListTests(request, response));
605 ::grpc::Status GetTestResults(::grpc::ServerContext* context,
606 const GetTestResultsRequest* request,
607 GetTestResultsResponse* response)
override {
608 return ConvertStatus(impl_->GetTestResults(request, response));
611 ::grpc::Status DiscoverWidgets(::grpc::ServerContext* context,
612 const DiscoverWidgetsRequest* request,
613 DiscoverWidgetsResponse* response)
override {
614 return ConvertStatus(impl_->DiscoverWidgets(request, response));
617 ::grpc::Status StartRecording(::grpc::ServerContext* context,
618 const StartRecordingRequest* request,
619 StartRecordingResponse* response)
override {
620 return ConvertStatus(impl_->StartRecording(request, response));
623 ::grpc::Status StopRecording(::grpc::ServerContext* context,
624 const StopRecordingRequest* request,
625 StopRecordingResponse* response)
override {
626 return ConvertStatus(impl_->StopRecording(request, response));
629 ::grpc::Status ReplayTest(::grpc::ServerContext* context,
630 const ReplayTestRequest* request,
631 ReplayTestResponse* response)
override {
632 return ConvertStatus(impl_->ReplayTest(request, response));
636 static ::grpc::Status ConvertStatus(
const absl::Status& status) {
638 return ::grpc::Status::OK;
641 ::grpc::StatusCode code = ::grpc::StatusCode::UNKNOWN;
642 switch (status.code()) {
643 case absl::StatusCode::kCancelled:
644 code = ::grpc::StatusCode::CANCELLED;
646 case absl::StatusCode::kUnknown:
647 code = ::grpc::StatusCode::UNKNOWN;
649 case absl::StatusCode::kInvalidArgument:
650 code = ::grpc::StatusCode::INVALID_ARGUMENT;
652 case absl::StatusCode::kDeadlineExceeded:
653 code = ::grpc::StatusCode::DEADLINE_EXCEEDED;
655 case absl::StatusCode::kNotFound:
656 code = ::grpc::StatusCode::NOT_FOUND;
658 case absl::StatusCode::kAlreadyExists:
659 code = ::grpc::StatusCode::ALREADY_EXISTS;
661 case absl::StatusCode::kPermissionDenied:
662 code = ::grpc::StatusCode::PERMISSION_DENIED;
664 case absl::StatusCode::kResourceExhausted:
665 code = ::grpc::StatusCode::RESOURCE_EXHAUSTED;
667 case absl::StatusCode::kFailedPrecondition:
668 code = ::grpc::StatusCode::FAILED_PRECONDITION;
670 case absl::StatusCode::kAborted:
671 code = ::grpc::StatusCode::ABORTED;
673 case absl::StatusCode::kOutOfRange:
674 code = ::grpc::StatusCode::OUT_OF_RANGE;
676 case absl::StatusCode::kUnimplemented:
677 code = ::grpc::StatusCode::UNIMPLEMENTED;
679 case absl::StatusCode::kInternal:
680 code = ::grpc::StatusCode::INTERNAL;
682 case absl::StatusCode::kUnavailable:
683 code = ::grpc::StatusCode::UNAVAILABLE;
685 case absl::StatusCode::kDataLoss:
686 code = ::grpc::StatusCode::DATA_LOSS;
688 case absl::StatusCode::kUnauthenticated:
689 code = ::grpc::StatusCode::UNAUTHENTICATED;
692 code = ::grpc::StatusCode::UNKNOWN;
696 return ::grpc::Status(
697 code, std::string(status.message().data(), status.message().size()));
700 ImGuiTestHarnessServiceImpl* impl_;
703std::unique_ptr<::grpc::Service> CreateImGuiTestHarnessServiceGrpc(
704 ImGuiTestHarnessServiceImpl* impl) {
705 return std::make_unique<ImGuiTestHarnessServiceGrpc>(impl);
712absl::Status ImGuiTestHarnessServiceImpl::Ping(
const PingRequest* request,
713 PingResponse* response) {
715 response->set_message(absl::StrFormat(
"Pong: %s", request->message()));
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());
726 return absl::OkStatus();
729absl::Status ImGuiTestHarnessServiceImpl::Click(
const ClickRequest* request,
730 ClickResponse* response) {
731 auto start = std::chrono::steady_clock::now();
733 TestRecorder::RecordedStep recorded_step;
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());
740 recorded_step.target = request->target();
742 recorded_step.click_type = ClickTypeToString(request->type());
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);
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"));
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)
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));
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,
786 test_manager_->AppendHarnessTestLog(test_id, message);
787 return finalize(absl::OkStatus());
790 if (!resolved_selector->resolved_widget_key.empty()) {
791 response->set_resolved_widget_key(resolved_selector->resolved_widget_key);
793 if (!resolved_selector->resolved_path.empty()) {
794 response->set_resolved_path(resolved_selector->resolved_path);
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;
801#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
802 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
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,
812 return finalize(absl::OkStatus());
815 const ClickRequest::ClickType click_type =
816 request ? request->type() : ClickRequest::CLICK_TYPE_LEFT;
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;
824 case ClickRequest::CLICK_TYPE_RIGHT:
825 mouse_button = ImGuiMouseButton_Right;
827 case ClickRequest::CLICK_TYPE_MIDDLE:
828 mouse_button = ImGuiMouseButton_Middle;
830 case ClickRequest::CLICK_TYPE_DOUBLE:
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);
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") {
852 auto* editor_manager =
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);
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);
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);
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);
888 ctx->KeyPress(chord);
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);
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);
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);
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);
920 if (click_type == ClickRequest::CLICK_TYPE_DOUBLE) {
921 ctx->ItemDoubleClick(ImGuiTestRef(item.ID));
923 ctx->ItemClick(ImGuiTestRef(item.ID), mouse_button);
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);
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,
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,
948 std::string test_name = absl::StrFormat(
950 static_cast<long long>(
951 std::chrono::system_clock::now().time_since_epoch().count()));
953 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
954 test->TestFunc = RunDynamicTest;
955 test->UserData = test_data.get();
957 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
958 KeepDynamicTestData(test_data);
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);
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,
974 test_manager_->AppendHarnessTestLog(test_id, message);
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());
983 return finalize(absl::OkStatus());
986absl::Status ImGuiTestHarnessServiceImpl::Type(
const TypeRequest* request,
987 TypeResponse* response) {
988 auto start = std::chrono::steady_clock::now();
990 TestRecorder::RecordedStep recorded_step;
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());
997 recorded_step.target = request->target();
999 recorded_step.text = request->text();
1000 recorded_step.clear_first = request->clear_first();
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);
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"));
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)
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));
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,
1044 test_manager_->AppendHarnessTestLog(test_id, message);
1045 return finalize(absl::OkStatus());
1048 if (!resolved_selector->resolved_widget_key.empty()) {
1049 response->set_resolved_widget_key(resolved_selector->resolved_widget_key);
1051 if (!resolved_selector->resolved_path.empty()) {
1052 response->set_resolved_path(resolved_selector->resolved_path);
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;
1059#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1060 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
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,
1070 return finalize(absl::OkStatus());
1072 std::string text = request->text();
1073 bool clear_first = request->clear_first();
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);
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);
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);
1098 ctx->ItemClick(ImGuiTestRef(item.ID));
1100 ctx->KeyPress(ImGuiMod_Shortcut | ImGuiKey_A);
1101 ctx->KeyPress(ImGuiKey_Delete);
1104 ctx->ItemInputValue(ImGuiTestRef(item.ID), text.c_str());
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);
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,
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,
1128 rpc_state->SetResult(
false, error_message);
1132 std::string test_name = absl::StrFormat(
1134 static_cast<long long>(
1135 std::chrono::system_clock::now().time_since_epoch().count()));
1137 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
1138 test->TestFunc = RunDynamicTest;
1139 test->UserData = test_data.get();
1141 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
1142 KeepDynamicTestData(test_data);
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,
1153 rpc_state->SetResult(
false, error_message);
1156 std::this_thread::sleep_for(std::chrono::milliseconds(100));
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);
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);
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,
1177 test_manager_->AppendHarnessTestLog(test_id, message);
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());
1186 return finalize(absl::OkStatus());
1189absl::Status ImGuiTestHarnessServiceImpl::Wait(
const WaitRequest* request,
1190 WaitResponse* response) {
1191 auto start = std::chrono::steady_clock::now();
1193 TestRecorder::RecordedStep recorded_step;
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());
1201 recorded_step.condition = request->condition();
1203 recorded_step.timeout_ms = request->timeout_ms();
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);
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"));
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;
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));
1240 internal::ResolvedWidgetSelector resolved_selector;
1241 absl::StatusOr<ParsedCondition> parsed_condition = ResolveCondition(
1242 requested_condition, requested_widget_key,
1243 "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,
1253 test_manager_->AppendHarnessTestLog(test_id, message);
1254 return finalize(absl::OkStatus());
1257 if (!resolved_selector.resolved_widget_key.empty()) {
1258 response->set_resolved_widget_key(resolved_selector.resolved_widget_key);
1260 if (!resolved_selector.resolved_path.empty()) {
1261 response->set_resolved_path(resolved_selector.resolved_path);
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);
1270#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1271 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
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,
1281 return finalize(absl::OkStatus());
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;
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);
1296 for (
int i = 0; i < 10; ++i) {
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());
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);
1320 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
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);
1330 if (current_state) {
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);
1343 std::this_thread::sleep_for(
1344 std::chrono::milliseconds(poll_interval_ms));
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,
1362 std::string test_name = absl::StrFormat(
1364 static_cast<long long>(
1365 std::chrono::system_clock::now().time_since_epoch().count()));
1367 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
1368 test->TestFunc = RunDynamicTest;
1369 test->UserData = test_data.get();
1371 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
1372 KeepDynamicTestData(test_data);
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);
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,
1388 test_manager_->AppendHarnessTestLog(test_id, message);
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());
1397 return finalize(absl::OkStatus());
1400absl::Status ImGuiTestHarnessServiceImpl::Assert(
const AssertRequest* request,
1401 AssertResponse* response) {
1402 TestRecorder::RecordedStep recorded_step;
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());
1410 recorded_step.condition = request->condition();
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);
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"));
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;
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));
1449 internal::ResolvedWidgetSelector resolved_selector;
1450 absl::StatusOr<ParsedCondition> parsed_condition = ResolveCondition(
1451 requested_condition, requested_widget_key,
1452 "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,
1461 test_manager_->AppendHarnessTestLog(test_id, message);
1462 return finalize(absl::OkStatus());
1465 if (!resolved_selector.resolved_widget_key.empty()) {
1466 response->set_resolved_widget_key(resolved_selector.resolved_widget_key);
1468 if (!resolved_selector.resolved_path.empty()) {
1469 response->set_resolved_path(resolved_selector.resolved_path);
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;
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,
1486 test_manager_->AppendHarnessTestLog(test_id, message);
1487 return finalize(absl::OkStatus());
1490#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1491 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
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,
1500 return finalize(absl::OkStatus());
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);
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(
1519 absl::StrFormat(
"Actual: %s | Expected: %s", actual, expected));
1521 manager->MarkHarnessTestCompleted(captured_id, status,
1522 passed ?
"" : message);
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());
1534 if (assertion_type ==
"visible") {
1535 const ImGuiTestItemInfo item =
1537 ? ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError)
1538 : ctx->WindowInfo(widget_ref, ImGuiTestOpFlags_NoError);
1539 const bool is_visible =
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;
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);
1555 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
1556 passed = is_enabled;
1557 actual_value = is_enabled ?
"enabled" :
"disabled";
1558 expected_value =
"enabled";
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);
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);
1576 actual_value =
"not found";
1577 expected_value = assertion_target;
1578 message = absl::StrFormat(
"Input '%s' not found", widget_label);
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,
1587 "'%s' does not equal '%s' (actual: '%s')",
1588 widget_label, expected_value, actual_value);
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);
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());
1611 ImGuiTestItemInfo item =
1612 ctx->ItemInfo(input_ref, ImGuiTestOpFlags_NoError);
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,
1622 "'%s' does not contain '%s' (actual: '%s')",
1623 input_target, expected_text, actual_text);
1626 actual_value =
"not found";
1627 expected_value = expected_text;
1628 message = absl::StrFormat(
"Input '%s' not found", input_target);
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);
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,
1650 std::string test_name = absl::StrFormat(
1652 static_cast<long long>(
1653 std::chrono::system_clock::now().time_since_epoch().count()));
1655 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
1656 test->TestFunc = RunDynamicTest;
1657 test->UserData = test_data.get();
1659 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
1660 KeepDynamicTestData(test_data);
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);
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,
1675 test_manager_->AppendHarnessTestLog(test_id, message);
1677 response->set_success(
false);
1678 response->set_message(message);
1679 response->set_actual_value(
"(stub)");
1680 response->set_expected_value(
"(stub)");
1683 return finalize(absl::OkStatus());
1686absl::Status ImGuiTestHarnessServiceImpl::FlushUiActions(
1687 const FlushUiActionsRequest* request, FlushUiActionsResponse* response) {
1689 return absl::InvalidArgumentError(
"response cannot be null");
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();
1698 UiSyncSnapshot initial = CaptureUiSyncSnapshot(test_manager_);
1699 UiSyncSnapshot snapshot = initial;
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();
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) {
1725 absl::SleepFor(absl::Milliseconds(10));
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();
1746absl::Status ImGuiTestHarnessServiceImpl::WaitForIdle(
1747 const WaitForIdleRequest* request, WaitForIdleResponse* response) {
1749 return absl::InvalidArgumentError(
"response cannot be null");
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;
1760 const absl::Time started_at = absl::Now();
1761 const absl::Time deadline = started_at + absl::Milliseconds(timeout_ms);
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;
1773 UiSyncSnapshot snapshot = CaptureUiSyncSnapshot(test_manager_);
1774 int32_t stable_frames_observed = 0;
1775 int64_t last_frame_id = -1;
1777 while (absl::Now() <= deadline) {
1778 snapshot = CaptureUiSyncSnapshot(test_manager_);
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;
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();
1805 absl::SleepFor(absl::Milliseconds(poll_interval_ms));
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();
1824absl::Status ImGuiTestHarnessServiceImpl::GetUiSyncState(
1825 const GetUiSyncStateRequest* ,
1826 GetUiSyncStateResponse* response) {
1828 return absl::InvalidArgumentError(
"response cannot be null");
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();
1844absl::Status ImGuiTestHarnessServiceImpl::Screenshot(
1845 const ScreenshotRequest* request, ScreenshotResponse* response) {
1847 return absl::InvalidArgumentError(
"response cannot be null");
1850 const std::string requested_path =
1851 request ? request->output_path() : std::string();
1856 std::atomic<bool> done{
false};
1857 absl::StatusOr<ScreenshotArtifact> result =
1858 absl::UnknownError(
"Not captured");
1860 auto state = std::make_shared<State>();
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);
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");
1877 std::this_thread::sleep_for(std::chrono::milliseconds(10));
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();
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,
1891 response->set_file_path(artifact.file_path);
1892 response->set_file_size_bytes(artifact.file_size_bytes);
1894 return absl::OkStatus();
1897absl::Status ImGuiTestHarnessServiceImpl::GetTestStatus(
1898 const GetTestStatusRequest* request, GetTestStatusResponse* response) {
1899 if (!test_manager_) {
1900 return absl::FailedPreconditionError(
"TestManager not available");
1903 if (request->test_id().empty()) {
1904 return absl::InvalidArgumentError(
"test_id must be provided");
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();
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);
1924 response->clear_error_message();
1927 response->clear_assertion_failures();
1928 for (
const auto& failure : execution.assertion_failures) {
1929 response->add_assertion_failures(failure);
1932 return absl::OkStatus();
1935absl::Status ImGuiTestHarnessServiceImpl::ListTests(
1936 const ListTestsRequest* request, ListTestsResponse* response) {
1937 if (!test_manager_) {
1938 return absl::FailedPreconditionError(
"TestManager not available");
1941 if (request->page_size() < 0) {
1942 return absl::InvalidArgumentError(
"page_size cannot be negative");
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;
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) ||
1956 return absl::InvalidArgumentError(
"Invalid page_token");
1958 start_index =
static_cast<size_t>(token_value);
1962 test_manager_->ListHarnessTestSummaries(request->category_filter());
1964 response->set_total_count(
static_cast<int32_t
>(summaries.size()));
1966 if (start_index >= summaries.size()) {
1967 response->clear_tests();
1968 response->clear_next_page_token();
1969 return absl::OkStatus();
1973 std::min(start_index +
static_cast<size_t>(page_size), summaries.size());
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;
1980 test_info->set_test_id(exec.test_id);
1981 test_info->set_name(exec.name);
1982 test_info->set_category(exec.category);
1984 int64_t last_run_ms = ToUnixMillisSafe(exec.completed_at);
1985 if (last_run_ms == 0) {
1986 last_run_ms = ToUnixMillisSafe(exec.started_at);
1988 if (last_run_ms == 0) {
1989 last_run_ms = ToUnixMillisSafe(exec.queued_at);
1991 test_info->set_last_run_timestamp_ms(last_run_ms);
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);
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);
2003 test_info->set_average_duration_ms(average_duration_ms);
2006 if (end_index < summaries.size()) {
2007 response->set_next_page_token(absl::StrCat(end_index));
2009 response->clear_next_page_token();
2012 return absl::OkStatus();
2015absl::Status ImGuiTestHarnessServiceImpl::GetTestResults(
2016 const GetTestResultsRequest* request, GetTestResultsResponse* response) {
2017 if (!test_manager_) {
2018 return absl::FailedPreconditionError(
"TestManager not available");
2021 if (request->test_id().empty()) {
2022 return absl::InvalidArgumentError(
"test_id must be provided");
2026 test_manager_->GetHarnessTestExecution(request->test_id());
2027 if (!execution_or.ok()) {
2028 return execution_or.status();
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);
2036 int64_t executed_at_ms = ToUnixMillisSafe(execution.completed_at);
2037 if (executed_at_ms == 0) {
2038 executed_at_ms = ToUnixMillisSafe(execution.started_at);
2040 if (executed_at_ms == 0) {
2041 executed_at_ms = ToUnixMillisSafe(execution.queued_at);
2043 response->set_executed_at_ms(executed_at_ms);
2044 response->set_duration_ms(ClampDurationToInt32(execution.duration));
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);
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);
2061 if (request->include_logs()) {
2062 for (
const auto& log_entry : execution.logs) {
2063 response->add_logs(log_entry);
2067 auto* metrics_map = response->mutable_metrics();
2068 for (
const auto& [key, value] : execution.metrics) {
2069 (*metrics_map)[
key] = value;
2073 if (!execution.screenshot_path.empty()) {
2074 response->set_screenshot_path(execution.screenshot_path);
2075 response->set_screenshot_size_bytes(execution.screenshot_size_bytes);
2077 if (!execution.failure_context.empty()) {
2078 response->set_failure_context(execution.failure_context);
2080 if (!execution.widget_state.empty()) {
2081 response->set_widget_state(execution.widget_state);
2084 return absl::OkStatus();
2087absl::Status ImGuiTestHarnessServiceImpl::DiscoverWidgets(
2088 const DiscoverWidgetsRequest* request, DiscoverWidgetsResponse* response) {
2090 return absl::InvalidArgumentError(
"request cannot be null");
2093 return absl::InvalidArgumentError(
"response cannot be null");
2096 if (!test_manager_) {
2097 return absl::FailedPreconditionError(
"TestManager not available");
2100 widget_discovery_service_.CollectWidgets(
nullptr, *request, response);
2101 return absl::OkStatus();
2104absl::Status ImGuiTestHarnessServiceImpl::StartRecording(
2105 const StartRecordingRequest* request, StartRecordingResponse* response) {
2107 return absl::InvalidArgumentError(
"request cannot be null");
2110 return absl::InvalidArgumentError(
"response cannot be null");
2113 TestRecorder::RecordingOptions options;
2114 options.output_path = request->output_path();
2115 options.session_name = request->session_name();
2116 options.description = request->description();
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");
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();
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();
2138absl::Status ImGuiTestHarnessServiceImpl::StopRecording(
2139 const StopRecordingRequest* request, StopRecordingResponse* response) {
2141 return absl::InvalidArgumentError(
"request cannot be null");
2144 return absl::InvalidArgumentError(
"response cannot be null");
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();
2155 response->set_success(
true);
2156 if (summary->saved) {
2157 response->set_message(
"Recording saved");
2159 response->set_message(
"Recording discarded");
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();
2167absl::Status ImGuiTestHarnessServiceImpl::ReplayTest(
2168 const ReplayTestRequest* request, ReplayTestResponse* response) {
2170 return absl::InvalidArgumentError(
"request cannot be null");
2173 return absl::InvalidArgumentError(
"response cannot be null");
2176 response->clear_logs();
2177 response->clear_assertions();
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");
2185 absl::StatusOr<TestScript> script_or =
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();
2192 TestScript script = std::move(*script_or);
2194 absl::flat_hash_map<std::string, std::string> overrides;
2195 for (
const auto& entry : request->parameter_overrides()) {
2196 overrides[entry.first] = entry.second;
2199 response->set_replay_session_id(absl::StrFormat(
2201 absl::FormatTime(
"%Y%m%dT%H%M%S", absl::Now(), absl::UTCTimeZone())));
2203 auto suspension = test_recorder_.Suspend();
2205 std::vector<std::string> logs;
2206 logs.reserve(script.steps.size() * 2 + 4);
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();
2213 for (
const auto& step : script.steps) {
2215 std::string action_label =
2216 absl::StrFormat(
"Step %d: %s", steps_executed, step.action);
2217 logs.push_back(action_label);
2219 absl::Status status = absl::OkStatus();
2220 bool step_success =
false;
2221 std::string step_message;
2222 HarnessTestExecution execution;
2223 bool have_execution =
false;
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,
2242 status = wait_status;
2243 step_success =
false;
2244 step_message = std::string(wait_status.message());
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,
2265 status = wait_status;
2266 step_success =
false;
2267 step_message = std::string(wait_status.message());
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);
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,
2289 status = wait_status;
2290 step_success =
false;
2291 step_message = std::string(wait_status.message());
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,
2310 status = wait_status;
2311 step_success =
false;
2312 step_message = std::string(wait_status.message());
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));
2321 ScreenshotResponse sub_response;
2322 status = Screenshot(&sub_request, &sub_response);
2323 step_success = sub_response.success();
2324 step_message = sub_response.message();
2326 status = absl::InvalidArgumentError(
2327 absl::StrFormat(
"Unsupported action '%s'", step.action));
2329 std::string(status.message().data(), status.message().size());
2332 auto* assertion = response->add_assertions();
2333 assertion->set_description(
2334 absl::StrFormat(
"Step %d (%s)", steps_executed, step.action));
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;
2347 bool expectations_met = (step_success == step.expect_success);
2348 std::string expectation_error;
2350 if (!expectations_met) {
2352 absl::StrFormat(
"Expected success=%s but got %s",
2353 step.expect_success ?
"true" :
"false",
2354 step_success ?
"true" :
"false");
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(
"; ");
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(
"; ");
2372 expectation_error.append(absl::StrFormat(
2373 "Expected status %s but observed %s", step.expect_status,
2374 ::yaze::test::HarnessStatusToString(execution.status)));
2376 if (have_execution) {
2377 assertion->set_actual_value(
2378 ::yaze::test::HarnessStatusToString(execution.status));
2379 assertion->set_expected_value(step.expect_status);
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;
2388 if (actual_message.find(step.expect_message) == std::string::npos) {
2389 expectations_met =
false;
2390 if (!expectation_error.empty()) {
2391 expectation_error.append(
"; ");
2393 expectation_error.append(
2394 absl::StrFormat(
"Expected message containing '%s' but got '%s'",
2395 step.expect_message, actual_message));
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;
2405 absl::StrFormat(
" Failed expectations: %s", expectation_error));
2406 if (request->ci_mode()) {
2410 assertion->set_passed(
true);
2411 logs.push_back(absl::StrFormat(
" Result: %s", step_message));
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));
2420 if (!overall_success && request->ci_mode()) {
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);
2432 return overall_rpc_status;
2439ImGuiTestHarnessServer& ImGuiTestHarnessServer::Instance() {
2440 static ImGuiTestHarnessServer* instance =
new ImGuiTestHarnessServer();
2444ImGuiTestHarnessServer::~ImGuiTestHarnessServer() {
2448absl::Status ImGuiTestHarnessServer::Start(
int port,
2449 TestManager* test_manager) {
2451 return absl::FailedPreconditionError(
"Server already running");
2454 if (!test_manager) {
2455 return absl::InvalidArgumentError(
"TestManager cannot be null");
2459 service_ = std::make_unique<ImGuiTestHarnessServiceImpl>(test_manager);
2463 grpc_service_ = std::make_unique<ImGuiTestHarnessServiceGrpc>(service_.get());
2465 std::string server_address = absl::StrFormat(
"0.0.0.0:%d", port);
2467 grpc::ServerBuilder builder;
2470 builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
2473 builder.RegisterService(grpc_service_.get());
2476 server_ = builder.BuildAndStart();
2479 return absl::InternalError(
2480 absl::StrFormat(
"Failed to start gRPC server on %s", server_address));
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";
2490 return absl::OkStatus();
2493void ImGuiTestHarnessServer::Shutdown() {
2495 std::cout <<
"⏹ Shutting down ImGuiTestHarness gRPC server...\n";
2496 server_->Shutdown();
2500 std::cout <<
"✓ ImGuiTestHarness gRPC server stopped\n";
Controller * GetController()
static Application & Instance()
editor::EditorManager * editor_manager()
void RequestScreenshot(const ScreenshotRequest &request)
UiSyncState GetUiSyncStateSnapshot() const
static absl::StatusOr< TestScript > ParseFromFile(const std::string &path)
#define YAZE_VERSION_STRING
SDL2/SDL3 compatibility layer.
Public YAZE API umbrella header.