Skip to content

Commit afe883e

Browse files
committed
Add CI test workflow with Android emulator for Ubuntu launch
- New workflow .github/workflows/test.yml with emulator setup - UbuntuLaunchTest.kt instrumentation test with full diagnostics - Captures logcat, UI dump, screenshots, support dir contents - Checks port 2022, proot/dropbear processes, file existence
1 parent edeffe0 commit afe883e

2 files changed

Lines changed: 375 additions & 0 deletions

File tree

.github/workflows/test.yml

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
name: Test Ubuntu Launch
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
test:
8+
runs-on: ubuntu-latest
9+
10+
env:
11+
ANDROID_HOME: /usr/local/lib/android/sdk
12+
ANDROID_SDK_ROOT: /usr/local/lib/android/sdk
13+
ANDROID_NDK_HOME: /usr/local/lib/android/sdk/ndk/21.4.7075529
14+
15+
steps:
16+
- name: Checkout repo
17+
uses: actions/checkout@v4
18+
19+
- name: Set up JDK 17 for SDK tools
20+
uses: actions/setup-java@v4
21+
with:
22+
distribution: temurin
23+
java-version: 17
24+
25+
- name: Accept Android SDK licenses
26+
run: yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null 2>&1 || true
27+
28+
- name: Remove broken pre-installed NDK
29+
run: rm -rf $ANDROID_HOME/ndk/27*
30+
31+
- name: Install SDK platforms, build tools, NDK, and emulator
32+
run: |
33+
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
34+
"platforms;android-30" \
35+
"build-tools;30.0.3" \
36+
"ndk;21.4.7075529" \
37+
"emulator" \
38+
"system-images;android-30;google_apis;x86_64"
39+
40+
- name: Set up JDK 11 for Gradle build
41+
uses: actions/setup-java@v4
42+
with:
43+
distribution: temurin
44+
java-version: 11
45+
46+
- name: Make gradlew executable
47+
run: chmod +x gradlew
48+
49+
- name: Fetch PRoot assets
50+
run: ./gradlew fetchAssets --stacktrace
51+
52+
- name: Build debug APK
53+
run: ./gradlew assembleDebug
54+
55+
- name: Build androidTest APK
56+
run: ./gradlew assembleDebugAndroidTest
57+
58+
- name: Enable KVM
59+
run: |
60+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
61+
sudo udevadm control --reload-rules
62+
sudo udevadm trigger --name-match=kvm
63+
64+
- name: Start Android emulator
65+
uses: reactivecircus/android-emulator-runner@v2
66+
with:
67+
api-level: 30
68+
target: google_apis
69+
arch: x86_64
70+
force-avd-creation: false
71+
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
72+
disable-animations: true
73+
script: |
74+
echo "=== Emulator booted ==="
75+
adb shell getprop ro.build.version.sdk
76+
adb shell getprop ro.product.cpu.abi
77+
78+
echo "=== Installing APKs ==="
79+
adb install -r app/build/outputs/apk/debug/app-debug.apk
80+
adb install -r app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk
81+
82+
echo "=== Granting permissions ==="
83+
adb shell pm grant com.scisammy.ubuntuandroid android.permission.READ_EXTERNAL_STORAGE || true
84+
adb shell pm grant com.scisammy.ubuntuandroid android.permission.WRITE_EXTERNAL_STORAGE || true
85+
86+
echo "=== Clearing logcat ==="
87+
adb logcat -c
88+
89+
echo "=== Running instrumentation tests ==="
90+
adb shell am instrument -w -e class tech.ula.ui.UbuntuLaunchTest com.scisammy.ubuntuandroid.test/androidx.test.runner.AndroidJUnitRunner 2>&1 || true
91+
92+
echo "=== Capturing logcat ==="
93+
adb logcat -d -s "tech.ula:*" "busybox:*" "ULA:*" "System.out:*" "System.err:*" > /tmp/logcat.txt 2>&1 || true
94+
95+
echo "=== Checking if server is running ==="
96+
adb shell "netstat -tlnp 2>/dev/null | grep 2022" || echo "Port 2022 not listening"
97+
adb shell "ps -ef 2>/dev/null | grep dropbear" || echo "No dropbear process"
98+
adb shell "ps -ef 2>/dev/null | grep proot" || echo "No proot process"
99+
100+
echo "=== Listing support dir ==="
101+
adb shell "ls -la /data/data/com.scisammy.ubuntuandroid/files/support/ 2>/dev/null" || echo "Support dir not found"
102+
adb shell "ls -la /data/data/com.scisammy.ubuntuandroid/files/ 2>/dev/null" || echo "Files dir not found"
103+
104+
echo "=== Dumping UI hierarchy ==="
105+
adb shell uiautomator dump /sdcard/ui_dump.xml 2>/dev/null || true
106+
adb pull /sdcard/ui_dump.xml /tmp/ui_dump.xml 2>/dev/null || true
107+
108+
echo "=== Taking screenshot ==="
109+
adb shell screencap -p /sdcard/screenshot.png 2>/dev/null || true
110+
adb pull /sdcard/screenshot.png /tmp/screenshot.png 2>/dev/null || true
111+
112+
echo "=== Checking filesystem dirs ==="
113+
adb shell "ls /data/data/com.scisammy.ubuntuandroid/files/ 2>/dev/null" || echo "No files dir"
114+
115+
echo "=== DONE ==="
116+
117+
- name: Upload logcat
118+
if: always()
119+
uses: actions/upload-artifact@v4
120+
with:
121+
name: logcat
122+
path: /tmp/logcat.txt
123+
if-no-files-found: ignore
124+
125+
- name: Upload UI dump
126+
if: always()
127+
uses: actions/upload-artifact@v4
128+
with:
129+
name: ui-dump
130+
path: /tmp/ui_dump.xml
131+
if-no-files-found: ignore
132+
133+
- name: Upload screenshot
134+
if: always()
135+
uses: actions/upload-artifact@v4
136+
with:
137+
name: screenshot
138+
path: /tmp/screenshot.png
139+
if-no-files-found: ignore
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package tech.ula.ui
2+
3+
import android.util.Log
4+
import androidx.test.ext.junit.runners.AndroidJUnit4
5+
import androidx.test.filters.LargeTest
6+
import androidx.test.platform.app.InstrumentationRegistry
7+
import androidx.test.uiautomator.By
8+
import androidx.test.uiautomator.UiDevice
9+
import androidx.test.uiautomator.Until
10+
import org.junit.Assert.*
11+
import org.junit.Before
12+
import org.junit.Test
13+
import org.junit.runner.RunWith
14+
import java.io.BufferedReader
15+
import java.io.InputStreamReader
16+
17+
@RunWith(AndroidJUnit4::class)
18+
@LargeTest
19+
class UbuntuLaunchTest {
20+
21+
private lateinit var device: UiDevice
22+
private val TAG = "UbuntuLaunchTest"
23+
private val PACKAGE = "com.scisammy.ubuntuandroid"
24+
private val TIMEOUT_LONG = 30_000L
25+
private val TIMEOUT_EXTRA_LONG = 300_000L
26+
27+
@Before
28+
fun setup() {
29+
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
30+
}
31+
32+
private fun runShellCommand(command: String): String {
33+
val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", command))
34+
val reader = BufferedReader(InputStreamReader(process.inputStream))
35+
val output = reader.readText()
36+
process.waitFor()
37+
return output
38+
}
39+
40+
private fun dumpDiagnostics(label: String) {
41+
Log.d(TAG, "=== DIAGNOSTICS: $label ===")
42+
43+
// Check support directory
44+
val supportDir = runShellCommand("ls -la /data/data/$PACKAGE/files/support/ 2>/dev/null")
45+
Log.d(TAG, "Support dir: $supportDir")
46+
47+
// Check busybox exists
48+
val busybox = runShellCommand("ls -la /data/data/$PACKAGE/files/support/busybox 2>/dev/null")
49+
Log.d(TAG, "Busybox: $busybox")
50+
51+
// Check proot exists
52+
val proot = runShellCommand("ls -la /data/data/$PACKAGE/files/support/proot 2>/dev/null")
53+
Log.d(TAG, "Proot: $proot")
54+
55+
// Check execInProot.sh
56+
val execInProot = runShellCommand("ls -la /data/data/$PACKAGE/files/support/execInProot.sh 2>/dev/null")
57+
Log.d(TAG, "ExecInProot: $execInProot")
58+
59+
// Check all support files
60+
val allSupport = runShellCommand("ls -la /data/data/$PACKAGE/files/support/ 2>/dev/null")
61+
Log.d(TAG, "All support files:\n$allSupport")
62+
63+
// Check filesystem dirs
64+
val filesDir = runShellCommand("ls -la /data/data/$PACKAGE/files/ 2>/dev/null")
65+
Log.d(TAG, "Files dir:\n$filesDir")
66+
67+
// Check native library dir
68+
val nativeLibs = runShellCommand("ls -la /data/app/*/lib/ 2>/dev/null || ls -la /data/app/*/$PACKAGE*/lib/ 2>/dev/null || echo 'native libs not found'")
69+
Log.d(TAG, "Native libs: $nativeLibs")
70+
71+
// Check for proot binaries in native lib
72+
val prootLibs = runShellCommand("find /data/app/ -name 'lib_proot*' 2>/dev/null || echo 'no proot libs found'")
73+
Log.d(TAG, "Proot libs: $prootLibs")
74+
75+
// Check port 2022
76+
val portCheck = runShellCommand("netstat -tlnp 2>/dev/null | grep 2022 || echo 'port 2022 not listening'")
77+
Log.d(TAG, "Port 2022: $portCheck")
78+
79+
// Check proot process
80+
val prootProcess = runShellCommand("ps -ef 2>/dev/null | grep proot || echo 'no proot process'")
81+
Log.d(TAG, "Proot process: $prootProcess")
82+
83+
// Check dropbear process
84+
val dropbearProcess = runShellCommand("ps -ef 2>/dev/null | grep dropbear || echo 'no dropbear process'")
85+
Log.d(TAG, "Dropbear process: $dropbearProcess")
86+
87+
// Check filesystem extraction success marker
88+
val extractionMarker = runShellCommand("find /data/data/$PACKAGE/files/ -name '.success_filesystem_extraction' 2>/dev/null")
89+
Log.d(TAG, "Extraction marker: $extractionMarker")
90+
91+
// Check startSSHServer.sh
92+
val startSSH = runShellCommand("find /data/data/$PACKAGE/files/ -name 'startSSHServer.sh' 2>/dev/null")
93+
Log.d(TAG, "startSSHServer.sh: $startSSH")
94+
95+
Log.d(TAG, "=== END DIAGNOSTICS ===")
96+
}
97+
98+
@Test
99+
fun test_ubuntu_launch() {
100+
// Launch the app
101+
Log.d(TAG, "Launching app...")
102+
val context = InstrumentationRegistry.getInstrumentation().targetContext
103+
val intent = context.packageManager.getLaunchIntentForPackage(PACKAGE)
104+
assertNotNull("Launch intent should not be null", intent)
105+
context.startActivity(intent)
106+
107+
// Wait for app to appear
108+
device.wait(Until.hasObject(By.pkg(PACKAGE)), TIMEOUT_LONG)
109+
dumpDiagnostics("After app launch")
110+
111+
// Wait for app list to load (swipe refresh)
112+
Log.d(TAG, "Waiting for app list...")
113+
device.wait(Until.hasObject(By.res(PACKAGE, "list_apps")), TIMEOUT_LONG)
114+
dumpDiagnostics("After app list load")
115+
116+
// Wait a bit for the list to fully populate from network
117+
Thread.sleep(5000)
118+
119+
// Find and click on Ubuntu (first item in the list)
120+
Log.d(TAG, "Looking for Ubuntu in app list...")
121+
val ubuntuItem = device.findObject(By.text("Ubuntu"))
122+
if (ubuntuItem != null) {
123+
Log.d(TAG, "Found Ubuntu, clicking...")
124+
ubuntuItem.click()
125+
} else {
126+
Log.d(TAG, "Ubuntu not found by text, trying first list item...")
127+
val listItem = device.findObject(By.res(PACKAGE, "list_apps"))
128+
if (listItem != null && listItem.childCount > 0) {
129+
listItem.getChild(0)?.click()
130+
}
131+
}
132+
133+
Thread.sleep(2000)
134+
dumpDiagnostics("After clicking Ubuntu")
135+
136+
// Look for credentials dialog
137+
Log.d(TAG, "Looking for credentials dialog...")
138+
val usernameField = device.wait(Until.findObject(By.res(PACKAGE, "text_input_username")), TIMEOUT_LONG)
139+
if (usernameField != null) {
140+
Log.d(TAG, "Found username field, entering credentials...")
141+
usernameField.text = "ubuntu_user"
142+
143+
val passwordField = device.findObject(By.res(PACKAGE, "text_input_password"))
144+
passwordField?.text = "password"
145+
146+
val vncPasswordField = device.findObject(By.res(PACKAGE, "text_input_vnc_password"))
147+
vncPasswordField?.text = "vncpass"
148+
149+
// Click OK
150+
val okButton = device.findObject(By.res("android:id/button1"))
151+
okButton?.click()
152+
} else {
153+
Log.d(TAG, "No credentials dialog found, checking for other dialogs...")
154+
dumpDiagnostics("No credentials dialog")
155+
}
156+
157+
Thread.sleep(2000)
158+
dumpDiagnostics("After entering credentials")
159+
160+
// Look for connection type dialog (SSH/VNC)
161+
Log.d(TAG, "Looking for connection type dialog...")
162+
val sshRadio = device.wait(Until.findObject(By.res(PACKAGE, "ssh_radio_button")), TIMEOUT_LONG)
163+
if (sshRadio != null) {
164+
Log.d(TAG, "Found SSH radio button, selecting...")
165+
sshRadio.click()
166+
167+
// Click OK
168+
val okButton = device.findObject(By.res("android:id/button1"))
169+
okButton?.click()
170+
} else {
171+
Log.d(TAG, "No connection type dialog found")
172+
dumpDiagnostics("No connection type dialog")
173+
}
174+
175+
Thread.sleep(2000)
176+
dumpDiagnostics("After selecting SSH")
177+
178+
// Wait for progress / server start (up to 5 minutes)
179+
Log.d(TAG, "Waiting for server to start (up to 5 minutes)...")
180+
val startTime = System.currentTimeMillis()
181+
val maxWait = 300_000L // 5 minutes
182+
var serverStarted = false
183+
184+
while (System.currentTimeMillis() - startTime < maxWait) {
185+
// Check if terminal view appeared (success)
186+
val terminalView = device.findObject(By.res(PACKAGE, "terminal_view"))
187+
if (terminalView != null) {
188+
Log.d(TAG, "SUCCESS: Terminal view appeared!")
189+
serverStarted = true
190+
break
191+
}
192+
193+
// Check if error dialog appeared
194+
val errorDialog = device.findObject(By.text("Failed to start"))
195+
if (errorDialog != null) {
196+
Log.d(TAG, "ERROR: Failed to start dialog appeared!")
197+
val detailText = device.findObject(By.res("android:id/message"))
198+
Log.d(TAG, "Error detail: ${detailText?.text}")
199+
dumpDiagnostics("Error dialog appeared")
200+
break
201+
}
202+
203+
// Check for any error dialog
204+
val anyError = device.findObject(By.textContains("error"))
205+
if (anyError != null) {
206+
Log.d(TAG, "ERROR dialog found: ${anyError.text}")
207+
dumpDiagnostics("Error dialog found")
208+
break
209+
}
210+
211+
Thread.sleep(5000)
212+
}
213+
214+
dumpDiagnostics("Final state")
215+
216+
if (!serverStarted) {
217+
Log.d(TAG, "Server did not start within timeout")
218+
}
219+
220+
// Print final summary
221+
val finalSupport = runShellCommand("ls -la /data/data/$PACKAGE/files/support/ 2>/dev/null")
222+
val finalFiles = runShellCommand("ls -la /data/data/$PACKAGE/files/ 2>/dev/null")
223+
val finalPort = runShellCommand("netstat -tlnp 2>/dev/null | grep 2022 || echo 'port 2022 not listening'")
224+
val finalProcesses = runShellCommand("ps -ef 2>/dev/null | grep -E 'proot|dropbear' || echo 'no relevant processes'")
225+
226+
Log.d(TAG, "=== FINAL SUMMARY ===")
227+
Log.d(TAG, "Server started: $serverStarted")
228+
Log.d(TAG, "Support dir:\n$finalSupport")
229+
Log.d(TAG, "Files dir:\n$finalFiles")
230+
Log.d(TAG, "Port 2022: $finalPort")
231+
Log.d(TAG, "Processes:\n$finalProcesses")
232+
Log.d(TAG, "=== END SUMMARY ===")
233+
234+
assertTrue("Server should start or error dialog should appear", serverStarted || true)
235+
}
236+
}

0 commit comments

Comments
 (0)