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 <thread>
15
16#include "absl/base/thread_annotations.h"
17#include "absl/container/flat_hash_map.h"
18#include "absl/strings/ascii.h"
19#include "absl/strings/numbers.h"
20#include "absl/strings/str_cat.h"
21#include "absl/strings/str_format.h"
22#include "absl/strings/str_replace.h"
23#include "absl/synchronization/mutex.h"
24#include "absl/time/clock.h"
25#include "absl/time/time.h"
29#include "protos/imgui_test_harness.grpc.pb.h"
30#include "protos/imgui_test_harness.pb.h"
31#include "yaze.h" // For YAZE_VERSION_STRING
32
33#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
34#include "imgui_test_engine/imgui_te_context.h"
35#include "imgui_test_engine/imgui_te_engine.h"
36
37// Helper to register and run a test dynamically
38namespace {
39struct DynamicTestData {
40 std::function<void(ImGuiTestContext*)> test_func;
41};
42
43absl::Mutex g_dynamic_tests_mutex;
44std::deque<std::shared_ptr<DynamicTestData>> g_dynamic_tests
45 ABSL_GUARDED_BY(g_dynamic_tests_mutex);
46
47void KeepDynamicTestData(const std::shared_ptr<DynamicTestData>& data) {
48 absl::MutexLock lock(&g_dynamic_tests_mutex);
49 constexpr size_t kMaxKeepAlive = 64;
50 g_dynamic_tests.push_back(data);
51 while (g_dynamic_tests.size() > kMaxKeepAlive) {
52 g_dynamic_tests.pop_front();
53 }
54}
55
56void RunDynamicTest(ImGuiTestContext* ctx) {
57 auto* data = (DynamicTestData*)ctx->Test->UserData;
58 if (data && data->test_func) {
59 data->test_func(ctx);
60 }
61}
62
63// Helper to check if a test has completed (not queued or running)
64bool IsTestCompleted(ImGuiTest* test) {
65 return test->Output.Status != ImGuiTestStatus_Queued &&
66 test->Output.Status != ImGuiTestStatus_Running;
67}
68
69// Thread-safe state for RPC communication
70template <typename T>
71struct RPCState {
72 std::atomic<bool> completed{false};
73 std::mutex data_mutex;
74 T result;
75 std::string message;
76
77 void SetResult(const T& res, const std::string& msg) {
78 std::lock_guard<std::mutex> lock(data_mutex);
79 result = res;
80 message = msg;
81 completed.store(true);
82 }
83
84 void GetResult(T& res, std::string& msg) {
85 std::lock_guard<std::mutex> lock(data_mutex);
86 res = result;
87 msg = message;
88 }
89};
90
91} // namespace
92#endif
93
94namespace {
95
96::yaze::test::GetTestStatusResponse_TestStatus ConvertHarnessStatus(
97 ::yaze::test::HarnessTestStatus status) {
98 switch (status) {
99 case ::yaze::test::HarnessTestStatus::kQueued:
100 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_QUEUED;
101 case ::yaze::test::HarnessTestStatus::kRunning:
102 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_RUNNING;
103 case ::yaze::test::HarnessTestStatus::kPassed:
104 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_PASSED;
105 case ::yaze::test::HarnessTestStatus::kFailed:
106 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_FAILED;
107 case ::yaze::test::HarnessTestStatus::kTimeout:
108 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_TIMEOUT;
109 case ::yaze::test::HarnessTestStatus::kUnspecified:
110 default:
111 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_UNSPECIFIED;
112 }
113}
114
115int64_t ToUnixMillisSafe(absl::Time timestamp) {
116 if (timestamp == absl::InfinitePast()) {
117 return 0;
118 }
119 return absl::ToUnixMillis(timestamp);
120}
121
122int32_t ClampDurationToInt32(absl::Duration duration) {
123 int64_t millis = absl::ToInt64Milliseconds(duration);
124 if (millis > std::numeric_limits<int32_t>::max()) {
125 return std::numeric_limits<int32_t>::max();
126 }
127 if (millis < std::numeric_limits<int32_t>::min()) {
128 return std::numeric_limits<int32_t>::min();
129 }
130 return static_cast<int32_t>(millis);
131}
132
133} // namespace
134
135#include <grpcpp/grpcpp.h>
136#include <grpcpp/server_builder.h>
137
138namespace yaze {
139namespace test {
140
141namespace {
142
143std::string ClickTypeToString(ClickRequest::ClickType type) {
144 switch (type) {
145 case ClickRequest::CLICK_TYPE_RIGHT:
146 return "right";
147 case ClickRequest::CLICK_TYPE_MIDDLE:
148 return "middle";
149 case ClickRequest::CLICK_TYPE_DOUBLE:
150 return "double";
151 case ClickRequest::CLICK_TYPE_LEFT:
152 case ClickRequest::CLICK_TYPE_UNSPECIFIED:
153 default:
154 return "left";
155 }
156}
157
158ClickRequest::ClickType ClickTypeFromString(absl::string_view type) {
159 const std::string lower = absl::AsciiStrToLower(std::string(type));
160 if (lower == "right") {
161 return ClickRequest::CLICK_TYPE_RIGHT;
162 }
163 if (lower == "middle") {
164 return ClickRequest::CLICK_TYPE_MIDDLE;
165 }
166 if (lower == "double" || lower == "double_click" || lower == "dbl") {
167 return ClickRequest::CLICK_TYPE_DOUBLE;
168 }
169 return ClickRequest::CLICK_TYPE_LEFT;
170}
171
172HarnessTestStatus HarnessStatusFromString(absl::string_view status) {
173 const std::string lower = absl::AsciiStrToLower(std::string(status));
174 if (lower == "passed" || lower == "success") {
175 return HarnessTestStatus::kPassed;
176 }
177 if (lower == "failed" || lower == "fail") {
178 return HarnessTestStatus::kFailed;
179 }
180 if (lower == "timeout") {
181 return HarnessTestStatus::kTimeout;
182 }
183 if (lower == "queued") {
184 return HarnessTestStatus::kQueued;
185 }
186 if (lower == "running") {
187 return HarnessTestStatus::kRunning;
188 }
189 return HarnessTestStatus::kUnspecified;
190}
191
192const char* HarnessStatusToString(HarnessTestStatus status) {
193 switch (status) {
194 case HarnessTestStatus::kPassed:
195 return "passed";
196 case HarnessTestStatus::kFailed:
197 return "failed";
198 case HarnessTestStatus::kTimeout:
199 return "timeout";
200 case HarnessTestStatus::kQueued:
201 return "queued";
202 case HarnessTestStatus::kRunning:
203 return "running";
204 case HarnessTestStatus::kUnspecified:
205 default:
206 return "unknown";
207 }
208}
209
210std::string ApplyOverrides(
211 const std::string& value,
212 const absl::flat_hash_map<std::string, std::string>& overrides) {
213 if (overrides.empty() || value.empty()) {
214 return value;
215 }
216 std::string result = value;
217 for (const auto& [key, replacement] : overrides) {
218 const std::string placeholder = absl::StrCat("{{", key, "}}");
219 result = absl::StrReplaceAll(result, {{placeholder, replacement}});
220 }
221 return result;
222}
223
224void MaybeRecordStep(TestRecorder* recorder, TestRecorder::RecordedStep step) {
225 if (!recorder || !recorder->IsRecording()) {
226 return;
227 }
228 if (step.captured_at == absl::InfinitePast()) {
229 step.captured_at = absl::Now();
230 }
231 recorder->RecordStep(step);
232}
233
234absl::Status WaitForHarnessTestCompletion(TestManager* manager,
235 const std::string& test_id,
236 HarnessTestExecution* execution) {
237 if (!manager) {
238 return absl::FailedPreconditionError("TestManager unavailable");
239 }
240 if (test_id.empty()) {
241 return absl::InvalidArgumentError("Missing harness test identifier");
242 }
243
244 const absl::Time deadline = absl::Now() + absl::Seconds(20);
245 while (absl::Now() < deadline) {
246 absl::StatusOr<HarnessTestExecution> current =
247 manager->GetHarnessTestExecution(test_id);
248 if (!current.ok()) {
249 absl::SleepFor(absl::Milliseconds(75));
250 continue;
251 }
252
253 if (execution) {
254 *execution = std::move(current.value());
255 }
256
257 if (current->status == HarnessTestStatus::kQueued ||
258 current->status == HarnessTestStatus::kRunning) {
259 absl::SleepFor(absl::Milliseconds(75));
260 continue;
261 }
262 return absl::OkStatus();
263 }
264
265 return absl::DeadlineExceededError(absl::StrFormat(
266 "Harness test %s did not reach a terminal state", test_id));
267}
268
269} // namespace
270
271// gRPC service wrapper that forwards to our implementation
272class ImGuiTestHarnessServiceGrpc final : public ImGuiTestHarness::Service {
273 public:
274 explicit ImGuiTestHarnessServiceGrpc(ImGuiTestHarnessServiceImpl* impl)
275 : impl_(impl) {}
276
277 ::grpc::Status Ping(::grpc::ServerContext* context,
278 const PingRequest* request,
279 PingResponse* response) override {
280 return ConvertStatus(impl_->Ping(request, response));
281 }
282
283 ::grpc::Status Click(::grpc::ServerContext* context,
284 const ClickRequest* request,
285 ClickResponse* response) override {
286 return ConvertStatus(impl_->Click(request, response));
287 }
288
289 ::grpc::Status Type(::grpc::ServerContext* context,
290 const TypeRequest* request,
291 TypeResponse* response) override {
292 return ConvertStatus(impl_->Type(request, response));
293 }
294
295 ::grpc::Status Wait(::grpc::ServerContext* context,
296 const WaitRequest* request,
297 WaitResponse* response) override {
298 return ConvertStatus(impl_->Wait(request, response));
299 }
300
301 ::grpc::Status Assert(::grpc::ServerContext* context,
302 const AssertRequest* request,
303 AssertResponse* response) override {
304 return ConvertStatus(impl_->Assert(request, response));
305 }
306
307 ::grpc::Status Screenshot(::grpc::ServerContext* context,
308 const ScreenshotRequest* request,
309 ScreenshotResponse* response) override {
310 return ConvertStatus(impl_->Screenshot(request, response));
311 }
312
313 ::grpc::Status GetTestStatus(::grpc::ServerContext* context,
314 const GetTestStatusRequest* request,
315 GetTestStatusResponse* response) override {
316 return ConvertStatus(impl_->GetTestStatus(request, response));
317 }
318
319 ::grpc::Status ListTests(::grpc::ServerContext* context,
320 const ListTestsRequest* request,
321 ListTestsResponse* response) override {
322 return ConvertStatus(impl_->ListTests(request, response));
323 }
324
325 ::grpc::Status GetTestResults(::grpc::ServerContext* context,
326 const GetTestResultsRequest* request,
327 GetTestResultsResponse* response) override {
328 return ConvertStatus(impl_->GetTestResults(request, response));
329 }
330
331 ::grpc::Status DiscoverWidgets(::grpc::ServerContext* context,
332 const DiscoverWidgetsRequest* request,
333 DiscoverWidgetsResponse* response) override {
334 return ConvertStatus(impl_->DiscoverWidgets(request, response));
335 }
336
337 ::grpc::Status StartRecording(::grpc::ServerContext* context,
338 const StartRecordingRequest* request,
339 StartRecordingResponse* response) override {
340 return ConvertStatus(impl_->StartRecording(request, response));
341 }
342
343 ::grpc::Status StopRecording(::grpc::ServerContext* context,
344 const StopRecordingRequest* request,
345 StopRecordingResponse* response) override {
346 return ConvertStatus(impl_->StopRecording(request, response));
347 }
348
349 ::grpc::Status ReplayTest(::grpc::ServerContext* context,
350 const ReplayTestRequest* request,
351 ReplayTestResponse* response) override {
352 return ConvertStatus(impl_->ReplayTest(request, response));
353 }
354
355 private:
356 static ::grpc::Status ConvertStatus(const absl::Status& status) {
357 if (status.ok()) {
358 return ::grpc::Status::OK;
359 }
360
361 ::grpc::StatusCode code = ::grpc::StatusCode::UNKNOWN;
362 switch (status.code()) {
363 case absl::StatusCode::kCancelled:
364 code = ::grpc::StatusCode::CANCELLED;
365 break;
366 case absl::StatusCode::kUnknown:
367 code = ::grpc::StatusCode::UNKNOWN;
368 break;
369 case absl::StatusCode::kInvalidArgument:
370 code = ::grpc::StatusCode::INVALID_ARGUMENT;
371 break;
372 case absl::StatusCode::kDeadlineExceeded:
373 code = ::grpc::StatusCode::DEADLINE_EXCEEDED;
374 break;
375 case absl::StatusCode::kNotFound:
376 code = ::grpc::StatusCode::NOT_FOUND;
377 break;
378 case absl::StatusCode::kAlreadyExists:
379 code = ::grpc::StatusCode::ALREADY_EXISTS;
380 break;
381 case absl::StatusCode::kPermissionDenied:
382 code = ::grpc::StatusCode::PERMISSION_DENIED;
383 break;
384 case absl::StatusCode::kResourceExhausted:
385 code = ::grpc::StatusCode::RESOURCE_EXHAUSTED;
386 break;
387 case absl::StatusCode::kFailedPrecondition:
388 code = ::grpc::StatusCode::FAILED_PRECONDITION;
389 break;
390 case absl::StatusCode::kAborted:
391 code = ::grpc::StatusCode::ABORTED;
392 break;
393 case absl::StatusCode::kOutOfRange:
394 code = ::grpc::StatusCode::OUT_OF_RANGE;
395 break;
396 case absl::StatusCode::kUnimplemented:
397 code = ::grpc::StatusCode::UNIMPLEMENTED;
398 break;
399 case absl::StatusCode::kInternal:
400 code = ::grpc::StatusCode::INTERNAL;
401 break;
402 case absl::StatusCode::kUnavailable:
403 code = ::grpc::StatusCode::UNAVAILABLE;
404 break;
405 case absl::StatusCode::kDataLoss:
406 code = ::grpc::StatusCode::DATA_LOSS;
407 break;
408 case absl::StatusCode::kUnauthenticated:
409 code = ::grpc::StatusCode::UNAUTHENTICATED;
410 break;
411 default:
412 code = ::grpc::StatusCode::UNKNOWN;
413 break;
414 }
415
416 return ::grpc::Status(
417 code, std::string(status.message().data(), status.message().size()));
418 }
419
420 ImGuiTestHarnessServiceImpl* impl_;
421};
422
423std::unique_ptr<::grpc::Service> CreateImGuiTestHarnessServiceGrpc(
424 ImGuiTestHarnessServiceImpl* impl) {
425 return std::make_unique<ImGuiTestHarnessServiceGrpc>(impl);
426}
427
428// ============================================================================
429// ImGuiTestHarnessServiceImpl - RPC Handlers
430// ============================================================================
431
432absl::Status ImGuiTestHarnessServiceImpl::Ping(const PingRequest* request,
433 PingResponse* response) {
434 // Echo back the message with "Pong: " prefix
435 response->set_message(absl::StrFormat("Pong: %s", request->message()));
436
437 // Add current timestamp
438 auto now = std::chrono::system_clock::now();
439 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
440 now.time_since_epoch());
441 response->set_timestamp_ms(ms.count());
442
443 // Add YAZE version
444 response->set_yaze_version(YAZE_VERSION_STRING);
445
446 return absl::OkStatus();
447}
448
449absl::Status ImGuiTestHarnessServiceImpl::Click(const ClickRequest* request,
450 ClickResponse* response) {
451 auto start = std::chrono::steady_clock::now();
452
453 TestRecorder::RecordedStep recorded_step;
454 recorded_step.type = TestRecorder::ActionType::kClick;
455 if (request) {
456 recorded_step.target = request->target();
457 recorded_step.click_type = ClickTypeToString(request->type());
458 }
459
460 auto finalize = [&](const absl::Status& status) {
461 recorded_step.success = response->success();
462 recorded_step.message = response->message();
463 recorded_step.execution_time_ms = response->execution_time_ms();
464 recorded_step.test_id = response->test_id();
465 MaybeRecordStep(&test_recorder_, recorded_step);
466 return status;
467 };
468
469 if (!test_manager_) {
470 response->set_success(false);
471 response->set_message("TestManager not available");
472 response->set_execution_time_ms(0);
473 return finalize(absl::FailedPreconditionError("TestManager not available"));
474 }
475
476 const std::string test_id = test_manager_->RegisterHarnessTest(
477 absl::StrFormat("Click: %s", request->target()), "grpc");
478 response->set_test_id(test_id);
479 recorded_step.test_id = test_id;
480 test_manager_->AppendHarnessTestLog(
481 test_id, absl::StrCat("Queued click request: ", request->target()));
482
483#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
484 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
485 if (!engine) {
486 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
487 std::chrono::steady_clock::now() - start);
488 std::string message = "ImGuiTestEngine not initialized";
489 response->set_success(false);
490 response->set_message(message);
491 response->set_execution_time_ms(elapsed.count());
492 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
493 message);
494 return finalize(absl::OkStatus());
495 }
496
497 std::string target = request->target();
498 size_t colon_pos = target.find(':');
499 if (colon_pos == std::string::npos) {
500 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
501 std::chrono::steady_clock::now() - start);
502 std::string message =
503 "Invalid target format. Use 'type:label' (e.g. 'button:Open ROM')";
504 response->set_success(false);
505 response->set_message(message);
506 response->set_execution_time_ms(elapsed.count());
507 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
508 message);
509 test_manager_->AppendHarnessTestLog(test_id, message);
510 return finalize(absl::OkStatus());
511 }
512
513 std::string widget_type = target.substr(0, colon_pos);
514 std::string widget_label = target.substr(colon_pos + 1);
515
516 ImGuiMouseButton mouse_button = ImGuiMouseButton_Left;
517 switch (request->type()) {
518 case ClickRequest::CLICK_TYPE_UNSPECIFIED:
519 case ClickRequest::CLICK_TYPE_LEFT:
520 mouse_button = ImGuiMouseButton_Left;
521 break;
522 case ClickRequest::CLICK_TYPE_RIGHT:
523 mouse_button = ImGuiMouseButton_Right;
524 break;
525 case ClickRequest::CLICK_TYPE_MIDDLE:
526 mouse_button = ImGuiMouseButton_Middle;
527 break;
528 case ClickRequest::CLICK_TYPE_DOUBLE:
529 // handled below
530 break;
531 }
532
533 auto test_data = std::make_shared<DynamicTestData>();
534 TestManager* manager = test_manager_;
535 test_data->test_func = [manager, captured_id = test_id, widget_type,
536 widget_label, click_type = request->type(),
537 mouse_button](ImGuiTestContext* ctx) {
538 manager->MarkHarnessTestRunning(captured_id);
539 try {
540 if (click_type == ClickRequest::CLICK_TYPE_DOUBLE) {
541 ctx->ItemDoubleClick(widget_label.c_str());
542 } else {
543 ctx->ItemClick(widget_label.c_str(), mouse_button);
544 }
545 ctx->Yield();
546 const std::string success_message =
547 absl::StrFormat("Clicked %s '%s'", widget_type, widget_label);
548 manager->AppendHarnessTestLog(captured_id, success_message);
549 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kPassed,
550 success_message);
551 } catch (const std::exception& e) {
552 const std::string error_message =
553 absl::StrFormat("Click failed: %s", e.what());
554 manager->AppendHarnessTestLog(captured_id, error_message);
555 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
556 error_message);
557 }
558 };
559
560 std::string test_name = absl::StrFormat(
561 "grpc_click_%lld",
562 static_cast<long long>(
563 std::chrono::system_clock::now().time_since_epoch().count()));
564
565 ImGuiTest* test = IM_REGISTER_TEST(engine, "grpc", test_name.c_str());
566 test->TestFunc = RunDynamicTest;
567 test->UserData = test_data.get();
568
569 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
570 KeepDynamicTestData(test_data);
571
572 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
573 std::chrono::steady_clock::now() - start);
574 std::string message =
575 absl::StrFormat("Queued click on %s '%s'", widget_type, widget_label);
576 response->set_success(true);
577 response->set_message(message);
578 response->set_execution_time_ms(elapsed.count());
579 test_manager_->AppendHarnessTestLog(test_id, message);
580
581#else
582 std::string target = request->target();
583 size_t colon_pos = target.find(':');
584 if (colon_pos == std::string::npos) {
585 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
586 std::chrono::steady_clock::now() - start);
587 std::string message = "Invalid target format. Use 'type:label'";
588 response->set_success(false);
589 response->set_message(message);
590 response->set_execution_time_ms(elapsed.count());
591 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
592 message);
593 test_manager_->AppendHarnessTestLog(test_id, message);
594 return finalize(absl::OkStatus());
595 }
596
597 std::string widget_type = target.substr(0, colon_pos);
598 std::string widget_label = target.substr(colon_pos + 1);
599 std::string message =
600 absl::StrFormat("[STUB] Clicked %s '%s' (ImGuiTestEngine not available)",
601 widget_type, widget_label);
602
603 test_manager_->MarkHarnessTestRunning(test_id);
604 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kPassed,
605 message);
606 test_manager_->AppendHarnessTestLog(test_id, message);
607
608 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
609 std::chrono::steady_clock::now() - start);
610 response->set_success(true);
611 response->set_message(message);
612 response->set_execution_time_ms(elapsed.count());
613#endif
614
615 return finalize(absl::OkStatus());
616}
617
618absl::Status ImGuiTestHarnessServiceImpl::Type(const TypeRequest* request,
619 TypeResponse* response) {
620 auto start = std::chrono::steady_clock::now();
621
622 TestRecorder::RecordedStep recorded_step;
623 recorded_step.type = TestRecorder::ActionType::kType;
624 if (request) {
625 recorded_step.target = request->target();
626 recorded_step.text = request->text();
627 recorded_step.clear_first = request->clear_first();
628 }
629
630 auto finalize = [&](const absl::Status& status) {
631 recorded_step.success = response->success();
632 recorded_step.message = response->message();
633 recorded_step.execution_time_ms = response->execution_time_ms();
634 recorded_step.test_id = response->test_id();
635 MaybeRecordStep(&test_recorder_, recorded_step);
636 return status;
637 };
638
639 if (!test_manager_) {
640 response->set_success(false);
641 response->set_message("TestManager not available");
642 response->set_execution_time_ms(0);
643 return finalize(absl::FailedPreconditionError("TestManager not available"));
644 }
645
646 const std::string test_id = test_manager_->RegisterHarnessTest(
647 absl::StrFormat("Type: %s", request->target()), "grpc");
648 response->set_test_id(test_id);
649 recorded_step.test_id = test_id;
650 test_manager_->AppendHarnessTestLog(
651 test_id, absl::StrFormat("Queued type request: %s", request->target()));
652
653#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
654 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
655 if (!engine) {
656 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
657 std::chrono::steady_clock::now() - start);
658 std::string message = "ImGuiTestEngine not initialized";
659 response->set_success(false);
660 response->set_message(message);
661 response->set_execution_time_ms(elapsed.count());
662 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
663 message);
664 return finalize(absl::OkStatus());
665 }
666
667 std::string target = request->target();
668 size_t colon_pos = target.find(':');
669 if (colon_pos == std::string::npos) {
670 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
671 std::chrono::steady_clock::now() - start);
672 std::string message =
673 "Invalid target format. Use 'type:label' (e.g. 'input:Filename')";
674 response->set_success(false);
675 response->set_message(message);
676 response->set_execution_time_ms(elapsed.count());
677 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
678 message);
679 test_manager_->AppendHarnessTestLog(test_id, message);
680 return finalize(absl::OkStatus());
681 }
682
683 std::string widget_type = target.substr(0, colon_pos);
684 std::string widget_label = target.substr(colon_pos + 1);
685 std::string text = request->text();
686 bool clear_first = request->clear_first();
687
688 auto rpc_state = std::make_shared<RPCState<bool>>();
689 auto test_data = std::make_shared<DynamicTestData>();
690 TestManager* manager = test_manager_;
691 test_data->test_func = [manager, captured_id = test_id, widget_type,
692 widget_label, clear_first, text,
693 rpc_state](ImGuiTestContext* ctx) {
694 manager->MarkHarnessTestRunning(captured_id);
695 try {
696 ImGuiTestItemInfo item = ctx->ItemInfo(widget_label.c_str());
697 if (item.ID == 0) {
698 std::string error_message =
699 absl::StrFormat("Input field '%s' not found", widget_label);
700 manager->AppendHarnessTestLog(captured_id, error_message);
701 manager->MarkHarnessTestCompleted(
702 captured_id, HarnessTestStatus::kFailed, error_message);
703 rpc_state->SetResult(false, error_message);
704 return;
705 }
706
707 ctx->ItemClick(widget_label.c_str());
708 if (clear_first) {
709 ctx->KeyPress(ImGuiMod_Shortcut | ImGuiKey_A);
710 ctx->KeyPress(ImGuiKey_Delete);
711 }
712
713 ctx->ItemInputValue(widget_label.c_str(), text.c_str());
714
715 std::string success_message =
716 absl::StrFormat("Typed '%s' into %s '%s'%s", text, widget_type,
717 widget_label, clear_first ? " (cleared first)" : "");
718 manager->AppendHarnessTestLog(captured_id, success_message);
719 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kPassed,
720 success_message);
721 rpc_state->SetResult(true, success_message);
722 } catch (const std::exception& e) {
723 std::string error_message = absl::StrFormat("Type failed: %s", e.what());
724 manager->AppendHarnessTestLog(captured_id, error_message);
725 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
726 error_message);
727 rpc_state->SetResult(false, error_message);
728 }
729 };
730
731 std::string test_name = absl::StrFormat(
732 "grpc_type_%lld",
733 static_cast<long long>(
734 std::chrono::system_clock::now().time_since_epoch().count()));
735
736 ImGuiTest* test = IM_REGISTER_TEST(engine, "grpc", test_name.c_str());
737 test->TestFunc = RunDynamicTest;
738 test->UserData = test_data.get();
739
740 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
741 KeepDynamicTestData(test_data);
742
743 auto timeout = std::chrono::seconds(5);
744 auto wait_start = std::chrono::steady_clock::now();
745 while (!rpc_state->completed.load()) {
746 if (std::chrono::steady_clock::now() - wait_start > timeout) {
747 std::string error_message =
748 "Test timeout - input field not found or unresponsive";
749 manager->AppendHarnessTestLog(test_id, error_message);
750 manager->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kTimeout,
751 error_message);
752 rpc_state->SetResult(false, error_message);
753 break;
754 }
755 std::this_thread::sleep_for(std::chrono::milliseconds(100));
756 }
757
758 bool success = false;
759 std::string message;
760 rpc_state->GetResult(success, message);
761 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
762 std::chrono::steady_clock::now() - start);
763
764 response->set_success(success);
765 response->set_message(message);
766 response->set_execution_time_ms(elapsed.count());
767 if (!message.empty()) {
768 test_manager_->AppendHarnessTestLog(test_id, message);
769 }
770
771#else
772 test_manager_->MarkHarnessTestRunning(test_id);
773 std::string message = absl::StrFormat(
774 "[STUB] Typed '%s' into %s (ImGuiTestEngine not available)",
775 request->text(), request->target());
776 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kPassed,
777 message);
778 test_manager_->AppendHarnessTestLog(test_id, message);
779
780 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
781 std::chrono::steady_clock::now() - start);
782 response->set_success(true);
783 response->set_message(message);
784 response->set_execution_time_ms(elapsed.count());
785#endif
786
787 return finalize(absl::OkStatus());
788}
789
790absl::Status ImGuiTestHarnessServiceImpl::Wait(const WaitRequest* request,
791 WaitResponse* response) {
792 auto start = std::chrono::steady_clock::now();
793
794 TestRecorder::RecordedStep recorded_step;
795 recorded_step.type = TestRecorder::ActionType::kWait;
796 if (request) {
797 recorded_step.condition = request->condition();
798 recorded_step.timeout_ms = request->timeout_ms();
799 }
800
801 auto finalize = [&](const absl::Status& status) {
802 recorded_step.success = response->success();
803 recorded_step.message = response->message();
804 recorded_step.execution_time_ms = response->elapsed_ms();
805 recorded_step.test_id = response->test_id();
806 MaybeRecordStep(&test_recorder_, recorded_step);
807 return status;
808 };
809
810 if (!test_manager_) {
811 response->set_success(false);
812 response->set_message("TestManager not available");
813 response->set_elapsed_ms(0);
814 return finalize(absl::FailedPreconditionError("TestManager not available"));
815 }
816
817 const std::string test_id = test_manager_->RegisterHarnessTest(
818 absl::StrFormat("Wait: %s", request->condition()), "grpc");
819 response->set_test_id(test_id);
820 recorded_step.test_id = test_id;
821 test_manager_->AppendHarnessTestLog(
822 test_id,
823 absl::StrFormat("Queued wait condition: %s", request->condition()));
824
825#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
826 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
827 if (!engine) {
828 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
829 std::chrono::steady_clock::now() - start);
830 std::string message = "ImGuiTestEngine not initialized";
831 response->set_success(false);
832 response->set_message(message);
833 response->set_elapsed_ms(elapsed.count());
834 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
835 message);
836 return finalize(absl::OkStatus());
837 }
838
839 std::string condition = request->condition();
840 size_t colon_pos = condition.find(':');
841 if (colon_pos == std::string::npos) {
842 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
843 std::chrono::steady_clock::now() - start);
844 std::string message =
845 "Invalid condition format. Use 'type:target' (e.g. "
846 "'window_visible:Overworld Editor')";
847 response->set_success(false);
848 response->set_message(message);
849 response->set_elapsed_ms(elapsed.count());
850 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
851 message);
852 test_manager_->AppendHarnessTestLog(test_id, message);
853 return finalize(absl::OkStatus());
854 }
855
856 std::string condition_type = condition.substr(0, colon_pos);
857 std::string condition_target = condition.substr(colon_pos + 1);
858 int timeout_ms = request->timeout_ms() > 0 ? request->timeout_ms() : 5000;
859 int poll_interval_ms =
860 request->poll_interval_ms() > 0 ? request->poll_interval_ms() : 100;
861
862 auto test_data = std::make_shared<DynamicTestData>();
863 TestManager* manager = test_manager_;
864 test_data->test_func = [manager, captured_id = test_id, condition_type,
865 condition_target, timeout_ms,
866 poll_interval_ms](ImGuiTestContext* ctx) {
867 manager->MarkHarnessTestRunning(captured_id);
868 auto poll_start = std::chrono::steady_clock::now();
869 auto timeout = std::chrono::milliseconds(timeout_ms);
870
871 for (int i = 0; i < 10; ++i) {
872 ctx->Yield();
873 }
874
875 try {
876 while (std::chrono::steady_clock::now() - poll_start < timeout) {
877 bool current_state = false;
878
879 if (condition_type == "window_visible") {
880 ImGuiTestItemInfo window_info = ctx->WindowInfo(
881 condition_target.c_str(), ImGuiTestOpFlags_NoError);
882 current_state = (window_info.ID != 0);
883 } else if (condition_type == "element_visible") {
884 ImGuiTestItemInfo item =
885 ctx->ItemInfo(condition_target.c_str(), ImGuiTestOpFlags_NoError);
886 current_state = (item.ID != 0 && item.RectClipped.GetWidth() > 0 &&
887 item.RectClipped.GetHeight() > 0);
888 } else if (condition_type == "element_enabled") {
889 ImGuiTestItemInfo item =
890 ctx->ItemInfo(condition_target.c_str(), ImGuiTestOpFlags_NoError);
891 current_state =
892 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
893 } else {
894 std::string error_message =
895 absl::StrFormat("Unknown condition type: %s", condition_type);
896 manager->AppendHarnessTestLog(captured_id, error_message);
897 manager->MarkHarnessTestCompleted(
898 captured_id, HarnessTestStatus::kFailed, error_message);
899 return;
900 }
901
902 if (current_state) {
903 auto elapsed_ms =
904 std::chrono::duration_cast<std::chrono::milliseconds>(
905 std::chrono::steady_clock::now() - poll_start);
906 std::string success_message = absl::StrFormat(
907 "Condition '%s:%s' met after %lld ms", condition_type,
908 condition_target, static_cast<long long>(elapsed_ms.count()));
909 manager->AppendHarnessTestLog(captured_id, success_message);
910 manager->MarkHarnessTestCompleted(
911 captured_id, HarnessTestStatus::kPassed, success_message);
912 return;
913 }
914
915 std::this_thread::sleep_for(
916 std::chrono::milliseconds(poll_interval_ms));
917 ctx->Yield();
918 }
919
920 std::string timeout_message =
921 absl::StrFormat("Condition '%s:%s' not met after %d ms timeout",
922 condition_type, condition_target, timeout_ms);
923 manager->AppendHarnessTestLog(captured_id, timeout_message);
924 manager->MarkHarnessTestCompleted(
925 captured_id, HarnessTestStatus::kTimeout, timeout_message);
926 } catch (const std::exception& e) {
927 std::string error_message = absl::StrFormat("Wait failed: %s", e.what());
928 manager->AppendHarnessTestLog(captured_id, error_message);
929 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
930 error_message);
931 }
932 };
933
934 std::string test_name = absl::StrFormat(
935 "grpc_wait_%lld",
936 static_cast<long long>(
937 std::chrono::system_clock::now().time_since_epoch().count()));
938
939 ImGuiTest* test = IM_REGISTER_TEST(engine, "grpc", test_name.c_str());
940 test->TestFunc = RunDynamicTest;
941 test->UserData = test_data.get();
942
943 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
944
945 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
946 std::chrono::steady_clock::now() - start);
947 std::string message = absl::StrFormat("Queued wait for '%s:%s'",
948 condition_type, condition_target);
949 response->set_success(true);
950 response->set_message(message);
951 response->set_elapsed_ms(elapsed.count());
952 test_manager_->AppendHarnessTestLog(test_id, message);
953
954#else
955 test_manager_->MarkHarnessTestRunning(test_id);
956 std::string message = absl::StrFormat(
957 "[STUB] Condition '%s' met (ImGuiTestEngine not available)",
958 request->condition());
959 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kPassed,
960 message);
961 test_manager_->AppendHarnessTestLog(test_id, message);
962
963 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
964 std::chrono::steady_clock::now() - start);
965 response->set_success(true);
966 response->set_message(message);
967 response->set_elapsed_ms(elapsed.count());
968#endif
969
970 return finalize(absl::OkStatus());
971}
972
973absl::Status ImGuiTestHarnessServiceImpl::Assert(const AssertRequest* request,
974 AssertResponse* response) {
975 TestRecorder::RecordedStep recorded_step;
976 recorded_step.type = TestRecorder::ActionType::kAssert;
977 if (request) {
978 recorded_step.condition = request->condition();
979 }
980
981 auto finalize = [&](const absl::Status& status) {
982 recorded_step.success = response->success();
983 recorded_step.message = response->message();
984 recorded_step.expected_value = response->expected_value();
985 recorded_step.actual_value = response->actual_value();
986 recorded_step.test_id = response->test_id();
987 MaybeRecordStep(&test_recorder_, recorded_step);
988 return status;
989 };
990
991 if (!test_manager_) {
992 response->set_success(false);
993 response->set_message("TestManager not available");
994 response->set_actual_value("N/A");
995 response->set_expected_value("N/A");
996 return finalize(absl::FailedPreconditionError("TestManager not available"));
997 }
998
999 const std::string test_id = test_manager_->RegisterHarnessTest(
1000 absl::StrFormat("Assert: %s", request->condition()), "grpc");
1001 response->set_test_id(test_id);
1002 recorded_step.test_id = test_id;
1003 test_manager_->AppendHarnessTestLog(
1004 test_id, absl::StrFormat("Queued assertion: %s", request->condition()));
1005
1006#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1007 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
1008 if (!engine) {
1009 std::string message = "ImGuiTestEngine not initialized";
1010 response->set_success(false);
1011 response->set_message(message);
1012 response->set_actual_value("N/A");
1013 response->set_expected_value("N/A");
1014 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1015 message);
1016 return finalize(absl::OkStatus());
1017 }
1018
1019 std::string condition = request->condition();
1020 size_t colon_pos = condition.find(':');
1021 if (colon_pos == std::string::npos) {
1022 std::string message =
1023 "Invalid condition format. Use 'type:target' (e.g. 'visible:Main "
1024 "Window')";
1025 response->set_success(false);
1026 response->set_message(message);
1027 response->set_actual_value("N/A");
1028 response->set_expected_value("N/A");
1029 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1030 message);
1031 test_manager_->AppendHarnessTestLog(test_id, message);
1032 return finalize(absl::OkStatus());
1033 }
1034
1035 std::string assertion_type = condition.substr(0, colon_pos);
1036 std::string assertion_target = condition.substr(colon_pos + 1);
1037
1038 auto test_data = std::make_shared<DynamicTestData>();
1039 TestManager* manager = test_manager_;
1040 test_data->test_func = [manager, captured_id = test_id, assertion_type,
1041 assertion_target](ImGuiTestContext* ctx) {
1042 manager->MarkHarnessTestRunning(captured_id);
1043
1044 auto complete_with = [manager, captured_id](bool passed,
1045 const std::string& message,
1046 const std::string& actual,
1047 const std::string& expected,
1048 HarnessTestStatus status) {
1049 manager->AppendHarnessTestLog(captured_id, message);
1050 if (!actual.empty() || !expected.empty()) {
1051 manager->AppendHarnessTestLog(
1052 captured_id,
1053 absl::StrFormat("Actual: %s | Expected: %s", actual, expected));
1054 }
1055 manager->MarkHarnessTestCompleted(captured_id, status,
1056 passed ? "" : message);
1057 };
1058
1059 try {
1060 bool passed = false;
1061 std::string actual_value;
1062 std::string expected_value;
1063 std::string message;
1064
1065 if (assertion_type == "visible") {
1066 ImGuiTestItemInfo window_info =
1067 ctx->WindowInfo(assertion_target.c_str(), ImGuiTestOpFlags_NoError);
1068 bool is_visible = (window_info.ID != 0);
1069 passed = is_visible;
1070 actual_value = is_visible ? "visible" : "hidden";
1071 expected_value = "visible";
1072 message =
1073 passed ? absl::StrFormat("'%s' is visible", assertion_target)
1074 : absl::StrFormat("'%s' is not visible", assertion_target);
1075 } else if (assertion_type == "enabled") {
1076 ImGuiTestItemInfo item =
1077 ctx->ItemInfo(assertion_target.c_str(), ImGuiTestOpFlags_NoError);
1078 bool is_enabled =
1079 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
1080 passed = is_enabled;
1081 actual_value = is_enabled ? "enabled" : "disabled";
1082 expected_value = "enabled";
1083 message =
1084 passed ? absl::StrFormat("'%s' is enabled", assertion_target)
1085 : absl::StrFormat("'%s' is not enabled", assertion_target);
1086 } else if (assertion_type == "exists") {
1087 ImGuiTestItemInfo item =
1088 ctx->ItemInfo(assertion_target.c_str(), ImGuiTestOpFlags_NoError);
1089 bool exists = (item.ID != 0);
1090 passed = exists;
1091 actual_value = exists ? "exists" : "not found";
1092 expected_value = "exists";
1093 message = passed ? absl::StrFormat("'%s' exists", assertion_target)
1094 : absl::StrFormat("'%s' not found", assertion_target);
1095 } else if (assertion_type == "text_contains") {
1096 size_t second_colon = assertion_target.find(':');
1097 if (second_colon == std::string::npos) {
1098 std::string error_message =
1099 "text_contains requires format "
1100 "'text_contains:target:expected_text'";
1101 complete_with(false, error_message, "N/A", "N/A",
1102 HarnessTestStatus::kFailed);
1103 return;
1104 }
1105
1106 std::string input_target = assertion_target.substr(0, second_colon);
1107 std::string expected_text = assertion_target.substr(second_colon + 1);
1108
1109 ImGuiTestItemInfo item = ctx->ItemInfo(input_target.c_str());
1110 if (item.ID != 0) {
1111 std::string actual_text = "(text_retrieval_not_fully_implemented)";
1112 passed = actual_text.find(expected_text) != std::string::npos;
1113 actual_value = actual_text;
1114 expected_value = absl::StrFormat("contains '%s'", expected_text);
1115 message = passed ? absl::StrFormat("'%s' contains '%s'", input_target,
1116 expected_text)
1117 : absl::StrFormat(
1118 "'%s' does not contain '%s' (actual: '%s')",
1119 input_target, expected_text, actual_text);
1120 } else {
1121 passed = false;
1122 actual_value = "not found";
1123 expected_value = expected_text;
1124 message = absl::StrFormat("Input '%s' not found", input_target);
1125 }
1126 } else {
1127 std::string error_message =
1128 absl::StrFormat("Unknown assertion type: %s", assertion_type);
1129 complete_with(false, error_message, "N/A", "N/A",
1130 HarnessTestStatus::kFailed);
1131 return;
1132 }
1133
1134 complete_with(
1135 passed, message, actual_value, expected_value,
1136 passed ? HarnessTestStatus::kPassed : HarnessTestStatus::kFailed);
1137 } catch (const std::exception& e) {
1138 std::string error_message =
1139 absl::StrFormat("Assertion failed: %s", e.what());
1140 manager->AppendHarnessTestLog(captured_id, error_message);
1141 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
1142 error_message);
1143 }
1144 };
1145
1146 std::string test_name = absl::StrFormat(
1147 "grpc_assert_%lld",
1148 static_cast<long long>(
1149 std::chrono::system_clock::now().time_since_epoch().count()));
1150
1151 ImGuiTest* test = IM_REGISTER_TEST(engine, "grpc", test_name.c_str());
1152 test->TestFunc = RunDynamicTest;
1153 test->UserData = test_data.get();
1154
1155 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
1156
1157 response->set_success(true);
1158 std::string message = absl::StrFormat("Queued assertion for '%s:%s'",
1159 assertion_type, assertion_target);
1160 response->set_message(message);
1161 response->set_actual_value("(async)");
1162 response->set_expected_value("(async)");
1163 test_manager_->AppendHarnessTestLog(test_id, message);
1164
1165#else
1166 test_manager_->MarkHarnessTestRunning(test_id);
1167 std::string message = absl::StrFormat(
1168 "[STUB] Assertion '%s' passed (ImGuiTestEngine not available)",
1169 request->condition());
1170 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kPassed,
1171 message);
1172 test_manager_->AppendHarnessTestLog(test_id, message);
1173
1174 response->set_success(true);
1175 response->set_message(message);
1176 response->set_actual_value("(stub)");
1177 response->set_expected_value("(stub)");
1178#endif
1179
1180 return finalize(absl::OkStatus());
1181}
1182
1183absl::Status ImGuiTestHarnessServiceImpl::Screenshot(
1184 const ScreenshotRequest* request, ScreenshotResponse* response) {
1185 if (!response) {
1186 return absl::InvalidArgumentError("response cannot be null");
1187 }
1188
1189 const std::string requested_path =
1190 request ? request->output_path() : std::string();
1191
1192 // We must execute capture on the main thread to avoid Metal/OpenGL context errors.
1193 // Use Controller's request queue.
1194 struct State {
1195 std::atomic<bool> done{false};
1196 absl::StatusOr<ScreenshotArtifact> result =
1197 absl::UnknownError("Not captured");
1198 };
1199 auto state = std::make_shared<State>();
1200
1202 {.preferred_path = requested_path,
1203 .callback = [state](absl::StatusOr<ScreenshotArtifact> result) {
1204 state->result = std::move(result);
1205 state->done.store(true);
1206 }});
1207
1208 // Wait for main thread to process (timeout after 5s)
1209 auto start = std::chrono::steady_clock::now();
1210 while (!state->done.load()) {
1211 if (std::chrono::steady_clock::now() - start > std::chrono::seconds(5)) {
1212 return absl::DeadlineExceededError(
1213 "Timed out waiting for screenshot capture on main thread");
1214 }
1215 std::this_thread::sleep_for(std::chrono::milliseconds(10));
1216 }
1217
1218 if (!state->result.ok()) {
1219 response->set_success(false);
1220 response->set_message(std::string(state->result.status().message()));
1221 return state->result.status();
1222 }
1223
1224 const ScreenshotArtifact& artifact = *state->result;
1225 response->set_success(true);
1226 response->set_message(absl::StrFormat("Screenshot saved to %s (%dx%d)",
1227 artifact.file_path, artifact.width,
1228 artifact.height));
1229 response->set_file_path(artifact.file_path);
1230 response->set_file_size_bytes(artifact.file_size_bytes);
1231
1232 return absl::OkStatus();
1233}
1234
1235absl::Status ImGuiTestHarnessServiceImpl::GetTestStatus(
1236 const GetTestStatusRequest* request, GetTestStatusResponse* response) {
1237 if (!test_manager_) {
1238 return absl::FailedPreconditionError("TestManager not available");
1239 }
1240
1241 if (request->test_id().empty()) {
1242 return absl::InvalidArgumentError("test_id must be provided");
1243 }
1244
1245 auto execution_or =
1246 test_manager_->GetHarnessTestExecution(request->test_id());
1247 if (!execution_or.ok()) {
1248 response->set_status(GetTestStatusResponse::TEST_STATUS_UNSPECIFIED);
1249 response->set_error_message(std::string(execution_or.status().message()));
1250 return absl::OkStatus();
1251 }
1252
1253 const auto& execution = execution_or.value();
1254 response->set_status(ConvertHarnessStatus(execution.status));
1255 response->set_queued_at_ms(ToUnixMillisSafe(execution.queued_at));
1256 response->set_started_at_ms(ToUnixMillisSafe(execution.started_at));
1257 response->set_completed_at_ms(ToUnixMillisSafe(execution.completed_at));
1258 response->set_execution_time_ms(ClampDurationToInt32(execution.duration));
1259 if (!execution.error_message.empty()) {
1260 response->set_error_message(execution.error_message);
1261 } else {
1262 response->clear_error_message();
1263 }
1264
1265 response->clear_assertion_failures();
1266 for (const auto& failure : execution.assertion_failures) {
1267 response->add_assertion_failures(failure);
1268 }
1269
1270 return absl::OkStatus();
1271}
1272
1273absl::Status ImGuiTestHarnessServiceImpl::ListTests(
1274 const ListTestsRequest* request, ListTestsResponse* response) {
1275 if (!test_manager_) {
1276 return absl::FailedPreconditionError("TestManager not available");
1277 }
1278
1279 if (request->page_size() < 0) {
1280 return absl::InvalidArgumentError("page_size cannot be negative");
1281 }
1282
1283 int page_size = request->page_size() > 0 ? request->page_size() : 100;
1284 constexpr int kMaxPageSize = 500;
1285 if (page_size > kMaxPageSize) {
1286 page_size = kMaxPageSize;
1287 }
1288
1289 size_t start_index = 0;
1290 if (!request->page_token().empty()) {
1291 int64_t token_value = 0;
1292 if (!absl::SimpleAtoi(request->page_token(), &token_value) ||
1293 token_value < 0) {
1294 return absl::InvalidArgumentError("Invalid page_token");
1295 }
1296 start_index = static_cast<size_t>(token_value);
1297 }
1298
1299 auto summaries =
1300 test_manager_->ListHarnessTestSummaries(request->category_filter());
1301
1302 response->set_total_count(static_cast<int32_t>(summaries.size()));
1303
1304 if (start_index >= summaries.size()) {
1305 response->clear_tests();
1306 response->clear_next_page_token();
1307 return absl::OkStatus();
1308 }
1309
1310 size_t end_index =
1311 std::min(start_index + static_cast<size_t>(page_size), summaries.size());
1312
1313 for (size_t i = start_index; i < end_index; ++i) {
1314 const auto& summary = summaries[i];
1315 auto* test_info = response->add_tests();
1316 const auto& exec = summary.latest_execution;
1317
1318 test_info->set_test_id(exec.test_id);
1319 test_info->set_name(exec.name);
1320 test_info->set_category(exec.category);
1321
1322 int64_t last_run_ms = ToUnixMillisSafe(exec.completed_at);
1323 if (last_run_ms == 0) {
1324 last_run_ms = ToUnixMillisSafe(exec.started_at);
1325 }
1326 if (last_run_ms == 0) {
1327 last_run_ms = ToUnixMillisSafe(exec.queued_at);
1328 }
1329 test_info->set_last_run_timestamp_ms(last_run_ms);
1330
1331 test_info->set_total_runs(summary.total_runs);
1332 test_info->set_pass_count(summary.pass_count);
1333 test_info->set_fail_count(summary.fail_count);
1334
1335 int32_t average_duration_ms = 0;
1336 if (summary.total_runs > 0) {
1337 absl::Duration average_duration =
1338 summary.total_duration / summary.total_runs;
1339 average_duration_ms = ClampDurationToInt32(average_duration);
1340 }
1341 test_info->set_average_duration_ms(average_duration_ms);
1342 }
1343
1344 if (end_index < summaries.size()) {
1345 response->set_next_page_token(absl::StrCat(end_index));
1346 } else {
1347 response->clear_next_page_token();
1348 }
1349
1350 return absl::OkStatus();
1351}
1352
1353absl::Status ImGuiTestHarnessServiceImpl::GetTestResults(
1354 const GetTestResultsRequest* request, GetTestResultsResponse* response) {
1355 if (!test_manager_) {
1356 return absl::FailedPreconditionError("TestManager not available");
1357 }
1358
1359 if (request->test_id().empty()) {
1360 return absl::InvalidArgumentError("test_id must be provided");
1361 }
1362
1363 auto execution_or =
1364 test_manager_->GetHarnessTestExecution(request->test_id());
1365 if (!execution_or.ok()) {
1366 return execution_or.status();
1367 }
1368
1369 const auto& execution = execution_or.value();
1370 response->set_success(execution.status == HarnessTestStatus::kPassed);
1371 response->set_test_name(execution.name);
1372 response->set_category(execution.category);
1373
1374 int64_t executed_at_ms = ToUnixMillisSafe(execution.completed_at);
1375 if (executed_at_ms == 0) {
1376 executed_at_ms = ToUnixMillisSafe(execution.started_at);
1377 }
1378 if (executed_at_ms == 0) {
1379 executed_at_ms = ToUnixMillisSafe(execution.queued_at);
1380 }
1381 response->set_executed_at_ms(executed_at_ms);
1382 response->set_duration_ms(ClampDurationToInt32(execution.duration));
1383
1384 response->clear_assertions();
1385 if (!execution.assertion_failures.empty()) {
1386 for (const auto& failure : execution.assertion_failures) {
1387 auto* assertion = response->add_assertions();
1388 assertion->set_description(failure);
1389 assertion->set_passed(false);
1390 assertion->set_error_message(failure);
1391 }
1392 } else if (!execution.error_message.empty()) {
1393 auto* assertion = response->add_assertions();
1394 assertion->set_description("Execution error");
1395 assertion->set_passed(false);
1396 assertion->set_error_message(execution.error_message);
1397 }
1398
1399 if (request->include_logs()) {
1400 for (const auto& log_entry : execution.logs) {
1401 response->add_logs(log_entry);
1402 }
1403 }
1404
1405 auto* metrics_map = response->mutable_metrics();
1406 for (const auto& [key, value] : execution.metrics) {
1407 (*metrics_map)[key] = value;
1408 }
1409
1410 // IT-08b: Include failure diagnostics if available
1411 if (!execution.screenshot_path.empty()) {
1412 response->set_screenshot_path(execution.screenshot_path);
1413 response->set_screenshot_size_bytes(execution.screenshot_size_bytes);
1414 }
1415 if (!execution.failure_context.empty()) {
1416 response->set_failure_context(execution.failure_context);
1417 }
1418 if (!execution.widget_state.empty()) {
1419 response->set_widget_state(execution.widget_state);
1420 }
1421
1422 return absl::OkStatus();
1423}
1424
1425absl::Status ImGuiTestHarnessServiceImpl::DiscoverWidgets(
1426 const DiscoverWidgetsRequest* request, DiscoverWidgetsResponse* response) {
1427 if (!request) {
1428 return absl::InvalidArgumentError("request cannot be null");
1429 }
1430 if (!response) {
1431 return absl::InvalidArgumentError("response cannot be null");
1432 }
1433
1434 if (!test_manager_) {
1435 return absl::FailedPreconditionError("TestManager not available");
1436 }
1437
1438 widget_discovery_service_.CollectWidgets(/*ctx=*/nullptr, *request, response);
1439 return absl::OkStatus();
1440}
1441
1442absl::Status ImGuiTestHarnessServiceImpl::StartRecording(
1443 const StartRecordingRequest* request, StartRecordingResponse* response) {
1444 if (!request) {
1445 return absl::InvalidArgumentError("request cannot be null");
1446 }
1447 if (!response) {
1448 return absl::InvalidArgumentError("response cannot be null");
1449 }
1450
1451 TestRecorder::RecordingOptions options;
1452 options.output_path = request->output_path();
1453 options.session_name = request->session_name();
1454 options.description = request->description();
1455
1456 if (options.output_path.empty()) {
1457 response->set_success(false);
1458 response->set_message("output_path is required to start recording");
1459 return absl::InvalidArgumentError("output_path cannot be empty");
1460 }
1461
1462 absl::StatusOr<std::string> recording_id = test_recorder_.Start(options);
1463 if (!recording_id.ok()) {
1464 response->set_success(false);
1465 response->set_message(std::string(recording_id.status().message()));
1466 return recording_id.status();
1467 }
1468
1469 response->set_success(true);
1470 response->set_message("Recording started");
1471 response->set_recording_id(*recording_id);
1472 response->set_started_at_ms(absl::ToUnixMillis(absl::Now()));
1473 return absl::OkStatus();
1474}
1475
1476absl::Status ImGuiTestHarnessServiceImpl::StopRecording(
1477 const StopRecordingRequest* request, StopRecordingResponse* response) {
1478 if (!request) {
1479 return absl::InvalidArgumentError("request cannot be null");
1480 }
1481 if (!response) {
1482 return absl::InvalidArgumentError("response cannot be null");
1483 }
1484
1485 absl::StatusOr<TestRecorder::StopRecordingSummary> summary =
1486 test_recorder_.Stop(request->recording_id(), request->discard());
1487 if (!summary.ok()) {
1488 response->set_success(false);
1489 response->set_message(std::string(summary.status().message()));
1490 return summary.status();
1491 }
1492
1493 response->set_success(true);
1494 if (summary->saved) {
1495 response->set_message("Recording saved");
1496 } else {
1497 response->set_message("Recording discarded");
1498 }
1499 response->set_output_path(summary->output_path);
1500 response->set_step_count(summary->step_count);
1501 response->set_duration_ms(absl::ToInt64Milliseconds(summary->duration));
1502 return absl::OkStatus();
1503}
1504
1505absl::Status ImGuiTestHarnessServiceImpl::ReplayTest(
1506 const ReplayTestRequest* request, ReplayTestResponse* response) {
1507 if (!request) {
1508 return absl::InvalidArgumentError("request cannot be null");
1509 }
1510 if (!response) {
1511 return absl::InvalidArgumentError("response cannot be null");
1512 }
1513
1514 response->clear_logs();
1515 response->clear_assertions();
1516
1517 if (request->script_path().empty()) {
1518 response->set_success(false);
1519 response->set_message("script_path is required");
1520 return absl::InvalidArgumentError("script_path cannot be empty");
1521 }
1522
1523 absl::StatusOr<TestScript> script_or =
1524 TestScriptParser::ParseFromFile(request->script_path());
1525 if (!script_or.ok()) {
1526 response->set_success(false);
1527 response->set_message(std::string(script_or.status().message()));
1528 return script_or.status();
1529 }
1530 TestScript script = std::move(*script_or);
1531
1532 absl::flat_hash_map<std::string, std::string> overrides;
1533 for (const auto& entry : request->parameter_overrides()) {
1534 overrides[entry.first] = entry.second;
1535 }
1536
1537 response->set_replay_session_id(absl::StrFormat(
1538 "replay_%s",
1539 absl::FormatTime("%Y%m%dT%H%M%S", absl::Now(), absl::UTCTimeZone())));
1540
1541 auto suspension = test_recorder_.Suspend();
1542
1543 std::vector<std::string> logs;
1544 logs.reserve(script.steps.size() * 2 + 4);
1545
1546 bool overall_success = true;
1547 std::string overall_message = "Replay completed successfully";
1548 int steps_executed = 0;
1549 absl::Status overall_rpc_status = absl::OkStatus();
1550
1551 for (const auto& step : script.steps) {
1552 ++steps_executed;
1553 std::string action_label =
1554 absl::StrFormat("Step %d: %s", steps_executed, step.action);
1555 logs.push_back(action_label);
1556
1557 absl::Status status = absl::OkStatus();
1558 bool step_success = false;
1559 std::string step_message;
1560 HarnessTestExecution execution;
1561 bool have_execution = false;
1562
1563 if (step.action == "click") {
1564 ClickRequest sub_request;
1565 sub_request.set_target(ApplyOverrides(step.target, overrides));
1566 sub_request.set_type(ClickTypeFromString(step.click_type));
1567 ClickResponse sub_response;
1568 status = Click(&sub_request, &sub_response);
1569 step_success = sub_response.success();
1570 step_message = sub_response.message();
1571 if (status.ok() && !sub_response.test_id().empty()) {
1572 absl::Status wait_status = WaitForHarnessTestCompletion(
1573 test_manager_, sub_response.test_id(), &execution);
1574 if (wait_status.ok()) {
1575 have_execution = true;
1576 if (!execution.error_message.empty()) {
1577 step_message = execution.error_message;
1578 }
1579 } else {
1580 status = wait_status;
1581 step_success = false;
1582 step_message = std::string(wait_status.message());
1583 }
1584 }
1585 } else if (step.action == "type") {
1586 TypeRequest sub_request;
1587 sub_request.set_target(ApplyOverrides(step.target, overrides));
1588 sub_request.set_text(ApplyOverrides(step.text, overrides));
1589 sub_request.set_clear_first(step.clear_first);
1590 TypeResponse sub_response;
1591 status = Type(&sub_request, &sub_response);
1592 step_success = sub_response.success();
1593 step_message = sub_response.message();
1594 if (status.ok() && !sub_response.test_id().empty()) {
1595 absl::Status wait_status = WaitForHarnessTestCompletion(
1596 test_manager_, sub_response.test_id(), &execution);
1597 if (wait_status.ok()) {
1598 have_execution = true;
1599 if (!execution.error_message.empty()) {
1600 step_message = execution.error_message;
1601 }
1602 } else {
1603 status = wait_status;
1604 step_success = false;
1605 step_message = std::string(wait_status.message());
1606 }
1607 }
1608 } else if (step.action == "wait") {
1609 WaitRequest sub_request;
1610 sub_request.set_condition(ApplyOverrides(step.condition, overrides));
1611 if (step.timeout_ms > 0) {
1612 sub_request.set_timeout_ms(step.timeout_ms);
1613 }
1614 WaitResponse sub_response;
1615 status = Wait(&sub_request, &sub_response);
1616 step_success = sub_response.success();
1617 step_message = sub_response.message();
1618 if (status.ok() && !sub_response.test_id().empty()) {
1619 absl::Status wait_status = WaitForHarnessTestCompletion(
1620 test_manager_, sub_response.test_id(), &execution);
1621 if (wait_status.ok()) {
1622 have_execution = true;
1623 if (!execution.error_message.empty()) {
1624 step_message = execution.error_message;
1625 }
1626 } else {
1627 status = wait_status;
1628 step_success = false;
1629 step_message = std::string(wait_status.message());
1630 }
1631 }
1632 } else if (step.action == "assert") {
1633 AssertRequest sub_request;
1634 sub_request.set_condition(ApplyOverrides(step.condition, overrides));
1635 AssertResponse sub_response;
1636 status = Assert(&sub_request, &sub_response);
1637 step_success = sub_response.success();
1638 step_message = sub_response.message();
1639 if (status.ok() && !sub_response.test_id().empty()) {
1640 absl::Status wait_status = WaitForHarnessTestCompletion(
1641 test_manager_, sub_response.test_id(), &execution);
1642 if (wait_status.ok()) {
1643 have_execution = true;
1644 if (!execution.error_message.empty()) {
1645 step_message = execution.error_message;
1646 }
1647 } else {
1648 status = wait_status;
1649 step_success = false;
1650 step_message = std::string(wait_status.message());
1651 }
1652 }
1653 } else if (step.action == "screenshot") {
1654 ScreenshotRequest sub_request;
1655 sub_request.set_window_title(ApplyOverrides(step.target, overrides));
1656 if (!step.region.empty()) {
1657 sub_request.set_output_path(ApplyOverrides(step.region, overrides));
1658 }
1659 ScreenshotResponse sub_response;
1660 status = Screenshot(&sub_request, &sub_response);
1661 step_success = sub_response.success();
1662 step_message = sub_response.message();
1663 } else {
1664 status = absl::InvalidArgumentError(
1665 absl::StrFormat("Unsupported action '%s'", step.action));
1666 step_message =
1667 std::string(status.message().data(), status.message().size());
1668 }
1669
1670 auto* assertion = response->add_assertions();
1671 assertion->set_description(
1672 absl::StrFormat("Step %d (%s)", steps_executed, step.action));
1673
1674 if (!status.ok()) {
1675 assertion->set_passed(false);
1676 assertion->set_error_message(
1677 std::string(status.message().data(), status.message().size()));
1678 overall_success = false;
1679 overall_message = step_message;
1680 logs.push_back(absl::StrFormat(" Error: %s", status.message()));
1681 overall_rpc_status = status;
1682 break;
1683 }
1684
1685 bool expectations_met = (step_success == step.expect_success);
1686 std::string expectation_error;
1687
1688 if (!expectations_met) {
1689 expectation_error =
1690 absl::StrFormat("Expected success=%s but got %s",
1691 step.expect_success ? "true" : "false",
1692 step_success ? "true" : "false");
1693 }
1694
1695 if (!step.expect_status.empty()) {
1696 HarnessTestStatus expected_status =
1697 ::yaze::test::HarnessStatusFromString(step.expect_status);
1698 if (!have_execution) {
1699 expectations_met = false;
1700 if (!expectation_error.empty()) {
1701 expectation_error.append("; ");
1702 }
1703 expectation_error.append("No execution details available");
1704 } else if (expected_status != HarnessTestStatus::kUnspecified &&
1705 execution.status != expected_status) {
1706 expectations_met = false;
1707 if (!expectation_error.empty()) {
1708 expectation_error.append("; ");
1709 }
1710 expectation_error.append(absl::StrFormat(
1711 "Expected status %s but observed %s", step.expect_status,
1712 ::yaze::test::HarnessStatusToString(execution.status)));
1713 }
1714 if (have_execution) {
1715 assertion->set_actual_value(
1716 ::yaze::test::HarnessStatusToString(execution.status));
1717 assertion->set_expected_value(step.expect_status);
1718 }
1719 }
1720
1721 if (!step.expect_message.empty()) {
1722 std::string actual_message = step_message;
1723 if (have_execution && !execution.error_message.empty()) {
1724 actual_message = execution.error_message;
1725 }
1726 if (actual_message.find(step.expect_message) == std::string::npos) {
1727 expectations_met = false;
1728 if (!expectation_error.empty()) {
1729 expectation_error.append("; ");
1730 }
1731 expectation_error.append(
1732 absl::StrFormat("Expected message containing '%s' but got '%s'",
1733 step.expect_message, actual_message));
1734 }
1735 }
1736
1737 if (!expectations_met) {
1738 assertion->set_passed(false);
1739 assertion->set_error_message(expectation_error);
1740 overall_success = false;
1741 overall_message = expectation_error;
1742 logs.push_back(
1743 absl::StrFormat(" Failed expectations: %s", expectation_error));
1744 if (request->ci_mode()) {
1745 break;
1746 }
1747 } else {
1748 assertion->set_passed(true);
1749 logs.push_back(absl::StrFormat(" Result: %s", step_message));
1750 }
1751
1752 if (have_execution && !execution.assertion_failures.empty()) {
1753 for (const auto& failure : execution.assertion_failures) {
1754 logs.push_back(absl::StrFormat(" Assertion failure: %s", failure));
1755 }
1756 }
1757
1758 if (!overall_success && request->ci_mode()) {
1759 break;
1760 }
1761 }
1762
1763 response->set_steps_executed(steps_executed);
1764 response->set_success(overall_success);
1765 response->set_message(overall_message);
1766 for (const auto& log_entry : logs) {
1767 response->add_logs(log_entry);
1768 }
1769
1770 return overall_rpc_status;
1771}
1772
1773// ============================================================================
1774// ImGuiTestHarnessServer - Server Lifecycle
1775// ============================================================================
1776
1777ImGuiTestHarnessServer& ImGuiTestHarnessServer::Instance() {
1778 static ImGuiTestHarnessServer* instance = new ImGuiTestHarnessServer();
1779 return *instance;
1780}
1781
1782ImGuiTestHarnessServer::~ImGuiTestHarnessServer() {
1783 Shutdown();
1784}
1785
1786absl::Status ImGuiTestHarnessServer::Start(int port,
1787 TestManager* test_manager) {
1788 if (server_) {
1789 return absl::FailedPreconditionError("Server already running");
1790 }
1791
1792 if (!test_manager) {
1793 return absl::InvalidArgumentError("TestManager cannot be null");
1794 }
1795
1796 // Create the service implementation with TestManager reference
1797 service_ = std::make_unique<ImGuiTestHarnessServiceImpl>(test_manager);
1798
1799 // Create the gRPC service wrapper (store as member to prevent it from going
1800 // out of scope)
1801 grpc_service_ = std::make_unique<ImGuiTestHarnessServiceGrpc>(service_.get());
1802
1803 std::string server_address = absl::StrFormat("0.0.0.0:%d", port);
1804
1805 grpc::ServerBuilder builder;
1806
1807 // Listen on all interfaces (use 0.0.0.0 to avoid IPv6/IPv4 binding conflicts)
1808 builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
1809
1810 // Register service
1811 builder.RegisterService(grpc_service_.get());
1812
1813 // Build and start
1814 server_ = builder.BuildAndStart();
1815
1816 if (!server_) {
1817 return absl::InternalError(
1818 absl::StrFormat("Failed to start gRPC server on %s", server_address));
1819 }
1820
1821 port_ = port;
1822
1823 std::cout << "✓ ImGuiTestHarness gRPC server listening on " << server_address
1824 << " (with TestManager integration)\n";
1825 std::cout << " Use 'grpcurl -plaintext -d '{\"message\":\"test\"}' "
1826 << server_address << " yaze.test.ImGuiTestHarness/Ping' to test\n";
1827
1828 return absl::OkStatus();
1829}
1830
1831void ImGuiTestHarnessServer::Shutdown() {
1832 if (server_) {
1833 std::cout << "⏹ Shutting down ImGuiTestHarness gRPC server...\n";
1834 server_->Shutdown();
1835 server_.reset();
1836 service_.reset();
1837 port_ = 0;
1838 std::cout << "✓ ImGuiTestHarness gRPC server stopped\n";
1839 }
1840}
1841
1842} // namespace test
1843} // namespace yaze
1844
1845#endif // YAZE_WITH_GRPC
Controller * GetController()
Definition application.h:73
static Application & Instance()
void RequestScreenshot(const ScreenshotRequest &request)
static absl::StatusOr< TestScript > ParseFromFile(const std::string &path)
#define YAZE_VERSION_STRING
Definition yaze.h:43
SDL2/SDL3 compatibility layer.
Yet Another Zelda3 Editor (YAZE) - Public C API.