1 // Copyright 2024 The Pigweed Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 // use this file except in compliance with the License. You may obtain a copy of
5 // the License at
6 //
7 // https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 // License for the specific language governing permissions and limitations under
13 // the License.
14 #pragma once
15
16 #include <cstddef>
17 #include <cstdint>
18
19 #include "pw_allocator/block/detailed_block.h"
20 #include "pw_allocator/block/testing.h"
21 #include "pw_allocator/block_allocator.h"
22 #include "pw_status/status.h"
23 #include "pw_unit_test/framework.h"
24
25 namespace pw::allocator::test {
26
27 /// Test fixture responsible for managing a memory region and an allocator that
28 /// allocates block of memory from it.
29 ///
30 /// This base class contains all the code that does not depend specific
31 /// `Block` or `BlockAllocator` types.
32 class BlockAllocatorTestBase : public ::testing::Test {
33 public:
34 static constexpr size_t kDefaultBlockOverhead =
35 DetailedBlock<>::kBlockOverhead;
36
37 // Size of the memory region to use in the tests below.
38 // This must be large enough so that BlockType::Init does not fail.
39 static constexpr size_t kCapacity = 1024;
40
41 // The number of allocated pointers cached by the test fixture.
42 static constexpr size_t kNumPtrs = 16;
43
44 // Represents the sizes of various allocations.
45 static constexpr size_t kLargeInnerSize = kCapacity / 8;
46 static constexpr size_t kLargeOuterSize =
47 kDefaultBlockOverhead + kLargeInnerSize;
48
49 static constexpr size_t kSmallInnerSize = kDefaultBlockOverhead * 2;
50 static constexpr size_t kSmallOuterSize =
51 kDefaultBlockOverhead + kSmallInnerSize;
52
53 static constexpr size_t kSmallerOuterSize = kSmallInnerSize;
54 static constexpr size_t kLargerOuterSize =
55 kLargeOuterSize + kSmallerOuterSize;
56
57 protected:
58 // Test fixtures.
59 void SetUp() override;
60
61 /// Returns the underlying memory region.
62 virtual ByteSpan GetBytes() = 0;
63
64 /// Initialize the allocator with a region of memory and return it.
65 virtual Allocator& GetGenericAllocator() = 0;
66
67 /// Initialize the allocator with a sequence of preallocated blocks and return
68 /// it.
69 ///
70 /// See also ``Preallocation``.
71 virtual Allocator& GetGenericAllocator(
72 std::initializer_list<Preallocation> preallocations) = 0;
73
74 /// Gets the next allocation from an allocated pointer.
75 virtual void* NextAfter(size_t index) = 0;
76
77 /// Store an allocated pointer in the test's cache of pointers.
78 void Store(size_t index, void* ptr);
79
80 /// Retrieve an allocated pointer from the test's cache of pointers.
81 void* Fetch(size_t index);
82
83 /// Swaps the pointer at indices `i` and `j`.
84 void Swap(size_t i, size_t j);
85
86 /// Ensures the memory is usable by writing to it.
87 void UseMemory(void* ptr, size_t size);
88
89 // Unit tests.
90 void GetCapacity();
91 void AllocateLarge();
92 void AllocateSmall();
93 void AllocateTooLarge();
94 void AllocateLargeAlignment();
95 void AllocateAlignmentFailure();
96 void DeallocateNull();
97 void DeallocateShuffled();
98 void ResizeNull();
99 void ResizeLargeSame();
100 void ResizeLargeSmaller();
101 void ResizeLargeLarger();
102 void ResizeLargeLargerFailure();
103 void ResizeSmallSame();
104 void ResizeSmallSmaller();
105 void ResizeSmallLarger();
106 void ResizeSmallLargerFailure();
107
108 private:
109 std::array<void*, kNumPtrs> ptrs_;
110 };
111
112 /// Test fixture responsible for managing a memory region and an allocator that
113 /// allocates block of memory from it.
114 ///
115 /// This derived class contains all the code that depends specific `Block` or
116 /// `BlockAllocator` types.
117 ///
118 /// @tparam BlockAllocatorType The type of the `BlockAllocator` being tested.
119 template <typename BlockAllocatorType>
120 class BlockAllocatorTest : public BlockAllocatorTestBase {
121 public:
122 using BlockType = typename BlockAllocatorType::BlockType;
123
124 protected:
125 // Test fixtures.
BlockAllocatorTest(BlockAllocatorType & allocator)126 BlockAllocatorTest(BlockAllocatorType& allocator) : allocator_(allocator) {
127 bytes_ = ByteSpan(buffer_);
128 }
129
GetBytes()130 ByteSpan GetBytes() override { return bytes_; }
131
GetGenericAllocator()132 Allocator& GetGenericAllocator() override { return GetAllocator(); }
133
134 BlockAllocatorType& GetAllocator();
135
GetGenericAllocator(std::initializer_list<Preallocation> preallocations)136 Allocator& GetGenericAllocator(
137 std::initializer_list<Preallocation> preallocations) override {
138 return GetAllocator(preallocations);
139 }
140
141 BlockAllocatorType& GetAllocator(
142 std::initializer_list<Preallocation> preallocations);
143
144 void* NextAfter(size_t index) override;
145
146 void TearDown() override;
147
148 // Unit tests.
149 static void AutomaticallyInit(BlockAllocatorType& allocator);
150 void ExplicitlyInit(BlockAllocatorType& allocator);
151 void IterateOverBlocks();
152 void MeasureFragmentation();
153 void PoisonPeriodically();
154
155 private:
156 BlockAllocatorType& allocator_;
157 alignas(BlockType::kAlignment) std::array<std::byte, kCapacity> buffer_;
158 ByteSpan bytes_;
159 };
160
161 // Test fixture template method implementations.
162
163 template <typename BlockAllocatorType>
GetAllocator()164 BlockAllocatorType& BlockAllocatorTest<BlockAllocatorType>::GetAllocator() {
165 allocator_.Init(GetBytes());
166 return allocator_;
167 }
168
169 template <typename BlockAllocatorType>
GetAllocator(std::initializer_list<Preallocation> preallocations)170 BlockAllocatorType& BlockAllocatorTest<BlockAllocatorType>::GetAllocator(
171 std::initializer_list<Preallocation> preallocations) {
172 auto* first = Preallocate<BlockType>(GetBytes(), preallocations);
173 size_t index = 0;
174 for (BlockType* block = first; block != nullptr; block = block->Next()) {
175 Store(index, block->IsFree() ? nullptr : block->UsableSpace());
176 ++index;
177 }
178 allocator_.Init(first);
179 return allocator_;
180 }
181
182 template <typename BlockAllocatorType>
NextAfter(size_t index)183 void* BlockAllocatorTest<BlockAllocatorType>::NextAfter(size_t index) {
184 void* ptr = Fetch(index);
185 if (ptr == nullptr) {
186 return nullptr;
187 }
188
189 auto* block = BlockType::FromUsableSpace(ptr);
190 block = block->Next();
191 while (block != nullptr && block->IsFree()) {
192 block = block->Next();
193 }
194 return block == nullptr ? nullptr : block->UsableSpace();
195 }
196
197 template <typename BlockAllocatorType>
TearDown()198 void BlockAllocatorTest<BlockAllocatorType>::TearDown() {
199 for (size_t i = 0; i < kNumPtrs; ++i) {
200 void* ptr = Fetch(i);
201 if (ptr != nullptr) {
202 allocator_.Deallocate(ptr);
203 }
204 }
205 allocator_.Reset();
206 }
207
208 // Unit tests template method implementations.
209
210 template <typename BlockAllocatorType>
AutomaticallyInit(BlockAllocatorType & allocator)211 void BlockAllocatorTest<BlockAllocatorType>::AutomaticallyInit(
212 BlockAllocatorType& allocator) {
213 EXPECT_NE(*(allocator.blocks().begin()), nullptr);
214 }
215
216 template <typename BlockAllocatorType>
ExplicitlyInit(BlockAllocatorType & allocator)217 void BlockAllocatorTest<BlockAllocatorType>::ExplicitlyInit(
218 BlockAllocatorType& allocator) {
219 EXPECT_EQ(*(allocator.blocks().begin()), nullptr);
220 allocator.Init(GetBytes());
221 EXPECT_NE(*(allocator.blocks().begin()), nullptr);
222 }
223
224 template <typename BlockAllocatorType>
IterateOverBlocks()225 void BlockAllocatorTest<BlockAllocatorType>::IterateOverBlocks() {
226 Allocator& allocator = GetGenericAllocator({
227 {kSmallOuterSize, Preallocation::kFree},
228 {kLargeOuterSize, Preallocation::kUsed},
229 {kSmallOuterSize, Preallocation::kFree},
230 {kLargeOuterSize, Preallocation::kUsed},
231 {kSmallOuterSize, Preallocation::kFree},
232 {kLargeOuterSize, Preallocation::kUsed},
233 {Preallocation::kSizeRemaining, Preallocation::kFree},
234 });
235
236 // Count the blocks. The unallocated ones vary in size, but the allocated ones
237 // should all be the same.
238 size_t free_count = 0;
239 size_t used_count = 0;
240 auto& block_allocator = static_cast<BlockAllocatorType&>(allocator);
241 for (auto* block : block_allocator.blocks()) {
242 if (block->IsFree()) {
243 ++free_count;
244 } else {
245 EXPECT_EQ(block->OuterSize(), kLargeOuterSize);
246 ++used_count;
247 }
248 }
249 EXPECT_EQ(used_count, 3U);
250 EXPECT_EQ(free_count, 4U);
251 }
252
253 template <typename BlockAllocatorType>
MeasureFragmentation()254 void BlockAllocatorTest<BlockAllocatorType>::MeasureFragmentation() {
255 Allocator& allocator = GetGenericAllocator({
256 {0x020, Preallocation::kFree},
257 {0x040, Preallocation::kUsed},
258 {0x080, Preallocation::kFree},
259 {0x100, Preallocation::kUsed},
260 {0x200, Preallocation::kFree},
261 {Preallocation::kSizeRemaining, Preallocation::kUsed},
262 });
263
264 auto& block_allocator = static_cast<BlockAllocatorType&>(allocator);
265 constexpr size_t alignment = BlockAllocatorType::BlockType::kAlignment;
266 size_t sum_of_squares = 0;
267 size_t sum = 0;
268 for (const auto block : block_allocator.blocks()) {
269 if (block->IsFree()) {
270 size_t inner_size = block->InnerSize() / alignment;
271 sum_of_squares += inner_size * inner_size;
272 sum += inner_size;
273 }
274 }
275
276 Fragmentation fragmentation = block_allocator.MeasureFragmentation();
277 EXPECT_EQ(fragmentation.sum_of_squares.hi, 0U);
278 EXPECT_EQ(fragmentation.sum_of_squares.lo, sum_of_squares);
279 EXPECT_EQ(fragmentation.sum, sum);
280 }
281
282 template <typename BlockAllocatorType>
PoisonPeriodically()283 void BlockAllocatorTest<BlockAllocatorType>::PoisonPeriodically() {
284 // Allocate 8 blocks to prevent every other from being merged when freed.
285 Allocator& allocator = GetGenericAllocator({
286 {kSmallOuterSize, Preallocation::kUsed},
287 {kSmallOuterSize, Preallocation::kUsed},
288 {kSmallOuterSize, Preallocation::kUsed},
289 {kSmallOuterSize, Preallocation::kUsed},
290 {kSmallOuterSize, Preallocation::kUsed},
291 {kSmallOuterSize, Preallocation::kUsed},
292 {kSmallOuterSize, Preallocation::kUsed},
293 {Preallocation::kSizeRemaining, Preallocation::kUsed},
294 });
295 ASSERT_LT(BlockType::kPoisonOffset, kSmallInnerSize);
296
297 // Since the test poisons blocks, it cannot iterate over the blocks without
298 // crashing. Use `Fetch` instead.
299 for (size_t i = 0; i < 8; ++i) {
300 if (i % 2 != 0) {
301 continue;
302 }
303 auto* bytes = cpp20::bit_cast<std::byte*>(Fetch(i));
304 auto* block = BlockType::FromUsableSpace(bytes);
305 allocator.Deallocate(bytes);
306 EXPECT_TRUE(block->IsFree());
307 EXPECT_TRUE(block->IsValid());
308 bytes[BlockType::kPoisonOffset] = ~bytes[BlockType::kPoisonOffset];
309
310 if (i == 6) {
311 // The test_config is defined to only detect corruption is on every fourth
312 // freed block. Fix up the block to avoid crashing on teardown.
313 EXPECT_FALSE(block->IsValid());
314 bytes[BlockType::kPoisonOffset] = ~bytes[BlockType::kPoisonOffset];
315 } else {
316 EXPECT_TRUE(block->IsValid());
317 }
318 Store(i, nullptr);
319 }
320 }
321
322 } // namespace pw::allocator::test
323