16#include "absl/base/thread_annotations.h"
17#include "absl/container/flat_hash_map.h"
18#include "absl/strings/ascii.h"
19#include "absl/strings/numbers.h"
20#include "absl/strings/str_cat.h"
21#include "absl/strings/str_format.h"
22#include "absl/strings/str_replace.h"
23#include "absl/synchronization/mutex.h"
24#include "absl/time/clock.h"
25#include "absl/time/time.h"
29#include "protos/imgui_test_harness.grpc.pb.h"
30#include "protos/imgui_test_harness.pb.h"
33#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
34#include "imgui_test_engine/imgui_te_context.h"
35#include "imgui_test_engine/imgui_te_engine.h"
39struct DynamicTestData {
40 std::function<void(ImGuiTestContext*)> test_func;
43absl::Mutex g_dynamic_tests_mutex;
44std::deque<std::shared_ptr<DynamicTestData>> g_dynamic_tests
45 ABSL_GUARDED_BY(g_dynamic_tests_mutex);
47void KeepDynamicTestData(
const std::shared_ptr<DynamicTestData>& data) {
48 absl::MutexLock lock(&g_dynamic_tests_mutex);
49 constexpr size_t kMaxKeepAlive = 64;
50 g_dynamic_tests.push_back(data);
51 while (g_dynamic_tests.size() > kMaxKeepAlive) {
52 g_dynamic_tests.pop_front();
56void RunDynamicTest(ImGuiTestContext* ctx) {
57 auto* data = (DynamicTestData*)ctx->Test->UserData;
58 if (data && data->test_func) {
64bool IsTestCompleted(ImGuiTest* test) {
65 return test->Output.Status != ImGuiTestStatus_Queued &&
66 test->Output.Status != ImGuiTestStatus_Running;
72 std::atomic<bool> completed{
false};
73 std::mutex data_mutex;
77 void SetResult(
const T& res,
const std::string& msg) {
78 std::lock_guard<std::mutex> lock(data_mutex);
81 completed.store(
true);
84 void GetResult(T& res, std::string& msg) {
85 std::lock_guard<std::mutex> lock(data_mutex);
96::yaze::test::GetTestStatusResponse_TestStatus ConvertHarnessStatus(
97 ::yaze::test::HarnessTestStatus status) {
99 case ::yaze::test::HarnessTestStatus::kQueued:
100 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_QUEUED;
101 case ::yaze::test::HarnessTestStatus::kRunning:
102 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_RUNNING;
103 case ::yaze::test::HarnessTestStatus::kPassed:
104 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_PASSED;
105 case ::yaze::test::HarnessTestStatus::kFailed:
106 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_FAILED;
107 case ::yaze::test::HarnessTestStatus::kTimeout:
108 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_TIMEOUT;
109 case ::yaze::test::HarnessTestStatus::kUnspecified:
111 return ::yaze::test::GetTestStatusResponse::TEST_STATUS_UNSPECIFIED;
115int64_t ToUnixMillisSafe(absl::Time timestamp) {
116 if (timestamp == absl::InfinitePast()) {
119 return absl::ToUnixMillis(timestamp);
122int32_t ClampDurationToInt32(absl::Duration duration) {
123 int64_t millis = absl::ToInt64Milliseconds(duration);
124 if (millis > std::numeric_limits<int32_t>::max()) {
125 return std::numeric_limits<int32_t>::max();
127 if (millis < std::numeric_limits<int32_t>::min()) {
128 return std::numeric_limits<int32_t>::min();
130 return static_cast<int32_t
>(millis);
135#include <grpcpp/grpcpp.h>
136#include <grpcpp/server_builder.h>
143std::string ClickTypeToString(ClickRequest::ClickType type) {
145 case ClickRequest::CLICK_TYPE_RIGHT:
147 case ClickRequest::CLICK_TYPE_MIDDLE:
149 case ClickRequest::CLICK_TYPE_DOUBLE:
151 case ClickRequest::CLICK_TYPE_LEFT:
152 case ClickRequest::CLICK_TYPE_UNSPECIFIED:
158ClickRequest::ClickType ClickTypeFromString(absl::string_view type) {
159 const std::string lower = absl::AsciiStrToLower(std::string(type));
160 if (lower ==
"right") {
161 return ClickRequest::CLICK_TYPE_RIGHT;
163 if (lower ==
"middle") {
164 return ClickRequest::CLICK_TYPE_MIDDLE;
166 if (lower ==
"double" || lower ==
"double_click" || lower ==
"dbl") {
167 return ClickRequest::CLICK_TYPE_DOUBLE;
169 return ClickRequest::CLICK_TYPE_LEFT;
172HarnessTestStatus HarnessStatusFromString(absl::string_view status) {
173 const std::string lower = absl::AsciiStrToLower(std::string(status));
174 if (lower ==
"passed" || lower ==
"success") {
175 return HarnessTestStatus::kPassed;
177 if (lower ==
"failed" || lower ==
"fail") {
178 return HarnessTestStatus::kFailed;
180 if (lower ==
"timeout") {
181 return HarnessTestStatus::kTimeout;
183 if (lower ==
"queued") {
184 return HarnessTestStatus::kQueued;
186 if (lower ==
"running") {
187 return HarnessTestStatus::kRunning;
189 return HarnessTestStatus::kUnspecified;
192const char* HarnessStatusToString(HarnessTestStatus status) {
194 case HarnessTestStatus::kPassed:
196 case HarnessTestStatus::kFailed:
198 case HarnessTestStatus::kTimeout:
200 case HarnessTestStatus::kQueued:
202 case HarnessTestStatus::kRunning:
204 case HarnessTestStatus::kUnspecified:
210std::string ApplyOverrides(
211 const std::string& value,
212 const absl::flat_hash_map<std::string, std::string>& overrides) {
213 if (overrides.empty() || value.empty()) {
216 std::string result = value;
217 for (
const auto& [key, replacement] : overrides) {
218 const std::string placeholder = absl::StrCat(
"{{", key,
"}}");
219 result = absl::StrReplaceAll(result, {{placeholder, replacement}});
224void MaybeRecordStep(TestRecorder* recorder, TestRecorder::RecordedStep step) {
225 if (!recorder || !recorder->IsRecording()) {
228 if (step.captured_at == absl::InfinitePast()) {
229 step.captured_at = absl::Now();
231 recorder->RecordStep(step);
234absl::Status WaitForHarnessTestCompletion(TestManager* manager,
235 const std::string& test_id,
236 HarnessTestExecution* execution) {
238 return absl::FailedPreconditionError(
"TestManager unavailable");
240 if (test_id.empty()) {
241 return absl::InvalidArgumentError(
"Missing harness test identifier");
244 const absl::Time deadline = absl::Now() + absl::Seconds(20);
245 while (absl::Now() < deadline) {
246 absl::StatusOr<HarnessTestExecution> current =
247 manager->GetHarnessTestExecution(test_id);
249 absl::SleepFor(absl::Milliseconds(75));
254 *execution = std::move(current.value());
257 if (current->status == HarnessTestStatus::kQueued ||
258 current->status == HarnessTestStatus::kRunning) {
259 absl::SleepFor(absl::Milliseconds(75));
262 return absl::OkStatus();
265 return absl::DeadlineExceededError(absl::StrFormat(
266 "Harness test %s did not reach a terminal state", test_id));
272class ImGuiTestHarnessServiceGrpc final :
public ImGuiTestHarness::Service {
274 explicit ImGuiTestHarnessServiceGrpc(ImGuiTestHarnessServiceImpl* impl)
277 ::grpc::Status Ping(::grpc::ServerContext* context,
278 const PingRequest* request,
279 PingResponse* response)
override {
280 return ConvertStatus(impl_->Ping(request, response));
283 ::grpc::Status Click(::grpc::ServerContext* context,
284 const ClickRequest* request,
285 ClickResponse* response)
override {
286 return ConvertStatus(impl_->Click(request, response));
289 ::grpc::Status Type(::grpc::ServerContext* context,
290 const TypeRequest* request,
291 TypeResponse* response)
override {
292 return ConvertStatus(impl_->Type(request, response));
295 ::grpc::Status Wait(::grpc::ServerContext* context,
296 const WaitRequest* request,
297 WaitResponse* response)
override {
298 return ConvertStatus(impl_->Wait(request, response));
301 ::grpc::Status Assert(::grpc::ServerContext* context,
302 const AssertRequest* request,
303 AssertResponse* response)
override {
304 return ConvertStatus(impl_->Assert(request, response));
307 ::grpc::Status Screenshot(::grpc::ServerContext* context,
308 const ScreenshotRequest* request,
309 ScreenshotResponse* response)
override {
310 return ConvertStatus(impl_->Screenshot(request, response));
313 ::grpc::Status GetTestStatus(::grpc::ServerContext* context,
314 const GetTestStatusRequest* request,
315 GetTestStatusResponse* response)
override {
316 return ConvertStatus(impl_->GetTestStatus(request, response));
319 ::grpc::Status ListTests(::grpc::ServerContext* context,
320 const ListTestsRequest* request,
321 ListTestsResponse* response)
override {
322 return ConvertStatus(impl_->ListTests(request, response));
325 ::grpc::Status GetTestResults(::grpc::ServerContext* context,
326 const GetTestResultsRequest* request,
327 GetTestResultsResponse* response)
override {
328 return ConvertStatus(impl_->GetTestResults(request, response));
331 ::grpc::Status DiscoverWidgets(::grpc::ServerContext* context,
332 const DiscoverWidgetsRequest* request,
333 DiscoverWidgetsResponse* response)
override {
334 return ConvertStatus(impl_->DiscoverWidgets(request, response));
337 ::grpc::Status StartRecording(::grpc::ServerContext* context,
338 const StartRecordingRequest* request,
339 StartRecordingResponse* response)
override {
340 return ConvertStatus(impl_->StartRecording(request, response));
343 ::grpc::Status StopRecording(::grpc::ServerContext* context,
344 const StopRecordingRequest* request,
345 StopRecordingResponse* response)
override {
346 return ConvertStatus(impl_->StopRecording(request, response));
349 ::grpc::Status ReplayTest(::grpc::ServerContext* context,
350 const ReplayTestRequest* request,
351 ReplayTestResponse* response)
override {
352 return ConvertStatus(impl_->ReplayTest(request, response));
356 static ::grpc::Status ConvertStatus(
const absl::Status& status) {
358 return ::grpc::Status::OK;
361 ::grpc::StatusCode code = ::grpc::StatusCode::UNKNOWN;
362 switch (status.code()) {
363 case absl::StatusCode::kCancelled:
364 code = ::grpc::StatusCode::CANCELLED;
366 case absl::StatusCode::kUnknown:
367 code = ::grpc::StatusCode::UNKNOWN;
369 case absl::StatusCode::kInvalidArgument:
370 code = ::grpc::StatusCode::INVALID_ARGUMENT;
372 case absl::StatusCode::kDeadlineExceeded:
373 code = ::grpc::StatusCode::DEADLINE_EXCEEDED;
375 case absl::StatusCode::kNotFound:
376 code = ::grpc::StatusCode::NOT_FOUND;
378 case absl::StatusCode::kAlreadyExists:
379 code = ::grpc::StatusCode::ALREADY_EXISTS;
381 case absl::StatusCode::kPermissionDenied:
382 code = ::grpc::StatusCode::PERMISSION_DENIED;
384 case absl::StatusCode::kResourceExhausted:
385 code = ::grpc::StatusCode::RESOURCE_EXHAUSTED;
387 case absl::StatusCode::kFailedPrecondition:
388 code = ::grpc::StatusCode::FAILED_PRECONDITION;
390 case absl::StatusCode::kAborted:
391 code = ::grpc::StatusCode::ABORTED;
393 case absl::StatusCode::kOutOfRange:
394 code = ::grpc::StatusCode::OUT_OF_RANGE;
396 case absl::StatusCode::kUnimplemented:
397 code = ::grpc::StatusCode::UNIMPLEMENTED;
399 case absl::StatusCode::kInternal:
400 code = ::grpc::StatusCode::INTERNAL;
402 case absl::StatusCode::kUnavailable:
403 code = ::grpc::StatusCode::UNAVAILABLE;
405 case absl::StatusCode::kDataLoss:
406 code = ::grpc::StatusCode::DATA_LOSS;
408 case absl::StatusCode::kUnauthenticated:
409 code = ::grpc::StatusCode::UNAUTHENTICATED;
412 code = ::grpc::StatusCode::UNKNOWN;
416 return ::grpc::Status(
417 code, std::string(status.message().data(), status.message().size()));
420 ImGuiTestHarnessServiceImpl* impl_;
423std::unique_ptr<::grpc::Service> CreateImGuiTestHarnessServiceGrpc(
424 ImGuiTestHarnessServiceImpl* impl) {
425 return std::make_unique<ImGuiTestHarnessServiceGrpc>(impl);
432absl::Status ImGuiTestHarnessServiceImpl::Ping(
const PingRequest* request,
433 PingResponse* response) {
435 response->set_message(absl::StrFormat(
"Pong: %s", request->message()));
438 auto now = std::chrono::system_clock::now();
439 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
440 now.time_since_epoch());
441 response->set_timestamp_ms(ms.count());
446 return absl::OkStatus();
449absl::Status ImGuiTestHarnessServiceImpl::Click(
const ClickRequest* request,
450 ClickResponse* response) {
451 auto start = std::chrono::steady_clock::now();
453 TestRecorder::RecordedStep recorded_step;
456 recorded_step.target = request->target();
457 recorded_step.click_type = ClickTypeToString(request->type());
460 auto finalize = [&](
const absl::Status& status) {
461 recorded_step.success = response->success();
462 recorded_step.message = response->message();
463 recorded_step.execution_time_ms = response->execution_time_ms();
464 recorded_step.test_id = response->test_id();
465 MaybeRecordStep(&test_recorder_, recorded_step);
469 if (!test_manager_) {
470 response->set_success(
false);
471 response->set_message(
"TestManager not available");
472 response->set_execution_time_ms(0);
473 return finalize(absl::FailedPreconditionError(
"TestManager not available"));
476 const std::string test_id = test_manager_->RegisterHarnessTest(
477 absl::StrFormat(
"Click: %s", request->target()),
"grpc");
478 response->set_test_id(test_id);
479 recorded_step.test_id = test_id;
480 test_manager_->AppendHarnessTestLog(
481 test_id, absl::StrCat(
"Queued click request: ", request->target()));
483#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
484 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
486 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
487 std::chrono::steady_clock::now() - start);
488 std::string message =
"ImGuiTestEngine not initialized";
489 response->set_success(
false);
490 response->set_message(message);
491 response->set_execution_time_ms(elapsed.count());
492 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
494 return finalize(absl::OkStatus());
497 std::string target = request->target();
498 size_t colon_pos = target.find(
':');
499 if (colon_pos == std::string::npos) {
500 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
501 std::chrono::steady_clock::now() - start);
502 std::string message =
503 "Invalid target format. Use 'type:label' (e.g. 'button:Open ROM')";
504 response->set_success(
false);
505 response->set_message(message);
506 response->set_execution_time_ms(elapsed.count());
507 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
509 test_manager_->AppendHarnessTestLog(test_id, message);
510 return finalize(absl::OkStatus());
513 std::string widget_type = target.substr(0, colon_pos);
514 std::string widget_label = target.substr(colon_pos + 1);
516 ImGuiMouseButton mouse_button = ImGuiMouseButton_Left;
517 switch (request->type()) {
518 case ClickRequest::CLICK_TYPE_UNSPECIFIED:
519 case ClickRequest::CLICK_TYPE_LEFT:
520 mouse_button = ImGuiMouseButton_Left;
522 case ClickRequest::CLICK_TYPE_RIGHT:
523 mouse_button = ImGuiMouseButton_Right;
525 case ClickRequest::CLICK_TYPE_MIDDLE:
526 mouse_button = ImGuiMouseButton_Middle;
528 case ClickRequest::CLICK_TYPE_DOUBLE:
533 auto test_data = std::make_shared<DynamicTestData>();
534 TestManager* manager = test_manager_;
535 test_data->test_func = [manager, captured_id = test_id, widget_type,
536 widget_label, click_type = request->type(),
537 mouse_button](ImGuiTestContext* ctx) {
538 manager->MarkHarnessTestRunning(captured_id);
540 if (click_type == ClickRequest::CLICK_TYPE_DOUBLE) {
541 ctx->ItemDoubleClick(widget_label.c_str());
543 ctx->ItemClick(widget_label.c_str(), mouse_button);
546 const std::string success_message =
547 absl::StrFormat(
"Clicked %s '%s'", widget_type, widget_label);
548 manager->AppendHarnessTestLog(captured_id, success_message);
549 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kPassed,
551 }
catch (
const std::exception& e) {
552 const std::string error_message =
553 absl::StrFormat(
"Click failed: %s", e.what());
554 manager->AppendHarnessTestLog(captured_id, error_message);
555 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
560 std::string test_name = absl::StrFormat(
562 static_cast<long long>(
563 std::chrono::system_clock::now().time_since_epoch().count()));
565 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
566 test->TestFunc = RunDynamicTest;
567 test->UserData = test_data.get();
569 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
570 KeepDynamicTestData(test_data);
572 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
573 std::chrono::steady_clock::now() - start);
574 std::string message =
575 absl::StrFormat(
"Queued click on %s '%s'", widget_type, widget_label);
576 response->set_success(
true);
577 response->set_message(message);
578 response->set_execution_time_ms(elapsed.count());
579 test_manager_->AppendHarnessTestLog(test_id, message);
582 std::string target = request->target();
583 size_t colon_pos = target.find(
':');
584 if (colon_pos == std::string::npos) {
585 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
586 std::chrono::steady_clock::now() - start);
587 std::string message =
"Invalid target format. Use 'type:label'";
588 response->set_success(
false);
589 response->set_message(message);
590 response->set_execution_time_ms(elapsed.count());
591 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
593 test_manager_->AppendHarnessTestLog(test_id, message);
594 return finalize(absl::OkStatus());
597 std::string widget_type = target.substr(0, colon_pos);
598 std::string widget_label = target.substr(colon_pos + 1);
599 std::string message =
600 absl::StrFormat(
"[STUB] Clicked %s '%s' (ImGuiTestEngine not available)",
601 widget_type, widget_label);
603 test_manager_->MarkHarnessTestRunning(test_id);
604 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kPassed,
606 test_manager_->AppendHarnessTestLog(test_id, message);
608 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
609 std::chrono::steady_clock::now() - start);
610 response->set_success(
true);
611 response->set_message(message);
612 response->set_execution_time_ms(elapsed.count());
615 return finalize(absl::OkStatus());
618absl::Status ImGuiTestHarnessServiceImpl::Type(
const TypeRequest* request,
619 TypeResponse* response) {
620 auto start = std::chrono::steady_clock::now();
622 TestRecorder::RecordedStep recorded_step;
625 recorded_step.target = request->target();
626 recorded_step.text = request->text();
627 recorded_step.clear_first = request->clear_first();
630 auto finalize = [&](
const absl::Status& status) {
631 recorded_step.success = response->success();
632 recorded_step.message = response->message();
633 recorded_step.execution_time_ms = response->execution_time_ms();
634 recorded_step.test_id = response->test_id();
635 MaybeRecordStep(&test_recorder_, recorded_step);
639 if (!test_manager_) {
640 response->set_success(
false);
641 response->set_message(
"TestManager not available");
642 response->set_execution_time_ms(0);
643 return finalize(absl::FailedPreconditionError(
"TestManager not available"));
646 const std::string test_id = test_manager_->RegisterHarnessTest(
647 absl::StrFormat(
"Type: %s", request->target()),
"grpc");
648 response->set_test_id(test_id);
649 recorded_step.test_id = test_id;
650 test_manager_->AppendHarnessTestLog(
651 test_id, absl::StrFormat(
"Queued type request: %s", request->target()));
653#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
654 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
656 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
657 std::chrono::steady_clock::now() - start);
658 std::string message =
"ImGuiTestEngine not initialized";
659 response->set_success(
false);
660 response->set_message(message);
661 response->set_execution_time_ms(elapsed.count());
662 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
664 return finalize(absl::OkStatus());
667 std::string target = request->target();
668 size_t colon_pos = target.find(
':');
669 if (colon_pos == std::string::npos) {
670 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
671 std::chrono::steady_clock::now() - start);
672 std::string message =
673 "Invalid target format. Use 'type:label' (e.g. 'input:Filename')";
674 response->set_success(
false);
675 response->set_message(message);
676 response->set_execution_time_ms(elapsed.count());
677 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
679 test_manager_->AppendHarnessTestLog(test_id, message);
680 return finalize(absl::OkStatus());
683 std::string widget_type = target.substr(0, colon_pos);
684 std::string widget_label = target.substr(colon_pos + 1);
685 std::string text = request->text();
686 bool clear_first = request->clear_first();
688 auto rpc_state = std::make_shared<RPCState<bool>>();
689 auto test_data = std::make_shared<DynamicTestData>();
690 TestManager* manager = test_manager_;
691 test_data->test_func = [manager, captured_id = test_id, widget_type,
692 widget_label, clear_first, text,
693 rpc_state](ImGuiTestContext* ctx) {
694 manager->MarkHarnessTestRunning(captured_id);
696 ImGuiTestItemInfo item = ctx->ItemInfo(widget_label.c_str());
698 std::string error_message =
699 absl::StrFormat(
"Input field '%s' not found", widget_label);
700 manager->AppendHarnessTestLog(captured_id, error_message);
701 manager->MarkHarnessTestCompleted(
702 captured_id, HarnessTestStatus::kFailed, error_message);
703 rpc_state->SetResult(
false, error_message);
707 ctx->ItemClick(widget_label.c_str());
709 ctx->KeyPress(ImGuiMod_Shortcut | ImGuiKey_A);
710 ctx->KeyPress(ImGuiKey_Delete);
713 ctx->ItemInputValue(widget_label.c_str(), text.c_str());
715 std::string success_message =
716 absl::StrFormat(
"Typed '%s' into %s '%s'%s", text, widget_type,
717 widget_label, clear_first ?
" (cleared first)" :
"");
718 manager->AppendHarnessTestLog(captured_id, success_message);
719 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kPassed,
721 rpc_state->SetResult(
true, success_message);
722 }
catch (
const std::exception& e) {
723 std::string error_message = absl::StrFormat(
"Type failed: %s", e.what());
724 manager->AppendHarnessTestLog(captured_id, error_message);
725 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
727 rpc_state->SetResult(
false, error_message);
731 std::string test_name = absl::StrFormat(
733 static_cast<long long>(
734 std::chrono::system_clock::now().time_since_epoch().count()));
736 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
737 test->TestFunc = RunDynamicTest;
738 test->UserData = test_data.get();
740 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
741 KeepDynamicTestData(test_data);
743 auto timeout = std::chrono::seconds(5);
744 auto wait_start = std::chrono::steady_clock::now();
745 while (!rpc_state->completed.load()) {
746 if (std::chrono::steady_clock::now() - wait_start > timeout) {
747 std::string error_message =
748 "Test timeout - input field not found or unresponsive";
749 manager->AppendHarnessTestLog(test_id, error_message);
750 manager->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kTimeout,
752 rpc_state->SetResult(
false, error_message);
755 std::this_thread::sleep_for(std::chrono::milliseconds(100));
758 bool success =
false;
760 rpc_state->GetResult(success, message);
761 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
762 std::chrono::steady_clock::now() - start);
764 response->set_success(success);
765 response->set_message(message);
766 response->set_execution_time_ms(elapsed.count());
767 if (!message.empty()) {
768 test_manager_->AppendHarnessTestLog(test_id, message);
772 test_manager_->MarkHarnessTestRunning(test_id);
773 std::string message = absl::StrFormat(
774 "[STUB] Typed '%s' into %s (ImGuiTestEngine not available)",
775 request->text(), request->target());
776 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kPassed,
778 test_manager_->AppendHarnessTestLog(test_id, message);
780 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
781 std::chrono::steady_clock::now() - start);
782 response->set_success(
true);
783 response->set_message(message);
784 response->set_execution_time_ms(elapsed.count());
787 return finalize(absl::OkStatus());
790absl::Status ImGuiTestHarnessServiceImpl::Wait(
const WaitRequest* request,
791 WaitResponse* response) {
792 auto start = std::chrono::steady_clock::now();
794 TestRecorder::RecordedStep recorded_step;
797 recorded_step.condition = request->condition();
798 recorded_step.timeout_ms = request->timeout_ms();
801 auto finalize = [&](
const absl::Status& status) {
802 recorded_step.success = response->success();
803 recorded_step.message = response->message();
804 recorded_step.execution_time_ms = response->elapsed_ms();
805 recorded_step.test_id = response->test_id();
806 MaybeRecordStep(&test_recorder_, recorded_step);
810 if (!test_manager_) {
811 response->set_success(
false);
812 response->set_message(
"TestManager not available");
813 response->set_elapsed_ms(0);
814 return finalize(absl::FailedPreconditionError(
"TestManager not available"));
817 const std::string test_id = test_manager_->RegisterHarnessTest(
818 absl::StrFormat(
"Wait: %s", request->condition()),
"grpc");
819 response->set_test_id(test_id);
820 recorded_step.test_id = test_id;
821 test_manager_->AppendHarnessTestLog(
823 absl::StrFormat(
"Queued wait condition: %s", request->condition()));
825#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
826 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
828 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
829 std::chrono::steady_clock::now() - start);
830 std::string message =
"ImGuiTestEngine not initialized";
831 response->set_success(
false);
832 response->set_message(message);
833 response->set_elapsed_ms(elapsed.count());
834 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
836 return finalize(absl::OkStatus());
839 std::string condition = request->condition();
840 size_t colon_pos = condition.find(
':');
841 if (colon_pos == std::string::npos) {
842 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
843 std::chrono::steady_clock::now() - start);
844 std::string message =
845 "Invalid condition format. Use 'type:target' (e.g. "
846 "'window_visible:Overworld Editor')";
847 response->set_success(
false);
848 response->set_message(message);
849 response->set_elapsed_ms(elapsed.count());
850 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
852 test_manager_->AppendHarnessTestLog(test_id, message);
853 return finalize(absl::OkStatus());
856 std::string condition_type = condition.substr(0, colon_pos);
857 std::string condition_target = condition.substr(colon_pos + 1);
858 int timeout_ms = request->timeout_ms() > 0 ? request->timeout_ms() : 5000;
859 int poll_interval_ms =
860 request->poll_interval_ms() > 0 ? request->poll_interval_ms() : 100;
862 auto test_data = std::make_shared<DynamicTestData>();
863 TestManager* manager = test_manager_;
864 test_data->test_func = [manager, captured_id = test_id, condition_type,
865 condition_target, timeout_ms,
866 poll_interval_ms](ImGuiTestContext* ctx) {
867 manager->MarkHarnessTestRunning(captured_id);
868 auto poll_start = std::chrono::steady_clock::now();
869 auto timeout = std::chrono::milliseconds(timeout_ms);
871 for (
int i = 0; i < 10; ++i) {
876 while (std::chrono::steady_clock::now() - poll_start < timeout) {
877 bool current_state =
false;
879 if (condition_type ==
"window_visible") {
880 ImGuiTestItemInfo window_info = ctx->WindowInfo(
881 condition_target.c_str(), ImGuiTestOpFlags_NoError);
882 current_state = (window_info.ID != 0);
883 }
else if (condition_type ==
"element_visible") {
884 ImGuiTestItemInfo item =
885 ctx->ItemInfo(condition_target.c_str(), ImGuiTestOpFlags_NoError);
886 current_state = (item.ID != 0 && item.RectClipped.GetWidth() > 0 &&
887 item.RectClipped.GetHeight() > 0);
888 }
else if (condition_type ==
"element_enabled") {
889 ImGuiTestItemInfo item =
890 ctx->ItemInfo(condition_target.c_str(), ImGuiTestOpFlags_NoError);
892 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
894 std::string error_message =
895 absl::StrFormat(
"Unknown condition type: %s", condition_type);
896 manager->AppendHarnessTestLog(captured_id, error_message);
897 manager->MarkHarnessTestCompleted(
898 captured_id, HarnessTestStatus::kFailed, error_message);
904 std::chrono::duration_cast<std::chrono::milliseconds>(
905 std::chrono::steady_clock::now() - poll_start);
906 std::string success_message = absl::StrFormat(
907 "Condition '%s:%s' met after %lld ms", condition_type,
908 condition_target,
static_cast<long long>(elapsed_ms.count()));
909 manager->AppendHarnessTestLog(captured_id, success_message);
910 manager->MarkHarnessTestCompleted(
911 captured_id, HarnessTestStatus::kPassed, success_message);
915 std::this_thread::sleep_for(
916 std::chrono::milliseconds(poll_interval_ms));
920 std::string timeout_message =
921 absl::StrFormat(
"Condition '%s:%s' not met after %d ms timeout",
922 condition_type, condition_target, timeout_ms);
923 manager->AppendHarnessTestLog(captured_id, timeout_message);
924 manager->MarkHarnessTestCompleted(
925 captured_id, HarnessTestStatus::kTimeout, timeout_message);
926 }
catch (
const std::exception& e) {
927 std::string error_message = absl::StrFormat(
"Wait failed: %s", e.what());
928 manager->AppendHarnessTestLog(captured_id, error_message);
929 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
934 std::string test_name = absl::StrFormat(
936 static_cast<long long>(
937 std::chrono::system_clock::now().time_since_epoch().count()));
939 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
940 test->TestFunc = RunDynamicTest;
941 test->UserData = test_data.get();
943 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
945 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
946 std::chrono::steady_clock::now() - start);
947 std::string message = absl::StrFormat(
"Queued wait for '%s:%s'",
948 condition_type, condition_target);
949 response->set_success(
true);
950 response->set_message(message);
951 response->set_elapsed_ms(elapsed.count());
952 test_manager_->AppendHarnessTestLog(test_id, message);
955 test_manager_->MarkHarnessTestRunning(test_id);
956 std::string message = absl::StrFormat(
957 "[STUB] Condition '%s' met (ImGuiTestEngine not available)",
958 request->condition());
959 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kPassed,
961 test_manager_->AppendHarnessTestLog(test_id, message);
963 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
964 std::chrono::steady_clock::now() - start);
965 response->set_success(
true);
966 response->set_message(message);
967 response->set_elapsed_ms(elapsed.count());
970 return finalize(absl::OkStatus());
973absl::Status ImGuiTestHarnessServiceImpl::Assert(
const AssertRequest* request,
974 AssertResponse* response) {
975 TestRecorder::RecordedStep recorded_step;
978 recorded_step.condition = request->condition();
981 auto finalize = [&](
const absl::Status& status) {
982 recorded_step.success = response->success();
983 recorded_step.message = response->message();
984 recorded_step.expected_value = response->expected_value();
985 recorded_step.actual_value = response->actual_value();
986 recorded_step.test_id = response->test_id();
987 MaybeRecordStep(&test_recorder_, recorded_step);
991 if (!test_manager_) {
992 response->set_success(
false);
993 response->set_message(
"TestManager not available");
994 response->set_actual_value(
"N/A");
995 response->set_expected_value(
"N/A");
996 return finalize(absl::FailedPreconditionError(
"TestManager not available"));
999 const std::string test_id = test_manager_->RegisterHarnessTest(
1000 absl::StrFormat(
"Assert: %s", request->condition()),
"grpc");
1001 response->set_test_id(test_id);
1002 recorded_step.test_id = test_id;
1003 test_manager_->AppendHarnessTestLog(
1004 test_id, absl::StrFormat(
"Queued assertion: %s", request->condition()));
1006#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
1007 ImGuiTestEngine* engine = test_manager_->GetUITestEngine();
1009 std::string message =
"ImGuiTestEngine not initialized";
1010 response->set_success(
false);
1011 response->set_message(message);
1012 response->set_actual_value(
"N/A");
1013 response->set_expected_value(
"N/A");
1014 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1016 return finalize(absl::OkStatus());
1019 std::string condition = request->condition();
1020 size_t colon_pos = condition.find(
':');
1021 if (colon_pos == std::string::npos) {
1022 std::string message =
1023 "Invalid condition format. Use 'type:target' (e.g. 'visible:Main "
1025 response->set_success(
false);
1026 response->set_message(message);
1027 response->set_actual_value(
"N/A");
1028 response->set_expected_value(
"N/A");
1029 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kFailed,
1031 test_manager_->AppendHarnessTestLog(test_id, message);
1032 return finalize(absl::OkStatus());
1035 std::string assertion_type = condition.substr(0, colon_pos);
1036 std::string assertion_target = condition.substr(colon_pos + 1);
1038 auto test_data = std::make_shared<DynamicTestData>();
1039 TestManager* manager = test_manager_;
1040 test_data->test_func = [manager, captured_id = test_id, assertion_type,
1041 assertion_target](ImGuiTestContext* ctx) {
1042 manager->MarkHarnessTestRunning(captured_id);
1044 auto complete_with = [manager, captured_id](
bool passed,
1045 const std::string& message,
1046 const std::string& actual,
1047 const std::string& expected,
1048 HarnessTestStatus status) {
1049 manager->AppendHarnessTestLog(captured_id, message);
1050 if (!actual.empty() || !expected.empty()) {
1051 manager->AppendHarnessTestLog(
1053 absl::StrFormat(
"Actual: %s | Expected: %s", actual, expected));
1055 manager->MarkHarnessTestCompleted(captured_id, status,
1056 passed ?
"" : message);
1060 bool passed =
false;
1061 std::string actual_value;
1062 std::string expected_value;
1063 std::string message;
1065 if (assertion_type ==
"visible") {
1066 ImGuiTestItemInfo window_info =
1067 ctx->WindowInfo(assertion_target.c_str(), ImGuiTestOpFlags_NoError);
1068 bool is_visible = (window_info.ID != 0);
1069 passed = is_visible;
1070 actual_value = is_visible ?
"visible" :
"hidden";
1071 expected_value =
"visible";
1073 passed ? absl::StrFormat(
"'%s' is visible", assertion_target)
1074 : absl::StrFormat(
"'%s' is not visible", assertion_target);
1075 }
else if (assertion_type ==
"enabled") {
1076 ImGuiTestItemInfo item =
1077 ctx->ItemInfo(assertion_target.c_str(), ImGuiTestOpFlags_NoError);
1079 (item.ID != 0 && !(item.ItemFlags & ImGuiItemFlags_Disabled));
1080 passed = is_enabled;
1081 actual_value = is_enabled ?
"enabled" :
"disabled";
1082 expected_value =
"enabled";
1084 passed ? absl::StrFormat(
"'%s' is enabled", assertion_target)
1085 : absl::StrFormat(
"'%s' is not enabled", assertion_target);
1086 }
else if (assertion_type ==
"exists") {
1087 ImGuiTestItemInfo item =
1088 ctx->ItemInfo(assertion_target.c_str(), ImGuiTestOpFlags_NoError);
1089 bool exists = (item.ID != 0);
1091 actual_value = exists ?
"exists" :
"not found";
1092 expected_value =
"exists";
1093 message = passed ? absl::StrFormat(
"'%s' exists", assertion_target)
1094 : absl::StrFormat(
"'%s' not found", assertion_target);
1095 }
else if (assertion_type ==
"text_contains") {
1096 size_t second_colon = assertion_target.find(
':');
1097 if (second_colon == std::string::npos) {
1098 std::string error_message =
1099 "text_contains requires format "
1100 "'text_contains:target:expected_text'";
1101 complete_with(
false, error_message,
"N/A",
"N/A",
1102 HarnessTestStatus::kFailed);
1106 std::string input_target = assertion_target.substr(0, second_colon);
1107 std::string expected_text = assertion_target.substr(second_colon + 1);
1109 ImGuiTestItemInfo item = ctx->ItemInfo(input_target.c_str());
1111 std::string actual_text =
"(text_retrieval_not_fully_implemented)";
1112 passed = actual_text.find(expected_text) != std::string::npos;
1113 actual_value = actual_text;
1114 expected_value = absl::StrFormat(
"contains '%s'", expected_text);
1115 message = passed ? absl::StrFormat(
"'%s' contains '%s'", input_target,
1118 "'%s' does not contain '%s' (actual: '%s')",
1119 input_target, expected_text, actual_text);
1122 actual_value =
"not found";
1123 expected_value = expected_text;
1124 message = absl::StrFormat(
"Input '%s' not found", input_target);
1127 std::string error_message =
1128 absl::StrFormat(
"Unknown assertion type: %s", assertion_type);
1129 complete_with(
false, error_message,
"N/A",
"N/A",
1130 HarnessTestStatus::kFailed);
1135 passed, message, actual_value, expected_value,
1136 passed ? HarnessTestStatus::kPassed : HarnessTestStatus::
kFailed);
1137 }
catch (
const std::exception& e) {
1138 std::string error_message =
1139 absl::StrFormat(
"Assertion failed: %s", e.what());
1140 manager->AppendHarnessTestLog(captured_id, error_message);
1141 manager->MarkHarnessTestCompleted(captured_id, HarnessTestStatus::kFailed,
1146 std::string test_name = absl::StrFormat(
1148 static_cast<long long>(
1149 std::chrono::system_clock::now().time_since_epoch().count()));
1151 ImGuiTest* test = IM_REGISTER_TEST(engine,
"grpc", test_name.c_str());
1152 test->TestFunc = RunDynamicTest;
1153 test->UserData = test_data.get();
1155 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromGui);
1157 response->set_success(
true);
1158 std::string message = absl::StrFormat(
"Queued assertion for '%s:%s'",
1159 assertion_type, assertion_target);
1160 response->set_message(message);
1161 response->set_actual_value(
"(async)");
1162 response->set_expected_value(
"(async)");
1163 test_manager_->AppendHarnessTestLog(test_id, message);
1166 test_manager_->MarkHarnessTestRunning(test_id);
1167 std::string message = absl::StrFormat(
1168 "[STUB] Assertion '%s' passed (ImGuiTestEngine not available)",
1169 request->condition());
1170 test_manager_->MarkHarnessTestCompleted(test_id, HarnessTestStatus::kPassed,
1172 test_manager_->AppendHarnessTestLog(test_id, message);
1174 response->set_success(
true);
1175 response->set_message(message);
1176 response->set_actual_value(
"(stub)");
1177 response->set_expected_value(
"(stub)");
1180 return finalize(absl::OkStatus());
1183absl::Status ImGuiTestHarnessServiceImpl::Screenshot(
1184 const ScreenshotRequest* request, ScreenshotResponse* response) {
1186 return absl::InvalidArgumentError(
"response cannot be null");
1189 const std::string requested_path =
1190 request ? request->output_path() : std::string();
1195 std::atomic<bool> done{
false};
1196 absl::StatusOr<ScreenshotArtifact> result =
1197 absl::UnknownError(
"Not captured");
1199 auto state = std::make_shared<State>();
1202 {.preferred_path = requested_path,
1203 .callback = [state](absl::StatusOr<ScreenshotArtifact> result) {
1204 state->result = std::move(result);
1205 state->done.store(
true);
1209 auto start = std::chrono::steady_clock::now();
1210 while (!state->done.load()) {
1211 if (std::chrono::steady_clock::now() - start > std::chrono::seconds(5)) {
1212 return absl::DeadlineExceededError(
1213 "Timed out waiting for screenshot capture on main thread");
1215 std::this_thread::sleep_for(std::chrono::milliseconds(10));
1218 if (!state->result.ok()) {
1219 response->set_success(
false);
1220 response->set_message(std::string(state->result.status().message()));
1221 return state->result.status();
1224 const ScreenshotArtifact& artifact = *state->result;
1225 response->set_success(
true);
1226 response->set_message(absl::StrFormat(
"Screenshot saved to %s (%dx%d)",
1227 artifact.file_path, artifact.width,
1229 response->set_file_path(artifact.file_path);
1230 response->set_file_size_bytes(artifact.file_size_bytes);
1232 return absl::OkStatus();
1235absl::Status ImGuiTestHarnessServiceImpl::GetTestStatus(
1236 const GetTestStatusRequest* request, GetTestStatusResponse* response) {
1237 if (!test_manager_) {
1238 return absl::FailedPreconditionError(
"TestManager not available");
1241 if (request->test_id().empty()) {
1242 return absl::InvalidArgumentError(
"test_id must be provided");
1246 test_manager_->GetHarnessTestExecution(request->test_id());
1247 if (!execution_or.ok()) {
1248 response->set_status(GetTestStatusResponse::TEST_STATUS_UNSPECIFIED);
1249 response->set_error_message(std::string(execution_or.status().message()));
1250 return absl::OkStatus();
1253 const auto& execution = execution_or.value();
1254 response->set_status(ConvertHarnessStatus(execution.status));
1255 response->set_queued_at_ms(ToUnixMillisSafe(execution.queued_at));
1256 response->set_started_at_ms(ToUnixMillisSafe(execution.started_at));
1257 response->set_completed_at_ms(ToUnixMillisSafe(execution.completed_at));
1258 response->set_execution_time_ms(ClampDurationToInt32(execution.duration));
1259 if (!execution.error_message.empty()) {
1260 response->set_error_message(execution.error_message);
1262 response->clear_error_message();
1265 response->clear_assertion_failures();
1266 for (
const auto& failure : execution.assertion_failures) {
1267 response->add_assertion_failures(failure);
1270 return absl::OkStatus();
1273absl::Status ImGuiTestHarnessServiceImpl::ListTests(
1274 const ListTestsRequest* request, ListTestsResponse* response) {
1275 if (!test_manager_) {
1276 return absl::FailedPreconditionError(
"TestManager not available");
1279 if (request->page_size() < 0) {
1280 return absl::InvalidArgumentError(
"page_size cannot be negative");
1283 int page_size = request->page_size() > 0 ? request->page_size() : 100;
1284 constexpr int kMaxPageSize = 500;
1285 if (page_size > kMaxPageSize) {
1286 page_size = kMaxPageSize;
1289 size_t start_index = 0;
1290 if (!request->page_token().empty()) {
1291 int64_t token_value = 0;
1292 if (!absl::SimpleAtoi(request->page_token(), &token_value) ||
1294 return absl::InvalidArgumentError(
"Invalid page_token");
1296 start_index =
static_cast<size_t>(token_value);
1300 test_manager_->ListHarnessTestSummaries(request->category_filter());
1302 response->set_total_count(
static_cast<int32_t
>(summaries.size()));
1304 if (start_index >= summaries.size()) {
1305 response->clear_tests();
1306 response->clear_next_page_token();
1307 return absl::OkStatus();
1311 std::min(start_index +
static_cast<size_t>(page_size), summaries.size());
1313 for (
size_t i = start_index; i < end_index; ++i) {
1314 const auto& summary = summaries[i];
1315 auto* test_info = response->add_tests();
1316 const auto& exec = summary.latest_execution;
1318 test_info->set_test_id(exec.test_id);
1319 test_info->set_name(exec.name);
1320 test_info->set_category(exec.category);
1322 int64_t last_run_ms = ToUnixMillisSafe(exec.completed_at);
1323 if (last_run_ms == 0) {
1324 last_run_ms = ToUnixMillisSafe(exec.started_at);
1326 if (last_run_ms == 0) {
1327 last_run_ms = ToUnixMillisSafe(exec.queued_at);
1329 test_info->set_last_run_timestamp_ms(last_run_ms);
1331 test_info->set_total_runs(summary.total_runs);
1332 test_info->set_pass_count(summary.pass_count);
1333 test_info->set_fail_count(summary.fail_count);
1335 int32_t average_duration_ms = 0;
1336 if (summary.total_runs > 0) {
1337 absl::Duration average_duration =
1338 summary.total_duration / summary.total_runs;
1339 average_duration_ms = ClampDurationToInt32(average_duration);
1341 test_info->set_average_duration_ms(average_duration_ms);
1344 if (end_index < summaries.size()) {
1345 response->set_next_page_token(absl::StrCat(end_index));
1347 response->clear_next_page_token();
1350 return absl::OkStatus();
1353absl::Status ImGuiTestHarnessServiceImpl::GetTestResults(
1354 const GetTestResultsRequest* request, GetTestResultsResponse* response) {
1355 if (!test_manager_) {
1356 return absl::FailedPreconditionError(
"TestManager not available");
1359 if (request->test_id().empty()) {
1360 return absl::InvalidArgumentError(
"test_id must be provided");
1364 test_manager_->GetHarnessTestExecution(request->test_id());
1365 if (!execution_or.ok()) {
1366 return execution_or.status();
1369 const auto& execution = execution_or.value();
1370 response->set_success(execution.status == HarnessTestStatus::kPassed);
1371 response->set_test_name(execution.name);
1372 response->set_category(execution.category);
1374 int64_t executed_at_ms = ToUnixMillisSafe(execution.completed_at);
1375 if (executed_at_ms == 0) {
1376 executed_at_ms = ToUnixMillisSafe(execution.started_at);
1378 if (executed_at_ms == 0) {
1379 executed_at_ms = ToUnixMillisSafe(execution.queued_at);
1381 response->set_executed_at_ms(executed_at_ms);
1382 response->set_duration_ms(ClampDurationToInt32(execution.duration));
1384 response->clear_assertions();
1385 if (!execution.assertion_failures.empty()) {
1386 for (
const auto& failure : execution.assertion_failures) {
1387 auto* assertion = response->add_assertions();
1388 assertion->set_description(failure);
1389 assertion->set_passed(
false);
1390 assertion->set_error_message(failure);
1392 }
else if (!execution.error_message.empty()) {
1393 auto* assertion = response->add_assertions();
1394 assertion->set_description(
"Execution error");
1395 assertion->set_passed(
false);
1396 assertion->set_error_message(execution.error_message);
1399 if (request->include_logs()) {
1400 for (
const auto& log_entry : execution.logs) {
1401 response->add_logs(log_entry);
1405 auto* metrics_map = response->mutable_metrics();
1406 for (
const auto& [key, value] : execution.metrics) {
1407 (*metrics_map)[
key] = value;
1411 if (!execution.screenshot_path.empty()) {
1412 response->set_screenshot_path(execution.screenshot_path);
1413 response->set_screenshot_size_bytes(execution.screenshot_size_bytes);
1415 if (!execution.failure_context.empty()) {
1416 response->set_failure_context(execution.failure_context);
1418 if (!execution.widget_state.empty()) {
1419 response->set_widget_state(execution.widget_state);
1422 return absl::OkStatus();
1425absl::Status ImGuiTestHarnessServiceImpl::DiscoverWidgets(
1426 const DiscoverWidgetsRequest* request, DiscoverWidgetsResponse* response) {
1428 return absl::InvalidArgumentError(
"request cannot be null");
1431 return absl::InvalidArgumentError(
"response cannot be null");
1434 if (!test_manager_) {
1435 return absl::FailedPreconditionError(
"TestManager not available");
1438 widget_discovery_service_.CollectWidgets(
nullptr, *request, response);
1439 return absl::OkStatus();
1442absl::Status ImGuiTestHarnessServiceImpl::StartRecording(
1443 const StartRecordingRequest* request, StartRecordingResponse* response) {
1445 return absl::InvalidArgumentError(
"request cannot be null");
1448 return absl::InvalidArgumentError(
"response cannot be null");
1451 TestRecorder::RecordingOptions options;
1452 options.output_path = request->output_path();
1453 options.session_name = request->session_name();
1454 options.description = request->description();
1456 if (options.output_path.empty()) {
1457 response->set_success(
false);
1458 response->set_message(
"output_path is required to start recording");
1459 return absl::InvalidArgumentError(
"output_path cannot be empty");
1462 absl::StatusOr<std::string> recording_id = test_recorder_.Start(options);
1463 if (!recording_id.ok()) {
1464 response->set_success(
false);
1465 response->set_message(std::string(recording_id.status().message()));
1466 return recording_id.status();
1469 response->set_success(
true);
1470 response->set_message(
"Recording started");
1471 response->set_recording_id(*recording_id);
1472 response->set_started_at_ms(absl::ToUnixMillis(absl::Now()));
1473 return absl::OkStatus();
1476absl::Status ImGuiTestHarnessServiceImpl::StopRecording(
1477 const StopRecordingRequest* request, StopRecordingResponse* response) {
1479 return absl::InvalidArgumentError(
"request cannot be null");
1482 return absl::InvalidArgumentError(
"response cannot be null");
1485 absl::StatusOr<TestRecorder::StopRecordingSummary> summary =
1486 test_recorder_.Stop(request->recording_id(), request->discard());
1487 if (!summary.ok()) {
1488 response->set_success(
false);
1489 response->set_message(std::string(summary.status().message()));
1490 return summary.status();
1493 response->set_success(
true);
1494 if (summary->saved) {
1495 response->set_message(
"Recording saved");
1497 response->set_message(
"Recording discarded");
1499 response->set_output_path(summary->output_path);
1500 response->set_step_count(summary->step_count);
1501 response->set_duration_ms(absl::ToInt64Milliseconds(summary->duration));
1502 return absl::OkStatus();
1505absl::Status ImGuiTestHarnessServiceImpl::ReplayTest(
1506 const ReplayTestRequest* request, ReplayTestResponse* response) {
1508 return absl::InvalidArgumentError(
"request cannot be null");
1511 return absl::InvalidArgumentError(
"response cannot be null");
1514 response->clear_logs();
1515 response->clear_assertions();
1517 if (request->script_path().empty()) {
1518 response->set_success(
false);
1519 response->set_message(
"script_path is required");
1520 return absl::InvalidArgumentError(
"script_path cannot be empty");
1523 absl::StatusOr<TestScript> script_or =
1525 if (!script_or.ok()) {
1526 response->set_success(
false);
1527 response->set_message(std::string(script_or.status().message()));
1528 return script_or.status();
1530 TestScript script = std::move(*script_or);
1532 absl::flat_hash_map<std::string, std::string> overrides;
1533 for (
const auto& entry : request->parameter_overrides()) {
1534 overrides[entry.first] = entry.second;
1537 response->set_replay_session_id(absl::StrFormat(
1539 absl::FormatTime(
"%Y%m%dT%H%M%S", absl::Now(), absl::UTCTimeZone())));
1541 auto suspension = test_recorder_.Suspend();
1543 std::vector<std::string> logs;
1544 logs.reserve(script.steps.size() * 2 + 4);
1546 bool overall_success =
true;
1547 std::string overall_message =
"Replay completed successfully";
1548 int steps_executed = 0;
1549 absl::Status overall_rpc_status = absl::OkStatus();
1551 for (
const auto& step : script.steps) {
1553 std::string action_label =
1554 absl::StrFormat(
"Step %d: %s", steps_executed, step.action);
1555 logs.push_back(action_label);
1557 absl::Status status = absl::OkStatus();
1558 bool step_success =
false;
1559 std::string step_message;
1560 HarnessTestExecution execution;
1561 bool have_execution =
false;
1563 if (step.action ==
"click") {
1564 ClickRequest sub_request;
1565 sub_request.set_target(ApplyOverrides(step.target, overrides));
1566 sub_request.set_type(ClickTypeFromString(step.click_type));
1567 ClickResponse sub_response;
1568 status = Click(&sub_request, &sub_response);
1569 step_success = sub_response.success();
1570 step_message = sub_response.message();
1571 if (status.ok() && !sub_response.test_id().empty()) {
1572 absl::Status wait_status = WaitForHarnessTestCompletion(
1573 test_manager_, sub_response.test_id(), &execution);
1574 if (wait_status.ok()) {
1575 have_execution =
true;
1576 if (!execution.error_message.empty()) {
1577 step_message = execution.error_message;
1580 status = wait_status;
1581 step_success =
false;
1582 step_message = std::string(wait_status.message());
1585 }
else if (step.action ==
"type") {
1586 TypeRequest sub_request;
1587 sub_request.set_target(ApplyOverrides(step.target, overrides));
1588 sub_request.set_text(ApplyOverrides(step.text, overrides));
1589 sub_request.set_clear_first(step.clear_first);
1590 TypeResponse sub_response;
1591 status = Type(&sub_request, &sub_response);
1592 step_success = sub_response.success();
1593 step_message = sub_response.message();
1594 if (status.ok() && !sub_response.test_id().empty()) {
1595 absl::Status wait_status = WaitForHarnessTestCompletion(
1596 test_manager_, sub_response.test_id(), &execution);
1597 if (wait_status.ok()) {
1598 have_execution =
true;
1599 if (!execution.error_message.empty()) {
1600 step_message = execution.error_message;
1603 status = wait_status;
1604 step_success =
false;
1605 step_message = std::string(wait_status.message());
1608 }
else if (step.action ==
"wait") {
1609 WaitRequest sub_request;
1610 sub_request.set_condition(ApplyOverrides(step.condition, overrides));
1611 if (step.timeout_ms > 0) {
1612 sub_request.set_timeout_ms(step.timeout_ms);
1614 WaitResponse sub_response;
1615 status = Wait(&sub_request, &sub_response);
1616 step_success = sub_response.success();
1617 step_message = sub_response.message();
1618 if (status.ok() && !sub_response.test_id().empty()) {
1619 absl::Status wait_status = WaitForHarnessTestCompletion(
1620 test_manager_, sub_response.test_id(), &execution);
1621 if (wait_status.ok()) {
1622 have_execution =
true;
1623 if (!execution.error_message.empty()) {
1624 step_message = execution.error_message;
1627 status = wait_status;
1628 step_success =
false;
1629 step_message = std::string(wait_status.message());
1632 }
else if (step.action ==
"assert") {
1633 AssertRequest sub_request;
1634 sub_request.set_condition(ApplyOverrides(step.condition, overrides));
1635 AssertResponse sub_response;
1636 status = Assert(&sub_request, &sub_response);
1637 step_success = sub_response.success();
1638 step_message = sub_response.message();
1639 if (status.ok() && !sub_response.test_id().empty()) {
1640 absl::Status wait_status = WaitForHarnessTestCompletion(
1641 test_manager_, sub_response.test_id(), &execution);
1642 if (wait_status.ok()) {
1643 have_execution =
true;
1644 if (!execution.error_message.empty()) {
1645 step_message = execution.error_message;
1648 status = wait_status;
1649 step_success =
false;
1650 step_message = std::string(wait_status.message());
1653 }
else if (step.action ==
"screenshot") {
1654 ScreenshotRequest sub_request;
1655 sub_request.set_window_title(ApplyOverrides(step.target, overrides));
1656 if (!step.region.empty()) {
1657 sub_request.set_output_path(ApplyOverrides(step.region, overrides));
1659 ScreenshotResponse sub_response;
1660 status = Screenshot(&sub_request, &sub_response);
1661 step_success = sub_response.success();
1662 step_message = sub_response.message();
1664 status = absl::InvalidArgumentError(
1665 absl::StrFormat(
"Unsupported action '%s'", step.action));
1667 std::string(status.message().data(), status.message().size());
1670 auto* assertion = response->add_assertions();
1671 assertion->set_description(
1672 absl::StrFormat(
"Step %d (%s)", steps_executed, step.action));
1675 assertion->set_passed(
false);
1676 assertion->set_error_message(
1677 std::string(status.message().data(), status.message().size()));
1678 overall_success =
false;
1679 overall_message = step_message;
1680 logs.push_back(absl::StrFormat(
" Error: %s", status.message()));
1681 overall_rpc_status = status;
1685 bool expectations_met = (step_success == step.expect_success);
1686 std::string expectation_error;
1688 if (!expectations_met) {
1690 absl::StrFormat(
"Expected success=%s but got %s",
1691 step.expect_success ?
"true" :
"false",
1692 step_success ?
"true" :
"false");
1695 if (!step.expect_status.empty()) {
1696 HarnessTestStatus expected_status =
1697 ::yaze::test::HarnessStatusFromString(step.expect_status);
1698 if (!have_execution) {
1699 expectations_met =
false;
1700 if (!expectation_error.empty()) {
1701 expectation_error.append(
"; ");
1703 expectation_error.append(
"No execution details available");
1704 }
else if (expected_status != HarnessTestStatus::kUnspecified &&
1705 execution.status != expected_status) {
1706 expectations_met =
false;
1707 if (!expectation_error.empty()) {
1708 expectation_error.append(
"; ");
1710 expectation_error.append(absl::StrFormat(
1711 "Expected status %s but observed %s", step.expect_status,
1712 ::yaze::test::HarnessStatusToString(execution.status)));
1714 if (have_execution) {
1715 assertion->set_actual_value(
1716 ::yaze::test::HarnessStatusToString(execution.status));
1717 assertion->set_expected_value(step.expect_status);
1721 if (!step.expect_message.empty()) {
1722 std::string actual_message = step_message;
1723 if (have_execution && !execution.error_message.empty()) {
1724 actual_message = execution.error_message;
1726 if (actual_message.find(step.expect_message) == std::string::npos) {
1727 expectations_met =
false;
1728 if (!expectation_error.empty()) {
1729 expectation_error.append(
"; ");
1731 expectation_error.append(
1732 absl::StrFormat(
"Expected message containing '%s' but got '%s'",
1733 step.expect_message, actual_message));
1737 if (!expectations_met) {
1738 assertion->set_passed(
false);
1739 assertion->set_error_message(expectation_error);
1740 overall_success =
false;
1741 overall_message = expectation_error;
1743 absl::StrFormat(
" Failed expectations: %s", expectation_error));
1744 if (request->ci_mode()) {
1748 assertion->set_passed(
true);
1749 logs.push_back(absl::StrFormat(
" Result: %s", step_message));
1752 if (have_execution && !execution.assertion_failures.empty()) {
1753 for (
const auto& failure : execution.assertion_failures) {
1754 logs.push_back(absl::StrFormat(
" Assertion failure: %s", failure));
1758 if (!overall_success && request->ci_mode()) {
1763 response->set_steps_executed(steps_executed);
1764 response->set_success(overall_success);
1765 response->set_message(overall_message);
1766 for (
const auto& log_entry : logs) {
1767 response->add_logs(log_entry);
1770 return overall_rpc_status;
1777ImGuiTestHarnessServer& ImGuiTestHarnessServer::Instance() {
1778 static ImGuiTestHarnessServer* instance =
new ImGuiTestHarnessServer();
1782ImGuiTestHarnessServer::~ImGuiTestHarnessServer() {
1786absl::Status ImGuiTestHarnessServer::Start(
int port,
1787 TestManager* test_manager) {
1789 return absl::FailedPreconditionError(
"Server already running");
1792 if (!test_manager) {
1793 return absl::InvalidArgumentError(
"TestManager cannot be null");
1797 service_ = std::make_unique<ImGuiTestHarnessServiceImpl>(test_manager);
1801 grpc_service_ = std::make_unique<ImGuiTestHarnessServiceGrpc>(service_.get());
1803 std::string server_address = absl::StrFormat(
"0.0.0.0:%d", port);
1805 grpc::ServerBuilder builder;
1808 builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
1811 builder.RegisterService(grpc_service_.get());
1814 server_ = builder.BuildAndStart();
1817 return absl::InternalError(
1818 absl::StrFormat(
"Failed to start gRPC server on %s", server_address));
1823 std::cout <<
"✓ ImGuiTestHarness gRPC server listening on " << server_address
1824 <<
" (with TestManager integration)\n";
1825 std::cout <<
" Use 'grpcurl -plaintext -d '{\"message\":\"test\"}' "
1826 << server_address <<
" yaze.test.ImGuiTestHarness/Ping' to test\n";
1828 return absl::OkStatus();
1831void ImGuiTestHarnessServer::Shutdown() {
1833 std::cout <<
"⏹ Shutting down ImGuiTestHarness gRPC server...\n";
1834 server_->Shutdown();
1838 std::cout <<
"✓ ImGuiTestHarness gRPC server stopped\n";
Controller * GetController()
static Application & Instance()
void RequestScreenshot(const ScreenshotRequest &request)
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.