From 93655fd3c87a99562cab217e8c59d3416743d33b Mon Sep 17 00:00:00 2001 From: tzcnt Date: Tue, 2 Jun 2026 21:23:38 -0700 Subject: [PATCH] tests for Chase-Lev deque --- tests/CMakeLists.txt | 3 +- tests/test_chase_lev_deque.cpp | 522 +++++++++++++++++++++++++++++++++ tests/test_qu_lockfree.cpp | 104 ------- tests/test_qu_mc.cpp | 59 ++++ 4 files changed, 583 insertions(+), 105 deletions(-) create mode 100644 tests/test_chase_lev_deque.cpp delete mode 100644 tests/test_qu_lockfree.cpp create mode 100644 tests/test_qu_mc.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f9cfd56..c26846b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -133,7 +133,8 @@ make_exe_base( test_yield.cpp test_misc.cpp test_coro_functor.cpp - test_qu_lockfree.cpp + test_chase_lev_deque.cpp + test_qu_mc.cpp test_qu_mpsc_unbounded.cpp test_qu_mpsc_bounded.cpp test_qu_spsc_unbounded.cpp diff --git a/tests/test_chase_lev_deque.cpp b/tests/test_chase_lev_deque.cpp new file mode 100644 index 0000000..35e2bdf --- /dev/null +++ b/tests/test_chase_lev_deque.cpp @@ -0,0 +1,522 @@ +// Tests for tmc::detail::chase_lev_deque. +// +// Covers single-threaded correctness (push/pop LIFO at the tail, +// steal FIFO at the head), buffer growth, post_bulk, the CAS race on +// the last element, index wrap-around, and multi-threaded +// owner-vs-stealers scenarios. + +#include "tmc/all_headers.hpp" // IWYU pragma: keep + +#include + +#include +#include +#include +#include +#include +#include + +#define CATEGORY test_chase_lev_deque_32bit + +using tmc::detail::chase_lev_deque; + +namespace { + +// Helper: drain everything the owner can pop and return it in pop order +// (which is LIFO, i.e. reverse of the push order). +template std::vector drain_pop(chase_lev_deque& Q) { + std::vector out; + T v{}; + while (Q.try_pop(v)) { + out.push_back(v); + } + return out; +} + +} // namespace + +class CATEGORY : public testing::Test {}; + +// --------------------------------------------------------------------------- +// Construction +// --------------------------------------------------------------------------- + +TEST_F(CATEGORY, default_construct_is_empty) { + chase_lev_deque q; + EXPECT_TRUE(q.empty()); + EXPECT_EQ(0u, q.size_approx()); + + size_t v = 42; + EXPECT_FALSE(q.try_pop(v)); + EXPECT_FALSE(q.steal(v)); +} + +TEST_F(CATEGORY, custom_initial_capacity_rounds_up_to_power_of_two) { + // 3 -> rounds up to 4; we should be able to push 4 items without + // triggering a grow (we cannot directly observe capacity from outside, + // but we can at least confirm operations succeed). + chase_lev_deque q(3); + for (size_t i = 0; i < 4; ++i) { + q.push(i); + } + EXPECT_EQ(4u, q.size_approx()); +} + +TEST_F(CATEGORY, capacity_exactly_one) { + chase_lev_deque q(1); + q.push(7u); + // Force a grow on the next push (capacity == 1). + q.push(8u); + EXPECT_EQ(2u, q.size_approx()); + + size_t v = 0; + EXPECT_TRUE(q.try_pop(v)); + EXPECT_EQ(8, v); + EXPECT_TRUE(q.try_pop(v)); + EXPECT_EQ(7, v); + EXPECT_FALSE(q.try_pop(v)); +} + +// --------------------------------------------------------------------------- +// Single-threaded push / pop / steal +// --------------------------------------------------------------------------- + +TEST_F(CATEGORY, push_then_pop_is_lifo) { + chase_lev_deque q; + for (size_t i = 0; i < 10; ++i) { + q.push(i); + } + EXPECT_EQ(10u, q.size_approx()); + + auto popped = drain_pop(q); + ASSERT_EQ(10u, popped.size()); + for (size_t i = 0; i < 10; ++i) { + EXPECT_EQ(9 - i, popped[static_cast(i)]); + } + EXPECT_TRUE(q.empty()); +} + +TEST_F(CATEGORY, push_then_steal_is_fifo) { + chase_lev_deque q; + for (size_t i = 0; i < 10; ++i) { + q.push(i); + } + + std::vector stolen; + size_t v = 0; + while (q.steal(v)) { + stolen.push_back(v); + } + ASSERT_EQ(10u, stolen.size()); + for (size_t i = 0; i < 10; ++i) { + EXPECT_EQ(i, stolen[static_cast(i)]); + } + EXPECT_TRUE(q.empty()); +} + +TEST_F(CATEGORY, alternating_push_pop) { + chase_lev_deque q; + size_t v = 0; + for (size_t i = 0; i < 100; ++i) { + q.push(i); + EXPECT_TRUE(q.try_pop(v)); + EXPECT_EQ(i, v); + EXPECT_TRUE(q.empty()); + } +} + +TEST_F(CATEGORY, pop_last_element_via_cas_path) { + // pop() of the final element takes the "race with stealers" branch + // that does a CAS on top_. Repeat to make sure it works. + chase_lev_deque q; + for (size_t i = 0; i < 50; ++i) { + q.push(i * 7); + size_t v = 0; + EXPECT_TRUE(q.try_pop(v)); + EXPECT_EQ(i * 7, v); + EXPECT_FALSE(q.try_pop(v)); + EXPECT_TRUE(q.empty()); + } +} + +TEST_F(CATEGORY, steal_then_pop_until_empty) { + chase_lev_deque q; + for (size_t i = 0; i < 8; ++i) { + q.push(i); + } + size_t v = 0; + // Steal from the head. + EXPECT_TRUE(q.steal(v)); + EXPECT_EQ(0, v); + EXPECT_TRUE(q.steal(v)); + EXPECT_EQ(1, v); + + // Pop the rest from the tail in LIFO order. + auto popped = drain_pop(q); + std::vector expected{7, 6, 5, 4, 3, 2}; + EXPECT_EQ(expected, popped); +} + +// --------------------------------------------------------------------------- +// Buffer growth +// --------------------------------------------------------------------------- + +TEST_F(CATEGORY, push_grows_buffer) { + // Start small so growth triggers fast and many times. + chase_lev_deque q(2); + constexpr size_t N = 10000; + for (size_t i = 0; i < N; ++i) { + q.push(i); + } + EXPECT_EQ(static_cast(N), q.size_approx()); + + // Stealing should still return items in FIFO order. + std::vector stolen; + size_t v = 0; + while (q.steal(v)) { + stolen.push_back(v); + } + ASSERT_EQ(static_cast(N), stolen.size()); + for (size_t i = 0; i < N; ++i) { + EXPECT_EQ(i, stolen[static_cast(i)]); + } +} + +TEST_F(CATEGORY, grow_preserves_partial_window) { + // Push, pop some, then push enough to force a grow. The remaining items + // (which live at non-zero indices in the old buffer) must be copied to + // the correct positions in the new buffer. + chase_lev_deque q(4); // capacity 4 + for (size_t i = 0; i < 4; ++i) { + q.push(i); // contents: [0,1,2,3] + } + size_t v = 0; + EXPECT_TRUE(q.steal(v)); // remove 0 from head -> top=1 + EXPECT_EQ(0, v); + EXPECT_TRUE(q.steal(v)); // remove 1 from head -> top=2 + EXPECT_EQ(1, v); + // Now logical contents are [2,3] but at indices 2,3 in the old buffer. + // Push 3 more -> would exceed capacity 4 and trigger grow. + q.push(4u); + q.push(5u); + q.push(6u); // <-- forces grow + EXPECT_EQ(5u, q.size_approx()); + + // Stealing from head must produce 2,3,4,5,6. + std::vector stolen; + while (q.steal(v)) { + stolen.push_back(v); + } + std::vector expected{2, 3, 4, 5, 6}; + EXPECT_EQ(expected, stolen); +} + +// --------------------------------------------------------------------------- +// post_bulk +// --------------------------------------------------------------------------- + +TEST_F(CATEGORY, post_bulk_zero_is_noop) { + chase_lev_deque q; + std::vector src{1, 2, 3}; + q.post_bulk(src.begin(), 0); + EXPECT_TRUE(q.empty()); + size_t v = 0; + EXPECT_FALSE(q.try_pop(v)); + EXPECT_FALSE(q.steal(v)); +} + +TEST_F(CATEGORY, post_bulk_basic) { + chase_lev_deque q; + std::vector src(20); + std::iota(src.begin(), src.end(), 100); // 100..119 + q.post_bulk(src.begin(), src.size()); + EXPECT_EQ(20u, q.size_approx()); + + // Steal in order. + size_t v = 0; + for (size_t i = 0; i < 20; ++i) { + ASSERT_TRUE(q.steal(v)); + EXPECT_EQ(100 + i, v); + } + EXPECT_TRUE(q.empty()); +} + +TEST_F(CATEGORY, post_bulk_grows_to_exact_fit) { + // Initial capacity 2, post 1000 items in one bulk -> grow must + // double until newCap >= needed. Verify all items are recoverable. + chase_lev_deque q(2); + std::vector src(1000); + std::iota(src.begin(), src.end(), 0); + q.post_bulk(src.begin(), src.size()); + EXPECT_EQ(1000u, q.size_approx()); + + size_t v = 0; + for (size_t i = 0; i < 1000; ++i) { + ASSERT_TRUE(q.steal(v)); + EXPECT_EQ(i, v); + } +} + +TEST_F(CATEGORY, mixed_push_and_post_bulk) { + chase_lev_deque q(4); + q.push(1u); + q.push(2u); + std::vector src{3, 4, 5, 6, 7}; + q.post_bulk(src.begin(), src.size()); + q.push(8u); + + std::vector stolen; + size_t v = 0; + while (q.steal(v)) { + stolen.push_back(v); + } + std::vector expected{1, 2, 3, 4, 5, 6, 7, 8}; + EXPECT_EQ(expected, stolen); +} + +TEST_F(CATEGORY, post_bulk_when_partially_drained_grows_correctly) { + // Drain part of the queue then bulk-push enough to grow. + chase_lev_deque q(4); + for (size_t i = 0; i < 4; ++i) { + q.push(i); + } + size_t v = 0; + EXPECT_TRUE(q.steal(v)); + EXPECT_EQ(0, v); + + // Logical contents [1,2,3] at indices 1,2,3. Bulk-push 20 -> must grow. + std::vector src(20); + std::iota(src.begin(), src.end(), 10); + q.post_bulk(src.begin(), src.size()); + + std::vector stolen; + while (q.steal(v)) { + stolen.push_back(v); + } + std::vector expected{1, 2, 3}; + for (size_t i = 0; i < 20; ++i) { + expected.push_back(10 + i); + } + EXPECT_EQ(expected, stolen); +} + +TEST_F(CATEGORY, push_pop_wraparound) { + chase_lev_deque q(2); + constexpr size_t N = 1 << 10; // 1k push/pop pairs + size_t v = 0; + for (size_t i = 0; i < N; ++i) { + q.push(i); + ASSERT_TRUE(q.try_pop(v)); + ASSERT_EQ(i, v); + } + EXPECT_TRUE(q.empty()); + EXPECT_FALSE(q.try_pop(v)); + EXPECT_FALSE(q.steal(v)); +} + +TEST_F(CATEGORY, push_steal_wraparound) { + chase_lev_deque q(2); + constexpr size_t N = 1 << 10; + size_t v = 0; + for (size_t i = 0; i < N; ++i) { + q.push(i); + ASSERT_TRUE(q.steal(v)); + ASSERT_EQ(i, v); + } + EXPECT_TRUE(q.empty()); + EXPECT_FALSE(q.try_pop(v)); + EXPECT_FALSE(q.steal(v)); +} + +// --------------------------------------------------------------------------- +// size_approx / empty +// --------------------------------------------------------------------------- + +TEST_F(CATEGORY, size_approx_and_empty_track_pushes_and_pops) { + chase_lev_deque q; + EXPECT_TRUE(q.empty()); + EXPECT_EQ(0u, q.size_approx()); + + for (size_t i = 1; i <= 5; ++i) { + q.push(i); + EXPECT_FALSE(q.empty()); + EXPECT_EQ(static_cast(i), q.size_approx()); + } + + size_t v = 0; + for (size_t i = 5; i >= 1; --i) { + EXPECT_EQ(static_cast(i), q.size_approx()); + ASSERT_TRUE(q.try_pop(v)); + } + EXPECT_TRUE(q.empty()); + EXPECT_EQ(0u, q.size_approx()); +} + +// --------------------------------------------------------------------------- +// Multi-threaded: owner pushes/pops, many stealers steal. +// Verify that every produced item is observed exactly once across the +// owner's pops and the stealers' steals. +// --------------------------------------------------------------------------- + +TEST_F(CATEGORY, concurrent_owner_and_stealers) { + constexpr size_t N = 50000; + constexpr size_t NUM_STEALERS = 4; + + chase_lev_deque q(8); + + std::atomic producer_done{false}; + std::vector> stolen(NUM_STEALERS); + std::vector popped; + popped.reserve(N); + + std::vector stealers; + stealers.reserve(NUM_STEALERS); + for (size_t s = 0; s < NUM_STEALERS; ++s) { + stealers.emplace_back([&, s]() { + auto& out = stolen[static_cast(s)]; + out.reserve(N / NUM_STEALERS); + size_t v = 0; + while (true) { + if (q.steal(v)) { + out.push_back(v); + } else { + if (producer_done.load(std::memory_order_acquire) && q.empty()) { + // Drain any remainder racing with the producer's completion. + while (q.steal(v)) { + out.push_back(v); + } + return; + } + std::this_thread::yield(); + } + } + }); + } + + // Owner thread: push N items, occasionally popping a few back. + std::thread owner([&]() { + size_t v = 0; + for (size_t i = 0; i < N; ++i) { + q.push(i); + // Every 17 pushes, try to pop one back so we exercise pop too. + if ((i % 17) == 0) { + if (q.try_pop(v)) { + popped.push_back(v); + } + } + } + // Drain owner's tail. + while (q.try_pop(v)) { + popped.push_back(v); + } + producer_done.store(true, std::memory_order_release); + }); + + owner.join(); + for (auto& t : stealers) { + t.join(); + } + + // Now every value in [0, N) must appear exactly once across popped + // and all stolen vectors. + size_t total = popped.size(); + for (auto& s : stolen) { + total += s.size(); + } + EXPECT_EQ(static_cast(N), total); + + std::vector seen(N, false); + auto mark = [&](size_t v) { + ASSERT_GE(v, 0); + ASSERT_LT(v, N); + ASSERT_FALSE(seen[static_cast(v)]) + << "value " << v << " duplicated"; + seen[static_cast(v)] = true; + }; + for (size_t v : popped) { + mark(v); + } + for (auto& s : stolen) { + for (size_t v : s) { + mark(v); + } + } + for (size_t i = 0; i < N; ++i) { + EXPECT_TRUE(seen[static_cast(i)]) << "value " << i << " missing"; + } +} + +// Bulk-push concurrent with multiple stealers. +TEST_F(CATEGORY, concurrent_post_bulk_with_stealers) { + constexpr size_t N = 50000; + constexpr size_t CHUNK = 64; + constexpr size_t NUM_STEALERS = 4; + + chase_lev_deque q(8); + std::atomic producer_done{false}; + std::vector> stolen(NUM_STEALERS); + std::vector owner_popped; + + std::vector stealers; + stealers.reserve(NUM_STEALERS); + for (size_t s = 0; s < NUM_STEALERS; ++s) { + stealers.emplace_back([&, s]() { + auto& out = stolen[static_cast(s)]; + size_t v = 0; + while (true) { + if (q.steal(v)) { + out.push_back(v); + } else { + if (producer_done.load(std::memory_order_acquire) && q.empty()) { + while (q.steal(v)) { + out.push_back(v); + } + return; + } + std::this_thread::yield(); + } + } + }); + } + + // Owner thread: bulk-post in chunks until N items have been posted. + std::thread owner([&]() { + std::vector buf(CHUNK); + size_t next = 0; + while (next < N) { + size_t sz = std::min(CHUNK, N - next); + for (size_t i = 0; i < sz; ++i) { + buf[static_cast(i)] = next + i; + } + q.post_bulk(buf.begin(), static_cast(sz)); + next += sz; + } + // Owner drains anything still in the tail. + size_t v = 0; + while (q.try_pop(v)) { + owner_popped.push_back(v); + } + producer_done.store(true, std::memory_order_release); + }); + + owner.join(); + for (auto& t : stealers) { + t.join(); + } + + std::set seen; + for (size_t v : owner_popped) { + ASSERT_TRUE(seen.insert(v).second) << "duplicate value " << v; + } + for (auto& s : stolen) { + for (size_t v : s) { + ASSERT_TRUE(seen.insert(v).second) << "duplicate value " << v; + } + } + EXPECT_EQ(static_cast(N), seen.size()); + for (size_t i = 0; i < N; ++i) { + EXPECT_TRUE(seen.count(i) == 1) << "missing value " << i; + } +} + +#undef CATEGORY diff --git a/tests/test_qu_lockfree.cpp b/tests/test_qu_lockfree.cpp deleted file mode 100644 index d532901..0000000 --- a/tests/test_qu_lockfree.cpp +++ /dev/null @@ -1,104 +0,0 @@ -// Various tests to increase code coverage of qu_lockfree in ways that it is -// not normally used by the library. - -#include "test_common.hpp" -#include "tmc/detail/qu_lockfree.hpp" -#include "tmc/sync.hpp" - -#include - -#include - -#define CATEGORY test_qu_lockfree - -class CATEGORY : public testing::Test { -protected: - static void SetUpTestSuite() { - tmc::cpu_executor().set_thread_count(4).init(); - } - - static void TearDownTestSuite() { tmc::cpu_executor().teardown(); } - - static tmc::ex_cpu& ex() { return tmc::cpu_executor(); } -}; - -TEST_F(CATEGORY, expand_implicit_producer_index) { - auto t1 = tmc::post_bulk_waitable( - ex(), tmc::iter_adapter(0, [](int) -> tmc::task { co_return; }), 8000 - ); - - auto t2 = tmc::post_bulk_waitable( - ex(), tmc::iter_adapter(0, [](int) -> tmc::task { co_return; }), 32000 - ); - t1.wait(); - t2.wait(); -} - -TEST_F(CATEGORY, expand_explicit_producer_index) { - test_async_main(ex(), []() -> tmc::task { - auto t1 = - tmc::spawn_many( - tmc::iter_adapter(0, [](int) -> tmc::task { co_return; }), 8000 - ) - .fork(); - co_await tmc::spawn_many( - tmc::iter_adapter(0, [](int) -> tmc::task { co_return; }), 32000 - ); - co_await std::move(t1); - }()); -} - -TEST_F(CATEGORY, destroy_implicit_non_empty) { - std::atomic destroyCount; - { - tmc::queue::ConcurrentQueue q; - q.enqueue(destructor_counter(&destroyCount)); - } - EXPECT_EQ(destroyCount, 1); -} - -TEST_F(CATEGORY, destroy_implicit_non_empty_multi_block) { - using queue_t = tmc::queue::ConcurrentQueue; - auto Count = queue_t::ConcurrentQueue::PRODUCER_BLOCK_SIZE; - std::atomic destroyCount; - { - tmc::queue::ConcurrentQueue q; - for (size_t i = 0; i < Count + 1; ++i) { - q.enqueue(destructor_counter(&destroyCount)); - } - } - EXPECT_EQ(destroyCount, Count + 1); -} - -TEST_F(CATEGORY, destroy_explicit_non_empty) { - using queue_t = tmc::queue::ConcurrentQueue; - std::atomic destroyCount; - { - // mimic the way that explicit producers are used by ex_cpu - queue_t q; - q.staticProducers = new queue_t::ExplicitProducer[1]; - q.staticProducers[0].init(&q); - q.staticProducers[0].enqueue(destructor_counter(&destroyCount)); - delete[] q.staticProducers; - } - EXPECT_EQ(destroyCount, 1); -} - -TEST_F(CATEGORY, destroy_explicit_non_empty_multi_block) { - using queue_t = tmc::queue::ConcurrentQueue; - auto Count = queue_t::ConcurrentQueue::PRODUCER_BLOCK_SIZE; - std::atomic destroyCount; - { - // mimic the way that explicit producers are used by ex_cpu - queue_t q; - q.staticProducers = new queue_t::ExplicitProducer[1]; - q.staticProducers[0].init(&q); - for (size_t i = 0; i < Count + 1; ++i) { - q.staticProducers[0].enqueue(destructor_counter(&destroyCount)); - } - delete[] q.staticProducers; - } - EXPECT_EQ(destroyCount, Count + 1); -} - -#undef CATEGORY diff --git a/tests/test_qu_mc.cpp b/tests/test_qu_mc.cpp new file mode 100644 index 0000000..361828f --- /dev/null +++ b/tests/test_qu_mc.cpp @@ -0,0 +1,59 @@ +// Various tests to increase code coverage of qu_mc in ways that it is +// not normally used by the library. + +#include "test_common.hpp" +#include "tmc/detail/qu_mc.hpp" +#include "tmc/sync.hpp" + +#include + +#include + +#define CATEGORY test_qu_mc + +class CATEGORY : public testing::Test { +protected: + static void SetUpTestSuite() { + tmc::cpu_executor().set_thread_count(4).init(); + } + + static void TearDownTestSuite() { tmc::cpu_executor().teardown(); } + + static tmc::ex_cpu& ex() { return tmc::cpu_executor(); } +}; + +TEST_F(CATEGORY, expand_implicit_producer_index) { + auto t1 = tmc::post_bulk_waitable( + ex(), tmc::iter_adapter(0, [](int) -> tmc::task { co_return; }), 8000 + ); + + auto t2 = tmc::post_bulk_waitable( + ex(), tmc::iter_adapter(0, [](int) -> tmc::task { co_return; }), 32000 + ); + t1.wait(); + t2.wait(); +} + +TEST_F(CATEGORY, destroy_implicit_non_empty) { + std::atomic destroyCount; + { + tmc::queue::ConcurrentQueue q; + q.enqueue(destructor_counter(&destroyCount)); + } + EXPECT_EQ(destroyCount, 1); +} + +TEST_F(CATEGORY, destroy_implicit_non_empty_multi_block) { + using queue_t = tmc::queue::ConcurrentQueue; + auto Count = queue_t::ConcurrentQueue::PRODUCER_BLOCK_SIZE; + std::atomic destroyCount; + { + tmc::queue::ConcurrentQueue q; + for (size_t i = 0; i < Count + 1; ++i) { + q.enqueue(destructor_counter(&destroyCount)); + } + } + EXPECT_EQ(destroyCount, Count + 1); +} + +#undef CATEGORY