Skip to content

Commit 52c0094

Browse files
DevmateUtgenCppGeneralFBOrg Botfacebook-github-bot
authored andcommitted
xplat/js/react-native-github/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp
Differential Revision: D108739423
1 parent e04ff69 commit 52c0094

1 file changed

Lines changed: 159 additions & 0 deletions

File tree

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <react/fabric/FabricMountingManager.h>
9+
10+
#include <fbjni/fbjni.h>
11+
#include <gtest/gtest.h>
12+
13+
#include <memory>
14+
15+
namespace facebook::react {
16+
17+
/**
18+
* Pure-C++ unit tests for the surface-registry behavior of
19+
* `FabricMountingManager`. The class as a whole is heavily JNI-coupled; full
20+
* end-to-end coverage lives in the Robolectric-driven instrumentation tests
21+
* under
22+
* `ReactAndroid/src/test/java/com/facebook/react/fabric/FabricMountingManagerInstrumentationTest`.
23+
*
24+
* These tests focus on the small, JNI-free slice of the public API:
25+
* - `onSurfaceStart` — populates the allocated-view registry for a new
26+
* surface.
27+
* - `onSurfaceStop` — removes the registry entry for a surface.
28+
* - `isViewAllocated` — returns whether a `(surfaceId, tag)` pair is
29+
* registered.
30+
*
31+
* These three methods only touch in-memory state guarded by
32+
* `allocatedViewsMutex_`; they never call into Java. That is what makes them
33+
* unit-testable here.
34+
*
35+
* NOTE: `~FabricMountingManager()` calls `jni::ThreadScope::WithClassLoader`,
36+
* which throws `std::runtime_error` when no JavaVM has been attached (see
37+
* `xplat/spectrum/androidLibs/fbjni/cxx/fbjni/detail/Environment.h`). Because
38+
* no JVM is available in this host-side test, the manager instance is
39+
* intentionally never destroyed: each test allocates a single
40+
* `FabricMountingManager` on the heap and wraps it in a `std::shared_ptr`
41+
* with a no-op deleter. The resulting per-test leak is bounded (one ~100-byte
42+
* instance) and does not cross test boundaries.
43+
*/
44+
class FabricMountingManagerTest : public ::testing::Test {
45+
protected:
46+
// Returns a `FabricMountingManager` whose destructor is suppressed.
47+
// See the class-level note above for why this is necessary.
48+
static std::shared_ptr<FabricMountingManager> makeManager() {
49+
// `jni::global_ref<>` default-constructs to an empty (null) reference,
50+
// so no JNI calls are performed during construction. The empty
51+
// reference is safe to copy into the manager because the
52+
// surface-registry methods under test never dereference
53+
// `javaUIManager_`.
54+
auto* emptyRef = new jni::global_ref<JFabricUIManager::javaobject>();
55+
auto* raw = new FabricMountingManager(*emptyRef);
56+
// `emptyRef` is intentionally leaked: resetting it would also engage
57+
// `WithClassLoader`, which requires an attached JavaVM.
58+
return {raw, [](FabricMountingManager* /*unused*/) noexcept {
59+
// No-op deleter: see class-level note.
60+
}};
61+
}
62+
};
63+
64+
/*
65+
* Verifies that `onSurfaceStart` seeds the allocated-view registry for the
66+
* new surface with the surface tag itself. Downstream, `executeMount` relies
67+
* on this seeding to detect "executing commit after surface ... was stopped"
68+
* and to short-circuit a redundant Create mount item for the root view.
69+
*
70+
* Bug this catches: if a refactor stops registering the root surface tag
71+
* (e.g. switches to `unordered_set<Tag>{}` instead of
72+
* `unordered_set<Tag>({surfaceId})`), the root view would be re-created on
73+
* every commit. Without this assertion, that regression would only surface
74+
* via end-to-end Java tests.
75+
*/
76+
TEST_F(
77+
FabricMountingManagerTest,
78+
onSurfaceStartRegistersSurfaceTagAsAllocated) {
79+
auto manager = makeManager();
80+
constexpr SurfaceId kSurfaceId = 42;
81+
82+
manager->onSurfaceStart(kSurfaceId);
83+
84+
EXPECT_TRUE(manager->isViewAllocated(kSurfaceId, /*tag=*/kSurfaceId));
85+
}
86+
87+
/*
88+
* Verifies that `isViewAllocated` returns false for a surface that has not
89+
* been started. The contract is "false when the surface is unknown", not
90+
* "throw" or "abort"; `executeMount` and `preallocateShadowView` both rely
91+
* on this to gracefully no-op when a surface has been stopped concurrently.
92+
*/
93+
TEST_F(
94+
FabricMountingManagerTest,
95+
isViewAllocatedReturnsFalseForSurfaceThatWasNeverStarted) {
96+
auto manager = makeManager();
97+
98+
// Tag is arbitrary; the registry has no entry for surface 9999 so any
99+
// tag must report unallocated.
100+
EXPECT_FALSE(manager->isViewAllocated(/*surfaceId=*/9999, /*tag=*/12345));
101+
}
102+
103+
/*
104+
* Verifies that `onSurfaceStop` removes the surface's entire registry entry
105+
* — not just clears its contents. After `onSurfaceStop`, `isViewAllocated`
106+
* must return false for *any* tag on that surface, including the surface
107+
* tag itself (which `onSurfaceStart` had inserted).
108+
*
109+
* Bug this catches: if `onSurfaceStop` only `.clear()`-ed the inner set
110+
* instead of erasing the outer map entry, `executeMount`'s "surface was
111+
* stopped" log would never fire, masking commit-after-stop bugs.
112+
*/
113+
TEST_F(
114+
FabricMountingManagerTest,
115+
onSurfaceStopRemovesEntireRegistryForSurface) {
116+
auto manager = makeManager();
117+
constexpr SurfaceId kSurfaceId = 7;
118+
manager->onSurfaceStart(kSurfaceId);
119+
ASSERT_TRUE(manager->isViewAllocated(kSurfaceId, kSurfaceId));
120+
121+
manager->onSurfaceStop(kSurfaceId);
122+
123+
EXPECT_FALSE(manager->isViewAllocated(kSurfaceId, kSurfaceId));
124+
}
125+
126+
/*
127+
* Verifies that registries for distinct surfaces are isolated: starting and
128+
* stopping one surface must not leak state into another. This is the
129+
* concurrency-safety invariant that lets multiple React surfaces coexist in
130+
* the same process.
131+
*
132+
* Bug this catches: if a future change accidentally shared the
133+
* `unordered_set<Tag>` across surfaces (e.g. via a misplaced static), tags
134+
* from one surface would falsely appear as allocated on another, and
135+
* stopping any surface would purge unrelated surfaces' registries.
136+
*/
137+
TEST_F(
138+
FabricMountingManagerTest,
139+
onSurfaceStartCreatesIsolatedRegistriesPerSurface) {
140+
auto manager = makeManager();
141+
constexpr SurfaceId kSurfaceA = 1;
142+
constexpr SurfaceId kSurfaceB = 2;
143+
144+
manager->onSurfaceStart(kSurfaceA);
145+
manager->onSurfaceStart(kSurfaceB);
146+
147+
// Each surface knows about its own tag but not the other surface's tag.
148+
EXPECT_TRUE(manager->isViewAllocated(kSurfaceA, /*tag=*/kSurfaceA));
149+
EXPECT_FALSE(manager->isViewAllocated(kSurfaceA, /*tag=*/kSurfaceB));
150+
EXPECT_TRUE(manager->isViewAllocated(kSurfaceB, /*tag=*/kSurfaceB));
151+
EXPECT_FALSE(manager->isViewAllocated(kSurfaceB, /*tag=*/kSurfaceA));
152+
153+
// Stopping surface A must not affect surface B.
154+
manager->onSurfaceStop(kSurfaceA);
155+
EXPECT_FALSE(manager->isViewAllocated(kSurfaceA, kSurfaceA));
156+
EXPECT_TRUE(manager->isViewAllocated(kSurfaceB, kSurfaceB));
157+
}
158+
159+
} // namespace facebook::react

0 commit comments

Comments
 (0)