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