From 123276157121f8d87d1245f94cf5f87edeeb18b6 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Mon, 2 Jun 2025 19:32:45 -0700 Subject: [PATCH] YGPersistentNodeCloningTest (#1813) Summary: Pull Request resolved: https://github.com/facebook/yoga/pull/1813 This adds a unit test to Yoga, which emulates the model of "persistent Yoga nodes" and cloning used by React Fabric, including the private (but relied on) Yoga APIs. It models the over-invalidation exposed in D75287261, which reproduces (due to Yoga incorrectly measuring flex-basis under fit-content, and that constraint changing when sibling changes) but this test for now sets a definite height on A, to show that we only clone what is neccesary, when measure constraints do not have to change. Having a minimal version of Fabric's model in Yoga unit tests should make some of these interesting interactions a bit easier to debug. Changelog: [Internal] Reviewed By: javache Differential Revision: D75572762 fbshipit-source-id: cda8b3fdd6e538a55dd100494518688c864bd233 --- tests/YGPersistentNodeCloningTest.cpp | 184 ++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 tests/YGPersistentNodeCloningTest.cpp diff --git a/tests/YGPersistentNodeCloningTest.cpp b/tests/YGPersistentNodeCloningTest.cpp new file mode 100644 index 00000000..137da26c --- /dev/null +++ b/tests/YGPersistentNodeCloningTest.cpp @@ -0,0 +1,184 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include +#include + +#include +#include +#include + +namespace facebook::yoga { + +struct YGPersistentNodeCloningTest : public ::testing::Test { + struct NodeWrapper { + explicit NodeWrapper( + YGConfigRef config, + std::vector> children = {}) + : node{YGNodeNewWithConfig(config)}, children{std::move(children)} { + YGNodeSetContext(node, this); + + auto privateNode = resolveRef(node); + for (const auto& child : this->children) { + auto privateChild = resolveRef(child->node); + // Claim first ownership of not yet owned nodes, to avoid immediately + // cloning them + if (YGNodeGetOwner(child->node) == nullptr) { + privateChild->setOwner(privateNode); + } + privateNode->insertChild(privateChild, privateNode->getChildCount()); + } + } + + // Clone, with current children, for mutation + NodeWrapper(const NodeWrapper& other) + : node{YGNodeClone(other.node)}, children{other.children} { + YGNodeSetContext(node, this); + + auto privateNode = resolveRef(node); + privateNode->setOwner(nullptr); + } + + // Clone, with new children + NodeWrapper( + const NodeWrapper& other, + std::vector> children) + : node{YGNodeClone(other.node)}, children{std::move(children)} { + YGNodeSetContext(node, this); + + auto privateNode = resolveRef(node); + privateNode->setOwner(nullptr); + privateNode->setChildren({}); + privateNode->setDirty(true); + + for (const auto& child : this->children) { + auto privateChild = resolveRef(child->node); + // Claim first ownership of not yet owned nodes, to avoid immediately + // cloning them + if (YGNodeGetOwner(child->node) == nullptr) { + privateChild->setOwner(privateNode); + } + privateNode->insertChild(privateChild, privateNode->getChildCount()); + } + } + + NodeWrapper(NodeWrapper&&) = delete; + + ~NodeWrapper() { + YGNodeFree(node); + } + + NodeWrapper& operator=(const NodeWrapper& other) = delete; + NodeWrapper& operator=(NodeWrapper&& other) = delete; + + YGNodeRef node; + std::vector> children; + }; + + struct ConfigWrapper { + ConfigWrapper() { + YGConfigSetCloneNodeFunc( + config, + [](YGNodeConstRef oldNode, YGNodeConstRef owner, size_t childIndex) { + onClone(oldNode, owner, childIndex); + auto wrapper = static_cast(YGNodeGetContext(owner)); + auto old = static_cast(YGNodeGetContext(oldNode)); + + wrapper->children[childIndex] = std::make_shared(*old); + return wrapper->children[childIndex]->node; + }); + } + + ConfigWrapper(const ConfigWrapper&) = delete; + ConfigWrapper(ConfigWrapper&&) = delete; + + ~ConfigWrapper() { + YGConfigFree(config); + } + + ConfigWrapper& operator=(const ConfigWrapper&) = delete; + ConfigWrapper& operator=(ConfigWrapper&&) = delete; + + YGConfigRef config{YGConfigNew()}; + }; + + ConfigWrapper configWrapper; + YGConfigRef config{configWrapper.config}; + + void SetUp() override { + onClone = [](...) {}; + } + + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + static inline std::function + onClone; +}; + +TEST_F( + YGPersistentNodeCloningTest, + changing_sibling_height_does_not_clone_neighbors) { + // + // + // + // + // + // + // + // + // + // + + auto sibling = std::make_shared(config); + YGNodeStyleSetHeight(sibling->node, 1); + + auto d = std::make_shared(config); + auto c = std::make_shared(config, std::vector{d}); + auto b = std::make_shared(config, std::vector{c}); + auto a = std::make_shared(config, std::vector{b}); + YGNodeStyleSetHeight(a->node, 1); + + auto scrollContentView = + std::make_shared(config, std::vector{sibling, a}); + YGNodeStyleSetPositionType(scrollContentView->node, YGPositionTypeAbsolute); + + auto scrollView = + std::make_shared(config, std::vector{scrollContentView}); + YGNodeStyleSetWidth(scrollView->node, 100); + YGNodeStyleSetHeight(scrollView->node, 100); + + // We don't expect any cloning during the first layout + onClone = [](...) { FAIL(); }; + + YGNodeCalculateLayout( + scrollView->node, YGUndefined, YGUndefined, YGDirectionLTR); + + auto siblingPrime = std::make_shared(config); + YGNodeStyleSetHeight(siblingPrime->node, 2); + + auto scrollContentViewPrime = std::make_shared( + *scrollContentView, std::vector{siblingPrime, a}); + auto scrollViewPrime = std::make_shared( + *scrollView, std::vector{scrollContentViewPrime}); + + std::vector nodesCloned; + // We should only need to clone "A" + onClone = [&](YGNodeConstRef oldNode, + YGNodeConstRef /*owner*/, + size_t /*childIndex*/) { + nodesCloned.push_back(static_cast(YGNodeGetContext(oldNode))); + }; + + YGNodeCalculateLayout( + scrollViewPrime->node, YGUndefined, YGUndefined, YGDirectionLTR); + + EXPECT_EQ(nodesCloned.size(), 1); + EXPECT_EQ(nodesCloned[0], a.get()); +} + +} // namespace facebook::yoga