Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bf99a0d
Initial plan
Copilot Jan 16, 2026
1bc0790
Add UI instrumentation test for DL3 demo similar to LlamaDemo
Copilot Jan 16, 2026
091384b
Improve bitmap comparison in UIWorkflowTest to avoid reference equali…
Copilot Jan 16, 2026
8371735
Revert version downgrades - keep AGP 8.9.0 and Gradle 8.11.1
Copilot Jan 16, 2026
16e91a3
Add CI workflow for DL3 instrumentation tests
Copilot Jan 16, 2026
804a8f0
Fix test failures: update SanityCheck to use app storage and increase…
Copilot Jan 16, 2026
44fd8f8
Merge main and update tests for Kotlin/Compose migration
Copilot Jan 21, 2026
7271c22
Make workflow generic and add model download to SanityCheck
Copilot Jan 21, 2026
08907f6
Add Compose UI tests for download and model run functionality
Copilot Jan 21, 2026
6cdad07
Disable testMultipleConsecutiveRuns test due to known issue
Copilot Jan 21, 2026
44f7215
Add MV3 demo, inline test script, and remove separate .sh file
Copilot Jan 27, 2026
727bfe1
Merge remote-tracking branch 'origin/main' into copilot/add-ui-instru…
kirklandsign Jan 27, 2026
1fe0263
No sleep
kirklandsign Jan 27, 2026
ea70309
Remove unused demo_path input from android-emulator workflow
kirklandsign Jan 27, 2026
ec7d621
Fix
kirklandsign Jan 27, 2026
210272b
Update
kirklandsign Jan 28, 2026
f4aeb7f
Fix workflow script syntax and Android build warnings
Copilot Jan 30, 2026
918b5c3
Disable fail-fast in instrumentation test job
kirklandsign Jan 30, 2026
97e345d
Remove complete workflow test from UIWorkflowTest
kirklandsign Jan 30, 2026
8099c1d
KI
kirklandsign Jan 30, 2026
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
128 changes: 128 additions & 0 deletions .github/workflows/android-emulator.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

name: Android Emulator Tests

on:
pull_request:
branches: [main]
paths:
- 'dl3/android/**'
- 'mv3/android/**'
- '.github/workflows/android-emulator.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
instrumentation-test:
runs-on: 8-core-ubuntu
strategy:
fail-fast: false
matrix:
demo:
- path: 'dl3/android/DeepLabV3Demo'
name: 'DeepLabV3Demo'
- path: 'mv3/android/MV3Demo'
name: 'MV3Demo'
env:
API_LEVEL: 34
ARCH: x86_64
DEMO_PATH: ${{ matrix.demo.path }}

name: Instrumentation Test ${{ matrix.demo.name }}
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Determine demo name
id: demo-info
run: |
DEMO_NAME=$(basename "${{ env.DEMO_PATH }}")
echo "demo_name=$DEMO_NAME" >> $GITHUB_OUTPUT
echo "Testing: $DEMO_NAME"
echo "=== Test Configuration ==="
echo "Demo: $DEMO_NAME"
echo "Model will be downloaded by the test if not present"

- 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
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: AVD cache
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-${{ env.API_LEVEL }}-${{ env.ARCH }}-ram8G-disk8G-${{ steps.demo-info.outputs.demo_name }}-v1

- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.API_LEVEL }}
arch: ${{ env.ARCH }}
ram-size: 8192M
disk-size: 8192M
force-avd-creation: true
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-snapshot-save -memory 8192
disable-animations: false
working-directory: ${{ env.DEMO_PATH }}
script: echo "Generated AVD snapshot for caching."

- name: Run instrumentation tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.API_LEVEL }}
arch: ${{ env.ARCH }}
ram-size: 8192M
disk-size: 8192M
force-avd-creation: true
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-snapshot-save -memory 8192
disable-animations: true
working-directory: ${{ env.DEMO_PATH }}
script: |
set -ex
DEMO_NAME=$(basename "$PWD")
LOGCAT_FILE="/tmp/logcat-${DEMO_NAME}.txt"
echo "=== Emulator Memory Info ==="
adb shell cat /proc/meminfo | head -5
echo "=== Emulator Disk Space ==="
adb shell df -h /data
adb logcat -c
adb logcat > "$LOGCAT_FILE" &
LOGCAT_PID=$!
echo "=== Starting Gradle ==="
./gradlew connectedCheck
TEST_EXIT_CODE=$?
if [ -n "$LOGCAT_PID" ]; then kill $LOGCAT_PID 2>/dev/null || true; fi
echo "=== Test completion status ==="
if [ $TEST_EXIT_CODE -eq 0 ]; then echo "✅ Tests passed successfully"; else echo "❌ Tests failed with exit code $TEST_EXIT_CODE"; fi
echo "=== Checking for test results in logcat ==="
grep -E "SanityCheck|UIWorkflowTest" "$LOGCAT_FILE" || echo "No test logs found"
exit $TEST_EXIT_CODE

- name: Upload logcat
if: always()
uses: actions/upload-artifact@v4
with:
name: logcat-${{ steps.demo-info.outputs.demo_name }}
path: /tmp/logcat-*.txt
retention-days: 7
18 changes: 17 additions & 1 deletion dl3/android/DeepLabV3Demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,20 @@ The app detects all 21 PASCAL VOC classes with distinct color overlays:
```

### Using Android Studio
Open `app/src/androidTest/java/org/pytorch/executorchexamples/dl3/SanityCheck.kt` and click the Play button.
Open `app/src/androidTest/java/org/pytorch/executorchexamples/dl3/SanityCheck.kt` or `UIWorkflowTest.kt` and click the Play button.

### Test Files
- **SanityCheck.kt**: Basic module forward pass test
- Downloads model automatically if not present
- Tests model loading from app's private storage
- Validates model output shape (batch_size × classes × width × height)

- **UIWorkflowTest.kt**: Compose UI workflow tests including:
- Initial UI state verification
- Download button functionality (with and without model present)
- Model run/segmentation testing with inference time display
- Next button to cycle through sample images
- Reset button functionality
- Complete end-to-end workflow (Next → Run → Reset)
- Multiple consecutive runs to test model reusability

9 changes: 8 additions & 1 deletion dl3/android/DeepLabV3Demo/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ plugins {
id("org.jetbrains.kotlin.android")
}

val useLocalAar: Boolean? = (project.findProperty("useLocalAar") as? String)?.toBoolean()

android {
namespace = "org.pytorch.executorchexamples.dl3"
compileSdk = 34
Expand Down Expand Up @@ -52,7 +54,12 @@ dependencies {
implementation("androidx.compose.material3:material3")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("org.pytorch:executorch-android:1.0.0")
if (useLocalAar == true) {
implementation(files("libs/executorch.aar"))
implementation("com.facebook.fbjni:fbjni:0.5.1")
} else {
implementation("org.pytorch:executorch-android:1.0.1")
}
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,85 @@

package org.pytorch.executorchexamples.dl3

import android.content.Context
import android.util.Log
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SmallTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.pytorch.executorch.Module
import org.pytorch.executorch.Tensor
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL

/**
* Sanity check test for model loading.
*
* This test downloads the model if not available and validates model functionality.
* The model is stored in the app's private storage (same location as MainActivity uses).
*/
@SmallTest
class SanityCheck {

companion object {
private const val MODEL_FILENAME = "dl3_xnnpack_fp32.pte"
private const val MODEL_URL = "https://ossci-android.s3.amazonaws.com/executorch/models/snapshot-20260116/dl3_xnnpack_fp32.pte"
private const val TAG = "SanityCheck"
}

private lateinit var modelPath: String
private lateinit var context: Context

@Before
fun setUp() {
// Use the app's private files directory (same as MainActivity)
context = ApplicationProvider.getApplicationContext()
modelPath = "${context.filesDir.absolutePath}/$MODEL_FILENAME"

// Download model if not present
val modelFile = File(modelPath)
if (!modelFile.exists()) {
Log.i(TAG, "Model not found at $modelPath, downloading...")
downloadModel()
} else {
Log.i(TAG, "Model found at $modelPath")
}
}

private fun downloadModel() {
try {
val url = URL(MODEL_URL)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.instanceFollowRedirects = true
connection.connect()

if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw RuntimeException("Server returned HTTP ${connection.responseCode}")
}

connection.inputStream.use { input ->
FileOutputStream(modelPath).use { output ->
val buffer = ByteArray(4096)
var bytesRead: Int
while (input.read(buffer).also { bytesRead = it } != -1) {
output.write(buffer, 0, bytesRead)
}
}
}
Log.i(TAG, "Model downloaded successfully to $modelPath")
} catch (e: Exception) {
Log.e(TAG, "Failed to download model", e)
throw RuntimeException("Failed to download model: ${e.message}", e)
}
}

@Test
fun testModuleForward() {
val module = Module.load("/data/local/tmp/dl3_xnnpack_fp32.pte")
val module = Module.load(modelPath)
// Test with sample inputs (ones) and make sure there is no crash.
val outputTensor: Tensor = module.forward()[0].toTensor()
val shape = outputTensor.shape()
Expand Down
Loading
Loading