Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .github/actions/run-cxx-tests/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Run C++ Tests
description: Builds and runs the ReactCommon C++ (gtest) unit-test harness on Linux
inputs:
gradle-cache-encryption-key:
description: 'The encryption key needed to store the Gradle Configuration cache'
runs:
using: composite
steps:
- name: Install dependencies
shell: bash
run: |
sudo apt update
sudo apt install -y git cmake openssl libssl-dev clang
- name: Setup git safe folders
shell: bash
run: git config --global --add safe.directory '*'
- name: Setup node.js
uses: ./.github/actions/setup-node
- name: Install node dependencies
uses: ./.github/actions/yarn-install
- name: Setup gradle
uses: ./.github/actions/setup-gradle
with:
cache-read-only: 'false'
cache-encryption-key: ${{ inputs.gradle-cache-encryption-key }}
- name: Restore C++ tests ccache
uses: actions/cache/restore@v5
with:
path: /github/home/.cache/ccache
key:
v1-ccache-cxx-tests-${{ github.job }}-${{ github.ref }}-${{ hashFiles(
'packages/react-native/ReactCommon/**/*.cpp',
'packages/react-native/ReactCommon/**/*.h',
'packages/react-native/ReactCommon/**/CMakeLists.txt'
) }}
restore-keys: |
v1-ccache-cxx-tests-${{ github.job }}-${{ github.ref }}-
v1-ccache-cxx-tests-${{ github.job }}-
v1-ccache-cxx-tests-
- name: Show ccache stats (before)
shell: bash
run: ccache -s -v
- name: Build and run C++ tests
shell: bash
run: yarn workspace @react-native/tests-cxx test
env:
CC: clang
CXX: clang++
# libhermesvm.so is a shared library; gtest_discover_tests runs the
# binaries at build time and CTest runs them again, so it must be
# loadable in both phases.
LD_LIBRARY_PATH: ${{ github.workspace }}/packages/react-native/ReactAndroid/hermes-engine/build/hermes/lib
- name: Save C++ tests ccache
if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, '-stable') }}
uses: actions/cache/save@v5
with:
path: /github/home/.cache/ccache
key:
v1-ccache-cxx-tests-${{ github.job }}-${{ github.ref }}-${{ hashFiles(
'packages/react-native/ReactCommon/**/*.cpp',
'packages/react-native/ReactCommon/**/*.h',
'packages/react-native/ReactCommon/**/CMakeLists.txt'
) }}
- name: Show ccache stats (after)
shell: bash
run: ccache -s -v
- name: Upload test results
if: ${{ always() }}
uses: actions/upload-artifact@v6
with:
name: run-cxx-tests-results
compression-level: 1
path: |
private/react-native-tests/build/reports
25 changes: 25 additions & 0 deletions .github/workflows/test-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,31 @@ jobs:
fail-on-error: true
secrets: inherit

test_cxx:
runs-on: 8-core-ubuntu
needs: [check_code_changes, lint]
if: needs.check_code_changes.outputs.any_code_change == 'true'
# Advisory during rollout: a harness issue must not block merges. Once this
# job is consistently green, drop continue-on-error and add it to branch
# protection so it becomes a required merge gate.
continue-on-error: true
container:
image: reactnativecommunity/react-native-android:latest
env:
# Set the encoding to resolve a known character encoding issue with decompressing tar.gz files in containers
# via Gradle: https://github.com/gradle/gradle/issues/23391#issuecomment-1878979127
LC_ALL: C.UTF8
TERM: 'dumb'
GRADLE_OPTS: '-Dorg.gradle.daemon=false'
REACT_NATIVE_DOWNLOADS_DIR: /opt/react-native-downloads
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Run C++ Tests
uses: ./.github/actions/run-cxx-tests
with:
gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}

build_android:
runs-on: 8-core-ubuntu
needs: [set_release_type, check_code_changes]
Expand Down
79 changes: 79 additions & 0 deletions private/react-native-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# @react-native/tests-cxx

A Linux + CMake harness that builds and runs a **representative subset** of
ReactCommon's C++ (gtest) unit tests on GitHub CI.

## Why this exists

ReactCommon's C++ test sources (`<subsystem>/tests/*.cpp`) are part of the
repository, but the build rules that compile and run them are Meta-internal and
are not part of the open-source tree. As a result these unit tests did not run
on GitHub CI at all — the renderer, layout, and runtime C++ core was only
covered by higher-level integration tests (Fantom) and by platform builds.

This harness closes that gap cheaply: C++ unit tests build and run on standard
(free) Linux runners, so GitHub CI can gate merges on them.

The goal is a **representative**, high-signal subset — not full coverage. Suites
are added deliberately, weighted by how often they catch regressions and by
build cost.

## How it works

The harness reuses the exact desktop C++ toolchain already proven by the Fantom
tester (`private/react-native-fantom`):

- **Gradle** (`build.gradle.kts`) stages the third-party dependencies (folly,
boost, glog, double-conversion, fast_float, fmt, gflags) by depending on the
Fantom tester's `prepareNative3pDependencies`, then invokes CMake.
- **CMake** (`cxx/CMakeLists.txt`) compiles each in-scope ReactCommon subsystem
into its library and links its `tests/*.cpp` into a per-subsystem gtest binary,
registered with CTest. GoogleTest itself is fetched via `FetchContent`.

## Coverage

30 suites (~900 gtest cases) run today, spanning the C++ core:

- **Primitives / parsing / serialization:** graphics, css, utils, mapbuffer,
timing, featureflags
- **Fabric renderer:** renderer/core, mounting, components/{view, text,
scrollview, image, root}, attributedstring, textlayoutmanager, element,
componentregistry (via deps), imagemanager, renderer/debug
- **Scheduling / runtime:** runtimescheduler, scheduler, uimanager,
uimanager/consistency, performance/timeline, animated, animations
- **Debugger / infra:** debug/redbox, reactperflogger/fusebox, jserrorhandler,
telemetry, cxxreact

Suites for `renderer/core`, `runtimescheduler`, `scheduler` and `animated`
create a JS runtime and link the Hermes VM.

Not yet included: `jsinspector-modern` and `jsinspector-modern/tracing` (their
tests depend on a specific GoogleTest version / `std::source_location` support
that this harness's GoogleTest doesn't match), and `react/bridging` (its test
helper header uses a Buck header-namespace that doesn't map to CMake).

## Running locally (Meta-internal)

Requires the Android SDK's CMake and the same environment used to build the
Fantom tester.

```sh
yarn workspace @react-native/tests-cxx test # build + run (ctest)
yarn workspace @react-native/tests-cxx build # build only
```

## Adding a subsystem

1. Add the subsystem and any missing dependencies to the
`add_react_common_subdir(...)` list in `cxx/CMakeLists.txt`.
2. Add a `react_native_add_cxx_test_suite(...)` call listing the subsystem's
full library closure (subsystem lib + its ReactCommon deps + third-party).
ReactCommon libraries are CMake `OBJECT` libraries, so the closure must be
listed explicitly rather than relying on transitive linking.
3. Confirm the new suite builds and passes in the `test_cxx` CI job.

## Rollout

The `test_cxx` GitHub Actions job starts **advisory** (`continue-on-error`) so a
harness issue cannot block merges. Once it is consistently green it should be
made a **required** check via branch protection.
139 changes: 139 additions & 0 deletions private/react-native-tests/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import com.facebook.react.tasks.internal.*
import com.facebook.react.tasks.internal.utils.*

plugins { id("com.facebook.react") }

// CMake shipped with the Android SDK (same version the Fantom tester uses). If
// missing it is downloaded automatically by the Android SDK manager.
val cmakeVersion = System.getenv("CMAKE_VERSION") ?: "3.30.5"
val cmakePath = "${getSDKPath()}/cmake/$cmakeVersion"
val cmakeBinaryPath = "${cmakePath}/bin/cmake"
val ctestBinaryPath = "${cmakePath}/bin/ctest"
val buildJobs = Runtime.getRuntime().availableProcessors().toString()

fun getSDKPath(): String {
val androidSdkRoot = System.getenv("ANDROID_SDK_ROOT")
val androidHome = System.getenv("ANDROID_HOME")
return when {
!androidSdkRoot.isNullOrBlank() -> androidSdkRoot
!androidHome.isNullOrBlank() -> androidHome
else -> throw IllegalStateException("Neither ANDROID_SDK_ROOT nor ANDROID_HOME is set.")
}
}

val buildDir = project.layout.buildDirectory.get().asFile
val reportsDir = File("$buildDir/reports")
val reactNativeRootDir = projectDir.parentFile.parentFile
val reactNativeDir = File("$reactNativeRootDir/packages/react-native")
val reactAndroidDir = File("$reactNativeDir/ReactAndroid")
val reactAndroidBuildDir = File("$reactAndroidDir/build")

// The C++ test harness reuses the third-party dependencies staged by the Fantom
// tester build: folly and gflags land in <fantom>/build/third-party; glog,
// double-conversion, fast_float, fmt and boost land in
// ReactAndroid/build/third-party-ndk. Depending on Fantom's
// prepareNative3pDependencies guarantees all of them are prepared.
val fantomDir = File("$reactNativeRootDir/private/react-native-fantom")
val stagedThirdPartyDir = File("$fantomDir/build/third-party")
val testerThirdPartySrcDir = File("$fantomDir/tester/third-party")

val cxxDir = File("$projectDir/cxx")
val cxxBuildDir = File("$buildDir/cxx")
val cxxBuildOutputFileTree =
fileTree(cxxBuildDir.toString())
.include("**/*.cmake", "**/*.marks", "**/compiler_depends.ts", "**/Makefile", "**/link.txt")

val createReportsDir by tasks.registering { reportsDir.mkdirs() }

// Generated codegen sources (FBReactNativeSpec) that Fabric component tests
// depend on. `generateCodegenArtifactsFromSchema` produces them under
// ReactAndroid/build/generated; stage them next to codegen/CMakeLists.txt.
val codegenSrcDir = File("$reactAndroidBuildDir/generated/source/codegen/jni")
val codegenOutDir = File("$buildDir/codegen")
val prepareRNCodegen by
tasks.registering(Copy::class) {
dependsOn(":packages:react-native:ReactAndroid:generateCodegenArtifactsFromSchema")
from(codegenSrcDir)
from("codegen")
include("react/**/*.h", "react/**/*.cpp", "CMakeLists.txt")
includeEmptyDirs = false
duplicatesStrategy = DuplicatesStrategy.INCLUDE
into(codegenOutDir)
}

// Hermes VM + prefab headers, for suites whose tests create a JS runtime
// (hermes::makeHermesRuntime). Mirrors the Fantom tester's Hermes setup.
val enableHermesBuild by tasks.registering {
project(":packages:react-native:ReactAndroid:hermes-engine") {
tasks.configureEach { enabled = true }
}
}
val prepareHermesDependencies by tasks.registering {
dependsOn(
enableHermesBuild,
":packages:react-native:ReactAndroid:hermes-engine:buildHermesLibWithDebugger",
":packages:react-native:ReactAndroid:hermes-engine:prepareHeadersForPrefabWithDebugger",
)
}

val configureCxxTests by
tasks.registering(CustomExecTask::class) {
dependsOn(
createReportsDir,
prepareRNCodegen,
prepareHermesDependencies,
":private:react-native-fantom:prepareNative3pDependencies",
)
workingDir(cxxDir)
inputs.dir(cxxDir)
outputs.files(cxxBuildOutputFileTree)
commandLine(
cmakeBinaryPath,
"--log-level=ERROR",
"-S",
".",
"-B",
cxxBuildDir.toString(),
"-DCMAKE_BUILD_TYPE=Debug",
"-DREACT_ANDROID_DIR=$reactAndroidDir",
"-DREACT_COMMON_DIR=$reactNativeDir/ReactCommon",
"-DREACT_THIRD_PARTY_NDK_DIR=$reactAndroidBuildDir/third-party-ndk",
"-DRN_STAGED_THIRD_PARTY_DIR=$stagedThirdPartyDir",
"-DRN_TESTER_THIRD_PARTY_SRC_DIR=$testerThirdPartySrcDir",
"-DRN_CODEGEN_DIR=$codegenOutDir",
"-DRN_ENABLE_DEBUG_STRING_CONVERTIBLE=ON",
)
standardOutputFile.set(project.file("$buildDir/reports/configure-cxx-tests.log"))
errorOutputFile.set(project.file("$buildDir/reports/configure-cxx-tests.error.log"))
}

val buildCxxTests by
tasks.registering(CustomExecTask::class) {
dependsOn(configureCxxTests)
workingDir(cxxDir)
inputs.files(cxxBuildOutputFileTree)
commandLine(cmakeBinaryPath, "--build", cxxBuildDir.toString(), "-j", buildJobs)
standardOutputFile.set(project.file("$buildDir/reports/build-cxx-tests.log"))
errorOutputFile.set(project.file("$buildDir/reports/build-cxx-tests.error.log"))
}

val runCxxTests by
tasks.registering(CustomExecTask::class) {
dependsOn(buildCxxTests)
workingDir(cxxBuildDir)
commandLine(
ctestBinaryPath,
"--output-on-failure",
"--output-junit",
"$reportsDir/cxx-tests-results.xml",
)
standardOutputFile.set(project.file("$buildDir/reports/run-cxx-tests.log"))
errorOutputFile.set(project.file("$buildDir/reports/run-cxx-tests.error.log"))
}
11 changes: 11 additions & 0 deletions private/react-native-tests/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

set -e

pushd ../..
./gradlew :private:react-native-tests:buildCxxTests
popd
43 changes: 43 additions & 0 deletions private/react-native-tests/codegen/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

# Builds the generated codegen sources (FBReactNativeSpec) that Fabric component
# tests depend on. Populated by the Gradle `prepareRNCodegen` task, which runs
# `generateCodegenArtifactsFromSchema` and stages the generated headers/sources
# next to this file. Mirrors private/react-native-fantom/tester/codegen.

cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)

file(GLOB react_codegen_SRCS CONFIGURE_DEPENDS react/renderer/components/FBReactNativeSpec/*.cpp)

add_library(
react_codegen_rncore
OBJECT
${react_codegen_SRCS}
)

target_include_directories(react_codegen_rncore
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/.
${CMAKE_CURRENT_SOURCE_DIR}/react/renderer/components/FBReactNativeSpec
${CMAKE_CURRENT_SOURCE_DIR}/react/renderer/components)

target_link_libraries(
react_codegen_rncore
jsi
react_nativemodule_core
rrc_view
)

target_compile_options(
react_codegen_rncore
PRIVATE
-DLOG_TAG=\"ReactNative\"
-fexceptions
-frtti
-std=c++20
-Wall
)
Loading
Loading