Skip to content

Commit baf0cba

Browse files
[RN] Run ReactCommon C++ unit tests on GitHub CI
## Summary 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 small 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. Suites covered initially: `react/renderer/graphics`, `react/utils`, `react/renderer/css`; more can be added incrementally (see `private/react-native-tests/README.md`). The CI job is advisory (`continue-on-error`) 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 and runs the gtest suites via CTest.
1 parent db11646 commit baf0cba

11 files changed

Lines changed: 719 additions & 0 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
sudo apt update
13+
sudo apt install -y git cmake openssl libssl-dev clang
14+
- name: Setup git safe folders
15+
shell: bash
16+
run: git config --global --add safe.directory '*'
17+
- name: Setup node.js
18+
uses: ./.github/actions/setup-node
19+
- name: Install node dependencies
20+
uses: ./.github/actions/yarn-install
21+
- name: Setup gradle
22+
uses: ./.github/actions/setup-gradle
23+
with:
24+
cache-read-only: 'false'
25+
cache-encryption-key: ${{ inputs.gradle-cache-encryption-key }}
26+
- name: Restore C++ tests ccache
27+
uses: actions/cache/restore@v5
28+
with:
29+
path: /github/home/.cache/ccache
30+
key:
31+
v1-ccache-cxx-tests-${{ github.job }}-${{ github.ref }}-${{ hashFiles(
32+
'packages/react-native/ReactCommon/**/*.cpp',
33+
'packages/react-native/ReactCommon/**/*.h',
34+
'packages/react-native/ReactCommon/**/CMakeLists.txt'
35+
) }}
36+
restore-keys: |
37+
v1-ccache-cxx-tests-${{ github.job }}-${{ github.ref }}-
38+
v1-ccache-cxx-tests-${{ github.job }}-
39+
v1-ccache-cxx-tests-
40+
- name: Show ccache stats (before)
41+
shell: bash
42+
run: ccache -s -v
43+
- name: Build and run C++ tests
44+
shell: bash
45+
run: yarn workspace @react-native/tests-cxx test
46+
env:
47+
CC: clang
48+
CXX: clang++
49+
- name: Save C++ tests ccache
50+
if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, '-stable') }}
51+
uses: actions/cache/save@v5
52+
with:
53+
path: /github/home/.cache/ccache
54+
key:
55+
v1-ccache-cxx-tests-${{ github.job }}-${{ github.ref }}-${{ hashFiles(
56+
'packages/react-native/ReactCommon/**/*.cpp',
57+
'packages/react-native/ReactCommon/**/*.h',
58+
'packages/react-native/ReactCommon/**/CMakeLists.txt'
59+
) }}
60+
- name: Show ccache stats (after)
61+
shell: bash
62+
run: ccache -s -v
63+
- name: Upload test results
64+
if: ${{ always() }}
65+
uses: actions/upload-artifact@v6
66+
with:
67+
name: run-cxx-tests-results
68+
compression-level: 1
69+
path: |
70+
private/react-native-tests/build/reports

.github/workflows/test-all.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,31 @@ 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+
# Advisory during rollout: a harness issue must not block merges. Once this
308+
# job is consistently green, drop continue-on-error and add it to branch
309+
# protection so it becomes a required merge gate.
310+
continue-on-error: true
311+
container:
312+
image: reactnativecommunity/react-native-android:latest
313+
env:
314+
# Set the encoding to resolve a known character encoding issue with decompressing tar.gz files in containers
315+
# via Gradle: https://github.com/gradle/gradle/issues/23391#issuecomment-1878979127
316+
LC_ALL: C.UTF8
317+
TERM: 'dumb'
318+
GRADLE_OPTS: '-Dorg.gradle.daemon=false'
319+
REACT_NATIVE_DOWNLOADS_DIR: /opt/react-native-downloads
320+
steps:
321+
- name: Checkout
322+
uses: actions/checkout@v6
323+
- name: Run C++ Tests
324+
uses: ./.github/actions/run-cxx-tests
325+
with:
326+
gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
327+
303328
build_android:
304329
runs-on: 8-core-ubuntu
305330
needs: [set_release_type, check_code_changes]
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
## Running locally (Meta-internal)
34+
35+
Requires the Android SDK's CMake and the same environment used to build the
36+
Fantom tester.
37+
38+
```sh
39+
yarn workspace @react-native/tests-cxx test # build + run (ctest)
40+
yarn workspace @react-native/tests-cxx build # build only
41+
```
42+
43+
## Adding a subsystem
44+
45+
1. Add the subsystem and any missing dependencies to the
46+
`add_react_common_subdir(...)` list in `cxx/CMakeLists.txt`.
47+
2. Add a `react_native_add_cxx_test_suite(...)` call listing the subsystem's
48+
full library closure (subsystem lib + its ReactCommon deps + third-party).
49+
ReactCommon libraries are CMake `OBJECT` libraries, so the closure must be
50+
listed explicitly rather than relying on transitive linking.
51+
3. Confirm the new suite builds and passes in the `test_cxx` CI job.
52+
53+
## Rollout
54+
55+
The `test_cxx` GitHub Actions job starts **advisory** (`continue-on-error`) so a
56+
harness issue cannot block merges. Once it is consistently green it should be
57+
made a **required** check via branch protection.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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). If
14+
// missing it is downloaded automatically by the Android SDK manager.
15+
val cmakeVersion = System.getenv("CMAKE_VERSION") ?: "3.30.5"
16+
val cmakePath = "${getSDKPath()}/cmake/$cmakeVersion"
17+
val cmakeBinaryPath = "${cmakePath}/bin/cmake"
18+
val ctestBinaryPath = "${cmakePath}/bin/ctest"
19+
val buildJobs = Runtime.getRuntime().availableProcessors().toString()
20+
21+
fun getSDKPath(): String {
22+
val androidSdkRoot = System.getenv("ANDROID_SDK_ROOT")
23+
val androidHome = System.getenv("ANDROID_HOME")
24+
return when {
25+
!androidSdkRoot.isNullOrBlank() -> androidSdkRoot
26+
!androidHome.isNullOrBlank() -> androidHome
27+
else -> throw IllegalStateException("Neither ANDROID_SDK_ROOT nor ANDROID_HOME is set.")
28+
}
29+
}
30+
31+
val buildDir = project.layout.buildDirectory.get().asFile
32+
val reportsDir = File("$buildDir/reports")
33+
val reactNativeRootDir = projectDir.parentFile.parentFile
34+
val reactNativeDir = File("$reactNativeRootDir/packages/react-native")
35+
val reactAndroidDir = File("$reactNativeDir/ReactAndroid")
36+
val reactAndroidBuildDir = File("$reactAndroidDir/build")
37+
38+
// The C++ test harness reuses the third-party dependencies staged by the Fantom
39+
// tester build: folly and gflags land in <fantom>/build/third-party; glog,
40+
// double-conversion, fast_float, fmt and boost land in
41+
// ReactAndroid/build/third-party-ndk. Depending on Fantom's
42+
// prepareNative3pDependencies guarantees all of them are prepared.
43+
val fantomDir = File("$reactNativeRootDir/private/react-native-fantom")
44+
val stagedThirdPartyDir = File("$fantomDir/build/third-party")
45+
val testerThirdPartySrcDir = File("$fantomDir/tester/third-party")
46+
47+
val cxxDir = File("$projectDir/cxx")
48+
val cxxBuildDir = File("$buildDir/cxx")
49+
val cxxBuildOutputFileTree =
50+
fileTree(cxxBuildDir.toString())
51+
.include("**/*.cmake", "**/*.marks", "**/compiler_depends.ts", "**/Makefile", "**/link.txt")
52+
53+
val createReportsDir by tasks.registering { reportsDir.mkdirs() }
54+
55+
// Generated codegen sources (FBReactNativeSpec) that Fabric component tests
56+
// depend on. `generateCodegenArtifactsFromSchema` produces them under
57+
// ReactAndroid/build/generated; stage them next to codegen/CMakeLists.txt.
58+
val codegenSrcDir = File("$reactAndroidBuildDir/generated/source/codegen/jni")
59+
val codegenOutDir = File("$buildDir/codegen")
60+
val prepareRNCodegen by
61+
tasks.registering(Copy::class) {
62+
dependsOn(":packages:react-native:ReactAndroid:generateCodegenArtifactsFromSchema")
63+
from(codegenSrcDir)
64+
from("codegen")
65+
include("react/**/*.h", "react/**/*.cpp", "CMakeLists.txt")
66+
includeEmptyDirs = false
67+
duplicatesStrategy = DuplicatesStrategy.INCLUDE
68+
into(codegenOutDir)
69+
}
70+
71+
val configureCxxTests by
72+
tasks.registering(CustomExecTask::class) {
73+
dependsOn(
74+
createReportsDir,
75+
prepareRNCodegen,
76+
":private:react-native-fantom:prepareNative3pDependencies",
77+
)
78+
workingDir(cxxDir)
79+
inputs.dir(cxxDir)
80+
outputs.files(cxxBuildOutputFileTree)
81+
commandLine(
82+
cmakeBinaryPath,
83+
"--log-level=ERROR",
84+
"-S",
85+
".",
86+
"-B",
87+
cxxBuildDir.toString(),
88+
"-DCMAKE_BUILD_TYPE=Debug",
89+
"-DREACT_ANDROID_DIR=$reactAndroidDir",
90+
"-DREACT_COMMON_DIR=$reactNativeDir/ReactCommon",
91+
"-DREACT_THIRD_PARTY_NDK_DIR=$reactAndroidBuildDir/third-party-ndk",
92+
"-DRN_STAGED_THIRD_PARTY_DIR=$stagedThirdPartyDir",
93+
"-DRN_TESTER_THIRD_PARTY_SRC_DIR=$testerThirdPartySrcDir",
94+
"-DRN_CODEGEN_DIR=$codegenOutDir",
95+
"-DRN_ENABLE_DEBUG_STRING_CONVERTIBLE=ON",
96+
)
97+
standardOutputFile.set(project.file("$buildDir/reports/configure-cxx-tests.log"))
98+
errorOutputFile.set(project.file("$buildDir/reports/configure-cxx-tests.error.log"))
99+
}
100+
101+
val buildCxxTests by
102+
tasks.registering(CustomExecTask::class) {
103+
dependsOn(configureCxxTests)
104+
workingDir(cxxDir)
105+
inputs.files(cxxBuildOutputFileTree)
106+
commandLine(cmakeBinaryPath, "--build", cxxBuildDir.toString(), "-j", buildJobs)
107+
standardOutputFile.set(project.file("$buildDir/reports/build-cxx-tests.log"))
108+
errorOutputFile.set(project.file("$buildDir/reports/build-cxx-tests.error.log"))
109+
}
110+
111+
val runCxxTests by
112+
tasks.registering(CustomExecTask::class) {
113+
dependsOn(buildCxxTests)
114+
workingDir(cxxBuildDir)
115+
commandLine(
116+
ctestBinaryPath,
117+
"--output-on-failure",
118+
"--output-junit",
119+
"$reportsDir/cxx-tests-results.xml",
120+
)
121+
standardOutputFile.set(project.file("$buildDir/reports/run-cxx-tests.log"))
122+
errorOutputFile.set(project.file("$buildDir/reports/run-cxx-tests.error.log"))
123+
}
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
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
# Builds the generated codegen sources (FBReactNativeSpec) that Fabric component
7+
# tests depend on. Populated by the Gradle `prepareRNCodegen` task, which runs
8+
# `generateCodegenArtifactsFromSchema` and stages the generated headers/sources
9+
# next to this file. Mirrors private/react-native-fantom/tester/codegen.
10+
11+
cmake_minimum_required(VERSION 3.13)
12+
set(CMAKE_VERBOSE_MAKEFILE on)
13+
14+
file(GLOB react_codegen_SRCS CONFIGURE_DEPENDS react/renderer/components/FBReactNativeSpec/*.cpp)
15+
16+
add_library(
17+
react_codegen_rncore
18+
OBJECT
19+
${react_codegen_SRCS}
20+
)
21+
22+
target_include_directories(react_codegen_rncore
23+
PUBLIC
24+
${CMAKE_CURRENT_SOURCE_DIR}/.
25+
${CMAKE_CURRENT_SOURCE_DIR}/react/renderer/components/FBReactNativeSpec
26+
${CMAKE_CURRENT_SOURCE_DIR}/react/renderer/components)
27+
28+
target_link_libraries(
29+
react_codegen_rncore
30+
jsi
31+
react_nativemodule_core
32+
rrc_view
33+
)
34+
35+
target_compile_options(
36+
react_codegen_rncore
37+
PRIVATE
38+
-DLOG_TAG=\"ReactNative\"
39+
-fexceptions
40+
-frtti
41+
-std=c++20
42+
-Wall
43+
)

0 commit comments

Comments
 (0)