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
5#include <SDL.h>
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/container/flat_hash_map.h"
33#include "absl/base/thread_annotations.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"
42#include "protos/imgui_test_harness.grpc.pb.h"
43#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_engine.h"
51#include "imgui_test_engine/imgui_te_context.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()));
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(
480 absl::FailedPreconditionError("TestManager not available"));
481 }
482
483 const std::string test_id = test_manager_->RegisterHarnessTest(
484 absl::StrFormat("Click: %s", request->target()), "grpc");
485 response->set_test_id(test_id);
486 recorded_step.test_id = test_id;
487 test_manager_->AppendHarnessTestLog(
488 test_id, absl::StrCat("Queued click request: ", request->target()));
489
490#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
491 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
492 if (!engine) {
493 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
494 std::chrono::steady_clock::now() - start);
495 std::string message = "ImGuiTestEngine not initialized";
496 response->set_success(false);
497 response->set_message(message);
498 response->set_execution_time_ms(elapsed.count());
499 test_manager_->MarkHarnessTestCompleted(
500 test_id, HarnessTestStatus::kFailed, message);
501 return finalize(absl::OkStatus());
502 }
503
504 std::string target = request->target();
505 size_t colon_pos = target.find(':');
506 if (colon_pos == std::string::npos) {
507 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
508 std::chrono::steady_clock::now() - start);
509 std::string message =
510 "Invalid target format. Use 'type:label' (e.g. 'button:Open ROM')";
511 response->set_success(false);
512 response->set_message(message);
513 response->set_execution_time_ms(elapsed.count());
514 test_manager_->MarkHarnessTestCompleted(
515 test_id, HarnessTestStatus::kFailed, message);
516 test_manager_->AppendHarnessTestLog(test_id, message);
517 return finalize(absl::OkStatus());
518 }
519
520 std::string widget_type = target.substr(0, colon_pos);
521 std::string widget_label = target.substr(colon_pos + 1);
522
523 ImGuiMouseButton mouse_button = ImGuiMouseButton_Left;
524 switch (request->type()) {
525 case ClickRequest::CLICK_TYPE_UNSPECIFIED:
526 case ClickRequest::CLICK_TYPE_LEFT:
527 mouse_button = ImGuiMouseButton_Left;
528 break;
529 case ClickRequest::CLICK_TYPE_RIGHT:
530 mouse_button = ImGuiMouseButton_Right;
531 break;
532 case ClickRequest::CLICK_TYPE_MIDDLE:
533 mouse_button = ImGuiMouseButton_Middle;
534 break;
535 case ClickRequest::CLICK_TYPE_DOUBLE:
536 // handled below
537 break;
538 }
539
540 auto test_data = std::make_shared<DynamicTestData>();
541 TestManager* manager = test_manager_;
542 test_data->test_func = [manager, captured_id = test_id, widget_type,
543 widget_label, click_type = request->type(),
544 mouse_button](ImGuiTestContext* ctx) {
545 manager->MarkHarnessTestRunning(captured_id);
546 try {
547 if (click_type == ClickRequest::CLICK_TYPE_DOUBLE) {
548 ctx->ItemDoubleClick(widget_label.c_str());
549 } else {
550 ctx->ItemClick(widget_label.c_str(), mouse_button);
551 }
552 ctx->Yield();
553 const std::string success_message =
554 absl::StrFormat("Clicked %s '%s'", widget_type, widget_label);
555 manager->AppendHarnessTestLog(captured_id, success_message);
556 manager->MarkHarnessTestCompleted(captured_id,
557 HarnessTestStatus::kPassed,
558 success_message);
559 } catch (const std::exception& e) {
560 const std::string error_message =
561 absl::StrFormat("Click failed: %s", e.what());
562 manager->AppendHarnessTestLog(captured_id, error_message);
563 manager->MarkHarnessTestCompleted(captured_id,
564 HarnessTestStatus::kFailed,
565 error_message);
566 }
567 };
568
569 std::string test_name = absl::StrFormat(
570 "grpc_click_%lld",
571 static_cast<long long>(
572 std::chrono::system_clock::now().time_since_epoch().count()));
573
574 ImGuiTest* test = IM_REGISTER_TEST(engine, "grpc", test_name.c_str());
575 test->TestFunc = RunDynamicTest;
576 test->UserData = test_data.get();
577
578 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
579 KeepDynamicTestData(test_data);
580
581 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
582 std::chrono::steady_clock::now() - start);
583 std::string message =
584 absl::StrFormat("Queued click on %s '%s'", widget_type, widget_label);
585 response->set_success(true);
586 response->set_message(message);
587 response->set_execution_time_ms(elapsed.count());
588 test_manager_->AppendHarnessTestLog(test_id, message);
589
590#else
591 std::string target = request->target();
592 size_t colon_pos = target.find(':');
593 if (colon_pos == std::string::npos) {
594 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
595 std::chrono::steady_clock::now() - start);
596 std::string message = "Invalid target format. Use 'type:label'";
597 response->set_success(false);
598 response->set_message(message);
599 response->set_execution_time_ms(elapsed.count());
600 test_manager_->MarkHarnessTestCompleted(test_id,
601 HarnessTestStatus::kFailed,
602 message);
603 test_manager_->AppendHarnessTestLog(test_id, message);
604 return finalize(absl::OkStatus());
605 }
606
607 std::string widget_type = target.substr(0, colon_pos);
608 std::string widget_label = target.substr(colon_pos + 1);
609 std::string message = absl::StrFormat(
610 "[STUB] Clicked %s '%s' (ImGuiTestEngine not available)",
611 widget_type, widget_label);
612
613 test_manager_->MarkHarnessTestRunning(test_id);
614 test_manager_->MarkHarnessTestCompleted(test_id,
615 HarnessTestStatus::kPassed,
616 message);
617 test_manager_->AppendHarnessTestLog(test_id, message);
618
619 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
620 std::chrono::steady_clock::now() - start);
621 response->set_success(true);
622 response->set_message(message);
623 response->set_execution_time_ms(elapsed.count());
624#endif
625
626 return finalize(absl::OkStatus());
627}
628
629absl::Status ImGuiTestHarnessServiceImpl::Type(const TypeRequest* request,
630 TypeResponse* response) {
631 auto start = std::chrono::steady_clock::now();
632
633 TestRecorder::RecordedStep recorded_step;
634 recorded_step.type = TestRecorder::ActionType::kType;
635 if (request) {
636 recorded_step.target = request->target();
637 recorded_step.text = request->text();
638 recorded_step.clear_first = request->clear_first();
639 }
640
641 auto finalize = [&](const absl::Status& status) {
642 recorded_step.success = response->success();
643 recorded_step.message = response->message();
644 recorded_step.execution_time_ms = response->execution_time_ms();
645 recorded_step.test_id = response->test_id();
646 MaybeRecordStep(&test_recorder_, recorded_step);
647 return status;
648 };
649
650 if (!test_manager_) {
651 response->set_success(false);
652 response->set_message("TestManager not available");
653 response->set_execution_time_ms(0);
654 return finalize(
655 absl::FailedPreconditionError("TestManager not available"));
656 }
657
658 const std::string test_id = test_manager_->RegisterHarnessTest(
659 absl::StrFormat("Type: %s", request->target()), "grpc");
660 response->set_test_id(test_id);
661 recorded_step.test_id = test_id;
662 test_manager_->AppendHarnessTestLog(
663 test_id, absl::StrFormat("Queued type request: %s", request->target()));
664
665#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
666 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
667 if (!engine) {
668 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
669 std::chrono::steady_clock::now() - start);
670 std::string message = "ImGuiTestEngine not initialized";
671 response->set_success(false);
672 response->set_message(message);
673 response->set_execution_time_ms(elapsed.count());
674 test_manager_->MarkHarnessTestCompleted(
675 test_id, HarnessTestStatus::kFailed, message);
676 return finalize(absl::OkStatus());
677 }
678
679 std::string target = request->target();
680 size_t colon_pos = target.find(':');
681 if (colon_pos == std::string::npos) {
682 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
683 std::chrono::steady_clock::now() - start);
684 std::string message =
685 "Invalid target format. Use 'type:label' (e.g. 'input:Filename')";
686 response->set_success(false);
687 response->set_message(message);
688 response->set_execution_time_ms(elapsed.count());
689 test_manager_->MarkHarnessTestCompleted(
690 test_id, HarnessTestStatus::kFailed, message);
691 test_manager_->AppendHarnessTestLog(test_id, message);
692 return finalize(absl::OkStatus());
693 }
694
695 std::string widget_type = target.substr(0, colon_pos);
696 std::string widget_label = target.substr(colon_pos + 1);
697 std::string text = request->text();
698 bool clear_first = request->clear_first();
699
700 auto rpc_state = std::make_shared<RPCState<bool>>();
701 auto test_data = std::make_shared<DynamicTestData>();
702 TestManager* manager = test_manager_;
703 test_data->test_func = [manager, captured_id = test_id, widget_type,
704 widget_label, clear_first, text, rpc_state](
705 ImGuiTestContext* ctx) {
706 manager->MarkHarnessTestRunning(captured_id);
707 try {
708 ImGuiTestItemInfo item = ctx->ItemInfo(widget_label.c_str());
709 if (item.ID == 0) {
710 std::string error_message =
711 absl::StrFormat("Input field '%s' not found", widget_label);
712 manager->AppendHarnessTestLog(captured_id, error_message);
713 manager->MarkHarnessTestCompleted(captured_id,
714 HarnessTestStatus::kFailed,
715 error_message);
716 rpc_state->SetResult(false, error_message);
717 return;
718 }
719
720 ctx->ItemClick(widget_label.c_str());
721 if (clear_first) {
722 ctx->KeyPress(ImGuiMod_Shortcut | ImGuiKey_A);
723 ctx->KeyPress(ImGuiKey_Delete);
724 }
725
726 ctx->ItemInputValue(widget_label.c_str(), text.c_str());
727
728 std::string success_message = absl::StrFormat(
729 "Typed '%s' into %s '%s'%s", text, widget_type, widget_label,
730 clear_first ? " (cleared first)" : "");
731 manager->AppendHarnessTestLog(captured_id, success_message);
732 manager->MarkHarnessTestCompleted(captured_id,
733 HarnessTestStatus::kPassed,
734 success_message);
735 rpc_state->SetResult(true, success_message);
736 } catch (const std::exception& e) {
737 std::string error_message =
738 absl::StrFormat("Type failed: %s", e.what());
739 manager->AppendHarnessTestLog(captured_id, error_message);
740 manager->MarkHarnessTestCompleted(captured_id,
741 HarnessTestStatus::kFailed,
742 error_message);
743 rpc_state->SetResult(false, error_message);
744 }
745 };
746
747 std::string test_name = absl::StrFormat(
748 "grpc_type_%lld",
749 static_cast<long long>(
750 std::chrono::system_clock::now().time_since_epoch().count()));
751
752 ImGuiTest* test = IM_REGISTER_TEST(engine, "grpc", test_name.c_str());
753 test->TestFunc = RunDynamicTest;
754 test->UserData = test_data.get();
755
756 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
757 KeepDynamicTestData(test_data);
758
759 auto timeout = std::chrono::seconds(5);
760 auto wait_start = std::chrono::steady_clock::now();
761 while (!rpc_state->completed.load()) {
762 if (std::chrono::steady_clock::now() - wait_start > timeout) {
763 std::string error_message =
764 "Test timeout - input field not found or unresponsive";
765 manager->AppendHarnessTestLog(test_id, error_message);
766 manager->MarkHarnessTestCompleted(
767 test_id, HarnessTestStatus::kTimeout, error_message);
768 rpc_state->SetResult(false, error_message);
769 break;
770 }
771 std::this_thread::sleep_for(std::chrono::milliseconds(100));
772 }
773
774 bool success = false;
775 std::string message;
776 rpc_state->GetResult(success, message);
777 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
778 std::chrono::steady_clock::now() - start);
779
780 response->set_success(success);
781 response->set_message(message);
782 response->set_execution_time_ms(elapsed.count());
783 if (!message.empty()) {
784 test_manager_->AppendHarnessTestLog(test_id, message);
785 }
786
787#else
788 test_manager_->MarkHarnessTestRunning(test_id);
789 std::string message = absl::StrFormat(
790 "[STUB] Typed '%s' into %s (ImGuiTestEngine not available)",
791 request->text(), request->target());
792 test_manager_->MarkHarnessTestCompleted(test_id,
793 HarnessTestStatus::kPassed,
794 message);
795 test_manager_->AppendHarnessTestLog(test_id, message);
796
797 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
798 std::chrono::steady_clock::now() - start);
799 response->set_success(true);
800 response->set_message(message);
801 response->set_execution_time_ms(elapsed.count());
802#endif
803
804 return finalize(absl::OkStatus());
805}
806
807absl::Status ImGuiTestHarnessServiceImpl::Wait(const WaitRequest* request,
808 WaitResponse* response) {
809 auto start = std::chrono::steady_clock::now();
810
811 TestRecorder::RecordedStep recorded_step;
812 recorded_step.type = TestRecorder::ActionType::kWait;
813 if (request) {
814 recorded_step.condition = request->condition();
815 recorded_step.timeout_ms = request->timeout_ms();
816 }
817
818 auto finalize = [&](const absl::Status& status) {
819 recorded_step.success = response->success();
820 recorded_step.message = response->message();
821 recorded_step.execution_time_ms = response->elapsed_ms();
822 recorded_step.test_id = response->test_id();
823 MaybeRecordStep(&test_recorder_, recorded_step);
824 return status;
825 };
826
827 if (!test_manager_) {
828 response->set_success(false);
829 response->set_message("TestManager not available");
830 response->set_elapsed_ms(0);
831 return finalize(
832 absl::FailedPreconditionError("TestManager not available"));
833 }
834
835 const std::string test_id = test_manager_->RegisterHarnessTest(
836 absl::StrFormat("Wait: %s", request->condition()), "grpc");
837 response->set_test_id(test_id);
838 recorded_step.test_id = test_id;
839 test_manager_->AppendHarnessTestLog(
840 test_id, absl::StrFormat("Queued wait condition: %s",
841 request->condition()));
842
843#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
844 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
845 if (!engine) {
846 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
847 std::chrono::steady_clock::now() - start);
848 std::string message = "ImGuiTestEngine not initialized";
849 response->set_success(false);
850 response->set_message(message);
851 response->set_elapsed_ms(elapsed.count());
852 test_manager_->MarkHarnessTestCompleted(
853 test_id, HarnessTestStatus::kFailed, message);
854 return finalize(absl::OkStatus());
855 }
856
857 std::string condition = request->condition();
858 size_t colon_pos = condition.find(':');
859 if (colon_pos == std::string::npos) {
860 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
861 std::chrono::steady_clock::now() - start);
862 std::string message =
863 "Invalid condition format. Use 'type:target' (e.g. 'window_visible:Overworld Editor')";
864 response->set_success(false);
865 response->set_message(message);
866 response->set_elapsed_ms(elapsed.count());
867 test_manager_->MarkHarnessTestCompleted(
868 test_id, HarnessTestStatus::kFailed, message);
869 test_manager_->AppendHarnessTestLog(test_id, message);
870 return finalize(absl::OkStatus());
871 }
872
873 std::string condition_type = condition.substr(0, colon_pos);
874 std::string condition_target = condition.substr(colon_pos + 1);
875 int timeout_ms = request->timeout_ms() > 0 ? request->timeout_ms() : 5000;
876 int poll_interval_ms = request->poll_interval_ms() > 0
877 ? request->poll_interval_ms()
878 : 100;
879
880 auto test_data = std::make_shared<DynamicTestData>();
881 TestManager* manager = test_manager_;
882 test_data->test_func = [manager, captured_id = test_id, condition_type,
883 condition_target, timeout_ms, poll_interval_ms](
884 ImGuiTestContext* ctx) {
885 manager->MarkHarnessTestRunning(captured_id);
886 auto poll_start = std::chrono::steady_clock::now();
887 auto timeout = std::chrono::milliseconds(timeout_ms);
888
889 for (int i = 0; i < 10; ++i) {
890 ctx->Yield();
891 }
892
893 try {
894 while (std::chrono::steady_clock::now() - poll_start < timeout) {
895 bool current_state = false;
896
897 if (condition_type == "window_visible") {
898 ImGuiTestItemInfo window_info = ctx->WindowInfo(
899 condition_target.c_str(), ImGuiTestOpFlags_NoError);
900 current_state = (window_info.ID != 0);
901 } else if (condition_type == "element_visible") {
902 ImGuiTestItemInfo item = ctx->ItemInfo(
903 condition_target.c_str(), ImGuiTestOpFlags_NoError);
904 current_state = (item.ID != 0 && item.RectClipped.GetWidth() > 0 &&
905 item.RectClipped.GetHeight() > 0);
906 } else if (condition_type == "element_enabled") {
907 ImGuiTestItemInfo item = ctx->ItemInfo(
908 condition_target.c_str(), ImGuiTestOpFlags_NoError);
909 current_state =
910 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
911 } else {
912 std::string error_message =
913 absl::StrFormat("Unknown condition type: %s", condition_type);
914 manager->AppendHarnessTestLog(captured_id, error_message);
915 manager->MarkHarnessTestCompleted(captured_id,
916 HarnessTestStatus::kFailed,
917 error_message);
918 return;
919 }
920
921 if (current_state) {
922 auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
923 std::chrono::steady_clock::now() - poll_start);
924 std::string success_message = absl::StrFormat(
925 "Condition '%s:%s' met after %lld ms", condition_type,
926 condition_target, static_cast<long long>(elapsed_ms.count()));
927 manager->AppendHarnessTestLog(captured_id, success_message);
928 manager->MarkHarnessTestCompleted(captured_id,
929 HarnessTestStatus::kPassed,
930 success_message);
931 return;
932 }
933
934 std::this_thread::sleep_for(
935 std::chrono::milliseconds(poll_interval_ms));
936 ctx->Yield();
937 }
938
939 std::string timeout_message = absl::StrFormat(
940 "Condition '%s:%s' not met after %d ms timeout", condition_type,
941 condition_target, timeout_ms);
942 manager->AppendHarnessTestLog(captured_id, timeout_message);
943 manager->MarkHarnessTestCompleted(captured_id,
944 HarnessTestStatus::kTimeout,
945 timeout_message);
946 } catch (const std::exception& e) {
947 std::string error_message =
948 absl::StrFormat("Wait failed: %s", e.what());
949 manager->AppendHarnessTestLog(captured_id, error_message);
950 manager->MarkHarnessTestCompleted(captured_id,
951 HarnessTestStatus::kFailed,
952 error_message);
953 }
954 };
955
956 std::string test_name = absl::StrFormat(
957 "grpc_wait_%lld",
958 static_cast<long long>(
959 std::chrono::system_clock::now().time_since_epoch().count()));
960
961 ImGuiTest* test = IM_REGISTER_TEST(engine, "grpc", test_name.c_str());
962 test->TestFunc = RunDynamicTest;
963 test->UserData = test_data.get();
964
965 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
966
967 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
968 std::chrono::steady_clock::now() - start);
969 std::string message =
970 absl::StrFormat("Queued wait for '%s:%s'", condition_type,
971 condition_target);
972 response->set_success(true);
973 response->set_message(message);
974 response->set_elapsed_ms(elapsed.count());
975 test_manager_->AppendHarnessTestLog(test_id, message);
976
977#else
978 test_manager_->MarkHarnessTestRunning(test_id);
979 std::string message = absl::StrFormat(
980 "[STUB] Condition '%s' met (ImGuiTestEngine not available)",
981 request->condition());
982 test_manager_->MarkHarnessTestCompleted(test_id,
983 HarnessTestStatus::kPassed,
984 message);
985 test_manager_->AppendHarnessTestLog(test_id, message);
986
987 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
988 std::chrono::steady_clock::now() - start);
989 response->set_success(true);
990 response->set_message(message);
991 response->set_elapsed_ms(elapsed.count());
992#endif
993
994 return finalize(absl::OkStatus());
995}
996
997absl::Status ImGuiTestHarnessServiceImpl::Assert(const AssertRequest* request,
998 AssertResponse* response) {
999 TestRecorder::RecordedStep recorded_step;
1000 recorded_step.type = TestRecorder::ActionType::kAssert;
1001 if (request) {
1002 recorded_step.condition = request->condition();
1003 }
1004
1005 auto finalize = [&](const absl::Status& status) {
1006 recorded_step.success = response->success();
1007 recorded_step.message = response->message();
1008 recorded_step.expected_value = response->expected_value();
1009 recorded_step.actual_value = response->actual_value();
1010 recorded_step.test_id = response->test_id();
1011 MaybeRecordStep(&test_recorder_, recorded_step);
1012 return status;
1013 };
1014
1015 if (!test_manager_) {
1016 response->set_success(false);
1017 response->set_message("TestManager not available");
1018 response->set_actual_value("N/A");
1019 response->set_expected_value("N/A");
1020 return finalize(
1021 absl::FailedPreconditionError("TestManager not available"));
1022 }
1023
1024 const std::string test_id = test_manager_->RegisterHarnessTest(
1025 absl::StrFormat("Assert: %s", request->condition()), "grpc");
1026 response->set_test_id(test_id);
1027 recorded_step.test_id = test_id;
1028 test_manager_->AppendHarnessTestLog(
1029 test_id, absl::StrFormat("Queued assertion: %s", request->condition()));
1030
1031#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1032 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
1033 if (!engine) {
1034 std::string message = "ImGuiTestEngine not initialized";
1035 response->set_success(false);
1036 response->set_message(message);
1037 response->set_actual_value("N/A");
1038 response->set_expected_value("N/A");
1039 test_manager_->MarkHarnessTestCompleted(
1040 test_id, HarnessTestStatus::kFailed, message);
1041 return finalize(absl::OkStatus());
1042 }
1043
1044 std::string condition = request->condition();
1045 size_t colon_pos = condition.find(':');
1046 if (colon_pos == std::string::npos) {
1047 std::string message =
1048 "Invalid condition format. Use 'type:target' (e.g. 'visible:Main Window')";
1049 response->set_success(false);
1050 response->set_message(message);
1051 response->set_actual_value("N/A");
1052 response->set_expected_value("N/A");
1053 test_manager_->MarkHarnessTestCompleted(
1054 test_id, HarnessTestStatus::kFailed, message);
1055 test_manager_->AppendHarnessTestLog(test_id, message);
1056 return finalize(absl::OkStatus());
1057 }
1058
1059 std::string assertion_type = condition.substr(0, colon_pos);
1060 std::string assertion_target = condition.substr(colon_pos + 1);
1061
1062 auto test_data = std::make_shared<DynamicTestData>();
1063 TestManager* manager = test_manager_;
1064 test_data->test_func = [manager, captured_id = test_id, assertion_type,
1065 assertion_target](ImGuiTestContext* ctx) {
1066 manager->MarkHarnessTestRunning(captured_id);
1067
1068 auto complete_with =
1069 [manager, captured_id](bool passed, const std::string& message,
1070 const std::string& actual,
1071 const std::string& expected,
1072 HarnessTestStatus status) {
1073 manager->AppendHarnessTestLog(captured_id, message);
1074 if (!actual.empty() || !expected.empty()) {
1075 manager->AppendHarnessTestLog(
1076 captured_id,
1077 absl::StrFormat("Actual: %s | Expected: %s", actual,
1078 expected));
1079 }
1080 manager->MarkHarnessTestCompleted(
1081 captured_id, status,
1082 passed ? "" : message);
1083 };
1084
1085 try {
1086 bool passed = false;
1087 std::string actual_value;
1088 std::string expected_value;
1089 std::string message;
1090
1091 if (assertion_type == "visible") {
1092 ImGuiTestItemInfo window_info = ctx->WindowInfo(
1093 assertion_target.c_str(), ImGuiTestOpFlags_NoError);
1094 bool is_visible = (window_info.ID != 0);
1095 passed = is_visible;
1096 actual_value = is_visible ? "visible" : "hidden";
1097 expected_value = "visible";
1098 message = passed ?
1099 absl::StrFormat("'%s' is visible", assertion_target) :
1100 absl::StrFormat("'%s' is not visible", assertion_target);
1101 } else if (assertion_type == "enabled") {
1102 ImGuiTestItemInfo item = ctx->ItemInfo(
1103 assertion_target.c_str(), ImGuiTestOpFlags_NoError);
1104 bool is_enabled = (item.ID != 0 &&
1105 !(item.ItemFlags & ImGuiItemFlags_Disabled));
1106 passed = is_enabled;
1107 actual_value = is_enabled ? "enabled" : "disabled";
1108 expected_value = "enabled";
1109 message = passed ?
1110 absl::StrFormat("'%s' is enabled", assertion_target) :
1111 absl::StrFormat("'%s' is not enabled", assertion_target);
1112 } else if (assertion_type == "exists") {
1113 ImGuiTestItemInfo item = ctx->ItemInfo(
1114 assertion_target.c_str(), ImGuiTestOpFlags_NoError);
1115 bool exists = (item.ID != 0);
1116 passed = exists;
1117 actual_value = exists ? "exists" : "not found";
1118 expected_value = "exists";
1119 message = passed ?
1120 absl::StrFormat("'%s' exists", assertion_target) :
1121 absl::StrFormat("'%s' not found", assertion_target);
1122 } else if (assertion_type == "text_contains") {
1123 size_t second_colon = assertion_target.find(':');
1124 if (second_colon == std::string::npos) {
1125 std::string error_message =
1126 "text_contains requires format 'text_contains:target:expected_text'";
1127 complete_with(false, error_message, "N/A", "N/A",
1128 HarnessTestStatus::kFailed);
1129 return;
1130 }
1131
1132 std::string input_target = assertion_target.substr(0, second_colon);
1133 std::string expected_text = assertion_target.substr(second_colon + 1);
1134
1135 ImGuiTestItemInfo item = ctx->ItemInfo(input_target.c_str());
1136 if (item.ID != 0) {
1137 std::string actual_text = "(text_retrieval_not_fully_implemented)";
1138 passed = actual_text.find(expected_text) != std::string::npos;
1139 actual_value = actual_text;
1140 expected_value = absl::StrFormat("contains '%s'", expected_text);
1141 message = passed ?
1142 absl::StrFormat("'%s' contains '%s'", input_target,
1143 expected_text) :
1144 absl::StrFormat(
1145 "'%s' does not contain '%s' (actual: '%s')",
1146 input_target, expected_text, actual_text);
1147 } else {
1148 passed = false;
1149 actual_value = "not found";
1150 expected_value = expected_text;
1151 message = absl::StrFormat("Input '%s' not found", input_target);
1152 }
1153 } else {
1154 std::string error_message =
1155 absl::StrFormat("Unknown assertion type: %s", assertion_type);
1156 complete_with(false, error_message, "N/A", "N/A",
1157 HarnessTestStatus::kFailed);
1158 return;
1159 }
1160
1161 complete_with(passed, message, actual_value, expected_value,
1162 passed ? HarnessTestStatus::kPassed
1163 : HarnessTestStatus::kFailed);
1164 } catch (const std::exception& e) {
1165 std::string error_message =
1166 absl::StrFormat("Assertion failed: %s", e.what());
1167 manager->AppendHarnessTestLog(captured_id, error_message);
1168 manager->MarkHarnessTestCompleted(captured_id,
1169 HarnessTestStatus::kFailed,
1170 error_message);
1171 }
1172 };
1173
1174 std::string test_name = absl::StrFormat(
1175 "grpc_assert_%lld",
1176 static_cast<long long>(
1177 std::chrono::system_clock::now().time_since_epoch().count()));
1178
1179 ImGuiTest* test = IM_REGISTER_TEST(engine, "grpc", test_name.c_str());
1180 test->TestFunc = RunDynamicTest;
1181 test->UserData = test_data.get();
1182
1183 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
1184
1185 response->set_success(true);
1186 std::string message = absl::StrFormat(
1187 "Queued assertion for '%s:%s'", assertion_type, assertion_target);
1188 response->set_message(message);
1189 response->set_actual_value("(async)");
1190 response->set_expected_value("(async)");
1191 test_manager_->AppendHarnessTestLog(test_id, message);
1192
1193#else
1194 test_manager_->MarkHarnessTestRunning(test_id);
1195 std::string message = absl::StrFormat(
1196 "[STUB] Assertion '%s' passed (ImGuiTestEngine not available)",
1197 request->condition());
1198 test_manager_->MarkHarnessTestCompleted(test_id,
1199 HarnessTestStatus::kPassed,
1200 message);
1201 test_manager_->AppendHarnessTestLog(test_id, message);
1202
1203 response->set_success(true);
1204 response->set_message(message);
1205 response->set_actual_value("(stub)");
1206 response->set_expected_value("(stub)");
1207#endif
1208
1209 return finalize(absl::OkStatus());
1210}
1211
1212absl::Status ImGuiTestHarnessServiceImpl::Screenshot(
1213 const ScreenshotRequest* request, ScreenshotResponse* response) {
1214 if (!response) {
1215 return absl::InvalidArgumentError("response cannot be null");
1216 }
1217
1218 const std::string requested_path =
1219 request ? request->output_path() : std::string();
1220 absl::StatusOr<ScreenshotArtifact> artifact_or =
1221 CaptureHarnessScreenshot(requested_path);
1222 if (!artifact_or.ok()) {
1223 response->set_success(false);
1224 response->set_message(std::string(artifact_or.status().message()));
1225 return artifact_or.status();
1226 }
1227
1228 const ScreenshotArtifact& artifact = *artifact_or;
1229 response->set_success(true);
1230 response->set_message(absl::StrFormat("Screenshot saved to %s (%dx%d)",
1231 artifact.file_path, artifact.width,
1232 artifact.height));
1233 response->set_file_path(artifact.file_path);
1234 response->set_file_size_bytes(artifact.file_size_bytes);
1235
1236 return absl::OkStatus();
1237}
1238
1239absl::Status ImGuiTestHarnessServiceImpl::GetTestStatus(
1240 const GetTestStatusRequest* request, GetTestStatusResponse* response) {
1241 if (!test_manager_) {
1242 return absl::FailedPreconditionError("TestManager not available");
1243 }
1244
1245 if (request->test_id().empty()) {
1246 return absl::InvalidArgumentError("test_id must be provided");
1247 }
1248
1249 auto execution_or =
1250 test_manager_->GetHarnessTestExecution(request->test_id());
1251 if (!execution_or.ok()) {
1252 response->set_status(GetTestStatusResponse::TEST_STATUS_UNSPECIFIED);
1253 response->set_error_message(std::string(execution_or.status().message()));
1254 return absl::OkStatus();
1255 }
1256
1257 const auto& execution = execution_or.value();
1258 response->set_status(ConvertHarnessStatus(execution.status));
1259 response->set_queued_at_ms(ToUnixMillisSafe(execution.queued_at));
1260 response->set_started_at_ms(ToUnixMillisSafe(execution.started_at));
1261 response->set_completed_at_ms(ToUnixMillisSafe(execution.completed_at));
1262 response->set_execution_time_ms(ClampDurationToInt32(execution.duration));
1263 if (!execution.error_message.empty()) {
1264 response->set_error_message(execution.error_message);
1265 } else {
1266 response->clear_error_message();
1267 }
1268
1269 response->clear_assertion_failures();
1270 for (const auto& failure : execution.assertion_failures) {
1271 response->add_assertion_failures(failure);
1272 }
1273
1274 return absl::OkStatus();
1275}
1276
1277absl::Status ImGuiTestHarnessServiceImpl::ListTests(
1278 const ListTestsRequest* request, ListTestsResponse* response) {
1279 if (!test_manager_) {
1280 return absl::FailedPreconditionError("TestManager not available");
1281 }
1282
1283 if (request->page_size() < 0) {
1284 return absl::InvalidArgumentError("page_size cannot be negative");
1285 }
1286
1287 int page_size = request->page_size() > 0 ? request->page_size() : 100;
1288 constexpr int kMaxPageSize = 500;
1289 if (page_size > kMaxPageSize) {
1290 page_size = kMaxPageSize;
1291 }
1292
1293 size_t start_index = 0;
1294 if (!request->page_token().empty()) {
1295 int64_t token_value = 0;
1296 if (!absl::SimpleAtoi(request->page_token(), &token_value) ||
1297 token_value < 0) {
1298 return absl::InvalidArgumentError("Invalid page_token");
1299 }
1300 start_index = static_cast<size_t>(token_value);
1301 }
1302
1303 auto summaries =
1304 test_manager_->ListHarnessTestSummaries(request->category_filter());
1305
1306 response->set_total_count(static_cast<int32_t>(summaries.size()));
1307
1308 if (start_index >= summaries.size()) {
1309 response->clear_tests();
1310 response->clear_next_page_token();
1311 return absl::OkStatus();
1312 }
1313
1314 size_t end_index =
1315 std::min(start_index + static_cast<size_t>(page_size),
1316 summaries.size());
1317
1318 for (size_t i = start_index; i < end_index; ++i) {
1319 const auto& summary = summaries[i];
1320 auto* test_info = response->add_tests();
1321 const auto& exec = summary.latest_execution;
1322
1323 test_info->set_test_id(exec.test_id);
1324 test_info->set_name(exec.name);
1325 test_info->set_category(exec.category);
1326
1327 int64_t last_run_ms = ToUnixMillisSafe(exec.completed_at);
1328 if (last_run_ms == 0) {
1329 last_run_ms = ToUnixMillisSafe(exec.started_at);
1330 }
1331 if (last_run_ms == 0) {
1332 last_run_ms = ToUnixMillisSafe(exec.queued_at);
1333 }
1334 test_info->set_last_run_timestamp_ms(last_run_ms);
1335
1336 test_info->set_total_runs(summary.total_runs);
1337 test_info->set_pass_count(summary.pass_count);
1338 test_info->set_fail_count(summary.fail_count);
1339
1340 int32_t average_duration_ms = 0;
1341 if (summary.total_runs > 0) {
1342 absl::Duration average_duration =
1343 summary.total_duration / summary.total_runs;
1344 average_duration_ms = ClampDurationToInt32(average_duration);
1345 }
1346 test_info->set_average_duration_ms(average_duration_ms);
1347 }
1348
1349 if (end_index < summaries.size()) {
1350 response->set_next_page_token(absl::StrCat(end_index));
1351 } else {
1352 response->clear_next_page_token();
1353 }
1354
1355 return absl::OkStatus();
1356}
1357
1358absl::Status ImGuiTestHarnessServiceImpl::GetTestResults(
1359 const GetTestResultsRequest* request,
1360 GetTestResultsResponse* response) {
1361 if (!test_manager_) {
1362 return absl::FailedPreconditionError("TestManager not available");
1363 }
1364
1365 if (request->test_id().empty()) {
1366 return absl::InvalidArgumentError("test_id must be provided");
1367 }
1368
1369 auto execution_or =
1370 test_manager_->GetHarnessTestExecution(request->test_id());
1371 if (!execution_or.ok()) {
1372 return execution_or.status();
1373 }
1374
1375 const auto& execution = execution_or.value();
1376 response->set_success(
1377 execution.status == HarnessTestStatus::kPassed);
1378 response->set_test_name(execution.name);
1379 response->set_category(execution.category);
1380
1381 int64_t executed_at_ms = ToUnixMillisSafe(execution.completed_at);
1382 if (executed_at_ms == 0) {
1383 executed_at_ms = ToUnixMillisSafe(execution.started_at);
1384 }
1385 if (executed_at_ms == 0) {
1386 executed_at_ms = ToUnixMillisSafe(execution.queued_at);
1387 }
1388 response->set_executed_at_ms(executed_at_ms);
1389 response->set_duration_ms(ClampDurationToInt32(execution.duration));
1390
1391 response->clear_assertions();
1392 if (!execution.assertion_failures.empty()) {
1393 for (const auto& failure : execution.assertion_failures) {
1394 auto* assertion = response->add_assertions();
1395 assertion->set_description(failure);
1396 assertion->set_passed(false);
1397 assertion->set_error_message(failure);
1398 }
1399 } else if (!execution.error_message.empty()) {
1400 auto* assertion = response->add_assertions();
1401 assertion->set_description("Execution error");
1402 assertion->set_passed(false);
1403 assertion->set_error_message(execution.error_message);
1404 }
1405
1406 if (request->include_logs()) {
1407 for (const auto& log_entry : execution.logs) {
1408 response->add_logs(log_entry);
1409 }
1410 }
1411
1412 auto* metrics_map = response->mutable_metrics();
1413 for (const auto& [key, value] : execution.metrics) {
1414 (*metrics_map)[key] = value;
1415 }
1416
1417 // IT-08b: Include failure diagnostics if available
1418 if (!execution.screenshot_path.empty()) {
1419 response->set_screenshot_path(execution.screenshot_path);
1420 response->set_screenshot_size_bytes(execution.screenshot_size_bytes);
1421 }
1422 if (!execution.failure_context.empty()) {
1423 response->set_failure_context(execution.failure_context);
1424 }
1425 if (!execution.widget_state.empty()) {
1426 response->set_widget_state(execution.widget_state);
1427 }
1428
1429 return absl::OkStatus();
1430}
1431
1432absl::Status ImGuiTestHarnessServiceImpl::DiscoverWidgets(
1433 const DiscoverWidgetsRequest* request,
1434 DiscoverWidgetsResponse* response) {
1435 if (!request) {
1436 return absl::InvalidArgumentError("request cannot be null");
1437 }
1438 if (!response) {
1439 return absl::InvalidArgumentError("response cannot be null");
1440 }
1441
1442 if (!test_manager_) {
1443 return absl::FailedPreconditionError("TestManager not available");
1444 }
1445
1446 widget_discovery_service_.CollectWidgets(/*ctx=*/nullptr, *request,
1447 response);
1448 return absl::OkStatus();
1449}
1450
1451absl::Status ImGuiTestHarnessServiceImpl::StartRecording(
1452 const StartRecordingRequest* request, StartRecordingResponse* response) {
1453 if (!request) {
1454 return absl::InvalidArgumentError("request cannot be null");
1455 }
1456 if (!response) {
1457 return absl::InvalidArgumentError("response cannot be null");
1458 }
1459
1460 TestRecorder::RecordingOptions options;
1461 options.output_path = request->output_path();
1462 options.session_name = request->session_name();
1463 options.description = request->description();
1464
1465 if (options.output_path.empty()) {
1466 response->set_success(false);
1467 response->set_message("output_path is required to start recording");
1468 return absl::InvalidArgumentError("output_path cannot be empty");
1469 }
1470
1471 absl::StatusOr<std::string> recording_id = test_recorder_.Start(options);
1472 if (!recording_id.ok()) {
1473 response->set_success(false);
1474 response->set_message(std::string(recording_id.status().message()));
1475 return recording_id.status();
1476 }
1477
1478 response->set_success(true);
1479 response->set_message("Recording started");
1480 response->set_recording_id(*recording_id);
1481 response->set_started_at_ms(absl::ToUnixMillis(absl::Now()));
1482 return absl::OkStatus();
1483}
1484
1485absl::Status ImGuiTestHarnessServiceImpl::StopRecording(
1486 const StopRecordingRequest* request, StopRecordingResponse* response) {
1487 if (!request) {
1488 return absl::InvalidArgumentError("request cannot be null");
1489 }
1490 if (!response) {
1491 return absl::InvalidArgumentError("response cannot be null");
1492 }
1493
1494 absl::StatusOr<TestRecorder::StopRecordingSummary> summary =
1495 test_recorder_.Stop(request->recording_id(), request->discard());
1496 if (!summary.ok()) {
1497 response->set_success(false);
1498 response->set_message(std::string(summary.status().message()));
1499 return summary.status();
1500 }
1501
1502 response->set_success(true);
1503 if (summary->saved) {
1504 response->set_message("Recording saved");
1505 } else {
1506 response->set_message("Recording discarded");
1507 }
1508 response->set_output_path(summary->output_path);
1509 response->set_step_count(summary->step_count);
1510 response->set_duration_ms(absl::ToInt64Milliseconds(summary->duration));
1511 return absl::OkStatus();
1512}
1513
1514absl::Status ImGuiTestHarnessServiceImpl::ReplayTest(
1515 const ReplayTestRequest* request, ReplayTestResponse* response) {
1516 if (!request) {
1517 return absl::InvalidArgumentError("request cannot be null");
1518 }
1519 if (!response) {
1520 return absl::InvalidArgumentError("response cannot be null");
1521 }
1522
1523 response->clear_logs();
1524 response->clear_assertions();
1525
1526 if (request->script_path().empty()) {
1527 response->set_success(false);
1528 response->set_message("script_path is required");
1529 return absl::InvalidArgumentError("script_path cannot be empty");
1530 }
1531
1532 absl::StatusOr<TestScript> script_or =
1533 TestScriptParser::ParseFromFile(request->script_path());
1534 if (!script_or.ok()) {
1535 response->set_success(false);
1536 response->set_message(std::string(script_or.status().message()));
1537 return script_or.status();
1538 }
1539 TestScript script = std::move(*script_or);
1540
1541 absl::flat_hash_map<std::string, std::string> overrides;
1542 for (const auto& entry : request->parameter_overrides()) {
1543 overrides[entry.first] = entry.second;
1544 }
1545
1546 response->set_replay_session_id(
1547 absl::StrFormat("replay_%s",
1548 absl::FormatTime("%Y%m%dT%H%M%S", absl::Now(),
1549 absl::UTCTimeZone())));
1550
1551 auto suspension = test_recorder_.Suspend();
1552
1553 std::vector<std::string> logs;
1554 logs.reserve(script.steps.size() * 2 + 4);
1555
1556 bool overall_success = true;
1557 std::string overall_message = "Replay completed successfully";
1558 int steps_executed = 0;
1559 absl::Status overall_rpc_status = absl::OkStatus();
1560
1561 for (const auto& step : script.steps) {
1562 ++steps_executed;
1563 std::string action_label =
1564 absl::StrFormat("Step %d: %s", steps_executed, step.action);
1565 logs.push_back(action_label);
1566
1567 absl::Status status = absl::OkStatus();
1568 bool step_success = false;
1569 std::string step_message;
1570 HarnessTestExecution execution;
1571 bool have_execution = false;
1572
1573 if (step.action == "click") {
1574 ClickRequest sub_request;
1575 sub_request.set_target(ApplyOverrides(step.target, overrides));
1576 sub_request.set_type(ClickTypeFromString(step.click_type));
1577 ClickResponse sub_response;
1578 status = Click(&sub_request, &sub_response);
1579 step_success = sub_response.success();
1580 step_message = sub_response.message();
1581 if (status.ok() && !sub_response.test_id().empty()) {
1582 absl::Status wait_status = WaitForHarnessTestCompletion(
1583 test_manager_, sub_response.test_id(), &execution);
1584 if (wait_status.ok()) {
1585 have_execution = true;
1586 if (!execution.error_message.empty()) {
1587 step_message = execution.error_message;
1588 }
1589 } else {
1590 status = wait_status;
1591 step_success = false;
1592 step_message = std::string(wait_status.message());
1593 }
1594 }
1595 } else if (step.action == "type") {
1596 TypeRequest sub_request;
1597 sub_request.set_target(ApplyOverrides(step.target, overrides));
1598 sub_request.set_text(ApplyOverrides(step.text, overrides));
1599 sub_request.set_clear_first(step.clear_first);
1600 TypeResponse sub_response;
1601 status = Type(&sub_request, &sub_response);
1602 step_success = sub_response.success();
1603 step_message = sub_response.message();
1604 if (status.ok() && !sub_response.test_id().empty()) {
1605 absl::Status wait_status = WaitForHarnessTestCompletion(
1606 test_manager_, sub_response.test_id(), &execution);
1607 if (wait_status.ok()) {
1608 have_execution = true;
1609 if (!execution.error_message.empty()) {
1610 step_message = execution.error_message;
1611 }
1612 } else {
1613 status = wait_status;
1614 step_success = false;
1615 step_message = std::string(wait_status.message());
1616 }
1617 }
1618 } else if (step.action == "wait") {
1619 WaitRequest sub_request;
1620 sub_request.set_condition(ApplyOverrides(step.condition, overrides));
1621 if (step.timeout_ms > 0) {
1622 sub_request.set_timeout_ms(step.timeout_ms);
1623 }
1624 WaitResponse sub_response;
1625 status = Wait(&sub_request, &sub_response);
1626 step_success = sub_response.success();
1627 step_message = sub_response.message();
1628 if (status.ok() && !sub_response.test_id().empty()) {
1629 absl::Status wait_status = WaitForHarnessTestCompletion(
1630 test_manager_, sub_response.test_id(), &execution);
1631 if (wait_status.ok()) {
1632 have_execution = true;
1633 if (!execution.error_message.empty()) {
1634 step_message = execution.error_message;
1635 }
1636 } else {
1637 status = wait_status;
1638 step_success = false;
1639 step_message = std::string(wait_status.message());
1640 }
1641 }
1642 } else if (step.action == "assert") {
1643 AssertRequest sub_request;
1644 sub_request.set_condition(ApplyOverrides(step.condition, overrides));
1645 AssertResponse sub_response;
1646 status = Assert(&sub_request, &sub_response);
1647 step_success = sub_response.success();
1648 step_message = sub_response.message();
1649 if (status.ok() && !sub_response.test_id().empty()) {
1650 absl::Status wait_status = WaitForHarnessTestCompletion(
1651 test_manager_, sub_response.test_id(), &execution);
1652 if (wait_status.ok()) {
1653 have_execution = true;
1654 if (!execution.error_message.empty()) {
1655 step_message = execution.error_message;
1656 }
1657 } else {
1658 status = wait_status;
1659 step_success = false;
1660 step_message = std::string(wait_status.message());
1661 }
1662 }
1663 } else if (step.action == "screenshot") {
1664 ScreenshotRequest sub_request;
1665 sub_request.set_window_title(ApplyOverrides(step.target, overrides));
1666 if (!step.region.empty()) {
1667 sub_request.set_output_path(ApplyOverrides(step.region, overrides));
1668 }
1669 ScreenshotResponse sub_response;
1670 status = Screenshot(&sub_request, &sub_response);
1671 step_success = sub_response.success();
1672 step_message = sub_response.message();
1673 } else {
1674 status = absl::InvalidArgumentError(
1675 absl::StrFormat("Unsupported action '%s'", step.action));
1676 step_message = std::string(status.message());
1677 }
1678
1679 auto* assertion = response->add_assertions();
1680 assertion->set_description(
1681 absl::StrFormat("Step %d (%s)", steps_executed, step.action));
1682
1683 if (!status.ok()) {
1684 assertion->set_passed(false);
1685 assertion->set_error_message(std::string(status.message()));
1686 overall_success = false;
1687 overall_message = step_message;
1688 logs.push_back(absl::StrFormat(" Error: %s", status.message()));
1689 overall_rpc_status = status;
1690 break;
1691 }
1692
1693 bool expectations_met = (step_success == step.expect_success);
1694 std::string expectation_error;
1695
1696 if (!expectations_met) {
1697 expectation_error = absl::StrFormat(
1698 "Expected success=%s but got %s",
1699 step.expect_success ? "true" : "false",
1700 step_success ? "true" : "false");
1701 }
1702
1703 if (!step.expect_status.empty()) {
1704 HarnessTestStatus expected_status =
1705 ::yaze::test::HarnessStatusFromString(step.expect_status);
1706 if (!have_execution) {
1707 expectations_met = false;
1708 if (!expectation_error.empty()) {
1709 expectation_error.append("; ");
1710 }
1711 expectation_error.append("No execution details available");
1712 } else if (expected_status != HarnessTestStatus::kUnspecified &&
1713 execution.status != expected_status) {
1714 expectations_met = false;
1715 if (!expectation_error.empty()) {
1716 expectation_error.append("; ");
1717 }
1718 expectation_error.append(absl::StrFormat(
1719 "Expected status %s but observed %s",
1720 step.expect_status, ::yaze::test::HarnessStatusToString(execution.status)));
1721 }
1722 if (have_execution) {
1723 assertion->set_actual_value(::yaze::test::HarnessStatusToString(execution.status));
1724 assertion->set_expected_value(step.expect_status);
1725 }
1726 }
1727
1728 if (!step.expect_message.empty()) {
1729 std::string actual_message = step_message;
1730 if (have_execution && !execution.error_message.empty()) {
1731 actual_message = execution.error_message;
1732 }
1733 if (actual_message.find(step.expect_message) == std::string::npos) {
1734 expectations_met = false;
1735 if (!expectation_error.empty()) {
1736 expectation_error.append("; ");
1737 }
1738 expectation_error.append(absl::StrFormat(
1739 "Expected message containing '%s' but got '%s'",
1740 step.expect_message, actual_message));
1741 }
1742 }
1743
1744 if (!expectations_met) {
1745 assertion->set_passed(false);
1746 assertion->set_error_message(expectation_error);
1747 overall_success = false;
1748 overall_message = expectation_error;
1749 logs.push_back(absl::StrFormat(" Failed expectations: %s",
1750 expectation_error));
1751 if (request->ci_mode()) {
1752 break;
1753 }
1754 } else {
1755 assertion->set_passed(true);
1756 logs.push_back(absl::StrFormat(" Result: %s", step_message));
1757 }
1758
1759 if (have_execution && !execution.assertion_failures.empty()) {
1760 for (const auto& failure : execution.assertion_failures) {
1761 logs.push_back(absl::StrFormat(" Assertion failure: %s", failure));
1762 }
1763 }
1764
1765 if (!overall_success && request->ci_mode()) {
1766 break;
1767 }
1768 }
1769
1770 response->set_steps_executed(steps_executed);
1771 response->set_success(overall_success);
1772 response->set_message(overall_message);
1773 for (const auto& log_entry : logs) {
1774 response->add_logs(log_entry);
1775 }
1776
1777 return overall_rpc_status;
1778}
1779
1780// ============================================================================
1781// ImGuiTestHarnessServer - Server Lifecycle
1782// ============================================================================
1783
1784ImGuiTestHarnessServer& ImGuiTestHarnessServer::Instance() {
1785 static ImGuiTestHarnessServer* instance = new ImGuiTestHarnessServer();
1786 return *instance;
1787}
1788
1789ImGuiTestHarnessServer::~ImGuiTestHarnessServer() {
1790 Shutdown();
1791}
1792
1793absl::Status ImGuiTestHarnessServer::Start(int port, TestManager* test_manager) {
1794 if (server_) {
1795 return absl::FailedPreconditionError("Server already running");
1796 }
1797
1798 if (!test_manager) {
1799 return absl::InvalidArgumentError("TestManager cannot be null");
1800 }
1801
1802 // Create the service implementation with TestManager reference
1803 service_ = std::make_unique<ImGuiTestHarnessServiceImpl>(test_manager);
1804
1805 // Create the gRPC service wrapper (store as member to prevent it from going out of scope)
1806 grpc_service_ = std::make_unique<ImGuiTestHarnessServiceGrpc>(service_.get());
1807
1808 std::string server_address = absl::StrFormat("0.0.0.0:%d", port);
1809
1810 grpc::ServerBuilder builder;
1811
1812 // Listen on all interfaces (use 0.0.0.0 to avoid IPv6/IPv4 binding conflicts)
1813 builder.AddListeningPort(server_address,
1814 grpc::InsecureServerCredentials());
1815
1816 // Register service
1817 builder.RegisterService(grpc_service_.get());
1818
1819 // Build and start
1820 server_ = builder.BuildAndStart();
1821
1822 if (!server_) {
1823 return absl::InternalError(
1824 absl::StrFormat("Failed to start gRPC server on %s", server_address));
1825 }
1826
1827 port_ = port;
1828
1829 std::cout << "✓ ImGuiTestHarness gRPC server listening on " << server_address
1830 << " (with TestManager integration)\n";
1831 std::cout << " Use 'grpcurl -plaintext -d '{\"message\":\"test\"}' "
1832 << server_address << " yaze.test.ImGuiTestHarness/Ping' to test\n";
1833
1834 return absl::OkStatus();
1835}
1836
1837void ImGuiTestHarnessServer::Shutdown() {
1838 if (server_) {
1839 std::cout << "⏹ Shutting down ImGuiTestHarness gRPC server...\n";
1840 server_->Shutdown();
1841 server_.reset();
1842 service_.reset();
1843 port_ = 0;
1844 std::cout << "✓ ImGuiTestHarness gRPC server stopped\n";
1845 }
1846}
1847
1848} // namespace test
1849} // namespace yaze
1850
1851#endif // YAZE_WITH_GRPC
static absl::StatusOr< TestScript > ParseFromFile(const std::string &path)
#define YAZE_VERSION_STRING
Definition yaze.h:32
Main namespace for the application.
Definition controller.cc:20
Yet Another Zelda3 Editor (YAZE) - Public C API.