/******************************************************************************** * * * This file is part of IfcOpenShell. * * * * IfcOpenShell is free software: you can redistribute it and/or modify * * it under the terms of the Lesser GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * * IfcOpenShell is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * Lesser GNU General Public License for more details. * * * * You should have received a copy of the Lesser GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ // Tier-1 coverage of BufferPool's sub-allocator. The pool's free-list + // coalescing logic is pure CPU bookkeeping; wgpu calls only happen inside // addSubBuffer() during growth. We pre-seed sub-pools via the test-only // addSubBufferForTesting() seam so the tests don't need a real device, // then exercise alloc / free / alignment / coalescing / multi-sub-pool // behaviour against the public API. // // The fake handles below are never dereferenced — BufferPool treats // WGPUBuffer as an opaque token it just hands back inside Slice. Using // `reinterpret_cast(0x100)` etc. gives us stable identities // for cross-pool sub_idx assertions. #include "BufferPool.h" #include #include namespace { WGPUBuffer fake_handle(uintptr_t id) { // Any non-null pointer works; the value is only used for == comparisons // and never dereferenced. Adding an offset by id keeps multiple fakes // visibly distinct in failure messages. return reinterpret_cast(static_cast(0x1000) + id); } // RAII guard so the pool's destructor doesn't try to wgpuBufferRelease() // our fake handles. Drops the sub-pools via the test seam first. struct FakePoolGuard { BufferPool& p; ~FakePoolGuard() { p.clearSubPoolsForTesting(); } }; } // namespace TEST_CASE("empty pool reports zero capacity and refuses allocs", "[buffer_pool]") { BufferPool pool; FakePoolGuard guard{pool}; REQUIRE(pool.sub_buffer_count() == 0); REQUIRE(pool.total_capacity_bytes() == 0); REQUIRE(pool.total_used_bytes() == 0); REQUIRE(pool.total_free_bytes() == 0); REQUIRE(pool.largest_free_run_bytes() == 0); // No sub-pool exists yet; without a configured device addSubBuffer // can't grow, so alloc returns an invalid Slice rather than UB. auto s = pool.alloc(64, 16); REQUIRE_FALSE(s.valid()); REQUIRE(s.size == 0); } TEST_CASE("single alloc returns a valid aligned slice", "[buffer_pool]") { BufferPool pool; FakePoolGuard guard{pool}; pool.addSubBufferForTesting(fake_handle(1), 1024); REQUIRE(pool.sub_buffer_count() == 1); REQUIRE(pool.total_capacity_bytes() == 1024); REQUIRE(pool.total_used_bytes() == 0); REQUIRE(pool.largest_free_run_bytes() == 1024); auto s = pool.alloc(/*size=*/100, /*align=*/256); REQUIRE(s.valid()); REQUIRE(s.buffer == fake_handle(1)); REQUIRE(s.size == 100); REQUIRE((s.offset % 256) == 0); REQUIRE(s.sub_idx == 0); // `used` tracks alloc sizes (excludes pad). Free space drops by both. REQUIRE(pool.total_used_bytes() == 100); REQUIRE(pool.total_free_bytes() == 1024 - 100); } TEST_CASE("alloc-then-free round-trip returns the slot to the pool", "[buffer_pool]") { BufferPool pool; FakePoolGuard guard{pool}; pool.addSubBufferForTesting(fake_handle(1), 1024); auto s = pool.alloc(256, 1); REQUIRE(s.valid()); REQUIRE(pool.total_used_bytes() == 256); pool.free(s); REQUIRE(pool.total_used_bytes() == 0); REQUIRE(pool.largest_free_run_bytes() == 1024); // After the free-with-coalesce the pool is byte-identical to its // initial state, so an alloc of the original size can reuse the // same offset. auto s2 = pool.alloc(256, 1); REQUIRE(s2.valid()); REQUIRE(s2.offset == s.offset); } TEST_CASE("alignment padding is reclaimable by smaller allocs", "[buffer_pool]") { BufferPool pool; FakePoolGuard guard{pool}; pool.addSubBufferForTesting(fake_handle(1), 1024); // First alloc requests 256-byte alignment; offset 0 already satisfies // it, so no pad. Second alloc of size 100 follows at offset 256. auto a = pool.alloc(100, 256); auto b = pool.alloc(100, 256); REQUIRE(a.offset == 0); REQUIRE(b.offset == 256); REQUIRE(b.offset >= a.offset + a.size); // Used = sum of allocation sizes only. The 156 bytes of pad inside the // first 256-byte slot remain in free_ranges and are reclaimable by an // alloc small enough to fit them. REQUIRE(pool.total_used_bytes() == 200); auto c = pool.alloc(50, 1); REQUIRE(c.valid()); REQUIRE(c.offset >= 100); // lands in the leading pad of slot 0 REQUIRE(c.offset < 256); } TEST_CASE("free coalesces adjacent ranges in the same sub-pool", "[buffer_pool]") { BufferPool pool; FakePoolGuard guard{pool}; pool.addSubBufferForTesting(fake_handle(1), 1024); auto a = pool.alloc(256, 1); auto b = pool.alloc(256, 1); auto c = pool.alloc(256, 1); REQUIRE(a.offset + a.size == b.offset); REQUIRE(b.offset + b.size == c.offset); // Free in non-adjacent order: a, then c, leaves a hole around b. pool.free(a); pool.free(c); // largest_free_run can be a (256), b (still alloc'd, no), c+tail // (256 + remaining = at least 256). It's not 768 because b is in // the middle. REQUIRE(pool.largest_free_run_bytes() < 768); pool.free(b); // Now all three runs collapse into one contiguous free block, plus // the tail. largest_free_run is the entire sub-pool again. REQUIRE(pool.largest_free_run_bytes() == 1024); REQUIRE(pool.total_used_bytes() == 0); } TEST_CASE("alloc fails gracefully when no sub-pool can fit", "[buffer_pool]") { BufferPool pool; FakePoolGuard guard{pool}; pool.addSubBufferForTesting(fake_handle(1), 512); auto big = pool.alloc(512, 1); REQUIRE(big.valid()); REQUIRE(pool.total_used_bytes() == 512); // Pool is now full and can_grow() is false (we never configure'd // a device, so per_sub_buffer_capacity_ is 0). alloc returns // an invalid Slice rather than asserting or growing into garbage. REQUIRE_FALSE(pool.can_grow()); auto fail = pool.alloc(1, 1); REQUIRE_FALSE(fail.valid()); } TEST_CASE("multi sub-pool alloc spans pools and reports correct sub_idx", "[buffer_pool]") { BufferPool pool; FakePoolGuard guard{pool}; pool.addSubBufferForTesting(fake_handle(1), 256); pool.addSubBufferForTesting(fake_handle(2), 256); REQUIRE(pool.sub_buffer_count() == 2); REQUIRE(pool.total_capacity_bytes() == 512); // First alloc fits in sub-pool 0. auto a = pool.alloc(256, 1); REQUIRE(a.valid()); REQUIRE(a.buffer == fake_handle(1)); REQUIRE(a.sub_idx == 0); // Second alloc can't fit in sub-pool 0 (full); first-fit moves to // sub-pool 1. auto b = pool.alloc(256, 1); REQUIRE(b.valid()); REQUIRE(b.buffer == fake_handle(2)); REQUIRE(b.sub_idx == 1); REQUIRE(pool.total_used_bytes() == 512); REQUIRE(pool.total_free_bytes() == 0); } TEST_CASE("free routes by sub_idx — no cross-pool coalescing", "[buffer_pool]") { BufferPool pool; FakePoolGuard guard{pool}; pool.addSubBufferForTesting(fake_handle(1), 256); pool.addSubBufferForTesting(fake_handle(2), 256); auto a = pool.alloc(256, 1); // sub 0 auto b = pool.alloc(256, 1); // sub 1 REQUIRE(a.sub_idx == 0); REQUIRE(b.sub_idx == 1); pool.free(a); pool.free(b); // Both sub-pools are individually empty, but they are distinct // buffers — largest_free_run is per-sub-buffer, not summed across. REQUIRE(pool.total_used_bytes() == 0); REQUIRE(pool.largest_free_run_bytes() == 256); } TEST_CASE("free with invalid slice is a no-op", "[buffer_pool]") { BufferPool pool; FakePoolGuard guard{pool}; pool.addSubBufferForTesting(fake_handle(1), 512); auto a = pool.alloc(128, 1); REQUIRE(a.valid()); const uint64_t used_before = pool.total_used_bytes(); // Default-constructed Slice has size=0 + null buffer + sub_idx=-1. // free() should silently ignore it (this is the path real callers // hit when an alloc failed earlier and they unconditionally free). BufferPool::Slice junk; REQUIRE_FALSE(junk.valid()); pool.free(junk); REQUIRE(pool.total_used_bytes() == used_before); // Sub-index out of range is also ignored. BufferPool::Slice bad_idx = a; bad_idx.sub_idx = 99; pool.free(bad_idx); REQUIRE(pool.total_used_bytes() == used_before); // Real free still works after these no-ops. pool.free(a); REQUIRE(pool.total_used_bytes() == 0); }