| // 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. |
| |
| // Differential property-based fuzz test comparing quiche::StableBlockList |
| // against std::list. |
| // |
| // The test executes randomized sequences of container operations (push_back, |
| // emplace_back, insert at end, single-element erase, range erase including |
| // empty ranges, pop_front, pop_back, clear, shrink_to_fit, move, and swap) |
| // across various block capacities (1, 2, 4, 16, 32, 64). |
| // |
| // After every operation, it rigorously verifies container invariants: |
| // - Element order, empty(), and size() parity with std::list. |
| // - Forward and reverse iterator traversal parity. |
| // - Bidirectional iterator step navigation across erased holes and block |
| // boundaries. |
| // - Pointer and reference stability: surviving element addresses remain |
| // stable across arbitrary insertions, deletions, block deallocations, and |
| // memory shrink operations. |
| // - Iterator stability: saved iterators to surviving elements remain valid, |
| // dereference to the correct values, and step correctly to surviving |
| // neighbors. |
| |
| #include <cstddef> |
| #include <iterator> |
| #include <list> |
| #include <utility> |
| #include <variant> |
| #include <vector> |
| |
| #include "absl/functional/overload.h" |
| #include "quiche/common/platform/api/quiche_fuzztest.h" |
| #include "quiche/common/platform/api/quiche_test.h" |
| #include "quiche/common/stable_block_list.h" |
| |
| namespace quiche { |
| namespace { |
| |
| using ::fuzztest::Arbitrary; |
| using ::fuzztest::Domain; |
| using ::fuzztest::VariantOf; |
| using ::fuzztest::VectorOf; |
| |
| struct PushBackOp { |
| int value; |
| }; |
| struct EmplaceBackOp { |
| int value; |
| }; |
| struct InsertEndOp { |
| int value; |
| }; |
| struct EraseIndexOp { |
| size_t index; |
| }; |
| struct EraseRangeOp { |
| size_t start_index; |
| size_t count; |
| }; |
| struct PopFrontOp {}; |
| struct PopBackOp {}; |
| struct ClearOp {}; |
| struct ShrinkToFitOp {}; |
| struct MoveOp {}; |
| struct SwapOp {}; |
| |
| using Op = std::variant<PushBackOp, EmplaceBackOp, InsertEndOp, EraseIndexOp, |
| EraseRangeOp, PopFrontOp, PopBackOp, ClearOp, |
| ShrinkToFitOp, MoveOp, SwapOp>; |
| |
| Domain<Op> AnyOpDomain() { |
| return VariantOf(Arbitrary<PushBackOp>(), Arbitrary<EmplaceBackOp>(), |
| Arbitrary<InsertEndOp>(), Arbitrary<EraseIndexOp>(), |
| Arbitrary<EraseRangeOp>(), Arbitrary<PopFrontOp>(), |
| Arbitrary<PopBackOp>(), Arbitrary<ClearOp>(), |
| Arbitrary<ShrinkToFitOp>(), Arbitrary<MoveOp>(), |
| Arbitrary<SwapOp>()); |
| } |
| |
| template <size_t BlockCapacity> |
| struct ElementTracker { |
| int value; |
| const int* sbl_ptr; |
| typename StableBlockList<int, BlockCapacity>::iterator sbl_it; |
| const int* std_ptr; |
| std::list<int>::iterator std_it; |
| }; |
| |
| template <size_t BlockCapacity> |
| void VerifyInvariants( |
| const StableBlockList<int, BlockCapacity>& sbl, |
| const std::list<int>& std_list, |
| const std::vector<ElementTracker<BlockCapacity>>& active_trackers) { |
| // 1. Size & Empty parity |
| ASSERT_EQ(sbl.size(), std_list.size()); |
| ASSERT_EQ(sbl.size(), active_trackers.size()); |
| ASSERT_EQ(sbl.empty(), std_list.empty()); |
| ASSERT_EQ(sbl.empty(), active_trackers.empty()); |
| |
| if (sbl.empty()) { |
| EXPECT_TRUE(sbl.begin() == sbl.end()); |
| EXPECT_TRUE(sbl.cbegin() == sbl.cend()); |
| EXPECT_TRUE(sbl.rbegin() == sbl.rend()); |
| EXPECT_TRUE(sbl.crbegin() == sbl.crend()); |
| return; |
| } |
| |
| // 2. Front & Back parity |
| EXPECT_EQ(sbl.front(), std_list.front()); |
| EXPECT_EQ(sbl.front(), active_trackers.front().value); |
| EXPECT_EQ(sbl.back(), std_list.back()); |
| EXPECT_EQ(sbl.back(), active_trackers.back().value); |
| |
| // 3. Forward iterator traversal and element order |
| auto sbl_it = sbl.begin(); |
| auto std_it = std_list.begin(); |
| size_t idx = 0; |
| while (sbl_it != sbl.end() && std_it != std_list.end()) { |
| EXPECT_EQ(*sbl_it, *std_it); |
| EXPECT_EQ(*sbl_it, active_trackers[idx].value); |
| EXPECT_EQ(&*sbl_it, active_trackers[idx].sbl_ptr); |
| EXPECT_EQ(&*std_it, active_trackers[idx].std_ptr); |
| EXPECT_TRUE(sbl_it == active_trackers[idx].sbl_it); |
| EXPECT_TRUE(std_it == active_trackers[idx].std_it); |
| ++sbl_it; |
| ++std_it; |
| ++idx; |
| } |
| EXPECT_TRUE(sbl_it == sbl.end()); |
| EXPECT_TRUE(std_it == std_list.end()); |
| EXPECT_EQ(idx, active_trackers.size()); |
| |
| // 4. Const forward iterator traversal |
| auto sbl_cit = sbl.cbegin(); |
| auto std_cit = std_list.cbegin(); |
| size_t cidx = 0; |
| while (sbl_cit != sbl.cend() && std_cit != std_list.cend()) { |
| EXPECT_EQ(*sbl_cit, *std_cit); |
| EXPECT_EQ(*sbl_cit, active_trackers[cidx].value); |
| EXPECT_EQ(&*sbl_cit, active_trackers[cidx].sbl_ptr); |
| EXPECT_EQ(&*std_cit, active_trackers[cidx].std_ptr); |
| ++sbl_cit; |
| ++std_cit; |
| ++cidx; |
| } |
| EXPECT_TRUE(sbl_cit == sbl.cend()); |
| EXPECT_TRUE(std_cit == std_list.cend()); |
| EXPECT_EQ(cidx, active_trackers.size()); |
| |
| // 5. Reverse iterator traversal |
| auto sbl_rit = sbl.rbegin(); |
| auto std_rit = std_list.rbegin(); |
| size_t ridx = active_trackers.size(); |
| while (sbl_rit != sbl.rend() && std_rit != std_list.rend()) { |
| --ridx; |
| EXPECT_EQ(*sbl_rit, *std_rit); |
| EXPECT_EQ(*sbl_rit, active_trackers[ridx].value); |
| EXPECT_EQ(&*sbl_rit, active_trackers[ridx].sbl_ptr); |
| EXPECT_EQ(&*std_rit, active_trackers[ridx].std_ptr); |
| ++sbl_rit; |
| ++std_rit; |
| } |
| EXPECT_TRUE(sbl_rit == sbl.rend()); |
| EXPECT_TRUE(std_rit == std_list.rend()); |
| EXPECT_EQ(ridx, 0); |
| |
| // 6. Const reverse iterator traversal |
| auto sbl_crit = sbl.crbegin(); |
| auto std_crit = std_list.crbegin(); |
| size_t cridx = active_trackers.size(); |
| while (sbl_crit != sbl.crend() && std_crit != std_list.crend()) { |
| --cridx; |
| EXPECT_EQ(*sbl_crit, *std_crit); |
| EXPECT_EQ(*sbl_crit, active_trackers[cridx].value); |
| EXPECT_EQ(&*sbl_crit, active_trackers[cridx].sbl_ptr); |
| EXPECT_EQ(&*std_crit, active_trackers[cridx].std_ptr); |
| ++sbl_crit; |
| ++std_crit; |
| } |
| EXPECT_TRUE(sbl_crit == sbl.crend()); |
| EXPECT_TRUE(std_crit == std_list.crend()); |
| EXPECT_EQ(cridx, 0); |
| |
| // 7. Bidirectional stepping (decrementing from end()) |
| { |
| auto b_sbl_it = sbl.end(); |
| auto b_std_it = std_list.end(); |
| size_t b_idx = active_trackers.size(); |
| do { |
| --b_sbl_it; |
| --b_std_it; |
| --b_idx; |
| EXPECT_EQ(*b_sbl_it, *b_std_it); |
| EXPECT_EQ(*b_sbl_it, active_trackers[b_idx].value); |
| EXPECT_EQ(&*b_sbl_it, active_trackers[b_idx].sbl_ptr); |
| EXPECT_EQ(&*b_std_it, active_trackers[b_idx].std_ptr); |
| } while (b_sbl_it != sbl.begin() && b_std_it != std_list.begin()); |
| EXPECT_TRUE(b_sbl_it == sbl.begin()); |
| EXPECT_TRUE(b_std_it == std_list.begin()); |
| EXPECT_EQ(b_idx, 0); |
| } |
| |
| // 8. Individual pointer & iterator stability and neighbor connectivity |
| for (size_t i = 0; i < active_trackers.size(); ++i) { |
| const auto& tracker = active_trackers[i]; |
| EXPECT_EQ(*tracker.sbl_ptr, tracker.value); |
| EXPECT_EQ(*tracker.std_ptr, tracker.value); |
| EXPECT_EQ(*tracker.sbl_it, tracker.value); |
| EXPECT_EQ(*tracker.std_it, tracker.value); |
| EXPECT_EQ(&*tracker.sbl_it, tracker.sbl_ptr); |
| EXPECT_EQ(&*tracker.std_it, tracker.std_ptr); |
| |
| if (i + 1 < active_trackers.size()) { |
| EXPECT_TRUE(std::next(tracker.sbl_it) == active_trackers[i + 1].sbl_it); |
| EXPECT_TRUE(std::next(tracker.std_it) == active_trackers[i + 1].std_it); |
| } else { |
| EXPECT_TRUE(std::next(tracker.sbl_it) == sbl.end()); |
| EXPECT_TRUE(std::next(tracker.std_it) == std_list.end()); |
| } |
| |
| if (i > 0) { |
| EXPECT_TRUE(std::prev(tracker.sbl_it) == active_trackers[i - 1].sbl_it); |
| EXPECT_TRUE(std::prev(tracker.std_it) == active_trackers[i - 1].std_it); |
| } else { |
| EXPECT_TRUE(tracker.sbl_it == sbl.begin()); |
| EXPECT_TRUE(tracker.std_it == std_list.begin()); |
| } |
| } |
| } |
| |
| template <size_t BlockCapacity> |
| void StableBlockListMatchesStdList(const std::vector<Op>& ops) { |
| StableBlockList<int, BlockCapacity> sbl; |
| std::list<int> std_list; |
| std::vector<ElementTracker<BlockCapacity>> active_trackers; |
| |
| StableBlockList<int, BlockCapacity> secondary_sbl; |
| std::list<int> secondary_std; |
| std::vector<ElementTracker<BlockCapacity>> secondary_trackers; |
| |
| VerifyInvariants(sbl, std_list, active_trackers); |
| |
| for (const Op& op : ops) { |
| std::visit( |
| absl::Overload{ |
| [&](const PushBackOp& op) { |
| sbl.push_back(op.value); |
| std_list.push_back(op.value); |
| auto sbl_it = std::prev(sbl.end()); |
| auto std_it = std::prev(std_list.end()); |
| active_trackers.push_back( |
| {op.value, &*sbl_it, sbl_it, &*std_it, std_it}); |
| }, |
| [&](const EmplaceBackOp& op) { |
| auto sbl_it = sbl.emplace(sbl.end(), op.value); |
| auto std_it = std_list.emplace(std_list.end(), op.value); |
| active_trackers.push_back( |
| {op.value, &*sbl_it, sbl_it, &*std_it, std_it}); |
| }, |
| [&](const InsertEndOp& op) { |
| auto sbl_it = sbl.insert(sbl.end(), op.value); |
| auto std_it = std_list.insert(std_list.end(), op.value); |
| active_trackers.push_back( |
| {op.value, &*sbl_it, sbl_it, &*std_it, std_it}); |
| }, |
| [&](const EraseIndexOp& op) { |
| if (active_trackers.empty()) { |
| return; |
| } |
| size_t target_idx = op.index % active_trackers.size(); |
| auto sbl_ret = sbl.erase(active_trackers[target_idx].sbl_it); |
| auto std_ret = std_list.erase(active_trackers[target_idx].std_it); |
| if (target_idx + 1 < active_trackers.size()) { |
| EXPECT_EQ(*sbl_ret, *std_ret); |
| EXPECT_EQ(*sbl_ret, active_trackers[target_idx + 1].value); |
| } else { |
| EXPECT_TRUE(sbl_ret == sbl.end()); |
| EXPECT_TRUE(std_ret == std_list.end()); |
| } |
| active_trackers.erase(active_trackers.begin() + target_idx); |
| }, |
| [&](const EraseRangeOp& op) { |
| if (active_trackers.empty()) { |
| auto sbl_ret = sbl.erase(sbl.begin(), sbl.end()); |
| auto std_ret = std_list.erase(std_list.begin(), std_list.end()); |
| EXPECT_TRUE(sbl_ret == sbl.end()); |
| EXPECT_TRUE(std_ret == std_list.end()); |
| return; |
| } |
| size_t start = op.start_index % (active_trackers.size() + 1); |
| size_t max_count = active_trackers.size() - start; |
| size_t num_to_erase = op.count % (max_count + 1); |
| |
| auto sbl_first = (start == active_trackers.size()) |
| ? sbl.end() |
| : active_trackers[start].sbl_it; |
| auto std_first = (start == active_trackers.size()) |
| ? std_list.end() |
| : active_trackers[start].std_it; |
| auto sbl_last = |
| (start + num_to_erase == active_trackers.size()) |
| ? sbl.end() |
| : active_trackers[start + num_to_erase].sbl_it; |
| auto std_last = |
| (start + num_to_erase == active_trackers.size()) |
| ? std_list.end() |
| : active_trackers[start + num_to_erase].std_it; |
| auto sbl_ret = sbl.erase(sbl_first, sbl_last); |
| auto std_ret = std_list.erase(std_first, std_last); |
| if (start + num_to_erase < active_trackers.size()) { |
| EXPECT_EQ(*sbl_ret, *std_ret); |
| EXPECT_EQ(*sbl_ret, |
| active_trackers[start + num_to_erase].value); |
| } else { |
| EXPECT_TRUE(sbl_ret == sbl.end()); |
| EXPECT_TRUE(std_ret == std_list.end()); |
| } |
| active_trackers.erase( |
| active_trackers.begin() + start, |
| active_trackers.begin() + start + num_to_erase); |
| }, |
| [&](const PopFrontOp&) { |
| if (active_trackers.empty()) { |
| return; |
| } |
| sbl.erase(sbl.begin()); |
| std_list.pop_front(); |
| active_trackers.erase(active_trackers.begin()); |
| }, |
| [&](const PopBackOp&) { |
| if (active_trackers.empty()) { |
| return; |
| } |
| sbl.erase(std::prev(sbl.end())); |
| std_list.pop_back(); |
| active_trackers.pop_back(); |
| }, |
| [&](const ClearOp&) { |
| sbl.clear(); |
| std_list.clear(); |
| active_trackers.clear(); |
| }, |
| [&](const ShrinkToFitOp&) { sbl.shrink_to_fit(); }, |
| [&](const MoveOp&) { |
| StableBlockList<int, BlockCapacity> temp_sbl = std::move(sbl); |
| std::list<int> temp_std = std::move(std_list); |
| sbl = std::move(temp_sbl); |
| std_list = std::move(temp_std); |
| }, |
| [&](const SwapOp&) { |
| sbl.swap(secondary_sbl); |
| std_list.swap(secondary_std); |
| active_trackers.swap(secondary_trackers); |
| }, |
| }, |
| op); |
| |
| VerifyInvariants(sbl, std_list, active_trackers); |
| VerifyInvariants(secondary_sbl, secondary_std, secondary_trackers); |
| } |
| } |
| |
| void StableBlockListMatchesStdListCap1( |
| const std::vector<Op>& ops) { // NOLINT(readability-redundant-declaration) |
| StableBlockListMatchesStdList<1>(ops); |
| } |
| |
| void StableBlockListMatchesStdListCap2( |
| const std::vector<Op>& ops) { // NOLINT(readability-redundant-declaration) |
| StableBlockListMatchesStdList<2>(ops); |
| } |
| |
| void StableBlockListMatchesStdListCap4( |
| const std::vector<Op>& ops) { // NOLINT(readability-redundant-declaration) |
| StableBlockListMatchesStdList<4>(ops); |
| } |
| |
| void StableBlockListMatchesStdListCap16( |
| const std::vector<Op>& ops) { // NOLINT(readability-redundant-declaration) |
| StableBlockListMatchesStdList<16>(ops); |
| } |
| |
| void StableBlockListMatchesStdListCap32( |
| const std::vector<Op>& ops) { // NOLINT(readability-redundant-declaration) |
| StableBlockListMatchesStdList<32>(ops); |
| } |
| |
| void StableBlockListMatchesStdListCap64( |
| const std::vector<Op>& ops) { // NOLINT(readability-redundant-declaration) |
| StableBlockListMatchesStdList<64>(ops); |
| } |
| |
| FUZZ_TEST(StableBlockListFuzzTest, // NOLINT(readability-redundant-declaration) |
| StableBlockListMatchesStdListCap1) |
| .WithDomains(VectorOf(AnyOpDomain()).WithMinSize(100)); |
| |
| FUZZ_TEST(StableBlockListFuzzTest, // NOLINT(readability-redundant-declaration) |
| StableBlockListMatchesStdListCap2) |
| .WithDomains(VectorOf(AnyOpDomain()).WithMinSize(100)); |
| |
| FUZZ_TEST(StableBlockListFuzzTest, // NOLINT(readability-redundant-declaration) |
| StableBlockListMatchesStdListCap4) |
| .WithDomains(VectorOf(AnyOpDomain()).WithMinSize(100)); |
| |
| FUZZ_TEST(StableBlockListFuzzTest, // NOLINT(readability-redundant-declaration) |
| StableBlockListMatchesStdListCap16) |
| .WithDomains(VectorOf(AnyOpDomain()).WithMinSize(100)); |
| |
| FUZZ_TEST(StableBlockListFuzzTest, // NOLINT(readability-redundant-declaration) |
| StableBlockListMatchesStdListCap32) |
| .WithDomains(VectorOf(AnyOpDomain()).WithMinSize(100)); |
| |
| FUZZ_TEST(StableBlockListFuzzTest, // NOLINT(readability-redundant-declaration) |
| StableBlockListMatchesStdListCap64) |
| .WithDomains(VectorOf(AnyOpDomain()).WithMinSize(100)); |
| |
| TEST(StableBlockListFuzzTest, DeterministicBasicParity) { |
| std::vector<Op> ops = { |
| PushBackOp{10}, PushBackOp{20}, PushBackOp{30}, PopFrontOp{}, |
| PushBackOp{40}, EraseIndexOp{0}, InsertEndOp{50}, EmplaceBackOp{60}, |
| EraseRangeOp{0, 2}, ShrinkToFitOp{}, ClearOp{}, PushBackOp{70}, |
| }; |
| StableBlockListMatchesStdList<4>(ops); |
| StableBlockListMatchesStdList<16>(ops); |
| } |
| |
| TEST(StableBlockListFuzzTest, EmptyRangeErasureParity) { |
| std::vector<Op> ops = { |
| EraseRangeOp{0, 0}, // On empty container |
| PushBackOp{10}, PushBackOp{20}, |
| PushBackOp{30}, EraseRangeOp{0, 0}, // Empty range at begin |
| EraseRangeOp{1, 0}, // Empty range in middle |
| EraseRangeOp{2, 0}, // Empty range before end |
| EraseRangeOp{3, 0}, // Empty range at end |
| PushBackOp{40}, |
| }; |
| StableBlockListMatchesStdList<1>(ops); |
| StableBlockListMatchesStdList<2>(ops); |
| StableBlockListMatchesStdList<4>(ops); |
| StableBlockListMatchesStdList<16>(ops); |
| StableBlockListMatchesStdList<32>(ops); |
| StableBlockListMatchesStdList<64>(ops); |
| } |
| |
| } // namespace |
| } // namespace quiche |