blob: 2ed54d17d3a12958511eb7a3ead7775b0b751bb5 [file] [log] [blame] [edit]
//===-- Unittests for tsearch ---------------------------------------------===//
//
// 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 "hdr/types/posix_tnode.h"
#include "src/search/tdelete.h"
#include "src/search/tdestroy.h"
#include "src/search/tfind.h"
#include "src/search/tsearch.h"
#include "src/search/twalk.h"
#include "src/search/twalk_r.h"
#include "src/string/strcmp.h"
#include "src/string/strcpy.h"
#include "src/string/strlen.h"
#include "test/UnitTest/Test.h"
// ---------------------------------------------------------------------------
// Helpers: encode integers as void* keys (no heap allocation needed).
// ---------------------------------------------------------------------------
static void *encode(int val) {
return reinterpret_cast<void *>(static_cast<intptr_t>(val));
}
static int decode(const void *val) {
return static_cast<int>(reinterpret_cast<intptr_t>(val));
}
static int read_node(const __llvm_libc_tnode *node) {
return static_cast<int>(*static_cast<const intptr_t *>(node));
}
static int int_compare(const void *a, const void *b) {
int x = decode(a);
int y = decode(b);
return (x > y) - (x < y);
}
static void noop_free(void *) {}
// ===== tsearch tests =======================================================
TEST(LlvmLibcTSearchTest, InsertIntoEmptyTree) {
void *root = nullptr;
void *r = LIBC_NAMESPACE::tsearch(encode(42), &root, int_compare);
ASSERT_NE(r, nullptr);
ASSERT_EQ(read_node(r), 42);
LIBC_NAMESPACE::tdestroy(root, noop_free);
}
TEST(LlvmLibcTSearchTest, InsertMultiple) {
void *root = nullptr;
for (int i = 0; i < 8; ++i) {
void *r = LIBC_NAMESPACE::tsearch(encode(i), &root, int_compare);
ASSERT_NE(r, nullptr);
ASSERT_EQ(read_node(r), i);
}
LIBC_NAMESPACE::tdestroy(root, noop_free);
}
TEST(LlvmLibcTSearchTest, InsertDuplicateReturnsSameNode) {
void *root = nullptr;
void *first = LIBC_NAMESPACE::tsearch(encode(7), &root, int_compare);
ASSERT_NE(first, nullptr);
// Inserting the same key again must return the existing node.
void *second = LIBC_NAMESPACE::tsearch(encode(7), &root, int_compare);
ASSERT_EQ(first, second);
ASSERT_EQ(read_node(second), 7);
LIBC_NAMESPACE::tdestroy(root, noop_free);
}
// ===== tfind tests =========================================================
TEST(LlvmLibcTSearchTest, FindInEmptyTree) {
void *root = nullptr;
void *r = LIBC_NAMESPACE::tfind(encode(1), &root, int_compare);
ASSERT_EQ(r, nullptr);
}
TEST(LlvmLibcTSearchTest, FindExistingKey) {
void *root = nullptr;
LIBC_NAMESPACE::tsearch(encode(5), &root, int_compare);
LIBC_NAMESPACE::tsearch(encode(10), &root, int_compare);
LIBC_NAMESPACE::tsearch(encode(15), &root, int_compare);
void *r = LIBC_NAMESPACE::tfind(encode(10), &root, int_compare);
ASSERT_NE(r, nullptr);
ASSERT_EQ(read_node(r), 10);
LIBC_NAMESPACE::tdestroy(root, noop_free);
}
TEST(LlvmLibcTSearchTest, FindNonExistentKey) {
void *root = nullptr;
LIBC_NAMESPACE::tsearch(encode(1), &root, int_compare);
LIBC_NAMESPACE::tsearch(encode(3), &root, int_compare);
void *r = LIBC_NAMESPACE::tfind(encode(2), &root, int_compare);
ASSERT_EQ(r, nullptr); // not exist
LIBC_NAMESPACE::tdestroy(root, noop_free);
}
// ===== tdelete tests =======================================================
TEST(LlvmLibcTSearchTest, DeleteFromEmptyTree) {
void *root = nullptr;
void *r = LIBC_NAMESPACE::tdelete(encode(1), &root, int_compare);
ASSERT_EQ(r, nullptr);
}
TEST(LlvmLibcTSearchTest, DeleteNonExistentKey) {
void *root = nullptr;
LIBC_NAMESPACE::tsearch(encode(10), &root, int_compare);
void *r = LIBC_NAMESPACE::tdelete(encode(99), &root, int_compare);
ASSERT_EQ(r, nullptr);
// Original key must still be findable.
ASSERT_NE(LIBC_NAMESPACE::tfind(encode(10), &root, int_compare), nullptr);
LIBC_NAMESPACE::tdestroy(root, noop_free);
}
TEST(LlvmLibcTSearchTest, DeleteOnlyElement) {
void *root = nullptr;
LIBC_NAMESPACE::tsearch(encode(42), &root, int_compare);
void *r = LIBC_NAMESPACE::tdelete(encode(42), &root, int_compare);
// POSIX: on success tdelete returns a non-null value.
ASSERT_NE(r, nullptr);
// The tree is now empty.
ASSERT_EQ(root, nullptr);
}
TEST(LlvmLibcTSearchTest, DeleteLeaf) {
void *root = nullptr;
LIBC_NAMESPACE::tsearch(encode(10), &root, int_compare);
LIBC_NAMESPACE::tsearch(encode(5), &root, int_compare);
LIBC_NAMESPACE::tsearch(encode(15), &root, int_compare);
void *r = LIBC_NAMESPACE::tdelete(encode(5), &root, int_compare);
ASSERT_NE(r, nullptr);
ASSERT_EQ(LIBC_NAMESPACE::tfind(encode(5), &root, int_compare), nullptr);
// Siblings remain.
ASSERT_NE(LIBC_NAMESPACE::tfind(encode(10), &root, int_compare), nullptr);
ASSERT_NE(LIBC_NAMESPACE::tfind(encode(15), &root, int_compare), nullptr);
LIBC_NAMESPACE::tdestroy(root, noop_free);
}
TEST(LlvmLibcTSearchTest, DeleteInternalNode) {
void *root = nullptr;
// Build a small chain: 10 -> 5 -> 3
LIBC_NAMESPACE::tsearch(encode(10), &root, int_compare);
LIBC_NAMESPACE::tsearch(encode(5), &root, int_compare);
LIBC_NAMESPACE::tsearch(encode(3), &root, int_compare);
void *r = LIBC_NAMESPACE::tdelete(encode(5), &root, int_compare);
ASSERT_NE(r, nullptr);
ASSERT_EQ(LIBC_NAMESPACE::tfind(encode(5), &root, int_compare), nullptr);
ASSERT_NE(LIBC_NAMESPACE::tfind(encode(3), &root, int_compare), nullptr);
ASSERT_NE(LIBC_NAMESPACE::tfind(encode(10), &root, int_compare), nullptr);
LIBC_NAMESPACE::tdestroy(root, noop_free);
}
TEST(LlvmLibcTSearchTest, DeleteNodeWithTwoChildren) {
void *root = nullptr;
// 10
// / \
// 5 \
// /\ \
// 3 7 15
// For insertion process, see libc/test/src/__support/weak_avl_test.cpp:208
LIBC_NAMESPACE::tsearch(encode(10), &root, int_compare);
LIBC_NAMESPACE::tsearch(encode(5), &root, int_compare);
LIBC_NAMESPACE::tsearch(encode(15), &root, int_compare);
LIBC_NAMESPACE::tsearch(encode(3), &root, int_compare);
LIBC_NAMESPACE::tsearch(encode(7), &root, int_compare);
void *r = LIBC_NAMESPACE::tdelete(encode(5), &root, int_compare);
ASSERT_NE(r, nullptr);
ASSERT_EQ(LIBC_NAMESPACE::tfind(encode(5), &root, int_compare), nullptr);
// All other keys survive.
ASSERT_NE(LIBC_NAMESPACE::tfind(encode(3), &root, int_compare), nullptr);
ASSERT_NE(LIBC_NAMESPACE::tfind(encode(7), &root, int_compare), nullptr);
ASSERT_NE(LIBC_NAMESPACE::tfind(encode(10), &root, int_compare), nullptr);
ASSERT_NE(LIBC_NAMESPACE::tfind(encode(15), &root, int_compare), nullptr);
LIBC_NAMESPACE::tdestroy(root, noop_free);
}
// ===== twalk tests =========================================================
static int walk_sum;
static void sum_action(const __llvm_libc_tnode *node, VISIT visit, int) {
if (visit == leaf || visit == postorder)
walk_sum += read_node(node);
}
TEST(LlvmLibcTSearchTest, WalkEmptyTree) {
walk_sum = 0;
LIBC_NAMESPACE::twalk(nullptr, sum_action);
ASSERT_EQ(walk_sum, 0);
}
TEST(LlvmLibcTSearchTest, WalkSingleNode) {
void *root = nullptr;
LIBC_NAMESPACE::tsearch(encode(99), &root, int_compare);
walk_sum = 0;
LIBC_NAMESPACE::twalk(root, sum_action);
// A single node is a leaf, visited once.
ASSERT_EQ(walk_sum, 99);
LIBC_NAMESPACE::tdestroy(root, noop_free);
}
TEST(LlvmLibcTSearchTest, WalkSumsCorrectly) {
void *root = nullptr;
int expected = 0;
for (int i = 1; i <= 10; ++i) {
ASSERT_NE(LIBC_NAMESPACE::tsearch(encode(i), &root, int_compare), nullptr);
expected += i;
}
walk_sum = 0;
LIBC_NAMESPACE::twalk(root, sum_action);
ASSERT_EQ(walk_sum, expected);
LIBC_NAMESPACE::tdestroy(root, noop_free);
}
// Verify that twalk visits nodes in sorted (in-order) sequence.
static int walk_prev;
static bool walk_sorted;
static void sorted_action(const __llvm_libc_tnode *node, VISIT visit, int) {
if (visit == leaf || visit == postorder) {
int val = read_node(node);
if (val <= walk_prev)
walk_sorted = false;
walk_prev = val;
}
}
TEST(LlvmLibcTSearchTest, WalkInOrder) {
void *root = nullptr;
// Insert in pseudo-random order.
constexpr int N = 32;
for (int i = 0; i < N; ++i)
ASSERT_NE(
LIBC_NAMESPACE::tsearch(encode((i * 13 + 7) % N), &root, int_compare),
nullptr);
walk_prev = -1;
walk_sorted = true;
LIBC_NAMESPACE::twalk(root, sorted_action);
ASSERT_TRUE(walk_sorted);
LIBC_NAMESPACE::tdestroy(root, noop_free);
}
// Verify twalk depth parameter.
static int walk_max_depth;
static int walk_node_count;
static void depth_action(const __llvm_libc_tnode *, VISIT visit, int depth) {
if (visit == leaf || visit == postorder) {
++walk_node_count;
if (depth > walk_max_depth)
walk_max_depth = depth;
}
}
TEST(LlvmLibcTSearchTest, WalkReportsDepth) {
void *root = nullptr;
for (int i = 1; i <= 15; ++i)
ASSERT_NE(LIBC_NAMESPACE::tsearch(encode(i), &root, int_compare), nullptr);
walk_max_depth = -1;
walk_node_count = 0;
LIBC_NAMESPACE::twalk(root, depth_action);
ASSERT_EQ(walk_node_count, 15);
// The maximum depth must be positive for a multi-node tree.
ASSERT_GT(walk_max_depth, 0);
LIBC_NAMESPACE::tdestroy(root, noop_free);
}
// ===== twalk_r tests =======================================================
static void collect_action(const __llvm_libc_tnode *node, VISIT visit,
void *closure) {
if (visit == leaf || visit == postorder) {
int **cursor = static_cast<int **>(closure);
**cursor = read_node(node);
*cursor += 1;
}
}
TEST(LlvmLibcTSearchTest, WalkRProducesSortedOutput) {
void *root = nullptr;
constexpr int N = 64;
for (int i = 0; i < N; ++i)
ASSERT_NE(LIBC_NAMESPACE::tsearch(encode((i * 37) % N), &root, int_compare),
nullptr);
int sorted[N];
int *cursor = sorted;
LIBC_NAMESPACE::twalk_r(root, collect_action, &cursor);
for (int i = 0; i < N; ++i)
ASSERT_EQ(sorted[i], i);
LIBC_NAMESPACE::tdestroy(root, noop_free);
}
TEST(LlvmLibcTSearchTest, WalkREmptyTree) {
int count = 0;
auto counter = [](const __llvm_libc_tnode *, VISIT, void *c) {
++*static_cast<int *>(c);
};
LIBC_NAMESPACE::twalk_r(nullptr, counter, &count);
ASSERT_EQ(count, 0);
}
// ===== tdestroy tests ======================================================
constexpr size_t MAX_VALUE = 64;
static bool free_flags[MAX_VALUE + 1];
static void clear_free_flags() {
for (bool &flag : free_flags)
flag = false;
}
static void flag_free(void *node) {
int key = decode(node);
free_flags[key] = true;
}
TEST(LlvmLibcTSearchTest, DestroyNullTree) {
// tdestroy on a null root must not crash.
LIBC_NAMESPACE::tdestroy(nullptr, flag_free);
}
TEST(LlvmLibcTSearchTest, DestroyVisitsAllNodes) {
void *root = nullptr;
clear_free_flags();
constexpr int N = 10;
for (int i = 0; i < N; ++i)
ASSERT_NE(LIBC_NAMESPACE::tsearch(encode(i), &root, int_compare), nullptr);
LIBC_NAMESPACE::tdestroy(root, flag_free);
for (int i = 0; i < N; ++i)
ASSERT_TRUE(free_flags[i]);
}
// ---------------------------------------------------------------------------
// tdestroy with a free-function that has real semantics:
// nodes hold heap-allocated C strings; the free-function calls free() on them.
// ---------------------------------------------------------------------------
static int string_compare(const void *a, const void *b) {
return LIBC_NAMESPACE::strcmp(static_cast<const char *>(a),
static_cast<const char *>(b));
}
static int strings_freed;
static void string_free(void *ptr) {
++strings_freed;
delete[] (static_cast<char *>(ptr));
}
TEST(LlvmLibcTSearchTest, DestroyHeapStrings) {
void *root = nullptr;
// Include deliberate duplicates to exercise the tsearch "already exists"
// path.
const char *words[] = {"cherry", "apple", "banana", "date",
"elderberry", "apple", "cherry"};
constexpr int NWORDS = sizeof(words) / sizeof(words[0]);
constexpr int UNIQUE_WORDS = 5; // number of distinct strings
int duplicates_detected = 0;
for (int i = 0; i < NWORDS; ++i) {
char *dup = new char[LIBC_NAMESPACE::strlen(words[i]) + 1];
ASSERT_NE(dup, nullptr);
LIBC_NAMESPACE::strcpy(dup, words[i]);
void *r = LIBC_NAMESPACE::tsearch(dup, &root, string_compare);
ASSERT_NE(r, nullptr);
// tsearch returns a pointer to the node's datum. If the stored key
// is not our newly allocated string, a pre-existing node was returned
// and we must free the unused duplicate.
if (*static_cast<void **>(r) != dup) {
++duplicates_detected;
delete[] dup; // match new[] with delete[]
}
}
ASSERT_EQ(duplicates_detected, NWORDS - UNIQUE_WORDS);
// Every unique word should be findable.
const char *unique[] = {"cherry", "apple", "banana", "date", "elderberry"};
for (int i = 0; i < UNIQUE_WORDS; ++i) {
void *r = LIBC_NAMESPACE::tfind(unique[i], &root, string_compare);
ASSERT_NE(r, nullptr);
}
strings_freed = 0;
LIBC_NAMESPACE::tdestroy(root, string_free);
ASSERT_EQ(strings_freed, UNIQUE_WORDS);
}
// ---------------------------------------------------------------------------
// tdestroy via heap-allocated int boxes – demonstrates real resource cleanup
// with proper duplicate handling.
// ---------------------------------------------------------------------------
static int boxed_int_compare(const void *a, const void *b) {
int x = *static_cast<const int *>(a);
int y = *static_cast<const int *>(b);
return (x > y) - (x < y);
}
static int int_box_freed;
static void int_box_free(void *ptr) {
++int_box_freed;
delete static_cast<int *>(ptr);
}
TEST(LlvmLibcTSearchTest, DestroyIntBoxes) {
int_box_freed = 0;
void *root = nullptr;
constexpr int N = 8;
// Insert 0..N-1, each as a heap-allocated int.
for (int i = 0; i < N; ++i) {
int *p = new int(i);
ASSERT_NE(p, nullptr);
void *r = LIBC_NAMESPACE::tsearch(p, &root, boxed_int_compare);
ASSERT_NE(r, nullptr);
// All keys are unique here, so the stored key must be ours.
ASSERT_EQ(*static_cast<void **>(r), static_cast<void *>(p));
}
// Now re-insert some duplicates; the caller must free the rejected key.
int duplicates_detected = 0;
for (int i = 0; i < N; i += 2) {
int *p = new int(i);
void *r = LIBC_NAMESPACE::tsearch(p, &root, boxed_int_compare);
ASSERT_NE(r, nullptr);
if (*static_cast<void **>(r) != static_cast<void *>(p)) {
++duplicates_detected;
delete p; // match new with delete
}
}
ASSERT_EQ(duplicates_detected, N / 2);
// Destroy the tree; int_box_free must be called exactly N times
// (once per unique key that was actually stored).
LIBC_NAMESPACE::tdestroy(root, int_box_free);
ASSERT_EQ(int_box_freed, N);
}