Skip to content

Commit e9123e5

Browse files
committed
UI update and test
1 parent b58a1d2 commit e9123e5

4 files changed

Lines changed: 294 additions & 6 deletions

File tree

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
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 com.example.executorchllamademo
10+
11+
import android.content.Context
12+
import android.util.Log
13+
import androidx.compose.ui.semantics.SemanticsProperties
14+
import androidx.compose.ui.test.assertIsEnabled
15+
import androidx.compose.ui.test.hasContentDescription
16+
import androidx.compose.ui.test.junit4.createAndroidComposeRule
17+
import androidx.compose.ui.test.onAllNodesWithText
18+
import androidx.compose.ui.test.onNodeWithContentDescription
19+
import androidx.compose.ui.test.onNodeWithTag
20+
import androidx.compose.ui.test.onNodeWithText
21+
import androidx.compose.ui.test.performClick
22+
import androidx.compose.ui.test.performTextInput
23+
import androidx.test.core.app.ApplicationProvider
24+
import androidx.test.ext.junit.runners.AndroidJUnit4
25+
import androidx.test.filters.LargeTest
26+
import org.junit.Assert.assertTrue
27+
import org.junit.Before
28+
import org.junit.Rule
29+
import org.junit.Test
30+
import org.junit.runner.RunWith
31+
32+
/**
33+
* Preset model sanity test that validates the preset model download and chat workflow.
34+
*
35+
* This test validates:
36+
* 1. Navigate from Welcome screen to Preset model screen
37+
* 2. Select Stories 110M and download it
38+
* 3. After download completes, tap to load and enter chat view
39+
* 4. Type "Once upon a time" and generate a response
40+
*/
41+
@RunWith(AndroidJUnit4::class)
42+
@LargeTest
43+
class PresetSanityTest {
44+
45+
companion object {
46+
private const val TAG = "PresetSanityTest"
47+
private const val RESPONSE_TAG = "LLAMA_RESPONSE"
48+
}
49+
50+
@get:Rule
51+
val composeTestRule = createAndroidComposeRule<WelcomeActivity>()
52+
53+
@Before
54+
fun setUp() {
55+
// Clear SharedPreferences before test to ensure a clean state
56+
val context = ApplicationProvider.getApplicationContext<Context>()
57+
val prefs = context.getSharedPreferences(
58+
context.getString(R.string.demo_pref_file_key),
59+
Context.MODE_PRIVATE
60+
)
61+
prefs.edit().clear().commit()
62+
}
63+
64+
/**
65+
* Types text into the chat input field using testTag.
66+
*/
67+
private fun typeInChatInput(text: String) {
68+
composeTestRule.onNodeWithTag("chat_input_field").performClick()
69+
composeTestRule.waitForIdle()
70+
composeTestRule.onNodeWithTag("chat_input_field").performTextInput(text)
71+
composeTestRule.waitForIdle()
72+
}
73+
74+
/**
75+
* Waits for generation to complete by checking for tokens-per-second metrics.
76+
*/
77+
private fun waitForGenerationComplete(timeoutMs: Long = 120000): Boolean {
78+
return try {
79+
composeTestRule.waitUntil(timeoutMillis = timeoutMs) {
80+
val tpsNodes = composeTestRule.onAllNodesWithText("t/s", substring = true)
81+
.fetchSemanticsNodes()
82+
val tokpsNodes = composeTestRule.onAllNodesWithText("tok/s", substring = true)
83+
.fetchSemanticsNodes()
84+
tpsNodes.isNotEmpty() || tokpsNodes.isNotEmpty()
85+
}
86+
Log.i(TAG, "Generation complete - found generation metrics")
87+
true
88+
} catch (e: Exception) {
89+
Log.e(TAG, "Generation timed out after ${timeoutMs}ms")
90+
false
91+
}
92+
}
93+
94+
/**
95+
* Waits for the model to be loaded by checking for success or error messages.
96+
*/
97+
private fun waitForModelLoaded(timeoutMs: Long = 60000): Boolean {
98+
return try {
99+
var wasSuccess = false
100+
composeTestRule.waitUntil(timeoutMillis = timeoutMs) {
101+
val successNodes = composeTestRule.onAllNodesWithText("Successfully loaded", substring = true)
102+
.fetchSemanticsNodes()
103+
val errorNodes = composeTestRule.onAllNodesWithText("Model load failure", substring = true)
104+
.fetchSemanticsNodes()
105+
wasSuccess = successNodes.isNotEmpty()
106+
successNodes.isNotEmpty() || errorNodes.isNotEmpty()
107+
}
108+
if (wasSuccess) {
109+
Log.i(TAG, "Model loaded successfully")
110+
} else {
111+
Log.e(TAG, "Model load failed")
112+
}
113+
wasSuccess
114+
} catch (e: Exception) {
115+
Log.e(TAG, "Model loading timed out after ${timeoutMs}ms: ${e.message}")
116+
false
117+
}
118+
}
119+
120+
/**
121+
* Verifies that the model generated a non-empty response.
122+
*/
123+
private fun assertModelResponseNotEmpty(timeoutMs: Long = 10000) {
124+
try {
125+
composeTestRule.waitUntil(timeoutMillis = timeoutMs) {
126+
val tpsNodes = composeTestRule.onAllNodesWithText("t/s", substring = true)
127+
.fetchSemanticsNodes()
128+
val tokpsNodes = composeTestRule.onAllNodesWithText("tok/s", substring = true)
129+
.fetchSemanticsNodes()
130+
tpsNodes.isNotEmpty() || tokpsNodes.isNotEmpty()
131+
}
132+
Log.i(TAG, "Model response verified - found generation metrics")
133+
} catch (e: Exception) {
134+
throw AssertionError("Model response appears to be empty - no generation metrics found after ${timeoutMs}ms")
135+
}
136+
}
137+
138+
/**
139+
* Logs the model response text for CI output.
140+
*/
141+
private fun logModelResponse() {
142+
try {
143+
Log.i(RESPONSE_TAG, "BEGIN_RESPONSE")
144+
val responseNodes = composeTestRule.onAllNodesWithText("t/s", substring = true)
145+
.fetchSemanticsNodes()
146+
for (node in responseNodes) {
147+
val text = node.config.getOrElse(SemanticsProperties.Text) { emptyList() }
148+
.joinToString(" ") { it.text }
149+
if (text.isNotBlank()) {
150+
Log.i(RESPONSE_TAG, text)
151+
}
152+
}
153+
Log.i(RESPONSE_TAG, "END_RESPONSE")
154+
} catch (e: Exception) {
155+
Log.d(TAG, "Could not log model response: ${e.message}")
156+
}
157+
}
158+
159+
/**
160+
* Tests the complete preset model download and chat workflow:
161+
* 1. From Welcome screen, tap "Preset model" card
162+
* 2. Find Stories 110M and tap Download
163+
* 3. Wait for download to complete
164+
* 4. Tap the card to load model and enter chat
165+
* 5. Type "Once upon a time" and send
166+
* 6. Verify response is generated
167+
*/
168+
@Test
169+
fun testPresetModelDownloadAndChat() {
170+
composeTestRule.waitForIdle()
171+
172+
// Step 1: From Welcome screen, tap "Preset model" card
173+
Log.i(TAG, "Step 1: Navigating to Preset model screen")
174+
composeTestRule.onNodeWithText("Preset model").performClick()
175+
composeTestRule.waitUntil(timeoutMillis = 5000) {
176+
composeTestRule.onAllNodesWithText("Download Preset Model").fetchSemanticsNodes().isNotEmpty()
177+
}
178+
179+
// Step 2: Find Stories 110M and tap Download
180+
Log.i(TAG, "Step 2: Finding Stories 110M and starting download")
181+
composeTestRule.onNodeWithText("Stories 110M").assertExists()
182+
183+
// Check if already downloaded (Ready to use) or needs download
184+
val readyNodes = composeTestRule.onAllNodesWithText("Ready to use", substring = true)
185+
.fetchSemanticsNodes()
186+
187+
if (readyNodes.isEmpty()) {
188+
// Need to download - click Download button
189+
composeTestRule.onNodeWithText("Download").performClick()
190+
191+
// Step 3: Wait for download to complete (may take a while for large files)
192+
Log.i(TAG, "Step 3: Waiting for download to complete")
193+
composeTestRule.waitUntil(timeoutMillis = 300000) { // 5 minutes timeout for download
194+
composeTestRule.onAllNodesWithText("Ready to use", substring = true)
195+
.fetchSemanticsNodes().isNotEmpty()
196+
}
197+
Log.i(TAG, "Download completed")
198+
} else {
199+
Log.i(TAG, "Model already downloaded, skipping download step")
200+
}
201+
202+
// Step 4: Tap the card to load model and enter chat
203+
Log.i(TAG, "Step 4: Tapping card to load model")
204+
composeTestRule.onNodeWithText("Stories 110M").performClick()
205+
206+
// Wait for Activity transition - MainActivity needs time to launch and set content
207+
// The SelectPresetModelActivity calls finish() after starting MainActivity
208+
Thread.sleep(2000)
209+
210+
// Wait for model to load and chat screen to appear
211+
Log.i(TAG, "Waiting for model to load")
212+
val modelLoaded = waitForModelLoaded(90000)
213+
assertTrue("Model should be loaded successfully", modelLoaded)
214+
Log.i(TAG, "Model loaded successfully")
215+
216+
// Step 5: Type "Once upon a time" and send
217+
Log.i(TAG, "Step 5: Typing prompt and sending")
218+
typeInChatInput("Once upon a time")
219+
220+
// Wait for send button to be enabled
221+
composeTestRule.waitUntil(timeoutMillis = 5000) {
222+
try {
223+
composeTestRule.onNodeWithContentDescription("Send").assertIsEnabled()
224+
true
225+
} catch (e: AssertionError) {
226+
false
227+
}
228+
}
229+
230+
composeTestRule.onNodeWithContentDescription("Send").performClick()
231+
composeTestRule.waitForIdle()
232+
233+
// Step 6: Wait for generation to complete and verify response
234+
Log.i(TAG, "Step 6: Waiting for generation to complete")
235+
val generationComplete = waitForGenerationComplete(120000)
236+
assertTrue("Generation should complete", generationComplete)
237+
238+
assertModelResponseNotEmpty()
239+
logModelResponse()
240+
241+
Log.i(TAG, "Preset model sanity test completed successfully")
242+
}
243+
}

llm/android/LlamaDemo/app/src/main/java/com/example/executorchllamademo/SelectPresetModelActivity.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ class SelectPresetModelActivity : ComponentActivity() {
6363
onDownloadClick = { key ->
6464
viewModel.downloadModel(key)
6565
},
66+
onDeleteClick = { key ->
67+
viewModel.deleteModel(key)
68+
},
6669
onModelClick = { key ->
6770
if (viewModel.loadModelAndStartChat(key)) {
6871
// Navigate to MainActivity (conversation) after loading model

llm/android/LlamaDemo/app/src/main/java/com/example/executorchllamademo/ui/screens/SelectPresetModelScreen.kt

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import androidx.compose.foundation.verticalScroll
2626
import androidx.compose.material.icons.Icons
2727
import androidx.compose.material.icons.filled.ArrowBack
2828
import androidx.compose.material.icons.filled.Check
29+
import androidx.compose.material.icons.filled.Delete
2930
import androidx.compose.material.icons.filled.Download
3031
import androidx.compose.material3.Button
3132
import androidx.compose.material3.ButtonDefaults
@@ -53,6 +54,7 @@ fun SelectPresetModelScreen(
5354
modelStates: Map<String, ModelDownloadState>,
5455
onBackPressed: () -> Unit,
5556
onDownloadClick: (String) -> Unit,
57+
onDeleteClick: (String) -> Unit,
5658
onModelClick: (String) -> Unit
5759
) {
5860
val appColors = LocalAppColors.current
@@ -119,6 +121,7 @@ fun SelectPresetModelScreen(
119121
state = state,
120122
isReady = isReady,
121123
onDownloadClick = { onDownloadClick(key) },
124+
onDeleteClick = { onDeleteClick(key) },
122125
onCardClick = { if (isReady) onModelClick(key) }
123126
)
124127
}
@@ -135,6 +138,7 @@ private fun PresetModelCard(
135138
state: ModelDownloadState,
136139
isReady: Boolean,
137140
onDownloadClick: () -> Unit,
141+
onDeleteClick: () -> Unit,
138142
onCardClick: () -> Unit
139143
) {
140144
val appColors = LocalAppColors.current
@@ -190,12 +194,25 @@ private fun PresetModelCard(
190194
strokeWidth = 3.dp
191195
)
192196
} else if (isReady) {
193-
Icon(
194-
imageVector = Icons.Filled.Check,
195-
contentDescription = "Ready",
196-
tint = Color(0xFF4CAF50),
197-
modifier = Modifier.size(36.dp)
198-
)
197+
Row(
198+
verticalAlignment = Alignment.CenterVertically
199+
) {
200+
Icon(
201+
imageVector = Icons.Filled.Check,
202+
contentDescription = "Ready",
203+
tint = Color(0xFF4CAF50),
204+
modifier = Modifier.size(36.dp)
205+
)
206+
Spacer(modifier = Modifier.width(8.dp))
207+
IconButton(onClick = onDeleteClick) {
208+
Icon(
209+
imageVector = Icons.Filled.Delete,
210+
contentDescription = "Delete",
211+
tint = Color.Red,
212+
modifier = Modifier.size(24.dp)
213+
)
214+
}
215+
}
199216
} else {
200217
Button(
201218
onClick = onDownloadClick,

llm/android/LlamaDemo/app/src/main/java/com/example/executorchllamademo/ui/viewmodel/SelectPresetModelViewModel.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,29 @@ class SelectPresetModelViewModel : ViewModel() {
191191
demoSharedPreferences?.saveModuleSettings(moduleSettings)
192192
return true
193193
}
194+
195+
fun deleteModel(key: String) {
196+
val modelInfo = availableModels[key] ?: return
197+
val modelsDir = getModelsDirectory()
198+
199+
val modelFile = File(modelsDir, modelInfo.modelFilename)
200+
val tokenizerFile = File(modelsDir, modelInfo.tokenizerFilename)
201+
202+
// Delete both files
203+
if (modelFile.exists()) {
204+
modelFile.delete()
205+
}
206+
if (tokenizerFile.exists()) {
207+
tokenizerFile.delete()
208+
}
209+
210+
// Update state
211+
modelStates[key] = ModelDownloadState(
212+
isModelDownloaded = false,
213+
isTokenizerDownloaded = false,
214+
isDownloading = false,
215+
downloadProgress = 0f,
216+
downloadError = null
217+
)
218+
}
194219
}

0 commit comments

Comments
 (0)