yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
imgui_test_harness_service.cc
Go to the documentation of this file.
2#include "app/application.h"
3
4#ifdef YAZE_WITH_GRPC
5
7
8#include <algorithm>
9#include <chrono>
10#include <deque>
11#include <fstream>
12#include <iostream>
13#include <limits>
14#include <optional>
15#include <thread>
16
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"
33#include "yaze.h" // For YAZE_VERSION_STRING
34
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"
38
39// Helper to register and run a test dynamically
40namespace {
41struct DynamicTestData {
42 std::function<void(ImGuiTestContext*)> test_func;
43};
44
45absl::Mutex g_dynamic_tests_mutex;
46std::deque<std::shared_ptr<DynamicTestData>> g_dynamic_tests
47 ABSL_GUARDED_BY(g_dynamic_tests_mutex);
48
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();
55 }
56}
57
58void RunDynamicTest(ImGuiTestContext* ctx) {
59 auto* data = (DynamicTestData*)ctx->Test->UserData;
60 if (data && data->test_func) {
61 data->test_func(ctx);
62 }
63}
64
65// Helper to check if a test has completed (not queued or running)
66bool IsTestCompleted(ImGuiTest* test) {
67 return test->Output.Status != ImGuiTestStatus_Queued &&
68 test->Output.Status != ImGuiTestStatus_Running;
69}
70
71// Thread-safe state for RPC communication
72template <typename T>
73struct RPCState {
74 std::atomic<bool> completed{false};
75 std::mutex data_mutex;
76 T result;
77 std::string message;
78
79 void SetResult(const T& res, const std::string& msg) {
80 std::lock_guard<std::mutex> lock(data_mutex);
81 result = res;
82 message = msg;
83 completed.store(true);
84 }
85
86 void GetResult(T& res, std::string& msg) {
87 std::lock_guard<std::mutex> lock(data_mutex);
88 res = result;
89 msg = message;
90 }
91};
92
93} // namespace
94#endif
95
96namespace {
97
98::yaze::test::GetTestStatusResponse_TestStatus ConvertHarnessStatus(
99 ::yaze::test::HarnessTestStatus status) {
100 switch (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:
112 default:
113 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_UNSPECIFIED;
114 }
115}
116
117int64_t ToUnixMillisSafe(absl::Time timestamp) {
118 if (timestamp == absl::InfinitePast()) {
119 return 0;
120 }
121 return absl::ToUnixMillis(timestamp);
122}
123
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();
128 }
129 if (millis < std::numeric_limits<int32_t>::min()) {
130 return std::numeric_limits<int32_t>::min();
131 }
132 return static_cast<int32_t>(millis);
133}
134
135} // namespace
136
137#include <grpcpp/grpcpp.h>
138#include <grpcpp/server_builder.h>
139
140namespace yaze {
141namespace test {
142
143namespace {
144
145std::string ClickTypeToString(ClickRequest::ClickType type) {
146 switch (type) {
147 case ClickRequest::CLICK_TYPE_RIGHT:
148 return "right";
149 case ClickRequest::CLICK_TYPE_MIDDLE:
150 return "middle";
151 case ClickRequest::CLICK_TYPE_DOUBLE:
152 return "double";
153 case ClickRequest::CLICK_TYPE_LEFT:
154 case ClickRequest::CLICK_TYPE_UNSPECIFIED:
155 default:
156 return "left";
157 }
158}
159
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;
164 }
165 if (lower == "middle") {
166 return ClickRequest::CLICK_TYPE_MIDDLE;
167 }
168 if (lower == "double" || lower == "double_click" || lower == "dbl") {
169 return ClickRequest::CLICK_TYPE_DOUBLE;
170 }
171 return ClickRequest::CLICK_TYPE_LEFT;
172}
173
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;
178 }
179 if (lower == "failed" || lower == "fail") {
180 return HarnessTestStatus::kFailed;
181 }
182 if (lower == "timeout") {
183 return HarnessTestStatus::kTimeout;
184 }
185 if (lower == "queued") {
186 return HarnessTestStatus::kQueued;
187 }
188 if (lower == "running") {
189 return HarnessTestStatus::kRunning;
190 }
191 return HarnessTestStatus::kUnspecified;
192}
193
194const char* HarnessStatusToString(HarnessTestStatus status) {
195 switch (status) {
196 case HarnessTestStatus::kPassed:
197 return "passed";
198 case HarnessTestStatus::kFailed:
199 return "failed";
200 case HarnessTestStatus::kTimeout:
201 return "timeout";
202 case HarnessTestStatus::kQueued:
203 return "queued";
204 case HarnessTestStatus::kRunning:
205 return "running";
206 case HarnessTestStatus::kUnspecified:
207 default:
208 return "unknown";
209 }
210}
211
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()) {
216 return value;
217 }
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}});
222 }
223 return result;
224}
225
226void MaybeRecordStep(TestRecorder* recorder, TestRecorder::RecordedStep step) {
227 if (!recorder || !recorder->IsRecording()) {
228 return;
229 }
230 if (step.captured_at == absl::InfinitePast()) {
231 step.captured_at = absl::Now();
232 }
233 recorder->RecordStep(step);
234}
235
236absl::Status WaitForHarnessTestCompletion(TestManager* manager,
237 const std::string& test_id,
238 HarnessTestExecution* execution) {
239 if (!manager) {
240 return absl::FailedPreconditionError("TestManager unavailable");
241 }
242 if (test_id.empty()) {
243 return absl::InvalidArgumentError("Missing harness test identifier");
244 }
245
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);
250 if (!current.ok()) {
251 absl::SleepFor(absl::Milliseconds(75));
252 continue;
253 }
254
255 if (execution) {
256 *execution = std::move(current.value());
257 }
258
259 if (current->status == HarnessTestStatus::kQueued ||
260 current->status == HarnessTestStatus::kRunning) {
261 absl::SleepFor(absl::Milliseconds(75));
262 continue;
263 }
264 return absl::OkStatus();
265 }
266
267 return absl::DeadlineExceededError(absl::StrFormat(
268 "Harness test %s did not reach a terminal state", test_id));
269}
270
271struct ParsedTarget {
272 std::string type;
273 std::string label;
274};
275
276struct ResolvedWidgetSelector {
277 ParsedTarget target;
278 std::string resolved_widget_key;
279 std::string resolved_path;
280};
281
282struct ParsedCondition {
283 std::string type;
284 std::string target;
285};
286
287std::string ExtractLabelFromPath(absl::string_view path) {
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));
291 }
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));
295 }
296 return std::string(path);
297}
298
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) {
305 return "widget";
306 }
307 return std::string(segment.substr(0, colon));
308}
309
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.");
314 }
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').");
320 }
321
322 ParsedTarget parsed;
323 parsed.type = std::string(target.substr(0, colon_pos));
324 parsed.label = std::string(target.substr(colon_pos + 1));
325 return parsed;
326}
327
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 =
335 if (!info) {
336 return absl::NotFoundError(absl::StrFormat(
337 "widget_key '%s' was not found in WidgetIdRegistry", key));
338 }
339
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()
347 ? ExtractLabelFromPath(resolved.resolved_path)
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));
352 }
353 if (resolved.target.type.empty()) {
354 resolved.target.type = "widget";
355 }
356 return resolved;
357 }
358
359 absl::StatusOr<ParsedTarget> parsed = ParseTargetString(target);
360 if (!parsed.ok()) {
361 return parsed.status();
362 }
363 resolved.target = *parsed;
364 return resolved;
365}
366
367absl::StatusOr<ParsedCondition> ParseConditionString(absl::string_view value) {
368 if (value.empty()) {
369 return absl::InvalidArgumentError(
370 "Missing condition. Provide 'type:target' or widget_key.");
371 }
372
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'.");
377 }
378
379 ParsedCondition parsed;
380 parsed.type = std::string(value.substr(0, colon_pos));
381 parsed.target = std::string(value.substr(colon_pos + 1));
382 return parsed;
383}
384
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();
395 }
396 widget_selector = *resolved;
397 if (resolved_selector) {
398 *resolved_selector = *resolved;
399 }
400 } else if (resolved_selector) {
401 *resolved_selector = ResolvedWidgetSelector{};
402 }
403
404 if (condition.empty()) {
405 if (!widget_selector.has_value()) {
406 return absl::InvalidArgumentError(
407 "Missing condition. Provide 'type:target' or widget_key.");
408 }
409 ParsedCondition parsed;
410 parsed.type = std::string(default_type_for_widget);
411 parsed.target = widget_selector->target.label;
412 return parsed;
413 }
414
415 absl::StatusOr<ParsedCondition> parsed = ParseConditionString(condition);
416 if (!parsed.ok()) {
417 return parsed.status();
418 }
419
420 if (widget_selector.has_value() && parsed->target.empty()) {
421 parsed->target = widget_selector->target.label;
422 }
423
424 if (parsed->target.empty()) {
425 return absl::InvalidArgumentError(
426 "Condition target is empty. Provide 'type:target'.");
427 }
428
429 return parsed;
430}
431
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;
440};
441
442editor::EditorManager* GetEditorManagerForSync() {
443 Controller* controller = Application::Instance().GetController();
444 if (!controller) {
445 return nullptr;
446 }
447 return controller->editor_manager();
448}
449
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;
459 }
460
461 if (test_manager) {
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++;
468 break;
469 case HarnessTestStatus::kRunning:
470 snapshot.running_harness_tests++;
471 break;
472 default:
473 break;
474 }
475 }
476 }
477
478 snapshot.pending_harness_tests =
479 snapshot.queued_harness_tests + snapshot.running_harness_tests;
480 return snapshot;
481}
482
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;
488}
489
490std::string BuildIdleTimeoutReason(const UiSyncSnapshot& snapshot) {
491 return absl::StrFormat(
492 "pending_editor=%d pending_layout=%d layout_rebuild=%s queued_tests=%d "
493 "running_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);
497}
498
499int32_t NormalizeTimeoutMs(int32_t timeout_ms, int32_t fallback_ms) {
500 return timeout_ms <= 0 ? fallback_ms : timeout_ms;
501}
502
503int32_t NormalizePollIntervalMs(int32_t poll_interval_ms) {
504 if (poll_interval_ms <= 0) {
505 return 25;
506 }
507 return std::clamp(poll_interval_ms, 5, 1000);
508}
509
510} // namespace
511
512// gRPC service wrapper that forwards to our implementation
513class ImGuiTestHarnessServiceGrpc final : public ImGuiTestHarness::Service {
514 public:
515 explicit ImGuiTestHarnessServiceGrpc(ImGuiTestHarnessServiceImpl* impl)
516 : impl_(impl) {}
517
518 ::grpc::Status Ping(::grpc::ServerContext* context,
519 const PingRequest* request,
520 PingResponse* response) override {
521 return ConvertStatus(impl_->Ping(request, response));
522 }
523
524 ::grpc::Status Click(::grpc::ServerContext* context,
525 const ClickRequest* request,
526 ClickResponse* response) override {
527 return ConvertStatus(impl_->Click(request, response));
528 }
529
530 ::grpc::Status Type(::grpc::ServerContext* context,
531 const TypeRequest* request,
532 TypeResponse* response) override {
533 return ConvertStatus(impl_->Type(request, response));
534 }
535
536 ::grpc::Status Wait(::grpc::ServerContext* context,
537 const WaitRequest* request,
538 WaitResponse* response) override {
539 return ConvertStatus(impl_->Wait(request, response));
540 }
541
542 ::grpc::Status Assert(::grpc::ServerContext* context,
543 const AssertRequest* request,
544 AssertResponse* response) override {
545 return ConvertStatus(impl_->Assert(request, response));
546 }
547
548 ::grpc::Status FlushUiActions(::grpc::ServerContext* context,
549 const FlushUiActionsRequest* request,
550 FlushUiActionsResponse* response) override {
551 return ConvertStatus(impl_->FlushUiActions(request, response));
552 }
553
554 ::grpc::Status WaitForIdle(::grpc::ServerContext* context,
555 const WaitForIdleRequest* request,
556 WaitForIdleResponse* response) override {
557 return ConvertStatus(impl_->WaitForIdle(request, response));
558 }
559
560 ::grpc::Status GetUiSyncState(::grpc::ServerContext* context,
561 const GetUiSyncStateRequest* request,
562 GetUiSyncStateResponse* response) override {
563 return ConvertStatus(impl_->GetUiSyncState(request, response));
564 }
565
566 ::grpc::Status Screenshot(::grpc::ServerContext* context,
567 const ScreenshotRequest* request,
568 ScreenshotResponse* response) override {
569 return ConvertStatus(impl_->Screenshot(request, response));
570 }
571
572 ::grpc::Status GetTestStatus(::grpc::ServerContext* context,
573 const GetTestStatusRequest* request,
574 GetTestStatusResponse* response) override {
575 return ConvertStatus(impl_->GetTestStatus(request, response));
576 }
577
578 ::grpc::Status ListTests(::grpc::ServerContext* context,
579 const ListTestsRequest* request,
580 ListTestsResponse* response) override {
581 return ConvertStatus(impl_->ListTests(request, response));
582 }
583
584 ::grpc::Status GetTestResults(::grpc::ServerContext* context,
585 const GetTestResultsRequest* request,
586 GetTestResultsResponse* response) override {
587 return ConvertStatus(impl_->GetTestResults(request, response));
588 }
589
590 ::grpc::Status DiscoverWidgets(::grpc::ServerContext* context,
591 const DiscoverWidgetsRequest* request,
592 DiscoverWidgetsResponse* response) override {
593 return ConvertStatus(impl_->DiscoverWidgets(request, response));
594 }
595
596 ::grpc::Status StartRecording(::grpc::ServerContext* context,
597 const StartRecordingRequest* request,
598 StartRecordingResponse* response) override {
599 return ConvertStatus(impl_->StartRecording(request, response));
600 }
601
602 ::grpc::Status StopRecording(::grpc::ServerContext* context,
603 const StopRecordingRequest* request,
604 StopRecordingResponse* response) override {
605 return ConvertStatus(impl_->StopRecording(request, response));
606 }
607
608 ::grpc::Status ReplayTest(::grpc::ServerContext* context,
609 const ReplayTestRequest* request,
610 ReplayTestResponse* response) override {
611 return ConvertStatus(impl_->ReplayTest(request, response));
612 }
613
614 private:
615 static ::grpc::Status ConvertStatus(const absl::Status& status) {
616 if (status.ok()) {
617 return ::grpc::Status::OK;
618 }
619
620 ::grpc::StatusCode code = ::grpc::StatusCode::UNKNOWN;
621 switch (status.code()) {
622 case absl::StatusCode::kCancelled:
623 code = ::grpc::StatusCode::CANCELLED;
624 break;
625 case absl::StatusCode::kUnknown:
626 code = ::grpc::StatusCode::UNKNOWN;
627 break;
628 case absl::StatusCode::kInvalidArgument:
629 code = ::grpc::StatusCode::INVALID_ARGUMENT;
630 break;
631 case absl::StatusCode::kDeadlineExceeded:
632 code = ::grpc::StatusCode::DEADLINE_EXCEEDED;
633 break;
634 case absl::StatusCode::kNotFound:
635 code = ::grpc::StatusCode::NOT_FOUND;
636 break;
637 case absl::StatusCode::kAlreadyExists:
638 code = ::grpc::StatusCode::ALREADY_EXISTS;
639 break;
640 case absl::StatusCode::kPermissionDenied:
641 code = ::grpc::StatusCode::PERMISSION_DENIED;
642 break;
643 case absl::StatusCode::kResourceExhausted:
644 code = ::grpc::StatusCode::RESOURCE_EXHAUSTED;
645 break;
646 case absl::StatusCode::kFailedPrecondition:
647 code = ::grpc::StatusCode::FAILED_PRECONDITION;
648 break;
649 case absl::StatusCode::kAborted:
650 code = ::grpc::StatusCode::ABORTED;
651 break;
652 case absl::StatusCode::kOutOfRange:
653 code = ::grpc::StatusCode::OUT_OF_RANGE;
654 break;
655 case absl::StatusCode::kUnimplemented:
656 code = ::grpc::StatusCode::UNIMPLEMENTED;
657 break;
658 case absl::StatusCode::kInternal:
659 code = ::grpc::StatusCode::INTERNAL;
660 break;
661 case absl::StatusCode::kUnavailable:
662 code = ::grpc::StatusCode::UNAVAILABLE;
663 break;
664 case absl::StatusCode::kDataLoss:
665 code = ::grpc::StatusCode::DATA_LOSS;
666 break;
667 case absl::StatusCode::kUnauthenticated:
668 code = ::grpc::StatusCode::UNAUTHENTICATED;
669 break;
670 default:
671 code = ::grpc::StatusCode::UNKNOWN;
672 break;
673 }
674
675 return ::grpc::Status(
676 code, std::string(status.message().data(), status.message().size()));
677 }
678
679 ImGuiTestHarnessServiceImpl* impl_;
680};
681
682std::unique_ptr<::grpc::Service> CreateImGuiTestHarnessServiceGrpc(
683 ImGuiTestHarnessServiceImpl* impl) {
684 return std::make_unique<ImGuiTestHarnessServiceGrpc>(impl);
685}
686
687// ============================================================================
688// ImGuiTestHarnessServiceImpl - RPC Handlers
689// ============================================================================
690
691absl::Status ImGuiTestHarnessServiceImpl::Ping(const PingRequest* request,
692 PingResponse* response) {
693 // Echo back the message with "Pong: " prefix
694 response->set_message(absl::StrFormat("Pong: %s", request->message()));
695
696 // Add current timestamp
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());
701
702 // Add YAZE version
703 response->set_yaze_version(YAZE_VERSION_STRING);
704
705 return absl::OkStatus();
706}
707
708absl::Status ImGuiTestHarnessServiceImpl::Click(const ClickRequest* request,
709 ClickResponse* response) {
710 auto start = std::chrono::steady_clock::now();
711
712 TestRecorder::RecordedStep recorded_step;
713 recorded_step.type = TestRecorder::ActionType::kClick;
714 if (request) {
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());
718 } else {
719 recorded_step.target = request->target();
720 }
721 recorded_step.click_type = ClickTypeToString(request->type());
722 }
723
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);
730 return status;
731 };
732
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"));
738 }
739
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)
745 : requested_target;
746
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));
753
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,
764 message);
765 test_manager_->AppendHarnessTestLog(test_id, message);
766 return finalize(absl::OkStatus());
767 }
768
769 if (!resolved_selector->resolved_widget_key.empty()) {
770 response->set_resolved_widget_key(resolved_selector->resolved_widget_key);
771 }
772 if (!resolved_selector->resolved_path.empty()) {
773 response->set_resolved_path(resolved_selector->resolved_path);
774 }
775
776 const std::string widget_type = resolved_selector->target.type;
777 const std::string widget_label = resolved_selector->target.label;
778
779#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
780 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
781 if (!engine) {
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,
789 message);
790 return finalize(absl::OkStatus());
791 }
792
793 const ClickRequest::ClickType click_type =
794 request ? request->type() : ClickRequest::CLICK_TYPE_LEFT;
795
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;
801 break;
802 case ClickRequest::CLICK_TYPE_RIGHT:
803 mouse_button = ImGuiMouseButton_Right;
804 break;
805 case ClickRequest::CLICK_TYPE_MIDDLE:
806 mouse_button = ImGuiMouseButton_Middle;
807 break;
808 case ClickRequest::CLICK_TYPE_DOUBLE:
809 // handled below
810 break;
811 }
812
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);
819 try {
820 if (click_type == ClickRequest::CLICK_TYPE_DOUBLE) {
821 ctx->ItemDoubleClick(widget_label.c_str());
822 } else {
823 ctx->ItemClick(widget_label.c_str(), mouse_button);
824 }
825 ctx->Yield();
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,
830 success_message);
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,
836 error_message);
837 }
838 };
839
840 std::string test_name = absl::StrFormat(
841 "grpc_click_%lld",
842 static_cast<long long>(
843 std::chrono::system_clock::now().time_since_epoch().count()));
844
845 ImGuiTest* test = IM_REGISTER_TEST(engine, "grpc", test_name.c_str());
846 test->TestFunc = RunDynamicTest;
847 test->UserData = test_data.get();
848
849 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
850 KeepDynamicTestData(test_data);
851
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);
860
861#else
862 std::string message =
863 absl::StrFormat("[STUB] Clicked %s '%s' (ImGuiTestEngine not available)",
864 widget_type, widget_label);
865
866 test_manager_->MarkHarnessTestRunning(test_id);
867 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kPassed,
868 message);
869 test_manager_->AppendHarnessTestLog(test_id, message);
870
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());
876#endif
877
878 return finalize(absl::OkStatus());
879}
880
881absl::Status ImGuiTestHarnessServiceImpl::Type(const TypeRequest* request,
882 TypeResponse* response) {
883 auto start = std::chrono::steady_clock::now();
884
885 TestRecorder::RecordedStep recorded_step;
886 recorded_step.type = TestRecorder::ActionType::kType;
887 if (request) {
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());
891 } else {
892 recorded_step.target = request->target();
893 }
894 recorded_step.text = request->text();
895 recorded_step.clear_first = request->clear_first();
896 }
897
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);
904 return status;
905 };
906
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"));
912 }
913
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)
919 : requested_target;
920
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));
927
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,
938 message);
939 test_manager_->AppendHarnessTestLog(test_id, message);
940 return finalize(absl::OkStatus());
941 }
942
943 if (!resolved_selector->resolved_widget_key.empty()) {
944 response->set_resolved_widget_key(resolved_selector->resolved_widget_key);
945 }
946 if (!resolved_selector->resolved_path.empty()) {
947 response->set_resolved_path(resolved_selector->resolved_path);
948 }
949
950 const std::string widget_type = resolved_selector->target.type;
951 const std::string widget_label = resolved_selector->target.label;
952
953#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
954 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
955 if (!engine) {
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,
963 message);
964 return finalize(absl::OkStatus());
965 }
966 std::string text = request->text();
967 bool clear_first = request->clear_first();
968
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);
976 try {
977 ImGuiTestItemInfo item = ctx->ItemInfo(widget_label.c_str());
978 if (item.ID == 0) {
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);
985 return;
986 }
987
988 ctx->ItemClick(widget_label.c_str());
989 if (clear_first) {
990 ctx->KeyPress(ImGuiMod_Shortcut | ImGuiKey_A);
991 ctx->KeyPress(ImGuiKey_Delete);
992 }
993
994 ctx->ItemInputValue(widget_label.c_str(), text.c_str());
995
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,
1001 success_message);
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,
1007 error_message);
1008 rpc_state->SetResult(false, error_message);
1009 }
1010 };
1011
1012 std::string test_name = absl::StrFormat(
1013 "grpc_type_%lld",
1014 static_cast<long long>(
1015 std::chrono::system_clock::now().time_since_epoch().count()));
1016
1017 ImGuiTest* test = IM_REGISTER_TEST(engine, "grpc", test_name.c_str());
1018 test->TestFunc = RunDynamicTest;
1019 test->UserData = test_data.get();
1020
1021 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
1022 KeepDynamicTestData(test_data);
1023
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,
1032 error_message);
1033 rpc_state->SetResult(false, error_message);
1034 break;
1035 }
1036 std::this_thread::sleep_for(std::chrono::milliseconds(100));
1037 }
1038
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);
1044
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);
1050 }
1051
1052#else
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,
1058 message);
1059 test_manager_->AppendHarnessTestLog(test_id, message);
1060
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());
1066#endif
1067
1068 return finalize(absl::OkStatus());
1069}
1070
1071absl::Status ImGuiTestHarnessServiceImpl::Wait(const WaitRequest* request,
1072 WaitResponse* response) {
1073 auto start = std::chrono::steady_clock::now();
1074
1075 TestRecorder::RecordedStep recorded_step;
1076 recorded_step.type = TestRecorder::ActionType::kWait;
1077 if (request) {
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());
1082 } else {
1083 recorded_step.condition = request->condition();
1084 }
1085 recorded_step.timeout_ms = request->timeout_ms();
1086 }
1087
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);
1094 return status;
1095 };
1096
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"));
1102 }
1103
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;
1114
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));
1121
1122 ResolvedWidgetSelector resolved_selector;
1123 absl::StatusOr<ParsedCondition> parsed_condition = ResolveCondition(
1124 requested_condition, requested_widget_key,
1125 /*default_type_for_widget=*/"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,
1134 message);
1135 test_manager_->AppendHarnessTestLog(test_id, message);
1136 return finalize(absl::OkStatus());
1137 }
1138
1139 if (!resolved_selector.resolved_widget_key.empty()) {
1140 response->set_resolved_widget_key(resolved_selector.resolved_widget_key);
1141 }
1142 if (!resolved_selector.resolved_path.empty()) {
1143 response->set_resolved_path(resolved_selector.resolved_path);
1144 }
1145
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);
1150
1151#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1152 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
1153 if (!engine) {
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,
1161 message);
1162 return finalize(absl::OkStatus());
1163 }
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;
1167
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);
1176
1177 for (int i = 0; i < 10; ++i) {
1178 ctx->Yield();
1179 }
1180
1181 try {
1182 while (std::chrono::steady_clock::now() - poll_start < timeout) {
1183 bool current_state = false;
1184
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);
1197 current_state =
1198 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
1199 } else {
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);
1205 return;
1206 }
1207
1208 if (current_state) {
1209 auto elapsed_ms =
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);
1218 return;
1219 }
1220
1221 std::this_thread::sleep_for(
1222 std::chrono::milliseconds(poll_interval_ms));
1223 ctx->Yield();
1224 }
1225
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,
1236 error_message);
1237 }
1238 };
1239
1240 std::string test_name = absl::StrFormat(
1241 "grpc_wait_%lld",
1242 static_cast<long long>(
1243 std::chrono::system_clock::now().time_since_epoch().count()));
1244
1245 ImGuiTest* test = IM_REGISTER_TEST(engine, "grpc", test_name.c_str());
1246 test->TestFunc = RunDynamicTest;
1247 test->UserData = test_data.get();
1248
1249 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
1250
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);
1259
1260#else
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,
1266 message);
1267 test_manager_->AppendHarnessTestLog(test_id, message);
1268
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());
1274#endif
1275
1276 return finalize(absl::OkStatus());
1277}
1278
1279absl::Status ImGuiTestHarnessServiceImpl::Assert(const AssertRequest* request,
1280 AssertResponse* response) {
1281 TestRecorder::RecordedStep recorded_step;
1282 recorded_step.type = TestRecorder::ActionType::kAssert;
1283 if (request) {
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());
1288 } else {
1289 recorded_step.condition = request->condition();
1290 }
1291 }
1292
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);
1300 return status;
1301 };
1302
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"));
1309 }
1310
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;
1320
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));
1327
1328 ResolvedWidgetSelector resolved_selector;
1329 absl::StatusOr<ParsedCondition> parsed_condition = ResolveCondition(
1330 requested_condition, requested_widget_key,
1331 /*default_type_for_widget=*/"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,
1339 message);
1340 test_manager_->AppendHarnessTestLog(test_id, message);
1341 return finalize(absl::OkStatus());
1342 }
1343
1344 if (!resolved_selector.resolved_widget_key.empty()) {
1345 response->set_resolved_widget_key(resolved_selector.resolved_widget_key);
1346 }
1347 if (!resolved_selector.resolved_path.empty()) {
1348 response->set_resolved_path(resolved_selector.resolved_path);
1349 }
1350
1351 const std::string assertion_type = parsed_condition->type;
1352 const std::string assertion_target = parsed_condition->target;
1353
1354#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1355 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
1356 if (!engine) {
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,
1363 message);
1364 return finalize(absl::OkStatus());
1365 }
1366
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);
1372
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(
1381 captured_id,
1382 absl::StrFormat("Actual: %s | Expected: %s", actual, expected));
1383 }
1384 manager->MarkHarnessTestCompleted(captured_id, status,
1385 passed ? "" : message);
1386 };
1387
1388 try {
1389 bool passed = false;
1390 std::string actual_value;
1391 std::string expected_value;
1392 std::string message;
1393
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";
1401 message =
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);
1407 bool is_enabled =
1408 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
1409 passed = is_enabled;
1410 actual_value = is_enabled ? "enabled" : "disabled";
1411 expected_value = "enabled";
1412 message =
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);
1419 passed = exists;
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);
1432 return;
1433 }
1434
1435 std::string input_target = assertion_target.substr(0, second_colon);
1436 std::string expected_text = assertion_target.substr(second_colon + 1);
1437
1438 ImGuiTestItemInfo item = ctx->ItemInfo(input_target.c_str());
1439 if (item.ID != 0) {
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,
1445 expected_text)
1446 : absl::StrFormat(
1447 "'%s' does not contain '%s' (actual: '%s')",
1448 input_target, expected_text, actual_text);
1449 } else {
1450 passed = false;
1451 actual_value = "not found";
1452 expected_value = expected_text;
1453 message = absl::StrFormat("Input '%s' not found", input_target);
1454 }
1455 } else {
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);
1460 return;
1461 }
1462
1463 complete_with(
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,
1471 error_message);
1472 }
1473 };
1474
1475 std::string test_name = absl::StrFormat(
1476 "grpc_assert_%lld",
1477 static_cast<long long>(
1478 std::chrono::system_clock::now().time_since_epoch().count()));
1479
1480 ImGuiTest* test = IM_REGISTER_TEST(engine, "grpc", test_name.c_str());
1481 test->TestFunc = RunDynamicTest;
1482 test->UserData = test_data.get();
1483
1484 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
1485
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);
1493
1494#else
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,
1500 message);
1501 test_manager_->AppendHarnessTestLog(test_id, message);
1502
1503 response->set_success(true);
1504 response->set_message(message);
1505 response->set_actual_value("(stub)");
1506 response->set_expected_value("(stub)");
1507#endif
1508
1509 return finalize(absl::OkStatus());
1510}
1511
1512absl::Status ImGuiTestHarnessServiceImpl::FlushUiActions(
1513 const FlushUiActionsRequest* request, FlushUiActionsResponse* response) {
1514 if (!response) {
1515 return absl::InvalidArgumentError("response cannot be null");
1516 }
1517
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();
1523
1524 UiSyncSnapshot initial = CaptureUiSyncSnapshot(test_manager_);
1525 UiSyncSnapshot snapshot = initial;
1526
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();
1540 }
1541
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) {
1549 break;
1550 }
1551 absl::SleepFor(absl::Milliseconds(10));
1552 }
1553
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();
1570}
1571
1572absl::Status ImGuiTestHarnessServiceImpl::WaitForIdle(
1573 const WaitForIdleRequest* request, WaitForIdleResponse* response) {
1574 if (!response) {
1575 return absl::InvalidArgumentError("response cannot be null");
1576 }
1577
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;
1585
1586 const absl::Time started_at = absl::Now();
1587 const absl::Time deadline = started_at + absl::Milliseconds(timeout_ms);
1588
1589 if (flush_first) {
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;
1596 }
1597 }
1598
1599 UiSyncSnapshot snapshot = CaptureUiSyncSnapshot(test_manager_);
1600 int32_t stable_frames_observed = 0;
1601 int64_t last_frame_id = -1;
1602
1603 while (absl::Now() <= deadline) {
1604 snapshot = CaptureUiSyncSnapshot(test_manager_);
1605
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;
1612 }
1613
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();
1629 }
1630
1631 absl::SleepFor(absl::Milliseconds(poll_interval_ms));
1632 }
1633
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();
1648}
1649
1650absl::Status ImGuiTestHarnessServiceImpl::GetUiSyncState(
1651 const GetUiSyncStateRequest* /*request*/,
1652 GetUiSyncStateResponse* response) {
1653 if (!response) {
1654 return absl::InvalidArgumentError("response cannot be null");
1655 }
1656
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();
1668}
1669
1670absl::Status ImGuiTestHarnessServiceImpl::Screenshot(
1671 const ScreenshotRequest* request, ScreenshotResponse* response) {
1672 if (!response) {
1673 return absl::InvalidArgumentError("response cannot be null");
1674 }
1675
1676 const std::string requested_path =
1677 request ? request->output_path() : std::string();
1678
1679 // We must execute capture on the main thread to avoid Metal/OpenGL context errors.
1680 // Use Controller's request queue.
1681 struct State {
1682 std::atomic<bool> done{false};
1683 absl::StatusOr<ScreenshotArtifact> result =
1684 absl::UnknownError("Not captured");
1685 };
1686 auto state = std::make_shared<State>();
1687
1689 {.preferred_path = requested_path,
1690 .callback = [state](absl::StatusOr<ScreenshotArtifact> result) {
1691 state->result = std::move(result);
1692 state->done.store(true);
1693 }});
1694
1695 // Wait for main thread to process (timeout after 5s)
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");
1701 }
1702 std::this_thread::sleep_for(std::chrono::milliseconds(10));
1703 }
1704
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();
1709 }
1710
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,
1715 artifact.height));
1716 response->set_file_path(artifact.file_path);
1717 response->set_file_size_bytes(artifact.file_size_bytes);
1718
1719 return absl::OkStatus();
1720}
1721
1722absl::Status ImGuiTestHarnessServiceImpl::GetTestStatus(
1723 const GetTestStatusRequest* request, GetTestStatusResponse* response) {
1724 if (!test_manager_) {
1725 return absl::FailedPreconditionError("TestManager not available");
1726 }
1727
1728 if (request->test_id().empty()) {
1729 return absl::InvalidArgumentError("test_id must be provided");
1730 }
1731
1732 auto execution_or =
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();
1738 }
1739
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);
1748 } else {
1749 response->clear_error_message();
1750 }
1751
1752 response->clear_assertion_failures();
1753 for (const auto& failure : execution.assertion_failures) {
1754 response->add_assertion_failures(failure);
1755 }
1756
1757 return absl::OkStatus();
1758}
1759
1760absl::Status ImGuiTestHarnessServiceImpl::ListTests(
1761 const ListTestsRequest* request, ListTestsResponse* response) {
1762 if (!test_manager_) {
1763 return absl::FailedPreconditionError("TestManager not available");
1764 }
1765
1766 if (request->page_size() < 0) {
1767 return absl::InvalidArgumentError("page_size cannot be negative");
1768 }
1769
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;
1774 }
1775
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) ||
1780 token_value < 0) {
1781 return absl::InvalidArgumentError("Invalid page_token");
1782 }
1783 start_index = static_cast<size_t>(token_value);
1784 }
1785
1786 auto summaries =
1787 test_manager_->ListHarnessTestSummaries(request->category_filter());
1788
1789 response->set_total_count(static_cast<int32_t>(summaries.size()));
1790
1791 if (start_index >= summaries.size()) {
1792 response->clear_tests();
1793 response->clear_next_page_token();
1794 return absl::OkStatus();
1795 }
1796
1797 size_t end_index =
1798 std::min(start_index + static_cast<size_t>(page_size), summaries.size());
1799
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;
1804
1805 test_info->set_test_id(exec.test_id);
1806 test_info->set_name(exec.name);
1807 test_info->set_category(exec.category);
1808
1809 int64_t last_run_ms = ToUnixMillisSafe(exec.completed_at);
1810 if (last_run_ms == 0) {
1811 last_run_ms = ToUnixMillisSafe(exec.started_at);
1812 }
1813 if (last_run_ms == 0) {
1814 last_run_ms = ToUnixMillisSafe(exec.queued_at);
1815 }
1816 test_info->set_last_run_timestamp_ms(last_run_ms);
1817
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);
1821
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);
1827 }
1828 test_info->set_average_duration_ms(average_duration_ms);
1829 }
1830
1831 if (end_index < summaries.size()) {
1832 response->set_next_page_token(absl::StrCat(end_index));
1833 } else {
1834 response->clear_next_page_token();
1835 }
1836
1837 return absl::OkStatus();
1838}
1839
1840absl::Status ImGuiTestHarnessServiceImpl::GetTestResults(
1841 const GetTestResultsRequest* request, GetTestResultsResponse* response) {
1842 if (!test_manager_) {
1843 return absl::FailedPreconditionError("TestManager not available");
1844 }
1845
1846 if (request->test_id().empty()) {
1847 return absl::InvalidArgumentError("test_id must be provided");
1848 }
1849
1850 auto execution_or =
1851 test_manager_->GetHarnessTestExecution(request->test_id());
1852 if (!execution_or.ok()) {
1853 return execution_or.status();
1854 }
1855
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);
1860
1861 int64_t executed_at_ms = ToUnixMillisSafe(execution.completed_at);
1862 if (executed_at_ms == 0) {
1863 executed_at_ms = ToUnixMillisSafe(execution.started_at);
1864 }
1865 if (executed_at_ms == 0) {
1866 executed_at_ms = ToUnixMillisSafe(execution.queued_at);
1867 }
1868 response->set_executed_at_ms(executed_at_ms);
1869 response->set_duration_ms(ClampDurationToInt32(execution.duration));
1870
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);
1878 }
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);
1884 }
1885
1886 if (request->include_logs()) {
1887 for (const auto& log_entry : execution.logs) {
1888 response->add_logs(log_entry);
1889 }
1890 }
1891
1892 auto* metrics_map = response->mutable_metrics();
1893 for (const auto& [key, value] : execution.metrics) {
1894 (*metrics_map)[key] = value;
1895 }
1896
1897 // IT-08b: Include failure diagnostics if available
1898 if (!execution.screenshot_path.empty()) {
1899 response->set_screenshot_path(execution.screenshot_path);
1900 response->set_screenshot_size_bytes(execution.screenshot_size_bytes);
1901 }
1902 if (!execution.failure_context.empty()) {
1903 response->set_failure_context(execution.failure_context);
1904 }
1905 if (!execution.widget_state.empty()) {
1906 response->set_widget_state(execution.widget_state);
1907 }
1908
1909 return absl::OkStatus();
1910}
1911
1912absl::Status ImGuiTestHarnessServiceImpl::DiscoverWidgets(
1913 const DiscoverWidgetsRequest* request, DiscoverWidgetsResponse* response) {
1914 if (!request) {
1915 return absl::InvalidArgumentError("request cannot be null");
1916 }
1917 if (!response) {
1918 return absl::InvalidArgumentError("response cannot be null");
1919 }
1920
1921 if (!test_manager_) {
1922 return absl::FailedPreconditionError("TestManager not available");
1923 }
1924
1925 widget_discovery_service_.CollectWidgets(/*ctx=*/nullptr, *request, response);
1926 return absl::OkStatus();
1927}
1928
1929absl::Status ImGuiTestHarnessServiceImpl::StartRecording(
1930 const StartRecordingRequest* request, StartRecordingResponse* response) {
1931 if (!request) {
1932 return absl::InvalidArgumentError("request cannot be null");
1933 }
1934 if (!response) {
1935 return absl::InvalidArgumentError("response cannot be null");
1936 }
1937
1938 TestRecorder::RecordingOptions options;
1939 options.output_path = request->output_path();
1940 options.session_name = request->session_name();
1941 options.description = request->description();
1942
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");
1947 }
1948
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();
1954 }
1955
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();
1961}
1962
1963absl::Status ImGuiTestHarnessServiceImpl::StopRecording(
1964 const StopRecordingRequest* request, StopRecordingResponse* response) {
1965 if (!request) {
1966 return absl::InvalidArgumentError("request cannot be null");
1967 }
1968 if (!response) {
1969 return absl::InvalidArgumentError("response cannot be null");
1970 }
1971
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();
1978 }
1979
1980 response->set_success(true);
1981 if (summary->saved) {
1982 response->set_message("Recording saved");
1983 } else {
1984 response->set_message("Recording discarded");
1985 }
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();
1990}
1991
1992absl::Status ImGuiTestHarnessServiceImpl::ReplayTest(
1993 const ReplayTestRequest* request, ReplayTestResponse* response) {
1994 if (!request) {
1995 return absl::InvalidArgumentError("request cannot be null");
1996 }
1997 if (!response) {
1998 return absl::InvalidArgumentError("response cannot be null");
1999 }
2000
2001 response->clear_logs();
2002 response->clear_assertions();
2003
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");
2008 }
2009
2010 absl::StatusOr<TestScript> script_or =
2011 TestScriptParser::ParseFromFile(request->script_path());
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();
2016 }
2017 TestScript script = std::move(*script_or);
2018
2019 absl::flat_hash_map<std::string, std::string> overrides;
2020 for (const auto& entry : request->parameter_overrides()) {
2021 overrides[entry.first] = entry.second;
2022 }
2023
2024 response->set_replay_session_id(absl::StrFormat(
2025 "replay_%s",
2026 absl::FormatTime("%Y%m%dT%H%M%S", absl::Now(), absl::UTCTimeZone())));
2027
2028 auto suspension = test_recorder_.Suspend();
2029
2030 std::vector<std::string> logs;
2031 logs.reserve(script.steps.size() * 2 + 4);
2032
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();
2037
2038 for (const auto& step : script.steps) {
2039 ++steps_executed;
2040 std::string action_label =
2041 absl::StrFormat("Step %d: %s", steps_executed, step.action);
2042 logs.push_back(action_label);
2043
2044 absl::Status status = absl::OkStatus();
2045 bool step_success = false;
2046 std::string step_message;
2047 HarnessTestExecution execution;
2048 bool have_execution = false;
2049
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;
2066 }
2067 } else {
2068 status = wait_status;
2069 step_success = false;
2070 step_message = std::string(wait_status.message());
2071 }
2072 }
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;
2090 }
2091 } else {
2092 status = wait_status;
2093 step_success = false;
2094 step_message = std::string(wait_status.message());
2095 }
2096 }
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);
2103 }
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;
2115 }
2116 } else {
2117 status = wait_status;
2118 step_success = false;
2119 step_message = std::string(wait_status.message());
2120 }
2121 }
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;
2137 }
2138 } else {
2139 status = wait_status;
2140 step_success = false;
2141 step_message = std::string(wait_status.message());
2142 }
2143 }
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));
2149 }
2150 ScreenshotResponse sub_response;
2151 status = Screenshot(&sub_request, &sub_response);
2152 step_success = sub_response.success();
2153 step_message = sub_response.message();
2154 } else {
2155 status = absl::InvalidArgumentError(
2156 absl::StrFormat("Unsupported action '%s'", step.action));
2157 step_message =
2158 std::string(status.message().data(), status.message().size());
2159 }
2160
2161 auto* assertion = response->add_assertions();
2162 assertion->set_description(
2163 absl::StrFormat("Step %d (%s)", steps_executed, step.action));
2164
2165 if (!status.ok()) {
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;
2173 break;
2174 }
2175
2176 bool expectations_met = (step_success == step.expect_success);
2177 std::string expectation_error;
2178
2179 if (!expectations_met) {
2180 expectation_error =
2181 absl::StrFormat("Expected success=%s but got %s",
2182 step.expect_success ? "true" : "false",
2183 step_success ? "true" : "false");
2184 }
2185
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("; ");
2193 }
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("; ");
2200 }
2201 expectation_error.append(absl::StrFormat(
2202 "Expected status %s but observed %s", step.expect_status,
2203 ::yaze::test::HarnessStatusToString(execution.status)));
2204 }
2205 if (have_execution) {
2206 assertion->set_actual_value(
2207 ::yaze::test::HarnessStatusToString(execution.status));
2208 assertion->set_expected_value(step.expect_status);
2209 }
2210 }
2211
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;
2216 }
2217 if (actual_message.find(step.expect_message) == std::string::npos) {
2218 expectations_met = false;
2219 if (!expectation_error.empty()) {
2220 expectation_error.append("; ");
2221 }
2222 expectation_error.append(
2223 absl::StrFormat("Expected message containing '%s' but got '%s'",
2224 step.expect_message, actual_message));
2225 }
2226 }
2227
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;
2233 logs.push_back(
2234 absl::StrFormat(" Failed expectations: %s", expectation_error));
2235 if (request->ci_mode()) {
2236 break;
2237 }
2238 } else {
2239 assertion->set_passed(true);
2240 logs.push_back(absl::StrFormat(" Result: %s", step_message));
2241 }
2242
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));
2246 }
2247 }
2248
2249 if (!overall_success && request->ci_mode()) {
2250 break;
2251 }
2252 }
2253
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);
2259 }
2260
2261 return overall_rpc_status;
2262}
2263
2264// ============================================================================
2265// ImGuiTestHarnessServer - Server Lifecycle
2266// ============================================================================
2267
2268ImGuiTestHarnessServer& ImGuiTestHarnessServer::Instance() {
2269 static ImGuiTestHarnessServer* instance = new ImGuiTestHarnessServer();
2270 return *instance;
2271}
2272
2273ImGuiTestHarnessServer::~ImGuiTestHarnessServer() {
2274 Shutdown();
2275}
2276
2277absl::Status ImGuiTestHarnessServer::Start(int port,
2278 TestManager* test_manager) {
2279 if (server_) {
2280 return absl::FailedPreconditionError("Server already running");
2281 }
2282
2283 if (!test_manager) {
2284 return absl::InvalidArgumentError("TestManager cannot be null");
2285 }
2286
2287 // Create the service implementation with TestManager reference
2288 service_ = std::make_unique<ImGuiTestHarnessServiceImpl>(test_manager);
2289
2290 // Create the gRPC service wrapper (store as member to prevent it from going
2291 // out of scope)
2292 grpc_service_ = std::make_unique<ImGuiTestHarnessServiceGrpc>(service_.get());
2293
2294 std::string server_address = absl::StrFormat("0.0.0.0:%d", port);
2295
2296 grpc::ServerBuilder builder;
2297
2298 // Listen on all interfaces (use 0.0.0.0 to avoid IPv6/IPv4 binding conflicts)
2299 builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
2300
2301 // Register service
2302 builder.RegisterService(grpc_service_.get());
2303
2304 // Build and start
2305 server_ = builder.BuildAndStart();
2306
2307 if (!server_) {
2308 return absl::InternalError(
2309 absl::StrFormat("Failed to start gRPC server on %s", server_address));
2310 }
2311
2312 port_ = port;
2313
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";
2318
2319 return absl::OkStatus();
2320}
2321
2322void ImGuiTestHarnessServer::Shutdown() {
2323 if (server_) {
2324 std::cout << "⏹ Shutting down ImGuiTestHarness gRPC server...\n";
2325 server_->Shutdown();
2326 server_.reset();
2327 service_.reset();
2328 port_ = 0;
2329 std::cout << "✓ ImGuiTestHarness gRPC server stopped\n";
2330 }
2331}
2332
2333} // namespace test
2334} // namespace yaze
2335
2336#endif // YAZE_WITH_GRPC
Controller * GetController()
Definition application.h:81
static Application & Instance()
editor::EditorManager * editor_manager()
Definition controller.h:69
void RequestScreenshot(const ScreenshotRequest &request)
UiSyncState GetUiSyncStateSnapshot() const
const WidgetInfo * GetWidgetInfo(const std::string &full_path) const
static WidgetIdRegistry & Instance()
static absl::StatusOr< TestScript > ParseFromFile(const std::string &path)
#define YAZE_VERSION_STRING
std::string ExtractLabelFromPath(absl::string_view path)
SDL2/SDL3 compatibility layer.
Public YAZE API umbrella header.