| /* | |
| * Copyright 2022 Rive | |
| */ | |
| #include <rive/simple_array.hpp> | |
| #include <catch.hpp> | |
| #include <rive/text_engine.hpp> | |
| #include <cstdint> | |
| #include <limits> | |
| using namespace rive; | |
| TEST_CASE("array initializes as expected", "[simple array]") | |
| { | |
| SimpleArray<int> array; | |
| REQUIRE(array.empty()); | |
| REQUIRE(array.size() == 0); | |
| REQUIRE(array.size_bytes() == 0); | |
| REQUIRE(array.begin() == array.end()); | |
| } | |
| TEST_CASE("simple array can be created", "[simple array]") | |
| { | |
| std::vector<int> v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; | |
| SimpleArray<int> array(v); | |
| REQUIRE(!array.empty()); | |
| REQUIRE(array.size() == 10); | |
| REQUIRE(array.size_bytes() == 10 * sizeof(int)); | |
| REQUIRE(array.begin() + array.size() == array.end()); | |
| int counter = 0; | |
| int sum = 0; | |
| for (auto s : array) | |
| { | |
| counter += 1; | |
| sum += s; | |
| } | |
| REQUIRE(counter == 10); | |
| REQUIRE(sum == 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9); | |
| } | |
| TEST_CASE("can iterate simple array", "[simple array]") | |
| { | |
| const int carray[] = {2, 4, 8, 16}; | |
| SimpleArray<int> array(carray, 4); | |
| int expect = 2; | |
| for (auto value : array) | |
| { | |
| REQUIRE(value == expect); | |
| expect *= 2; | |
| } | |
| } | |
| TEST_CASE("can build up a simple array", "[simple array]") | |
| { | |
| SimpleArrayBuilder<int> builder; | |
| builder.add(1); | |
| builder.add(2); | |
| REQUIRE(builder.size() == 2); | |
| REQUIRE(builder.capacity() == 2); | |
| builder.add(3); | |
| REQUIRE(builder.size() == 3); | |
| REQUIRE(builder.capacity() == 4); | |
| int iterationCount = 0; | |
| int expect = 1; | |
| for (auto value : builder) | |
| { | |
| REQUIRE(value == expect++); | |
| iterationCount++; | |
| } | |
| // Should only iterate what's been written so far. | |
| REQUIRE(iterationCount == 3); | |
| int reallocCountBeforeMove = SimpleArrayTesting::reallocCount; | |
| SimpleArray<int> array = std::move(builder); | |
| REQUIRE(array[0] == 1); | |
| REQUIRE(array[1] == 2); | |
| REQUIRE(array[2] == 3); | |
| REQUIRE(array.size() == 3); | |
| REQUIRE(SimpleArrayTesting::reallocCount == reallocCountBeforeMove + 1); | |
| } | |
| struct StructA | |
| { | |
| rive::SimpleArray<uint32_t> numbers; | |
| }; | |
| static SimpleArray<StructA> buildStructs() | |
| { | |
| SimpleArrayTesting::resetCounters(); | |
| std::vector<uint32_t> vA{33, 22, 44, 66}; | |
| SimpleArray<uint32_t> numbersA(vA); | |
| StructA dataA = {std::move(numbersA)}; | |
| // We moved the data so expect only one alloc and 0 reallocs. | |
| REQUIRE(SimpleArrayTesting::mallocCount == 1); | |
| REQUIRE(SimpleArrayTesting::reallocCount == 0); | |
| REQUIRE(dataA.numbers.size() == 4); | |
| REQUIRE(numbersA.size() == 0); | |
| std::vector<uint32_t> vB{1, 2, 3}; | |
| SimpleArray<uint32_t> numbersB(vB); | |
| StructA dataB = {std::move(numbersB)}; | |
| REQUIRE(SimpleArrayTesting::mallocCount == 2); | |
| REQUIRE(SimpleArrayTesting::reallocCount == 0); | |
| REQUIRE(dataB.numbers.size() == 3); | |
| REQUIRE(numbersB.size() == 0); | |
| SimpleArray<StructA> structs(2); | |
| structs[0] = std::move(dataA); | |
| structs[1] = std::move(dataB); | |
| // Should've alloc one more time to create the structs object. | |
| REQUIRE(SimpleArrayTesting::mallocCount == 3); | |
| REQUIRE(SimpleArrayTesting::reallocCount == 0); | |
| REQUIRE(structs.size() == 2); | |
| REQUIRE(structs[0].numbers.size() == 4); | |
| REQUIRE(structs[1].numbers.size() == 3); | |
| return structs; | |
| } | |
| TEST_CASE("arrays of arrays work", "[simple array]") | |
| { | |
| auto structs = buildStructs(); | |
| REQUIRE(SimpleArrayTesting::mallocCount == 3); | |
| REQUIRE(SimpleArrayTesting::reallocCount == 0); | |
| REQUIRE(structs.size() == 2); | |
| REQUIRE(structs[0].numbers.size() == 4); | |
| REQUIRE(structs[1].numbers.size() == 3); | |
| } | |
| static SimpleArray<StructA> buildStructsWithBuilder() | |
| { | |
| SimpleArrayTesting::resetCounters(); | |
| SimpleArrayBuilder<StructA> structs(2); | |
| REQUIRE(SimpleArrayTesting::mallocCount == 1); | |
| for (int i = 0; i < 3; i++) | |
| { | |
| SimpleArray<uint32_t> numbers({33, 22, 44, 66}); | |
| StructA data = {std::move(numbers)}; | |
| structs.add(std::move(data)); | |
| } | |
| REQUIRE(SimpleArrayTesting::mallocCount == 4); | |
| // Realloc once because we'd reserved 2 and actually added 3. | |
| REQUIRE(SimpleArrayTesting::reallocCount == 1); | |
| return std::move(structs); | |
| } | |
| TEST_CASE("builder arrays of arrays work", "[simple array]") | |
| { | |
| auto structs = buildStructsWithBuilder(); | |
| // alloc counters should still be the same | |
| REQUIRE(SimpleArrayTesting::mallocCount == 4); | |
| // Realloc one more time as we sized down. | |
| REQUIRE(SimpleArrayTesting::reallocCount == 2); | |
| } | |
| TEST_CASE("builders can be reset", "[simple array]") | |
| { | |
| SimpleArrayTesting::resetCounters(); | |
| SimpleArrayBuilder<uint32_t> builder(3); | |
| builder.add(1); | |
| builder.add(2); | |
| builder.add(3); | |
| REQUIRE(SimpleArrayTesting::mallocCount == 1); | |
| REQUIRE(SimpleArrayTesting::freeCount == 0); | |
| REQUIRE(SimpleArrayTesting::reallocCount == 0); | |
| builder = SimpleArrayBuilder<uint32_t>(4); | |
| // Previous builder got freed. | |
| REQUIRE(SimpleArrayTesting::freeCount == 1); | |
| // We allocated more memory. | |
| REQUIRE(SimpleArrayTesting::mallocCount == 2); | |
| REQUIRE(SimpleArrayTesting::reallocCount == 0); | |
| builder.add(3); | |
| builder.add(2); | |
| SimpleArrayTesting::resetCounters(); | |
| SimpleArray<uint32_t> array = std::move(builder); | |
| // Realloc'd down | |
| REQUIRE(SimpleArrayTesting::reallocCount == 1); | |
| // Free and malloc counts didn't move. | |
| REQUIRE(SimpleArrayTesting::freeCount == 0); | |
| REQUIRE(SimpleArrayTesting::mallocCount == 0); | |
| REQUIRE(array.size() == 2); | |
| } | |
| TEST_CASE("ctor returns empty array on size*sizeof(T) overflow", | |
| "[simple array]") | |
| { | |
| SimpleArrayTesting::resetCounters(); | |
| // (SIZE_MAX/4) * sizeof(uint64_t) = (SIZE_MAX/4) * 8 wraps SIZE_MAX. | |
| // checkedMul rejects before malloc is called, so this is safe to | |
| // construct in a unit test — no gigantic allocation is attempted. | |
| constexpr size_t huge = std::numeric_limits<size_t>::max() / 4; | |
| SimpleArray<uint64_t> array(huge); | |
| REQUIRE(array.empty()); | |
| REQUIRE(array.size() == 0); | |
| REQUIRE(array.size_bytes() == 0); | |
| REQUIRE(array.data() == nullptr); | |
| REQUIRE(SimpleArrayTesting::mallocCount == 0); | |
| } | |
| // Skip under AddressSanitizer: ASAN aborts on requested sizes above its | |
| // internal cap (~1 TiB) before the system allocator can return null, so the | |
| // production null-check this test exists to exercise never runs. The defense | |
| // itself (m_ptr null-check in the ctor) remains in the code; ASAN catches | |
| // the same class of misuse via its allocation-size-too-big diagnostic. | |
| // __SANITIZE_ADDRESS__ covers GCC, MSVC, and Clang 13+; older Apple Clang | |
| // only sets __has_feature(address_sanitizer), so check both. The | |
| // __has_feature shim keeps MSVC's preprocessor (which doesn't know | |
| // __has_feature) from choking on the second condition. | |
| #ifndef __has_feature | |
| #define __has_feature(x) 0 | |
| #endif | |
| #if !defined(__SANITIZE_ADDRESS__) && !__has_feature(address_sanitizer) | |
| TEST_CASE("ctor returns empty array when malloc fails (OOM)", "[simple array]") | |
| { | |
| SimpleArrayTesting::resetCounters(); | |
| // SIZE_MAX * 1 does not overflow checkedMul, but no real allocator can | |
| // satisfy SIZE_MAX bytes — malloc returns nullptr and the ctor must | |
| // tolerate it without writing through the null pointer. | |
| SimpleArray<uint8_t> array(std::numeric_limits<size_t>::max()); | |
| REQUIRE(array.empty()); | |
| REQUIRE(array.size() == 0); | |
| REQUIRE(array.data() == nullptr); | |
| REQUIRE(SimpleArrayTesting::mallocCount == 0); | |
| } | |
| #endif | |
| TEST_CASE("delegating ctor stays safe on overflow", "[simple array]") | |
| { | |
| SimpleArrayTesting::resetCounters(); | |
| constexpr size_t huge = std::numeric_limits<size_t>::max() / 4; | |
| const uint64_t dummy = 0; | |
| SimpleArray<uint64_t> array(&dummy, huge); | |
| REQUIRE(array.empty()); | |
| REQUIRE(array.size() == 0); | |
| REQUIRE(array.data() == nullptr); | |
| REQUIRE(SimpleArrayTesting::mallocCount == 0); | |
| } | |
| TEST_CASE("ctor still works for normal sizes after overflow guard", | |
| "[simple array]") | |
| { | |
| SimpleArrayTesting::resetCounters(); | |
| SimpleArray<uint32_t> array(8); | |
| REQUIRE(!array.empty()); | |
| REQUIRE(array.size() == 8); | |
| REQUIRE(array.data() != nullptr); | |
| REQUIRE(SimpleArrayTesting::mallocCount == 1); | |
| } | |
| TEST_CASE("overflow-failed array stays composable", "[simple array]") | |
| { | |
| // Construct an array that fails the size*sizeof(T) overflow check, then | |
| // exercise the rest of the API on the empty state. Locks the | |
| // m_ptr=nullptr/m_size=0 invariant against future refactors of either | |
| // the ctor or the move/copy paths. | |
| constexpr size_t huge = std::numeric_limits<size_t>::max() / 4; | |
| SimpleArrayTesting::resetCounters(); | |
| SimpleArray<uint64_t> failed(huge); | |
| REQUIRE(failed.empty()); | |
| REQUIRE(failed.begin() == failed.end()); | |
| // range-for should iterate zero times. | |
| int iterations = 0; | |
| for (auto& v : failed) | |
| { | |
| (void)v; | |
| iterations++; | |
| } | |
| REQUIRE(iterations == 0); | |
| // move-construct from the failed array — moved should still be empty, | |
| // failed is left valid-but-empty per move semantics. | |
| SimpleArray<uint64_t> moved = std::move(failed); | |
| REQUIRE(moved.empty()); | |
| REQUIRE(moved.data() == nullptr); | |
| // copy-construct from another freshly-failed array — delegates through | |
| // SimpleArray(const T*, size_t) which short-circuits in the size==0 ctor. | |
| SimpleArray<uint64_t> another(huge); | |
| SimpleArray<uint64_t> copy(another); | |
| REQUIRE(copy.empty()); | |
| REQUIRE(copy.data() == nullptr); | |
| // No real allocations should have happened along any of these paths. | |
| REQUIRE(SimpleArrayTesting::mallocCount == 0); | |
| // Destruction at scope end must not crash on free(nullptr). | |
| } | |
| TEST_CASE("delegating ctor accepts (nullptr, 0) without UB", "[simple array]") | |
| { | |
| // The delegating SimpleArray(const T*, size_t) ctor must early-return | |
| // before touching `ptr` when size is zero; otherwise nullptr arithmetic | |
| // and memcpy(nullptr, ...) would be UB even with size == 0. | |
| SimpleArrayTesting::resetCounters(); | |
| SimpleArray<int> array(static_cast<const int*>(nullptr), 0); | |
| REQUIRE(array.empty()); | |
| REQUIRE(array.data() == nullptr); | |
| REQUIRE(SimpleArrayTesting::mallocCount == 0); | |
| } |