diff --git a/.cspell-wordlist.txt b/.cspell-wordlist.txt index fcef68ce2..e952f80e6 100644 --- a/.cspell-wordlist.txt +++ b/.cspell-wordlist.txt @@ -94,3 +94,6 @@ Português codegen cstdint ocurred +libfbjni +libc +gradlew diff --git a/.gitignore b/.gitignore index c0c49ef68..780eaf46f 100644 --- a/.gitignore +++ b/.gitignore @@ -93,3 +93,4 @@ apps/*/android/ # custom *.tgz Makefile +*.pte diff --git a/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.cpp b/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.cpp index 53d035afd..bd27fe641 100644 --- a/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.cpp +++ b/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.cpp @@ -28,17 +28,48 @@ LLM::LLM(const std::string &modelSource, const std::string &tokenizerSource, } // TODO: add a way to manipulate the generation config with params +#ifdef TEST_BUILD +std::string LLM::generate(std::string input, + std::shared_ptr callback) { + if (!runner || !runner->is_loaded()) { + throw RnExecutorchError(RnExecutorchErrorCode::ModuleNotLoaded, + "Runner is not loaded"); + } + + std::string output; + + // Create a native callback that accumulates tokens and optionally invokes JS + auto nativeCallback = [this, callback, &output](const std::string &token) { + output += token; + if (callback && callInvoker) { + callInvoker->invokeAsync([callback, token](jsi::Runtime &runtime) { + callback->call(runtime, jsi::String::createFromUtf8(runtime, token)); + }); + } + }; + + auto config = llm::GenerationConfig{.echo = false, .warming = false}; + auto error = runner->generate(input, config, nativeCallback, {}); + if (error != executorch::runtime::Error::Ok) { + throw RnExecutorchError(error, "Failed to generate text"); + } + + return output; +} +#else void LLM::generate(std::string input, std::shared_ptr callback) { if (!runner || !runner->is_loaded()) { throw RnExecutorchError(RnExecutorchErrorCode::ModuleNotLoaded, "Runner is not loaded"); } - // Create a native callback that will invoke the JS callback on the JS thread + // Create a native callback that only invokes JS (no accumulation) auto nativeCallback = [this, callback](const std::string &token) { - callInvoker->invokeAsync([callback, token](jsi::Runtime &runtime) { - callback->call(runtime, jsi::String::createFromUtf8(runtime, token)); - }); + if (callback && callInvoker) { + callInvoker->invokeAsync([callback, token](jsi::Runtime &runtime) { + callback->call(runtime, jsi::String::createFromUtf8(runtime, token)); + }); + } }; auto config = llm::GenerationConfig{.echo = false, .warming = false}; @@ -47,6 +78,7 @@ void LLM::generate(std::string input, std::shared_ptr callback) { throw RnExecutorchError(error, "Failed to generate text"); } } +#endif void LLM::interrupt() { if (!runner || !runner->is_loaded()) { diff --git a/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.h b/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.h index 5ba83df8c..d1b62e463 100644 --- a/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.h +++ b/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.h @@ -18,7 +18,12 @@ class LLM : public BaseModel { const std::string &tokenizerSource, std::shared_ptr callInvoker); +#ifdef TEST_BUILD + std::string generate(std::string input, + std::shared_ptr callback); +#else void generate(std::string input, std::shared_ptr callback); +#endif void interrupt(); void unload() noexcept; size_t getGeneratedTokenCount() const noexcept; diff --git a/packages/react-native-executorch/common/rnexecutorch/models/ocr/utils/RecognizerUtils.cpp b/packages/react-native-executorch/common/rnexecutorch/models/ocr/utils/RecognizerUtils.cpp index 25c2cea61..89a84fa67 100644 --- a/packages/react-native-executorch/common/rnexecutorch/models/ocr/utils/RecognizerUtils.cpp +++ b/packages/react-native-executorch/common/rnexecutorch/models/ocr/utils/RecognizerUtils.cpp @@ -7,7 +7,19 @@ cv::Mat softmax(const cv::Mat &inputs) { cv::Mat maxVal; cv::reduce(inputs, maxVal, 1, cv::REDUCE_MAX, CV_32F); cv::Mat expInputs; - cv::exp(inputs - cv::repeat(maxVal, 1, inputs.cols), expInputs); + cv::Mat repeated = inputs - cv::repeat(maxVal, 1, inputs.cols); + repeated.convertTo(repeated, CV_32F); +#ifdef TEST_BUILD + // Manually compute exp to avoid SIMD issues in test environment + expInputs = cv::Mat(repeated.size(), CV_32F); + for (int i = 0; i < repeated.rows; i++) { + for (int j = 0; j < repeated.cols; j++) { + expInputs.at(i, j) = std::exp(repeated.at(i, j)); + } + } +#else + cv::exp(repeated, expInputs); +#endif cv::Mat sumExp; cv::reduce(expInputs, sumExp, 1, cv::REDUCE_SUM, CV_32F); cv::Mat softmaxOutput = expInputs / cv::repeat(sumExp, 1, inputs.cols); diff --git a/packages/react-native-executorch/common/rnexecutorch/models/style_transfer/StyleTransfer.cpp b/packages/react-native-executorch/common/rnexecutorch/models/style_transfer/StyleTransfer.cpp index 9030807a3..3b9c0187b 100644 --- a/packages/react-native-executorch/common/rnexecutorch/models/style_transfer/StyleTransfer.cpp +++ b/packages/react-native-executorch/common/rnexecutorch/models/style_transfer/StyleTransfer.cpp @@ -10,7 +10,6 @@ namespace rnexecutorch::models::style_transfer { using namespace facebook; using executorch::extension::TensorPtr; -using executorch::runtime::Error; StyleTransfer::StyleTransfer(const std::string &modelSource, std::shared_ptr callInvoker) diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/CMakeLists.txt b/packages/react-native-executorch/common/rnexecutorch/tests/CMakeLists.txt index 82d41e3a8..215a33c6f 100644 --- a/packages/react-native-executorch/common/rnexecutorch/tests/CMakeLists.txt +++ b/packages/react-native-executorch/common/rnexecutorch/tests/CMakeLists.txt @@ -1,34 +1,257 @@ -cmake_minimum_required(VERSION 3.10) +if(NOT ANDROID_ABI) + message(FATAL_ERROR "Tests can be only built for Android simulator") +endif() + +cmake_minimum_required(VERSION 3.13) project(RNExecutorchTests) -# C++ standard set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED TRUE) -# googletest subdirectory -# Using an absolute path from the top-level source directory -add_subdirectory(${CMAKE_SOURCE_DIR}/../../../../../third-party/googletest ${PROJECT_BINARY_DIR}/googletest) +# tests/ <- CMAKE_SOURCE_DIR (this file's location) +# rnexecutorch/ <- RNEXECUTORCH_DIR (parent of tests) +# common/ <- COMMON_DIR +# react-native-executorch/ <- PACKAGE_ROOT +# / <- MONOREPO_ROOT +# /third-party/ <- THIRD_PARTY_DIR +set(RNEXECUTORCH_DIR "${CMAKE_SOURCE_DIR}/..") +set(COMMON_DIR "${RNEXECUTORCH_DIR}/..") +set(PACKAGE_ROOT "${COMMON_DIR}/..") +set(MONOREPO_ROOT "${PACKAGE_ROOT}/../..") +set(THIRD_PARTY_DIR "${MONOREPO_ROOT}/third-party") +set(REACT_NATIVE_DIR "${MONOREPO_ROOT}/node_modules/react-native") +set(ANDROID_THIRD_PARTY "${PACKAGE_ROOT}/third-party/android/libs/") + +# Add Gtest as a subdirectory +add_subdirectory(${THIRD_PARTY_DIR}/googletest ${PROJECT_BINARY_DIR}/googletest) + +# ExecuTorch Prebuilt binaries +add_library(executorch_prebuilt SHARED IMPORTED) +set_target_properties(executorch_prebuilt PROPERTIES + IMPORTED_LOCATION "${ANDROID_THIRD_PARTY}/executorch/${ANDROID_ABI}/libexecutorch.so" +) + +# pthreadpool and cpuinfo (needed for OpenMP/OpenCV) +if(ANDROID_ABI STREQUAL "arm64-v8a") + add_library(pthreadpool SHARED IMPORTED) + set_target_properties(pthreadpool PROPERTIES + IMPORTED_LOCATION "${ANDROID_THIRD_PARTY}/pthreadpool/${ANDROID_ABI}/libpthreadpool.so" + ) + + add_library(cpuinfo SHARED IMPORTED) + set_target_properties(cpuinfo PROPERTIES + IMPORTED_LOCATION "${ANDROID_THIRD_PARTY}/cpuinfo/${ANDROID_ABI}/libcpuinfo.so" + ) + + set(EXECUTORCH_LIBS pthreadpool cpuinfo) +else() + set(EXECUTORCH_LIBS "") +endif() + +# OpenCV (Interface Library) +set(OPENCV_LIBS_DIR "${ANDROID_THIRD_PARTY}/opencv/${ANDROID_ABI}") +set(OPENCV_THIRD_PARTY_DIR "${ANDROID_THIRD_PARTY}/opencv-third-party/${ANDROID_ABI}") -# Directories to include -include_directories(${CMAKE_SOURCE_DIR}/../data_processing) -include_directories(${CMAKE_SOURCE_DIR}/..) +if(ANDROID_ABI STREQUAL "arm64-v8a") + set(OPENCV_THIRD_PARTY_LIBS + "${OPENCV_THIRD_PARTY_DIR}/libkleidicv_hal.a" + "${OPENCV_THIRD_PARTY_DIR}/libkleidicv_thread.a" + "${OPENCV_THIRD_PARTY_DIR}/libkleidicv.a" + ) +elseif(ANDROID_ABI STREQUAL "x86_64") + set(OPENCV_THIRD_PARTY_LIBS "") +endif() -# Source files -set(SOURCE_FILES ${CMAKE_SOURCE_DIR}/../data_processing/Numerical.cpp - ${CMAKE_SOURCE_DIR}/../data_processing/FileUtils.h) -# Executables for the tests -add_executable(NumericalTests NumericalTest.cpp ${SOURCE_FILES}) -add_executable(FileUtilsTests FileUtilsTest.cpp ${SOURCE_FILES}) -add_executable(LogTests LogTest.cpp) +add_library(opencv_deps INTERFACE) +target_link_libraries(opencv_deps INTERFACE + ${OPENCV_LIBS_DIR}/libopencv_core.a + ${OPENCV_LIBS_DIR}/libopencv_features2d.a + ${OPENCV_LIBS_DIR}/libopencv_highgui.a + ${OPENCV_LIBS_DIR}/libopencv_imgproc.a + ${OPENCV_LIBS_DIR}/libopencv_photo.a + ${OPENCV_LIBS_DIR}/libopencv_video.a + ${OPENCV_THIRD_PARTY_LIBS} + ${EXECUTORCH_LIBS} + z + dl + m + log +) +target_link_options(opencv_deps INTERFACE -fopenmp -static-openmp) -# Libraries linking -target_link_libraries(NumericalTests gtest gtest_main) -target_link_libraries(FileUtilsTests gtest gtest_main) -target_link_libraries(LogTests gtest gtest_main) +# Tokenizers (Interface Library) +set(TOKENIZERS_LIBS_DIR "${ANDROID_THIRD_PARTY}/tokenizers-cpp/${ANDROID_ABI}") +add_library(tokenizers_deps INTERFACE) +target_link_libraries(tokenizers_deps INTERFACE + ${TOKENIZERS_LIBS_DIR}/libtokenizers_cpp.a + ${TOKENIZERS_LIBS_DIR}/libtokenizers_c.a + ${TOKENIZERS_LIBS_DIR}/libsentencepiece.a +) + +# Source Definitions +set(CORE_SOURCES + ${RNEXECUTORCH_DIR}/models/BaseModel.cpp + ${RNEXECUTORCH_DIR}/data_processing/Numerical.cpp + ${CMAKE_SOURCE_DIR}/integration/stubs/jsi_stubs.cpp +) + +set(IMAGE_UTILS_SOURCES + ${RNEXECUTORCH_DIR}/data_processing/ImageProcessing.cpp + ${RNEXECUTORCH_DIR}/data_processing/base64.cpp + ${COMMON_DIR}/ada/ada.cpp +) + +set(TOKENIZER_SOURCES ${RNEXECUTORCH_DIR}/TokenizerModule.cpp) +set(DSP_SOURCES ${RNEXECUTORCH_DIR}/data_processing/dsp.cpp) + +# Core Library +add_library(rntests_core STATIC ${CORE_SOURCES}) + +target_include_directories(rntests_core PUBLIC + ${RNEXECUTORCH_DIR}/data_processing + ${RNEXECUTORCH_DIR} + ${COMMON_DIR} + ${PACKAGE_ROOT}/third-party/include + ${REACT_NATIVE_DIR}/ReactCommon + ${REACT_NATIVE_DIR}/ReactCommon/jsi + ${REACT_NATIVE_DIR}/ReactCommon/callinvoker + ${COMMON_DIR}/ada +) + +target_link_libraries(rntests_core PUBLIC + executorch_prebuilt + gtest + log +) -# Testing functionalities enable_testing() -add_test(NAME NumericalTests COMMAND NumericalTests) -add_test(NAME FileUtilsTests COMMAND FileUtilsTests) -add_test(NAME LogTests COMMAND LogTests) +function(add_rn_test TEST_TARGET TEST_FILENAME) + cmake_parse_arguments(ARG "" "" "SOURCES;LIBS" ${ARGN}) + # Create executable using the explicit filename provided + add_executable(${TEST_TARGET} ${TEST_FILENAME} ${ARG_SOURCES}) + + target_compile_definitions(${TEST_TARGET} PRIVATE TEST_BUILD) + target_link_libraries(${TEST_TARGET} PRIVATE rntests_core gtest_main ${ARG_LIBS}) + target_link_options(${TEST_TARGET} PRIVATE "LINKER:-z,max-page-size=16384") + + add_test(NAME ${TEST_TARGET} COMMAND ${TEST_TARGET}) +endfunction() + +add_rn_test(NumericalTests unit/NumericalTest.cpp) +add_rn_test(LogTests unit/LogTest.cpp) +add_rn_test(BaseModelTests integration/BaseModelTest.cpp) + +add_rn_test(ClassificationTests integration/ClassificationTest.cpp + SOURCES + ${RNEXECUTORCH_DIR}/models/classification/Classification.cpp + ${IMAGE_UTILS_SOURCES} + LIBS opencv_deps +) + +add_rn_test(ObjectDetectionTests integration/ObjectDetectionTest.cpp + SOURCES + ${RNEXECUTORCH_DIR}/models/object_detection/ObjectDetection.cpp + ${RNEXECUTORCH_DIR}/models/object_detection/Utils.cpp + ${IMAGE_UTILS_SOURCES} + LIBS opencv_deps +) + +add_rn_test(ImageEmbeddingsTests integration/ImageEmbeddingsTest.cpp + SOURCES + ${RNEXECUTORCH_DIR}/models/embeddings/image/ImageEmbeddings.cpp + ${RNEXECUTORCH_DIR}/models/embeddings/BaseEmbeddings.cpp + ${IMAGE_UTILS_SOURCES} + LIBS opencv_deps +) + +add_rn_test(TextEmbeddingsTests integration/TextEmbeddingsTest.cpp + SOURCES + ${RNEXECUTORCH_DIR}/models/embeddings/text/TextEmbeddings.cpp + ${RNEXECUTORCH_DIR}/models/embeddings/BaseEmbeddings.cpp + ${TOKENIZER_SOURCES} + LIBS tokenizers_deps +) + +add_rn_test(StyleTransferTests integration/StyleTransferTest.cpp + SOURCES + ${RNEXECUTORCH_DIR}/models/style_transfer/StyleTransfer.cpp + ${IMAGE_UTILS_SOURCES} + LIBS opencv_deps +) + +add_rn_test(VADTests integration/VoiceActivityDetectionTest.cpp + SOURCES + ${RNEXECUTORCH_DIR}/models/voice_activity_detection/VoiceActivityDetection.cpp + ${RNEXECUTORCH_DIR}/models/voice_activity_detection/Utils.cpp + ${DSP_SOURCES} +) + +add_rn_test(TokenizerModuleTests integration/TokenizerModuleTest.cpp + SOURCES ${TOKENIZER_SOURCES} + LIBS tokenizers_deps +) + +add_rn_test(SpeechToTextTests integration/SpeechToTextTest.cpp + SOURCES + ${RNEXECUTORCH_DIR}/models/speech_to_text/SpeechToText.cpp + ${RNEXECUTORCH_DIR}/models/speech_to_text/asr/ASR.cpp + ${RNEXECUTORCH_DIR}/models/speech_to_text/stream/HypothesisBuffer.cpp + ${RNEXECUTORCH_DIR}/models/speech_to_text/stream/OnlineASRProcessor.cpp + ${RNEXECUTORCH_DIR}/data_processing/gzip.cpp + ${TOKENIZER_SOURCES} + ${DSP_SOURCES} + LIBS tokenizers_deps z +) + +add_rn_test(LLMTests integration/LLMTest.cpp + SOURCES + ${RNEXECUTORCH_DIR}/models/llm/LLM.cpp + ${COMMON_DIR}/runner/runner.cpp + ${COMMON_DIR}/runner/text_prefiller.cpp + ${COMMON_DIR}/runner/text_decoder_runner.cpp + ${COMMON_DIR}/runner/sampler.cpp + ${COMMON_DIR}/runner/arange_util.cpp + LIBS tokenizers_deps +) + +add_rn_test(TextToImageTests integration/TextToImageTest.cpp + SOURCES + ${RNEXECUTORCH_DIR}/models/text_to_image/TextToImage.cpp + ${RNEXECUTORCH_DIR}/models/text_to_image/Encoder.cpp + ${RNEXECUTORCH_DIR}/models/text_to_image/UNet.cpp + ${RNEXECUTORCH_DIR}/models/text_to_image/Decoder.cpp + ${RNEXECUTORCH_DIR}/models/text_to_image/Scheduler.cpp + ${RNEXECUTORCH_DIR}/models/embeddings/text/TextEmbeddings.cpp + ${RNEXECUTORCH_DIR}/models/embeddings/BaseEmbeddings.cpp + ${TOKENIZER_SOURCES} + LIBS tokenizers_deps +) + +add_rn_test(OCRTests integration/OCRTest.cpp + SOURCES + ${RNEXECUTORCH_DIR}/models/ocr/OCR.cpp + ${RNEXECUTORCH_DIR}/models/ocr/CTCLabelConverter.cpp + ${RNEXECUTORCH_DIR}/models/ocr/Detector.cpp + ${RNEXECUTORCH_DIR}/models/ocr/RecognitionHandler.cpp + ${RNEXECUTORCH_DIR}/models/ocr/Recognizer.cpp + ${RNEXECUTORCH_DIR}/models/ocr/utils/DetectorUtils.cpp + ${RNEXECUTORCH_DIR}/models/ocr/utils/RecognitionHandlerUtils.cpp + ${RNEXECUTORCH_DIR}/models/ocr/utils/RecognizerUtils.cpp + ${IMAGE_UTILS_SOURCES} + LIBS opencv_deps +) + +add_rn_test(VerticalOCRTests integration/VerticalOCRTest.cpp + SOURCES + ${RNEXECUTORCH_DIR}/models/vertical_ocr/VerticalOCR.cpp + ${RNEXECUTORCH_DIR}/models/vertical_ocr/VerticalDetector.cpp + ${RNEXECUTORCH_DIR}/models/ocr/Detector.cpp + ${RNEXECUTORCH_DIR}/models/ocr/CTCLabelConverter.cpp + ${RNEXECUTORCH_DIR}/models/ocr/Recognizer.cpp + ${RNEXECUTORCH_DIR}/models/ocr/utils/DetectorUtils.cpp + ${RNEXECUTORCH_DIR}/models/ocr/utils/RecognitionHandlerUtils.cpp + ${RNEXECUTORCH_DIR}/models/ocr/utils/RecognizerUtils.cpp + ${IMAGE_UTILS_SOURCES} + LIBS opencv_deps +) diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/README.md b/packages/react-native-executorch/common/rnexecutorch/tests/README.md index 9ce6f5b36..1a35743df 100644 --- a/packages/react-native-executorch/common/rnexecutorch/tests/README.md +++ b/packages/react-native-executorch/common/rnexecutorch/tests/README.md @@ -8,30 +8,66 @@ To test the native code we use [`googletest`](https://github.com/google/googlete The googletest is already in repo in `react-native-executorch/third-party/googletest`. Firstly, you need to fetch googletest locally, run from root directory of project: * `git submodule update --init --recursive third-party/googletest` -### Build Test Files -To run tests navigate tests directory namely: -* `cd packages/react-native-executorch/common/rnexecutorch/tests` -and then type: -* `mkdir build && cd build` -* `cmake ..` -* `make` +### Running tests -### Run Tests -To run tests use the following command in `packages/react-native-executorch/common/rnexecutorch/tests/build`: -* `ctest --verbose` +#### Prerequisites -Every time you updated the source code, you need to recompile the test files using: `cmake .. && make`. +- **Android NDK**: The `ANDROID_NDK` environment variable must be set +- **wget**: Must be in your PATH +- **Android emulator**: Must be running before executing tests +- **Device requirements**: + - 16GB disk storage (minimum) + - 8GB RAM (minimum) + +#### First-time setup + +Before running tests, you need to build an app to generate required native libraries (`libfbjni.so` and `libc++_shared.so`). The test script automatically searches for these in the monorepo. + +If the script reports missing libraries, build any example app: +```bash +cd apps/computer-vision/android +./gradlew assembleDebug +# or +./gradlew assembleRelease +``` + +#### Running the tests + +Navigate to the tests directory: +```bash +cd packages/react-native-executorch/common/rnexecutorch/tests +``` + +Run the test script: +```bash +bash ./run_tests.sh +``` + +This script: +- Downloads all needed models +- Pushes executables, models, assets, and shared libraries via ADB to the running emulator +- Runs the pre-compiled test executables + +#### Available flags + +* `--refresh-models` - Forcefully downloads all the models. By default, models are not downloaded unless they are missing from the specified directory. +* `--skip-build` - Skips the cmake build step. ### How to add a new test To add new test you need to: -* Place `*.cpp` file with tests using googletest in this directory. -* In `CMakeLists.txt`, add all executables and link them with googletest, e.g.: - ``` - set(SOURCE_FILES ${CMAKE_SOURCE_DIR}/../data_processing/Numerical.cpp) - add_executable(NumericalTests tests/NumericalTest.cpp ${SOURCE_FILES}) - target_link_libraries(NumericalTests gtest gtest_main) - ``` -* Add test execution, e.g.: - ``` - add_test(NAME NumericalTests COMMAND NumericalTests) +* Add a new .cpp file to either integration/ or unit/, depending on the type of the test. +* In `CMakeLists.txt`, add all executables and link all the needed libraries against the executable, for example you can use the `add_rn_test`, which is a helper function that links core libs. Example: + ```cmake + # unit + add_rn_test(BaseModelTests integration/BaseModelTest.cpp) + + # integration + add_rn_test(ClassificationTests integration/ClassificationTest.cpp + SOURCES + ${RNEXECUTORCH_DIR}/models/classification/Classification.cpp + ${IMAGE_UTILS_SOURCES} + LIBS opencv_deps + ) ``` +* Lastly, add the test executable name to the run_tests script along with all the needed URL and assets. + diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/BaseModelTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/integration/BaseModelTest.cpp new file mode 100644 index 000000000..faf21f5b5 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/integration/BaseModelTest.cpp @@ -0,0 +1,207 @@ +#include "BaseModelTests.h" +#include +#include +#include +#include +#include +#include + +using namespace rnexecutorch; +using namespace rnexecutorch::models; +using namespace executorch::extension; +using namespace model_tests; +using executorch::runtime::EValue; + +constexpr auto kValidStyleTransferModelPath = + "style_transfer_candy_xnnpack.pte"; + +// ============================================================================ +// Common tests via typed test suite +// ============================================================================ +namespace model_tests { +template <> struct ModelTraits { + using ModelType = BaseModel; + + static ModelType createValid() { + return ModelType(kValidStyleTransferModelPath, nullptr); + } + + static ModelType createInvalid() { + return ModelType("nonexistent.pte", nullptr); + } + + static void callGenerate(ModelType &model) { + std::vector shape = {1, 3, 640, 640}; + size_t numElements = 1 * 3 * 640 * 640; + std::vector inputData(numElements, 0.5f); + auto tensorPtr = make_tensor_ptr(shape, inputData.data()); + EValue input(*tensorPtr); + (void)model.forward(input); + } +}; +} // namespace model_tests + +using BaseModelTypes = ::testing::Types; +INSTANTIATE_TYPED_TEST_SUITE_P(BaseModel, CommonModelTest, BaseModelTypes); + +// ============================================================================ +// BaseModel-specific tests (methods not in all models) +// ============================================================================ + +TEST(BaseModelGetInputShapeTests, ValidMethodCorrectShape) { + BaseModel model(kValidStyleTransferModelPath, nullptr); + auto forwardShape = model.getInputShape("forward", 0); + std::vector expectedShape = {1, 3, 640, 640}; + EXPECT_EQ(forwardShape, expectedShape); +} + +TEST(BaseModelGetInputShapeTests, InvalidMethodThrows) { + BaseModel model(kValidStyleTransferModelPath, nullptr); + EXPECT_THROW((void)model.getInputShape("this_method_does_not_exist", 0), + RnExecutorchError); +} + +TEST(BaseModelGetInputShapeTests, ValidMethodInvalidIndexThrows) { + BaseModel model(kValidStyleTransferModelPath, nullptr); + EXPECT_THROW( + (void)model.getInputShape("forward", std::numeric_limits::min()), + RnExecutorchError); +} + +TEST(BaseModelGetAllInputShapesTests, ValidMethodReturnsShapes) { + BaseModel model(kValidStyleTransferModelPath, nullptr); + auto allShapes = model.getAllInputShapes("forward"); + EXPECT_FALSE(allShapes.empty()); + std::vector expectedFirstShape = {1, 3, 640, 640}; + EXPECT_EQ(allShapes[0], expectedFirstShape); +} + +TEST(BaseModelGetAllInputShapesTests, InvalidMethodThrows) { + BaseModel model(kValidStyleTransferModelPath, nullptr); + EXPECT_THROW(model.getAllInputShapes("non_existent_method"), + RnExecutorchError); +} + +TEST(BaseModelGetMethodMetaTests, ValidMethodReturnsOk) { + BaseModel model(kValidStyleTransferModelPath, nullptr); + auto result = model.getMethodMeta("forward"); + EXPECT_TRUE(result.ok()); +} + +TEST(BaseModelGetMethodMetaTests, InvalidMethodReturnsError) { + BaseModel model(kValidStyleTransferModelPath, nullptr); + auto result = model.getMethodMeta("non_existent_method"); + EXPECT_FALSE(result.ok()); +} + +TEST(BaseModelForwardTests, ForwardWithValidInputReturnsOk) { + BaseModel model(kValidStyleTransferModelPath, nullptr); + std::vector shape = {1, 3, 640, 640}; + size_t numElements = 1 * 3 * 640 * 640; + std::vector inputData(numElements, 0.5f); + auto tensorPtr = make_tensor_ptr(shape, inputData.data()); + EValue input(*tensorPtr); + + auto result = model.forward(input); + EXPECT_TRUE(result.ok()); +} + +TEST(BaseModelForwardTests, ForwardWithVectorInputReturnsOk) { + BaseModel model(kValidStyleTransferModelPath, nullptr); + std::vector shape = {1, 3, 640, 640}; + size_t numElements = 1 * 3 * 640 * 640; + std::vector inputData(numElements, 0.5f); + auto tensorPtr = make_tensor_ptr(shape, inputData.data()); + std::vector inputs; + inputs.emplace_back(*tensorPtr); + + auto result = model.forward(inputs); + EXPECT_TRUE(result.ok()); +} + +TEST(BaseModelForwardTests, ForwardReturnsCorrectOutputShape) { + BaseModel model(kValidStyleTransferModelPath, nullptr); + std::vector shape = {1, 3, 640, 640}; + size_t numElements = 1 * 3 * 640 * 640; + std::vector inputData(numElements, 0.5f); + auto tensorPtr = make_tensor_ptr(shape, inputData.data()); + EValue input(*tensorPtr); + + auto result = model.forward(input); + ASSERT_TRUE(result.ok()); + ASSERT_FALSE(result->empty()); + + auto &outputTensor = result->at(0).toTensor(); + auto outputSizes = outputTensor.sizes(); + EXPECT_EQ(outputSizes.size(), 4); + EXPECT_EQ(outputSizes[0], 1); + EXPECT_EQ(outputSizes[1], 3); + EXPECT_EQ(outputSizes[2], 640); + EXPECT_EQ(outputSizes[3], 640); +} + +TEST(BaseModelForwardTests, ForwardAfterUnloadThrows) { + BaseModel model(kValidStyleTransferModelPath, nullptr); + model.unload(); + + std::vector shape = {1, 3, 640, 640}; + size_t numElements = 1 * 3 * 640 * 640; + std::vector inputData(numElements, 0.5f); + auto tensorPtr = make_tensor_ptr(shape, inputData.data()); + EValue input(*tensorPtr); + + EXPECT_THROW(model.forward(input), RnExecutorchError); +} + +TEST(BaseModelForwardJSTests, ForwardJSWithValidInputReturnsOutput) { + BaseModel model(kValidStyleTransferModelPath, nullptr); + std::vector shape = {1, 3, 640, 640}; + size_t numElements = 1 * 3 * 640 * 640; + std::vector inputData(numElements, 0.5f); + + JSTensorViewIn tensorView; + tensorView.dataPtr = inputData.data(); + tensorView.sizes = shape; + tensorView.scalarType = executorch::aten::ScalarType::Float; + + std::vector inputs = {tensorView}; + auto outputs = model.forwardJS(inputs); + + EXPECT_FALSE(outputs.empty()); +} + +TEST(BaseModelForwardJSTests, ForwardJSReturnsCorrectOutputShape) { + BaseModel model(kValidStyleTransferModelPath, nullptr); + std::vector shape = {1, 3, 640, 640}; + size_t numElements = 1 * 3 * 640 * 640; + std::vector inputData(numElements, 0.5f); + + JSTensorViewIn tensorView; + tensorView.dataPtr = inputData.data(); + tensorView.sizes = shape; + tensorView.scalarType = executorch::aten::ScalarType::Float; + + std::vector inputs = {tensorView}; + auto outputs = model.forwardJS(inputs); + + ASSERT_EQ(outputs.size(), 1); + std::vector expectedShape = {1, 3, 640, 640}; + EXPECT_EQ(outputs[0].sizes, expectedShape); +} + +TEST(BaseModelForwardJSTests, ForwardJSAfterUnloadThrows) { + BaseModel model(kValidStyleTransferModelPath, nullptr); + model.unload(); + + std::vector shape = {1, 3, 640, 640}; + size_t numElements = 1 * 3 * 640 * 640; + std::vector inputData(numElements, 0.5f); + + JSTensorViewIn tensorView; + tensorView.dataPtr = inputData.data(); + tensorView.sizes = shape; + tensorView.scalarType = executorch::aten::ScalarType::Float; + + std::vector inputs = {tensorView}; + EXPECT_THROW((void)model.forwardJS(inputs), RnExecutorchError); +} diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/BaseModelTests.h b/packages/react-native-executorch/common/rnexecutorch/tests/integration/BaseModelTests.h new file mode 100644 index 000000000..e1c6e0107 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/integration/BaseModelTests.h @@ -0,0 +1,120 @@ +#pragma once + +#include "gtest/gtest.h" +#include + +namespace facebook::react { +class CallInvoker; +} + +namespace rnexecutorch { +std::shared_ptr createMockCallInvoker(); +} + +namespace model_tests { + +inline auto getMockInvoker() { return rnexecutorch::createMockCallInvoker(); } + +/// Helper macro to access Traits in typed tests +#define SETUP_TRAITS() using Traits = typename TestFixture::Traits + +/// Trait struct that each model must specialize +/// This defines how to construct and test each model type +template struct ModelTraits; + +/// Example of what a specialization looks like: +/// +/// template<> +/// struct ModelTraits { +/// using ModelType = Classification; +/// +/// // Create valid model instance +/// static ModelType createValid() { +/// return ModelType("valid_model.pte", nullptr); +/// } +/// +/// // Create invalid model instance (should throw in constructor) +/// static ModelType createInvalid() { +/// return ModelType("nonexistent.pte", nullptr); +/// } +/// +/// // Call the model's generate/forward function with valid input +/// // Used to test that generate throws after unload +/// static void callGenerate(ModelType& model) { +/// (void)model.generate("valid_input.jpg"); +/// } +/// }; +// Typed test fixture for common model tests +template class CommonModelTest : public ::testing::Test { +protected: + using Traits = ModelTraits; + using ModelType = typename Traits::ModelType; +}; + +// Define the test suite +TYPED_TEST_SUITE_P(CommonModelTest); + +// Constructor tests +TYPED_TEST_P(CommonModelTest, InvalidPathThrows) { + SETUP_TRAITS(); + EXPECT_THROW(Traits::createInvalid(), rnexecutorch::RnExecutorchError); +} + +TYPED_TEST_P(CommonModelTest, ValidPathDoesntThrow) { + SETUP_TRAITS(); + EXPECT_NO_THROW(Traits::createValid()); +} + +// Memory tests +TYPED_TEST_P(CommonModelTest, GetMemoryLowerBoundValue) { + SETUP_TRAITS(); + auto model = Traits::createValid(); + EXPECT_GT(model.getMemoryLowerBound(), 0u); +} + +TYPED_TEST_P(CommonModelTest, GetMemoryLowerBoundConsistent) { + SETUP_TRAITS(); + auto model = Traits::createValid(); + auto bound1 = model.getMemoryLowerBound(); + auto bound2 = model.getMemoryLowerBound(); + EXPECT_EQ(bound1, bound2); +} + +// Unload tests +TYPED_TEST_P(CommonModelTest, UnloadDoesntThrow) { + SETUP_TRAITS(); + auto model = Traits::createValid(); + EXPECT_NO_THROW(model.unload()); +} + +TYPED_TEST_P(CommonModelTest, MultipleUnloadsSafe) { + SETUP_TRAITS(); + auto model = Traits::createValid(); + EXPECT_NO_THROW(model.unload()); + EXPECT_NO_THROW(model.unload()); + EXPECT_NO_THROW(model.unload()); +} + +TYPED_TEST_P(CommonModelTest, GenerateAfterUnloadThrows) { + SETUP_TRAITS(); + auto model = Traits::createValid(); + model.unload(); + EXPECT_THROW(Traits::callGenerate(model), rnexecutorch::RnExecutorchError); +} + +TYPED_TEST_P(CommonModelTest, MultipleGeneratesWork) { + SETUP_TRAITS(); + auto model = Traits::createValid(); + EXPECT_NO_THROW(Traits::callGenerate(model)); + EXPECT_NO_THROW(Traits::callGenerate(model)); + EXPECT_NO_THROW(Traits::callGenerate(model)); +} + +// Register all tests in the suite +REGISTER_TYPED_TEST_SUITE_P(CommonModelTest, InvalidPathThrows, + ValidPathDoesntThrow, GetMemoryLowerBoundValue, + GetMemoryLowerBoundConsistent, UnloadDoesntThrow, + MultipleUnloadsSafe, GenerateAfterUnloadThrows, + MultipleGeneratesWork); + +} // namespace model_tests diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/ClassificationTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/integration/ClassificationTest.cpp new file mode 100644 index 000000000..10aa663a4 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/integration/ClassificationTest.cpp @@ -0,0 +1,117 @@ +#include "BaseModelTests.h" +#include +#include +#include +#include + +using namespace rnexecutorch; +using namespace rnexecutorch::models::classification; +using namespace model_tests; + +constexpr auto kValidClassificationModelPath = "efficientnet_v2_s_xnnpack.pte"; +constexpr auto kValidTestImagePath = + "file:///data/local/tmp/rnexecutorch_tests/test_image.jpg"; + +// ============================================================================ +// Common tests via typed test suite +// ============================================================================ +namespace model_tests { +template <> struct ModelTraits { + using ModelType = Classification; + + static ModelType createValid() { + return ModelType(kValidClassificationModelPath, nullptr); + } + + static ModelType createInvalid() { + return ModelType("nonexistent.pte", nullptr); + } + + static void callGenerate(ModelType &model) { + (void)model.generate(kValidTestImagePath); + } +}; +} // namespace model_tests + +using ClassificationTypes = ::testing::Types; +INSTANTIATE_TYPED_TEST_SUITE_P(Classification, CommonModelTest, + ClassificationTypes); + +// ============================================================================ +// Model-specific tests +// ============================================================================ +TEST(ClassificationGenerateTests, InvalidImagePathThrows) { + Classification model(kValidClassificationModelPath, nullptr); + EXPECT_THROW((void)model.generate("nonexistent_image.jpg"), + RnExecutorchError); +} + +TEST(ClassificationGenerateTests, EmptyImagePathThrows) { + Classification model(kValidClassificationModelPath, nullptr); + EXPECT_THROW((void)model.generate(""), RnExecutorchError); +} + +TEST(ClassificationGenerateTests, MalformedURIThrows) { + Classification model(kValidClassificationModelPath, nullptr); + EXPECT_THROW((void)model.generate("not_a_valid_uri://bad"), + RnExecutorchError); +} + +TEST(ClassificationGenerateTests, ValidImageReturnsResults) { + Classification model(kValidClassificationModelPath, nullptr); + auto results = model.generate(kValidTestImagePath); + EXPECT_FALSE(results.empty()); +} + +TEST(ClassificationGenerateTests, ResultsHaveCorrectSize) { + Classification model(kValidClassificationModelPath, nullptr); + auto results = model.generate(kValidTestImagePath); + auto expectedNumClasses = constants::kImagenet1kV1Labels.size(); + EXPECT_EQ(results.size(), expectedNumClasses); +} + +TEST(ClassificationGenerateTests, ResultsContainValidProbabilities) { + Classification model(kValidClassificationModelPath, nullptr); + auto results = model.generate(kValidTestImagePath); + + float sum = 0.0f; + for (const auto &[label, prob] : results) { + EXPECT_GE(prob, 0.0f); + EXPECT_LE(prob, 1.0f); + sum += prob; + } + EXPECT_NEAR(sum, 1.0f, 0.01f); +} + +TEST(ClassificationGenerateTests, TopPredictionHasReasonableConfidence) { + Classification model(kValidClassificationModelPath, nullptr); + auto results = model.generate(kValidTestImagePath); + + float maxProb = 0.0f; + for (const auto &[label, prob] : results) { + if (prob > maxProb) { + maxProb = prob; + } + } + EXPECT_GT(maxProb, 0.0f); +} + +TEST(ClassificationInheritedTests, GetInputShapeWorks) { + Classification model(kValidClassificationModelPath, nullptr); + auto shape = model.getInputShape("forward", 0); + EXPECT_EQ(shape.size(), 4); + EXPECT_EQ(shape[0], 1); + EXPECT_EQ(shape[1], 3); +} + +TEST(ClassificationInheritedTests, GetAllInputShapesWorks) { + Classification model(kValidClassificationModelPath, nullptr); + auto shapes = model.getAllInputShapes("forward"); + EXPECT_FALSE(shapes.empty()); +} + +TEST(ClassificationInheritedTests, GetMethodMetaWorks) { + Classification model(kValidClassificationModelPath, nullptr); + auto result = model.getMethodMeta("forward"); + EXPECT_TRUE(result.ok()); +} diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/ImageEmbeddingsTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/integration/ImageEmbeddingsTest.cpp new file mode 100644 index 000000000..2e8a53e75 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/integration/ImageEmbeddingsTest.cpp @@ -0,0 +1,122 @@ +#include "BaseModelTests.h" +#include +#include +#include +#include + +using namespace rnexecutorch; +using namespace rnexecutorch::models::embeddings; +using namespace model_tests; + +constexpr auto kValidImageEmbeddingsModelPath = + "clip-vit-base-patch32-vision_xnnpack.pte"; +constexpr auto kValidTestImagePath = + "file:///data/local/tmp/rnexecutorch_tests/test_image.jpg"; + +// ============================================================================ +// Common tests via typed test suite +// ============================================================================ +namespace model_tests { +template <> struct ModelTraits { + using ModelType = ImageEmbeddings; + + static ModelType createValid() { + return ModelType(kValidImageEmbeddingsModelPath, nullptr); + } + + static ModelType createInvalid() { + return ModelType("nonexistent.pte", nullptr); + } + + static void callGenerate(ModelType &model) { + (void)model.generate(kValidTestImagePath); + } +}; +} // namespace model_tests + +using ImageEmbeddingsTypes = ::testing::Types; +INSTANTIATE_TYPED_TEST_SUITE_P(ImageEmbeddings, CommonModelTest, + ImageEmbeddingsTypes); + +// ============================================================================ +// Model-specific tests +// ============================================================================ +TEST(ImageEmbeddingsGenerateTests, InvalidImagePathThrows) { + ImageEmbeddings model(kValidImageEmbeddingsModelPath, nullptr); + EXPECT_THROW((void)model.generate("nonexistent_image.jpg"), + RnExecutorchError); +} + +TEST(ImageEmbeddingsGenerateTests, EmptyImagePathThrows) { + ImageEmbeddings model(kValidImageEmbeddingsModelPath, nullptr); + EXPECT_THROW((void)model.generate(""), RnExecutorchError); +} + +TEST(ImageEmbeddingsGenerateTests, MalformedURIThrows) { + ImageEmbeddings model(kValidImageEmbeddingsModelPath, nullptr); + EXPECT_THROW((void)model.generate("not_a_valid_uri://bad"), + RnExecutorchError); +} + +TEST(ImageEmbeddingsGenerateTests, ValidImageReturnsResults) { + ImageEmbeddings model(kValidImageEmbeddingsModelPath, nullptr); + auto result = model.generate(kValidTestImagePath); + EXPECT_NE(result, nullptr); + EXPECT_GT(result->size(), 0u); +} + +TEST(ImageEmbeddingsGenerateTests, ResultsHaveCorrectSize) { + ImageEmbeddings model(kValidImageEmbeddingsModelPath, nullptr); + auto result = model.generate(kValidTestImagePath); + size_t numFloats = result->size() / sizeof(float); + constexpr size_t kClipEmbeddingDimensions = 512; + EXPECT_EQ(numFloats, kClipEmbeddingDimensions); +} + +TEST(ImageEmbeddingsGenerateTests, ResultsAreNormalized) { + ImageEmbeddings model(kValidImageEmbeddingsModelPath, nullptr); + auto result = model.generate(kValidTestImagePath); + + const float *data = reinterpret_cast(result->data()); + size_t numFloats = result->size() / sizeof(float); + + float sumOfSquares = 0.0f; + for (size_t i = 0; i < numFloats; ++i) { + sumOfSquares += data[i] * data[i]; + } + float norm = std::sqrt(sumOfSquares); + EXPECT_NEAR(norm, 1.0f, 0.01f); +} + +TEST(ImageEmbeddingsGenerateTests, ResultsContainValidValues) { + ImageEmbeddings model(kValidImageEmbeddingsModelPath, nullptr); + auto result = model.generate(kValidTestImagePath); + + const float *data = reinterpret_cast(result->data()); + size_t numFloats = result->size() / sizeof(float); + + for (size_t i = 0; i < numFloats; ++i) { + EXPECT_FALSE(std::isnan(data[i])); + EXPECT_FALSE(std::isinf(data[i])); + } +} + +TEST(ImageEmbeddingsInheritedTests, GetInputShapeWorks) { + ImageEmbeddings model(kValidImageEmbeddingsModelPath, nullptr); + auto shape = model.getInputShape("forward", 0); + EXPECT_EQ(shape.size(), 4); + EXPECT_EQ(shape[0], 1); + EXPECT_EQ(shape[1], 3); +} + +TEST(ImageEmbeddingsInheritedTests, GetAllInputShapesWorks) { + ImageEmbeddings model(kValidImageEmbeddingsModelPath, nullptr); + auto shapes = model.getAllInputShapes("forward"); + EXPECT_FALSE(shapes.empty()); +} + +TEST(ImageEmbeddingsInheritedTests, GetMethodMetaWorks) { + ImageEmbeddings model(kValidImageEmbeddingsModelPath, nullptr); + auto result = model.getMethodMeta("forward"); + EXPECT_TRUE(result.ok()); +} diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/ImageSegmentationTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/integration/ImageSegmentationTest.cpp new file mode 100644 index 000000000..8885848d7 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/integration/ImageSegmentationTest.cpp @@ -0,0 +1,152 @@ +#include +#include +#include +#include +#include +#include + +#include + +using namespace rnexecutorch; +using namespace rnexecutorch::models::image_segmentation; +using executorch::extension::make_tensor_ptr; +using executorch::extension::TensorPtr; +using executorch::runtime::EValue; + +constexpr auto kValidImageSegmentationModelPath = "deeplabV3_xnnpack_fp32.pte"; + +// Test fixture for tests that need dummy input data +class ImageSegmentationForwardTest : public ::testing::Test { +protected: + void SetUp() override { + model = std::make_unique( + kValidImageSegmentationModelPath, nullptr); + auto shapes = model->getAllInputShapes("forward"); + ASSERT_FALSE(shapes.empty()); + shape = shapes[0]; + + size_t numElements = 1; + for (auto dim : shape) { + numElements *= dim; + } + dummyData = std::vector(numElements, 0.5f); + + sizes = std::vector(shape.begin(), shape.end()); + inputTensor = + make_tensor_ptr(sizes, dummyData.data(), exec_aten::ScalarType::Float); + } + + std::unique_ptr model; + std::vector shape; + std::vector dummyData; + std::vector sizes; + TensorPtr inputTensor; +}; + +TEST(ImageSegmentationCtorTests, InvalidPathThrows) { + EXPECT_THROW(ImageSegmentation("this_file_does_not_exist.pte", nullptr), + RnExecutorchError); +} + +TEST(ImageSegmentationCtorTests, ValidPathDoesntThrow) { + EXPECT_NO_THROW(ImageSegmentation(kValidImageSegmentationModelPath, nullptr)); +} + +TEST_F(ImageSegmentationForwardTest, ForwardWithValidTensorSucceeds) { + auto result = model->forward(EValue(inputTensor)); + EXPECT_TRUE(result.ok()); +} + +TEST_F(ImageSegmentationForwardTest, ForwardOutputHasCorrectDimensions) { + auto result = model->forward(EValue(inputTensor)); + ASSERT_TRUE(result.ok()); + + auto &outputs = result.get(); + ASSERT_FALSE(outputs.empty()); + + auto outputTensor = outputs[0].toTensor(); + EXPECT_EQ(outputTensor.dim(), 4); // NCHW format +} + +TEST_F(ImageSegmentationForwardTest, ForwardOutputHas21Classes) { + auto result = model->forward(EValue(inputTensor)); + ASSERT_TRUE(result.ok()); + + auto &outputs = result.get(); + ASSERT_FALSE(outputs.empty()); + + auto outputTensor = outputs[0].toTensor(); + EXPECT_EQ(outputTensor.size(1), 21); // DeepLabV3 has 21 classes +} + +TEST_F(ImageSegmentationForwardTest, MultipleForwardsWork) { + auto result1 = model->forward(EValue(inputTensor)); + EXPECT_TRUE(result1.ok()); + + auto result2 = model->forward(EValue(inputTensor)); + EXPECT_TRUE(result2.ok()); +} + +TEST_F(ImageSegmentationForwardTest, ForwardAfterUnloadThrows) { + model->unload(); + EXPECT_THROW((void)model->forward(EValue(inputTensor)), RnExecutorchError); +} + +TEST(ImageSegmentationInheritedTests, GetInputShapeWorks) { + ImageSegmentation model(kValidImageSegmentationModelPath, nullptr); + auto shape = model.getInputShape("forward", 0); + EXPECT_EQ(shape.size(), 4); + EXPECT_EQ(shape[0], 1); // Batch size + EXPECT_EQ(shape[1], 3); // RGB channels +} + +TEST(ImageSegmentationInheritedTests, GetAllInputShapesWorks) { + ImageSegmentation model(kValidImageSegmentationModelPath, nullptr); + auto shapes = model.getAllInputShapes("forward"); + EXPECT_FALSE(shapes.empty()); +} + +TEST(ImageSegmentationInheritedTests, GetMethodMetaWorks) { + ImageSegmentation model(kValidImageSegmentationModelPath, nullptr); + auto result = model.getMethodMeta("forward"); + EXPECT_TRUE(result.ok()); +} + +TEST(ImageSegmentationInheritedTests, GetMemoryLowerBoundReturnsPositive) { + ImageSegmentation model(kValidImageSegmentationModelPath, nullptr); + EXPECT_GT(model.getMemoryLowerBound(), 0u); +} + +TEST(ImageSegmentationInheritedTests, InputShapeIsSquare) { + ImageSegmentation model(kValidImageSegmentationModelPath, nullptr); + auto shape = model.getInputShape("forward", 0); + EXPECT_EQ(shape[2], shape[3]); // Height == Width for DeepLabV3 +} + +TEST(ImageSegmentationConstantsTests, ClassLabelsHas21Entries) { + EXPECT_EQ(constants::kDeeplabV3Resnet50Labels.size(), 21u); +} + +TEST(ImageSegmentationConstantsTests, ClassLabelsContainExpectedClasses) { + auto &labels = constants::kDeeplabV3Resnet50Labels; + bool hasBackground = false; + bool hasPerson = false; + bool hasCat = false; + bool hasDog = false; + + for (const auto &label : labels) { + if (label == "BACKGROUND") + hasBackground = true; + if (label == "PERSON") + hasPerson = true; + if (label == "CAT") + hasCat = true; + if (label == "DOG") + hasDog = true; + } + + EXPECT_TRUE(hasBackground); + EXPECT_TRUE(hasPerson); + EXPECT_TRUE(hasCat); + EXPECT_TRUE(hasDog); +} diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/LLMTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/integration/LLMTest.cpp new file mode 100644 index 000000000..e79294090 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/integration/LLMTest.cpp @@ -0,0 +1,155 @@ +#include "BaseModelTests.h" +#include +#include +#include + +#include +#include +#include + +using namespace rnexecutorch; +using namespace rnexecutorch::models::llm; +using namespace model_tests; + +constexpr auto kValidModelPath = "smolLm2_135M_8da4w.pte"; +constexpr auto kValidTokenizerPath = "smollm_tokenizer.json"; +constexpr auto kSystemPrompt = "You are a helpful assistant. Assist the user " + "to the best of your abilities."; + +// Forward declaration from jsi_stubs.cpp +namespace rnexecutorch { +std::shared_ptr createMockCallInvoker(); +} + +// Helper to format prompt in ChatML format for SmolLM2 +std::string formatChatML(const std::string &systemPrompt, + const std::string &userMessage) { + return "<|im_start|>system\n" + systemPrompt + "<|im_end|>\n" + + "<|im_start|>user\n" + userMessage + "<|im_end|>\n" + + "<|im_start|>assistant\n"; +} + +// ============================================================================ +// Common tests via typed test suite +// ============================================================================ +namespace model_tests { +template <> struct ModelTraits { + using ModelType = LLM; + + static ModelType createValid() { + return ModelType(kValidModelPath, kValidTokenizerPath, + rnexecutorch::createMockCallInvoker()); + } + + static ModelType createInvalid() { + return ModelType("nonexistent.pte", kValidTokenizerPath, + rnexecutorch::createMockCallInvoker()); + } + + static void callGenerate(ModelType &model) { + std::string prompt = formatChatML(kSystemPrompt, "Hello"); + (void)model.generate(prompt, nullptr); + } +}; +} // namespace model_tests + +using LLMTypes = ::testing::Types; +INSTANTIATE_TYPED_TEST_SUITE_P(LLM, CommonModelTest, LLMTypes); + +// ============================================================================ +// LLM-specific fixture tests +// ============================================================================ +class LLMTest : public ::testing::Test { +protected: + std::shared_ptr mockInvoker_; + + void SetUp() override { mockInvoker_ = createMockCallInvoker(); } +}; + +TEST(LLMCtorTests, InvalidTokenizerPathThrows) { + EXPECT_THROW(LLM(kValidModelPath, "nonexistent_tokenizer.json", + createMockCallInvoker()), + RnExecutorchError); +} + +TEST_F(LLMTest, GetGeneratedTokenCountInitiallyZero) { + LLM model(kValidModelPath, kValidTokenizerPath, mockInvoker_); + EXPECT_EQ(model.getGeneratedTokenCount(), 0); +} + +TEST_F(LLMTest, SetTemperature) { + LLM model(kValidModelPath, kValidTokenizerPath, mockInvoker_); + // Should not throw for valid values + EXPECT_NO_THROW(model.setTemperature(0.5f)); + EXPECT_NO_THROW(model.setTemperature(1.0f)); + EXPECT_NO_THROW(model.setTemperature(0.0f)); +} + +TEST_F(LLMTest, SetTemperatureNegativeThrows) { + LLM model(kValidModelPath, kValidTokenizerPath, mockInvoker_); + EXPECT_THROW(model.setTemperature(-0.1f), RnExecutorchError); +} + +TEST_F(LLMTest, SetTopp) { + LLM model(kValidModelPath, kValidTokenizerPath, mockInvoker_); + EXPECT_NO_THROW(model.setTopp(0.9f)); + EXPECT_NO_THROW(model.setTopp(0.5f)); + EXPECT_NO_THROW(model.setTopp(1.0f)); +} + +TEST_F(LLMTest, SetToppInvalidThrows) { + LLM model(kValidModelPath, kValidTokenizerPath, mockInvoker_); + EXPECT_THROW(model.setTopp(-0.1f), RnExecutorchError); + EXPECT_THROW(model.setTopp(1.1f), RnExecutorchError); +} + +TEST_F(LLMTest, SetCountInterval) { + LLM model(kValidModelPath, kValidTokenizerPath, mockInvoker_); + EXPECT_NO_THROW(model.setCountInterval(5)); + EXPECT_NO_THROW(model.setCountInterval(10)); +} + +TEST_F(LLMTest, SetTimeInterval) { + LLM model(kValidModelPath, kValidTokenizerPath, mockInvoker_); + EXPECT_NO_THROW(model.setTimeInterval(100)); + EXPECT_NO_THROW(model.setTimeInterval(500)); +} + +TEST_F(LLMTest, InterruptThrowsWhenUnloaded) { + LLM model(kValidModelPath, kValidTokenizerPath, mockInvoker_); + model.unload(); + EXPECT_THROW(model.interrupt(), RnExecutorchError); +} + +TEST_F(LLMTest, SettersThrowWhenUnloaded) { + LLM model(kValidModelPath, kValidTokenizerPath, mockInvoker_); + model.unload(); + // All setters should throw when model is unloaded + EXPECT_THROW(model.setTemperature(0.5f), RnExecutorchError); + EXPECT_THROW(model.setTopp(0.9f), RnExecutorchError); + EXPECT_THROW(model.setCountInterval(5), RnExecutorchError); + EXPECT_THROW(model.setTimeInterval(100), RnExecutorchError); +} + +TEST_F(LLMTest, GenerateProducesValidOutput) { + LLM model(kValidModelPath, kValidTokenizerPath, mockInvoker_); + model.setTemperature(0.0f); + std::string prompt = + formatChatML(kSystemPrompt, "Repeat exactly this: `naszponcilem testy`"); + std::string output = model.generate(prompt, nullptr); + EXPECT_EQ(output, "`naszponcilem testy`<|im_end|>"); +} + +TEST_F(LLMTest, GenerateUpdatesTokenCount) { + LLM model(kValidModelPath, kValidTokenizerPath, mockInvoker_); + EXPECT_EQ(model.getGeneratedTokenCount(), 0); + std::string prompt = + formatChatML(kSystemPrompt, "Repeat exactly this: 'naszponcilem testy'"); + model.generate(prompt, nullptr); + EXPECT_GT(model.getGeneratedTokenCount(), 0); +} + +TEST_F(LLMTest, EmptyPromptThrows) { + LLM model(kValidModelPath, kValidTokenizerPath, mockInvoker_); + EXPECT_THROW((void)model.generate("", nullptr), RnExecutorchError); +} diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/OCRTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/integration/OCRTest.cpp new file mode 100644 index 000000000..428fb5afb --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/integration/OCRTest.cpp @@ -0,0 +1,128 @@ +#include "BaseModelTests.h" +#include +#include +#include +#include + +using namespace rnexecutorch; +using namespace rnexecutorch::models::ocr; +using namespace model_tests; + +namespace rnexecutorch { +std::shared_ptr createMockCallInvoker(); +} + +constexpr auto kValidDetectorPath = "xnnpack_craft_quantized.pte"; +constexpr auto kValidRecognizerPath = "xnnpack_crnn_english.pte"; +constexpr auto kValidTestImagePath = + "file:///data/local/tmp/rnexecutorch_tests/we_are_software_mansion.jpg"; + +// English alphabet symbols (must match alphabets.english from symbols.ts) +const std::string ENGLISH_SYMBOLS = + "0123456789!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ " + "\xE2\x82\xAC" // Euro sign (€) + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +// ============================================================================ +// Common tests via typed test suite +// ============================================================================ +namespace model_tests { +template <> struct ModelTraits { + using ModelType = OCR; + + static ModelType createValid() { + return ModelType(kValidDetectorPath, kValidRecognizerPath, ENGLISH_SYMBOLS, + rnexecutorch::createMockCallInvoker()); + } + + static ModelType createInvalid() { + return ModelType("nonexistent.pte", kValidRecognizerPath, ENGLISH_SYMBOLS, + rnexecutorch::createMockCallInvoker()); + } + + static void callGenerate(ModelType &model) { + (void)model.generate(kValidTestImagePath); + } +}; +} // namespace model_tests + +using OCRTypes = ::testing::Types; +INSTANTIATE_TYPED_TEST_SUITE_P(OCR, CommonModelTest, OCRTypes); + +// ============================================================================ +// Model-specific tests +// ============================================================================ +TEST(OCRCtorTests, InvalidRecognizerPathThrows) { + EXPECT_THROW(OCR(kValidDetectorPath, "nonexistent.pte", ENGLISH_SYMBOLS, + createMockCallInvoker()), + RnExecutorchError); +} + +TEST(OCRCtorTests, EmptySymbolsThrows) { + EXPECT_THROW(OCR(kValidDetectorPath, kValidRecognizerPath, "", + createMockCallInvoker()), + RnExecutorchError); +} + +TEST(OCRGenerateTests, InvalidImagePathThrows) { + OCR model(kValidDetectorPath, kValidRecognizerPath, ENGLISH_SYMBOLS, + createMockCallInvoker()); + EXPECT_THROW((void)model.generate("nonexistent_image.jpg"), + RnExecutorchError); +} + +TEST(OCRGenerateTests, EmptyImagePathThrows) { + OCR model(kValidDetectorPath, kValidRecognizerPath, ENGLISH_SYMBOLS, + createMockCallInvoker()); + EXPECT_THROW((void)model.generate(""), RnExecutorchError); +} + +TEST(OCRGenerateTests, MalformedURIThrows) { + OCR model(kValidDetectorPath, kValidRecognizerPath, ENGLISH_SYMBOLS, + createMockCallInvoker()); + EXPECT_THROW((void)model.generate("not_a_valid_uri://bad"), + RnExecutorchError); +} + +TEST(OCRGenerateTests, ValidImageReturnsResults) { + OCR model(kValidDetectorPath, kValidRecognizerPath, ENGLISH_SYMBOLS, + createMockCallInvoker()); + auto results = model.generate(kValidTestImagePath); + // May or may not have detections depending on image content + EXPECT_GE(results.size(), 0u); +} + +TEST(OCRGenerateTests, DetectionsHaveValidBoundingBoxes) { + OCR model(kValidDetectorPath, kValidRecognizerPath, ENGLISH_SYMBOLS, + createMockCallInvoker()); + auto results = model.generate(kValidTestImagePath); + + for (const auto &detection : results) { + // Each bbox should have 4 points + EXPECT_EQ(detection.bbox.size(), 4u); + for (const auto &point : detection.bbox) { + EXPECT_GE(point.x, 0.0f); + EXPECT_GE(point.y, 0.0f); + } + } +} + +TEST(OCRGenerateTests, DetectionsHaveValidScores) { + OCR model(kValidDetectorPath, kValidRecognizerPath, ENGLISH_SYMBOLS, + createMockCallInvoker()); + auto results = model.generate(kValidTestImagePath); + + for (const auto &detection : results) { + EXPECT_GE(detection.score, 0.0f); + EXPECT_LE(detection.score, 1.0f); + } +} + +TEST(OCRGenerateTests, DetectionsHaveNonEmptyText) { + OCR model(kValidDetectorPath, kValidRecognizerPath, ENGLISH_SYMBOLS, + createMockCallInvoker()); + auto results = model.generate(kValidTestImagePath); + for (const auto &detection : results) { + EXPECT_FALSE(detection.text.empty()); + } +} diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/ObjectDetectionTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/integration/ObjectDetectionTest.cpp new file mode 100644 index 000000000..ae80208a6 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/integration/ObjectDetectionTest.cpp @@ -0,0 +1,135 @@ +#include "BaseModelTests.h" +#include +#include +#include +#include + +using namespace rnexecutorch; +using namespace rnexecutorch::models::object_detection; +using namespace model_tests; + +constexpr auto kValidObjectDetectionModelPath = + "ssdlite320-mobilenetv3-large.pte"; +constexpr auto kValidTestImagePath = + "file:///data/local/tmp/rnexecutorch_tests/test_image.jpg"; + +// ============================================================================ +// Common tests via typed test suite +// ============================================================================ +namespace model_tests { +template <> struct ModelTraits { + using ModelType = ObjectDetection; + + static ModelType createValid() { + return ModelType(kValidObjectDetectionModelPath, nullptr); + } + + static ModelType createInvalid() { + return ModelType("nonexistent.pte", nullptr); + } + + static void callGenerate(ModelType &model) { + (void)model.generate(kValidTestImagePath, 0.5); + } +}; +} // namespace model_tests + +using ObjectDetectionTypes = ::testing::Types; +INSTANTIATE_TYPED_TEST_SUITE_P(ObjectDetection, CommonModelTest, + ObjectDetectionTypes); + +// ============================================================================ +// Model-specific tests +// ============================================================================ +TEST(ObjectDetectionGenerateTests, InvalidImagePathThrows) { + ObjectDetection model(kValidObjectDetectionModelPath, nullptr); + EXPECT_THROW((void)model.generate("nonexistent_image.jpg", 0.5), + RnExecutorchError); +} + +TEST(ObjectDetectionGenerateTests, EmptyImagePathThrows) { + ObjectDetection model(kValidObjectDetectionModelPath, nullptr); + EXPECT_THROW((void)model.generate("", 0.5), RnExecutorchError); +} + +TEST(ObjectDetectionGenerateTests, MalformedURIThrows) { + ObjectDetection model(kValidObjectDetectionModelPath, nullptr); + EXPECT_THROW((void)model.generate("not_a_valid_uri://bad", 0.5), + RnExecutorchError); +} + +TEST(ObjectDetectionGenerateTests, NegativeThresholdThrows) { + ObjectDetection model(kValidObjectDetectionModelPath, nullptr); + EXPECT_THROW((void)model.generate(kValidTestImagePath, -0.1), + RnExecutorchError); +} + +TEST(ObjectDetectionGenerateTests, ThresholdAboveOneThrows) { + ObjectDetection model(kValidObjectDetectionModelPath, nullptr); + EXPECT_THROW((void)model.generate(kValidTestImagePath, 1.1), + RnExecutorchError); +} + +TEST(ObjectDetectionGenerateTests, ValidImageReturnsResults) { + ObjectDetection model(kValidObjectDetectionModelPath, nullptr); + auto results = model.generate(kValidTestImagePath, 0.3); + EXPECT_GE(results.size(), 0u); +} + +TEST(ObjectDetectionGenerateTests, HighThresholdReturnsFewerResults) { + ObjectDetection model(kValidObjectDetectionModelPath, nullptr); + auto lowThresholdResults = model.generate(kValidTestImagePath, 0.1); + auto highThresholdResults = model.generate(kValidTestImagePath, 0.9); + EXPECT_GE(lowThresholdResults.size(), highThresholdResults.size()); +} + +TEST(ObjectDetectionGenerateTests, DetectionsHaveValidBoundingBoxes) { + ObjectDetection model(kValidObjectDetectionModelPath, nullptr); + auto results = model.generate(kValidTestImagePath, 0.3); + + for (const auto &detection : results) { + EXPECT_LE(detection.x1, detection.x2); + EXPECT_LE(detection.y1, detection.y2); + EXPECT_GE(detection.x1, 0.0f); + EXPECT_GE(detection.y1, 0.0f); + } +} + +TEST(ObjectDetectionGenerateTests, DetectionsHaveValidScores) { + ObjectDetection model(kValidObjectDetectionModelPath, nullptr); + auto results = model.generate(kValidTestImagePath, 0.3); + + for (const auto &detection : results) { + EXPECT_GE(detection.score, 0.0f); + EXPECT_LE(detection.score, 1.0f); + } +} + +TEST(ObjectDetectionGenerateTests, DetectionsHaveValidLabels) { + ObjectDetection model(kValidObjectDetectionModelPath, nullptr); + auto results = model.generate(kValidTestImagePath, 0.3); + + for (const auto &detection : results) { + EXPECT_GE(detection.label, 0); + } +} + +TEST(ObjectDetectionInheritedTests, GetInputShapeWorks) { + ObjectDetection model(kValidObjectDetectionModelPath, nullptr); + auto shape = model.getInputShape("forward", 0); + EXPECT_EQ(shape.size(), 4); + EXPECT_EQ(shape[0], 1); + EXPECT_EQ(shape[1], 3); +} + +TEST(ObjectDetectionInheritedTests, GetAllInputShapesWorks) { + ObjectDetection model(kValidObjectDetectionModelPath, nullptr); + auto shapes = model.getAllInputShapes("forward"); + EXPECT_FALSE(shapes.empty()); +} + +TEST(ObjectDetectionInheritedTests, GetMethodMetaWorks) { + ObjectDetection model(kValidObjectDetectionModelPath, nullptr); + auto result = model.getMethodMeta("forward"); + EXPECT_TRUE(result.ok()); +} diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/SpeechToTextTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/integration/SpeechToTextTest.cpp new file mode 100644 index 000000000..b9a1d884c --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/integration/SpeechToTextTest.cpp @@ -0,0 +1,97 @@ +#include "BaseModelTests.h" +#include "utils/TestUtils.h" +#include +#include +#include + +using namespace rnexecutorch; +using namespace rnexecutorch::models::speech_to_text; +using namespace test_utils; +using namespace model_tests; + +constexpr auto kValidEncoderPath = "whisper_tiny_en_encoder_xnnpack.pte"; +constexpr auto kValidDecoderPath = "whisper_tiny_en_decoder_xnnpack.pte"; +constexpr auto kValidTokenizerPath = "whisper_tokenizer.json"; + +// ============================================================================ +// Common tests via typed test suite +// ============================================================================ +namespace model_tests { +template <> struct ModelTraits { + using ModelType = SpeechToText; + + static ModelType createValid() { + return ModelType(kValidEncoderPath, kValidDecoderPath, kValidTokenizerPath, + nullptr); + } + + static ModelType createInvalid() { + return ModelType("nonexistent.pte", kValidDecoderPath, kValidTokenizerPath, + nullptr); + } + + static void callGenerate(ModelType &model) { + auto audio = test_utils::loadAudioFromFile("test_audio_float.raw"); + (void)model.transcribe(audio, "en"); + } +}; +} // namespace model_tests + +using SpeechToTextTypes = ::testing::Types; +INSTANTIATE_TYPED_TEST_SUITE_P(SpeechToText, CommonModelTest, + SpeechToTextTypes); + +// ============================================================================ +// Model-specific tests +// ============================================================================ +TEST(S2TCtorTests, InvalidDecoderPathThrows) { + EXPECT_THROW(SpeechToText(kValidEncoderPath, "nonexistent.pte", + kValidTokenizerPath, nullptr), + RnExecutorchError); +} + +TEST(S2TCtorTests, InvalidTokenizerPathThrows) { + EXPECT_THROW(SpeechToText(kValidEncoderPath, kValidDecoderPath, + "nonexistent.json", nullptr), + RnExecutorchError); +} + +TEST(S2TEncodeTests, EncodeReturnsNonNull) { + SpeechToText model(kValidEncoderPath, kValidDecoderPath, kValidTokenizerPath, + nullptr); + auto audio = loadAudioFromFile("test_audio_float.raw"); + ASSERT_FALSE(audio.empty()); + auto result = model.encode(audio); + EXPECT_NE(result, nullptr); + EXPECT_GT(result->size(), 0u); +} + +TEST(S2TTranscribeTests, TranscribeReturnsValidChars) { + SpeechToText model(kValidEncoderPath, kValidDecoderPath, kValidTokenizerPath, + nullptr); + auto audio = loadAudioFromFile("test_audio_float.raw"); + ASSERT_FALSE(audio.empty()); + auto result = model.transcribe(audio, "en"); + ASSERT_FALSE(result.empty()); + for (char c : result) { + EXPECT_GE(static_cast(c), 0); + EXPECT_LE(static_cast(c), 127); + } +} + +TEST(S2TTranscribeTests, EmptyResultOnSilence) { + SpeechToText model(kValidEncoderPath, kValidDecoderPath, kValidTokenizerPath, + nullptr); + auto audio = generateSilence(16000 * 5); + auto result = model.transcribe(audio, "en"); + EXPECT_TRUE(result.empty()); +} + +TEST(S2TTranscribeTests, InvalidLanguageThrows) { + SpeechToText model(kValidEncoderPath, kValidDecoderPath, kValidTokenizerPath, + nullptr); + auto audio = loadAudioFromFile("test_audio_float.raw"); + ASSERT_FALSE(audio.empty()); + EXPECT_THROW((void)model.transcribe(audio, "invalid_language_code"), + RnExecutorchError); +} diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/StyleTransferTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/integration/StyleTransferTest.cpp new file mode 100644 index 000000000..3e6951617 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/integration/StyleTransferTest.cpp @@ -0,0 +1,112 @@ +#include "BaseModelTests.h" +#include "utils/TestUtils.h" +#include +#include +#include +#include + +using namespace rnexecutorch; +using namespace rnexecutorch::models::style_transfer; +using namespace model_tests; + +constexpr auto kValidStyleTransferModelPath = + "style_transfer_candy_xnnpack.pte"; +constexpr auto kValidTestImagePath = + "file:///data/local/tmp/rnexecutorch_tests/test_image.jpg"; + +// ============================================================================ +// Common tests via typed test suite +// ============================================================================ +namespace model_tests { +template <> struct ModelTraits { + using ModelType = StyleTransfer; + + static ModelType createValid() { + return ModelType(kValidStyleTransferModelPath, nullptr); + } + + static ModelType createInvalid() { + return ModelType("nonexistent.pte", nullptr); + } + + static void callGenerate(ModelType &model) { + (void)model.generate(kValidTestImagePath); + } +}; +} // namespace model_tests + +using StyleTransferTypes = ::testing::Types; +INSTANTIATE_TYPED_TEST_SUITE_P(StyleTransfer, CommonModelTest, + StyleTransferTypes); + +// ============================================================================ +// Model-specific tests +// ============================================================================ +TEST(StyleTransferGenerateTests, InvalidImagePathThrows) { + StyleTransfer model(kValidStyleTransferModelPath, nullptr); + EXPECT_THROW((void)model.generate("nonexistent_image.jpg"), + RnExecutorchError); +} + +TEST(StyleTransferGenerateTests, EmptyImagePathThrows) { + StyleTransfer model(kValidStyleTransferModelPath, nullptr); + EXPECT_THROW((void)model.generate(""), RnExecutorchError); +} + +TEST(StyleTransferGenerateTests, MalformedURIThrows) { + StyleTransfer model(kValidStyleTransferModelPath, nullptr); + EXPECT_THROW((void)model.generate("not_a_valid_uri://bad"), + RnExecutorchError); +} + +TEST(StyleTransferGenerateTests, ValidImageReturnsFilePath) { + StyleTransfer model(kValidStyleTransferModelPath, nullptr); + auto result = model.generate(kValidTestImagePath); + EXPECT_FALSE(result.empty()); +} + +TEST(StyleTransferGenerateTests, ResultIsValidFilePath) { + StyleTransfer model(kValidStyleTransferModelPath, nullptr); + auto result = model.generate(kValidTestImagePath); + test_utils::trimFilePrefix(result); + EXPECT_TRUE(std::filesystem::exists(result)); +} + +TEST(StyleTransferGenerateTests, ResultFileHasContent) { + StyleTransfer model(kValidStyleTransferModelPath, nullptr); + auto result = model.generate(kValidTestImagePath); + test_utils::trimFilePrefix(result); + auto fileSize = std::filesystem::file_size(result); + EXPECT_GT(fileSize, 0u); +} + +TEST(StyleTransferGenerateTests, MultipleGeneratesWork) { + StyleTransfer model(kValidStyleTransferModelPath, nullptr); + EXPECT_NO_THROW((void)model.generate(kValidTestImagePath)); + auto result1 = model.generate(kValidTestImagePath); + auto result2 = model.generate(kValidTestImagePath); + test_utils::trimFilePrefix(result1); + test_utils::trimFilePrefix(result2); + EXPECT_TRUE(std::filesystem::exists(result1)); + EXPECT_TRUE(std::filesystem::exists(result2)); +} + +TEST(StyleTransferInheritedTests, GetInputShapeWorks) { + StyleTransfer model(kValidStyleTransferModelPath, nullptr); + auto shape = model.getInputShape("forward", 0); + EXPECT_EQ(shape.size(), 4); + EXPECT_EQ(shape[0], 1); + EXPECT_EQ(shape[1], 3); +} + +TEST(StyleTransferInheritedTests, GetAllInputShapesWorks) { + StyleTransfer model(kValidStyleTransferModelPath, nullptr); + auto shapes = model.getAllInputShapes("forward"); + EXPECT_FALSE(shapes.empty()); +} + +TEST(StyleTransferInheritedTests, GetMethodMetaWorks) { + StyleTransfer model(kValidStyleTransferModelPath, nullptr); + auto result = model.getMethodMeta("forward"); + EXPECT_TRUE(result.ok()); +} diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/TextEmbeddingsTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/integration/TextEmbeddingsTest.cpp new file mode 100644 index 000000000..ff1abd4c3 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/integration/TextEmbeddingsTest.cpp @@ -0,0 +1,164 @@ +#include "BaseModelTests.h" +#include +#include +#include +#include +#include + +using namespace rnexecutorch; +using namespace rnexecutorch::models::embeddings; +using namespace model_tests; + +constexpr auto kValidTextEmbeddingsModelPath = "all-MiniLM-L6-v2_xnnpack.pte"; +constexpr auto kValidTextEmbeddingsTokenizerPath = "tokenizer.json"; +constexpr size_t kMiniLmEmbeddingDimensions = 384; + +// ============================================================================ +// Common tests via typed test suite +// ============================================================================ +namespace model_tests { +template <> struct ModelTraits { + using ModelType = TextEmbeddings; + + static ModelType createValid() { + return ModelType(kValidTextEmbeddingsModelPath, + kValidTextEmbeddingsTokenizerPath, nullptr); + } + + static ModelType createInvalid() { + return ModelType("nonexistent.pte", kValidTextEmbeddingsTokenizerPath, + nullptr); + } + + static void callGenerate(ModelType &model) { + (void)model.generate("Hello, world!"); + } +}; +} // namespace model_tests + +using TextEmbeddingsTypes = ::testing::Types; +INSTANTIATE_TYPED_TEST_SUITE_P(TextEmbeddings, CommonModelTest, + TextEmbeddingsTypes); + +// ============================================================================ +// Model-specific tests +// ============================================================================ +TEST(TextEmbeddingsCtorTests, InvalidTokenizerPathThrows) { + EXPECT_THROW(TextEmbeddings(kValidTextEmbeddingsModelPath, + "this_tokenizer_does_not_exist.json", nullptr), + std::exception); +} + +TEST(TextEmbeddingsGenerateTests, EmptyStringReturnsResults) { + TextEmbeddings model(kValidTextEmbeddingsModelPath, + kValidTextEmbeddingsTokenizerPath, nullptr); + auto result = model.generate(""); + EXPECT_NE(result, nullptr); + EXPECT_GT(result->size(), 0u); +} + +TEST(TextEmbeddingsGenerateTests, ValidTextReturnsResults) { + TextEmbeddings model(kValidTextEmbeddingsModelPath, + kValidTextEmbeddingsTokenizerPath, nullptr); + auto result = model.generate("Hello, world!"); + EXPECT_NE(result, nullptr); + EXPECT_GT(result->size(), 0u); +} + +TEST(TextEmbeddingsGenerateTests, ResultsHaveCorrectSize) { + TextEmbeddings model(kValidTextEmbeddingsModelPath, + kValidTextEmbeddingsTokenizerPath, nullptr); + auto result = model.generate("This is a test sentence."); + size_t numFloats = result->size() / sizeof(float); + EXPECT_EQ(numFloats, kMiniLmEmbeddingDimensions); +} + +TEST(TextEmbeddingsGenerateTests, ResultsAreNormalized) { + TextEmbeddings model(kValidTextEmbeddingsModelPath, + kValidTextEmbeddingsTokenizerPath, nullptr); + auto result = model.generate("The quick brown fox jumps over the lazy dog."); + + const float *data = reinterpret_cast(result->data()); + size_t numFloats = result->size() / sizeof(float); + + float sumOfSquares = 0.0f; + for (size_t i = 0; i < numFloats; ++i) { + sumOfSquares += data[i] * data[i]; + } + float norm = std::sqrt(sumOfSquares); + EXPECT_NEAR(norm, 1.0f, 0.01f); +} + +TEST(TextEmbeddingsGenerateTests, ResultsContainValidValues) { + TextEmbeddings model(kValidTextEmbeddingsModelPath, + kValidTextEmbeddingsTokenizerPath, nullptr); + auto result = model.generate("Testing valid values."); + + const float *data = reinterpret_cast(result->data()); + size_t numFloats = result->size() / sizeof(float); + + for (size_t i = 0; i < numFloats; ++i) { + EXPECT_FALSE(std::isnan(data[i])); + EXPECT_FALSE(std::isinf(data[i])); + } +} + +TEST(TextEmbeddingsGenerateTests, DifferentTextProducesDifferentEmbeddings) { + TextEmbeddings model(kValidTextEmbeddingsModelPath, + kValidTextEmbeddingsTokenizerPath, nullptr); + + auto result1 = model.generate("Hello, world!"); + auto result2 = model.generate("Goodbye, moon!"); + + const float *data1 = reinterpret_cast(result1->data()); + const float *data2 = reinterpret_cast(result2->data()); + size_t numFloats = result1->size() / sizeof(float); + + bool allEqual = true; + for (size_t i = 0; i < numFloats; ++i) { + if (std::abs(data1[i] - data2[i]) > 1e-6f) { + allEqual = false; + break; + } + } + EXPECT_FALSE(allEqual); +} + +TEST(TextEmbeddingsGenerateTests, SimilarTextProducesSimilarEmbeddings) { + TextEmbeddings model(kValidTextEmbeddingsModelPath, + kValidTextEmbeddingsTokenizerPath, nullptr); + + auto result1 = model.generate("I love programming"); + auto result2 = model.generate("I enjoy coding"); + + const float *data1 = reinterpret_cast(result1->data()); + const float *data2 = reinterpret_cast(result2->data()); + size_t numFloats = result1->size() / sizeof(float); + + float dotProduct = 0.0f; + for (size_t i = 0; i < numFloats; ++i) { + dotProduct += data1[i] * data2[i]; + } + EXPECT_GT(dotProduct, 0.5f); +} + +TEST(TextEmbeddingsInheritedTests, GetInputShapeWorks) { + TextEmbeddings model(kValidTextEmbeddingsModelPath, + kValidTextEmbeddingsTokenizerPath, nullptr); + auto shape = model.getInputShape("forward", 0); + EXPECT_GE(shape.size(), 2u); +} + +TEST(TextEmbeddingsInheritedTests, GetAllInputShapesWorks) { + TextEmbeddings model(kValidTextEmbeddingsModelPath, + kValidTextEmbeddingsTokenizerPath, nullptr); + auto shapes = model.getAllInputShapes("forward"); + EXPECT_FALSE(shapes.empty()); +} + +TEST(TextEmbeddingsInheritedTests, GetMethodMetaWorks) { + TextEmbeddings model(kValidTextEmbeddingsModelPath, + kValidTextEmbeddingsTokenizerPath, nullptr); + auto result = model.getMethodMeta("forward"); + EXPECT_TRUE(result.ok()); +} diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/TextToImageTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/integration/TextToImageTest.cpp new file mode 100644 index 000000000..712967854 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/integration/TextToImageTest.cpp @@ -0,0 +1,149 @@ +#include "BaseModelTests.h" +#include +#include +#include +#include + +using namespace rnexecutorch; +using namespace rnexecutorch::models::text_to_image; +using namespace model_tests; + +namespace rnexecutorch { +std::shared_ptr createMockCallInvoker(); +} + +constexpr auto kValidTokenizerPath = "t2i_tokenizer.json"; +constexpr auto kValidEncoderPath = "t2i_encoder.pte"; +constexpr auto kValidUnetPath = "t2i_unet.pte"; +constexpr auto kValidDecoderPath = "t2i_decoder.pte"; + +constexpr float kSchedulerBetaStart = 0.00085f; +constexpr float kSchedulerBetaEnd = 0.012f; +constexpr int32_t kSchedulerNumTrainTimesteps = 1000; +constexpr int32_t kSchedulerStepsOffset = 1; + +// ============================================================================ +// Common tests via typed test suite +// ============================================================================ +namespace model_tests { +template <> struct ModelTraits { + using ModelType = TextToImage; + + static ModelType createValid() { + return ModelType(kValidTokenizerPath, kValidEncoderPath, kValidUnetPath, + kValidDecoderPath, kSchedulerBetaStart, kSchedulerBetaEnd, + kSchedulerNumTrainTimesteps, kSchedulerStepsOffset, + rnexecutorch::createMockCallInvoker()); + } + + static ModelType createInvalid() { + return ModelType("nonexistent.json", kValidEncoderPath, kValidUnetPath, + kValidDecoderPath, kSchedulerBetaStart, kSchedulerBetaEnd, + kSchedulerNumTrainTimesteps, kSchedulerStepsOffset, + rnexecutorch::createMockCallInvoker()); + } + + static void callGenerate(ModelType &model) { + (void)model.generate("a cat", 128, 1, 42, nullptr); + } +}; +} // namespace model_tests + +using TextToImageTypes = ::testing::Types; +INSTANTIATE_TYPED_TEST_SUITE_P(TextToImage, CommonModelTest, TextToImageTypes); + +// ============================================================================ +// Model-specific tests +// ============================================================================ +TEST(TextToImageCtorTests, InvalidEncoderPathThrows) { + EXPECT_THROW(TextToImage(kValidTokenizerPath, "nonexistent.pte", + kValidUnetPath, kValidDecoderPath, + kSchedulerBetaStart, kSchedulerBetaEnd, + kSchedulerNumTrainTimesteps, kSchedulerStepsOffset, + createMockCallInvoker()), + RnExecutorchError); +} + +TEST(TextToImageCtorTests, InvalidUnetPathThrows) { + EXPECT_THROW(TextToImage(kValidTokenizerPath, kValidEncoderPath, + "nonexistent.pte", kValidDecoderPath, + kSchedulerBetaStart, kSchedulerBetaEnd, + kSchedulerNumTrainTimesteps, kSchedulerStepsOffset, + createMockCallInvoker()), + RnExecutorchError); +} + +TEST(TextToImageCtorTests, InvalidDecoderPathThrows) { + EXPECT_THROW(TextToImage(kValidTokenizerPath, kValidEncoderPath, + kValidUnetPath, "nonexistent.pte", + kSchedulerBetaStart, kSchedulerBetaEnd, + kSchedulerNumTrainTimesteps, kSchedulerStepsOffset, + createMockCallInvoker()), + RnExecutorchError); +} + +TEST(TextToImageGenerateTests, InvalidImageSizeThrows) { + TextToImage model(kValidTokenizerPath, kValidEncoderPath, kValidUnetPath, + kValidDecoderPath, kSchedulerBetaStart, kSchedulerBetaEnd, + kSchedulerNumTrainTimesteps, kSchedulerStepsOffset, + createMockCallInvoker()); + EXPECT_THROW((void)model.generate("a cat", 100, 1, 42, nullptr), + RnExecutorchError); +} + +TEST(TextToImageGenerateTests, EmptyPromptThrows) { + TextToImage model(kValidTokenizerPath, kValidEncoderPath, kValidUnetPath, + kValidDecoderPath, kSchedulerBetaStart, kSchedulerBetaEnd, + kSchedulerNumTrainTimesteps, kSchedulerStepsOffset, + createMockCallInvoker()); + EXPECT_THROW((void)model.generate("", 128, 1, 42, nullptr), + RnExecutorchError); +} + +TEST(TextToImageGenerateTests, ZeroStepsThrows) { + TextToImage model(kValidTokenizerPath, kValidEncoderPath, kValidUnetPath, + kValidDecoderPath, kSchedulerBetaStart, kSchedulerBetaEnd, + kSchedulerNumTrainTimesteps, kSchedulerStepsOffset, + createMockCallInvoker()); + EXPECT_THROW((void)model.generate("a cat", 128, 0, 42, nullptr), + RnExecutorchError); +} + +TEST(TextToImageGenerateTests, GenerateReturnsNonNull) { + TextToImage model(kValidTokenizerPath, kValidEncoderPath, kValidUnetPath, + kValidDecoderPath, kSchedulerBetaStart, kSchedulerBetaEnd, + kSchedulerNumTrainTimesteps, kSchedulerStepsOffset, + createMockCallInvoker()); + auto result = model.generate("a cat", 128, 1, 42, nullptr); + EXPECT_NE(result, nullptr); +} + +TEST(TextToImageGenerateTests, GenerateReturnsCorrectSize) { + TextToImage model(kValidTokenizerPath, kValidEncoderPath, kValidUnetPath, + kValidDecoderPath, kSchedulerBetaStart, kSchedulerBetaEnd, + kSchedulerNumTrainTimesteps, kSchedulerStepsOffset, + createMockCallInvoker()); + int32_t imageSize = 128; + auto result = model.generate("a cat", imageSize, 1, 42, nullptr); + ASSERT_NE(result, nullptr); + size_t expectedSize = imageSize * imageSize * 4; + EXPECT_EQ(result->size(), expectedSize); +} + +TEST(TextToImageGenerateTests, SameSeedProducesSameResult) { + TextToImage model(kValidTokenizerPath, kValidEncoderPath, kValidUnetPath, + kValidDecoderPath, kSchedulerBetaStart, kSchedulerBetaEnd, + kSchedulerNumTrainTimesteps, kSchedulerStepsOffset, + createMockCallInvoker()); + auto result1 = model.generate("a cat", 128, 1, 42, nullptr); + auto result2 = model.generate("a cat", 128, 1, 42, nullptr); + ASSERT_NE(result1, nullptr); + ASSERT_NE(result2, nullptr); + ASSERT_EQ(result1->size(), result2->size()); + + auto data1 = static_cast(result1->data()); + auto data2 = static_cast(result2->data()); + for (size_t i = 0; i < result1->size(); i++) { + EXPECT_EQ(data1[i], data2[i]) << "at index: " << i; + } +} diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/TokenizerModuleTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/integration/TokenizerModuleTest.cpp new file mode 100644 index 000000000..393ce78d3 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/integration/TokenizerModuleTest.cpp @@ -0,0 +1,98 @@ +#include +#include +#include + +using namespace rnexecutorch; + +constexpr auto kValidTokenizerPath = "tokenizer.json"; + +TEST(TokenizerCtorTests, InvalidPathThrows) { + EXPECT_THROW(TokenizerModule("nonexistent_tokenizer.json", nullptr), + RnExecutorchError); +} + +TEST(TokenizerCtorTests, ValidPathDoesntThrow) { + EXPECT_NO_THROW(TokenizerModule(kValidTokenizerPath, nullptr)); +} + +TEST(TokenizerMemoryTests, MemoryLowerBoundIsPositive) { + TokenizerModule tokenizer(kValidTokenizerPath, nullptr); + EXPECT_GT(tokenizer.getMemoryLowerBound(), 0u); +} + +TEST(TokenizerEncodeTests, EmptyStringReturnsEmptyString) { + TokenizerModule tokenizer(kValidTokenizerPath, nullptr); + auto tokens = tokenizer.encode(""); + EXPECT_TRUE(tokens.empty()); +} + +TEST(TokenizerEncodeTests, SimpleTextReturnsTokens) { + TokenizerModule tokenizer(kValidTokenizerPath, nullptr); + auto tokens = tokenizer.encode("Hello world"); + EXPECT_GT(tokens.size(), 0u); +} + +TEST(TokenizerEncodeTests, SameTextReturnsSameTokens) { + TokenizerModule tokenizer(kValidTokenizerPath, nullptr); + auto tokens1 = tokenizer.encode("test"); + auto tokens2 = tokenizer.encode("test"); + EXPECT_EQ(tokens1, tokens2); +} + +TEST(TokenizerEncodeTests, DifferentTextReturnsDifferentTokens) { + TokenizerModule tokenizer(kValidTokenizerPath, nullptr); + auto tokens1 = tokenizer.encode("hello"); + auto tokens2 = tokenizer.encode("goodbye"); + EXPECT_NE(tokens1, tokens2); +} + +TEST(TokenizerEncodeTests, SpecialCharactersWork) { + TokenizerModule tokenizer(kValidTokenizerPath, nullptr); + auto tokens = tokenizer.encode("!@#$%^&*()"); + EXPECT_GT(tokens.size(), 0u); +} + +TEST(TokenizerEncodeTests, VeryLongTextWorks) { + TokenizerModule tokenizer(kValidTokenizerPath, nullptr); + std::string longText(10000, 'a'); + EXPECT_NO_THROW((void)tokenizer.encode(longText)); +} + +TEST(TokenizerDecodeTests, DecodeEncodedTextReturnsOriginal) { + TokenizerModule tokenizer(kValidTokenizerPath, nullptr); + std::string original = "szponcik"; + auto tokens = tokenizer.encode(original); + auto decoded = tokenizer.decode(tokens, true); + EXPECT_EQ(decoded, original); +} + +TEST(TokenizerDecodeTests, DecodeEmptyVectorReturnsEmpty) { + TokenizerModule tokenizer(kValidTokenizerPath, nullptr); + auto decoded = tokenizer.decode({}, true); + EXPECT_TRUE(decoded.empty()); +} + +TEST(TokenizerIdToTokenTests, ValidIdReturnsToken) { + TokenizerModule tokenizer(kValidTokenizerPath, nullptr); + auto token = tokenizer.idToToken(0); + EXPECT_FALSE(token.empty()); +} + +TEST(TokenizerTokenToIdTests, RoundTripWorks) { + TokenizerModule tokenizer(kValidTokenizerPath, nullptr); + auto token = tokenizer.idToToken(100); + auto id = tokenizer.tokenToId(token); + EXPECT_EQ(id, 100); +} + +TEST(TokenizerVocabTests, VocabSizeIsPositive) { + TokenizerModule tokenizer(kValidTokenizerPath, nullptr); + EXPECT_GT(tokenizer.getVocabSize(), 0u); +} + +TEST(TokenizerVocabTests, VocabSizeIsReasonable) { + TokenizerModule tokenizer(kValidTokenizerPath, nullptr); + auto vocabSize = tokenizer.getVocabSize(); + EXPECT_GT(vocabSize, 1000u); + EXPECT_LT(vocabSize, 1000000u); +} diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/VerticalOCRTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/integration/VerticalOCRTest.cpp new file mode 100644 index 000000000..7b1010a81 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/integration/VerticalOCRTest.cpp @@ -0,0 +1,238 @@ +#include "BaseModelTests.h" +#include +#include +#include +#include + +using namespace rnexecutorch; +using namespace rnexecutorch::models::ocr; +using namespace model_tests; + +namespace rnexecutorch { +std::shared_ptr createMockCallInvoker(); +} + +constexpr auto kValidVerticalDetectorPath = "xnnpack_craft_quantized.pte"; +constexpr auto kValidVerticalRecognizerPath = "xnnpack_crnn_english.pte"; +constexpr auto kValidVerticalTestImagePath = + "file:///data/local/tmp/rnexecutorch_tests/we_are_software_mansion.jpg"; + +// English alphabet symbols (must match alphabets.english from symbols.ts) +const std::string ENGLISH_SYMBOLS = + "0123456789!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ " + "\xE2\x82\xAC" // Euro sign (€) + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +// ============================================================================ +// Common tests via typed test suite +// ============================================================================ +namespace model_tests { +template <> struct ModelTraits { + using ModelType = VerticalOCR; + + static ModelType createValid() { + return ModelType(kValidVerticalDetectorPath, kValidVerticalRecognizerPath, + ENGLISH_SYMBOLS, false, + rnexecutorch::createMockCallInvoker()); + } + + static ModelType createInvalid() { + return ModelType("nonexistent.pte", kValidVerticalRecognizerPath, + ENGLISH_SYMBOLS, false, + rnexecutorch::createMockCallInvoker()); + } + + static void callGenerate(ModelType &model) { + (void)model.generate(kValidVerticalTestImagePath); + } +}; +} // namespace model_tests + +using VerticalOCRTypes = ::testing::Types; +INSTANTIATE_TYPED_TEST_SUITE_P(VerticalOCR, CommonModelTest, VerticalOCRTypes); + +// ============================================================================ +// VerticalOCR-specific tests +// ============================================================================ + +// Constructor tests +TEST(VerticalOCRCtorTests, InvalidRecognizerPathThrows) { + EXPECT_THROW(VerticalOCR(kValidVerticalDetectorPath, "nonexistent.pte", + ENGLISH_SYMBOLS, false, createMockCallInvoker()), + RnExecutorchError); +} + +TEST(VerticalOCRCtorTests, EmptySymbolsThrows) { + EXPECT_THROW(VerticalOCR(kValidVerticalDetectorPath, + kValidVerticalRecognizerPath, "", false, + createMockCallInvoker()), + RnExecutorchError); +} + +TEST(VerticalOCRCtorTests, IndependentCharsTrueDoesntThrow) { + EXPECT_NO_THROW(VerticalOCR(kValidVerticalDetectorPath, + kValidVerticalRecognizerPath, ENGLISH_SYMBOLS, + true, createMockCallInvoker())); +} + +TEST(VerticalOCRCtorTests, IndependentCharsFalseDoesntThrow) { + EXPECT_NO_THROW(VerticalOCR(kValidVerticalDetectorPath, + kValidVerticalRecognizerPath, ENGLISH_SYMBOLS, + false, createMockCallInvoker())); +} + +// Generate tests - Independent Characters strategy +TEST(VerticalOCRGenerateTests, IndependentCharsInvalidImageThrows) { + VerticalOCR model(kValidVerticalDetectorPath, kValidVerticalRecognizerPath, + ENGLISH_SYMBOLS, true, createMockCallInvoker()); + EXPECT_THROW((void)model.generate("nonexistent_image.jpg"), + RnExecutorchError); +} + +TEST(VerticalOCRGenerateTests, IndependentCharsEmptyImagePathThrows) { + VerticalOCR model(kValidVerticalDetectorPath, kValidVerticalRecognizerPath, + ENGLISH_SYMBOLS, true, createMockCallInvoker()); + EXPECT_THROW((void)model.generate(""), RnExecutorchError); +} + +TEST(VerticalOCRGenerateTests, IndependentCharsMalformedURIThrows) { + VerticalOCR model(kValidVerticalDetectorPath, kValidVerticalRecognizerPath, + ENGLISH_SYMBOLS, true, createMockCallInvoker()); + EXPECT_THROW((void)model.generate("not_a_valid_uri://bad"), + RnExecutorchError); +} + +TEST(VerticalOCRGenerateTests, IndependentCharsValidImageReturnsResults) { + VerticalOCR model(kValidVerticalDetectorPath, kValidVerticalRecognizerPath, + ENGLISH_SYMBOLS, true, createMockCallInvoker()); + auto results = model.generate(kValidVerticalTestImagePath); + EXPECT_GE(results.size(), 0u); +} + +TEST(VerticalOCRGenerateTests, IndependentCharsDetectionsHaveValidBBoxes) { + VerticalOCR model(kValidVerticalDetectorPath, kValidVerticalRecognizerPath, + ENGLISH_SYMBOLS, true, createMockCallInvoker()); + auto results = model.generate(kValidVerticalTestImagePath); + + for (const auto &detection : results) { + EXPECT_EQ(detection.bbox.size(), 4u); + for (const auto &point : detection.bbox) { + EXPECT_GE(point.x, 0.0f); + EXPECT_GE(point.y, 0.0f); + } + } +} + +TEST(VerticalOCRGenerateTests, IndependentCharsDetectionsHaveValidScores) { + VerticalOCR model(kValidVerticalDetectorPath, kValidVerticalRecognizerPath, + ENGLISH_SYMBOLS, true, createMockCallInvoker()); + auto results = model.generate(kValidVerticalTestImagePath); + + for (const auto &detection : results) { + EXPECT_GE(detection.score, 0.0f); + EXPECT_LE(detection.score, 1.0f); + } +} + +TEST(VerticalOCRGenerateTests, IndependentCharsDetectionsHaveNonEmptyText) { + VerticalOCR model(kValidVerticalDetectorPath, kValidVerticalRecognizerPath, + ENGLISH_SYMBOLS, true, createMockCallInvoker()); + auto results = model.generate(kValidVerticalTestImagePath); + + for (const auto &detection : results) { + EXPECT_FALSE(detection.text.empty()); + } +} + +// Generate tests - Joint Characters strategy +TEST(VerticalOCRGenerateTests, JointCharsInvalidImageThrows) { + VerticalOCR model(kValidVerticalDetectorPath, kValidVerticalRecognizerPath, + ENGLISH_SYMBOLS, false, createMockCallInvoker()); + EXPECT_THROW((void)model.generate("nonexistent_image.jpg"), + RnExecutorchError); +} + +TEST(VerticalOCRGenerateTests, JointCharsEmptyImagePathThrows) { + VerticalOCR model(kValidVerticalDetectorPath, kValidVerticalRecognizerPath, + ENGLISH_SYMBOLS, false, createMockCallInvoker()); + EXPECT_THROW((void)model.generate(""), RnExecutorchError); +} + +TEST(VerticalOCRGenerateTests, JointCharsMalformedURIThrows) { + VerticalOCR model(kValidVerticalDetectorPath, kValidVerticalRecognizerPath, + ENGLISH_SYMBOLS, false, createMockCallInvoker()); + EXPECT_THROW((void)model.generate("not_a_valid_uri://bad"), + RnExecutorchError); +} + +TEST(VerticalOCRGenerateTests, JointCharsValidImageReturnsResults) { + VerticalOCR model(kValidVerticalDetectorPath, kValidVerticalRecognizerPath, + ENGLISH_SYMBOLS, false, createMockCallInvoker()); + auto results = model.generate(kValidVerticalTestImagePath); + EXPECT_GE(results.size(), 0u); +} + +TEST(VerticalOCRGenerateTests, JointCharsDetectionsHaveValidBBoxes) { + VerticalOCR model(kValidVerticalDetectorPath, kValidVerticalRecognizerPath, + ENGLISH_SYMBOLS, false, createMockCallInvoker()); + auto results = model.generate(kValidVerticalTestImagePath); + + for (const auto &detection : results) { + EXPECT_EQ(detection.bbox.size(), 4u); + for (const auto &point : detection.bbox) { + EXPECT_GE(point.x, 0.0f); + EXPECT_GE(point.y, 0.0f); + } + } +} + +TEST(VerticalOCRGenerateTests, JointCharsDetectionsHaveValidScores) { + VerticalOCR model(kValidVerticalDetectorPath, kValidVerticalRecognizerPath, + ENGLISH_SYMBOLS, false, createMockCallInvoker()); + auto results = model.generate(kValidVerticalTestImagePath); + + for (const auto &detection : results) { + EXPECT_GE(detection.score, 0.0f); + EXPECT_LE(detection.score, 1.0f); + } +} + +TEST(VerticalOCRGenerateTests, JointCharsDetectionsHaveNonEmptyText) { + VerticalOCR model(kValidVerticalDetectorPath, kValidVerticalRecognizerPath, + ENGLISH_SYMBOLS, false, createMockCallInvoker()); + auto results = model.generate(kValidVerticalTestImagePath); + + for (const auto &detection : results) { + EXPECT_FALSE(detection.text.empty()); + } +} + +// Strategy comparison tests +TEST(VerticalOCRStrategyTests, BothStrategiesRunSuccessfully) { + VerticalOCR independentModel(kValidVerticalDetectorPath, + kValidVerticalRecognizerPath, ENGLISH_SYMBOLS, + true, createMockCallInvoker()); + VerticalOCR jointModel(kValidVerticalDetectorPath, + kValidVerticalRecognizerPath, ENGLISH_SYMBOLS, false, + createMockCallInvoker()); + + EXPECT_NO_THROW((void)independentModel.generate(kValidVerticalTestImagePath)); + EXPECT_NO_THROW((void)jointModel.generate(kValidVerticalTestImagePath)); +} + +TEST(VerticalOCRStrategyTests, BothStrategiesReturnValidResults) { + VerticalOCR independentModel(kValidVerticalDetectorPath, + kValidVerticalRecognizerPath, ENGLISH_SYMBOLS, + true, createMockCallInvoker()); + VerticalOCR jointModel(kValidVerticalDetectorPath, + kValidVerticalRecognizerPath, ENGLISH_SYMBOLS, false, + createMockCallInvoker()); + + auto independentResults = + independentModel.generate(kValidVerticalTestImagePath); + auto jointResults = jointModel.generate(kValidVerticalTestImagePath); + + // Both should return some results (or none if no text detected) + EXPECT_GE(independentResults.size(), 0u); + EXPECT_GE(jointResults.size(), 0u); +} diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/VoiceActivityDetectionTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/integration/VoiceActivityDetectionTest.cpp new file mode 100644 index 000000000..5c19b74f6 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/integration/VoiceActivityDetectionTest.cpp @@ -0,0 +1,99 @@ +#include "BaseModelTests.h" +#include "utils/TestUtils.h" +#include +#include +#include + +using namespace rnexecutorch; +using namespace rnexecutorch::models::voice_activity_detection; +using namespace test_utils; +using namespace model_tests; + +constexpr auto kValidVadModelPath = "fsmn-vad_xnnpack.pte"; + +// ============================================================================ +// Common tests via typed test suite +// ============================================================================ +namespace model_tests { +template <> struct ModelTraits { + using ModelType = VoiceActivityDetection; + + static ModelType createValid() { + return ModelType(kValidVadModelPath, nullptr); + } + + static ModelType createInvalid() { + return ModelType("nonexistent.pte", nullptr); + } + + static void callGenerate(ModelType &model) { + auto audio = loadAudioFromFile("test_audio_float.raw"); + (void)model.generate(audio); + } +}; +} // namespace model_tests + +using VADTypes = ::testing::Types; +INSTANTIATE_TYPED_TEST_SUITE_P(VAD, CommonModelTest, VADTypes); + +// ============================================================================ +// Model-specific tests +// ============================================================================ +TEST(VADGenerateTests, SilenceReturnsNoSegments) { + VoiceActivityDetection model(kValidVadModelPath, nullptr); + auto silence = generateSilence(16000 * 5); + auto segments = model.generate(silence); + EXPECT_TRUE(segments.empty()); +} + +TEST(VADGenerateTests, SegmentsHaveValidBounds) { + VoiceActivityDetection model(kValidVadModelPath, nullptr); + auto audio = loadAudioFromFile("test_audio_float.raw"); + ASSERT_FALSE(audio.empty()); + auto segments = model.generate(audio); + + for (const auto &segment : segments) { + EXPECT_LE(segment.start, segment.end); + EXPECT_LE(segment.end, audio.size()); + } +} + +TEST(VADGenerateTests, SegmentsAreNonOverlapping) { + VoiceActivityDetection model(kValidVadModelPath, nullptr); + auto audio = loadAudioFromFile("test_audio_float.raw"); + ASSERT_FALSE(audio.empty()); + auto segments = model.generate(audio); + for (size_t i = 1; i < segments.size(); ++i) { + EXPECT_LE(segments[i - 1].end, segments[i].start); + } +} + +TEST(VADGenerateTests, LongAudioSegmentBoundsValid) { + VoiceActivityDetection model(kValidVadModelPath, nullptr); + auto audio = loadAudioFromFile("test_audio_float.raw"); + ASSERT_FALSE(audio.empty()); + auto segments = model.generate(audio); + + for (const auto &segment : segments) { + EXPECT_LE(segment.start, segment.end); + EXPECT_LE(segment.end, audio.size()); + } +} + +TEST(VADInheritedTests, GetInputShapeWorks) { + VoiceActivityDetection model(kValidVadModelPath, nullptr); + auto shape = model.getInputShape("forward", 0); + EXPECT_GE(shape.size(), 2u); +} + +TEST(VADInheritedTests, GetAllInputShapesWorks) { + VoiceActivityDetection model(kValidVadModelPath, nullptr); + auto shapes = model.getAllInputShapes("forward"); + EXPECT_FALSE(shapes.empty()); +} + +TEST(VADInheritedTests, GetMethodMetaWorks) { + VoiceActivityDetection model(kValidVadModelPath, nullptr); + auto result = model.getMethodMeta("forward"); + EXPECT_TRUE(result.ok()); +} diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/assets/test_audio_float.raw b/packages/react-native-executorch/common/rnexecutorch/tests/integration/assets/test_audio_float.raw new file mode 100644 index 000000000..177c7b891 Binary files /dev/null and b/packages/react-native-executorch/common/rnexecutorch/tests/integration/assets/test_audio_float.raw differ diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/assets/we_are_software_mansion.jpg b/packages/react-native-executorch/common/rnexecutorch/tests/integration/assets/we_are_software_mansion.jpg new file mode 100644 index 000000000..521416d00 Binary files /dev/null and b/packages/react-native-executorch/common/rnexecutorch/tests/integration/assets/we_are_software_mansion.jpg differ diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/stubs/jsi_stubs.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/integration/stubs/jsi_stubs.cpp new file mode 100644 index 000000000..39b8ae09c --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/integration/stubs/jsi_stubs.cpp @@ -0,0 +1,45 @@ +// Stub implementations for JSI and other symbols to satisfy the linker +// These are never actually called in tests + +#include +#include +#include +#include +#include +#include + +namespace facebook::jsi { + +// MutableBuffer destructor - needed by OwningArrayBuffer +MutableBuffer::~MutableBuffer() {} +Value::~Value() {} +Value::Value(Value &&other) noexcept {} +} // namespace facebook::jsi + +namespace facebook::react { + +// Needed by LLM test +class MockCallInvoker : public CallInvoker { +public: + void invokeAsync(CallFunc &&func) noexcept override {} + + void invokeSync(CallFunc &&func) override {} +}; + +} // namespace facebook::react + +namespace rnexecutorch { + +// Stub for fetchUrlFunc - used by ImageProcessing for remote URLs +// Tests only use local files, so this is never called +using FetchUrlFunc_t = std::function(std::string)>; +FetchUrlFunc_t fetchUrlFunc = [](std::string) -> std::vector { + return {}; +}; + +// Global mock call invoker for tests +std::shared_ptr createMockCallInvoker() { + return std::make_shared(); +} + +} // namespace rnexecutorch diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/integration/utils/TestUtils.h b/packages/react-native-executorch/common/rnexecutorch/tests/integration/utils/TestUtils.h new file mode 100644 index 000000000..1e0240815 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/integration/utils/TestUtils.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +namespace test_utils { + +inline void trimFilePrefix(std::string &filepath) { + constexpr std::string_view prefix = "file://"; + if (filepath.starts_with(prefix)) { + filepath.erase(0, prefix.size()); + } +} + +inline std::vector loadAudioFromFile(const std::string &filepath) { + std::ifstream file(filepath, std::ios::binary | std::ios::ate); + if (!file.is_open()) { + return {}; + } + + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + + size_t numSamples = size / sizeof(float); + std::vector buffer(numSamples); + + file.read(reinterpret_cast(buffer.data()), size); + return buffer; +} + +inline std::vector generateSilence(size_t numSamples) { + return std::vector(numSamples, 0.0f); +} + +} // namespace test_utils diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/run_tests.sh b/packages/react-native-executorch/common/rnexecutorch/tests/run_tests.sh new file mode 100755 index 000000000..58244ac27 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/run_tests.sh @@ -0,0 +1,326 @@ +#!/bin/bash +set -e + +# ============================================================================ +# Constants +# ============================================================================ +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACKAGE_ROOT="$SCRIPT_DIR/../../.." +ANDROID_ABI="arm64-v8a" +ANDROID_LIBS_DIR="$PACKAGE_ROOT/third-party/android/libs" +DEVICE_TEST_DIR="/data/local/tmp/rnexecutorch_tests" +MODELS_DIR="$SCRIPT_DIR/integration/assets/models" + +# ============================================================================ +# Test executables +# ============================================================================ +TEST_EXECUTABLES=( + "NumericalTests" + "LogTests" + "BaseModelTests" + "ClassificationTests" + "ObjectDetectionTests" + "ImageEmbeddingsTests" + "TextEmbeddingsTests" + "StyleTransferTests" + "VADTests" + "TokenizerModuleTests" + "SpeechToTextTests" + "LLMTests" + "ImageSegmentationTests" + "TextToImageTests" + "OCRTests" + "VerticalOCRTests" +) + +# ============================================================================ +# Test assets +# ============================================================================ +TEST_ASSETS=( + "integration/assets/test_audio_float.raw" + "integration/assets/we_are_software_mansion.jpg" +) + +# ============================================================================ +# Models to download (format: "filename|url") +# ============================================================================ +MODELS=( + "style_transfer_candy_xnnpack.pte|https://huggingface.co/software-mansion/react-native-executorch-style-transfer-candy/resolve/main/xnnpack/style_transfer_candy_xnnpack.pte" + "efficientnet_v2_s_xnnpack.pte|https://huggingface.co/software-mansion/react-native-executorch-efficientnet-v2-s/resolve/v0.6.0/xnnpack/efficientnet_v2_s_xnnpack.pte" + "ssdlite320-mobilenetv3-large.pte|https://huggingface.co/software-mansion/react-native-executorch-ssdlite320-mobilenet-v3-large/resolve/v0.6.0/ssdlite320-mobilenetv3-large.pte" + "test_image.jpg|https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Cat_November_2010-1a.jpg/1200px-Cat_November_2010-1a.jpg" + "clip-vit-base-patch32-vision_xnnpack.pte|https://huggingface.co/software-mansion/react-native-executorch-clip-vit-base-patch32/resolve/v0.6.0/clip-vit-base-patch32-vision_xnnpack.pte" + "all-MiniLM-L6-v2_xnnpack.pte|https://huggingface.co/software-mansion/react-native-executorch-all-MiniLM-L6-v2/resolve/v0.6.0/all-MiniLM-L6-v2_xnnpack.pte" + "tokenizer.json|https://huggingface.co/software-mansion/react-native-executorch-all-MiniLM-L6-v2/resolve/v0.6.0/tokenizer.json" + "fsmn-vad_xnnpack.pte|https://huggingface.co/software-mansion/react-native-executorch-fsmn-vad/resolve/main/xnnpack/fsmn-vad_xnnpack.pte" + "whisper_tiny_en_encoder_xnnpack.pte|https://huggingface.co/software-mansion/react-native-executorch-whisper-tiny.en/resolve/main/xnnpack/whisper_tiny_en_encoder_xnnpack.pte" + "whisper_tiny_en_decoder_xnnpack.pte|https://huggingface.co/software-mansion/react-native-executorch-whisper-tiny.en/resolve/main/xnnpack/whisper_tiny_en_decoder_xnnpack.pte" + "whisper_tokenizer.json|https://huggingface.co/software-mansion/react-native-executorch-whisper-tiny.en/resolve/v0.6.0/tokenizer.json" + "smolLm2_135M_8da4w.pte|https://huggingface.co/software-mansion/react-native-executorch-smolLm-2/resolve/v0.6.0/smolLm-2-135M/quantized/smolLm2_135M_8da4w.pte" + "smollm_tokenizer.json|https://huggingface.co/software-mansion/react-native-executorch-smolLm-2/resolve/v0.6.0/tokenizer.json" + "deeplabV3_xnnpack_fp32.pte|https://huggingface.co/software-mansion/react-native-executorch-deeplab-v3/resolve/v0.6.0/xnnpack/deeplabV3_xnnpack_fp32.pte" + "xnnpack_crnn_english.pte|https://huggingface.co/software-mansion/react-native-executorch-recognizer-crnn.en/resolve/v0.7.0/xnnpack/english/xnnpack_crnn_english.pte" + "xnnpack_craft_quantized.pte|https://huggingface.co/software-mansion/react-native-executorch-detector-craft/resolve/v0.7.0/xnnpack/xnnpack_craft.pte" + "t2i_tokenizer.json|https://huggingface.co/software-mansion/react-native-executorch-bk-sdm-tiny/resolve/v0.6.0/tokenizer/tokenizer.json" + "t2i_encoder.pte|https://huggingface.co/software-mansion/react-native-executorch-bk-sdm-tiny/resolve/v0.6.0/text_encoder/model.pte" + "t2i_unet.pte|https://huggingface.co/software-mansion/react-native-executorch-bk-sdm-tiny/resolve/v0.6.0/unet/model.256.pte" + "t2i_decoder.pte|https://huggingface.co/software-mansion/react-native-executorch-bk-sdm-tiny/resolve/v0.6.0/vae/model.256.pte" +) + +# ============================================================================ +# Libraries to push +# ============================================================================ +REQUIRED_LIBS=( + "$ANDROID_LIBS_DIR/executorch/$ANDROID_ABI/libexecutorch.so:libexecutorch_jni.so" + "$ANDROID_LIBS_DIR/pthreadpool/$ANDROID_ABI/libpthreadpool.so:libpthreadpool.so" + "$ANDROID_LIBS_DIR/cpuinfo/$ANDROID_ABI/libcpuinfo.so:libcpuinfo.so" +) + +# Dynamically find libfbjni.so and libc++_shared.so from CMake builds +# These are built by other native modules (e.g., react-native-reanimated, react-native-skia) +MONOREPO_ROOT="$PACKAGE_ROOT/../../.." + +LIBFBJNI_PATH=$(find "$MONOREPO_ROOT" -path "*/android/build/intermediates/cmake/*/obj/$ANDROID_ABI/libfbjni.so" -type f 2>/dev/null | head -1) +LIBCPP_PATH=$(find "$MONOREPO_ROOT" -path "*/android/build/intermediates/cmake/*/obj/$ANDROID_ABI/libc++_shared.so" -type f 2>/dev/null | head -1) + +if [ -z "$LIBFBJNI_PATH" ]; then + echo "Error: libfbjni.so not found in monorepo." + echo "Please build an app first: cd apps/computer-vision/android && ./gradlew assembleRelease" + exit 1 +fi + +if [ -z "$LIBCPP_PATH" ]; then + echo "Error: libc++_shared.so not found in monorepo." + echo "Please build an app first: cd apps/computer-vision/android && ./gradlew assembleRelease" + exit 1 +fi + +GRADLE_LIBS=( + "$LIBFBJNI_PATH:libfbjni.so" + "$LIBCPP_PATH:libc++_shared.so" +) + +# ============================================================================ +# Flags +# ============================================================================ +REFRESH_MODELS=false +SKIP_BUILD=false + +# ============================================================================ +# Functions +# ============================================================================ + +print_usage() { + cat <&2 +} + +download_if_needed() { + local url="$1" + local output="$2" + local filepath="$MODELS_DIR/$output" + + if [ "$REFRESH_MODELS" = true ] || [ ! -f "$filepath" ]; then + log "Downloading $output..." + wget -q -O "$filepath" "$url" + else + log "$output already exists, skipping" + fi +} + +push_file() { + local src="$1" + local dest="$2" + + if [ -f "$src" ]; then + adb push "$src" "$dest" >/dev/null + else + error "File not found: $src" + fi +} + +run_test() { + local test_exe="$1" + + if adb shell "[ -f $DEVICE_TEST_DIR/$test_exe ]"; then + log "Running $test_exe" + if ! adb shell "cd $DEVICE_TEST_DIR && LD_LIBRARY_PATH=. ./$test_exe --gtest_color=yes"; then + return 1 + fi + fi + return 0 +} + +# ============================================================================ +# Parse arguments +# ============================================================================ +for arg in "$@"; do + case $arg in + --refresh-models) + REFRESH_MODELS=true + shift + ;; + --skip-build) + SKIP_BUILD=true + shift + ;; + --help) + print_usage + exit 0 + ;; + *) + error "Unknown option: $arg" + print_usage + exit 1 + ;; + esac +done + +# ============================================================================ +# Validate environment +# ============================================================================ +if ! adb shell ":"; then + error "ADB shell couldn't run successfully" + exit 1 +fi + +if [ -z "$ANDROID_NDK" ]; then + error "ANDROID_NDK is not set" + exit 1 +fi + +log "ANDROID_NDK = $ANDROID_NDK" + +# ============================================================================ +# Build tests +# ============================================================================ +if [ "$SKIP_BUILD" = false ]; then + log "Building tests..." + rm -rf build + mkdir build + cd build + + cmake .. \ + -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \ + -DANDROID_ABI=$ANDROID_ABI \ + -DANDROID_PLATFORM=android-34 \ + -DANDROID_STL=c++_shared + + make +else + if ! [ -d build ]; then + error "Build was skipped and the build directory doesn't exist" + exit 1 + fi + log "Skipping build..." + cd build +fi + +# ============================================================================ +# Prepare device +# ============================================================================ +log "Creating device test directory..." +adb shell "mkdir -p $DEVICE_TEST_DIR" + +# ============================================================================ +# Push test executables +# ============================================================================ +log "Pushing test executables to device..." +for test_exe in "${TEST_EXECUTABLES[@]}"; do + if [ -f "$test_exe" ]; then + push_file "$test_exe" "$DEVICE_TEST_DIR/" + adb shell "chmod +x $DEVICE_TEST_DIR/$test_exe" + fi +done + +# ============================================================================ +# Push test assets +# ============================================================================ +log "Pushing test assets to device..." +for asset in "${TEST_ASSETS[@]}"; do + push_file "../$asset" "$DEVICE_TEST_DIR/" +done + +# ============================================================================ +# Download models +# ============================================================================ +log "Downloading models (use --refresh-models to force re-download)..." +mkdir -p "$MODELS_DIR" + +for entry in "${MODELS[@]}"; do + IFS='|' read -r filename url <<<"$entry" + download_if_needed "$url" "$filename" +done + +# ============================================================================ +# Push models +# ============================================================================ +log "Pushing models to device..." +for model in "$MODELS_DIR"/*; do + if [ -f "$model" ]; then + push_file "$model" "$DEVICE_TEST_DIR/" + fi +done + +# ============================================================================ +# Push libraries +# ============================================================================ +log "Pushing shared libraries to device..." + +for lib_entry in "${REQUIRED_LIBS[@]}"; do + IFS=':' read -r src dest <<<"$lib_entry" + if [ -f "$src" ]; then + push_file "$src" "$DEVICE_TEST_DIR/$dest" + fi +done + +for lib_entry in "${GRADLE_LIBS[@]}"; do + IFS=':' read -r src dest <<<"$lib_entry" + if [ -f "$src" ]; then + push_file "$src" "$DEVICE_TEST_DIR/" + else + error "Library not found: $src" + fi +done + +# ============================================================================ +# Run tests +# ============================================================================ +log "Running tests on device..." +FAILED=0 +for test_exe in "${TEST_EXECUTABLES[@]}"; do + if ! run_test "$test_exe"; then + FAILED=1 + fi +done + +# ============================================================================ +# Cleanup +# ============================================================================ +log "Cleaning up device..." +adb shell "rm -rf $DEVICE_TEST_DIR" + +cd .. + +if [ $FAILED -eq 0 ]; then + log "All tests passed!" +else + error "Some tests failed" +fi + +exit $FAILED diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/FileUtilsTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/unit/FileUtilsTest.cpp similarity index 91% rename from packages/react-native-executorch/common/rnexecutorch/tests/FileUtilsTest.cpp rename to packages/react-native-executorch/common/rnexecutorch/tests/unit/FileUtilsTest.cpp index ce632212c..ed9d80236 100644 --- a/packages/react-native-executorch/common/rnexecutorch/tests/FileUtilsTest.cpp +++ b/packages/react-native-executorch/common/rnexecutorch/tests/unit/FileUtilsTest.cpp @@ -17,9 +17,7 @@ class FileIOTest : public ::testing::Test { out.close(); } - void TearDown() override { - std::remove(tempFileName.c_str()); - } + void TearDown() override { std::remove(tempFileName.c_str()); } }; TEST_F(FileIOTest, LoadBytesFromFileSuccessfully) { diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/LogTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/unit/LogTest.cpp similarity index 99% rename from packages/react-native-executorch/common/rnexecutorch/tests/LogTest.cpp rename to packages/react-native-executorch/common/rnexecutorch/tests/unit/LogTest.cpp index 18462c324..1fb2f3fdf 100644 --- a/packages/react-native-executorch/common/rnexecutorch/tests/LogTest.cpp +++ b/packages/react-native-executorch/common/rnexecutorch/tests/unit/LogTest.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/NumericalTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/unit/NumericalTest.cpp similarity index 91% rename from packages/react-native-executorch/common/rnexecutorch/tests/NumericalTest.cpp rename to packages/react-native-executorch/common/rnexecutorch/tests/unit/NumericalTest.cpp index 050c9579a..fa7f8cfdd 100644 --- a/packages/react-native-executorch/common/rnexecutorch/tests/NumericalTest.cpp +++ b/packages/react-native-executorch/common/rnexecutorch/tests/unit/NumericalTest.cpp @@ -1,15 +1,16 @@ #include "../data_processing/Numerical.h" #include #include +#include #include -#include #include namespace rnexecutorch::numerical { // Helper function to check if two float vectors are approximately equal void expect_vectors_eq(const std::vector &vector1, - const std::vector &vector2, float atol = 1.0e-6F) { + const std::vector &vector2, + float atol = 1.0e-6F) { ASSERT_EQ(vector1.size(), vector2.size()); for (size_t i = 0; i < vector1.size(); i++) { EXPECT_NEAR(vector1[i], vector2[i], atol); @@ -93,18 +94,14 @@ TEST(MeanPoolingTests, InvalidDimensionSize) { const std::vector modelOutput = {1.0F, 2.0F, 3.0F, 4.0F}; const std::vector attnMask = {1, 1, 1}; - EXPECT_THROW( - { meanPooling(modelOutput, attnMask); }, - std::invalid_argument); + EXPECT_THROW({ meanPooling(modelOutput, attnMask); }, RnExecutorchError); } TEST(MeanPoolingTests, EmptyAttentionMask) { const std::vector modelOutput = {1.0F, 2.0F, 3.0F, 4.0F}; const std::vector attnMask = {}; - EXPECT_THROW( - { meanPooling(modelOutput, attnMask); }, - std::invalid_argument); + EXPECT_THROW({ meanPooling(modelOutput, attnMask); }, RnExecutorchError); } } // namespace rnexecutorch::numerical diff --git a/packages/react-native-executorch/common/runner/runner.cpp b/packages/react-native-executorch/common/runner/runner.cpp index 12b0d5fa6..3b24d6ec2 100644 --- a/packages/react-native-executorch/common/runner/runner.cpp +++ b/packages/react-native-executorch/common/runner/runner.cpp @@ -268,7 +268,9 @@ Error Runner::generate(const std::string &prompt, ET_LOG(Info, "Warmup run finished!"); } else { // Do not print report during warmup +#ifndef TEST_BUILD ::executorch::llm::print_report(stats_); +#endif } if (stats_callback) { stats_callback(stats_);