Skip to content

Commit 2052cfe

Browse files
authored
Fix instrumentation test preset and avoid sleep during test (#155)
1 parent 8dc20c1 commit 2052cfe

4 files changed

Lines changed: 156 additions & 12 deletions

File tree

llm/android/LlamaDemo/app/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,14 @@ android {
220220
versionName = "1.0"
221221

222222
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
223+
224+
// Automatically set instrumentation arguments based on model preset
225+
val preset = modelPresets[modelPreset]
226+
if (preset != null) {
227+
testInstrumentationRunnerArguments["modelFile"] = preset["pteFile"] as String
228+
testInstrumentationRunnerArguments["tokenizerFile"] = preset["tokenizerFile"] as String
229+
}
230+
223231
vectorDrawables { useSupportLibrary = true }
224232
externalNativeBuild { cmake { cppFlags += "" } }
225233
}

llm/android/LlamaDemo/app/src/androidTest/java/com/example/executorchllamademo/UIWorkflowTest.java

Lines changed: 146 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import static androidx.test.espresso.matcher.ViewMatchers.withId;
2020
import static androidx.test.espresso.matcher.ViewMatchers.withText;
2121
import static org.hamcrest.Matchers.anything;
22-
import static org.hamcrest.Matchers.containsString;
22+
import static org.hamcrest.Matchers.endsWith;
2323
import static org.hamcrest.Matchers.greaterThan;
2424
import static org.hamcrest.Matchers.hasToString;
2525
import static org.hamcrest.Matchers.not;
@@ -118,12 +118,12 @@ public void testModelLoadingWorkflow() throws Exception {
118118
// Step 3: Click model selection button and select the model file
119119
onView(withId(R.id.modelImageButton)).perform(click());
120120
// Select the model file matching the configured filename
121-
onData(hasToString(containsString(modelFile))).inRoot(isDialog()).perform(click());
121+
onData(hasToString(endsWith(modelFile))).inRoot(isDialog()).perform(click());
122122

123123
// Step 4: Click tokenizer selection button and select the tokenizer file
124124
onView(withId(R.id.tokenizerImageButton)).perform(click());
125125
// Select the tokenizer file matching the configured filename
126-
onData(hasToString(containsString(tokenizerFile))).inRoot(isDialog()).perform(click());
126+
onData(hasToString(endsWith(tokenizerFile))).inRoot(isDialog()).perform(click());
127127

128128
// Step 5: Click load model button
129129
onView(withId(R.id.loadModelButton)).perform(click());
@@ -165,13 +165,13 @@ public void testSendMessageAndReceiveResponse() throws Exception {
165165
// Select model - choose the configured model file
166166
onView(withId(R.id.modelImageButton)).perform(click());
167167
Thread.sleep(300); // Wait for dialog to appear
168-
onData(hasToString(containsString(modelFile))).inRoot(isDialog()).perform(click());
168+
onData(hasToString(endsWith(modelFile))).inRoot(isDialog()).perform(click());
169169
Thread.sleep(300); // Wait for dialog to dismiss and UI to update
170170

171171
// Select tokenizer - choose the configured tokenizer file
172172
onView(withId(R.id.tokenizerImageButton)).perform(click());
173173
Thread.sleep(300); // Wait for dialog to appear
174-
onData(hasToString(containsString(tokenizerFile))).inRoot(isDialog()).perform(click());
174+
onData(hasToString(endsWith(tokenizerFile))).inRoot(isDialog()).perform(click());
175175
Thread.sleep(300); // Wait for dialog to dismiss and UI to update
176176

177177
// Verify load button is now enabled
@@ -198,9 +198,10 @@ public void testSendMessageAndReceiveResponse() throws Exception {
198198
// Click send button
199199
onView(withId(R.id.sendButton)).perform(click());
200200

201-
// --- Wait for response and validate ---
202-
// Wait 50 seconds for model to generate response
203-
Thread.sleep(50000);
201+
// --- Wait for response ---
202+
// Poll until we have some response text (at least 50 characters)
203+
boolean hasResponse = waitForResponseLength(scenario, 50, 60000);
204+
assertTrue("Model should generate a response", hasResponse);
204205

205206
// Extract all messages from the list
206207
AtomicInteger messageCount = new AtomicInteger(0);
@@ -265,6 +266,143 @@ private boolean waitForModelLoaded(ActivityScenario<MainActivity> scenario, long
265266
return false;
266267
}
267268

269+
/**
270+
* Tests stopping generation mid-way:
271+
* 1. Load model
272+
* 2. Send a message to start generation
273+
* 3. Wait for generation to start (button changes to stop mode)
274+
* 4. Click stop button
275+
* 5. Verify generation stops (button returns to send mode)
276+
* 6. Verify partial response was received
277+
*/
278+
@Test
279+
public void testStopGeneration() throws Exception {
280+
try (ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class)) {
281+
// Wait for activity to fully load
282+
Thread.sleep(1000);
283+
284+
// Dismiss the "Please Select a Model" dialog
285+
onView(withText(android.R.string.ok)).inRoot(isDialog()).perform(click());
286+
287+
// --- Load model ---
288+
onView(withId(R.id.settings)).perform(click());
289+
Thread.sleep(500);
290+
291+
// Select model
292+
onView(withId(R.id.modelImageButton)).perform(click());
293+
Thread.sleep(300);
294+
onData(hasToString(endsWith(modelFile))).inRoot(isDialog()).perform(click());
295+
Thread.sleep(300);
296+
297+
// Select tokenizer
298+
onView(withId(R.id.tokenizerImageButton)).perform(click());
299+
Thread.sleep(300);
300+
onData(hasToString(endsWith(tokenizerFile))).inRoot(isDialog()).perform(click());
301+
Thread.sleep(300);
302+
303+
// Load model
304+
onView(withId(R.id.loadModelButton)).perform(click());
305+
onView(withText(android.R.string.yes)).inRoot(isDialog()).perform(click());
306+
307+
// Wait for model to load
308+
boolean modelLoaded = waitForModelLoaded(scenario, 60000);
309+
assertTrue("Model should be loaded successfully", modelLoaded);
310+
311+
// --- Send a message to start generation ---
312+
onView(withId(R.id.editTextMessage)).perform(typeText("Write a very long story about a brave knight"), ViewActions.closeSoftKeyboard());
313+
onView(withId(R.id.sendButton)).perform(click());
314+
315+
// --- Wait for generation to start (some response text appears) ---
316+
boolean generationStarted = waitForResponseStarted(scenario, 30000);
317+
assertTrue("Generation should start (some response text should appear)", generationStarted);
318+
319+
// --- Wait for some text to generate (at least 20 characters) ---
320+
boolean hasEnoughText = waitForResponseLength(scenario, 20, 30000);
321+
assertTrue("Should generate some text before stopping", hasEnoughText);
322+
323+
// --- Click stop button ---
324+
onView(withId(R.id.sendButton)).perform(click());
325+
326+
// --- Wait for generation to stop ---
327+
// Give it a moment to process the stop
328+
Thread.sleep(1000);
329+
330+
// --- Verify we got a partial response ---
331+
AtomicReference<String> responseText = new AtomicReference<>("");
332+
scenario.onActivity(activity -> {
333+
ListView messagesView = activity.findViewById(R.id.messages_view);
334+
if (messagesView != null && messagesView.getAdapter() != null) {
335+
for (int i = 0; i < messagesView.getAdapter().getCount(); i++) {
336+
Object item = messagesView.getAdapter().getItem(i);
337+
if (item instanceof Message) {
338+
Message message = (Message) item;
339+
// Find the model response (not sent by user, not system message)
340+
if (!message.getIsSent() && !message.getText().contains("Successfully loaded")) {
341+
responseText.set(message.getText());
342+
}
343+
}
344+
}
345+
}
346+
});
347+
348+
// Log the partial response
349+
android.util.Log.i("STOP_TEST", "Partial response after stop: " + responseText.get());
350+
351+
// We should have received some tokens before stopping
352+
assertTrue("Should have received some response before stopping",
353+
responseText.get() != null && !responseText.get().isEmpty());
354+
}
355+
}
356+
357+
/**
358+
* Waits for generation to start by checking for model response text.
359+
*
360+
* @param scenario the activity scenario
361+
* @param timeoutMs maximum time to wait in milliseconds
362+
* @return true if response text appeared, false if timeout
363+
*/
364+
private boolean waitForResponseStarted(ActivityScenario<MainActivity> scenario, long timeoutMs) throws InterruptedException {
365+
return waitForResponseLength(scenario, 1, timeoutMs);
366+
}
367+
368+
/**
369+
* Waits for the model response to reach a minimum length.
370+
*
371+
* @param scenario the activity scenario
372+
* @param minLength minimum response length in characters
373+
* @param timeoutMs maximum time to wait in milliseconds
374+
* @return true if response reached minimum length, false if timeout
375+
*/
376+
private boolean waitForResponseLength(ActivityScenario<MainActivity> scenario, int minLength, long timeoutMs) throws InterruptedException {
377+
long startTime = System.currentTimeMillis();
378+
while (System.currentTimeMillis() - startTime < timeoutMs) {
379+
AtomicInteger responseLength = new AtomicInteger(0);
380+
scenario.onActivity(activity -> {
381+
ListView messagesView = activity.findViewById(R.id.messages_view);
382+
if (messagesView != null && messagesView.getAdapter() != null) {
383+
for (int i = 0; i < messagesView.getAdapter().getCount(); i++) {
384+
Object item = messagesView.getAdapter().getItem(i);
385+
if (item instanceof Message) {
386+
Message message = (Message) item;
387+
// Look for a model response (not sent, not system message)
388+
if (!message.getIsSent()
389+
&& !message.getText().contains("Successfully loaded")
390+
&& !message.getText().contains("Loading model")
391+
&& !message.getText().contains("To get started")) {
392+
responseLength.set(message.getText().length());
393+
}
394+
}
395+
}
396+
}
397+
});
398+
if (responseLength.get() >= minLength) {
399+
return true;
400+
}
401+
Thread.sleep(200); // Poll every 200ms
402+
}
403+
return false;
404+
}
405+
268406
/**
269407
* Writes the model response to logcat with a special tag for extraction.
270408
* The response can be extracted from logcat using: grep "LLAMA_RESPONSE"

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,7 @@ private void addSelectedImagesToChatThread(List<Uri> selectedImageUri) {
776776
}
777777

778778
private void onModelRunStarted() {
779-
mSendButton.setClickable(false);
779+
mSendButton.setClickable(true);
780780
mSendButton.setImageResource(R.drawable.baseline_stop_24);
781781
mSendButton.setOnClickListener(
782782
view -> {

llm/android/LlamaDemo/scripts/run-ci-tests.sh

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,7 @@ LOGCAT_PID=$!
9292
echo "=== Starting Gradle ==="
9393
./gradlew connectedCheck \
9494
-PskipModelDownload=true \
95-
-PmodelPreset="$MODEL_PRESET" \
96-
-Pandroid.testInstrumentationRunnerArguments.modelFile="$MODEL_FILE" \
97-
-Pandroid.testInstrumentationRunnerArguments.tokenizerFile="$TOKENIZER_FILE"
95+
-PmodelPreset="$MODEL_PRESET"
9896
TEST_EXIT_CODE=$?
9997

10098
echo "=== Model directory after Gradle ==="

0 commit comments

Comments
 (0)