diff --git a/.github/workflows/agent_ci.yml b/.github/workflows/agent_ci.yml index 0cdde55..1db0435 100644 --- a/.github/workflows/agent_ci.yml +++ b/.github/workflows/agent_ci.yml @@ -44,7 +44,7 @@ jobs: - name: Check Code Formatting run: | - find sources/agent/src -name "*.cc" -o -name "*.h" | xargs clang-format --dry-run --Werror -style=Google + find sources/agent/src sources/agent/tests -name "*.cc" -o -name "*.h" | xargs clang-format --dry-run --Werror -style=Google - name: CMake Configure working-directory: sources/agent @@ -54,6 +54,10 @@ jobs: working-directory: sources/agent run: cmake --build --preset default -j$(nproc) + - name: Test + working-directory: sources/agent/build + run: ctest --output-on-failure + - name: Analyze Logic run: | cppcheck --enable=warning,performance,portability \ @@ -61,6 +65,7 @@ jobs: --suppress=missingIncludeSystem \ --inline-suppr \ --force \ + --library=googletest \ -i sources/agent/build \ -i sources/agent/gen \ sources/agent diff --git a/sources/agent/CMakeLists.txt b/sources/agent/CMakeLists.txt index cc18f66..bea2896 100644 --- a/sources/agent/CMakeLists.txt +++ b/sources/agent/CMakeLists.txt @@ -73,25 +73,26 @@ target_link_libraries(volta_proto PUBLIC gRPC::grpc++_reflection ) -# --- Agent executable --- -add_executable(volta) +# --- Agent library (shared by volta and volta_tests) --- +file(GLOB_RECURSE AGENT_LIB_SOURCES CONFIGURE_DEPENDS "src/*.cc") +list(FILTER AGENT_LIB_SOURCES EXCLUDE REGEX ".*/main\\.cc$") + +add_library(volta_lib STATIC) +target_sources(volta_lib PRIVATE ${AGENT_LIB_SOURCES}) -target_compile_options(volta PRIVATE -std=c++20) +target_compile_options(volta_lib PRIVATE -std=c++20) if(CMAKE_BUILD_TYPE STREQUAL "Debug") message(STATUS "Debug build - adding DEBUG macro") - target_compile_definitions(volta PRIVATE DEBUG) + target_compile_definitions(volta_lib PUBLIC DEBUG) endif() -file(GLOB_RECURSE AGENT_SOURCES CONFIGURE_DEPENDS "src/*.cc") -target_sources(volta PRIVATE ${AGENT_SOURCES}) - -target_include_directories(volta PRIVATE +target_include_directories(volta_lib PUBLIC src ${CMAKE_SOURCE_DIR}/libs/include/nvidia ) -target_link_libraries(volta PRIVATE +target_link_libraries(volta_lib PUBLIC volta_proto Threads::Threads fmt::fmt @@ -99,7 +100,11 @@ target_link_libraries(volta PRIVATE ${CMAKE_DL_LIBS} ) -target_compile_definitions(volta PRIVATE NVML_NO_UNVERSIONED_FUNC_DEFS) +target_compile_definitions(volta_lib PUBLIC NVML_NO_UNVERSIONED_FUNC_DEFS) + +# --- Agent executable --- +add_executable(volta src/main.cc) +target_link_libraries(volta PRIVATE volta_lib) # --- Config file --- if(NOT EXISTS ${CMAKE_SOURCE_DIR}/agent.conf) @@ -131,18 +136,36 @@ add_custom_command( COMMENT "Creating hardlink voltad..." ) +# --- Unit tests --- +include(CTest) +if(BUILD_TESTING) + find_package(GTest CONFIG REQUIRED) + + add_executable(volta_tests) + target_sources(volta_tests PRIVATE + tests/series_buffer_test.cc + ) + target_link_libraries(volta_tests PRIVATE + volta_lib + GTest::gtest_main + ) + + include(GoogleTest) + gtest_discover_tests(volta_tests) +endif() + # --- Formatting --- find_program(CLANG_FORMAT_EXE clang-format) if(CLANG_FORMAT_EXE) add_custom_target(format - COMMAND find src -name "*.cc" -o -name "*.h" | xargs ${CLANG_FORMAT_EXE} -i -style=Google + COMMAND find src tests -name "*.cc" -o -name "*.h" | xargs ${CLANG_FORMAT_EXE} -i -style=Google WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMENT "Running clang-format on agent sources..." VERBATIM ) add_custom_target(check-format - COMMAND find src -name "*.cc" -o -name "*.h" | xargs ${CLANG_FORMAT_EXE} --dry-run --Werror -style=Google + COMMAND find src tests -name "*.cc" -o -name "*.h" | xargs ${CLANG_FORMAT_EXE} --dry-run --Werror -style=Google WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMENT "Checking agent source formatting..." VERBATIM diff --git a/sources/agent/README.md b/sources/agent/README.md index b5ae934..d19c8f3 100644 --- a/sources/agent/README.md +++ b/sources/agent/README.md @@ -37,7 +37,21 @@ cmake -B build -S . --preset default cmake --build build ``` -### 4. Run Agent +### 4. Run Tests + +Tests build by default (`BUILD_TESTING=ON`). After a successful build: + +```bash +# Run the discovered GoogleTest suite via CTest +ctest --test-dir build --output-on-failure + +# Or run the test binary directly +./build/volta_tests +``` + +To configure without tests: `cmake -B build -S . --preset default -DBUILD_TESTING=OFF`. + +### 5. Run Agent ```bash # May require root/admin privileges to access RAPL/Affinity features. diff --git a/sources/agent/src/buffer.cc b/sources/agent/src/buffer.cc index fbff1e9..d9a459d 100644 --- a/sources/agent/src/buffer.cc +++ b/sources/agent/src/buffer.cc @@ -51,6 +51,7 @@ void SeriesBuffer::SetCapacity(size_t capacity) { samples_.reserve(capacity_); samples_.resize(capacity_); head_ = 0; + tail_ = 0; } void SeriesBuffer::Push(const Sample& sample) { diff --git a/sources/agent/src/buffer.h b/sources/agent/src/buffer.h index 0fe06d7..3ae6d19 100644 --- a/sources/agent/src/buffer.h +++ b/sources/agent/src/buffer.h @@ -74,8 +74,8 @@ class SeriesBuffer { void SetCapacity(size_t capacity); size_t Capacity() const { return capacity_; } - size_t Size() const { return samples_.size(); } - bool Empty() const { return samples_.empty(); } + size_t Size() const { return head_ - tail_; } + bool Empty() const { return Size() == 0; } size_t GetHead() const { return head_; } size_t GetTail() const { return tail_; } diff --git a/sources/agent/tests/series_buffer_test.cc b/sources/agent/tests/series_buffer_test.cc new file mode 100644 index 0000000..b0057c1 --- /dev/null +++ b/sources/agent/tests/series_buffer_test.cc @@ -0,0 +1,189 @@ +#include + +#include "buffer.h" +#include "gtest/gtest.h" + +namespace volta { +namespace agent { +namespace { + +Sample MakeSample(int64_t timestamp_ns, double value) { + return Sample{.timestamp_ns = timestamp_ns, .value = value}; +} + +TEST(SeriesBufferTest, FreshBufferIsEmptyAndHasNoLatest) { + SeriesBuffer buffer(4); + + EXPECT_TRUE(buffer.Empty()); + EXPECT_EQ(buffer.Size(), 0u); + EXPECT_EQ(buffer.Capacity(), 4u); + EXPECT_FALSE(buffer.Latest().has_value()); + EXPECT_EQ(buffer.GetNextSnapshotSize(), 0u); +} + +TEST(SeriesBufferTest, PushToZeroCapacityIsNoOp) { + SeriesBuffer buffer(0); + + buffer.Push(MakeSample(1, 1.0)); + + EXPECT_TRUE(buffer.Empty()); + EXPECT_EQ(buffer.Size(), 0u); + EXPECT_FALSE(buffer.Latest().has_value()); + EXPECT_EQ(buffer.GetNextSnapshotSize(), 0u); +} + +TEST(SeriesBufferTest, PushUpToCapacityRetainsSamplesInOrder) { + SeriesBuffer buffer(3); + buffer.Push(MakeSample(10, 1.0)); + buffer.Push(MakeSample(20, 2.0)); + buffer.Push(MakeSample(30, 3.0)); + + EXPECT_FALSE(buffer.Empty()); + EXPECT_EQ(buffer.Size(), 3u); + EXPECT_EQ(buffer.GetNextSnapshotSize(), 3u); + + const auto snapshot = buffer.GetSnapshot(); + ASSERT_EQ(snapshot.samples.size(), 3u); + EXPECT_EQ(snapshot.samples[0].timestamp_ns, 10); + EXPECT_EQ(snapshot.samples[0].value, 1.0); + EXPECT_EQ(snapshot.samples[1].timestamp_ns, 20); + EXPECT_EQ(snapshot.samples[1].value, 2.0); + EXPECT_EQ(snapshot.samples[2].timestamp_ns, 30); + EXPECT_EQ(snapshot.samples[2].value, 3.0); + EXPECT_EQ(snapshot.end, buffer.GetHead()); +} + +TEST(SeriesBufferTest, PushBeyondCapacityEvictsOldestAndAdvancesTail) { + SeriesBuffer buffer(2); + buffer.Push(MakeSample(10, 1.0)); + buffer.Push(MakeSample(20, 2.0)); + const size_t tail_before_overflow = buffer.GetTail(); + + buffer.Push(MakeSample(30, 3.0)); + + EXPECT_EQ(buffer.Size(), 2u); + EXPECT_EQ(buffer.GetTail(), tail_before_overflow + 1); + EXPECT_EQ(buffer.GetNextSnapshotSize(), 2u); + + const auto snapshot = buffer.GetSnapshot(); + ASSERT_EQ(snapshot.samples.size(), 2u); + EXPECT_EQ(snapshot.samples[0].timestamp_ns, 20); + EXPECT_EQ(snapshot.samples[0].value, 2.0); + EXPECT_EQ(snapshot.samples[1].timestamp_ns, 30); + EXPECT_EQ(snapshot.samples[1].value, 3.0); +} + +TEST(SeriesBufferTest, LatestReturnsMostRecentSampleIncludingAfterWrap) { + SeriesBuffer buffer(2); + buffer.Push(MakeSample(10, 1.0)); + ASSERT_TRUE(buffer.Latest().has_value()); + EXPECT_EQ(buffer.Latest()->timestamp_ns, 10); + EXPECT_EQ(buffer.Latest()->value, 1.0); + + buffer.Push(MakeSample(20, 2.0)); + buffer.Push(MakeSample(30, 3.0)); + + ASSERT_TRUE(buffer.Latest().has_value()); + EXPECT_EQ(buffer.Latest()->timestamp_ns, 30); + EXPECT_EQ(buffer.Latest()->value, 3.0); +} + +TEST(SeriesBufferTest, GetNextSnapshotSizeEqualsUnsentCount) { + SeriesBuffer buffer(4); + EXPECT_EQ(buffer.GetNextSnapshotSize(), 0u); + + buffer.Push(MakeSample(1, 1.0)); + buffer.Push(MakeSample(2, 2.0)); + EXPECT_EQ(buffer.GetNextSnapshotSize(), 2u); + + const auto snapshot = buffer.GetSnapshot(); + buffer.AckSnapshot(snapshot); + EXPECT_EQ(buffer.GetNextSnapshotSize(), 0u); + + buffer.Push(MakeSample(3, 3.0)); + EXPECT_EQ(buffer.GetNextSnapshotSize(), 1u); +} + +TEST(SeriesBufferTest, GetSnapshotReturnsUnsentSamplesAndEndMarker) { + SeriesBuffer buffer(4); + buffer.Push(MakeSample(11, 1.5)); + buffer.Push(MakeSample(22, 2.5)); + + const auto snapshot = buffer.GetSnapshot(); + + ASSERT_EQ(snapshot.samples.size(), 2u); + EXPECT_EQ(snapshot.samples[0].timestamp_ns, 11); + EXPECT_EQ(snapshot.samples[0].value, 1.5); + EXPECT_EQ(snapshot.samples[1].timestamp_ns, 22); + EXPECT_EQ(snapshot.samples[1].value, 2.5); + EXPECT_EQ(snapshot.end, buffer.GetHead()); +} + +TEST(SeriesBufferTest, AckSnapshotAdvancesTailPastAckedSamples) { + SeriesBuffer buffer(4); + buffer.Push(MakeSample(1, 1.0)); + buffer.Push(MakeSample(2, 2.0)); + buffer.Push(MakeSample(3, 3.0)); + + const auto snapshot = buffer.GetSnapshot(); + const size_t head_at_ack = snapshot.end; + buffer.AckSnapshot(snapshot); + + EXPECT_EQ(buffer.GetTail(), head_at_ack); + EXPECT_EQ(buffer.GetNextSnapshotSize(), 0u); + EXPECT_TRUE(buffer.Empty()); +} + +TEST(SeriesBufferTest, AckSnapshotIsMonotonicAndIdempotent) { + SeriesBuffer buffer(4); + buffer.Push(MakeSample(1, 1.0)); + buffer.Push(MakeSample(2, 2.0)); + buffer.Push(MakeSample(3, 3.0)); + + const auto first = buffer.GetSnapshot(); + buffer.AckSnapshot(first); + const size_t tail_after_first = buffer.GetTail(); + + buffer.AckSnapshot(first); + EXPECT_EQ(buffer.GetTail(), tail_after_first); + + SeriesBuffer::Snapshot stale{.samples = {}, .end = 0}; + buffer.AckSnapshot(stale); + EXPECT_EQ(buffer.GetTail(), tail_after_first); + + buffer.Push(MakeSample(4, 4.0)); + const auto second = buffer.GetSnapshot(); + ASSERT_GT(second.end, first.end); + buffer.AckSnapshot(second); + EXPECT_EQ(buffer.GetTail(), second.end); + EXPECT_GE(buffer.GetTail(), tail_after_first); +} + +TEST(SeriesBufferTest, SetCapacityResetsToEmptyConsistentState) { + SeriesBuffer buffer(3); + buffer.Push(MakeSample(1, 1.0)); + buffer.Push(MakeSample(2, 2.0)); + buffer.Push(MakeSample(3, 3.0)); + buffer.Push(MakeSample(4, 4.0)); // wrap; advances tail + ASSERT_GT(buffer.GetTail(), 0u); + ASSERT_GT(buffer.GetHead(), 0u); + + buffer.SetCapacity(5); + + EXPECT_EQ(buffer.Capacity(), 5u); + EXPECT_EQ(buffer.GetHead(), 0u); + EXPECT_EQ(buffer.GetTail(), 0u); + EXPECT_TRUE(buffer.Empty()); + EXPECT_EQ(buffer.Size(), 0u); + EXPECT_EQ(buffer.GetNextSnapshotSize(), 0u); + EXPECT_FALSE(buffer.Latest().has_value()); + + // Snapshot sizing must not underflow after reset on a previously live buffer. + const auto snapshot = buffer.GetSnapshot(); + EXPECT_TRUE(snapshot.samples.empty()); + EXPECT_EQ(snapshot.end, 0u); +} + +} // namespace +} // namespace agent +} // namespace volta diff --git a/sources/agent/vcpkg.json b/sources/agent/vcpkg.json index f7b4c46..8f542e8 100644 --- a/sources/agent/vcpkg.json +++ b/sources/agent/vcpkg.json @@ -5,6 +5,7 @@ "dependencies": [ "fmt", "grpc", + "gtest", "protobuf", "tomlplusplus" ]