OSDN Git Service

Add a callback for rendernode parentcount=0
authorJohn Reck <jreck@google.com>
Fri, 25 Mar 2016 21:29:48 +0000 (14:29 -0700)
committerJohn Reck <jreck@google.com>
Thu, 31 Mar 2016 15:36:50 +0000 (08:36 -0700)
Bug: 27709981
Fixes: 22565656

Change-Id: I1cb4461baf9069dc4e7ca6de10d5862578c107f4

14 files changed:
core/java/android/view/RenderNode.java
core/java/android/view/ThreadedRenderer.java
core/java/android/view/View.java
core/java/android/view/ViewRootImpl.java
core/java/android/webkit/WebViewDelegate.java
core/jni/android_view_RenderNode.cpp
core/jni/android_view_ThreadedRenderer.cpp
libs/hwui/Android.mk
libs/hwui/RenderNode.cpp
libs/hwui/RenderNode.h
libs/hwui/TreeInfo.h
libs/hwui/renderthread/CanvasContext.cpp
libs/hwui/tests/common/TestUtils.h
libs/hwui/tests/unit/RenderNodeTests.cpp [new file with mode: 0644]

index a19254f..4ffb3d3 100644 (file)
@@ -763,6 +763,16 @@ public class RenderNode {
         return nGetDebugSize(mNativeRenderNode);
     }
 
+    /**
+     * Called by native when the passed displaylist is removed from the draw tree
+     */
+    void onRenderNodeDetached() {
+        discardDisplayList();
+        if (mOwningView != null) {
+            mOwningView.onRenderNodeDetached(this);
+        }
+    }
+
     ///////////////////////////////////////////////////////////////////////////
     // Animations
     ///////////////////////////////////////////////////////////////////////////
@@ -795,7 +805,9 @@ public class RenderNode {
     // Native methods
     ///////////////////////////////////////////////////////////////////////////
 
-    private static native long nCreate(String name);
+    // Intentionally not static because it acquires a reference to 'this'
+    private native long nCreate(String name);
+
     private static native void nDestroyRenderNode(long renderNode);
     private static native void nSetDisplayList(long renderNode, long newData);
 
index c972476..df774b4 100644 (file)
@@ -794,7 +794,8 @@ public final class ThreadedRenderer {
         }
 
         final long[] frameInfo = choreographer.mFrameInfo.mFrameInfo;
-        int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length);
+        int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length,
+                mRootNode.mNativeRenderNode);
         if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) {
             setEnabled(false);
             attachInfo.mViewRootImpl.mSurface.release();
@@ -993,7 +994,8 @@ public final class ThreadedRenderer {
     private static native void nSetLightCenter(long nativeProxy,
             float lightX, float lightY, float lightZ);
     private static native void nSetOpaque(long nativeProxy, boolean opaque);
-    private static native int nSyncAndDrawFrame(long nativeProxy, long[] frameInfo, int size);
+    private static native int nSyncAndDrawFrame(long nativeProxy, long[] frameInfo, int size,
+            long rootRenderNode);
     private static native void nDestroy(long nativeProxy, long rootRenderNode);
     private static native void nRegisterAnimatingRenderNode(long rootRenderNode, long animatingNode);
 
index dd79e62..3ee6a8d 100644 (file)
@@ -16009,6 +16009,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     }
 
     /**
+     * Called when the passed RenderNode is removed from the draw tree
+     * @hide
+     */
+    public void onRenderNodeDetached(RenderNode renderNode) {
+    }
+
+    /**
      * <p>Calling this method is equivalent to calling <code>getDrawingCache(false)</code>.</p>
      *
      * @return A non-scaled bitmap representing this view or null if cache is disabled.
index 5d4ee87..420c4f2 100644 (file)
@@ -774,7 +774,7 @@ public final class ViewRootImpl implements ViewParent,
      *                          has invoked. If false, the functor may be invoked
      *                          asynchronously.
      */
-    public void invokeFunctor(long functor, boolean waitForCompletion) {
+    public static void invokeFunctor(long functor, boolean waitForCompletion) {
         ThreadedRenderer.invokeFunctor(functor, waitForCompletion);
     }
 
index 94dc03c..8104f7d 100644 (file)
@@ -86,8 +86,7 @@ public final class WebViewDelegate {
      */
     public void invokeDrawGlFunctor(View containerView, long nativeDrawGLFunctor,
             boolean waitForCompletion) {
-        ViewRootImpl viewRootImpl = containerView.getViewRootImpl();
-        viewRootImpl.invokeFunctor(nativeDrawGLFunctor, waitForCompletion);
+        ViewRootImpl.invokeFunctor(nativeDrawGLFunctor, waitForCompletion);
     }
 
     /**
index 79b518f..27e2ee8 100644 (file)
@@ -43,6 +43,54 @@ using namespace uirenderer;
         ? (reinterpret_cast<RenderNode*>(renderNodePtr)->setPropertyFieldsDirty(dirtyFlag), true) \
         : false)
 
+static JNIEnv* getenv(JavaVM* vm) {
+    JNIEnv* env;
+    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+        LOG_ALWAYS_FATAL("Failed to get JNIEnv for JavaVM: %p", vm);
+    }
+    return env;
+}
+
+static jmethodID gOnRenderNodeDetached;
+
+class RenderNodeContext : public VirtualLightRefBase {
+public:
+    RenderNodeContext(JNIEnv* env, jobject jobjRef) {
+        env->GetJavaVM(&mVm);
+        // This holds a weak ref because otherwise there's a cyclic global ref
+        // with this holding a strong global ref to the view which holds
+        // a strong ref to RenderNode which holds a strong ref to this.
+        mWeakRef = env->NewWeakGlobalRef(jobjRef);
+    }
+
+    virtual ~RenderNodeContext() {
+        JNIEnv* env = getenv(mVm);
+        env->DeleteWeakGlobalRef(mWeakRef);
+    }
+
+    jobject acquireLocalRef(JNIEnv* env) {
+        return env->NewLocalRef(mWeakRef);
+    }
+
+private:
+    JavaVM* mVm;
+    jweak mWeakRef;
+};
+
+// Called by ThreadedRenderer's JNI layer
+void onRenderNodeRemoved(JNIEnv* env, RenderNode* node) {
+    auto context = reinterpret_cast<RenderNodeContext*>(node->getUserContext());
+    if (!context) return;
+    jobject jnode = context->acquireLocalRef(env);
+    if (!jnode) {
+        // The owning node has been GC'd, release the context
+        node->setUserContext(nullptr);
+        return;
+    }
+    env->CallVoidMethod(jnode, gOnRenderNodeDetached);
+    env->DeleteLocalRef(jnode);
+}
+
 // ----------------------------------------------------------------------------
 // DisplayList view properties
 // ----------------------------------------------------------------------------
@@ -59,7 +107,8 @@ static jint android_view_RenderNode_getDebugSize(JNIEnv* env,
     return renderNode->getDebugSize();
 }
 
-static jlong android_view_RenderNode_create(JNIEnv* env, jobject clazz, jstring name) {
+static jlong android_view_RenderNode_create(JNIEnv* env, jobject thiz,
+        jstring name) {
     RenderNode* renderNode = new RenderNode();
     renderNode->incStrong(0);
     if (name != NULL) {
@@ -67,6 +116,7 @@ static jlong android_view_RenderNode_create(JNIEnv* env, jobject clazz, jstring
         renderNode->setName(textArray);
         env->ReleaseStringUTFChars(name, textArray);
     }
+    renderNode->setUserContext(new RenderNodeContext(env, thiz));
     return reinterpret_cast<jlong>(renderNode);
 }
 
@@ -627,6 +677,9 @@ int register_android_view_RenderNode(JNIEnv* env) {
     jclass clazz = FindClassOrDie(env, "android/view/SurfaceView");
     gSurfaceViewPositionUpdateMethod = GetMethodIDOrDie(env, clazz,
             "updateWindowPositionRT", "(JIIII)V");
+    clazz = FindClassOrDie(env, "android/view/RenderNode");
+    gOnRenderNodeDetached = GetMethodIDOrDie(env, clazz,
+            "onRenderNodeDetached", "()V");
     return RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods));
 }
 
index 07868c5..8019326 100644 (file)
@@ -120,7 +120,13 @@ private:
     std::string mMessage;
 };
 
-class RootRenderNode : public RenderNode, ErrorHandler {
+// TODO: Clean this up, it's a bit odd to need to call over to
+// rendernode's jni layer. Probably means RootRenderNode should be pulled
+// into HWUI with appropriate callbacks for the various JNI hooks so
+// that RenderNode's JNI layer can handle its own thing
+void onRenderNodeRemoved(JNIEnv* env, RenderNode* node);
+
+class RootRenderNode : public RenderNode, ErrorHandler, TreeObserver {
 public:
     RootRenderNode(JNIEnv* env) : RenderNode() {
         mLooper = Looper::getForThread();
@@ -131,12 +137,15 @@ public:
 
     virtual ~RootRenderNode() {}
 
-    virtual void onError(const std::string& message) {
+    virtual void onError(const std::string& message) override {
         mLooper->sendMessage(new RenderingException(mVm, message), 0);
     }
 
-    virtual void prepareTree(TreeInfo& info) {
+    virtual void prepareTree(TreeInfo& info) override {
         info.errorHandler = this;
+        if (info.mode == TreeInfo::MODE_FULL) {
+            info.observer = this;
+        }
         // TODO: This is hacky
         info.windowInsetLeft = -stagingProperties().getLeft();
         info.windowInsetTop = -stagingProperties().getTop();
@@ -145,7 +154,8 @@ public:
         info.updateWindowPositions = false;
         info.windowInsetLeft = 0;
         info.windowInsetTop = 0;
-        info.errorHandler = NULL;
+        info.errorHandler = nullptr;
+        info.observer = nullptr;
     }
 
     void sendMessage(const sp<MessageHandler>& handler) {
@@ -171,10 +181,27 @@ public:
         mPendingAnimatingRenderNodes.clear();
     }
 
+    virtual void onMaybeRemovedFromTree(RenderNode* node) override {
+        mMaybeRemovedNodes.insert(sp<RenderNode>(node));
+    }
+
+    void processMaybeRemovedNodes(JNIEnv* env) {
+        // We can safely access mMaybeRemovedNodes here because
+        // we will only modify it in prepareTree calls that are
+        // MODE_FULL
+
+        for (auto& node : mMaybeRemovedNodes) {
+            if (node->hasParents()) continue;
+            onRenderNodeRemoved(env, node.get());
+        }
+        mMaybeRemovedNodes.clear();
+    }
+
 private:
     sp<Looper> mLooper;
     JavaVM* mVm;
     std::vector< sp<RenderNode> > mPendingAnimatingRenderNodes;
+    std::set< sp<RenderNode> > mMaybeRemovedNodes;
 };
 
 class AnimationContextBridge : public AnimationContext {
@@ -473,13 +500,16 @@ static void android_view_ThreadedRenderer_setOpaque(JNIEnv* env, jobject clazz,
 }
 
 static int android_view_ThreadedRenderer_syncAndDrawFrame(JNIEnv* env, jobject clazz,
-        jlong proxyPtr, jlongArray frameInfo, jint frameInfoSize) {
+        jlong proxyPtr, jlongArray frameInfo, jint frameInfoSize, jlong rootNodePtr) {
     LOG_ALWAYS_FATAL_IF(frameInfoSize != UI_THREAD_FRAME_INFO_SIZE,
             "Mismatched size expectations, given %d expected %d",
             frameInfoSize, UI_THREAD_FRAME_INFO_SIZE);
     RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
+    RootRenderNode* rootRenderNode = reinterpret_cast<RootRenderNode*>(rootNodePtr);
     env->GetLongArrayRegion(frameInfo, 0, frameInfoSize, proxy->frameInfo());
-    return proxy->syncAndDrawFrame();
+    int ret = proxy->syncAndDrawFrame();
+    rootRenderNode->processMaybeRemovedNodes(env);
+    return ret;
 }
 
 static void android_view_ThreadedRenderer_destroy(JNIEnv* env, jobject clazz,
@@ -706,7 +736,7 @@ static const JNINativeMethod gMethods[] = {
     { "nSetup", "(JIIFII)V", (void*) android_view_ThreadedRenderer_setup },
     { "nSetLightCenter", "(JFFF)V", (void*) android_view_ThreadedRenderer_setLightCenter },
     { "nSetOpaque", "(JZ)V", (void*) android_view_ThreadedRenderer_setOpaque },
-    { "nSyncAndDrawFrame", "(J[JI)I", (void*) android_view_ThreadedRenderer_syncAndDrawFrame },
+    { "nSyncAndDrawFrame", "(J[JIJ)I", (void*) android_view_ThreadedRenderer_syncAndDrawFrame },
     { "nDestroy", "(JJ)V", (void*) android_view_ThreadedRenderer_destroy },
     { "nRegisterAnimatingRenderNode", "(JJ)V", (void*) android_view_ThreadedRenderer_registerAnimatingRenderNode },
     { "nInvokeFunctor", "(JZ)V", (void*) android_view_ThreadedRenderer_invokeFunctor },
index 1f17851..be816f7 100644 (file)
@@ -252,6 +252,7 @@ LOCAL_SRC_FILES += \
     tests/unit/LinearAllocatorTests.cpp \
     tests/unit/MatrixTests.cpp \
     tests/unit/OffscreenBufferPoolTests.cpp \
+    tests/unit/RenderNodeTests.cpp \
     tests/unit/SkiaBehaviorTests.cpp \
     tests/unit/StringUtilsTests.cpp \
     tests/unit/TextDropShadowCacheTests.cpp \
index 61441ce..f6f92f7 100644 (file)
@@ -68,7 +68,7 @@ RenderNode::RenderNode()
 }
 
 RenderNode::~RenderNode() {
-    deleteDisplayList();
+    deleteDisplayList(nullptr);
     delete mStagingDisplayList;
 #if HWUI_NEW_OPS
     LOG_ALWAYS_FATAL_IF(mLayer, "layer missed detachment!");
@@ -88,7 +88,7 @@ void RenderNode::setStagingDisplayList(DisplayList* displayList) {
     // If mParentCount == 0 we are the sole reference to this RenderNode,
     // so immediately free the old display list
     if (!mParentCount && !mStagingDisplayList) {
-        deleteDisplayList();
+        deleteDisplayList(nullptr);
     }
 }
 
@@ -462,7 +462,7 @@ void RenderNode::applyLayerPropertiesToLayer(TreeInfo& info) {
 }
 #endif
 
-void RenderNode::syncDisplayList() {
+void RenderNode::syncDisplayList(TreeObserver* observer) {
     // Make sure we inc first so that we don't fluctuate between 0 and 1,
     // which would thrash the layer cache
     if (mStagingDisplayList) {
@@ -470,7 +470,7 @@ void RenderNode::syncDisplayList() {
             child->renderNode->incParentRefCount();
         }
     }
-    deleteDisplayList();
+    deleteDisplayList(observer);
     mDisplayList = mStagingDisplayList;
     mStagingDisplayList = nullptr;
     if (mDisplayList) {
@@ -486,15 +486,15 @@ void RenderNode::pushStagingDisplayListChanges(TreeInfo& info) {
         // Damage with the old display list first then the new one to catch any
         // changes in isRenderable or, in the future, bounds
         damageSelf(info);
-        syncDisplayList();
+        syncDisplayList(info.observer);
         damageSelf(info);
     }
 }
 
-void RenderNode::deleteDisplayList() {
+void RenderNode::deleteDisplayList(TreeObserver* observer) {
     if (mDisplayList) {
         for (auto&& child : mDisplayList->getChildren()) {
-            child->renderNode->decParentRefCount();
+            child->renderNode->decParentRefCount(observer);
         }
     }
     delete mDisplayList;
@@ -526,32 +526,35 @@ void RenderNode::prepareSubTree(TreeInfo& info, bool functorsNeedLayer, DisplayL
     }
 }
 
-void RenderNode::destroyHardwareResources() {
+void RenderNode::destroyHardwareResources(TreeObserver* observer) {
     if (mLayer) {
         destroyLayer(mLayer);
         mLayer = nullptr;
     }
     if (mDisplayList) {
         for (auto&& child : mDisplayList->getChildren()) {
-            child->renderNode->destroyHardwareResources();
+            child->renderNode->destroyHardwareResources(observer);
         }
         if (mNeedsDisplayListSync) {
             // Next prepare tree we are going to push a new display list, so we can
             // drop our current one now
-            deleteDisplayList();
+            deleteDisplayList(observer);
         }
     }
 }
 
-void RenderNode::decParentRefCount() {
+void RenderNode::decParentRefCount(TreeObserver* observer) {
     LOG_ALWAYS_FATAL_IF(!mParentCount, "already 0!");
     mParentCount--;
     if (!mParentCount) {
+        if (observer) {
+            observer->onMaybeRemovedFromTree(this);
+        }
         // If a child of ours is being attached to our parent then this will incorrectly
         // destroy its hardware resources. However, this situation is highly unlikely
         // and the failure is "just" that the layer is re-created, so this should
         // be safe enough
-        destroyHardwareResources();
+        destroyHardwareResources(observer);
     }
 }
 
index 8381925..b0136cf 100644 (file)
@@ -68,6 +68,7 @@ class SaveLayerOp;
 class SaveOp;
 class RestoreToCountOp;
 class TreeInfo;
+class TreeObserver;
 
 namespace proto {
 class RenderNode;
@@ -154,6 +155,14 @@ public:
         }
     }
 
+    VirtualLightRefBase* getUserContext() const {
+        return mUserContext.get();
+    }
+
+    void setUserContext(VirtualLightRefBase* context) {
+        mUserContext = context;
+    }
+
     bool isPropertyFieldDirty(DirtyPropertyMask field) const {
         return mDirtyPropertyFields & field;
     }
@@ -187,7 +196,7 @@ public:
     }
 
     ANDROID_API virtual void prepareTree(TreeInfo& info);
-    void destroyHardwareResources();
+    void destroyHardwareResources(TreeObserver* observer);
 
     // UI thread only!
     ANDROID_API void addAnimator(const sp<BaseRenderNodeAnimator>& animator);
@@ -232,6 +241,12 @@ public:
         mPositionListener.reset(listener);
     }
 
+    // This is only modified in MODE_FULL, so it can be safely accessed
+    // on the UI thread.
+    ANDROID_API bool hasParents() {
+        return mParentCount;
+    }
+
 private:
     typedef key_value_pair_t<float, DrawRenderNodeOp*> ZDrawRenderNodeOpPair;
 
@@ -291,7 +306,7 @@ private:
 
 
     void syncProperties();
-    void syncDisplayList();
+    void syncDisplayList(TreeObserver* observer);
 
     void prepareTreeImpl(TreeInfo& info, bool functorsNeedLayer);
     void pushStagingPropertiesChanges(TreeInfo& info);
@@ -302,13 +317,14 @@ private:
 #endif
     void prepareLayer(TreeInfo& info, uint32_t dirtyMask);
     void pushLayerUpdate(TreeInfo& info);
-    void deleteDisplayList();
+    void deleteDisplayList(TreeObserver* observer);
     void damageSelf(TreeInfo& info);
 
     void incParentRefCount() { mParentCount++; }
-    void decParentRefCount();
+    void decParentRefCount(TreeObserver* observer);
 
     String8 mName;
+    sp<VirtualLightRefBase> mUserContext;
 
     uint32_t mDirtyPropertyFields;
     RenderProperties mProperties;
index accd303..a43e544 100644 (file)
@@ -32,6 +32,7 @@ class CanvasContext;
 class DamageAccumulator;
 class LayerUpdateQueue;
 class OpenGLRenderer;
+class RenderNode;
 class RenderState;
 
 class ErrorHandler {
@@ -41,6 +42,17 @@ protected:
     ~ErrorHandler() {}
 };
 
+class TreeObserver {
+public:
+    // Called when a RenderNode's parent count hits 0.
+    // Due to the unordered nature of tree pushes, once prepareTree
+    // is finished it is possible that the node was "resurrected" and has
+    // a non-zero parent count.
+    virtual void onMaybeRemovedFromTree(RenderNode* node) {}
+protected:
+    ~TreeObserver() {}
+};
+
 // This would be a struct, but we want to PREVENT_COPY_AND_ASSIGN
 class TreeInfo {
     PREVENT_COPY_AND_ASSIGN(TreeInfo);
@@ -86,6 +98,10 @@ public:
 #endif
     ErrorHandler* errorHandler = nullptr;
 
+    // Optional, may be nullptr. Used to allow things to observe interesting
+    // tree state changes
+    TreeObserver* observer = nullptr;
+
     // Frame number for use with synchronized surfaceview position updating
     int64_t frameNumber = -1;
     int32_t windowInsetLeft = 0;
index 6933b2f..63fa788 100644 (file)
@@ -576,7 +576,7 @@ void CanvasContext::markLayerInUse(RenderNode* node) {
 
 static void destroyPrefetechedNode(RenderNode* node) {
     ALOGW("Incorrectly called buildLayer on View: %s, destroying layer...", node->getName());
-    node->destroyHardwareResources();
+    node->destroyHardwareResources(nullptr);
     node->decStrong(nullptr);
 }
 
@@ -641,7 +641,7 @@ void CanvasContext::destroyHardwareResources() {
     if (mEglManager.hasEglContext()) {
         freePrefetechedLayers();
         for (const sp<RenderNode>& node : mRenderNodes) {
-            node->destroyHardwareResources();
+            node->destroyHardwareResources(nullptr);
         }
         Caches& caches = Caches::getInstance();
         // Make sure to release all the textures we were owning as there won't
index 2d1e2e9..5492035 100644 (file)
@@ -218,7 +218,7 @@ public:
 private:
     static void syncHierarchyPropertiesAndDisplayListImpl(RenderNode* node) {
         node->syncProperties();
-        node->syncDisplayList();
+        node->syncDisplayList(nullptr);
         auto displayList = node->getDisplayList();
         if (displayList) {
             for (auto&& childOp : displayList->getChildren()) {
diff --git a/libs/hwui/tests/unit/RenderNodeTests.cpp b/libs/hwui/tests/unit/RenderNodeTests.cpp
new file mode 100644 (file)
index 0000000..7c57a50
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gtest/gtest.h>
+
+#include "RenderNode.h"
+#include "TreeInfo.h"
+#include "tests/common/TestUtils.h"
+#include "utils/Color.h"
+
+using namespace android;
+using namespace android::uirenderer;
+
+TEST(RenderNode, hasParents) {
+    auto child = TestUtils::createNode(0, 0, 200, 400,
+            [](RenderProperties& props, TestCanvas& canvas) {
+        canvas.drawColor(Color::Red_500, SkXfermode::kSrcOver_Mode);
+    });
+    auto parent = TestUtils::createNode(0, 0, 200, 400,
+            [&child](RenderProperties& props, TestCanvas& canvas) {
+        canvas.drawRenderNode(child.get());
+    });
+
+    TestUtils::syncHierarchyPropertiesAndDisplayList(parent);
+
+    EXPECT_TRUE(child->hasParents()) << "Child node has no parent";
+    EXPECT_FALSE(parent->hasParents()) << "Root node shouldn't have any parents";
+
+    TestUtils::recordNode(*parent, [](TestCanvas& canvas) {
+        canvas.drawColor(Color::Amber_500, SkXfermode::kSrcOver_Mode);
+    });
+
+    EXPECT_TRUE(child->hasParents()) << "Child should still have a parent";
+    EXPECT_FALSE(parent->hasParents()) << "Root node shouldn't have any parents";
+
+    TestUtils::syncHierarchyPropertiesAndDisplayList(parent);
+
+    EXPECT_FALSE(child->hasParents()) << "Child should be removed";
+    EXPECT_FALSE(parent->hasParents()) << "Root node shouldn't have any parents";
+}