yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
gui_automation_client.cc
Go to the documentation of this file.
1// gui_automation_client.cc
2// Implementation of gRPC client for YAZE GUI automation
3
5
6#include "absl/strings/str_cat.h"
7#include "absl/strings/str_format.h"
8#include "absl/time/time.h"
9
10#include <utility>
11
12namespace yaze {
13namespace cli {
14
15namespace {
16
17#ifdef YAZE_WITH_GRPC
18std::optional<absl::Time> OptionalTimeFromMillis(int64_t millis) {
19 if (millis <= 0) {
20 return std::nullopt;
21 }
22 return absl::FromUnixMillis(millis);
23}
24
25yaze::test::WidgetType ConvertWidgetTypeFilterToProto(WidgetTypeFilter filter) {
26 using ProtoType = yaze::test::WidgetType;
27 switch (filter) {
29 return ProtoType::WIDGET_TYPE_ALL;
31 return ProtoType::WIDGET_TYPE_BUTTON;
33 return ProtoType::WIDGET_TYPE_INPUT;
35 return ProtoType::WIDGET_TYPE_MENU;
37 return ProtoType::WIDGET_TYPE_TAB;
39 return ProtoType::WIDGET_TYPE_CHECKBOX;
41 return ProtoType::WIDGET_TYPE_SLIDER;
43 return ProtoType::WIDGET_TYPE_CANVAS;
45 return ProtoType::WIDGET_TYPE_SELECTABLE;
47 return ProtoType::WIDGET_TYPE_OTHER;
49 default:
50 return ProtoType::WIDGET_TYPE_UNSPECIFIED;
51 }
52}
53
54TestRunStatus ConvertStatusProto(
55 yaze::test::GetTestStatusResponse::TestStatus status) {
56 using ProtoStatus = yaze::test::GetTestStatusResponse::TestStatus;
57 switch (status) {
58 case ProtoStatus::GetTestStatusResponse_TestStatus_TEST_STATUS_QUEUED:
60 case ProtoStatus::GetTestStatusResponse_TestStatus_TEST_STATUS_RUNNING:
62 case ProtoStatus::GetTestStatusResponse_TestStatus_TEST_STATUS_PASSED:
64 case ProtoStatus::GetTestStatusResponse_TestStatus_TEST_STATUS_FAILED:
66 case ProtoStatus::GetTestStatusResponse_TestStatus_TEST_STATUS_TIMEOUT:
68 case ProtoStatus::GetTestStatusResponse_TestStatus_TEST_STATUS_UNSPECIFIED:
69 default:
71 }
72}
73#endif // YAZE_WITH_GRPC
74
75} // namespace
76
77GuiAutomationClient::GuiAutomationClient(const std::string& server_address)
78 : server_address_(server_address) {}
79
81#ifdef YAZE_WITH_GRPC
82 auto channel =
83 grpc::CreateChannel(server_address_, grpc::InsecureChannelCredentials());
84 if (!channel) {
85 return absl::InternalError("Failed to create gRPC channel");
86 }
87
88 stub_ = yaze::test::ImGuiTestHarness::NewStub(channel);
89 if (!stub_) {
90 return absl::InternalError("Failed to create gRPC stub");
91 }
92
93 // Test connection with a ping
94 auto result = Ping("connection_test");
95 if (!result.ok()) {
96 return absl::UnavailableError(
97 absl::StrFormat("Failed to connect to test harness at %s: %s",
98 server_address_, result.status().message()));
99 }
100
101 connected_ = true;
102 return absl::OkStatus();
103#else
104 return absl::UnimplementedError(
105 "GUI automation requires YAZE_WITH_GRPC=ON at build time");
106#endif
107}
108
109absl::StatusOr<AutomationResult> GuiAutomationClient::Ping(
110 const std::string& message) {
111#ifdef YAZE_WITH_GRPC
112 if (!stub_) {
113 return absl::FailedPreconditionError(
114 "Not connected. Call Connect() first.");
115 }
116
117 yaze::test::PingRequest request;
118 request.set_message(message);
119
120 yaze::test::PingResponse response;
121 grpc::ClientContext context;
122
123 grpc::Status status = stub_->Ping(&context, request, &response);
124
125 if (!status.ok()) {
126 return absl::InternalError(
127 absl::StrFormat("Ping RPC failed: %s", status.error_message()));
128 }
129
130 AutomationResult result;
131 result.success = true;
132 result.message =
133 absl::StrFormat("Server version: %s (timestamp: %lld)",
134 response.yaze_version(), response.timestamp_ms());
135 result.execution_time = std::chrono::milliseconds(0);
136 result.test_id.clear();
137 return result;
138#else
139 return absl::UnimplementedError("gRPC not available");
140#endif
141}
142
143absl::StatusOr<ReplayTestResult> GuiAutomationClient::ReplayTest(
144 const std::string& script_path, bool ci_mode,
145 const std::map<std::string, std::string>& parameter_overrides) {
146#ifdef YAZE_WITH_GRPC
147 if (!stub_) {
148 return absl::FailedPreconditionError(
149 "Not connected. Call Connect() first.");
150 }
151
152 yaze::test::ReplayTestRequest request;
153 request.set_script_path(script_path);
154 request.set_ci_mode(ci_mode);
155 for (const auto& [key, value] : parameter_overrides) {
156 (*request.mutable_parameter_overrides())[key] = value;
157 }
158
159 yaze::test::ReplayTestResponse response;
160 grpc::ClientContext context;
161
162 grpc::Status status = stub_->ReplayTest(&context, request, &response);
163 if (!status.ok()) {
164 return absl::InternalError(
165 absl::StrCat("ReplayTest RPC failed: ", status.error_message()));
166 }
167
168 ReplayTestResult result;
169 result.success = response.success();
170 result.message = response.message();
171 result.replay_session_id = response.replay_session_id();
172 result.steps_executed = response.steps_executed();
173 result.logs.assign(response.logs().begin(), response.logs().end());
174 result.assertions.reserve(response.assertions_size());
175 for (const auto& assertion_proto : response.assertions()) {
176 AssertionOutcome assertion;
177 assertion.description = assertion_proto.description();
178 assertion.passed = assertion_proto.passed();
179 assertion.expected_value = assertion_proto.expected_value();
180 assertion.actual_value = assertion_proto.actual_value();
181 assertion.error_message = assertion_proto.error_message();
182 result.assertions.push_back(std::move(assertion));
183 }
184
185 return result;
186#else
187 return absl::UnimplementedError("gRPC not available");
188#endif
189}
190
191absl::StatusOr<StartRecordingResult> GuiAutomationClient::StartRecording(
192 const std::string& output_path, const std::string& session_name,
193 const std::string& description) {
194#ifdef YAZE_WITH_GRPC
195 if (!stub_) {
196 return absl::FailedPreconditionError(
197 "Not connected. Call Connect() first.");
198 }
199
200 yaze::test::StartRecordingRequest request;
201 request.set_output_path(output_path);
202 request.set_session_name(session_name);
203 request.set_description(description);
204
205 yaze::test::StartRecordingResponse response;
206 grpc::ClientContext context;
207 grpc::Status status = stub_->StartRecording(&context, request, &response);
208
209 if (!status.ok()) {
210 return absl::InternalError(
211 absl::StrCat("StartRecording RPC failed: ", status.error_message()));
212 }
213
215 result.success = response.success();
216 result.message = response.message();
217 result.recording_id = response.recording_id();
218 result.started_at = OptionalTimeFromMillis(response.started_at_ms());
219 return result;
220#else
221 return absl::UnimplementedError("gRPC not available");
222#endif
223}
224
225absl::StatusOr<StopRecordingResult> GuiAutomationClient::StopRecording(
226 const std::string& recording_id, bool discard) {
227#ifdef YAZE_WITH_GRPC
228 if (!stub_) {
229 return absl::FailedPreconditionError(
230 "Not connected. Call Connect() first.");
231 }
232 if (recording_id.empty()) {
233 return absl::InvalidArgumentError("recording_id must not be empty");
234 }
235
236 yaze::test::StopRecordingRequest request;
237 request.set_recording_id(recording_id);
238 request.set_discard(discard);
239
240 yaze::test::StopRecordingResponse response;
241 grpc::ClientContext context;
242 grpc::Status status = stub_->StopRecording(&context, request, &response);
243
244 if (!status.ok()) {
245 return absl::InternalError(
246 absl::StrCat("StopRecording RPC failed: ", status.error_message()));
247 }
248
249 StopRecordingResult result;
250 result.success = response.success();
251 result.message = response.message();
252 result.output_path = response.output_path();
253 result.step_count = response.step_count();
254 result.duration = std::chrono::milliseconds(response.duration_ms());
255 return result;
256#else
257 return absl::UnimplementedError("gRPC not available");
258#endif
259}
260
261absl::StatusOr<AutomationResult> GuiAutomationClient::Click(
262 const std::string& target, ClickType type) {
263#ifdef YAZE_WITH_GRPC
264 if (!stub_) {
265 return absl::FailedPreconditionError(
266 "Not connected. Call Connect() first.");
267 }
268
269 yaze::test::ClickRequest request;
270 request.set_target(target);
271
272 switch (type) {
273 case ClickType::kLeft:
274 request.set_type(yaze::test::ClickRequest::CLICK_TYPE_LEFT);
275 break;
277 request.set_type(yaze::test::ClickRequest::CLICK_TYPE_RIGHT);
278 break;
280 request.set_type(yaze::test::ClickRequest::CLICK_TYPE_MIDDLE);
281 break;
283 request.set_type(yaze::test::ClickRequest::CLICK_TYPE_DOUBLE);
284 break;
285 }
286
287 yaze::test::ClickResponse response;
288 grpc::ClientContext context;
289
290 grpc::Status status = stub_->Click(&context, request, &response);
291
292 if (!status.ok()) {
293 return absl::InternalError(
294 absl::StrFormat("Click RPC failed: %s", status.error_message()));
295 }
296
297 AutomationResult result;
298 result.success = response.success();
299 result.message = response.message();
300 result.execution_time =
301 std::chrono::milliseconds(response.execution_time_ms());
302 result.test_id = response.test_id();
303 return result;
304#else
305 return absl::UnimplementedError("gRPC not available");
306#endif
307}
308
309absl::StatusOr<AutomationResult> GuiAutomationClient::Type(
310 const std::string& target, const std::string& text, bool clear_first) {
311#ifdef YAZE_WITH_GRPC
312 if (!stub_) {
313 return absl::FailedPreconditionError(
314 "Not connected. Call Connect() first.");
315 }
316
317 yaze::test::TypeRequest request;
318 request.set_target(target);
319 request.set_text(text);
320 request.set_clear_first(clear_first);
321
322 yaze::test::TypeResponse response;
323 grpc::ClientContext context;
324
325 grpc::Status status = stub_->Type(&context, request, &response);
326
327 if (!status.ok()) {
328 return absl::InternalError(
329 absl::StrFormat("Type RPC failed: %s", status.error_message()));
330 }
331
332 AutomationResult result;
333 result.success = response.success();
334 result.message = response.message();
335 result.execution_time =
336 std::chrono::milliseconds(response.execution_time_ms());
337 result.test_id = response.test_id();
338 return result;
339#else
340 return absl::UnimplementedError("gRPC not available");
341#endif
342}
343
344absl::StatusOr<AutomationResult> GuiAutomationClient::Wait(
345 const std::string& condition, int timeout_ms, int poll_interval_ms) {
346#ifdef YAZE_WITH_GRPC
347 if (!stub_) {
348 return absl::FailedPreconditionError(
349 "Not connected. Call Connect() first.");
350 }
351
352 yaze::test::WaitRequest request;
353 request.set_condition(condition);
354 request.set_timeout_ms(timeout_ms);
355 request.set_poll_interval_ms(poll_interval_ms);
356
357 yaze::test::WaitResponse response;
358 grpc::ClientContext context;
359
360 grpc::Status status = stub_->Wait(&context, request, &response);
361
362 if (!status.ok()) {
363 return absl::InternalError(
364 absl::StrFormat("Wait RPC failed: %s", status.error_message()));
365 }
366
367 AutomationResult result;
368 result.success = response.success();
369 result.message = response.message();
370 result.execution_time = std::chrono::milliseconds(response.elapsed_ms());
371 result.test_id = response.test_id();
372 return result;
373#else
374 return absl::UnimplementedError("gRPC not available");
375#endif
376}
377
378absl::StatusOr<AutomationResult> GuiAutomationClient::Assert(
379 const std::string& condition) {
380#ifdef YAZE_WITH_GRPC
381 if (!stub_) {
382 return absl::FailedPreconditionError(
383 "Not connected. Call Connect() first.");
384 }
385
386 yaze::test::AssertRequest request;
387 request.set_condition(condition);
388
389 yaze::test::AssertResponse response;
390 grpc::ClientContext context;
391
392 grpc::Status status = stub_->Assert(&context, request, &response);
393
394 if (!status.ok()) {
395 return absl::InternalError(
396 absl::StrFormat("Assert RPC failed: %s", status.error_message()));
397 }
398
399 AutomationResult result;
400 result.success = response.success();
401 result.message = response.message();
402 result.actual_value = response.actual_value();
403 result.expected_value = response.expected_value();
404 result.execution_time = std::chrono::milliseconds(0);
405 result.test_id = response.test_id();
406 return result;
407#else
408 return absl::UnimplementedError("gRPC not available");
409#endif
410}
411
412absl::StatusOr<AutomationResult> GuiAutomationClient::Screenshot(
413 const std::string& region, const std::string& format) {
414#ifdef YAZE_WITH_GRPC
415 if (!stub_) {
416 return absl::FailedPreconditionError(
417 "Not connected. Call Connect() first.");
418 }
419
420 yaze::test::ScreenshotRequest request;
421 request.set_window_title(""); // Empty = main window
422 request.set_output_path("/tmp/yaze_screenshot.png"); // Default path
423 request.set_format(
424 yaze::test::ScreenshotRequest::IMAGE_FORMAT_PNG); // Always PNG for now
425
426 yaze::test::ScreenshotResponse response;
427 grpc::ClientContext context;
428
429 grpc::Status status = stub_->Screenshot(&context, request, &response);
430
431 if (!status.ok()) {
432 return absl::InternalError(
433 absl::StrFormat("Screenshot RPC failed: %s", status.error_message()));
434 }
435
436 AutomationResult result;
437 result.success = response.success();
438 result.message = response.message();
439 result.execution_time = std::chrono::milliseconds(0);
440 result.test_id.clear();
441 return result;
442#else
443 return absl::UnimplementedError("gRPC not available");
444#endif
445}
446
447absl::StatusOr<TestStatusDetails> GuiAutomationClient::GetTestStatus(
448 const std::string& test_id) {
449#ifdef YAZE_WITH_GRPC
450 if (!stub_) {
451 return absl::FailedPreconditionError(
452 "Not connected. Call Connect() first.");
453 }
454
455 yaze::test::GetTestStatusRequest request;
456 request.set_test_id(test_id);
457
458 yaze::test::GetTestStatusResponse response;
459 grpc::ClientContext context;
460
461 grpc::Status status = stub_->GetTestStatus(&context, request, &response);
462
463 if (!status.ok()) {
464 return absl::InternalError(absl::StrFormat("GetTestStatus RPC failed: %s",
465 status.error_message()));
466 }
467
468 TestStatusDetails details;
469 details.test_id = test_id;
470 details.status = ConvertStatusProto(response.status());
471 details.queued_at = OptionalTimeFromMillis(response.queued_at_ms());
472 details.started_at = OptionalTimeFromMillis(response.started_at_ms());
473 details.completed_at = OptionalTimeFromMillis(response.completed_at_ms());
474 details.execution_time_ms = response.execution_time_ms();
475 details.error_message = response.error_message();
476 details.assertion_failures.assign(response.assertion_failures().begin(),
477 response.assertion_failures().end());
478 return details;
479#else
480 return absl::UnimplementedError("gRPC not available");
481#endif
482}
483
484absl::StatusOr<ListTestsResult> GuiAutomationClient::ListTests(
485 const std::string& category_filter, int page_size,
486 const std::string& page_token) {
487#ifdef YAZE_WITH_GRPC
488 if (!stub_) {
489 return absl::FailedPreconditionError(
490 "Not connected. Call Connect() first.");
491 }
492
493 yaze::test::ListTestsRequest request;
494 if (!category_filter.empty()) {
495 request.set_category_filter(category_filter);
496 }
497 if (page_size > 0) {
498 request.set_page_size(page_size);
499 }
500 if (!page_token.empty()) {
501 request.set_page_token(page_token);
502 }
503
504 yaze::test::ListTestsResponse response;
505 grpc::ClientContext context;
506
507 grpc::Status status = stub_->ListTests(&context, request, &response);
508
509 if (!status.ok()) {
510 return absl::InternalError(
511 absl::StrFormat("ListTests RPC failed: %s", status.error_message()));
512 }
513
514 ListTestsResult result;
515 result.total_count = response.total_count();
516 result.next_page_token = response.next_page_token();
517 result.tests.reserve(response.tests_size());
518
519 for (const auto& test_info : response.tests()) {
520 HarnessTestSummary summary;
521 summary.test_id = test_info.test_id();
522 summary.name = test_info.name();
523 summary.category = test_info.category();
524 summary.last_run_at =
525 OptionalTimeFromMillis(test_info.last_run_timestamp_ms());
526 summary.total_runs = test_info.total_runs();
527 summary.pass_count = test_info.pass_count();
528 summary.fail_count = test_info.fail_count();
529 summary.average_duration_ms = test_info.average_duration_ms();
530 result.tests.push_back(std::move(summary));
531 }
532
533 return result;
534#else
535 return absl::UnimplementedError("gRPC not available");
536#endif
537}
538
539absl::StatusOr<TestResultDetails> GuiAutomationClient::GetTestResults(
540 const std::string& test_id, bool include_logs) {
541#ifdef YAZE_WITH_GRPC
542 if (!stub_) {
543 return absl::FailedPreconditionError(
544 "Not connected. Call Connect() first.");
545 }
546
547 yaze::test::GetTestResultsRequest request;
548 request.set_test_id(test_id);
549 request.set_include_logs(include_logs);
550
551 yaze::test::GetTestResultsResponse response;
552 grpc::ClientContext context;
553
554 grpc::Status status = stub_->GetTestResults(&context, request, &response);
555
556 if (!status.ok()) {
557 return absl::InternalError(absl::StrFormat("GetTestResults RPC failed: %s",
558 status.error_message()));
559 }
560
561 TestResultDetails result;
562 result.test_id = test_id;
563 result.success = response.success();
564 result.test_name = response.test_name();
565 result.category = response.category();
566 result.executed_at = OptionalTimeFromMillis(response.executed_at_ms());
567 result.duration_ms = response.duration_ms();
568
569 result.assertions.reserve(response.assertions_size());
570 for (const auto& assertion : response.assertions()) {
571 AssertionOutcome outcome;
572 outcome.description = assertion.description();
573 outcome.passed = assertion.passed();
574 outcome.expected_value = assertion.expected_value();
575 outcome.actual_value = assertion.actual_value();
576 outcome.error_message = assertion.error_message();
577 result.assertions.push_back(std::move(outcome));
578 }
579
580 if (include_logs) {
581 result.logs.assign(response.logs().begin(), response.logs().end());
582 }
583
584 for (const auto& metric : response.metrics()) {
585 result.metrics.emplace(metric.first, metric.second);
586 }
587
588 result.screenshot_path = response.screenshot_path();
589 result.screenshot_size_bytes = response.screenshot_size_bytes();
590 result.failure_context = response.failure_context();
591 result.widget_state = response.widget_state();
592
593 return result;
594#else
595 return absl::UnimplementedError("gRPC not available");
596#endif
597}
598
599absl::StatusOr<DiscoverWidgetsResult> GuiAutomationClient::DiscoverWidgets(
600 const DiscoverWidgetsQuery& query) {
601#ifdef YAZE_WITH_GRPC
602 if (!stub_) {
603 return absl::FailedPreconditionError(
604 "Not connected. Call Connect() first.");
605 }
606
607 yaze::test::DiscoverWidgetsRequest request;
608 if (!query.window_filter.empty()) {
609 request.set_window_filter(query.window_filter);
610 }
611 request.set_type_filter(ConvertWidgetTypeFilterToProto(query.type_filter));
612 if (!query.path_prefix.empty()) {
613 request.set_path_prefix(query.path_prefix);
614 }
615 request.set_include_invisible(query.include_invisible);
616 request.set_include_disabled(query.include_disabled);
617
618 yaze::test::DiscoverWidgetsResponse response;
619 grpc::ClientContext context;
620 grpc::Status status = stub_->DiscoverWidgets(&context, request, &response);
621
622 if (!status.ok()) {
623 return absl::InternalError(absl::StrFormat("DiscoverWidgets RPC failed: %s",
624 status.error_message()));
625 }
626
628 result.total_widgets = response.total_widgets();
629 if (response.generated_at_ms() > 0) {
630 result.generated_at = OptionalTimeFromMillis(response.generated_at_ms());
631 }
632
633 result.windows.reserve(response.windows_size());
634 for (const auto& window_proto : response.windows()) {
635 DiscoveredWindowInfo window_info;
636 window_info.name = window_proto.name();
637 window_info.visible = window_proto.visible();
638 window_info.widgets.reserve(window_proto.widgets_size());
639
640 for (const auto& widget_proto : window_proto.widgets()) {
641 WidgetDescriptor widget;
642 widget.path = widget_proto.path();
643 widget.label = widget_proto.label();
644 widget.type = widget_proto.type();
645 widget.description = widget_proto.description();
646 widget.suggested_action = widget_proto.suggested_action();
647 widget.visible = widget_proto.visible();
648 widget.enabled = widget_proto.enabled();
649 widget.has_bounds = widget_proto.has_bounds();
650 if (widget.has_bounds) {
651 widget.bounds.min_x = widget_proto.bounds().min_x();
652 widget.bounds.min_y = widget_proto.bounds().min_y();
653 widget.bounds.max_x = widget_proto.bounds().max_x();
654 widget.bounds.max_y = widget_proto.bounds().max_y();
655 } else {
656 widget.bounds = WidgetBoundingBox();
657 }
658 widget.widget_id = widget_proto.widget_id();
659 widget.last_seen_frame = widget_proto.last_seen_frame();
660 widget.last_seen_at =
661 OptionalTimeFromMillis(widget_proto.last_seen_at_ms());
662 widget.stale = widget_proto.stale();
663 window_info.widgets.push_back(std::move(widget));
664 }
665
666 result.windows.push_back(std::move(window_info));
667 }
668
669 return result;
670#else
671 (void)query;
672 return absl::UnimplementedError("gRPC not available");
673#endif
674}
675
676} // namespace cli
677} // namespace yaze
absl::StatusOr< ReplayTestResult > ReplayTest(const std::string &script_path, bool ci_mode, const std::map< std::string, std::string > &parameter_overrides={})
absl::StatusOr< AutomationResult > Screenshot(const std::string &region="full", const std::string &format="PNG")
Capture a screenshot.
absl::Status Connect()
Connect to the test harness server.
absl::StatusOr< StartRecordingResult > StartRecording(const std::string &output_path, const std::string &session_name, const std::string &description)
absl::StatusOr< AutomationResult > Ping(const std::string &message="ping")
Check if the server is reachable and responsive.
absl::StatusOr< TestStatusDetails > GetTestStatus(const std::string &test_id)
Fetch the current execution status for a harness test.
GuiAutomationClient(const std::string &server_address)
Construct a new GUI automation client.
absl::StatusOr< AutomationResult > Type(const std::string &target, const std::string &text, bool clear_first=false)
Type text into an input field.
absl::StatusOr< DiscoverWidgetsResult > DiscoverWidgets(const DiscoverWidgetsQuery &query)
absl::StatusOr< AutomationResult > Wait(const std::string &condition, int timeout_ms=5000, int poll_interval_ms=100)
Wait for a condition to be met.
absl::StatusOr< TestResultDetails > GetTestResults(const std::string &test_id, bool include_logs=false)
Retrieve detailed results for a harness test execution.
absl::StatusOr< StopRecordingResult > StopRecording(const std::string &recording_id, bool discard=false)
absl::StatusOr< AutomationResult > Assert(const std::string &condition)
Assert a GUI state condition.
absl::StatusOr< AutomationResult > Click(const std::string &target, ClickType type=ClickType::kLeft)
Click a GUI element.
absl::StatusOr< ListTestsResult > ListTests(const std::string &category_filter="", int page_size=100, const std::string &page_token="")
Enumerate harness tests with optional filtering.
TestRunStatus
Execution status codes returned by the harness.
ClickType
Type of click action to perform.
Main namespace for the application.
Definition controller.cc:20
Individual assertion outcome within a harness test.
Result of a GUI automation action.
std::chrono::milliseconds execution_time
std::vector< DiscoveredWindowInfo > windows
std::optional< absl::Time > generated_at
std::vector< WidgetDescriptor > widgets
Aggregated metadata about a harness test.
std::optional< absl::Time > last_run_at
Result container for ListTests RPC.
std::vector< HarnessTestSummary > tests
std::vector< AssertionOutcome > assertions
std::vector< std::string > logs
std::optional< absl::Time > started_at
std::chrono::milliseconds duration
Detailed execution results for a specific harness test.
std::optional< absl::Time > executed_at
std::map< std::string, int > metrics
std::vector< AssertionOutcome > assertions
std::vector< std::string > logs
Detailed information about an individual test execution.
std::vector< std::string > assertion_failures
std::optional< absl::Time > completed_at
std::optional< absl::Time > started_at
std::optional< absl::Time > queued_at
std::optional< absl::Time > last_seen_at