// 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 #if __cplusplus >= 202002L #include #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); #endif // __cplusplus >= 202002L void TakesSpan([[maybe_unused]] ByteSpan span) {} TEST(Chunk, IsImplicitlyConvertibleToSpan) { AllocatorForTest allocator; std::optional chunk = HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, kArbitraryChunkSize); ASSERT_TRUE(chunk.has_value()); // ``Chunk`` should convert to ``ByteSpan``. TakesSpan(**chunk); } TEST(OwnedChunk, ReleaseDestroysChunkRegion) { AllocatorForTest 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 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 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 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 allocator; std::optional 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 allocator; std::optional 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 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 allocator; std::optional 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 allocator; std::optional 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 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 allocator; std::optional 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 allocator; const auto& metrics = allocator.metrics(); std::optional 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 allocator; std::optional 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 allocator; std::optional 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 allocator; std::optional 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 allocator; std::optional 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 allocator; std::optional 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 allocator; std::optional 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 allocator; std::optional 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 allocator; std::optional 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 allocator; std::optional 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 allocator; std::optional 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 allocator; std::optional chunk_1_opt = HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, kArbitraryChunkSize); ASSERT_TRUE(chunk_1_opt.has_value()); OwnedChunk& chunk_1 = *chunk_1_opt; std::optional 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 allocator; std::optional chunk_1_opt = HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, kArbitraryChunkSize); ASSERT_TRUE(chunk_1_opt.has_value()); OwnedChunk& chunk_1 = *chunk_1_opt; std::optional chunk_2_opt = chunk_1->TakeSuffix(kTakenFromOne); ASSERT_TRUE(chunk_2_opt.has_value()); OwnedChunk& chunk_2 = *chunk_2_opt; std::optional 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 allocator; std::optional chunk_1_opt = HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, kArbitraryChunkSize); ASSERT_TRUE(chunk_1_opt.has_value()); OwnedChunk& chunk_1 = *chunk_1_opt; std::optional chunk_2_opt = chunk_1->TakeSuffix(kTakenFromOne); ASSERT_TRUE(chunk_2_opt.has_value()); OwnedChunk& chunk_2 = *chunk_2_opt; std::optional 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 allocator; std::optional chunk_1_opt = HeaderChunkRegionTracker::AllocateRegionAsChunk(allocator, kArbitraryChunkSize); ASSERT_TRUE(chunk_1_opt.has_value()); OwnedChunk& chunk_1 = *chunk_1_opt; std::optional 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