#include "zeroerr/unittest.h" #include "common/CrashHandler.h" #include #if defined(EVENGINE_ANDROID) #include #include #include #include #include #include #include #include #include namespace { // Route C++ iostream output to Android logcat so `adb logcat` can show the // zeroerr console reporter (stdout/stderr are not attached to anything on Android). class LogcatBuf : public std::streambuf { public: LogcatBuf() { setp(buffer_, buffer_ + sizeof(buffer_) - 1); } ~LogcatBuf() override { sync(); } protected: int overflow(int ch) override { if (ch != traits_type::eof()) { *pptr() = static_cast(ch); pbump(1); } return sync() == 0 ? traits_type::not_eof(ch) : traits_type::eof(); } int sync() override { if (pptr() == pbase()) return 0; *pptr() = '\0'; __android_log_print(ANDROID_LOG_INFO, "EVEngineTest", "%s", pbase()); setp(buffer_, buffer_ + sizeof(buffer_) - 1); return 0; } private: char buffer_[1024]; }; // Root of the unpacked test tree: /evengine_test. The Java // activity unpacks the APK's assets/test + assets/examples here before SDL_main runs. std::string testRoot() { const char *internal = SDL_AndroidGetInternalStoragePath(); if (!internal) return {}; return std::string(internal) + "/evengine_test"; } } // namespace #elif defined(EVENGINE_IOS_TEST_APP) #include #include #include "ios_test.h" #include #include #include #include #include #include #include #include namespace { // Route C++ iostream output to unified logging (os_log) so `log stream` can // show the zeroerr console reporter from a device (stdout/stderr are not // attached to anything when the app is launched from the home screen). class OSLogBuf : public std::streambuf { public: OSLogBuf() { setp(buffer_, buffer_ + sizeof(buffer_) - 1); } ~OSLogBuf() override { sync(); } protected: int overflow(int ch) override { if (ch != traits_type::eof()) { *pptr() = static_cast(ch); pbump(1); } return sync() == 0 ? traits_type::not_eof(ch) : traits_type::eof(); } int sync() override { if (pptr() == pbase()) return 0; *pptr() = '\0'; eve::ios_test::logLine(pbase()); setp(buffer_, buffer_ + sizeof(buffer_) - 1); return 0; } private: char buffer_[1024]; }; // Root of the staged test tree (Caches/evengine_test), unpacked by // test/ios_test.mm from the app bundle's test/ + examples/ resources. The // runner chdirs there so EVENGINE_SOURCE_DIR="." / EVENGINE_TEST_BINARY_DIR="." // resolve on-device, exactly like the Android test app. std::string testRoot() { return eve::ios_test::stagedTestRoot(); } // Escape regex metacharacters in a test-file basename so --file matching is // exact (e.g. "data.cpp" must not also match "hex_level_data.cpp"). std::string escapeRegex(const std::string &s) { std::string out; for (char c : s) { if (std::string(".^$|()[]{}*+?\\").find(c) != std::string::npos) out += '\\'; out += c; } return out; } } // namespace #endif // EVENGINE_ANDROID / EVENGINE_IOS_TEST_APP // Signature must match the `extern "C" int SDL_main(int, char*[])` declared by // SDL_main.h so the symbol stays unmangled (Android: SDLActivity JNI dlsym; // iOS: SDL's UIKit main calls it). A `const char**` parameter would become a // C++ overload and be mangled. int main(int argc, char **argv) { #if defined(EVENGINE_WINDOWS) || defined(_WIN32) // Same symbolized crash backtrace as the engine binary (main.cpp), so a // crashing test prints a stack trace instead of a bare access-violation // exit code. eve::installCrashHandler(); #endif #if defined(EVENGINE_ANDROID) static LogcatBuf logcatBuf; std::cout.rdbuf(&logcatBuf); std::cerr.rdbuf(&logcatBuf); std::clog.rdbuf(&logcatBuf); std::cout.setf(std::ios::unitbuf); std::cerr.setf(std::ios::unitbuf); const std::string root = testRoot(); if (root.empty()) { __android_log_print(ANDROID_LOG_ERROR, "EVEngineTest", "no internal storage path"); } else if (chdir(root.c_str()) != 0) { __android_log_print(ANDROID_LOG_ERROR, "EVEngineTest", "chdir(%s) failed: %s", root.c_str(), strerror(errno)); } else { __android_log_print(ANDROID_LOG_INFO, "EVEngineTest", "test root: %s", root.c_str()); } // SDLActivity already ran SDL_SetMainReady(); init the subsystems the tests use. if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER | SDL_INIT_AUDIO) != 0) { __android_log_print(ANDROID_LOG_ERROR, "EVEngineTest", "SDL_Init failed: %s", SDL_GetError()); } __android_log_print(ANDROID_LOG_INFO, "EVEngineTest", "unit_test android starting"); #elif defined(EVENGINE_IOS_TEST_APP) static OSLogBuf osLogBuf; std::cout.rdbuf(&osLogBuf); std::cerr.rdbuf(&osLogBuf); std::clog.rdbuf(&osLogBuf); std::cout.setf(std::ios::unitbuf); std::cerr.setf(std::ios::unitbuf); // The suite can run for minutes; keep the screen on so iOS does not // suspend/force-kill the foreground app when the screen auto-locks. eve::ios_test::keepAwake(); const std::string root = testRoot(); if (root.empty()) { eve::ios_test::logLine("no writable test root"); } else if (chdir(root.c_str()) != 0) { eve::ios_test::logLine(("chdir(" + root + ") failed: " + std::strerror(errno)).c_str()); } else { eve::ios_test::logLine(("test root: " + root).c_str()); } // SDL's UIKit main already ran SDL_SetMainReady(); init the subsystems the // tests use (window/graphics tests open a real SDL_WINDOW_VULKAN surface). if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER | SDL_INIT_AUDIO) != 0) { eve::ios_test::logLine(("SDL_Init failed: " + std::string(SDL_GetError())).c_str()); } eve::ios_test::logLine("unit_test ios starting"); // Inject --testcase= from the launch arguments / UserDefaults // (mirrors the evengine.test.filter intent extra on Android). A file // filter (-evengine.test.file ) takes precedence so the suite // can be run one test file per process (see run/ios-test-all-debug). const std::string fileFilter = eve::ios_test::launchFileFilter(); const std::string filter = eve::ios_test::launchFilter(); static std::string filterArg; if (!fileFilter.empty()) { // TestCase.file stores the compiled __FILE__ path; regex_match needs a // full match, so anchor the escaped basename after a path separator at // the end of the path. filterArg = "--file=.*/" + escapeRegex(fileFilter); static std::vector injected; injected = {argc > 0 ? argv[0] : const_cast("eve"), filterArg.data()}; argc = static_cast(injected.size()); argv = injected.data(); } else if (!filter.empty()) { filterArg = "--testcase=" + filter; static std::vector injected; injected = {argc > 0 ? argv[0] : const_cast("eve"), filterArg.data()}; argc = static_cast(injected.size()); argv = injected.data(); } #endif // EVENGINE_ANDROID / EVENGINE_IOS_TEST_APP const int result = zeroerr::UnitTest().parseArgs(argc, const_cast(argv)).run(); #if defined(EVENGINE_WINDOWS) || defined(_WIN32) // Process-isolated CTest cases have already reported and released their // test-local state here. Some Release-only process-lifetime services block // in static destruction after a passing summary, so terminate with the // authoritative zeroerr result instead of letting CTest misclassify the // case as a 25-minute timeout. std::cout.flush(); std::cerr.flush(); std::_Exit(result); #elif defined(EVENGINE_IOS_TEST_APP) // SDL's UIKit main keeps the app resident after SDL_main returns; a test // runner should terminate once the suite is done so per-file launches // (--console) return and a driver can move on to the next file. eve::ios_test::logLine("unit_test ios done"); std::exit(result); #else return result; #endif }