blob: 23b65c84c05c5e41d5b997e04dc9d40c883f3a74 [file] [edit]
//===- EytzingerTest.cpp - EytzingerTableSpan unit tests ------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "llvm/ADT/Eytzinger.h"
#include "llvm/Support/Endian.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
using namespace llvm;
namespace {
TEST(EytzingerTest, EmptyTable) {
// Default constructed table span should be empty and return nullopt.
EytzingerTableSpan<int> Empty;
EXPECT_TRUE(Empty.empty());
EXPECT_EQ(Empty.size(), 0u);
EXPECT_EQ(Empty.data(), nullptr);
EXPECT_EQ(Empty.begin(), Empty.end());
EXPECT_EQ(Empty.begin(), nullptr);
EXPECT_TRUE(Empty.isSorted());
EXPECT_EQ(Empty.findIndex(42), std::nullopt);
EXPECT_FALSE(Empty.contains(42));
// Span initialized with nullptr and zero size should behave identically.
EytzingerTableSpan<int> NullSpan(nullptr, 0);
EXPECT_TRUE(NullSpan.empty());
EXPECT_EQ(NullSpan.size(), 0u);
EXPECT_EQ(NullSpan.begin(), NullSpan.end());
EXPECT_TRUE(NullSpan.isSorted());
EXPECT_EQ(NullSpan.findIndex(42), std::nullopt);
EXPECT_FALSE(NullSpan.contains(42));
}
TEST(EytzingerTest, SingleElementTable) {
// Table with a single element at index 0.
const int Data[] = {100};
EytzingerTableSpan<int> Span(Data, 1);
EXPECT_FALSE(Span.empty());
EXPECT_EQ(Span.size(), 1u);
EXPECT_TRUE(Span.isSorted());
EXPECT_EQ(Span[0], 100);
// Successful lookup for the single element.
EXPECT_EQ(Span.findIndex(100), 0u);
EXPECT_TRUE(Span.contains(100));
// Unsuccessful lookups for keys smaller and larger than the element.
EXPECT_EQ(Span.findIndex(99), std::nullopt);
EXPECT_FALSE(Span.contains(99));
EXPECT_EQ(Span.findIndex(101), std::nullopt);
EXPECT_FALSE(Span.contains(101));
}
TEST(EytzingerTest, BinaryTreeWithSevenElements) {
// Binary tree of 7 elements (3 full levels).
// In Eytzinger layout (breadth-first order of complete BST):
//
// 40
// / \
// 20 60
// / \ / \
// 10 30 50 70
const int Data[] = {40, 20, 60, 10, 30, 50, 70};
EytzingerTableSpan<int> Span(Data, 7);
EXPECT_EQ(Span.size(), 7u);
EXPECT_TRUE(Span.isSorted());
// Verify successful lookups for every node in the tree.
EXPECT_EQ(Span.findIndex(40), 0u);
EXPECT_TRUE(Span.contains(40));
EXPECT_EQ(Span.findIndex(20), 1u);
EXPECT_TRUE(Span.contains(20));
EXPECT_EQ(Span.findIndex(60), 2u);
EXPECT_TRUE(Span.contains(60));
EXPECT_EQ(Span.findIndex(10), 3u);
EXPECT_TRUE(Span.contains(10));
EXPECT_EQ(Span.findIndex(30), 4u);
EXPECT_TRUE(Span.contains(30));
EXPECT_EQ(Span.findIndex(50), 5u);
EXPECT_TRUE(Span.contains(50));
EXPECT_EQ(Span.findIndex(70), 6u);
EXPECT_TRUE(Span.contains(70));
// Verify unsuccessful lookups for values not in the tree.
EXPECT_EQ(Span.findIndex(0), std::nullopt);
EXPECT_FALSE(Span.contains(0));
EXPECT_EQ(Span.findIndex(15), std::nullopt);
EXPECT_FALSE(Span.contains(15));
EXPECT_EQ(Span.findIndex(25), std::nullopt);
EXPECT_FALSE(Span.contains(25));
EXPECT_EQ(Span.findIndex(35), std::nullopt);
EXPECT_FALSE(Span.contains(35));
EXPECT_EQ(Span.findIndex(45), std::nullopt);
EXPECT_FALSE(Span.contains(45));
EXPECT_EQ(Span.findIndex(55), std::nullopt);
EXPECT_FALSE(Span.contains(55));
EXPECT_EQ(Span.findIndex(65), std::nullopt);
EXPECT_FALSE(Span.contains(65));
EXPECT_EQ(Span.findIndex(80), std::nullopt);
EXPECT_FALSE(Span.contains(80));
}
TEST(EytzingerTest, BinaryTreeWithFiveElements) {
// Binary tree of 5 elements (non-power-of-two minus one).
// In Eytzinger layout:
//
// 40
// / \
// 20 50
// / \
// 10 30
const int Data[] = {40, 20, 50, 10, 30};
EytzingerTableSpan<int> Span(Data, 5);
EXPECT_EQ(Span.size(), 5u);
EXPECT_TRUE(Span.isSorted());
// Verify lookups on existing elements.
EXPECT_EQ(Span.findIndex(40), 0u);
EXPECT_TRUE(Span.contains(40));
EXPECT_EQ(Span.findIndex(20), 1u);
EXPECT_TRUE(Span.contains(20));
EXPECT_EQ(Span.findIndex(50), 2u);
EXPECT_TRUE(Span.contains(50));
EXPECT_EQ(Span.findIndex(10), 3u);
EXPECT_TRUE(Span.contains(10));
EXPECT_EQ(Span.findIndex(30), 4u);
EXPECT_TRUE(Span.contains(30));
// Verify lookups on missing values across various boundary conditions.
EXPECT_EQ(Span.findIndex(5), std::nullopt);
EXPECT_FALSE(Span.contains(5));
EXPECT_EQ(Span.findIndex(15), std::nullopt);
EXPECT_FALSE(Span.contains(15));
EXPECT_EQ(Span.findIndex(25), std::nullopt);
EXPECT_FALSE(Span.contains(25));
EXPECT_EQ(Span.findIndex(35), std::nullopt);
EXPECT_FALSE(Span.contains(35));
EXPECT_EQ(Span.findIndex(45), std::nullopt);
EXPECT_FALSE(Span.contains(45));
EXPECT_EQ(Span.findIndex(60), std::nullopt);
EXPECT_FALSE(Span.contains(60));
}
TEST(EytzingerTest, EndianSpecificIntegerType) {
// Verify compatibility with LLVM endian-specific wrapper types such as
// support::ulittle64_t which are commonly used in binary profile formats.
const support::ulittle64_t Data[] = {
support::ulittle64_t(400), support::ulittle64_t(200),
support::ulittle64_t(600), support::ulittle64_t(100),
support::ulittle64_t(300), support::ulittle64_t(500),
support::ulittle64_t(700)};
EytzingerTableSpan<support::ulittle64_t> Span(Data, 7);
EXPECT_TRUE(Span.isSorted());
EXPECT_EQ(Span.findIndex(uint64_t(400)), 0u);
EXPECT_TRUE(Span.contains(uint64_t(400)));
EXPECT_EQ(Span.findIndex(uint64_t(200)), 1u);
EXPECT_TRUE(Span.contains(uint64_t(200)));
EXPECT_EQ(Span.findIndex(uint64_t(600)), 2u);
EXPECT_TRUE(Span.contains(uint64_t(600)));
EXPECT_EQ(Span.findIndex(uint64_t(100)), 3u);
EXPECT_TRUE(Span.contains(uint64_t(100)));
EXPECT_EQ(Span.findIndex(uint64_t(300)), 4u);
EXPECT_TRUE(Span.contains(uint64_t(300)));
EXPECT_EQ(Span.findIndex(uint64_t(500)), 5u);
EXPECT_TRUE(Span.contains(uint64_t(500)));
EXPECT_EQ(Span.findIndex(uint64_t(700)), 6u);
EXPECT_TRUE(Span.contains(uint64_t(700)));
EXPECT_EQ(Span.findIndex(uint64_t(999)), std::nullopt);
EXPECT_FALSE(Span.contains(uint64_t(999)));
}
TEST(EytzingerTest, IsSortedVerification) {
// Verify detection of local parent-child violations.
// Root (40), left child (60 > 40), right child (20 < 40).
const int InvalidChildOrder[] = {40, 60, 20};
EXPECT_FALSE(EytzingerTableSpan<int>(InvalidChildOrder, 3).isSorted());
// Verify detection of across-level ancestor bounds violations.
// Root (40), left (20), right (60). Left of 20 is 10, right of 20 is 50.
// Although 50 > 20 (local parent check passes), 50 > 40 violates the root
// bound.
const int AncestorViolation[] = {40, 20, 60, 10, 50, 55, 70};
EXPECT_FALSE(EytzingerTableSpan<int>(AncestorViolation, 7).isSorted());
// Verify that tables with duplicate values are flagged as unsorted because
// EytzingerTableSpan requires strictly ascending in-order keys.
const int Duplicates[] = {30, 30, 30};
EXPECT_FALSE(EytzingerTableSpan<int>(Duplicates, 3).isSorted());
}
TEST(EytzingerTest, EytzingerTableCreateAndLookup) {
// Construct table from an unsorted vector with duplicate keys for a non-full
// bottom level (10 unique elements, size != 2^k - 1).
std::vector<int> Unsorted = {70, 20, 40, 10, 60, 30, 50,
80, 90, 40, 100, 20, 10};
auto Table = EytzingerTable<int>::create(std::move(Unsorted));
// Should contain exactly 10 unique elements after deduplication.
EXPECT_EQ(Table.size(), 10u);
EXPECT_FALSE(Table.empty());
EXPECT_TRUE(Table.isSorted());
// Root of 10-element complete BST is 70.
EXPECT_EQ(Table[0], 70);
EXPECT_EQ(Table.findIndex(70), 0u);
EXPECT_TRUE(Table.contains(70));
// Verify successful lookups for all unique keys explicitly so line numbers
// identify failures.
EXPECT_TRUE(Table.contains(10));
EXPECT_NE(Table.findIndex(10), std::nullopt);
EXPECT_TRUE(Table.contains(20));
EXPECT_NE(Table.findIndex(20), std::nullopt);
EXPECT_TRUE(Table.contains(30));
EXPECT_NE(Table.findIndex(30), std::nullopt);
EXPECT_TRUE(Table.contains(40));
EXPECT_NE(Table.findIndex(40), std::nullopt);
EXPECT_TRUE(Table.contains(50));
EXPECT_NE(Table.findIndex(50), std::nullopt);
EXPECT_TRUE(Table.contains(60));
EXPECT_NE(Table.findIndex(60), std::nullopt);
EXPECT_TRUE(Table.contains(70));
EXPECT_NE(Table.findIndex(70), std::nullopt);
EXPECT_TRUE(Table.contains(80));
EXPECT_NE(Table.findIndex(80), std::nullopt);
EXPECT_TRUE(Table.contains(90));
EXPECT_NE(Table.findIndex(90), std::nullopt);
EXPECT_TRUE(Table.contains(100));
EXPECT_NE(Table.findIndex(100), std::nullopt);
// Verify missing keys across various bounds and gaps.
EXPECT_FALSE(Table.contains(0));
EXPECT_EQ(Table.findIndex(0), std::nullopt);
EXPECT_FALSE(Table.contains(15));
EXPECT_EQ(Table.findIndex(15), std::nullopt);
EXPECT_FALSE(Table.contains(75));
EXPECT_EQ(Table.findIndex(75), std::nullopt);
EXPECT_FALSE(Table.contains(110));
EXPECT_EQ(Table.findIndex(110), std::nullopt);
}
TEST(EytzingerTest, EytzingerTableEmptyAndSingle) {
auto EmptyTable = EytzingerTable<int>::create(std::vector<int>{});
EXPECT_TRUE(EmptyTable.empty());
EXPECT_EQ(EmptyTable.size(), 0u);
EXPECT_EQ(EmptyTable.begin(), EmptyTable.end());
EXPECT_TRUE(EmptyTable.isSorted());
EXPECT_FALSE(EmptyTable.contains(42));
auto SingleTable = EytzingerTable<int>::create(std::vector<int>{42, 42});
EXPECT_FALSE(SingleTable.empty());
EXPECT_EQ(SingleTable.size(), 1u);
EXPECT_NE(SingleTable.begin(), SingleTable.end());
EXPECT_EQ(*SingleTable.begin(), 42);
EXPECT_TRUE(SingleTable.isSorted());
EXPECT_EQ(SingleTable[0], 42);
EXPECT_TRUE(SingleTable.contains(42));
EXPECT_EQ(SingleTable.findIndex(42), 0u);
}
TEST(EytzingerTest, EytzingerTableEndianSpecific) {
std::vector<support::ulittle64_t> Input = {
support::ulittle64_t(500), support::ulittle64_t(100),
support::ulittle64_t(300), support::ulittle64_t(300)};
auto Table = EytzingerTable<support::ulittle64_t>::create(Input);
EXPECT_EQ(Table.size(), 3u);
EXPECT_TRUE(Table.isSorted());
EXPECT_TRUE(Table.contains(uint64_t(300)));
EXPECT_EQ(Table.findIndex(uint64_t(300)), 0u);
EXPECT_FALSE(Table.contains(uint64_t(999)));
}
TEST(EytzingerTest, EytzingerTableHeterogeneousCreate) {
// Construct EytzingerTable<support::ulittle64_t> directly from a vector of
// native uint64_t keys with an imperfect tree size (6 elements).
std::vector<uint64_t> NativeKeys = {500ULL, 100ULL, 300ULL, 600ULL,
200ULL, 400ULL, 300ULL};
auto Table = EytzingerTable<support::ulittle64_t>::create(NativeKeys);
EXPECT_EQ(Table.size(), 6u);
EXPECT_TRUE(Table.isSorted());
// Root of 6-element complete BST is 400.
EXPECT_EQ(uint64_t(Table[0]), 400ULL);
EXPECT_EQ(Table.findIndex(uint64_t(400ULL)), 0u);
EXPECT_TRUE(Table.contains(uint64_t(100ULL)));
EXPECT_TRUE(Table.contains(uint64_t(200ULL)));
EXPECT_TRUE(Table.contains(uint64_t(300ULL)));
EXPECT_TRUE(Table.contains(uint64_t(400ULL)));
EXPECT_TRUE(Table.contains(uint64_t(500ULL)));
EXPECT_TRUE(Table.contains(uint64_t(600ULL)));
EXPECT_FALSE(Table.contains(uint64_t(999ULL)));
}
TEST(EytzingerTest, EytzingerTableSpanBeginEnd) {
const int Data[] = {40, 20, 60, 10, 30, 50, 70};
EytzingerTableSpan<int> Span(Data, 7);
EXPECT_EQ(Span.begin(), Data);
EXPECT_EQ(Span.end(), Data + 7);
EXPECT_EQ(std::distance(Span.begin(), Span.end()), 7);
std::vector<int> Traversed(Span.begin(), Span.end());
const int Expected[] = {40, 20, 60, 10, 30, 50, 70};
EXPECT_THAT(Traversed, testing::ElementsAreArray(Expected));
// Range-based for loop verification.
std::vector<int> RangeTraversed;
for (const int &Val : Span)
RangeTraversed.push_back(Val);
EXPECT_THAT(RangeTraversed, testing::ElementsAreArray(Expected));
}
TEST(EytzingerTest, EytzingerTableBeginEnd) {
std::vector<int> Unsorted = {70, 20, 40, 10, 60, 30, 50};
auto Table = EytzingerTable<int>::create(std::move(Unsorted));
EXPECT_EQ(std::distance(Table.begin(), Table.end()), 7);
std::vector<int> Traversed(Table.begin(), Table.end());
const int Expected[] = {40, 20, 60, 10, 30, 50, 70};
EXPECT_THAT(Traversed, testing::ElementsAreArray(Expected));
// Range-based for loop verification on both non-const and const tables.
std::vector<int> RangeTraversed;
for (const int &Val : Table)
RangeTraversed.push_back(Val);
EXPECT_THAT(RangeTraversed, testing::ElementsAreArray(Expected));
const auto &ConstTable = Table;
std::vector<int> ConstTraversed(ConstTable.begin(), ConstTable.end());
EXPECT_THAT(ConstTraversed, testing::ElementsAreArray(Expected));
}
} // namespace