8#include "absl/status/statusor.h"
9#include "absl/strings/match.h"
10#include "absl/strings/str_cat.h"
11#include "absl/strings/str_format.h"
12#include "absl/strings/str_replace.h"
13#include "absl/synchronization/mutex.h"
14#include "absl/time/clock.h"
15#include "absl/time/time.h"
23#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
25#include "imgui_internal.h"
39#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
42#if __has_include("imgui_test_engine/imgui_te_engine.h")
43#include "imgui_test_engine/imgui_te_engine.h"
44#elif __has_include("imgui_te_engine.h")
45#include "imgui_te_engine.h"
47#error "ImGui Test Engine headers not found"
57 std::filesystem::path base_dir = std::filesystem::temp_directory_path() /
58 "yaze" /
"test-results" / test_id;
60 std::filesystem::create_directories(base_dir, ec);
62 const int64_t timestamp_ms = absl::ToUnixMillis(absl::Now());
63 std::filesystem::path file_path =
64 base_dir / std::filesystem::path(absl::StrFormat(
65 "failure_%lld.bmp",
static_cast<long long>(timestamp_ms)));
66 return file_path.string();
117 return ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
120#if defined(YAZE_WITH_GRPC)
121const char* HarnessStatusToString(HarnessTestStatus status) {
123 case HarnessTestStatus::kPassed:
125 case HarnessTestStatus::kFailed:
127 case HarnessTestStatus::kTimeout:
129 case HarnessTestStatus::kRunning:
131 case HarnessTestStatus::kQueued:
133 case HarnessTestStatus::kUnspecified:
135 return "unspecified";
139HarnessTestStatus HarnessStatusFromString(absl::string_view status) {
140 if (absl::EqualsIgnoreCase(status,
"passed")) {
141 return HarnessTestStatus::kPassed;
143 if (absl::EqualsIgnoreCase(status,
"failed")) {
144 return HarnessTestStatus::kFailed;
146 if (absl::EqualsIgnoreCase(status,
"timeout")) {
147 return HarnessTestStatus::kTimeout;
149 if (absl::EqualsIgnoreCase(status,
"running")) {
150 return HarnessTestStatus::kRunning;
152 if (absl::EqualsIgnoreCase(status,
"queued")) {
153 return HarnessTestStatus::kQueued;
155 return HarnessTestStatus::kUnspecified;
171#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
176#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
180 if (ImGui::GetCurrentContext() ==
nullptr) {
182 "ImGui context not ready, deferring test engine initialization");
189 test_io.ConfigVerboseLevel = ImGuiTestVerboseLevel_Info;
190 test_io.ConfigVerboseLevelOnError = ImGuiTestVerboseLevel_Debug;
191 test_io.ConfigRunSpeed = ImGuiTestRunSpeed_Fast;
201 "ImGuiTestEngine initialized with dungeon + overworld UI tests");
208 UpdateRemoteReplayFocus();
213 UpdateRemoteReplayFocus();
217void TestManager::RegisterRemoteReplayTest(ImGuiTest* test,
218 const std::string& harness_id) {
219 absl::MutexLock lock(&
mutex_);
220 remote_replay_tests_[test] = harness_id;
223absl::Status TestManager::BeginRemoteReplayFocus(ImGuiTest* test) {
224 ImVector<ImGuiTestRunTask> queue;
227 absl::MutexLock lock(&
mutex_);
230 if (!remote_replay_tests_.contains(test))
231 return absl::OkStatus();
232 for (
const auto& task : queue) {
233 if (!remote_replay_tests_.contains(task.Test)) {
234 RestoreRemoteReplayFocus();
235 return absl::FailedPreconditionError(
236 "Remote replay cannot share a queue with ordinary UI tests");
243 io.ConfigRestoreFocusAfterTests =
false;
244 return absl::OkStatus();
247void TestManager::RestoreRemoteReplayFocus() {
255void TestManager::UpdateRemoteReplayFocus() {
256 ImVector<ImGuiTestRunTask> queue;
258 absl::MutexLock lock(&
mutex_);
259 bool ordinary_job =
false;
260 for (
const auto& task : queue)
261 ordinary_job |= !remote_replay_tests_.contains(task.Test);
264 if (queue.empty() || ordinary_job)
265 RestoreRemoteReplayFocus();
267 remote_replay_tests_.clear();
274 RestoreRemoteReplayFocus();
275 std::vector<std::string> cancelled;
277 absl::MutexLock lock(&
mutex_);
278 for (
const auto& entry : remote_replay_tests_)
279 cancelled.push_back(entry.second);
280 remote_replay_tests_.clear();
283 for (
const auto&
id : cancelled) {
284 auto execution = GetHarnessTestExecution(
id);
285 if (execution.ok() && (execution->status == HarnessTestStatus::kQueued ||
286 execution->status == HarnessTestStatus::kRunning))
287 MarkHarnessTestCompleted(
id, HarnessTestStatus::kFailed,
288 "Remote replay cancelled with UI test engine");
296 RestoreRemoteReplayFocus();
311 return absl::FailedPreconditionError(
"Tests are already running");
320 if (suite->IsEnabled()) {
335 return absl::OkStatus();
340 return absl::FailedPreconditionError(
"Tests are already running");
348 std::vector<TestSuite*> filtered_suites;
350 if (suite->IsEnabled() && suite->GetCategory() == category) {
351 filtered_suites.push_back(suite.get());
355 for (
auto* suite : filtered_suites) {
369 return absl::OkStatus();
374 return absl::FailedPreconditionError(
"Tests are already running");
379 return absl::NotFoundError(
"Test suite not found: " + suite_name);
398 std::string name = suite->GetName();
405 std::vector<std::string> names;
408 names.push_back(suite->GetName());
425 return absl::InvalidArgumentError(
"Test suite is null");
446 size_t completed = 0;
448 if (suite->IsEnabled()) {
458 stats.
timestamp = std::chrono::steady_clock::now();
485 bool* dashboard_flag = show_dashboard ? show_dashboard : &
show_dashboard_;
488 ImGui::SetNextWindowSize(ImVec2(900, 700), ImGuiCond_FirstUseEver);
490 if (!ImGui::Begin(
"Test Dashboard", dashboard_flag,
491 ImGuiWindowFlags_MenuBar)) {
500 static int frame_counter = 0;
502 if (frame_counter % 60 == 0) {
505 "TestManager ROM status check - Frame %d: ROM %p, loaded: %s",
506 frame_counter, (
void*)
current_rom_, has_rom ?
"true" :
"false");
509 if (ImGui::BeginTable(
"ROM_Status_Table", 2, ImGuiTableFlags_BordersInner)) {
510 ImGui::TableSetupColumn(
"Property", ImGuiTableColumnFlags_WidthFixed, 120);
511 ImGui::TableSetupColumn(
"Value", ImGuiTableColumnFlags_WidthStretch);
513 ImGui::TableNextRow();
514 ImGui::TableNextColumn();
515 ImGui::Text(
"ROM Status:");
516 ImGui::TableNextColumn();
521 ImGui::TableNextRow();
522 ImGui::TableNextColumn();
523 ImGui::Text(
"ROM Title:");
524 ImGui::TableNextColumn();
527 ImGui::TableNextRow();
528 ImGui::TableNextColumn();
529 ImGui::Text(
"File Name:");
530 ImGui::TableNextColumn();
533 ImGui::TableNextRow();
534 ImGui::TableNextColumn();
535 ImGui::Text(
"Size:");
536 ImGui::TableNextColumn();
540 ImGui::TableNextRow();
541 ImGui::TableNextColumn();
542 ImGui::Text(
"Modified:");
543 ImGui::TableNextColumn();
550 ImGui::TableNextRow();
551 ImGui::TableNextColumn();
552 ImGui::Text(
"ROM Pointer:");
553 ImGui::TableNextColumn();
556 ImGui::TableNextRow();
557 ImGui::TableNextColumn();
558 ImGui::Text(
"Actions:");
559 ImGui::TableNextColumn();
560 if (ImGui::Button(
"Refresh ROM Reference")) {
567 ImGui::TableNextRow();
568 ImGui::TableNextColumn();
569 ImGui::Text(
"ROM Pointer:");
570 ImGui::TableNextColumn();
572 ImGui::TableNextRow();
573 ImGui::TableNextColumn();
574 ImGui::Text(
"Status:");
575 ImGui::TableNextColumn();
576 ImGui::Text(
"ROM-dependent tests will be skipped");
578 ImGui::TableNextRow();
579 ImGui::TableNextColumn();
580 ImGui::Text(
"Actions:");
581 ImGui::TableNextColumn();
582 if (ImGui::Button(
"Refresh ROM Reference")) {
586 if (ImGui::Button(
"Debug ROM State")) {
587 LOG_INFO(
"TestManager",
"=== ROM DEBUG INFO ===");
588 LOG_INFO(
"TestManager",
"current_rom_ pointer: %p",
591 LOG_INFO(
"TestManager",
"ROM title: '%s'",
594 LOG_INFO(
"TestManager",
"ROM is_loaded(): %s",
596 LOG_INFO(
"TestManager",
"ROM data pointer: %p",
599 LOG_INFO(
"TestManager",
"======================");
608 if (ImGui::BeginMenuBar()) {
609 if (ImGui::BeginMenu(
"Run")) {
610 if (ImGui::MenuItem(
"All Tests",
"Ctrl+T",
false, !
is_running_)) {
614 if (ImGui::MenuItem(
"Unit Tests",
nullptr,
false, !
is_running_)) {
617 if (ImGui::MenuItem(
"Integration Tests",
nullptr,
false, !
is_running_)) {
618 [[maybe_unused]]
auto status =
621 if (ImGui::MenuItem(
"UI Tests",
nullptr,
false, !
is_running_)) {
624 if (ImGui::MenuItem(
"Performance Tests",
nullptr,
false, !
is_running_)) {
625 [[maybe_unused]]
auto status =
628 if (ImGui::MenuItem(
"Memory Tests",
nullptr,
false, !
is_running_)) {
629 [[maybe_unused]]
auto status =
635 if (ImGui::BeginMenu(
"View")) {
640 if (ImGui::MenuItem(
"Export Results",
nullptr,
false,
647 if (ImGui::BeginMenu(
"ROM")) {
648 if (ImGui::MenuItem(
"Test Current ROM",
nullptr,
false,
650 [[maybe_unused]]
auto status =
653 if (ImGui::MenuItem(
"Load ROM for Testing...")) {
657 if (ImGui::MenuItem(
"Refresh ROM Reference")) {
663 if (ImGui::BeginMenu(
"Configure")) {
664 if (ImGui::MenuItem(
"Test Configuration")) {
669 if (ImGui::MenuItem(
"Use NFD File Dialog",
nullptr, &nfd_mode)) {
671 LOG_INFO(
"TestManager",
"Global file dialog mode changed to: %s",
672 nfd_mode ?
"NFD" :
"Bespoke");
681 int enabled_count = 0;
683 static const std::vector<std::string> all_test_names = {
684 "ROM_Header_Validation_Test",
"ROM_Data_Access_Test",
685 "ROM_Graphics_Extraction_Test",
"ROM_Overworld_Loading_Test",
686 "Tile16_Editor_Test",
"Comprehensive_Save_Test",
687 "ROM_Sprite_Data_Test",
"ROM_Music_Data_Test"};
689 for (
const auto& test_name : all_test_names) {
698 if (enabled_count < total_count) {
701 "(Some tests disabled - check Configuration)");
712 ImGui::ProgressBar(
progress_, ImVec2(-1, 0),
713 absl::StrFormat(
"%.0f%%",
progress_ * 100.0f).c_str());
727 if (ImGui::Button(absl::StrCat(
ICON_MD_SPEED,
" Quick Test").c_str(),
735 std::optional<gui::StyleColorGuard> rom_btn_guard;
742 [[maybe_unused]]
auto status =
747 if (ImGui::IsItemHovered()) {
749 ImGui::SetTooltip(
"Run tests on current ROM: %s",
752 ImGui::SetTooltip(
"Load a ROM to enable ROM-dependent tests");
757 if (ImGui::Button(absl::StrCat(
ICON_MD_CLEAR,
" Clear").c_str(),
785 pass_rate, ImVec2(-1, 0),
786 absl::StrFormat(
"Pass Rate: %.1f%%", pass_rate * 100.0f).c_str());
805 if (ImGui::CollapsingHeader(
"Test Suite Breakdown")) {
806 std::unordered_map<std::string, std::pair<size_t, size_t>>
809 suite_stats[result.suite_name].second++;
811 suite_stats[result.suite_name].first++;
815 for (
const auto& [suite_name, stats] : suite_stats) {
816 float suite_pass_rate =
817 stats.second > 0 ?
static_cast<float>(stats.first) / stats.second
819 ImGui::Text(
"%s: %zu/%zu (%.0f%%)", suite_name.c_str(), stats.first,
820 stats.second, suite_pass_rate * 100.0f);
831 const char* categories[] = {
"All",
"Unit",
"Integration",
832 "UI",
"Performance",
"Memory"};
833 static int selected_category = 0;
834 if (ImGui::Combo(
"Category", &selected_category, categories,
835 IM_ARRAYSIZE(categories))) {
836 switch (selected_category) {
859 static char filter_buffer[256] =
"";
860 ImGui::SetNextItemWidth(-80);
861 if (ImGui::InputTextWithHint(
"##filter",
"Search tests...", filter_buffer,
862 sizeof(filter_buffer))) {
866 if (ImGui::Button(
"Clear")) {
867 filter_buffer[0] =
'\0';
874 if (ImGui::BeginTabBar(
"TestResultsTabs", ImGuiTabBarFlags_None)) {
876 if (ImGui::BeginTabItem(
"Test Results")) {
877 if (ImGui::BeginChild(
"TestResults", ImVec2(0, 0),
true)) {
880 "No test results to display. Run some tests to "
881 "see results here.");
885 bool category_match = (selected_category == 0) ||
890 result.suite_name.find(
test_filter_) != std::string::npos;
892 if (!category_match || !text_match) {
896 ImGui::PushID(&result);
900 switch (result.status) {
918 status_icon, result.suite_name.c_str(),
919 result.name.c_str());
922 if (ImGui::GetContentRegionAvail().x > 200) {
925 result.duration.count());
930 !result.error_message.empty()) {
936 result.error_message.c_str());
943 !result.error_message.empty()) {
949 result.error_message.c_str());
963#if defined(YAZE_WITH_GRPC)
964 if (ImGui::BeginTabItem(
"GUI Automation Tests")) {
965 if (ImGui::BeginChild(
"HarnessTests", ImVec2(0, 0),
true)) {
967 auto summaries = ListHarnessTestSummaries();
969 if (summaries.empty()) {
972 "No GUI automation test results yet.\n\n"
973 "These tests are run via the ImGuiTestHarness gRPC service.\n"
974 "Results will appear here after running GUI automation tests.");
977 ImGui::Text(
"Total Tests: %zu", summaries.size());
981 if (ImGui::BeginTable(
"HarnessTestTable", 6,
982 ImGuiTableFlags_Borders |
983 ImGuiTableFlags_RowBg |
984 ImGuiTableFlags_Resizable)) {
985 ImGui::TableSetupColumn(
"Status", ImGuiTableColumnFlags_WidthFixed,
987 ImGui::TableSetupColumn(
"Test Name",
988 ImGuiTableColumnFlags_WidthStretch);
989 ImGui::TableSetupColumn(
"Category",
990 ImGuiTableColumnFlags_WidthFixed, 100);
991 ImGui::TableSetupColumn(
"Runs", ImGuiTableColumnFlags_WidthFixed,
993 ImGui::TableSetupColumn(
"Pass Rate",
994 ImGuiTableColumnFlags_WidthFixed, 80);
995 ImGui::TableSetupColumn(
"Duration",
996 ImGuiTableColumnFlags_WidthFixed, 80);
997 ImGui::TableHeadersRow();
999 for (
const auto& summary : summaries) {
1000 const auto& exec = summary.latest_execution;
1002 ImGui::TableNextRow();
1003 ImGui::TableNextColumn();
1006 ImVec4 status_color;
1007 const char* status_icon;
1008 const char* status_text;
1010 switch (exec.status) {
1011 case HarnessTestStatus::kPassed:
1014 status_text =
"Passed";
1016 case HarnessTestStatus::kFailed:
1019 status_text =
"Failed";
1021 case HarnessTestStatus::kTimeout:
1024 status_text =
"Timeout";
1026 case HarnessTestStatus::kRunning:
1029 status_text =
"Running";
1031 case HarnessTestStatus::kQueued:
1034 status_text =
"Queued";
1039 status_text =
"Unknown";
1043 ImGui::TextColored(status_color,
"%s %s", status_icon,
1046 ImGui::TableNextColumn();
1047 ImGui::Text(
"%s", exec.name.c_str());
1050 if (exec.status == HarnessTestStatus::kFailed &&
1051 !exec.error_message.empty()) {
1054 exec.error_message.c_str());
1057 ImGui::TableNextColumn();
1058 ImGui::Text(
"%s", exec.category.c_str());
1060 ImGui::TableNextColumn();
1061 ImGui::Text(
"%d", summary.total_runs);
1063 ImGui::TableNextColumn();
1064 if (summary.total_runs > 0) {
1066 static_cast<float>(summary.pass_count) / summary.total_runs;
1070 ImGui::TextColored(rate_color,
"%.0f%%", pass_rate * 100.0f);
1075 ImGui::TableNextColumn();
1076 double duration_ms =
1077 absl::ToDoubleMilliseconds(summary.total_duration);
1078 if (summary.total_runs > 0) {
1079 ImGui::Text(
"%.0f ms", duration_ms / summary.total_runs);
1085 if (ImGui::TreeNode((
"Details##" + exec.test_id).c_str())) {
1086 ImGui::Text(
"Test ID: %s", exec.test_id.c_str());
1087 ImGui::Text(
"Total Runs: %d (Pass: %d, Fail: %d)",
1088 summary.total_runs, summary.pass_count,
1089 summary.fail_count);
1091 if (!exec.logs.empty()) {
1094 for (
const auto& log : exec.logs) {
1095 ImGui::BulletText(
"%s", log.c_str());
1099 if (!exec.assertion_failures.empty()) {
1103 for (
const auto& failure : exec.assertion_failures) {
1104 ImGui::BulletText(
"%s", failure.c_str());
1108 if (!exec.screenshot_path.empty()) {
1111 exec.screenshot_path.c_str());
1112 ImGui::Text(
"Size: %.2f KB",
1113 exec.screenshot_size_bytes / 1024.0);
1125 ImGui::EndTabItem();
1136 ImGui::Begin(absl::StrCat(
ICON_MD_MONITOR,
" Resource Monitor").c_str(),
1141 ImGui::Text(
"%s Textures: %zu",
ICON_MD_TEXTURE, latest.texture_count);
1142 ImGui::Text(
"%s Surfaces: %zu",
ICON_MD_LAYERS, latest.surface_count);
1143 ImGui::Text(
"%s Memory: %zu MB",
ICON_MD_MEMORY, latest.memory_usage_mb);
1144 ImGui::Text(
"%s FPS: %.1f",
ICON_MD_SPEED, latest.frame_rate);
1148 std::vector<float> texture_counts;
1149 std::vector<float> surface_counts;
1154 texture_counts.push_back(
static_cast<float>(stats.texture_count));
1155 surface_counts.push_back(
static_cast<float>(stats.surface_count));
1158 ImGui::PlotLines(
"Textures", texture_counts.data(),
1159 static_cast<int>(texture_counts.size()), 0,
nullptr,
1160 0.0f, FLT_MAX, ImVec2(0, 80));
1161 ImGui::PlotLines(
"Surfaces", surface_counts.data(),
1162 static_cast<int>(surface_counts.size()), 0,
nullptr,
1163 0.0f, FLT_MAX, ImVec2(0, 80));
1172 ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver);
1177#ifdef YAZE_ENABLE_GTEST
1178 ImGui::Text(
"Google Test framework is available");
1180 if (ImGui::Button(
"Run All Google Tests")) {
1182 LOG_INFO(
"TestManager",
"Running Google Tests...");
1186 if (ImGui::Button(
"Run Specific Test Suite")) {
1191 ImGui::Text(
"Available Test Suites:");
1192 ImGui::BulletText(
"Unit Tests");
1193 ImGui::BulletText(
"Integration Tests");
1194 ImGui::BulletText(
"Performance Tests");
1197 "%s Google Test framework not available",
1199 ImGui::Text(
"Enable YAZE_ENABLE_GTEST to use Google Test integration");
1207 ImGui::SetNextWindowSize(ImVec2(700, 500), ImGuiCond_FirstUseEver);
1216 if (ImGui::CollapsingHeader(
"ROM Data Integrity",
1217 ImGuiTreeNodeFlags_DefaultOpen)) {
1221 if (ImGui::Button(
"Run Data Integrity Check")) {
1223 [[maybe_unused]]
auto suite_status =
1228 if (ImGui::CollapsingHeader(
"Save/Load Testing")) {
1229 ImGui::Text(
"Test ROM save and load operations");
1231 if (ImGui::Button(
"Test Save Operations")) {
1236 if (ImGui::Button(
"Test Load Operations")) {
1241 if (ImGui::CollapsingHeader(
"Editor Integration")) {
1242 ImGui::Text(
"Test editor components with current ROM");
1244 if (ImGui::Button(
"Test Overworld Editor")) {
1249 if (ImGui::Button(
"Test Tile16 Editor")) {
1264 ImGui::SetNextWindowSize(ImVec2(400, 200), ImGuiCond_Appearing);
1266 ImGuiWindowFlags_NoResize)) {
1270 ImGui::Text(
"Select a ROM file to run tests on:");
1272 if (ImGui::Button(
"Browse ROM File...", ImVec2(-1, 0))) {
1279 if (ImGui::Button(
"Cancel", ImVec2(-1, 0))) {
1288 ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_FirstUseEver);
1294 if (ImGui::CollapsingHeader(
"File Dialog Settings",
1295 ImGuiTreeNodeFlags_DefaultOpen)) {
1296 ImGui::Text(
"File Dialog Implementation:");
1299 if (ImGui::RadioButton(
"NFD (Native File Dialog)", nfd_mode)) {
1301 LOG_INFO(
"TestManager",
"Global file dialog mode set to: NFD");
1303 if (ImGui::IsItemHovered()) {
1305 "Use NFD library for native OS file dialogs (global setting)");
1308 if (ImGui::RadioButton(
"Bespoke Implementation", !nfd_mode)) {
1310 LOG_INFO(
"TestManager",
"Global file dialog mode set to: Bespoke");
1312 if (ImGui::IsItemHovered()) {
1314 "Use custom file dialog implementation (global setting)");
1323 "Note: This setting affects ALL file dialogs in the application");
1325 if (ImGui::Button(
"Test Current File Dialog")) {
1327 LOG_INFO(
"TestManager",
"Testing global file dialog mode: %s",
1333 if (!result.empty()) {
1334 LOG_INFO(
"TestManager",
"File dialog test successful: %s",
1338 "File dialog test: No file selected or dialog canceled");
1343 if (ImGui::Button(
"Test NFD Directly")) {
1345 if (!result.empty()) {
1346 LOG_INFO(
"TestManager",
"NFD test successful: %s", result.c_str());
1349 "NFD test: No file selected, canceled, or error occurred");
1354 if (ImGui::Button(
"Test Bespoke Directly")) {
1356 if (!result.empty()) {
1357 LOG_INFO(
"TestManager",
"Bespoke test successful: %s",
1361 "Bespoke test: No file selected or not implemented");
1367 if (ImGui::CollapsingHeader(
"Test Selection",
1368 ImGuiTreeNodeFlags_DefaultOpen)) {
1369 ImGui::Text(
"Enable/Disable Individual Tests:");
1373 static const std::vector<std::pair<std::string, std::string>>
1375 {
"ROM_Header_Validation_Test",
1376 "Safe - Read-only ROM header validation"},
1377 {
"ROM_Data_Access_Test",
1378 "Safe - Basic ROM data access testing"},
1379 {
"ROM_Graphics_Extraction_Test",
1380 "Safe - Graphics data extraction testing"},
1381 {
"ROM_Overworld_Loading_Test",
1382 "Safe - Overworld data loading testing"},
1383 {
"Tile16_Editor_Test",
1384 "Moderate - Tile16 editor initialization"},
1385 {
"Comprehensive_Save_Test",
1386 "DANGEROUS - Known to crash, uses ROM copies"},
1387 {
"ROM_Sprite_Data_Test",
"Safe - Sprite data validation"},
1388 {
"ROM_Music_Data_Test",
"Safe - Music data validation"}};
1391 static bool initialized_defaults =
false;
1392 if (!initialized_defaults) {
1395 initialized_defaults =
true;
1398 if (ImGui::BeginTable(
1400 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
1401 ImGui::TableSetupColumn(
"Test Name", ImGuiTableColumnFlags_WidthFixed,
1403 ImGui::TableSetupColumn(
"Risk Level",
1404 ImGuiTableColumnFlags_WidthStretch);
1405 ImGui::TableSetupColumn(
"Status", ImGuiTableColumnFlags_WidthFixed,
1407 ImGui::TableSetupColumn(
"Action", ImGuiTableColumnFlags_WidthFixed,
1409 ImGui::TableHeadersRow();
1411 for (
const auto& [test_name, description] : known_tests) {
1414 ImGui::TableNextRow();
1415 ImGui::TableNextColumn();
1416 ImGui::Text(
"%s", test_name.c_str());
1418 ImGui::TableNextColumn();
1420 if (description.find(
"DANGEROUS") != std::string::npos) {
1422 description.c_str());
1423 }
else if (description.find(
"Moderate") != std::string::npos) {
1425 description.c_str());
1428 description.c_str());
1431 ImGui::TableNextColumn();
1440 ImGui::TableNextColumn();
1441 ImGui::PushID(test_name.c_str());
1443 if (ImGui::Button(
"Disable")) {
1445 LOG_INFO(
"TestManager",
"Disabled test: %s", test_name.c_str());
1448 if (ImGui::Button(
"Enable")) {
1450 LOG_INFO(
"TestManager",
"Enabled test: %s", test_name.c_str());
1460 ImGui::Text(
"Quick Actions:");
1462 if (ImGui::Button(
"Enable Safe Tests Only")) {
1463 for (
const auto& [test_name, description] : known_tests) {
1464 if (description.find(
"Safe") != std::string::npos) {
1470 LOG_INFO(
"TestManager",
"Enabled only safe tests");
1474 if (ImGui::Button(
"Enable All Tests")) {
1475 for (
const auto& [test_name, description] : known_tests) {
1479 "Enabled all tests (including dangerous ones)");
1483 if (ImGui::Button(
"Disable All Tests")) {
1484 for (
const auto& [test_name, description] : known_tests) {
1487 LOG_INFO(
"TestManager",
"Disabled all tests");
1493 " Recommendation: Use 'Enable Safe Tests Only' to avoid crashes");
1497 if (ImGui::CollapsingHeader(
"Platform Settings")) {
1498 ImGui::Text(
"macOS Tahoe Compatibility:");
1499 ImGui::BulletText(
"NFD may have issues on macOS Sequoia+");
1500 ImGui::BulletText(
"Bespoke dialog provides fallback option");
1502 "Global setting affects File → Open, Project dialogs, etc.");
1505 ImGui::Text(
"Test Both Implementations:");
1507 if (ImGui::Button(
"Quick Test NFD")) {
1509 LOG_INFO(
"TestManager",
"NFD test result: %s",
1510 result.empty() ?
"Failed/Canceled" : result.c_str());
1513 if (ImGui::Button(
"Quick Test Bespoke")) {
1515 LOG_INFO(
"TestManager",
"Bespoke test result: %s",
1516 result.empty() ?
"Failed/Not Implemented" : result.c_str());
1521 "Note: These tests don't change the global setting");
1529 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
1530 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
1531 ImGui::SetNextWindowSize(ImVec2(500, 300), ImGuiCond_Appearing);
1534 ImGuiWindowFlags_NoResize)) {
1538 ImGui::Text(
"A test ROM has been created with your modifications:");
1543 auto last_slash = display_filename.find_last_of(
"/\\");
1544 if (last_slash != std::string::npos) {
1545 display_filename = display_filename.substr(last_slash + 1);
1549 ImGui::Text(
"Would you like to open this test ROM in a new session?");
1552 absl::StrFormat(
"%s Open in New Session",
ICON_MD_TAB).c_str(),
1557 "User requested to open test ROM in new session: %s",
1564 absl::StrFormat(
"%s Keep Current Session",
ICON_MD_CLOSE).c_str(),
1572 "Note: Test ROM contains your modifications and can be");
1574 "opened later using File → Open");
1581 LOG_INFO(
"TestManager",
"=== TestManager ROM Refresh ===");
1586 LOG_INFO(
"TestManager",
"ROM is_loaded(): %s",
1590 LOG_INFO(
"TestManager",
"ROM size: %.2f MB",
1592 LOG_INFO(
"TestManager",
"ROM dirty: %s",
1596 LOG_INFO(
"TestManager",
"TestManager ROM pointer is null");
1598 "Note: ROM should be set by EditorManager when ROM is loaded");
1600 LOG_INFO(
"TestManager",
"===============================");
1604 std::unique_ptr<Rom>& test_rom) {
1605 if (!source_rom || !source_rom->
is_loaded()) {
1606 return absl::FailedPreconditionError(
"Source ROM not loaded");
1609 LOG_INFO(
"TestManager",
"Creating test ROM copy from: %s",
1610 source_rom->
title().c_str());
1613 test_rom = std::make_unique<Rom>();
1616 auto rom_data = source_rom->
vector();
1617 auto load_status = test_rom->LoadFromData(rom_data);
1618 if (!load_status.ok()) {
1622 LOG_INFO(
"TestManager",
"Test ROM copy created successfully (size: %.2f MB)",
1623 test_rom->size() / 1048576.0f);
1624 return absl::OkStatus();
1629 auto now = std::chrono::system_clock::now();
1630 auto time_t = std::chrono::system_clock::to_time_t(now);
1631 auto local_time = *std::localtime(&time_t);
1633 std::string timestamp =
1634 absl::StrFormat(
"%04d%02d%02d_%02d%02d%02d", local_time.tm_year + 1900,
1635 local_time.tm_mon + 1, local_time.tm_mday,
1636 local_time.tm_hour, local_time.tm_min, local_time.tm_sec);
1638 std::string base_filename = base_name;
1640 auto last_slash = base_filename.find_last_of(
"/\\");
1641 if (last_slash != std::string::npos) {
1642 base_filename = base_filename.substr(last_slash + 1);
1644 auto last_dot = base_filename.find_last_of(
'.');
1645 if (last_dot != std::string::npos) {
1646 base_filename = base_filename.substr(0, last_dot);
1649 return absl::StrFormat(
"%s_test_%s.sfc", base_filename.c_str(),
1660 Rom* source_rom, std::function<absl::Status(
Rom*)> test_function) {
1661 if (!source_rom || !source_rom->
is_loaded()) {
1662 return absl::FailedPreconditionError(
"Source ROM not loaded");
1666 std::unique_ptr<Rom> test_rom;
1669 LOG_INFO(
"TestManager",
"Executing test function on ROM copy");
1672 auto test_result = test_function(test_rom.get());
1674 LOG_INFO(
"TestManager",
"Test function completed with status: %s",
1675 test_result.ToString().c_str());
1683 LOG_INFO(
"TestManager",
"Request to load ROM for testing: %s",
1685 return absl::UnimplementedError(
1686 "ROM loading for testing not yet implemented");
1691 if (ImGui::Begin(
"ROM Comparison Results")) {
1695 if (ImGui::BeginTable(
"RomComparison", 3, ImGuiTableFlags_Borders)) {
1696 ImGui::TableSetupColumn(
"Property");
1697 ImGui::TableSetupColumn(
"Before");
1698 ImGui::TableSetupColumn(
"After");
1699 ImGui::TableHeadersRow();
1701 ImGui::TableNextRow();
1702 ImGui::TableNextColumn();
1703 ImGui::Text(
"Size");
1704 ImGui::TableNextColumn();
1705 ImGui::Text(
"%.2f MB", before.
size() / 1048576.0f);
1706 ImGui::TableNextColumn();
1707 ImGui::Text(
"%.2f MB", after.
size() / 1048576.0f);
1709 ImGui::TableNextRow();
1710 ImGui::TableNextColumn();
1711 ImGui::Text(
"Modified");
1712 ImGui::TableNextColumn();
1713 ImGui::Text(
"%s", before.
dirty() ?
"Yes" :
"No");
1714 ImGui::TableNextColumn();
1715 ImGui::Text(
"%s", after.
dirty() ?
"Yes" :
"No");
1725 return absl::FailedPreconditionError(
"No ROM loaded for testing");
1730 LOG_INFO(
"TestManager",
"Testing ROM save/load operations on copy: %s",
1731 test_rom->
title().c_str());
1740 auto save_status = test_rom->
SaveToFile(settings);
1741 if (!save_status.ok()) {
1745 LOG_INFO(
"TestManager",
"Test ROM saved successfully to: %s",
1751 return absl::OkStatus();
1757 return absl::FailedPreconditionError(
"No ROM loaded for testing");
1763 LOG_INFO(
"TestManager",
"Testing ROM data integrity on copy: %s",
1764 test_rom->
title().c_str());
1770 if (test_rom->
size() < 0x100000) {
1771 return absl::FailedPreconditionError(
1772 "ROM file too small for A Link to the Past");
1777 if (!header_status.ok()) {
1778 return header_status.status();
1781 LOG_INFO(
"TestManager",
"ROM integrity check passed for: %s",
1782 test_rom->
title().c_str());
1783 return absl::OkStatus();
1787#if defined(YAZE_WITH_GRPC)
1789 const std::string& category) {
1790 absl::MutexLock lock(&harness_history_mutex_);
1791 std::string test_id =
1792 absl::StrCat(
"harness_", absl::ToUnixMicros(absl::Now()),
"_",
1793 harness_history_.size());
1794 HarnessTestExecution execution;
1795 execution.test_id = test_id;
1796 execution.name = name;
1797 execution.category = category;
1798 execution.status = HarnessTestStatus::kQueued;
1799 execution.queued_at = absl::Now();
1800 execution.logs.reserve(32);
1801 harness_history_[test_id] = execution;
1802 TrimHarnessHistoryLocked();
1803 HarnessAggregate& aggregate = harness_aggregates_[name];
1804 aggregate.category = category;
1805 aggregate.latest_execution = execution;
1806 harness_history_order_.push_back(test_id);
1811 const std::string& category) {
1818#if defined(YAZE_WITH_GRPC)
1819void TestManager::MarkHarnessTestRunning(
const std::string& test_id) {
1820 absl::MutexLock lock(&harness_history_mutex_);
1821 auto it = harness_history_.find(test_id);
1822 if (it == harness_history_.end()) {
1825 HarnessTestExecution& execution = it->second;
1826 execution.status = HarnessTestStatus::kRunning;
1827 execution.started_at = absl::Now();
1831#if defined(YAZE_WITH_GRPC)
1832void TestManager::MarkHarnessTestCompleted(
1833 const std::string& test_id, HarnessTestStatus status,
1834 const std::string& message,
1835 const std::vector<std::string>& assertion_failures,
1836 const std::vector<std::string>& logs,
1837 const std::map<std::string, int32_t>& metrics) {
1838 HarnessTestExecution execution_snapshot;
1840 absl::MutexLock lock(&harness_history_mutex_);
1841 auto it = harness_history_.find(test_id);
1842 if (it == harness_history_.end()) {
1845 HarnessTestExecution& execution = it->second;
1846 execution.status = status;
1847 execution.completed_at = absl::Now();
1848 execution.duration = execution.completed_at - execution.started_at;
1849 execution.error_message = message;
1850 execution.metrics = metrics;
1851 execution.assertion_failures = assertion_failures;
1852 execution.logs.insert(execution.logs.end(), logs.begin(), logs.end());
1854 HarnessAggregate& aggregate = harness_aggregates_[execution.name];
1855 aggregate.latest_execution = execution;
1856 aggregate.total_runs += 1;
1857 if (status == HarnessTestStatus::kPassed) {
1858 aggregate.pass_count += 1;
1859 }
else if (status == HarnessTestStatus::kFailed ||
1860 status == HarnessTestStatus::kTimeout) {
1861 aggregate.fail_count += 1;
1863 aggregate.total_duration += execution.duration;
1864 aggregate.last_run = execution.completed_at;
1865 execution_snapshot = execution;
1868 NotifyHarnessListener(execution_snapshot);
1869 if (status == HarnessTestStatus::kFailed ||
1870 status == HarnessTestStatus::kTimeout) {
1876#if defined(YAZE_WITH_GRPC)
1877void TestManager::AppendHarnessTestLog(
const std::string& test_id,
1878 const std::string& log_entry) {
1879 absl::MutexLock lock(&harness_history_mutex_);
1880 auto it = harness_history_.find(test_id);
1881 if (it == harness_history_.end()) {
1884 HarnessTestExecution& execution = it->second;
1885 execution.logs.push_back(log_entry);
1886 harness_aggregates_[execution.name].latest_execution.logs = execution.logs;
1890#if defined(YAZE_WITH_GRPC)
1891absl::StatusOr<HarnessTestExecution> TestManager::GetHarnessTestExecution(
1892 const std::string& test_id)
const {
1893 absl::MutexLock lock(&harness_history_mutex_);
1894 auto it = harness_history_.find(test_id);
1895 if (it == harness_history_.end()) {
1896 return absl::NotFoundError(
1897 absl::StrFormat(
"Test ID '%s' not found", test_id));
1903#if defined(YAZE_WITH_GRPC)
1904std::vector<HarnessTestSummary> TestManager::ListHarnessTestSummaries(
1905 const std::string& category_filter)
const {
1906 absl::MutexLock lock(&harness_history_mutex_);
1907 std::vector<HarnessTestSummary> summaries;
1908 summaries.reserve(harness_aggregates_.size());
1909 for (
const auto& [name, aggregate] : harness_aggregates_) {
1910 if (!category_filter.empty() && aggregate.category != category_filter) {
1913 HarnessTestSummary summary;
1914 summary.latest_execution = aggregate.latest_execution;
1915 summary.total_runs = aggregate.total_runs;
1916 summary.pass_count = aggregate.pass_count;
1917 summary.fail_count = aggregate.fail_count;
1918 summary.total_duration = aggregate.total_duration;
1919 summaries.push_back(summary);
1921 std::sort(summaries.begin(), summaries.end(),
1922 [](
const HarnessTestSummary& a,
const HarnessTestSummary& b) {
1923 absl::Time time_a = a.latest_execution.completed_at;
1924 if (time_a == absl::InfinitePast()) {
1925 time_a = a.latest_execution.queued_at;
1927 absl::Time time_b = b.latest_execution.completed_at;
1928 if (time_b == absl::InfinitePast()) {
1929 time_b = b.latest_execution.queued_at;
1931 return time_a > time_b;
1937#if defined(YAZE_WITH_GRPC)
1938void TestManager::CaptureFailureContext(
const std::string& test_id) {
1939 HarnessTestStatus expected_status = HarnessTestStatus::kUnspecified;
1940 absl::Time expected_completed_at = absl::InfinitePast();
1942 absl::MutexLock lock(&harness_history_mutex_);
1943 auto it = harness_history_.find(test_id);
1944 if (it == harness_history_.end()) {
1947 expected_status = it->second.status;
1948 expected_completed_at = it->second.completed_at;
1951 auto apply_capture =
1952 [
this, test_id, expected_status, expected_completed_at](
1953 absl::StatusOr<ScreenshotArtifact> screenshot_artifact) {
1954 absl::MutexLock lock(&harness_history_mutex_);
1955 auto it = harness_history_.find(test_id);
1956 if (it == harness_history_.end()) {
1959 HarnessTestExecution& execution = it->second;
1960 if (execution.status != expected_status ||
1961 execution.completed_at != expected_completed_at) {
1964 execution.failure_context =
1965 screenshot_artifact.ok()
1966 ?
"Harness failure context captured successfully"
1967 :
"Harness failure context capture unavailable";
1968 if (screenshot_artifact.ok()) {
1969 execution.screenshot_path = screenshot_artifact->file_path;
1970 execution.screenshot_size_bytes =
1971 screenshot_artifact->file_size_bytes;
1973 HarnessAggregate& aggregate = harness_aggregates_[execution.name];
1974 if (aggregate.latest_execution.test_id == test_id) {
1975 harness_aggregates_[execution.name].latest_execution = execution;
1979 FailureScreenshotRequester screenshot_requester;
1981 absl::MutexLock lock(&mutex_);
1982 screenshot_requester = failure_screenshot_requester_;
1984 if (!screenshot_requester) {
1985 apply_capture(absl::FailedPreconditionError(
"Controller unavailable"));
1990 std::move(apply_capture));
1993void TestManager::CaptureFailureContext(
const std::string& test_id) {
1998#if defined(YAZE_WITH_GRPC)
1999void TestManager::TrimHarnessHistoryLocked() {
2000 while (harness_history_order_.size() > harness_history_limit_) {
2001 const std::string& oldest_test = harness_history_order_.front();
2002 harness_history_order_.pop_front();
2003 harness_history_.erase(oldest_test);
2007absl::Status TestManager::ReplayLastPlan() {
2008 return absl::FailedPreconditionError(
"Harness plan replay not available");
2011void TestManager::SetHarnessListener(
2012 std::shared_ptr<HarnessListener> listener) {
2013 absl::MutexLock lock(&mutex_);
2014 harness_listener_ = std::move(listener);
2017void TestManager::ClearHarnessListener(
const HarnessListener* listener) {
2018 absl::MutexLock lock(&mutex_);
2019 if (harness_listener_.get() == listener) {
2020 harness_listener_.reset();
2024void TestManager::SetFailureScreenshotRequester(
2025 FailureScreenshotRequester requester) {
2026 absl::MutexLock lock(&mutex_);
2027 failure_screenshot_requester_ = std::move(requester);
2030void TestManager::NotifyHarnessListener(
const HarnessTestExecution& execution) {
2031 std::shared_ptr<HarnessListener> listener;
2033 absl::MutexLock lock(&mutex_);
2034 listener = harness_listener_;
2037 listener->OnHarnessTestUpdated(execution);
2041absl::Status TestManager::ReplayLastPlan() {
2042 return absl::UnimplementedError(
"Harness features require YAZE_WITH_GRPC");
2046absl::Status TestManager::ShowHarnessDashboard() {
2049#if defined(YAZE_WITH_GRPC)
2050 return absl::OkStatus();
2052 return absl::UnimplementedError(
"Harness features require YAZE_WITH_GRPC");
2056absl::Status TestManager::ShowHarnessActiveTests() {
2057#if defined(YAZE_WITH_GRPC)
2058 return absl::OkStatus();
2060 return absl::UnimplementedError(
"Harness features require YAZE_WITH_GRPC");
2064void TestManager::RecordPlanSummary(
const std::string& summary) {
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
absl::StatusOr< std::vector< uint8_t > > ReadByteVector(uint32_t offset, uint32_t length) const
const auto & vector() const
absl::Status SaveToFile(const SaveSettings &settings)
RAII guard for ImGui style colors.
bool show_resource_monitor_
std::string GenerateTestRomFilename(const std::string &base_name)
void CaptureFailureContext(const std::string &test_id)
std::string current_test_name_
bool show_test_configuration_
bool show_test_session_dialog_
TestCategory category_filter_
std::vector< ResourceStats > resource_history_
void RegisterTestSuite(std::unique_ptr< TestSuite > suite)
absl::Status TestRomWithCopy(Rom *source_rom, std::function< absl::Status(Rom *)> test_function)
std::unordered_map< std::string, TestSuite * > suite_lookup_
absl::Status CreateTestRomCopy(Rom *source_rom, std::unique_ptr< Rom > &test_rom)
bool IsTestEnabled(const std::string &test_name) const
absl::Status RunTestSuite(const std::string &suite_name)
std::string RegisterHarnessTest(const std::string &name, const std::string &category)
absl::Status ExecuteTestSuite(TestSuite *suite)
bool show_rom_test_results_
std::optional< bool > remote_replay_restore_focus_
void DestroyUITestingContext()
std::string test_rom_path_for_session_
bool show_rom_file_dialog_
absl::Status TestRomDataIntegrity(Rom *rom)
static constexpr size_t kMaxResourceHistorySize
void TrimResourceHistory()
ImGuiTestEngine * ui_test_engine_
void DrawTestDashboard(bool *show_dashboard=nullptr)
TestSuite * GetTestSuite(const std::string &name)
absl::Status RunAllTests()
static TestManager & Get()
void OfferTestSessionCreation(const std::string &test_rom_path)
TestResults last_results_
void EnableTest(const std::string &test_name)
void CollectResourceStats()
void DisableTest(const std::string &test_name)
std::vector< std::unique_ptr< TestSuite > > test_suites_
absl::Status TestRomSaveLoad(Rom *rom)
absl::Status RunTestsByCategory(TestCategory category)
void UpdateResourceStats()
void ShowRomComparisonResults(const Rom &before, const Rom &after)
void InitializeUITesting()
std::vector< std::string > GetTestSuiteNames() const
absl::Status LoadRomForTesting(const std::string &filename)
virtual absl::Status RunTests(TestResults &results)=0
static std::string ShowOpenFileDialogBespoke()
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
static std::string ShowOpenFileDialogNFD()
#define ICON_MD_FOLDER_OPEN
#define ICON_MD_ASSESSMENT
#define ICON_MD_CAMERA_ALT
#define ICON_MD_CHECKLIST
#define ICON_MD_PLAY_ARROW
#define ICON_MD_PLAY_CIRCLE_FILLED
#define ICON_MD_TIMER_OFF
#define ICON_MD_ERROR_OUTLINE
#define ICON_MD_CHECK_CIRCLE
#define ICON_MD_SKIP_NEXT
#define ICON_MD_DESCRIPTION
#define ICON_MD_FILTER_LIST
#define ICON_MD_ANALYTICS
#define LOG_WARN(category, format,...)
#define LOG_INFO(category, format,...)
ButtonColorSet GetSuccessButtonColors()
ImVec4 GetDisabledColor()
std::string GenerateFailureScreenshotPath(const std::string &test_id)
const char * TestStatusToString(TestStatus status)
void RegisterOverworldUITests(ImGuiTestEngine *)
void RegisterDungeonUITests(ImGuiTestEngine *)
const char * TestCategoryToString(TestCategory category)
ImVec4 GetTestStatusColor(TestStatus status)
#define RETURN_IF_ERROR(expr)
bool kUseNativeFileDialog
std::chrono::time_point< std::chrono::steady_clock > timestamp
std::vector< TestResult > individual_results
std::chrono::milliseconds total_duration
float GetPassRate() const