test: expand Android lifecycle and API matrix coverage
This commit is contained in:
parent
378d0b4563
commit
460ec21253
|
|
@ -27,6 +27,8 @@ WNH_BASE_URL=
|
|||
# production capacity recommendations.
|
||||
WNH_TRACKING_MIN_TIME_MS=5000
|
||||
WNH_TRACKING_HTTP_TIMEOUT_MS=15000
|
||||
WNH_ANDROID_TEST_API_MATRIX=30 34 37.0
|
||||
WNH_ANDROID_TEST_DATA_PARTITION_SIZE=4G
|
||||
# Public raster tile template used by MapLibre. Use a provider whose policy and
|
||||
# capacity match the deployment before a public launch.
|
||||
MAP_TILE_URL=https://tile.openstreetmap.org/{z}/{x}/{y}.png
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -247,21 +247,25 @@ It retains traces, screenshots, video, and Compose logs under the ignored
|
|||
networks are removed automatically; the normal `who_need_help` Compose project
|
||||
is not recreated.
|
||||
|
||||
The Android device suite builds a dedicated debug and instrumentation APK,
|
||||
boots an API 37 emulator in an isolated container without external networking,
|
||||
and serves its HTTP fixture only on device loopback:
|
||||
The Android device suite builds dedicated debug and instrumentation APKs,
|
||||
boots a fresh emulator in an isolated container without external networking,
|
||||
and serves its HTTP fixture only on device loopback. Run the default API 37
|
||||
probe or the complete API 30/34/37 matrix:
|
||||
|
||||
```bash
|
||||
./scripts/android-instrumentation-test.sh
|
||||
./scripts/android-matrix-test.sh
|
||||
```
|
||||
|
||||
On first run it generates the ignored `.env.android-test` with a randomized
|
||||
device-loopback origin and mode `0600`. The suite covers denied and granted
|
||||
location permission, same-origin deep links, Activity recreation, foreground
|
||||
location upload, the persistent notification Stop action, and a disconnected
|
||||
Stop request with visible retry state. Results and failure diagnostics are
|
||||
retained under ignored `output/android-instrumentation/`; the exact emulator
|
||||
container and one-run image are removed automatically.
|
||||
location upload while the Activity is backgrounded and destroyed, the
|
||||
persistent notification Stop action, a disconnected Stop request with visible
|
||||
retry state, and externally forced process death for the non-sticky service.
|
||||
Results and failure diagnostics are retained by API under ignored
|
||||
`output/android-instrumentation/`; the exact emulator container and one-run
|
||||
image are removed automatically.
|
||||
|
||||
## First administrator
|
||||
|
||||
|
|
|
|||
|
|
@ -57,11 +57,15 @@ FROM android-base AS emulator
|
|||
USER root
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
ARG ANDROID_EMULATOR_SYSTEM_IMAGE=system-images/android-37.0/google_apis_ps16k/x86_64
|
||||
|
||||
RUN android --no-metrics --sdk="${ANDROID_HOME}" sdk install \
|
||||
"emulator" \
|
||||
"system-images/android-37.0/google_apis_ps16k/x86_64" \
|
||||
"${ANDROID_EMULATOR_SYSTEM_IMAGE}" \
|
||||
&& chown -R gradle:gradle "${ANDROID_HOME}"
|
||||
|
||||
ARG ANDROID_EMULATOR_DATA_PARTITION_SIZE=4G
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
libdbus-1-3=1.16.2-2ubuntu4 \
|
||||
|
|
@ -89,8 +93,11 @@ USER gradle
|
|||
|
||||
RUN echo no | avdmanager create avd \
|
||||
--force \
|
||||
--name who_need_help_api37 \
|
||||
--package "system-images;android-37.0;google_apis_ps16k;x86_64"
|
||||
--name who_need_help_test \
|
||||
--package "$(printf '%s' "${ANDROID_EMULATOR_SYSTEM_IMAGE}" | tr '/' ';')" \
|
||||
&& sed -i \
|
||||
"s/^disk.dataPartition.size=.*/disk.dataPartition.size=${ANDROID_EMULATOR_DATA_PARTITION_SIZE}/" \
|
||||
/home/gradle/.android/avd/who_need_help_test.avd/config.ini
|
||||
|
||||
COPY --from=android-sdk --chown=gradle:gradle \
|
||||
/workspace/android/app/build/outputs/apk/debug/app-debug.apk \
|
||||
|
|
@ -100,7 +107,7 @@ COPY --from=android-sdk --chown=gradle:gradle \
|
|||
/opt/who-need-help/who-need-help-debug-androidTest.apk
|
||||
|
||||
CMD ["/opt/android-sdk/emulator/emulator", \
|
||||
"-avd", "who_need_help_api37", \
|
||||
"-avd", "who_need_help_test", \
|
||||
"-no-window", \
|
||||
"-no-audio", \
|
||||
"-no-boot-anim", \
|
||||
|
|
|
|||
|
|
@ -73,28 +73,33 @@ The Docker build runs JVM unit tests, Android lint, `assembleDebug`, and
|
|||
`assembleDebugAndroidTest` before it exports the application APK and lint
|
||||
report.
|
||||
|
||||
## Automated API 37 device tests
|
||||
## Automated device tests
|
||||
|
||||
Run the complete device suite from the repository root:
|
||||
Run the default API 37 suite or the complete API 30/34/37 matrix from the
|
||||
repository root:
|
||||
|
||||
```sh
|
||||
./scripts/android-instrumentation-test.sh
|
||||
./scripts/android-matrix-test.sh
|
||||
```
|
||||
|
||||
The command requires `/dev/kvm`. It generates an ignored
|
||||
`.env.android-test` once with a randomized `http://127.0.0.1:PORT` origin and
|
||||
mode `0600`; the application and its in-process fixture server both derive the
|
||||
origin from that file. It then builds both APKs, boots a fresh API 37 emulator
|
||||
origin from that file. It then builds both APKs, boots a fresh selected emulator
|
||||
container without external networking, injects emulator coordinates, and runs
|
||||
`AndroidJUnitRunner`.
|
||||
`AndroidJUnitRunner`. `WNH_ANDROID_TEST_API` selects one supported API, while
|
||||
`WNH_ANDROID_TEST_API_MATRIX` controls the matrix command.
|
||||
|
||||
The tests cover the missing-location-permission boundary, the WebView-triggered
|
||||
Android permission dialog, same-origin deep-link routing across Activity
|
||||
recreation, foreground location upload, the persistent notification Stop
|
||||
action, and a disconnected Stop request with visible retry state. Results are
|
||||
stored under ignored `output/android-instrumentation/`. On failure, logcat,
|
||||
service state, and emulator logs are retained; the exact container and
|
||||
one-run image are removed in either outcome.
|
||||
recreation, foreground location upload after Home and Activity destruction, the
|
||||
persistent notification Stop action, a disconnected Stop request with visible
|
||||
retry state, and externally forced process death for the non-sticky tracking
|
||||
service. Results are stored by API under ignored
|
||||
`output/android-instrumentation/`. On failure, logcat, service state, and
|
||||
emulator logs are retained; the exact container and one-run image are removed
|
||||
in either outcome.
|
||||
|
||||
## Emulator verification
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ public final class AndroidClientInstrumentedTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void test02WebGeolocationRequestsAndGrantsRuntimePermission() {
|
||||
public void test02WebGeolocationRequestsAndGrantsRuntimePermission() throws Exception {
|
||||
assertFalse(hasLocationPermission());
|
||||
|
||||
try (
|
||||
|
|
@ -119,7 +119,18 @@ public final class AndroidClientInstrumentedTest {
|
|||
assertNotNull("Android location permission prompt was not shown", allow);
|
||||
allow.click();
|
||||
|
||||
assertTrue(hasLocationPermission());
|
||||
waitForLocationPermission();
|
||||
assertNotNull(
|
||||
"WebView geolocation did not report a granted position",
|
||||
server.awaitRequestEndingWith(
|
||||
"/geolocation-granted",
|
||||
15,
|
||||
TimeUnit.SECONDS
|
||||
)
|
||||
);
|
||||
onWebView()
|
||||
.withElement(findElement(Locator.ID, "marker"))
|
||||
.check(webMatches(getText(), containsString("location-granted")));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -213,6 +224,64 @@ public final class AndroidClientInstrumentedTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test06TrackingSurvivesHomeAndActivityDestruction() throws Exception {
|
||||
grantTrackingPermissions();
|
||||
String assignmentId = UUID.randomUUID().toString();
|
||||
String positionPath = "/mobile/tracking/" + assignmentId + "/position";
|
||||
ActivityScenario<MainActivity> scenario = launch("/tracking-lifecycle");
|
||||
|
||||
try {
|
||||
startTracking(scenario, assignmentId);
|
||||
waitForServiceState(true);
|
||||
assertNotNull(
|
||||
"Foreground service did not upload its first location",
|
||||
server.awaitRequestEndingWith(positionPath, 15, TimeUnit.SECONDS)
|
||||
);
|
||||
|
||||
int foregroundCount = server.requestCountEndingWith(positionPath);
|
||||
device.pressHome();
|
||||
|
||||
assertNotNull(
|
||||
"Tracking stopped after the Activity left the foreground",
|
||||
server.awaitRequestAfterCount(
|
||||
positionPath,
|
||||
foregroundCount,
|
||||
15,
|
||||
TimeUnit.SECONDS
|
||||
)
|
||||
);
|
||||
assertTrue(serviceIsRunning());
|
||||
|
||||
int stoppedActivityCount = server.requestCountEndingWith(positionPath);
|
||||
scenario.close();
|
||||
|
||||
assertNotNull(
|
||||
"Tracking stopped after the Activity was destroyed",
|
||||
server.awaitRequestAfterCount(
|
||||
positionPath,
|
||||
stoppedActivityCount,
|
||||
15,
|
||||
TimeUnit.SECONDS
|
||||
)
|
||||
);
|
||||
assertTrue(serviceIsRunning());
|
||||
|
||||
openNotificationAndClickStop();
|
||||
assertNotNull(
|
||||
"Notification Stop did not work after Activity destruction",
|
||||
server.awaitRequestEndingWith(
|
||||
"/mobile/tracking/" + assignmentId + "/stop",
|
||||
10,
|
||||
TimeUnit.SECONDS
|
||||
)
|
||||
);
|
||||
waitForServiceState(false);
|
||||
} finally {
|
||||
scenario.close();
|
||||
}
|
||||
}
|
||||
|
||||
private ActivityScenario<MainActivity> launch(String path) {
|
||||
Intent intent = new Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
|
|
@ -262,6 +331,20 @@ public final class AndroidClientInstrumentedTest {
|
|||
== android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private void waitForLocationPermission() throws InterruptedException {
|
||||
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(UI_TIMEOUT_MS);
|
||||
|
||||
while (System.nanoTime() < deadline) {
|
||||
if (hasLocationPermission()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Thread.sleep(50);
|
||||
}
|
||||
|
||||
assertTrue("Android location permission was not granted", hasLocationPermission());
|
||||
}
|
||||
|
||||
private void grantTrackingPermissions() throws Exception {
|
||||
String packageName = context.getPackageName();
|
||||
device.executeShellCommand(
|
||||
|
|
|
|||
|
|
@ -66,16 +66,45 @@ final class FixtureHttpServer implements Closeable {
|
|||
|
||||
RecordedRequest awaitRequestEndingWith(String suffix, long timeout, TimeUnit unit)
|
||||
throws InterruptedException {
|
||||
long deadline = System.nanoTime() + unit.toNanos(timeout);
|
||||
return awaitRequestAfterCount(suffix, 0, timeout, unit);
|
||||
}
|
||||
|
||||
int requestCountEndingWith(String suffix) {
|
||||
int count = 0;
|
||||
|
||||
while (System.nanoTime() < deadline) {
|
||||
synchronized (requests) {
|
||||
for (RecordedRequest request : requests) {
|
||||
if (request.path.endsWith(suffix)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
RecordedRequest awaitRequestAfterCount(
|
||||
String suffix,
|
||||
int previousCount,
|
||||
long timeout,
|
||||
TimeUnit unit
|
||||
) throws InterruptedException {
|
||||
long deadline = System.nanoTime() + unit.toNanos(timeout);
|
||||
|
||||
while (System.nanoTime() < deadline) {
|
||||
int count = 0;
|
||||
|
||||
synchronized (requests) {
|
||||
for (RecordedRequest request : requests) {
|
||||
if (request.path.endsWith(suffix)) {
|
||||
count++;
|
||||
|
||||
if (count > previousCount) {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Thread.sleep(50);
|
||||
}
|
||||
|
|
@ -138,7 +167,7 @@ final class FixtureHttpServer implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
byte[] bodyBytes = input.readNBytes(contentLength);
|
||||
byte[] bodyBytes = readExactly(input, contentLength);
|
||||
RecordedRequest request = new RecordedRequest(
|
||||
parts[0],
|
||||
parts[1],
|
||||
|
|
@ -182,8 +211,11 @@ final class FixtureHttpServer implements Closeable {
|
|||
+ "<title>Who Need Help Android fixture</title></head><body>"
|
||||
+ "<h1 id=\"marker\">loaded:" + escapedPath + "</h1>"
|
||||
+ "<button id=\"location\" onclick=\"navigator.geolocation.getCurrentPosition("
|
||||
+ "()=>document.getElementById('marker').textContent='location-granted',"
|
||||
+ "()=>document.getElementById('marker').textContent='location-denied')\">"
|
||||
+ "()=>{document.getElementById('marker').textContent='location-granted';"
|
||||
+ "fetch('/geolocation-granted')},"
|
||||
+ "()=>{document.getElementById('marker').textContent='location-denied';"
|
||||
+ "fetch('/geolocation-denied')},"
|
||||
+ "{enableHighAccuracy:true,maximumAge:0,timeout:10000})\">"
|
||||
+ "Request location</button>"
|
||||
+ "</body></html>";
|
||||
}
|
||||
|
|
@ -206,7 +238,25 @@ final class FixtureHttpServer implements Closeable {
|
|||
return null;
|
||||
}
|
||||
|
||||
return line.toString(StandardCharsets.US_ASCII);
|
||||
return new String(line.toByteArray(), StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
private static byte[] readExactly(BufferedInputStream input, int length)
|
||||
throws IOException {
|
||||
byte[] result = new byte[length];
|
||||
int offset = 0;
|
||||
|
||||
while (offset < length) {
|
||||
int count = input.read(result, offset, length - offset);
|
||||
|
||||
if (count == -1) {
|
||||
throw new IOException("Unexpected end of HTTP request body");
|
||||
}
|
||||
|
||||
offset += count;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void writeResponse(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
package org.whoneedhelp.mobile;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.uiautomator.UiDevice;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class TrackingProcessDeathProbeTest {
|
||||
@Test
|
||||
public void startsNonStickyTrackingForExternalProcessKill() throws Exception {
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
|
||||
String packageName = context.getPackageName();
|
||||
|
||||
context.stopService(new Intent(context, TrackingService.class));
|
||||
context.getSystemService(NotificationManager.class).cancelAll();
|
||||
device.executeShellCommand(
|
||||
"pm grant " + packageName + " " + Manifest.permission.ACCESS_FINE_LOCATION
|
||||
);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
device.executeShellCommand(
|
||||
"pm grant " + packageName + " " + Manifest.permission.POST_NOTIFICATIONS
|
||||
);
|
||||
}
|
||||
|
||||
context.startForegroundService(
|
||||
TrackingService.startIntent(
|
||||
context,
|
||||
UUID.randomUUID().toString(),
|
||||
"process-death-csrf",
|
||||
"_session=process-death"
|
||||
)
|
||||
);
|
||||
|
||||
// The host harness kills this exact process after observing the active
|
||||
// service and notification. Reaching the deadline means the harness did
|
||||
// not perform the required process-death probe.
|
||||
TimeUnit.SECONDS.sleep(30);
|
||||
fail("The external process-death harness did not kill the tracking process");
|
||||
}
|
||||
}
|
||||
|
|
@ -46,11 +46,14 @@ The goal remains open while any row lacks reproducible local evidence.
|
|||
- A forced LiveSocket transport loss while Chromium is offline exposes the
|
||||
reconnect alert, and restoring the network clears it and reconnects the
|
||||
authenticated LiveView.
|
||||
- The isolated API 37 Android suite passes five device tests: missing and
|
||||
granted location permission, same-origin deep-link routing across Activity
|
||||
recreation, native foreground location upload, notification Stop with remote
|
||||
cleanup, and a disconnected Stop request with visible retry state. It uses an
|
||||
in-process loopback fixture, runs its emulator without external networking,
|
||||
- The isolated API 30/34/37 Android matrix passes six device tests per API:
|
||||
missing and granted location permission, same-origin deep-link routing across
|
||||
Activity recreation, native foreground location upload, notification Stop
|
||||
with remote cleanup, a disconnected Stop request with visible retry state,
|
||||
and continued uploads after Home and Activity destruction. A separate probe
|
||||
externally kills the instrumentation process and verifies that the
|
||||
`START_NOT_STICKY` service and notification do not return. The suite uses an
|
||||
in-process loopback fixture, runs each emulator without external networking,
|
||||
retains diagnostics on failure, and removes its exact container and image.
|
||||
- The containerized `scripts/quality.sh` gate passes ShellCheck, Hadolint,
|
||||
actionlint, all Compose renders, Helm lint, a Trivy scan of tracked source and
|
||||
|
|
|
|||
|
|
@ -5,10 +5,37 @@ ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
|||
ANDROID_ENV="$ROOT/.env"
|
||||
TEST_ENV="$ROOT/.env.android-test"
|
||||
run_id=$(date -u +%Y%m%d%H%M%S)-$$
|
||||
image="who-need-help-android:instrumentation-$run_id"
|
||||
container="who-need-help-android-instrumentation-$run_id"
|
||||
output="$ROOT/output/android-instrumentation/$run_id"
|
||||
android_api=${WNH_ANDROID_TEST_API:-37.0}
|
||||
android_data_partition_size=${WNH_ANDROID_TEST_DATA_PARTITION_SIZE:-4G}
|
||||
|
||||
if ! printf '%s\n' "$android_data_partition_size" |
|
||||
grep -Eq '^[1-9][0-9]*[GKM]$'; then
|
||||
echo "WNH_ANDROID_TEST_DATA_PARTITION_SIZE must be a positive integer followed by G, M, or K." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$android_api" in
|
||||
30)
|
||||
android_system_image="system-images/android-30/google_apis/x86_64"
|
||||
;;
|
||||
34)
|
||||
android_system_image="system-images/android-34/google_apis/x86_64"
|
||||
;;
|
||||
37.0)
|
||||
android_system_image="system-images/android-37.0/google_apis_ps16k/x86_64"
|
||||
;;
|
||||
*)
|
||||
echo "WNH_ANDROID_TEST_API must be one of: 30, 34, 37.0." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
api_label=$(printf '%s' "$android_api" | tr '.' '-')
|
||||
image="who-need-help-android:instrumentation-api${api_label}-$run_id"
|
||||
container="who-need-help-android-instrumentation-api${api_label}-$run_id"
|
||||
output="$ROOT/output/android-instrumentation/api${api_label}/$run_id"
|
||||
geo_pid=""
|
||||
death_runner_pid=""
|
||||
|
||||
if [ ! -e /dev/kvm ]; then
|
||||
echo "/dev/kvm is required for the containerized Android emulator." >&2
|
||||
|
|
@ -62,6 +89,11 @@ cleanup() {
|
|||
wait "$geo_pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [ -n "$death_runner_pid" ]; then
|
||||
kill "$death_runner_pid" >/dev/null 2>&1 || true
|
||||
wait "$death_runner_pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [ "$status" -ne 0 ] && docker inspect "$container" >/dev/null 2>&1; then
|
||||
docker exec "$container" adb logcat -d > "$output/logcat.txt" 2>&1 || true
|
||||
docker exec "$container" adb shell dumpsys activity services \
|
||||
|
|
@ -80,6 +112,8 @@ docker build \
|
|||
--build-arg "WNH_DEBUG_BASE_URL=$WNH_ANDROID_TEST_BASE_URL" \
|
||||
--build-arg "WNH_TRACKING_MIN_TIME_MS=$WNH_TRACKING_MIN_TIME_MS" \
|
||||
--build-arg "WNH_TRACKING_HTTP_TIMEOUT_MS=$WNH_TRACKING_HTTP_TIMEOUT_MS" \
|
||||
--build-arg "ANDROID_EMULATOR_SYSTEM_IMAGE=$android_system_image" \
|
||||
--build-arg "ANDROID_EMULATOR_DATA_PARTITION_SIZE=$android_data_partition_size" \
|
||||
--target emulator \
|
||||
-t "$image" \
|
||||
"$ROOT/android"
|
||||
|
|
@ -136,6 +170,7 @@ geo_pid=$!
|
|||
|
||||
set +e
|
||||
docker exec "$container" adb shell am instrument -w -r \
|
||||
-e class org.whoneedhelp.mobile.AndroidClientInstrumentedTest \
|
||||
org.whoneedhelp.mobile.debug.test/androidx.test.runner.AndroidJUnitRunner \
|
||||
> "$output/results.txt" 2>&1
|
||||
instrumentation_status=$?
|
||||
|
|
@ -149,4 +184,88 @@ if [ "$instrumentation_status" -ne 0 ] \
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "Android instrumentation passed. Evidence: $output"
|
||||
docker exec "$container" adb shell am force-stop org.whoneedhelp.mobile.debug
|
||||
docker exec "$container" adb root >"$output/adb-root.txt"
|
||||
docker exec "$container" adb wait-for-device
|
||||
docker exec "$container" adb shell am instrument -w -r \
|
||||
-e class org.whoneedhelp.mobile.TrackingProcessDeathProbeTest \
|
||||
org.whoneedhelp.mobile.debug.test/androidx.test.runner.AndroidJUnitRunner \
|
||||
>"$output/process-death-results.txt" 2>&1 &
|
||||
death_runner_pid=$!
|
||||
|
||||
service_observed=""
|
||||
attempt=0
|
||||
while [ "$attempt" -lt 20 ]; do
|
||||
if docker exec "$container" adb shell dumpsys activity services \
|
||||
org.whoneedhelp.mobile.debug |
|
||||
grep -q 'org.whoneedhelp.mobile.TrackingService'; then
|
||||
service_observed=1
|
||||
break
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ -z "$service_observed" ]; then
|
||||
echo "Process-death probe never observed the foreground service." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
app_pid=$(docker exec "$container" adb shell pidof org.whoneedhelp.mobile.debug | tr -d '\r')
|
||||
|
||||
case "$app_pid" in
|
||||
""|*[!0-9]*)
|
||||
echo "Process-death probe could not identify one target process." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
docker exec "$container" adb logcat -c
|
||||
docker exec "$container" adb shell kill -9 "$app_pid"
|
||||
|
||||
set +e
|
||||
wait "$death_runner_pid"
|
||||
death_status=$?
|
||||
set -e
|
||||
death_runner_pid=""
|
||||
cat "$output/process-death-results.txt"
|
||||
docker exec "$container" adb logcat -d >"$output/process-death-logcat.txt"
|
||||
|
||||
if ! grep -Eq \
|
||||
'Process org\.whoneedhelp\.mobile\.debug .* has died:.*FGS|Crash of app org\.whoneedhelp\.mobile\.debug running instrumentation|Process crashed' \
|
||||
"$output/process-death-logcat.txt"; then
|
||||
echo "The expected instrumentation process death was not observed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Expected killed instrumentation exit status: %s\n' "$death_status" \
|
||||
>"$output/process-death-status.txt"
|
||||
|
||||
attempt=0
|
||||
while [ "$attempt" -lt 10 ]; do
|
||||
if ! docker exec "$container" adb shell dumpsys activity services \
|
||||
org.whoneedhelp.mobile.debug |
|
||||
grep -q 'org.whoneedhelp.mobile.TrackingService'; then
|
||||
break
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if docker exec "$container" adb shell dumpsys activity services \
|
||||
org.whoneedhelp.mobile.debug |
|
||||
grep -q 'org.whoneedhelp.mobile.TrackingService'; then
|
||||
echo "START_NOT_STICKY tracking restarted after process death." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if docker exec "$container" adb shell dumpsys notification --noredact |
|
||||
grep -q 'Sharing live location'; then
|
||||
echo "The tracking notification remained after process death." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Android API $android_api instrumentation and process-death probes passed."
|
||||
echo "Evidence: $output"
|
||||
|
|
|
|||
20
scripts/android-matrix-test.sh
Executable file
20
scripts/android-matrix-test.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
matrix=${WNH_ANDROID_TEST_API_MATRIX:-"30 34 37.0"}
|
||||
|
||||
for api in $matrix; do
|
||||
case "$api" in
|
||||
30|34|37.0) ;;
|
||||
*)
|
||||
echo "WNH_ANDROID_TEST_API_MATRIX contains unsupported API: $api" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Running Android device suite on API $api."
|
||||
WNH_ANDROID_TEST_API=$api "$ROOT/scripts/android-instrumentation-test.sh"
|
||||
done
|
||||
|
||||
echo "Android device matrix passed: $matrix"
|
||||
Loading…
Reference in New Issue
Block a user