17#include "absl/base/thread_annotations.h"
18#include "absl/container/flat_hash_map.h"
19#include "absl/strings/ascii.h"
20#include "absl/strings/numbers.h"
21#include "absl/strings/str_cat.h"
22#include "absl/strings/str_format.h"
23#include "absl/strings/str_replace.h"
24#include "absl/synchronization/mutex.h"
25#include "absl/time/clock.h"
26#include "absl/time/time.h"
31#include "protos/imgui_test_harness.grpc.pb.h"
32#include "protos/imgui_test_harness.pb.h"
35#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
36#include "imgui_test_engine/imgui_te_context.h"
37#include "imgui_test_engine/imgui_te_engine.h"
41struct DynamicTestData {
42 std::function<void(ImGuiTestContext*)> test_func;
45absl::Mutex g_dynamic_tests_mutex;
46std::deque<std::shared_ptr<DynamicTestData>> g_dynamic_tests
47 ABSL_GUARDED_BY(g_dynamic_tests_mutex);
49void KeepDynamicTestData(
const std::shared_ptr<DynamicTestData>& data) {
50 absl::MutexLock lock(&g_dynamic_tests_mutex);
51 constexpr size_t kMaxKeepAlive = 64;
52 g_dynamic_tests.push_back(data);
53 while (g_dynamic_tests.size() > kMaxKeepAlive) {
54 g_dynamic_tests.pop_front();
58void RunDynamicTest(ImGuiTestContext* ctx) {
59 auto* data = (DynamicTestData*)ctx->Test->UserData;
60 if (data && data->test_func) {
66bool IsTestCompleted(ImGuiTest* test) {
67 return test->Output.Status != ImGuiTestStatus_Queued &&
68 test->Output.Status != ImGuiTestStatus_Running;
74 std::atomic<bool> completed{
false};
75 std::mutex data_mutex;
79 void SetResult(
const T& res,
const std::string& msg) {
80 std::lock_guard<std::mutex> lock(data_mutex);
83 completed.store(
true);
86 void GetResult(T& res, std::string& msg) {
87 std::lock_guard<std::mutex> lock(data_mutex);
98::yaze::test::GetTestStatusResponse_TestStatus ConvertHarnessStatus(
99 ::yaze::test::HarnessTestStatus status) {
101 case ::yaze::test::HarnessTestStatus::kQueued:
102 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_QUEUED;
103 case ::yaze::test::HarnessTestStatus::kRunning:
104 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_RUNNING;
105 case ::yaze::test::HarnessTestStatus::kPassed:
106 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_PASSED;
107 case ::yaze::test::HarnessTestStatus::kFailed:
108 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_FAILED;
109 case ::yaze::test::HarnessTestStatus::kTimeout:
110 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_TIMEOUT;
111 case ::yaze::test::HarnessTestStatus::kUnspecified:
113 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_UNSPECIFIED;
117int64_t ToUnixMillisSafe(absl::Time timestamp) {
118 if (timestamp == absl::InfinitePast()) {
121 return absl::ToUnixMillis(timestamp);
124int32_t ClampDurationToInt32(absl::Duration duration) {
125 int64_t millis = absl::ToInt64Milliseconds(duration);
126 if (millis > std::numeric_limits<int32_t>::max()) {
127 return std::numeric_limits<int32_t>::max();
129 if (millis < std::numeric_limits<int32_t>::min()) {
130 return std::numeric_limits<int32_t>::min();
132 return static_cast<int32_t
>(millis);
137#include <grpcpp/grpcpp.h>
138#include <grpcpp/server_builder.h>
145std::string ClickTypeToString(ClickRequest::ClickType type) {
147 case ClickRequest::CLICK_TYPE_RIGHT:
149 case ClickRequest::CLICK_TYPE_MIDDLE:
151 case ClickRequest::CLICK_TYPE_DOUBLE:
153 case ClickRequest::CLICK_TYPE_LEFT:
154 case ClickRequest::CLICK_TYPE_UNSPECIFIED:
160ClickRequest::ClickType ClickTypeFromString(absl::string_view type) {
161 const std::string lower = absl::AsciiStrToLower(std::string(type));
162 if (lower ==
"right") {
163 return ClickRequest::CLICK_TYPE_RIGHT;
165 if (lower ==
"middle") {
166 return ClickRequest::CLICK_TYPE_MIDDLE;
168 if (lower ==
"double" || lower ==
"double_click" || lower ==
"dbl") {
169 return ClickRequest::CLICK_TYPE_DOUBLE;
171 return ClickRequest::CLICK_TYPE_LEFT;
174HarnessTestStatus HarnessStatusFromString(absl::string_view status) {
175 const std::string lower = absl::AsciiStrToLower(std::string(status));
176 if (lower ==
"passed" || lower ==
"success") {
177 return HarnessTestStatus::kPassed;
179 if (lower ==
"failed" || lower ==
"fail") {
180 return HarnessTestStatus::kFailed;
182 if (lower ==
"timeout") {
183 return HarnessTestStatus::kTimeout;
185 if (lower ==
"queued") {
186 return HarnessTestStatus::kQueued;
188 if (lower ==
"running") {
189 return HarnessTestStatus::kRunning;
191 return HarnessTestStatus::kUnspecified;
194const char* HarnessStatusToString(HarnessTestStatus status) {
196 case HarnessTestStatus::kPassed:
198 case HarnessTestStatus::kFailed:
200 case HarnessTestStatus::kTimeout:
202 case HarnessTestStatus::kQueued:
204 case HarnessTestStatus::kRunning:
206 case HarnessTestStatus::kUnspecified:
212std::string ApplyOverrides(
213 const std::string& value,
214 const absl::flat_hash_map<std::string, std::string>& overrides) {
215 if (overrides.empty() || value.empty()) {
218 std::string result = value;
219 for (
const auto& [key, replacement] : overrides) {
220 const std::string placeholder = absl::StrCat(
"{{", key,
"}}");
221 result = absl::StrReplaceAll(result, {{placeholder, replacement}});
226void MaybeRecordStep(TestRecorder* recorder, TestRecorder::RecordedStep step) {
227 if (!recorder || !recorder->IsRecording()) {
230 if (step.captured_at == absl::InfinitePast()) {
231 step.captured_at = absl::Now();
233 recorder->RecordStep(step);
236absl::Status WaitForHarnessTestCompletion(TestManager* manager,
237 const std::string& test_id,
238 HarnessTestExecution* execution) {
240 return absl::FailedPreconditionError(
"TestManager unavailable");
242 if (test_id.empty()) {
243 return absl::InvalidArgumentError(
"Missing harness test identifier");
246 const absl::Time deadline = absl::Now() + absl::Seconds(20);
247 while (absl::Now() < deadline) {
248 absl::StatusOr<HarnessTestExecution> current =
249 manager->GetHarnessTestExecution(test_id);
251 absl::SleepFor(absl::Milliseconds(75));
256 *execution = std::move(current.value());
259 if (current->status == HarnessTestStatus::kQueued ||
260 current->status == HarnessTestStatus::kRunning) {
261 absl::SleepFor(absl::Milliseconds(75));
264 return absl::OkStatus();
267 return absl::DeadlineExceededError(absl::StrFormat(
268 "Harness test %s did not reach a terminal state", test_id));
276struct ResolvedWidgetSelector {
278 std::string resolved_widget_key;
279 std::string resolved_path;
282struct ParsedCondition {
288 size_t colon = path.rfind(
':');
289 if (colon != absl::string_view::npos && colon + 1 < path.size()) {
290 return std::string(path.substr(colon + 1));
292 size_t slash = path.rfind(
'/');
293 if (slash != absl::string_view::npos && slash + 1 < path.size()) {
294 return std::string(path.substr(slash + 1));
296 return std::string(path);
299std::string ExtractTypeFromPath(absl::string_view path) {
300 size_t slash = path.rfind(
'/');
301 absl::string_view segment =
302 slash == absl::string_view::npos ? path : path.substr(slash + 1);
303 size_t colon = segment.find(
':');
304 if (colon == absl::string_view::npos || colon == 0) {
307 return std::string(segment.substr(0, colon));
310absl::StatusOr<ParsedTarget> ParseTargetString(absl::string_view target) {
311 if (target.empty()) {
312 return absl::InvalidArgumentError(
313 "Missing target. Provide 'type:label' or widget_key.");
315 size_t colon_pos = target.find(
':');
316 if (colon_pos == absl::string_view::npos || colon_pos == 0 ||
317 colon_pos + 1 >= target.size()) {
318 return absl::InvalidArgumentError(
319 "Invalid target format. Use 'type:label' (e.g. 'button:Open ROM').");
323 parsed.type = std::string(target.substr(0, colon_pos));
324 parsed.label = std::string(target.substr(colon_pos + 1));
328absl::StatusOr<ResolvedWidgetSelector> ResolveWidgetSelector(
329 absl::string_view target, absl::string_view widget_key) {
330 ResolvedWidgetSelector resolved;
331 if (!widget_key.empty()) {
332 const std::string
key(widget_key);
333 const gui::WidgetIdRegistry::WidgetInfo* info =
336 return absl::NotFoundError(absl::StrFormat(
337 "widget_key '%s' was not found in WidgetIdRegistry", key));
340 resolved.resolved_widget_key =
key;
341 resolved.resolved_path =
342 info->full_path.empty() ?
key : std::string(info->full_path);
343 resolved.target.type = info->type.empty()
344 ? ExtractTypeFromPath(resolved.resolved_path)
345 : std::string(info->type);
346 resolved.target.label = info->label.empty()
348 : std::string(info->label);
349 if (resolved.target.label.empty()) {
350 return absl::FailedPreconditionError(absl::StrFormat(
351 "widget_key '%s' resolved without a usable label", key));
353 if (resolved.target.type.empty()) {
354 resolved.target.type =
"widget";
359 absl::StatusOr<ParsedTarget> parsed = ParseTargetString(target);
361 return parsed.status();
363 resolved.target = *parsed;
367absl::StatusOr<ParsedCondition> ParseConditionString(absl::string_view value) {
369 return absl::InvalidArgumentError(
370 "Missing condition. Provide 'type:target' or widget_key.");
373 size_t colon_pos = value.find(
':');
374 if (colon_pos == absl::string_view::npos || colon_pos == 0) {
375 return absl::InvalidArgumentError(
376 "Invalid condition format. Use 'type:target'.");
379 ParsedCondition parsed;
380 parsed.type = std::string(value.substr(0, colon_pos));
381 parsed.target = std::string(value.substr(colon_pos + 1));
385absl::StatusOr<ParsedCondition> ResolveCondition(
386 absl::string_view condition, absl::string_view widget_key,
387 absl::string_view default_type_for_widget,
388 ResolvedWidgetSelector* resolved_selector) {
389 std::optional<ResolvedWidgetSelector> widget_selector;
390 if (!widget_key.empty()) {
391 absl::StatusOr<ResolvedWidgetSelector> resolved =
392 ResolveWidgetSelector(
"", widget_key);
393 if (!resolved.ok()) {
394 return resolved.status();
396 widget_selector = *resolved;
397 if (resolved_selector) {
398 *resolved_selector = *resolved;
400 }
else if (resolved_selector) {
401 *resolved_selector = ResolvedWidgetSelector{};
404 if (condition.empty()) {
405 if (!widget_selector.has_value()) {
406 return absl::InvalidArgumentError(
407 "Missing condition. Provide 'type:target' or widget_key.");
409 ParsedCondition parsed;
410 parsed.type = std::string(default_type_for_widget);
411 parsed.target = widget_selector->target.label;
415 absl::StatusOr<ParsedCondition> parsed = ParseConditionString(condition);
417 return parsed.status();
420 if (widget_selector.has_value() && parsed->target.empty()) {
421 parsed->target = widget_selector->target.label;
424 if (parsed->target.empty()) {
425 return absl::InvalidArgumentError(
426 "Condition target is empty. Provide 'type:target'.");
432struct UiSyncSnapshot {
433 int64_t frame_id = 0;
434 int32_t pending_editor_actions = 0;
435 int32_t pending_layout_actions = 0;
436 bool layout_rebuild_pending =
false;
437 int32_t pending_harness_tests = 0;
438 int32_t queued_harness_tests = 0;
439 int32_t running_harness_tests = 0;
442editor::EditorManager* GetEditorManagerForSync() {
450UiSyncSnapshot CaptureUiSyncSnapshot(TestManager* test_manager) {
451 UiSyncSnapshot snapshot;
452 if (editor::EditorManager* editor_manager = GetEditorManagerForSync()) {
453 const editor::EditorManager::UiSyncState state =
454 editor_manager->GetUiSyncStateSnapshot();
455 snapshot.frame_id =
static_cast<int64_t
>(state.frame_id);
456 snapshot.pending_editor_actions = state.pending_editor_actions;
457 snapshot.pending_layout_actions = state.pending_layout_actions;
458 snapshot.layout_rebuild_pending = state.layout_rebuild_pending;
462 const std::vector<HarnessTestSummary> summaries =
463 test_manager->ListHarnessTestSummaries();
464 for (
const HarnessTestSummary& summary : summaries) {
465 switch (summary.latest_execution.status) {
466 case HarnessTestStatus::kQueued:
467 snapshot.queued_harness_tests++;
469 case HarnessTestStatus::kRunning:
470 snapshot.running_harness_tests++;
478 snapshot.pending_harness_tests =
479 snapshot.queued_harness_tests + snapshot.running_harness_tests;
483bool IsUiIdle(
const UiSyncSnapshot& snapshot) {
484 return snapshot.pending_editor_actions == 0 &&
485 snapshot.pending_layout_actions == 0 &&
486 !snapshot.layout_rebuild_pending &&
487 snapshot.pending_harness_tests == 0;
490std::string BuildIdleTimeoutReason(
const UiSyncSnapshot& snapshot) {
491 return absl::StrFormat(
492 "pending_editor=%d pending_layout=%d layout_rebuild=%s queued_tests=%d "
494 snapshot.pending_editor_actions, snapshot.pending_layout_actions,
495 snapshot.layout_rebuild_pending ?
"true" :
"false",
496 snapshot.queued_harness_tests, snapshot.running_harness_tests);
499int32_t NormalizeTimeoutMs(int32_t timeout_ms, int32_t fallback_ms) {
500 return timeout_ms <= 0 ? fallback_ms : timeout_ms;
503int32_t NormalizePollIntervalMs(int32_t poll_interval_ms) {
504 if (poll_interval_ms <= 0) {
507 return std::clamp(poll_interval_ms, 5, 1000);
513class ImGuiTestHarnessServiceGrpc final :
public ImGuiTestHarness::Service {
515 explicit ImGuiTestHarnessServiceGrpc(ImGuiTestHarnessServiceImpl* impl)
518 ::grpc::Status Ping(::grpc::ServerContext* context,
519 const PingRequest* request,
520 PingResponse* response)
override {
521 return ConvertStatus(impl_->Ping(request, response));
524 ::grpc::Status Click(::grpc::ServerContext* context,
525 const ClickRequest* request,
526 ClickResponse* response)
override {
527 return ConvertStatus(impl_->Click(request, response));
530 ::grpc::Status Type(::grpc::ServerContext* context,
531 const TypeRequest* request,
532 TypeResponse* response)
override {
533 return ConvertStatus(impl_->Type(request, response));
536 ::grpc::Status Wait(::grpc::ServerContext* context,
537 const WaitRequest* request,
538 WaitResponse* response)
override {
539 return ConvertStatus(impl_->Wait(request, response));
542 ::grpc::Status Assert(::grpc::ServerContext* context,
543 const AssertRequest* request,
544 AssertResponse* response)
override {
545 return ConvertStatus(impl_->Assert(request, response));
548 ::grpc::Status FlushUiActions(::grpc::ServerContext* context,
549 const FlushUiActionsRequest* request,
550 FlushUiActionsResponse* response)
override {
551 return ConvertStatus(impl_->FlushUiActions(request, response));
554 ::grpc::Status WaitForIdle(::grpc::ServerContext* context,
555 const WaitForIdleRequest* request,
556 WaitForIdleResponse* response)
override {
557 return ConvertStatus(impl_->WaitForIdle(request, response));
560 ::grpc::Status GetUiSyncState(::grpc::ServerContext* context,
561 const GetUiSyncStateRequest* request,
562 GetUiSyncStateResponse* response)
override {
563 return ConvertStatus(impl_->GetUiSyncState(request, response));
566 ::grpc::Status Screenshot(::grpc::ServerContext* context,
567 const ScreenshotRequest* request,
568 ScreenshotResponse* response)
override {
569 return ConvertStatus(impl_->Screenshot(request, response));
572 ::grpc::Status GetTestStatus(::grpc::ServerContext* context,
573 const GetTestStatusRequest* request,
574 GetTestStatusResponse* response)
override {
575 return ConvertStatus(impl_->GetTestStatus(request, response));
578 ::grpc::Status ListTests(::grpc::ServerContext* context,
579 const ListTestsRequest* request,
580 ListTestsResponse* response)
override {
581 return ConvertStatus(impl_->ListTests(request, response));
584 ::grpc::Status GetTestResults(::grpc::ServerContext* context,
585 const GetTestResultsRequest* request,
586 GetTestResultsResponse* response)
override {
587 return ConvertStatus(impl_->GetTestResults(request, response));
590 ::grpc::Status DiscoverWidgets(::grpc::ServerContext* context,
591 const DiscoverWidgetsRequest* request,
592 DiscoverWidgetsResponse* response)
override {
593 return ConvertStatus(impl_->DiscoverWidgets(request, response));
596 ::grpc::Status StartRecording(::grpc::ServerContext* context,
597 const StartRecordingRequest* request,
598 StartRecordingResponse* response)
override {
599 return ConvertStatus(impl_->StartRecording(request, response));
602 ::grpc::Status StopRecording(::grpc::ServerContext* context,
603 const StopRecordingRequest* request,
604 StopRecordingResponse* response)
override {
605 return ConvertStatus(impl_->StopRecording(request, response));
608 ::grpc::Status ReplayTest(::grpc::ServerContext* context,
609 const ReplayTestRequest* request,
610 ReplayTestResponse* response)
override {
611 return ConvertStatus(impl_->ReplayTest(request, response));
615 static ::grpc::Status ConvertStatus(
const absl::Status& status) {
617 return ::grpc::Status::OK;
620 ::grpc::StatusCode code = ::grpc::StatusCode::UNKNOWN;
621 switch (status.code()) {
622 case absl::StatusCode::kCancelled:
623 code = ::grpc::StatusCode::CANCELLED;
625 case absl::StatusCode::kUnknown:
626 code = ::grpc::StatusCode::UNKNOWN;
628 case absl::StatusCode::kInvalidArgument:
629 code = ::grpc::StatusCode::INVALID_ARGUMENT;
631 case absl::StatusCode::kDeadlineExceeded:
632 code = ::grpc::StatusCode::DEADLINE_EXCEEDED;
634 case absl::StatusCode::kNotFound:
635 code = ::grpc::StatusCode::NOT_FOUND;
637 case absl::StatusCode::kAlreadyExists:
638 code = ::grpc::StatusCode::ALREADY_EXISTS;
640 case absl::StatusCode::kPermissionDenied:
641 code = ::grpc::StatusCode::PERMISSION_DENIED;
643 case absl::StatusCode::kResourceExhausted:
644 code = ::grpc::StatusCode::RESOURCE_EXHAUSTED;
646 case absl::StatusCode::kFailedPrecondition:
647 code = ::grpc::StatusCode::FAILED_PRECONDITION;
649 case absl::StatusCode::kAborted:
650 code = ::grpc::StatusCode::ABORTED;
652 case absl::StatusCode::kOutOfRange:
653 code = ::grpc::StatusCode::OUT_OF_RANGE;
655 case absl::StatusCode::kUnimplemented:
656 code = ::grpc::StatusCode::UNIMPLEMENTED;
658 case absl::StatusCode::kInternal:
659 code = ::grpc::StatusCode::INTERNAL;
661 case absl::StatusCode::kUnavailable:
662 code = ::grpc::StatusCode::UNAVAILABLE;
664 case absl::StatusCode::kDataLoss:
665 code = ::grpc::StatusCode::DATA_LOSS;
667 case absl::StatusCode::kUnauthenticated:
668 code = ::grpc::StatusCode::UNAUTHENTICATED;
671 code = ::grpc::StatusCode::UNKNOWN;
675 return ::grpc::Status(
676 code, std::string(status.message().data(), status.message().size()));
679 ImGuiTestHarnessServiceImpl* impl_;
682std::unique_ptr<::grpc::Service> CreateImGuiTestHarnessServiceGrpc(
683 ImGuiTestHarnessServiceImpl* impl) {
684 return std::make_unique<ImGuiTestHarnessServiceGrpc>(impl);
691absl::Status ImGuiTestHarnessServiceImpl::Ping(
const PingRequest* request,
692 PingResponse* response) {
694 response->set_message(absl::StrFormat(
"Pong: %s", request->message()));
697 auto now = std::chrono::system_clock::now();
698 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
699 now.time_since_epoch());
700 response->set_timestamp_ms(ms.count());
705 return absl::OkStatus();
708absl::Status ImGuiTestHarnessServiceImpl::Click(
const ClickRequest* request,
709 ClickResponse* response) {
710 auto start = std::chrono::steady_clock::now();
712 TestRecorder::RecordedStep recorded_step;
715 recorded_step.widget_key = request->widget_key();
716 if (!request->widget_key().empty()) {
717 recorded_step.target = absl::StrCat(
"widget_key:", request->widget_key());
719 recorded_step.target = request->target();
721 recorded_step.click_type = ClickTypeToString(request->type());
724 auto finalize = [&](
const absl::Status& status) {
725 recorded_step.success = response->success();
726 recorded_step.message = response->message();
727 recorded_step.execution_time_ms = response->execution_time_ms();
728 recorded_step.test_id = response->test_id();
729 MaybeRecordStep(&test_recorder_, recorded_step);
733 if (!test_manager_) {
734 response->set_success(
false);
735 response->set_message(
"TestManager not available");
736 response->set_execution_time_ms(0);
737 return finalize(absl::FailedPreconditionError(
"TestManager not available"));
740 const std::string requested_target = request ? request->target() :
"";
741 const std::string requested_widget_key = request ? request->widget_key() :
"";
742 const std::string request_summary =
743 !requested_widget_key.empty()
744 ? absl::StrFormat(
"widget_key:%s", requested_widget_key)
747 const std::string test_id = test_manager_->RegisterHarnessTest(
748 absl::StrFormat(
"Click: %s", request_summary),
"grpc");
749 response->set_test_id(test_id);
750 recorded_step.test_id = test_id;
751 test_manager_->AppendHarnessTestLog(
752 test_id, absl::StrCat(
"Queued click request: ", request_summary));
754 absl::StatusOr<ResolvedWidgetSelector> resolved_selector =
755 ResolveWidgetSelector(requested_target, requested_widget_key);
756 if (!resolved_selector.ok()) {
757 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
758 std::chrono::steady_clock::now() - start);
759 std::string message = std::string(resolved_selector.status().message());
760 response->set_success(
false);
761 response->set_message(message);
762 response->set_execution_time_ms(elapsed.count());
763 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
765 test_manager_->AppendHarnessTestLog(test_id, message);
766 return finalize(absl::OkStatus());
769 if (!resolved_selector->resolved_widget_key.empty()) {
770 response->set_resolved_widget_key(resolved_selector->resolved_widget_key);
772 if (!resolved_selector->resolved_path.empty()) {
773 response->set_resolved_path(resolved_selector->resolved_path);
776 const std::string widget_type = resolved_selector->target.type;
777 const std::string widget_label = resolved_selector->target.label;
779#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
780 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
782 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
783 std::chrono::steady_clock::now() - start);
784 std::string message =
"ImGuiTestEngine not initialized";
785 response->set_success(
false);
786 response->set_message(message);
787 response->set_execution_time_ms(elapsed.count());
788 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
790 return finalize(absl::OkStatus());
793 const ClickRequest::ClickType click_type =
794 request ? request->type() : ClickRequest::CLICK_TYPE_LEFT;
796 ImGuiMouseButton mouse_button = ImGuiMouseButton_Left;
797 switch (click_type) {
798 case ClickRequest::CLICK_TYPE_UNSPECIFIED:
799 case ClickRequest::CLICK_TYPE_LEFT:
800 mouse_button = ImGuiMouseButton_Left;
802 case ClickRequest::CLICK_TYPE_RIGHT:
803 mouse_button = ImGuiMouseButton_Right;
805 case ClickRequest::CLICK_TYPE_MIDDLE:
806 mouse_button = ImGuiMouseButton_Middle;
808 case ClickRequest::CLICK_TYPE_DOUBLE:
813 auto test_data = std::make_shared<DynamicTestData>();
814 TestManager* manager = test_manager_;
815 test_data->test_func = [manager, captured_id = test_id, widget_type,
816 widget_label, click_type,
817 mouse_button](ImGuiTestContext* ctx) {
818 manager->MarkHarnessTestRunning(captured_id);
820 if (click_type == ClickRequest::CLICK_TYPE_DOUBLE) {
821 ctx->ItemDoubleClick(widget_label.c_str());
823 ctx->ItemClick(widget_label.c_str(), mouse_button);
826 const std::string success_message =
827 absl::StrFormat(
"Clicked %s '%s'", widget_type, widget_label);
828 manager->AppendHarnessTestLog(captured_id, success_message);
829 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kPassed,
831 }
catch (
const std::exception& e) {
832 const std::string error_message =
833 absl::StrFormat(
"Click failed: %s", e.what());
834 manager->AppendHarnessTestLog(captured_id, error_message);
835 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
840 std::string test_name = absl::StrFormat(
842 static_cast<long long>(
843 std::chrono::system_clock::now().time_since_epoch().count()));
845 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
846 test->TestFunc = RunDynamicTest;
847 test->UserData = test_data.get();
849 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
850 KeepDynamicTestData(test_data);
852 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
853 std::chrono::steady_clock::now() - start);
854 std::string message =
855 absl::StrFormat(
"Queued click on %s '%s'", widget_type, widget_label);
856 response->set_success(
true);
857 response->set_message(message);
858 response->set_execution_time_ms(elapsed.count());
859 test_manager_->AppendHarnessTestLog(test_id, message);
862 std::string message =
863 absl::StrFormat(
"[STUB] Clicked %s '%s' (ImGuiTestEngine not available)",
864 widget_type, widget_label);
866 test_manager_->MarkHarnessTestRunning(test_id);
867 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kPassed,
869 test_manager_->AppendHarnessTestLog(test_id, message);
871 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
872 std::chrono::steady_clock::now() - start);
873 response->set_success(
true);
874 response->set_message(message);
875 response->set_execution_time_ms(elapsed.count());
878 return finalize(absl::OkStatus());
881absl::Status ImGuiTestHarnessServiceImpl::Type(
const TypeRequest* request,
882 TypeResponse* response) {
883 auto start = std::chrono::steady_clock::now();
885 TestRecorder::RecordedStep recorded_step;
888 recorded_step.widget_key = request->widget_key();
889 if (!request->widget_key().empty()) {
890 recorded_step.target = absl::StrCat(
"widget_key:", request->widget_key());
892 recorded_step.target = request->target();
894 recorded_step.text = request->text();
895 recorded_step.clear_first = request->clear_first();
898 auto finalize = [&](
const absl::Status& status) {
899 recorded_step.success = response->success();
900 recorded_step.message = response->message();
901 recorded_step.execution_time_ms = response->execution_time_ms();
902 recorded_step.test_id = response->test_id();
903 MaybeRecordStep(&test_recorder_, recorded_step);
907 if (!test_manager_) {
908 response->set_success(
false);
909 response->set_message(
"TestManager not available");
910 response->set_execution_time_ms(0);
911 return finalize(absl::FailedPreconditionError(
"TestManager not available"));
914 const std::string requested_target = request ? request->target() :
"";
915 const std::string requested_widget_key = request ? request->widget_key() :
"";
916 const std::string request_summary =
917 !requested_widget_key.empty()
918 ? absl::StrFormat(
"widget_key:%s", requested_widget_key)
921 const std::string test_id = test_manager_->RegisterHarnessTest(
922 absl::StrFormat(
"Type: %s", request_summary),
"grpc");
923 response->set_test_id(test_id);
924 recorded_step.test_id = test_id;
925 test_manager_->AppendHarnessTestLog(
926 test_id, absl::StrFormat(
"Queued type request: %s", request_summary));
928 absl::StatusOr<ResolvedWidgetSelector> resolved_selector =
929 ResolveWidgetSelector(requested_target, requested_widget_key);
930 if (!resolved_selector.ok()) {
931 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
932 std::chrono::steady_clock::now() - start);
933 std::string message = std::string(resolved_selector.status().message());
934 response->set_success(
false);
935 response->set_message(message);
936 response->set_execution_time_ms(elapsed.count());
937 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
939 test_manager_->AppendHarnessTestLog(test_id, message);
940 return finalize(absl::OkStatus());
943 if (!resolved_selector->resolved_widget_key.empty()) {
944 response->set_resolved_widget_key(resolved_selector->resolved_widget_key);
946 if (!resolved_selector->resolved_path.empty()) {
947 response->set_resolved_path(resolved_selector->resolved_path);
950 const std::string widget_type = resolved_selector->target.type;
951 const std::string widget_label = resolved_selector->target.label;
953#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
954 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
956 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
957 std::chrono::steady_clock::now() - start);
958 std::string message =
"ImGuiTestEngine not initialized";
959 response->set_success(
false);
960 response->set_message(message);
961 response->set_execution_time_ms(elapsed.count());
962 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
964 return finalize(absl::OkStatus());
966 std::string text = request->text();
967 bool clear_first = request->clear_first();
969 auto rpc_state = std::make_shared<RPCState<bool>>();
970 auto test_data = std::make_shared<DynamicTestData>();
971 TestManager* manager = test_manager_;
972 test_data->test_func = [manager, captured_id = test_id, widget_type,
973 widget_label, clear_first, text,
974 rpc_state](ImGuiTestContext* ctx) {
975 manager->MarkHarnessTestRunning(captured_id);
977 ImGuiTestItemInfo item = ctx->ItemInfo(widget_label.c_str());
979 std::string error_message =
980 absl::StrFormat(
"Input field '%s' not found", widget_label);
981 manager->AppendHarnessTestLog(captured_id, error_message);
982 manager->MarkHarnessTestCompleted(
983 captured_id, HarnessTestStatus::kFailed, error_message);
984 rpc_state->SetResult(
false, error_message);
988 ctx->ItemClick(widget_label.c_str());
990 ctx->KeyPress(ImGuiMod_Shortcut | ImGuiKey_A);
991 ctx->KeyPress(ImGuiKey_Delete);
994 ctx->ItemInputValue(widget_label.c_str(), text.c_str());
996 std::string success_message =
997 absl::StrFormat(
"Typed '%s' into %s '%s'%s", text, widget_type,
998 widget_label, clear_first ?
" (cleared first)" :
"");
999 manager->AppendHarnessTestLog(captured_id, success_message);
1000 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kPassed,
1002 rpc_state->SetResult(
true, success_message);
1003 }
catch (
const std::exception& e) {
1004 std::string error_message = absl::StrFormat(
"Type failed: %s", e.what());
1005 manager->AppendHarnessTestLog(captured_id, error_message);
1006 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
1008 rpc_state->SetResult(
false, error_message);
1012 std::string test_name = absl::StrFormat(
1014 static_cast<long long>(
1015 std::chrono::system_clock::now().time_since_epoch().count()));
1017 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
1018 test->TestFunc = RunDynamicTest;
1019 test->UserData = test_data.get();
1021 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
1022 KeepDynamicTestData(test_data);
1024 auto timeout = std::chrono::seconds(5);
1025 auto wait_start = std::chrono::steady_clock::now();
1026 while (!rpc_state->completed.load()) {
1027 if (std::chrono::steady_clock::now() - wait_start > timeout) {
1028 std::string error_message =
1029 "Test timeout - input field not found or unresponsive";
1030 manager->AppendHarnessTestLog(test_id, error_message);
1031 manager->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kTimeout,
1033 rpc_state->SetResult(
false, error_message);
1036 std::this_thread::sleep_for(std::chrono::milliseconds(100));
1039 bool success =
false;
1040 std::string message;
1041 rpc_state->GetResult(success, message);
1042 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1043 std::chrono::steady_clock::now() - start);
1045 response->set_success(success);
1046 response->set_message(message);
1047 response->set_execution_time_ms(elapsed.count());
1048 if (!message.empty()) {
1049 test_manager_->AppendHarnessTestLog(test_id, message);
1053 test_manager_->MarkHarnessTestRunning(test_id);
1054 std::string message = absl::StrFormat(
1055 "[STUB] Typed '%s' into %s '%s' (ImGuiTestEngine not available)",
1056 request->text(), widget_type, widget_label);
1057 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kPassed,
1059 test_manager_->AppendHarnessTestLog(test_id, message);
1061 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1062 std::chrono::steady_clock::now() - start);
1063 response->set_success(
true);
1064 response->set_message(message);
1065 response->set_execution_time_ms(elapsed.count());
1068 return finalize(absl::OkStatus());
1071absl::Status ImGuiTestHarnessServiceImpl::Wait(
const WaitRequest* request,
1072 WaitResponse* response) {
1073 auto start = std::chrono::steady_clock::now();
1075 TestRecorder::RecordedStep recorded_step;
1078 recorded_step.widget_key = request->widget_key();
1079 if (!request->widget_key().empty() && request->condition().empty()) {
1080 recorded_step.condition =
1081 absl::StrCat(
"widget_key:", request->widget_key());
1083 recorded_step.condition = request->condition();
1085 recorded_step.timeout_ms = request->timeout_ms();
1088 auto finalize = [&](
const absl::Status& status) {
1089 recorded_step.success = response->success();
1090 recorded_step.message = response->message();
1091 recorded_step.execution_time_ms = response->elapsed_ms();
1092 recorded_step.test_id = response->test_id();
1093 MaybeRecordStep(&test_recorder_, recorded_step);
1097 if (!test_manager_) {
1098 response->set_success(
false);
1099 response->set_message(
"TestManager not available");
1100 response->set_elapsed_ms(0);
1101 return finalize(absl::FailedPreconditionError(
"TestManager not available"));
1104 const std::string requested_condition = request ? request->condition() :
"";
1105 const std::string requested_widget_key = request ? request->widget_key() :
"";
1106 const std::string request_summary =
1107 !requested_widget_key.empty()
1108 ? absl::StrFormat(
"%s [widget_key:%s]",
1109 requested_condition.empty()
1110 ?
"element_visible:<auto>"
1111 : requested_condition,
1112 requested_widget_key)
1113 : requested_condition;
1115 const std::string test_id = test_manager_->RegisterHarnessTest(
1116 absl::StrFormat(
"Wait: %s", request_summary),
"grpc");
1117 response->set_test_id(test_id);
1118 recorded_step.test_id = test_id;
1119 test_manager_->AppendHarnessTestLog(
1120 test_id, absl::StrFormat(
"Queued wait condition: %s", request_summary));
1122 ResolvedWidgetSelector resolved_selector;
1123 absl::StatusOr<ParsedCondition> parsed_condition = ResolveCondition(
1124 requested_condition, requested_widget_key,
1125 "element_visible", &resolved_selector);
1126 if (!parsed_condition.ok()) {
1127 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1128 std::chrono::steady_clock::now() - start);
1129 std::string message = std::string(parsed_condition.status().message());
1130 response->set_success(
false);
1131 response->set_message(message);
1132 response->set_elapsed_ms(elapsed.count());
1133 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1135 test_manager_->AppendHarnessTestLog(test_id, message);
1136 return finalize(absl::OkStatus());
1139 if (!resolved_selector.resolved_widget_key.empty()) {
1140 response->set_resolved_widget_key(resolved_selector.resolved_widget_key);
1142 if (!resolved_selector.resolved_path.empty()) {
1143 response->set_resolved_path(resolved_selector.resolved_path);
1146 const std::string condition_type = parsed_condition->type;
1147 const std::string condition_target = parsed_condition->target;
1148 const std::string resolved_condition =
1149 absl::StrFormat(
"%s:%s", condition_type, condition_target);
1151#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1152 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
1154 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1155 std::chrono::steady_clock::now() - start);
1156 std::string message =
"ImGuiTestEngine not initialized";
1157 response->set_success(
false);
1158 response->set_message(message);
1159 response->set_elapsed_ms(elapsed.count());
1160 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1162 return finalize(absl::OkStatus());
1164 int timeout_ms = request->timeout_ms() > 0 ? request->timeout_ms() : 5000;
1165 int poll_interval_ms =
1166 request->poll_interval_ms() > 0 ? request->poll_interval_ms() : 100;
1168 auto test_data = std::make_shared<DynamicTestData>();
1169 TestManager* manager = test_manager_;
1170 test_data->test_func = [manager, captured_id = test_id, condition_type,
1171 condition_target, timeout_ms,
1172 poll_interval_ms](ImGuiTestContext* ctx) {
1173 manager->MarkHarnessTestRunning(captured_id);
1174 auto poll_start = std::chrono::steady_clock::now();
1175 auto timeout = std::chrono::milliseconds(timeout_ms);
1177 for (
int i = 0; i < 10; ++i) {
1182 while (std::chrono::steady_clock::now() - poll_start < timeout) {
1183 bool current_state =
false;
1185 if (condition_type ==
"window_visible") {
1186 ImGuiTestItemInfo window_info = ctx->WindowInfo(
1187 condition_target.c_str(), ImGuiTestOpFlags_NoError);
1188 current_state = (window_info.ID != 0);
1189 }
else if (condition_type ==
"element_visible") {
1190 ImGuiTestItemInfo item =
1191 ctx->ItemInfo(condition_target.c_str(), ImGuiTestOpFlags_NoError);
1192 current_state = (item.ID != 0 && item.RectClipped.GetWidth() > 0 &&
1193 item.RectClipped.GetHeight() > 0);
1194 }
else if (condition_type ==
"element_enabled") {
1195 ImGuiTestItemInfo item =
1196 ctx->ItemInfo(condition_target.c_str(), ImGuiTestOpFlags_NoError);
1198 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
1200 std::string error_message =
1201 absl::StrFormat(
"Unknown condition type: %s", condition_type);
1202 manager->AppendHarnessTestLog(captured_id, error_message);
1203 manager->MarkHarnessTestCompleted(
1204 captured_id, HarnessTestStatus::kFailed, error_message);
1208 if (current_state) {
1210 std::chrono::duration_cast<std::chrono::milliseconds>(
1211 std::chrono::steady_clock::now() - poll_start);
1212 std::string success_message = absl::StrFormat(
1213 "Condition '%s:%s' met after %lld ms", condition_type,
1214 condition_target,
static_cast<long long>(elapsed_ms.count()));
1215 manager->AppendHarnessTestLog(captured_id, success_message);
1216 manager->MarkHarnessTestCompleted(
1217 captured_id, HarnessTestStatus::kPassed, success_message);
1221 std::this_thread::sleep_for(
1222 std::chrono::milliseconds(poll_interval_ms));
1226 std::string timeout_message =
1227 absl::StrFormat(
"Condition '%s:%s' not met after %d ms timeout",
1228 condition_type, condition_target, timeout_ms);
1229 manager->AppendHarnessTestLog(captured_id, timeout_message);
1230 manager->MarkHarnessTestCompleted(
1231 captured_id, HarnessTestStatus::kTimeout, timeout_message);
1232 }
catch (
const std::exception& e) {
1233 std::string error_message = absl::StrFormat(
"Wait failed: %s", e.what());
1234 manager->AppendHarnessTestLog(captured_id, error_message);
1235 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
1240 std::string test_name = absl::StrFormat(
1242 static_cast<long long>(
1243 std::chrono::system_clock::now().time_since_epoch().count()));
1245 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
1246 test->TestFunc = RunDynamicTest;
1247 test->UserData = test_data.get();
1249 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
1251 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1252 std::chrono::steady_clock::now() - start);
1253 std::string message =
1254 absl::StrFormat(
"Queued wait for '%s'", resolved_condition);
1255 response->set_success(
true);
1256 response->set_message(message);
1257 response->set_elapsed_ms(elapsed.count());
1258 test_manager_->AppendHarnessTestLog(test_id, message);
1261 test_manager_->MarkHarnessTestRunning(test_id);
1262 std::string message = absl::StrFormat(
1263 "[STUB] Condition '%s' met (ImGuiTestEngine not available)",
1264 resolved_condition);
1265 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kPassed,
1267 test_manager_->AppendHarnessTestLog(test_id, message);
1269 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
1270 std::chrono::steady_clock::now() - start);
1271 response->set_success(
true);
1272 response->set_message(message);
1273 response->set_elapsed_ms(elapsed.count());
1276 return finalize(absl::OkStatus());
1279absl::Status ImGuiTestHarnessServiceImpl::Assert(
const AssertRequest* request,
1280 AssertResponse* response) {
1281 TestRecorder::RecordedStep recorded_step;
1284 recorded_step.widget_key = request->widget_key();
1285 if (!request->widget_key().empty() && request->condition().empty()) {
1286 recorded_step.condition =
1287 absl::StrCat(
"widget_key:", request->widget_key());
1289 recorded_step.condition = request->condition();
1293 auto finalize = [&](
const absl::Status& status) {
1294 recorded_step.success = response->success();
1295 recorded_step.message = response->message();
1296 recorded_step.expected_value = response->expected_value();
1297 recorded_step.actual_value = response->actual_value();
1298 recorded_step.test_id = response->test_id();
1299 MaybeRecordStep(&test_recorder_, recorded_step);
1303 if (!test_manager_) {
1304 response->set_success(
false);
1305 response->set_message(
"TestManager not available");
1306 response->set_actual_value(
"N/A");
1307 response->set_expected_value(
"N/A");
1308 return finalize(absl::FailedPreconditionError(
"TestManager not available"));
1311 const std::string requested_condition = request ? request->condition() :
"";
1312 const std::string requested_widget_key = request ? request->widget_key() :
"";
1313 const std::string request_summary =
1314 !requested_widget_key.empty()
1315 ? absl::StrFormat(
"%s [widget_key:%s]",
1316 requested_condition.empty() ?
"exists:<auto>"
1317 : requested_condition,
1318 requested_widget_key)
1319 : requested_condition;
1321 const std::string test_id = test_manager_->RegisterHarnessTest(
1322 absl::StrFormat(
"Assert: %s", request_summary),
"grpc");
1323 response->set_test_id(test_id);
1324 recorded_step.test_id = test_id;
1325 test_manager_->AppendHarnessTestLog(
1326 test_id, absl::StrFormat(
"Queued assertion: %s", request_summary));
1328 ResolvedWidgetSelector resolved_selector;
1329 absl::StatusOr<ParsedCondition> parsed_condition = ResolveCondition(
1330 requested_condition, requested_widget_key,
1331 "exists", &resolved_selector);
1332 if (!parsed_condition.ok()) {
1333 std::string message = std::string(parsed_condition.status().message());
1334 response->set_success(
false);
1335 response->set_message(message);
1336 response->set_actual_value(
"N/A");
1337 response->set_expected_value(
"N/A");
1338 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1340 test_manager_->AppendHarnessTestLog(test_id, message);
1341 return finalize(absl::OkStatus());
1344 if (!resolved_selector.resolved_widget_key.empty()) {
1345 response->set_resolved_widget_key(resolved_selector.resolved_widget_key);
1347 if (!resolved_selector.resolved_path.empty()) {
1348 response->set_resolved_path(resolved_selector.resolved_path);
1351 const std::string assertion_type = parsed_condition->type;
1352 const std::string assertion_target = parsed_condition->target;
1354#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1355 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
1357 std::string message =
"ImGuiTestEngine not initialized";
1358 response->set_success(
false);
1359 response->set_message(message);
1360 response->set_actual_value(
"N/A");
1361 response->set_expected_value(
"N/A");
1362 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1364 return finalize(absl::OkStatus());
1367 auto test_data = std::make_shared<DynamicTestData>();
1368 TestManager* manager = test_manager_;
1369 test_data->test_func = [manager, captured_id = test_id, assertion_type,
1370 assertion_target](ImGuiTestContext* ctx) {
1371 manager->MarkHarnessTestRunning(captured_id);
1373 auto complete_with = [manager, captured_id](
bool passed,
1374 const std::string& message,
1375 const std::string& actual,
1376 const std::string& expected,
1377 HarnessTestStatus status) {
1378 manager->AppendHarnessTestLog(captured_id, message);
1379 if (!actual.empty() || !expected.empty()) {
1380 manager->AppendHarnessTestLog(
1382 absl::StrFormat(
"Actual: %s | Expected: %s", actual, expected));
1384 manager->MarkHarnessTestCompleted(captured_id, status,
1385 passed ?
"" : message);
1389 bool passed =
false;
1390 std::string actual_value;
1391 std::string expected_value;
1392 std::string message;
1394 if (assertion_type ==
"visible") {
1395 ImGuiTestItemInfo window_info =
1396 ctx->WindowInfo(assertion_target.c_str(), ImGuiTestOpFlags_NoError);
1397 bool is_visible = (window_info.ID != 0);
1398 passed = is_visible;
1399 actual_value = is_visible ?
"visible" :
"hidden";
1400 expected_value =
"visible";
1402 passed ? absl::StrFormat(
"'%s' is visible", assertion_target)
1403 : absl::StrFormat(
"'%s' is not visible", assertion_target);
1404 }
else if (assertion_type ==
"enabled") {
1405 ImGuiTestItemInfo item =
1406 ctx->ItemInfo(assertion_target.c_str(), ImGuiTestOpFlags_NoError);
1408 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
1409 passed = is_enabled;
1410 actual_value = is_enabled ?
"enabled" :
"disabled";
1411 expected_value =
"enabled";
1413 passed ? absl::StrFormat(
"'%s' is enabled", assertion_target)
1414 : absl::StrFormat(
"'%s' is not enabled", assertion_target);
1415 }
else if (assertion_type ==
"exists") {
1416 ImGuiTestItemInfo item =
1417 ctx->ItemInfo(assertion_target.c_str(), ImGuiTestOpFlags_NoError);
1418 bool exists = (item.ID != 0);
1420 actual_value = exists ?
"exists" :
"not found";
1421 expected_value =
"exists";
1422 message = passed ? absl::StrFormat(
"'%s' exists", assertion_target)
1423 : absl::StrFormat(
"'%s' not found", assertion_target);
1424 }
else if (assertion_type ==
"text_contains") {
1425 size_t second_colon = assertion_target.find(
':');
1426 if (second_colon == std::string::npos) {
1427 std::string error_message =
1428 "text_contains requires format "
1429 "'text_contains:target:expected_text'";
1430 complete_with(
false, error_message,
"N/A",
"N/A",
1431 HarnessTestStatus::kFailed);
1435 std::string input_target = assertion_target.substr(0, second_colon);
1436 std::string expected_text = assertion_target.substr(second_colon + 1);
1438 ImGuiTestItemInfo item = ctx->ItemInfo(input_target.c_str());
1440 std::string actual_text =
"(text_retrieval_not_fully_implemented)";
1441 passed = actual_text.find(expected_text) != std::string::npos;
1442 actual_value = actual_text;
1443 expected_value = absl::StrFormat(
"contains '%s'", expected_text);
1444 message = passed ? absl::StrFormat(
"'%s' contains '%s'", input_target,
1447 "'%s' does not contain '%s' (actual: '%s')",
1448 input_target, expected_text, actual_text);
1451 actual_value =
"not found";
1452 expected_value = expected_text;
1453 message = absl::StrFormat(
"Input '%s' not found", input_target);
1456 std::string error_message =
1457 absl::StrFormat(
"Unknown assertion type: %s", assertion_type);
1458 complete_with(
false, error_message,
"N/A",
"N/A",
1459 HarnessTestStatus::kFailed);
1464 passed, message, actual_value, expected_value,
1465 passed ? HarnessTestStatus::kPassed : HarnessTestStatus::
kFailed);
1466 }
catch (
const std::exception& e) {
1467 std::string error_message =
1468 absl::StrFormat(
"Assertion failed: %s", e.what());
1469 manager->AppendHarnessTestLog(captured_id, error_message);
1470 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
1475 std::string test_name = absl::StrFormat(
1477 static_cast<long long>(
1478 std::chrono::system_clock::now().time_since_epoch().count()));
1480 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
1481 test->TestFunc = RunDynamicTest;
1482 test->UserData = test_data.get();
1484 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
1486 response->set_success(
true);
1487 std::string message = absl::StrFormat(
"Queued assertion for '%s:%s'",
1488 assertion_type, assertion_target);
1489 response->set_message(message);
1490 response->set_actual_value(
"(async)");
1491 response->set_expected_value(
"(async)");
1492 test_manager_->AppendHarnessTestLog(test_id, message);
1495 test_manager_->MarkHarnessTestRunning(test_id);
1496 std::string message = absl::StrFormat(
1497 "[STUB] Assertion '%s:%s' passed (ImGuiTestEngine not available)",
1498 assertion_type, assertion_target);
1499 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kPassed,
1501 test_manager_->AppendHarnessTestLog(test_id, message);
1503 response->set_success(
true);
1504 response->set_message(message);
1505 response->set_actual_value(
"(stub)");
1506 response->set_expected_value(
"(stub)");
1509 return finalize(absl::OkStatus());
1512absl::Status ImGuiTestHarnessServiceImpl::FlushUiActions(
1513 const FlushUiActionsRequest* request, FlushUiActionsResponse* response) {
1515 return absl::InvalidArgumentError(
"response cannot be null");
1518 const int32_t timeout_ms =
1519 NormalizeTimeoutMs(request ? request->timeout_ms() : 0, 2000);
1520 const absl::Duration timeout = absl::Milliseconds(timeout_ms);
1521 const absl::Time deadline = absl::Now() + timeout;
1522 const absl::Time started_at = absl::Now();
1524 UiSyncSnapshot initial = CaptureUiSyncSnapshot(test_manager_);
1525 UiSyncSnapshot snapshot = initial;
1527 if (initial.pending_editor_actions == 0 &&
1528 initial.pending_layout_actions == 0 && !initial.layout_rebuild_pending) {
1529 response->set_success(
true);
1530 response->set_message(
"UI queues already idle");
1531 response->set_elapsed_ms(0);
1532 response->set_frame_id(initial.frame_id);
1533 response->set_pending_editor_actions(initial.pending_editor_actions);
1534 response->set_pending_layout_actions(initial.pending_layout_actions);
1535 response->set_layout_rebuild_pending(initial.layout_rebuild_pending);
1536 response->set_pending_harness_tests(initial.pending_harness_tests);
1537 response->set_queued_harness_tests(initial.queued_harness_tests);
1538 response->set_running_harness_tests(initial.running_harness_tests);
1539 return absl::OkStatus();
1542 while (absl::Now() <= deadline) {
1543 snapshot = CaptureUiSyncSnapshot(test_manager_);
1544 const bool frame_advanced = snapshot.frame_id > initial.frame_id;
1545 const bool queues_flushed = snapshot.pending_editor_actions == 0 &&
1546 snapshot.pending_layout_actions == 0 &&
1547 !snapshot.layout_rebuild_pending;
1548 if (frame_advanced && queues_flushed) {
1551 absl::SleepFor(absl::Milliseconds(10));
1554 const int32_t elapsed_ms = ClampDurationToInt32(absl::Now() - started_at);
1555 const bool success = snapshot.pending_editor_actions == 0 &&
1556 snapshot.pending_layout_actions == 0 &&
1557 !snapshot.layout_rebuild_pending;
1558 response->set_success(success);
1559 response->set_message(success ?
"Deferred UI actions flushed"
1560 :
"Timed out waiting for UI action flush");
1561 response->set_elapsed_ms(elapsed_ms);
1562 response->set_frame_id(snapshot.frame_id);
1563 response->set_pending_editor_actions(snapshot.pending_editor_actions);
1564 response->set_pending_layout_actions(snapshot.pending_layout_actions);
1565 response->set_layout_rebuild_pending(snapshot.layout_rebuild_pending);
1566 response->set_pending_harness_tests(snapshot.pending_harness_tests);
1567 response->set_queued_harness_tests(snapshot.queued_harness_tests);
1568 response->set_running_harness_tests(snapshot.running_harness_tests);
1569 return absl::OkStatus();
1572absl::Status ImGuiTestHarnessServiceImpl::WaitForIdle(
1573 const WaitForIdleRequest* request, WaitForIdleResponse* response) {
1575 return absl::InvalidArgumentError(
"response cannot be null");
1578 const int32_t timeout_ms =
1579 NormalizeTimeoutMs(request ? request->timeout_ms() : 0, 5000);
1580 const int32_t required_stable_frames =
1581 std::max(1, request ? request->stable_frames() : 2);
1582 const int32_t poll_interval_ms =
1583 NormalizePollIntervalMs(request ? request->poll_interval_ms() : 25);
1584 const bool flush_first = request ? request->flush_first() :
false;
1586 const absl::Time started_at = absl::Now();
1587 const absl::Time deadline = started_at + absl::Milliseconds(timeout_ms);
1590 FlushUiActionsRequest flush_request;
1591 flush_request.set_timeout_ms(std::min(timeout_ms, 2000));
1592 FlushUiActionsResponse flush_response;
1593 absl::Status flush_status = FlushUiActions(&flush_request, &flush_response);
1594 if (!flush_status.ok()) {
1595 return flush_status;
1599 UiSyncSnapshot snapshot = CaptureUiSyncSnapshot(test_manager_);
1600 int32_t stable_frames_observed = 0;
1601 int64_t last_frame_id = -1;
1603 while (absl::Now() <= deadline) {
1604 snapshot = CaptureUiSyncSnapshot(test_manager_);
1606 const bool is_idle_now = IsUiIdle(snapshot);
1607 if (snapshot.frame_id != last_frame_id) {
1608 last_frame_id = snapshot.frame_id;
1609 stable_frames_observed = is_idle_now ? (stable_frames_observed + 1) : 0;
1610 }
else if (!is_idle_now) {
1611 stable_frames_observed = 0;
1614 if (stable_frames_observed >= required_stable_frames) {
1615 response->set_success(
true);
1616 response->set_message(absl::StrFormat(
1617 "UI idle for %d consecutive frame(s)", stable_frames_observed));
1618 response->set_elapsed_ms(ClampDurationToInt32(absl::Now() - started_at));
1619 response->set_last_frame_id(snapshot.frame_id);
1620 response->set_stable_frames_observed(stable_frames_observed);
1621 response->set_pending_editor_actions(snapshot.pending_editor_actions);
1622 response->set_pending_layout_actions(snapshot.pending_layout_actions);
1623 response->set_layout_rebuild_pending(snapshot.layout_rebuild_pending);
1624 response->set_pending_harness_tests(snapshot.pending_harness_tests);
1625 response->set_queued_harness_tests(snapshot.queued_harness_tests);
1626 response->set_running_harness_tests(snapshot.running_harness_tests);
1627 response->set_timeout_reason(
"");
1628 return absl::OkStatus();
1631 absl::SleepFor(absl::Milliseconds(poll_interval_ms));
1634 const std::string timeout_reason = BuildIdleTimeoutReason(snapshot);
1635 response->set_success(
false);
1636 response->set_message(
"Timed out waiting for deterministic UI idle");
1637 response->set_elapsed_ms(ClampDurationToInt32(absl::Now() - started_at));
1638 response->set_last_frame_id(snapshot.frame_id);
1639 response->set_stable_frames_observed(stable_frames_observed);
1640 response->set_pending_editor_actions(snapshot.pending_editor_actions);
1641 response->set_pending_layout_actions(snapshot.pending_layout_actions);
1642 response->set_layout_rebuild_pending(snapshot.layout_rebuild_pending);
1643 response->set_pending_harness_tests(snapshot.pending_harness_tests);
1644 response->set_queued_harness_tests(snapshot.queued_harness_tests);
1645 response->set_running_harness_tests(snapshot.running_harness_tests);
1646 response->set_timeout_reason(timeout_reason);
1647 return absl::OkStatus();
1650absl::Status ImGuiTestHarnessServiceImpl::GetUiSyncState(
1651 const GetUiSyncStateRequest* ,
1652 GetUiSyncStateResponse* response) {
1654 return absl::InvalidArgumentError(
"response cannot be null");
1657 const UiSyncSnapshot snapshot = CaptureUiSyncSnapshot(test_manager_);
1658 response->set_frame_id(snapshot.frame_id);
1659 response->set_pending_editor_actions(snapshot.pending_editor_actions);
1660 response->set_pending_layout_actions(snapshot.pending_layout_actions);
1661 response->set_layout_rebuild_pending(snapshot.layout_rebuild_pending);
1662 response->set_pending_harness_tests(snapshot.pending_harness_tests);
1663 response->set_queued_harness_tests(snapshot.queued_harness_tests);
1664 response->set_running_harness_tests(snapshot.running_harness_tests);
1665 response->set_sampled_at_ms(absl::ToUnixMillis(absl::Now()));
1666 response->set_idle(IsUiIdle(snapshot));
1667 return absl::OkStatus();
1670absl::Status ImGuiTestHarnessServiceImpl::Screenshot(
1671 const ScreenshotRequest* request, ScreenshotResponse* response) {
1673 return absl::InvalidArgumentError(
"response cannot be null");
1676 const std::string requested_path =
1677 request ? request->output_path() : std::string();
1682 std::atomic<bool> done{
false};
1683 absl::StatusOr<ScreenshotArtifact> result =
1684 absl::UnknownError(
"Not captured");
1686 auto state = std::make_shared<State>();
1689 {.preferred_path = requested_path,
1690 .callback = [state](absl::StatusOr<ScreenshotArtifact> result) {
1691 state->result = std::move(result);
1692 state->done.store(
true);
1696 auto start = std::chrono::steady_clock::now();
1697 while (!state->done.load()) {
1698 if (std::chrono::steady_clock::now() - start > std::chrono::seconds(5)) {
1699 return absl::DeadlineExceededError(
1700 "Timed out waiting for screenshot capture on main thread");
1702 std::this_thread::sleep_for(std::chrono::milliseconds(10));
1705 if (!state->result.ok()) {
1706 response->set_success(
false);
1707 response->set_message(std::string(state->result.status().message()));
1708 return state->result.status();
1711 const ScreenshotArtifact& artifact = *state->result;
1712 response->set_success(
true);
1713 response->set_message(absl::StrFormat(
"Screenshot saved to %s (%dx%d)",
1714 artifact.file_path, artifact.width,
1716 response->set_file_path(artifact.file_path);
1717 response->set_file_size_bytes(artifact.file_size_bytes);
1719 return absl::OkStatus();
1722absl::Status ImGuiTestHarnessServiceImpl::GetTestStatus(
1723 const GetTestStatusRequest* request, GetTestStatusResponse* response) {
1724 if (!test_manager_) {
1725 return absl::FailedPreconditionError(
"TestManager not available");
1728 if (request->test_id().empty()) {
1729 return absl::InvalidArgumentError(
"test_id must be provided");
1733 test_manager_->GetHarnessTestExecution(request->test_id());
1734 if (!execution_or.ok()) {
1735 response->set_status(GetTestStatusResponse::TEST_STATUS_UNSPECIFIED);
1736 response->set_error_message(std::string(execution_or.status().message()));
1737 return absl::OkStatus();
1740 const auto& execution = execution_or.value();
1741 response->set_status(ConvertHarnessStatus(execution.status));
1742 response->set_queued_at_ms(ToUnixMillisSafe(execution.queued_at));
1743 response->set_started_at_ms(ToUnixMillisSafe(execution.started_at));
1744 response->set_completed_at_ms(ToUnixMillisSafe(execution.completed_at));
1745 response->set_execution_time_ms(ClampDurationToInt32(execution.duration));
1746 if (!execution.error_message.empty()) {
1747 response->set_error_message(execution.error_message);
1749 response->clear_error_message();
1752 response->clear_assertion_failures();
1753 for (
const auto& failure : execution.assertion_failures) {
1754 response->add_assertion_failures(failure);
1757 return absl::OkStatus();
1760absl::Status ImGuiTestHarnessServiceImpl::ListTests(
1761 const ListTestsRequest* request, ListTestsResponse* response) {
1762 if (!test_manager_) {
1763 return absl::FailedPreconditionError(
"TestManager not available");
1766 if (request->page_size() < 0) {
1767 return absl::InvalidArgumentError(
"page_size cannot be negative");
1770 int page_size = request->page_size() > 0 ? request->page_size() : 100;
1771 constexpr int kMaxPageSize = 500;
1772 if (page_size > kMaxPageSize) {
1773 page_size = kMaxPageSize;
1776 size_t start_index = 0;
1777 if (!request->page_token().empty()) {
1778 int64_t token_value = 0;
1779 if (!absl::SimpleAtoi(request->page_token(), &token_value) ||
1781 return absl::InvalidArgumentError(
"Invalid page_token");
1783 start_index =
static_cast<size_t>(token_value);
1787 test_manager_->ListHarnessTestSummaries(request->category_filter());
1789 response->set_total_count(
static_cast<int32_t
>(summaries.size()));
1791 if (start_index >= summaries.size()) {
1792 response->clear_tests();
1793 response->clear_next_page_token();
1794 return absl::OkStatus();
1798 std::min(start_index +
static_cast<size_t>(page_size), summaries.size());
1800 for (
size_t i = start_index; i < end_index; ++i) {
1801 const auto& summary = summaries[i];
1802 auto* test_info = response->add_tests();
1803 const auto& exec = summary.latest_execution;
1805 test_info->set_test_id(exec.test_id);
1806 test_info->set_name(exec.name);
1807 test_info->set_category(exec.category);
1809 int64_t last_run_ms = ToUnixMillisSafe(exec.completed_at);
1810 if (last_run_ms == 0) {
1811 last_run_ms = ToUnixMillisSafe(exec.started_at);
1813 if (last_run_ms == 0) {
1814 last_run_ms = ToUnixMillisSafe(exec.queued_at);
1816 test_info->set_last_run_timestamp_ms(last_run_ms);
1818 test_info->set_total_runs(summary.total_runs);
1819 test_info->set_pass_count(summary.pass_count);
1820 test_info->set_fail_count(summary.fail_count);
1822 int32_t average_duration_ms = 0;
1823 if (summary.total_runs > 0) {
1824 absl::Duration average_duration =
1825 summary.total_duration / summary.total_runs;
1826 average_duration_ms = ClampDurationToInt32(average_duration);
1828 test_info->set_average_duration_ms(average_duration_ms);
1831 if (end_index < summaries.size()) {
1832 response->set_next_page_token(absl::StrCat(end_index));
1834 response->clear_next_page_token();
1837 return absl::OkStatus();
1840absl::Status ImGuiTestHarnessServiceImpl::GetTestResults(
1841 const GetTestResultsRequest* request, GetTestResultsResponse* response) {
1842 if (!test_manager_) {
1843 return absl::FailedPreconditionError(
"TestManager not available");
1846 if (request->test_id().empty()) {
1847 return absl::InvalidArgumentError(
"test_id must be provided");
1851 test_manager_->GetHarnessTestExecution(request->test_id());
1852 if (!execution_or.ok()) {
1853 return execution_or.status();
1856 const auto& execution = execution_or.value();
1857 response->set_success(execution.status == HarnessTestStatus::kPassed);
1858 response->set_test_name(execution.name);
1859 response->set_category(execution.category);
1861 int64_t executed_at_ms = ToUnixMillisSafe(execution.completed_at);
1862 if (executed_at_ms == 0) {
1863 executed_at_ms = ToUnixMillisSafe(execution.started_at);
1865 if (executed_at_ms == 0) {
1866 executed_at_ms = ToUnixMillisSafe(execution.queued_at);
1868 response->set_executed_at_ms(executed_at_ms);
1869 response->set_duration_ms(ClampDurationToInt32(execution.duration));
1871 response->clear_assertions();
1872 if (!execution.assertion_failures.empty()) {
1873 for (
const auto& failure : execution.assertion_failures) {
1874 auto* assertion = response->add_assertions();
1875 assertion->set_description(failure);
1876 assertion->set_passed(
false);
1877 assertion->set_error_message(failure);
1879 }
else if (!execution.error_message.empty()) {
1880 auto* assertion = response->add_assertions();
1881 assertion->set_description(
"Execution error");
1882 assertion->set_passed(
false);
1883 assertion->set_error_message(execution.error_message);
1886 if (request->include_logs()) {
1887 for (
const auto& log_entry : execution.logs) {
1888 response->add_logs(log_entry);
1892 auto* metrics_map = response->mutable_metrics();
1893 for (
const auto& [key, value] : execution.metrics) {
1894 (*metrics_map)[
key] = value;
1898 if (!execution.screenshot_path.empty()) {
1899 response->set_screenshot_path(execution.screenshot_path);
1900 response->set_screenshot_size_bytes(execution.screenshot_size_bytes);
1902 if (!execution.failure_context.empty()) {
1903 response->set_failure_context(execution.failure_context);
1905 if (!execution.widget_state.empty()) {
1906 response->set_widget_state(execution.widget_state);
1909 return absl::OkStatus();
1912absl::Status ImGuiTestHarnessServiceImpl::DiscoverWidgets(
1913 const DiscoverWidgetsRequest* request, DiscoverWidgetsResponse* response) {
1915 return absl::InvalidArgumentError(
"request cannot be null");
1918 return absl::InvalidArgumentError(
"response cannot be null");
1921 if (!test_manager_) {
1922 return absl::FailedPreconditionError(
"TestManager not available");
1925 widget_discovery_service_.CollectWidgets(
nullptr, *request, response);
1926 return absl::OkStatus();
1929absl::Status ImGuiTestHarnessServiceImpl::StartRecording(
1930 const StartRecordingRequest* request, StartRecordingResponse* response) {
1932 return absl::InvalidArgumentError(
"request cannot be null");
1935 return absl::InvalidArgumentError(
"response cannot be null");
1938 TestRecorder::RecordingOptions options;
1939 options.output_path = request->output_path();
1940 options.session_name = request->session_name();
1941 options.description = request->description();
1943 if (options.output_path.empty()) {
1944 response->set_success(
false);
1945 response->set_message(
"output_path is required to start recording");
1946 return absl::InvalidArgumentError(
"output_path cannot be empty");
1949 absl::StatusOr<std::string> recording_id = test_recorder_.Start(options);
1950 if (!recording_id.ok()) {
1951 response->set_success(
false);
1952 response->set_message(std::string(recording_id.status().message()));
1953 return recording_id.status();
1956 response->set_success(
true);
1957 response->set_message(
"Recording started");
1958 response->set_recording_id(*recording_id);
1959 response->set_started_at_ms(absl::ToUnixMillis(absl::Now()));
1960 return absl::OkStatus();
1963absl::Status ImGuiTestHarnessServiceImpl::StopRecording(
1964 const StopRecordingRequest* request, StopRecordingResponse* response) {
1966 return absl::InvalidArgumentError(
"request cannot be null");
1969 return absl::InvalidArgumentError(
"response cannot be null");
1972 absl::StatusOr<TestRecorder::StopRecordingSummary> summary =
1973 test_recorder_.Stop(request->recording_id(), request->discard());
1974 if (!summary.ok()) {
1975 response->set_success(
false);
1976 response->set_message(std::string(summary.status().message()));
1977 return summary.status();
1980 response->set_success(
true);
1981 if (summary->saved) {
1982 response->set_message(
"Recording saved");
1984 response->set_message(
"Recording discarded");
1986 response->set_output_path(summary->output_path);
1987 response->set_step_count(summary->step_count);
1988 response->set_duration_ms(absl::ToInt64Milliseconds(summary->duration));
1989 return absl::OkStatus();
1992absl::Status ImGuiTestHarnessServiceImpl::ReplayTest(
1993 const ReplayTestRequest* request, ReplayTestResponse* response) {
1995 return absl::InvalidArgumentError(
"request cannot be null");
1998 return absl::InvalidArgumentError(
"response cannot be null");
2001 response->clear_logs();
2002 response->clear_assertions();
2004 if (request->script_path().empty()) {
2005 response->set_success(
false);
2006 response->set_message(
"script_path is required");
2007 return absl::InvalidArgumentError(
"script_path cannot be empty");
2010 absl::StatusOr<TestScript> script_or =
2012 if (!script_or.ok()) {
2013 response->set_success(
false);
2014 response->set_message(std::string(script_or.status().message()));
2015 return script_or.status();
2017 TestScript script = std::move(*script_or);
2019 absl::flat_hash_map<std::string, std::string> overrides;
2020 for (
const auto& entry : request->parameter_overrides()) {
2021 overrides[entry.first] = entry.second;
2024 response->set_replay_session_id(absl::StrFormat(
2026 absl::FormatTime(
"%Y%m%dT%H%M%S", absl::Now(), absl::UTCTimeZone())));
2028 auto suspension = test_recorder_.Suspend();
2030 std::vector<std::string> logs;
2031 logs.reserve(script.steps.size() * 2 + 4);
2033 bool overall_success =
true;
2034 std::string overall_message =
"Replay completed successfully";
2035 int steps_executed = 0;
2036 absl::Status overall_rpc_status = absl::OkStatus();
2038 for (
const auto& step : script.steps) {
2040 std::string action_label =
2041 absl::StrFormat(
"Step %d: %s", steps_executed, step.action);
2042 logs.push_back(action_label);
2044 absl::Status status = absl::OkStatus();
2045 bool step_success =
false;
2046 std::string step_message;
2047 HarnessTestExecution execution;
2048 bool have_execution =
false;
2050 if (step.action ==
"click") {
2051 ClickRequest sub_request;
2052 sub_request.set_target(ApplyOverrides(step.target, overrides));
2053 sub_request.set_widget_key(ApplyOverrides(step.widget_key, overrides));
2054 sub_request.set_type(ClickTypeFromString(step.click_type));
2055 ClickResponse sub_response;
2056 status = Click(&sub_request, &sub_response);
2057 step_success = sub_response.success();
2058 step_message = sub_response.message();
2059 if (status.ok() && !sub_response.test_id().empty()) {
2060 absl::Status wait_status = WaitForHarnessTestCompletion(
2061 test_manager_, sub_response.test_id(), &execution);
2062 if (wait_status.ok()) {
2063 have_execution =
true;
2064 if (!execution.error_message.empty()) {
2065 step_message = execution.error_message;
2068 status = wait_status;
2069 step_success =
false;
2070 step_message = std::string(wait_status.message());
2073 }
else if (step.action ==
"type") {
2074 TypeRequest sub_request;
2075 sub_request.set_target(ApplyOverrides(step.target, overrides));
2076 sub_request.set_widget_key(ApplyOverrides(step.widget_key, overrides));
2077 sub_request.set_text(ApplyOverrides(step.text, overrides));
2078 sub_request.set_clear_first(step.clear_first);
2079 TypeResponse sub_response;
2080 status = Type(&sub_request, &sub_response);
2081 step_success = sub_response.success();
2082 step_message = sub_response.message();
2083 if (status.ok() && !sub_response.test_id().empty()) {
2084 absl::Status wait_status = WaitForHarnessTestCompletion(
2085 test_manager_, sub_response.test_id(), &execution);
2086 if (wait_status.ok()) {
2087 have_execution =
true;
2088 if (!execution.error_message.empty()) {
2089 step_message = execution.error_message;
2092 status = wait_status;
2093 step_success =
false;
2094 step_message = std::string(wait_status.message());
2097 }
else if (step.action ==
"wait") {
2098 WaitRequest sub_request;
2099 sub_request.set_condition(ApplyOverrides(step.condition, overrides));
2100 sub_request.set_widget_key(ApplyOverrides(step.widget_key, overrides));
2101 if (step.timeout_ms > 0) {
2102 sub_request.set_timeout_ms(step.timeout_ms);
2104 WaitResponse sub_response;
2105 status = Wait(&sub_request, &sub_response);
2106 step_success = sub_response.success();
2107 step_message = sub_response.message();
2108 if (status.ok() && !sub_response.test_id().empty()) {
2109 absl::Status wait_status = WaitForHarnessTestCompletion(
2110 test_manager_, sub_response.test_id(), &execution);
2111 if (wait_status.ok()) {
2112 have_execution =
true;
2113 if (!execution.error_message.empty()) {
2114 step_message = execution.error_message;
2117 status = wait_status;
2118 step_success =
false;
2119 step_message = std::string(wait_status.message());
2122 }
else if (step.action ==
"assert") {
2123 AssertRequest sub_request;
2124 sub_request.set_condition(ApplyOverrides(step.condition, overrides));
2125 sub_request.set_widget_key(ApplyOverrides(step.widget_key, overrides));
2126 AssertResponse sub_response;
2127 status = Assert(&sub_request, &sub_response);
2128 step_success = sub_response.success();
2129 step_message = sub_response.message();
2130 if (status.ok() && !sub_response.test_id().empty()) {
2131 absl::Status wait_status = WaitForHarnessTestCompletion(
2132 test_manager_, sub_response.test_id(), &execution);
2133 if (wait_status.ok()) {
2134 have_execution =
true;
2135 if (!execution.error_message.empty()) {
2136 step_message = execution.error_message;
2139 status = wait_status;
2140 step_success =
false;
2141 step_message = std::string(wait_status.message());
2144 }
else if (step.action ==
"screenshot") {
2145 ScreenshotRequest sub_request;
2146 sub_request.set_window_title(ApplyOverrides(step.target, overrides));
2147 if (!step.region.empty()) {
2148 sub_request.set_output_path(ApplyOverrides(step.region, overrides));
2150 ScreenshotResponse sub_response;
2151 status = Screenshot(&sub_request, &sub_response);
2152 step_success = sub_response.success();
2153 step_message = sub_response.message();
2155 status = absl::InvalidArgumentError(
2156 absl::StrFormat(
"Unsupported action '%s'", step.action));
2158 std::string(status.message().data(), status.message().size());
2161 auto* assertion = response->add_assertions();
2162 assertion->set_description(
2163 absl::StrFormat(
"Step %d (%s)", steps_executed, step.action));
2166 assertion->set_passed(
false);
2167 assertion->set_error_message(
2168 std::string(status.message().data(), status.message().size()));
2169 overall_success =
false;
2170 overall_message = step_message;
2171 logs.push_back(absl::StrFormat(
" Error: %s", status.message()));
2172 overall_rpc_status = status;
2176 bool expectations_met = (step_success == step.expect_success);
2177 std::string expectation_error;
2179 if (!expectations_met) {
2181 absl::StrFormat(
"Expected success=%s but got %s",
2182 step.expect_success ?
"true" :
"false",
2183 step_success ?
"true" :
"false");
2186 if (!step.expect_status.empty()) {
2187 HarnessTestStatus expected_status =
2188 ::yaze::test::HarnessStatusFromString(step.expect_status);
2189 if (!have_execution) {
2190 expectations_met =
false;
2191 if (!expectation_error.empty()) {
2192 expectation_error.append(
"; ");
2194 expectation_error.append(
"No execution details available");
2195 }
else if (expected_status != HarnessTestStatus::kUnspecified &&
2196 execution.status != expected_status) {
2197 expectations_met =
false;
2198 if (!expectation_error.empty()) {
2199 expectation_error.append(
"; ");
2201 expectation_error.append(absl::StrFormat(
2202 "Expected status %s but observed %s", step.expect_status,
2203 ::yaze::test::HarnessStatusToString(execution.status)));
2205 if (have_execution) {
2206 assertion->set_actual_value(
2207 ::yaze::test::HarnessStatusToString(execution.status));
2208 assertion->set_expected_value(step.expect_status);
2212 if (!step.expect_message.empty()) {
2213 std::string actual_message = step_message;
2214 if (have_execution && !execution.error_message.empty()) {
2215 actual_message = execution.error_message;
2217 if (actual_message.find(step.expect_message) == std::string::npos) {
2218 expectations_met =
false;
2219 if (!expectation_error.empty()) {
2220 expectation_error.append(
"; ");
2222 expectation_error.append(
2223 absl::StrFormat(
"Expected message containing '%s' but got '%s'",
2224 step.expect_message, actual_message));
2228 if (!expectations_met) {
2229 assertion->set_passed(
false);
2230 assertion->set_error_message(expectation_error);
2231 overall_success =
false;
2232 overall_message = expectation_error;
2234 absl::StrFormat(
" Failed expectations: %s", expectation_error));
2235 if (request->ci_mode()) {
2239 assertion->set_passed(
true);
2240 logs.push_back(absl::StrFormat(
" Result: %s", step_message));
2243 if (have_execution && !execution.assertion_failures.empty()) {
2244 for (
const auto& failure : execution.assertion_failures) {
2245 logs.push_back(absl::StrFormat(
" Assertion failure: %s", failure));
2249 if (!overall_success && request->ci_mode()) {
2254 response->set_steps_executed(steps_executed);
2255 response->set_success(overall_success);
2256 response->set_message(overall_message);
2257 for (
const auto& log_entry : logs) {
2258 response->add_logs(log_entry);
2261 return overall_rpc_status;
2268ImGuiTestHarnessServer& ImGuiTestHarnessServer::Instance() {
2269 static ImGuiTestHarnessServer* instance =
new ImGuiTestHarnessServer();
2273ImGuiTestHarnessServer::~ImGuiTestHarnessServer() {
2277absl::Status ImGuiTestHarnessServer::Start(
int port,
2278 TestManager* test_manager) {
2280 return absl::FailedPreconditionError(
"Server already running");
2283 if (!test_manager) {
2284 return absl::InvalidArgumentError(
"TestManager cannot be null");
2288 service_ = std::make_unique<ImGuiTestHarnessServiceImpl>(test_manager);
2292 grpc_service_ = std::make_unique<ImGuiTestHarnessServiceGrpc>(service_.get());
2294 std::string server_address = absl::StrFormat(
"0.0.0.0:%d", port);
2296 grpc::ServerBuilder builder;
2299 builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
2302 builder.RegisterService(grpc_service_.get());
2305 server_ = builder.BuildAndStart();
2308 return absl::InternalError(
2309 absl::StrFormat(
"Failed to start gRPC server on %s", server_address));
2314 std::cout <<
"✓ ImGuiTestHarness gRPC server listening on " << server_address
2315 <<
" (with TestManager integration)\n";
2316 std::cout <<
" Use 'grpcurl -plaintext -d '{\"message\":\"test\"}' "
2317 << server_address <<
" yaze.test.ImGuiTestHarness/Ping' to test\n";
2319 return absl::OkStatus();
2322void ImGuiTestHarnessServer::Shutdown() {
2324 std::cout <<
"⏹ Shutting down ImGuiTestHarness gRPC server...\n";
2325 server_->Shutdown();
2329 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.