|
| 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