| //===-- 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); | |
| } |