From 053c6cfd645d31d6fb9b54c3af87c874e68d35f2 Mon Sep 17 00:00:00 2001 From: Georg Hagen Date: Thu, 1 May 2025 11:25:11 +0200 Subject: [PATCH] Add more tests --- openVulkanoCpp/Data/Containers/RingBuffer.hpp | 31 + tests/Data/Containers/RingBufferTest.cpp | 611 ++++++++++++++---- 2 files changed, 526 insertions(+), 116 deletions(-) diff --git a/openVulkanoCpp/Data/Containers/RingBuffer.hpp b/openVulkanoCpp/Data/Containers/RingBuffer.hpp index 43dbcc5..44004a4 100644 --- a/openVulkanoCpp/Data/Containers/RingBuffer.hpp +++ b/openVulkanoCpp/Data/Containers/RingBuffer.hpp @@ -382,6 +382,37 @@ namespace OpenVulkano new (&data()[tail]) T(std::forward(args)...); return oldData; } + + T PushAndOverwrite(const T& value) + { + if (HasFree()) [[unlikely]] throw std::runtime_error("Can't overwrite data because there are still empty slots."); + Impl().IncrementHead(); + T oldData(std::move(Front())); + data()[head].~T(); + new (&data()[head]) T(value); + return oldData; + } + + T PushAndOverwrite(T&& value) + { + if (HasFree()) [[unlikely]] throw std::runtime_error("Can't overwrite data because there are still empty slots."); + Impl().IncrementHead(); + T oldData(std::move(Front())); + data()[head].~T(); + new (&data()[head]) T(std::move(value)); + return oldData; + } + + template + T PushAndOverwrite(Args&&... args) + { + if (HasFree()) [[unlikely]] throw std::runtime_error("Can't overwrite data because there are still empty slots."); + Impl().IncrementHead(); + T oldData(std::move(Front())); + data()[head].~T(); + new (&data()[head]) T(std::forward(args)...); + return oldData; + } //endregion [[nodiscard]] T& at(size_t idx) { if (idx >= Size()) throw std::range_error("Out of bounds"); return (*this)[idx]; } diff --git a/tests/Data/Containers/RingBufferTest.cpp b/tests/Data/Containers/RingBufferTest.cpp index b49864d..ac59c61 100644 --- a/tests/Data/Containers/RingBufferTest.cpp +++ b/tests/Data/Containers/RingBufferTest.cpp @@ -8,182 +8,561 @@ #include "Data/Containers/RingBuffer.hpp" #include #include +#include +#include +#include using namespace OpenVulkano; // Helper to track construction/destruction struct Tracked { - static inline int ctorCount = 0; - static inline int dtorCount = 0; - int value; + static inline int ctorCount = 0; + static inline int dtorCount = 0; + static inline int moveCount = 0; + static inline int copyCount = 0; + int value; - Tracked(int v = 0) : value(v) { ++ctorCount; } - Tracked(const Tracked& other) : value(other.value) { ++ctorCount; } - Tracked(Tracked&& other) noexcept : value(other.value) { ++ctorCount; } - ~Tracked() { ++dtorCount; } + Tracked(int v = 0) : value(v) { ++ctorCount; } + Tracked(const Tracked& other) : value(other.value) { ++ctorCount; ++copyCount; } + Tracked(Tracked&& other) noexcept : value(other.value) { ++ctorCount; ++moveCount; } + ~Tracked() { ++dtorCount; } - Tracked& operator=(const Tracked&) = default; - Tracked& operator=(Tracked&&) = default; + Tracked& operator=(const Tracked& other) { + value = other.value; + ++copyCount; + return *this; + } + + Tracked& operator=(Tracked&& other) noexcept { + value = other.value; + ++moveCount; + return *this; + } - friend bool operator==(const Tracked& lhs, const Tracked& rhs) { - return lhs.value == rhs.value; - } + friend bool operator==(const Tracked& lhs, const Tracked& rhs) { + return lhs.value == rhs.value; + } }; void reset_tracking() { - Tracked::ctorCount = 0; - Tracked::dtorCount = 0; + Tracked::ctorCount = 0; + Tracked::dtorCount = 0; + Tracked::moveCount = 0; + Tracked::copyCount = 0; +} + +TEST_CASE("RingBuffer Basic Construction", "[RingBuffer]") { + SECTION("Static buffer construction") { + RingBuffer buf; + REQUIRE(buf.Capacity() == 5); + REQUIRE(buf.IsEmpty()); + REQUIRE(buf.Count() == 0); + REQUIRE(buf.HasFree()); + } + + SECTION("Dynamic buffer construction") { + RingBuffer buf(10); + REQUIRE(buf.Capacity() == 10); + REQUIRE(buf.IsEmpty()); + REQUIRE(buf.Count() == 0); + REQUIRE(buf.HasFree()); + } } TEST_CASE("RingBuffer Tracking Destruction", "[RingBuffer][Tracked]") { - reset_tracking(); + reset_tracking(); - { - RingBuffer buf; - buf.Push(Tracked(1)); - buf.Push(Tracked(2)); - buf.PopFront(); - buf.Clear(); - } + { + RingBuffer buf; + buf.Push(Tracked(1)); + buf.Push(Tracked(2)); + buf.PopFront(); + buf.Clear(); + } - REQUIRE(Tracked::dtorCount == Tracked::ctorCount); + REQUIRE(Tracked::dtorCount == Tracked::ctorCount); +} + +TEST_CASE("RingBuffer Move and Copy Semantics", "[RingBuffer][Tracked]") { + reset_tracking(); + + { + RingBuffer buf; + + SECTION("Push with move") { + Tracked t(42); + buf.Push(std::move(t)); + REQUIRE(Tracked::moveCount >= 1); + REQUIRE(buf.Front().value == 42); + } + + SECTION("Push with copy") { + Tracked t(42); + buf.Push(t); + REQUIRE(Tracked::copyCount >= 1); + REQUIRE(buf.Front().value == 42); + } + + SECTION("Emplace constructs in-place") { + int initialCtors = Tracked::ctorCount; + buf.Emplace(99); + REQUIRE(Tracked::ctorCount == initialCtors + 1); + REQUIRE(buf.Front().value == 99); + } + } + + REQUIRE(Tracked::dtorCount == Tracked::ctorCount); } TEST_CASE("RingBuffer Push and Pop Operations", "[RingBuffer]") { - RingBuffer buf; + RingBuffer buf; - buf.Push(1); - buf.Push(2); - buf.Push(3); + buf.Push(1); + buf.Push(2); + buf.Push(3); - REQUIRE(buf.Count() == 3); - REQUIRE_FALSE(buf.HasFree()); + REQUIRE(buf.Count() == 3); + REQUIRE_FALSE(buf.HasFree()); - SECTION("PopFront/Back correctness") { - REQUIRE(buf.PopBack() == 1); - REQUIRE(buf.PopFront() == 3); - REQUIRE(buf.Count() == 1); - } + SECTION("PopFront/Back correctness") { + REQUIRE(buf.PopBack() == 1); + REQUIRE(buf.PopFront() == 3); + REQUIRE(buf.Count() == 1); + REQUIRE(buf.PopFront() == 2); + REQUIRE(buf.IsEmpty()); + REQUIRE_THROWS_AS(buf.PopFront(), std::underflow_error); + REQUIRE_THROWS_AS(buf.PopBack(), std::underflow_error); + } - SECTION("Clear empties the buffer") { - buf.Clear(); - REQUIRE(buf.IsEmpty()); - REQUIRE(buf.Count() == 0); - } + SECTION("Clear empties the buffer") { + buf.Clear(); + REQUIRE(buf.IsEmpty()); + REQUIRE(buf.Count() == 0); + } + + SECTION("Front and Back access") { + REQUIRE(buf.Front() == 3); + REQUIRE(buf.Back() == 1); + + buf.PopFront(); + REQUIRE(buf.Front() == 2); + REQUIRE(buf.Back() == 1); + + buf.PopBack(); + REQUIRE(buf.Front() == 2); + REQUIRE(buf.Back() == 2); + } +} + +TEST_CASE("RingBuffer PushNoOverwrite operation", "[RingBuffer]") { + RingBuffer buf; + + buf.PushNoOverwrite(1); + buf.PushNoOverwrite(2); + REQUIRE_THROWS_AS(buf.PushNoOverwrite(3), std::overflow_error); + + SECTION("EmplaceNoOverwrite") { + RingBuffer strBuf; + strBuf.EmplaceNoOverwrite("first"); + strBuf.EmplaceNoOverwrite("second"); + REQUIRE_THROWS_AS(strBuf.EmplaceNoOverwrite("third"), std::overflow_error); + } +} + +TEST_CASE("RingBuffer PushAndOverwrite operation", "[RingBuffer]") { + RingBuffer buf; + + SECTION("Cannot use PushAndOverwrite when buffer not full") { + buf.Push(1); + REQUIRE_THROWS_AS(buf.PushAndOverwrite(5), std::runtime_error); + } + + SECTION("PushAndOverwrite returns overwritten value") { + buf.Fill(42); + REQUIRE(buf.Count() == 2); + + int overwritten = buf.PushAndOverwrite(99); + REQUIRE(overwritten == 42); + REQUIRE(buf.Front() == 99); + REQUIRE(buf.Back() == 42); + } } TEST_CASE("PushBack and PushFront Overwrite Logic", "[RingBuffer]") { - RingBuffer buf; - buf.Push(1); - buf.Push(2); + RingBuffer buf; + buf.Push(1); + buf.Push(2); - auto overwrittenFront = buf.PushFront(3); - REQUIRE(overwrittenFront.has_value()); - REQUIRE(overwrittenFront.value() == 1); + SECTION("PushFront overwrites correctly") { + auto overwrittenFront = buf.PushFront(3); + REQUIRE(overwrittenFront.has_value()); + REQUIRE(overwrittenFront.value() == 1); + REQUIRE(buf.Front() == 3); + REQUIRE(buf.Back() == 2); + } - auto overwrittenBack = buf.PushBack(4); - REQUIRE(overwrittenBack.has_value()); - REQUIRE(overwrittenBack.value() == 2); - - REQUIRE(buf.Front() == 3); - REQUIRE(buf.Back() == 4); + SECTION("PushBack overwrites correctly") { + auto overwrittenBack = buf.PushBack(4); + REQUIRE(overwrittenBack.has_value()); + REQUIRE(overwrittenBack.value() == 1); + REQUIRE(buf.Front() == 2); + REQUIRE(buf.Back() == 4); + } + + SECTION("PushFront with available space doesn't overwrite") { + RingBuffer buf2; + buf2.Push(10); + + auto result = buf2.PushFront(20); + REQUIRE_FALSE(result.has_value()); + REQUIRE(buf2.Count() == 2); + REQUIRE(buf2.Front() == 20); + REQUIRE(buf2.Back() == 10); + } + + SECTION("PushBack with available space doesn't overwrite") { + RingBuffer buf2; + buf2.Push(10); + + auto result = buf2.PushBack(20); + REQUIRE_FALSE(result.has_value()); + REQUIRE(buf2.Count() == 2); + REQUIRE(buf2.Front() == 10); + REQUIRE(buf2.Back() == 20); + } } TEST_CASE("at() bounds checking", "[RingBuffer]") { - RingBuffer buf; - buf.Push(100); - buf.Push(200); + RingBuffer buf; + buf.Push(100); + buf.Push(200); - REQUIRE(buf.at(0) == 100); - REQUIRE(buf.at(1) == 200); - REQUIRE_THROWS_AS(buf.at(2), std::range_error); + REQUIRE(buf.at(0) == 100); + REQUIRE(buf.at(1) == 200); + REQUIRE_THROWS_AS(buf.at(2), std::range_error); + + SECTION("const at() works correctly") { + const auto& constBuf = buf; + REQUIRE(constBuf.at(0) == 100); + REQUIRE(constBuf.at(1) == 200); + REQUIRE_THROWS_AS(constBuf.at(2), std::range_error); + } } TEST_CASE("Index-based Access and Wraparound", "[RingBuffer]") { - RingBuffer buf; + RingBuffer buf; - buf.Push(1); - buf.Push(2); - buf.Push(3); - REQUIRE(buf[0] == 1); - REQUIRE(buf[1] == 2); - REQUIRE(buf[2] == 3); + buf.Push(1); + buf.Push(2); + buf.Push(3); + REQUIRE(buf[0] == 1); + REQUIRE(buf[1] == 2); + REQUIRE(buf[2] == 3); - buf.PopBack(); - buf.Push(4); + buf.PopBack(); + buf.Push(4); - REQUIRE(buf[0] == 2); - REQUIRE(buf[1] == 3); - REQUIRE(buf[2] == 4); + REQUIRE(buf[0] == 2); + REQUIRE(buf[1] == 3); + REQUIRE(buf[2] == 4); - // Test full wraparound - buf.Push(5); - buf.Push(6); + // Test full wraparound + buf.Push(5); + buf.Push(6); - REQUIRE(buf[0] == 4); - REQUIRE(buf[1] == 5); - REQUIRE(buf[2] == 6); + REQUIRE(buf[0] == 4); + REQUIRE(buf[1] == 5); + REQUIRE(buf[2] == 6); + + SECTION("Wraparound with power-of-2 size") { + RingBuffer powBuf; // Power of 2 size for different code path + + for (int i = 1; i <= 4; i++) { + powBuf.Push(i); + } + + REQUIRE(powBuf[0] == 1); + REQUIRE(powBuf[3] == 4); + + powBuf.PopBack(); + powBuf.Push(5); + + REQUIRE(powBuf[0] == 2); + REQUIRE(powBuf[3] == 5); + } +} + +TEST_CASE("Fill operation", "[RingBuffer]") { + RingBuffer buf; + + SECTION("Fill empty buffer") { + buf.Fill(42); + REQUIRE(buf.Count() == 5); + REQUIRE_FALSE(buf.HasFree()); + + for (int i = 0; i < 5; i++) { + REQUIRE(buf[i] == 42); + } + } + + SECTION("Fill partially full buffer") { + buf.Push(1); + buf.Push(2); + + buf.Fill(99); + REQUIRE(buf.Count() == 5); + REQUIRE_FALSE(buf.HasFree()); + + REQUIRE(buf[0] == 1); + REQUIRE(buf[1] == 2); + REQUIRE(buf[2] == 99); + REQUIRE(buf[3] == 99); + REQUIRE(buf[4] == 99); + } } TEST_CASE("Emplace and EmplaceBack/Front work correctly", "[RingBuffer]") { - RingBuffer buf; + RingBuffer buf; - buf.Emplace("first"); - buf.EmplaceBack("second"); + buf.Emplace("first"); + buf.EmplaceBack("second"); - REQUIRE(buf.Count() == 2); - REQUIRE(buf[1] == "first"); - REQUIRE(buf[0] == "second"); + REQUIRE(buf.Count() == 2); + REQUIRE(buf[1] == "first"); + REQUIRE(buf[0] == "second"); - auto overwritten = buf.EmplaceFront("new"); - REQUIRE(overwritten.has_value()); - REQUIRE(overwritten.value() == "second"); - REQUIRE(buf.Front() == "new"); + auto overwritten = buf.EmplaceFront("new"); + REQUIRE(overwritten.has_value()); + REQUIRE(overwritten.value() == "second"); + REQUIRE(buf.Front() == "new"); + + SECTION("EmplaceNoOverwrite throws when full") { + RingBuffer buf2; + buf2.EmplaceNoOverwrite("one"); + buf2.EmplaceNoOverwrite("two"); + REQUIRE_THROWS_AS(buf2.EmplaceNoOverwrite("three"), std::overflow_error); + } } TEST_CASE("Iterators forward and reverse", "[RingBuffer]") { - RingBuffer buf; - buf.Push(10); - buf.Push(20); - buf.Push(30); + RingBuffer buf; + buf.Push(10); + buf.Push(20); + buf.Push(30); - std::vector forward; - for (int val : buf) forward.push_back(val); - REQUIRE(forward == std::vector{10, 20, 30}); + SECTION("Forward iteration") { + std::vector forward; + for (int val : buf) forward.push_back(val); + REQUIRE(forward == std::vector{10, 20, 30}); + } - std::vector reverse; - for (auto it = buf.rbegin(); it != buf.rend(); ++it) - reverse.push_back(*it); - REQUIRE(reverse == std::vector{30, 20, 10}); + SECTION("Reverse iteration") { + std::vector reverse; + for (auto it = buf.rbegin(); it != buf.rend(); ++it) + reverse.push_back(*it); + REQUIRE(reverse == std::vector{30, 20, 10}); + } + + SECTION("Iterator decrement") { + auto it = buf.end(); + --it; + REQUIRE(*it == 30); + --it; + REQUIRE(*it == 20); + --it; + REQUIRE(*it == 10); + } + + SECTION("Reverse iterator decrement") { + auto it = buf.rend(); + --it; + REQUIRE(*it == 10); + --it; + REQUIRE(*it == 20); + --it; + REQUIRE(*it == 30); + } + + SECTION("Iterator post-increment/decrement") { + auto it = buf.begin(); + REQUIRE(*(it++) == 10); + REQUIRE(*it == 20); + + auto it2 = --buf.end(); + REQUIRE(*(it2--) == 30); + REQUIRE(*it2 == 20); + } + + SECTION("Empty buffer iteration") { + RingBuffer emptyBuf; + REQUIRE(emptyBuf.begin() == emptyBuf.end()); + REQUIRE(emptyBuf.rbegin() == emptyBuf.rend()); + } } TEST_CASE("Const correctness in iterators", "[RingBuffer][Const]") { - RingBuffer buf; - buf.Push(5); - buf.Push(6); + RingBuffer buf; + buf.Push(5); + buf.Push(6); - const auto& constBuf = buf; + const auto& constBuf = buf; - std::ostringstream oss; - for (auto it = constBuf.cbegin(); it != constBuf.cend(); ++it) { - oss << *it << " "; - } - - REQUIRE(oss.str() == "5 6 "); + SECTION("const_iterator usage") { + std::ostringstream oss; + for (auto it = constBuf.cbegin(); it != constBuf.cend(); ++it) { + oss << *it << " "; + } + REQUIRE(oss.str() == "5 6 "); + } + + SECTION("const rbegin/rend usage") { + std::vector values; + for (auto it = constBuf.crbegin(); it != constBuf.crend(); ++it) { + values.push_back(*it); + } + REQUIRE(values == std::vector{6, 5}); + } + + SECTION("const operator[]") { + REQUIRE(constBuf[0] == 5); + REQUIRE(constBuf[1] == 6); + } } TEST_CASE("Dynamic RingBuffer behaves like static", "[RingBuffer][Dynamic]") { - RingBuffer buf(5); + RingBuffer buf(5); - for (int i = 0; i < 5; ++i) - buf.Push(i * 10); + for (int i = 0; i < 5; ++i) + buf.Push(i * 10); - REQUIRE(buf.Count() == 5); - REQUIRE_FALSE(buf.HasFree()); - REQUIRE(buf.Front() == 40); - REQUIRE(buf.Back() == 0); + REQUIRE(buf.Count() == 5); + REQUIRE_FALSE(buf.HasFree()); + REQUIRE(buf.Front() == 40); + REQUIRE(buf.Back() == 0); - auto val = buf.PopBack(); - REQUIRE(val == 0); - REQUIRE(buf.Count() == 4); + auto val = buf.PopBack(); + REQUIRE(val == 0); + REQUIRE(buf.Count() == 4); + + SECTION("Dynamic buffer wraparound") { + buf.Push(50); + REQUIRE(buf.Front() == 50); + REQUIRE(buf.Back() == 10); + + buf.Push(60); + REQUIRE(buf[0] == 20); + REQUIRE(buf[1] == 30); + REQUIRE(buf[2] == 40); + REQUIRE(buf[3] == 50); + REQUIRE(buf[4] == 60); + } } + +TEST_CASE("RingBuffer with complex types", "[RingBuffer][Complex]") { + RingBuffer, 3> buf; + + buf.Push(std::vector{1, 2, 3}); + buf.Emplace(std::initializer_list{4, 5, 6}); + + REQUIRE(buf.Count() == 2); + REQUIRE(buf.Front()[0] == 4); + REQUIRE(buf.Front()[2] == 6); + REQUIRE(buf.Back()[0] == 1); + REQUIRE(buf.Back()[2] == 3); + + buf.PushFront(std::vector{7, 8, 9}); + REQUIRE(buf.Front()[0] == 7); + + buf.Clear(); + REQUIRE(buf.IsEmpty()); +} + +TEST_CASE("Iterator operations with wrapped buffer", "[RingBuffer][Iterators]") { + RingBuffer buf; + + // Fill buffer + for (int i = 1; i <= 5; i++) { + buf.Push(i); + } + + // Force wraparound + buf.PopBack(); + buf.PopBack(); + buf.Push(6); + buf.Push(7); + + // Buffer should now be: 3, 4, 5, 6, 7 with internal wraparound + + std::vector values; + for (auto val : buf) { + values.push_back(val); + } + + REQUIRE(values == std::vector{3, 4, 5, 6, 7}); + + std::vector reverseValues; + for (auto it = buf.rbegin(); it != buf.rend(); ++it) { + reverseValues.push_back(*it); + } + + REQUIRE(reverseValues == std::vector{7, 6, 5, 4, 3}); +} + +TEST_CASE("Edge cases", "[RingBuffer][EdgeCases]") { + SECTION("Capacity of 1") { + RingBuffer buf; + REQUIRE(buf.Capacity() == 1); + REQUIRE(buf.IsEmpty()); + + buf.Push(42); + REQUIRE(buf.Front() == 42); + REQUIRE(buf.Back() == 42); + REQUIRE_FALSE(buf.HasFree()); + + buf.Push(99); + REQUIRE(buf.Front() == 99); + REQUIRE(buf.Back() == 99); + + buf.PopFront(); + REQUIRE(buf.IsEmpty()); + REQUIRE_THROWS_AS(buf.PopFront(), std::underflow_error); + } + + SECTION("Dynamic buffer with size 1") { + RingBuffer buf(1); + buf.Push(42); + REQUIRE(buf.Front() == 42); + REQUIRE_FALSE(buf.HasFree()); + + buf.Push(99); + REQUIRE(buf.Front() == 99); + } +} + +TEST_CASE("Algorithm compatibility", "[RingBuffer][Algorithms]") { + RingBuffer buf; + for (int i = 1; i <= 5; i++) { + buf.Push(i); + } + + SECTION("std::find") { + auto it = std::find(buf.begin(), buf.end(), 3); + REQUIRE(it != buf.end()); + REQUIRE(*it == 3); + + auto notFound = std::find(buf.begin(), buf.end(), 99); + REQUIRE(notFound == buf.end()); + } + + SECTION("std::count") { + buf.Push(3); // Overwrites the 1 + int count = std::count(buf.begin(), buf.end(), 3); + REQUIRE(count == 2); + } + + SECTION("std::copy") { + std::vector target(5); + std::copy(buf.begin(), buf.end(), target.begin()); + REQUIRE(target == std::vector{1, 2, 3, 4, 5}); + } +} \ No newline at end of file