//===-- 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(static_cast(val)); } static int decode(const void *val) { return static_cast(reinterpret_cast(val)); } static int read_node(const __llvm_libc_tnode *node) { return static_cast(*static_cast(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(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(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(a), static_cast(b)); } static int strings_freed; static void string_free(void *ptr) { ++strings_freed; delete[] (static_cast(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(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(a); int y = *static_cast(b); return (x > y) - (x < y); } static int int_box_freed; static void int_box_free(void *ptr) { ++int_box_freed; delete static_cast(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(r), static_cast(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(r) != static_cast(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); }