From e9bfdf458d4e92f33deacf0e250d82574aba2529 Mon Sep 17 00:00:00 2001 From: thunderbiscuit Date: Mon, 10 Aug 2026 13:57:30 -0400 Subject: [PATCH 1/2] build: build the release-smaller profile of rust lib --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0ee54ce..ab9ae5d 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,8 @@ "!**/.*" ], "scripts": { - "ubrn:ios": "ubrn build ios --and-generate", - "ubrn:android": "ubrn build android --and-generate", + "ubrn:ios": "ubrn build ios --and-generate --profile release-smaller", + "ubrn:android": "ubrn build android --and-generate --profile release-smaller", "ubrn:checkout": "ubrn checkout", "ubrn:clean": "rm -rfv cpp/ android/CMakeLists.txt android/src/main/java android/*.cpp ios/ src/Native* src/index.*ts* src/generated/", "test": "jest", From 82a43bbcc65f5842f485f0c52c9154e719d7fd44 Mon Sep 17 00:00:00 2001 From: thunderbiscuit Date: Mon, 10 Aug 2026 14:20:08 -0400 Subject: [PATCH 2/2] test: run test suite on Android emulator in CI --- .github/actions/setup/action.yml | 36 ------- .github/workflows/ci.yml | 153 --------------------------- .github/workflows/test-android.yaml | 158 ++++++++++++++++++++++++++++ justfile | 6 +- tests/pnpm-lock.yaml | 2 +- tests/scripts/run-android-tests.sh | 75 +++++++++++++ 6 files changed, 237 insertions(+), 193 deletions(-) delete mode 100644 .github/actions/setup/action.yml delete mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/test-android.yaml create mode 100755 tests/scripts/run-android-tests.sh diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml deleted file mode 100644 index a613332..0000000 --- a/.github/actions/setup/action.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Setup -description: Setup Node.js and install dependencies - -runs: - using: composite - steps: - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version-file: .nvmrc - - - name: Restore dependencies - id: yarn-cache - uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: | - **/node_modules - .yarn/install-state.gz - key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-${{ hashFiles('**/package.json', '!node_modules/**') }} - restore-keys: | - ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - ${{ runner.os }}-yarn- - - - name: Install dependencies - if: steps.yarn-cache.outputs.cache-hit != 'true' - run: yarn install --immutable - shell: bash - - - name: Cache dependencies - if: steps.yarn-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: | - **/node_modules - .yarn/install-state.gz - key: ${{ steps.yarn-cache.outputs.cache-primary-key }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index aab59d3..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,153 +0,0 @@ -name: CI -on: - push: - branches: - - main - pull_request: - branches: - - main - merge_group: - types: - - checks_requested - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Setup - uses: ./.github/actions/setup - - - name: Lint files - run: yarn lint - - - name: Typecheck files - run: yarn typecheck - - test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Setup - uses: ./.github/actions/setup - - - name: Run unit tests - run: yarn test --maxWorkers=2 --coverage - - build-library: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Setup - uses: ./.github/actions/setup - - - name: Build package - run: yarn prepare - - build-android: - runs-on: ubuntu-latest - env: - TURBO_CACHE_DIR: .turbo/android - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Setup - uses: ./.github/actions/setup - - - name: Cache turborepo for Android - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: ${{ env.TURBO_CACHE_DIR }} - key: ${{ runner.os }}-turborepo-android-${{ hashFiles('yarn.lock') }} - restore-keys: | - ${{ runner.os }}-turborepo-android- - - - name: Check turborepo cache for Android - run: | - TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:android').cache.status") - - if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then - echo "turbo_cache_hit=1" >> $GITHUB_ENV - fi - - - name: Install JDK - if: env.turbo_cache_hit != 1 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 - with: - distribution: 'zulu' - java-version: '17' - - - name: Finalize Android SDK - if: env.turbo_cache_hit != 1 - run: | - /bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null" - - - name: Cache Gradle - if: env.turbo_cache_hit != 1 - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: | - ~/.gradle/wrapper - ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('example/android/gradle/wrapper/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: Build example for Android - env: - JAVA_OPTS: "-XX:MaxHeapSize=6g" - run: | - yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" - - build-ios: - runs-on: macos-latest - env: - XCODE_VERSION: 16.2 - TURBO_CACHE_DIR: .turbo/ios - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Setup - uses: ./.github/actions/setup - - - name: Cache turborepo for iOS - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: ${{ env.TURBO_CACHE_DIR }} - key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('yarn.lock') }} - restore-keys: | - ${{ runner.os }}-turborepo-ios- - - - name: Check turborepo cache for iOS - run: | - TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status") - - if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then - echo "turbo_cache_hit=1" >> $GITHUB_ENV - fi - - - name: Use appropriate Xcode version - if: env.turbo_cache_hit != 1 - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0 - with: - xcode-version: ${{ env.XCODE_VERSION }} - - - name: Install cocoapods - if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true' - run: | - cd example - bundle install - bundle exec pod repo update --verbose - bundle exec pod install --project-directory=ios - - - name: Build example for iOS - run: | - yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" diff --git a/.github/workflows/test-android.yaml b/.github/workflows/test-android.yaml new file mode 100644 index 0000000..d2c9147 --- /dev/null +++ b/.github/workflows/test-android.yaml @@ -0,0 +1,158 @@ +name: Test Android + +# Builds the bdk-rn tarball for the emulator's ABI, installs it into the standalone +# tests/ app the way a downstream consumer would, and runs the integration suite on +# an Android emulator. +# +# The suite reports through logcat rather than an instrumentation runner, so +# tests/scripts/run-android-tests.sh is what turns its output into an exit code. + +on: + workflow_dispatch: + push: + branches: + - master + pull_request: + +permissions: {} + +env: + # tests/android/build.gradle pins this exact NDK; gradle and cargo-ndk must agree. + NDK_VERSION: "27.1.12297006" + +jobs: + test-android: + name: "Test Android library" + runs-on: ubuntu-24.04 + + services: + # Backs the esplora background-sync test, which talks to http://10.0.2.2:3002 + # (the emulator's route back to the runner host). + bitcoin-regtest: + image: ghcr.io/thunderbiscuit/podman-regtest-infinity-pro:0.4.0 + ports: + - 18443:18443 + - 18444:18444 + - 3002:3002 + - 60401:60401 + # Holds other steps until regtest has mined enough blocks to be usable. + options: >- + --health-cmd "test $(bitcoin-cli -regtest -rpcuser=regtest -rpcpassword=password getblockcount) -gt 100" + --health-interval 3s + --health-timeout 5s + --health-retries 20 + + steps: + - name: "Checkout" + uses: actions/checkout@v5 + with: + # bdk-ffi is a submodule and the Rust build needs it. + submodules: true + persist-credentials: false + + # Up front rather than just before the emulator: the Rust target directory, + # node_modules and the emulator system image are each multi-GB. The regtest + # image is referenced by a running container, so the docker prune skips it. + - name: "Free disk space" + uses: AdityaGarg8/remove-unwanted-software@v5 + with: + remove-dotnet: true + remove-haskell: true + remove-codeql: true + remove-docker-images: true + remove-large-packages: false + + - name: "Set up JDK" + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + + - name: "Set up Android SDK" + uses: android-actions/setup-android@v4 + + - name: "Set up Android NDK" + run: | + sdkmanager --install "ndk;${NDK_VERSION}" + echo "ANDROID_NDK_HOME=${ANDROID_HOME}/ndk/${NDK_VERSION}" >> "${GITHUB_ENV}" + echo "ANDROID_NDK_ROOT=${ANDROID_HOME}/ndk/${NDK_VERSION}" >> "${GITHUB_ENV}" + + - name: "Set up Rust" + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + target: x86_64-linux-android + cache: false + + - name: "Install cargo-ndk" + run: cargo install cargo-ndk --locked + + - name: "Set up just" + uses: extractions/setup-just@v3 + + - name: "Set up pnpm" + uses: pnpm/action-setup@v5 + + - name: "Set up Node" + uses: actions/setup-node@v5 + with: + node-version-file: .nvmrc + cache: pnpm + + # Required for the async Electrum and Esplora methods the tests exercise. + - name: "Apply the bdk-ffi async-sync patches" + run: just submodule-apply-patch + + # x86_64 only. The emulator below runs that ABI, and also building the + # arm64-v8a we ship would double the Rust build for nothing. + - name: "Build the library and pack the tarball" + run: just build-tarball-android x86_64 + + # tests/package.json pins the tarball by filename, so a version bump that + # misses it would otherwise fail deep inside pnpm. + - name: "Check tests/ references the tarball we just built" + run: | + expected="bdk-rn-$(node --print "require('./package.json').version").tgz" + referenced="$(node --print "require('./tests/package.json').dependencies['bdk-rn']")" + if [ "${referenced}" != "file:../${expected}" ]; then + echo "::error::tests/package.json depends on '${referenced}' but the library packs '${expected}'." + exit 1 + fi + test -f "${expected}" + + # --no-frozen-lockfile is required, not laziness: the lockfile records an + # integrity hash for the local tarball, and that hash changes on every build. + - name: "Install the test app dependencies" + working-directory: tests + run: | + rm -rf node_modules/bdk-rn + pnpm install --ignore-workspace --no-frozen-lockfile + + # Assembled before the emulator boots so that a gradle failure is reported on + # its own, and so the emulator step stays short. + - name: "Build the test app" + working-directory: tests/android + run: ./gradlew :app:assembleRelease --console=plain + + - name: "Enable KVM" + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: "Run the integration tests" + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 34 + target: google_apis + arch: x86_64 + profile: Nexus 6 + emulator-options: "-no-window -gpu swiftshader_indirect -no-snapshot" + script: ./tests/scripts/run-android-tests.sh + + - name: "Upload the test log" + if: always() + uses: actions/upload-artifact@v5 + with: + name: android-test-log + path: tests/tests.log + if-no-files-found: ignore diff --git a/justfile b/justfile index 323b2d0..c686066 100644 --- a/justfile +++ b/justfile @@ -58,10 +58,10 @@ submodule-apply-patch: && git apply -C1 ../patches/bdk-ffi-async-sync-electrum.patch [group("Build")] -[doc("Build the tarball for Android only.")] -build-tarball-android: +[doc("Build the tarball for Android only. Pass ABIs to override ubrn.config.yaml, e.g. `just build-tarball-android x86_64` for a CI emulator.")] +build-tarball-android targets="": pnpm install --ignore-scripts - pnpm ubrn:android --config ubrn.config.yaml + pnpm ubrn:android --config ubrn.config.yaml {{ if targets == "" { "" } else { "--targets " + targets } }} pnpm pack [group("Build")] diff --git a/tests/pnpm-lock.yaml b/tests/pnpm-lock.yaml index c4c0e16..302175b 100644 --- a/tests/pnpm-lock.yaml +++ b/tests/pnpm-lock.yaml @@ -1359,7 +1359,7 @@ packages: hasBin: true bdk-rn@file:../bdk-rn-1.1.0-alpha.0.tgz: - resolution: {integrity: sha512-3pacjZuYkja2fo8cmyOj2+8R9FXQOAewQXrKq01bO2c4kYkw0YyV3wCW3KMqz2q6gI2ZM3zgoTaFe7UqfFukTQ==, tarball: file:../bdk-rn-1.1.0-alpha.0.tgz} + resolution: {integrity: sha512-RmKbfu8Q/VzYxiztclFpdv0u4tq4Ry12mGwWlf/qkIbqx+jNvhPtkqZkB4QWqZdvz+kNLrsqVBaORdR6dinguw==, tarball: file:../bdk-rn-1.1.0-alpha.0.tgz} version: 1.1.0-alpha.0 peerDependencies: react: '*' diff --git a/tests/scripts/run-android-tests.sh b/tests/scripts/run-android-tests.sh new file mode 100755 index 0000000..0e01c43 --- /dev/null +++ b/tests/scripts/run-android-tests.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# +# Build, install and run the integration test app on an already-running Android +# emulator or device, then turn its logcat output into a process exit code. +# +# The app runs its tests on launch and prints results to the JS console (see +# tests/tests/testRunner.ts), finishing with either [TEST RUN PASSED] or +# [TEST RUN FAILED]. There is no instrumentation runner to give us an exit +# status, so we watch logcat for those sentinels. +# +# A release build is used deliberately: it embeds the JS bundle in the APK, so +# no Metro dev server has to be kept alive alongside the emulator. The release +# buildType is signed with the debug keystore (see android/app/build.gradle), +# so this needs no signing secrets. +# +# Usage: tests/scripts/run-android-tests.sh +# Env: +# TEST_TIMEOUT_SECONDS how long to wait for the run to finish (default 300) + +set -euo pipefail + +TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TIMEOUT="${TEST_TIMEOUT_SECONDS:-300}" +APP_ID="com.tests" +LOG_FILE="${TESTS_DIR}/tests.log" + +cd "${TESTS_DIR}" + +echo "==> Waiting for a device" +adb wait-for-device +until [ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ]; do + sleep 2 +done + +echo "==> Building and installing the release APK" +# installRelease covers assemble + install, and skips the Metro dev server. +(cd android && ./gradlew :app:installRelease --console=plain) + +echo "==> Launching ${APP_ID}" +adb logcat -c +adb shell am start -n "${APP_ID}/.MainActivity" > /dev/null + +echo "==> Waiting up to ${TIMEOUT}s for the test run to finish" +elapsed=0 +while [ "${elapsed}" -lt "${TIMEOUT}" ]; do + adb logcat -d -s ReactNativeJS > "${LOG_FILE}" 2>/dev/null || true + if grep --quiet --fixed-strings -e '[TEST RUN PASSED]' -e '[TEST RUN FAILED]' "${LOG_FILE}"; then + break + fi + sleep 2 + elapsed=$((elapsed + 2)) +done + +adb logcat -d -s ReactNativeJS > "${LOG_FILE}" 2>/dev/null || true + +echo +echo "================ test output ================" +cat "${LOG_FILE}" +echo "=============================================" +echo + +if grep --quiet --fixed-strings '[TEST RUN PASSED]' "${LOG_FILE}"; then + echo "All integration tests passed." + exit 0 +fi + +if grep --quiet --fixed-strings '[TEST RUN FAILED]' "${LOG_FILE}"; then + echo "Integration tests failed." >&2 + exit 1 +fi + +echo "Timed out after ${TIMEOUT}s without reaching [TEST RUN PASSED] or [TEST RUN FAILED]." >&2 +echo "The app may have crashed on launch; full logcat follows." >&2 +adb logcat -d -t 500 >&2 || true +exit 1