diff --git a/.clang-tidy b/.clang-tidy index 374f90e..8530684 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -27,6 +27,8 @@ Checks: >- -readability-identifier-length, -*-use-trailing-return-type, -*-named-parameter, + -bugprone-suspicious-include, + -misc-include-cleaner, CheckOptions: - key: readability-function-cognitive-complexity.Threshold value: '90' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ffd19a6..4c01d98 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -85,6 +85,5 @@ jobs: - uses: DoozyX/clang-format-lint-action@v0.18.1 with: source: '.' - exclude: './unity' extensions: 'c,h,cpp,hpp' clangFormatVersion: 18 diff --git a/CMakeLists.txt b/CMakeLists.txt index cf5182e..b9442eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ # Copyright (c) 2024 Zubax Robotics cmake_minimum_required(VERSION 3.12) -project(embedded_scheduler_tests CXX) +project(embedded_scheduler_tests C CXX) enable_testing() set(CTEST_OUTPUT_ON_FAILURE ON) @@ -28,6 +28,16 @@ else () add_custom_target(format COMMAND ${clang_format} -i -fallback-style=none -style=file --verbose ${format_files}) endif () +include(FetchContent) +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.15.2 +) +FetchContent_MakeAvailable(googletest) +#add_library(GTest::GTest INTERFACE IMPORTED) +#target_link_libraries(GTest::GTest INTERFACE gtest_main) + set(CMAKE_C_STANDARD 11) set(CMAKE_C_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD 20) @@ -42,4 +52,12 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-attributes") add_executable(test_embedded_scheduler_cpp20 ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_embedded_scheduler.cpp) set_target_properties(test_embedded_scheduler_cpp20 PROPERTIES CXX_STANDARD 20) target_include_directories(test_embedded_scheduler_cpp20 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) -add_test("run_test_embedded_scheduler_cpp20" "test_embedded_scheduler_cpp20") +target_include_directories(test_embedded_scheduler_cpp20 SYSTEM PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/lib) +target_link_libraries(test_embedded_scheduler_cpp20 + PRIVATE + GTest::gmock_main +) + +#add_test("run_test_embedded_scheduler_cpp20" "test_embedded_scheduler_cpp20") +include(GoogleTest) +gtest_discover_tests(test_embedded_scheduler_cpp20) diff --git a/TODO.md b/TODO.md index 46fdd2f..c028e01 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,8 @@ ## TODOs: +- [ ] Search for... + - [ ] "dyshlo" + - [ ] "telega" + - [ ] "serge" + - [ ] Replace "serges147" with proper organization name when ready diff --git a/include/embedded_scheduler/scheduler.hpp b/include/embedded_scheduler/scheduler.hpp index c08a80c..3e8414d 100644 --- a/include/embedded_scheduler/scheduler.hpp +++ b/include/embedded_scheduler/scheduler.hpp @@ -17,9 +17,8 @@ #pragma once -#include - #include +#include #include #include diff --git a/lib/cavl/cavl.hpp b/lib/cavl/cavl.hpp new file mode 100644 index 0000000..f324bdb --- /dev/null +++ b/lib/cavl/cavl.hpp @@ -0,0 +1,632 @@ +/// Source: https://github.com/pavel-kirienko/cavl +/// +/// This is a single-header C++14 library providing an implementation of AVL tree suitable for deeply embedded systems. +/// To integrate it into your project, simply copy this file into your source tree. Read the API docs below. +/// The implementation does not use RTTI, exceptions, or dynamic memory. +/// +/// See also O1Heap -- a deterministic memory manager for hard-real-time +/// high-integrity embedded systems. +/// +/// Copyright (c) 2021 Pavel Kirienko +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +/// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +/// and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in all copies or substantial portions of +/// the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +/// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +/// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +/// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#pragma once + +#include +#include + +/// If CAVL is used in throughput-critical code, then it is recommended to disable assertion checks as they may +/// be costly in terms of execution time. +#ifndef CAVL_ASSERT +# if defined(CAVL_NO_ASSERT) && CAVL_NO_ASSERT +# define CAVL_ASSERT(x) (void) 0 +# else +# include // NOLINTNEXTLINE function-like macro +# define CAVL_ASSERT(x) assert(x) +# endif +#endif + +namespace cavl +{ +template +class Tree; + +/// The tree node type is to be composed with the user type through CRTP inheritance. +/// For instance, the derived type might be a key-value pair struct defined in the user code. +/// The worst-case complexity of all operations is O(log n), unless specifically noted otherwise. +/// Note that this class has no public members. The user type should re-export them if needed (usually it is not). +/// The size of this type is 4x pointer size (16 bytes on a 32-bit platform). +template +class Node +{ +public: + /// Helper aliases. + using TreeType = Tree; + using DerivedType = Derived; + + // Tree nodes cannot be copied for obvious reasons. + Node(const Node&) = delete; + auto operator=(const Node&) -> Node& = delete; + + // They can't be moved either, but the reason is less obvious. + // While we can trivially update the pointers in the adjacent nodes to keep the tree valid, + // we can't update external references to the tree. This breaks the tree if one attempted to move its root node. + Node(Node&& other) = delete; + auto operator=(Node&& other) -> Node& = delete; + +protected: + Node() = default; + ~Node() = default; + + /// Accessors for advanced tree introspection. Not needed for typical usage. + auto getParentNode() noexcept -> Derived* { return down(up); } + auto getParentNode() const noexcept -> const Derived* { return down(up); } + auto getChildNode(const bool right) noexcept -> Derived* { return down(lr[right]); } + auto getChildNode(const bool right) const noexcept -> const Derived* { return down(lr[right]); } + auto getBalanceFactor() const noexcept { return bf; } + + /// Find a node for which the predicate returns zero, or nullptr if there is no such node or the tree is empty. + /// The predicate is invoked with a single argument which is a constant reference to Derived. + /// The predicate returns POSITIVE if the search target is GREATER than the provided node, negative if smaller. + /// The predicate should be noexcept. + template + static auto search(Node* const root, const Pre& predicate) noexcept -> Derived* + { + Derived* p = down(root); + Derived* const out = search
(p, predicate, []() -> Derived* { return nullptr; });
+        CAVL_ASSERT(p == root);
+        return out;
+    }
+
+    /// Same but const.
+    template 
+    static auto search(const Node* const root, const Pre& predicate) noexcept -> const Derived*
+    {
+        const Node* out = nullptr;
+        const Node* n   = root;
+        while (n != nullptr)
+        {
+            const auto cmp = predicate(*down(n));
+            if (0 == cmp)
+            {
+                out = n;
+                break;
+            }
+            n = n->lr[cmp > 0];
+        }
+        return down(out);
+    }
+
+    /// This is like the regular search function except that if the node is missing, the factory will be invoked
+    /// (without arguments) to construct a new one and insert it into the tree immediately.
+    /// The root node may be replaced in the process. If the factory returns true, the tree is not modified.
+    /// The factory does not need to be noexcept (may throw).
+    template 
+    static auto search(Derived*& root, const Pre& predicate, const Fac& factory) -> Derived*;
+
+    /// Remove the specified node from its tree. The root node may be replaced in the process.
+    /// The function has no effect if the node pointer is nullptr.
+    /// If the node is not in the tree, the behavior is undefined; it may create cycles in the tree which is deadly.
+    /// It is safe to pass the result of search() directly as the second argument:
+    ///     Node::remove(root, Node::search(root, search_predicate));
+    static void remove(Derived*& root, const Node* const node) noexcept;
+
+    /// This is like the const overload of remove() except that the node pointers are invalidated afterward for safety.
+    static void remove(Derived*& root, Node* const node) noexcept
+    {
+        remove(root, static_cast(node));
+        node->unlink();
+    }
+
+    /// These methods provide very fast retrieval of min/max values, either const or mutable.
+    /// They return nullptr iff the tree is empty.
+    static auto min(Node* const root) noexcept -> Derived* { return extremum(root, false); }
+    static auto max(Node* const root) noexcept -> Derived* { return extremum(root, true); }
+    static auto min(const Node* const root) noexcept -> const Derived* { return extremum(root, false); }
+    static auto max(const Node* const root) noexcept -> const Derived* { return extremum(root, true); }
+
+    /// In-order or reverse-in-order traversal of the tree; the visitor is invoked with a reference to each node.
+    /// Required stack depth is less than 2*log2(size).
+    /// If the return type is non-void, then it shall be default-constructable and convertible to bool; in this case,
+    /// traversal will stop when the first true value is returned, which is propagated back to the caller; if none
+    /// of the calls returned true or the tree is empty, a default value is constructed and returned.
+    /// The tree shall not be modified while traversal is in progress, otherwise bad memory access will likely occur.
+    template >
+    static auto traverse(Derived* const root, const Vis& visitor, const bool reverse = false)
+        -> std::enable_if_t, R>
+    {
+        if (Node* const n = root)
+        {
+            if (auto t = Node::traverse(down(n->lr[reverse]), visitor, reverse))  // NOLINT qualified-auto
+            {
+                return t;
+            }
+            if (auto t = visitor(*root))  // NOLINT qualified-auto
+            {
+                return t;
+            }
+            return Node::traverse(down(n->lr[!reverse]), visitor, reverse);
+        }
+        return R{};
+    }
+    template 
+    static auto traverse(Derived* const root, const Vis& visitor, const bool reverse = false)
+        -> std::enable_if_t>>
+    {
+        if (Node* const n = root)
+        {
+            Node::traverse(down(n->lr[reverse]), visitor, reverse);
+            visitor(*root);
+            Node::traverse(down(n->lr[!reverse]), visitor, reverse);
+        }
+    }
+    template >
+    static auto traverse(const Derived* const root, const Vis& visitor, const bool reverse = false)
+        -> std::enable_if_t, R>
+    {
+        if (const Node* const n = root)
+        {
+            if (auto t = Node::traverse(down(n->lr[reverse]), visitor, reverse))  // NOLINT qualified-auto
+            {
+                return t;
+            }
+            if (auto t = visitor(*root))  // NOLINT qualified-auto
+            {
+                return t;
+            }
+            return Node::traverse(down(n->lr[!reverse]), visitor, reverse);
+        }
+        return R{};
+    }
+    template 
+    static auto traverse(const Derived* const root, const Vis& visitor, const bool reverse = false)
+        -> std::enable_if_t>>
+    {
+        if (const Node* const n = root)
+        {
+            Node::traverse(down(n->lr[reverse]), visitor, reverse);
+            visitor(*root);
+            Node::traverse(down(n->lr[!reverse]), visitor, reverse);
+        }
+    }
+
+private:
+    void rotate(const bool r) noexcept
+    {
+        CAVL_ASSERT((lr[!r] != nullptr) && ((bf >= -1) && (bf <= +1)));
+        Node* const z = lr[!r];
+        if (up != nullptr)
+        {
+            up->lr[up->lr[1] == this] = z;
+        }
+        z->up  = up;
+        up     = z;
+        lr[!r] = z->lr[r];
+        if (lr[!r] != nullptr)
+        {
+            lr[!r]->up = this;
+        }
+        z->lr[r] = this;
+    }
+
+    auto adjustBalance(const bool increment) noexcept -> Node*;
+
+    auto retraceOnGrowth() noexcept -> Node*;
+
+    void unlink() noexcept
+    {
+        up    = nullptr;
+        lr[0] = nullptr;
+        lr[1] = nullptr;
+        bf    = 0;
+    }
+
+    static auto extremum(Node* const root, const bool maximum) noexcept -> Derived*
+    {
+        Node* result = nullptr;
+        Node* c      = root;
+        while (c != nullptr)
+        {
+            result = c;
+            c      = c->lr[maximum];
+        }
+        return down(result);
+    }
+    static auto extremum(const Node* const root, const bool maximum) noexcept -> const Derived*
+    {
+        const Node* result = nullptr;
+        const Node* c      = root;
+        while (c != nullptr)
+        {
+            result = c;
+            c      = c->lr[maximum];
+        }
+        return down(result);
+    }
+
+    // This is MISRA-compliant as long as we are not polymorphic. The derived class may be polymorphic though.
+    static auto down(Node* x) noexcept -> Derived* { return static_cast(x); }
+    static auto down(const Node* x) noexcept -> const Derived* { return static_cast(x); }
+
+    friend class Tree;
+
+    // The binary layout is compatible with the C version.
+    Node*       up = nullptr;
+    Node*       lr[2]{};
+    std::int8_t bf = 0;
+};
+
+template 
+template 
+auto Node::search(Derived*& root, const Pre& predicate, const Fac& factory) -> Derived*
+{
+    Node* out = nullptr;
+    Node* up  = root;
+    Node* n   = root;
+    bool  r   = false;
+    while (n != nullptr)
+    {
+        const auto cmp = predicate(static_cast(*n));
+        if (0 == cmp)
+        {
+            out = n;
+            break;
+        }
+        r  = cmp > 0;
+        up = n;
+        n  = n->lr[r];
+        CAVL_ASSERT((nullptr == n) || (n->up == up));
+    }
+    if (nullptr == out)
+    {
+        out = factory();
+        if (out != nullptr)
+        {
+            if (up != nullptr)
+            {
+                CAVL_ASSERT(up->lr[r] == nullptr);
+                up->lr[r] = out;
+            }
+            else
+            {
+                root = down(out);
+            }
+            out->unlink();
+            out->up = up;
+            if (Node* const rt = out->retraceOnGrowth())
+            {
+                root = down(rt);
+            }
+        }
+    }
+    return down(out);
+}
+
+template 
+void Node::remove(Derived*& root, const Node* const node) noexcept
+{
+    if (node != nullptr)
+    {
+        CAVL_ASSERT(root != nullptr);  // Otherwise, the node would have to be nullptr.
+        CAVL_ASSERT((node->up != nullptr) || (node == root));
+        Node* p = nullptr;  // The lowest parent node that suffered a shortening of its subtree.
+        bool  r = false;    // Which side of the above was shortened.
+        // The first step is to update the topology and remember the node where to start the retracing from later.
+        // Balancing is not performed yet so we may end up with an unbalanced tree.
+        if ((node->lr[0] != nullptr) && (node->lr[1] != nullptr))
+        {
+            Node* const re = min(node->lr[1]);
+            CAVL_ASSERT((re != nullptr) && (nullptr == re->lr[0]) && (re->up != nullptr));
+            re->bf        = node->bf;
+            re->lr[0]     = node->lr[0];
+            re->lr[0]->up = re;
+            if (re->up != node)
+            {
+                p = re->up;  // Retracing starts with the ex-parent of our replacement node.
+                CAVL_ASSERT(p->lr[0] == re);
+                p->lr[0] = re->lr[1];  // Reducing the height of the left subtree here.
+                if (p->lr[0] != nullptr)
+                {
+                    p->lr[0]->up = p;
+                }
+                re->lr[1]     = node->lr[1];
+                re->lr[1]->up = re;
+                r             = false;
+            }
+            else  // In this case, we are reducing the height of the right subtree, so r=1.
+            {
+                p = re;    // Retracing starts with the replacement node itself as we are deleting its parent.
+                r = true;  // The right child of the replacement node remains the same so we don't bother relinking
+                // it.
+            }
+            re->up = node->up;
+            if (re->up != nullptr)
+            {
+                re->up->lr[re->up->lr[1] == node] = re;  // Replace link in the parent of node.
+            }
+            else
+            {
+                root = down(re);
+            }
+        }
+        else  // Either or both of the children are nullptr.
+        {
+            p             = node->up;
+            const bool rr = node->lr[1] != nullptr;
+            if (node->lr[rr] != nullptr)
+            {
+                node->lr[rr]->up = p;
+            }
+            if (p != nullptr)
+            {
+                r        = p->lr[1] == node;
+                p->lr[r] = node->lr[rr];
+                if (p->lr[r] != nullptr)
+                {
+                    p->lr[r]->up = p;
+                }
+            }
+            else
+            {
+                root = down(node->lr[rr]);
+            }
+        }
+        // Now that the topology is updated, perform the retracing to restore balance. We climb up adjusting the
+        // balance factors until we reach the root or a parent whose balance factor becomes plus/minus one, which
+        // means that that parent was able to absorb the balance delta; in other words, the height of the outer
+        // subtree is unchanged, so upper balance factors shall be kept unchanged.
+        if (p != nullptr)
+        {
+            Node* c = nullptr;
+            for (;;)
+            {
+                c = p->adjustBalance(!r);
+                p = c->up;
+                if ((c->bf != 0) || (nullptr == p))  // Reached the root or the height difference is absorbed by c.
+                {
+                    break;
+                }
+                r = p->lr[1] == c;
+            }
+            if (nullptr == p)
+            {
+                CAVL_ASSERT(c != nullptr);
+                root = down(c);
+            }
+        }
+    }
+}
+
+template 
+auto Node::adjustBalance(const bool increment) noexcept -> Node*
+{
+    CAVL_ASSERT(((bf >= -1) && (bf <= +1)));
+    Node*      out    = this;
+    const auto new_bf = static_cast(bf + (increment ? +1 : -1));
+    if ((new_bf < -1) || (new_bf > 1))
+    {
+        const bool   r    = new_bf < 0;   // bf<0 if left-heavy --> right rotation is needed.
+        const int8_t sign = r ? +1 : -1;  // Positive if we are rotating right.
+        Node* const  z    = lr[!r];
+        CAVL_ASSERT(z != nullptr);  // Heavy side cannot be empty. NOLINTNEXTLINE(clang-analyzer-core.NullDereference)
+        if ((z->bf * sign) <= 0)    // Parent and child are heavy on the same side or the child is balanced.
+        {
+            out = z;
+            rotate(r);
+            if (0 == z->bf)
+            {
+                bf    = static_cast(-sign);
+                z->bf = static_cast(+sign);
+            }
+            else
+            {
+                bf    = 0;
+                z->bf = 0;
+            }
+        }
+        else  // Otherwise, the child needs to be rotated in the opposite direction first.
+        {
+            Node* const y = z->lr[r];
+            CAVL_ASSERT(y != nullptr);  // Heavy side cannot be empty.
+            out = y;
+            z->rotate(!r);
+            rotate(r);
+            if ((y->bf * sign) < 0)
+            {
+                bf    = static_cast(+sign);
+                y->bf = 0;
+                z->bf = 0;
+            }
+            else if ((y->bf * sign) > 0)
+            {
+                bf    = 0;
+                y->bf = 0;
+                z->bf = static_cast(-sign);
+            }
+            else
+            {
+                bf    = 0;
+                z->bf = 0;
+            }
+        }
+    }
+    else
+    {
+        bf = new_bf;  // Balancing not needed, just update the balance factor and call it a day.
+    }
+    return out;
+}
+
+template 
+auto Node::retraceOnGrowth() noexcept -> Node*
+{
+    CAVL_ASSERT(0 == bf);
+    Node* c = this;      // Child
+    Node* p = this->up;  // Parent
+    while (p != nullptr)
+    {
+        const bool r = p->lr[1] == c;  // c is the right child of parent
+        CAVL_ASSERT(p->lr[r] == c);
+        c = p->adjustBalance(r);
+        p = c->up;
+        if (0 == c->bf)
+        {           // The height change of the subtree made this parent perfectly balanced (as all things should be),
+            break;  // hence, the height of the outer subtree is unchanged, so upper balance factors are unchanged.
+        }
+    }
+    CAVL_ASSERT(c != nullptr);
+    return (nullptr == p) ? c : nullptr;  // New root or nothing.
+}
+
+/// This is a very simple convenience wrapper that is entirely optional to use.
+/// It simply keeps a single root pointer of the tree. The methods are mere wrappers over the static methods
+/// defined in the Node<> template class, such that the node pointer kept in the instance of this class is passed
+/// as the first argument of the static methods of Node<>.
+/// Note that this type is implicitly convertible to Node<>* as the root node.
+template 
+class Tree final
+{
+public:
+    /// Helper alias of the compatible node type.
+    using NodeType    = ::cavl::Node;
+    using DerivedType = Derived;
+
+    explicit Tree(Derived* const root) : root_(root) {}
+    Tree()  = default;
+    ~Tree() = default;
+
+    /// Trees cannot be copied.
+    Tree(const Tree&)                    = delete;
+    auto operator=(const Tree&) -> Tree& = delete;
+
+    /// Trees can be easily moved in constant time. This does not actually affect the tree itself, only this object.
+    Tree(Tree&& other) noexcept : root_(other.root_)
+    {
+        CAVL_ASSERT(!traversal_in_progress_);  // Cannot modify the tree while it is being traversed.
+        other.root_ = nullptr;
+    }
+    auto operator=(Tree&& other) noexcept -> Tree&
+    {
+        CAVL_ASSERT(!traversal_in_progress_);  // Cannot modify the tree while it is being traversed.
+        root_       = other.root_;
+        other.root_ = nullptr;
+        return *this;
+    }
+
+    /// Wraps NodeType<>::search().
+    template 
+    auto search(const Pre& predicate) noexcept -> Derived*
+    {
+        return NodeType::template search
(*this, predicate);
+    }
+    template 
+    auto search(const Pre& predicate) const noexcept -> const Derived*
+    {
+        return NodeType::template search
(*this, predicate);
+    }
+    template 
+    auto search(const Pre& predicate, const Fac& factory) -> Derived*
+    {
+        CAVL_ASSERT(!traversal_in_progress_);  // Cannot modify the tree while it is being traversed.
+        return NodeType::template search(root_, predicate, factory);
+    }
+
+    /// Wraps NodeType<>::remove().
+    void remove(const NodeType* const node) const noexcept
+    {
+        CAVL_ASSERT(!traversal_in_progress_);  // Cannot modify the tree while it is being traversed.
+        return NodeType::remove(root_, node);
+    }
+    void remove(NodeType* const node) noexcept
+    {
+        CAVL_ASSERT(!traversal_in_progress_);  // Cannot modify the tree while it is being traversed.
+        return NodeType::remove(root_, node);
+    }
+
+    /// Wraps NodeType<>::min/max().
+    auto min() noexcept -> Derived* { return NodeType::min(*this); }
+    auto max() noexcept -> Derived* { return NodeType::max(*this); }
+    auto min() const noexcept -> const Derived* { return NodeType::min(*this); }
+    auto max() const noexcept -> const Derived* { return NodeType::max(*this); }
+
+    /// Wraps NodeType<>::traverse().
+    template 
+    auto traverse(const Vis& visitor, const bool reverse = false)
+    {
+        TraversalIndicatorUpdater upd(*this);
+        return NodeType::template traverse(*this, visitor, reverse);
+    }
+    template 
+    auto traverse(const Vis& visitor, const bool reverse = false) const
+    {
+        TraversalIndicatorUpdater upd(*this);
+        return NodeType::template traverse(*this, visitor, reverse);
+    }
+
+    /// Normally these are not needed except if advanced introspection is desired.
+    operator Derived*() noexcept { return root_; }              // NOLINT implicit conversion by design
+    operator const Derived*() const noexcept { return root_; }  // NOLINT ditto
+
+    /// Access i-th element of the tree in linear time. Returns nullptr if the index is out of bounds.
+    auto operator[](const std::size_t index) -> Derived*
+    {
+        std::size_t i = index;
+        return traverse([&i](auto& x) { return (i-- == 0) ? &x : nullptr; });
+    }
+    auto operator[](const std::size_t index) const -> const Derived*
+    {
+        std::size_t i = index;
+        return traverse([&i](const auto& x) { return (i-- == 0) ? &x : nullptr; });
+    }
+
+    /// Beware that this convenience method has linear complexity and uses recursion. Use responsibly.
+    auto size() const noexcept
+    {
+        auto i = 0UL;
+        traverse([&i](auto& /*unused*/) { i++; });
+        return i;
+    }
+
+    /// Unlike size(), this one is constant-complexity.
+    auto empty() const noexcept { return root_ == nullptr; }
+
+private:
+    static_assert(!std::is_polymorphic_v);
+    static_assert(std::is_same_v, typename NodeType::TreeType>);
+
+    /// We use a simple boolean flag instead of a nesting counter to avoid race conditions on the counter update.
+    /// This implies that in the case of concurrent or recursive traversal (more than one call to traverse() within
+    /// the same call stack) we may occasionally fail to detect a bona fide case of a race condition, but this is
+    /// acceptable because the purpose of this feature is to provide a mere best-effort data race detection.
+    class TraversalIndicatorUpdater final
+    {
+    public:
+        explicit TraversalIndicatorUpdater(const Tree& sup) noexcept : that(sup) { that.traversal_in_progress_ = true; }
+        ~TraversalIndicatorUpdater() noexcept { that.traversal_in_progress_ = false; }
+
+        TraversalIndicatorUpdater(const TraversalIndicatorUpdater&)                    = delete;
+        TraversalIndicatorUpdater(TraversalIndicatorUpdater&&)                         = delete;
+        auto operator=(const TraversalIndicatorUpdater&) -> TraversalIndicatorUpdater& = delete;
+        auto operator=(TraversalIndicatorUpdater&&) -> TraversalIndicatorUpdater&      = delete;
+
+    private:
+        const Tree& that;
+    };
+
+    Derived*              root_                  = nullptr;
+    mutable volatile bool traversal_in_progress_ = false;
+};
+
+}  // namespace cavl
diff --git a/lib/platform/heap.hpp b/lib/platform/heap.hpp
new file mode 100644
index 0000000..fc75488
--- /dev/null
+++ b/lib/platform/heap.hpp
@@ -0,0 +1,101 @@
+// Unauthorized copying and distribution of this file via any medium is strictly prohibited.
+// Proprietary and confidential.
+// Copyright (c) 2021  Zubax Robotics  
+
+#pragma once
+
+#include 
+#include 
+
+namespace platform::heap
+{
+/// These functions are constant-complexity and adhere to the half-fit worst-case fragmentation model.
+/// This free store shall only be used for small objects (typ. under 1 KiB) to avoid raising the Robson limit
+/// and mitigate the risk of fragmentation.
+/// For the related theory, see https://github.com/pavel-kirienko/o1heap.
+inline void* allocate(const std::size_t amount) noexcept
+{
+    return std::malloc(amount);
+}
+inline void deallocate(void* const pointer) noexcept
+{
+    std::free(pointer);
+}
+
+/// Call destructor (unless nullptr) and then deallocate().
+/// Direct usage is not recommended; use smart pointers instead.
+template 
+inline void destroy(T* const obj)
+{
+    static_assert((sizeof(T) > 0) && (!std::is_void_v), "incomplete type");  // NOLINT(bugprone-sizeof-expression)
+    if (obj != nullptr)
+    {
+        obj->~T();
+    }
+    deallocate(obj);
+}
+
+/// This is a no-op helper wrapper over destroy() needed for delayed function template instantiation.
+struct Destroyer final
+{
+    template 
+    void operator()(T* const obj)
+    {
+        destroy(obj);
+    }
+};
+
+/// Simple unique pointer that automatically calls destroy().
+template 
+class UniquePtr final : public std::unique_ptr
+{
+    using P = std::unique_ptr;
+
+public:
+    using typename P::pointer;
+    using typename P::element_type;
+
+             UniquePtr() noexcept : P(nullptr, {}) {}
+    explicit UniquePtr(T* const p) noexcept : P(p, {}) {}
+    // NOLINTNEXTLINE(google-explicit-constructor,hicpp-explicit-conversions)
+    UniquePtr(std::nullptr_t) noexcept : P(nullptr) {}
+    template ::pointer, typename P::pointer> &&
+                                          !std::is_array_v>>
+    // NOLINTNEXTLINE(google-explicit-constructor,hicpp-explicit-conversions)
+    UniquePtr(UniquePtr&& other) noexcept : UniquePtr(other.release())
+    {}
+
+               UniquePtr(UniquePtr&&) noexcept = default;
+    UniquePtr& operator=(UniquePtr&&) noexcept = default;
+
+    ~UniquePtr() noexcept = default;
+
+               UniquePtr(const UniquePtr&) = delete;
+    UniquePtr& operator=(const UniquePtr&) = delete;
+};
+
+/// Simple object construction helper that allocates memory and calls placement new on it with the specified args.
+/// Returns unique pointer which is nullptr if OOM.
+template 
+[[nodiscard]] inline UniquePtr construct(CtorArgs&&... args)
+{
+    if (void* const mem = allocate(sizeof(T)))
+    {
+        return UniquePtr(new (mem) T(std::forward(args)...));
+    }
+    return UniquePtr();
+}
+
+/// This diagnostics info can be queried by the application to diagnose heap utilization.
+struct Diagnostics
+{
+    std::size_t   capacity;           ///< Total heap capacity, this one is constant.
+    std::size_t   allocated;          ///< Memory in use at the moment, including all overheads.
+    std::size_t   peak_allocated;     ///< Largest seen value of the above since initialization.
+    std::size_t   peak_request_size;  ///< Largest seen argument to allocate() since initialization.
+    std::uint64_t oom_count;          ///< How many allocation requests were denied due to lack of memory.
+};
+Diagnostics getDiagnostics() noexcept;
+
+}  // namespace platform::heap
diff --git a/tests/test_embedded_scheduler.cpp b/tests/test_embedded_scheduler.cpp
index 0b68961..fb5a5c3 100644
--- a/tests/test_embedded_scheduler.cpp
+++ b/tests/test_embedded_scheduler.cpp
@@ -15,36 +15,59 @@
 /// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 /// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
-#include 
-#include 
-#include 
-#include 
+#include "embedded_scheduler/scheduler.hpp"
 
-namespace dyshlo::sitl
-{
-template 
-static auto& operator<<(auto& str, const typename embedded_scheduler::scheduler::SpinResult& obj)
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+using testing::IsNull;
+using testing::NotNull;
+
+// NOLINTBEGIN(readability-function-cognitive-complexity, misc-const-correctness)
+// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers)
+
+namespace
 {
-    str << "SpinResult{next_deadline=" << obj.next_deadline << ", worst_lateness=" << obj.worst_lateness << "}";
-    return str;
-}
-template 
-static auto& operator<<(auto& str, const typename embedded_scheduler::scheduler::Arg& obj)
+
+/// This clock has to keep global state to implement the TrivialClock trait.
+class SteadyClockMock final
 {
-    str << "Arg{event=" << &obj.event << ", deadline=" << obj.deadline << ", approx_now=" << obj.approx_now << "}";
-    return str;
-}
-}  // namespace dyshlo::sitl
+public:
+    using rep        = std::int64_t;
+    using period     = std::ratio<1, 1'000>;
+    using duration   = std::chrono::duration;
+    using time_point = std::chrono::time_point;
+
+    [[maybe_unused]] static constexpr bool is_steady = true;
+
+    static time_point& now() noexcept
+    {
+        static time_point g_now_;
+        return g_now_;
+    }
+
+    static void reset() noexcept { now() = {}; }
+
+    template 
+    static void advance(const std::chrono::duration dur)
+    {
+        now() += std::chrono::duration_cast(dur);
+    }
+};
+
+}  // namespace
 
 namespace embedded_scheduler::verification
 {
-TEST_CASE(EventLoopBasic)
+TEST(TestEmbeddedScheduler, EventLoopBasic)
 {
-    using test_helpers::SteadyClockMock;
     using std::chrono_literals::operator""ms;
 
-    const auto original_heap_diag = platform::heap::getDiagnostics();
-
     // Initial configuration of the clock.
     SteadyClockMock::reset();
     SteadyClockMock::advance(10'000ms);
@@ -60,42 +83,42 @@ TEST_CASE(EventLoopBasic)
     std::optional d;
 
     auto out = evl->spin();  // Nothing to do.
-    semihost::log(__LINE__, " ", out);
-    TEST_ASSERT(SteadyClockMock::time_point::max() == out.next_deadline);
-    TEST_ASSERT(SteadyClockMock::duration::zero() == out.worst_lateness);
-    TEST_ASSERT(evl->isEmpty());
-    TEST_ASSERT_NULL(evl->getTree()[0U]);
+    // semihost::log(__LINE__, " ", out);
+    EXPECT_THAT(out.next_deadline, SteadyClockMock::time_point::max());
+    EXPECT_THAT(out.worst_lateness, SteadyClockMock::duration::zero());
+    EXPECT_TRUE(evl->isEmpty());
+    EXPECT_THAT(evl->getTree()[0U], IsNull());
 
     // Ensure invalid arguments are rejected.
-    TEST_ASSERT_NULL(evl->repeat(0ms, [&](auto tp) { a.emplace(tp); }));  // Period shall be positive.
-    TEST_ASSERT(evl->isEmpty());
+    EXPECT_THAT(evl->repeat(0ms, [&](auto tp) { a.emplace(tp); }), IsNull());  // Period shall be positive.
+    EXPECT_TRUE(evl->isEmpty());
 
     // Register our handlers. Events with same deadline are ordered such that the one added later is processed later.
-    semihost::log("Alloc ", __LINE__, ": ", platform::heap::getDiagnostics().allocated);
+    // semihost::log("Alloc ", __LINE__, ": ", platform::heap::getDiagnostics().allocated);
     auto evt_a = evl->repeat(1000ms, [&](auto tp) {
         a.emplace(tp);
-        semihost::log("A! ", tp);
+        // semihost::log("A! ", tp);
     });
-    TEST_ASSERT_NOT_NULL(evt_a);
-    TEST_ASSERT_EQUAL(11'000ms, evl->getTree()[0U]->getDeadline().value().time_since_epoch());
-    TEST_ASSERT_NULL(evl->getTree()[1U]);
-    TEST_ASSERT(!evl->isEmpty());
+    EXPECT_THAT(evt_a, NotNull());
+    EXPECT_THAT(evl->getTree()[0U]->getDeadline().value().time_since_epoch(), 11'000ms);
+    EXPECT_THAT(evl->getTree()[1U], IsNull());
+    EXPECT_FALSE(evl->isEmpty());
 
-    semihost::log("Alloc ", __LINE__, ": ", platform::heap::getDiagnostics().allocated);
+    // semihost::log("Alloc ", __LINE__, ": ", platform::heap::getDiagnostics().allocated);
     auto evt_b = evl->repeat(100ms,  // Smaller deadline goes on the left.
                              [&](auto tp) {
                                  b.emplace(tp);
-                                 semihost::log("B! ", tp);
+                                 // semihost::log("B! ", tp);
                              });
-    TEST_ASSERT_NOT_NULL(evt_b);
-    TEST_ASSERT_EQUAL(10'100ms, evl->getTree()[0U]->getDeadline().value().time_since_epoch());
-    TEST_ASSERT_EQUAL(11'000ms, evl->getTree()[1U]->getDeadline().value().time_since_epoch());
-    TEST_ASSERT_NULL(evl->getTree()[2U]);
-
-    semihost::log("Alloc ", __LINE__, ": ", platform::heap::getDiagnostics().allocated);
+    EXPECT_THAT(evt_b, NotNull());
+    EXPECT_THAT(evl->getTree()[0U]->getDeadline().value().time_since_epoch(), 10'100ms);
+    EXPECT_THAT(evl->getTree()[1U]->getDeadline().value().time_since_epoch(), 11'000ms);
+    EXPECT_THAT(evl->getTree()[2U], IsNull());
+/*
+    // semihost::log("Alloc ", __LINE__, ": ", platform::heap::getDiagnostics().allocated);
     auto evt_c = evl->defer(SteadyClockMock::now() + 2000ms, [&](auto tp) {
         c.emplace(tp);
-        semihost::log("C! ", tp);
+        // semihost::log("C! ", tp);
     });
     TEST_ASSERT_NOT_NULL(evt_c);
     TEST_ASSERT_EQUAL(10'100ms, evl->getTree()[0U]->getDeadline().value().time_since_epoch());
@@ -104,11 +127,11 @@ TEST_CASE(EventLoopBasic)
     TEST_ASSERT_EQUAL(12'000ms, f3->getDeadline().value().time_since_epoch());  // New entry.
     TEST_ASSERT_NULL(evl->getTree()[3U]);
 
-    semihost::log("Alloc ", __LINE__, ": ", platform::heap::getDiagnostics().allocated);
+    // semihost::log("Alloc ", __LINE__, ": ", platform::heap::getDiagnostics().allocated);
     auto evt_d = evl->defer(SteadyClockMock::now() + 2000ms,  // Same deadline!
                             [&](auto tp) {
                                 d.emplace(tp);
-                                semihost::log("D! ", tp);
+                                // semihost::log("D! ", tp);
                             });
     TEST_ASSERT_NOT_NULL(evt_d);
     TEST_ASSERT_EQUAL(10'100ms, evl->getTree()[0U]->getDeadline().value().time_since_epoch());
@@ -121,7 +144,7 @@ TEST_CASE(EventLoopBasic)
 
     // Poll but there are no pending Events yet.
     out = evl->spin();
-    semihost::log(__LINE__, " ", out);
+    // semihost::log(__LINE__, " ", out);
     TEST_ASSERT_EQUAL(10'100ms, out.next_deadline.time_since_epoch());
     TEST_ASSERT_EQUAL(0ms, out.worst_lateness);
     TEST_ASSERT_EQUAL_UINT64(4, evl->getTree().size());
@@ -134,7 +157,7 @@ TEST_CASE(EventLoopBasic)
     SteadyClockMock::advance(1100ms);
     TEST_ASSERT_EQUAL(11'100ms, SteadyClockMock::now().time_since_epoch());
     out = evl->spin();
-    semihost::log(__LINE__, " ", out);
+    // semihost::log(__LINE__, " ", out);
     TEST_ASSERT_EQUAL(11'200ms, out.next_deadline.time_since_epoch());
     TEST_ASSERT_EQUAL(1000ms, out.worst_lateness);
     TEST_ASSERT_EQUAL_UINT64(4, evl->getTree().size());
@@ -152,7 +175,7 @@ TEST_CASE(EventLoopBasic)
     SteadyClockMock::advance(900ms);
     TEST_ASSERT_EQUAL(12'000ms, SteadyClockMock::now().time_since_epoch());
     out = evl->spin();
-    semihost::log(__LINE__, " ", out);
+    // semihost::log(__LINE__, " ", out);
     TEST_ASSERT_EQUAL(12'100ms, out.next_deadline.time_since_epoch());
     TEST_ASSERT_EQUAL(800ms, out.worst_lateness);
     TEST_ASSERT_EQUAL(12'100ms, evl->getTree()[0U]->getDeadline().value().time_since_epoch());
@@ -186,7 +209,7 @@ TEST_CASE(EventLoopBasic)
     TEST_ASSERT(!evt_b->getDeadline());                  // Ditto.
     TEST_ASSERT_EQUAL_UINT64(1, evl->getTree().size());  // Ditto.
     out = evl->spin();
-    semihost::log(__LINE__, " ", out);
+    // semihost::log(__LINE__, " ", out);
     TEST_ASSERT_EQUAL(14'000ms, out.next_deadline.time_since_epoch());  // B removed so the next one is A.
     TEST_ASSERT_EQUAL(50ms, out.worst_lateness);
     TEST_ASSERT_EQUAL(14'000ms, evl->getTree()[0U]->getDeadline().value().time_since_epoch());
@@ -200,7 +223,7 @@ TEST_CASE(EventLoopBasic)
 
     // Nothing to do yet.
     out = evl->spin();
-    semihost::log(__LINE__, " ", out);
+    // semihost::log(__LINE__, " ", out);
     TEST_ASSERT_EQUAL(14'000ms, out.next_deadline.time_since_epoch());  // Same up.
     TEST_ASSERT_EQUAL(0ms, out.worst_lateness);
     TEST_ASSERT_FALSE(a);
@@ -209,20 +232,18 @@ TEST_CASE(EventLoopBasic)
     TEST_ASSERT_FALSE(d);
 
     // Ensure the memory is properly reclaimed and there have been no OOMs.
-    semihost::log("Alloc before dtors: ", platform::heap::getDiagnostics().allocated);
+    // semihost::log("Alloc before dtors: ", platform::heap::getDiagnostics().allocated);
     evt_a.reset();
     evt_b.reset();
     evt_c.reset();
     evt_d.reset();
     evl.reset();  // The destructor would panic unless all events are destroyed.
-    semihost::log("Alloc after dtors: ", platform::heap::getDiagnostics().allocated);
-    TEST_ASSERT_EQUAL_UINT64(original_heap_diag.allocated, platform::heap::getDiagnostics().allocated);
-    TEST_ASSERT_EQUAL_UINT64(original_heap_diag.oom_count, platform::heap::getDiagnostics().oom_count);
+    // semihost::log("Alloc after dtors: ", platform::heap::getDiagnostics().allocated);
+*/
 }
-
-TEST_CASE(EventLoopTotalOrdering)
+/*
+TEST(TestEmbeddedScheduler, EventLoopTotalOrdering)
 {
-    using test_helpers::SteadyClockMock;
     using std::chrono_literals::operator""ms;
     SteadyClockMock::reset();
     EventLoop evl;
@@ -230,7 +251,9 @@ TEST_CASE(EventLoopTotalOrdering)
     std::uint8_t               b      = 0;
     std::uint8_t               c      = 0;
     const auto                 report = [&](const auto tp, const char* const letter) {
-        semihost::log(tp, " ", letter, "! a=", a, " b=", b, " c=", c);  //
+        (void) tp;
+        (void) letter;
+        // semihost::log(tp, " ", letter, "! a=", a, " b=", b, " c=", c);  //
     };
     const auto evt_a = evl.repeat(10ms, [&](auto tp) {
         report(tp, "A");
@@ -260,15 +283,14 @@ TEST_CASE(EventLoopTotalOrdering)
     TEST_ASSERT_EQUAL_INT64(5, c);
 }
 
-TEST_CASE(EventLoopPoll)
+TEST(TestEmbeddedScheduler, EventLoopPoll)
 {
-    using test_helpers::SteadyClockMock;
     using time_point = SteadyClockMock::time_point;
     using std::chrono_literals::operator""ms;
     SteadyClockMock::reset();
     EventLoop evl;
 
-    TEST_ASSERT_NULL(evl.poll(0ms, [&](auto /*unused*/) {}));  // Period shall be positive.
+    TEST_ASSERT_NULL(evl.poll(0ms, [&](auto) {}));  // Period shall be positive.
     TEST_ASSERT(evl.isEmpty());
 
     std::optional last_tp{};
@@ -297,18 +319,17 @@ TEST_CASE(EventLoopPoll)
     TEST_ASSERT_EQUAL(210ms, evl.getTree()[0U]->getDeadline().value().time_since_epoch());  // Skipped ahead!
 }
 
-TEST_CASE(HandleMovement)
+TEST(TestEmbeddedScheduler, HandleMovement)
 {
-    using test_helpers::SteadyClockMock;
     using std::chrono_literals::operator""ms;
 
     SteadyClockMock::reset();
 
     EventLoop evl;
 
-    auto a = evl.repeat(100ms, [&](auto /**/) {});
-    auto b = evl.repeat(103ms, [&](auto /**/) {});
-    auto c = evl.repeat(107ms, [&](auto /**/) {});
+    auto a = evl.repeat(100ms, [&](auto) {});
+    auto b = evl.repeat(103ms, [&](auto) {});
+    auto c = evl.repeat(107ms, [&](auto) {});
     TEST_ASSERT_EQUAL_UINT64(3, evl.getTree().size());
 
     SteadyClockMock::advance(1000ms);
@@ -339,5 +360,8 @@ TEST_CASE(HandleMovement)
     (void) evl.spin();
     TEST_ASSERT_EQUAL_UINT64(0, evl.getTree().size());
 }
-
+*/
 }  // namespace embedded_scheduler::verification
+
+// NOLINTEND(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers)
+// NOLINTEND(readability-function-cognitive-complexity, misc-const-correctness)