Skip to content

Commit 6deb704

Browse files
vaslabsirodotos7
andauthored
Working towards android support (#4169)
## Part of Android support ### Relevant issues: - #3868 - #3550 ### Outline - Follow android studio + gradle standard for directory structure - Unit test support (local runs - no android device needed) - Integration test support (android tests, needs an android device/emulator) ### New features - Ability to install an apk in the default available android device (tested against AVD) - Ability to run all integration tests with the install command (android manifest with instrumentation is needed, this needs to be a functionality of integration tests support in the future instead) - Classpath resolution (runtimeDependencies) for packaging with dependencies and user's source code (tested against integration test and running the android app itself via the emulator) - Special classpath resolution and packaging for android tests (integration tests) - Mill integrates with avdmanager, adb and emulator to automate the run of integration tests by creating virtual devices and controlling the emulator (start, stop). Examples are added in documentation of both kotlin and java app. This includes the ability to use different emulators for different apps, by specifying identifiers and ports, though this use case should be rare in app development lifecycles. - Github actions & mill support to run integration tests in (headless) emulator & differentiate from local mode (windowed emulator + default gpu settings) ### Github actions A sample repo to demonstrate what's needed to run the emulator https://github.com/vaslabs-ltd/test-android-emulator In the end what slowed me down was that for github actions, the AVD home env var was needed, which wasn't obvious until I've tested it in isolation . All the flags and kvm configs are also needed and it's what https://github.com/ReactiveCircus/android-emulator-runner does ### Stubs and missing features An effort was made to standardise the structure faster, so follow up PRs will not use a non-standard structure. Things that are working but not "production ready" are: 1. Integration tests debug apk: This inherits the current apk construction, debug functionality needs to be added and separated from the release androidApk task 2. No testOnly for integration tests: this is easy to do, but was not added in the interest of not increasing the size of the PR and the testing surface 3. Classpath resolution: For kotlin, jvm specific dependencies are removed in a hardcoded way. 4. Desugaring: This shows as a warning in the android tests apk creation, while the flow works, the d8 will need to be configured to desugar annotations properly. 5. Emulator CI mode only recognises github actions ### Demo #### Running android tests https://github.com/user-attachments/assets/0cb47ac7-32ff-4d5c-a678-11cd18807b10 --------- Co-authored-by: irodotos7 <[email protected]>
1 parent 4cc94cf commit 6deb704

File tree

29 files changed

+781
-75
lines changed

29 files changed

+781
-75
lines changed

.github/workflows/post-build-selective.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,19 @@ jobs:
5353
if: ${{ inputs.install-android-sdk }}
5454
with:
5555
log-accepted-android-sdk-licenses: false
56+
cmdline-tools-version: 11076708
57+
packages: tools platform-tools emulator system-images;android-35;google_apis_playstore;x86_64
58+
59+
- name: Enable KVM group perms
60+
if: ${{ inputs.install-android-sdk }}
61+
run: |
62+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
63+
sudo udevadm control --reload-rules
64+
sudo udevadm trigger --name-match=kvm
65+
66+
- name: Set AVD environment variable globally
67+
if: ${{ inputs.install-android-sdk }}
68+
run: echo "ANDROID_AVD_HOME=/home/runner/.config/.android/avd" >> $GITHUB_ENV
5669

5770
- run: ./mill -i -k selective.resolve ${{ inputs.millargs }}
5871

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.helloworld.app;
2+
3+
import static org.junit.Assert.*;
4+
5+
import android.content.Context;
6+
import androidx.test.ext.junit.runners.AndroidJUnit4;
7+
import androidx.test.platform.app.InstrumentationRegistry;
8+
import org.junit.Test;
9+
import org.junit.runner.RunWith;
10+
11+
/**
12+
* Instrumented test, which will execute on an Android device.
13+
*
14+
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
15+
*/
16+
@RunWith(AndroidJUnit4.class)
17+
public class ExampleInstrumentedTest {
18+
@Test
19+
public void useAppContext() {
20+
// Context of the app under test.
21+
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
22+
assertEquals("com.helloworld.app", appContext.getPackageName());
23+
}
24+
}

example/android/javalib/2-app-bundle/bundle/AndroidManifest.xml renamed to example/android/javalib/1-hello-world/app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@
99
</intent-filter>
1010
</activity>
1111
</application>
12+
<instrumentation
13+
android:name="androidx.test.runner.AndroidJUnitRunner"
14+
android:targetPackage="com.helloworld.app" />
1215
</manifest>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.helloworld;
2+
3+
class SampleLogic {
4+
5+
public static float textSize() {
6+
return 32f;
7+
}
8+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.helloworld;
2+
3+
import static org.junit.Assert.*;
4+
5+
import org.junit.Test;
6+
7+
/**
8+
* Example local unit test, which will execute on the development machine (host).
9+
*
10+
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
11+
*/
12+
public class ExampleUnitTest {
13+
@Test
14+
public void textSize_isCorrect() {
15+
assertEquals(32f, SampleLogic.textSize(), 0.000001f);
16+
}
17+
}

example/android/javalib/1-hello-world/build.mill

Lines changed: 118 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99
//// SNIPPET:BUILD
1010
package build
1111

12-
import mill._
12+
import mill._, javalib._
13+
1314
import mill.javalib.android.{AndroidAppModule, AndroidSdkModule}
15+
import coursier.maven.MavenRepository
16+
import mill.javalib.android.AndroidTestModule
1417

1518
// Create and configure an Android SDK module to manage Android SDK paths and tools.
1619
object androidSdkModule0 extends AndroidSdkModule {
@@ -23,11 +26,71 @@ object app extends AndroidAppModule {
2326
def androidSdkModule = mill.define.ModuleRef(androidSdkModule0)
2427

2528
// Configuration for ReleaseKey
29+
override def releaseKeyPath = super.millSourcePath
30+
2631
def androidReleaseKeyName: T[String] = Task { "releaseKey.jks" }
2732
def androidReleaseKeyAlias: T[String] = Task { "releaseKey" }
2833
def androidReleaseKeyPass: T[String] = Task { "MillBuildTool" }
2934
def androidReleaseKeyStorePass: T[String] = Task { "MillBuildTool" }
3035

36+
override def androidVirtualDeviceIdentifier: String = "java-test"
37+
38+
private def mainRoot = millSourcePath
39+
40+
object test extends AndroidAppTests with TestModule.Junit4 {
41+
def testFramework = "com.novocode.junit.JUnitFramework"
42+
def ivyDeps = super.ivyDeps() ++ Agg(
43+
ivy"junit:junit:4.13.2"
44+
)
45+
}
46+
47+
object it extends AndroidAppIntegrationTests with AndroidTestModule.AndroidJUnit {
48+
def repositoriesTask = Task.Anon {
49+
super.repositoriesTask() ++
50+
Seq(MavenRepository("https://maven.google.com"))
51+
}
52+
53+
def androidSdkModule = mill.define.ModuleRef(androidSdkModule0)
54+
55+
override def instrumentationPackage = "com.helloworld.app"
56+
57+
/* TODO this needs to change to the location of the debug keystore once integration test */
58+
override def releaseKeyPath = mainRoot
59+
60+
def androidReleaseKeyName: T[String] = Task {
61+
"releaseKey.jks"
62+
}
63+
64+
def androidReleaseKeyAlias: T[String] = Task {
65+
"releaseKey"
66+
}
67+
68+
def androidReleaseKeyPass: T[String] = Task {
69+
"MillBuildTool"
70+
}
71+
72+
def androidReleaseKeyStorePass: T[String] = Task {
73+
"MillBuildTool"
74+
}
75+
76+
/* TODO currently the dependency resolution ignores the platform type and kotlinx-coroutines-core has
77+
* conflicting classes with kotlinx-coroutines-core-jvm . Remove the exclusions once the dependency
78+
* resolution resolves conflicts between androidJvm and jvm platform types
79+
*/
80+
def ivyDeps = super.ivyDeps() ++ Agg(
81+
ivy"androidx.test.ext:junit:1.2.1".exclude((
82+
"org.jetbrains.kotlinx",
83+
"kotlinx-coroutines-core-jvm"
84+
)),
85+
ivy"androidx.test:runner:1.6.2",
86+
ivy"androidx.test.espresso:espresso-core:3.5.1".exclude((
87+
"org.jetbrains.kotlinx",
88+
"kotlinx-coroutines-core-jvm"
89+
)),
90+
ivy"junit:junit:4.13.2"
91+
)
92+
}
93+
3194
}
3295

3396
////SNIPPET:END
@@ -52,14 +115,59 @@ object app extends AndroidAppModule {
52115
//
53116
// ----
54117
// .
55-
// ├── app
56-
// │ ├── AndroidManifest.xml
57-
// │ ├── releaseKey.jks
58-
// │ ├── resources
59-
// │ │ └── values
60-
// │ │ ├── colors.xml
61-
// │ │ └── strings.xml
62-
// │ └── src/main/java/com/helloworld/app/MainActivity.java
63-
// └── build.mill
118+
//├── app
119+
//│ └── src
120+
//│ ├── androidTest/java/com/helloworld/app/ExampleInstrumentedTest.java
121+
//│ ├── main
122+
//│ │ ├── AndroidManifest.xml
123+
//│ │ ├── java/com/helloworld/app/MainActivity.java
124+
//│ │ └── res
125+
//│ │ └── values
126+
//│ │ ├── colors.xml
127+
//│ │ └── strings.xml
128+
//│ └── test/java/com/helloworld/app/ExampleUnitTest.java
129+
//└── build.mill
64130
// ----
65131
//
132+
133+
/** Usage
134+
135+
> ./mill show app.test
136+
...compiling 3 Java sources...
137+
138+
> cat out/app/test/test.dest/out.json
139+
["",[{"fullyQualifiedName":"com.helloworld.ExampleUnitTest.textSize_isCorrect","selector":"com.helloworld.ExampleUnitTest.textSize_isCorrect","duration":...,"status":"Success"}]]
140+
141+
*/
142+
143+
// This command runs unit tests on your local environment.
144+
145+
/** Usage
146+
147+
> ./mill show app.createAndroidVirtualDevice
148+
...Name: java-test, DeviceId: medium_phone...
149+
150+
> ./mill show app.startAndroidEmulator
151+
152+
> ./mill show app.waitForDevice
153+
...emulator-5554...
154+
155+
> ./mill show app.it | grep '"OK (1 test)"'
156+
..."OK (1 test)",
157+
158+
> cat out/app/it/test.json | grep '"OK (1 test)"'
159+
..."OK (1 test)"...
160+
161+
> ./mill show app.stopAndroidEmulator
162+
163+
> ./mill show app.deleteAndroidVirtualDevice
164+
165+
*/
166+
167+
// The android tests (existing typically in androidTest directory, aka instrumented tests)
168+
// typically run on an android device.
169+
// The createAndroidVirtualDevice command creates an AVD (Android Virtual Device)
170+
// and the startAndroidEmulator command starts the AVD. The it task runs the android tests
171+
// against the available AVD. The stopAndroidEmulator command stops the AVD and the
172+
// destroyAndroidVirtualDevice command destroys the AVD.
173+
// The provided commands can be used in a CI/CD pipeline assuming the right setup is in place.

example/android/javalib/2-app-bundle/build.mill

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ object androidSdkModule0 extends AndroidSdkModule {
1818
object bundle extends AndroidAppBundle {
1919
def androidSdkModule = mill.define.ModuleRef(androidSdkModule0)
2020

21+
override def releaseKeyPath = millSourcePath
22+
2123
def androidReleaseKeyName: T[String] = Task { "releaseKey.jks" }
2224
def androidReleaseKeyAlias: T[String] = Task { "releaseKey" }
2325
def androidReleaseKeyPass: T[String] = Task { "MillBuildTool" }
@@ -45,14 +47,15 @@ object bundle extends AndroidAppBundle {
4547
//
4648
// ----
4749
// .
48-
// ├── app
49-
// │ ├── AndroidManifest.xml
50-
// │ ├── releaseKey.jks
51-
// │ ├── resources
52-
// │ │ └── values
53-
// │ │ ├── colors.xml
54-
// │ │ └── strings.xml
55-
// │ └── src/main/java/com/helloworld/app/MainActivity.java
56-
// └── build.mill
50+
//bundle/
51+
//└── src
52+
// ├── main
53+
// │ ├── AndroidManifest.xml
54+
// │ └── java/com/helloworld/app/MainActivity.java
55+
// └── res
56+
// └── values
57+
// ├── colors.xml
58+
// └── strings.xml
59+
//
5760
// ----
5861
//

0 commit comments

Comments
 (0)