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"
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"
55struct DynamicTestData {
56 std::function<void(ImGuiTestContext*)> test_func;
59absl::Mutex g_dynamic_tests_mutex;
60std::deque<std::shared_ptr<DynamicTestData>> g_dynamic_tests
61 ABSL_GUARDED_BY(g_dynamic_tests_mutex);
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();
72void RunDynamicTest(ImGuiTestContext* ctx) {
73 auto* data = (DynamicTestData*)ctx->Test->UserData;
74 if (data && data->test_func) {
80bool IsTestCompleted(ImGuiTest* test) {
81 return test->Output.Status != ImGuiTestStatus_Queued &&
82 test->Output.Status != ImGuiTestStatus_Running;
88 std::atomic<bool> completed{
false};
89 std::mutex data_mutex;
93 void SetResult(
const T& res,
const std::string& msg) {
94 std::lock_guard<std::mutex> lock(data_mutex);
97 completed.store(
true);
100 void GetResult(T& res, std::string& msg) {
101 std::lock_guard<std::mutex> lock(data_mutex);
112::yaze::test::GetTestStatusResponse_TestStatus ConvertHarnessStatus(
113 ::yaze::test::HarnessTestStatus 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:
127 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_UNSPECIFIED;
131int64_t ToUnixMillisSafe(absl::Time timestamp) {
132 if (timestamp == absl::InfinitePast()) {
135 return absl::ToUnixMillis(timestamp);
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();
143 if (millis < std::numeric_limits<int32_t>::min()) {
144 return std::numeric_limits<int32_t>::min();
146 return static_cast<int32_t
>(millis);
151#include <grpcpp/grpcpp.h>
152#include <grpcpp/server_builder.h>
159std::string ClickTypeToString(ClickRequest::ClickType type) {
161 case ClickRequest::CLICK_TYPE_RIGHT:
163 case ClickRequest::CLICK_TYPE_MIDDLE:
165 case ClickRequest::CLICK_TYPE_DOUBLE:
167 case ClickRequest::CLICK_TYPE_LEFT:
168 case ClickRequest::CLICK_TYPE_UNSPECIFIED:
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;
179 if (lower ==
"middle") {
180 return ClickRequest::CLICK_TYPE_MIDDLE;
182 if (lower ==
"double" || lower ==
"double_click" || lower ==
"dbl") {
183 return ClickRequest::CLICK_TYPE_DOUBLE;
185 return ClickRequest::CLICK_TYPE_LEFT;
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;
193 if (lower ==
"failed" || lower ==
"fail") {
194 return HarnessTestStatus::kFailed;
196 if (lower ==
"timeout") {
197 return HarnessTestStatus::kTimeout;
199 if (lower ==
"queued") {
200 return HarnessTestStatus::kQueued;
202 if (lower ==
"running") {
203 return HarnessTestStatus::kRunning;
205 return HarnessTestStatus::kUnspecified;
208const char* HarnessStatusToString(HarnessTestStatus status) {
210 case HarnessTestStatus::kPassed:
212 case HarnessTestStatus::kFailed:
214 case HarnessTestStatus::kTimeout:
216 case HarnessTestStatus::kQueued:
218 case HarnessTestStatus::kRunning:
220 case HarnessTestStatus::kUnspecified:
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()) {
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}});
240void MaybeRecordStep(TestRecorder* recorder, TestRecorder::RecordedStep step) {
241 if (!recorder || !recorder->IsRecording()) {
244 if (step.captured_at == absl::InfinitePast()) {
245 step.captured_at = absl::Now();
247 recorder->RecordStep(step);
250absl::Status WaitForHarnessTestCompletion(TestManager* manager,
251 const std::string& test_id,
252 HarnessTestExecution* execution) {
254 return absl::FailedPreconditionError(
"TestManager unavailable");
256 if (test_id.empty()) {
257 return absl::InvalidArgumentError(
"Missing harness test identifier");
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);
265 absl::SleepFor(absl::Milliseconds(75));
270 *execution = std::move(current.value());
273 if (current->status == HarnessTestStatus::kQueued ||
274 current->status == HarnessTestStatus::kRunning) {
275 absl::SleepFor(absl::Milliseconds(75));
278 return absl::OkStatus();
281 return absl::DeadlineExceededError(absl::StrFormat(
282 "Harness test %s did not reach a terminal state", test_id));
288class ImGuiTestHarnessServiceGrpc final :
public ImGuiTestHarness::Service {
290 explicit ImGuiTestHarnessServiceGrpc(ImGuiTestHarnessServiceImpl* impl)
293 grpc::Status Ping(grpc::ServerContext* context,
const PingRequest* request,
294 PingResponse* response)
override {
295 return ConvertStatus(impl_->Ping(request, response));
298 grpc::Status Click(grpc::ServerContext* context,
const ClickRequest* request,
299 ClickResponse* response)
override {
300 return ConvertStatus(impl_->Click(request, response));
303 grpc::Status Type(grpc::ServerContext* context,
const TypeRequest* request,
304 TypeResponse* response)
override {
305 return ConvertStatus(impl_->Type(request, response));
308 grpc::Status Wait(grpc::ServerContext* context,
const WaitRequest* request,
309 WaitResponse* response)
override {
310 return ConvertStatus(impl_->Wait(request, response));
313 grpc::Status Assert(grpc::ServerContext* context,
314 const AssertRequest* request,
315 AssertResponse* response)
override {
316 return ConvertStatus(impl_->Assert(request, response));
319 grpc::Status Screenshot(grpc::ServerContext* context,
320 const ScreenshotRequest* request,
321 ScreenshotResponse* response)
override {
322 return ConvertStatus(impl_->Screenshot(request, response));
325 grpc::Status GetTestStatus(grpc::ServerContext* context,
326 const GetTestStatusRequest* request,
327 GetTestStatusResponse* response)
override {
328 return ConvertStatus(impl_->GetTestStatus(request, response));
331 grpc::Status ListTests(grpc::ServerContext* context,
332 const ListTestsRequest* request,
333 ListTestsResponse* response)
override {
334 return ConvertStatus(impl_->ListTests(request, response));
337 grpc::Status GetTestResults(grpc::ServerContext* context,
338 const GetTestResultsRequest* request,
339 GetTestResultsResponse* response)
override {
340 return ConvertStatus(impl_->GetTestResults(request, response));
343 grpc::Status DiscoverWidgets(grpc::ServerContext* context,
344 const DiscoverWidgetsRequest* request,
345 DiscoverWidgetsResponse* response)
override {
346 return ConvertStatus(impl_->DiscoverWidgets(request, response));
349 grpc::Status StartRecording(grpc::ServerContext* context,
350 const StartRecordingRequest* request,
351 StartRecordingResponse* response)
override {
352 return ConvertStatus(impl_->StartRecording(request, response));
355 grpc::Status StopRecording(grpc::ServerContext* context,
356 const StopRecordingRequest* request,
357 StopRecordingResponse* response)
override {
358 return ConvertStatus(impl_->StopRecording(request, response));
361 grpc::Status ReplayTest(grpc::ServerContext* context,
362 const ReplayTestRequest* request,
363 ReplayTestResponse* response)
override {
364 return ConvertStatus(impl_->ReplayTest(request, response));
368 static grpc::Status ConvertStatus(
const absl::Status& status) {
370 return grpc::Status::OK;
373 grpc::StatusCode code = grpc::StatusCode::UNKNOWN;
374 switch (status.code()) {
375 case absl::StatusCode::kCancelled:
376 code = grpc::StatusCode::CANCELLED;
378 case absl::StatusCode::kUnknown:
379 code = grpc::StatusCode::UNKNOWN;
381 case absl::StatusCode::kInvalidArgument:
382 code = grpc::StatusCode::INVALID_ARGUMENT;
384 case absl::StatusCode::kDeadlineExceeded:
385 code = grpc::StatusCode::DEADLINE_EXCEEDED;
387 case absl::StatusCode::kNotFound:
388 code = grpc::StatusCode::NOT_FOUND;
390 case absl::StatusCode::kAlreadyExists:
391 code = grpc::StatusCode::ALREADY_EXISTS;
393 case absl::StatusCode::kPermissionDenied:
394 code = grpc::StatusCode::PERMISSION_DENIED;
396 case absl::StatusCode::kResourceExhausted:
397 code = grpc::StatusCode::RESOURCE_EXHAUSTED;
399 case absl::StatusCode::kFailedPrecondition:
400 code = grpc::StatusCode::FAILED_PRECONDITION;
402 case absl::StatusCode::kAborted:
403 code = grpc::StatusCode::ABORTED;
405 case absl::StatusCode::kOutOfRange:
406 code = grpc::StatusCode::OUT_OF_RANGE;
408 case absl::StatusCode::kUnimplemented:
409 code = grpc::StatusCode::UNIMPLEMENTED;
411 case absl::StatusCode::kInternal:
412 code = grpc::StatusCode::INTERNAL;
414 case absl::StatusCode::kUnavailable:
415 code = grpc::StatusCode::UNAVAILABLE;
417 case absl::StatusCode::kDataLoss:
418 code = grpc::StatusCode::DATA_LOSS;
420 case absl::StatusCode::kUnauthenticated:
421 code = grpc::StatusCode::UNAUTHENTICATED;
424 code = grpc::StatusCode::UNKNOWN;
428 return grpc::Status(code, std::string(status.message().data(), status.message().size()));
431 ImGuiTestHarnessServiceImpl* impl_;
438absl::Status ImGuiTestHarnessServiceImpl::Ping(
const PingRequest* request,
439 PingResponse* response) {
441 response->set_message(absl::StrFormat(
"Pong: %s", request->message()));
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());
452 return absl::OkStatus();
455absl::Status ImGuiTestHarnessServiceImpl::Click(
const ClickRequest* request,
456 ClickResponse* response) {
457 auto start = std::chrono::steady_clock::now();
459 TestRecorder::RecordedStep recorded_step;
462 recorded_step.target = request->target();
463 recorded_step.click_type = ClickTypeToString(request->type());
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);
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"));
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()));
489#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
490 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
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,
500 return finalize(absl::OkStatus());
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,
515 test_manager_->AppendHarnessTestLog(test_id, message);
516 return finalize(absl::OkStatus());
519 std::string widget_type = target.substr(0, colon_pos);
520 std::string widget_label = target.substr(colon_pos + 1);
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;
528 case ClickRequest::CLICK_TYPE_RIGHT:
529 mouse_button = ImGuiMouseButton_Right;
531 case ClickRequest::CLICK_TYPE_MIDDLE:
532 mouse_button = ImGuiMouseButton_Middle;
534 case ClickRequest::CLICK_TYPE_DOUBLE:
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);
546 if (click_type == ClickRequest::CLICK_TYPE_DOUBLE) {
547 ctx->ItemDoubleClick(widget_label.c_str());
549 ctx->ItemClick(widget_label.c_str(), mouse_button);
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,
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,
566 std::string test_name = absl::StrFormat(
568 static_cast<long long>(
569 std::chrono::system_clock::now().time_since_epoch().count()));
571 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
572 test->TestFunc = RunDynamicTest;
573 test->UserData = test_data.get();
575 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
576 KeepDynamicTestData(test_data);
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);
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,
599 test_manager_->AppendHarnessTestLog(test_id, message);
600 return finalize(absl::OkStatus());
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);
609 test_manager_->MarkHarnessTestRunning(test_id);
610 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kPassed,
612 test_manager_->AppendHarnessTestLog(test_id, message);
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());
621 return finalize(absl::OkStatus());
624absl::Status ImGuiTestHarnessServiceImpl::Type(
const TypeRequest* request,
625 TypeResponse* response) {
626 auto start = std::chrono::steady_clock::now();
628 TestRecorder::RecordedStep recorded_step;
631 recorded_step.target = request->target();
632 recorded_step.text = request->text();
633 recorded_step.clear_first = request->clear_first();
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);
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"));
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()));
659#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
660 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
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,
670 return finalize(absl::OkStatus());
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,
685 test_manager_->AppendHarnessTestLog(test_id, message);
686 return finalize(absl::OkStatus());
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();
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);
702 ImGuiTestItemInfo item = ctx->ItemInfo(widget_label.c_str());
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);
713 ctx->ItemClick(widget_label.c_str());
715 ctx->KeyPress(ImGuiMod_Shortcut | ImGuiKey_A);
716 ctx->KeyPress(ImGuiKey_Delete);
719 ctx->ItemInputValue(widget_label.c_str(), text.c_str());
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,
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,
733 rpc_state->SetResult(
false, error_message);
737 std::string test_name = absl::StrFormat(
739 static_cast<long long>(
740 std::chrono::system_clock::now().time_since_epoch().count()));
742 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
743 test->TestFunc = RunDynamicTest;
744 test->UserData = test_data.get();
746 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
747 KeepDynamicTestData(test_data);
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,
758 rpc_state->SetResult(
false, error_message);
761 std::this_thread::sleep_for(std::chrono::milliseconds(100));
764 bool success =
false;
766 rpc_state->GetResult(success, message);
767 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
768 std::chrono::steady_clock::now() - start);
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);
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,
784 test_manager_->AppendHarnessTestLog(test_id, message);
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());
793 return finalize(absl::OkStatus());
796absl::Status ImGuiTestHarnessServiceImpl::Wait(
const WaitRequest* request,
797 WaitResponse* response) {
798 auto start = std::chrono::steady_clock::now();
800 TestRecorder::RecordedStep recorded_step;
803 recorded_step.condition = request->condition();
804 recorded_step.timeout_ms = request->timeout_ms();
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);
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"));
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(
829 absl::StrFormat(
"Queued wait condition: %s", request->condition()));
831#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
832 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
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,
842 return finalize(absl::OkStatus());
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,
858 test_manager_->AppendHarnessTestLog(test_id, message);
859 return finalize(absl::OkStatus());
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;
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);
877 for (
int i = 0; i < 10; ++i) {
882 while (std::chrono::steady_clock::now() - poll_start < timeout) {
883 bool current_state =
false;
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);
898 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
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);
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);
921 std::this_thread::sleep_for(
922 std::chrono::milliseconds(poll_interval_ms));
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,
940 std::string test_name = absl::StrFormat(
942 static_cast<long long>(
943 std::chrono::system_clock::now().time_since_epoch().count()));
945 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
946 test->TestFunc = RunDynamicTest;
947 test->UserData = test_data.get();
949 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
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);
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,
967 test_manager_->AppendHarnessTestLog(test_id, message);
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());
976 return finalize(absl::OkStatus());
979absl::Status ImGuiTestHarnessServiceImpl::Assert(
const AssertRequest* request,
980 AssertResponse* response) {
981 TestRecorder::RecordedStep recorded_step;
984 recorded_step.condition = request->condition();
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);
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"));
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()));
1012#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1013 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
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,
1022 return finalize(absl::OkStatus());
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 "
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,
1037 test_manager_->AppendHarnessTestLog(test_id, message);
1038 return finalize(absl::OkStatus());
1041 std::string assertion_type = condition.substr(0, colon_pos);
1042 std::string assertion_target = condition.substr(colon_pos + 1);
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);
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(
1059 absl::StrFormat(
"Actual: %s | Expected: %s", actual, expected));
1061 manager->MarkHarnessTestCompleted(captured_id, status,
1062 passed ?
"" : message);
1066 bool passed =
false;
1067 std::string actual_value;
1068 std::string expected_value;
1069 std::string message;
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";
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);
1085 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
1086 passed = is_enabled;
1087 actual_value = is_enabled ?
"enabled" :
"disabled";
1088 expected_value =
"enabled";
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);
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);
1112 std::string input_target = assertion_target.substr(0, second_colon);
1113 std::string expected_text = assertion_target.substr(second_colon + 1);
1115 ImGuiTestItemInfo item = ctx->ItemInfo(input_target.c_str());
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,
1124 "'%s' does not contain '%s' (actual: '%s')",
1125 input_target, expected_text, actual_text);
1128 actual_value =
"not found";
1129 expected_value = expected_text;
1130 message = absl::StrFormat(
"Input '%s' not found", input_target);
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);
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,
1152 std::string test_name = absl::StrFormat(
1154 static_cast<long long>(
1155 std::chrono::system_clock::now().time_since_epoch().count()));
1157 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
1158 test->TestFunc = RunDynamicTest;
1159 test->UserData = test_data.get();
1161 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
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);
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,
1178 test_manager_->AppendHarnessTestLog(test_id, message);
1180 response->set_success(
true);
1181 response->set_message(message);
1182 response->set_actual_value(
"(stub)");
1183 response->set_expected_value(
"(stub)");
1186 return finalize(absl::OkStatus());
1189absl::Status ImGuiTestHarnessServiceImpl::Screenshot(
1190 const ScreenshotRequest* request, ScreenshotResponse* response) {
1192 return absl::InvalidArgumentError(
"response cannot be null");
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();
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,
1210 response->set_file_path(artifact.file_path);
1211 response->set_file_size_bytes(artifact.file_size_bytes);
1213 return absl::OkStatus();
1216absl::Status ImGuiTestHarnessServiceImpl::GetTestStatus(
1217 const GetTestStatusRequest* request, GetTestStatusResponse* response) {
1218 if (!test_manager_) {
1219 return absl::FailedPreconditionError(
"TestManager not available");
1222 if (request->test_id().empty()) {
1223 return absl::InvalidArgumentError(
"test_id must be provided");
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();
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);
1243 response->clear_error_message();
1246 response->clear_assertion_failures();
1247 for (
const auto& failure : execution.assertion_failures) {
1248 response->add_assertion_failures(failure);
1251 return absl::OkStatus();
1254absl::Status ImGuiTestHarnessServiceImpl::ListTests(
1255 const ListTestsRequest* request, ListTestsResponse* response) {
1256 if (!test_manager_) {
1257 return absl::FailedPreconditionError(
"TestManager not available");
1260 if (request->page_size() < 0) {
1261 return absl::InvalidArgumentError(
"page_size cannot be negative");
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;
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) ||
1275 return absl::InvalidArgumentError(
"Invalid page_token");
1277 start_index =
static_cast<size_t>(token_value);
1281 test_manager_->ListHarnessTestSummaries(request->category_filter());
1283 response->set_total_count(
static_cast<int32_t
>(summaries.size()));
1285 if (start_index >= summaries.size()) {
1286 response->clear_tests();
1287 response->clear_next_page_token();
1288 return absl::OkStatus();
1292 std::min(start_index +
static_cast<size_t>(page_size), summaries.size());
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;
1299 test_info->set_test_id(exec.test_id);
1300 test_info->set_name(exec.name);
1301 test_info->set_category(exec.category);
1303 int64_t last_run_ms = ToUnixMillisSafe(exec.completed_at);
1304 if (last_run_ms == 0) {
1305 last_run_ms = ToUnixMillisSafe(exec.started_at);
1307 if (last_run_ms == 0) {
1308 last_run_ms = ToUnixMillisSafe(exec.queued_at);
1310 test_info->set_last_run_timestamp_ms(last_run_ms);
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);
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);
1322 test_info->set_average_duration_ms(average_duration_ms);
1325 if (end_index < summaries.size()) {
1326 response->set_next_page_token(absl::StrCat(end_index));
1328 response->clear_next_page_token();
1331 return absl::OkStatus();
1334absl::Status ImGuiTestHarnessServiceImpl::GetTestResults(
1335 const GetTestResultsRequest* request, GetTestResultsResponse* response) {
1336 if (!test_manager_) {
1337 return absl::FailedPreconditionError(
"TestManager not available");
1340 if (request->test_id().empty()) {
1341 return absl::InvalidArgumentError(
"test_id must be provided");
1345 test_manager_->GetHarnessTestExecution(request->test_id());
1346 if (!execution_or.ok()) {
1347 return execution_or.status();
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);
1355 int64_t executed_at_ms = ToUnixMillisSafe(execution.completed_at);
1356 if (executed_at_ms == 0) {
1357 executed_at_ms = ToUnixMillisSafe(execution.started_at);
1359 if (executed_at_ms == 0) {
1360 executed_at_ms = ToUnixMillisSafe(execution.queued_at);
1362 response->set_executed_at_ms(executed_at_ms);
1363 response->set_duration_ms(ClampDurationToInt32(execution.duration));
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);
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);
1380 if (request->include_logs()) {
1381 for (
const auto& log_entry : execution.logs) {
1382 response->add_logs(log_entry);
1386 auto* metrics_map = response->mutable_metrics();
1387 for (
const auto& [key, value] : execution.metrics) {
1388 (*metrics_map)[
key] = value;
1392 if (!execution.screenshot_path.empty()) {
1393 response->set_screenshot_path(execution.screenshot_path);
1394 response->set_screenshot_size_bytes(execution.screenshot_size_bytes);
1396 if (!execution.failure_context.empty()) {
1397 response->set_failure_context(execution.failure_context);
1399 if (!execution.widget_state.empty()) {
1400 response->set_widget_state(execution.widget_state);
1403 return absl::OkStatus();
1406absl::Status ImGuiTestHarnessServiceImpl::DiscoverWidgets(
1407 const DiscoverWidgetsRequest* request, DiscoverWidgetsResponse* response) {
1409 return absl::InvalidArgumentError(
"request cannot be null");
1412 return absl::InvalidArgumentError(
"response cannot be null");
1415 if (!test_manager_) {
1416 return absl::FailedPreconditionError(
"TestManager not available");
1419 widget_discovery_service_.CollectWidgets(
nullptr, *request, response);
1420 return absl::OkStatus();
1423absl::Status ImGuiTestHarnessServiceImpl::StartRecording(
1424 const StartRecordingRequest* request, StartRecordingResponse* response) {
1426 return absl::InvalidArgumentError(
"request cannot be null");
1429 return absl::InvalidArgumentError(
"response cannot be null");
1432 TestRecorder::RecordingOptions options;
1433 options.output_path = request->output_path();
1434 options.session_name = request->session_name();
1435 options.description = request->description();
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");
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();
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();
1457absl::Status ImGuiTestHarnessServiceImpl::StopRecording(
1458 const StopRecordingRequest* request, StopRecordingResponse* response) {
1460 return absl::InvalidArgumentError(
"request cannot be null");
1463 return absl::InvalidArgumentError(
"response cannot be null");
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();
1474 response->set_success(
true);
1475 if (summary->saved) {
1476 response->set_message(
"Recording saved");
1478 response->set_message(
"Recording discarded");
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();
1486absl::Status ImGuiTestHarnessServiceImpl::ReplayTest(
1487 const ReplayTestRequest* request, ReplayTestResponse* response) {
1489 return absl::InvalidArgumentError(
"request cannot be null");
1492 return absl::InvalidArgumentError(
"response cannot be null");
1495 response->clear_logs();
1496 response->clear_assertions();
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");
1504 absl::StatusOr<TestScript> script_or =
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();
1511 TestScript script = std::move(*script_or);
1513 absl::flat_hash_map<std::string, std::string> overrides;
1514 for (
const auto& entry : request->parameter_overrides()) {
1515 overrides[entry.first] = entry.second;
1518 response->set_replay_session_id(absl::StrFormat(
1520 absl::FormatTime(
"%Y%m%dT%H%M%S", absl::Now(), absl::UTCTimeZone())));
1522 auto suspension = test_recorder_.Suspend();
1524 std::vector<std::string> logs;
1525 logs.reserve(script.steps.size() * 2 + 4);
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();
1532 for (
const auto& step : script.steps) {
1534 std::string action_label =
1535 absl::StrFormat(
"Step %d: %s", steps_executed, step.action);
1536 logs.push_back(action_label);
1538 absl::Status status = absl::OkStatus();
1539 bool step_success =
false;
1540 std::string step_message;
1541 HarnessTestExecution execution;
1542 bool have_execution =
false;
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;
1561 status = wait_status;
1562 step_success =
false;
1563 step_message = std::string(wait_status.message());
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;
1584 status = wait_status;
1585 step_success =
false;
1586 step_message = std::string(wait_status.message());
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);
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;
1608 status = wait_status;
1609 step_success =
false;
1610 step_message = std::string(wait_status.message());
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;
1629 status = wait_status;
1630 step_success =
false;
1631 step_message = std::string(wait_status.message());
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));
1640 ScreenshotResponse sub_response;
1641 status = Screenshot(&sub_request, &sub_response);
1642 step_success = sub_response.success();
1643 step_message = sub_response.message();
1645 status = absl::InvalidArgumentError(
1646 absl::StrFormat(
"Unsupported action '%s'", step.action));
1647 step_message = std::string(status.message().data(), status.message().size());
1650 auto* assertion = response->add_assertions();
1651 assertion->set_description(
1652 absl::StrFormat(
"Step %d (%s)", steps_executed, step.action));
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;
1664 bool expectations_met = (step_success == step.expect_success);
1665 std::string expectation_error;
1667 if (!expectations_met) {
1669 absl::StrFormat(
"Expected success=%s but got %s",
1670 step.expect_success ?
"true" :
"false",
1671 step_success ?
"true" :
"false");
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(
"; ");
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(
"; ");
1689 expectation_error.append(absl::StrFormat(
1690 "Expected status %s but observed %s", step.expect_status,
1691 ::yaze::test::HarnessStatusToString(execution.status)));
1693 if (have_execution) {
1694 assertion->set_actual_value(
1695 ::yaze::test::HarnessStatusToString(execution.status));
1696 assertion->set_expected_value(step.expect_status);
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;
1705 if (actual_message.find(step.expect_message) == std::string::npos) {
1706 expectations_met =
false;
1707 if (!expectation_error.empty()) {
1708 expectation_error.append(
"; ");
1710 expectation_error.append(
1711 absl::StrFormat(
"Expected message containing '%s' but got '%s'",
1712 step.expect_message, actual_message));
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;
1722 absl::StrFormat(
" Failed expectations: %s", expectation_error));
1723 if (request->ci_mode()) {
1727 assertion->set_passed(
true);
1728 logs.push_back(absl::StrFormat(
" Result: %s", step_message));
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));
1737 if (!overall_success && request->ci_mode()) {
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);
1749 return overall_rpc_status;
1756ImGuiTestHarnessServer& ImGuiTestHarnessServer::Instance() {
1757 static ImGuiTestHarnessServer* instance =
new ImGuiTestHarnessServer();
1761ImGuiTestHarnessServer::~ImGuiTestHarnessServer() {
1765absl::Status ImGuiTestHarnessServer::Start(
int port,
1766 TestManager* test_manager) {
1768 return absl::FailedPreconditionError(
"Server already running");
1771 if (!test_manager) {
1772 return absl::InvalidArgumentError(
"TestManager cannot be null");
1776 service_ = std::make_unique<ImGuiTestHarnessServiceImpl>(test_manager);
1780 grpc_service_ = std::make_unique<ImGuiTestHarnessServiceGrpc>(service_.get());
1782 std::string server_address = absl::StrFormat(
"0.0.0.0:%d", port);
1784 grpc::ServerBuilder builder;
1787 builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
1790 builder.RegisterService(grpc_service_.get());
1793 server_ = builder.BuildAndStart();
1796 return absl::InternalError(
1797 absl::StrFormat(
"Failed to start gRPC server on %s", server_address));
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";
1807 return absl::OkStatus();
1810void ImGuiTestHarnessServer::Shutdown() {
1812 std::cout <<
"⏹ Shutting down ImGuiTestHarness gRPC server...\n";
1813 server_->Shutdown();
1817 std::cout <<
"✓ ImGuiTestHarness gRPC server stopped\n";
static absl::StatusOr< TestScript > ParseFromFile(const std::string &path)
#define YAZE_VERSION_STRING
SDL2/SDL3 compatibility layer.
Yet Another Zelda3 Editor (YAZE) - Public C API.