| // Copyright 2023 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| #include "pw_multibuf/chunk.h" |
| |
| #include <memory> |
| |
| #if __cplusplus >= 202002L |
| #include <ranges> |
| #endif // __cplusplus >= 202002L |
| |
| #include "pw_allocator/testing.h" |
| #include "pw_multibuf/header_chunk_region_tracker.h" |
| #include "pw_unit_test/framework.h" |
| |
| namespace pw::multibuf { |
| namespace { |
| |
| using ::pw::allocator::test::AllocatorForTest; |
| |
| /// Returns literal with ``_size`` suffix as a ``size_t``. |
| /// |
| /// This is useful for writing size-related test assertions without |
| /// explicit (verbose) casts. |
| constexpr size_t operator"" _size(unsigned long long n) { return n; } |
| |
| const size_t kArbitraryAllocatorSize = 1024; |
| const size_t kArbitraryChunkSize = 32; |
| |
| #if __cplusplus >= 202002L |
| static_assert(std::ranges::contiguous_range<Chunk>); |
| #endif // __cplusplus >= 202002L |
| |
| void TakesSpan([[maybe_unused]] ByteSpan span) {} |
| |
| TEST(Chunk, IsImplicitlyConvertibleToSpan) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk.has_value()); |
| // ``Chunk`` should convert to ``ByteSpan``. |
| TakesSpan(**chunk); |
| } |
| |
| TEST(OwnedChunk, ReleaseDestroysChunkRegion) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| const auto& metrics = allocator.metrics(); |
| auto tracker = |
| HeaderChunkRegionTracker::AllocateRegion(allocator, kArbitraryChunkSize); |
| ASSERT_NE(tracker, nullptr); |
| EXPECT_EQ(metrics.num_allocations.value(), 1_size); |
| |
| std::optional<OwnedChunk> chunk_opt = tracker->CreateFirstChunk(); |
| ASSERT_TRUE(chunk_opt.has_value()); |
| auto& chunk = *chunk_opt; |
| EXPECT_EQ(metrics.num_allocations.value(), 2_size); |
| EXPECT_EQ(chunk.size(), kArbitraryChunkSize); |
| |
| chunk.Release(); |
| EXPECT_EQ(chunk.size(), 0_size); |
| EXPECT_EQ(metrics.num_deallocations.value(), 2_size); |
| EXPECT_EQ(metrics.allocated_bytes.value(), 0_size); |
| } |
| |
| TEST(OwnedChunk, DestructorDestroysChunkRegion) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| const auto& metrics = allocator.metrics(); |
| auto tracker = |
| HeaderChunkRegionTracker::AllocateRegion(allocator, kArbitraryChunkSize); |
| ASSERT_NE(tracker, nullptr); |
| EXPECT_EQ(metrics.num_allocations.value(), 1_size); |
| |
| { |
| std::optional<OwnedChunk> chunk = tracker->CreateFirstChunk(); |
| ASSERT_TRUE(chunk.has_value()); |
| EXPECT_EQ(metrics.num_allocations.value(), 2_size); |
| EXPECT_EQ(chunk->size(), kArbitraryChunkSize); |
| } |
| |
| EXPECT_EQ(metrics.num_deallocations.value(), 2_size); |
| EXPECT_EQ(metrics.allocated_bytes.value(), 0_size); |
| } |
| |
| TEST(Chunk, DiscardPrefixDiscardsPrefixOfSpan) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_opt.has_value()); |
| auto& chunk = *chunk_opt; |
| ConstByteSpan old_span = chunk; |
| const size_t kDiscarded = 4; |
| chunk->DiscardPrefix(kDiscarded); |
| EXPECT_EQ(chunk.size(), old_span.size() - kDiscarded); |
| EXPECT_EQ(chunk.data(), old_span.data() + kDiscarded); |
| } |
| |
| TEST(Chunk, TakePrefixTakesPrefixOfSpan) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_opt.has_value()); |
| auto& chunk = *chunk_opt; |
| ConstByteSpan old_span = chunk; |
| const size_t kTaken = 4; |
| std::optional<OwnedChunk> front_opt = chunk->TakePrefix(kTaken); |
| ASSERT_TRUE(front_opt.has_value()); |
| auto& front = *front_opt; |
| EXPECT_EQ(front->size(), kTaken); |
| EXPECT_EQ(front->data(), old_span.data()); |
| EXPECT_EQ(chunk.size(), old_span.size() - kTaken); |
| EXPECT_EQ(chunk.data(), old_span.data() + kTaken); |
| } |
| |
| TEST(Chunk, TruncateDiscardsEndOfSpan) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_opt.has_value()); |
| auto& chunk = *chunk_opt; |
| ConstByteSpan old_span = chunk; |
| const size_t kShorter = 5; |
| chunk->Truncate(old_span.size() - kShorter); |
| EXPECT_EQ(chunk.size(), old_span.size() - kShorter); |
| EXPECT_EQ(chunk.data(), old_span.data()); |
| } |
| |
| TEST(Chunk, TakeSuffixTakesEndOfSpan) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_opt.has_value()); |
| auto& chunk = *chunk_opt; |
| ConstByteSpan old_span = chunk; |
| const size_t kTaken = 5; |
| std::optional<OwnedChunk> tail_opt = chunk->TakeSuffix(kTaken); |
| ASSERT_TRUE(tail_opt.has_value()); |
| auto& tail = *tail_opt; |
| EXPECT_EQ(tail.size(), kTaken); |
| EXPECT_EQ(tail.data(), old_span.data() + old_span.size() - kTaken); |
| EXPECT_EQ(chunk.size(), old_span.size() - kTaken); |
| EXPECT_EQ(chunk.data(), old_span.data()); |
| } |
| |
| TEST(Chunk, SliceRemovesSidesOfSpan) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_opt.has_value()); |
| auto& chunk = *chunk_opt; |
| ConstByteSpan old_span = chunk; |
| const size_t kBegin = 4; |
| const size_t kEnd = 9; |
| chunk->Slice(kBegin, kEnd); |
| EXPECT_EQ(chunk.data(), old_span.data() + kBegin); |
| EXPECT_EQ(chunk.size(), kEnd - kBegin); |
| } |
| |
| TEST(Chunk, RegionPersistsUntilAllChunksReleased) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| const auto& metrics = allocator.metrics(); |
| std::optional<OwnedChunk> chunk_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_opt.has_value()); |
| auto& chunk = *chunk_opt; |
| // One allocation for the region tracker, one for the chunk. |
| EXPECT_EQ(metrics.num_allocations.value(), 2_size); |
| const size_t kSplitPoint = 13; |
| auto split_opt = chunk->TakePrefix(kSplitPoint); |
| ASSERT_TRUE(split_opt.has_value()); |
| auto& split = *split_opt; |
| // One allocation for the region tracker, one for each of two chunks. |
| EXPECT_EQ(metrics.num_allocations.value(), 3_size); |
| chunk.Release(); |
| EXPECT_EQ(metrics.num_deallocations.value(), 1_size); |
| split.Release(); |
| EXPECT_EQ(metrics.num_deallocations.value(), 3_size); |
| } |
| |
| TEST(Chunk, ClaimPrefixReclaimsDiscardedPrefix) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_opt.has_value()); |
| auto& chunk = *chunk_opt; |
| ConstByteSpan old_span = chunk; |
| const size_t kDiscarded = 4; |
| chunk->DiscardPrefix(kDiscarded); |
| EXPECT_TRUE(chunk->ClaimPrefix(kDiscarded)); |
| EXPECT_EQ(chunk.size(), old_span.size()); |
| EXPECT_EQ(chunk.data(), old_span.data()); |
| } |
| |
| TEST(Chunk, ClaimPrefixFailsOnFullRegionChunk) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_opt.has_value()); |
| auto& chunk = *chunk_opt; |
| EXPECT_FALSE(chunk->ClaimPrefix(1)); |
| } |
| |
| TEST(Chunk, ClaimPrefixFailsOnNeighboringChunk) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_opt.has_value()); |
| auto& chunk = *chunk_opt; |
| const size_t kSplitPoint = 22; |
| auto front = chunk->TakePrefix(kSplitPoint); |
| ASSERT_TRUE(front.has_value()); |
| EXPECT_FALSE(chunk->ClaimPrefix(1)); |
| } |
| |
| TEST(Chunk, |
| ClaimPrefixFailsAtStartOfRegionEvenAfterReleasingChunkAtEndOfRegion) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_opt.has_value()); |
| auto& chunk = *chunk_opt; |
| const size_t kTaken = 13; |
| auto split = chunk->TakeSuffix(kTaken); |
| ASSERT_TRUE(split.has_value()); |
| split->Release(); |
| EXPECT_FALSE(chunk->ClaimPrefix(1)); |
| } |
| |
| TEST(Chunk, ClaimPrefixReclaimsPrecedingChunksDiscardedSuffix) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_opt.has_value()); |
| auto& chunk = *chunk_opt; |
| const size_t kSplitPoint = 13; |
| auto split_opt = chunk->TakePrefix(kSplitPoint); |
| ASSERT_TRUE(split_opt.has_value()); |
| auto& split = *split_opt; |
| const size_t kDiscard = 3; |
| split->Truncate(split.size() - kDiscard); |
| EXPECT_TRUE(chunk->ClaimPrefix(kDiscard)); |
| EXPECT_FALSE(chunk->ClaimPrefix(1)); |
| } |
| |
| TEST(Chunk, ClaimSuffixReclaimsTruncatedEnd) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_opt.has_value()); |
| auto& chunk = *chunk_opt; |
| ConstByteSpan old_span = *chunk; |
| const size_t kDiscarded = 4; |
| chunk->Truncate(old_span.size() - kDiscarded); |
| EXPECT_TRUE(chunk->ClaimSuffix(kDiscarded)); |
| EXPECT_EQ(chunk->size(), old_span.size()); |
| EXPECT_EQ(chunk->data(), old_span.data()); |
| } |
| |
| TEST(Chunk, ClaimSuffixFailsOnFullRegionChunk) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_opt.has_value()); |
| auto& chunk = *chunk_opt; |
| EXPECT_FALSE(chunk->ClaimSuffix(1)); |
| } |
| |
| TEST(Chunk, ClaimSuffixFailsWithNeighboringChunk) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_opt.has_value()); |
| auto& chunk = *chunk_opt; |
| const size_t kSplitPoint = 22; |
| auto split_opt = chunk->TakePrefix(kSplitPoint); |
| ASSERT_TRUE(split_opt.has_value()); |
| auto& split = *split_opt; |
| EXPECT_FALSE(split->ClaimSuffix(1)); |
| } |
| |
| TEST(Chunk, ClaimSuffixFailsAtEndOfRegionEvenAfterReleasingFirstChunkInRegion) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_opt.has_value()); |
| auto& chunk = *chunk_opt; |
| const size_t kTaken = 22; |
| auto split_opt = chunk->TakeSuffix(kTaken); |
| ASSERT_TRUE(split_opt.has_value()); |
| auto& split = *split_opt; |
| EXPECT_FALSE(split->ClaimSuffix(1)); |
| } |
| |
| TEST(Chunk, ClaimSuffixReclaimsFollowingChunksDiscardedPrefix) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_opt.has_value()); |
| auto& chunk = *chunk_opt; |
| const size_t kSplitPoint = 22; |
| auto split_opt = chunk->TakePrefix(kSplitPoint); |
| ASSERT_TRUE(split_opt.has_value()); |
| auto& split = *split_opt; |
| const size_t kDiscarded = 3; |
| chunk->DiscardPrefix(kDiscarded); |
| EXPECT_TRUE(split->ClaimSuffix(kDiscarded)); |
| EXPECT_FALSE(split->ClaimSuffix(1)); |
| } |
| |
| TEST(Chunk, MergeReturnsFalseForChunksFromDifferentRegions) { |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_1_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_1_opt.has_value()); |
| OwnedChunk& chunk_1 = *chunk_1_opt; |
| std::optional<OwnedChunk> chunk_2_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_2_opt.has_value()); |
| OwnedChunk& chunk_2 = *chunk_2_opt; |
| EXPECT_FALSE(chunk_1->CanMerge(*chunk_2)); |
| EXPECT_FALSE(chunk_1->Merge(chunk_2)); |
| // Ensure that neither chunk was modified |
| EXPECT_EQ(chunk_1.size(), kArbitraryChunkSize); |
| EXPECT_EQ(chunk_2.size(), kArbitraryChunkSize); |
| } |
| |
| TEST(Chunk, MergeReturnsFalseForNonAdjacentChunksFromSameRegion) { |
| const size_t kTakenFromOne = 8; |
| const size_t kTakenFromTwo = 4; |
| |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_1_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_1_opt.has_value()); |
| OwnedChunk& chunk_1 = *chunk_1_opt; |
| |
| std::optional<OwnedChunk> chunk_2_opt = chunk_1->TakeSuffix(kTakenFromOne); |
| ASSERT_TRUE(chunk_2_opt.has_value()); |
| OwnedChunk& chunk_2 = *chunk_2_opt; |
| |
| std::optional<OwnedChunk> chunk_3_opt = chunk_2->TakeSuffix(kTakenFromTwo); |
| ASSERT_TRUE(chunk_3_opt.has_value()); |
| OwnedChunk& chunk_3 = *chunk_3_opt; |
| |
| EXPECT_FALSE(chunk_1->CanMerge(*chunk_3)); |
| EXPECT_FALSE(chunk_1->Merge(chunk_3)); |
| EXPECT_EQ(chunk_1.size(), kArbitraryChunkSize - kTakenFromOne); |
| EXPECT_EQ(chunk_2.size(), kTakenFromOne - kTakenFromTwo); |
| EXPECT_EQ(chunk_3.size(), kTakenFromTwo); |
| } |
| |
| TEST(Chunk, MergeJoinsMultipleAdjacentChunksFromSameRegion) { |
| const size_t kTakenFromOne = 8; |
| const size_t kTakenFromTwo = 4; |
| |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_1_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_1_opt.has_value()); |
| OwnedChunk& chunk_1 = *chunk_1_opt; |
| |
| std::optional<OwnedChunk> chunk_2_opt = chunk_1->TakeSuffix(kTakenFromOne); |
| ASSERT_TRUE(chunk_2_opt.has_value()); |
| OwnedChunk& chunk_2 = *chunk_2_opt; |
| |
| std::optional<OwnedChunk> chunk_3_opt = chunk_2->TakeSuffix(kTakenFromTwo); |
| ASSERT_TRUE(chunk_3_opt.has_value()); |
| OwnedChunk& chunk_3 = *chunk_3_opt; |
| |
| EXPECT_TRUE(chunk_1->CanMerge(*chunk_2)); |
| EXPECT_TRUE(chunk_1->Merge(chunk_2)); |
| EXPECT_TRUE(chunk_1->CanMerge(*chunk_3)); |
| EXPECT_TRUE(chunk_1->Merge(chunk_3)); |
| |
| EXPECT_EQ(chunk_1.size(), kArbitraryChunkSize); |
| EXPECT_EQ(chunk_2.size(), 0_size); |
| EXPECT_EQ(chunk_3.size(), 0_size); |
| } |
| |
| TEST(Chunk, MergeJoinsAdjacentChunksFromSameRegion) { |
| const size_t kTaken = 4; |
| |
| AllocatorForTest<kArbitraryAllocatorSize> allocator; |
| std::optional<OwnedChunk> chunk_1_opt = |
| HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, |
| kArbitraryChunkSize); |
| ASSERT_TRUE(chunk_1_opt.has_value()); |
| OwnedChunk& chunk_1 = *chunk_1_opt; |
| std::optional<OwnedChunk> chunk_2_opt = chunk_1->TakeSuffix(kTaken); |
| ASSERT_TRUE(chunk_2_opt.has_value()); |
| OwnedChunk& chunk_2 = *chunk_2_opt; |
| EXPECT_EQ(chunk_1.size(), kArbitraryChunkSize - kTaken); |
| EXPECT_EQ(chunk_2.size(), kTaken); |
| |
| EXPECT_TRUE(chunk_1->CanMerge(*chunk_2)); |
| EXPECT_TRUE(chunk_1->Merge(chunk_2)); |
| EXPECT_EQ(chunk_1.size(), kArbitraryChunkSize); |
| EXPECT_EQ(chunk_2.size(), 0_size); |
| } |
| |
| } // namespace |
| } // namespace pw::multibuf |