Skip to content

Commit 7f27960

Browse files
[RN] Run ReactCommon C++ unit tests on GitHub CI
ReactCommon's C++ unit tests (`<subsystem>/tests/*.cpp`) ship with the repository but were not built or run by CI, so the renderer, layout, and runtime C++ core was only exercised indirectly (integration tests and platform builds). This adds a CMake harness under `private/react-native-tests` that compiles a representative subset of those gtest suites on Linux and runs them with CTest, reusing the desktop C++ toolchain and Gradle-staged third-party dependencies already used by the Fantom tester. A new `test_cxx` GitHub Actions job builds and runs the harness on a standard Linux runner. It currently covers 31 gtest suites (~900 cases) spanning the Fabric renderer, layout, CSS, cxxreact bridge, and bridgeless runtime core; more can be added incrementally (see `private/react-native-tests/README.md`). Compilation is routed through ccache (installed by the action, enabled via `CMAKE_CXX_COMPILER_LAUNCHER`) and the job asserts a nonzero cacheable-call count. The CI job is advisory (`continue-on-error`, bounded by `timeout-minutes`) so a harness issue cannot block merges, and should be promoted to a required check once consistently green. Changelog: [Internal] Test Plan: Runs in the new advisory `test_cxx` CI job on this PR (Linux): builds the CMake harness, runs the 31 gtest suites via CTest, and asserts ccache intercepted compiles. Not built internally per react-native-github build rules (internal builds use Buck, not Gradle).
1 parent 7115c2b commit 7f27960

11 files changed

Lines changed: 928 additions & 0 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
name: Run C++ Tests
2+
description: Builds and runs the ReactCommon C++ (gtest) unit-test harness on Linux
3+
inputs:
4+
gradle-cache-encryption-key:
5+
description: 'The encryption key needed to store the Gradle Configuration cache'
6+
runs:
7+
using: composite
8+
steps:
9+
- name: Install dependencies
10+
shell: bash
11+
run: |
12+
set -e
13+
sudo apt update
14+
# ccache: enabled via CMAKE_CXX_COMPILER_LAUNCHER in cxx/CMakeLists.txt.
15+
# CMake itself is the Android SDK's (3.30.5), driven by Gradle — the
16+
# distro package is intentionally not installed.
17+
sudo apt install -y ccache clang git libssl-dev openssl
18+
- name: Setup git safe folders
19+
shell: bash
20+
run: git config --global --add safe.directory '*'
21+
- name: Setup node.js
22+
uses: ./.github/actions/setup-node
23+
- name: Install node dependencies
24+
uses: ./.github/actions/yarn-install
25+
- name: Setup gradle
26+
uses: ./.github/actions/setup-gradle
27+
with:
28+
cache-read-only: 'false'
29+
cache-encryption-key: ${{ inputs.gradle-cache-encryption-key }}
30+
- name: Restore C++ tests ccache
31+
uses: actions/cache/restore@v5
32+
with:
33+
path: /github/home/.cache/ccache
34+
key:
35+
v1-ccache-cxx-tests-${{ github.job }}-${{ github.ref }}-${{ hashFiles(
36+
'packages/react-native/ReactCommon/**/*.cpp',
37+
'packages/react-native/ReactCommon/**/*.h',
38+
'packages/react-native/ReactCommon/**/CMakeLists.txt'
39+
) }}
40+
restore-keys: |
41+
v1-ccache-cxx-tests-${{ github.job }}-${{ github.ref }}-
42+
v1-ccache-cxx-tests-${{ github.job }}-
43+
v1-ccache-cxx-tests-
44+
- name: Show ccache stats (before)
45+
shell: bash
46+
run: ccache -s -v
47+
- name: Build and run C++ tests
48+
shell: bash
49+
run: yarn workspace @react-native/tests-cxx test
50+
env:
51+
CC: clang
52+
CXX: clang++
53+
# libhermesvm.so is a shared library; gtest_discover_tests runs the
54+
# binaries at build time and CTest runs them again, so it must be
55+
# loadable in both phases.
56+
LD_LIBRARY_PATH: ${{ github.workspace }}/packages/react-native/ReactAndroid/hermes-engine/build/hermes/lib
57+
- name: Save C++ tests ccache
58+
if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, '-stable') }}
59+
uses: actions/cache/save@v5
60+
with:
61+
path: /github/home/.cache/ccache
62+
key:
63+
v1-ccache-cxx-tests-${{ github.job }}-${{ github.ref }}-${{ hashFiles(
64+
'packages/react-native/ReactCommon/**/*.cpp',
65+
'packages/react-native/ReactCommon/**/*.h',
66+
'packages/react-native/ReactCommon/**/CMakeLists.txt'
67+
) }}
68+
- name: Show ccache stats (after) and assert compiles were cacheable
69+
shell: bash
70+
run: |
71+
ccache -s -v
72+
# Zero cacheable calls means the CMake compiler launcher isn't wired to
73+
# ccache and the restored cache is dead weight. The job is advisory during
74+
# rollout, so this surfaces the defect without blocking merges.
75+
calls=$(ccache --print-stats | awk -F'\t' \
76+
'/^(direct_cache_hit|preprocessed_cache_hit|cache_miss)$/ {s+=$2} END {print s+0}')
77+
echo "ccache cacheable calls this run: $calls"
78+
test "$calls" -ge 1
79+
- name: Upload test results
80+
if: ${{ always() }}
81+
uses: actions/upload-artifact@v6
82+
with:
83+
name: run-cxx-tests-results
84+
compression-level: 1
85+
path: |
86+
private/react-native-tests/build/reports

.github/workflows/test-all.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,37 @@ jobs:
300300
fail-on-error: true
301301
secrets: inherit
302302

303+
test_cxx:
304+
runs-on: 8-core-ubuntu
305+
needs: [check_code_changes, lint]
306+
if: needs.check_code_changes.outputs.any_code_change == 'true'
307+
# Bound this advisory build+network job so a hang can't tie up a runner.
308+
timeout-minutes: 60
309+
# Advisory during rollout: a harness issue must not block merges. Once this
310+
# job is consistently green, drop continue-on-error and add it to branch
311+
# protection so it becomes a required merge gate.
312+
continue-on-error: true
313+
container:
314+
# `:latest` matches every other job in this workflow (repo convention). The
315+
# harness is kept resilient to image drift instead of pinning one job out of
316+
# step: ccache is installed explicitly and the SDK CMake version is pinned in
317+
# build.gradle.kts, so it does not rely on whatever the image happens to ship.
318+
image: reactnativecommunity/react-native-android:latest
319+
env:
320+
# Set the encoding to resolve a known character encoding issue with decompressing tar.gz files in containers
321+
# via Gradle: https://github.com/gradle/gradle/issues/23391#issuecomment-1878979127
322+
LC_ALL: C.UTF8
323+
TERM: 'dumb'
324+
GRADLE_OPTS: '-Dorg.gradle.daemon=false'
325+
REACT_NATIVE_DOWNLOADS_DIR: /opt/react-native-downloads
326+
steps:
327+
- name: Checkout
328+
uses: actions/checkout@v6
329+
- name: Run C++ Tests
330+
uses: ./.github/actions/run-cxx-tests
331+
with:
332+
gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
333+
303334
build_android:
304335
runs-on: 8-core-ubuntu
305336
needs: [set_release_type, check_code_changes]
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# @react-native/tests-cxx
2+
3+
A Linux + CMake harness that builds and runs a **representative subset** of
4+
ReactCommon's C++ (gtest) unit tests on GitHub CI.
5+
6+
## Why this exists
7+
8+
ReactCommon's C++ test sources (`<subsystem>/tests/*.cpp`) are part of the
9+
repository, but the build rules that compile and run them are Meta-internal and
10+
are not part of the open-source tree. As a result these unit tests did not run
11+
on GitHub CI at all — the renderer, layout, and runtime C++ core was only
12+
covered by higher-level integration tests (Fantom) and by platform builds.
13+
14+
This harness closes that gap cheaply: C++ unit tests build and run on standard
15+
(free) Linux runners, so GitHub CI can gate merges on them.
16+
17+
The goal is a **representative**, high-signal subset — not full coverage. Suites
18+
are added deliberately, weighted by how often they catch regressions and by
19+
build cost.
20+
21+
## How it works
22+
23+
The harness reuses the exact desktop C++ toolchain already proven by the Fantom
24+
tester (`private/react-native-fantom`):
25+
26+
- **Gradle** (`build.gradle.kts`) stages the third-party dependencies (folly,
27+
boost, glog, double-conversion, fast_float, fmt, gflags) by depending on the
28+
Fantom tester's `prepareNative3pDependencies`, then invokes CMake.
29+
- **CMake** (`cxx/CMakeLists.txt`) compiles each in-scope ReactCommon subsystem
30+
into its library and links its `tests/*.cpp` into a per-subsystem gtest binary,
31+
registered with CTest. GoogleTest itself is fetched via `FetchContent`.
32+
33+
## Coverage
34+
35+
31 suites (~900 gtest cases) run today, spanning the C++ core:
36+
37+
- **Primitives / parsing / serialization:** graphics, css, utils, mapbuffer,
38+
timing, featureflags
39+
- **Fabric renderer:** renderer/core, mounting, components/{view, text,
40+
scrollview, image, root}, attributedstring, textlayoutmanager, element,
41+
componentregistry (via deps), imagemanager, renderer/debug
42+
- **Scheduling / runtime:** runtimescheduler, scheduler, uimanager,
43+
uimanager/consistency, performance/timeline, animated, animations
44+
- **Debugger / infra:** debug/redbox, reactperflogger/fusebox, jserrorhandler,
45+
telemetry, cxxreact
46+
47+
Suites for `renderer/core`, `runtimescheduler`, `scheduler` and `animated`
48+
create a JS runtime and link the Hermes VM.
49+
50+
Not yet included: `jsinspector-modern` and `jsinspector-modern/tracing` (their
51+
tests depend on a specific GoogleTest version / `std::source_location` support
52+
that this harness's GoogleTest doesn't match), and `react/bridging` (its test
53+
helper header uses a Buck header-namespace that doesn't map to CMake).
54+
55+
## Running locally (Meta-internal)
56+
57+
Requires the Android SDK's CMake and the same environment used to build the
58+
Fantom tester.
59+
60+
```sh
61+
yarn workspace @react-native/tests-cxx test # build + run (ctest)
62+
yarn workspace @react-native/tests-cxx build # build only
63+
```
64+
65+
## Adding a subsystem
66+
67+
1. Add the subsystem and any missing dependencies to the
68+
`add_react_common_subdir(...)` list in `cxx/CMakeLists.txt`.
69+
2. Add a `react_native_add_cxx_test_suite(...)` call listing the subsystem's
70+
full library closure (subsystem lib + its ReactCommon deps + third-party).
71+
ReactCommon libraries are CMake `OBJECT` libraries, so the closure must be
72+
listed explicitly rather than relying on transitive linking.
73+
3. Confirm the new suite builds and passes in the `test_cxx` CI job.
74+
75+
## Rollout
76+
77+
The `test_cxx` GitHub Actions job starts **advisory** (`continue-on-error`) so a
78+
harness issue cannot block merges. Once it is consistently green it should be
79+
made a **required** check via branch protection.
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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+
import com.facebook.react.tasks.internal.*
9+
import com.facebook.react.tasks.internal.utils.*
10+
11+
plugins { id("com.facebook.react") }
12+
13+
// CMake shipped with the Android SDK (same version the Fantom tester uses). The
14+
// CI container provides it and this path is executed directly, so the component
15+
// must exist; override CMAKE_VERSION to point at a different installed SDK CMake.
16+
val cmakeVersion = System.getenv("CMAKE_VERSION") ?: "3.30.5"
17+
val cmakePath = "${getSDKPath()}/cmake/$cmakeVersion"
18+
val cmakeBinaryPath = "${cmakePath}/bin/cmake"
19+
val ctestBinaryPath = "${cmakePath}/bin/ctest"
20+
val buildJobs = Runtime.getRuntime().availableProcessors().toString()
21+
22+
fun getSDKPath(): String {
23+
val androidSdkRoot = System.getenv("ANDROID_SDK_ROOT")
24+
val androidHome = System.getenv("ANDROID_HOME")
25+
return when {
26+
!androidSdkRoot.isNullOrBlank() -> androidSdkRoot
27+
!androidHome.isNullOrBlank() -> androidHome
28+
else -> throw IllegalStateException("Neither ANDROID_SDK_ROOT nor ANDROID_HOME is set.")
29+
}
30+
}
31+
32+
val buildDir = project.layout.buildDirectory.get().asFile
33+
val reportsDir = File("$buildDir/reports")
34+
val reactNativeRootDir = projectDir.parentFile.parentFile
35+
val reactNativeDir = File("$reactNativeRootDir/packages/react-native")
36+
val reactAndroidDir = File("$reactNativeDir/ReactAndroid")
37+
val reactAndroidBuildDir = File("$reactAndroidDir/build")
38+
39+
// The C++ test harness reuses the third-party dependencies staged by the Fantom
40+
// tester build: folly and gflags land in <fantom>/build/third-party; glog,
41+
// double-conversion, fast_float, fmt and boost land in
42+
// ReactAndroid/build/third-party-ndk. Depending on Fantom's
43+
// prepareNative3pDependencies guarantees all of them are prepared.
44+
val fantomDir = File("$reactNativeRootDir/private/react-native-fantom")
45+
val stagedThirdPartyDir = File("$fantomDir/build/third-party")
46+
val testerThirdPartySrcDir = File("$fantomDir/tester/third-party")
47+
48+
val cxxDir = File("$projectDir/cxx")
49+
val cxxBuildDir = File("$buildDir/cxx")
50+
val cxxBuildOutputFileTree =
51+
fileTree(cxxBuildDir.toString())
52+
.include("**/*.cmake", "**/*.marks", "**/compiler_depends.ts", "**/Makefile", "**/link.txt")
53+
54+
val createReportsDir by tasks.registering { reportsDir.mkdirs() }
55+
56+
// Generated codegen sources (FBReactNativeSpec) that Fabric component tests
57+
// depend on. `generateCodegenArtifactsFromSchema` produces them under
58+
// ReactAndroid/build/generated; stage them next to codegen/CMakeLists.txt.
59+
val codegenSrcDir = File("$reactAndroidBuildDir/generated/source/codegen/jni")
60+
val codegenOutDir = File("$buildDir/codegen")
61+
val prepareRNCodegen by
62+
tasks.registering(Copy::class) {
63+
dependsOn(":packages:react-native:ReactAndroid:generateCodegenArtifactsFromSchema")
64+
from(codegenSrcDir)
65+
from("codegen")
66+
include("react/**/*.h", "react/**/*.cpp", "CMakeLists.txt")
67+
includeEmptyDirs = false
68+
duplicatesStrategy = DuplicatesStrategy.INCLUDE
69+
into(codegenOutDir)
70+
}
71+
72+
// Hermes VM + prefab headers, for suites whose tests create a JS runtime
73+
// (hermes::makeHermesRuntime). Mirrors the Fantom tester's Hermes setup.
74+
val enableHermesBuild by tasks.registering {
75+
project(":packages:react-native:ReactAndroid:hermes-engine") {
76+
tasks.configureEach { enabled = true }
77+
}
78+
}
79+
val prepareHermesDependencies by tasks.registering {
80+
dependsOn(
81+
enableHermesBuild,
82+
":packages:react-native:ReactAndroid:hermes-engine:buildHermesLibWithDebugger",
83+
":packages:react-native:ReactAndroid:hermes-engine:prepareHeadersForPrefabWithDebugger",
84+
)
85+
}
86+
87+
val configureCxxTests by
88+
tasks.registering(CustomExecTask::class) {
89+
dependsOn(
90+
createReportsDir,
91+
prepareRNCodegen,
92+
prepareHermesDependencies,
93+
":private:react-native-fantom:prepareNative3pDependencies",
94+
)
95+
workingDir(cxxDir)
96+
inputs.dir(cxxDir)
97+
outputs.files(cxxBuildOutputFileTree)
98+
commandLine(
99+
cmakeBinaryPath,
100+
"--log-level=ERROR",
101+
"-S",
102+
".",
103+
"-B",
104+
cxxBuildDir.toString(),
105+
"-DCMAKE_BUILD_TYPE=Debug",
106+
"-DREACT_ANDROID_DIR=$reactAndroidDir",
107+
"-DREACT_COMMON_DIR=$reactNativeDir/ReactCommon",
108+
"-DREACT_THIRD_PARTY_NDK_DIR=$reactAndroidBuildDir/third-party-ndk",
109+
"-DRN_STAGED_THIRD_PARTY_DIR=$stagedThirdPartyDir",
110+
"-DRN_TESTER_THIRD_PARTY_SRC_DIR=$testerThirdPartySrcDir",
111+
"-DRN_CODEGEN_DIR=$codegenOutDir",
112+
"-DRN_ENABLE_DEBUG_STRING_CONVERTIBLE=ON",
113+
)
114+
standardOutputFile.set(project.file("$buildDir/reports/configure-cxx-tests.log"))
115+
errorOutputFile.set(project.file("$buildDir/reports/configure-cxx-tests.error.log"))
116+
}
117+
118+
val buildCxxTests by
119+
tasks.registering(CustomExecTask::class) {
120+
dependsOn(configureCxxTests)
121+
workingDir(cxxDir)
122+
inputs.files(cxxBuildOutputFileTree)
123+
commandLine(cmakeBinaryPath, "--build", cxxBuildDir.toString(), "-j", buildJobs)
124+
standardOutputFile.set(project.file("$buildDir/reports/build-cxx-tests.log"))
125+
errorOutputFile.set(project.file("$buildDir/reports/build-cxx-tests.error.log"))
126+
}
127+
128+
val runCxxTests by
129+
tasks.registering(CustomExecTask::class) {
130+
dependsOn(buildCxxTests)
131+
workingDir(cxxBuildDir)
132+
commandLine(
133+
ctestBinaryPath,
134+
"--output-on-failure",
135+
"--output-junit",
136+
"$reportsDir/cxx-tests-results.xml",
137+
)
138+
standardOutputFile.set(project.file("$buildDir/reports/run-cxx-tests.log"))
139+
errorOutputFile.set(project.file("$buildDir/reports/run-cxx-tests.error.log"))
140+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/bash
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+
set -e
8+
9+
pushd ../..
10+
./gradlew :private:react-native-tests:buildCxxTests
11+
popd

0 commit comments

Comments
 (0)