diff --git a/.gitignore b/.gitignore index b3f34dd6..1cd3248b 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ platform/android/apk/app/src/main/jniLibs/ # Filled by `make sync/android-assets` (source: platform/android/game-shell or example/). platform/android/apk/app/src/main/assets/game/* !platform/android/apk/app/src/main/assets/game/.gitkeep +# Filled by `make sync/android-test-assets` (zeroerr suite + examples on-device tree). +platform/android/apk/app/src/main/assets/test/ +platform/android/apk/app/src/main/assets/examples/ diff --git a/CMakeLists.txt b/CMakeLists.txt index fd87a1bc..4350becf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,12 @@ set(EVENGINE_PATCH_VERSION "0") option(BUILD_TESTING "enable testing" ON) option(EVENGINE_BUILD_DEMO "Build optional Demo module (procedural demo assets + default demo.nut)" ON) +option(EVENGINE_ANDROID_TEST_APP + "Build the unit_test suite as an Android test app (libmain.so entry replaced by the zeroerr runner)" + OFF) message("Build UnitTest: " ${BUILD_TESTING}) message("Build Demo module: " ${EVENGINE_BUILD_DEMO}) +message("Build Android test app: " ${EVENGINE_ANDROID_TEST_APP}) if (CMAKE_BUILD_TYPE STREQUAL "") set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "The build type (Debug, Release)") endif() @@ -46,8 +50,12 @@ function(main) set(USE_MOLD OFF CACHE BOOL "" FORCE) set(COLORFUL_OUTPUT OFF CACHE STRING "" FORCE) if(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) - set(BUILD_TESTING OFF CACHE BOOL "enable testing" FORCE) - message(STATUS "Android build: unit tests disabled") + if(EVENGINE_ANDROID_TEST_APP) + message(STATUS "Android build: unit tests enabled (Android test app)") + else() + set(BUILD_TESTING OFF CACHE BOOL "enable testing" FORCE) + message(STATUS "Android build: unit tests disabled") + endif() endif() if(CMAKE_SYSTEM_NAME STREQUAL "iOS") set(BUILD_TESTING OFF CACHE BOOL "enable testing" FORCE) diff --git a/COMMIT_MSG.txt b/COMMIT_MSG.txt new file mode 100644 index 00000000..472b5ee6 --- /dev/null +++ b/COMMIT_MSG.txt @@ -0,0 +1,18 @@ +feat(android): run zeroerr unit_test suite as an Android test app + +Adds EVENGINE_ANDROID_TEST_APP build mode: the unit_test sources are +compiled into libmain.so (SDL entry) and packaged by the existing Gradle +APK along with the test assets, so the full zeroerr suite can run on a +device via adb logcat. + +- test/main.cpp: Android runner with LogcatBuf (cout/cerr -> logcat), + chdir to /evengine_test, SDL_Init; keeps SDL_main symbol unmangled. +- test/PathBesideSource.h: shared pathBesideThisSource helper that maps + __FILE__ to the on-device unpacked tree on Android. +- EVTestActivity: unpacks assets/test + assets/examples to internal + storage and passes an optional --testcase filter from an intent extra. +- test/CMakeLists.txt: builds `main` instead of unit_test on Android, + excludes desktop-only devtools tests, links Android system libs. +- Makefile: build/android-debug-test, sync/android-test-assets, + run/android-test-debug, log/android-test targets. +- Gradle wrapper bumped to 8.5 for Java 21 compatibility. diff --git a/Makefile b/Makefile index 1d19d659..30be8ff2 100644 --- a/Makefile +++ b/Makefile @@ -100,11 +100,13 @@ GAME ?= .PHONY: all build/win32 build/linux build/macosx build/android build/ios \ build/win32-debug build/linux-debug build/macosx-debug build/android-debug build/ios-debug \ + build/android-debug-test \ wsl/linux wsl/linux-debug show-targets \ debug release example \ run run/win32 run/linux run/macosx \ run/win32-debug run/linux-debug run/macosx-debug \ - sync/android-libs sync/android-assets install/android-debug run/android-debug log/android \ + sync/android-libs sync/android-assets sync/android-test-assets install/android-debug run/android-debug log/android \ + run/android-test-debug log/android-test \ install/ios-debug run/ios-debug log/ios \ sdk/win32 sdk/linux sdk/macosx sdk/android sdk/ios \ sdk/win32-debug sdk/linux-debug sdk/macosx-debug sdk/android-debug sdk/ios-debug \ @@ -340,9 +342,16 @@ reinstall/third-party/ios-debug: cmake --build build/third-party/ios-debug --target install -j 8 # Copy native shared libraries into the Gradle jniLibs tree. +# In Android test-app mode the runner lives in test/libmain.so (zeroerr suite); +# otherwise it is the engine's src/engine/libmain.so. sync/android-libs: mkdir -p $(JNI_LIBS) - cp -f $(BUILD_DIR)/src/engine/libmain.so $(JNI_LIBS)/ + @if [ -f $(BUILD_DIR)/test/libmain.so ]; then \ + echo "syncing test runner libmain.so"; \ + cp -f $(BUILD_DIR)/test/libmain.so $(JNI_LIBS)/; \ + else \ + cp -f $(BUILD_DIR)/src/engine/libmain.so $(JNI_LIBS)/; \ + fi @TP_DIR=build/third-party-binary/$$(basename $(BUILD_DIR)); \ if [ ! -d "$$TP_DIR/lib" ]; then TP_DIR=build/third-party-binary/android-debug; fi; \ if [ ! -d "$$TP_DIR/lib" ]; then TP_DIR=build/third-party-binary/android; fi; \ @@ -373,6 +382,16 @@ sync/android-assets: echo "Synced Android game assets ($$src) -> $(ANDROID_ASSETS_GAME)"; \ ls -la "$(ANDROID_ASSETS_GAME)" +# Populate APK assets/test + assets/examples for the Android test app +# (EVTestActivity unpacks them to /evengine_test at first launch). +sync/android-test-assets: + @rm -rf "$(APK_DIR)/app/src/main/assets/test" "$(APK_DIR)/app/src/main/assets/examples" "$(APK_DIR)/app/src/main/assets/game" + @mkdir -p "$(APK_DIR)/app/src/main/assets/test" "$(APK_DIR)/app/src/main/assets/examples" + cp -R test/. "$(APK_DIR)/app/src/main/assets/test/" + cp -R examples/. "$(APK_DIR)/app/src/main/assets/examples/" + @echo "Synced Android test assets -> $(APK_DIR)/app/src/main/assets/{test,examples}" + ls -la "$(APK_DIR)/app/src/main/assets/test" | head -20 + install/android-debug: $(ANDROID_SDK)/platform-tools/adb install -r \ $(APK_DIR)/app/build/outputs/apk/debug/app-debug.apk @@ -383,6 +402,46 @@ run/android-debug: install/android-debug log/android: $(ANDROID_SDK)/platform-tools/adb logcat -s EVEngineActivity:I SDL:V SDL/APP:V vulkan:V libc:F DEBUG:F +# ---- Android test app (zeroerr suite as libmain.so) ---- +# make build/android-debug-test # configure + deps + engine/tests + APK +# make run/android-test-debug [FILTER=math.*] # install + launch on device +# make log/android-test # stream test results from logcat +build/android-debug-test: build/android-debug-test/build.ninja + cmake --build $@ --target deps -j $(ANDROID_JOBS) + cmake --build $@ -j $(ANDROID_JOBS) + $(MAKE) sync/android-libs BUILD_DIR=build/android-debug-test + $(MAKE) sync/android-test-assets + cd $(APK_DIR) && JAVA_HOME="$(JAVA_HOME)" ANDROID_HOME="$(ANDROID_SDK)" ./gradlew assembleDebug + +build/android-debug-test/build.ninja: + cmake -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE=$(ANDROID_NDK)/build/cmake/android.toolchain.cmake \ + -DANDROID_ABI=$(ANDROID_ABI) \ + -DANDROID_PLATFORM=$(ANDROID_PLATFORM) \ + -DANDROID_STL=$(ANDROID_STL) \ + -DCMAKE_BUILD_TYPE=Debug \ + -DBUILD_PLATFORM=android \ + -DBUILD_TESTING=ON \ + -DEVENGINE_ANDROID_TEST_APP=ON \ + -DEVENGINE_DOWNLOAD_CLASSIC_SCENES=OFF \ + -DEVENGINE_DOWNLOAD_SPINE_MODELS=OFF \ + -DEVENGINE_DOWNLOAD_SKINNED_CHARACTER=OFF \ + $(CMAKE_EXTRA_ARGS) \ + -B build/android-debug-test -S . + +# Convenience: reuse the game APK's install/run with the test filter extra. +FILTER ?= +run/android-test-debug: install/android-debug + @if [ -n "$(FILTER)" ]; then \ + $(ANDROID_SDK)/platform-tools/adb shell am start -n com.evengine.example/.EVTestActivity --es evengine.test.filter "$(FILTER)"; \ + else \ + $(ANDROID_SDK)/platform-tools/adb shell am start -n com.evengine.example/.EVTestActivity; \ + fi + +log/android-test: + $(ANDROID_SDK)/platform-tools/adb logcat -c + $(ANDROID_SDK)/platform-tools/adb logcat -s EVEngineTest:V SDL:V vulkan:V libc:F DEBUG:F + # Install Debug .app to the first connected physical iOS/iPadOS device. install/ios-debug: @test -n "$$(security find-identity -v -p codesigning 2>/dev/null | sed -n '/Apple Development/p')" \ diff --git a/platform/android/apk/app/src/main/AndroidManifest.xml b/platform/android/apk/app/src/main/AndroidManifest.xml index bca3cb03..fcb71b9b 100644 --- a/platform/android/apk/app/src/main/AndroidManifest.xml +++ b/platform/android/apk/app/src/main/AndroidManifest.xml @@ -16,7 +16,7 @@ android:hardwareAccelerated="true"> /evengine_test}) and launches the zeroerr runner built + * into {@code libmain.so}. SDL_main chdirs to that tree, so the compile-time + * {@code EVENGINE_SOURCE_DIR="."} / {@code EVENGINE_TEST_BINARY_DIR="."} paths + * resolve on-device. A test filter can be passed as the + * {@code evengine.test.filter} intent extra, e.g. + * {@code adb shell am start -n com.evengine.example/.EVTestActivity --es evengine.test.filter "math.*"}. + */ +public class EVTestActivity extends SDLActivity { + private static final String TAG = "EVTestActivity"; + private String testDir; + + @Override + protected void onCreate(Bundle savedInstanceState) { + testDir = new File(getFilesDir(), "evengine_test").getAbsolutePath(); + try { + // Wipe first so assets removed from the package do not linger across upgrades. + deleteRecursive(new File(testDir)); + copyAssetDir("test", testDir); + copyAssetDir("examples", testDir + "/examples"); + Log.i(TAG, "Test assets ready at " + testDir); + } catch (IOException e) { + Log.e(TAG, "Failed to unpack test assets", e); + } + super.onCreate(savedInstanceState); + } + + @Override + protected String[] getLibraries() { + return new String[] { + "SDL2", + "c++_shared", + "main" + }; + } + + @Override + protected String[] getArguments() { + if (getIntent() != null) { + String filter = getIntent().getStringExtra("evengine.test.filter"); + if (filter != null && !filter.isEmpty()) { + return new String[] { "--testcase=" + filter }; + } + } + return new String[0]; + } + + private void copyAssetDir(String assetPath, String destPath) throws IOException { + AssetManager am = getAssets(); + String[] children = am.list(assetPath); + File dest = new File(destPath); + if (!dest.exists() && !dest.mkdirs()) { + throw new IOException("Cannot create " + destPath); + } + if (children == null || children.length == 0) { + // Leaf file + copyAssetFile(assetPath, destPath); + return; + } + for (String child : children) { + String childAsset = assetPath.isEmpty() ? child : assetPath + "/" + child; + String childDest = destPath + "/" + child; + String[] grand = am.list(childAsset); + if (grand != null && grand.length > 0) { + copyAssetDir(childAsset, childDest); + } else { + copyAssetFile(childAsset, childDest); + } + } + } + + private void copyAssetFile(String assetPath, String destPath) throws IOException { + File outFile = new File(destPath); + File parent = outFile.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) { + throw new IOException("Cannot create parent for " + destPath); + } + try (InputStream in = getAssets().open(assetPath); + OutputStream out = new FileOutputStream(outFile)) { + byte[] buf = new byte[16 * 1024]; + int n; + while ((n = in.read(buf)) > 0) { + out.write(buf, 0, n); + } + } + } + + private static void deleteRecursive(File f) { + if (f == null || !f.exists()) return; + File[] children = f.listFiles(); + if (children != null) { + for (File c : children) deleteRecursive(c); + } + //noinspection ResultOfMethodCallIgnored + f.delete(); + } +} diff --git a/platform/android/apk/gradle/wrapper/gradle-wrapper.properties b/platform/android/apk/gradle/wrapper/gradle-wrapper.properties index 62f495df..1af9e093 100644 --- a/platform/android/apk/gradle/wrapper/gradle-wrapper.properties +++ b/platform/android/apk/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/engine/CMakeLists.txt b/src/engine/CMakeLists.txt index e232d76d..e391cbf4 100644 --- a/src/engine/CMakeLists.txt +++ b/src/engine/CMakeLists.txt @@ -10,8 +10,12 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/common/config.h.in common/config.h) set(EVENGINE_IOS_DEPLOYMENT_TARGET "13.0" CACHE STRING "Minimum iOS/iPadOS deployment target") if(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) - # SDLActivity loads libmain.so and calls SDL_main (via SDL_main.h macro on main). - add_library(main SHARED main.cpp) + if(NOT EVENGINE_ANDROID_TEST_APP) + # SDLActivity loads libmain.so and calls SDL_main (via SDL_main.h macro on main). + # In Android test-app mode the shared `main` library is built by test/CMakeLists.txt + # from the zeroerr unit-test runner instead of the engine main.cpp. + add_library(main SHARED main.cpp) + endif() set(EVENGINE_NATIVE_TARGET main CACHE INTERNAL "Host target for SDK install") elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS" OR EVENGINE_IOS) # UIKit SDL entry: MACOSX_BUNDLE .app with SDL_main. @@ -199,7 +203,8 @@ if(EVENGINE_PLATFORM_LIB) list(APPEND EVELIBS ${EVENGINE_PLATFORM_LIB}) endif() -if(CMAKE_GENERATOR MATCHES "Xcode") +if(NOT EVENGINE_ANDROID_TEST_APP) + if(CMAKE_GENERATOR MATCHES "Xcode") # create_module() emits STATIC libraries here, so the linker would drop the # translation units whose only job is to self-register a module or a # subcommand at static-init time. Pull the archives in whole. @@ -208,8 +213,9 @@ if(CMAKE_GENERATOR MATCHES "Xcode") list(APPEND _eve_whole_archive_libs "$") endforeach() target_link_libraries(${EVENGINE_NATIVE_TARGET} ${_eve_whole_archive_libs} zeroerr ${ThirdParty} ${EVENGINE_VULKAN_LIB}) -else() + else() target_link_libraries(${EVENGINE_NATIVE_TARGET} ${EVELIBS} zeroerr ${ThirdParty} ${EVENGINE_VULKAN_LIB}) + endif() endif() if(CMAKE_SYSTEM_NAME STREQUAL "iOS" OR EVENGINE_IOS) @@ -300,6 +306,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "iOS" OR EVENGINE_IOS) endif() endif() +if(NOT EVENGINE_ANDROID_TEST_APP) # Plugins resolve ModuleManager / ModuleRegister symbols from the host process. # Export surface is EVENGINE_API (__declspec(dllexport)) only — do not enable # WINDOWS_EXPORT_ALL_SYMBOLS: it pulls Poco/ImGui template instantiations into @@ -326,3 +333,4 @@ elseif(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) VISIBILITY_INLINES_HIDDEN OFF ) endif() +endif() diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 1e494cb2..5bf14d46 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -103,6 +103,16 @@ if(EVENGINE_BUILD_DEMO) list(APPEND all_test_cpp demo.cpp) endif() +# Desktop-only DevTools (DAP / MCP / debugger / callgraph / renderflow) are not +# compiled into the Android runtime, so their test files cannot link there. +# all_test_cpp holds absolute paths from file(GLOB), so filter by basename. +if(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) + foreach(_eve_devtool_test IN ITEMS callgraph.cpp renderflow.cpp debugger.cpp dap.cpp mcp.cpp) + list(FILTER all_test_cpp EXCLUDE REGEX "/${_eve_devtool_test}$") + endforeach() + message(STATUS "Android test: removed devtools test files, remaining=${all_test_cpp}") +endif() + file(GLOB all_test_scripts filesystem.nut graphic.nut @@ -119,15 +129,36 @@ add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/nut_scripts.cpp WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) -add_executable( - unit_test - ${all_test_cpp} - ${CMAKE_CURRENT_BINARY_DIR}/nut_scripts.cpp -) +# On Android the zeroerr runner is packaged as the app's native entry point +# (libmain.so loaded by SDLActivity), replacing the engine main.cpp. +if(EVENGINE_ANDROID_TEST_APP) + add_library(main SHARED + ${all_test_cpp} + ${CMAKE_CURRENT_BINARY_DIR}/nut_scripts.cpp + ) +else() + add_executable( + unit_test + ${all_test_cpp} + ${CMAKE_CURRENT_BINARY_DIR}/nut_scripts.cpp + ) +endif() if(WIN32) set(SystemLibraries ${SystemLibraries} winmm version imm32 Setupapi ws2_32 iphlpapi shlwapi) +elseif(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) + find_library(ANDROID_LOG_LIB log) + find_library(ANDROID_LIB android) + find_library(ANDROID_OPENSLES_LIB OpenSLES) + set(SystemLibraries ${SystemLibraries} + ${ANDROID_LOG_LIB} + ${ANDROID_LIB} + ${ANDROID_OPENSLES_LIB} + dl + m + c++_shared + ) elseif(APPLE) set(SystemLibraries ${SystemLibraries} dl pthread iconv @@ -191,6 +222,11 @@ else() set(ThirdParty ${ThirdParty} simplesquirrel_staticmd sqstdlib_staticmd squirrel_staticmd SDL2 PocoDataSQLitemd PocoDatamd PocoFoundationmd PocoNetmd PocoJSONmd PocoXMLmd ${SystemLibraries}) + elseif(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) + # Same unprefixed static names as the engine Android build; SDL2 stays shared. + set(ThirdParty ${ThirdParty} simplesquirrel_static sqstdlib_static squirrel_static SDL2 SDL2main + PocoDataSQLite PocoData PocoNet PocoXML PocoJSON PocoFoundation + ${SystemLibraries}) else() set(ThirdParty ${ThirdParty} simplesquirrel_static sqstdlib_static squirrel_static SDL2 PocoDataSQLite PocoData PocoNet PocoXML PocoJSON PocoFoundation @@ -209,7 +245,6 @@ set(EVELIBS EVTouch EVGraphics EVImage - EVDevTools EVCommon EVEvent EVTimer @@ -245,6 +280,10 @@ set(EVELIBS EVStylize EVEditor) +if(NOT (ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android"))) + list(APPEND EVELIBS EVDevTools) +endif() + if(EVENGINE_BUILD_DEMO) list(APPEND EVELIBS EVDemo) @@ -254,14 +293,30 @@ if(EVENGINE_PLATFORM_LIB) list(APPEND EVELIBS ${EVENGINE_PLATFORM_LIB}) endif() -target_link_libraries(unit_test ${EVELIBS} zeroerr ${ThirdParty} ${EVENGINE_VULKAN_LIB}) -target_compile_definitions(unit_test PRIVATE - EVENGINE_TEST_BINARY_DIR="${CMAKE_CURRENT_BINARY_DIR}" - EVENGINE_SOURCE_DIR="${CMAKE_SOURCE_DIR}") +if(EVENGINE_ANDROID_TEST_APP) + target_link_libraries(main ${EVELIBS} zeroerr ${ThirdParty} ${EVENGINE_VULKAN_LIB}) + # Source/binary roots are resolved at runtime: the Android test runner chdirs to + # the unpacked asset root before running, so "." means "repo root on device". + target_compile_definitions(main PRIVATE + EVENGINE_TEST_BINARY_DIR="." + EVENGINE_SOURCE_DIR="." + EVENGINE_ANDROID=1) + # Keep SDL_main / module registration symbols visible for the dlopened SDLActivity. + set_target_properties(main PROPERTIES + CXX_VISIBILITY_PRESET default + VISIBILITY_INLINES_HIDDEN OFF) +else() + target_link_libraries(unit_test ${EVELIBS} zeroerr ${ThirdParty} ${EVENGINE_VULKAN_LIB}) + target_compile_definitions(unit_test PRIVATE + EVENGINE_TEST_BINARY_DIR="${CMAKE_CURRENT_BINARY_DIR}" + EVENGINE_SOURCE_DIR="${CMAKE_SOURCE_DIR}") +endif() # Large classic scenes (Sponza / DamagedHelmet). Cornell Box is committed. # Enable with -DEVENGINE_DOWNLOAD_CLASSIC_SCENES=ON, or build target # `download_classic_scenes` before running ClassicScenes.* tests. +# (Skipped on the Android test app: bash/python3 download steps do not run there.) +if(NOT EVENGINE_ANDROID_TEST_APP) option(EVENGINE_DOWNLOAD_CLASSIC_SCENES "Download large classic render test scenes (Sponza, DamagedHelmet)" OFF) set(_eve_classic_scenes_script "${CMAKE_SOURCE_DIR}/scripts/download_classic_scenes.sh") @@ -326,6 +381,9 @@ if(EVENGINE_DOWNLOAD_SKINNED_CHARACTER) COMMENT "PRE_BUILD: download skinned character (CesiumMan)" VERBATIM) endif() +endif() # NOT EVENGINE_ANDROID_TEST_APP +if(NOT EVENGINE_ANDROID_TEST_APP) include(${CMAKE_SOURCE_DIR}/cmake/ZeroErrDiscoverTests.cmake) zeroerr_discover_tests(unit_test) +endif() diff --git a/test/ClassicScenes.cpp b/test/ClassicScenes.cpp index e3b1cf9f..82e49556 100644 --- a/test/ClassicScenes.cpp +++ b/test/ClassicScenes.cpp @@ -46,12 +46,8 @@ using namespace eve::graphics; namespace { -std::string pathBesideThisSource(const char *filename) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + filename; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() std::vector readBinaryFile(const std::string &path) { std::ifstream in(path, std::ios::binary); diff --git a/test/IBL.cpp b/test/IBL.cpp index 970106a2..0e6b5c81 100644 --- a/test/IBL.cpp +++ b/test/IBL.cpp @@ -24,12 +24,8 @@ using namespace eve::graphics; namespace { -std::string pathBesideThisSource(const char *filename) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + filename; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() std::vector readBinaryFile(const std::string &path) { std::ifstream in(path, std::ios::binary); diff --git a/test/PathBesideSource.h b/test/PathBesideSource.h new file mode 100644 index 00000000..32be9c1e --- /dev/null +++ b/test/PathBesideSource.h @@ -0,0 +1,65 @@ +#pragma once +// test/PathBesideSource.h +// +// Shared helper for resolving test asset paths relative to the test source file. +// On desktop the path is dirname(__FILE__); on Android the build-host path baked +// into __FILE__ is replaced with the on-device unpacked tree root so that +// `pathBesideThisSource("assets/...")` resolves to /evengine_test/assets/... +// +// Deliberately uses its own `eve_test_path` namespace (NOT `eve`): this header is +// included *inside* the anonymous namespaces of several test files, so introducing +// `namespace eve` there would shadow the global ::eve and break every +// `eve::window::...` / `eve::image::...` reference in those files. + +#include + +#if defined(EVENGINE_ANDROID) +#include +#endif + +namespace eve_test_path { + +#if defined(EVENGINE_ANDROID) +inline std::string androidTestRoot() { + static const std::string root = [] { + const char *internal = SDL_AndroidGetInternalStoragePath(); + return internal ? std::string(internal) + "/evengine_test" : std::string(); + }(); + return root; +} + +// Map a build-host source path (__FILE__) onto the on-device test layout. +// All test sources are flat in test/, and the APK unpacks assets/test/* directly +// into the device root (/evengine_test/*), so the basename is the only +// meaningful part of __FILE__ here. +inline std::string androidSourcePath(const char *file) { + std::string s = file ? file : ""; + const std::string root = androidTestRoot(); + if (root.empty()) + return s; + size_t slash = s.find_last_of("/\\"); + std::string rel = (slash == std::string::npos) ? s : s.substr(slash + 1); + return root + "/" + rel; +} +#endif // EVENGINE_ANDROID + +inline std::string pathBesideSourceImpl(const char *file, const char *relative) { +#if defined(EVENGINE_ANDROID) + std::string source = androidSourcePath(file); + size_t slash = source.find_last_of('/'); + std::string dir = (slash == std::string::npos) ? std::string(".") : source.substr(0, slash); + return dir + "/" + relative; +#else + std::string here = file ? file : ""; + auto slash = here.find_last_of("/\\"); + std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); + return dir + "/" + relative; +#endif +} + +} // namespace eve_test_path + +#define EVE_DEFINE_PATH_BESIDE_SOURCE() \ + static std::string pathBesideThisSource(const char *filename) { \ + return eve_test_path::pathBesideSourceImpl(__FILE__, filename); \ + } diff --git a/test/RenderImageAudit.cpp b/test/RenderImageAudit.cpp index c86fdbba..903c8d77 100644 --- a/test/RenderImageAudit.cpp +++ b/test/RenderImageAudit.cpp @@ -866,12 +866,8 @@ struct CloseWin { } }; -std::string pathBesideThisSource(const char *relative) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + relative; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() Mesh *makeFloor(Graphics *gfx, float half = 4.5f) { const float pos[] = { diff --git a/test/animation_skinned.cpp b/test/animation_skinned.cpp index f6b0c99f..8321b4ec 100644 --- a/test/animation_skinned.cpp +++ b/test/animation_skinned.cpp @@ -42,12 +42,8 @@ using namespace eve::graphics; namespace { -std::string pathBesideThisSource(const char *relative) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + relative; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() bool fileExists(const std::string &path) { return std::filesystem::is_regular_file(path); diff --git a/test/audio.cpp b/test/audio.cpp index f0327d97..f868aa1b 100644 --- a/test/audio.cpp +++ b/test/audio.cpp @@ -20,12 +20,8 @@ namespace { -std::string pathBesideThisSource(const char *filename) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + filename; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() std::vector readBinaryFile(const std::string &path) { std::ifstream in(path, std::ios::binary); diff --git a/test/font.cpp b/test/font.cpp index 7a148e37..43360f58 100644 --- a/test/font.cpp +++ b/test/font.cpp @@ -18,12 +18,8 @@ namespace { // FontAwesome private-use icon with a non-empty bitmap in the fixture. constexpr int kIconCodepoint = 0xF000; -std::string pathBesideThisSource(const char *filename) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + filename; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() std::vector readBinaryFile(const std::string &path) { std::ifstream in(path, std::ios::binary); diff --git a/test/graphics_font.cpp b/test/graphics_font.cpp index be53ae36..5b03d087 100644 --- a/test/graphics_font.cpp +++ b/test/graphics_font.cpp @@ -27,12 +27,8 @@ namespace { constexpr int kIconCodepoint = 0xF000; constexpr const char *kIconUtf8 = "\xEF\x80\x80"; // UTF-8 for U+F000 -std::string pathBesideThisSource(const char *filename) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + filename; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() std::vector readBinaryFile(const std::string &path) { std::ifstream in(path, std::ios::binary); diff --git a/test/main.cpp b/test/main.cpp index 5542f493..f93cf89b 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -1,5 +1,89 @@ #include "zeroerr/unittest.h" -int main(int argc, const char** argv) { - return zeroerr::UnitTest().parseArgs(argc, argv).run(); +#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 +#endif // EVENGINE_ANDROID + +// Signature must match the `extern "C" int SDL_main(int, char*[])` declared by +// SDL_main.h so the symbol stays unmangled ("SDL_main" for the SDLActivity JNI +// dlsym). A `const char**` parameter would become a C++ overload and be mangled. +int main(int argc, char **argv) { +#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"); +#endif // EVENGINE_ANDROID + + return zeroerr::UnitTest().parseArgs(argc, const_cast(argv)).run(); } diff --git a/test/model3d.cpp b/test/model3d.cpp index 9cfb03d3..7628fe02 100644 --- a/test/model3d.cpp +++ b/test/model3d.cpp @@ -43,12 +43,8 @@ using namespace eve::graphics; namespace { -std::string pathBesideThisSource(const char *filename) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + filename; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() std::vector readBinaryFile(const std::string &path) { std::ifstream in(path, std::ios::binary); diff --git a/test/particles_attach_extra.cpp b/test/particles_attach_extra.cpp index a5f91678..279aa21e 100644 --- a/test/particles_attach_extra.cpp +++ b/test/particles_attach_extra.cpp @@ -40,12 +40,8 @@ using namespace eve::ik; namespace { -std::string pathBesideThisSource(const char *relative) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + relative; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() bool fileExists(const std::string &path) { return std::filesystem::is_regular_file(path); } diff --git a/test/particles_attach_more.cpp b/test/particles_attach_more.cpp index 508b6c43..aa1e3a59 100644 --- a/test/particles_attach_more.cpp +++ b/test/particles_attach_more.cpp @@ -38,12 +38,8 @@ using namespace eve::ik; namespace { -std::string pathBesideThisSource(const char *relative) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + relative; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() bool fileExists(const std::string &path) { return std::filesystem::is_regular_file(path); } diff --git a/test/particles_attach_skin.cpp b/test/particles_attach_skin.cpp index 3477959f..f97c18d9 100644 --- a/test/particles_attach_skin.cpp +++ b/test/particles_attach_skin.cpp @@ -26,12 +26,8 @@ using namespace eve::animation; namespace { -std::string pathBesideThisSource(const char *relative) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + relative; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() bool fileExists(const std::string &path) { return std::filesystem::is_regular_file(path); } diff --git a/test/sound.cpp b/test/sound.cpp index 0580f61e..8f40d32b 100644 --- a/test/sound.cpp +++ b/test/sound.cpp @@ -18,12 +18,8 @@ namespace { -std::string pathBesideThisSource(const char *filename) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + filename; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() std::vector readBinaryFile(const std::string &path) { std::ifstream in(path, std::ios::binary);