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 <files>/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.
This commit is contained in:
西风逍遥游 committed 2026-08-14 23:05:42 +08:00
1 parent d2eafa3899
commit a6901a0691
23 files changed
+463 -94

No files matched your search

+3
View File
@@ -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/
+10 -2
View File
@@ -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)
+18
View File
@@ -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 <files>/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.
+61 -2
View File
@@ -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 <files>/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')" \
@@ -16,7 +16,7 @@
android:hardwareAccelerated="true">
<activity
android:name=".EVEngineActivity"
android:name=".EVTestActivity"
android:exported="true"
android:label="@string/app_name"
android:alwaysRetainTaskState="true"
@@ -0,0 +1,114 @@
package com.evengine.example;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.util.Log;
import org.libsdl.app.SDLActivity;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Android host for the unit_test suite.
*
* Unpacks the APK's {@code assets/test} + {@code assets/examples} into internal
* storage ({@code <files>/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();
}
}
@@ -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
+12 -4
View File
@@ -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 "$<LINK_LIBRARY:WHOLE_ARCHIVE,${_eve_lib}>")
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()
+68 -10
View File
@@ -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()
+2 -6
View File
@@ -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<char> readBinaryFile(const std::string &path) {
std::ifstream in(path, std::ios::binary);
+2 -6
View File
@@ -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<char> readBinaryFile(const std::string &path) {
std::ifstream in(path, std::ios::binary);
+65
View File
@@ -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 <internal>/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 <string>
#if defined(EVENGINE_ANDROID)
#include <SDL2/SDL.h>
#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 (<internal>/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); \
}
+2 -6
View File
@@ -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[] = {
+2 -6
View File
@@ -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);
+2 -6
View File
@@ -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<char> readBinaryFile(const std::string &path) {
std::ifstream in(path, std::ios::binary);
+2 -6
View File
@@ -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<char> readBinaryFile(const std::string &path) {
std::ifstream in(path, std::ios::binary);
+2 -6
View File
@@ -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<char> readBinaryFile(const std::string &path) {
std::ifstream in(path, std::ios::binary);
+86 -2
View File
@@ -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 <SDL2/SDL.h>
#include <SDL2/SDL_main.h>
#include <android/log.h>
#include <cerrno>
#include <cstring>
#include <iostream>
#include <string>
#include <streambuf>
#include <unistd.h>
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<char>(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: <internal storage>/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<const char **>(argv)).run();
}
+2 -6
View File
@@ -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<char> readBinaryFile(const std::string &path) {
std::ifstream in(path, std::ios::binary);
+2 -6
View File
@@ -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); }
+2 -6
View File
@@ -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); }
+2 -6
View File
@@ -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); }
+2 -6
View File
@@ -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<char> readBinaryFile(const std::string &path) {
std::ifstream in(path, std::ios::binary);