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;
386absl::StatusOr<ScreenshotArtifact> CaptureScreenshotOnRenderThread(
387 const std::string& preferred_path,
const std::string& window_title,
391 return absl::FailedPreconditionError(
392 "Application controller not available");
398 std::atomic<bool> done{
false};
399 absl::StatusOr<ScreenshotArtifact> result =
400 absl::UnknownError(
"Not captured");
402 auto state = std::make_shared<State>();
404 controller->RequestScreenshot(
405 {.preferred_path = preferred_path,
406 .window_title = window_title,
408 .reveal_to_user =
false,
409 .callback = [state](absl::StatusOr<ScreenshotArtifact> result) {
410 state->result = std::move(result);
411 state->done.store(
true);
414 const auto deadline = std::chrono::steady_clock::now() + timeout;
415 while (!state->done.load()) {
416 if (std::chrono::steady_clock::now() > deadline) {
417 return absl::DeadlineExceededError(
418 "Timed out waiting for screenshot capture on main thread");
420 std::this_thread::sleep_for(std::chrono::milliseconds(10));
422 return state->result;
427absl::StatusOr<ParsedCondition> ParseConditionString(absl::string_view value) {
429 return absl::InvalidArgumentError(
430 "Missing condition. Provide 'type:target' or widget_key.");
433 size_t colon_pos = value.find(
':');
434 if (colon_pos == absl::string_view::npos || colon_pos == 0) {
435 return absl::InvalidArgumentError(
436 "Invalid condition format. Use 'type:target'.");
439 ParsedCondition parsed;
440 parsed.type = std::string(value.substr(0, colon_pos));
441 parsed.target = std::string(value.substr(colon_pos + 1));
445absl::StatusOr<ParsedCondition> ResolveCondition(
446 absl::string_view condition, absl::string_view widget_key,
447 absl::string_view default_type_for_widget,
448 internal::ResolvedWidgetSelector* resolved_selector) {
449 std::optional<internal::ResolvedWidgetSelector> widget_selector;
450 if (!widget_key.empty()) {
451 absl::StatusOr<internal::ResolvedWidgetSelector> resolved =
452 internal::ResolveWidgetSelector(
"", widget_key);
453 if (!resolved.ok()) {
454 return resolved.status();
456 widget_selector = *resolved;
457 if (resolved_selector) {
458 *resolved_selector = *resolved;
460 }
else if (resolved_selector) {
461 *resolved_selector = internal::ResolvedWidgetSelector{};
464 if (condition.empty()) {
465 if (!widget_selector.has_value()) {
466 return absl::InvalidArgumentError(
467 "Missing condition. Provide 'type:target' or widget_key.");
469 ParsedCondition parsed;
470 parsed.type = std::string(default_type_for_widget);
471 parsed.target = widget_selector->target.label;
475 absl::StatusOr<ParsedCondition> parsed = ParseConditionString(condition);
477 return parsed.status();
480 if (widget_selector.has_value() && parsed->target.empty()) {
481 parsed->target = widget_selector->target.label;
484 if (parsed->target.empty()) {
485 return absl::InvalidArgumentError(
486 "Condition target is empty. Provide 'type:target'.");
492struct UiSyncSnapshot {
493 int64_t frame_id = 0;
494 int32_t pending_editor_actions = 0;
495 int32_t pending_layout_actions = 0;
496 bool layout_rebuild_pending =
false;
497 int32_t pending_harness_tests = 0;
498 int32_t queued_harness_tests = 0;
499 int32_t running_harness_tests = 0;
502editor::EditorManager* GetEditorManagerForSync() {
510UiSyncSnapshot CaptureUiSyncSnapshot(TestManager* test_manager) {
511 UiSyncSnapshot snapshot;
512 if (editor::EditorManager* editor_manager = GetEditorManagerForSync()) {
513 const editor::EditorManager::UiSyncState state =
514 editor_manager->GetUiSyncStateSnapshot();
515 snapshot.frame_id =
static_cast<int64_t
>(state.frame_id);
516 snapshot.pending_editor_actions = state.pending_editor_actions;
517 snapshot.pending_layout_actions = state.pending_layout_actions;
518 snapshot.layout_rebuild_pending = state.layout_rebuild_pending;
522 const std::vector<HarnessTestSummary> summaries =
523 test_manager->ListHarnessTestSummaries();
524 for (
const HarnessTestSummary& summary : summaries) {
525 switch (summary.latest_execution.status) {
526 case HarnessTestStatus::kQueued:
527 snapshot.queued_harness_tests++;
529 case HarnessTestStatus::kRunning:
530 snapshot.running_harness_tests++;
538 snapshot.pending_harness_tests =
539 snapshot.queued_harness_tests + snapshot.running_harness_tests;
543bool IsUiIdle(
const UiSyncSnapshot& snapshot) {
544 return snapshot.pending_editor_actions == 0 &&
545 snapshot.pending_layout_actions == 0 &&
546 !snapshot.layout_rebuild_pending &&
547 snapshot.pending_harness_tests == 0;
550std::string BuildIdleTimeoutReason(
const UiSyncSnapshot& snapshot) {
551 return absl::StrFormat(
552 "pending_editor=%d pending_layout=%d layout_rebuild=%s queued_tests=%d "
554 snapshot.pending_editor_actions, snapshot.pending_layout_actions,
555 snapshot.layout_rebuild_pending ?
"true" :
"false",
556 snapshot.queued_harness_tests, snapshot.running_harness_tests);
559int32_t NormalizeTimeoutMs(int32_t timeout_ms, int32_t fallback_ms) {
560 return timeout_ms <= 0 ? fallback_ms : timeout_ms;
563int32_t NormalizePollIntervalMs(int32_t poll_interval_ms) {
564 if (poll_interval_ms <= 0) {
567 return std::clamp(poll_interval_ms, 5, 1000);
573class ImGuiTestHarnessServiceGrpc final :
public ImGuiTestHarness::Service {
575 explicit ImGuiTestHarnessServiceGrpc(ImGuiTestHarnessServiceImpl* impl)
578 ::grpc::Status Ping(::grpc::ServerContext* context,
579 const PingRequest* request,
580 PingResponse* response)
override {
581 return ConvertStatus(impl_->Ping(request, response));
584 ::grpc::Status Click(::grpc::ServerContext* context,
585 const ClickRequest* request,
586 ClickResponse* response)
override {
587 return ConvertStatus(impl_->Click(request, response));
590 ::grpc::Status Type(::grpc::ServerContext* context,
591 const TypeRequest* request,
592 TypeResponse* response)
override {
593 return ConvertStatus(impl_->Type(request, response));
596 ::grpc::Status Wait(::grpc::ServerContext* context,
597 const WaitRequest* request,
598 WaitResponse* response)
override {
599 return ConvertStatus(impl_->Wait(request, response));
602 ::grpc::Status Assert(::grpc::ServerContext* context,
603 const AssertRequest* request,
604 AssertResponse* response)
override {
605 return ConvertStatus(impl_->Assert(request, response));
608 ::grpc::Status FlushUiActions(::grpc::ServerContext* context,
609 const FlushUiActionsRequest* request,
610 FlushUiActionsResponse* response)
override {
611 return ConvertStatus(impl_->FlushUiActions(request, response));
614 ::grpc::Status WaitForIdle(::grpc::ServerContext* context,
615 const WaitForIdleRequest* request,
616 WaitForIdleResponse* response)
override {
617 return ConvertStatus(impl_->WaitForIdle(request, response));
620 ::grpc::Status GetUiSyncState(::grpc::ServerContext* context,
621 const GetUiSyncStateRequest* request,
622 GetUiSyncStateResponse* response)
override {
623 return ConvertStatus(impl_->GetUiSyncState(request, response));
626 ::grpc::Status Screenshot(::grpc::ServerContext* context,
627 const ScreenshotRequest* request,
628 ScreenshotResponse* response)
override {
629 return ConvertStatus(impl_->Screenshot(request, response));
632 ::grpc::Status GetTestStatus(::grpc::ServerContext* context,
633 const GetTestStatusRequest* request,
634 GetTestStatusResponse* response)
override {
635 return ConvertStatus(impl_->GetTestStatus(request, response));
638 ::grpc::Status ListTests(::grpc::ServerContext* context,
639 const ListTestsRequest* request,
640 ListTestsResponse* response)
override {
641 return ConvertStatus(impl_->ListTests(request, response));
644 ::grpc::Status GetTestResults(::grpc::ServerContext* context,
645 const GetTestResultsRequest* request,
646 GetTestResultsResponse* response)
override {
647 return ConvertStatus(impl_->GetTestResults(request, response));
650 ::grpc::Status DiscoverWidgets(::grpc::ServerContext* context,
651 const DiscoverWidgetsRequest* request,
652 DiscoverWidgetsResponse* response)
override {
653 return ConvertStatus(impl_->DiscoverWidgets(request, response));
656 ::grpc::Status StartRecording(::grpc::ServerContext* context,
657 const StartRecordingRequest* request,
658 StartRecordingResponse* response)
override {
659 return ConvertStatus(impl_->StartRecording(request, response));
662 ::grpc::Status StopRecording(::grpc::ServerContext* context,
663 const StopRecordingRequest* request,
664 StopRecordingResponse* response)
override {
665 return ConvertStatus(impl_->StopRecording(request, response));
668 ::grpc::Status ReplayTest(::grpc::ServerContext* context,
669 const ReplayTestRequest* request,
670 ReplayTestResponse* response)
override {
671 return ConvertStatus(impl_->ReplayTest(request, response));
675 static ::grpc::Status ConvertStatus(
const absl::Status& status) {
677 return ::grpc::Status::OK;
680 ::grpc::StatusCode code = ::grpc::StatusCode::UNKNOWN;
681 switch (status.code()) {
682 case absl::StatusCode::kCancelled:
683 code = ::grpc::StatusCode::CANCELLED;
685 case absl::StatusCode::kUnknown:
686 code = ::grpc::StatusCode::UNKNOWN;
688 case absl::StatusCode::kInvalidArgument:
689 code = ::grpc::StatusCode::INVALID_ARGUMENT;
691 case absl::StatusCode::kDeadlineExceeded:
692 code = ::grpc::StatusCode::DEADLINE_EXCEEDED;
694 case absl::StatusCode::kNotFound:
695 code = ::grpc::StatusCode::NOT_FOUND;
697 case absl::StatusCode::kAlreadyExists:
698 code = ::grpc::StatusCode::ALREADY_EXISTS;
700 case absl::StatusCode::kPermissionDenied:
701 code = ::grpc::StatusCode::PERMISSION_DENIED;
703 case absl::StatusCode::kResourceExhausted:
704 code = ::grpc::StatusCode::RESOURCE_EXHAUSTED;
706 case absl::StatusCode::kFailedPrecondition:
707 code = ::grpc::StatusCode::FAILED_PRECONDITION;
709 case absl::StatusCode::kAborted:
710 code = ::grpc::StatusCode::ABORTED;
712 case absl::StatusCode::kOutOfRange:
713 code = ::grpc::StatusCode::OUT_OF_RANGE;
715 case absl::StatusCode::kUnimplemented:
716 code = ::grpc::StatusCode::UNIMPLEMENTED;
718 case absl::StatusCode::kInternal:
719 code = ::grpc::StatusCode::INTERNAL;
721 case absl::StatusCode::kUnavailable:
722 code = ::grpc::StatusCode::UNAVAILABLE;
724 case absl::StatusCode::kDataLoss:
725 code = ::grpc::StatusCode::DATA_LOSS;
727 case absl::StatusCode::kUnauthenticated:
728 code = ::grpc::StatusCode::UNAUTHENTICATED;
731 code = ::grpc::StatusCode::UNKNOWN;
735 return ::grpc::Status(
736 code, std::string(status.message().data(), status.message().size()));
739 ImGuiTestHarnessServiceImpl* impl_;
742std::unique_ptr<::grpc::Service> CreateImGuiTestHarnessServiceGrpc(
743 ImGuiTestHarnessServiceImpl* impl) {
744 return std::make_unique<ImGuiTestHarnessServiceGrpc>(impl);
751absl::Status ImGuiTestHarnessServiceImpl::Ping(
const PingRequest* request,
752 PingResponse* response) {
754 response->set_message(absl::StrFormat(
"Pong: %s", request->message()));
757 auto now = std::chrono::system_clock::now();
758 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
759 now.time_since_epoch());
760 response->set_timestamp_ms(ms.count());
765 return absl::OkStatus();
768absl::Status ImGuiTestHarnessServiceImpl::Click(
const ClickRequest* request,
769 ClickResponse* response) {
770 auto start = std::chrono::steady_clock::now();
772 TestRecorder::RecordedStep recorded_step;
775 recorded_step.widget_key = request->widget_key();
776 if (!request->widget_key().empty()) {
777 recorded_step.target = absl::StrCat(
"widget_key:", request->widget_key());
779 recorded_step.target = request->target();
781 recorded_step.click_type = ClickTypeToString(request->type());
784 auto finalize = [&](
const absl::Status& status) {
785 recorded_step.success = response->success();
786 recorded_step.message = response->message();
787 recorded_step.execution_time_ms = response->execution_time_ms();
788 recorded_step.test_id = response->test_id();
789 MaybeRecordStep(&test_recorder_, recorded_step);
793 if (!test_manager_) {
794 response->set_success(
false);
795 response->set_message(
"TestManager not available");
796 response->set_execution_time_ms(0);
797 return finalize(absl::FailedPreconditionError(
"TestManager not available"));
800 const std::string requested_target = request ? request->target() :
"";
801 const std::string requested_widget_key = request ? request->widget_key() :
"";
802 const std::string request_summary =
803 !requested_widget_key.empty()
804 ? absl::StrFormat(
"widget_key:%s", requested_widget_key)
807 const std::string test_id = test_manager_->RegisterHarnessTest(
808 absl::StrFormat(
"Click: %s", request_summary),
"grpc");
809 response->set_test_id(test_id);
810 recorded_step.test_id = test_id;
811 test_manager_->AppendHarnessTestLog(
812 test_id, absl::StrCat(
"Queued click request: ", request_summary));
814 absl::StatusOr<internal::ResolvedWidgetSelector> resolved_selector =
815 internal::ResolveWidgetSelector(requested_target, requested_widget_key);
816 if (!resolved_selector.ok()) {
817 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
818 std::chrono::steady_clock::now() - start);
819 std::string message = std::string(resolved_selector.status().message());
820 response->set_success(
false);
821 response->set_message(message);
822 response->set_execution_time_ms(elapsed.count());
823 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
825 test_manager_->AppendHarnessTestLog(test_id, message);
826 return finalize(absl::OkStatus());
829 if (!resolved_selector->resolved_widget_key.empty()) {
830 response->set_resolved_widget_key(resolved_selector->resolved_widget_key);
832 if (!resolved_selector->resolved_path.empty()) {
833 response->set_resolved_path(resolved_selector->resolved_path);
836 const std::string widget_type = resolved_selector->target.type;
837 const std::string widget_label = resolved_selector->target.label;
838 const ImGuiID widget_id = resolved_selector->imgui_id;
840#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
841 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
843 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
844 std::chrono::steady_clock::now() - start);
845 std::string message =
"ImGuiTestEngine not initialized";
846 response->set_success(
false);
847 response->set_message(message);
848 response->set_execution_time_ms(elapsed.count());
849 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
851 return finalize(absl::OkStatus());
854 const ClickRequest::ClickType click_type =
855 request ? request->type() : ClickRequest::CLICK_TYPE_LEFT;
857 ImGuiMouseButton mouse_button = ImGuiMouseButton_Left;
858 switch (click_type) {
859 case ClickRequest::CLICK_TYPE_UNSPECIFIED:
860 case ClickRequest::CLICK_TYPE_LEFT:
861 mouse_button = ImGuiMouseButton_Left;
863 case ClickRequest::CLICK_TYPE_RIGHT:
864 mouse_button = ImGuiMouseButton_Right;
866 case ClickRequest::CLICK_TYPE_MIDDLE:
867 mouse_button = ImGuiMouseButton_Middle;
869 case ClickRequest::CLICK_TYPE_DOUBLE:
874 auto test_data = std::make_shared<DynamicTestData>();
875 TestManager* manager = test_manager_;
876 test_data->test_func = [manager, captured_id = test_id, widget_type,
877 widget_label, widget_id, click_type,
878 mouse_button](ImGuiTestContext* ctx) {
879 manager->MarkHarnessTestRunning(captured_id);
881 if (widget_type ==
"shortcut") {
882 ImGuiKeyChord chord = 0;
883 if (widget_label ==
"Save ROM") {
884 chord = ImGuiMod_Shortcut | ImGuiKey_S;
885 }
else if (widget_label ==
"Settings") {
886 chord = ImGuiMod_Shortcut | ImGuiKey_Comma;
887 }
else if (widget_label ==
"Quit") {
893 auto* editor_manager =
895 if (editor_manager ==
nullptr) {
896 const std::string error_message =
897 "Application quit is unavailable before editor startup";
898 manager->AppendHarnessTestLog(captured_id, error_message);
899 manager->MarkHarnessTestCompleted(
900 captured_id, HarnessTestStatus::kFailed, error_message);
904 editor_manager->
Quit();
905 if (!editor_manager->quit()) {
906 const std::string error_message = absl::StrCat(
907 "Application quit was blocked: ",
908 editor_manager->GetPendingUnsavedSessionActionPrompt());
909 manager->AppendHarnessTestLog(captured_id, error_message);
910 manager->MarkHarnessTestCompleted(
911 captured_id, HarnessTestStatus::kFailed, error_message);
915 const std::string success_message =
"Requested application quit";
916 manager->AppendHarnessTestLog(captured_id, success_message);
917 manager->MarkHarnessTestCompleted(
918 captured_id, HarnessTestStatus::kPassed, success_message);
921 const std::string error_message = absl::StrFormat(
922 "Unsupported harness shortcut '%s'", widget_label);
923 manager->AppendHarnessTestLog(captured_id, error_message);
924 manager->MarkHarnessTestCompleted(
925 captured_id, HarnessTestStatus::kFailed, error_message);
929 ctx->KeyPress(chord);
931 if (ctx->IsError()) {
932 const std::string error_message =
933 absl::StrFormat(
"Shortcut failed for '%s'", widget_label);
934 manager->AppendHarnessTestLog(captured_id, error_message);
935 manager->MarkHarnessTestCompleted(
936 captured_id, HarnessTestStatus::kFailed, error_message);
939 const std::string success_message =
940 absl::StrFormat(
"Pressed shortcut '%s'", widget_label);
941 manager->AppendHarnessTestLog(captured_id, success_message);
942 manager->MarkHarnessTestCompleted(
943 captured_id, HarnessTestStatus::kPassed, success_message);
947 const ImGuiTestRef widget_ref = widget_id != 0
948 ? ImGuiTestRef(widget_id)
949 : ImGuiTestRef(widget_label.c_str());
950 const ImGuiTestItemInfo item =
951 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
953 const std::string error_message =
954 absl::StrFormat(
"Click target '%s' not found", widget_label);
955 manager->AppendHarnessTestLog(captured_id, error_message);
956 manager->MarkHarnessTestCompleted(
957 captured_id, HarnessTestStatus::kFailed, error_message);
961 if (click_type == ClickRequest::CLICK_TYPE_DOUBLE) {
962 ctx->ItemDoubleClick(ImGuiTestRef(item.ID));
964 ctx->ItemClick(ImGuiTestRef(item.ID), mouse_button);
967 if (ctx->IsError()) {
968 const std::string error_message =
969 absl::StrFormat(
"Click failed for '%s'", widget_label);
970 manager->AppendHarnessTestLog(captured_id, error_message);
971 manager->MarkHarnessTestCompleted(
972 captured_id, HarnessTestStatus::kFailed, error_message);
975 const std::string success_message =
976 absl::StrFormat(
"Clicked %s '%s'", widget_type, widget_label);
977 manager->AppendHarnessTestLog(captured_id, success_message);
978 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kPassed,
980 }
catch (
const std::exception& e) {
981 const std::string error_message =
982 absl::StrFormat(
"Click failed: %s", e.what());
983 manager->AppendHarnessTestLog(captured_id, error_message);
984 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
989 std::string test_name = absl::StrFormat(
991 static_cast<long long>(
992 std::chrono::system_clock::now().time_since_epoch().count()));
994 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
995 test->TestFunc = RunDynamicTest;
996 test->UserData = test_data.get();
998 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
999 KeepDynamicTestData(test_data);
1001 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1002 std::chrono::steady_clock::now() - start);
1003 std::string message =
1004 absl::StrFormat(
"Queued click on %s '%s'", widget_type, widget_label);
1005 response->set_success(
true);
1006 response->set_message(message);
1007 response->set_execution_time_ms(elapsed.count());
1008 test_manager_->AppendHarnessTestLog(test_id, message);
1011 const std::string message =
"ImGuiTestEngine is not available in this build";
1012 test_manager_->MarkHarnessTestRunning(test_id);
1013 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1015 test_manager_->AppendHarnessTestLog(test_id, message);
1017 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1018 std::chrono::steady_clock::now() - start);
1019 response->set_success(
false);
1020 response->set_message(message);
1021 response->set_execution_time_ms(elapsed.count());
1024 return finalize(absl::OkStatus());
1027absl::Status ImGuiTestHarnessServiceImpl::Type(
const TypeRequest* request,
1028 TypeResponse* response) {
1029 auto start = std::chrono::steady_clock::now();
1031 TestRecorder::RecordedStep recorded_step;
1034 recorded_step.widget_key = request->widget_key();
1035 if (!request->widget_key().empty()) {
1036 recorded_step.target = absl::StrCat(
"widget_key:", request->widget_key());
1038 recorded_step.target = request->target();
1040 recorded_step.text = request->text();
1041 recorded_step.clear_first = request->clear_first();
1044 auto finalize = [&](
const absl::Status& status) {
1045 recorded_step.success = response->success();
1046 recorded_step.message = response->message();
1047 recorded_step.execution_time_ms = response->execution_time_ms();
1048 recorded_step.test_id = response->test_id();
1049 MaybeRecordStep(&test_recorder_, recorded_step);
1053 if (!test_manager_) {
1054 response->set_success(
false);
1055 response->set_message(
"TestManager not available");
1056 response->set_execution_time_ms(0);
1057 return finalize(absl::FailedPreconditionError(
"TestManager not available"));
1060 const std::string requested_target = request ? request->target() :
"";
1061 const std::string requested_widget_key = request ? request->widget_key() :
"";
1062 const std::string request_summary =
1063 !requested_widget_key.empty()
1064 ? absl::StrFormat(
"widget_key:%s", requested_widget_key)
1067 const std::string test_id = test_manager_->RegisterHarnessTest(
1068 absl::StrFormat(
"Type: %s", request_summary),
"grpc");
1069 response->set_test_id(test_id);
1070 recorded_step.test_id = test_id;
1071 test_manager_->AppendHarnessTestLog(
1072 test_id, absl::StrFormat(
"Queued type request: %s", request_summary));
1074 absl::StatusOr<internal::ResolvedWidgetSelector> resolved_selector =
1075 internal::ResolveWidgetSelector(requested_target, requested_widget_key);
1076 if (!resolved_selector.ok()) {
1077 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1078 std::chrono::steady_clock::now() - start);
1079 std::string message = std::string(resolved_selector.status().message());
1080 response->set_success(
false);
1081 response->set_message(message);
1082 response->set_execution_time_ms(elapsed.count());
1083 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1085 test_manager_->AppendHarnessTestLog(test_id, message);
1086 return finalize(absl::OkStatus());
1089 if (!resolved_selector->resolved_widget_key.empty()) {
1090 response->set_resolved_widget_key(resolved_selector->resolved_widget_key);
1092 if (!resolved_selector->resolved_path.empty()) {
1093 response->set_resolved_path(resolved_selector->resolved_path);
1096 const std::string widget_type = resolved_selector->target.type;
1097 const std::string widget_label = resolved_selector->target.label;
1098 const ImGuiID widget_id = resolved_selector->imgui_id;
1100#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1101 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
1103 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1104 std::chrono::steady_clock::now() - start);
1105 std::string message =
"ImGuiTestEngine not initialized";
1106 response->set_success(
false);
1107 response->set_message(message);
1108 response->set_execution_time_ms(elapsed.count());
1109 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1111 return finalize(absl::OkStatus());
1113 std::string text = request->text();
1114 bool clear_first = request->clear_first();
1116 auto rpc_state = std::make_shared<RPCState<bool>>();
1117 auto test_data = std::make_shared<DynamicTestData>();
1118 TestManager* manager = test_manager_;
1119 test_data->test_func = [manager, captured_id = test_id, widget_type,
1120 widget_label, widget_id, clear_first, text,
1121 rpc_state](ImGuiTestContext* ctx) {
1122 manager->MarkHarnessTestRunning(captured_id);
1124 const ImGuiTestRef widget_ref = widget_id != 0
1125 ? ImGuiTestRef(widget_id)
1126 : ImGuiTestRef(widget_label.c_str());
1127 ImGuiTestItemInfo item =
1128 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1130 std::string error_message =
1131 absl::StrFormat(
"Input field '%s' not found", widget_label);
1132 manager->AppendHarnessTestLog(captured_id, error_message);
1133 manager->MarkHarnessTestCompleted(
1134 captured_id, HarnessTestStatus::kFailed, error_message);
1135 rpc_state->SetResult(
false, error_message);
1139 ctx->ItemClick(ImGuiTestRef(item.ID));
1141 ctx->KeyPress(ImGuiMod_Shortcut | ImGuiKey_A);
1142 ctx->KeyPress(ImGuiKey_Delete);
1145 ctx->ItemInputValue(ImGuiTestRef(item.ID), text.c_str());
1147 if (ctx->IsError()) {
1148 const std::string error_message =
1149 absl::StrFormat(
"Type failed for '%s'", widget_label);
1150 manager->AppendHarnessTestLog(captured_id, error_message);
1151 manager->MarkHarnessTestCompleted(
1152 captured_id, HarnessTestStatus::kFailed, error_message);
1153 rpc_state->SetResult(
false, error_message);
1157 std::string success_message =
1158 absl::StrFormat(
"Typed '%s' into %s '%s'%s", text, widget_type,
1159 widget_label, clear_first ?
" (cleared first)" :
"");
1160 manager->AppendHarnessTestLog(captured_id, success_message);
1161 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kPassed,
1163 rpc_state->SetResult(
true, success_message);
1164 }
catch (
const std::exception& e) {
1165 std::string error_message = absl::StrFormat(
"Type failed: %s", e.what());
1166 manager->AppendHarnessTestLog(captured_id, error_message);
1167 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
1169 rpc_state->SetResult(
false, error_message);
1173 std::string test_name = absl::StrFormat(
1175 static_cast<long long>(
1176 std::chrono::system_clock::now().time_since_epoch().count()));
1178 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
1179 test->TestFunc = RunDynamicTest;
1180 test->UserData = test_data.get();
1182 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
1183 KeepDynamicTestData(test_data);
1185 auto timeout = std::chrono::seconds(5);
1186 auto wait_start = std::chrono::steady_clock::now();
1187 while (!rpc_state->completed.load()) {
1188 if (std::chrono::steady_clock::now() - wait_start > timeout) {
1189 std::string error_message =
1190 "Test timeout - input field not found or unresponsive";
1191 manager->AppendHarnessTestLog(test_id, error_message);
1192 manager->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kTimeout,
1194 rpc_state->SetResult(
false, error_message);
1197 std::this_thread::sleep_for(std::chrono::milliseconds(100));
1200 bool success =
false;
1201 std::string message;
1202 rpc_state->GetResult(success, message);
1203 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1204 std::chrono::steady_clock::now() - start);
1206 response->set_success(success);
1207 response->set_message(message);
1208 response->set_execution_time_ms(elapsed.count());
1209 if (!message.empty()) {
1210 test_manager_->AppendHarnessTestLog(test_id, message);
1214 test_manager_->MarkHarnessTestRunning(test_id);
1215 const std::string message =
"ImGuiTestEngine is not available in this build";
1216 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1218 test_manager_->AppendHarnessTestLog(test_id, message);
1220 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1221 std::chrono::steady_clock::now() - start);
1222 response->set_success(
false);
1223 response->set_message(message);
1224 response->set_execution_time_ms(elapsed.count());
1227 return finalize(absl::OkStatus());
1230absl::Status ImGuiTestHarnessServiceImpl::Wait(
const WaitRequest* request,
1231 WaitResponse* response) {
1232 auto start = std::chrono::steady_clock::now();
1234 TestRecorder::RecordedStep recorded_step;
1237 recorded_step.widget_key = request->widget_key();
1238 if (!request->widget_key().empty() && request->condition().empty()) {
1239 recorded_step.condition =
1240 absl::StrCat(
"widget_key:", request->widget_key());
1242 recorded_step.condition = request->condition();
1244 recorded_step.timeout_ms = request->timeout_ms();
1247 auto finalize = [&](
const absl::Status& status) {
1248 recorded_step.success = response->success();
1249 recorded_step.message = response->message();
1250 recorded_step.execution_time_ms = response->elapsed_ms();
1251 recorded_step.test_id = response->test_id();
1252 MaybeRecordStep(&test_recorder_, recorded_step);
1256 if (!test_manager_) {
1257 response->set_success(
false);
1258 response->set_message(
"TestManager not available");
1259 response->set_elapsed_ms(0);
1260 return finalize(absl::FailedPreconditionError(
"TestManager not available"));
1263 const std::string requested_condition = request ? request->condition() :
"";
1264 const std::string requested_widget_key = request ? request->widget_key() :
"";
1265 const std::string request_summary =
1266 !requested_widget_key.empty()
1267 ? absl::StrFormat(
"%s [widget_key:%s]",
1268 requested_condition.empty()
1269 ?
"element_visible:<auto>"
1270 : requested_condition,
1271 requested_widget_key)
1272 : requested_condition;
1274 const std::string test_id = test_manager_->RegisterHarnessTest(
1275 absl::StrFormat(
"Wait: %s", request_summary),
"grpc");
1276 response->set_test_id(test_id);
1277 recorded_step.test_id = test_id;
1278 test_manager_->AppendHarnessTestLog(
1279 test_id, absl::StrFormat(
"Queued wait condition: %s", request_summary));
1281 internal::ResolvedWidgetSelector resolved_selector;
1282 absl::StatusOr<ParsedCondition> parsed_condition = ResolveCondition(
1283 requested_condition, requested_widget_key,
1284 "element_visible", &resolved_selector);
1285 if (!parsed_condition.ok()) {
1286 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1287 std::chrono::steady_clock::now() - start);
1288 std::string message = std::string(parsed_condition.status().message());
1289 response->set_success(
false);
1290 response->set_message(message);
1291 response->set_elapsed_ms(elapsed.count());
1292 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1294 test_manager_->AppendHarnessTestLog(test_id, message);
1295 return finalize(absl::OkStatus());
1298 if (!resolved_selector.resolved_widget_key.empty()) {
1299 response->set_resolved_widget_key(resolved_selector.resolved_widget_key);
1301 if (!resolved_selector.resolved_path.empty()) {
1302 response->set_resolved_path(resolved_selector.resolved_path);
1305 const std::string condition_type = parsed_condition->type;
1306 const std::string condition_target = parsed_condition->target;
1307 const ImGuiID widget_id = resolved_selector.imgui_id;
1308 const std::string resolved_condition =
1309 absl::StrFormat(
"%s:%s", condition_type, condition_target);
1311#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1312 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
1314 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1315 std::chrono::steady_clock::now() - start);
1316 std::string message =
"ImGuiTestEngine not initialized";
1317 response->set_success(
false);
1318 response->set_message(message);
1319 response->set_elapsed_ms(elapsed.count());
1320 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1322 return finalize(absl::OkStatus());
1324 int timeout_ms = request->timeout_ms() > 0 ? request->timeout_ms() : 5000;
1325 int poll_interval_ms =
1326 request->poll_interval_ms() > 0 ? request->poll_interval_ms() : 100;
1328 auto test_data = std::make_shared<DynamicTestData>();
1329 TestManager* manager = test_manager_;
1330 test_data->test_func = [manager, captured_id = test_id, condition_type,
1331 condition_target, widget_id, timeout_ms,
1332 poll_interval_ms](ImGuiTestContext* ctx) {
1333 manager->MarkHarnessTestRunning(captured_id);
1334 auto poll_start = std::chrono::steady_clock::now();
1335 auto timeout = std::chrono::milliseconds(timeout_ms);
1337 for (
int i = 0; i < 10; ++i) {
1342 while (std::chrono::steady_clock::now() - poll_start < timeout) {
1343 bool current_state =
false;
1344 const ImGuiTestRef widget_ref =
1345 widget_id != 0 ? ImGuiTestRef(widget_id)
1346 : ImGuiTestRef(condition_target.c_str());
1348 if (condition_type ==
"window_visible") {
1349 ImGuiTestItemInfo window_info =
1350 ctx->WindowInfo(widget_ref, ImGuiTestOpFlags_NoError);
1351 current_state = (window_info.ID != 0);
1352 }
else if (condition_type ==
"element_visible") {
1353 ImGuiTestItemInfo item =
1354 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1355 current_state = (item.ID != 0 && item.RectClipped.GetWidth() > 0 &&
1356 item.RectClipped.GetHeight() > 0);
1357 }
else if (condition_type ==
"element_enabled") {
1358 ImGuiTestItemInfo item =
1359 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1361 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
1363 std::string error_message =
1364 absl::StrFormat(
"Unknown condition type: %s", condition_type);
1365 manager->AppendHarnessTestLog(captured_id, error_message);
1366 manager->MarkHarnessTestCompleted(
1367 captured_id, HarnessTestStatus::kFailed, error_message);
1371 if (current_state) {
1373 std::chrono::duration_cast<std::chrono::milliseconds>(
1374 std::chrono::steady_clock::now() - poll_start);
1375 std::string success_message = absl::StrFormat(
1376 "Condition '%s:%s' met after %lld ms", condition_type,
1377 condition_target,
static_cast<long long>(elapsed_ms.count()));
1378 manager->AppendHarnessTestLog(captured_id, success_message);
1379 manager->MarkHarnessTestCompleted(
1380 captured_id, HarnessTestStatus::kPassed, success_message);
1384 std::this_thread::sleep_for(
1385 std::chrono::milliseconds(poll_interval_ms));
1389 std::string timeout_message =
1390 absl::StrFormat(
"Condition '%s:%s' not met after %d ms timeout",
1391 condition_type, condition_target, timeout_ms);
1392 manager->AppendHarnessTestLog(captured_id, timeout_message);
1393 manager->MarkHarnessTestCompleted(
1394 captured_id, HarnessTestStatus::kTimeout, timeout_message);
1395 }
catch (
const std::exception& e) {
1396 std::string error_message = absl::StrFormat(
"Wait failed: %s", e.what());
1397 manager->AppendHarnessTestLog(captured_id, error_message);
1398 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
1403 std::string test_name = absl::StrFormat(
1405 static_cast<long long>(
1406 std::chrono::system_clock::now().time_since_epoch().count()));
1408 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
1409 test->TestFunc = RunDynamicTest;
1410 test->UserData = test_data.get();
1412 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
1413 KeepDynamicTestData(test_data);
1415 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1416 std::chrono::steady_clock::now() - start);
1417 std::string message =
1418 absl::StrFormat(
"Queued wait for '%s'", resolved_condition);
1419 response->set_success(
true);
1420 response->set_message(message);
1421 response->set_elapsed_ms(elapsed.count());
1422 test_manager_->AppendHarnessTestLog(test_id, message);
1425 test_manager_->MarkHarnessTestRunning(test_id);
1426 const std::string message =
"ImGuiTestEngine is not available in this build";
1427 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1429 test_manager_->AppendHarnessTestLog(test_id, message);
1431 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1432 std::chrono::steady_clock::now() - start);
1433 response->set_success(
false);
1434 response->set_message(message);
1435 response->set_elapsed_ms(elapsed.count());
1438 return finalize(absl::OkStatus());
1441absl::Status ImGuiTestHarnessServiceImpl::Assert(
const AssertRequest* request,
1442 AssertResponse* response) {
1443 TestRecorder::RecordedStep recorded_step;
1446 recorded_step.widget_key = request->widget_key();
1447 if (!request->widget_key().empty() && request->condition().empty()) {
1448 recorded_step.condition =
1449 absl::StrCat(
"widget_key:", request->widget_key());
1451 recorded_step.condition = request->condition();
1455 auto finalize = [&](
const absl::Status& status) {
1456 recorded_step.success = response->success();
1457 recorded_step.message = response->message();
1458 recorded_step.expected_value = response->expected_value();
1459 recorded_step.actual_value = response->actual_value();
1460 recorded_step.test_id = response->test_id();
1461 MaybeRecordStep(&test_recorder_, recorded_step);
1465 if (!test_manager_) {
1466 response->set_success(
false);
1467 response->set_message(
"TestManager not available");
1468 response->set_actual_value(
"N/A");
1469 response->set_expected_value(
"N/A");
1470 return finalize(absl::FailedPreconditionError(
"TestManager not available"));
1473 const std::string requested_condition = request ? request->condition() :
"";
1474 const std::string requested_widget_key = request ? request->widget_key() :
"";
1475 const std::string request_summary =
1476 !requested_widget_key.empty()
1477 ? absl::StrFormat(
"%s [widget_key:%s]",
1478 requested_condition.empty() ?
"exists:<auto>"
1479 : requested_condition,
1480 requested_widget_key)
1481 : requested_condition;
1483 const std::string test_id = test_manager_->RegisterHarnessTest(
1484 absl::StrFormat(
"Assert: %s", request_summary),
"grpc");
1485 response->set_test_id(test_id);
1486 recorded_step.test_id = test_id;
1487 test_manager_->AppendHarnessTestLog(
1488 test_id, absl::StrFormat(
"Queued assertion: %s", request_summary));
1490 internal::ResolvedWidgetSelector resolved_selector;
1491 absl::StatusOr<ParsedCondition> parsed_condition = ResolveCondition(
1492 requested_condition, requested_widget_key,
1493 "exists", &resolved_selector);
1494 if (!parsed_condition.ok()) {
1495 std::string message = std::string(parsed_condition.status().message());
1496 response->set_success(
false);
1497 response->set_message(message);
1498 response->set_actual_value(
"N/A");
1499 response->set_expected_value(
"N/A");
1500 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1502 test_manager_->AppendHarnessTestLog(test_id, message);
1503 return finalize(absl::OkStatus());
1506 if (!resolved_selector.resolved_widget_key.empty()) {
1507 response->set_resolved_widget_key(resolved_selector.resolved_widget_key);
1509 if (!resolved_selector.resolved_path.empty()) {
1510 response->set_resolved_path(resolved_selector.resolved_path);
1513 const std::string assertion_type = parsed_condition->type;
1514 const std::string assertion_target = parsed_condition->target;
1515 const ImGuiID widget_id = resolved_selector.imgui_id;
1516 const std::string widget_label = resolved_selector.target.label;
1518 if (assertion_type ==
"value_equals" && widget_id == 0) {
1519 const std::string message =
1520 "value_equals requires widget_key so the input is unambiguous";
1521 response->set_success(
false);
1522 response->set_message(message);
1523 response->set_actual_value(
"N/A");
1524 response->set_expected_value(assertion_target);
1525 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1527 test_manager_->AppendHarnessTestLog(test_id, message);
1528 return finalize(absl::OkStatus());
1531#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1532 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
1534 std::string message =
"ImGuiTestEngine not initialized";
1535 response->set_success(
false);
1536 response->set_message(message);
1537 response->set_actual_value(
"N/A");
1538 response->set_expected_value(
"N/A");
1539 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1541 return finalize(absl::OkStatus());
1544 auto test_data = std::make_shared<DynamicTestData>();
1545 TestManager* manager = test_manager_;
1546 test_data->test_func = [manager, captured_id = test_id, assertion_type,
1547 assertion_target, widget_id,
1548 widget_label](ImGuiTestContext* ctx) {
1549 manager->MarkHarnessTestRunning(captured_id);
1551 auto complete_with = [manager, captured_id](
bool passed,
1552 const std::string& message,
1553 const std::string& actual,
1554 const std::string& expected,
1555 HarnessTestStatus status) {
1556 manager->AppendHarnessTestLog(captured_id, message);
1557 if (!actual.empty() || !expected.empty()) {
1558 manager->AppendHarnessTestLog(
1560 absl::StrFormat(
"Actual: %s | Expected: %s", actual, expected));
1562 manager->MarkHarnessTestCompleted(captured_id, status,
1563 passed ?
"" : message);
1567 bool passed =
false;
1568 std::string actual_value;
1569 std::string expected_value;
1570 std::string message;
1571 const ImGuiTestRef widget_ref =
1572 widget_id != 0 ? ImGuiTestRef(widget_id)
1573 : ImGuiTestRef(assertion_target.c_str());
1575 if (assertion_type ==
"visible") {
1576 const ImGuiTestItemInfo item =
1578 ? ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError)
1579 : ctx->WindowInfo(widget_ref, ImGuiTestOpFlags_NoError);
1580 const bool is_visible =
1582 (widget_id == 0 || (item.RectClipped.GetWidth() > 0 &&
1583 item.RectClipped.GetHeight() > 0));
1584 passed = is_visible;
1585 actual_value = is_visible ?
"visible" :
"hidden";
1586 expected_value =
"visible";
1587 const std::string target_description =
1588 widget_id != 0 ? widget_label : assertion_target;
1590 passed ? absl::StrFormat(
"'%s' is visible", target_description)
1591 : absl::StrFormat(
"'%s' is not visible", target_description);
1592 }
else if (assertion_type ==
"enabled") {
1593 ImGuiTestItemInfo item =
1594 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1596 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
1597 passed = is_enabled;
1598 actual_value = is_enabled ?
"enabled" :
"disabled";
1599 expected_value =
"enabled";
1601 passed ? absl::StrFormat(
"'%s' is enabled", assertion_target)
1602 : absl::StrFormat(
"'%s' is not enabled", assertion_target);
1603 }
else if (assertion_type ==
"exists") {
1604 ImGuiTestItemInfo item =
1605 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1606 bool exists = (item.ID != 0);
1608 actual_value = exists ?
"exists" :
"not found";
1609 expected_value =
"exists";
1610 message = passed ? absl::StrFormat(
"'%s' exists", assertion_target)
1611 : absl::StrFormat(
"'%s' not found", assertion_target);
1612 }
else if (assertion_type ==
"value_equals") {
1613 const ImGuiTestItemInfo item =
1614 ctx->ItemInfo(widget_ref, ImGuiTestOpFlags_NoError);
1617 actual_value =
"not found";
1618 expected_value = assertion_target;
1619 message = absl::StrFormat(
"Input '%s' not found", widget_label);
1621 const char* value = ctx->ItemReadAsString(ImGuiTestRef(item.ID));
1622 actual_value = value ? value :
"";
1623 expected_value = assertion_target;
1624 passed = !ctx->IsError() && actual_value == expected_value;
1625 message = passed ? absl::StrFormat(
"'%s' equals '%s'", widget_label,
1628 "'%s' does not equal '%s' (actual: '%s')",
1629 widget_label, expected_value, actual_value);
1631 }
else if (assertion_type ==
"text_contains") {
1632 size_t second_colon = assertion_target.find(
':');
1633 if (widget_id == 0 && second_colon == std::string::npos) {
1634 std::string error_message =
1635 "text_contains requires format "
1636 "'text_contains:target:expected_text'";
1637 complete_with(
false, error_message,
"N/A",
"N/A",
1638 HarnessTestStatus::kFailed);
1642 const std::string input_target =
1643 widget_id != 0 ? widget_label
1644 : assertion_target.substr(0, second_colon);
1645 const std::string expected_text =
1646 widget_id != 0 ? assertion_target
1647 : assertion_target.substr(second_colon + 1);
1648 const ImGuiTestRef input_ref = widget_id != 0
1649 ? ImGuiTestRef(widget_id)
1650 : ImGuiTestRef(input_target.c_str());
1652 ImGuiTestItemInfo item =
1653 ctx->ItemInfo(input_ref, ImGuiTestOpFlags_NoError);
1655 const char* value = ctx->ItemReadAsString(ImGuiTestRef(item.ID));
1656 const std::string actual_text = value ? value :
"";
1657 passed = actual_text.find(expected_text) != std::string::npos;
1658 actual_value = actual_text;
1659 expected_value = absl::StrFormat(
"contains '%s'", expected_text);
1660 message = passed ? absl::StrFormat(
"'%s' contains '%s'", input_target,
1663 "'%s' does not contain '%s' (actual: '%s')",
1664 input_target, expected_text, actual_text);
1667 actual_value =
"not found";
1668 expected_value = expected_text;
1669 message = absl::StrFormat(
"Input '%s' not found", input_target);
1672 std::string error_message =
1673 absl::StrFormat(
"Unknown assertion type: %s", assertion_type);
1674 complete_with(
false, error_message,
"N/A",
"N/A",
1675 HarnessTestStatus::kFailed);
1680 passed, message, actual_value, expected_value,
1681 passed ? HarnessTestStatus::kPassed : HarnessTestStatus::
kFailed);
1682 }
catch (
const std::exception& e) {
1683 std::string error_message =
1684 absl::StrFormat(
"Assertion failed: %s", e.what());
1685 manager->AppendHarnessTestLog(captured_id, error_message);
1686 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
1691 std::string test_name = absl::StrFormat(
1693 static_cast<long long>(
1694 std::chrono::system_clock::now().time_since_epoch().count()));
1696 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
1697 test->TestFunc = RunDynamicTest;
1698 test->UserData = test_data.get();
1700 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
1701 KeepDynamicTestData(test_data);
1703 response->set_success(
true);
1704 std::string message = absl::StrFormat(
"Queued assertion for '%s:%s'",
1705 assertion_type, assertion_target);
1706 response->set_message(message);
1707 response->set_actual_value(
"(async)");
1708 response->set_expected_value(
"(async)");
1709 test_manager_->AppendHarnessTestLog(test_id, message);
1712 test_manager_->MarkHarnessTestRunning(test_id);
1713 const std::string message =
"ImGuiTestEngine is not available in this build";
1714 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1716 test_manager_->AppendHarnessTestLog(test_id, message);
1718 response->set_success(
false);
1719 response->set_message(message);
1720 response->set_actual_value(
"(stub)");
1721 response->set_expected_value(
"(stub)");
1724 return finalize(absl::OkStatus());
1727absl::Status ImGuiTestHarnessServiceImpl::FlushUiActions(
1728 const FlushUiActionsRequest* request, FlushUiActionsResponse* response) {
1730 return absl::InvalidArgumentError(
"response cannot be null");
1733 const int32_t timeout_ms =
1734 NormalizeTimeoutMs(request ? request->timeout_ms() : 0, 2000);
1735 const absl::Duration timeout = absl::Milliseconds(timeout_ms);
1736 const absl::Time deadline = absl::Now() + timeout;
1737 const absl::Time started_at = absl::Now();
1739 UiSyncSnapshot initial = CaptureUiSyncSnapshot(test_manager_);
1740 UiSyncSnapshot snapshot = initial;
1742 if (initial.pending_editor_actions == 0 &&
1743 initial.pending_layout_actions == 0 && !initial.layout_rebuild_pending) {
1744 response->set_success(
true);
1745 response->set_message(
"UI queues already idle");
1746 response->set_elapsed_ms(0);
1747 response->set_frame_id(initial.frame_id);
1748 response->set_pending_editor_actions(initial.pending_editor_actions);
1749 response->set_pending_layout_actions(initial.pending_layout_actions);
1750 response->set_layout_rebuild_pending(initial.layout_rebuild_pending);
1751 response->set_pending_harness_tests(initial.pending_harness_tests);
1752 response->set_queued_harness_tests(initial.queued_harness_tests);
1753 response->set_running_harness_tests(initial.running_harness_tests);
1754 return absl::OkStatus();
1757 while (absl::Now() <= deadline) {
1758 snapshot = CaptureUiSyncSnapshot(test_manager_);
1759 const bool frame_advanced = snapshot.frame_id > initial.frame_id;
1760 const bool queues_flushed = snapshot.pending_editor_actions == 0 &&
1761 snapshot.pending_layout_actions == 0 &&
1762 !snapshot.layout_rebuild_pending;
1763 if (frame_advanced && queues_flushed) {
1766 absl::SleepFor(absl::Milliseconds(10));
1769 const int32_t elapsed_ms = ClampDurationToInt32(absl::Now() - started_at);
1770 const bool success = snapshot.pending_editor_actions == 0 &&
1771 snapshot.pending_layout_actions == 0 &&
1772 !snapshot.layout_rebuild_pending;
1773 response->set_success(success);
1774 response->set_message(success ?
"Deferred UI actions flushed"
1775 :
"Timed out waiting for UI action flush");
1776 response->set_elapsed_ms(elapsed_ms);
1777 response->set_frame_id(snapshot.frame_id);
1778 response->set_pending_editor_actions(snapshot.pending_editor_actions);
1779 response->set_pending_layout_actions(snapshot.pending_layout_actions);
1780 response->set_layout_rebuild_pending(snapshot.layout_rebuild_pending);
1781 response->set_pending_harness_tests(snapshot.pending_harness_tests);
1782 response->set_queued_harness_tests(snapshot.queued_harness_tests);
1783 response->set_running_harness_tests(snapshot.running_harness_tests);
1784 return absl::OkStatus();
1787absl::Status ImGuiTestHarnessServiceImpl::WaitForIdle(
1788 const WaitForIdleRequest* request, WaitForIdleResponse* response) {
1790 return absl::InvalidArgumentError(
"response cannot be null");
1793 const int32_t timeout_ms =
1794 NormalizeTimeoutMs(request ? request->timeout_ms() : 0, 5000);
1795 const int32_t required_stable_frames =
1796 std::max(1, request ? request->stable_frames() : 2);
1797 const int32_t poll_interval_ms =
1798 NormalizePollIntervalMs(request ? request->poll_interval_ms() : 25);
1799 const bool flush_first = request ? request->flush_first() :
false;
1801 const absl::Time started_at = absl::Now();
1802 const absl::Time deadline = started_at + absl::Milliseconds(timeout_ms);
1805 FlushUiActionsRequest flush_request;
1806 flush_request.set_timeout_ms(std::min(timeout_ms, 2000));
1807 FlushUiActionsResponse flush_response;
1808 absl::Status flush_status = FlushUiActions(&flush_request, &flush_response);
1809 if (!flush_status.ok()) {
1810 return flush_status;
1814 UiSyncSnapshot snapshot = CaptureUiSyncSnapshot(test_manager_);
1815 int32_t stable_frames_observed = 0;
1816 int64_t last_frame_id = -1;
1818 while (absl::Now() <= deadline) {
1819 snapshot = CaptureUiSyncSnapshot(test_manager_);
1821 const bool is_idle_now = IsUiIdle(snapshot);
1822 if (snapshot.frame_id != last_frame_id) {
1823 last_frame_id = snapshot.frame_id;
1824 stable_frames_observed = is_idle_now ? (stable_frames_observed + 1) : 0;
1825 }
else if (!is_idle_now) {
1826 stable_frames_observed = 0;
1829 if (stable_frames_observed >= required_stable_frames) {
1830 response->set_success(
true);
1831 response->set_message(absl::StrFormat(
1832 "UI idle for %d consecutive frame(s)", stable_frames_observed));
1833 response->set_elapsed_ms(ClampDurationToInt32(absl::Now() - started_at));
1834 response->set_last_frame_id(snapshot.frame_id);
1835 response->set_stable_frames_observed(stable_frames_observed);
1836 response->set_pending_editor_actions(snapshot.pending_editor_actions);
1837 response->set_pending_layout_actions(snapshot.pending_layout_actions);
1838 response->set_layout_rebuild_pending(snapshot.layout_rebuild_pending);
1839 response->set_pending_harness_tests(snapshot.pending_harness_tests);
1840 response->set_queued_harness_tests(snapshot.queued_harness_tests);
1841 response->set_running_harness_tests(snapshot.running_harness_tests);
1842 response->set_timeout_reason(
"");
1843 return absl::OkStatus();
1846 absl::SleepFor(absl::Milliseconds(poll_interval_ms));
1849 const std::string timeout_reason = BuildIdleTimeoutReason(snapshot);
1850 response->set_success(
false);
1851 response->set_message(
"Timed out waiting for deterministic UI idle");
1852 response->set_elapsed_ms(ClampDurationToInt32(absl::Now() - started_at));
1853 response->set_last_frame_id(snapshot.frame_id);
1854 response->set_stable_frames_observed(stable_frames_observed);
1855 response->set_pending_editor_actions(snapshot.pending_editor_actions);
1856 response->set_pending_layout_actions(snapshot.pending_layout_actions);
1857 response->set_layout_rebuild_pending(snapshot.layout_rebuild_pending);
1858 response->set_pending_harness_tests(snapshot.pending_harness_tests);
1859 response->set_queued_harness_tests(snapshot.queued_harness_tests);
1860 response->set_running_harness_tests(snapshot.running_harness_tests);
1861 response->set_timeout_reason(timeout_reason);
1862 return absl::OkStatus();
1865absl::Status ImGuiTestHarnessServiceImpl::GetUiSyncState(
1866 const GetUiSyncStateRequest* ,
1867 GetUiSyncStateResponse* response) {
1869 return absl::InvalidArgumentError(
"response cannot be null");
1872 const UiSyncSnapshot snapshot = CaptureUiSyncSnapshot(test_manager_);
1873 response->set_frame_id(snapshot.frame_id);
1874 response->set_pending_editor_actions(snapshot.pending_editor_actions);
1875 response->set_pending_layout_actions(snapshot.pending_layout_actions);
1876 response->set_layout_rebuild_pending(snapshot.layout_rebuild_pending);
1877 response->set_pending_harness_tests(snapshot.pending_harness_tests);
1878 response->set_queued_harness_tests(snapshot.queued_harness_tests);
1879 response->set_running_harness_tests(snapshot.running_harness_tests);
1880 response->set_sampled_at_ms(absl::ToUnixMillis(absl::Now()));
1881 response->set_idle(IsUiIdle(snapshot));
1882 return absl::OkStatus();
1885absl::Status ImGuiTestHarnessServiceImpl::Screenshot(
1886 const ScreenshotRequest* request, ScreenshotResponse* response) {
1888 return absl::InvalidArgumentError(
"response cannot be null");
1892 const std::string requested_path =
1893 request ? request->output_path() : std::string();
1896 switch (request->format()) {
1897 case ScreenshotRequest::IMAGE_FORMAT_UNSPECIFIED:
1899 case ScreenshotRequest::IMAGE_FORMAT_PNG:
1902 case ScreenshotRequest::IMAGE_FORMAT_BMP:
1905 case ScreenshotRequest::IMAGE_FORMAT_JPEG:
1906 return absl::UnimplementedError(
1907 "JPEG screenshots are not supported; request PNG or BMP");
1909 return absl::InvalidArgumentError(
"Unknown screenshot image format");
1912 auto format_or = ResolveScreenshotFormat(requested_path, format);
1913 if (!format_or.ok()) {
1914 return format_or.status();
1916 auto result = CaptureScreenshotOnRenderThread(
1917 requested_path, request ? request->window_title() : std::string(),
1919 if (absl::IsDeadlineExceeded(result.status())) {
1920 return result.status();
1923 response->set_success(
false);
1924 response->set_message(std::string(result.status().message()));
1925 return result.status();
1928 const ScreenshotArtifact& artifact = *result;
1929 response->set_success(
true);
1930 response->set_message(absl::StrFormat(
"Screenshot saved to %s (%dx%d)",
1931 artifact.file_path, artifact.width,
1933 response->set_file_path(artifact.file_path);
1934 response->set_file_size_bytes(artifact.file_size_bytes);
1936 return absl::OkStatus();
1939absl::Status ImGuiTestHarnessServiceImpl::GetTestStatus(
1940 const GetTestStatusRequest* request, GetTestStatusResponse* response) {
1941 if (!test_manager_) {
1942 return absl::FailedPreconditionError(
"TestManager not available");
1945 if (request->test_id().empty()) {
1946 return absl::InvalidArgumentError(
"test_id must be provided");
1950 test_manager_->GetHarnessTestExecution(request->test_id());
1951 if (!execution_or.ok()) {
1952 response->set_status(GetTestStatusResponse::TEST_STATUS_UNSPECIFIED);
1953 response->set_error_message(std::string(execution_or.status().message()));
1954 return absl::OkStatus();
1957 const auto& execution = execution_or.value();
1958 response->set_status(ConvertHarnessStatus(execution.status));
1959 response->set_queued_at_ms(ToUnixMillisSafe(execution.queued_at));
1960 response->set_started_at_ms(ToUnixMillisSafe(execution.started_at));
1961 response->set_completed_at_ms(ToUnixMillisSafe(execution.completed_at));
1962 response->set_execution_time_ms(ClampDurationToInt32(execution.duration));
1963 if (!execution.error_message.empty()) {
1964 response->set_error_message(execution.error_message);
1966 response->clear_error_message();
1969 response->clear_assertion_failures();
1970 for (
const auto& failure : execution.assertion_failures) {
1971 response->add_assertion_failures(failure);
1974 return absl::OkStatus();
1977absl::Status ImGuiTestHarnessServiceImpl::ListTests(
1978 const ListTestsRequest* request, ListTestsResponse* response) {
1979 if (!test_manager_) {
1980 return absl::FailedPreconditionError(
"TestManager not available");
1983 if (request->page_size() < 0) {
1984 return absl::InvalidArgumentError(
"page_size cannot be negative");
1987 int page_size = request->page_size() > 0 ? request->page_size() : 100;
1988 constexpr int kMaxPageSize = 500;
1989 if (page_size > kMaxPageSize) {
1990 page_size = kMaxPageSize;
1993 size_t start_index = 0;
1994 if (!request->page_token().empty()) {
1995 int64_t token_value = 0;
1996 if (!absl::SimpleAtoi(request->page_token(), &token_value) ||
1998 return absl::InvalidArgumentError(
"Invalid page_token");
2000 start_index =
static_cast<size_t>(token_value);
2004 test_manager_->ListHarnessTestSummaries(request->category_filter());
2006 response->set_total_count(
static_cast<int32_t
>(summaries.size()));
2008 if (start_index >= summaries.size()) {
2009 response->clear_tests();
2010 response->clear_next_page_token();
2011 return absl::OkStatus();
2015 std::min(start_index +
static_cast<size_t>(page_size), summaries.size());
2017 for (
size_t i = start_index; i < end_index; ++i) {
2018 const auto& summary = summaries[i];
2019 auto* test_info = response->add_tests();
2020 const auto& exec = summary.latest_execution;
2022 test_info->set_test_id(exec.test_id);
2023 test_info->set_name(exec.name);
2024 test_info->set_category(exec.category);
2026 int64_t last_run_ms = ToUnixMillisSafe(exec.completed_at);
2027 if (last_run_ms == 0) {
2028 last_run_ms = ToUnixMillisSafe(exec.started_at);
2030 if (last_run_ms == 0) {
2031 last_run_ms = ToUnixMillisSafe(exec.queued_at);
2033 test_info->set_last_run_timestamp_ms(last_run_ms);
2035 test_info->set_total_runs(summary.total_runs);
2036 test_info->set_pass_count(summary.pass_count);
2037 test_info->set_fail_count(summary.fail_count);
2039 int32_t average_duration_ms = 0;
2040 if (summary.total_runs > 0) {
2041 absl::Duration average_duration =
2042 summary.total_duration / summary.total_runs;
2043 average_duration_ms = ClampDurationToInt32(average_duration);
2045 test_info->set_average_duration_ms(average_duration_ms);
2048 if (end_index < summaries.size()) {
2049 response->set_next_page_token(absl::StrCat(end_index));
2051 response->clear_next_page_token();
2054 return absl::OkStatus();
2057absl::Status ImGuiTestHarnessServiceImpl::GetTestResults(
2058 const GetTestResultsRequest* request, GetTestResultsResponse* response) {
2059 if (!test_manager_) {
2060 return absl::FailedPreconditionError(
"TestManager not available");
2063 if (request->test_id().empty()) {
2064 return absl::InvalidArgumentError(
"test_id must be provided");
2068 test_manager_->GetHarnessTestExecution(request->test_id());
2069 if (!execution_or.ok()) {
2070 return execution_or.status();
2073 const auto& execution = execution_or.value();
2074 response->set_success(execution.status == HarnessTestStatus::kPassed);
2075 response->set_test_name(execution.name);
2076 response->set_category(execution.category);
2078 int64_t executed_at_ms = ToUnixMillisSafe(execution.completed_at);
2079 if (executed_at_ms == 0) {
2080 executed_at_ms = ToUnixMillisSafe(execution.started_at);
2082 if (executed_at_ms == 0) {
2083 executed_at_ms = ToUnixMillisSafe(execution.queued_at);
2085 response->set_executed_at_ms(executed_at_ms);
2086 response->set_duration_ms(ClampDurationToInt32(execution.duration));
2088 response->clear_assertions();
2089 if (!execution.assertion_failures.empty()) {
2090 for (
const auto& failure : execution.assertion_failures) {
2091 auto* assertion = response->add_assertions();
2092 assertion->set_description(failure);
2093 assertion->set_passed(
false);
2094 assertion->set_error_message(failure);
2096 }
else if (!execution.error_message.empty()) {
2097 auto* assertion = response->add_assertions();
2098 assertion->set_description(
"Execution error");
2099 assertion->set_passed(
false);
2100 assertion->set_error_message(execution.error_message);
2103 if (request->include_logs()) {
2104 for (
const auto& log_entry : execution.logs) {
2105 response->add_logs(log_entry);
2109 auto* metrics_map = response->mutable_metrics();
2110 for (
const auto& [key, value] : execution.metrics) {
2111 (*metrics_map)[key] = value;
2115 if (!execution.screenshot_path.empty()) {
2116 response->set_screenshot_path(execution.screenshot_path);
2117 response->set_screenshot_size_bytes(execution.screenshot_size_bytes);
2119 if (!execution.failure_context.empty()) {
2120 response->set_failure_context(execution.failure_context);
2122 if (!execution.widget_state.empty()) {
2123 response->set_widget_state(execution.widget_state);
2126 return absl::OkStatus();
2129absl::Status ImGuiTestHarnessServiceImpl::DiscoverWidgets(
2130 const DiscoverWidgetsRequest* request, DiscoverWidgetsResponse* response) {
2132 return absl::InvalidArgumentError(
"request cannot be null");
2135 return absl::InvalidArgumentError(
"response cannot be null");
2138 if (!test_manager_) {
2139 return absl::FailedPreconditionError(
"TestManager not available");
2142 widget_discovery_service_.CollectWidgets(
nullptr, *request, response);
2143 return absl::OkStatus();
2146absl::Status ImGuiTestHarnessServiceImpl::StartRecording(
2147 const StartRecordingRequest* request, StartRecordingResponse* response) {
2149 return absl::InvalidArgumentError(
"request cannot be null");
2152 return absl::InvalidArgumentError(
"response cannot be null");
2155 TestRecorder::RecordingOptions options;
2156 options.output_path = request->output_path();
2157 options.session_name = request->session_name();
2158 options.description = request->description();
2160 if (options.output_path.empty()) {
2161 response->set_success(
false);
2162 response->set_message(
"output_path is required to start recording");
2163 return absl::InvalidArgumentError(
"output_path cannot be empty");
2166 absl::StatusOr<std::string> recording_id = test_recorder_.Start(options);
2167 if (!recording_id.ok()) {
2168 response->set_success(
false);
2169 response->set_message(std::string(recording_id.status().message()));
2170 return recording_id.status();
2173 response->set_success(
true);
2174 response->set_message(
"Recording started");
2175 response->set_recording_id(*recording_id);
2176 response->set_started_at_ms(absl::ToUnixMillis(absl::Now()));
2177 return absl::OkStatus();
2180absl::Status ImGuiTestHarnessServiceImpl::StopRecording(
2181 const StopRecordingRequest* request, StopRecordingResponse* response) {
2183 return absl::InvalidArgumentError(
"request cannot be null");
2186 return absl::InvalidArgumentError(
"response cannot be null");
2189 absl::StatusOr<TestRecorder::StopRecordingSummary> summary =
2190 test_recorder_.Stop(request->recording_id(), request->discard());
2191 if (!summary.ok()) {
2192 response->set_success(
false);
2193 response->set_message(std::string(summary.status().message()));
2194 return summary.status();
2197 response->set_success(
true);
2198 if (summary->saved) {
2199 response->set_message(
"Recording saved");
2201 response->set_message(
"Recording discarded");
2203 response->set_output_path(summary->output_path);
2204 response->set_step_count(summary->step_count);
2205 response->set_duration_ms(absl::ToInt64Milliseconds(summary->duration));
2206 return absl::OkStatus();
2209absl::Status ImGuiTestHarnessServiceImpl::ReplayTest(
2210 const ReplayTestRequest* request, ReplayTestResponse* response) {
2212 return absl::InvalidArgumentError(
"request cannot be null");
2215 return absl::InvalidArgumentError(
"response cannot be null");
2218 response->clear_logs();
2219 response->clear_assertions();
2221 if (request->script_path().empty()) {
2222 response->set_success(
false);
2223 response->set_message(
"script_path is required");
2224 return absl::InvalidArgumentError(
"script_path cannot be empty");
2227 absl::StatusOr<TestScript> script_or =
2229 if (!script_or.ok()) {
2230 response->set_success(
false);
2231 response->set_message(std::string(script_or.status().message()));
2232 return script_or.status();
2234 TestScript script = std::move(*script_or);
2236 absl::flat_hash_map<std::string, std::string> overrides;
2237 for (
const auto& entry : request->parameter_overrides()) {
2238 overrides[entry.first] = entry.second;
2241 response->set_replay_session_id(absl::StrFormat(
2243 absl::FormatTime(
"%Y%m%dT%H%M%S", absl::Now(), absl::UTCTimeZone())));
2245 auto suspension = test_recorder_.Suspend();
2247 std::vector<std::string> logs;
2248 logs.reserve(script.steps.size() * 2 + 4);
2250 bool overall_success =
true;
2251 std::string overall_message =
"Replay completed successfully";
2252 int steps_executed = 0;
2253 absl::Status overall_rpc_status = absl::OkStatus();
2255 for (
const auto& step : script.steps) {
2257 std::string action_label =
2258 absl::StrFormat(
"Step %d: %s", steps_executed, step.action);
2259 logs.push_back(action_label);
2261 absl::Status status = absl::OkStatus();
2262 bool step_success =
false;
2263 std::string step_message;
2264 HarnessTestExecution execution;
2265 bool have_execution =
false;
2267 if (step.action ==
"click") {
2268 ClickRequest sub_request;
2269 sub_request.set_target(ApplyOverrides(step.target, overrides));
2270 sub_request.set_widget_key(ApplyOverrides(step.widget_key, overrides));
2271 sub_request.set_type(ClickTypeFromString(step.click_type));
2272 ClickResponse sub_response;
2273 status = Click(&sub_request, &sub_response);
2274 step_success = sub_response.success();
2275 step_message = sub_response.message();
2276 if (status.ok() && !sub_response.test_id().empty()) {
2277 absl::Status wait_status = WaitForHarnessTestCompletion(
2278 test_manager_, sub_response.test_id(), &execution);
2279 if (wait_status.ok()) {
2280 have_execution =
true;
2281 internal::ApplyTerminalHarnessExecution(execution, &step_success,
2284 status = wait_status;
2285 step_success =
false;
2286 step_message = std::string(wait_status.message());
2289 }
else if (step.action ==
"type") {
2290 TypeRequest sub_request;
2291 sub_request.set_target(ApplyOverrides(step.target, overrides));
2292 sub_request.set_widget_key(ApplyOverrides(step.widget_key, overrides));
2293 sub_request.set_text(ApplyOverrides(step.text, overrides));
2294 sub_request.set_clear_first(step.clear_first);
2295 TypeResponse sub_response;
2296 status = Type(&sub_request, &sub_response);
2297 step_success = sub_response.success();
2298 step_message = sub_response.message();
2299 if (status.ok() && !sub_response.test_id().empty()) {
2300 absl::Status wait_status = WaitForHarnessTestCompletion(
2301 test_manager_, sub_response.test_id(), &execution);
2302 if (wait_status.ok()) {
2303 have_execution =
true;
2304 internal::ApplyTerminalHarnessExecution(execution, &step_success,
2307 status = wait_status;
2308 step_success =
false;
2309 step_message = std::string(wait_status.message());
2312 }
else if (step.action ==
"wait") {
2313 WaitRequest sub_request;
2314 sub_request.set_condition(ApplyOverrides(step.condition, overrides));
2315 sub_request.set_widget_key(ApplyOverrides(step.widget_key, overrides));
2316 if (step.timeout_ms > 0) {
2317 sub_request.set_timeout_ms(step.timeout_ms);
2319 WaitResponse sub_response;
2320 status = Wait(&sub_request, &sub_response);
2321 step_success = sub_response.success();
2322 step_message = sub_response.message();
2323 if (status.ok() && !sub_response.test_id().empty()) {
2324 absl::Status wait_status = WaitForHarnessTestCompletion(
2325 test_manager_, sub_response.test_id(), &execution);
2326 if (wait_status.ok()) {
2327 have_execution =
true;
2328 internal::ApplyTerminalHarnessExecution(execution, &step_success,
2331 status = wait_status;
2332 step_success =
false;
2333 step_message = std::string(wait_status.message());
2336 }
else if (step.action ==
"assert") {
2337 AssertRequest sub_request;
2338 sub_request.set_condition(ApplyOverrides(step.condition, overrides));
2339 sub_request.set_widget_key(ApplyOverrides(step.widget_key, overrides));
2340 AssertResponse sub_response;
2341 status = Assert(&sub_request, &sub_response);
2342 step_success = sub_response.success();
2343 step_message = sub_response.message();
2344 if (status.ok() && !sub_response.test_id().empty()) {
2345 absl::Status wait_status = WaitForHarnessTestCompletion(
2346 test_manager_, sub_response.test_id(), &execution);
2347 if (wait_status.ok()) {
2348 have_execution =
true;
2349 internal::ApplyTerminalHarnessExecution(execution, &step_success,
2352 status = wait_status;
2353 step_success =
false;
2354 step_message = std::string(wait_status.message());
2357 }
else if (step.action ==
"screenshot") {
2358 ScreenshotRequest sub_request;
2359 sub_request.set_window_title(ApplyOverrides(step.target, overrides));
2360 if (!step.region.empty()) {
2361 sub_request.set_output_path(ApplyOverrides(step.region, overrides));
2363 ScreenshotResponse sub_response;
2364 status = Screenshot(&sub_request, &sub_response);
2365 step_success = sub_response.success();
2366 step_message = sub_response.message();
2368 status = absl::InvalidArgumentError(
2369 absl::StrFormat(
"Unsupported action '%s'", step.action));
2371 std::string(status.message().data(), status.message().size());
2374 auto* assertion = response->add_assertions();
2375 assertion->set_description(
2376 absl::StrFormat(
"Step %d (%s)", steps_executed, step.action));
2379 assertion->set_passed(
false);
2380 assertion->set_error_message(
2381 std::string(status.message().data(), status.message().size()));
2382 overall_success =
false;
2383 overall_message = step_message;
2384 logs.push_back(absl::StrFormat(
" Error: %s", status.message()));
2385 overall_rpc_status = status;
2389 bool expectations_met = (step_success == step.expect_success);
2390 std::string expectation_error;
2392 if (!expectations_met) {
2394 absl::StrFormat(
"Expected success=%s but got %s",
2395 step.expect_success ?
"true" :
"false",
2396 step_success ?
"true" :
"false");
2399 if (!step.expect_status.empty()) {
2400 HarnessTestStatus expected_status =
2401 ::yaze::test::HarnessStatusFromString(step.expect_status);
2402 if (!have_execution) {
2403 expectations_met =
false;
2404 if (!expectation_error.empty()) {
2405 expectation_error.append(
"; ");
2407 expectation_error.append(
"No execution details available");
2408 }
else if (expected_status != HarnessTestStatus::kUnspecified &&
2409 execution.status != expected_status) {
2410 expectations_met =
false;
2411 if (!expectation_error.empty()) {
2412 expectation_error.append(
"; ");
2414 expectation_error.append(absl::StrFormat(
2415 "Expected status %s but observed %s", step.expect_status,
2416 ::yaze::test::HarnessStatusToString(execution.status)));
2418 if (have_execution) {
2419 assertion->set_actual_value(
2420 ::yaze::test::HarnessStatusToString(execution.status));
2421 assertion->set_expected_value(step.expect_status);
2425 if (!step.expect_message.empty()) {
2426 std::string actual_message = step_message;
2427 if (have_execution && !execution.error_message.empty()) {
2428 actual_message = execution.error_message;
2430 if (actual_message.find(step.expect_message) == std::string::npos) {
2431 expectations_met =
false;
2432 if (!expectation_error.empty()) {
2433 expectation_error.append(
"; ");
2435 expectation_error.append(
2436 absl::StrFormat(
"Expected message containing '%s' but got '%s'",
2437 step.expect_message, actual_message));
2441 if (!expectations_met) {
2442 assertion->set_passed(
false);
2443 assertion->set_error_message(expectation_error);
2444 overall_success =
false;
2445 overall_message = expectation_error;
2447 absl::StrFormat(
" Failed expectations: %s", expectation_error));
2448 if (request->ci_mode()) {
2452 assertion->set_passed(
true);
2453 logs.push_back(absl::StrFormat(
" Result: %s", step_message));
2456 if (have_execution && !execution.assertion_failures.empty()) {
2457 for (
const auto& failure : execution.assertion_failures) {
2458 logs.push_back(absl::StrFormat(
" Assertion failure: %s", failure));
2462 if (!overall_success && request->ci_mode()) {
2467 response->set_steps_executed(steps_executed);
2468 response->set_success(overall_success);
2469 response->set_message(overall_message);
2470 for (
const auto& log_entry : logs) {
2471 response->add_logs(log_entry);
2474 return overall_rpc_status;
2481ImGuiTestHarnessServer& ImGuiTestHarnessServer::Instance() {
2482 static ImGuiTestHarnessServer* instance =
new ImGuiTestHarnessServer();
2486ImGuiTestHarnessServer::~ImGuiTestHarnessServer() {
2490absl::Status ImGuiTestHarnessServer::Start(
int port,
2491 TestManager* test_manager) {
2493 return absl::FailedPreconditionError(
"Server already running");
2496 if (!test_manager) {
2497 return absl::InvalidArgumentError(
"TestManager cannot be null");
2501 service_ = std::make_unique<ImGuiTestHarnessServiceImpl>(test_manager);
2505 grpc_service_ = std::make_unique<ImGuiTestHarnessServiceGrpc>(service_.get());
2507 std::string server_address = absl::StrFormat(
"0.0.0.0:%d", port);
2509 grpc::ServerBuilder builder;
2512 builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
2515 builder.RegisterService(grpc_service_.get());
2518 server_ = builder.BuildAndStart();
2521 return absl::InternalError(
2522 absl::StrFormat(
"Failed to start gRPC server on %s", server_address));
2527 std::cout <<
"✓ ImGuiTestHarness gRPC server listening on " << server_address
2528 <<
" (with TestManager integration)\n";
2529 std::cout <<
" Use 'grpcurl -plaintext -d '{\"message\":\"test\"}' "
2530 << server_address <<
" yaze.test.ImGuiTestHarness/Ping' to test\n";
2532 return absl::OkStatus();
2535void ImGuiTestHarnessServer::Shutdown() {
2537 std::cout <<
"⏹ Shutting down ImGuiTestHarness gRPC server...\n";
2538 server_->Shutdown();
2542 std::cout <<
"✓ ImGuiTestHarness gRPC server stopped\n";
Controller * GetController()
static Application & Instance()
editor::EditorManager * editor_manager()
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.