From ff01cea88439343c8e72289ef0dd2b8600b4e60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Wed, 6 May 2026 10:18:33 +0200 Subject: [PATCH 1/5] ci: add setup-key auth instrumentation test Adds SetupKeyAuthTest, a UiAutomator-driven instrumentation test that drives the in-app "Change server" screen end-to-end with a setup key and waits for the success dialog. The test taps "Use NetBird", so the management URL is the one hard-coded in the app (Preferences.defaultServer()). The setup key comes from an instrumentation runner argument so CI can inject it as a secret without baking it into the APK. Wires it into the existing build-debug workflow as a workflow_dispatch-only job that reuses the netbird-aar artifact, so PR builds are unaffected and the AAR is built only once per run. Required repo config: - Secret: INSTRUMENTATION_NB_SETUP_KEY (UUID, ideally reusable + ephemeral) --- .github/workflows/build-debug.yml | 17 +- .../io/netbird/client/SetupKeyAuthTest.java | 178 ++++++++++++++++++ 2 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 app/src/androidTest/java/io/netbird/client/SetupKeyAuthTest.java diff --git a/.github/workflows/build-debug.yml b/.github/workflows/build-debug.yml index 936762e4..321ce95b 100644 --- a/.github/workflows/build-debug.yml +++ b/.github/workflows/build-debug.yml @@ -5,6 +5,7 @@ on: push: branches: - main + workflow_dispatch: permissions: contents: read @@ -87,7 +88,10 @@ jobs: instrumented-tests: needs: build-debug + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest + timeout-minutes: 30 + environment: instrumentation-test-secrets steps: - name: Checkout repository uses: actions/checkout@v4 @@ -107,6 +111,15 @@ jobs: name: netbird-aar path: gomobile + - name: Verify required secrets + env: + INSTRUMENTATION_NB_SETUP_KEY: ${{ secrets.INSTRUMENTATION_NB_SETUP_KEY }} + run: | + if [ -z "$INSTRUMENTATION_NB_SETUP_KEY" ]; then + echo "::error::INSTRUMENTATION_NB_SETUP_KEY repository secret is not configured" + exit 1 + fi + - name: Enable KVM group perms run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules @@ -115,6 +128,8 @@ jobs: - name: Run instrumented tests uses: reactivecircus/android-emulator-runner@v2 + env: + INSTRUMENTATION_NB_SETUP_KEY: ${{ secrets.INSTRUMENTATION_NB_SETUP_KEY }} with: api-level: 30 target: google_apis @@ -123,7 +138,7 @@ jobs: disk-size: 4096M heap-size: 512M disable-animations: true - script: ./gradlew connectedDebugAndroidTest --no-daemon -Pandroid.testInstrumentationRunnerArguments.notClass=io.netbird.client.NetworkConnectivityStressTest + script: ./gradlew --no-daemon connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.notClass=io.netbird.client.NetworkConnectivityStressTest -Pandroid.testInstrumentationRunnerArguments.setupKey="$INSTRUMENTATION_NB_SETUP_KEY" - name: Upload test results if: always() diff --git a/app/src/androidTest/java/io/netbird/client/SetupKeyAuthTest.java b/app/src/androidTest/java/io/netbird/client/SetupKeyAuthTest.java new file mode 100644 index 00000000..bb973969 --- /dev/null +++ b/app/src/androidTest/java/io/netbird/client/SetupKeyAuthTest.java @@ -0,0 +1,178 @@ +package io.netbird.client; + +import android.os.Bundle; +import android.util.Log; +import android.view.View; + +import java.io.File; + +import androidx.navigation.NavController; +import androidx.navigation.NavOptions; +import androidx.navigation.Navigation; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.rule.ActivityTestRule; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.UiObject2; +import androidx.test.uiautomator.Until; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.netbird.client.ui.server.ChangeServerFragment; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Drives the "Change server" UI to authenticate against the default NetBird + * management server with a setup key — exactly the flow a user would use, but + * automated. + * + *

The setup key is read from an instrumentation runner argument so CI can + * inject it as a secret without baking it into the APK: + *

+ *   ./gradlew connectedDebugAndroidTest \
+ *     -Pandroid.testInstrumentationRunnerArguments.setupKey=<UUID>
+ * 
+ * + *

The test navigates straight to {@code nav_change_server} (skipping the + * first-install teaser screen), fills the setup key and taps the + * "Use NetBird" button, which uses the management URL hard-coded in the app + * ({@code Preferences.defaultServer()}). Then it waits for the success dialog. + */ +@RunWith(AndroidJUnit4.class) +public class SetupKeyAuthTest { + + private static final String TAG = "NBSetupKeyAuthTest"; + private static final String PACKAGE = "io.netbird.client"; + private static final long UI_TIMEOUT_MS = 5_000; + private static final long LOGIN_TIMEOUT_MS = 15_000; + + @SuppressWarnings("deprecation") + @Rule + public ActivityTestRule activityRule = + new ActivityTestRule<>(MainActivity.class, true, true); + + @Test + public void loginWithSetupKeyViaUi() throws Exception { + Bundle args = InstrumentationRegistry.getArguments(); + String setupKey = args.getString("setupKey"); + + assertNotNull("setupKey instrumentation argument is required", setupKey); + assertTrue("setupKey must not be blank", !setupKey.trim().isEmpty()); + + UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + device.waitForIdle(); + + // Try the navigation up to 2 times — on first launch the MainActivity + // pushes firstInstallFragment after onCreate, which can race with our + // navigate() call from the test thread. + UiObject2 setupKeyLabel = null; + for (int attempt = 1; attempt <= 2 && setupKeyLabel == null; attempt++) { + Log.i(TAG, "navigateToChangeServer attempt " + attempt); + navigateToChangeServer(); + dismissConfirmChangeServerDialog(device); + setupKeyLabel = device.wait( + Until.findObject(By.res(PACKAGE, "text_setup_key_label")), UI_TIMEOUT_MS); + } + if (setupKeyLabel == null) { + dumpScreenshot(device, "navigation-failed"); + fail("text_setup_key_label not found after 2 navigation attempts"); + } + setupKeyLabel.click(); + + UiObject2 setupKeyField = device.wait( + Until.findObject(By.res(PACKAGE, "edit_text_setup_key")), UI_TIMEOUT_MS); + assertNotNull("edit_text_setup_key must be present", setupKeyField); + setupKeyField.setText(setupKey.trim()); + + // "Use NetBird" submits with the app's default management URL. + UiObject2 submit = device.wait( + Until.findObject(By.res(PACKAGE, "btn_use_netbird")), UI_TIMEOUT_MS); + assertNotNull("btn_use_netbird must be present", submit); + submit.click(); + + // Either the success dialog ("btn_close") shows up, or the form re-enables + // itself with an error. + long deadline = System.currentTimeMillis() + LOGIN_TIMEOUT_MS; + while (System.currentTimeMillis() < deadline) { + UiObject2 closeBtn = device.findObject(By.res(PACKAGE, "btn_close")); + if (closeBtn != null) { + Log.i(TAG, "Setup-key login succeeded"); + closeBtn.click(); + return; + } + // If the "Change server" button is enabled again, the request came + // back with an error. + UiObject2 submitAgain = device.findObject(By.res(PACKAGE, "btn_change_server")); + if (submitAgain != null && submitAgain.isEnabled()) { + fail("Login failed: submit button re-enabled without success dialog"); + } + Thread.sleep(200); + } + dumpScreenshot(device, "login-timeout"); + fail("Login did not complete within " + (LOGIN_TIMEOUT_MS / 1000) + "s"); + } + + /** + * Skip the first-install teaser and jump straight to the "Change server" screen. + * {@code hideAlert=true} suppresses the "are you sure?" warning dialog so this + * is non-interactive. + */ + private void navigateToChangeServer() throws InterruptedException { + MainActivity activity = activityRule.getActivity(); + assertNotNull("MainActivity must be available", activity); + + activity.runOnUiThread(() -> { + View host = activity.findViewById(R.id.nav_host_fragment_content_main); + NavController nav = Navigation.findNavController(host); + Bundle bundle = new Bundle(); + bundle.putBoolean(ChangeServerFragment.HideAlertBundleArg, true); + // Same nav options the FirstInstallFragment uses when the user taps + // its "change_server" link, so we land in the same place. + NavOptions opts = new NavOptions.Builder() + .setPopUpTo(R.id.firstInstallFragment, true) + .build(); + nav.navigate(R.id.nav_change_server, bundle, opts); + }); + // Let the fragment transaction commit before UiAutomator looks for views. + Thread.sleep(1500); + } + + /** + * The Change Server fragment shows a "this will erase the local config" + * confirmation dialog whenever it opens — even when the caller passed + * {@code hideAlert=true}, because that arg is not currently honoured by + * the fragment. Tap Yes to dismiss so we can interact with the form. + */ + private static void dismissConfirmChangeServerDialog(UiDevice device) { + UiObject2 yes = device.wait( + Until.findObject(By.res(PACKAGE, "btn_yes")), UI_TIMEOUT_MS); + if (yes != null) { + Log.i(TAG, "Dismissing change-server confirmation dialog"); + yes.click(); + device.waitForIdle(); + } + } + + /** + * Take a screenshot via UiAutomator and write it into the test runner's + * working dir (cwd is /data/local/tmp/io.netbird.client.test on most + * devices, which `adb pull` can read). + */ + private static void dumpScreenshot(UiDevice device, String name) { + try { + File png = new File("/sdcard/Pictures/" + name + ".png"); + //noinspection ResultOfMethodCallIgnored + png.getParentFile().mkdirs(); + boolean ok = device.takeScreenshot(png); + Log.i(TAG, "Screenshot " + (ok ? "saved to " : "FAILED for ") + png); + } catch (Throwable t) { + Log.w(TAG, "Failed to dump screenshot: " + t.getMessage()); + } + } +} From 6c44c95df400c35fd532146953b5c3a3a950b735 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sun, 10 May 2026 21:45:58 +0200 Subject: [PATCH 2/5] ci: record emulator screen during instrumented tests Run adb screenrecord in a background loop (180s segments) for the full duration of connectedDebugAndroidTest, then upload the segments as an artifact. Helps diagnose UiAutomator failures that only repro in CI. --- .github/workflows/build-debug.yml | 39 ++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-debug.yml b/.github/workflows/build-debug.yml index 321ce95b..a05aaa0f 100644 --- a/.github/workflows/build-debug.yml +++ b/.github/workflows/build-debug.yml @@ -138,7 +138,35 @@ jobs: disk-size: 4096M heap-size: 512M disable-animations: true - script: ./gradlew --no-daemon connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.notClass=io.netbird.client.NetworkConnectivityStressTest -Pandroid.testInstrumentationRunnerArguments.setupKey="$INSTRUMENTATION_NB_SETUP_KEY" + script: | + set +e + mkdir -p screen-recordings + adb shell mkdir -p /sdcard/recordings + + # screenrecord caps each segment at 180s, so loop until we stop it. + ( + i=0 + while [ -f /tmp/record_active ]; do + seg=$(printf "seg_%03d.mp4" "$i") + adb shell screenrecord --time-limit 180 --bit-rate 4000000 /sdcard/recordings/$seg + i=$((i+1)) + done + ) & + REC_LOOP_PID=$! + touch /tmp/record_active + + ./gradlew --no-daemon connectedDebugAndroidTest \ + -Pandroid.testInstrumentationRunnerArguments.notClass=io.netbird.client.NetworkConnectivityStressTest \ + -Pandroid.testInstrumentationRunnerArguments.setupKey="$INSTRUMENTATION_NB_SETUP_KEY" + TEST_EXIT=$? + + rm -f /tmp/record_active + adb shell pkill -SIGINT screenrecord 2>/dev/null || true + wait $REC_LOOP_PID 2>/dev/null || true + sleep 3 + adb pull /sdcard/recordings ./screen-recordings/ || true + + exit $TEST_EXIT - name: Upload test results if: always() @@ -149,3 +177,12 @@ jobs: app/build/reports/androidTests/ tool/build/reports/androidTests/ retention-days: 3 + + - name: Upload screen recordings + if: always() + uses: actions/upload-artifact@v4 + with: + name: instrumented-test-screen-recordings + path: screen-recordings/ + if-no-files-found: warn + retention-days: 3 From 3692291d069c52f8e4bfb8a109cfd0f599cc80cb Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sun, 10 May 2026 21:57:46 +0200 Subject: [PATCH 3/5] ci: cache NDK/gomobile/AVD and move test runner to a script - setup-go: point cache-dependency-path to netbird/go.sum so the Go module cache actually restores (build-android-lib.sh shaves ~1-2 min). - Cache the NDK install dir and the gomobile binary by pinned version, skipping the corresponding install step on cache hit. - Cache the AVD plus the API 30 google_apis x86_64 system image, add a warm-up step that boots once to capture a snapshot, and run tests with force-avd-creation=false plus -no-snapshot-save so subsequent jobs load the cached snapshot without overwriting it. - Move the screenrecord/gradle/pull pipeline to .github/scripts/run-instrumented-tests.sh because the emulator-runner action executes each line of the inline script as a separate sh -c, which broke the multi-line subshell loop. --- .github/actions/build-android/action.yml | 17 +++++++ .github/scripts/run-instrumented-tests.sh | 35 ++++++++++++++ .github/workflows/build-debug.yml | 57 +++++++++++------------ 3 files changed, 80 insertions(+), 29 deletions(-) create mode 100755 .github/scripts/run-instrumented-tests.sh diff --git a/.github/actions/build-android/action.yml b/.github/actions/build-android/action.yml index a92c7efb..cdd59187 100644 --- a/.github/actions/build-android/action.yml +++ b/.github/actions/build-android/action.yml @@ -49,8 +49,17 @@ runs: uses: actions/setup-go@v5 with: go-version-file: "netbird/go.mod" + cache-dependency-path: "netbird/go.sum" + + - name: Cache Android NDK + id: ndk-cache + uses: actions/cache@v4 + with: + path: ${{ env.ANDROID_HOME }}/ndk/23.1.7779620 + key: ndk-23.1.7779620 - name: Setup NDK + if: steps.ndk-cache.outputs.cache-hit != 'true' shell: bash run: ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --install "ndk;23.1.7779620" @@ -58,7 +67,15 @@ runs: shell: bash run: echo "ANDROID_NDK_HOME=${ANDROID_HOME}/ndk/23.1.7779620" >> $GITHUB_ENV + - name: Cache gomobile binary + id: gomobile-cache + uses: actions/cache@v4 + with: + path: ~/go/bin/gomobile + key: gomobile-v0.0.0-20251113184115-a159579294ab + - name: Install gomobile + if: steps.gomobile-cache.outputs.cache-hit != 'true' shell: bash run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20251113184115-a159579294ab diff --git a/.github/scripts/run-instrumented-tests.sh b/.github/scripts/run-instrumented-tests.sh new file mode 100755 index 00000000..1d4548f1 --- /dev/null +++ b/.github/scripts/run-instrumented-tests.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Runs connectedDebugAndroidTest while recording the emulator screen in the +# background. screenrecord caps each clip at 180s, so we loop until the test +# finishes, then upload the segments as an artifact from the workflow. +# +# Expects $INSTRUMENTATION_NB_SETUP_KEY in the environment. + +set +e + +mkdir -p screen-recordings +adb shell mkdir -p /sdcard/recordings + +( + i=0 + while [ -f /tmp/record_active ]; do + seg=$(printf "seg_%03d.mp4" "$i") + adb shell screenrecord --time-limit 180 --bit-rate 4000000 "/sdcard/recordings/$seg" + i=$((i + 1)) + done +) & +REC_LOOP_PID=$! +touch /tmp/record_active + +./gradlew --no-daemon connectedDebugAndroidTest \ + -Pandroid.testInstrumentationRunnerArguments.notClass=io.netbird.client.NetworkConnectivityStressTest \ + -Pandroid.testInstrumentationRunnerArguments.setupKey="$INSTRUMENTATION_NB_SETUP_KEY" +TEST_EXIT=$? + +rm -f /tmp/record_active +adb shell pkill -SIGINT screenrecord 2>/dev/null || true +wait "$REC_LOOP_PID" 2>/dev/null || true +sleep 3 +adb pull /sdcard/recordings ./screen-recordings/ || true + +exit $TEST_EXIT diff --git a/.github/workflows/build-debug.yml b/.github/workflows/build-debug.yml index a05aaa0f..6e1a4d6c 100644 --- a/.github/workflows/build-debug.yml +++ b/.github/workflows/build-debug.yml @@ -126,6 +126,31 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm + - name: AVD cache + id: avd-cache + uses: actions/cache@v4 + with: + path: | + ~/.android/avd/* + ~/.android/adb* + ${{ env.ANDROID_HOME }}/system-images/android-30/google_apis/x86_64 + key: avd-api30-google_apis-x86_64-pixel_3a-v1 + + - name: Create AVD snapshot + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 30 + target: google_apis + arch: x86_64 + profile: pixel_3a + disk-size: 4096M + heap-size: 512M + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim + disable-animations: true + script: echo "Generated AVD snapshot for caching." + - name: Run instrumented tests uses: reactivecircus/android-emulator-runner@v2 env: @@ -137,36 +162,10 @@ jobs: profile: pixel_3a disk-size: 4096M heap-size: 512M + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim disable-animations: true - script: | - set +e - mkdir -p screen-recordings - adb shell mkdir -p /sdcard/recordings - - # screenrecord caps each segment at 180s, so loop until we stop it. - ( - i=0 - while [ -f /tmp/record_active ]; do - seg=$(printf "seg_%03d.mp4" "$i") - adb shell screenrecord --time-limit 180 --bit-rate 4000000 /sdcard/recordings/$seg - i=$((i+1)) - done - ) & - REC_LOOP_PID=$! - touch /tmp/record_active - - ./gradlew --no-daemon connectedDebugAndroidTest \ - -Pandroid.testInstrumentationRunnerArguments.notClass=io.netbird.client.NetworkConnectivityStressTest \ - -Pandroid.testInstrumentationRunnerArguments.setupKey="$INSTRUMENTATION_NB_SETUP_KEY" - TEST_EXIT=$? - - rm -f /tmp/record_active - adb shell pkill -SIGINT screenrecord 2>/dev/null || true - wait $REC_LOOP_PID 2>/dev/null || true - sleep 3 - adb pull /sdcard/recordings ./screen-recordings/ || true - - exit $TEST_EXIT + script: bash .github/scripts/run-instrumented-tests.sh - name: Upload test results if: always() From 173fd62e580c93d6d722fd44fda394df667ace54 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sun, 10 May 2026 22:12:10 +0200 Subject: [PATCH 4/5] ci: fix cache path resolution and screenrecord loop race The ${{ env.ANDROID_HOME }} expression resolves against workflow-level env, not the runner's process environment, so the NDK and AVD system- image cache paths came out empty-prefixed and the saves were dropped with "Path Validation Error". Hardcode the ubuntu-latest sdk path (/usr/local/lib/android/sdk) for both. Also fix the screen recording: touch the sentinel file BEFORE launching the background loop, otherwise the loop sees no file on its first iteration and exits immediately, producing zero recordings. --- .github/actions/build-android/action.yml | 4 +++- .github/scripts/run-instrumented-tests.sh | 7 ++++++- .github/workflows/build-debug.yml | 5 ++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/actions/build-android/action.yml b/.github/actions/build-android/action.yml index cdd59187..744839f3 100644 --- a/.github/actions/build-android/action.yml +++ b/.github/actions/build-android/action.yml @@ -55,7 +55,9 @@ runs: id: ndk-cache uses: actions/cache@v4 with: - path: ${{ env.ANDROID_HOME }}/ndk/23.1.7779620 + # ANDROID_HOME is set by the runner image but not visible to ${{ env.X }} + # in composite actions; the ubuntu-latest image pins it to this path. + path: /usr/local/lib/android/sdk/ndk/23.1.7779620 key: ndk-23.1.7779620 - name: Setup NDK diff --git a/.github/scripts/run-instrumented-tests.sh b/.github/scripts/run-instrumented-tests.sh index 1d4548f1..2c3009d6 100755 --- a/.github/scripts/run-instrumented-tests.sh +++ b/.github/scripts/run-instrumented-tests.sh @@ -10,16 +10,21 @@ set +e mkdir -p screen-recordings adb shell mkdir -p /sdcard/recordings +# Sentinel must exist before the background loop starts; otherwise the first +# iteration sees no file and exits immediately. +touch /tmp/record_active ( i=0 while [ -f /tmp/record_active ]; do seg=$(printf "seg_%03d.mp4" "$i") + echo "[record] starting $seg" adb shell screenrecord --time-limit 180 --bit-rate 4000000 "/sdcard/recordings/$seg" + echo "[record] $seg exited" i=$((i + 1)) done + echo "[record] loop ended" ) & REC_LOOP_PID=$! -touch /tmp/record_active ./gradlew --no-daemon connectedDebugAndroidTest \ -Pandroid.testInstrumentationRunnerArguments.notClass=io.netbird.client.NetworkConnectivityStressTest \ diff --git a/.github/workflows/build-debug.yml b/.github/workflows/build-debug.yml index 6e1a4d6c..359d6cf2 100644 --- a/.github/workflows/build-debug.yml +++ b/.github/workflows/build-debug.yml @@ -130,10 +130,13 @@ jobs: id: avd-cache uses: actions/cache@v4 with: + # ANDROID_HOME is set by the runner image but not visible to + # ${{ env.X }} at expression-eval time; hardcoded to the path on + # ubuntu-latest. path: | ~/.android/avd/* ~/.android/adb* - ${{ env.ANDROID_HOME }}/system-images/android-30/google_apis/x86_64 + /usr/local/lib/android/sdk/system-images/android-30/google_apis/x86_64 key: avd-api30-google_apis-x86_64-pixel_3a-v1 - name: Create AVD snapshot From c85852eabf8ad35f7411c38cbb6075a95754e80e Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sun, 10 May 2026 22:23:35 +0200 Subject: [PATCH 5/5] ci: stream adb logcat alongside the screen recording Save threadtime-format logcat to screen-recordings/logcat.log in the background, in parallel with the screenrecord loop, and stop it after the test exits. The existing screen-recordings/ artifact upload picks it up automatically. --- .github/scripts/run-instrumented-tests.sh | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/scripts/run-instrumented-tests.sh b/.github/scripts/run-instrumented-tests.sh index 2c3009d6..21411b7b 100755 --- a/.github/scripts/run-instrumented-tests.sh +++ b/.github/scripts/run-instrumented-tests.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash -# Runs connectedDebugAndroidTest while recording the emulator screen in the -# background. screenrecord caps each clip at 180s, so we loop until the test -# finishes, then upload the segments as an artifact from the workflow. +# Runs connectedDebugAndroidTest while capturing the emulator screen and +# logcat in the background. screenrecord caps each clip at 180s, so we loop +# until the test finishes; logcat streams continuously to a file. Both end +# up in screen-recordings/ and are uploaded as an artifact from the workflow. # # Expects $INSTRUMENTATION_NB_SETUP_KEY in the environment. @@ -10,6 +11,10 @@ set +e mkdir -p screen-recordings adb shell mkdir -p /sdcard/recordings +adb logcat -c +adb logcat -v threadtime > screen-recordings/logcat.log 2>&1 & +LOGCAT_PID=$! + # Sentinel must exist before the background loop starts; otherwise the first # iteration sees no file and exits immediately. touch /tmp/record_active @@ -37,4 +42,7 @@ wait "$REC_LOOP_PID" 2>/dev/null || true sleep 3 adb pull /sdcard/recordings ./screen-recordings/ || true +kill "$LOGCAT_PID" 2>/dev/null || true +wait "$LOGCAT_PID" 2>/dev/null || true + exit $TEST_EXIT