| // Copyright (c) 2026 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "quiche/common/stable_block_list.h" |
| |
| #include <memory> |
| #include <vector> |
| |
| #include "quiche/common/platform/api/quiche_test.h" |
| |
| namespace quiche { |
| namespace { |
| |
| struct AllocStats { |
| size_t allocs = 0; |
| size_t deallocs = 0; |
| }; |
| |
| template <typename T> |
| struct TrackingAllocator { |
| using value_type = T; |
| |
| std::shared_ptr<AllocStats> stats; |
| |
| TrackingAllocator() = default; |
| explicit TrackingAllocator(std::shared_ptr<AllocStats> s) |
| : stats(std::move(s)) {} |
| |
| template <typename U> |
| TrackingAllocator(const TrackingAllocator<U>& other) : stats(other.stats) {} |
| |
| T* allocate(std::size_t n) { |
| if (stats) stats->allocs++; |
| return std::allocator<T>().allocate(n); |
| } |
| |
| void deallocate(T* p, std::size_t n) { |
| if (stats) stats->deallocs++; |
| std::allocator<T>().deallocate(p, n); |
| } |
| |
| template <typename U> |
| bool operator==(const TrackingAllocator<U>& other) const { |
| return stats == other.stats; |
| } |
| template <typename U> |
| bool operator!=(const TrackingAllocator<U>& other) const { |
| return stats != other.stats; |
| } |
| }; |
| |
| TEST(StableBlockListTest, BasicOps) { |
| StableBlockList<int> list; |
| EXPECT_TRUE(list.empty()); |
| EXPECT_EQ(list.size(), 0); |
| |
| list.push_back(1); |
| list.push_back(2); |
| list.push_back(3); |
| |
| EXPECT_FALSE(list.empty()); |
| EXPECT_EQ(list.size(), 3); |
| EXPECT_EQ(list.front(), 1); |
| EXPECT_EQ(list.back(), 3); |
| |
| StableBlockList<int> list2 = std::move(list); |
| EXPECT_FALSE(list2.empty()); |
| EXPECT_EQ(list2.size(), 3); |
| EXPECT_EQ(list2.front(), 1); |
| EXPECT_EQ(list2.back(), 3); |
| |
| list2.clear(); |
| EXPECT_TRUE(list2.empty()); |
| EXPECT_EQ(list2.size(), 0); |
| } |
| |
| TEST(StableBlockListTest, Traversal) { |
| StableBlockList<int> list; |
| list.push_back(1); |
| list.push_back(2); |
| list.push_back(3); |
| |
| auto it = list.begin(); |
| EXPECT_EQ(*it, 1); |
| ++it; |
| EXPECT_EQ(*it, 2); |
| ++it; |
| EXPECT_EQ(*it, 3); |
| ++it; |
| EXPECT_TRUE(it == list.end()); |
| |
| --it; |
| EXPECT_EQ(*it, 3); |
| --it; |
| EXPECT_EQ(*it, 2); |
| --it; |
| EXPECT_EQ(*it, 1); |
| EXPECT_TRUE(it == list.begin()); |
| } |
| |
| TEST(StableBlockListTest, IteratorStabilityOnPushBack) { |
| StableBlockList<int, 4> |
| list; // Small capacity to force multi-block allocations |
| // Fill one chunk (4 elements) |
| for (int i = 0; i < 4; ++i) { |
| list.push_back(i * 10); // 0, 10, 20, 30 |
| } |
| |
| // Store iterators |
| std::vector<StableBlockList<int, 4>::iterator> iters; |
| for (auto it = list.begin(); it != list.end(); ++it) { |
| iters.push_back(it); |
| } |
| |
| // Push back more elements to force allocation of a new block |
| list.push_back(40); |
| list.push_back(50); |
| |
| EXPECT_EQ(list.size(), 6); |
| |
| // Verify stored iterators are still valid and point to the same values |
| EXPECT_EQ(*iters[0], 0); |
| EXPECT_EQ(*iters[1], 10); |
| EXPECT_EQ(*iters[2], 20); |
| EXPECT_EQ(*iters[3], 30); |
| |
| // Verify traversal order |
| std::vector<int> elements; |
| for (int x : list) { |
| elements.push_back(x); |
| } |
| EXPECT_THAT(elements, ::testing::ElementsAre(0, 10, 20, 30, 40, 50)); |
| } |
| |
| TEST(StableBlockListTest, IteratorStabilityOnErase) { |
| StableBlockList<int, 4> list; |
| for (int i = 0; i < 8; ++i) { |
| list.push_back(i * 10); // 0, 10, 20, 30, 40, 50, 60, 70 |
| } |
| |
| std::vector<StableBlockList<int, 4>::iterator> iters; |
| for (auto it = list.begin(); it != list.end(); ++it) { |
| iters.push_back(it); |
| } |
| |
| // Erase 30 (index 3, which is the last element of first block) |
| auto erase_pos = iters[3]; |
| auto next_it = list.erase(erase_pos); |
| EXPECT_EQ(*next_it, 40); |
| EXPECT_EQ(list.size(), 7); |
| |
| // Verify other iterators are still valid |
| EXPECT_EQ(*iters[0], 0); |
| EXPECT_EQ(*iters[1], 10); |
| EXPECT_EQ(*iters[2], 20); |
| // iters[3] is invalid (erased), we shouldn't dereference it. |
| EXPECT_EQ(*iters[4], 40); |
| EXPECT_EQ(*iters[5], 50); |
| EXPECT_EQ(*iters[6], 60); |
| EXPECT_EQ(*iters[7], 70); |
| |
| std::vector<int> elements; |
| for (int x : list) { |
| elements.push_back(x); |
| } |
| EXPECT_THAT(elements, ::testing::ElementsAre(0, 10, 20, 40, 50, 60, 70)); |
| } |
| |
| TEST(StableBlockListTest, MoveOnlyType) { |
| StableBlockList<std::unique_ptr<int>> list; |
| list.push_back(std::make_unique<int>(1)); |
| list.push_back(std::make_unique<int>(2)); |
| |
| EXPECT_EQ(list.size(), 2); |
| EXPECT_EQ(*list.front(), 1); |
| EXPECT_EQ(*list.back(), 2); |
| |
| list.push_back(std::make_unique<int>(3)); // [1, 2, 3] |
| |
| EXPECT_EQ(list.size(), 3); |
| auto it = list.begin(); |
| EXPECT_EQ(**it, 1); |
| ++it; |
| EXPECT_EQ(**it, 2); |
| ++it; |
| EXPECT_EQ(**it, 3); |
| |
| it = list.begin(); |
| ++it; // points to 2 |
| list.erase(it); // [1, 3] |
| EXPECT_EQ(list.size(), 2); |
| EXPECT_EQ(*list.front(), 1); |
| EXPECT_EQ(*list.back(), 3); |
| } |
| |
| TEST(StableBlockListTest, MultiBlockPushBackAndBackwardTraversal) { |
| StableBlockList<int, 8> list; |
| const int num_elements = 100; |
| for (int i = 0; i < num_elements; ++i) { |
| list.push_back(i); |
| } |
| EXPECT_EQ(list.size(), num_elements); |
| EXPECT_EQ(list.front(), 0); |
| EXPECT_EQ(list.back(), num_elements - 1); |
| |
| // Forward traversal across multiple blocks |
| int expected = 0; |
| for (int val : list) { |
| EXPECT_EQ(val, expected++); |
| } |
| EXPECT_EQ(expected, num_elements); |
| |
| // Backward traversal across multiple blocks |
| auto it = list.end(); |
| for (int i = num_elements - 1; i >= 0; --i) { |
| --it; |
| EXPECT_EQ(*it, i); |
| } |
| EXPECT_TRUE(it == list.begin()); |
| } |
| |
| TEST(StableBlockListTest, SingleBlockReverseIteration) { |
| StableBlockList<int, 4> list; |
| list.push_back(1); |
| list.push_back(2); |
| |
| auto it = list.rbegin(); |
| EXPECT_EQ(*it, 2); |
| ++it; |
| EXPECT_EQ(*it, 1); |
| ++it; |
| EXPECT_TRUE(it == list.rend()); |
| } |
| |
| TEST(StableBlockListTest, SingleElementReverseIteration) { |
| StableBlockList<int, 4> list; |
| list.push_back(1); |
| |
| auto it = list.rbegin(); |
| EXPECT_EQ(*it, 1); |
| ++it; |
| EXPECT_TRUE(it == list.rend()); |
| } |
| |
| TEST(StableBlockListTest, EmptyReverseIteration) { |
| StableBlockList<int, 4> list; |
| |
| auto it = list.rbegin(); |
| EXPECT_TRUE(it == list.rend()); |
| } |
| |
| TEST(StableBlockListTest, EraseLastElementOfMultiBlockList) { |
| StableBlockList<int, 4> list; |
| // Push 5 elements. Block capacity is 4. |
| // Block 0 will have 4 elements (0..3). |
| // Block 1 will have 1 element (4). |
| for (int i = 0; i < 5; ++i) { |
| list.push_back(i); |
| } |
| // Now erase the last element (4). |
| auto it = std::prev(list.end()); |
| EXPECT_EQ(*it, 4); |
| list.erase(it); |
| |
| // Check size |
| EXPECT_EQ(list.size(), 4); |
| // Check back |
| EXPECT_EQ(list.back(), 3); |
| |
| // Verify we can still traverse |
| int expected = 0; |
| for (int val : list) { |
| EXPECT_EQ(val, expected++); |
| } |
| EXPECT_EQ(expected, 4); |
| } |
| |
| TEST(StableBlockListTest, BlockDeallocationAndReallocation) { |
| StableBlockList<int, 2> list; |
| list.push_back(1); |
| list.push_back(2); |
| list.push_back(3); // Allocates 2nd block |
| |
| EXPECT_EQ(list.size(), 3); |
| |
| auto it = list.begin(); // points to 1 |
| list.erase(it); // 1 is erased |
| it = list.begin(); // points to 2 |
| list.erase(it); // 2 is erased, 1st block becomes empty and is deallocated! |
| |
| EXPECT_EQ(list.size(), 1); |
| EXPECT_EQ(list.front(), 3); |
| |
| // Now push back another element, it should go to the 2nd block if it has |
| // space, or allocate a new block. Since 2nd block has '3' at index 0, it has |
| // space at index 1. |
| list.push_back(4); |
| EXPECT_EQ(list.size(), 2); |
| EXPECT_EQ(list.back(), 4); |
| |
| // Now 2nd block is full (3, 4). Push another to allocate a new block. |
| list.push_back(5); |
| EXPECT_EQ(list.size(), 3); |
| EXPECT_EQ(list.back(), 5); |
| |
| std::vector<int> elements; |
| for (int x : list) { |
| elements.push_back(x); |
| } |
| EXPECT_THAT(elements, ::testing::ElementsAre(3, 4, 5)); |
| } |
| |
| TEST(StableBlockListTest, IteratorBoundaryCases) { |
| StableBlockList<int> empty_list; |
| |
| // 1. Decrementing end() on empty list should return end() (covers line 505) |
| auto empty_it = empty_list.end(); |
| --empty_it; |
| EXPECT_TRUE(empty_it == empty_list.end()); |
| |
| // 2. Decrementing a default-constructed iterator should return itself (covers |
| // line 505) |
| StableBlockList<int>::iterator default_it; |
| --default_it; |
| EXPECT_TRUE(default_it == StableBlockList<int>::iterator()); |
| |
| // 3. Decrementing begin() on non-empty list should return end() (covers line |
| // 530) |
| StableBlockList<int> list; |
| list.push_back(10); |
| auto it = list.begin(); |
| --it; |
| EXPECT_TRUE(it == list.end()); |
| } |
| |
| TEST(StableBlockListTest, ShrinkToFitReleasesMemory) { |
| auto stats = std::make_shared<AllocStats>(); |
| { |
| StableBlockList<int, 2, TrackingAllocator<int>> list( |
| (TrackingAllocator<int>(stats))); |
| |
| // Push 2 elements. Capacity is 2, so this allocates 1 block. |
| list.push_back(1); |
| list.push_back(2); |
| EXPECT_EQ(stats->allocs, 1); |
| EXPECT_EQ(stats->deallocs, 0); |
| |
| // Push 3rd element. Allocates 2nd block. |
| list.push_back(3); |
| EXPECT_EQ(stats->allocs, 2); |
| EXPECT_EQ(stats->deallocs, 0); |
| |
| // Erase 1st and 2nd elements. 1st block becomes empty and is unlinked. |
| // It should be moved to the free list, NOT deallocated. |
| auto it = list.begin(); |
| list.erase(it); |
| it = list.begin(); |
| list.erase(it); |
| |
| EXPECT_EQ(stats->allocs, 2); |
| EXPECT_EQ(stats->deallocs, 0); |
| |
| // Call shrink_to_fit(). This should deallocate the block in the free list. |
| list.shrink_to_fit(); |
| EXPECT_EQ(stats->allocs, 2); |
| EXPECT_EQ(stats->deallocs, 1); |
| |
| // Push 4th element. Goes to 2nd block (which contains 3 and has space). |
| // No new allocation. |
| list.push_back(4); |
| EXPECT_EQ(stats->allocs, 2); |
| EXPECT_EQ(stats->deallocs, 1); |
| |
| // Push 5th element. 2nd block is full [3, 4], allocates 3rd block. |
| list.push_back(5); |
| EXPECT_EQ(stats->allocs, 3); |
| EXPECT_EQ(stats->deallocs, 1); |
| } |
| // Destructor deallocates the remaining active blocks (Block 2 and Block 3). |
| EXPECT_EQ(stats->allocs, 3); |
| EXPECT_EQ(stats->deallocs, 3); |
| } |
| |
| } // namespace |
| } // namespace quiche |