diff --git a/.github/workflows/vrt-unit-test.yml b/.github/workflows/vrt-unit-test.yml new file mode 100644 index 00000000..570732e1 --- /dev/null +++ b/.github/workflows/vrt-unit-test.yml @@ -0,0 +1,74 @@ +# ################################################################################################## +# 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. +# ################################################################################################## + +name: VRT unit testing + +on: + push: + branches: + - main + - dev + pull_request: + +jobs: + vrt_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 upgrade -y + sudo apt install -y cmake pkg-config ninja-build \ + libxml2-dev libzmq3-dev libjsoncpp-dev zlib1g-dev \ + libsystemd-dev libinih-dev libcli11-dev lcov python3-zmq + + - name: Build and run VRT unit tests + run: | + mkdir vrt/build + cd vrt/build + cmake -DVRT_INCLUDE_VRTD=1 -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/build + lcov --capture --directory . --output-file coverage.info \ + --ignore-errors mismatch --ignore-errors negative + lcov --remove coverage.info \ + '/usr/*' '*/build/_deps/*' '*/vrtd/*' '*/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: vrt-coverage-report + path: vrt/build/coverage-report/ + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 880a6f96..7deb1aed 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,7 @@ Thumbs.db __pycache__/ *.pyc +.venv/ # Build files for package /pbuild/ diff --git a/requirements.txt b/requirements.txt index f2a982f9..a73d287b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +pyzmq jinja2 pytest pytest-cov diff --git a/submodules/AVED b/submodules/AVED new file mode 160000 index 00000000..839b4ad6 --- /dev/null +++ b/submodules/AVED @@ -0,0 +1 @@ +Subproject commit 839b4ad6a75433ab6a43f9f95790a61c2b85bb16 diff --git a/vrt/CMakeLists.txt b/vrt/CMakeLists.txt index e0b1d230..bdd8c2b8 100644 --- a/vrt/CMakeLists.txt +++ b/vrt/CMakeLists.txt @@ -43,6 +43,7 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) option(VRT_INCLUDE_VRTD "Include vrtd subdirectory instead of building from system" OFF) +option(VRT_BUILD_TESTS "Build unit tests" OFF) include(GNUInstallDirs) include(CMakePackageConfigHelpers) @@ -55,6 +56,15 @@ if(ENABLE_SANITIZERS) 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() + # Generate the header configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/cmake/vrt_version.hpp.in" @@ -131,6 +141,13 @@ target_link_libraries(vrt PkgConfig::PC_ZMQ ) +# Testing +if(VRT_BUILD_TESTS) + add_subdirectory(tests) +endif() + +# Installation + set_target_properties(vrt PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) install( diff --git a/vrt/include/vrt/kernel.hpp b/vrt/include/vrt/kernel.hpp index 43910a3a..48b75971 100644 --- a/vrt/include/vrt/kernel.hpp +++ b/vrt/include/vrt/kernel.hpp @@ -229,6 +229,12 @@ class Kernel { */ void setVrtdBar(const std::optional& bar); + /** + * @brief Sets the ZeroMQ server for emulation and simulation. + * @param server The ZeroMQ server handle. + */ + void setServer(std::shared_ptr server); + /** * @brief Writes a value to a register. * @param offset The offset of the register. diff --git a/vrt/include/vrt/streaming_buffer.hpp b/vrt/include/vrt/streaming_buffer.hpp index d0255b5a..a37525de 100644 --- a/vrt/include/vrt/streaming_buffer.hpp +++ b/vrt/include/vrt/streaming_buffer.hpp @@ -23,7 +23,7 @@ #include -#include "api/device.hpp" +#include "device.hpp" #include "qdma/qdma_connection.hpp" #include "qdma/qdma_intf.hpp" #include "utils/platform.hpp" @@ -110,7 +110,7 @@ template StreamingBuffer::StreamingBuffer(Device device, Kernel kernel, const std::string& portName, size_t size) : device(device), size(size), kernel(kernel), portName(portName) { - std::vector qdmaConnections = device.getQdmaConnections(); + std::vector qdmaConnections = device.getHandle()->getQdmaConnections(); bool gotQdma = false; for (const auto& con : qdmaConnections) { if (con.getKernel() == kernel.getName() && portName == con.getInterface()) { @@ -162,7 +162,7 @@ template void StreamingBuffer::sync() { Platform platform = device.getPlatform(); if (platform == Platform::EMULATION) { - ZmqServer* server = device.getHandle()->getZmqServer(); + auto server = device.getHandle()->getZmqServer(); if (syncType == StreamDirection::HOST_TO_DEVICE) { std::vector sendData; std::size_t dataSize = size * sizeof(T); diff --git a/vrt/src/device.cpp b/vrt/src/device.cpp index cf8865a6..22eab66c 100644 --- a/vrt/src/device.cpp +++ b/vrt/src/device.cpp @@ -306,12 +306,16 @@ void Device::parseSystemMap() { clockFreq = parser.getClockFrequency(); this->platform = parser.getPlatform(); kernels = parser.getKernels(); + + std::optional barHandle = std::nullopt; if (platform == Platform::HARDWARE && vrtdDevice.has_value()) { - std::optional barHandle = vrtdDevice->getBar(bar); - for (auto& kernel : kernels) { - kernel.second.setVrtdBar(barHandle); - kernel.second.setPlatform(platform); - } + barHandle = vrtdDevice->getBar(bar); + } + + for (auto&kernel : kernels) { + kernel.second.setPlatform(platform); + kernel.second.setVrtdBar(barHandle); + kernel.second.setServer(zmqServer); } this->qdmaConnections = parser.getQdmaConnections(); } diff --git a/vrt/src/kernel.cpp b/vrt/src/kernel.cpp index d02e2598..637cd29e 100644 --- a/vrt/src/kernel.cpp +++ b/vrt/src/kernel.cpp @@ -139,6 +139,8 @@ uint32_t Kernel::read(uint32_t offset) { void Kernel::setVrtdBar(const std::optional& bar) { this->vrtdBar = bar; } +void Kernel::setServer(std::shared_ptr server) { this->server = server; } + void Kernel::setFunctionalArgs(const std::vector& args) { functionalArgs = args; std::sort(functionalArgs.begin(), functionalArgs.end(), diff --git a/vrt/tests/CMakeLists.txt b/vrt/tests/CMakeLists.txt new file mode 100644 index 00000000..267425e6 --- /dev/null +++ b/vrt/tests/CMakeLists.txt @@ -0,0 +1,92 @@ +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_vrt_test test_name test_source) + add_executable(${test_name} ${test_source}) + target_link_libraries(${test_name} PRIVATE GTest::gtest_main GTest::gmock vrt::vrt) + gtest_discover_tests(${test_name}) + add_dependencies(unit_tests ${test_name}) +endmacro() + +add_vrt_test(register_test register_test.cpp) +add_vrt_test(qdma_connection_test qdma_connection_test.cpp) +add_vrt_test(logger_test logger_test.cpp) +add_vrt_test(utilization_data_test utilization_data_test.cpp) +add_vrt_test(filesystem_cache_test filesystem_cache_test.cpp) +add_vrt_test(xml_parser_test xml_parser_test.cpp) +add_vrt_test(utilization_parser_test utilization_parser_test.cpp) +add_vrt_test(kernel_test kernel_test.cpp) +add_vrt_test(vrtbin_test vrtbin_test.cpp) + +# --- Stub VBIN generation --- +set(STUB_VBIN_DIR ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/stub_vbin) + +set(STUB_EMU_VBIN ${CMAKE_CURRENT_BINARY_DIR}/stub_emu.vbin) +add_custom_command( + OUTPUT ${STUB_EMU_VBIN} + COMMAND ${CMAKE_COMMAND} -E rm -rf ${CMAKE_CURRENT_BINARY_DIR}/stub_emu_staging + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/stub_emu_staging + COMMAND ${CMAKE_COMMAND} -E copy + ${STUB_VBIN_DIR}/system_map_emu.xml + ${CMAKE_CURRENT_BINARY_DIR}/stub_emu_staging/system_map.xml + COMMAND ${CMAKE_COMMAND} -E copy + ${STUB_VBIN_DIR}/emu_manifest.json + ${CMAKE_CURRENT_BINARY_DIR}/stub_emu_staging/emu_manifest.json + COMMAND ${CMAKE_COMMAND} -E copy + ${STUB_VBIN_DIR}/vrt_stub_server.py + ${CMAKE_CURRENT_BINARY_DIR}/stub_emu_staging/vpp_emu + COMMAND chmod +x ${CMAKE_CURRENT_BINARY_DIR}/stub_emu_staging/vpp_emu + COMMAND tar cf ${STUB_EMU_VBIN} + -C ${CMAKE_CURRENT_BINARY_DIR}/stub_emu_staging . + DEPENDS + ${STUB_VBIN_DIR}/system_map_emu.xml + ${STUB_VBIN_DIR}/emu_manifest.json + ${STUB_VBIN_DIR}/vrt_stub_server.py + COMMENT "Creating emulation stub VBIN" +) + +set(STUB_SIM_VBIN ${CMAKE_CURRENT_BINARY_DIR}/stub_sim.vbin) +add_custom_command( + OUTPUT ${STUB_SIM_VBIN} + COMMAND ${CMAKE_COMMAND} -E rm -rf ${CMAKE_CURRENT_BINARY_DIR}/stub_sim_staging + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/stub_sim_staging + COMMAND ${CMAKE_COMMAND} -E copy + ${STUB_VBIN_DIR}/system_map_sim.xml + ${CMAKE_CURRENT_BINARY_DIR}/stub_sim_staging/system_map.xml + COMMAND ${CMAKE_COMMAND} -E copy + ${STUB_VBIN_DIR}/vrt_stub_server.py + ${CMAKE_CURRENT_BINARY_DIR}/stub_sim_staging/vpp_sim + COMMAND chmod +x ${CMAKE_CURRENT_BINARY_DIR}/stub_sim_staging/vpp_sim + COMMAND tar cf ${STUB_SIM_VBIN} + -C ${CMAKE_CURRENT_BINARY_DIR}/stub_sim_staging . + DEPENDS + ${STUB_VBIN_DIR}/system_map_sim.xml + ${STUB_VBIN_DIR}/vrt_stub_server.py + COMMENT "Creating simulation stub VBIN" +) + +add_custom_target(stub_vbins DEPENDS ${STUB_EMU_VBIN} ${STUB_SIM_VBIN}) + +macro(add_vrt_vbin_test test_name test_source) + add_executable(${test_name} ${test_source}) + target_link_libraries(${test_name} PRIVATE GTest::gtest_main GTest::gmock vrt::vrt) + target_compile_definitions(${test_name} PRIVATE + STUB_EMU_VBIN_PATH="${STUB_EMU_VBIN}" + STUB_SIM_VBIN_PATH="${STUB_SIM_VBIN}") + add_dependencies(${test_name} stub_vbins) + gtest_discover_tests(${test_name}) + add_dependencies(unit_tests ${test_name}) +endmacro() + +add_vrt_vbin_test(vrtbin_integration_test vrtbin_integration_test.cpp) +add_vrt_vbin_test(device_test device_test.cpp) diff --git a/vrt/tests/device_test.cpp b/vrt/tests/device_test.cpp new file mode 100644 index 00000000..593a8970 --- /dev/null +++ b/vrt/tests/device_test.cpp @@ -0,0 +1,218 @@ +/** + * 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 +#include + +#include +#include + +#include "test_helpers.hpp" + +using ::testing::Contains; + +class DeviceTest : public ::testing::Test, public ::testing::WithParamInterface { + protected: + std::filesystem::path tmpDir; + ScopedEnv* envCache = nullptr; + vrt::Platform platform; + vrt::Device device; + + void SetUp() override { + tmpDir = makeTempDir("device-test"); + envCache = new ScopedEnv("SLASH_CACHE_PATH", tmpDir.string()); + + platform = GetParam(); + std::array supported_platforms{vrt::Platform::EMULATION, vrt::Platform::SIMULATION}; + EXPECT_THAT(supported_platforms, Contains(platform)); + + std::string vbin_path; + if (platform == vrt::Platform::EMULATION) { + vbin_path = STUB_EMU_VBIN_PATH; + } else { + vbin_path = STUB_SIM_VBIN_PATH; + } + device = vrt::Device("0000:00:00", vbin_path, false); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + + void TearDown() override { + device.cleanup(); + delete envCache; + std::filesystem::remove_all(tmpDir); + } +}; + +TEST_P(DeviceTest, Construction) { + SUCCEED(); +} + +TEST_P(DeviceTest, GetPlatform) { + EXPECT_EQ(device.getPlatform(), platform); +} + +TEST_P(DeviceTest, GetFrequency) { + EXPECT_EQ(device.getFrequency(), 0u); +} + +TEST_P(DeviceTest, GetKernelVadd) { + auto kernel = device.getKernel("vadd"); + EXPECT_EQ(kernel.getName(), "vadd"); + EXPECT_EQ(kernel.getPhysAddr(), 0x10000u); +} + +TEST_P(DeviceTest, GetKernelPassthrough) { + auto kernel = device.getKernel("passthrough"); + EXPECT_EQ(kernel.getName(), "passthrough"); + EXPECT_EQ(kernel.getPhysAddr(), 0x20000u); +} + +TEST_P(DeviceTest, GetKernelUnknownThrows) { + EXPECT_THROW(device.getKernel("nonexistent"), std::runtime_error); +} + +TEST_P(DeviceTest, GetQdmaConnections) { + auto conns = device.getHandle()->getQdmaConnections(); + ASSERT_EQ(conns.size(), 2u); + EXPECT_EQ(conns[0].getKernel(), "vadd"); + EXPECT_EQ(conns[0].getInterface(), "axis_in"); + EXPECT_EQ(conns[0].getDirection(), vrt::StreamDirection::HOST_TO_DEVICE); + EXPECT_EQ(conns[0].getQid(), 0u); + EXPECT_EQ(conns[1].getInterface(), "axis_out"); + EXPECT_EQ(conns[1].getDirection(), vrt::StreamDirection::DEVICE_TO_HOST); + EXPECT_EQ(conns[1].getQid(), 1u); +} + +TEST_P(DeviceTest, KernelWrite) { + auto kernel = device.getKernel("vadd"); + EXPECT_NO_THROW(kernel.write(0x10, 0xDEAD)); +} + +TEST_P(DeviceTest, KernelRead) { + auto kernel = device.getKernel("vadd"); + uint32_t val = kernel.read(0x10); + EXPECT_EQ(val, 0u); +} + +TEST_P(DeviceTest, BufferDDRConstruction) { + EXPECT_NO_THROW({ + vrt::Buffer buf(device, 64, vrt::MemoryRangeType::DDR); + }); +} + +TEST_P(DeviceTest, BufferHBMWithPort) { + EXPECT_NO_THROW({ + vrt::Buffer buf(device, 64, vrt::MemoryRangeType::HBM, 0); + }); +} + +TEST_P(DeviceTest, BufferHBMVnoc) { + EXPECT_NO_THROW({ + vrt::Buffer buf(device, 64, vrt::MemoryRangeType::HBM_VNOC); + }); +} + +TEST_P(DeviceTest, BufferSyncRoundTrip) { + vrt::Buffer buf(device, 4, vrt::MemoryRangeType::DDR); + buf[0] = 10; + buf[1] = 20; + buf[2] = 30; + buf[3] = 40; + buf.sync(vrt::SyncType::HOST_TO_DEVICE); + buf[0] = 0; + buf[1] = 0; + buf[2] = 0; + buf[3] = 0; + buf.sync(vrt::SyncType::DEVICE_TO_HOST); + EXPECT_EQ(buf[0], 10); + EXPECT_EQ(buf[1], 20); + EXPECT_EQ(buf[2], 30); + EXPECT_EQ(buf[3], 40); +} + +TEST_P(DeviceTest, StreamingBufferH2D) { + if (platform == vrt::Platform::SIMULATION) { + GTEST_SKIP(); + } + auto kernel = device.getKernel("vadd"); + vrt::StreamingBuffer sbuf(device, kernel, "axis_in", 16); + sbuf[0] = 42; + EXPECT_NO_THROW(sbuf.sync()); +} + +TEST_P(DeviceTest, StreamingBufferD2H) { + if (platform == vrt::Platform::SIMULATION) { + GTEST_SKIP(); + } + auto kernel = device.getKernel("vadd"); + vrt::StreamingBuffer sbuf(device, kernel, "axis_out", 16); + EXPECT_NO_THROW(sbuf.sync()); +} + +TEST_P(DeviceTest, StreamingBufferWrongPortThrows) { + if (platform == vrt::Platform::SIMULATION) { + GTEST_SKIP(); + } + auto kernel = device.getKernel("vadd"); + EXPECT_THROW( + vrt::StreamingBuffer(device, kernel, "nonexistent_port", 16), + std::runtime_error); +} + +TEST_P(DeviceTest, StreamingBufferThrowsNotImplemented) { + if (platform != vrt::Platform::SIMULATION) { + GTEST_SKIP(); + } + auto kernel = device.getKernel("vadd"); + vrt::StreamingBuffer sbuf(device, kernel, "axis_in", 16); + EXPECT_THROW(sbuf.sync(), std::runtime_error); +} + +TEST_P(DeviceTest, KernelVaddRoundTrip) { + constexpr int N = 4; + vrt::Kernel kernel = device.getKernel("vadd"); + vrt::Buffer in1(device, N, vrt::MemoryRangeType::HBM, 0); + vrt::Buffer in2(device, N, vrt::MemoryRangeType::DDR); + vrt::Buffer out(device, N, vrt::MemoryRangeType::HBM_VNOC); + + for (int i = 0; i < N; ++i) { + in1[i] = i + 1; + in2[i] = (i + 1) * 10; + } + in1.sync(vrt::SyncType::HOST_TO_DEVICE); + in2.sync(vrt::SyncType::HOST_TO_DEVICE); + + kernel.setArg(0, static_cast(in1.getPhysAddr())); + kernel.setArg(1, static_cast(in2.getPhysAddr())); + kernel.setArg(2, static_cast(out.getPhysAddr())); + kernel.setArg(3, static_cast(N)); + ASSERT_NO_THROW(kernel.call()); + + out.sync(vrt::SyncType::DEVICE_TO_HOST); + for (int i = 0; i < N; ++i) { + EXPECT_EQ(out[i], in1[i] + in2[i]); + } +} + +INSTANTIATE_TEST_SUITE_P(DeviceTestSuite, DeviceTest, ::testing::Values(vrt::Platform::EMULATION, vrt::Platform::SIMULATION)); diff --git a/vrt/tests/filesystem_cache_test.cpp b/vrt/tests/filesystem_cache_test.cpp new file mode 100644 index 00000000..dd86926a --- /dev/null +++ b/vrt/tests/filesystem_cache_test.cpp @@ -0,0 +1,130 @@ +/** + * 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 "test_helpers.hpp" + +class FilesystemCacheTest : public ::testing::Test { + protected: + std::filesystem::path tmpDir; + + ScopedEnv* envSlashCache = nullptr; + ScopedEnv* envXdgCache = nullptr; + ScopedEnv* envHome = nullptr; + ScopedEnv* envSlashRuntime = nullptr; + ScopedEnv* envXdgRuntime = nullptr; + + void SetUp() override { tmpDir = makeTempDir("fscache-test"); } + + void TearDown() override { + delete envSlashCache; + delete envXdgCache; + delete envHome; + delete envSlashRuntime; + delete envXdgRuntime; + std::filesystem::remove_all(tmpDir); + } + + void clearCacheEnvVars() { + envSlashCache = new ScopedEnv("SLASH_CACHE_PATH"); + envXdgCache = new ScopedEnv("XDG_CACHE_HOME"); + envHome = new ScopedEnv("HOME"); + } + + void clearRuntimeEnvVars() { + envSlashRuntime = new ScopedEnv("SLASH_RUNTIME_PATH"); + envXdgRuntime = new ScopedEnv("XDG_RUNTIME_DIR"); + } +}; + +TEST_F(FilesystemCacheTest, CachePathFromSlashCachePath) { + clearCacheEnvVars(); + std::string target = (tmpDir / "slash-cache").string(); + ScopedEnv env("SLASH_CACHE_PATH", target); + auto path = FilesystemCache::getCachePath(); + EXPECT_EQ(path, std::filesystem::path(target)); +} + +TEST_F(FilesystemCacheTest, CachePathFromXdgCacheHome) { + clearCacheEnvVars(); + std::string xdg = (tmpDir / "xdg-cache").string(); + ScopedEnv env("XDG_CACHE_HOME", xdg); + auto path = FilesystemCache::getCachePath(); + EXPECT_EQ(path, std::filesystem::path(xdg) / "SLASH" / "vrt"); +} + +TEST_F(FilesystemCacheTest, CachePathFromHome) { + clearCacheEnvVars(); + std::string home = (tmpDir / "home").string(); + ScopedEnv env("HOME", home); + auto path = FilesystemCache::getCachePath(); + EXPECT_EQ(path, std::filesystem::path(home) / ".cache" / "SLASH" / "vrt"); +} + +TEST_F(FilesystemCacheTest, CachePathFallback) { + clearCacheEnvVars(); + auto path = FilesystemCache::getCachePath(); + std::string expected = "/tmp/SLASH-cache-" + std::to_string(getuid()) + "/vrt"; + EXPECT_EQ(path, std::filesystem::path(expected)); +} + +TEST_F(FilesystemCacheTest, RuntimePathFromSlashRuntimePath) { + clearRuntimeEnvVars(); + std::string target = (tmpDir / "slash-runtime").string(); + ScopedEnv env("SLASH_RUNTIME_PATH", target); + auto path = FilesystemCache::getRuntimePath(); + EXPECT_EQ(path, std::filesystem::path(target)); +} + +TEST_F(FilesystemCacheTest, RuntimePathFromXdgRuntimeDir) { + clearRuntimeEnvVars(); + std::string xdg = (tmpDir / "xdg-runtime").string(); + ScopedEnv env("XDG_RUNTIME_DIR", xdg); + auto path = FilesystemCache::getRuntimePath(); + EXPECT_EQ(path, std::filesystem::path(xdg) / "SLASH" / "vrt"); +} + +TEST_F(FilesystemCacheTest, RuntimePathFallback) { + clearRuntimeEnvVars(); + ScopedEnv envHome("HOME"); + auto path = FilesystemCache::getRuntimePath(); + std::string expected = "/tmp/SLASH-run-" + std::to_string(getuid()) + "/vrt"; + EXPECT_EQ(path, std::filesystem::path(expected)); +} + +TEST_F(FilesystemCacheTest, CachePathCreatesDirectory) { + clearCacheEnvVars(); + std::string target = (tmpDir / "new-cache-dir").string(); + ScopedEnv env("SLASH_CACHE_PATH", target); + auto path = FilesystemCache::getCachePath(); + EXPECT_TRUE(std::filesystem::is_directory(path)); +} + +TEST_F(FilesystemCacheTest, RuntimePathCreatesDirectory) { + clearRuntimeEnvVars(); + std::string target = (tmpDir / "new-runtime-dir").string(); + ScopedEnv env("SLASH_RUNTIME_PATH", target); + auto path = FilesystemCache::getRuntimePath(); + EXPECT_TRUE(std::filesystem::is_directory(path)); +} diff --git a/vrt/tests/fixtures/stub_vbin/emu_manifest.json b/vrt/tests/fixtures/stub_vbin/emu_manifest.json new file mode 100644 index 00000000..2fd5b5c4 --- /dev/null +++ b/vrt/tests/fixtures/stub_vbin/emu_manifest.json @@ -0,0 +1,30 @@ +{ + "kernels": [ + { + "instance": "vadd", + "call_args": [ + {"arg": "arg0", "kind": "buffer"}, + {"arg": "arg1", "kind": "buffer"}, + {"arg": "arg2", "kind": "buffer"}, + {"arg": "arg3", "kind": "scalar"} + ] + }, + { + "instance": "passthrough", + "call_args": [ + {"arg": "arg0", "kind": "scalar"} + ] + } + ], + "fetch": { + "scalar": [ + { + "function": "vadd", + "arg": "size", + "source": { + "register_offset": 40 + } + } + ] + } +} diff --git a/vrt/tests/fixtures/stub_vbin/system_map_emu.xml b/vrt/tests/fixtures/stub_vbin/system_map_emu.xml new file mode 100644 index 00000000..95128896 --- /dev/null +++ b/vrt/tests/fixtures/stub_vbin/system_map_emu.xml @@ -0,0 +1,45 @@ + + + Emulation + 100000000 + + + vadd + 0x10000 + 0x1000 + + + + + + + + + + + + + + passthrough + 0x20000 + 0x1000 + + + + + + + + vadd + axis_in + HostToDevice + 0 + + + + vadd + axis_out + DeviceToHost + 1 + + diff --git a/vrt/tests/fixtures/stub_vbin/system_map_sim.xml b/vrt/tests/fixtures/stub_vbin/system_map_sim.xml new file mode 100644 index 00000000..8489c1a3 --- /dev/null +++ b/vrt/tests/fixtures/stub_vbin/system_map_sim.xml @@ -0,0 +1,45 @@ + + + Simulation + 100000000 + + + vadd + 0x10000 + 0x1000 + + + + + + + + + + + + + + passthrough + 0x20000 + 0x1000 + + + + + + + + vadd + axis_in + HostToDevice + 0 + + + + vadd + axis_out + DeviceToHost + 1 + + diff --git a/vrt/tests/fixtures/stub_vbin/vrt_stub_server.py b/vrt/tests/fixtures/stub_vbin/vrt_stub_server.py new file mode 100644 index 00000000..e712e07b --- /dev/null +++ b/vrt/tests/fixtures/stub_vbin/vrt_stub_server.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# ################################################################################################## +# 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. +# ################################################################################################## +import json +import struct +import zmq + +# Kernel base addresses (must match system_map.xml) +VADD_BASE = 0x10000 + +# Functional arg register offsets relative to kernel base (must match system_map.xml) +VADD_IN1_OFFSET = 0x10 +VADD_IN2_OFFSET = 0x18 +VADD_OUT_OFFSET = 0x20 +VADD_SIZE_OFFSET = 0x28 + + +def run_vadd(buffers, in1_key, in2_key, out_key, size): + """Add two int32 buffers and store the result.""" + in1_bytes = buffers.get(in1_key, b"\x00" * (size * 4)) + in2_bytes = buffers.get(in2_key, b"\x00" * (size * 4)) + + n = min(size, len(in1_bytes) // 4, len(in2_bytes) // 4) + in1 = struct.unpack_from(f"<{n}i", in1_bytes) + in2 = struct.unpack_from(f"<{n}i", in2_bytes) + out = [a + b for a, b in zip(in1, in2)] + buffers[out_key] = struct.pack(f"<{n}i", *out) + + +def reconstruct_64bit(registers, base, offset): + """Reconstruct a 64-bit address from two consecutive 32-bit register writes.""" + lo = registers.get(base + offset, 0) + hi = registers.get(base + offset + 4, 0) + return (hi << 32) | lo + + +def main(): + context = zmq.Context() + socket = context.socket(zmq.REP) + socket.bind("tcp://*:5555") + + buffers = {} + streams = {} + registers = {} + + while True: + frames = [socket.recv()] + while socket.getsockopt(zmq.RCVMORE): + frames.append(socket.recv()) + + try: + message = json.loads(frames[0]) + except (json.JSONDecodeError, UnicodeDecodeError): + socket.send(b"OK") + continue + + command = message.get("command", "") + + if command == "exit": + socket.send(b"OK") + break + + elif command == "populate": + key = message.get("name", str(message.get("addr", ""))) + if len(frames) > 1: + buffers[key] = frames[1] + socket.send(b"OK") + + elif command == "stream_in": + key = message.get("name", "") + if len(frames) > 1: + streams[key] = frames[1] + socket.send(b"OK") + + elif command == "stream_out": + key = message.get("name", "") + size = message.get("size", 0) + data = streams.get(key, b"\x00" * size) + socket.send(data) + + elif command == "fetch": + typ = message.get("type", "") + if typ == "buffer": + key = message.get("name", str(message.get("addr", ""))) + if key in buffers: + data = list(buffers[key]) + else: + size = message.get("size", 0) + data = [0] * size + socket.send_string(json.dumps(data)) + else: + address = int(message.get("addr", message.get("name", ""))) + val = registers.get(address, 0) + socket.send_string(json.dumps(val)) + + elif command == "read_register": + socket.send_string("0") + + elif command == "reg": + address = int(message.get("addr", 0)) + val = int(message.get("val", 0)) + + if val & 0x1 and address == VADD_BASE: + # ap_start written to vadd CTRL — reconstruct args and run + in1_addr = reconstruct_64bit(registers, VADD_BASE, VADD_IN1_OFFSET) + in2_addr = reconstruct_64bit(registers, VADD_BASE, VADD_IN2_OFFSET) + out_addr = reconstruct_64bit(registers, VADD_BASE, VADD_OUT_OFFSET) + size_val = registers.get(VADD_BASE + VADD_SIZE_OFFSET, 0) + run_vadd(buffers, str(in1_addr), str(in2_addr), str(out_addr), size_val) + registers[address] = 0x6 # ap_done | ap_idle + else: + registers[address] = val + socket.send(b"OK") + + elif command == "call": + function = message.get("function", "") + arguments = message.get("args", {}) + if function == "vadd": + in1_name = arguments.get("arg0", {}).get("name", "") + in2_name = arguments.get("arg1", {}).get("name", "") + out_name = arguments.get("arg2", {}).get("name", "") + size_val = arguments.get("arg3", {}).get("value", 0) + run_vadd(buffers, in1_name, in2_name, out_name, size_val) + socket.send(b"OK") + + elif command == "wait": + socket.send(b"OK") + + else: + socket.send(b"OK") + + socket.close() + context.term() + + +if __name__ == "__main__": + main() diff --git a/vrt/tests/kernel_test.cpp b/vrt/tests/kernel_test.cpp new file mode 100644 index 00000000..823429fa --- /dev/null +++ b/vrt/tests/kernel_test.cpp @@ -0,0 +1,215 @@ +/** + * 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 + +static vrt::FunctionalArg makeArg(uint32_t idx, const std::string& name, const std::string& type, + uint32_t offset, uint32_t range = 32, bool readable = false, + bool writable = true, const std::string& port = "") { + vrt::FunctionalArg a; + a.idx = idx; + a.name = name; + a.type = type; + a.offset = offset; + a.range = range; + a.readable = readable; + a.writable = writable; + a.port = port; + return a; +} + +static vrt::Kernel makeTestKernel(const std::vector& args = {}, + const std::string& name = "testKernel", + uint64_t baseAddr = 0x1000, uint64_t range = 0x100) { + std::vector regs; + return vrt::Kernel(name, baseAddr, range, regs, args); +} + +TEST(KernelConstructTest, FiveArgConstructor) { + auto k = makeTestKernel(); + EXPECT_EQ(k.getName(), "testKernel"); + EXPECT_EQ(k.getPhysAddr(), 0x1000u); +} + +TEST(KernelConstructTest, HasFunctionalArgsTrue) { + auto k = makeTestKernel({makeArg(0, "a", "int", 0x10)}); + EXPECT_TRUE(k.hasFunctionalArgs()); +} + +TEST(KernelConstructTest, HasFunctionalArgsFalse) { + auto k = makeTestKernel(); + EXPECT_FALSE(k.hasFunctionalArgs()); +} + +TEST(KernelConstructTest, FunctionalArgsSortedOnConstruction) { + auto k = makeTestKernel( + {makeArg(2, "c", "int", 0x20), makeArg(0, "a", "int", 0x10), makeArg(1, "b", "int", 0x18)}); + auto& args = k.getFunctionalArgs(); + ASSERT_EQ(args.size(), 3u); + EXPECT_EQ(args[0].idx, 0u); + EXPECT_EQ(args[1].idx, 1u); + EXPECT_EQ(args[2].idx, 2u); +} + +TEST(KernelConstructTest, SetAndGetFunctionalArgs) { + auto k = makeTestKernel(); + std::vector newArgs = {makeArg(0, "x", "int", 0x10)}; + k.setFunctionalArgs(newArgs); + EXPECT_TRUE(k.hasFunctionalArgs()); + EXPECT_EQ(k.getFunctionalArgs().size(), 1u); + EXPECT_EQ(k.getFunctionalArgs()[0].name, "x"); +} + +TEST(KernelArgLookupTest, SetArgByIdx) { + auto k = makeTestKernel({makeArg(0, "input", "scalar", 0x10, 32)}); + EXPECT_NO_THROW(k.setArg(0, 42)); +} + +TEST(KernelArgLookupTest, SetArgByName) { + auto k = makeTestKernel({makeArg(0, "input", "scalar", 0x10, 32)}); + EXPECT_NO_THROW(k.setArg("input", 42)); +} + +TEST(KernelArgLookupTest, SetArgByNameWithRSuffix) { + auto k = makeTestKernel({makeArg(0, "input_r", "buffer", 0x10, 64)}); + EXPECT_NO_THROW(k.setArg("input", static_cast(0xDEAD))); +} + +TEST(KernelArgLookupTest, SetArgEmptyNameThrows) { + auto k = makeTestKernel({makeArg(0, "input", "scalar", 0x10)}); + EXPECT_THROW(k.setArg("", 42), std::runtime_error); +} + +TEST(KernelArgLookupTest, SetArgNameNotFoundThrows) { + auto k = makeTestKernel({makeArg(0, "input", "scalar", 0x10)}); + EXPECT_THROW(k.setArg("nonexistent", 42), std::runtime_error); +} + +TEST(KernelArgLookupTest, SetArgIdxNotFoundThrows) { + auto k = makeTestKernel({makeArg(0, "input", "scalar", 0x10)}); + EXPECT_THROW(k.setArg(99, 42), std::runtime_error); +} + +TEST(KernelArgLookupTest, SetArgNegativeIndexThrows) { + auto k = makeTestKernel({makeArg(0, "input", "scalar", 0x10)}); + EXPECT_THROW(k.setArg(-1, 42), std::runtime_error); +} + +TEST(KernelArgLookupTest, SetArgNoMetadataThrows) { + auto k = makeTestKernel(); + EXPECT_THROW(k.setArg(0, 42), std::runtime_error); +} + +TEST(KernelArgValidationTest, EnsureSetArgValuesComplete) { + auto k = makeTestKernel( + {makeArg(0, "a", "scalar", 0x10, 32), makeArg(1, "b", "scalar", 0x14, 32)}); + k.setArg(0, 1); + k.setArg(1, 2); + // With no platform set (UNKNOWN), call() skips all branches — validation is + // only exercised inside platform-specific blocks, so this tests that setArg + // itself succeeds for complete argument sets. + EXPECT_NO_THROW(k.call()); +} + +TEST(KernelArgValidationTest, EnsureSetArgValuesMissingThrows) { + auto k = makeTestKernel( + {makeArg(0, "a", "scalar", 0x10, 32), makeArg(1, "b", "scalar", 0x14, 32)}); + k.setPlatform(vrt::Platform::HARDWARE); + k.setArg(0, 1); + EXPECT_THROW(k.call(), std::runtime_error); +} + +TEST(KernelArgValidationTest, ReadOnlyArgNotRequiredForLaunch) { + auto k = makeTestKernel({makeArg(0, "a", "scalar", 0x10, 32, true, true), + makeArg(1, "status", "scalar", 0x14, 32, true, false)}); + k.setPlatform(vrt::Platform::HARDWARE); + k.setArg(0, 1); + // status is read-only (writable=false), so only "a" needs to be set. + // call() should reach ensureSetArgValuesCompleteForLaunch, which skips + // read-only args, then try writeBatch which throws because no BAR is set. + // The key assertion: it does NOT throw about a missing "status" arg. + EXPECT_THROW(k.call(), std::runtime_error); + try { + k.call(); + } catch (const std::runtime_error& e) { + std::string msg = e.what(); + EXPECT_EQ(msg.find("status"), std::string::npos) << "Should not require read-only arg"; + EXPECT_NE(msg.find("BAR"), std::string::npos) << "Should fail at BAR access, not arg validation"; + } +} + +TEST(KernelMemoryConfigTest, PortMemoryConfigDDR) { + auto k = makeTestKernel({makeArg(0, "in", "buffer", 0x10, 64, false, true, "m_axi_gmem0")}); + k.setConnections({{"m_axi_gmem0", "DDR"}}); + auto cfg = k.portMemoryConfig("m_axi_gmem0"); + EXPECT_EQ(cfg.type, vrt::MemoryRangeType::DDR); + EXPECT_FALSE(cfg.hbmPort.has_value()); +} + +TEST(KernelMemoryConfigTest, PortMemoryConfigHBM) { + auto k = makeTestKernel({makeArg(0, "in", "buffer", 0x10, 64, false, true, "m_axi_gmem0")}); + k.setConnections({{"m_axi_gmem0", "HBM3"}}); + auto cfg = k.portMemoryConfig("m_axi_gmem0"); + EXPECT_EQ(cfg.type, vrt::MemoryRangeType::HBM); + ASSERT_TRUE(cfg.hbmPort.has_value()); + EXPECT_EQ(cfg.hbmPort.value(), 3u); +} + +TEST(KernelMemoryConfigTest, PortMemoryConfigHBMVnoc) { + auto k = makeTestKernel(); + k.setConnections({{"port0", "HBM"}}); + auto cfg = k.portMemoryConfig("port0"); + EXPECT_EQ(cfg.type, vrt::MemoryRangeType::HBM_VNOC); +} + +TEST(KernelMemoryConfigTest, PortMemoryConfigMEM) { + auto k = makeTestKernel(); + k.setConnections({{"port0", "MEM"}}); + auto cfg = k.portMemoryConfig("port0"); + EXPECT_EQ(cfg.type, vrt::MemoryRangeType::HBM_VNOC); +} + +TEST(KernelMemoryConfigTest, PortMemoryConfigUnknownTargetThrows) { + auto k = makeTestKernel(); + k.setConnections({{"port0", "INVALID"}}); + EXPECT_THROW(k.portMemoryConfig("port0"), std::runtime_error); +} + +TEST(KernelMemoryConfigTest, PortMemoryConfigNoConnectionThrows) { + auto k = makeTestKernel(); + k.setConnections({{"port0", "DDR"}}); + EXPECT_THROW(k.portMemoryConfig("nonexistent"), std::runtime_error); +} + +TEST(KernelMemoryConfigTest, ArgMemoryConfigByName) { + auto k = makeTestKernel({makeArg(0, "input", "buffer", 0x10, 64, false, true, "m_axi_gmem0")}); + k.setConnections({{"m_axi_gmem0", "DDR"}}); + auto cfg = k.argMemoryConfig("input"); + EXPECT_EQ(cfg.type, vrt::MemoryRangeType::DDR); +} + +TEST(KernelMemoryConfigTest, ArgMemoryConfigNoPortThrows) { + auto k = makeTestKernel({makeArg(0, "scalar_arg", "scalar", 0x10, 32, false, true, "")}); + k.setConnections({}); + EXPECT_THROW(k.argMemoryConfig("scalar_arg"), std::runtime_error); +} diff --git a/vrt/tests/logger_test.cpp b/vrt/tests/logger_test.cpp new file mode 100644 index 00000000..3280c22e --- /dev/null +++ b/vrt/tests/logger_test.cpp @@ -0,0 +1,146 @@ +/** + * 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 +#include + +#include "test_helpers.hpp" + +class LoggerTest : public ::testing::Test { + protected: + std::filesystem::path tmpDir; + std::string logFile; + + void SetUp() override { + tmpDir = makeTempDir("logger-test"); + logFile = (tmpDir / "test.log").string(); + vrt::utils::Logger::setLogLevel(vrt::utils::LogLevel::DEBUG); + vrt::utils::Logger::setOutput(logFile); + } + + void TearDown() override { + vrt::utils::Logger::setLogLevel(vrt::utils::LogLevel::INFO); + std::filesystem::remove_all(tmpDir); + } + + std::string readLog() { + std::ifstream ifs(logFile); + return std::string((std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + } +}; + +TEST_F(LoggerTest, GeneralPlaceholder) { + vrt::utils::Logger::log(vrt::utils::LogLevel::INFO, "test", "hello {}", "world"); + EXPECT_NE(readLog().find("hello world"), std::string::npos); +} + +TEST_F(LoggerTest, GeneralPlaceholderInt) { + vrt::utils::Logger::log(vrt::utils::LogLevel::INFO, "test", "value={}", 42); + EXPECT_NE(readLog().find("value=42"), std::string::npos); +} + +TEST_F(LoggerTest, HexPlaceholder) { + vrt::utils::Logger::log(vrt::utils::LogLevel::INFO, "test", "addr={x}", 255); + std::string log = readLog(); + EXPECT_NE(log.find("0xff"), std::string::npos); +} + +TEST_F(LoggerTest, BinaryPlaceholder) { + vrt::utils::Logger::log(vrt::utils::LogLevel::INFO, "test", "bits={b}", + static_cast(5)); + std::string log = readLog(); + EXPECT_NE(log.find("0b00000101"), std::string::npos); +} + +TEST_F(LoggerTest, OctalPlaceholder) { + vrt::utils::Logger::log(vrt::utils::LogLevel::INFO, "test", "oct={o}", 8); + std::string log = readLog(); + EXPECT_NE(log.find("0o"), std::string::npos); +} + +TEST_F(LoggerTest, MultiplePlaceholders) { + vrt::utils::Logger::log(vrt::utils::LogLevel::INFO, "test", "{} + {} = {}", 1, 2, 3); + EXPECT_NE(readLog().find("1 + 2 = 3"), std::string::npos); +} + +TEST_F(LoggerTest, NoPlaceholders) { + vrt::utils::Logger::log(vrt::utils::LogLevel::INFO, "test", "literal message"); + EXPECT_NE(readLog().find("literal message"), std::string::npos); +} + +TEST_F(LoggerTest, TooFewArgsThrows) { + EXPECT_THROW( + vrt::utils::Logger::log(vrt::utils::LogLevel::INFO, "test", "{} {}", "only_one"), + std::runtime_error); +} + +TEST_F(LoggerTest, TooManyArgsThrows) { + EXPECT_THROW(vrt::utils::Logger::log(vrt::utils::LogLevel::INFO, "test", "{}", 1, 2), + std::runtime_error); +} + +TEST_F(LoggerTest, SetLogLevelFilters) { + vrt::utils::Logger::setLogLevel(vrt::utils::LogLevel::WARN); + vrt::utils::Logger::log(vrt::utils::LogLevel::INFO, "test", "should not appear"); + EXPECT_TRUE(readLog().empty()); +} + +TEST_F(LoggerTest, NoneBlocksAll) { + vrt::utils::Logger::setLogLevel(vrt::utils::LogLevel::NONE); + vrt::utils::Logger::log(vrt::utils::LogLevel::ERROR, "test", "blocked"); + vrt::utils::Logger::log(vrt::utils::LogLevel::WARN, "test", "blocked"); + EXPECT_TRUE(readLog().empty()); +} + +TEST_F(LoggerTest, WarnLevelPassesWarn) { + vrt::utils::Logger::setLogLevel(vrt::utils::LogLevel::WARN); + vrt::utils::Logger::log(vrt::utils::LogLevel::WARN, "test", "warn msg"); + EXPECT_NE(readLog().find("warn msg"), std::string::npos); +} + +TEST_F(LoggerTest, ErrorLevelBlockedByWarnThreshold) { + vrt::utils::Logger::setLogLevel(vrt::utils::LogLevel::WARN); + vrt::utils::Logger::log(vrt::utils::LogLevel::ERROR, "test", "error msg"); + EXPECT_EQ(readLog().find("error msg"), std::string::npos); +} + +TEST_F(LoggerTest, SetOutputToFile) { + vrt::utils::Logger::log(vrt::utils::LogLevel::INFO, "test", "file output"); + std::string log = readLog(); + EXPECT_FALSE(log.empty()); + EXPECT_NE(log.find("file output"), std::string::npos); +} + +TEST_F(LoggerTest, SetOutputInvalidPathFallsBack) { + EXPECT_NO_THROW(vrt::utils::Logger::setOutput("/nonexistent/path/log.txt")); +} + +TEST_F(LoggerTest, TimestampFormat) { + vrt::utils::Logger::log(vrt::utils::LogLevel::INFO, "test", "ts check"); + std::string log = readLog(); + std::regex tsPattern(R"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})"); + EXPECT_TRUE(std::regex_search(log, tsPattern)); +} diff --git a/vrt/tests/qdma_connection_test.cpp b/vrt/tests/qdma_connection_test.cpp new file mode 100644 index 00000000..dfac64b6 --- /dev/null +++ b/vrt/tests/qdma_connection_test.cpp @@ -0,0 +1,46 @@ +/** + * 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 + +TEST(QdmaConnectionTest, HostToDeviceDirection) { + vrt::QdmaConnection conn("myKernel", 0, "axis_port", "HostToDevice"); + EXPECT_EQ(conn.getDirection(), vrt::StreamDirection::HOST_TO_DEVICE); +} + +TEST(QdmaConnectionTest, DeviceToHostDirection) { + vrt::QdmaConnection conn("myKernel", 1, "axis_port", "DeviceToHost"); + EXPECT_EQ(conn.getDirection(), vrt::StreamDirection::DEVICE_TO_HOST); +} + +TEST(QdmaConnectionTest, GetKernel) { + vrt::QdmaConnection conn("testKernel", 3, "iface0", "HostToDevice"); + EXPECT_EQ(conn.getKernel(), "testKernel"); +} + +TEST(QdmaConnectionTest, GetQid) { + vrt::QdmaConnection conn("k", 42, "iface0", "HostToDevice"); + EXPECT_EQ(conn.getQid(), 42u); +} + +TEST(QdmaConnectionTest, GetInterface) { + vrt::QdmaConnection conn("k", 0, "my_interface", "DeviceToHost"); + EXPECT_EQ(conn.getInterface(), "my_interface"); +} diff --git a/vrt/tests/register_test.cpp b/vrt/tests/register_test.cpp new file mode 100644 index 00000000..60915b69 --- /dev/null +++ b/vrt/tests/register_test.cpp @@ -0,0 +1,67 @@ +/** + * 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 + +TEST(RegisterTest, ParameterizedConstructor) { + vrt::Register reg("CTRL", 0x10, 32, "RW", "Control register"); + EXPECT_EQ(reg.getRegisterName(), "CTRL"); + EXPECT_EQ(reg.getOffset(), 0x10u); + EXPECT_EQ(reg.getWidth(), 32u); + EXPECT_EQ(reg.getRW(), "RW"); + EXPECT_EQ(reg.getDescription(), "Control register"); +} + +TEST(RegisterTest, DefaultConstructor) { + vrt::Register reg; + EXPECT_EQ(reg.getRegisterName(), ""); + EXPECT_EQ(reg.getRW(), ""); + EXPECT_EQ(reg.getDescription(), ""); +} + +TEST(RegisterTest, SetRegisterName) { + vrt::Register reg; + reg.setRegisterName("STATUS"); + EXPECT_EQ(reg.getRegisterName(), "STATUS"); +} + +TEST(RegisterTest, SetOffset) { + vrt::Register reg; + reg.setOffset(0x20); + EXPECT_EQ(reg.getOffset(), 0x20u); +} + +TEST(RegisterTest, SetWidth) { + vrt::Register reg; + reg.setWidth(64); + EXPECT_EQ(reg.getWidth(), 64u); +} + +TEST(RegisterTest, SetRW) { + vrt::Register reg; + reg.setRW("RO"); + EXPECT_EQ(reg.getRW(), "RO"); +} + +TEST(RegisterTest, SetDescription) { + vrt::Register reg; + reg.setDescription("Status register for monitoring"); + EXPECT_EQ(reg.getDescription(), "Status register for monitoring"); +} diff --git a/vrt/tests/test_helpers.hpp b/vrt/tests/test_helpers.hpp new file mode 100644 index 00000000..f8568182 --- /dev/null +++ b/vrt/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 VRT_TEST_HELPERS_HPP +#define VRT_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 // VRT_TEST_HELPERS_HPP diff --git a/vrt/tests/utilization_data_test.cpp b/vrt/tests/utilization_data_test.cpp new file mode 100644 index 00000000..84aad4fc --- /dev/null +++ b/vrt/tests/utilization_data_test.cpp @@ -0,0 +1,76 @@ +/** + * 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 + +TEST(UtilizationDataTest, ResourceMetricsDefaults) { + vrt::ResourceMetrics m{}; + EXPECT_EQ(m.totalPplocs, 0u); + EXPECT_EQ(m.totalLuts, 0u); + EXPECT_EQ(m.lutram, 0u); + EXPECT_EQ(m.srl, 0u); + EXPECT_EQ(m.ff, 0u); + EXPECT_EQ(m.ramb36, 0u); + EXPECT_EQ(m.ramb18, 0u); + EXPECT_EQ(m.ramb, 0u); + EXPECT_EQ(m.uram, 0u); + EXPECT_EQ(m.dsp, 0u); +} + +TEST(UtilizationDataTest, OptionalFieldsDefaultToNullopt) { + vrt::ResourceMetrics m{}; + EXPECT_FALSE(m.totalLutsPct.has_value()); + EXPECT_FALSE(m.lutramPct.has_value()); + EXPECT_FALSE(m.srlPct.has_value()); + EXPECT_FALSE(m.ffPct.has_value()); + EXPECT_FALSE(m.ramb36Pct.has_value()); + EXPECT_FALSE(m.ramb18Pct.has_value()); + EXPECT_FALSE(m.uramPct.has_value()); + EXPECT_FALSE(m.dspPct.has_value()); +} + +TEST(UtilizationDataTest, ResourceMetricsAssignment) { + vrt::ResourceMetrics m{}; + m.totalLuts = 1000; + m.totalLutsPct = 5.2f; + EXPECT_EQ(m.totalLuts, 1000u); + ASSERT_TRUE(m.totalLutsPct.has_value()); + EXPECT_FLOAT_EQ(m.totalLutsPct.value(), 5.2f); +} + +TEST(UtilizationDataTest, UtilizationCellConstruction) { + vrt::UtilizationCell cell; + cell.instance = "k0"; + cell.module = "myKernel"; + cell.pr = "pblock_0"; + cell.metrics.totalLuts = 400; + EXPECT_EQ(cell.instance, "k0"); + EXPECT_EQ(cell.module, "myKernel"); + EXPECT_EQ(cell.metrics.totalLuts, 400u); +} + +TEST(UtilizationDataTest, UtilizationReportSlashPresent) { + vrt::UtilizationReport report; + report.slash.name = "slash"; + report.slash.totals.totalLuts = 500; + EXPECT_EQ(report.slash.name, "slash"); + EXPECT_EQ(report.slash.totals.totalLuts, 500u); + EXPECT_FALSE(report.serviceLayer.has_value()); +} diff --git a/vrt/tests/utilization_parser_test.cpp b/vrt/tests/utilization_parser_test.cpp new file mode 100644 index 00000000..dc2bdfa2 --- /dev/null +++ b/vrt/tests/utilization_parser_test.cpp @@ -0,0 +1,162 @@ +/** + * 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 "test_helpers.hpp" + +class UtilizationParserTest : public ::testing::Test { + protected: + std::filesystem::path tmpDir; + + void SetUp() override { tmpDir = makeTempDir("util-parser-test"); } + void TearDown() override { std::filesystem::remove_all(tmpDir); } + + std::string writeXml(const std::string& content) { + return writeTempFile(tmpDir, "utilization.xml", content); + } +}; + +TEST_F(UtilizationParserTest, ParseSlashBlock) { + auto path = writeXml(R"( + + + + +)"); + vrt::UtilizationParser parser(path); + parser.parse(); + auto& report = parser.getReport(); + EXPECT_EQ(report.slash.name, "slash"); + EXPECT_EQ(report.slash.totals.totalLuts, 1000u); + EXPECT_EQ(report.slash.totals.ff, 500u); + EXPECT_EQ(report.slash.totals.dsp, 10u); + EXPECT_EQ(report.slash.totals.ramb36, 5u); + EXPECT_EQ(report.slash.totals.ramb18, 3u); + EXPECT_EQ(report.slash.totals.uram, 2u); +} + +TEST_F(UtilizationParserTest, ParseSlashBlockWithKernels) { + auto path = writeXml(R"( + + + + + + + + + + +)"); + vrt::UtilizationParser parser(path); + parser.parse(); + auto& report = parser.getReport(); + ASSERT_TRUE(report.slash.subhierarchy.has_value()); + ASSERT_EQ(report.slash.subhierarchy->cells.size(), 1u); + EXPECT_EQ(report.slash.subhierarchy->cells[0].instance, "k0"); + EXPECT_EQ(report.slash.subhierarchy->cells[0].module, "myKernel"); + EXPECT_EQ(report.slash.subhierarchy->cells[0].metrics.totalLuts, 400u); + EXPECT_EQ(report.slash.subhierarchy->subhierarchySum.totalLuts, 400u); +} + +TEST_F(UtilizationParserTest, ParseSlashBlockWithSlashLogic) { + auto path = writeXml(R"( + + + + + + + + + + +)"); + vrt::UtilizationParser parser(path); + parser.parse(); + auto& sub = parser.getReport().slash.subhierarchy; + ASSERT_TRUE(sub.has_value()); + ASSERT_EQ(sub->slashLogic.size(), 1u); + EXPECT_EQ(sub->slashLogic[0].instance, "sl0"); + EXPECT_EQ(sub->slashLogicSum.totalLuts, 100u); +} + +TEST_F(UtilizationParserTest, ParseServiceLayer) { + auto path = writeXml(R"( + + + + + + + +)"); + vrt::UtilizationParser parser(path); + parser.parse(); + auto& report = parser.getReport(); + ASSERT_TRUE(report.serviceLayer.has_value()); + EXPECT_EQ(report.serviceLayer->name, "service_layer"); + EXPECT_EQ(report.serviceLayer->totals.totalLuts, 200u); + EXPECT_EQ(report.serviceLayer->totals.ff, 150u); +} + +TEST_F(UtilizationParserTest, ParseResourceMetricsPercentages) { + auto path = writeXml(R"( + + + + +)"); + vrt::UtilizationParser parser(path); + parser.parse(); + auto& m = parser.getReport().slash.totals; + ASSERT_TRUE(m.totalLutsPct.has_value()); + EXPECT_FLOAT_EQ(m.totalLutsPct.value(), 5.2f); + ASSERT_TRUE(m.ffPct.has_value()); + EXPECT_FLOAT_EQ(m.ffPct.value(), 3.1f); + EXPECT_FALSE(m.dspPct.has_value()); +} + +TEST_F(UtilizationParserTest, MissingSlashBlockThrows) { + auto path = writeXml(R"( +)"); + vrt::UtilizationParser parser(path); + EXPECT_THROW(parser.parse(), std::runtime_error); +} + +TEST_F(UtilizationParserTest, InvalidXmlThrows) { + auto path = writeTempFile(tmpDir, "bad.xml", "not valid xml <<<<"); + EXPECT_THROW(vrt::UtilizationParser parser(path), std::runtime_error); +} + +TEST_F(UtilizationParserTest, SlashBlockWithoutServiceLayer) { + auto path = writeXml(R"( + + + + +)"); + vrt::UtilizationParser parser(path); + parser.parse(); + EXPECT_FALSE(parser.getReport().serviceLayer.has_value()); +} diff --git a/vrt/tests/vrtbin_integration_test.cpp b/vrt/tests/vrtbin_integration_test.cpp new file mode 100644 index 00000000..d3c86879 --- /dev/null +++ b/vrt/tests/vrtbin_integration_test.cpp @@ -0,0 +1,112 @@ +/** + * 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 "test_helpers.hpp" + +class VrtbinEmuTest : public ::testing::Test { + protected: + std::filesystem::path tmpDir; + ScopedEnv* envCache = nullptr; + + void SetUp() override { + tmpDir = makeTempDir("vrtbin-emu-test"); + envCache = new ScopedEnv("SLASH_CACHE_PATH", tmpDir.string()); + } + + void TearDown() override { + delete envCache; + std::filesystem::remove_all(tmpDir); + } +}; + +TEST_F(VrtbinEmuTest, ExtractAndFindSystemMap) { + vrt::Vrtbin vrtbin(STUB_EMU_VBIN_PATH, "0000:00:00"); + EXPECT_FALSE(vrtbin.getSystemMapPath().empty()); + EXPECT_TRUE(std::filesystem::exists(vrtbin.getSystemMapPath())); +} + +TEST_F(VrtbinEmuTest, DetectsPlatformEmulation) { + vrt::Vrtbin vrtbin(STUB_EMU_VBIN_PATH, "0000:00:00"); + EXPECT_EQ(vrtbin.getPlatform(), vrt::Platform::EMULATION); +} + +TEST_F(VrtbinEmuTest, FindsEmulationExec) { + vrt::Vrtbin vrtbin(STUB_EMU_VBIN_PATH, "0000:00:00"); + EXPECT_FALSE(vrtbin.getEmulationExec().empty()); + EXPECT_TRUE(std::filesystem::exists(vrtbin.getEmulationExec())); +} + +TEST_F(VrtbinEmuTest, FindsEmulationManifest) { + vrt::Vrtbin vrtbin(STUB_EMU_VBIN_PATH, "0000:00:00"); + EXPECT_FALSE(vrtbin.getEmulationManifest().empty()); + EXPECT_TRUE(std::filesystem::exists(vrtbin.getEmulationManifest())); +} + +TEST_F(VrtbinEmuTest, NoPdiFilesForEmulation) { + vrt::Vrtbin vrtbin(STUB_EMU_VBIN_PATH, "0000:00:00"); + EXPECT_TRUE(vrtbin.getPdiPaths().empty()); +} + +class VrtbinSimTest : public ::testing::Test { + protected: + std::filesystem::path tmpDir; + ScopedEnv* envCache = nullptr; + + void SetUp() override { + tmpDir = makeTempDir("vrtbin-sim-test"); + envCache = new ScopedEnv("SLASH_CACHE_PATH", tmpDir.string()); + } + + void TearDown() override { + delete envCache; + std::filesystem::remove_all(tmpDir); + } +}; + +TEST_F(VrtbinSimTest, ExtractAndFindSystemMap) { + vrt::Vrtbin vrtbin(STUB_SIM_VBIN_PATH, "0000:00:00"); + EXPECT_FALSE(vrtbin.getSystemMapPath().empty()); + EXPECT_TRUE(std::filesystem::exists(vrtbin.getSystemMapPath())); +} + +TEST_F(VrtbinSimTest, DetectsPlatformSimulation) { + vrt::Vrtbin vrtbin(STUB_SIM_VBIN_PATH, "0000:00:00"); + EXPECT_EQ(vrtbin.getPlatform(), vrt::Platform::SIMULATION); +} + +TEST_F(VrtbinSimTest, FindsSimulationExec) { + vrt::Vrtbin vrtbin(STUB_SIM_VBIN_PATH, "0000:00:00"); + EXPECT_FALSE(vrtbin.getSimulationExec().empty()); + EXPECT_TRUE(std::filesystem::exists(vrtbin.getSimulationExec())); +} + +TEST_F(VrtbinSimTest, NoPdiFilesForSimulation) { + vrt::Vrtbin vrtbin(STUB_SIM_VBIN_PATH, "0000:00:00"); + EXPECT_TRUE(vrtbin.getPdiPaths().empty()); +} + +TEST(VrtbinErrorTest, NonexistentVbinThrows) { + EXPECT_THROW(vrt::Vrtbin("/nonexistent/path.vbin", "0000:00:00"), std::runtime_error); +} diff --git a/vrt/tests/vrtbin_test.cpp b/vrt/tests/vrtbin_test.cpp new file mode 100644 index 00000000..1734e71d --- /dev/null +++ b/vrt/tests/vrtbin_test.cpp @@ -0,0 +1,75 @@ +/** + * 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 "test_helpers.hpp" + +class VrtbinHelperTest : public ::testing::Test { + protected: + std::filesystem::path tmpDir; + ScopedEnv* envSlashCache = nullptr; + + void SetUp() override { + tmpDir = makeTempDir("vrtbin-test"); + envSlashCache = new ScopedEnv("SLASH_CACHE_PATH", tmpDir.string()); + } + + void TearDown() override { + delete envSlashCache; + std::filesystem::remove_all(tmpDir); + } +}; + +TEST_F(VrtbinHelperTest, GetSystemMapPathFromBdf) { + auto path = vrt::Vrtbin::getSystemMapPathFromBdf("0000:01:00.0"); + EXPECT_NE(path.find("metadata_0000_01_00_0"), std::string::npos); + EXPECT_NE(path.find("system_map.xml"), std::string::npos); +} + +TEST_F(VrtbinHelperTest, GetUtilizationReportPathFromBdf) { + auto path = vrt::Vrtbin::getUtilizationReportPathFromBdf("0000:01:00.0"); + EXPECT_NE(path.find("metadata_0000_01_00_0"), std::string::npos); + EXPECT_NE(path.find("report_utilization.xml"), std::string::npos); +} + +TEST_F(VrtbinHelperTest, SanitizeAlnum) { + auto path = vrt::Vrtbin::getSystemMapPathFromBdf("abc123"); + EXPECT_NE(path.find("metadata_abc123"), std::string::npos); +} + +TEST_F(VrtbinHelperTest, SanitizeSpecialChars) { + auto path = vrt::Vrtbin::getSystemMapPathFromBdf("0000:01:00.0"); + EXPECT_NE(path.find("metadata_0000_01_00_0"), std::string::npos); + EXPECT_EQ(path.find(":"), std::string::npos); +} + +TEST_F(VrtbinHelperTest, SanitizeEmpty) { + auto path = vrt::Vrtbin::getSystemMapPathFromBdf(""); + EXPECT_NE(path.find("metadata_default"), std::string::npos); +} + +TEST_F(VrtbinHelperTest, PathStartsWithCacheDir) { + auto path = vrt::Vrtbin::getSystemMapPathFromBdf("test"); + EXPECT_EQ(path.rfind(tmpDir.string(), 0), 0u); +} diff --git a/vrt/tests/xml_parser_test.cpp b/vrt/tests/xml_parser_test.cpp new file mode 100644 index 00000000..94b002b4 --- /dev/null +++ b/vrt/tests/xml_parser_test.cpp @@ -0,0 +1,246 @@ +/** + * 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 "test_helpers.hpp" + +class XMLParserTest : public ::testing::Test { + protected: + std::filesystem::path tmpDir; + + void SetUp() override { tmpDir = makeTempDir("xml-parser-test"); } + void TearDown() override { std::filesystem::remove_all(tmpDir); } + + std::string writeXml(const std::string& content) { + return writeTempFile(tmpDir, "system_map.xml", content); + } +}; + +TEST_F(XMLParserTest, ParseSingleKernel) { + auto path = writeXml(R"( + + + vadd + 0x1000 + 0x100 + + 300000000 + Hardware +)"); + vrt::XMLParser parser(path); + parser.parseXML(); + auto kernels = parser.getKernels(); + ASSERT_EQ(kernels.count("vadd"), 1u); + EXPECT_EQ(kernels["vadd"].getName(), "vadd"); + EXPECT_EQ(kernels["vadd"].getPhysAddr(), 0x1000u); +} + +TEST_F(XMLParserTest, ParseKernelRegisters) { + auto path = writeXml(R"( + + + k + 0x0 + 0x100 + + + Hardware +)"); + vrt::XMLParser parser(path); + parser.parseXML(); + auto kernels = parser.getKernels(); + ASSERT_EQ(kernels.count("k"), 1u); +} + +TEST_F(XMLParserTest, ParseKernelFunctionalArgs) { + auto path = writeXml(R"( + + + k + 0x0 + 0x100 + + + + + Hardware +)"); + vrt::XMLParser parser(path); + parser.parseXML(); + auto kernels = parser.getKernels(); + ASSERT_EQ(kernels.count("k"), 1u); + auto& args = kernels["k"].getFunctionalArgs(); + ASSERT_EQ(args.size(), 1u); + EXPECT_EQ(args[0].idx, 0u); + EXPECT_EQ(args[0].name, "input"); + EXPECT_EQ(args[0].type, "buffer"); + EXPECT_EQ(args[0].offset, 0x10u); + EXPECT_EQ(args[0].range, 64u); + EXPECT_FALSE(args[0].readable); + EXPECT_TRUE(args[0].writable); + EXPECT_EQ(args[0].port, "m_axi_gmem0"); +} + +TEST_F(XMLParserTest, FunctionalArgsSortedByIdx) { + auto path = writeXml(R"( + + + k + 0x0 + 0x100 + + + + + + + Hardware +)"); + vrt::XMLParser parser(path); + parser.parseXML(); + auto kernels = parser.getKernels(); + auto& args = kernels["k"].getFunctionalArgs(); + ASSERT_EQ(args.size(), 3u); + EXPECT_EQ(args[0].idx, 0u); + EXPECT_EQ(args[1].idx, 1u); + EXPECT_EQ(args[2].idx, 2u); +} + +TEST_F(XMLParserTest, ParseClockFrequency) { + auto path = writeXml(R"( + + 250000000 + Hardware +)"); + vrt::XMLParser parser(path); + parser.parseXML(); + EXPECT_EQ(parser.getClockFrequency(), 250000000u); +} + +TEST_F(XMLParserTest, ParsePlatformHardware) { + auto path = writeXml(R"( +Hardware)"); + vrt::XMLParser parser(path); + parser.parseXML(); + EXPECT_EQ(parser.getPlatform(), vrt::Platform::HARDWARE); +} + +TEST_F(XMLParserTest, ParsePlatformEmulation) { + auto path = writeXml(R"( +Emulation)"); + vrt::XMLParser parser(path); + parser.parseXML(); + EXPECT_EQ(parser.getPlatform(), vrt::Platform::EMULATION); +} + +TEST_F(XMLParserTest, ParsePlatformSimulation) { + auto path = writeXml(R"( +Simulation)"); + vrt::XMLParser parser(path); + parser.parseXML(); + EXPECT_EQ(parser.getPlatform(), vrt::Platform::SIMULATION); +} + +TEST_F(XMLParserTest, ParsePlatformUnknownThrows) { + auto path = writeXml(R"( +SomethingWeird)"); + vrt::XMLParser parser(path); + EXPECT_THROW(parser.parseXML(), std::runtime_error); +} + +TEST_F(XMLParserTest, ParseQdmaConnections) { + auto path = writeXml(R"( + + Hardware + + myKernel + axis_port + HostToDevice + 3 + +)"); + vrt::XMLParser parser(path); + parser.parseXML(); + auto conns = parser.getQdmaConnections(); + ASSERT_EQ(conns.size(), 1u); + EXPECT_EQ(conns[0].getKernel(), "myKernel"); + EXPECT_EQ(conns[0].getInterface(), "axis_port"); + EXPECT_EQ(conns[0].getDirection(), vrt::StreamDirection::HOST_TO_DEVICE); + EXPECT_EQ(conns[0].getQid(), 3u); +} + +TEST_F(XMLParserTest, ParseMultipleKernels) { + auto path = writeXml(R"( + + + k1 + 0x1000 + 0x100 + + + k2 + 0x2000 + 0x200 + + Hardware +)"); + vrt::XMLParser parser(path); + parser.parseXML(); + auto kernels = parser.getKernels(); + EXPECT_EQ(kernels.size(), 2u); + EXPECT_EQ(kernels.count("k1"), 1u); + EXPECT_EQ(kernels.count("k2"), 1u); +} + +TEST_F(XMLParserTest, InvalidXmlFileThrows) { + auto path = writeTempFile(tmpDir, "bad.xml", "not valid xml <<<<"); + EXPECT_THROW(vrt::XMLParser parser(path), std::runtime_error); +} + +TEST_F(XMLParserTest, EmptySystemMap) { + auto path = writeXml(R"( +)"); + vrt::XMLParser parser(path); + parser.parseXML(); + EXPECT_TRUE(parser.getKernels().empty()); + EXPECT_TRUE(parser.getQdmaConnections().empty()); +} + +TEST_F(XMLParserTest, ParseKernelConnections) { + auto path = writeXml(R"( + + + k + 0x0 + 0x100 + + + + Hardware +)"); + vrt::XMLParser parser(path); + parser.parseXML(); + auto kernels = parser.getKernels(); + ASSERT_EQ(kernels.count("k"), 1u); +}