From c9f87656e6e032b71d1f8a6f3d94c1afdeaf210b Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Wed, 4 Nov 2026 23:54:43 +0000 Subject: [PATCH] Run ReactCommon C++ unit tests in CI --- .github/workflows/test-all.yml | 54 +++++++ .../react-native/gradle/libs.versions.toml | 1 + private/react-native-fantom/build.gradle.kts | 58 +++++++ .../react-native-fantom/tester/CMakeLists.txt | 2 + .../tester/react-common-tests/CMakeLists.txt | 142 ++++++++++++++++++ 5 files changed, 257 insertions(+) create mode 100644 private/react-native-fantom/tester/react-common-tests/CMakeLists.txt diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index 2c67bb36dc05..0845202694ca 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -331,6 +331,60 @@ jobs: fail-on-error: true secrets: inherit + test_react_common_cxx: + runs-on: 8-core-ubuntu + needs: [check_code_changes, lint] + if: needs.check_code_changes.outputs.any_code_change == '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 + # The tests use C++20 features newer than the image's clang 14, such as + # std::source_location and parenthesised aggregate initialisation. + - name: Install dependencies + shell: bash + run: | + sudo apt update + sudo apt install -y git cmake openssl libssl-dev curl gnupg + codename=$(. /etc/os-release && echo "$VERSION_CODENAME") + curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc > /dev/null + echo "deb http://apt.llvm.org/$codename/ llvm-toolchain-$codename-18 main" | sudo tee /etc/apt/sources.list.d/llvm-18.list + sudo apt update + sudo apt install -y clang-18 + - 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-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} + - name: Run ReactCommon C++ tests + shell: bash + run: ./gradlew :private:react-native-fantom:testReactCommon + env: + CC: clang-18 + CXX: clang++-18 + - name: Upload test results + if: ${{ always() }} + uses: ./.github/actions/upload-artifact + with: + name: test-react-common-cxx-results + compression-level: 1 + if-no-files-found: ignore + path: private/react-native-fantom/build/reports + build_android: runs-on: 8-core-ubuntu needs: [set_release_type, check_code_changes] diff --git a/packages/react-native/gradle/libs.versions.toml b/packages/react-native/gradle/libs.versions.toml index b7ce9a7bf54b..35407c86b177 100644 --- a/packages/react-native/gradle/libs.versions.toml +++ b/packages/react-native/gradle/libs.versions.toml @@ -50,6 +50,7 @@ fmt="12.1.0" folly="2024.11.18.00" glog="0.3.5" gflags="2.2.0" +googletest="1.18.0" nlohmannjson="3.12.0" [libraries] diff --git a/private/react-native-fantom/build.gradle.kts b/private/react-native-fantom/build.gradle.kts index a0d5df7e93d0..6aa7635d0f10 100644 --- a/private/react-native-fantom/build.gradle.kts +++ b/private/react-native-fantom/build.gradle.kts @@ -35,6 +35,7 @@ fun getSDKPath(): String { val FOLLY_VERSION = libs.versions.folly.get() val GFLAGS_VERSION = libs.versions.gflags.get() +val GOOGLETEST_VERSION = libs.versions.googletest.get() val NLOHMANNJSON_VERSION = libs.versions.nlohmannjson.get() val buildDir = project.layout.buildDirectory.get().asFile @@ -104,6 +105,27 @@ val prepareGflags by outputDir.set(File(thirdParty, "gflags")) } +val downloadGoogleTestDest = File(downloadsDir, "googletest-${GOOGLETEST_VERSION}.tar.gz") +val downloadGoogleTest by + tasks.registering(Download::class) { + dependsOn(createNativeDepsDirectories) + src("https://github.com/google/googletest/archive/v${GOOGLETEST_VERSION}.tar.gz") + onlyIfModified(true) + overwrite(false) + retries(5) + quiet(true) + dest(downloadGoogleTestDest) + } + +val prepareGoogleTest by + tasks.registering(Copy::class) { + dependsOn(listOf(downloadGoogleTest)) + from(tarTree(downloadGoogleTestDest)) + eachFile { path = path.substringAfter("/") } + includeEmptyDirs = false + into("$thirdParty/googletest") + } + val downloadNlohmannJsonDest = File(downloadsDir, "nlohmann_json-${NLOHMANNJSON_VERSION}.tar.gz") val downloadNlohmannJson by tasks.registering(Download::class) { @@ -164,6 +186,7 @@ val prepareNative3pDependencies by tasks.registering { dependsOn( prepareGflags, + prepareGoogleTest, prepareNlohmannJson, prepareFolly, ":packages:react-native:ReactAndroid:prepareBoost", @@ -229,3 +252,38 @@ val buildFantomTester by standardOutputFile.set(project.file("$buildDir/reports/build-fantom_tester.log")) errorOutputFile.set(project.file("$buildDir/reports/build-fantom_tester.error.log")) } + +val buildReactCommonTests by + tasks.registering(CustomExecTask::class) { + dependsOn(configureFantomTester) + workingDir(testerDir) + inputs.files(testerBuildOutputFileTree) + outputs.upToDateWhen { false } + commandLine( + cmakeBinaryPath, + "--build", + testerBuildDir.toString(), + "--target", + "react_common_tests", + "-j", + ndkBuildJobs, + ) + standardOutputFile.set(project.file("$buildDir/reports/build-react_common_tests.log")) + errorOutputFile.set(project.file("$buildDir/reports/build-react_common_tests.error.log")) + } + +val testReactCommon by + tasks.registering(Exec::class) { + dependsOn(buildReactCommonTests) + outputs.upToDateWhen { false } + commandLine( + "$cmakePath/bin/ctest", + "--test-dir", + "$testerBuildDir/react-common-tests", + "--output-on-failure", + "--output-junit", + "$reportsDir/react-common-tests.xml", + "-j", + ndkBuildJobs, + ) + } diff --git a/private/react-native-fantom/tester/CMakeLists.txt b/private/react-native-fantom/tester/CMakeLists.txt index af4c741f5ab6..c5d7d495e845 100644 --- a/private/react-native-fantom/tester/CMakeLists.txt +++ b/private/react-native-fantom/tester/CMakeLists.txt @@ -258,3 +258,5 @@ target_compile_options(fantom_tester -fexceptions -frtti -std=c++20) + +add_subdirectory(react-common-tests) diff --git a/private/react-native-fantom/tester/react-common-tests/CMakeLists.txt b/private/react-native-fantom/tester/react-common-tests/CMakeLists.txt new file mode 100644 index 000000000000..57ec3737f44c --- /dev/null +++ b/private/react-native-fantom/tester/react-common-tests/CMakeLists.txt @@ -0,0 +1,142 @@ +# 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 C++ unit tests in ReactCommon/**/tests against the libraries the +# Fantom tester already compiles, one GoogleTest executable per tests +# directory. Run them with ctest from this directory's build directory. + +cmake_minimum_required(VERSION 3.13) + +enable_testing() +include(GoogleTest) + +set(INSTALL_GTEST OFF CACHE BOOL "" FORCE) +add_subdirectory(${FANTOM_THIRD_PARTY_DIR}/googletest googletest EXCLUDE_FROM_ALL) + +add_react_common_subdir(react/networking) +add_react_common_subdir(react/renderer/animations) +add_react_common_subdir(react/renderer/element) + +# rrc_textinput compiles the Android platform sources, which need the Android +# text layout; the tests only use the cross-platform base. +file(GLOB react_common_tests_textinput_SRC CONFIGURE_DEPENDS + ${REACT_COMMON_DIR}/react/renderer/components/textinput/*.cpp) +add_library(react_common_tests_textinput OBJECT ${react_common_tests_textinput_SRC}) +target_link_libraries(react_common_tests_textinput + PUBLIC + react_renderer_attributedstring + react_renderer_core + rrc_text + rrc_view) +target_compile_reactnative_options(react_common_tests_textinput PRIVATE) + +# The folly executors some tests use, and what they pull in, which +# folly_runtime leaves out. +set(folly_DIR ${FANTOM_THIRD_PARTY_DIR}/folly/folly) +add_library(react_common_tests_folly STATIC + ${folly_DIR}/ExceptionString.cpp + ${folly_DIR}/Executor.cpp + ${folly_DIR}/SingletonThreadLocal.cpp + ${folly_DIR}/concurrency/ProcessLocalUniqueId.cpp + ${folly_DIR}/detail/MemoryIdler.cpp + ${folly_DIR}/detail/StaticSingletonManager.cpp + ${folly_DIR}/detail/ThreadLocalDetail.cpp + ${folly_DIR}/detail/thread_local_globals.cpp + ${folly_DIR}/executors/ManualExecutor.cpp + ${folly_DIR}/executors/QueuedImmediateExecutor.cpp + ${folly_DIR}/io/async/Request.cpp + ${folly_DIR}/memory/MallctlHelper.cpp + ${folly_DIR}/memory/ReentrantAllocator.cpp + ${folly_DIR}/portability/SysMembarrier.cpp + ${folly_DIR}/synchronization/AsymmetricThreadFence.cpp + ${folly_DIR}/synchronization/Hazptr.cpp + ${folly_DIR}/synchronization/HazptrDomain.cpp + ${folly_DIR}/system/Pid.cpp) +target_compile_options(react_common_tests_folly PRIVATE -fexceptions -frtti) +target_link_libraries(react_common_tests_folly PUBLIC folly_runtime) + +# Test headers are included by the paths Buck exports them under. +set(react_common_tests_INCLUDE_DIR ${CMAKE_CURRENT_BINARY_DIR}/include) +configure_file( + ${REACT_COMMON_DIR}/callinvoker/ReactCommon/tests/TestCallInvoker.h + ${react_common_tests_INCLUDE_DIR}/ReactCommon/TestCallInvoker.h + COPYONLY) + +get_target_property(react_common_tests_LIBS fantom_tester LINK_LIBRARIES) + +# Pre-existing failures in OSS, on macOS and Linux alike, excluded by gtest +# name per test target. +set(jsinspector_modern_tests_EXCLUDED + # Debugger.setBreakpointByUrl resolves no locations for a columnless + # breakpoint. + JsiIntegrationHermesTest/*.ResolveColumnlessBreakpointAfterEval + JsiIntegrationHermesTest/*.ResolveColumnlessBreakpointAfterReload + # The second session gets no Debugger.scriptParsed once the first disables + # Debugger. + JsiIntegrationHermesTest/*.TwoConnectionsDebuggerLifecycle) + +file(GLOB_RECURSE react_common_tests_SRC CONFIGURE_DEPENDS ${REACT_COMMON_DIR}/*.cpp) +list(FILTER react_common_tests_SRC INCLUDE REGEX "/tests/") +list(FILTER react_common_tests_SRC EXCLUDE REGEX "/benchmarks/") + +set(react_common_tests_DIRS) +foreach(source ${react_common_tests_SRC}) + string(REGEX REPLACE "^(.*/tests)/.*$" "\\1" test_dir ${source}) + list(APPEND react_common_tests_DIRS ${test_dir}) +endforeach() +list(REMOVE_DUPLICATES react_common_tests_DIRS) +list(SORT react_common_tests_DIRS) + +set(react_common_tests_TARGETS) +foreach(test_dir ${react_common_tests_DIRS}) + file(RELATIVE_PATH test_name ${REACT_COMMON_DIR} ${test_dir}) + string(MAKE_C_IDENTIFIER ${test_name} test_target) + + set(test_dir_SRC ${react_common_tests_SRC}) + list(FILTER test_dir_SRC INCLUDE REGEX "^${test_dir}/") + + add_executable(${test_target} EXCLUDE_FROM_ALL ${test_dir_SRC}) + # Header-only ReactCommon libraries export -Werror -Wpedantic, and source + # options are the only ones placed after them on the command line. + set_source_files_properties(${test_dir_SRC} PROPERTIES COMPILE_OPTIONS -Wno-error) + target_include_directories(${test_target} + PRIVATE + ${react_common_tests_INCLUDE_DIR} + ${test_dir} + ${test_dir}/..) + target_link_libraries(${test_target} + PRIVATE + ${react_common_tests_LIBS} + react_common_tests_folly + react_common_tests_textinput + react_networking + react_renderer_animations + react_renderer_element + GTest::gmock_main) + target_compile_options(${test_target} + PRIVATE + -fexceptions + -frtti + -std=c++20) + + set(test_FILTER "*") + if(DEFINED ${test_target}_EXCLUDED) + list(JOIN ${test_target}_EXCLUDED ":" test_EXCLUDED) + set(test_FILTER "*-${test_EXCLUDED}") + endif() + + # One process per test case: some suites leave global state behind, such as + # registered HostTargets in jsinspector-modern, that later suites trip over. + gtest_discover_tests(${test_target} + TEST_PREFIX ${test_name}/ + TEST_FILTER ${test_FILTER} + NO_PRETTY_TYPES + NO_PRETTY_VALUES + DISCOVERY_MODE PRE_TEST) + list(APPEND react_common_tests_TARGETS ${test_target}) +endforeach() + +add_custom_target(react_common_tests) +add_dependencies(react_common_tests ${react_common_tests_TARGETS})