diff --git a/CMakeLists.txt b/CMakeLists.txt index f8ea764..df9e033 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,11 @@ -CMAKE_MINIMUM_REQUIRED(VERSION 2.6) +CMAKE_MINIMUM_REQUIRED(VERSION 3.10) PROJECT(hal-backend-ml CXX) INCLUDE(GNUInstallDirs) option(ENABLE_DUMMY "Enable dummy-passthrough backend" OFF) option(ENABLE_VIVANTE "Enable vivante backend" OFF) option(ENABLE_SNPE "Enable snpe backend" OFF) +option(BUILD_TESTS "Build unit tests" OFF) SET(HAL_LIBDIR ${CMAKE_HAL_LIBDIR_PREFIX}) SET(HAL_LICENSEDIR ${CMAKE_HAL_LICENSEDIR_PREFIX}) @@ -23,7 +24,7 @@ SET(VERSION_FLAGS "-DVERSION='\"${VERSION}\"' -DVERSION_MAJOR=${VERSION_MAJOR} - INCLUDE(FindPkgConfig) # Common Options -SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -O2 -fomit-frame-pointer -std=gnu++0x") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -O2 -fomit-frame-pointer -std=c++14") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdata-sections -ffunction-sections") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${VERSION_FLAGS}") @@ -98,3 +99,7 @@ ADD_LIBRARY(${SNPE_LIBRARY_NAME} SHARED ${SNPE_SRCS} ${UTIL_SRCS}) TARGET_LINK_LIBRARIES(${SNPE_LIBRARY_NAME} ${snpe_build_dep_pkgs_LDFLAGS}) INSTALL(TARGETS ${SNPE_LIBRARY_NAME} DESTINATION ${HAL_LIBDIR} COMPONENT RuntimeLibraries) ENDIF() + +IF(BUILD_TESTS) + ADD_SUBDIRECTORY(test) +ENDIF() diff --git a/README.md b/README.md index 7b8a017..cedbab4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # HAL ML Accelerator Backends -This document provides an overview of the available Machine Learning (ML) accelerator backends within this project. It details their purpose, configuration, and specific custom properties that can be used to tailor their behavior. +This document provides an overview of the available Machine Learning (ML) accelerator backends within this project. It details their purpose, configuration, specific custom properties, and testing setup. ## 1. Vivante Backend (`ml-vivante`) @@ -14,7 +14,7 @@ There are two options: 1. model.nb + model.json 2. model.nb + model.so -- **Model Files (`prop->model_files`):** +- **Model Files (`prop->model_files`):** - `model_files[0]`: Path to the Vivante model file (e.g., `my_model.nb`). This file contains the neural network graph and weights. - (Optional) `model_files[1]`: Path to the Vivante shared object file (e.g., `vnn_my_model.so`). This library provides functions like `vnn_CreateNeuralNetwork` and `vnn_ReleaseNeuralNetwork`. @@ -22,11 +22,12 @@ Note that if the shared object file is not provided, the backend attempts to loa ### Custom Properties (`prop->custom_properties`) -- **`json`**: +- **`json`**: - **Description:** Path to the JSON file for the given model file. - **Key:** `json` - **Value:** Path to the JSON file. - **Example:** `json:/path/to/my_model.json` + ```json // yolo-v8m.json { @@ -78,31 +79,32 @@ Note that if the shared object file is not provided, the backend attempts to loa ### Configuration -- **Model Files (`prop->model_files`):** +- **Model Files (`prop->model_files`):** - `model_files[0]`: Path to the SNPE model file (e.g., `my_model.dlc`). ### Custom Properties (`prop->custom_properties`) Custom properties for the SNPE backend are provided as a single string, with individual `key:value` pairs separated by commas. + **Format:** `key1:value1,key2:value2,key3:value3a;value3b` -- **`Runtime`**: +- **`Runtime`**: - **Description:** Specifies the preferred SNPE runtime target for model execution. - **Key:** `Runtime` - - **Value:** + - **Value:** - `CPU`: Use CPU runtime. - `GPU`: Use GPU runtime. - `DSP`: Use DSP runtime. - `NPU` or `AIP`: Use NPU/AIP runtime (specifically maps to `SNPE_RUNTIME_AIP_FIXED8_TF`). - **Example:** `Runtime:DSP` -- **`OutputTensor`**: +- **`OutputTensor`**: - **Description:** Specifies the names of the output tensors the application wishes to retrieve. If not provided, the backend uses all default output tensors defined in the model. - **Key:** `OutputTensor` - **Value:** A semicolon-separated list of output tensor names. Tensor names themselves can include colons. - **Example:** `OutputTensor:detection_scores:0;detection_classes:0;raw_outputs/box_encodings` -- **`OutputType`**: +- **`OutputType`**: - **Description:** Specifies the desired data types for the output tensors. The order of types in the list should correspond to the order of output tensors (either the default order or the order specified by the `OutputTensor` property). - **Key:** `OutputType` - **Value:** A semicolon-separated list of data types. @@ -110,7 +112,7 @@ Custom properties for the SNPE backend are provided as a single string, with ind - `TF8`: Output tensor data type will be 8-bit quantized (typically `uint8_t`). - **Example:** `OutputType:FLOAT32;TF8` (assuming two output tensors, the first as float32, the second as TF8) -- **`InputType`**: +- **`InputType`**: - **Description:** Specifies the data types for the input tensors. The order of types in the list should correspond to the order of input tensors as defined in the model. - **Key:** `InputType` - **Value:** A semicolon-separated list of data types. @@ -121,3 +123,133 @@ Custom properties for the SNPE backend are provided as a single string, with ind ### Example `custom_properties` String for SNPE: `"Runtime:DSP,OutputTensor:my_output_tensor1;my_output_tensor2,OutputType:FLOAT32;FLOAT32,InputType:TF8"` + +### Input Data Files for Testing + +For testing purposes, you can specify raw input data files to load instead of using zero-filled buffers. This is configured in the JSON config file under the `configParameters` object: + +- **`input_file`**: + - **Description:** Array of paths to raw binary input data files. One file per input tensor. + - **Format:** Array of file paths + - **Example:** `["sample_raw_0", "sample_raw_1"]` (for multi-input models) + - **Example:** `["bus_416x416_float.raw"]` (for single-input models) + +**Example JSON configuration with input files:** +```json +{ + + "metadata": { + "configParameters": [ + { + "fwname": "tizen-hal", + "fw_opened": 0, + "num_models": 1, + "model_files": ["inception-v3-99.nb"], + "input_file": ["pizza_3x299x299_uint8.raw"], + "input_configured": 0, + "output_configured": 0, + "custom_properties": "backend:vivante,json:inception-v3-99.json" + } + + ] + + } + + } + +``` + +The raw binary files should contain tensor data in the model's expected format (e.g., float32, uint8) with the exact size matching the input tensor dimensions. If input files are not provided, the test will use zero-filled buffers. + +## 3. Testing with GTest + +The project includes a comprehensive testing framework using Google Test (GTest) to validate backend functionality. + +### Test Structure + +The test setup consists of the following components: + +- **`test/main.cpp`**: Main test entry point that initializes GTest and manages the global properties structure. +- **`test/hal_backend_ml_vivante_test.cpp`**: Test cases for the Vivante backend. +- **`test/hal_backend_ml_snpe_test.cpp`**: Test cases for the SNPE backend. +- **`test/hal_backend_ml_dummy_passthrough_test.cpp`**: Test cases for the dummy passthrough backend. +- **`test/hal_backend_ml_test_util.cpp`**: Utility functions used by tests. +- **`test/hal_backend_ml_test_util.h`**: Header file for test utilities. +- **`test/hal_backend_ml_test_wrapper.h`**: Wrapper functions for backend APIs. + +### Building Tests + +Tests are built when the corresponding backend is enabled and BUILD_TESTS option is set to ON: + +**For Vivante tests:** +```bash +cmake -DENABLE_VIVANTE=ON -DBUILD_TESTS=ON . +make hal-backend-ml-vivante-test +``` + +**For SNPE tests:** +```bash +cmake -DENABLE_SNPE=ON -DBUILD_TESTS=ON . +make hal-backend-ml-snpe-test +``` + +**For Dummy Passthrough tests:** +```bash +cmake -DENABLE_DUMMY=ON -DBUILD_TESTS=ON . +make hal-backend-ml-dummy-passthrough-test +``` + +The test executables are linked against: +- `libgtest.so` - Google Test framework +- `libgtest_main.so` - Provides main() function for tests +- Backend-specific library (`libhal-backend-ml-vivante.so`, `libhal-backend-ml-snpe.so`, or `libhal-backend-ml-dummy-passthrough.so`) +- `pthread` - Threading support + +### Running Tests + +The test executables require a JSON configuration file path as a command-line argument: + +```bash +# Run Vivante backend tests +/hal/bin/ml-accelerator/hal-backend-ml-vivante-test /path/to/model_config.json + +# Run SNPE backend tests +/hal/bin/ml-accelerator/hal-backend-ml-snpe-test /path/to/model_config.json + +# Run Dummy Passthrough backend tests +/hal/bin/ml-accelerator/hal-backend-ml-dummy-passthrough-test /path/to/model_config.json +``` + +### Test Configuration + +The JSON configuration file should contain: +- Model file paths +- Input tensor specifications +- Output tensor specifications +- Quantization parameters +- Any backend-specific custom properties + +### Building Dependencies + +To build with GTest support, ensure the following dependencies are installed: + +**On Tizen systems:** + +You should set `build_tests` as `1` in gbs to enable test: +```bash +gbs build {your_gbs_options} --define "build_tests 1" +``` + +The build system automatically includes GTest support when building test executables via the CMake configuration in [`test/CMakeLists.txt`](./test/CMakeLists.txt). + +### Test Installations + +When building with tests enabled, test binaries are installed to: +``` +/hal/bin/ml-accelerator/ +``` + +For example: +- Vivante test: `/hal/bin/ml-accelerator/hal-backend-ml-vivante-test` +- SNPE test: `/hal/bin/ml-accelerator/hal-backend-ml-snpe-test` +- Dummy Passthrough test: `/hal/bin/ml-accelerator/hal-backend-ml-dummy-passthrough-test` diff --git a/packaging/hal-backend-ml-accelerator.spec b/packaging/hal-backend-ml-accelerator.spec index 17f0c28..547c044 100644 --- a/packaging/hal-backend-ml-accelerator.spec +++ b/packaging/hal-backend-ml-accelerator.spec @@ -9,6 +9,10 @@ Group: Machine Learning/ML Framework License: Apache-2.0 Source0: %{name}-%{version}.tar.gz +%define _module_name hal-backend-ml-accelerator +%define _module_name_snpe hal-backend-ml-snpe +%define _module_name_vivante hal-backend-ml-vivante +%define _module_name_dummypassthrough hal-backend-ml-dummy-passthrough BuildRequires: cmake BuildRequires: pkgconfig(hal-rootstrap) @@ -27,6 +31,9 @@ BuildRequires: pkgconfig(hal-rootstrap) %endif # For DA +%if 0%{?build_tests} +BuildRequires: gtest-devel +%endif %description ML HAL backend drivers for various targets @@ -57,6 +64,30 @@ Summary: hal-backend-ml-accelerator for snpe %define enable_snpe -DENABLE_SNPE=ON %endif +%if 0%{?build_tests} +%define _testdir %{_hal_bindir}/ml-accelerator/ +%define enable_tests -DBUILD_TESTS=ON -DTEST_DIR=%{_testdir} + +%package halbackendtest +Summary: Test Binary for Hal backend +Requires: %{name} = %{version}-%{release} +%description halbackendtest +Test Binary for hal-backend + +%files halbackendtest +%manifest packaging/hal-backend-ml-accelerator.manifest +%if 0%{?dummy_support} +%{_testdir}%{_module_name_dummypassthrough}-test +%endif +%if 0%{?vivante_support} +%{_testdir}%{_module_name_vivante}-test +%endif +%if 0%{?snpe_support} +%{_testdir}%{_module_name_snpe}-test +%endif +%else +%define enable_tests -DBUILD_TESTS=OFF +%endif %prep %setup -q @@ -68,8 +99,8 @@ Summary: hal-backend-ml-accelerator for snpe %{?enable_dummy} \ %{?enable_vivante} \ %{?enable_snpe} \ + %{?enable_tests} \ . - make %{?_smp_mflags} %install diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..005146f --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,54 @@ +# Check if we're in TEST mode +IF(NOT DEFINED TEST_DIR) + SET(TEST_DIR "/hal/bin/ml-accelerator/") +ENDIF() + +# Test installation directory +SET(TEST_INSTALL_DIR "${TEST_DIR}") + +INCLUDE_DIRECTORIES(${PROJECT_SOURCE_DIR}/src) +INCLUDE_DIRECTORIES(${PROJECT_SOURCE_DIR}/gtest) + +SET(PROJECT_NAME_FULL "hal-backend-ml-accelerator") +SET(PROJECT_NAME_SNPE "hal-backend-ml-snpe") +SET(PROJECT_NAME_VIVANTE "hal-backend-ml-vivante") +SET(PROJECT_NAME_DUMMY "hal-backend-ml-dummy-passthrough") + +# Common test sources +SET(COMMON_TEST_SRCS + main.cpp + hal_backend_ml_test_util.cc +) + +# Vivante tests +IF(ENABLE_VIVANTE) +ADD_EXECUTABLE(${VIVANTE_LIBRARY_NAME}-test + ${COMMON_TEST_SRCS} + ${CMAKE_CURRENT_SOURCE_DIR}/hal_backend_ml_vivante_test.cc +) +TARGET_LINK_LIBRARIES(${PROJECT_NAME_VIVANTE}-test libgtest.so libgtest_main.so -pthread) +TARGET_LINK_LIBRARIES(${VIVANTE_LIBRARY_NAME}-test ${VIVANTE_LIBRARY_NAME} ${TEST_PKGS_LDFLAGS} -lpthread) +INSTALL(TARGETS ${PROJECT_NAME_VIVANTE}-test RUNTIME DESTINATION ${TEST_INSTALL_DIR}) +ENDIF() + +# SNPE tests +IF(ENABLE_SNPE) +ADD_EXECUTABLE(${SNPE_LIBRARY_NAME}-test + ${COMMON_TEST_SRCS} + ${CMAKE_CURRENT_SOURCE_DIR}/hal_backend_ml_snpe_test.cc +) +TARGET_LINK_LIBRARIES(${PROJECT_NAME_SNPE}-test libgtest.so libgtest_main.so -pthread) +TARGET_LINK_LIBRARIES(${PROJECT_NAME_SNPE}-test ${SNPE_LIBRARY_NAME} ${TEST_PKGS_LDFLAGS} -lpthread) +INSTALL(TARGETS ${PROJECT_NAME_SNPE}-test RUNTIME DESTINATION ${TEST_INSTALL_DIR}) +ENDIF() + +# Dummy Passthrough tests +IF(ENABLE_DUMMY) +ADD_EXECUTABLE(${PROJECT_NAME_DUMMY}-test + ${COMMON_TEST_SRCS} + ${CMAKE_CURRENT_SOURCE_DIR}/hal_backend_ml_dummy_passthrough_test.cc +) +TARGET_LINK_LIBRARIES(${PROJECT_NAME_DUMMY}-test libgtest.so libgtest_main.so -pthread) +TARGET_LINK_LIBRARIES(${PROJECT_NAME_DUMMY}-test ${DUMMY_PASSTHROUGH_LIBRARY_NAME} ${TEST_PKGS_LDFLAGS} -lpthread) +INSTALL(TARGETS ${PROJECT_NAME_DUMMY}-test RUNTIME DESTINATION ${TEST_INSTALL_DIR}) +ENDIF() diff --git a/test/hal_backend_ml_dummy_passthrough_test.cc b/test/hal_backend_ml_dummy_passthrough_test.cc new file mode 100644 index 0000000..2040b00 --- /dev/null +++ b/test/hal_backend_ml_dummy_passthrough_test.cc @@ -0,0 +1,214 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +#define TESTING 1 +#include +#include +#include +#include +#include +#include "hal-backend-ml-util.h" +#include "hal_backend_ml_test_util.h" +#include "hal-backend-ml-util.cc" +#include "hal_backend_ml_test_wrapper.h" +#include "hal-backend-ml-dummy-passthrough.cc" + +// =================================================================== +// Basic Lifecycle Tests +// =================================================================== + +TEST_F(MLBackendTest, DummyPassthrough_InitAndExit) { + void* hal_data = nullptr; + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_init(&hal_data)); + EXPECT_NE(hal_data, nullptr); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_deinit(hal_data)); +} + +TEST_F(MLBackendTest, DummyPassthrough_configure_instance) { + void* hal_data = nullptr; + TestGstTensorFilterProperties* test_config = get_test_config(); + ASSERT_NE(test_config, nullptr) << "Test configuration not initialized"; + + // Initialize the backend + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_init(&hal_data)); + EXPECT_NE(hal_data, nullptr); + + // Configure the instance with properties + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_configure_instance(hal_data, &test_config->base)); + + // Deinitialize the backend + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_deinit(hal_data)); +} + +TEST_F(MLBackendTest, DummyPassthrough_inference) { + void* hal_data = nullptr; + GstTensorMemory input[NNS_TENSOR_MEMORY_MAX] = {0}; + GstTensorMemory output[NNS_TENSOR_MEMORY_MAX] = {0}; + GstTensorsInfo in_info = {0}; + GstTensorsInfo out_info = {0}; + TestGstTensorFilterProperties* test_config = get_test_config(); + ASSERT_NE(test_config, nullptr) << "Test configuration not initialized"; + + // Initialize the backend + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_init(&hal_data)); + ASSERT_NE(hal_data, nullptr); + + // Configure the instance with properties + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_configure_instance(hal_data, &test_config->base)); + + // Get input/output tensor info + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_get_model_info(hal_data, GET_IN_OUT_INFO, &in_info, &out_info)); + + // Allocate and load input/output buffers + allocate_and_load_test_buffers(input, output, &in_info, &out_info, test_config); + + // Perform inference + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_invoke(hal_data, input, output)); + + // Free input and output buffers + free_test_buffers(input, output, &in_info, &out_info); + + // Free tensor info memory + gst_tensors_info_free(&in_info); + gst_tensors_info_free(&out_info); + + // Deinitialize the backend + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_deinit(hal_data)); +} + +// =================================================================== +// Framework Info Tests +// =================================================================== + +TEST_F(MLBackendTest, DummyPassthrough_get_framework_info) { + void* hal_data = nullptr; + GstTensorFilterFrameworkInfo fw_info = {0}; + + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_init(&hal_data)); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_get_framework_info(hal_data, &fw_info)); + EXPECT_STREQ("dummy-passthrough", fw_info.name); + EXPECT_FALSE(fw_info.allow_in_place); + EXPECT_FALSE(fw_info.allocate_in_invoke); + EXPECT_FALSE(fw_info.run_without_model); + EXPECT_FALSE(fw_info.verify_model_path); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_deinit(hal_data)); +} + +// =================================================================== +// Model Info Tests +// =================================================================== + +TEST_F(MLBackendTest, DummyPassthrough_get_model_info) { + void* hal_data = nullptr; + GstTensorsInfo in_info = {0}; + GstTensorsInfo out_info = {0}; + TestGstTensorFilterProperties* test_config = get_test_config(); + ASSERT_NE(test_config, nullptr) << "Test configuration not initialized"; + + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_init(&hal_data)); + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_configure_instance(hal_data, &test_config->base)); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_get_model_info(hal_data, GET_IN_OUT_INFO, &in_info, &out_info)); + EXPECT_GT(in_info.num_tensors, 0); + EXPECT_GT(out_info.num_tensors, 0); + + // Free tensor info memory + gst_tensors_info_free(&in_info); + gst_tensors_info_free(&out_info); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_deinit(hal_data)); +} + +// =================================================================== +// Event Handler Tests +// =================================================================== + +TEST_F(MLBackendTest, DummyPassthrough_event_handler_not_supported) { + void* hal_data = nullptr; + + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_init(&hal_data)); + + EXPECT_EQ(HAL_ML_ERROR_NOT_SUPPORTED, ml_dummy_passthrough_event_handler(hal_data, 0, nullptr)); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_deinit(hal_data)); +} + +// =================================================================== +// Error Handling Tests - NULL Parameters +// =================================================================== + +TEST(DummyPassthroughTest, DeinitWithNullParameter) { + EXPECT_EQ(HAL_ML_ERROR_INVALID_PARAMETER, ml_dummy_passthrough_deinit(nullptr)); +} + +TEST(DummyPassthroughTest, ConfigureInstanceWithNullBackend) { + GstTensorFilterProperties test_prop = {0}; + EXPECT_EQ(HAL_ML_ERROR_INVALID_PARAMETER, ml_dummy_passthrough_configure_instance(nullptr, &test_prop)); +} + +TEST(DummyPassthroughTest, ConfigureInstanceWithNullProperties) { + void* hal_data = nullptr; + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_init(&hal_data)); + + EXPECT_EQ(HAL_ML_ERROR_INVALID_PARAMETER, ml_dummy_passthrough_configure_instance(hal_data, nullptr)); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_deinit(hal_data)); +} + + +TEST(DummyPassthroughTest, InvokeWithNullBackend) { + GstTensorMemory input[NNS_TENSOR_MEMORY_MAX] = {0}; + GstTensorMemory output[NNS_TENSOR_MEMORY_MAX] = {0}; + + EXPECT_EQ(HAL_ML_ERROR_INVALID_PARAMETER, ml_dummy_passthrough_invoke(nullptr, input, output)); +} + + +TEST(DummyPassthroughTest, GetModelInfoWithNullBackend) { + GstTensorsInfo in_info = {0}; + GstTensorsInfo out_info = {0}; + + EXPECT_EQ(HAL_ML_ERROR_INVALID_PARAMETER, + ml_dummy_passthrough_get_model_info(nullptr, GET_IN_OUT_INFO, &in_info, &out_info)); +} + +TEST(DummyPassthroughTest, GetFrameworkInfoWithNullBackend) { + GstTensorFilterFrameworkInfo fw_info = {0}; + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_get_framework_info(nullptr, &fw_info)); + EXPECT_STREQ("dummy-passthrough", fw_info.name); +} + +// =================================================================== +// Multiple Inference Tests +// =================================================================== + +TEST_F(MLBackendTest, DummyPassthrough_multiple_inferences) { + void* hal_data = nullptr; + GstTensorMemory input[NNS_TENSOR_MEMORY_MAX] = {0}; + GstTensorMemory output[NNS_TENSOR_MEMORY_MAX] = {0}; + GstTensorsInfo in_info = {0}; + GstTensorsInfo out_info = {0}; + TestGstTensorFilterProperties* test_config = get_test_config(); + ASSERT_NE(test_config, nullptr) << "Test configuration not initialized"; + + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_init(&hal_data)); + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_configure_instance(hal_data, &test_config->base)); + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_get_model_info(hal_data, GET_IN_OUT_INFO, &in_info, &out_info)); + + // Perform multiple inferences + for (int i = 0; i < 3; i++) { + allocate_and_load_test_buffers(input, output, &in_info, &out_info, test_config); + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_invoke(hal_data, input, output)); + free_test_buffers(input, output, &in_info, &out_info); + } + + // Free tensor info memory + gst_tensors_info_free(&in_info); + gst_tensors_info_free(&out_info); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_dummy_passthrough_deinit(hal_data)); +} diff --git a/test/hal_backend_ml_snpe_test.cc b/test/hal_backend_ml_snpe_test.cc new file mode 100644 index 0000000..9ad3a22 --- /dev/null +++ b/test/hal_backend_ml_snpe_test.cc @@ -0,0 +1,262 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +#define TESTING 1 +#include +#include +#include +#include +#include +#include "hal-backend-ml-util.h" +#include "hal_backend_ml_test_util.h" +#include "hal-backend-ml-util.cc" +#include "hal_backend_ml_test_wrapper.h" +#include "hal-backend-ml-snpe.cc" + +// =================================================================== +// Basic Lifecycle Tests +// =================================================================== + +TEST_F(MLBackendTest, Snpe_InitAndExit) { + void* hal_data = nullptr; + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_snpe_init(&hal_data)); + EXPECT_NE(hal_data, nullptr); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_snpe_deinit(hal_data)); +} + +TEST_F(MLBackendTest, Snpe_configure_instance) { + void* hal_data = nullptr; + TestGstTensorFilterProperties* test_config = get_test_config(); + ASSERT_NE(test_config, nullptr) << "Test configuration not initialized"; + + // Initialize the backend + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_snpe_init(&hal_data)); + EXPECT_NE(hal_data, nullptr); + + // Configure the instance with properties + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_snpe_configure_instance(hal_data, &test_config->base)); + + // Deinitialize the backend + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_snpe_deinit(hal_data)); +} + +TEST_F(MLBackendTest, Snpe_inference) { + void* hal_data = nullptr; + GstTensorMemory input[NNS_TENSOR_MEMORY_MAX] = {0}; + GstTensorMemory output[NNS_TENSOR_MEMORY_MAX] = {0}; + GstTensorsInfo in_info = {0}; + GstTensorsInfo out_info = {0}; + TestGstTensorFilterProperties* test_config = get_test_config(); + ASSERT_NE(test_config, nullptr) << "Test configuration not initialized"; + + // Initialize the backend + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_snpe_init(&hal_data)); + ASSERT_NE(hal_data, nullptr); + + // Configure the instance with properties + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_snpe_configure_instance(hal_data, &test_config->base)); + + // Get input/output tensor info + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_snpe_get_model_info(hal_data, GET_IN_OUT_INFO, &in_info, &out_info)); + + // Allocate and load input/output buffers + allocate_and_load_test_buffers(input, output, &in_info, &out_info, test_config); + + // Perform inference + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_snpe_invoke(hal_data, input, output)); + + // Free input and output buffers + free_test_buffers(input, output, &in_info, &out_info); + + // Free tensor info memory + gst_tensors_info_free(&in_info); + gst_tensors_info_free(&out_info); + + // Deinitialize the backend + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_snpe_deinit(hal_data)); +} + +// =================================================================== +// Framework Info Tests +// =================================================================== + +TEST_F(MLBackendTest, Snpe_get_framework_info) { + void* hal_data = nullptr; + GstTensorFilterFrameworkInfo fw_info = {0}; + + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_snpe_init(&hal_data)); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_snpe_get_framework_info(hal_data, &fw_info)); + EXPECT_STREQ("snpe", fw_info.name); + EXPECT_FALSE(fw_info.allow_in_place); + EXPECT_FALSE(fw_info.allocate_in_invoke); + EXPECT_FALSE(fw_info.run_without_model); + EXPECT_FALSE(fw_info.verify_model_path); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_snpe_deinit(hal_data)); +} + +// =================================================================== +// Model Info Tests +// =================================================================== + +TEST_F(MLBackendTest, Snpe_get_model_info) { + void* hal_data = nullptr; + GstTensorsInfo in_info = {0}; + GstTensorsInfo out_info = {0}; + TestGstTensorFilterProperties* test_config = get_test_config(); + ASSERT_NE(test_config, nullptr) << "Test configuration not initialized"; + + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_snpe_init(&hal_data)); + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_snpe_configure_instance(hal_data, &test_config->base)); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_snpe_get_model_info(hal_data, GET_IN_OUT_INFO, &in_info, &out_info)); + EXPECT_GT(in_info.num_tensors, 0); + EXPECT_GT(out_info.num_tensors, 0); + + // Free tensor info memory + gst_tensors_info_free(&in_info); + gst_tensors_info_free(&out_info); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_snpe_deinit(hal_data)); +} + +// =================================================================== +// Event Handler Tests +// =================================================================== + +TEST_F(MLBackendTest, Snpe_event_handler_not_supported) { + void* hal_data = nullptr; + + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_snpe_init(&hal_data)); + + EXPECT_EQ(HAL_ML_ERROR_NOT_SUPPORTED, ml_snpe_event_handler(hal_data, 0, nullptr)); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_snpe_deinit(hal_data)); +} + +// =================================================================== +// Error Handling Tests - NULL Parameters +// =================================================================== + +TEST(SnpeTest, DeinitWithNullParameter) { + EXPECT_EQ(HAL_ML_ERROR_INVALID_PARAMETER, ml_snpe_deinit(nullptr)); +} + +TEST(SnpeTest, ConfigureInstanceWithNullBackend) { + GstTensorFilterProperties test_prop = {0}; + EXPECT_EQ(HAL_ML_ERROR_INVALID_PARAMETER, ml_snpe_configure_instance(nullptr, &test_prop)); +} + +TEST(SnpeTest, ConfigureInstanceWithNullProperties) { + void* hal_data = nullptr; + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_snpe_init(&hal_data)); + + EXPECT_EQ(HAL_ML_ERROR_INVALID_PARAMETER, ml_snpe_configure_instance(hal_data, nullptr)); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_snpe_deinit(hal_data)); +} + +TEST(SnpeTest, InvokeWithNullBackend) { + GstTensorMemory input[NNS_TENSOR_MEMORY_MAX] = {0}; + GstTensorMemory output[NNS_TENSOR_MEMORY_MAX] = {0}; + + EXPECT_EQ(HAL_ML_ERROR_INVALID_PARAMETER, ml_snpe_invoke(nullptr, input, output)); +} + +TEST(SnpeTest, GetModelInfoWithNullBackend) { + GstTensorsInfo in_info = {0}; + GstTensorsInfo out_info = {0}; + + EXPECT_EQ(HAL_ML_ERROR_INVALID_PARAMETER, + ml_snpe_get_model_info(nullptr, GET_IN_OUT_INFO, &in_info, &out_info)); +} + +TEST(SnpeTest, GetFrameworkInfoWithNullBackend) { + GstTensorFilterFrameworkInfo fw_info = {0}; + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_snpe_get_framework_info(nullptr, &fw_info)); + EXPECT_STREQ("snpe", fw_info.name); +} + +// =================================================================== +// Static Helper Function Tests +// =================================================================== + +TEST(SnpeTest, TrimString_Normal) { + std::string result = _trim_string(" hello world "); + EXPECT_EQ("hello world", result); +} + +TEST(SnpeTest, TrimString_LeadingWhitespace) { + std::string result = _trim_string(" \t\n test"); + EXPECT_EQ("test", result); +} + +TEST(SnpeTest, TrimString_TrailingWhitespace) { + std::string result = _trim_string("test \t\n "); + EXPECT_EQ("test", result); +} + +TEST(SnpeTest, TrimString_EmptyString) { + std::string result = _trim_string(""); + EXPECT_EQ("", result); +} + +TEST(SnpeTest, TrimString_AllWhitespace) { + std::string result = _trim_string(" \t\n\r "); + EXPECT_EQ("", result); +} + +TEST(SnpeTest, TrimString_NoWhitespace) { + std::string result = _trim_string("helloworld"); + EXPECT_EQ("helloworld", result); +} + +TEST(SnpeTest, TrimString_MixedWhitespace) { + std::string result = _trim_string(" \t multiple \n spaces \r "); + EXPECT_EQ("multiple \n spaces", result); +} + +TEST(SnpeTest, SetEnvironmentVariableAdsp) { + // This test verifies the environment variable setup + // The function sets ADSP_LIBRARY_PATH based on config file or default + set_environment_var_adsp(); + + const char* adsp_path = g_getenv("ADSP_LIBRARY_PATH"); + ASSERT_NE(adsp_path, nullptr) << "ADSP_LIBRARY_PATH environment variable not set"; + ASSERT_STRNE("", adsp_path) << "ADSP_LIBRARY_PATH is empty"; +} + + +// =================================================================== +// Multiple Inference Tests +// =================================================================== + +TEST_F(MLBackendTest, Snpe_multiple_inferences) { + void* hal_data = nullptr; + GstTensorMemory input[NNS_TENSOR_MEMORY_MAX] = {0}; + GstTensorMemory output[NNS_TENSOR_MEMORY_MAX] = {0}; + GstTensorsInfo in_info = {0}; + GstTensorsInfo out_info = {0}; + TestGstTensorFilterProperties* test_config = get_test_config(); + ASSERT_NE(test_config, nullptr) << "Test configuration not initialized"; + + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_snpe_init(&hal_data)); + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_snpe_configure_instance(hal_data, &test_config->base)); + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_snpe_get_model_info(hal_data, GET_IN_OUT_INFO, &in_info, &out_info)); + + // Perform multiple inferences + for (int i = 0; i < 3; i++) { + allocate_and_load_test_buffers(input, output, &in_info, &out_info, test_config); + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_snpe_invoke(hal_data, input, output)); + free_test_buffers(input, output, &in_info, &out_info); + } + + // Free tensor info memory + gst_tensors_info_free(&in_info); + gst_tensors_info_free(&out_info); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_snpe_deinit(hal_data)); +} diff --git a/test/hal_backend_ml_test_util.cc b/test/hal_backend_ml_test_util.cc new file mode 100644 index 0000000..1e05985 --- /dev/null +++ b/test/hal_backend_ml_test_util.cc @@ -0,0 +1,229 @@ +#include "hal_backend_ml_test_util.h" +#include +#include +#include +#include "hal-backend-ml-util.h" +#include + +/* Global test configuration - accessed through getter/setter functions */ +static TestGstTensorFilterProperties* g_test_config = nullptr; + +TestGstTensorFilterProperties* +get_test_config() +{ + return g_test_config; +} + +void +set_test_config(TestGstTensorFilterProperties *config) +{ + g_test_config = config; +} + +int +parse_json_file (char * json_path, TestGstTensorFilterProperties *prop) +{ + JsonParser *parser = NULL; + GError *error = NULL; + JsonNode *rootNode = NULL; + JsonObject *rootObject = NULL; + JsonObject *metadata = NULL; + JsonArray *configParametersArray = NULL; + int ret = HAL_ML_ERROR_NONE; + + /* Input validation */ + if (!json_path) { + g_warning ("JSON file path is NULL"); + return HAL_ML_ERROR_INVALID_PARAMETER; + } + + if (!prop) { + g_warning ("TestGstTensorFilterProperties pointer is NULL"); + return HAL_ML_ERROR_INVALID_PARAMETER; + } + + /* Free existing allocations */ + if (prop->base.model_files) { + for (int i = 0; i < prop->base.num_models; ++i) { + g_free ((char *) prop->base.model_files[i]); + } + delete[] prop->base.model_files; + prop->base.model_files = NULL; + } + + if (prop->input_data_files) { + for (guint i = 0; i < prop->num_input_files; ++i) { + g_free ((char *) prop->input_data_files[i]); + } + delete[] prop->input_data_files; + prop->input_data_files = NULL; + } + + g_free ((char *) prop->base.fwname); + prop->base.fwname = NULL; + + g_free ((char *) prop->base.custom_properties); + prop->base.custom_properties = NULL; + + /* Declare variables before gotos to avoid crossing initialization */ + guint elements; + guint elem; + JsonNode *configNode; + JsonObject *configObject; + guint num_models; + guint i; + + /* Initialize parser */ + parser = json_parser_new (); + if (!parser) { + g_warning ("Failed to create JSON parser"); + return HAL_ML_ERROR_RUNTIME_ERROR; + } + + /* Load JSON file */ + if (!json_parser_load_from_file (parser, json_path, &error)) { + g_warning ("Failed to load JSON file: %s", error->message); + ret = HAL_ML_ERROR_IO_ERROR; + goto cleanup; + } + + /* Get root node */ + rootNode = json_parser_get_root (parser); + if (!rootNode || JSON_NODE_TYPE (rootNode) != JSON_NODE_OBJECT) { + g_warning ("JSON root is not an object"); + ret = HAL_ML_ERROR_INVALID_PARAMETER; + goto cleanup; + } + + /* Get root object */ + rootObject = json_node_get_object (rootNode); + if (!rootObject) { + g_warning ("Failed to get JSON object from root node"); + ret = HAL_ML_ERROR_INVALID_PARAMETER; + goto cleanup; + } + + /* Get metadata object */ + metadata = json_object_get_object_member (rootObject, "metadata"); + if (!metadata) { + g_warning ("'metadata' object not found in JSON"); + ret = HAL_ML_ERROR_INVALID_PARAMETER; + goto cleanup; + } + + /* Get configParameters array */ + configParametersArray = json_object_get_array_member (metadata, "configParameters"); + if (!configParametersArray) { + g_warning ("'configParameters' array not found in 'metadata'"); + ret = HAL_ML_ERROR_INVALID_PARAMETER; + goto cleanup; + } + + /* Process each config parameter element */ + elements = json_array_get_length (configParametersArray); + for (elem = 0; elem < elements; ++elem) { + configNode = json_array_get_element (configParametersArray, elem); + configObject = NULL; + + if (!configNode || JSON_NODE_TYPE (configNode) != JSON_NODE_OBJECT) { + g_warning ("Invalid element at index %u in 'configParameters'", elem); + continue; + } + + configObject = json_node_get_object (configNode); + if (!configObject) { + g_warning ("Failed to get object from element at index %u", elem); + continue; + } + + /* Parse 'fwname' */ + if (json_object_has_member (configObject, "fwname")) { + const gchar *fwname = json_object_get_string_member (configObject, "fwname"); + if (fwname) { + g_free ((char *) prop->base.fwname); + prop->base.fwname = g_strdup (fwname); + g_info ("fwname: %s", prop->base.fwname); + } else { + g_warning ("'fwname' is not a string"); + } + } + + /* Parse 'fw_opened' */ + prop->base.fw_opened = json_object_get_int_member_with_default (configObject, "fw_opened", 0); + + /* Parse 'num_models' (will be updated based on model_files array) */ + prop->base.num_models = json_object_get_int_member_with_default (configObject, "num_models", 0); + g_info ("num_models: %d", prop->base.num_models); + + /* Parse 'model_files' array */ + JsonArray *model_files_array = json_object_get_array_member (configObject, "model_files"); + if (model_files_array) { + num_models = json_array_get_length (model_files_array); + + if (num_models > 0) { + prop->base.num_models = num_models; + prop->base.model_files = new const char*[num_models]; + + for (i = 0; i < num_models; ++i) { + const gchar *file_name = json_array_get_string_element (model_files_array, i); + if (file_name) { + prop->base.model_files[i] = g_strdup (file_name); + g_info ("Model file %u: %s", i, prop->base.model_files[i]); + } else { + g_warning ("Failed to get model file name at index %u", i); + prop->base.model_files[i] = NULL; + } + } + } + } else { + g_warning ("'model_files' array not found in configParameters"); + } + + /* Parse 'input_configured' */ + prop->base.input_configured = json_object_get_int_member_with_default (configObject, "input_configured", 0); + + /* Parse 'output_configured' */ + prop->base.output_configured = json_object_get_int_member_with_default (configObject, "output_configured", 0); + + /* Parse 'custom_properties' */ + if (json_object_has_member (configObject, "custom_properties")) { + const gchar *custom_properties = json_object_get_string_member (configObject, "custom_properties"); + if (custom_properties) { + g_free ((char *) prop->base.custom_properties); + prop->base.custom_properties = g_strdup (custom_properties); + g_info ("Custom properties: %s", prop->base.custom_properties); + } + } + + /* Parse 'input_file' array */ + JsonArray *input_file_array = json_object_get_array_member (configObject, "input_file"); + if (input_file_array) { + guint num_input_files = json_array_get_length (input_file_array); + + if (num_input_files > 0) { + prop->input_data_files = new const char*[num_input_files]; + prop->num_input_files = num_input_files; + + for (i = 0; i < num_input_files; ++i) { + const gchar *file_path = json_array_get_string_element (input_file_array, i); + if (file_path) { + prop->input_data_files[i] = g_strdup (file_path); + g_info ("Input data file %u: %s", i, prop->input_data_files[i]); + } else { + g_warning ("Failed to get input file path at index %u", i); + prop->input_data_files[i] = NULL; + } + } + } + } else { + prop->num_input_files = 0; + prop->input_data_files = nullptr; + } + } + +cleanup: + g_clear_error (&error); + g_clear_object (&parser); + + return ret; +} diff --git a/test/hal_backend_ml_test_util.h b/test/hal_backend_ml_test_util.h new file mode 100644 index 0000000..5b249ed --- /dev/null +++ b/test/hal_backend_ml_test_util.h @@ -0,0 +1,169 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +#ifndef __HAL_BACKEND_ML_TEST_UTIL_H__ +#define __HAL_BACKEND_ML_TEST_UTIL_H__ + +#include "nnstreamer_plugin_api_filter.h" +#include +#include +#include "hal-backend-ml-util.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Test-specific extension of GstTensorFilterProperties + * + * This struct extends the original GstTensorFilterProperties with test-specific + * fields needed for unit testing. It uses composition to include all original + * fields and adds test-specific extensions. + */ +typedef struct _TestGstTensorFilterProperties +{ + GstTensorFilterProperties base; /**< Original GstTensorFilterProperties struct */ + + /* Test-specific extensions */ + const char **input_data_files; /**< Array of input data file paths for testing */ + guint num_input_files; /**< Number of input data files */ +} TestGstTensorFilterProperties; + +/** + * @brief Parse JSON configuration file for test setup + * + * Parses a JSON configuration file and populates the TestGstTensorFilterProperties + * structure with the configuration parameters. + * + * @param json_path Path to the JSON configuration file + * @param prop Pointer to TestGstTensorFilterProperties structure to populate + * @return HAL_ML_ERROR_NONE on success, error code on failure + */ +int parse_json_file(char * json_path, TestGstTensorFilterProperties *prop); + +/** + * @brief Get the test configuration + * + * Returns a pointer to the global test configuration structure. + * This is used by test cases to access the parsed configuration. + * + * @return Pointer to TestGstTensorFilterProperties structure + */ +TestGstTensorFilterProperties* get_test_config(); + +/** + * @brief Set the test configuration + * + * Sets the global test configuration structure pointer. + * This is called by main() after parsing the JSON configuration. + * + * @param config Pointer to TestGstTensorFilterProperties structure to set as global config + */ +void set_test_config(TestGstTensorFilterProperties *config); + +#ifdef __cplusplus +} + +/** + * @brief Base test fixture for ML backend tests + * + * This fixture provides a common base class for ML backend tests. + * Tests access the global test configuration through get_test_config(). + */ +class MLBackendTest : public ::testing::Test { +protected: + // No setup/teardown needed - tests use get_test_config() for configuration +}; + +#endif // __cplusplus + +/** + * @brief Allocate and load test buffers for inference + * + * Allocates input and output buffers, loads data from files if available, + * or fills with zeros if no input files are provided. + * + * @param input Input buffer array to allocate and load + * @param output Output buffer array to allocate + * @param in_info Input tensor info + * @param out_info Output tensor info + * @param prop Test properties containing input data file paths + */ +static inline void +allocate_and_load_test_buffers (GstTensorMemory *input, GstTensorMemory *output, + GstTensorsInfo *in_info, GstTensorsInfo *out_info, + TestGstTensorFilterProperties *prop) +{ + /* Allocate and load input buffers */ + for (guint i = 0; i < in_info->num_tensors; i++) { + GstTensorInfo *info = gst_tensors_info_get_nth_info (in_info, i); + input[i].size = gst_tensor_info_get_size (info); + input[i].data = g_malloc (input[i].size); + ASSERT_NE (input[i].data, nullptr); + + /* Load raw data from file if provided */ + if (prop->input_data_files && i < prop->num_input_files) { + const gchar *input_file = prop->input_data_files[i]; + FILE *fp = fopen (input_file, "rb"); + if (fp) { + size_t bytes_read = fread (input[i].data, 1, input[i].size, fp); + fclose (fp); + + ASSERT_EQ (bytes_read, input[i].size) + << "Read " << bytes_read << " bytes, expected " << input[i].size + << " bytes from file: " << input_file; + } else { + /* File open failed, use zero-filled buffer */ + memset (input[i].data, 0, input[i].size); + } + } else { + /* No input file provided, use zero-filled buffer */ + memset (input[i].data, 0, input[i].size); + } + } + + /* Allocate output buffers */ + for (guint i = 0; i < out_info->num_tensors; i++) { + GstTensorInfo *info = gst_tensors_info_get_nth_info (out_info, i); + output[i].size = gst_tensor_info_get_size (info); + output[i].data = g_malloc0 (output[i].size); + ASSERT_NE (output[i].data, nullptr); + } +} + +/** + * @brief Free allocated test buffers + * + * Frees all input and output buffers that were allocated for testing. + * Safely handles null pointers. + * + * @param input Input buffer array to free + * @param output Output buffer array to free + * @param in_info Input tensor info (for num_tensors) + * @param out_info Output tensor info (for num_tensors) + */ +static inline void +free_test_buffers (GstTensorMemory *input, GstTensorMemory *output, + GstTensorsInfo *in_info, GstTensorsInfo *out_info) +{ + /* Free input buffers */ + if (input) { + for (guint i = 0; i < in_info->num_tensors; i++) { + if (input[i].data) { + g_free (input[i].data); + input[i].data = nullptr; + } + } + } + + /* Free output buffers */ + if (output) { + for (guint i = 0; i < out_info->num_tensors; i++) { + if (output[i].data) { + g_free (output[i].data); + output[i].data = nullptr; + } + } + } +} + +#endif /* __HAL_BACKEND_ML_TEST_UTIL_H__ */ diff --git a/test/hal_backend_ml_test_wrapper.h b/test/hal_backend_ml_test_wrapper.h new file mode 100644 index 0000000..bee6835 --- /dev/null +++ b/test/hal_backend_ml_test_wrapper.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/** + * @file hal_backend_ml_test_wrapper.h + * @brief Wrapper header for testing static functions in hal-backend-ml-*.cc files + * + * This header provides a way to test static functions by undefining 'static' + * when TESTING is defined. This allows the test to compile the source file + * directly and access the functions. + */ + +#ifdef TESTING + +// Undefine static to make functions accessible during testing +#ifdef static +#undef static +#define static +#endif + +// Note: The source files (hal-backend-ml-*.cc) will be included +// by the test files, and with TESTING defined, static functions will +// become regular functions that can be called from tests. + +#endif // TESTING diff --git a/test/hal_backend_ml_vivante_test.cc b/test/hal_backend_ml_vivante_test.cc new file mode 100644 index 0000000..e58c3e0 --- /dev/null +++ b/test/hal_backend_ml_vivante_test.cc @@ -0,0 +1,272 @@ +#define TESTING 1 +#include +#include +#include +#include +#include +#include "hal-backend-ml-util.h" +#include "hal_backend_ml_test_util.h" +#include "hal-backend-ml-util.cc" +#include "hal_backend_ml_test_wrapper.h" +#include "hal-backend-ml-vivante.cc" + +// =================================================================== +// Basic Lifecycle Tests +// =================================================================== + +TEST_F(MLBackendTest, Vivante_InitAndExit) { + void* hal_data = nullptr; + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_vivante_init(&hal_data)); + EXPECT_NE(hal_data, nullptr); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_vivante_deinit(hal_data)); +} + +TEST_F(MLBackendTest, Vivante_configure_instance) { + void* hal_data = nullptr; + TestGstTensorFilterProperties* test_config = get_test_config(); + ASSERT_NE(test_config, nullptr) << "Test configuration not initialized"; + + // Initialize the backend + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_vivante_init(&hal_data)); + EXPECT_NE(hal_data, nullptr); + + // Configure the instance with properties + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_vivante_configure_instance(hal_data, &test_config->base)); + + // Deinitialize the backend + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_vivante_deinit(hal_data)); +} + +TEST_F(MLBackendTest, Vivante_inference) { + void* hal_data = nullptr; + GstTensorMemory input[NNS_TENSOR_MEMORY_MAX] = {0}; + GstTensorMemory output[NNS_TENSOR_MEMORY_MAX] = {0}; + GstTensorsInfo in_info = {0}; + GstTensorsInfo out_info = {0}; + TestGstTensorFilterProperties* test_config = get_test_config(); + ASSERT_NE(test_config, nullptr) << "Test configuration not initialized"; + + // Initialize the backend + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_vivante_init(&hal_data)); + ASSERT_NE(hal_data, nullptr); + + // Configure the instance with properties + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_vivante_configure_instance(hal_data, &test_config->base)); + + // Get input/output tensor info + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_vivante_get_model_info(hal_data, GET_IN_OUT_INFO, &in_info, &out_info)); + + // Allocate and load input/output buffers + allocate_and_load_test_buffers(input, output, &in_info, &out_info, test_config); + + // Perform inference + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_vivante_invoke(hal_data, input, output)); + + // Free input and output buffers + free_test_buffers(input, output, &in_info, &out_info); + + // Free tensor info memory + gst_tensors_info_free(&in_info); + gst_tensors_info_free(&out_info); + + // Deinitialize the backend + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_vivante_deinit(hal_data)); +} + +// =================================================================== +// Framework Info Tests +// =================================================================== + +TEST_F(MLBackendTest, Vivante_get_framework_info) { + void* hal_data = nullptr; + GstTensorFilterFrameworkInfo fw_info = {0}; + + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_vivante_init(&hal_data)); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_vivante_get_framework_info(hal_data, &fw_info)); + EXPECT_STREQ("vivante", fw_info.name); + EXPECT_FALSE(fw_info.allow_in_place); + EXPECT_FALSE(fw_info.allocate_in_invoke); + EXPECT_FALSE(fw_info.run_without_model); + EXPECT_FALSE(fw_info.verify_model_path); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_vivante_deinit(hal_data)); +} + +// =================================================================== +// Model Info Tests +// =================================================================== + +TEST_F(MLBackendTest, Vivante_get_model_info) { + void* hal_data = nullptr; + GstTensorsInfo in_info = {0}; + GstTensorsInfo out_info = {0}; + TestGstTensorFilterProperties* test_config = get_test_config(); + ASSERT_NE(test_config, nullptr) << "Test configuration not initialized"; + + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_vivante_init(&hal_data)); + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_vivante_configure_instance(hal_data, &test_config->base)); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_vivante_get_model_info(hal_data, GET_IN_OUT_INFO, &in_info, &out_info)); + EXPECT_GT(in_info.num_tensors, 0); + EXPECT_GT(out_info.num_tensors, 0); + + // Free tensor info memory + gst_tensors_info_free(&in_info); + gst_tensors_info_free(&out_info); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_vivante_deinit(hal_data)); +} + +// =================================================================== +// Event Handler Tests +// =================================================================== + +TEST_F(MLBackendTest, Vivante_event_handler_not_supported) { + void* hal_data = nullptr; + + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_vivante_init(&hal_data)); + + EXPECT_EQ(HAL_ML_ERROR_NOT_SUPPORTED, ml_vivante_event_handler(hal_data, 0, nullptr)); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_vivante_deinit(hal_data)); +} + +// =================================================================== +// Error Handling Tests - NULL Parameters +// =================================================================== + +TEST(VivanteTest, DeinitWithNullParameter) { + EXPECT_EQ(HAL_ML_ERROR_INVALID_PARAMETER, ml_vivante_deinit(nullptr)); +} + +TEST(VivanteTest, ConfigureInstanceWithNullBackend) { + GstTensorFilterProperties test_prop = {0}; + EXPECT_EQ(HAL_ML_ERROR_INVALID_PARAMETER, ml_vivante_configure_instance(nullptr, &test_prop)); +} + +TEST(VivanteTest, ConfigureInstanceWithNullProperties) { + void* hal_data = nullptr; + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_vivante_init(&hal_data)); + + EXPECT_EQ(HAL_ML_ERROR_INVALID_PARAMETER, ml_vivante_configure_instance(hal_data, nullptr)); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_vivante_deinit(hal_data)); +} + +TEST(VivanteTest, InvokeWithNullBackend) { + GstTensorMemory input[NNS_TENSOR_MEMORY_MAX] = {0}; + GstTensorMemory output[NNS_TENSOR_MEMORY_MAX] = {0}; + + EXPECT_EQ(HAL_ML_ERROR_INVALID_PARAMETER, ml_vivante_invoke(nullptr, input, output)); +} + +TEST(VivanteTest, GetModelInfoWithNullBackend) { + GstTensorsInfo in_info = {0}; + GstTensorsInfo out_info = {0}; + + EXPECT_EQ(HAL_ML_ERROR_INVALID_PARAMETER, + ml_vivante_get_model_info(nullptr, GET_IN_OUT_INFO, &in_info, &out_info)); +} + +TEST(VivanteTest, GetFrameworkInfoWithNullBackend) { + GstTensorFilterFrameworkInfo fw_info = {0}; + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_vivante_get_framework_info(nullptr, &fw_info)); + EXPECT_STREQ("vivante", fw_info.name); +} + +// =================================================================== +// Static Helper Function Tests - Type Conversions +// =================================================================== + +TEST(VivanteTest, VivanteVsiTypeFromString) { + EXPECT_EQ(VSI_NN_TYPE_INT8, vivante_vsi_type_from_string("VSI_NN_TYPE_INT8")); + EXPECT_EQ(VSI_NN_TYPE_UINT8, vivante_vsi_type_from_string("VSI_NN_TYPE_UINT8")); + EXPECT_EQ(VSI_NN_TYPE_INT16, vivante_vsi_type_from_string("VSI_NN_TYPE_INT16")); + EXPECT_EQ(VSI_NN_TYPE_UINT16, vivante_vsi_type_from_string("VSI_NN_TYPE_UINT16")); + EXPECT_EQ(VSI_NN_TYPE_INT32, vivante_vsi_type_from_string("VSI_NN_TYPE_INT32")); + EXPECT_EQ(VSI_NN_TYPE_UINT32, vivante_vsi_type_from_string("VSI_NN_TYPE_UINT32")); + EXPECT_EQ(VSI_NN_TYPE_INT64, vivante_vsi_type_from_string("VSI_NN_TYPE_INT64")); + EXPECT_EQ(VSI_NN_TYPE_UINT64, vivante_vsi_type_from_string("VSI_NN_TYPE_UINT64")); + EXPECT_EQ(VSI_NN_TYPE_FLOAT16, vivante_vsi_type_from_string("VSI_NN_TYPE_FLOAT16")); + EXPECT_EQ(VSI_NN_TYPE_FLOAT32, vivante_vsi_type_from_string("VSI_NN_TYPE_FLOAT32")); + EXPECT_EQ(VSI_NN_TYPE_FLOAT64, vivante_vsi_type_from_string("VSI_NN_TYPE_FLOAT64")); + EXPECT_EQ(VSI_NN_TYPE_BFLOAT16, vivante_vsi_type_from_string("VSI_NN_TYPE_BFLOAT16")); + EXPECT_EQ(VSI_NN_TYPE_BOOL8, vivante_vsi_type_from_string("VSI_NN_TYPE_BOOL8")); + + // Case insensitive + EXPECT_EQ(VSI_NN_TYPE_FLOAT32, vivante_vsi_type_from_string("vsi_nn_type_float32")); + EXPECT_EQ(VSI_NN_TYPE_FLOAT32, vivante_vsi_type_from_string("VSI_NN_TYPE_FLOAT32")); + + // NULL or unknown + EXPECT_EQ(VSI_NN_TYPE_NONE, vivante_vsi_type_from_string(nullptr)); + EXPECT_EQ(VSI_NN_TYPE_NONE, vivante_vsi_type_from_string("UNKNOWN_TYPE")); +} + +TEST(VivanteTest, VivanteQntTypeFromString) { + EXPECT_EQ(VSI_NN_QNT_TYPE_NONE, vivante_qnt_type_from_string("VSI_NN_QNT_TYPE_NONE")); + EXPECT_EQ(VSI_NN_QNT_TYPE_DFP, vivante_qnt_type_from_string("VSI_NN_QNT_TYPE_DFP")); + EXPECT_EQ(VSI_NN_QNT_TYPE_AFFINE_ASYMMETRIC, vivante_qnt_type_from_string("VSI_NN_QNT_TYPE_AFFINE_ASYMMETRIC")); + EXPECT_EQ(VSI_NN_QNT_TYPE_AFFINE_PERCHANNEL_SYMMETRIC, vivante_qnt_type_from_string("VSI_NN_QNT_TYPE_AFFINE_PERCHANNEL_SYMMETRIC")); + EXPECT_EQ(VSI_NN_QNT_TYPE_AFFINE_SYMMETRIC, vivante_qnt_type_from_string("VSI_NN_QNT_TYPE_AFFINE_SYMMETRIC")); + + // Case insensitive + EXPECT_EQ(VSI_NN_QNT_TYPE_DFP, vivante_qnt_type_from_string("vsi_nn_qnt_type_dfp")); + + // NULL or unknown + EXPECT_EQ(VSI_NN_QNT_TYPE_NONE, vivante_qnt_type_from_string(nullptr)); + EXPECT_EQ(VSI_NN_QNT_TYPE_NONE, vivante_qnt_type_from_string("UNKNOWN_QNT_TYPE")); +} + +TEST(VivanteTest, ConvertToTensorType) { + EXPECT_EQ(_NNS_INT8, convert_to_tensor_type(VSI_NN_TYPE_INT8)); + EXPECT_EQ(_NNS_INT8, convert_to_tensor_type(VSI_NN_TYPE_BOOL8)); + EXPECT_EQ(_NNS_UINT8, convert_to_tensor_type(VSI_NN_TYPE_UINT8)); + EXPECT_EQ(_NNS_INT16, convert_to_tensor_type(VSI_NN_TYPE_INT16)); + EXPECT_EQ(_NNS_UINT16, convert_to_tensor_type(VSI_NN_TYPE_UINT16)); + EXPECT_EQ(_NNS_INT32, convert_to_tensor_type(VSI_NN_TYPE_INT32)); + EXPECT_EQ(_NNS_UINT32, convert_to_tensor_type(VSI_NN_TYPE_UINT32)); + EXPECT_EQ(_NNS_INT64, convert_to_tensor_type(VSI_NN_TYPE_INT64)); + EXPECT_EQ(_NNS_UINT64, convert_to_tensor_type(VSI_NN_TYPE_UINT64)); + EXPECT_EQ(_NNS_FLOAT16, convert_to_tensor_type(VSI_NN_TYPE_FLOAT16)); + EXPECT_EQ(_NNS_FLOAT16, convert_to_tensor_type(VSI_NN_TYPE_BFLOAT16)); + EXPECT_EQ(_NNS_FLOAT32, convert_to_tensor_type(VSI_NN_TYPE_FLOAT32)); + EXPECT_EQ(_NNS_FLOAT64, convert_to_tensor_type(VSI_NN_TYPE_FLOAT64)); + + // Unknown type + EXPECT_EQ(_NNS_END, convert_to_tensor_type(VSI_NN_TYPE_NONE)); +} + +// =================================================================== +// Multiple Inference Tests +// =================================================================== + +TEST_F(MLBackendTest, Vivante_multiple_inferences) { + void* hal_data = nullptr; + GstTensorMemory input[NNS_TENSOR_MEMORY_MAX] = {0}; + GstTensorMemory output[NNS_TENSOR_MEMORY_MAX] = {0}; + GstTensorsInfo in_info = {0}; + GstTensorsInfo out_info = {0}; + TestGstTensorFilterProperties* test_config = get_test_config(); + ASSERT_NE(test_config, nullptr) << "Test configuration not initialized"; + + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_vivante_init(&hal_data)); + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_vivante_configure_instance(hal_data, &test_config->base)); + ASSERT_EQ(HAL_ML_ERROR_NONE, ml_vivante_get_model_info(hal_data, GET_IN_OUT_INFO, &in_info, &out_info)); + + // Perform multiple inferences + for (int i = 0; i < 3; i++) { + allocate_and_load_test_buffers(input, output, &in_info, &out_info, test_config); + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_vivante_invoke(hal_data, input, output)); + free_test_buffers(input, output, &in_info, &out_info); + } + + // Free tensor info memory + gst_tensors_info_free(&in_info); + gst_tensors_info_free(&out_info); + + EXPECT_EQ(HAL_ML_ERROR_NONE, ml_vivante_deinit(hal_data)); +} diff --git a/test/main.cpp b/test/main.cpp new file mode 100644 index 0000000..cb938a3 --- /dev/null +++ b/test/main.cpp @@ -0,0 +1,107 @@ +#include "gtest/gtest.h" +#include +#include "hal_backend_ml_test_util.h" +#include "hal-backend-ml-util.h" +#include +#include + +/** + * @brief Display usage information for the test executable + * + * Shows how to run the test executable with the required configuration file. + * + * @param program_name Name of the test executable + */ +static void +show_usage (const char *program_name) +{ + std::cerr << "\n"; + std::cerr << "=====================================================================\n"; + std::cerr << " HAL Backend ML Accelerator Test Runner\n"; + std::cerr << "=====================================================================\n\n"; + std::cerr << "Usage: " << program_name << " \n\n"; + std::cerr << "Required Arguments:\n"; + std::cerr << " config.json Path to JSON configuration file for test setup\n\n"; + std::cerr << "JSON Configuration File Format:\n"; + std::cerr << " The JSON file must contain the following structure:\n"; + std::cerr << " {\n"; + std::cerr << " \"metadata\": {\n"; + std::cerr << " \"configParameters\": [\n"; + std::cerr << " {\n"; + std::cerr << " \"fwname\": \"snpe|vivante|dummy-passthrough\",\n"; + std::cerr << " \"model_files\": [\"/path/to/model.dlc\"],\n"; + std::cerr << " \"input_file\": [\"/path/to/input1.bin\", ...],\n"; + std::cerr << " \"custom_properties\": \"key=value;...\"\n"; + std::cerr << " }\n"; + std::cerr << " ]\n"; + std::cerr << " }\n"; + std::cerr << " }\n\n"; + std::cerr << "Example:\n"; + std::cerr << " " << program_name << " /path/to/test_config.json\n\n"; + std::cerr << "=====================================================================\n\n"; +} + +int +main (int argc, char **argv) +{ + int ret = 0; + TestGstTensorFilterProperties* prop = nullptr; + + /* Check for --help flag */ + if (argc >= 2 && (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0)) { + show_usage(argv[0]); + return 0; + } + + /* Validate command line arguments */ + if (argc < 2) { + std::cerr << "Error: Missing JSON configuration file path\n"; + show_usage(argv[0]); + return 1; + } + + char *json_path; + json_path = argv[1]; + + /* Allocate and initialize test configuration */ + prop = new TestGstTensorFilterProperties(); + memset(prop, 0, sizeof(TestGstTensorFilterProperties)); + + /* Parse JSON configuration file */ + ret = parse_json_file(json_path, prop); + if (ret != HAL_ML_ERROR_NONE) { + std::cerr << "Error: Failed to parse JSON file (error code: " << ret << ")" << std::endl; + g_free ((char *) prop->base.fwname); + g_free ((char *) prop->base.custom_properties); + delete prop; + return 1; + } + + /* Set the global test configuration */ + set_test_config(prop); + + /* Run all tests */ + testing::InitGoogleTest (&argc, argv); + ret = RUN_ALL_TESTS (); + + /* Cleanup allocated memory */ + if (prop->base.model_files) { + for (int i = 0; i < prop->base.num_models; ++i) { + g_free ((char *) prop->base.model_files[i]); + } + delete[] prop->base.model_files; + } + + if (prop->input_data_files) { + for (guint i = 0; i < prop->num_input_files; ++i) { + g_free ((char *) prop->input_data_files[i]); + } + delete[] prop->input_data_files; + } + + g_free ((char *) prop->base.custom_properties); + g_free ((char *) prop->base.fwname); + delete prop; + + return ret; +} diff --git a/test/res/sample_snpe_test_config.json b/test/res/sample_snpe_test_config.json new file mode 100644 index 0000000..2d2662f --- /dev/null +++ b/test/res/sample_snpe_test_config.json @@ -0,0 +1,16 @@ +{ + "metadata": { + "configParameters": [ + { + "fwname": "tizen-hal", + "fw_opened": 0, + "num_models": 1, + "model_files": ["yolov8m_416_quantized.dlc"], + "input_file": ["bus_416x416_float.raw"], + "input_configured": 0, + "output_configured": 0, + "custom_properties": "backend:snpe,Runtime:DSP,InputType:TF8,OutputType:FLOAT32;FLOAT32,OutputTensor:/model.22/Sigmoid_output_0;/model.22/Mul_output_0" + } + ] + } +} diff --git a/test/res/sample_vivante_test_config.json b/test/res/sample_vivante_test_config.json new file mode 100644 index 0000000..dbe4123 --- /dev/null +++ b/test/res/sample_vivante_test_config.json @@ -0,0 +1,16 @@ +{ + "metadata": { + "configParameters": [ + { + "fwname": "tizen-hal", + "fw_opened": 0, + "num_models": 1, + "model_files": ["inception-v3.nb"], + "input_file": ["pizza_3x299x299_uint8.raw"], + "input_configured": 0, + "output_configured": 0, + "custom_properties": "backend:vivante,json:inception-v3.json" + } + ] + } +}