Skip to content

Commit 08907f6

Browse files
Copilotkirklandsign
andcommitted
Add Compose UI tests for download and model run functionality
1. Added testTag modifiers to all UI components in MainActivity.kt 2. Created comprehensive UIWorkflowTest.kt with: - Initial UI state verification - Download button testing (with rename-test-restore pattern) - Model run/segmentation testing - Next/Reset button functionality - Complete workflow testing - Multiple consecutive runs 3. Updated README with UI test documentation 4. Updated test script to check for UIWorkflowTest logs Co-authored-by: kirklandsign <107070759+kirklandsign@users.noreply.github.com>
1 parent 7271c22 commit 08907f6

4 files changed

Lines changed: 348 additions & 12 deletions

File tree

dl3/android/DeepLabV3Demo/README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,20 @@ The app detects all 21 PASCAL VOC classes with distinct color overlays:
102102
```
103103

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

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

113-
> **Note**: UI workflow tests for the Compose-based interface are planned for a future update.
113+
- **UIWorkflowTest.kt**: Compose UI workflow tests including:
114+
- Initial UI state verification
115+
- Download button functionality (with and without model present)
116+
- Model run/segmentation testing with inference time display
117+
- Next button to cycle through sample images
118+
- Reset button functionality
119+
- Complete end-to-end workflow (Next → Run → Reset)
120+
- Multiple consecutive runs to test model reusability
114121

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
package org.pytorch.executorchexamples.dl3
10+
11+
import android.content.Context
12+
import android.util.Log
13+
import androidx.compose.ui.test.*
14+
import androidx.compose.ui.test.junit4.createAndroidComposeRule
15+
import androidx.test.core.app.ApplicationProvider
16+
import androidx.test.ext.junit.runners.AndroidJUnit4
17+
import androidx.test.filters.LargeTest
18+
import org.junit.After
19+
import org.junit.Before
20+
import org.junit.Rule
21+
import org.junit.Test
22+
import org.junit.runner.RunWith
23+
import java.io.File
24+
25+
/**
26+
* UI workflow tests for the Compose-based DL3 demo.
27+
*
28+
* Tests include:
29+
* - Download button functionality with and without model present
30+
* - Model run/segmentation testing
31+
* - UI state management (Next, Reset buttons)
32+
* - Inference time display
33+
*/
34+
@RunWith(AndroidJUnit4::class)
35+
@LargeTest
36+
class UIWorkflowTest {
37+
38+
companion object {
39+
private const val MODEL_FILENAME = "dl3_xnnpack_fp32.pte"
40+
private const val MODEL_BACKUP_FILENAME = "dl3_xnnpack_fp32.pte.backup"
41+
private const val TAG = "UIWorkflowTest"
42+
private const val DOWNLOAD_TIMEOUT_MS = 120000L // 2 minutes
43+
private const val INFERENCE_TIMEOUT_MS = 30000L // 30 seconds
44+
}
45+
46+
@get:Rule
47+
val composeTestRule = createAndroidComposeRule<MainActivity>()
48+
49+
private lateinit var context: Context
50+
private lateinit var modelPath: String
51+
private lateinit var backupPath: String
52+
53+
@Before
54+
fun setUp() {
55+
context = ApplicationProvider.getApplicationContext()
56+
modelPath = "${context.filesDir.absolutePath}/$MODEL_FILENAME"
57+
backupPath = "${context.filesDir.absolutePath}/$MODEL_BACKUP_FILENAME"
58+
59+
// Ensure model is downloaded for most tests
60+
ensureModelAvailable()
61+
}
62+
63+
@After
64+
fun tearDown() {
65+
// Clean up backup file if exists
66+
val backupFile = File(backupPath)
67+
if (backupFile.exists()) {
68+
backupFile.delete()
69+
}
70+
}
71+
72+
/**
73+
* Ensures model is available by downloading if needed.
74+
*/
75+
private fun ensureModelAvailable() {
76+
val modelFile = File(modelPath)
77+
if (!modelFile.exists()) {
78+
Log.i(TAG, "Model not found, downloading...")
79+
// Click download button and wait for completion
80+
composeTestRule.onNodeWithTag("downloadButton").assertExists()
81+
composeTestRule.onNodeWithTag("downloadButton").performClick()
82+
83+
// Wait for download to complete (button should disappear)
84+
composeTestRule.waitUntil(timeoutMillis = DOWNLOAD_TIMEOUT_MS) {
85+
composeTestRule.onAllNodesWithTag("downloadButton")
86+
.fetchSemanticsNodes().isEmpty()
87+
}
88+
} else {
89+
Log.i(TAG, "Model already available at $modelPath")
90+
}
91+
}
92+
93+
/**
94+
* Tests that the initial UI is displayed correctly when model is ready.
95+
*/
96+
@Test
97+
fun testInitialUIWithModel() {
98+
// Model buttons should be visible
99+
composeTestRule.onNodeWithTag("nextButton").assertIsDisplayed()
100+
composeTestRule.onNodeWithTag("pickButton").assertIsDisplayed()
101+
composeTestRule.onNodeWithTag("runButton").assertIsDisplayed()
102+
composeTestRule.onNodeWithTag("resetButton").assertIsDisplayed()
103+
104+
// Image should be displayed
105+
composeTestRule.onNodeWithTag("segmentationImage").assertExists()
106+
107+
// Download button should not be visible
108+
composeTestRule.onNodeWithTag("downloadButton").assertDoesNotExist()
109+
110+
// Reset button should be disabled initially
111+
composeTestRule.onNodeWithTag("resetButton").assertIsNotEnabled()
112+
}
113+
114+
/**
115+
* Tests the download button functionality when model is not present.
116+
* Uses rename-test-restore pattern to simulate missing model.
117+
*/
118+
@Test
119+
fun testDownloadButtonWhenModelMissing() {
120+
val modelFile = File(modelPath)
121+
val backupFile = File(backupPath)
122+
123+
// Step 1: Rename existing model to backup
124+
if (modelFile.exists()) {
125+
modelFile.renameTo(backupFile)
126+
Log.i(TAG, "Renamed model to backup")
127+
}
128+
129+
try {
130+
// Step 2: Restart activity to show download button
131+
composeTestRule.activityRule.scenario.recreate()
132+
133+
// Wait for UI to settle
134+
Thread.sleep(1000)
135+
136+
// Download button should be visible
137+
composeTestRule.onNodeWithTag("downloadButton").assertIsDisplayed()
138+
composeTestRule.onNodeWithTag("downloadButton").assertIsEnabled()
139+
140+
// Model buttons should not be visible
141+
composeTestRule.onNodeWithTag("nextButton").assertDoesNotExist()
142+
composeTestRule.onNodeWithTag("runButton").assertDoesNotExist()
143+
144+
// Step 3: Click download button
145+
composeTestRule.onNodeWithTag("downloadButton").performClick()
146+
147+
// Progress indicator should appear
148+
composeTestRule.onNodeWithTag("progressIndicator").assertExists()
149+
150+
// Step 4: Wait for download to complete
151+
composeTestRule.waitUntil(timeoutMillis = DOWNLOAD_TIMEOUT_MS) {
152+
composeTestRule.onAllNodesWithTag("downloadButton")
153+
.fetchSemanticsNodes().isEmpty()
154+
}
155+
156+
// Model buttons should now be visible
157+
composeTestRule.onNodeWithTag("nextButton").assertIsDisplayed()
158+
composeTestRule.onNodeWithTag("runButton").assertIsDisplayed()
159+
160+
// Verify new model file exists
161+
assert(modelFile.exists()) { "Model should be downloaded" }
162+
163+
} finally {
164+
// Step 5: Restore backup if test downloaded new model and backup exists
165+
if (backupFile.exists()) {
166+
if (modelFile.exists()) {
167+
modelFile.delete()
168+
}
169+
backupFile.renameTo(modelFile)
170+
Log.i(TAG, "Restored model from backup")
171+
}
172+
}
173+
}
174+
175+
/**
176+
* Tests the "Next" button functionality to cycle through sample images.
177+
*/
178+
@Test
179+
fun testNextButtonCyclesSamples() {
180+
// Click Next button
181+
composeTestRule.onNodeWithTag("nextButton").performClick()
182+
183+
// Wait for image to change
184+
Thread.sleep(500)
185+
186+
// Image should still be displayed
187+
composeTestRule.onNodeWithTag("segmentationImage").assertExists()
188+
189+
// Can click Next again
190+
composeTestRule.onNodeWithTag("nextButton").performClick()
191+
Thread.sleep(500)
192+
composeTestRule.onNodeWithTag("segmentationImage").assertExists()
193+
}
194+
195+
/**
196+
* Tests running segmentation and verifying inference time display.
197+
*/
198+
@Test
199+
fun testRunSegmentation() {
200+
// Run button should be enabled
201+
composeTestRule.onNodeWithTag("runButton").assertIsEnabled()
202+
203+
// Click Run button
204+
composeTestRule.onNodeWithTag("runButton").performClick()
205+
206+
// Progress indicator should appear briefly
207+
// (might be too fast to catch consistently)
208+
209+
// Wait for inference to complete
210+
composeTestRule.waitUntil(timeoutMillis = INFERENCE_TIMEOUT_MS) {
211+
composeTestRule.onAllNodesWithTag("inferenceTime")
212+
.fetchSemanticsNodes().isNotEmpty()
213+
}
214+
215+
// Inference time should be displayed
216+
composeTestRule.onNodeWithTag("inferenceTime").assertIsDisplayed()
217+
218+
// Reset button should now be enabled
219+
composeTestRule.onNodeWithTag("resetButton").assertIsEnabled()
220+
221+
// Run button should still be enabled for next run
222+
composeTestRule.onNodeWithTag("runButton").assertIsEnabled()
223+
}
224+
225+
/**
226+
* Tests the Reset button functionality.
227+
*/
228+
@Test
229+
fun testResetButton() {
230+
// First run segmentation to enable reset
231+
composeTestRule.onNodeWithTag("runButton").performClick()
232+
233+
composeTestRule.waitUntil(timeoutMillis = INFERENCE_TIMEOUT_MS) {
234+
composeTestRule.onAllNodesWithTag("inferenceTime")
235+
.fetchSemanticsNodes().isNotEmpty()
236+
}
237+
238+
// Reset button should be enabled
239+
composeTestRule.onNodeWithTag("resetButton").assertIsEnabled()
240+
241+
// Click Reset button
242+
composeTestRule.onNodeWithTag("resetButton").performClick()
243+
244+
// Wait for reset to complete
245+
Thread.sleep(500)
246+
247+
// Reset button should be disabled again
248+
composeTestRule.onNodeWithTag("resetButton").assertIsNotEnabled()
249+
250+
// Inference time should disappear
251+
composeTestRule.onNodeWithTag("inferenceTime").assertDoesNotExist()
252+
}
253+
254+
/**
255+
* Tests the complete workflow: Next -> Run -> Reset.
256+
*/
257+
@Test
258+
fun testCompleteWorkflow() {
259+
// Step 1: Click Next to change sample
260+
composeTestRule.onNodeWithTag("nextButton").performClick()
261+
Thread.sleep(500)
262+
263+
// Step 2: Run segmentation
264+
composeTestRule.onNodeWithTag("runButton").performClick()
265+
266+
composeTestRule.waitUntil(timeoutMillis = INFERENCE_TIMEOUT_MS) {
267+
composeTestRule.onAllNodesWithTag("inferenceTime")
268+
.fetchSemanticsNodes().isNotEmpty()
269+
}
270+
271+
// Verify results are shown
272+
composeTestRule.onNodeWithTag("inferenceTime").assertIsDisplayed()
273+
composeTestRule.onNodeWithTag("resetButton").assertIsEnabled()
274+
275+
// Step 3: Reset image
276+
composeTestRule.onNodeWithTag("resetButton").performClick()
277+
Thread.sleep(500)
278+
279+
// Verify reset worked
280+
composeTestRule.onNodeWithTag("resetButton").assertIsNotEnabled()
281+
composeTestRule.onNodeWithTag("inferenceTime").assertDoesNotExist()
282+
283+
// Step 4: Can run segmentation again
284+
composeTestRule.onNodeWithTag("runButton").assertIsEnabled()
285+
}
286+
287+
/**
288+
* Tests multiple consecutive runs to ensure model can be reused.
289+
*/
290+
@Test
291+
fun testMultipleConsecutiveRuns() {
292+
for (i in 1..3) {
293+
Log.i(TAG, "Running segmentation iteration $i")
294+
295+
// Run segmentation
296+
composeTestRule.onNodeWithTag("runButton").performClick()
297+
298+
composeTestRule.waitUntil(timeoutMillis = INFERENCE_TIMEOUT_MS) {
299+
composeTestRule.onAllNodesWithTag("inferenceTime")
300+
.fetchSemanticsNodes().isNotEmpty()
301+
}
302+
303+
// Verify inference time is displayed
304+
composeTestRule.onNodeWithTag("inferenceTime").assertIsDisplayed()
305+
306+
// Reset for next iteration (except last)
307+
if (i < 3) {
308+
composeTestRule.onNodeWithTag("resetButton").performClick()
309+
Thread.sleep(500)
310+
}
311+
}
312+
}
313+
}

0 commit comments

Comments
 (0)