From de10d5ec83e5860836852ae5406091f9bcf21f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Oliver=20Opdenh=C3=B6vel?= Date: Tue, 21 Apr 2026 15:11:08 +0100 Subject: [PATCH 01/10] Testing low-hanging fruits in VRTD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Oliver Opdenhövel --- .github/workflows/vrtd-unit-test.yml | 71 +++++ vrt/vrtd/CMakeLists.txt | 17 ++ vrt/vrtd/src/CMakeLists.txt | 38 +-- vrt/vrtd/src/utils.h | 6 + vrt/vrtd/tests/CMakeLists.txt | 30 +++ vrt/vrtd/tests/allocator_test.cpp | 361 +++++++++++++++++++++++++ vrt/vrtd/tests/array_test.cpp | 222 ++++++++++++++++ vrt/vrtd/tests/auth_test.cpp | 379 +++++++++++++++++++++++++++ vrt/vrtd/tests/config_test.cpp | 223 ++++++++++++++++ vrt/vrtd/tests/hotplug_test.cpp | 141 ++++++++++ vrt/vrtd/tests/test_helpers.hpp | 82 ++++++ vrt/vrtd/tests/utils_test.cpp | 127 +++++++++ 12 files changed, 1679 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/vrtd-unit-test.yml create mode 100644 vrt/vrtd/tests/CMakeLists.txt create mode 100644 vrt/vrtd/tests/allocator_test.cpp create mode 100644 vrt/vrtd/tests/array_test.cpp create mode 100644 vrt/vrtd/tests/auth_test.cpp create mode 100644 vrt/vrtd/tests/config_test.cpp create mode 100644 vrt/vrtd/tests/hotplug_test.cpp create mode 100644 vrt/vrtd/tests/test_helpers.hpp create mode 100644 vrt/vrtd/tests/utils_test.cpp diff --git a/.github/workflows/vrtd-unit-test.yml b/.github/workflows/vrtd-unit-test.yml new file mode 100644 index 00000000..79bf6def --- /dev/null +++ b/.github/workflows/vrtd-unit-test.yml @@ -0,0 +1,71 @@ +# ################################################################################################## +# The MIT License (MIT) +# Copyright (c) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software +# and associated documentation files (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, publish, distribute, +# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or +# substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +# NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# ################################################################################################## + +name: VRTD unit testing + +on: + push: + branches: + - main + - dev + pull_request: + +jobs: + vrtd_unit_tests: + runs-on: ubuntu-24.04 + permissions: { contents: read } + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + submodules: "true" + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y cmake pkg-config ninja-build \ + libsystemd-dev libinih-dev lcov + + - name: Build and run VRTD unit tests + run: | + mkdir vrt/vrtd/build + cd vrt/vrtd/build + cmake -DVRTD_INCLUDE_LIBSLASH=1 -DENABLE_COVERAGE=1 .. + make unit_tests -j$(nproc) + cd tests && ctest --output-on-failure + + - name: Generate coverage report + run: | + cd vrt/vrtd/build + lcov --capture --directory . --output-file coverage.info \ + --ignore-errors mismatch --ignore-errors negative + lcov --remove coverage.info \ + '/usr/*' '*/build/_deps/*' '*/tests/*' \ + --output-file coverage.filtered.info \ + --ignore-errors unused + genhtml coverage.filtered.info --output-directory coverage-report + lcov --list coverage.filtered.info + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: vrtd-coverage-report + path: vrt/vrtd/build/coverage-report/ diff --git a/vrt/vrtd/CMakeLists.txt b/vrt/vrtd/CMakeLists.txt index 0a45d0ac..70723929 100644 --- a/vrt/vrtd/CMakeLists.txt +++ b/vrt/vrtd/CMakeLists.txt @@ -42,6 +42,21 @@ option(VRTD_BUILD_EXAMPLES "Build example executables" OFF) option(VRTD_INCLUDE_LIBSLASH "Include libslash subdirectory instead of building from system" OFF) option(BUILD_SHARED_LIBS "Build shared libraries" ON) +option(ENABLE_SANITIZERS "Build with AddressSanitizer and UBSan" OFF) +if(ENABLE_SANITIZERS) + add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer) + add_link_options(-fsanitize=address,undefined) +endif() + +option(ENABLE_COVERAGE "Build with gcov coverage instrumentation" OFF) +if(ENABLE_COVERAGE) + if(ENABLE_SANITIZERS) + message(FATAL_ERROR "ENABLE_COVERAGE and ENABLE_SANITIZERS cannot be used together") + endif() + add_compile_options(--coverage -fno-inline) + add_link_options(--coverage) +endif() + include(GNUInstallDirs) include(CMakePackageConfigHelpers) @@ -68,6 +83,8 @@ add_subdirectory(src) add_subdirectory(libvrtd) add_subdirectory(libvrtdpp) +add_subdirectory(tests) + #if(VRTD_BUILD_EXAMPLES) # add_subdirectory(examples) #endif() diff --git a/vrt/vrtd/src/CMakeLists.txt b/vrt/vrtd/src/CMakeLists.txt index 1f569775..60685591 100644 --- a/vrt/vrtd/src/CMakeLists.txt +++ b/vrt/vrtd/src/CMakeLists.txt @@ -1,16 +1,16 @@ # ################################################################################################## # The MIT License (MIT) # Copyright (c) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# +# # Permission is hereby granted, free of charge, to any person obtaining a copy of this software # and associated documentation files (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, publish, distribute, # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: -# +# # The above copyright notice and this permission notice shall be included in all copies or # substantial portions of the Software. -# +# # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, @@ -18,8 +18,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # ################################################################################################## -# Library sources (list explicitly; avoid GLOB for reproducible builds) -add_executable(vrtd +# Static library containing all daemon logic (everything except main.c). +# Used by both the vrtd executable and the unit test targets. +add_library(vrtd_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/accept.c ${CMAKE_CURRENT_SOURCE_DIR}/allocator.c ${CMAKE_CURRENT_SOURCE_DIR}/auth.c @@ -29,41 +30,42 @@ add_executable(vrtd ${CMAKE_CURRENT_SOURCE_DIR}/design_writer.c ${CMAKE_CURRENT_SOURCE_DIR}/device.c ${CMAKE_CURRENT_SOURCE_DIR}/hotplug.c - ${CMAKE_CURRENT_SOURCE_DIR}/main.c ${CMAKE_CURRENT_SOURCE_DIR}/reset.c ${CMAKE_CURRENT_SOURCE_DIR}/serve.c ${CMAKE_CURRENT_SOURCE_DIR}/signals.c ${CMAKE_CURRENT_SOURCE_DIR}/utils.c ) -# C standard / properties -set_target_properties(vrtd PROPERTIES +set_target_properties(vrtd_core PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED YES C_EXTENSIONS YES - VERSION ${PROJECT_VERSION} - SOVERSION ${PROJECT_VERSION_MAJOR} ) -# Public include dir (for consumers) and private include dir (for .c / private headers) -target_include_directories(vrtd +target_include_directories(vrtd_core PUBLIC $ $ - PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ) -target_link_libraries( - vrtd - - PRIVATE +target_link_libraries(vrtd_core + PUBLIC ami::ami slash::slash - PkgConfig::SYSTEMD PkgConfig::INIH Threads::Threads ) +# Daemon executable +add_executable(vrtd ${CMAKE_CURRENT_SOURCE_DIR}/main.c) + +target_link_libraries(vrtd PRIVATE vrtd_core) + +set_target_properties(vrtd PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} +) + add_executable(vrtd::vrtd ALIAS vrtd) diff --git a/vrt/vrtd/src/utils.h b/vrt/vrtd/src/utils.h index 0cf53575..acab1504 100644 --- a/vrt/vrtd/src/utils.h +++ b/vrt/vrtd/src/utils.h @@ -144,10 +144,16 @@ static inline uint64_t bit_ceil_u64(uint64_t n) { * Dispatches to bit_ceil_u32 or bit_ceil_u64 based on the argument type. */ /* ---- generic front-end ---- */ +#ifdef __cplusplus +#define bit_ceil(n) \ + (sizeof(n) <= sizeof(uint32_t) ? bit_ceil_u32(static_cast(n)) \ + : bit_ceil_u64(static_cast(n))) +#else #define bit_ceil(n) _Generic((n), \ uint32_t: bit_ceil_u32, \ uint64_t: bit_ceil_u64 \ )(n) +#endif /** * @brief Type-safe maximum of two values (GCC statement expression). diff --git a/vrt/vrtd/tests/CMakeLists.txt b/vrt/vrtd/tests/CMakeLists.txt new file mode 100644 index 00000000..b54f955b --- /dev/null +++ b/vrt/vrtd/tests/CMakeLists.txt @@ -0,0 +1,30 @@ +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.zip +) +FetchContent_MakeAvailable(googletest) + +enable_testing() + +include(GoogleTest) + +add_custom_target(unit_tests) + +macro(add_vrtd_test test_name test_source) + add_executable(${test_name} ${test_source}) + target_link_libraries(${test_name} PRIVATE GTest::gtest_main GTest::gmock vrtd_core) + set_target_properties(${test_name} PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED YES + ) + gtest_discover_tests(${test_name}) + add_dependencies(unit_tests ${test_name}) +endmacro() + +add_vrtd_test(allocator_test allocator_test.cpp) +add_vrtd_test(array_test array_test.cpp) +add_vrtd_test(utils_test utils_test.cpp) +add_vrtd_test(hotplug_test hotplug_test.cpp) +add_vrtd_test(config_test config_test.cpp) +add_vrtd_test(auth_test auth_test.cpp) diff --git a/vrt/vrtd/tests/allocator_test.cpp b/vrt/vrtd/tests/allocator_test.cpp new file mode 100644 index 00000000..e6970ce0 --- /dev/null +++ b/vrt/vrtd/tests/allocator_test.cpp @@ -0,0 +1,361 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include + +extern "C" { +#include "allocator.h" +} + +class AllocatorTest : public ::testing::Test { + protected: + struct device_memory_map *map = nullptr; + + void SetUp() override { + map = device_memory_map_create(); + ASSERT_NE(map, nullptr); + } + + void TearDown() override { + device_memory_map_cleanup(map); + } +}; + +// --- Create / Destroy --- + +TEST_F(AllocatorTest, CreateReturnsNonNull) { + EXPECT_NE(map, nullptr); +} + +TEST_F(AllocatorTest, FreshMapHasAllRegionsFree) { + for (int r = 0; r < DDR_REGIONS; r++) { + for (int s = 0; s < (int)SUBREGIONS_PER_REGION; s++) { + EXPECT_EQ(map->ddr_regions[r].client_id[s], 0u); + } + } + for (int r = 0; r < HBM_REGIONS; r++) { + for (int s = 0; s < (int)SUBREGIONS_PER_REGION; s++) { + EXPECT_EQ(map->hbm_regions[r].client_id[s], 0u); + } + } +} + +TEST_F(AllocatorTest, CleanupNullIsSafe) { + device_memory_map_cleanup(nullptr); +} + +// --- Argument validation --- + +TEST_F(AllocatorTest, AllocateNullMap) { + uint64_t size = SUBREGION_SIZE; + uint64_t addr; + EXPECT_EQ(device_memory_map_allocate(nullptr, ALLOCATION_TYPE_DDR, &size, 0, 1, &addr), + ALLOCATION_RESULT_BAD_ARGUMENT); +} + +TEST_F(AllocatorTest, AllocateNullSize) { + uint64_t addr; + EXPECT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, nullptr, 0, 1, &addr), + ALLOCATION_RESULT_BAD_ARGUMENT); +} + +TEST_F(AllocatorTest, AllocateNullAddrOut) { + uint64_t size = SUBREGION_SIZE; + EXPECT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 1, nullptr), + ALLOCATION_RESULT_BAD_ARGUMENT); +} + +TEST_F(AllocatorTest, AllocateZeroSize) { + uint64_t size = 0; + uint64_t addr; + EXPECT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 1, &addr), + ALLOCATION_RESULT_BAD_ARGUMENT); +} + +TEST_F(AllocatorTest, AllocateZeroClientId) { + uint64_t size = SUBREGION_SIZE; + uint64_t addr; + EXPECT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 0, &addr), + ALLOCATION_RESULT_BAD_ARGUMENT); +} + +TEST_F(AllocatorTest, FreeNullMap) { + EXPECT_EQ(device_memory_map_free(nullptr, ALLOCATION_TYPE_DDR, DDR_START_ADDRESS, SUBREGION_SIZE, 1), + ALLOCATION_RESULT_BAD_ARGUMENT); +} + +TEST_F(AllocatorTest, FreeZeroSize) { + EXPECT_EQ(device_memory_map_free(map, ALLOCATION_TYPE_DDR, DDR_START_ADDRESS, 0, 1), + ALLOCATION_RESULT_BAD_ARGUMENT); +} + +TEST_F(AllocatorTest, FreeZeroClientId) { + EXPECT_EQ(device_memory_map_free(map, ALLOCATION_TYPE_DDR, DDR_START_ADDRESS, SUBREGION_SIZE, 0), + ALLOCATION_RESULT_BAD_ARGUMENT); +} + +// --- DDR allocation --- + +TEST_F(AllocatorTest, DdrSingleSubregion) { + uint64_t size = SUBREGION_SIZE; + uint64_t addr; + EXPECT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 1, &addr), + ALLOCATION_RESULT_SUCCESS); + EXPECT_EQ(addr, DDR_START_ADDRESS); + EXPECT_EQ(size, SUBREGION_SIZE); +} + +TEST_F(AllocatorTest, DdrSizeRoundsUp) { + uint64_t size = 1; + uint64_t addr; + EXPECT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 1, &addr), + ALLOCATION_RESULT_SUCCESS); + EXPECT_EQ(size, SUBREGION_SIZE); +} + +TEST_F(AllocatorTest, DdrMultiSubregion) { + uint64_t size = 3 * SUBREGION_SIZE; + uint64_t addr; + EXPECT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 1, &addr), + ALLOCATION_RESULT_SUCCESS); + EXPECT_EQ(addr, DDR_START_ADDRESS); + EXPECT_EQ(size, 3 * SUBREGION_SIZE); + + for (int i = 0; i < 3; i++) { + EXPECT_EQ(map->ddr_regions[0].client_id[i], 1u); + } + for (int i = 3; i < (int)SUBREGIONS_PER_REGION; i++) { + EXPECT_EQ(map->ddr_regions[0].client_id[i], 0u); + } +} + +TEST_F(AllocatorTest, DdrFullRegion) { + uint64_t size = SUBREGIONS_PER_REGION * SUBREGION_SIZE; + uint64_t addr; + EXPECT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 1, &addr), + ALLOCATION_RESULT_SUCCESS); + EXPECT_EQ(addr, DDR_START_ADDRESS); +} + +TEST_F(AllocatorTest, DdrExceedsSingleRegion) { + uint64_t size = (SUBREGIONS_PER_REGION + 1) * SUBREGION_SIZE; + uint64_t addr; + EXPECT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 1, &addr), + ALLOCATION_RESULT_BAD_ARGUMENT); +} + +TEST_F(AllocatorTest, DdrSecondAllocationFollowsFirst) { + uint64_t size = SUBREGION_SIZE; + uint64_t addr1, addr2; + ASSERT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 1, &addr1), + ALLOCATION_RESULT_SUCCESS); + size = SUBREGION_SIZE; + ASSERT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 1, &addr2), + ALLOCATION_RESULT_SUCCESS); + EXPECT_EQ(addr2, DDR_START_ADDRESS + SUBREGION_SIZE); +} + +TEST_F(AllocatorTest, DdrSpillsToNextRegion) { + uint64_t size = SUBREGIONS_PER_REGION * SUBREGION_SIZE; + uint64_t addr; + ASSERT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 1, &addr), + ALLOCATION_RESULT_SUCCESS); + + size = SUBREGION_SIZE; + ASSERT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 2, &addr), + ALLOCATION_RESULT_SUCCESS); + EXPECT_EQ(addr, DDR_START_ADDRESS + REGION_SIZE); +} + +TEST_F(AllocatorTest, DdrExhaustAllRegions) { + for (int r = 0; r < DDR_REGIONS; r++) { + uint64_t size = SUBREGIONS_PER_REGION * SUBREGION_SIZE; + uint64_t addr; + ASSERT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 1, &addr), + ALLOCATION_RESULT_SUCCESS); + } + + uint64_t size = SUBREGION_SIZE; + uint64_t addr; + EXPECT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 1, &addr), + ALLOCATION_RESULT_NO_MEMORY); +} + +// --- HBM allocation (pinned region) --- + +TEST_F(AllocatorTest, HbmPinnedRegion) { + uint64_t size = SUBREGION_SIZE; + uint64_t addr; + EXPECT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_HBM, &size, 5, 1, &addr), + ALLOCATION_RESULT_SUCCESS); + EXPECT_EQ(addr, HBM_START_ADDRESS + 5 * REGION_SIZE); +} + +TEST_F(AllocatorTest, HbmRegionOutOfRange) { + uint64_t size = SUBREGION_SIZE; + uint64_t addr; + EXPECT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_HBM, &size, HBM_REGIONS, 1, &addr), + ALLOCATION_RESULT_BAD_ARGUMENT); +} + +TEST_F(AllocatorTest, HbmPinnedRegionExhaustion) { + for (int s = 0; s < (int)SUBREGIONS_PER_REGION; s++) { + uint64_t size = SUBREGION_SIZE; + uint64_t addr; + ASSERT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_HBM, &size, 0, 1, &addr), + ALLOCATION_RESULT_SUCCESS); + } + + uint64_t size = SUBREGION_SIZE; + uint64_t addr; + EXPECT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_HBM, &size, 0, 1, &addr), + ALLOCATION_RESULT_NO_MEMORY); +} + +// --- HBM_VNOC allocation (auto-region) --- + +TEST_F(AllocatorTest, HbmVnocAutoSelection) { + uint64_t size = SUBREGION_SIZE; + uint64_t addr; + EXPECT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_HBM_VNOC, &size, 0, 1, &addr), + ALLOCATION_RESULT_SUCCESS); + EXPECT_EQ(addr, HBM_START_ADDRESS); +} + +TEST_F(AllocatorTest, HbmVnocFirstFitAcrossRegions) { + for (int s = 0; s < (int)SUBREGIONS_PER_REGION; s++) { + uint64_t size = SUBREGION_SIZE; + uint64_t addr; + ASSERT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_HBM_VNOC, &size, 0, 1, &addr), + ALLOCATION_RESULT_SUCCESS); + } + + uint64_t size = SUBREGION_SIZE; + uint64_t addr; + ASSERT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_HBM_VNOC, &size, 0, 2, &addr), + ALLOCATION_RESULT_SUCCESS); + EXPECT_EQ(addr, HBM_START_ADDRESS + REGION_SIZE); +} + +// --- Free --- + +TEST_F(AllocatorTest, FreeAndReallocate) { + uint64_t size = SUBREGION_SIZE; + uint64_t addr; + ASSERT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 1, &addr), + ALLOCATION_RESULT_SUCCESS); + ASSERT_EQ(device_memory_map_free(map, ALLOCATION_TYPE_DDR, addr, size, 1), + ALLOCATION_RESULT_SUCCESS); + + EXPECT_EQ(map->ddr_regions[0].client_id[0], 0u); + + size = SUBREGION_SIZE; + uint64_t addr2; + ASSERT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 2, &addr2), + ALLOCATION_RESULT_SUCCESS); + EXPECT_EQ(addr2, addr); +} + +TEST_F(AllocatorTest, FreeWrongClientIdDenied) { + uint64_t size = SUBREGION_SIZE; + uint64_t addr; + ASSERT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 1, &addr), + ALLOCATION_RESULT_SUCCESS); + + EXPECT_EQ(device_memory_map_free(map, ALLOCATION_TYPE_DDR, addr, size, 99), + ALLOCATION_RESULT_BAD_ARGUMENT); + + EXPECT_EQ(map->ddr_regions[0].client_id[0], 1u); +} + +TEST_F(AllocatorTest, FreeUnalignedAddress) { + EXPECT_EQ(device_memory_map_free(map, ALLOCATION_TYPE_DDR, DDR_START_ADDRESS + 1, SUBREGION_SIZE, 1), + ALLOCATION_RESULT_BAD_ARGUMENT); +} + +TEST_F(AllocatorTest, FreeAddressOutOfRange) { + EXPECT_EQ(device_memory_map_free(map, ALLOCATION_TYPE_DDR, 0, SUBREGION_SIZE, 1), + ALLOCATION_RESULT_BAD_ARGUMENT); +} + +TEST_F(AllocatorTest, FreeCrossRegionSpan) { + uint64_t size = SUBREGION_SIZE; + uint64_t addr = DDR_START_ADDRESS + (SUBREGIONS_PER_REGION - 1) * SUBREGION_SIZE; + uint64_t free_size = 2 * SUBREGION_SIZE; + EXPECT_EQ(device_memory_map_free(map, ALLOCATION_TYPE_DDR, addr, free_size, 1), + ALLOCATION_RESULT_BAD_ARGUMENT); +} + +TEST_F(AllocatorTest, FreeHbm) { + uint64_t size = 2 * SUBREGION_SIZE; + uint64_t addr; + ASSERT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_HBM, &size, 3, 1, &addr), + ALLOCATION_RESULT_SUCCESS); + ASSERT_EQ(device_memory_map_free(map, ALLOCATION_TYPE_HBM, addr, size, 1), + ALLOCATION_RESULT_SUCCESS); + EXPECT_EQ(map->hbm_regions[3].client_id[0], 0u); + EXPECT_EQ(map->hbm_regions[3].client_id[1], 0u); +} + +// --- Multi-client isolation --- + +TEST_F(AllocatorTest, MultiClientIsolation) { + uint64_t size = SUBREGION_SIZE; + uint64_t addr1, addr2; + + ASSERT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 100, &addr1), + ALLOCATION_RESULT_SUCCESS); + size = SUBREGION_SIZE; + ASSERT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 200, &addr2), + ALLOCATION_RESULT_SUCCESS); + + EXPECT_EQ(map->ddr_regions[0].client_id[0], 100u); + EXPECT_EQ(map->ddr_regions[0].client_id[1], 200u); + + ASSERT_EQ(device_memory_map_free(map, ALLOCATION_TYPE_DDR, addr1, SUBREGION_SIZE, 100), + ALLOCATION_RESULT_SUCCESS); + EXPECT_EQ(map->ddr_regions[0].client_id[0], 0u); + EXPECT_EQ(map->ddr_regions[0].client_id[1], 200u); +} + +// --- Address math verification --- + +TEST_F(AllocatorTest, DdrAddressMath) { + for (int r = 0; r < 3; r++) { + uint64_t size = SUBREGIONS_PER_REGION * SUBREGION_SIZE; + uint64_t addr; + ASSERT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_DDR, &size, 0, 1, &addr), + ALLOCATION_RESULT_SUCCESS); + EXPECT_EQ(addr, DDR_START_ADDRESS + (uint64_t)r * REGION_SIZE); + } +} + +TEST_F(AllocatorTest, HbmVnocAddressMath) { + uint64_t size = 2 * SUBREGION_SIZE; + uint64_t addr; + ASSERT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_HBM_VNOC, &size, 0, 1, &addr), + ALLOCATION_RESULT_SUCCESS); + EXPECT_EQ(addr, HBM_START_ADDRESS); + + size = 2 * SUBREGION_SIZE; + ASSERT_EQ(device_memory_map_allocate(map, ALLOCATION_TYPE_HBM_VNOC, &size, 0, 1, &addr), + ALLOCATION_RESULT_SUCCESS); + EXPECT_EQ(addr, HBM_START_ADDRESS + 2 * SUBREGION_SIZE); +} diff --git a/vrt/vrtd/tests/array_test.cpp b/vrt/vrtd/tests/array_test.cpp new file mode 100644 index 00000000..8984f57b --- /dev/null +++ b/vrt/vrtd/tests/array_test.cpp @@ -0,0 +1,222 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include + +extern "C" { +#include "array.h" +} + +// --- Value array (int_array) --- + +TEST(IntArrayTest, InitIsEmpty) { + struct int_array arr = int_array_init(); + EXPECT_EQ(arr.len, 0u); + EXPECT_EQ(arr.cap, 0u); + EXPECT_EQ(arr.d, nullptr); + int_array_free(&arr); +} + +TEST(IntArrayTest, PushAndAccess) { + struct int_array arr = int_array_init(); + ASSERT_EQ(int_array_push(&arr, 42), 0); + ASSERT_EQ(int_array_push(&arr, 99), 0); + EXPECT_EQ(arr.len, 2u); + EXPECT_EQ(arr.d[0], 42); + EXPECT_EQ(arr.d[1], 99); + int_array_free(&arr); +} + +TEST(IntArrayTest, CapacityGrowsPowerOfTwo) { + struct int_array arr = int_array_init(); + for (int i = 0; i < 5; i++) { + ASSERT_EQ(int_array_push(&arr, i), 0); + } + EXPECT_EQ(arr.len, 5u); + EXPECT_GE(arr.cap, 5u); + EXPECT_EQ(arr.cap & (arr.cap - 1), 0u); + int_array_free(&arr); +} + +TEST(IntArrayTest, PopLifo) { + struct int_array arr = int_array_init(); + ASSERT_EQ(int_array_push(&arr, 10), 0); + ASSERT_EQ(int_array_push(&arr, 20), 0); + ASSERT_EQ(int_array_push(&arr, 30), 0); + + int val; + EXPECT_EQ(int_array_pop(&arr, &val), 0); + EXPECT_EQ(val, 30); + EXPECT_EQ(int_array_pop(&arr, &val), 0); + EXPECT_EQ(val, 20); + EXPECT_EQ(int_array_pop(&arr, &val), 0); + EXPECT_EQ(val, 10); + int_array_free(&arr); +} + +TEST(IntArrayTest, PopEmptyReturnsError) { + struct int_array arr = int_array_init(); + int val; + EXPECT_EQ(int_array_pop(&arr, &val), -1); + int_array_free(&arr); +} + +TEST(IntArrayTest, PopSafeEmptyIsNoop) { + struct int_array arr = int_array_init(); + int val = -1; + int_array_pop_safe(&arr, &val); + EXPECT_EQ(val, -1); + int_array_free(&arr); +} + +TEST(IntArrayTest, RmByValue) { + struct int_array arr = int_array_init(); + ASSERT_EQ(int_array_push(&arr, 1), 0); + ASSERT_EQ(int_array_push(&arr, 2), 0); + ASSERT_EQ(int_array_push(&arr, 3), 0); + ASSERT_EQ(int_array_push(&arr, 2), 0); + + int_array_rm_by_value(&arr, 2); + EXPECT_EQ(arr.len, 2u); + EXPECT_EQ(arr.d[0], 1); + EXPECT_EQ(arr.d[1], 3); + int_array_free(&arr); +} + +TEST(IntArrayTest, ShrinkToFit) { + struct int_array arr = int_array_init(); + for (int i = 0; i < 16; i++) { + ASSERT_EQ(int_array_push(&arr, i), 0); + } + // pop_safe doesn't call resize, so capacity stays at 16 + int val; + for (int i = 0; i < 8; i++) { + int_array_pop_safe(&arr, &val); + } + EXPECT_EQ(arr.len, 8u); + EXPECT_EQ(arr.cap, 16u); + + ASSERT_EQ(int_array_shrink_to_fit(&arr), 0); + EXPECT_EQ(arr.cap, 8u); + int_array_free(&arr); +} + +TEST(IntArrayTest, Zero) { + struct int_array arr = int_array_init(); + ASSERT_EQ(int_array_push(&arr, 42), 0); + ASSERT_EQ(int_array_push(&arr, 99), 0); + + int_array_zero(&arr); + EXPECT_EQ(arr.d[0], 0); + EXPECT_EQ(arr.d[1], 0); + EXPECT_EQ(arr.len, 2u); + int_array_free(&arr); +} + +TEST(IntArrayTest, Resize) { + struct int_array arr = int_array_init(); + ASSERT_EQ(int_array_resize(&arr, 10), 0); + EXPECT_GE(arr.cap, 10u); + int_array_free(&arr); +} + +TEST(IntArrayTest, FreeResetsState) { + struct int_array arr = int_array_init(); + ASSERT_EQ(int_array_push(&arr, 1), 0); + int_array_free(&arr); + EXPECT_EQ(arr.len, 0u); + EXPECT_EQ(arr.cap, 0u); + EXPECT_EQ(arr.d, nullptr); +} + +// --- Owning pointer array (str_array) --- + +static int g_cleanup_count = 0; + +struct dummy { + int value; +}; + +static void cleanup_dummy(struct dummy *d) { + g_cleanup_count++; + free(d); +} + +DECLARE_OWNING_PTR_ARRAY(dummy_ptr_array, struct dummy *, cleanup_dummy) + +TEST(OwningArrayTest, PushMoveNullifiesSource) { + struct dummy_ptr_array arr = dummy_ptr_array_init(); + auto *d = static_cast(calloc(1, sizeof(struct dummy))); + d->value = 42; + + struct dummy *src = d; + ASSERT_EQ(dummy_ptr_array_push_move(&arr, &src), 0); + EXPECT_EQ(src, nullptr); + EXPECT_EQ(arr.d[0]->value, 42); + dummy_ptr_array_free(&arr); +} + +TEST(OwningArrayTest, FreeCallsCleanup) { + struct dummy_ptr_array arr = dummy_ptr_array_init(); + for (int i = 0; i < 3; i++) { + auto *d = static_cast(calloc(1, sizeof(struct dummy))); + d->value = i; + struct dummy *src = d; + ASSERT_EQ(dummy_ptr_array_push_move(&arr, &src), 0); + } + + g_cleanup_count = 0; + dummy_ptr_array_free(&arr); + EXPECT_EQ(g_cleanup_count, 3); +} + +TEST(OwningArrayTest, RmByReferenceCallsCleanup) { + struct dummy_ptr_array arr = dummy_ptr_array_init(); + auto *d1 = static_cast(calloc(1, sizeof(struct dummy))); + auto *d2 = static_cast(calloc(1, sizeof(struct dummy))); + d1->value = 1; + d2->value = 2; + + struct dummy *src = d1; + ASSERT_EQ(dummy_ptr_array_push_move(&arr, &src), 0); + src = d2; + ASSERT_EQ(dummy_ptr_array_push_move(&arr, &src), 0); + + g_cleanup_count = 0; + dummy_ptr_array_rm_by_reference(&arr, d1); + EXPECT_EQ(g_cleanup_count, 1); + EXPECT_EQ(arr.len, 1u); + EXPECT_EQ(arr.d[0]->value, 2); + dummy_ptr_array_free(&arr); +} + +TEST(StrArrayTest, PushAndFree) { + struct str_array arr = str_array_init(); + char *s1 = strdup("hello"); + char *s2 = strdup("world"); + ASSERT_EQ(str_array_push_move(&arr, &s1), 0); + ASSERT_EQ(str_array_push_move(&arr, &s2), 0); + EXPECT_EQ(s1, nullptr); + EXPECT_EQ(s2, nullptr); + EXPECT_EQ(arr.len, 2u); + EXPECT_STREQ(arr.d[0], "hello"); + EXPECT_STREQ(arr.d[1], "world"); + str_array_free(&arr); +} diff --git a/vrt/vrtd/tests/auth_test.cpp b/vrt/vrtd/tests/auth_test.cpp new file mode 100644 index 00000000..c3fd3cc3 --- /dev/null +++ b/vrt/vrtd/tests/auth_test.cpp @@ -0,0 +1,379 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include + +#include + +extern "C" { +#include "auth.h" +#include "config.h" +#include "device.h" +#include "state.h" +} + +static struct device_policy *make_dp(const char *bdf, bool bar, bool qdma, bool buffer, + bool design_write, bool clock, bool pcie_hotplug, + bool raw_mem) { + auto *dp = static_cast(calloc(1, sizeof(struct device_policy))); + dp->bdf = strdup(bdf); + dp->bar = bar; + dp->qdma = qdma; + dp->buffer = buffer; + dp->design_write = design_write; + dp->clock = clock; + dp->pcie_hotplug = pcie_hotplug; + dp->raw_mem_access = raw_mem; + return dp; +} + +class AuthTest : public ::testing::Test { + protected: + struct config cfg{}; + struct device dev{}; + struct vrtd state{}; + struct client cl{}; + struct user_config default_user{}; + struct role *fullaccess_role = nullptr; + struct role *info_role = nullptr; + + void SetUp() override { + memset(&cfg, 0, sizeof(cfg)); + memset(&dev, 0, sizeof(dev)); + memset(&state, 0, sizeof(state)); + memset(&cl, 0, sizeof(cl)); + memset(&default_user, 0, sizeof(default_user)); + + strncpy(dev.pci_info.bdf, "0000:03:00", sizeof(dev.pci_info.bdf) - 1); + + ASSERT_EQ(role_merge_new(&fullaccess_role, "fullaccess"), 0); + fullaccess_role->query = true; + struct device_policy *dp = make_dp("any", true, true, true, true, true, true, true); + struct device_policy *dp_ptr = dp; + ASSERT_EQ(device_policy_ptr_array_push_move(&fullaccess_role->device_policies, &dp_ptr), 0); + + ASSERT_EQ(role_merge_new(&info_role, "info"), 0); + info_role->query = true; + + struct role *fa_ref = fullaccess_role; + ASSERT_EQ(role_ptr_array_push_move(&cfg.roles, &fa_ref), 0); + struct role *info_ref = info_role; + ASSERT_EQ(role_ptr_array_push_move(&cfg.roles, &info_ref), 0); + + default_user.name = strdup("*"); + cfg.default_user = &default_user; + + struct device *dev_ptr = &dev; + ASSERT_EQ(device_ptr_array_push(&state.devices, dev_ptr), 0); + + state.config = &cfg; + + cl.uid = getuid(); + cl.state = &state; + cl.role = nullptr; + cl.fd = -1; + } + + void TearDown() override { + if (cl.role != nullptr) { + cleanup_role(cl.role); + cl.role = nullptr; + } + gid_t_array_free(&cl.gids); + + // dev is stack-allocated, so we must not call the owning + // device_ptr_array_free (which would free(dev)). + free(state.devices.d); + state.devices.d = nullptr; + state.devices.len = 0; + state.devices.cap = 0; + + role_ptr_array_free(&cfg.roles); + + str_array_free(&default_user.role_names); + role_ref_array_free(&default_user.roles); + free(default_user.name); + + user_config_ptr_array_free(&cfg.users); + group_config_ptr_array_free(&cfg.groups); + } + + void assignRole(struct role *role_template) { + struct role *merged = nullptr; + ASSERT_EQ(role_merge_new(&merged, "merged"), 0); + ASSERT_EQ(role_merge_add_role(merged, role_template), 0); + cl.role = merged; + } +}; + +// --- Query-only operations --- + +TEST_F(AuthTest, GetNumDevicesAllowedWithQuery) { + assignRole(info_role); + struct vrtd_req_get_num_devices req{}; + EXPECT_EQ(auth_request_get_num_devices(&cl, &req), 1); +} + +TEST_F(AuthTest, GetNumDevicesDeniedWithoutQuery) { + struct role *empty = nullptr; + ASSERT_EQ(role_merge_new(&empty, "empty"), 0); + cl.role = empty; + struct vrtd_req_get_num_devices req{}; + EXPECT_EQ(auth_request_get_num_devices(&cl, &req), 0); +} + +TEST_F(AuthTest, GetDeviceInfoAllowed) { + assignRole(info_role); + struct vrtd_req_get_device_info req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_get_device_info(&cl, &req), 1); +} + +TEST_F(AuthTest, GetDeviceByBdfAllowed) { + assignRole(info_role); + struct vrtd_req_get_device_by_bdf req{}; + EXPECT_EQ(auth_request_get_device_by_bdf(&cl, &req), 1); +} + +TEST_F(AuthTest, GetBarInfoAllowed) { + assignRole(info_role); + struct vrtd_req_get_bar_info req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_get_bar_info(&cl, &req), 1); +} + +TEST_F(AuthTest, QdmaGetInfoAllowed) { + assignRole(info_role); + struct vrtd_req_qdma_get_info req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_qdma_get_info(&cl, &req), 1); +} + +TEST_F(AuthTest, GetSensorInfoAllowed) { + assignRole(info_role); + struct vrtd_req_get_sensor_info req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_get_sensor_info(&cl, &req), 1); +} + +// --- Device-access operations: fullaccess role --- + +TEST_F(AuthTest, GetBarFdAllowedFullaccess) { + assignRole(fullaccess_role); + struct vrtd_req_get_bar_fd req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_get_bar_fd(&cl, &req), 1); +} + +TEST_F(AuthTest, QdmaQpairAddAllowed) { + assignRole(fullaccess_role); + struct vrtd_req_qdma_qpair_add req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_qdma_qpair_add(&cl, &req), 1); +} + +TEST_F(AuthTest, BufferOpenAllowed) { + assignRole(fullaccess_role); + struct vrtd_req_buffer_open req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_buffer_open(&cl, &req), 1); +} + +TEST_F(AuthTest, BufferCloseAllowed) { + assignRole(fullaccess_role); + struct vrtd_req_buffer_close req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_buffer_close(&cl, &req), 1); +} + +TEST_F(AuthTest, DesignWriteAllowed) { + assignRole(fullaccess_role); + struct vrtd_req_design_write req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_design_write(&cl, &req), 1); +} + +TEST_F(AuthTest, ClockOpAllowed) { + assignRole(fullaccess_role); + struct vrtd_req_clock_op req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_clock_op(&cl, &req), 1); +} + +TEST_F(AuthTest, HotplugOpAllowed) { + assignRole(fullaccess_role); + struct vrtd_req_device_hotplug_op req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_device_hotplug_op(&cl, &req), 1); +} + +TEST_F(AuthTest, BufferOpenRawAllowed) { + assignRole(fullaccess_role); + struct vrtd_req_buffer_open_raw req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_buffer_open_raw(&cl, &req), 1); +} + +// --- Device-access denied with info-only role --- + +TEST_F(AuthTest, GetBarFdDeniedInfoOnly) { + assignRole(info_role); + struct vrtd_req_get_bar_fd req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_get_bar_fd(&cl, &req), 0); +} + +TEST_F(AuthTest, BufferOpenDeniedInfoOnly) { + assignRole(info_role); + struct vrtd_req_buffer_open req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_buffer_open(&cl, &req), 0); +} + +TEST_F(AuthTest, DesignWriteDeniedInfoOnly) { + assignRole(info_role); + struct vrtd_req_design_write req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_design_write(&cl, &req), 0); +} + +TEST_F(AuthTest, ClockOpDeniedInfoOnly) { + assignRole(info_role); + struct vrtd_req_clock_op req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_clock_op(&cl, &req), 0); +} + +TEST_F(AuthTest, HotplugOpDeniedInfoOnly) { + assignRole(info_role); + struct vrtd_req_device_hotplug_op req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_device_hotplug_op(&cl, &req), 0); +} + +// --- Exact BDF match vs "any" wildcard --- + +TEST_F(AuthTest, ExactBdfMatchTakesPriority) { + struct role *role = nullptr; + ASSERT_EQ(role_merge_new(&role, "mixed"), 0); + role->query = true; + + struct device_policy *any_dp = make_dp("any", false, false, false, false, false, false, false); + struct device_policy *any_ptr = any_dp; + ASSERT_EQ(device_policy_ptr_array_push_move(&role->device_policies, &any_ptr), 0); + + struct device_policy *exact_dp = make_dp("0000:03:00", true, true, true, true, true, true, true); + struct device_policy *exact_ptr = exact_dp; + ASSERT_EQ(device_policy_ptr_array_push_move(&role->device_policies, &exact_ptr), 0); + + cl.role = role; + + struct vrtd_req_get_bar_fd req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_get_bar_fd(&cl, &req), 1); +} + +TEST_F(AuthTest, WildcardFallbackWhenNoExactMatch) { + struct role *role = nullptr; + ASSERT_EQ(role_merge_new(&role, "wildcard_only"), 0); + role->query = true; + + struct device_policy *any_dp = make_dp("any", true, false, false, false, false, false, false); + struct device_policy *any_ptr = any_dp; + ASSERT_EQ(device_policy_ptr_array_push_move(&role->device_policies, &any_ptr), 0); + + cl.role = role; + + struct vrtd_req_get_bar_fd req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_get_bar_fd(&cl, &req), 1); +} + +TEST_F(AuthTest, NoPolicyMatchDenied) { + struct role *role = nullptr; + ASSERT_EQ(role_merge_new(&role, "no_devices"), 0); + role->query = true; + + struct device_policy *other_dp = make_dp("0000:99:00", true, true, true, true, true, true, true); + struct device_policy *other_ptr = other_dp; + ASSERT_EQ(device_policy_ptr_array_push_move(&role->device_policies, &other_ptr), 0); + + cl.role = role; + + struct vrtd_req_get_bar_fd req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_get_bar_fd(&cl, &req), 0); +} + +// --- Invalid device index --- + +TEST_F(AuthTest, DeviceIndexOutOfRange) { + assignRole(fullaccess_role); + struct vrtd_req_get_bar_fd req{}; + req.dev_number = 999; + EXPECT_EQ(auth_request_get_bar_fd(&cl, &req), 0); +} + +// --- ensure_role: lazy role merging via default_user --- + +TEST_F(AuthTest, EnsureRoleMergesDefaultUser) { + ASSERT_EQ(role_ref_array_push(&default_user.roles, fullaccess_role), 0); + + struct vrtd_req_get_num_devices req{}; + EXPECT_EQ(auth_request_get_num_devices(&cl, &req), 1); + EXPECT_NE(cl.role, nullptr); + EXPECT_TRUE(cl.role->query); +} + +TEST_F(AuthTest, EnsureRoleMergesUidUser) { + auto *uid_user = static_cast(calloc(1, sizeof(struct user_config))); + uid_user->name = strdup("testuser"); + uid_user->uid = getuid(); + ASSERT_EQ(role_ref_array_push(&uid_user->roles, fullaccess_role), 0); + + struct user_config *uid_ptr = uid_user; + ASSERT_EQ(user_config_ptr_array_push_move(&cfg.users, &uid_ptr), 0); + + struct vrtd_req_buffer_open req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_buffer_open(&cl, &req), 1); +} + +TEST_F(AuthTest, EnsureRoleMergesGidGroup) { + gid_t gid = getgid(); + ASSERT_EQ(gid_t_array_push(&cl.gids, gid), 0); + + auto *grp = static_cast(calloc(1, sizeof(struct group_config))); + grp->name = strdup("testgroup"); + grp->gid = gid; + ASSERT_EQ(role_ref_array_push(&grp->roles, fullaccess_role), 0); + + struct group_config *grp_ptr = grp; + ASSERT_EQ(group_config_ptr_array_push_move(&cfg.groups, &grp_ptr), 0); + + struct vrtd_req_design_write req{}; + req.dev_number = 0; + EXPECT_EQ(auth_request_design_write(&cl, &req), 1); +} + +TEST_F(AuthTest, EnsureRoleEmptyDefaultDeniesAll) { + struct vrtd_req_get_num_devices req{}; + EXPECT_EQ(auth_request_get_num_devices(&cl, &req), 0); +} diff --git a/vrt/vrtd/tests/config_test.cpp b/vrt/vrtd/tests/config_test.cpp new file mode 100644 index 00000000..bb4416b5 --- /dev/null +++ b/vrt/vrtd/tests/config_test.cpp @@ -0,0 +1,223 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include + +#include + +extern "C" { +#include "config.h" +} + +static struct device_policy *make_device_policy(const char *bdf, bool bar, bool qdma, bool buffer, + bool design_write, bool clock, bool pcie_hotplug, + bool raw_mem_access) { + auto *dp = static_cast(calloc(1, sizeof(struct device_policy))); + dp->bdf = strdup(bdf); + dp->bar = bar; + dp->qdma = qdma; + dp->buffer = buffer; + dp->design_write = design_write; + dp->clock = clock; + dp->pcie_hotplug = pcie_hotplug; + dp->raw_mem_access = raw_mem_access; + return dp; +} + +// --- role_merge_new --- + +TEST(RoleMergeTest, NewCreatesEmptyRole) { + struct role *r = nullptr; + ASSERT_EQ(role_merge_new(&r, "test_role"), 0); + ASSERT_NE(r, nullptr); + EXPECT_STREQ(r->name, "test_role"); + EXPECT_FALSE(r->query); + EXPECT_EQ(r->device_policies.len, 0u); + cleanup_role(r); +} + +// --- role_merge_add_role --- + +TEST(RoleMergeTest, MergeQueryFlag) { + struct role *dst = nullptr; + struct role *src = nullptr; + ASSERT_EQ(role_merge_new(&dst, "dst"), 0); + ASSERT_EQ(role_merge_new(&src, "src"), 0); + + EXPECT_FALSE(dst->query); + src->query = true; + + ASSERT_EQ(role_merge_add_role(dst, src), 0); + EXPECT_TRUE(dst->query); + + cleanup_role(src); + cleanup_role(dst); +} + +TEST(RoleMergeTest, MergeQueryOrSemantics) { + struct role *dst = nullptr; + struct role *src = nullptr; + ASSERT_EQ(role_merge_new(&dst, "dst"), 0); + ASSERT_EQ(role_merge_new(&src, "src"), 0); + + dst->query = true; + src->query = false; + + ASSERT_EQ(role_merge_add_role(dst, src), 0); + EXPECT_TRUE(dst->query); + + cleanup_role(src); + cleanup_role(dst); +} + +TEST(RoleMergeTest, MergeDevicePolicies) { + struct role *dst = nullptr; + struct role *src = nullptr; + ASSERT_EQ(role_merge_new(&dst, "dst"), 0); + ASSERT_EQ(role_merge_new(&src, "src"), 0); + + struct device_policy *dp = make_device_policy("0000:03:00", true, false, true, false, false, false, false); + struct device_policy *src_ptr = dp; + ASSERT_EQ(device_policy_ptr_array_push_move(&src->device_policies, &src_ptr), 0); + + ASSERT_EQ(role_merge_add_role(dst, src), 0); + + ASSERT_EQ(dst->device_policies.len, 1u); + EXPECT_STREQ(dst->device_policies.d[0]->bdf, "0000:03:00"); + EXPECT_TRUE(dst->device_policies.d[0]->bar); + EXPECT_FALSE(dst->device_policies.d[0]->qdma); + EXPECT_TRUE(dst->device_policies.d[0]->buffer); + + cleanup_role(src); + cleanup_role(dst); +} + +TEST(RoleMergeTest, MergeDevicePoliciesOrPerField) { + struct role *dst = nullptr; + struct role *src = nullptr; + ASSERT_EQ(role_merge_new(&dst, "dst"), 0); + ASSERT_EQ(role_merge_new(&src, "src"), 0); + + struct device_policy *dp1 = make_device_policy("0000:03:00", true, false, false, false, false, false, false); + struct device_policy *ptr1 = dp1; + ASSERT_EQ(device_policy_ptr_array_push_move(&dst->device_policies, &ptr1), 0); + + struct device_policy *dp2 = make_device_policy("0000:03:00", false, true, false, false, true, false, false); + struct device_policy *ptr2 = dp2; + ASSERT_EQ(device_policy_ptr_array_push_move(&src->device_policies, &ptr2), 0); + + ASSERT_EQ(role_merge_add_role(dst, src), 0); + + ASSERT_EQ(dst->device_policies.len, 1u); + EXPECT_TRUE(dst->device_policies.d[0]->bar); + EXPECT_TRUE(dst->device_policies.d[0]->qdma); + EXPECT_TRUE(dst->device_policies.d[0]->clock); + + cleanup_role(src); + cleanup_role(dst); +} + +TEST(RoleMergeTest, MergeDevicePoliciesDifferentBdf) { + struct role *dst = nullptr; + struct role *src = nullptr; + ASSERT_EQ(role_merge_new(&dst, "dst"), 0); + ASSERT_EQ(role_merge_new(&src, "src"), 0); + + struct device_policy *dp1 = make_device_policy("0000:03:00", true, false, false, false, false, false, false); + struct device_policy *ptr1 = dp1; + ASSERT_EQ(device_policy_ptr_array_push_move(&dst->device_policies, &ptr1), 0); + + struct device_policy *dp2 = make_device_policy("0000:04:00", false, true, false, false, false, false, false); + struct device_policy *ptr2 = dp2; + ASSERT_EQ(device_policy_ptr_array_push_move(&src->device_policies, &ptr2), 0); + + ASSERT_EQ(role_merge_add_role(dst, src), 0); + + ASSERT_EQ(dst->device_policies.len, 2u); + + cleanup_role(src); + cleanup_role(dst); +} + +TEST(RoleMergeTest, MergeWildcardPolicy) { + struct role *dst = nullptr; + struct role *src = nullptr; + ASSERT_EQ(role_merge_new(&dst, "dst"), 0); + ASSERT_EQ(role_merge_new(&src, "src"), 0); + + struct device_policy *dp = make_device_policy("any", true, true, true, true, true, true, true); + struct device_policy *ptr = dp; + ASSERT_EQ(device_policy_ptr_array_push_move(&src->device_policies, &ptr), 0); + + ASSERT_EQ(role_merge_add_role(dst, src), 0); + + ASSERT_EQ(dst->device_policies.len, 1u); + EXPECT_STREQ(dst->device_policies.d[0]->bdf, "any"); + EXPECT_TRUE(dst->device_policies.d[0]->bar); + EXPECT_TRUE(dst->device_policies.d[0]->raw_mem_access); + + cleanup_role(src); + cleanup_role(dst); +} + +// --- role_merge_add_array --- + +TEST(RoleMergeTest, MergeArray) { + struct role *dst = nullptr; + ASSERT_EQ(role_merge_new(&dst, "dst"), 0); + + struct role *r1 = nullptr; + struct role *r2 = nullptr; + ASSERT_EQ(role_merge_new(&r1, "r1"), 0); + ASSERT_EQ(role_merge_new(&r2, "r2"), 0); + + r1->query = true; + + struct device_policy *dp = make_device_policy("any", false, true, false, false, false, false, false); + struct device_policy *ptr = dp; + ASSERT_EQ(device_policy_ptr_array_push_move(&r2->device_policies, &ptr), 0); + + struct role_ref_array roles = role_ref_array_init(); + ASSERT_EQ(role_ref_array_push(&roles, r1), 0); + ASSERT_EQ(role_ref_array_push(&roles, r2), 0); + + ASSERT_EQ(role_merge_add_array(dst, &roles), 0); + + EXPECT_TRUE(dst->query); + ASSERT_EQ(dst->device_policies.len, 1u); + EXPECT_TRUE(dst->device_policies.d[0]->qdma); + + role_ref_array_free(&roles); + cleanup_role(r2); + cleanup_role(r1); + cleanup_role(dst); +} + +// --- Cleanup functions --- + +TEST(ConfigCleanupTest, CleanupDevicePolicyZeroed) { + auto *dp = static_cast(calloc(1, sizeof(struct device_policy))); + cleanup_device_policy(dp); +} + +TEST(ConfigCleanupTest, CleanupRoleZeroed) { + auto *r = static_cast(calloc(1, sizeof(struct role))); + cleanup_role(r); +} diff --git a/vrt/vrtd/tests/hotplug_test.cpp b/vrt/vrtd/tests/hotplug_test.cpp new file mode 100644 index 00000000..0e2abd85 --- /dev/null +++ b/vrt/vrtd/tests/hotplug_test.cpp @@ -0,0 +1,141 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include + +#include +#include + +extern "C" { +#include "hotplug.h" +} + +// --- pci_bdf_prefix --- + +TEST(PciBdfPrefixTest, StripsFunctionSuffix) { + char out[VRTD_PCI_BDF_LEN]; + EXPECT_EQ(pci_bdf_prefix("0000:65:00.2", out), 0); + EXPECT_STREQ(out, "0000:65:00"); +} + +TEST(PciBdfPrefixTest, AlreadyBoardLevel) { + char out[VRTD_PCI_BDF_LEN]; + EXPECT_EQ(pci_bdf_prefix("0000:65:00", out), 0); + EXPECT_STREQ(out, "0000:65:00"); +} + +TEST(PciBdfPrefixTest, NullBdf) { + char out[VRTD_PCI_BDF_LEN]; + EXPECT_EQ(pci_bdf_prefix(nullptr, out), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(PciBdfPrefixTest, NullOutput) { + EXPECT_EQ(pci_bdf_prefix("0000:65:00.2", nullptr), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(PciBdfPrefixTest, EmptyString) { + char out[VRTD_PCI_BDF_LEN]; + EXPECT_EQ(pci_bdf_prefix("", out), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(PciBdfPrefixTest, DotAtStart) { + char out[VRTD_PCI_BDF_LEN]; + EXPECT_EQ(pci_bdf_prefix(".2", out), 0); + EXPECT_STREQ(out, ".2"); +} + +// --- pci_bdf_set_function --- + +TEST(PciBdfSetFunctionTest, ReplacesFunction) { + char out[VRTD_PCI_BDF_LEN]; + EXPECT_EQ(pci_bdf_set_function("0000:65:00.0", 2, out), 0); + EXPECT_STREQ(out, "0000:65:00.2"); +} + +TEST(PciBdfSetFunctionTest, BoardLevelInput) { + char out[VRTD_PCI_BDF_LEN]; + EXPECT_EQ(pci_bdf_set_function("0000:65:00", 3, out), 0); + EXPECT_STREQ(out, "0000:65:00.3"); +} + +TEST(PciBdfSetFunctionTest, FunctionZero) { + char out[VRTD_PCI_BDF_LEN]; + EXPECT_EQ(pci_bdf_set_function("0000:65:00.7", 0, out), 0); + EXPECT_STREQ(out, "0000:65:00.0"); +} + +TEST(PciBdfSetFunctionTest, FunctionSeven) { + char out[VRTD_PCI_BDF_LEN]; + EXPECT_EQ(pci_bdf_set_function("0000:65:00.0", 7, out), 0); + EXPECT_STREQ(out, "0000:65:00.7"); +} + +TEST(PciBdfSetFunctionTest, FunctionTooLarge) { + char out[VRTD_PCI_BDF_LEN]; + EXPECT_EQ(pci_bdf_set_function("0000:65:00.0", 8, out), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(PciBdfSetFunctionTest, NullBdf) { + char out[VRTD_PCI_BDF_LEN]; + EXPECT_EQ(pci_bdf_set_function(nullptr, 0, out), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(PciBdfSetFunctionTest, NullOutput) { + EXPECT_EQ(pci_bdf_set_function("0000:65:00.0", 0, nullptr), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(PciBdfSetFunctionTest, EmptyString) { + char out[VRTD_PCI_BDF_LEN]; + EXPECT_EQ(pci_bdf_set_function("", 0, out), -1); + EXPECT_EQ(errno, EINVAL); +} + +// --- hotplug_errno_to_vrtd_ret --- + +TEST(HotplugErrnoTest, Einval) { + EXPECT_EQ(hotplug_errno_to_vrtd_ret(EINVAL), VRTD_RET_INVALID_ARGUMENT); +} + +TEST(HotplugErrnoTest, Enodev) { + EXPECT_EQ(hotplug_errno_to_vrtd_ret(ENODEV), VRTD_RET_NOEXIST); +} + +TEST(HotplugErrnoTest, Ebusy) { + EXPECT_EQ(hotplug_errno_to_vrtd_ret(EBUSY), VRTD_RET_BUSY); +} + +TEST(HotplugErrnoTest, Eperm) { + EXPECT_EQ(hotplug_errno_to_vrtd_ret(EPERM), VRTD_RET_AUTH_ERROR); +} + +TEST(HotplugErrnoTest, Eacces) { + EXPECT_EQ(hotplug_errno_to_vrtd_ret(EACCES), VRTD_RET_AUTH_ERROR); +} + +TEST(HotplugErrnoTest, UnknownErrno) { + EXPECT_EQ(hotplug_errno_to_vrtd_ret(ENOMEM), VRTD_RET_INTERNAL_ERROR); + EXPECT_EQ(hotplug_errno_to_vrtd_ret(EIO), VRTD_RET_INTERNAL_ERROR); +} diff --git a/vrt/vrtd/tests/test_helpers.hpp b/vrt/vrtd/tests/test_helpers.hpp new file mode 100644 index 00000000..a621583c --- /dev/null +++ b/vrt/vrtd/tests/test_helpers.hpp @@ -0,0 +1,82 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef VRTD_TEST_HELPERS_HPP +#define VRTD_TEST_HELPERS_HPP + +#include +#include +#include +#include +#include + +class ScopedEnv { + public: + explicit ScopedEnv(const char* name, std::optional value = std::nullopt) + : name_(name) { + const char* prev = std::getenv(name); + if (prev) { + oldValue_ = prev; + } + if (value) { + setenv(name, value->c_str(), 1); + } else { + unsetenv(name); + } + } + + ~ScopedEnv() { + if (oldValue_) { + setenv(name_.c_str(), oldValue_->c_str(), 1); + } else { + unsetenv(name_.c_str()); + } + } + + ScopedEnv(const ScopedEnv&) = delete; + ScopedEnv& operator=(const ScopedEnv&) = delete; + + private: + std::string name_; + std::optional oldValue_; +}; + +inline std::filesystem::path makeTempDir(const std::string& prefix) { + std::string tmpl = (std::filesystem::temp_directory_path() / (prefix + "-XXXXXX")).string(); + char* result = mkdtemp(tmpl.data()); + if (!result) { + throw std::runtime_error("Failed to create temp directory"); + } + return result; +} + +inline std::string writeTempFile(const std::filesystem::path& dir, const std::string& name, + const std::string& content) { + auto path = dir / name; + std::filesystem::create_directories(path.parent_path()); + std::ofstream ofs(path); + if (!ofs) { + throw std::runtime_error("Failed to create temp file: " + path.string()); + } + ofs << content; + ofs.close(); + return path.string(); +} + +#endif // VRTD_TEST_HELPERS_HPP diff --git a/vrt/vrtd/tests/utils_test.cpp b/vrt/vrtd/tests/utils_test.cpp new file mode 100644 index 00000000..ad81ff7d --- /dev/null +++ b/vrt/vrtd/tests/utils_test.cpp @@ -0,0 +1,127 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include + +extern "C" { +#include "utils.h" +} + +// --- string_to_bool --- + +TEST(StringToBoolTest, TruthyValues) { + EXPECT_TRUE(string_to_bool("1")); + EXPECT_TRUE(string_to_bool("y")); + EXPECT_TRUE(string_to_bool("Y")); + EXPECT_TRUE(string_to_bool("yes")); + EXPECT_TRUE(string_to_bool("YES")); + EXPECT_TRUE(string_to_bool("Yes")); + EXPECT_TRUE(string_to_bool("true")); + EXPECT_TRUE(string_to_bool("TRUE")); + EXPECT_TRUE(string_to_bool("True")); +} + +TEST(StringToBoolTest, FalsyValues) { + EXPECT_FALSE(string_to_bool("0")); + EXPECT_FALSE(string_to_bool("n")); + EXPECT_FALSE(string_to_bool("N")); + EXPECT_FALSE(string_to_bool("no")); + EXPECT_FALSE(string_to_bool("false")); + EXPECT_FALSE(string_to_bool("FALSE")); + EXPECT_FALSE(string_to_bool("")); + EXPECT_FALSE(string_to_bool("2")); + EXPECT_FALSE(string_to_bool("maybe")); +} + +TEST(StringToBoolTest, NullReturnsFalse) { + EXPECT_FALSE(string_to_bool(nullptr)); +} + +TEST(StringToBoolTest, WhitespaceTrimming) { + EXPECT_TRUE(string_to_bool(" yes ")); + EXPECT_TRUE(string_to_bool("\ttrue\n")); + EXPECT_TRUE(string_to_bool(" 1 ")); + EXPECT_FALSE(string_to_bool(" ")); +} + +// --- bit_ceil_u32 --- + +TEST(BitCeilU32Test, Zero) { + EXPECT_EQ(bit_ceil_u32(0u), 1u); +} + +TEST(BitCeilU32Test, One) { + EXPECT_EQ(bit_ceil_u32(1u), 1u); +} + +TEST(BitCeilU32Test, PowersOfTwo) { + EXPECT_EQ(bit_ceil_u32(2u), 2u); + EXPECT_EQ(bit_ceil_u32(4u), 4u); + EXPECT_EQ(bit_ceil_u32(8u), 8u); + EXPECT_EQ(bit_ceil_u32(0x80000000u), 0x80000000u); +} + +TEST(BitCeilU32Test, NonPowersRoundUp) { + EXPECT_EQ(bit_ceil_u32(3u), 4u); + EXPECT_EQ(bit_ceil_u32(5u), 8u); + EXPECT_EQ(bit_ceil_u32(7u), 8u); + EXPECT_EQ(bit_ceil_u32(9u), 16u); + EXPECT_EQ(bit_ceil_u32(100u), 128u); +} + +TEST(BitCeilU32Test, Overflow) { + EXPECT_EQ(bit_ceil_u32(0x80000001u), 0u); + EXPECT_EQ(bit_ceil_u32(0xFFFFFFFFu), 0u); +} + +// --- bit_ceil_u64 --- + +TEST(BitCeilU64Test, Zero) { + EXPECT_EQ(bit_ceil_u64(0ull), 1ull); +} + +TEST(BitCeilU64Test, One) { + EXPECT_EQ(bit_ceil_u64(1ull), 1ull); +} + +TEST(BitCeilU64Test, PowersOfTwo) { + EXPECT_EQ(bit_ceil_u64(2ull), 2ull); + EXPECT_EQ(bit_ceil_u64(0x100000000ull), 0x100000000ull); +} + +TEST(BitCeilU64Test, NonPowersRoundUp) { + EXPECT_EQ(bit_ceil_u64(3ull), 4ull); + EXPECT_EQ(bit_ceil_u64(5ull), 8ull); + EXPECT_EQ(bit_ceil_u64(0x100000001ull), 0x200000000ull); +} + +TEST(BitCeilU64Test, Overflow) { + EXPECT_EQ(bit_ceil_u64(0x8000000000000001ull), 0ull); +} + +// --- glob_err_to_string --- + +TEST(GlobErrTest, AllCodes) { + EXPECT_STREQ(glob_err_to_string(0), "OK"); + EXPECT_STREQ(glob_err_to_string(GLOB_NOSPACE), "out of memory"); + EXPECT_STREQ(glob_err_to_string(GLOB_ABORTED), "read error"); + EXPECT_STREQ(glob_err_to_string(GLOB_NOMATCH), "no matches found"); + EXPECT_STREQ(glob_err_to_string(999), "unknown glob(3) error"); +} From 3618a9d7eae9fbe89751b20101299d39268bb150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Oliver=20Opdenh=C3=B6vel?= Date: Tue, 21 Apr 2026 15:49:32 +0100 Subject: [PATCH 02/10] Temporarly also running the VRTD unit tests on the feature branch --- .github/workflows/vrtd-unit-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/vrtd-unit-test.yml b/.github/workflows/vrtd-unit-test.yml index 79bf6def..b40d5ce9 100644 --- a/.github/workflows/vrtd-unit-test.yml +++ b/.github/workflows/vrtd-unit-test.yml @@ -25,6 +25,7 @@ on: branches: - main - dev + - feature/vrtd_unit_tests pull_request: jobs: From 02eb9839085f0310cffa0ca3b8217a1055f54483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Oliver=20Opdenh=C3=B6vel?= Date: Tue, 21 Apr 2026 15:50:23 +0100 Subject: [PATCH 03/10] Using a wildcard for the feature branch --- .github/workflows/vrtd-unit-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/vrtd-unit-test.yml b/.github/workflows/vrtd-unit-test.yml index b40d5ce9..30c2d20c 100644 --- a/.github/workflows/vrtd-unit-test.yml +++ b/.github/workflows/vrtd-unit-test.yml @@ -25,7 +25,7 @@ on: branches: - main - dev - - feature/vrtd_unit_tests + - feature/* pull_request: jobs: From 1bfe40313a73d53a5444124a2ac5c8087a5ecd8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Oliver=20Opdenh=C3=B6vel?= Date: Tue, 21 Apr 2026 16:33:16 +0100 Subject: [PATCH 04/10] Adding unit tests for libslash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Oliver Opdenhövel --- driver/libslash/CMakeLists.txt | 18 +- driver/libslash/tests/CMakeLists.txt | 47 ++++++ driver/libslash/tests/ctldev_test.cpp | 219 +++++++++++++++++++++++++ driver/libslash/tests/hotplug_test.cpp | 104 ++++++++++++ driver/libslash/tests/qdma_test.cpp | 176 ++++++++++++++++++++ 5 files changed, 563 insertions(+), 1 deletion(-) create mode 100644 driver/libslash/tests/CMakeLists.txt create mode 100644 driver/libslash/tests/ctldev_test.cpp create mode 100644 driver/libslash/tests/hotplug_test.cpp create mode 100644 driver/libslash/tests/qdma_test.cpp diff --git a/driver/libslash/CMakeLists.txt b/driver/libslash/CMakeLists.txt index e609370b..b8a3cab7 100644 --- a/driver/libslash/CMakeLists.txt +++ b/driver/libslash/CMakeLists.txt @@ -34,16 +34,32 @@ message(STATUS "LIBSLASH version: ${LIBSLASH_VERSION} (${LIBSLASH_VERSION_MAJOR} project(libslash VERSION ${LIBSLASH_VERSION} - LANGUAGES C + LANGUAGES C CXX ) # Allow user to choose shared vs static (standard CMake variable) option(BUILD_SHARED_LIBS "Build shared libraries" ON) +option(ENABLE_SANITIZERS "Build with AddressSanitizer and UBSan" OFF) +if(ENABLE_SANITIZERS) + add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer) + add_link_options(-fsanitize=address,undefined) +endif() + +option(ENABLE_COVERAGE "Build with gcov coverage instrumentation" OFF) +if(ENABLE_COVERAGE) + if(ENABLE_SANITIZERS) + message(FATAL_ERROR "ENABLE_COVERAGE and ENABLE_SANITIZERS cannot be used together") + endif() + add_compile_options(--coverage -fno-inline) + add_link_options(--coverage) +endif() + include(GNUInstallDirs) include(CMakePackageConfigHelpers) add_subdirectory(src) +add_subdirectory(tests) # -------- Installation: headers and library -------- # Public headers are under include/ (layout: include/slash/*.h) diff --git a/driver/libslash/tests/CMakeLists.txt b/driver/libslash/tests/CMakeLists.txt new file mode 100644 index 00000000..d37fb072 --- /dev/null +++ b/driver/libslash/tests/CMakeLists.txt @@ -0,0 +1,47 @@ +# ################################################################################################## +# The MIT License (MIT) +# Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software +# and associated documentation files (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, publish, distribute, +# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or +# substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +# NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# ################################################################################################## + +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.zip +) +FetchContent_MakeAvailable(googletest) + +enable_testing() + +include(GoogleTest) + +add_custom_target(unit_tests) + +macro(add_slash_test test_name test_source) + add_executable(${test_name} ${test_source}) + target_link_libraries(${test_name} PRIVATE GTest::gtest_main GTest::gmock slash::slash) + set_target_properties(${test_name} PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED YES + ) + gtest_discover_tests(${test_name}) + add_dependencies(unit_tests ${test_name}) +endmacro() + +add_slash_test(ctldev_test ctldev_test.cpp) +add_slash_test(hotplug_test hotplug_test.cpp) +add_slash_test(qdma_test qdma_test.cpp) diff --git a/driver/libslash/tests/ctldev_test.cpp b/driver/libslash/tests/ctldev_test.cpp new file mode 100644 index 00000000..e6db386a --- /dev/null +++ b/driver/libslash/tests/ctldev_test.cpp @@ -0,0 +1,219 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include + +#include +#include +#include + +extern "C" { +#include +} + +static constexpr uint64_t MOCK_BAR_SIZE = 64ULL * 1024ULL * 1024ULL; +static constexpr const char *REAL_CTLDEV_PATH = "/dev/slash_ctl0"; + +// ─── Null / invalid argument tests (no hardware needed) ────────────────────── + +TEST(CtldevOpenTest, NullPath) { + errno = 0; + struct slash_ctldev *dev = slash_ctldev_open(nullptr); + EXPECT_EQ(dev, nullptr); + EXPECT_EQ(errno, EINVAL); +} + +TEST(CtldevCloseTest, NullHandle) { + errno = 0; + EXPECT_EQ(slash_ctldev_close(nullptr), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(CtldevBarFileCloseTest, NullHandle) { + errno = 0; + EXPECT_EQ(slash_bar_file_close(nullptr), -1); + EXPECT_EQ(errno, EINVAL); +} + +// ─── Mock-mode tests (no hardware needed) ──────────────────────────────────── + +class MockCtldevTest : public ::testing::Test { + protected: + void SetUp() override { + dev_ = slash_ctldev_open("@mock"); + ASSERT_NE(dev_, nullptr); + ASSERT_TRUE(dev_->mock); + } + + void TearDown() override { + if (dev_) { + slash_ctldev_close(dev_); + dev_ = nullptr; + } + } + + struct slash_ctldev *dev_ = nullptr; +}; + +TEST_F(MockCtldevTest, OpenReturnsMockHandle) { + EXPECT_TRUE(dev_->mock); + EXPECT_EQ(dev_->fd, -1); +} + +TEST_F(MockCtldevTest, CloseSucceeds) { + EXPECT_EQ(slash_ctldev_close(dev_), 0); + dev_ = nullptr; +} + +TEST_F(MockCtldevTest, DeviceInfoRead) { + struct slash_ioctl_device_info *info = slash_device_info_read(dev_); + ASSERT_NE(info, nullptr); + EXPECT_STREQ(info->bdf, "0000:00:00.0"); + slash_device_info_free(info); +} + +TEST_F(MockCtldevTest, DeviceInfoReadNullHandle) { + errno = 0; + EXPECT_EQ(slash_device_info_read(nullptr), nullptr); + EXPECT_EQ(errno, EINVAL); +} + +TEST_F(MockCtldevTest, BarInfoReadBar0Usable) { + struct slash_ioctl_bar_info *info = slash_bar_info_read(dev_, 0); + ASSERT_NE(info, nullptr); + EXPECT_NE(info->usable, 0); + EXPECT_EQ(info->length, MOCK_BAR_SIZE); + EXPECT_EQ(info->bar_number, 0); + slash_bar_info_free(info); +} + +TEST_F(MockCtldevTest, BarInfoReadNonZeroBarsNotUsable) { + for (int bar = 1; bar <= 5; ++bar) { + struct slash_ioctl_bar_info *info = slash_bar_info_read(dev_, bar); + ASSERT_NE(info, nullptr) << "bar=" << bar; + EXPECT_EQ(info->usable, 0) << "bar=" << bar; + slash_bar_info_free(info); + } +} + +TEST_F(MockCtldevTest, BarInfoReadNullHandle) { + errno = 0; + EXPECT_EQ(slash_bar_info_read(nullptr, 0), nullptr); + EXPECT_EQ(errno, EINVAL); +} + +TEST_F(MockCtldevTest, BarFileOpenBar0) { + struct slash_bar_file *bar = slash_bar_file_open(dev_, 0, 0); + ASSERT_NE(bar, nullptr); + EXPECT_NE(bar->map, nullptr); + EXPECT_EQ(bar->len, MOCK_BAR_SIZE); + EXPECT_TRUE(bar->mock); + EXPECT_EQ(slash_bar_file_close(bar), 0); +} + +TEST_F(MockCtldevTest, BarFileOpenNonZeroBarFails) { + errno = 0; + struct slash_bar_file *bar = slash_bar_file_open(dev_, 1, 0); + EXPECT_EQ(bar, nullptr); + EXPECT_EQ(errno, ENODEV); +} + +TEST_F(MockCtldevTest, BarFileOpenNullHandle) { + errno = 0; + EXPECT_EQ(slash_bar_file_open(nullptr, 0, 0), nullptr); + EXPECT_EQ(errno, EINVAL); +} + +TEST_F(MockCtldevTest, BarFileSyncIsNoopInMockMode) { + struct slash_bar_file *bar = slash_bar_file_open(dev_, 0, 0); + ASSERT_NE(bar, nullptr); + + EXPECT_EQ(slash_bar_file_start_write(bar), 0); + EXPECT_EQ(slash_bar_file_end_write(bar), 0); + EXPECT_EQ(slash_bar_file_start_read(bar), 0); + EXPECT_EQ(slash_bar_file_end_read(bar), 0); + + EXPECT_EQ(slash_bar_file_close(bar), 0); +} + +TEST_F(MockCtldevTest, BarFileMapIsReadWrite) { + struct slash_bar_file *bar = slash_bar_file_open(dev_, 0, 0); + ASSERT_NE(bar, nullptr); + + auto *p = static_cast(bar->map); + p[0] = 0xDEADBEEFu; + EXPECT_EQ(p[0], 0xDEADBEEFu); + + EXPECT_EQ(slash_bar_file_close(bar), 0); +} + +// ─── Real device tests (requires /dev/slash_ctl0) ──────────────────────────── + +class RealCtldevTest : public ::testing::Test { + protected: + void SetUp() override { + dev_ = slash_ctldev_open(REAL_CTLDEV_PATH); + if (!dev_) { + GTEST_SKIP() << REAL_CTLDEV_PATH << " not available (errno=" << errno << ")"; + } + } + + void TearDown() override { + if (dev_) { + slash_ctldev_close(dev_); + dev_ = nullptr; + } + } + + struct slash_ctldev *dev_ = nullptr; +}; + +TEST_F(RealCtldevTest, OpenSucceeds) { + EXPECT_FALSE(dev_->mock); + EXPECT_GE(dev_->fd, 0); +} + +TEST_F(RealCtldevTest, DeviceInfoBdfNonEmpty) { + struct slash_ioctl_device_info *info = slash_device_info_read(dev_); + ASSERT_NE(info, nullptr); + EXPECT_GT(strlen(info->bdf), 0u); + slash_device_info_free(info); +} + +TEST_F(RealCtldevTest, Bar0InfoUsable) { + struct slash_ioctl_bar_info *info = slash_bar_info_read(dev_, 0); + ASSERT_NE(info, nullptr); + EXPECT_NE(info->usable, 0); + EXPECT_GT(info->length, 0u); + slash_bar_info_free(info); +} + +TEST_F(RealCtldevTest, Bar0FileOpenAndSync) { + struct slash_bar_file *bar = slash_bar_file_open(dev_, 0, 0); + ASSERT_NE(bar, nullptr); + EXPECT_NE(bar->map, nullptr); + EXPECT_GT(bar->len, 0u); + EXPECT_FALSE(bar->mock); + + EXPECT_EQ(slash_bar_file_start_write(bar), 0); + EXPECT_EQ(slash_bar_file_end_write(bar), 0); + + EXPECT_EQ(slash_bar_file_close(bar), 0); +} diff --git a/driver/libslash/tests/hotplug_test.cpp b/driver/libslash/tests/hotplug_test.cpp new file mode 100644 index 00000000..6229b08e --- /dev/null +++ b/driver/libslash/tests/hotplug_test.cpp @@ -0,0 +1,104 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include + +#include + +extern "C" { +#include +} + +// ─── Null / invalid argument tests (no hardware needed) ────────────────────── + +TEST(HotplugOpenTest, NonexistentPathFails) { + errno = 0; + struct slash_hotplug *hp = slash_hotplug_open("/nonexistent/slash_hotplug"); + EXPECT_EQ(hp, nullptr); + EXPECT_EQ(errno, ENOENT); +} + +TEST(HotplugCloseTest, NullHandle) { + errno = 0; + EXPECT_EQ(slash_hotplug_close(nullptr), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(HotplugRescanTest, NullHandle) { + errno = 0; + EXPECT_EQ(slash_hotplug_rescan(nullptr), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(HotplugRemoveTest, NullHandle) { + errno = 0; + EXPECT_EQ(slash_hotplug_remove(nullptr, "0000:00:00.0"), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(HotplugToggleSbrTest, NullHandle) { + errno = 0; + EXPECT_EQ(slash_hotplug_toggle_sbr(nullptr, "0000:00:00.0"), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(HotplugHotplugTest, NullHandle) { + errno = 0; + EXPECT_EQ(slash_hotplug_hotplug(nullptr, "0000:00:00.0"), -1); + EXPECT_EQ(errno, EINVAL); +} + +// ─── Real device tests (requires /dev/slash_hotplug) ───────────────────────── + +class RealHotplugTest : public ::testing::Test { + protected: + void SetUp() override { + hp_ = slash_hotplug_open(SLASH_HOTPLUG_DEFAULT_PATH); + if (!hp_) { + GTEST_SKIP() << SLASH_HOTPLUG_DEFAULT_PATH + << " not available (errno=" << errno << ")"; + } + } + + void TearDown() override { + if (hp_) { + slash_hotplug_close(hp_); + hp_ = nullptr; + } + } + + struct slash_hotplug *hp_ = nullptr; +}; + +TEST_F(RealHotplugTest, OpenDefaultPathSucceeds) { + EXPECT_GE(hp_->fd, 0); +} + +TEST_F(RealHotplugTest, OpenExplicitPathSucceeds) { + struct slash_hotplug *hp2 = slash_hotplug_open("/dev/slash_hotplug"); + ASSERT_NE(hp2, nullptr); + EXPECT_GE(hp2->fd, 0); + EXPECT_EQ(slash_hotplug_close(hp2), 0); +} + +TEST_F(RealHotplugTest, CloseSucceeds) { + EXPECT_EQ(slash_hotplug_close(hp_), 0); + hp_ = nullptr; +} diff --git a/driver/libslash/tests/qdma_test.cpp b/driver/libslash/tests/qdma_test.cpp new file mode 100644 index 00000000..76390e9f --- /dev/null +++ b/driver/libslash/tests/qdma_test.cpp @@ -0,0 +1,176 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include + +#include +#include +#include + +extern "C" { +#include +} + +static constexpr const char *REAL_QDMA_PATH = "/dev/slash_qdma_ctl0"; +static constexpr uint64_t DDR_BASE_ADDRESS = 0x60000000000ULL; + +// ─── Null / invalid argument tests (no hardware needed) ────────────────────── + +TEST(QdmaOpenTest, NullPathFails) { + errno = 0; + EXPECT_EQ(slash_qdma_open(nullptr), nullptr); + EXPECT_EQ(errno, EINVAL); +} + +TEST(QdmaCloseTest, NullHandle) { + errno = 0; + EXPECT_EQ(slash_qdma_close(nullptr), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(QdmaInfoReadTest, NullHandle) { + struct slash_qdma_info info{}; + errno = 0; + EXPECT_EQ(slash_qdma_info_read(nullptr, &info), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(QdmaInfoReadTest, NullInfo) { + /* Construct a minimal fake handle — we only need errno set by the NULL info check. */ + struct slash_qdma fake{}; + fake.fd = -1; + errno = 0; + EXPECT_EQ(slash_qdma_info_read(&fake, nullptr), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(QdmaQpairAddTest, NullHandle) { + struct slash_qdma_qpair_add req{}; + errno = 0; + EXPECT_EQ(slash_qdma_qpair_add(nullptr, &req), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(QdmaQpairAddTest, NullReq) { + struct slash_qdma fake{}; + fake.fd = -1; + errno = 0; + EXPECT_EQ(slash_qdma_qpair_add(&fake, nullptr), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(QdmaQpairStartTest, NullHandle) { + errno = 0; + EXPECT_EQ(slash_qdma_qpair_start(nullptr, 0), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(QdmaQpairStopTest, NullHandle) { + errno = 0; + EXPECT_EQ(slash_qdma_qpair_stop(nullptr, 0), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(QdmaQpairDelTest, NullHandle) { + errno = 0; + EXPECT_EQ(slash_qdma_qpair_del(nullptr, 0), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(QdmaQpairGetFdTest, NullHandle) { + errno = 0; + EXPECT_EQ(slash_qdma_qpair_get_fd(nullptr, 0, 0), -1); + EXPECT_EQ(errno, EINVAL); +} + +// ─── Real device tests (requires /dev/slash_qdma_ctl0) ─────────────────────── + +class RealQdmaTest : public ::testing::Test { + protected: + void SetUp() override { + qdma_ = slash_qdma_open(REAL_QDMA_PATH); + if (!qdma_) { + GTEST_SKIP() << REAL_QDMA_PATH << " not available (errno=" << errno << ")"; + } + } + + void TearDown() override { + if (qdma_) { + slash_qdma_close(qdma_); + qdma_ = nullptr; + } + } + + struct slash_qdma *qdma_ = nullptr; +}; + +TEST_F(RealQdmaTest, OpenSucceeds) { + EXPECT_GE(qdma_->fd, 0); + EXPECT_FALSE(qdma_->mock); +} + +TEST_F(RealQdmaTest, InfoRead) { + struct slash_qdma_info info{}; + EXPECT_EQ(slash_qdma_info_read(qdma_, &info), 0); +} + +TEST_F(RealQdmaTest, QueueDmaTransfer) { + static constexpr size_t XFER_SIZE = 4096; + + // Add a Memory-Mapped queue pair with both H2C and C2H enabled. + struct slash_qdma_qpair_add req{}; + req.mode = 0; /* QDMA_Q_MODE_MM */ + req.dir_mask = 0x3; /* H2C | C2H */ + req.h2c_ring_sz = 0; + req.c2h_ring_sz = 0; + req.cmpt_ring_sz = 0; + + ASSERT_EQ(slash_qdma_qpair_add(qdma_, &req), 0); + uint32_t qid = req.qid; + + ASSERT_EQ(slash_qdma_qpair_start(qdma_, qid), 0); + + int queue_fd = slash_qdma_qpair_get_fd(qdma_, qid, 0); + ASSERT_GE(queue_fd, 0); + + // Write a known pattern to DDR (H2C). + uint8_t src[XFER_SIZE]; + for (size_t i = 0; i < XFER_SIZE; ++i) { + src[i] = static_cast(i & 0xFF); + } + ssize_t written = pwrite(queue_fd, src, XFER_SIZE, static_cast(DDR_BASE_ADDRESS)); + EXPECT_EQ(written, static_cast(XFER_SIZE)); + + // Read back from DDR (C2H) and verify. + uint8_t dst[XFER_SIZE]{}; + ssize_t read_bytes = pread(queue_fd, dst, XFER_SIZE, static_cast(DDR_BASE_ADDRESS)); + EXPECT_EQ(read_bytes, static_cast(XFER_SIZE)); + EXPECT_EQ(std::memcmp(src, dst, XFER_SIZE), 0); + + EXPECT_EQ(close(queue_fd), 0); + + EXPECT_EQ(slash_qdma_qpair_stop(qdma_, qid), 0); + EXPECT_EQ(slash_qdma_qpair_del(qdma_, qid), 0); +} + +TEST_F(RealQdmaTest, CloseSucceeds) { + EXPECT_EQ(slash_qdma_close(qdma_), 0); + qdma_ = nullptr; +} From e3491ad4b8291ca5df90dda2d26f8b3b792f2bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Oliver=20Opdenh=C3=B6vel?= Date: Tue, 21 Apr 2026 16:53:37 +0100 Subject: [PATCH 05/10] Also testing the mock QDMA system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Oliver Opdenhövel --- driver/libslash/tests/ctldev_test.cpp | 2 +- driver/libslash/tests/qdma_test.cpp | 44 ++++++++++++++++----------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/driver/libslash/tests/ctldev_test.cpp b/driver/libslash/tests/ctldev_test.cpp index e6db386a..c18cbbef 100644 --- a/driver/libslash/tests/ctldev_test.cpp +++ b/driver/libslash/tests/ctldev_test.cpp @@ -37,7 +37,7 @@ TEST(CtldevOpenTest, NullPath) { errno = 0; struct slash_ctldev *dev = slash_ctldev_open(nullptr); EXPECT_EQ(dev, nullptr); - EXPECT_EQ(errno, EINVAL); + EXPECT_EQ(errno, EFAULT); } TEST(CtldevCloseTest, NullHandle) { diff --git a/driver/libslash/tests/qdma_test.cpp b/driver/libslash/tests/qdma_test.cpp index 76390e9f..1256bb89 100644 --- a/driver/libslash/tests/qdma_test.cpp +++ b/driver/libslash/tests/qdma_test.cpp @@ -33,26 +33,26 @@ static constexpr uint64_t DDR_BASE_ADDRESS = 0x60000000000ULL; // ─── Null / invalid argument tests (no hardware needed) ────────────────────── -TEST(QdmaOpenTest, NullPathFails) { +TEST(QdmaNullTest, Open) { errno = 0; EXPECT_EQ(slash_qdma_open(nullptr), nullptr); EXPECT_EQ(errno, EINVAL); } -TEST(QdmaCloseTest, NullHandle) { +TEST(QdmaNullTest, Close) { errno = 0; EXPECT_EQ(slash_qdma_close(nullptr), -1); EXPECT_EQ(errno, EINVAL); } -TEST(QdmaInfoReadTest, NullHandle) { +TEST(QdmaNullTest, NullInfoRead) { struct slash_qdma_info info{}; errno = 0; EXPECT_EQ(slash_qdma_info_read(nullptr, &info), -1); EXPECT_EQ(errno, EINVAL); } -TEST(QdmaInfoReadTest, NullInfo) { +TEST(QdmaNullTest, FakeInfoRead) { /* Construct a minimal fake handle — we only need errno set by the NULL info check. */ struct slash_qdma fake{}; fake.fd = -1; @@ -61,14 +61,14 @@ TEST(QdmaInfoReadTest, NullInfo) { EXPECT_EQ(errno, EINVAL); } -TEST(QdmaQpairAddTest, NullHandle) { +TEST(QdmaNullTest, NullQpairAdd) { struct slash_qdma_qpair_add req{}; errno = 0; EXPECT_EQ(slash_qdma_qpair_add(nullptr, &req), -1); EXPECT_EQ(errno, EINVAL); } -TEST(QdmaQpairAddTest, NullReq) { +TEST(QdmaNullTest, FakeQpairAdd) { struct slash_qdma fake{}; fake.fd = -1; errno = 0; @@ -76,25 +76,25 @@ TEST(QdmaQpairAddTest, NullReq) { EXPECT_EQ(errno, EINVAL); } -TEST(QdmaQpairStartTest, NullHandle) { +TEST(QdmaNullTest, QpairStart) { errno = 0; EXPECT_EQ(slash_qdma_qpair_start(nullptr, 0), -1); EXPECT_EQ(errno, EINVAL); } -TEST(QdmaQpairStopTest, NullHandle) { +TEST(QdmaNullTest, QpairStop) { errno = 0; EXPECT_EQ(slash_qdma_qpair_stop(nullptr, 0), -1); EXPECT_EQ(errno, EINVAL); } -TEST(QdmaQpairDelTest, NullHandle) { +TEST(QdmaNullTest, QpairDel) { errno = 0; EXPECT_EQ(slash_qdma_qpair_del(nullptr, 0), -1); EXPECT_EQ(errno, EINVAL); } -TEST(QdmaQpairGetFdTest, NullHandle) { +TEST(QdmaNullTest, QpaiGetFd) { errno = 0; EXPECT_EQ(slash_qdma_qpair_get_fd(nullptr, 0, 0), -1); EXPECT_EQ(errno, EINVAL); @@ -102,12 +102,18 @@ TEST(QdmaQpairGetFdTest, NullHandle) { // ─── Real device tests (requires /dev/slash_qdma_ctl0) ─────────────────────── -class RealQdmaTest : public ::testing::Test { +class ParametrizedQdmaTest : public ::testing::TestWithParam { protected: void SetUp() override { - qdma_ = slash_qdma_open(REAL_QDMA_PATH); - if (!qdma_) { - GTEST_SKIP() << REAL_QDMA_PATH << " not available (errno=" << errno << ")"; + bool mock = GetParam(); + if (mock) { + qdma_ = slash_qdma_open("@mock"); + EXPECT_NE(qdma_, nullptr); + } else { + qdma_ = slash_qdma_open(REAL_QDMA_PATH); + if (!qdma_) { + GTEST_SKIP() << REAL_QDMA_PATH << " not available (errno=" << errno << ")"; + } } } @@ -121,17 +127,17 @@ class RealQdmaTest : public ::testing::Test { struct slash_qdma *qdma_ = nullptr; }; -TEST_F(RealQdmaTest, OpenSucceeds) { +TEST_P(ParametrizedQdmaTest, OpenSucceeds) { EXPECT_GE(qdma_->fd, 0); EXPECT_FALSE(qdma_->mock); } -TEST_F(RealQdmaTest, InfoRead) { +TEST_P(ParametrizedQdmaTest, InfoRead) { struct slash_qdma_info info{}; EXPECT_EQ(slash_qdma_info_read(qdma_, &info), 0); } -TEST_F(RealQdmaTest, QueueDmaTransfer) { +TEST_P(ParametrizedQdmaTest, QueueDmaTransfer) { static constexpr size_t XFER_SIZE = 4096; // Add a Memory-Mapped queue pair with both H2C and C2H enabled. @@ -170,7 +176,9 @@ TEST_F(RealQdmaTest, QueueDmaTransfer) { EXPECT_EQ(slash_qdma_qpair_del(qdma_, qid), 0); } -TEST_F(RealQdmaTest, CloseSucceeds) { +TEST_P(ParametrizedQdmaTest, CloseSucceeds) { EXPECT_EQ(slash_qdma_close(qdma_), 0); qdma_ = nullptr; } + +INSTANTIATE_TEST_SUITE_P(QdmaTest, ParametrizedQdmaTest, testing::Values(true, false)); \ No newline at end of file From 84263f6b6fe12df4f4c579cc3d1282f679a5d2eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Oliver=20Opdenh=C3=B6vel?= Date: Wed, 22 Apr 2026 08:52:11 +0100 Subject: [PATCH 06/10] Expecting the mock flag to be set in mock mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Oliver Opdenhövel --- driver/libslash/tests/qdma_test.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/driver/libslash/tests/qdma_test.cpp b/driver/libslash/tests/qdma_test.cpp index 1256bb89..cca18b6c 100644 --- a/driver/libslash/tests/qdma_test.cpp +++ b/driver/libslash/tests/qdma_test.cpp @@ -104,8 +104,10 @@ TEST(QdmaNullTest, QpaiGetFd) { class ParametrizedQdmaTest : public ::testing::TestWithParam { protected: + bool mock; + void SetUp() override { - bool mock = GetParam(); + mock = GetParam(); if (mock) { qdma_ = slash_qdma_open("@mock"); EXPECT_NE(qdma_, nullptr); @@ -129,7 +131,7 @@ class ParametrizedQdmaTest : public ::testing::TestWithParam { TEST_P(ParametrizedQdmaTest, OpenSucceeds) { EXPECT_GE(qdma_->fd, 0); - EXPECT_FALSE(qdma_->mock); + EXPECT_EQ(qdma_->mock, mock); } TEST_P(ParametrizedQdmaTest, InfoRead) { From 1feb6462547b3212412565d2f7566889255fe14a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Oliver=20Opdenh=C3=B6vel?= Date: Wed, 22 Apr 2026 10:06:34 +0100 Subject: [PATCH 07/10] Adding QDMA mocking to libslash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Oliver Opdenhövel --- driver/libslash/include/slash/qdma.h | 9 +- driver/libslash/src/CMakeLists.txt | 1 + driver/libslash/src/qdma.c | 39 +++- driver/libslash/src/qdma_mock.c | 259 +++++++++++++++++++++++++++ driver/libslash/src/qdma_mock.h | 37 ++++ driver/libslash/tests/qdma_test.cpp | 4 +- 6 files changed, 341 insertions(+), 8 deletions(-) create mode 100644 driver/libslash/src/qdma_mock.c create mode 100644 driver/libslash/src/qdma_mock.h diff --git a/driver/libslash/include/slash/qdma.h b/driver/libslash/include/slash/qdma.h index 04e67037..8d726544 100644 --- a/driver/libslash/include/slash/qdma.h +++ b/driver/libslash/include/slash/qdma.h @@ -45,7 +45,6 @@ #include "uapi/slash_interface.h" -#include #include #ifdef __cplusplus @@ -55,11 +54,13 @@ extern "C" { /** * @brief Handle to an open QDMA device. * - * \@mock is reserved for future use and is always set to false. + * \@priv is NULL for real hardware handles. When slash_qdma_open() is + * called with "\@mock", it points to an internal slash_qdma_mock context; + * callers should treat it as opaque. */ struct slash_qdma { - int fd; /**< File descriptor for the QDMA character device. */ - bool mock; /**< Reserved for mock support. */ + int fd; /**< File descriptor for the QDMA character device (-1 in mock mode). */ + void *priv; /**< Opaque mock context, or NULL for real hardware. */ }; /** diff --git a/driver/libslash/src/CMakeLists.txt b/driver/libslash/src/CMakeLists.txt index 8b01f719..5c8abf5b 100644 --- a/driver/libslash/src/CMakeLists.txt +++ b/driver/libslash/src/CMakeLists.txt @@ -24,6 +24,7 @@ add_library(slash ${CMAKE_CURRENT_SOURCE_DIR}/ctldev_mock.c ${CMAKE_CURRENT_SOURCE_DIR}/hotplug.c ${CMAKE_CURRENT_SOURCE_DIR}/qdma.c + ${CMAKE_CURRENT_SOURCE_DIR}/qdma_mock.c ) # Provide an alias target with namespace (nice for internal use too) diff --git a/driver/libslash/src/qdma.c b/driver/libslash/src/qdma.c index 7d8a4b91..68c38b6d 100644 --- a/driver/libslash/src/qdma.c +++ b/driver/libslash/src/qdma.c @@ -29,12 +29,15 @@ #include +#include "qdma_mock.h" + #include #include #include #include #include #include +#include #include @@ -47,6 +50,10 @@ struct slash_qdma *slash_qdma_open(const char *path) return NULL; } + if (strcmp(path, "@mock") == 0) { + return slash_qdma_mock_open(); + } + qdma = calloc(1, sizeof(*qdma)); if (qdma == NULL) { return NULL; @@ -58,8 +65,6 @@ struct slash_qdma *slash_qdma_open(const char *path) return NULL; } - qdma->mock = false; - return qdma; } @@ -72,6 +77,10 @@ int slash_qdma_close(struct slash_qdma *qdma) return -1; } + if (qdma->priv) { + return slash_qdma_mock_close(qdma); + } + ret = 0; if (qdma->fd >= 0 && close(qdma->fd) != 0) { ret = -1; @@ -93,6 +102,10 @@ int slash_qdma_info_read(struct slash_qdma *qdma, struct slash_qdma_info *info) return -1; } + if (qdma->priv) { + return slash_qdma_mock_info_read(qdma, info); + } + memset(&tmp, 0, sizeof(tmp)); tmp.size = sizeof(tmp); @@ -125,6 +138,10 @@ int slash_qdma_qpair_add(struct slash_qdma *qdma, return -1; } + if (qdma->priv) { + return slash_qdma_mock_qpair_add(qdma, req); + } + memset(&tmp, 0, sizeof(tmp)); tmp.size = sizeof(tmp); tmp.mode = req->mode; @@ -162,6 +179,20 @@ static int slash_qdma_qpair_op(struct slash_qdma *qdma, return -1; } + if (qdma->priv) { + switch (op) { + case SLASH_QDMA_QUEUE_OP_START: + return slash_qdma_mock_qpair_start(qdma, qid); + case SLASH_QDMA_QUEUE_OP_STOP: + return slash_qdma_mock_qpair_stop(qdma, qid); + case SLASH_QDMA_QUEUE_OP_DEL: + return slash_qdma_mock_qpair_del(qdma, qid); + default: + errno = EINVAL; + return -1; + } + } + memset(&req, 0, sizeof(req)); req.size = sizeof(req); req.qid = qid; @@ -200,6 +231,10 @@ int slash_qdma_qpair_get_fd(struct slash_qdma *qdma, uint32_t qid, int flags) return -1; } + if (qdma->priv) { + return slash_qdma_mock_qpair_get_fd(qdma, qid, flags); + } + memset(&req, 0, sizeof(req)); req.size = sizeof(req); req.qid = qid; diff --git a/driver/libslash/src/qdma_mock.c b/driver/libslash/src/qdma_mock.c new file mode 100644 index 00000000..92a24c6c --- /dev/null +++ b/driver/libslash/src/qdma_mock.c @@ -0,0 +1,259 @@ +/** + * Copyright (C) 2025 Advanced Micro Devices, Inc. All rights reserved. + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; if + * not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * @file qdma_mock.c + * @brief Mock QDMA implementation backed by memfd files. + * + * Each queue pair's I/O fd is a memfd_create() anonymous file. The kernel + * supports pread()/pwrite() at arbitrary offsets on memfds (tmpfs), so the + * test's DDR_BASE_ADDRESS offset is handled transparently via sparse pages. + * + * Queue state is tracked in a fixed-size table (QDMA_MOCK_MAX_QUEUES slots) + * stored in the slash_qdma_mock struct pointed to by qdma->priv. + */ + +#define _GNU_SOURCE + +#include "qdma_mock.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#define QDMA_MOCK_MAX_QUEUES 64 + +struct slash_qdma_mock_qpair { + bool in_use; + bool started; + int fd; /* backing memfd; -1 when slot is free */ +}; + +struct slash_qdma_mock { + struct slash_qdma_mock_qpair queues[QDMA_MOCK_MAX_QUEUES]; +}; + +static struct slash_qdma_mock *mock_ctx(struct slash_qdma *qdma) +{ + return (struct slash_qdma_mock *) qdma->priv; +} + +struct slash_qdma *slash_qdma_mock_open(void) +{ + struct slash_qdma *qdma; + struct slash_qdma_mock *ctx; + size_t i; + + qdma = calloc(1, sizeof(*qdma)); + if (qdma == NULL) { + return NULL; + } + + ctx = calloc(1, sizeof(*ctx)); + if (ctx == NULL) { + free(qdma); + return NULL; + } + + for (i = 0; i < QDMA_MOCK_MAX_QUEUES; ++i) { + ctx->queues[i].fd = -1; + } + + qdma->fd = -1; + qdma->priv = ctx; + + return qdma; +} + +int slash_qdma_mock_close(struct slash_qdma *qdma) +{ + struct slash_qdma_mock *ctx; + size_t i; + + if (qdma == NULL) { + errno = EINVAL; + return -1; + } + + ctx = mock_ctx(qdma); + + for (i = 0; i < QDMA_MOCK_MAX_QUEUES; ++i) { + if (ctx->queues[i].in_use && ctx->queues[i].fd >= 0) { + (void) close(ctx->queues[i].fd); + } + } + + free(ctx); + free(qdma); + + return 0; +} + +int slash_qdma_mock_info_read(struct slash_qdma *qdma, struct slash_qdma_info *info) +{ + if (qdma == NULL || info == NULL) { + errno = EINVAL; + return -1; + } + + memset(info, 0, sizeof(*info)); + info->size = sizeof(*info); + info->qsets_max = QDMA_MOCK_MAX_QUEUES; + info->msix_qvecs = 1; + + return 0; +} + +int slash_qdma_mock_qpair_add(struct slash_qdma *qdma, struct slash_qdma_qpair_add *req) +{ + struct slash_qdma_mock *ctx; + size_t i; + int fd; + + if (qdma == NULL || req == NULL) { + errno = EINVAL; + return -1; + } + + ctx = mock_ctx(qdma); + + for (i = 0; i < QDMA_MOCK_MAX_QUEUES; ++i) { + if (!ctx->queues[i].in_use) { + break; + } + } + + if (i == QDMA_MOCK_MAX_QUEUES) { + errno = ENOSPC; + return -1; + } + + fd = memfd_create("slash_qdma_mock", MFD_CLOEXEC); + if (fd < 0) { + return -1; + } + + ctx->queues[i].in_use = true; + ctx->queues[i].started = false; + ctx->queues[i].fd = fd; + + req->qid = (uint32_t) i; + + return 0; +} + +static int mock_qpair_op(struct slash_qdma *qdma, uint32_t qid, bool start) +{ + struct slash_qdma_mock *ctx; + + if (qdma == NULL) { + errno = EINVAL; + return -1; + } + + if (qid >= QDMA_MOCK_MAX_QUEUES) { + errno = EINVAL; + return -1; + } + + ctx = mock_ctx(qdma); + + if (!ctx->queues[qid].in_use) { + errno = EINVAL; + return -1; + } + + ctx->queues[qid].started = start; + + return 0; +} + +int slash_qdma_mock_qpair_start(struct slash_qdma *qdma, uint32_t qid) +{ + return mock_qpair_op(qdma, qid, true); +} + +int slash_qdma_mock_qpair_stop(struct slash_qdma *qdma, uint32_t qid) +{ + return mock_qpair_op(qdma, qid, false); +} + +int slash_qdma_mock_qpair_del(struct slash_qdma *qdma, uint32_t qid) +{ + struct slash_qdma_mock *ctx; + + if (qdma == NULL) { + errno = EINVAL; + return -1; + } + + if (qid >= QDMA_MOCK_MAX_QUEUES) { + errno = EINVAL; + return -1; + } + + ctx = mock_ctx(qdma); + + if (!ctx->queues[qid].in_use) { + errno = EINVAL; + return -1; + } + + if (ctx->queues[qid].fd >= 0) { + (void) close(ctx->queues[qid].fd); + } + + memset(&ctx->queues[qid], 0, sizeof(ctx->queues[qid])); + ctx->queues[qid].fd = -1; + + return 0; +} + +int slash_qdma_mock_qpair_get_fd(struct slash_qdma *qdma, uint32_t qid, int flags) +{ + struct slash_qdma_mock *ctx; + int new_fd; + (void) flags; /* O_CLOEXEC already set on the memfd */ + + if (qdma == NULL) { + errno = EINVAL; + return -1; + } + + if (qid >= QDMA_MOCK_MAX_QUEUES) { + errno = EINVAL; + return -1; + } + + ctx = mock_ctx(qdma); + + if (!ctx->queues[qid].in_use || !ctx->queues[qid].started) { + errno = EINVAL; + return -1; + } + + /* dup so the caller owns a separate fd they can close independently */ + new_fd = dup(ctx->queues[qid].fd); + if (new_fd < 0) { + return -1; + } + + return new_fd; +} diff --git a/driver/libslash/src/qdma_mock.h b/driver/libslash/src/qdma_mock.h new file mode 100644 index 00000000..36f3d596 --- /dev/null +++ b/driver/libslash/src/qdma_mock.h @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2025 Advanced Micro Devices, Inc. All rights reserved. + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; if + * not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * @file qdma_mock.h + * @brief Mock QDMA implementation for testing without hardware. + */ + +#ifndef LIBSLASH_QDMA_MOCK_H +#define LIBSLASH_QDMA_MOCK_H + +#include +#include + +#include + +struct slash_qdma *slash_qdma_mock_open(void); +int slash_qdma_mock_close(struct slash_qdma *qdma); +int slash_qdma_mock_info_read(struct slash_qdma *qdma, struct slash_qdma_info *info); +int slash_qdma_mock_qpair_add(struct slash_qdma *qdma, struct slash_qdma_qpair_add *req); +int slash_qdma_mock_qpair_start(struct slash_qdma *qdma, uint32_t qid); +int slash_qdma_mock_qpair_stop(struct slash_qdma *qdma, uint32_t qid); +int slash_qdma_mock_qpair_del(struct slash_qdma *qdma, uint32_t qid); +int slash_qdma_mock_qpair_get_fd(struct slash_qdma *qdma, uint32_t qid, int flags); + +#endif /* LIBSLASH_QDMA_MOCK_H */ diff --git a/driver/libslash/tests/qdma_test.cpp b/driver/libslash/tests/qdma_test.cpp index cca18b6c..5b024111 100644 --- a/driver/libslash/tests/qdma_test.cpp +++ b/driver/libslash/tests/qdma_test.cpp @@ -130,8 +130,8 @@ class ParametrizedQdmaTest : public ::testing::TestWithParam { }; TEST_P(ParametrizedQdmaTest, OpenSucceeds) { - EXPECT_GE(qdma_->fd, 0); - EXPECT_EQ(qdma_->mock, mock); + EXPECT_GE(qdma_->fd, mock ? -1 : 0); + EXPECT_EQ(qdma_->priv != nullptr, mock); } TEST_P(ParametrizedQdmaTest, InfoRead) { From 6f66a52cf73be431455c368bb9b4cb7c7de975a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Oliver=20Opdenh=C3=B6vel?= Date: Wed, 22 Apr 2026 11:43:56 +0100 Subject: [PATCH 08/10] Adding some more tests to VRTD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Oliver Opdenhövel --- driver/libslash/CMakeLists.txt | 6 +- vrt/vrtd/CMakeLists.txt | 5 +- vrt/vrtd/tests/CMakeLists.txt | 3 + vrt/vrtd/tests/buffer_test.cpp | 229 +++++++++++++++++++++++ vrt/vrtd/tests/design_writer_test.cpp | 253 ++++++++++++++++++++++++++ vrt/vrtd/tests/device_test.cpp | 184 +++++++++++++++++++ 6 files changed, 678 insertions(+), 2 deletions(-) create mode 100644 vrt/vrtd/tests/buffer_test.cpp create mode 100644 vrt/vrtd/tests/design_writer_test.cpp create mode 100644 vrt/vrtd/tests/device_test.cpp diff --git a/driver/libslash/CMakeLists.txt b/driver/libslash/CMakeLists.txt index b8a3cab7..7ecc1f6b 100644 --- a/driver/libslash/CMakeLists.txt +++ b/driver/libslash/CMakeLists.txt @@ -39,6 +39,7 @@ project(libslash # Allow user to choose shared vs static (standard CMake variable) option(BUILD_SHARED_LIBS "Build shared libraries" ON) +option(LIBSLASH_BUILD_TESTS "Build unit tests" OFF) option(ENABLE_SANITIZERS "Build with AddressSanitizer and UBSan" OFF) if(ENABLE_SANITIZERS) @@ -59,7 +60,10 @@ include(GNUInstallDirs) include(CMakePackageConfigHelpers) add_subdirectory(src) -add_subdirectory(tests) + +if(LIBSLASH_BUILD_TESTS) + add_subdirectory(tests) +endif() # -------- Installation: headers and library -------- # Public headers are under include/ (layout: include/slash/*.h) diff --git a/vrt/vrtd/CMakeLists.txt b/vrt/vrtd/CMakeLists.txt index 70723929..554552e3 100644 --- a/vrt/vrtd/CMakeLists.txt +++ b/vrt/vrtd/CMakeLists.txt @@ -39,6 +39,7 @@ project(vrtd # Optionally build examples option(VRTD_BUILD_EXAMPLES "Build example executables" OFF) +option(VRTD_BUILD_TESTS "Build unit tests" OFF) option(VRTD_INCLUDE_LIBSLASH "Include libslash subdirectory instead of building from system" OFF) option(BUILD_SHARED_LIBS "Build shared libraries" ON) @@ -83,7 +84,9 @@ add_subdirectory(src) add_subdirectory(libvrtd) add_subdirectory(libvrtdpp) -add_subdirectory(tests) +if(VRTD_BUILD_TESTS) + add_subdirectory(tests) +endif() #if(VRTD_BUILD_EXAMPLES) # add_subdirectory(examples) diff --git a/vrt/vrtd/tests/CMakeLists.txt b/vrt/vrtd/tests/CMakeLists.txt index b54f955b..f5197f45 100644 --- a/vrt/vrtd/tests/CMakeLists.txt +++ b/vrt/vrtd/tests/CMakeLists.txt @@ -28,3 +28,6 @@ add_vrtd_test(utils_test utils_test.cpp) add_vrtd_test(hotplug_test hotplug_test.cpp) add_vrtd_test(config_test config_test.cpp) add_vrtd_test(auth_test auth_test.cpp) +add_vrtd_test(buffer_test buffer_test.cpp) +add_vrtd_test(design_writer_test design_writer_test.cpp) +add_vrtd_test(device_test device_test.cpp) diff --git a/vrt/vrtd/tests/buffer_test.cpp b/vrt/vrtd/tests/buffer_test.cpp new file mode 100644 index 00000000..078f5819 --- /dev/null +++ b/vrt/vrtd/tests/buffer_test.cpp @@ -0,0 +1,229 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include + +#include +#include +#include +#include +#include + +extern "C" { +#include +#include "allocator.h" +#include "buffer.h" +} + +static constexpr const char *REAL_QDMA_PATH = "/dev/slash_qdma_ctl0"; +static constexpr uint64_t XFER_SIZE = 4096; +static constexpr uint64_t CLIENT_ID = 42; + +// ─── Null / argument validation (no hardware needed, always run) ────────────── + +TEST(BufferNullTest, NullQdma) { + struct device_memory_map *map = device_memory_map_create(); + ASSERT_NE(map, nullptr); + struct buffer *buf = buffer_create(nullptr, map, ALLOCATION_TYPE_DDR, + VRTD_ALLOC_DIR_HOST_TO_DEVICE, + XFER_SIZE, 0, CLIENT_ID, nullptr); + EXPECT_EQ(buf, nullptr); + device_memory_map_cleanup(map); +} + +TEST(BufferNullTest, NullMap) { + struct slash_qdma *qdma = slash_qdma_open("@mock"); + ASSERT_NE(qdma, nullptr); + struct buffer *buf = buffer_create(qdma, nullptr, ALLOCATION_TYPE_DDR, + VRTD_ALLOC_DIR_HOST_TO_DEVICE, + XFER_SIZE, 0, CLIENT_ID, nullptr); + EXPECT_EQ(buf, nullptr); + slash_qdma_close(qdma); +} + +TEST(BufferNullTest, ZeroSize) { + struct slash_qdma *qdma = slash_qdma_open("@mock"); + ASSERT_NE(qdma, nullptr); + struct device_memory_map *map = device_memory_map_create(); + ASSERT_NE(map, nullptr); + struct buffer *buf = buffer_create(qdma, map, ALLOCATION_TYPE_DDR, + VRTD_ALLOC_DIR_HOST_TO_DEVICE, + 0, 0, CLIENT_ID, nullptr); + EXPECT_EQ(buf, nullptr); + device_memory_map_cleanup(map); + slash_qdma_close(qdma); +} + +TEST(BufferNullTest, ZeroClientId) { + struct slash_qdma *qdma = slash_qdma_open("@mock"); + ASSERT_NE(qdma, nullptr); + struct device_memory_map *map = device_memory_map_create(); + ASSERT_NE(map, nullptr); + struct buffer *buf = buffer_create(qdma, map, ALLOCATION_TYPE_DDR, + VRTD_ALLOC_DIR_HOST_TO_DEVICE, + XFER_SIZE, 0, 0, nullptr); + EXPECT_EQ(buf, nullptr); + device_memory_map_cleanup(map); + slash_qdma_close(qdma); +} + +TEST(BufferNullTest, InvalidDirection) { + struct slash_qdma *qdma = slash_qdma_open("@mock"); + ASSERT_NE(qdma, nullptr); + struct device_memory_map *map = device_memory_map_create(); + ASSERT_NE(map, nullptr); + struct buffer *buf = buffer_create(qdma, map, ALLOCATION_TYPE_DDR, + static_cast(99), + XFER_SIZE, 0, CLIENT_ID, nullptr); + EXPECT_EQ(buf, nullptr); + device_memory_map_cleanup(map); + slash_qdma_close(qdma); +} + +TEST(BufferNullTest, CleanupNull) { + cleanup_buffer(nullptr); +} + +TEST(BufferNullTest, RawNullQdma) { + struct buffer *buf = buffer_create_raw(nullptr, DDR_START_ADDRESS, XFER_SIZE, + VRTD_ALLOC_DIR_HOST_TO_DEVICE); + EXPECT_EQ(buf, nullptr); + EXPECT_EQ(errno, EINVAL); +} + +TEST(BufferNullTest, RawZeroSize) { + struct slash_qdma *qdma = slash_qdma_open("@mock"); + ASSERT_NE(qdma, nullptr); + struct buffer *buf = buffer_create_raw(qdma, DDR_START_ADDRESS, 0, + VRTD_ALLOC_DIR_HOST_TO_DEVICE); + EXPECT_EQ(buf, nullptr); + EXPECT_EQ(errno, EINVAL); + slash_qdma_close(qdma); +} + +// ─── Parameterized fixture (mock + real hardware) ──────────────────────────── + +class BufferTest : public ::testing::TestWithParam { + protected: + bool mock; + struct slash_qdma *qdma_ = nullptr; + struct device_memory_map *map_ = nullptr; + + void SetUp() override { + mock = GetParam(); + if (mock) { + qdma_ = slash_qdma_open("@mock"); + ASSERT_NE(qdma_, nullptr); + } else { + qdma_ = slash_qdma_open(REAL_QDMA_PATH); + if (qdma_ == nullptr) { + GTEST_SKIP() << REAL_QDMA_PATH << " not available (errno=" << errno << ")"; + } + } + map_ = device_memory_map_create(); + ASSERT_NE(map_, nullptr); + } + + void TearDown() override { + device_memory_map_cleanup(map_); + map_ = nullptr; + if (qdma_) { + slash_qdma_close(qdma_); + qdma_ = nullptr; + } + } +}; + +TEST_P(BufferTest, LifecycleBidirectional) { + struct buffer *buf = buffer_create(qdma_, map_, ALLOCATION_TYPE_DDR, + VRTD_ALLOC_DIR_BIDIRECTIONAL, + XFER_SIZE, 0, CLIENT_ID, nullptr); + ASSERT_NE(buf, nullptr); + EXPECT_GE(buf->fd, 0); + + uint8_t src[XFER_SIZE]; + for (size_t i = 0; i < XFER_SIZE; ++i) + src[i] = static_cast(i & 0xFF); + + ssize_t written = pwrite(buf->fd, src, XFER_SIZE, static_cast(buf->addr)); + EXPECT_EQ(written, static_cast(XFER_SIZE)); + + uint8_t dst[XFER_SIZE]{}; + ssize_t read_bytes = pread(buf->fd, dst, XFER_SIZE, static_cast(buf->addr)); + EXPECT_EQ(read_bytes, static_cast(XFER_SIZE)); + EXPECT_EQ(std::memcmp(src, dst, XFER_SIZE), 0); + + cleanup_buffer(buf); +} + +TEST_P(BufferTest, RawCreateAndIO) { + struct buffer *buf = buffer_create_raw(qdma_, DDR_START_ADDRESS, XFER_SIZE, + VRTD_ALLOC_DIR_BIDIRECTIONAL); + ASSERT_NE(buf, nullptr); + EXPECT_GE(buf->fd, 0); + EXPECT_EQ(buf->addr, DDR_START_ADDRESS); + EXPECT_FALSE(buf->allocation_valid); + + uint8_t src[XFER_SIZE]; + std::memset(src, 0xCD, sizeof(src)); + ssize_t written = pwrite(buf->fd, src, XFER_SIZE, static_cast(DDR_START_ADDRESS)); + EXPECT_EQ(written, static_cast(XFER_SIZE)); + + uint8_t dst[XFER_SIZE]{}; + ssize_t n = pread(buf->fd, dst, XFER_SIZE, static_cast(DDR_START_ADDRESS)); + EXPECT_EQ(n, static_cast(XFER_SIZE)); + EXPECT_EQ(std::memcmp(src, dst, XFER_SIZE), 0); + + cleanup_buffer(buf); +} + +TEST_P(BufferTest, QueueExhaustion) { + /* The mock QDMA supports 64 queues (QDMA_MOCK_MAX_QUEUES). + * Real hardware queue limits vary and exhaustion may not be reachable, + * so this test is restricted to mock mode. */ + if (!mock) { + GTEST_SKIP() << "Queue exhaustion test is mock-only"; + } + + static constexpr int MAX_QUEUES = 64; + std::vector bufs; + bufs.reserve(MAX_QUEUES); + + for (int i = 0; i < MAX_QUEUES; ++i) { + struct buffer *buf = buffer_create_raw(qdma_, DDR_START_ADDRESS + i * XFER_SIZE, + XFER_SIZE, VRTD_ALLOC_DIR_HOST_TO_DEVICE); + ASSERT_NE(buf, nullptr) << "Expected success for queue " << i; + bufs.push_back(buf); + } + + /* 65th allocation must fail */ + struct buffer *overflow = buffer_create_raw(qdma_, DDR_START_ADDRESS, + XFER_SIZE, VRTD_ALLOC_DIR_HOST_TO_DEVICE); + EXPECT_EQ(overflow, nullptr); + EXPECT_EQ(errno, ENOSPC); + + for (struct buffer *b : bufs) + cleanup_buffer(b); +} + +INSTANTIATE_TEST_SUITE_P(BufferTest, BufferTest, testing::Values(true, false), + [](const testing::TestParamInfo &info) { + return info.param ? "Mock" : "RealHardware"; + }); diff --git a/vrt/vrtd/tests/design_writer_test.cpp b/vrt/vrtd/tests/design_writer_test.cpp new file mode 100644 index 00000000..b45c1cf7 --- /dev/null +++ b/vrt/vrtd/tests/design_writer_test.cpp @@ -0,0 +1,253 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * All tests here run against the mock QDMA only. Writing an arbitrary payload to + * a real FPGA via QDMA at the design-writer address risks corrupting the device + * bitstream, so real-hardware execution is intentionally excluded. + * + * The mock QDMA fd is a memfd_create() file, so lseek()+write() at the fixed + * QDMA bitstream address (0x102100000) works via sparse pages — no hardware needed. + */ + +#include + +#include +#include +#include +#include +#include + +extern "C" { +#include +#include "design_writer.h" +} + +// ─── Helper: create a small in-memory bitstream fd ─────────────────────────── + +static int make_bitstream_fd(uint8_t fill, size_t len) +{ + int fd = memfd_create("bitstream", MFD_CLOEXEC); + if (fd < 0) + return -1; + + std::vector buf(len, fill); + if (write(fd, buf.data(), len) != static_cast(len)) { + close(fd); + return -1; + } + + if (lseek(fd, 0, SEEK_SET) < 0) { + close(fd); + return -1; + } + + return fd; +} + +// ─── Null / argument validation (always run, no fixture needed) ─────────────── + +TEST(DesignWriterNullTest, CreateNullQdma) { + EXPECT_EQ(design_writer_create(nullptr), nullptr); +} + +TEST(DesignWriterNullTest, SubmitFdNullWriter) { + int fd = make_bitstream_fd(0xAB, 4096); + ASSERT_GE(fd, 0); + EXPECT_EQ(design_writer_submit_fd(nullptr, fd), -1); + close(fd); +} + +TEST(DesignWriterNullTest, SubmitAsyncNullWriter) { + int fd = make_bitstream_fd(0xAB, 4096); + ASSERT_GE(fd, 0); + EXPECT_EQ(design_writer_submit_fd_async(nullptr, fd), -1); + close(fd); +} + +TEST(DesignWriterNullTest, SubmitAsyncInvalidFd) { + struct slash_qdma *qdma = slash_qdma_open("@mock"); + ASSERT_NE(qdma, nullptr); + struct design_writer *dw = design_writer_create(qdma); + ASSERT_NE(dw, nullptr); + + EXPECT_EQ(design_writer_submit_fd_async(dw, -1), -1); + + cleanup_design_writer(dw); + slash_qdma_close(qdma); +} + +TEST(DesignWriterNullTest, PollResultNullWriter) { + bool done = false; + int last_error = 0; + EXPECT_EQ(design_writer_poll_result(nullptr, &done, &last_error), -1); +} + +TEST(DesignWriterNullTest, PollResultNullDone) { + struct slash_qdma *qdma = slash_qdma_open("@mock"); + ASSERT_NE(qdma, nullptr); + struct design_writer *dw = design_writer_create(qdma); + ASSERT_NE(dw, nullptr); + + int last_error = 0; + EXPECT_EQ(design_writer_poll_result(dw, nullptr, &last_error), -1); + + cleanup_design_writer(dw); + slash_qdma_close(qdma); +} + +TEST(DesignWriterNullTest, PollResultNullLastError) { + struct slash_qdma *qdma = slash_qdma_open("@mock"); + ASSERT_NE(qdma, nullptr); + struct design_writer *dw = design_writer_create(qdma); + ASSERT_NE(dw, nullptr); + + bool done = false; + EXPECT_EQ(design_writer_poll_result(dw, &done, nullptr), -1); + + cleanup_design_writer(dw); + slash_qdma_close(qdma); +} + +TEST(DesignWriterNullTest, IsBusyNullWriter) { + EXPECT_FALSE(design_writer_is_busy(nullptr)); +} + +// ─── Mock-only fixture ──────────────────────────────────────────────────────── + +class DesignWriterTest : public ::testing::Test { + protected: + struct slash_qdma *qdma_ = nullptr; + struct design_writer *writer_ = nullptr; + + void SetUp() override { + qdma_ = slash_qdma_open("@mock"); + ASSERT_NE(qdma_, nullptr); + writer_ = design_writer_create(qdma_); + ASSERT_NE(writer_, nullptr); + } + + void TearDown() override { + cleanup_design_writer(writer_); + writer_ = nullptr; + slash_qdma_close(qdma_); + qdma_ = nullptr; + } +}; + +TEST_F(DesignWriterTest, CreateDestroy) { + /* Fixture already creates and will destroy — just verify the handle is valid. */ + EXPECT_NE(writer_, nullptr); + EXPECT_TRUE(writer_->thread_started); + EXPECT_TRUE(writer_->qpair_created); + EXPECT_TRUE(writer_->qpair_started); + EXPECT_GE(writer_->fd, 0); +} + +TEST_F(DesignWriterTest, NotBusyInitially) { + EXPECT_FALSE(design_writer_is_busy(writer_)); +} + +TEST_F(DesignWriterTest, SyncTransfer) { + int fd = make_bitstream_fd(0xAB, 4096); + ASSERT_GE(fd, 0); + + /* design_writer_submit_fd closes fd on completion — do not close it ourselves. */ + EXPECT_EQ(design_writer_submit_fd(writer_, fd), 0); + EXPECT_FALSE(design_writer_is_busy(writer_)); +} + +TEST_F(DesignWriterTest, AsyncTransferPoll) { + int fd = make_bitstream_fd(0xCD, 8192); + ASSERT_GE(fd, 0); + + ASSERT_EQ(design_writer_submit_fd_async(writer_, fd), 0); + + bool done = false; + int last_error = 0; + /* Spin until the worker thread finishes (should be very fast on memfd). */ + for (int attempts = 0; attempts < 10000 && !done; ++attempts) { + ASSERT_EQ(design_writer_poll_result(writer_, &done, &last_error), 0); + if (!done) + usleep(1000); + } + + EXPECT_TRUE(done); + EXPECT_EQ(last_error, 0); +} + +TEST_F(DesignWriterTest, IsBusyTransitions) { + int fd = make_bitstream_fd(0xEF, 4096); + ASSERT_GE(fd, 0); + + EXPECT_FALSE(design_writer_is_busy(writer_)); + + ASSERT_EQ(design_writer_submit_fd_async(writer_, fd), 0); + + /* Spin until done, verifying is_busy is false afterwards. */ + bool done = false; + int last_error = 0; + for (int i = 0; i < 10000 && !done; ++i) { + ASSERT_EQ(design_writer_poll_result(writer_, &done, &last_error), 0); + if (!done) + usleep(1000); + } + ASSERT_TRUE(done); + EXPECT_FALSE(design_writer_is_busy(writer_)); +} + +TEST_F(DesignWriterTest, DoubleSubmitRejected) { + /* Submit a large payload so the first transfer is still running when we try again. */ + int fd1 = make_bitstream_fd(0x11, 64 * 1024); + ASSERT_GE(fd1, 0); + ASSERT_EQ(design_writer_submit_fd_async(writer_, fd1), 0); + + /* Second submit while busy must fail. */ + int fd2 = make_bitstream_fd(0x22, 4096); + ASSERT_GE(fd2, 0); + int ret = design_writer_submit_fd_async(writer_, fd2); + if (ret != -1) { + /* Transfer finished before we got here — acceptable, clean up fd2. */ + /* fd2 is now owned by the writer; wait for it to finish. */ + bool done = false; int err = 0; + for (int i = 0; i < 10000 && !done; ++i) { + design_writer_poll_result(writer_, &done, &err); + if (!done) usleep(1000); + } + } else { + /* Got the expected rejection — fd2 was not consumed, close it ourselves. */ + EXPECT_EQ(ret, -1); + close(fd2); + /* Let fd1's transfer finish before TearDown. */ + bool done = false; int err = 0; + for (int i = 0; i < 10000 && !done; ++i) { + design_writer_poll_result(writer_, &done, &err); + if (!done) usleep(1000); + } + } +} + +TEST_F(DesignWriterTest, CleanupWhileIdle) { + /* Immediate cleanup after create — TearDown does this, but we exercise it + * explicitly here with a freshly created writer to confirm no deadlock. */ + struct design_writer *dw = design_writer_create(qdma_); + ASSERT_NE(dw, nullptr); + cleanup_design_writer(dw); +} diff --git a/vrt/vrtd/tests/device_test.cpp b/vrt/vrtd/tests/device_test.cpp new file mode 100644 index 00000000..36518c10 --- /dev/null +++ b/vrt/vrtd/tests/device_test.cpp @@ -0,0 +1,184 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * Tests for device.c. + * + * device_open() and devices_contains_path() are static, so they are not + * directly callable from tests. The testable public surface is: + * + * cleanup_device() — exercised by constructing struct device + * instances manually with mock handles. + * devices_discover_and_open() — exercised only when /dev/slash_ctl* nodes + * are present; skipped otherwise. + * + * All cleanup tests use mock ctldev/qdma handles and are hardware-independent. + */ + +#include + +#include +#include + +extern "C" { +#include +#include +#include +#include "allocator.h" +#include "buffer.h" +#include "design_writer.h" +#include "device.h" +} + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +/** Allocate a zeroed struct device and set a mock path string. */ +static struct device *alloc_mock_device(void) +{ + struct device *d = static_cast(calloc(1, sizeof(*d))); + if (d == nullptr) + return nullptr; + d->path = strdup("@mock"); + d->buffers = buffer_ptr_array_init(); + return d; +} + +// ─── cleanup_device() tests (always run, mock handles only) ────────────────── + +TEST(DeviceCleanupTest, CleanupNull) { + /* Must be a silent no-op. */ + cleanup_device(nullptr); +} + +TEST(DeviceCleanupTest, CleanupMinimal) { + /* A calloc'd device with only a path set — all subsystem pointers are NULL. */ + struct device *d = alloc_mock_device(); + ASSERT_NE(d, nullptr); + /* Ownership of d passes to cleanup_device (it calls free). */ + cleanup_device(d); +} + +TEST(DeviceCleanupTest, CleanupWithCtl) { + struct device *d = alloc_mock_device(); + ASSERT_NE(d, nullptr); + + d->ctl = slash_ctldev_open("@mock"); + ASSERT_NE(d->ctl, nullptr); + + cleanup_device(d); +} + +TEST(DeviceCleanupTest, CleanupWithCtlAndQdma) { + struct device *d = alloc_mock_device(); + ASSERT_NE(d, nullptr); + + d->ctl = slash_ctldev_open("@mock"); + ASSERT_NE(d->ctl, nullptr); + d->qdma = slash_qdma_open("@mock"); + ASSERT_NE(d->qdma, nullptr); + + cleanup_device(d); +} + +TEST(DeviceCleanupTest, CleanupWithDesignWriter) { + struct device *d = alloc_mock_device(); + ASSERT_NE(d, nullptr); + + d->ctl = slash_ctldev_open("@mock"); + ASSERT_NE(d->ctl, nullptr); + d->qdma = slash_qdma_open("@mock"); + ASSERT_NE(d->qdma, nullptr); + d->design_writer = design_writer_create(d->qdma); + ASSERT_NE(d->design_writer, nullptr); + + cleanup_device(d); +} + +TEST(DeviceCleanupTest, CleanupWithBars) { + struct device *d = alloc_mock_device(); + ASSERT_NE(d, nullptr); + + d->ctl = slash_ctldev_open("@mock"); + ASSERT_NE(d->ctl, nullptr); + d->qdma = slash_qdma_open("@mock"); + ASSERT_NE(d->qdma, nullptr); + + /* Mock ctldev provides a usable BAR 0 (64 MB, backed by a temp file). */ + d->bar_info[0] = slash_bar_info_read(d->ctl, 0); + ASSERT_NE(d->bar_info[0], nullptr); + EXPECT_TRUE(d->bar_info[0]->usable); + + d->bar_files[0] = slash_bar_file_open(d->ctl, 0, O_CLOEXEC); + ASSERT_NE(d->bar_files[0], nullptr); + + /* BARs 1-5 are not usable in the mock — populate bar_info only (no bar_file). */ + for (int i = 1; i < 6; ++i) { + d->bar_info[i] = slash_bar_info_read(d->ctl, i); + ASSERT_NE(d->bar_info[i], nullptr); + EXPECT_FALSE(d->bar_info[i]->usable); + } + + cleanup_device(d); +} + +TEST(DeviceCleanupTest, CleanupWithBuffers) { + struct device *d = alloc_mock_device(); + ASSERT_NE(d, nullptr); + + d->ctl = slash_ctldev_open("@mock"); + ASSERT_NE(d->ctl, nullptr); + d->qdma = slash_qdma_open("@mock"); + ASSERT_NE(d->qdma, nullptr); + + /* Allocate a raw buffer on the mock QDMA and hand ownership to d->buffers. */ + struct buffer *buf = buffer_create_raw(d->qdma, DDR_START_ADDRESS, 4096, + VRTD_ALLOC_DIR_HOST_TO_DEVICE); + ASSERT_NE(buf, nullptr); + + int ret = buffer_ptr_array_push_move(&d->buffers, &buf); + ASSERT_EQ(ret, 0); + EXPECT_EQ(buf, nullptr); /* ownership transferred */ + EXPECT_EQ(d->buffers.len, 1u); + + cleanup_device(d); +} + +// ─── devices_discover_and_open() — real hardware only ──────────────────────── + +TEST(DeviceDiscoveryTest, DiscoverAndOpen) { + struct device_ptr_array devices = device_ptr_array_init(); + int ret = devices_discover_and_open(&devices); + + if (devices.len == 0) { + device_ptr_array_free(&devices); + GTEST_SKIP() << "No /dev/slash_ctl* devices found — skipping hardware test"; + } + + EXPECT_EQ(ret, 0); + EXPECT_GT(devices.len, 0u); + + /* Each discovered device must have at least a control handle. */ + for (size_t i = 0; i < devices.len; ++i) { + EXPECT_NE(devices.d[i], nullptr); + EXPECT_NE(devices.d[i]->ctl, nullptr); + } + + device_ptr_array_free(&devices); +} From 9066f9f53842b25b2f94e259ccc11e274dea1a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Oliver=20Opdenh=C3=B6vel?= Date: Wed, 22 Apr 2026 11:45:47 +0100 Subject: [PATCH 09/10] Fixing the CMake arguments and the library path for the VRTD unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Oliver Opdenhövel --- .github/workflows/vrtd-unit-test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/vrtd-unit-test.yml b/.github/workflows/vrtd-unit-test.yml index 30c2d20c..2fc92da6 100644 --- a/.github/workflows/vrtd-unit-test.yml +++ b/.github/workflows/vrtd-unit-test.yml @@ -49,8 +49,9 @@ jobs: run: | mkdir vrt/vrtd/build cd vrt/vrtd/build - cmake -DVRTD_INCLUDE_LIBSLASH=1 -DENABLE_COVERAGE=1 .. + cmake -DVRTD_BUILD_TESTS=1 -DVRTD_INCLUDE_LIBSLASH=1 -DENABLE_COVERAGE=1 .. make unit_tests -j$(nproc) + export LD_LIBRARY_PATH=$(pwd)/libslash/src/:$LD_LIBRARY_PATH cd tests && ctest --output-on-failure - name: Generate coverage report From bb9a393f1f6e5ce3214931bea48fb3ebf5569304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Oliver=20Opdenh=C3=B6vel?= Date: Wed, 22 Apr 2026 11:47:23 +0100 Subject: [PATCH 10/10] Disabling the VRTD unit testing workflow on feature branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Oliver Opdenhövel --- .github/workflows/vrtd-unit-test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/vrtd-unit-test.yml b/.github/workflows/vrtd-unit-test.yml index 2fc92da6..5d967b60 100644 --- a/.github/workflows/vrtd-unit-test.yml +++ b/.github/workflows/vrtd-unit-test.yml @@ -25,7 +25,6 @@ on: branches: - main - dev - - feature/* pull_request: jobs: