blob: 0ec0363116ed59b82113cafa60cc219acd52c313 [file] [log] [blame] [edit]
/*
* Copyright (C) 2025 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include <algorithm>
#include <random>
#include <wtf/DataLog.h>
#include <wtf/IntervalSet.h>
#include <wtf/ListDump.h>
#include <wtf/StringPrintStream.h>
#include <wtf/Vector.h>
namespace TestWebKitAPI {
struct IntervalSetTest {
static constexpr bool verbose = false;
};
using Point = uint32_t;
using Value = int;
using Interval = Range<Point>;
TEST(WTF_IntervalSet, Basic)
{
IntervalSet<Point, Value> intervalSet;
EXPECT_TRUE(intervalSet.isEmpty());
EXPECT_FALSE(intervalSet.hasOverlap({ 0, 10 }));
EXPECT_FALSE(intervalSet.find({ 0, 10 }));
EXPECT_EQ(intervalSet.begin(), intervalSet.end());
}
TEST(WTF_IntervalSet, SingleInterval)
{
IntervalSet<Point, Value> intervalSet;
// Insert a single interval [10, 20) with value 42
intervalSet.insert({ 10, 20 }, 42);
EXPECT_FALSE(intervalSet.isEmpty());
EXPECT_NE(intervalSet.begin(), intervalSet.end());
auto it = intervalSet.begin();
EXPECT_EQ(it.interval(), Interval(10, 20));
EXPECT_EQ(it.value(), 42);
++it;
EXPECT_EQ(it, intervalSet.end());
// Test overlap detection
EXPECT_TRUE(intervalSet.hasOverlap({ 15, 25 })); // Overlaps
EXPECT_TRUE(intervalSet.hasOverlap({ 5, 15 })); // Overlaps
EXPECT_TRUE(intervalSet.hasOverlap({ 10, 20 })); // Exact match
EXPECT_FALSE(intervalSet.hasOverlap({ 0, 10 })); // No overlap (adjacent)
EXPECT_FALSE(intervalSet.hasOverlap({ 20, 30 })); // No overlap (adjacent)
EXPECT_FALSE(intervalSet.hasOverlap({ 0, 5 })); // No overlap (before)
EXPECT_FALSE(intervalSet.hasOverlap({ 25, 30 })); // No overlap (after)
// Test find
auto result = intervalSet.find({ 15, 16 });
EXPECT_TRUE(result);
EXPECT_EQ(result->first, Interval(10, 20));
EXPECT_EQ(result->second, 42);
// Test find with non-overlapping interval
EXPECT_FALSE(intervalSet.find({ 0, 5 }));
// Test erase functionality
intervalSet.erase({ 10, 20 });
EXPECT_EQ(intervalSet.begin(), intervalSet.end());
// After erase, all overlap checks should return false
EXPECT_FALSE(intervalSet.hasOverlap({ 15, 25 })); // No longer overlaps
EXPECT_FALSE(intervalSet.hasOverlap({ 5, 15 })); // No longer overlaps
EXPECT_FALSE(intervalSet.hasOverlap({ 10, 20 })); // No longer overlaps
EXPECT_FALSE(intervalSet.hasOverlap({ 0, 10 })); // Still no overlap
EXPECT_FALSE(intervalSet.hasOverlap({ 20, 30 })); // Still no overlap
EXPECT_FALSE(intervalSet.hasOverlap({ 0, 5 })); // Still no overlap
EXPECT_FALSE(intervalSet.hasOverlap({ 25, 30 })); // Still no overlap
// After erase, all find operations should return nullopt
EXPECT_FALSE(intervalSet.find({ 15, 16 }));
EXPECT_FALSE(intervalSet.find({ 10, 20 }));
EXPECT_FALSE(intervalSet.find({ 0, 5 }));
}
TEST(WTF_IntervalSet, EraseTests)
{
IntervalSet<Point, Value> intervalSet;
// Test basic erase functionality
intervalSet.insert({ 10, 20 }, 100);
intervalSet.insert({ 30, 40 }, 200);
intervalSet.insert({ 50, 60 }, 300);
// Verify iterator traverses all three intervals
size_t count = 0;
for (auto it = intervalSet.begin(); it != intervalSet.end(); ++it)
count++;
EXPECT_EQ(count, 3u);
// Verify all intervals are present
EXPECT_FALSE(intervalSet.isEmpty());
EXPECT_TRUE(intervalSet.hasOverlap({ 10, 20 }));
EXPECT_TRUE(intervalSet.hasOverlap({ 30, 40 }));
EXPECT_TRUE(intervalSet.hasOverlap({ 50, 60 }));
// Erase middle interval
intervalSet.erase({ 30, 40 });
// Verify iterator now traverses only two intervals
count = 0;
for (auto it = intervalSet.begin(); it != intervalSet.end(); ++it)
count++;
EXPECT_EQ(count, 2u);
// Verify middle interval is gone, others remain
EXPECT_FALSE(intervalSet.isEmpty());
EXPECT_TRUE(intervalSet.hasOverlap({ 10, 20 }));
EXPECT_FALSE(intervalSet.hasOverlap({ 30, 40 }));
EXPECT_TRUE(intervalSet.hasOverlap({ 50, 60 }));
// Verify find operations
auto result = intervalSet.find({ 15, 16 });
EXPECT_TRUE(result);
EXPECT_EQ(result->first, Interval(10, 20));
EXPECT_EQ(result->second, 100);
EXPECT_FALSE(intervalSet.find({ 35, 36 }));
result = intervalSet.find({ 55, 56 });
EXPECT_TRUE(result);
EXPECT_EQ(result->first, Interval(50, 60));
EXPECT_EQ(result->second, 300);
// Erase first interval
intervalSet.erase({ 10, 20 });
// Verify iterator now traverses only one interval
count = 0;
for (auto it = intervalSet.begin(); it != intervalSet.end(); ++it)
count++;
EXPECT_EQ(count, 1u);
EXPECT_FALSE(intervalSet.isEmpty());
EXPECT_FALSE(intervalSet.hasOverlap({ 10, 20 }));
EXPECT_FALSE(intervalSet.hasOverlap({ 30, 40 }));
EXPECT_TRUE(intervalSet.hasOverlap({ 50, 60 }));
// Erase last interval (should make set empty)
intervalSet.erase({ 50, 60 });
// Verify iterator shows empty set
EXPECT_EQ(intervalSet.begin(), intervalSet.end());
EXPECT_TRUE(intervalSet.isEmpty());
EXPECT_FALSE(intervalSet.hasOverlap({ 10, 20 }));
EXPECT_FALSE(intervalSet.hasOverlap({ 30, 40 }));
EXPECT_FALSE(intervalSet.hasOverlap({ 50, 60 }));
// Verify all finds return nullopt on empty set
EXPECT_FALSE(intervalSet.find({ 15, 16 }));
EXPECT_FALSE(intervalSet.find({ 35, 36 }));
EXPECT_FALSE(intervalSet.find({ 55, 56 }));
}
TEST(WTF_IntervalSet, EdgeCases)
{
IntervalSet<Point, Value> intervalSet;
// Insert interval [0, 1) - single unit interval
intervalSet.insert({ 0, 1 }, 100);
EXPECT_TRUE(intervalSet.hasOverlap({ 0, 1 }));
EXPECT_FALSE(intervalSet.hasOverlap({ 1, 2 }));
auto result = intervalSet.find({ 0, 1 });
EXPECT_TRUE(result);
EXPECT_EQ(result->first, Interval(0, 1));
EXPECT_EQ(result->second, 100);
// Test with larger intervals that span the small one
EXPECT_TRUE(intervalSet.hasOverlap({ 0, 10 }));
result = intervalSet.find({ 0, 10 });
EXPECT_TRUE(result);
EXPECT_EQ(result->first, Interval(0, 1));
EXPECT_EQ(result->second, 100);
}
enum class IntervalOrdering {
Ascending,
Descending,
Random,
};
template<unsigned numCacheLines>
static void stressTest(IntervalOrdering ordering)
{
constexpr size_t numberTestIntervals = 10000;
constexpr size_t maxGap = 1000;
constexpr size_t maxSize = 1000;
constexpr size_t maxPoint = numberTestIntervals * (maxGap + maxSize);
struct TestCase : public std::pair<Interval, Value> {
TestCase() = default;
TestCase(const Interval& interval, const Value& value)
: std::pair<Interval, Value>(interval, value) { }
void dump(PrintStream& out) const
{
out.print("{ ", first, ", ", second, " }");
}
};
using TestIntervalSet = IntervalSet<Point, Value, numCacheLines>;
TestIntervalSet intervalSet;
std::mt19937 gen(testing::UnitTest::GetInstance()->random_seed());
std::uniform_int_distribution<size_t> gapDist(0, maxGap);
std::uniform_int_distribution<size_t> sizeDist(1, maxSize);
std::uniform_int_distribution<Value> valueDist(0, 10000);
// Generate non-overlapping intervals by sorting start points
Vector<TestCase> testData;
Point end = 0;
for (unsigned i = 0; i < numberTestIntervals; ++i) {
Point start = end + gapDist(gen);
end = start + sizeDist(gen);
Value value = valueDist(gen);
testData.append(TestCase({ start, end }, value));
}
dataLogLnIf(IntervalSetTest::verbose, "Test data: ", WTF::listDump(testData));
auto shuffledTestData = testData;
switch (ordering) {
case IntervalOrdering::Ascending:
break;
case IntervalOrdering::Descending:
std::reverse(shuffledTestData.begin(), shuffledTestData.end());
break;
case IntervalOrdering::Random:
// Shuffle the intervals to insert them in random order
std::shuffle(shuffledTestData.begin(), shuffledTestData.end(), gen);
break;
}
dataLogLnIf(IntervalSetTest::verbose, "After shuffle: ", WTF::listDump(shuffledTestData));
// Track which intervals are currently in the set for erase operations
Vector<TestCase> currentlyInserted;
std::uniform_int_distribution<int> eraseDist(1, 4); // 1 in 4 chance to erase
auto maybeEraseInterval = [&]() {
if (currentlyInserted.size() > 1 && eraseDist(gen) == 1) {
std::uniform_int_distribution<size_t> eraseIndexDist(0, currentlyInserted.size() - 1);
size_t eraseIndex = eraseIndexDist(gen);
TestCase toErase = currentlyInserted[eraseIndex];
intervalSet.erase(toErase.first);
currentlyInserted.removeAt(eraseIndex);
dataLogLnIf(IntervalSetTest::verbose, "Erased ", toErase.first, "=", toErase.second, ": ", intervalSet);
}
};
for (const auto& entry : shuffledTestData) {
intervalSet.insert(entry.first, entry.second);
currentlyInserted.append(entry);
dataLogLnIf(IntervalSetTest::verbose, "Added ", entry.first, "=", entry.second, ": ", intervalSet);
maybeEraseInterval();
}
// Validate that nodes are densely populated.
size_t capacity = TestIntervalSet::leafOrder;
unsigned expectedHeight = 0;
while (capacity < currentlyInserted.size()) {
capacity *= TestIntervalSet::innerOrder;
expectedHeight++;
}
EXPECT_EQ(intervalSet.height(), expectedHeight);
// Validate iterator traversal: count and ordering
size_t iteratorCount = 0;
Point lastEnd = 0;
for (auto it = intervalSet.begin(); it != intervalSet.end(); ++it) {
iteratorCount++;
auto interval = (*it).first;
// Verify intervals are in sorted order (non-overlapping by construction)
EXPECT_GE(interval.begin(), lastEnd);
lastEnd = interval.end();
}
EXPECT_EQ(iteratorCount, currentlyInserted.size());
// Test that all currently inserted intervals can be found with correct values
std::shuffle(currentlyInserted.begin(), currentlyInserted.end(), gen);
for (const auto& data : currentlyInserted) {
dataLogLnIf(IntervalSetTest::verbose, "Testing: interval=", data.first, " value=", data.second);
EXPECT_TRUE(intervalSet.hasOverlap(data.first));
auto found = intervalSet.find(data.first);
EXPECT_TRUE(found);
EXPECT_EQ(found->first, data.first);
EXPECT_EQ(found->second, data.second);
}
// Sort currentlyInserted by interval start for correct expected value calculation
std::ranges::sort(currentlyInserted, [](const TestCase& a, const TestCase& b) {
return a.first.begin() < b.first.begin();
});
std::uniform_int_distribution<size_t> pointDist(0, maxPoint);
// Test random queries with occasional erase operations
for (unsigned i = 0; i < 500; ++i) {
Point start = pointDist(gen);
Point end = start + sizeDist(gen);
Interval query = { start, end };
std::optional<std::pair<Interval, Value>> expected;
for (const auto& data : currentlyInserted) {
if (query.overlaps(data.first)) {
expected = std::make_pair(data.first, data.second);
break;
}
}
dataLogLnIf(IntervalSetTest::verbose, "Testing: random interval=", query);
EXPECT_EQ(expected.has_value(), intervalSet.hasOverlap(query));
auto found = intervalSet.find(query);
if (expected) {
EXPECT_TRUE(found);
EXPECT_EQ(found->first, expected->first);
EXPECT_EQ(found->second, expected->second);
} else
EXPECT_FALSE(found);
// Occasionally erase an interval during query phase (reduced frequency)
if (i % 2)
maybeEraseInterval();
}
}
static constexpr unsigned stressNumCacheLines = 2;
TEST(WTF_IntervalSet, AscendingStressTest)
{
stressTest<stressNumCacheLines>(IntervalOrdering::Ascending);
}
TEST(WTF_IntervalSet, DescendingStressTest)
{
stressTest<stressNumCacheLines>(IntervalOrdering::Descending);
}
TEST(WTF_IntervalSet, RandomStressTest)
{
stressTest<stressNumCacheLines>(IntervalOrdering::Random);
}
TEST(WTF_IntervalSet, Dump)
{
IntervalSet<int, const char*> intervalSet;
// Test empty tree
StringPrintStream emptyOutput;
intervalSet.dump(emptyOutput);
String emptyResult = emptyOutput.toString();
EXPECT_EQ(emptyResult, String("IntervalSet(height=0, leafOrder=4, innerOrder=4) <empty>"_s));
intervalSet.insert({ 10, 20 }, "first");
intervalSet.insert({ 30, 40 }, "second");
intervalSet.insert({ 50, 60 }, "third");
// Single leaf node
StringPrintStream basicOutput;
intervalSet.dump(basicOutput);
String basicResult = basicOutput.toString();
String expectedBasic = "IntervalSet(height=0, leafOrder=4, innerOrder=4) coverage=10...60\nLeaf(size=3): 10...20=first, 30...40=second, 50...60=third\n"_s;
EXPECT_EQ(basicResult, expectedBasic);
// Add more intervals to cause split
intervalSet.insert({ 5, 8 }, "before");
intervalSet.insert({ 25, 28 }, "middle");
intervalSet.insert({ 65, 70 }, "after");
StringPrintStream fullOutput;
intervalSet.dump(fullOutput);
String fullResult = fullOutput.toString();
String expectedFull = "IntervalSet(height=1, leafOrder=4, innerOrder=4) coverage=5...70\nInner(size=2, coverage=5...70):\n [0] 5...28\n Leaf(size=3): 5...8=before, 10...20=first, 25...28=middle\n [1] 30...70\n Leaf(size=3): 30...40=second, 50...60=third, 65...70=after\n"_s;
EXPECT_EQ(fullResult, expectedFull);
}
TEST(WTF_IntervalSet, DestructorMemoryManagement)
{
// Test destructor with single leaf node
{
IntervalSet<Point, Value> intervalSet;
intervalSet.insert({ 10, 20 }, 42);
intervalSet.insert({ 30, 40 }, 84);
}
// Test destructor with multi-level tree (force tree growth)
{
IntervalSet<Point, Value> intervalSet;
// Insert enough intervals to force tree growth beyond single leaf
for (Point i = 0; i < 100; ++i) {
Point start = i * 10;
Point end = start + 5;
intervalSet.insert({ start, end }, static_cast<Value>(i));
}
}
// Test destructor with empty tree
{
IntervalSet<Point, Value> intervalSet;
}
}
TEST(WTF_IntervalSet, EraseLastItemSingleLeaf)
{
IntervalSet<Point, Value> intervalSet;
// Test case: Tree with only a single leaf node, erase the last (and only) item
intervalSet.insert({ 10, 20 }, 42);
// Verify the interval is present
EXPECT_TRUE(intervalSet.hasOverlap({ 10, 20 }));
auto result = intervalSet.find({ 15, 16 });
EXPECT_TRUE(result);
EXPECT_EQ(result->first, Interval(10, 20));
EXPECT_EQ(result->second, 42);
// Erase the only interval - this should make the tree empty
intervalSet.erase({ 10, 20 });
// Verify the tree is now empty
EXPECT_FALSE(intervalSet.hasOverlap({ 10, 20 }));
EXPECT_FALSE(intervalSet.find({ 15, 16 }));
EXPECT_FALSE(intervalSet.find({ 0, 100 })); // Any query should return nullopt
// Test that we can still insert after emptying the tree
intervalSet.insert({ 30, 40 }, 100);
EXPECT_TRUE(intervalSet.hasOverlap({ 30, 40 }));
result = intervalSet.find({ 35, 36 });
EXPECT_TRUE(result);
EXPECT_EQ(result->first, Interval(30, 40));
EXPECT_EQ(result->second, 100);
}
TEST(WTF_IntervalSet, EraseLastItemWithInnerNodes)
{
IntervalSet<Point, Value> intervalSet;
// Build a tree with inner nodes by inserting many intervals
Vector<Interval> intervals;
for (Point i = 0; i < 50; ++i) {
Point start = i * 10;
Point end = start + 5;
Interval interval = { start, end };
intervals.append(interval);
intervalSet.insert(interval, static_cast<Value>(i));
}
// Verify we have a multi-level tree by checking all intervals are present
for (size_t i = 0; i < intervals.size(); ++i) {
EXPECT_TRUE(intervalSet.hasOverlap(intervals[i]));
auto result = intervalSet.find(intervals[i]);
EXPECT_TRUE(result);
EXPECT_EQ(result->first, intervals[i]);
EXPECT_EQ(result->second, static_cast<Value>(i));
}
// Erase all intervals one by one until only one remains
for (size_t i = 0; i < intervals.size() - 1; ++i) {
intervalSet.erase(intervals[i]);
// Verify the erased interval is gone
EXPECT_FALSE(intervalSet.hasOverlap(intervals[i]));
EXPECT_FALSE(intervalSet.find(intervals[i]));
// Verify remaining intervals are still present
for (size_t j = i + 1; j < intervals.size(); ++j)
EXPECT_TRUE(intervalSet.hasOverlap(intervals[j]));
}
// Now erase the very last interval - this should collapse the tree to empty
Interval lastInterval = intervals.last();
Value lastValue = static_cast<Value>(intervals.size() - 1);
// Verify the last interval is still present
EXPECT_TRUE(intervalSet.hasOverlap(lastInterval));
auto result = intervalSet.find(lastInterval);
EXPECT_TRUE(result);
EXPECT_EQ(result->first, lastInterval);
EXPECT_EQ(result->second, lastValue);
intervalSet.erase(lastInterval);
EXPECT_FALSE(intervalSet.hasOverlap(lastInterval));
EXPECT_FALSE(intervalSet.find(lastInterval));
EXPECT_FALSE(intervalSet.hasOverlap({ 0, 1000 }));
EXPECT_FALSE(intervalSet.find({ 0, 1000 }));
// Verify we can still insert after completely emptying a complex tree
intervalSet.insert({ 1000, 2000 }, 999);
EXPECT_TRUE(intervalSet.hasOverlap({ 1000, 2000 }));
result = intervalSet.find({ 1500, 1600 });
EXPECT_TRUE(result);
EXPECT_EQ(result->first, Interval(1000, 2000));
EXPECT_EQ(result->second, 999);
}
} // namespace TestWebKitAPI