Skip to content
Merged
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
14 changes: 14 additions & 0 deletions mobile-app/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ android {
versionName = flutter.versionName

multiDexEnabled true

// Patrol native automation: run integration tests through Patrol's JUnit
// runner. `clearPackageData` wipes app data between test runs so each
// Dart test starts from a clean state (mirrors iOS --full-isolation).
testInstrumentationRunner "pl.leancode.patrol.PatrolJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: "true"
}

testOptions {
execution "ANDROIDX_TEST_ORCHESTRATOR"
}

buildTypes {
Expand Down Expand Up @@ -84,6 +94,10 @@ flutter {

dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'

// Patrol native automation: AndroidX Test Orchestrator runs each Dart test
// in its own instrumentation invocation (required by ANDROIDX_TEST_ORCHESTRATOR).
androidTestUtil 'androidx.test:orchestrator:1.5.1'
}

// Apply cargokit directly to the main app
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.quantus.wallet;

import androidx.test.platform.app.InstrumentationRegistry;

import com.example.resonance_network_wallet.MainActivity;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

import pl.leancode.patrol.PatrolJUnitRunner;

// This is the entry point that lets Patrol drive the Flutter integration tests
// under patrol_test/ as native Android instrumentation tests. Each Dart test is
// surfaced as a parameterized JUnit test case so it can be run/filtered
// individually by xcodebuild's Android equivalent (the orchestrator).
@RunWith(Parameterized.class)
public class MainActivityTest {
@Parameters(name = "{0}")
public static Object[] testCases() {
PatrolJUnitRunner instrumentation =
(PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation();
instrumentation.setUp(MainActivity.class);
instrumentation.waitForPatrolAppService();
return instrumentation.listDartTests();
}

public MainActivityTest(String dartTestName) {
this.dartTestName = dartTestName;
}

private final String dartTestName;

@Test
public void runDartTest() {
PatrolJUnitRunner instrumentation =
(PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation();
instrumentation.runDartTest(dartTestName);
}
}
105 changes: 105 additions & 0 deletions mobile-app/scripts/lib/patrol_common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Shared helpers for Patrol E2E runner scripts under mobile-app/scripts/.
# Source from a script in that directory, e.g.:
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/patrol_common.sh"

# Resolve SCRIPT_DIR / APP_ROOT from the calling script and cd to APP_ROOT.
patrol_resolve_app_root() {
local caller_source="${1:?patrol_resolve_app_root requires the caller script path}"
SCRIPT_DIR="$(cd "$(dirname "$caller_source")" && pwd)"
APP_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$APP_ROOT"
}

# Default BUILD_MODE and TEST_TARGETS before parsing CLI args.
patrol_init_runner_args() {
BUILD_MODE="--debug"
TEST_TARGETS=()
}

# Parse shared Patrol runner flags. Platform-specific options are delegated to
# the optional callback, which must set PLATFORM_CONSUMED to the number of args
# consumed and return 0 when it handles a flag (return 1 when unrecognized).
#
# Usage:
# patrol_parse_runner_args <usage_fn> <supports_build_mode:true|false> \
# [<platform_option_fn>] "$@"
patrol_parse_runner_args() {
local usage_fn="$1"
local supports_build_mode="$2"
local platform_option_fn="${3:--}"
shift 3

patrol_init_runner_args

while [[ $# -gt 0 ]]; do
PLATFORM_CONSUMED=0
if [[ "$platform_option_fn" != "-" ]] && "$platform_option_fn" "$@"; then
shift "$PLATFORM_CONSUMED"
continue
fi

case "$1" in
--debug|--release)
if [[ "$supports_build_mode" != "true" ]]; then
echo "ERROR: unknown option: $1" >&2
"$usage_fn"
exit 1
fi
BUILD_MODE="$1"
shift
;;
-h|--help)
"$usage_fn"
exit 0
;;
--)
shift
TEST_TARGETS+=("$@")
break
;;
-*)
echo "ERROR: unknown option: $1" >&2
"$usage_fn"
exit 1
;;
*)
TEST_TARGETS+=("$1")
shift
;;
esac
done
}

# Test secrets/fixtures (e.g. TEST_IMPORT_MNEMONIC) are injected at build time via
# --dart-define so they are never bundled into the app as an asset.
# * Locally: read from a gitignored .env.test (key=value) via --dart-define-from-file.
# * CI: export TEST_IMPORT_MNEMONIC (and any others) from the runner's secret store.
patrol_collect_dart_defines() {
DART_DEFINES=()
if [[ -f .env.test ]]; then
echo "==> Injecting test secrets from .env.test"
DART_DEFINES+=(--dart-define-from-file=.env.test)
elif [[ -n "${TEST_IMPORT_MNEMONIC:-}" ]]; then
echo "==> Injecting test secrets from environment"
DART_DEFINES+=(--dart-define=TEST_IMPORT_MNEMONIC="$TEST_IMPORT_MNEMONIC")
else
echo "WARNING: no .env.test file and TEST_IMPORT_MNEMONIC is unset;" \
"tests that need a seed phrase (e.g. import_wallet) will fail." >&2
fi
}

# Build the `-t <target>` flags from TEST_TARGETS. With no targets, patrol bundles
# every `*_test.dart` under `patrol_test/`, i.e. the whole suite in one binary.
# Optional first argument is the verb in the "no targets" message (default: running).
patrol_build_target_args() {
local action="${1:-running}"
TARGET_ARGS=()
if [[ ${#TEST_TARGETS[@]} -gt 0 ]]; then
for target in "${TEST_TARGETS[@]}"; do
TARGET_ARGS+=(-t "$target")
done
echo "==> Test targets: ${TEST_TARGETS[*]}"
else
echo "==> No targets given; ${action} the WHOLE patrol_test suite."
fi
}
173 changes: 173 additions & 0 deletions mobile-app/scripts/patrol_android_emulator.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
#!/usr/bin/env bash
#
# Run Patrol E2E tests on an Android emulator.
#
# Android counterpart of scripts/patrol_ios_device.sh, scoped to emulators for
# local E2E runs. Unlike iOS physical devices, `patrol test` can build and run
# in one step on Android without the xcodebuild destination-timeout workaround.
#
# Usage:
# scripts/patrol_android_emulator.sh [options] [test_target ...]
#
# Options:
# -d, --device <serial> Emulator serial (default: first running emulator).
# -a, --avd <name> Start this AVD when no emulator is running.
# --debug Build a debug binary (default).
# --release Build a release binary.
#
# Environment:
# ANDROID_EMULATOR_AVD Default AVD to boot when none is running and -a
# is not passed (falls back to the first listed AVD).
# EMULATOR_BOOT_TIMEOUT Seconds to wait for boot (default: 120).
#
# Test targets:
# * Pass zero targets to run the WHOLE suite: patrol bundles every
# `*_test.dart` under `patrol_test/` into a single app binary.
# * Pass one or more targets to run only those files.
#
# Examples:
# # Run all e2e tests on the first running emulator:
# scripts/patrol_android_emulator.sh
#
# # Boot a specific AVD, then run tests:
# scripts/patrol_android_emulator.sh -a Pixel_8_API_35
#
# # Run a single test:
# scripts/patrol_android_emulator.sh patrol_test/smoke/hello_world_test.dart
#
# Notes:
# * Start an emulator in Android Studio, or pass -a / set ANDROID_EMULATOR_AVD.
# * Run from anywhere; paths are resolved relative to this script.

# Note: intentionally not using `pipefail`. A step pipes into `head`, which
# closes the pipe early and would otherwise raise SIGPIPE (exit 141) and abort
# the whole script under `set -e`.
set -eu

# shellcheck source=lib/patrol_common.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/patrol_common.sh"

usage() {
echo "Usage: patrol_android_emulator.sh [-d <serial>] [-a <avd>] [--debug|--release] [test_target ...]" >&2
}

patrol_android_emulator_platform_option() {
case "$1" in
-d|--device)
DEVICE_SERIAL="${2:?--device requires a serial}"
PLATFORM_CONSUMED=2
return 0
;;
-a|--avd)
AVD_NAME="${2:?--avd requires a name}"
PLATFORM_CONSUMED=2
return 0
;;
esac
return 1
}

first_running_emulator() {
adb devices 2>/dev/null | awk '$2 == "device" && $1 ~ /^emulator-/ {print $1; exit}'
}

first_listed_avd() {
if ! command -v emulator >/dev/null 2>&1; then
return 1
fi
emulator -list-avds 2>/dev/null | head -1
}

wait_for_emulator_boot() {
local serial="$1"
local timeout="${EMULATOR_BOOT_TIMEOUT:-120}"
echo "==> Waiting for $serial to finish booting (timeout ${timeout}s)..."
adb -s "$serial" wait-for-device
local start
start="$(date +%s)"
while true; do
local boot_completed
boot_completed="$(adb -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')"
if [[ "$boot_completed" == "1" ]]; then
echo "==> Emulator ready: $serial"
return 0
fi
if (( $(date +%s) - start > timeout )); then
echo "ERROR: Emulator $serial did not boot within ${timeout}s." >&2
exit 1
fi
sleep 2
done
}

start_emulator() {
local avd_name="$1"
if ! command -v emulator >/dev/null 2>&1; then
echo "ERROR: \`emulator\` not found. Install the Android SDK emulator or" \
"start an AVD from Android Studio." >&2
exit 1
fi

echo "==> Starting emulator: $avd_name"
emulator -avd "$avd_name" -no-snapshot-load >/dev/null 2>&1 &
local start
start="$(date +%s)"
local timeout="${EMULATOR_BOOT_TIMEOUT:-120}"
while true; do
local serial
serial="$(first_running_emulator || true)"
if [[ -n "$serial" ]]; then
DEVICE_SERIAL="$serial"
return 0
fi
if (( $(date +%s) - start > timeout )); then
echo "ERROR: Emulator process started but no serial appeared within ${timeout}s." >&2
exit 1
fi
sleep 2
done
}

DEVICE_SERIAL=""
AVD_NAME=""
patrol_parse_runner_args usage true patrol_android_emulator_platform_option "$@"

patrol_resolve_app_root "${BASH_SOURCE[0]}"

# Pick or boot an emulator.
if [[ -z "$DEVICE_SERIAL" ]]; then
DEVICE_SERIAL="$(first_running_emulator || true)"
if [[ -n "$DEVICE_SERIAL" ]]; then
echo "==> Auto-selected running emulator: $DEVICE_SERIAL"
fi
fi

if [[ -z "$DEVICE_SERIAL" ]]; then
if [[ -z "$AVD_NAME" ]]; then
AVD_NAME="${ANDROID_EMULATOR_AVD:-$(first_listed_avd || true)}"
fi
if [[ -z "$AVD_NAME" ]]; then
echo "ERROR: No running emulator found and no AVD to boot." >&2
echo " Start one in Android Studio, pass -a <avd>, or set ANDROID_EMULATOR_AVD." >&2
exit 1
fi
start_emulator "$AVD_NAME"
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reused emulator skips boot wait

Medium Severity

If a running emulator is auto-selected or chosen with -d, the script never calls wait_for_emulator_boot, unlike the path that starts a new AVD. Patrol can start while sys.boot_completed is still unset, leading to flaky or failed instrumentation runs.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7c38e21. Configure here.


wait_for_emulator_boot "$DEVICE_SERIAL"

if [[ "$DEVICE_SERIAL" != emulator-* ]]; then
echo "WARNING: $DEVICE_SERIAL does not look like an emulator serial." >&2
fi

patrol_collect_dart_defines
patrol_build_target_args running

echo "==> Running Patrol tests on $DEVICE_SERIAL ($BUILD_MODE)..."
patrol test \
--device "$DEVICE_SERIAL" \
"$BUILD_MODE" \
${TARGET_ARGS[@]+"${TARGET_ARGS[@]}"} \
${DART_DEFINES[@]+"${DART_DEFINES[@]}"}

echo "==> Done."
Loading
Loading