From 09a15fbedc9bd139875be3063aa130532320346c Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:19:12 +0900 Subject: [PATCH] [Testing] Get tests running (dirty hack for linux) --- src/xenia/app/xenia_main.cc | 2 +- src/xenia/base/platform_amd64.cc | 4 +- src/xenia/cpu/backend/x64/x64_seq_memory.cc | 7 +- src/xenia/cpu/ppc/testing/instr_vmaddfp.s | 2 +- src/xenia/cpu/ppc/testing/ppc_testing_main.cc | 175 +++++++++++++++--- .../ppc/testing/ppc_testing_native_main.cc | 5 +- src/xenia/cpu/ppc/testing/premake5.lua | 25 ++- src/xenia/cpu/test_module.cc | 8 + src/xenia/kernel/xbdm/xbdm_misc.cc | 2 +- src/xenia/kernel/xbdm/xbdm_module.cc | 8 +- third_party/binutils/build.sh | 2 +- tools/build/src/test_suite_main.cc | 1 + xenia-build.py | 14 +- 13 files changed, 209 insertions(+), 46 deletions(-) diff --git a/src/xenia/app/xenia_main.cc b/src/xenia/app/xenia_main.cc index b70ba2d7c..cf8451aef 100644 --- a/src/xenia/app/xenia_main.cc +++ b/src/xenia/app/xenia_main.cc @@ -91,7 +91,7 @@ DEFINE_bool(mount_scratch, false, "Enable scratch mount", "Storage"); DEFINE_bool(mount_cache, true, "Enable cache mount", "Storage"); UPDATE_from_bool(mount_cache, 2024, 8, 31, 20, false); -DEFINE_bool(force_mount_devkit, false, "Force devkit mount", "Storage"); +DECLARE_bool(force_mount_devkit); DEFINE_transient_path(target, "", "Specifies the target .xex or .iso to execute.", diff --git a/src/xenia/base/platform_amd64.cc b/src/xenia/base/platform_amd64.cc index c4f8fa3e1..70ff811bb 100644 --- a/src/xenia/base/platform_amd64.cc +++ b/src/xenia/base/platform_amd64.cc @@ -36,7 +36,9 @@ namespace amd64 { static uint64_t g_feature_flags = 0U; static bool g_did_initialize_feature_flags = false; uint64_t GetFeatureFlags() { - xenia_assert(g_did_initialize_feature_flags); + if (!g_did_initialize_feature_flags) { + InitFeatureFlags(); + } return g_feature_flags; } XE_COLD diff --git a/src/xenia/cpu/backend/x64/x64_seq_memory.cc b/src/xenia/cpu/backend/x64/x64_seq_memory.cc index e77669d9b..d4256b4dc 100644 --- a/src/xenia/cpu/backend/x64/x64_seq_memory.cc +++ b/src/xenia/cpu/backend/x64/x64_seq_memory.cc @@ -1185,7 +1185,12 @@ static bool IsPossibleMMIOInstruction(X64Emitter& e, const hir::Instr* i) { return false; } - auto flags = e.GuestModule()->GetInstructionAddressFlags(guestaddr); + auto guest_module = e.GuestModule(); + if (!guest_module) { + return false; + } + + auto flags = guest_module->GetInstructionAddressFlags(guestaddr); return flags && flags->accessed_mmio; } diff --git a/src/xenia/cpu/ppc/testing/instr_vmaddfp.s b/src/xenia/cpu/ppc/testing/instr_vmaddfp.s index 74005af4c..97a99c9b0 100644 --- a/src/xenia/cpu/ppc/testing/instr_vmaddfp.s +++ b/src/xenia/cpu/ppc/testing/instr_vmaddfp.s @@ -3,7 +3,7 @@ test_vmaddfp_1: # 1.0, 1.5, 1.1, 1.9 vmaddfp v3, v4, v4, v4 blr - #_ REGISTER_OUT v3 [40000000, 40700000, 4013d70a, 40b051eb] + #_ REGISTER_OUT v3 [40000000, 40700000, 4013d70b, 40b051eb] #_ REGISTER_OUT v4 [3f800000, 3fc00000, 3f8ccccd, 3ff33333] # 2.0, 3.75, 2.31, 5.51 # 40b051eb is actually 5.50999975, not 5.51? diff --git a/src/xenia/cpu/ppc/testing/ppc_testing_main.cc b/src/xenia/cpu/ppc/testing/ppc_testing_main.cc index 32ce71e87..26280403a 100644 --- a/src/xenia/cpu/ppc/testing/ppc_testing_main.cc +++ b/src/xenia/cpu/ppc/testing/ppc_testing_main.cc @@ -27,6 +27,9 @@ #if XE_COMPILER_MSVC #include "xenia/base/platform_win.h" +#else +#include +#include #endif // XE_COMPILER_MSVC DEFINE_path(test_path, "src/xenia/cpu/ppc/testing/", @@ -191,6 +194,10 @@ class TestRunner { } bool Setup(TestSuite& suite) { + // Reset thread state first so it can properly deinitialize with the + // existing processor before we destroy the processor. + thread_state_.reset(); + // Reset memory. memory_->Reset(); @@ -245,14 +252,17 @@ class TestRunner { bool Run(TestCase& test_case) { // Setup test state from annotations. if (!SetupTestState(test_case)) { - XELOGE("Test setup failed"); + fprintf(stderr, " [%s] Test setup failed\n", test_case.name.c_str()); + fflush(stderr); return false; } // Execute test. auto fn = processor_->ResolveFunction(test_case.address); if (!fn) { - XELOGE("Entry function not found"); + fprintf(stderr, " [%s] Entry function not found\n", + test_case.name.c_str()); + fflush(stderr); return false; } @@ -316,9 +326,13 @@ class TestRunner { if (!ppc_context->CompareRegWithString( reg_name.c_str(), reg_value.c_str(), actual_value)) { any_failed = true; - XELOGE("Register {} assert failed:\n", reg_name); - XELOGE(" Expected: {} == {}\n", reg_name, reg_value); - XELOGE(" Actual: {} == {}\n", reg_name, actual_value); + fprintf(stderr, " [%s] Register %s assert failed:\n", + test_case.name.c_str(), reg_name.c_str()); + fprintf(stderr, " Expected: %s == %s\n", reg_name.c_str(), + reg_value.c_str()); + fprintf(stderr, " Actual: %s == %s\n", reg_name.c_str(), + actual_value.c_str()); + fflush(stderr); } } else if (it.first == "MEMORY_OUT") { size_t space_pos = it.second.find(" "); @@ -355,9 +369,11 @@ class TestRunner { ++p; } if (failed) { - XELOGE("Memory {} assert failed:\n", address_str); - XELOGE(" Expected:{}\n", expecteds.to_string()); - XELOGE(" Actual:{}\n", actuals.to_string()); + fprintf(stderr, " [%s] Memory %s assert failed:\n", + test_case.name.c_str(), address_str.c_str()); + fprintf(stderr, " Expected:%s\n", expecteds.to_string().c_str()); + fprintf(stderr, " Actual:%s\n", actuals.to_string().c_str()); + fflush(stderr); } } } @@ -375,7 +391,11 @@ bool DiscoverTests(const std::filesystem::path& test_path, auto file_infos = xe::filesystem::ListFiles(test_path); for (auto& file_info : file_infos) { if (file_info.name.extension() == ".s") { - test_files.push_back(test_path / file_info.name); + // Only include test files (instr_*.s), not helper files + auto filename = file_info.name.filename().string(); + if (filename.find("instr_") == 0) { + test_files.push_back(test_path / file_info.name); + } } } return true; @@ -390,27 +410,128 @@ int filter(unsigned int code) { } #endif // XE_COMPILER_MSVC +#if !XE_COMPILER_MSVC +// Run test in isolated child process to catch crashes +enum class TestResult { + kPassed, + kFailed, + kCrashed, +}; + +TestResult RunTestInChildProcess(TestSuite& test_suite, TestCase& test_case) { + pid_t pid = fork(); + + if (pid == -1) { + // Fork failed + fprintf(stderr, " [%s] TEST FAILED (fork failed)\n", + test_case.name.c_str()); + fflush(stderr); + return TestResult::kFailed; + } + + if (pid == 0) { + // Child process - create a fresh TestRunner to avoid inherited state issues + TestRunner child_runner; + if (!child_runner.Setup(test_suite)) { + _exit(2); // Setup failure + } + if (child_runner.Run(test_case)) { + _exit(0); // Test passed + } else { + _exit(1); // Test failed + } + } + + // Parent process - wait for child + int status; + pid_t result = waitpid(pid, &status, 0); + + if (result == -1) { + fprintf(stderr, " [%s] TEST FAILED (waitpid failed, pid %d)\n", + test_case.name.c_str(), pid); + fflush(stderr); + return TestResult::kFailed; + } + + if (WIFEXITED(status)) { + int exit_code = WEXITSTATUS(status); + if (exit_code == 0) { + // Test passed - don't print anything + return TestResult::kPassed; + } else if (exit_code == 2) { + fprintf(stderr, " [%s] FAILED SETUP (exit code %d)\n", + test_case.name.c_str(), exit_code); + fflush(stderr); + return TestResult::kFailed; + } else { + fprintf(stderr, " [%s] FAILED (exit code %d)\n", test_case.name.c_str(), + exit_code); + fflush(stderr); + return TestResult::kFailed; + } + } + + if (WIFSIGNALED(status)) { + int signal = WTERMSIG(status); + const char* signal_name = "UNKNOWN"; + switch (signal) { + case SIGSEGV: + signal_name = "SIGSEGV"; + break; + case SIGILL: + signal_name = "SIGILL"; + break; + case SIGFPE: + signal_name = "SIGFPE"; + break; + case SIGBUS: + signal_name = "SIGBUS"; + break; + case SIGABRT: + signal_name = "SIGABRT"; + break; + } + fprintf(stderr, " [%s] CRASHED (%s)\n", test_case.name.c_str(), + signal_name); + fflush(stderr); + return TestResult::kCrashed; + } + + fprintf(stderr, " [%s] FAILED (unknown reason)\n", test_case.name.c_str()); + fflush(stderr); + return TestResult::kFailed; +} +#endif // !XE_COMPILER_MSVC + void ProtectedRunTest(TestSuite& test_suite, TestRunner& runner, TestCase& test_case, int& failed_count, int& passed_count) { #if XE_COMPILER_MSVC __try { -#endif // XE_COMPILER_MSVC - if (!runner.Setup(test_suite)) { - XELOGE(" TEST FAILED SETUP"); + XELOGE(" [{}] TEST FAILED SETUP", test_case.name); ++failed_count; + return; } if (runner.Run(test_case)) { ++passed_count; } else { - XELOGE(" TEST FAILED"); + XELOGE(" [{}] TEST FAILED", test_case.name); ++failed_count; } - -#if XE_COMPILER_MSVC } __except (filter(GetExceptionCode())) { - XELOGE(" TEST FAILED (UNSUPPORTED INSTRUCTION)"); + XELOGE(" [{}] TEST FAILED (UNSUPPORTED INSTRUCTION)", test_case.name); + ++failed_count; + } +#else + // Use fork to isolate crashes on POSIX systems + // Note: runner parameter is not used on POSIX + (void)runner; // Suppress unused parameter warning + TestResult result = RunTestInChildProcess(test_suite, test_case); + + if (result == TestResult::kPassed) { + ++passed_count; + } else { ++failed_count; } #endif // XE_COMPILER_MSVC @@ -456,23 +577,27 @@ bool RunTests(const std::string_view test_name) { } XELOGI("{} tests loaded.", test_suites.size()); +#if XE_COMPILER_MSVC + // On Windows, use a single shared test runner TestRunner runner; +#else + // On POSIX, each test will create its own runner in a forked process + // Pass a dummy value that won't be used + TestRunner* runner_ptr = nullptr; + TestRunner& runner = *runner_ptr; // Never dereferenced on POSIX +#endif for (auto& test_suite : test_suites) { - XELOGI("{}.s:", test_suite.name()); - for (auto& test_case : test_suite.test_cases()) { - XELOGI(" - {}", test_case.name); ProtectedRunTest(test_suite, runner, test_case, failed_count, passed_count); } - - XELOGI(""); } - XELOGI(""); - XELOGI("Total tests: {}", failed_count + passed_count); - XELOGI("Passed: {}", passed_count); - XELOGI("Failed: {}", failed_count); + fprintf(stderr, "\n"); + fprintf(stderr, "Total tests: %d\n", failed_count + passed_count); + fprintf(stderr, "Passed: %d\n", passed_count); + fprintf(stderr, "Failed: %d\n", failed_count); + fflush(stderr); return failed_count ? false : true; } diff --git a/src/xenia/cpu/ppc/testing/ppc_testing_native_main.cc b/src/xenia/cpu/ppc/testing/ppc_testing_native_main.cc index e98a16361..d556437c6 100644 --- a/src/xenia/cpu/ppc/testing/ppc_testing_native_main.cc +++ b/src/xenia/cpu/ppc/testing/ppc_testing_native_main.cc @@ -411,7 +411,10 @@ bool DiscoverTests(std::wstring& test_path, for (auto& file_info : file_infos) { if (file_info.name != L"." && file_info.name != L".." && file_info.name.rfind(L".s") == file_info.name.size() - 2) { - test_files.push_back(xe::join_paths(test_path, file_info.name)); + // Only include test files (instr_*.s), not helper files + if (file_info.name.find(L"instr_") == 0) { + test_files.push_back(xe::join_paths(test_path, file_info.name)); + } } } return true; diff --git a/src/xenia/cpu/ppc/testing/premake5.lua b/src/xenia/cpu/ppc/testing/premake5.lua index 2b724eed2..6db094847 100644 --- a/src/xenia/cpu/ppc/testing/premake5.lua +++ b/src/xenia/cpu/ppc/testing/premake5.lua @@ -7,17 +7,25 @@ project("xenia-cpu-ppc-tests") kind("ConsoleApp") language("C++") links({ - "capstone", -- cpu-backend-x64 - "fmt", - "mspack", - "imgui", + "xenia-apu", + "xenia-apu-nop", + "xenia-base", "xenia-core", "xenia-cpu", "xenia-gpu", - "xenia-base", + "xenia-gpu-null", + "xenia-hid", + "xenia-hid-nop", "xenia-kernel", "xenia-patcher", - "xenia-hid-skylander", + "xenia-ui", + "xenia-vfs", + }) + links({ + "capstone", + "fmt", + "imgui", + "mspack", }) files({ "ppc_testing_main.cc", @@ -41,10 +49,7 @@ project("xenia-cpu-ppc-tests") "1>scratch/stdout-testing.txt", }) - -- xenia-base needs this - links({ - "xenia-ui", - }) + filter({}) if ARCH == "ppc64" or ARCH == "powerpc64" then diff --git a/src/xenia/cpu/test_module.cc b/src/xenia/cpu/test_module.cc index 9ad0c12b9..181680daf 100644 --- a/src/xenia/cpu/test_module.cc +++ b/src/xenia/cpu/test_module.cc @@ -85,8 +85,13 @@ Symbol::Status TestModule::DeclareFunction(uint32_t address, // Reset() all caching when we leave. xe::make_reset_scope(compiler_); xe::make_reset_scope(assembler_); + xe::make_reset_scope(builder_); + + // Set the HIRBuilder as current for this thread + builder_->MakeCurrent(); if (!generate_(*builder_.get())) { + builder_->RemoveCurrent(); function->set_status(Symbol::Status::kFailed); return Symbol::Status::kFailed; } @@ -97,6 +102,9 @@ Symbol::Status TestModule::DeclareFunction(uint32_t address, // Assemble the function. assembler_->Assemble(function, builder_.get(), 0, nullptr); + // Remove the HIRBuilder as current for this thread + builder_->RemoveCurrent(); + status = Symbol::Status::kDefined; function->set_status(status); } diff --git a/src/xenia/kernel/xbdm/xbdm_misc.cc b/src/xenia/kernel/xbdm/xbdm_misc.cc index 264b3d50c..f19db13f6 100644 --- a/src/xenia/kernel/xbdm/xbdm_misc.cc +++ b/src/xenia/kernel/xbdm/xbdm_misc.cc @@ -20,7 +20,7 @@ DECLARE_bool(debug); -DECLARE_bool(force_mount_devkit); +DEFINE_bool(force_mount_devkit, false, "Force devkit mount", "Storage"); namespace xe { namespace kernel { diff --git a/src/xenia/kernel/xbdm/xbdm_module.cc b/src/xenia/kernel/xbdm/xbdm_module.cc index 0bb6674e8..4eb457d6f 100644 --- a/src/xenia/kernel/xbdm/xbdm_module.cc +++ b/src/xenia/kernel/xbdm/xbdm_module.cc @@ -28,9 +28,14 @@ XbdmModule::XbdmModule(Emulator* emulator, KernelState* kernel_state) #undef XE_MODULE_EXPORT_GROUP } -std::vector xbdm_exports(4096); +// Use construct-on-first-use idiom to avoid static initialization order issues +std::vector& GetXbdmExports() { + static std::vector xbdm_exports(4096); + return xbdm_exports; +} xe::cpu::Export* RegisterExport_xbdm(xe::cpu::Export* export_entry) { + auto& xbdm_exports = GetXbdmExports(); assert_true(export_entry->ordinal < xbdm_exports.size()); xbdm_exports[export_entry->ordinal] = export_entry; return export_entry; @@ -45,6 +50,7 @@ void XbdmModule::RegisterExportTable(xe::cpu::ExportResolver* export_resolver) { #include "xenia/kernel/xbdm/xbdm_table.inc" }; #include "xenia/kernel/util/export_table_post.inc" + auto& xbdm_exports = GetXbdmExports(); for (size_t i = 0; i < xe::countof(xbdm_export_table); ++i) { auto& export_entry = xbdm_export_table[i]; assert_true(export_entry.ordinal < xbdm_exports.size()); diff --git a/third_party/binutils/build.sh b/third_party/binutils/build.sh index 21a119ce5..587356210 100644 --- a/third_party/binutils/build.sh +++ b/third_party/binutils/build.sh @@ -21,7 +21,7 @@ SNAPSHOT_FILE=binutils-$SNAPSHOT_VERSION.tar.gz if [ ! -f $SNAPSHOT_FILE ]; then echo "" echo "Grabbing binutils snapshot $SNAPSHOT_FILE..." - curl -nv http://mirrors.kernel.org/sourceware/binutils/releases/$SNAPSHOT_FILE > $SNAPSHOT_FILE + curl -L http://mirrors.kernel.org/sourceware/binutils/releases/$SNAPSHOT_FILE -o $SNAPSHOT_FILE else echo "" echo "Reusing existing binutils snapshot file $SNAPSHOT_FILE..." diff --git a/tools/build/src/test_suite_main.cc b/tools/build/src/test_suite_main.cc index 1e34b4c8f..4bf3842a1 100644 --- a/tools/build/src/test_suite_main.cc +++ b/tools/build/src/test_suite_main.cc @@ -15,6 +15,7 @@ #include "xenia/base/console_app_main.h" #include "xenia/base/cvar.h" +#include "xenia/base/platform.h" #define CATCH_CONFIG_RUNNER #include "third_party/catch/single_include/catch2/catch.hpp" diff --git a/xenia-build.py b/xenia-build.py index 3057a39f2..8398f68b8 100755 --- a/xenia-build.py +++ b/xenia-build.py @@ -539,7 +539,7 @@ def run_premake(target_os, action, cc=None): os.path.join("tools", "build", "premake.py"), "--file=premake5.lua", f"--os={target_os}", - #"--test-suite-mode=combined", + "--test-suite-mode=combined", "--verbose", action, ] @@ -1218,12 +1218,20 @@ class TestCommand(BaseBuildCommand): print_error(f"Unable to find {test_targets[i]} - build it.") return 1 + # Prepare environment with Qt bin directory in PATH if available + test_env = dict(os.environ) + qt_dir = os.environ.get("QT_DIR") + if qt_dir and sys.platform == "win32": + qt_bin = os.path.join(qt_dir, "bin") + if os.path.exists(qt_bin): + test_env["PATH"] = f"{qt_bin}{os.pathsep}{test_env['PATH']}" + print(f"- Qt bin directory added to PATH: {qt_bin}\n") + # Run tests. any_failed = False for test_executable in test_executables: print(f"- {test_executable}") - result = shell_call([test_executable] + pass_args, - throw_on_error=False) + result = subprocess.call([test_executable] + pass_args, env=test_env) if result: any_failed = True if args["continue"]: