diff --git a/ReactCommon/react/renderer/mounting/StubViewTree.cpp b/ReactCommon/react/renderer/mounting/StubViewTree.cpp index bf596ea33ce606..703d621d061446 100644 --- a/ReactCommon/react/renderer/mounting/StubViewTree.cpp +++ b/ReactCommon/react/renderer/mounting/StubViewTree.cpp @@ -72,7 +72,7 @@ void StubViewTree::mutate(ShadowViewMutationList const &mutations) { auto stubView = registry[tag]; if ((ShadowView)(*stubView) != mutation.oldChildShadowView) { LOG(ERROR) - << "StubView: ASSERT FAILURE: DELETE mutation assertion failure: oldChildShadowView doesn't match stubView: [" + << "StubView: ASSERT FAILURE: DELETE mutation assertion failure: oldChildShadowView does not match stubView: [" << mutation.oldChildShadowView.tag << "] stub hash: ##" << std::hash{}((ShadowView)*stubView) << " old mutation hash: ##" @@ -154,7 +154,7 @@ void StubViewTree::mutate(ShadowViewMutationList const &mutations) { auto childStubView = registry[childTag]; if ((ShadowView)(*childStubView) != mutation.oldChildShadowView) { LOG(ERROR) - << "StubView: ASSERT FAILURE: REMOVE mutation assertion failure: oldChildShadowView doesn't match oldStubView: [" + << "StubView: ASSERT FAILURE: REMOVE mutation assertion failure: oldChildShadowView does not match oldStubView: [" << mutation.oldChildShadowView.tag << "] stub hash: ##" << std::hash{}((ShadowView)*childStubView) << " old mutation hash: ##" @@ -214,7 +214,7 @@ void StubViewTree::mutate(ShadowViewMutationList const &mutations) { react_native_assert(oldStubView->tag != 0); if ((ShadowView)(*oldStubView) != mutation.oldChildShadowView) { LOG(ERROR) - << "StubView: ASSERT FAILURE: UPDATE mutation assertion failure: oldChildShadowView doesn't match oldStubView: [" + << "StubView: ASSERT FAILURE: UPDATE mutation assertion failure: oldChildShadowView does not match oldStubView: [" << mutation.oldChildShadowView.tag << "] old stub hash: ##" << std::hash{}((ShadowView)*oldStubView) << " old mutation hash: ##" diff --git a/ReactCommon/react/renderer/mounting/tests/Entropy.h b/ReactCommon/react/renderer/mounting/tests/Entropy.h index 54a87118bb179f..7bbaf91012f2e2 100644 --- a/ReactCommon/react/renderer/mounting/tests/Entropy.h +++ b/ReactCommon/react/renderer/mounting/tests/Entropy.h @@ -75,6 +75,10 @@ class Entropy final { result = generator() % 10000 < 10000 * ratio; } + void generateRandomValue(Generator &generator, int &result) const { + result = generator(); + } + void generateRandomValue(Generator &generator, int &result, int min, int max) const { std::uniform_int_distribution distribution(min, max); diff --git a/ReactCommon/react/renderer/mounting/tests/ShadowTreeLifeCycleTest.cpp b/ReactCommon/react/renderer/mounting/tests/ShadowTreeLifeCycleTest.cpp index 5b408b3a15067c..1362d2ee65ac0f 100644 --- a/ReactCommon/react/renderer/mounting/tests/ShadowTreeLifeCycleTest.cpp +++ b/ReactCommon/react/renderer/mounting/tests/ShadowTreeLifeCycleTest.cpp @@ -87,7 +87,7 @@ static void testShadowNodeTreeLifeCycle( { &messWithChildren, &messWithYogaStyles, - &messWithLayotableOnlyFlag, + &messWithLayoutableOnlyFlag, }); std::vector affectedLayoutableNodes{}; @@ -142,7 +142,155 @@ static void testShadowNodeTreeLifeCycle( // There are some issues getting `getDebugDescription` to compile // under test on Android for now. -#ifndef ANDROID +#ifdef RN_DEBUG_STRING_CONVERTIBLE + LOG(ERROR) << "Shadow Tree before: \n" + << currentRootNode->getDebugDescription(); + LOG(ERROR) << "Shadow Tree after: \n" + << nextRootNode->getDebugDescription(); + + LOG(ERROR) << "View Tree before: \n" + << getDebugDescription(viewTree.getRootStubView(), {}); + LOG(ERROR) << "View Tree after: \n" + << getDebugDescription( + rebuiltViewTree.getRootStubView(), {}); + + LOG(ERROR) << "Mutations:" + << "\n" + << getDebugDescription(mutations, {}); +#endif + + FAIL(); + } + + currentRootNode = nextRootNode; + } + } + + SUCCEED(); +} + +static void testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening( + uint_fast32_t seed, + int treeSize, + int repeats, + int stages) { + auto entropy = seed == 0 ? Entropy() : Entropy(seed); + + auto eventDispatcher = EventDispatcher::Shared{}; + auto contextContainer = std::make_shared(); + auto componentDescriptorParameters = + ComponentDescriptorParameters{eventDispatcher, contextContainer, nullptr}; + auto viewComponentDescriptor = + ViewComponentDescriptor(componentDescriptorParameters); + auto rootComponentDescriptor = + RootComponentDescriptor(componentDescriptorParameters); + auto noopEventEmitter = + std::make_shared(nullptr, -1, eventDispatcher); + + auto allNodes = std::vector{}; + + for (int i = 0; i < repeats; i++) { + allNodes.clear(); + + auto family = rootComponentDescriptor.createFamily( + {Tag(1), SurfaceId(1), nullptr}, nullptr); + + // Creating an initial root shadow node. + auto emptyRootNode = std::const_pointer_cast( + std::static_pointer_cast( + rootComponentDescriptor.createShadowNode( + ShadowNodeFragment{RootShadowNode::defaultSharedProps()}, + family))); + + // Applying size constraints. + emptyRootNode = emptyRootNode->clone( + LayoutConstraints{ + Size{512, 0}, Size{512, std::numeric_limits::infinity()}}, + LayoutContext{}); + + // Generation of a random tree. + auto singleRootChildNode = + generateShadowNodeTree(entropy, viewComponentDescriptor, treeSize); + + // Injecting a tree into the root node. + auto currentRootNode = std::static_pointer_cast( + emptyRootNode->ShadowNode::clone(ShadowNodeFragment{ + ShadowNodeFragment::propsPlaceholder(), + std::make_shared( + SharedShadowNodeList{singleRootChildNode})})); + + // Building an initial view hierarchy. + auto viewTree = buildStubViewTreeWithoutUsingDifferentiator(*emptyRootNode); + viewTree.mutate( + calculateShadowViewMutations(*emptyRootNode, *currentRootNode, true)); + + for (int j = 0; j < stages; j++) { + auto nextRootNode = currentRootNode; + + // Mutating the tree. + alterShadowTree( + entropy, + nextRootNode, + { + &messWithYogaStyles, + &messWithLayoutableOnlyFlag, + }); + alterShadowTree(entropy, nextRootNode, &messWithNodeFlattenednessFlags); + alterShadowTree(entropy, nextRootNode, &messWithChildren); + + std::vector affectedLayoutableNodes{}; + affectedLayoutableNodes.reserve(1024); + + // Laying out the tree. + std::const_pointer_cast(nextRootNode) + ->layoutIfNeeded(&affectedLayoutableNodes); + + nextRootNode->sealRecursive(); + allNodes.push_back(nextRootNode); + + // Calculating mutations. + auto mutations = + calculateShadowViewMutations(*currentRootNode, *nextRootNode, true); + + // Make sure that in a single frame, a DELETE for a + // view is not followed by a CREATE for the same view. + { + std::vector deletedTags{}; + for (auto const &mutation : mutations) { + if (mutation.type == ShadowViewMutation::Type::Delete) { + deletedTags.push_back(mutation.oldChildShadowView.tag); + } + } + for (auto const &mutation : mutations) { + if (mutation.type == ShadowViewMutation::Type::Create) { + if (std::find( + deletedTags.begin(), + deletedTags.end(), + mutation.newChildShadowView.tag) != deletedTags.end()) { + LOG(ERROR) << "Deleted tag was recreated in mutations list: [" + << mutation.newChildShadowView.tag << "]"; + FAIL(); + } + } + } + } + + // Mutating the view tree. + viewTree.mutate(mutations); + + // Building a view tree to compare with. + auto rebuiltViewTree = + buildStubViewTreeWithoutUsingDifferentiator(*nextRootNode); + + // Comparing the newly built tree with the updated one. + if (rebuiltViewTree != viewTree) { + // Something went wrong. + + LOG(ERROR) << "Entropy seed: " << entropy.getSeed() << "\n"; + + // There are some issues getting `getDebugDescription` to compile + // under test on Android for now. +#ifdef RN_DEBUG_STRING_CONVERTIBLE LOG(ERROR) << "Shadow Tree before: \n" << currentRootNode->getDebugDescription(); LOG(ERROR) << "Shadow Tree after: \n" @@ -203,3 +351,33 @@ TEST( /* repeats */ 512, /* stages */ 32); } + +TEST( + ShadowTreeLifecyleTest, + unstableSmallerTreeFewerIterationsExtensiveFlatteningUnflattening) { + testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening( + /* seed */ 1337, + /* size */ 32, + /* repeats */ 32, + /* stages */ 32); +} + +TEST( + ShadowTreeLifecyleTest, + unstableBiggerTreeFewerIterationsExtensiveFlatteningUnflattening) { + testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening( + /* seed */ 1337, + /* size */ 256, + /* repeats */ 32, + /* stages */ 32); +} + +TEST( + ShadowTreeLifecyleTest, + unstableSmallerTreeMoreIterationsExtensiveFlatteningUnflattening) { + testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening( + /* seed */ 1337, + /* size */ 32, + /* repeats */ 512, + /* stages */ 32); +} diff --git a/ReactCommon/react/renderer/mounting/tests/shadowTreeGeneration.h b/ReactCommon/react/renderer/mounting/tests/shadowTreeGeneration.h index 093272090d1246..bd17f9036aa951 100644 --- a/ReactCommon/react/renderer/mounting/tests/shadowTreeGeneration.h +++ b/ReactCommon/react/renderer/mounting/tests/shadowTreeGeneration.h @@ -116,7 +116,7 @@ static inline ShadowNode::Unshared messWithChildren( std::make_shared(children)}); } -static inline ShadowNode::Unshared messWithLayotableOnlyFlag( +static inline ShadowNode::Unshared messWithLayoutableOnlyFlag( Entropy const &entropy, ShadowNode const &shadowNode) { auto oldProps = shadowNode.getProps(); @@ -150,7 +150,7 @@ static inline ShadowNode::Unshared messWithLayotableOnlyFlag( } if (entropy.random(0.1)) { - viewProps.zIndex = entropy.random() ? 1 : 0; + viewProps.zIndex = entropy.random(); } if (entropy.random(0.1)) { @@ -163,6 +163,48 @@ static inline ShadowNode::Unshared messWithLayotableOnlyFlag( : Transform::Perspective(42); } + if (entropy.random(0.1)) { + viewProps.elevation = entropy.random() ? 1 : 0; + } + + return shadowNode.clone({newProps}); +} + +// Similar to `messWithLayoutableOnlyFlag` but has a 50/50 chance of flattening +// (or unflattening) a node's children. +static inline ShadowNode::Unshared messWithNodeFlattenednessFlags( + Entropy const &entropy, + ShadowNode const &shadowNode) { + auto oldProps = shadowNode.getProps(); + auto newProps = shadowNode.getComponentDescriptor().cloneProps( + oldProps, RawProps(folly::dynamic::object())); + + auto &viewProps = + const_cast(static_cast(*newProps)); + + if (entropy.random(0.5)) { + viewProps.nativeId = ""; + viewProps.collapsable = true; + viewProps.backgroundColor = SharedColor(); + viewProps.foregroundColor = SharedColor(); + viewProps.shadowColor = SharedColor(); + viewProps.accessible = false; + viewProps.zIndex = {}; + viewProps.pointerEvents = PointerEventsMode::Auto; + viewProps.transform = Transform::Identity(); + viewProps.elevation = 0; + } else { + viewProps.nativeId = "42"; + viewProps.backgroundColor = whiteColor(); + viewProps.foregroundColor = blackColor(); + viewProps.shadowColor = blackColor(); + viewProps.accessible = true; + viewProps.zIndex = {entropy.random()}; + viewProps.pointerEvents = PointerEventsMode::None; + viewProps.transform = Transform::Perspective(entropy.random()); + viewProps.elevation = entropy.random(); + } + return shadowNode.clone({newProps}); }