/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include #include #include "ds/AvlTree.h" #include "jsapi-tests/tests.h" using namespace js; //////////////////////////////////////////////////////////////////////// // // // AvlTree testing interface. // // // //////////////////////////////////////////////////////////////////////// // The "standard" AVL interface, `class AvlTree` at the end of // js/src/ds/AvlTree.h, is too restrictive to allow proper testing of the AVL // tree internals. In particular it disallows insertion of duplicate values // and removal of non-present values, and lacks various secondary functions // such as for counting the number of nodes. // // So, for testing, we wrap an alternative interface `AvlTreeTestIF` around // the core implementation. template class AvlTreeTestIF : public AvlTreeImpl { // Shorthands for names in the implementation (parent) class. using Impl = AvlTreeImpl; using ImplTag = typename AvlTreeImpl::Tag; using ImplNode = typename AvlTreeImpl::Node; using ImplResult = typename AvlTreeImpl::Result; using ImplNodeAndResult = typename AvlTreeImpl::NodeAndResult; public: explicit AvlTreeTestIF(LifoAlloc* alloc = nullptr) : Impl(alloc) {} // Insert `v` if it isn't already there, else leave the tree unchanged. // Returns true iff an insertion happened. bool testInsert(const T& v) { ImplNode* new_root = Impl::insert_worker(v); if (!new_root) { // OOM MOZ_CRASH(); } if (uintptr_t(new_root) == uintptr_t(1)) { // Already present return false; } Impl::root_ = new_root; return true; } // Remove `v` if it is present. Returns true iff a removal happened. bool testRemove(const T& v) { ImplNodeAndResult pair = Impl::delete_worker(Impl::root_, v); ImplNode* new_root = pair.first; ImplResult res = pair.second; if (res == ImplResult::Error) { // `v` isn't in the tree. return false; } else { Impl::root_ = new_root; return true; } } // Count number of elements size_t testSize_worker(ImplNode* n) const { if (n) { return 1 + testSize_worker(n->left) + testSize_worker(n->right); } return 0; } size_t testSize() const { return testSize_worker(Impl::root_); } size_t testDepth_worker(ImplNode* n) const { if (n) { size_t depthL = testDepth_worker(n->left); size_t depthR = testDepth_worker(n->right); return 1 + (depthL > depthR ? depthL : depthR); } return 0; } size_t testDepth() const { return testDepth_worker(Impl::root_); } bool testContains(const T& v) const { ImplNode* node = Impl::find_worker(v); return node != nullptr; } ImplNode* testGetRoot() const { return Impl::root_; } ImplNode* testGetFreeList() const { return Impl::freeList_; } bool testFreeListLooksValid(size_t maxElems) { size_t numElems = 0; ImplNode* node = Impl::freeList_; while (node) { numElems++; if (numElems > maxElems) { return false; } if (node->tag != ImplTag::Free || node->right != nullptr) { return false; } node = node->left; } return true; } // For debugging only private: void testShow_worker(int depth, const ImplNode* node) const { if (node) { testShow_worker(depth + 1, node->right); for (int i = 0; i < depth; i++) { printf(" "); } char* str = node->item.show(); printf("%s\n", str); free(str); testShow_worker(depth + 1, node->left); } } public: // For debugging only void testShow() const { testShow_worker(0, Impl::root_); } // AvlTree::Iter is also public; it's just pass-through from AvlTreeImpl. }; //////////////////////////////////////////////////////////////////////// // // // AvlTree test cases. // // // //////////////////////////////////////////////////////////////////////// class CmpInt { int x_; public: explicit CmpInt(int x) : x_(x) {} ~CmpInt() {} static int compare(const CmpInt& me, const CmpInt& other) { if (me.x_ < other.x_) return -1; if (me.x_ > other.x_) return 1; return 0; } int get() const { return x_; } char* show() const { const size_t length = 16; char* str = (char*)calloc(length, 1); snprintf(str, length, "%d", x_); return str; } }; bool TreeIsPlausible(const AvlTreeTestIF& tree, const std::set& should_be_in_tree, int UNIV_MIN, int UNIV_MAX) { // Same cardinality size_t n_in_set = should_be_in_tree.size(); size_t n_in_tree = tree.testSize(); if (n_in_set != n_in_tree) { return false; } // Tree is not wildly out of balance. Depth should not exceed 1.44 * // log2(size). size_t tree_depth = tree.testDepth(); size_t log2_size = 0; { size_t n = n_in_tree; while (n > 0) { n = n >> 1; log2_size += 1; } } // Actually a tighter limit than stated above. For these test cases, the // tree is either perfectly balanced or within one level of being so (hence // the +1). if (tree_depth > log2_size + 1) { return false; } // Check that everything that should be in the tree is in it, and vice // versa. for (int i = UNIV_MIN; i < UNIV_MAX; i++) { bool should_be_in = should_be_in_tree.find(i) != should_be_in_tree.end(); // Look it up with a null comparator (so `contains` compares // directly) bool is_in = tree.testContains(CmpInt(i)); if (is_in != should_be_in) { return false; } } return true; } template bool SetContains(std::set s, const T& v) { return s.find(v) != s.end(); } BEGIN_TEST(testAvlTree_main) { static const int UNIV_MIN = 5000; static const int UNIV_MAX = 5999; static const int UNIV_SIZE = UNIV_MAX - UNIV_MIN + 1; LifoAlloc alloc(4096); AvlTreeTestIF tree(&alloc); std::set should_be_in_tree; // Add numbers to the tree, checking as we go. for (int i = UNIV_MIN; i < UNIV_MAX; i++) { // Idiotic but simple if (i % 3 != 0) { continue; } bool was_added = tree.testInsert(CmpInt(i)); should_be_in_tree.insert(i); CHECK(was_added); CHECK(TreeIsPlausible(tree, should_be_in_tree, UNIV_MIN, UNIV_MAX)); } // Then remove the middle half of the tree, also checking. for (int i = UNIV_MIN + UNIV_SIZE / 4; i < UNIV_MIN + 3 * (UNIV_SIZE / 4); i++) { // Note that here, we're asking to delete a bunch of numbers that aren't // in the tree. It should remain valid throughout. bool was_removed = tree.testRemove(CmpInt(i)); bool should_have_been_removed = SetContains(should_be_in_tree, i); CHECK(was_removed == should_have_been_removed); should_be_in_tree.erase(i); CHECK(TreeIsPlausible(tree, should_be_in_tree, UNIV_MIN, UNIV_MAX)); } // Now add some numbers which are already in the tree. for (int i = UNIV_MIN; i < UNIV_MIN + UNIV_SIZE / 4; i++) { if (i % 3 != 0) { continue; } bool was_added = tree.testInsert(CmpInt(i)); bool should_have_been_added = !SetContains(should_be_in_tree, i); CHECK(was_added == should_have_been_added); should_be_in_tree.insert(i); CHECK(TreeIsPlausible(tree, should_be_in_tree, UNIV_MIN, UNIV_MAX)); } // Then remove all numbers from the tree, in reverse order. for (int ir = UNIV_MIN; ir < UNIV_MAX; ir++) { int i = UNIV_MIN + (UNIV_MAX - ir); bool was_removed = tree.testRemove(CmpInt(i)); bool should_have_been_removed = SetContains(should_be_in_tree, i); CHECK(was_removed == should_have_been_removed); should_be_in_tree.erase(i); CHECK(TreeIsPlausible(tree, should_be_in_tree, UNIV_MIN, UNIV_MAX)); } // Now the tree should be empty. CHECK(should_be_in_tree.empty()); CHECK(tree.testSize() == 0); // Now delete some more stuff. Tree should still be empty :-) for (int i = UNIV_MIN + 10; i < UNIV_MIN + 100; i++) { CHECK(should_be_in_tree.empty()); CHECK(tree.testSize() == 0); bool was_removed = tree.testRemove(CmpInt(i)); CHECK(!was_removed); CHECK(TreeIsPlausible(tree, should_be_in_tree, UNIV_MIN, UNIV_MAX)); } // The tree root should be NULL. CHECK(tree.testGetRoot() == nullptr); CHECK(tree.testGetFreeList() != nullptr); // Check the freelist to the extent we can: it's non-circular, and the // elements look plausible. If it's not shorter than the specified length // then it must have a cycle, since the tests above won't have resulted in // more than 400 free nodes at the end. CHECK(tree.testFreeListLooksValid(400 /*arbitrary*/)); // Test iteration, first in a tree with 9 nodes. This tests the general // case. { CHECK(tree.testSize() == 0); for (int i = 10; i < 100; i += 10) { bool was_inserted = tree.testInsert(CmpInt(i)); CHECK(was_inserted); } // Test iteration across the whole tree. AvlTreeTestIF::Iter iter(&tree); // `expect` produces (independently) the next expected number. `remaining` // counts the number items of items remaining. int expect = 10; int remaining = 9; while (iter.hasMore()) { CmpInt ci = iter.next(); CHECK(ci.get() == expect); expect += 10; remaining--; } CHECK(remaining == 0); // Test iteration from a specified start point. for (int i = 10; i < 100; i += 10) { for (int j = i - 1; j <= i + 1; j++) { // Set up `expect` and `remaining`. remaining = (100 - i) / 10; switch (j % 10) { case 0: expect = j; break; case 1: expect = j + 9; remaining--; break; case 9: expect = j + 1; break; default: MOZ_CRASH(); } AvlTreeTestIF::Iter iter(&tree, CmpInt(j)); while (iter.hasMore()) { CmpInt ci = iter.next(); CHECK(ci.get() == expect); expect += 10; remaining--; } CHECK(remaining == 0); } } } // Now with a completely empty tree. { AvlTreeTestIF emptyTree(&alloc); CHECK(emptyTree.testSize() == 0); // Full tree iteration gets us nothing. AvlTreeTestIF::Iter iter1(&emptyTree); CHECK(!iter1.hasMore()); // Starting iteration with any number gets us nothing. AvlTreeTestIF::Iter iter2(&emptyTree, CmpInt(42)); CHECK(!iter2.hasMore()); } // Finally with a one-element tree. { AvlTreeTestIF unitTree(&alloc); bool was_inserted = unitTree.testInsert(CmpInt(1337)); CHECK(was_inserted); CHECK(unitTree.testSize() == 1); // Try full tree iteration. AvlTreeTestIF::Iter iter3(&unitTree); CHECK(iter3.hasMore()); CmpInt ci = iter3.next(); CHECK(ci.get() == 1337); CHECK(!iter3.hasMore()); for (int i = 1336; i <= 1338; i++) { int remaining = i < 1338 ? 1 : 0; int expect = i < 1338 ? 1337 : 99999 /*we'll never use this*/; AvlTreeTestIF::Iter iter4(&unitTree, CmpInt(i)); while (iter4.hasMore()) { CmpInt ci = iter4.next(); CHECK(ci.get() == expect); remaining--; // expect doesn't change, we only expect it (or nothing) } CHECK(remaining == 0); } } return true; } END_TEST(testAvlTree_main)