Skip to content

Commit 90fba35

Browse files
Run android emulator on ubuntu build agent. gradle-managed-devices is used instead of reactivecircus/android-emulator-runner GitHub action task (#524)
1 parent 096f468 commit 90fba35

File tree

24 files changed

+228
-132
lines changed

24 files changed

+228
-132
lines changed

.github/workflows/pull_request.yml

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,46 +8,41 @@ on:
88
branches: [ master ]
99

1010
jobs:
11+
jobEmulatorMatrixSetup:
12+
runs-on: ubuntu-latest
13+
outputs:
14+
emulator_jobs_matrix: ${{ steps.dataStep.outputs.emulator_jobs_matrix }}
15+
steps:
16+
- uses: actions/checkout@v4
17+
- name: Set up JDK
18+
uses: actions/setup-java@v3
19+
with:
20+
distribution: 'zulu'
21+
java-version: '17'
22+
cache: gradle
23+
- name: Prepare the matrix JSON
24+
run: ./gradlew ciEmulatorJobsMatrixSetup
25+
- id: dataStep
26+
run: echo "emulator_jobs_matrix=$(jq -c . < ./build/emulator_jobs_matrix.json)" >> $GITHUB_OUTPUT
1127
build-android:
12-
runs-on: macos-13
28+
needs: jobEmulatorMatrixSetup
29+
runs-on: ubuntu-latest
1330
strategy:
14-
matrix:
15-
api-level: [ 34 ]
31+
fail-fast: false
32+
matrix: ${{ fromJson(needs.jobEmulatorMatrixSetup.outputs.emulator_jobs_matrix) }}
1633
steps:
1734
- uses: actions/checkout@v3
35+
- name: Enable KVM group perms
36+
run: |
37+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
38+
sudo udevadm control --reload-rules
39+
sudo udevadm trigger --name-match=kvm
1840
- name: Setup test environment
1941
uses: ./.github/actions/setup_test_action
20-
- name: AVD cache
21-
uses: actions/cache@v3
22-
id: avd-cache
23-
with:
24-
path: |
25-
~/.android/avd/*
26-
~/.android/adb*
27-
key: avd-${{ matrix.api-level }}-${{ runner.os }}-${{ runner.arch }}
28-
- name: create AVD and generate snapshot for caching
29-
if: steps.avd-cache.outputs.cache-hit != 'true'
30-
uses: reactivecircus/android-emulator-runner@v2
31-
with:
32-
api-level: ${{ matrix.api-level }}
33-
arch: x86_64
34-
target: google_apis
35-
avd-name: pixel6_API${{ matrix.api-level }}
36-
force-avd-creation: false
37-
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
38-
disable-animations: false
39-
script: echo "Generated AVD snapshot for caching."
42+
- name: Apply Android licenses
43+
run: ./gradlew ciSdkManagerLicenses
4044
- name: Run Android Instrumented Tests
41-
uses: reactivecircus/android-emulator-runner@v2
42-
with:
43-
api-level: ${{ matrix.api-level }}
44-
arch: x86_64
45-
target: google_apis
46-
avd-name: pixel6_API${{ matrix.api-level }}
47-
force-avd-creation: false
48-
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
49-
disable-animations: true
50-
script: ./gradlew connectedAndroidTest
45+
run: ./gradlew ${{ matrix.gradle_tasks }}
5146
- name: Upload Android test artifact
5247
uses: actions/upload-artifact@v3
5348
if: failure()

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Project exclude paths
2-
/.gradle/
2+
/**/.gradle/
33
/**/build/
44
/.idea/
55
local.properties

build.gradle.kts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import org.apache.tools.ant.taskdefs.condition.Os
12
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
23
import org.gradle.api.tasks.testing.logging.TestLogEvent
4+
import java.io.InputStream
35

46
plugins {
57
alias(libs.plugins.android.application) apply false
@@ -9,6 +11,7 @@ plugins {
911
alias(libs.plugins.test.logger.plugin) apply false
1012
alias(libs.plugins.ben.manes.versions) apply false
1113
id("base")
14+
id("testOptionsConvention")
1215
}
1316

1417
val compileSdkVersion by extra(34)
@@ -201,3 +204,50 @@ tasks.withType<com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
201204
reportfileName = "dependency-updates"
202205
}
203206
// check for latest dependencies - ./gradlew dependencyUpdates -Drevision=release
207+
208+
tasks.register("devRunEmulatorTests") {
209+
doLast {
210+
EmulatorJobsMatrix().getTaskList(rootProject = rootProject).forEach { gradleTasks ->
211+
exec {
212+
executable = File(
213+
project.rootDir,
214+
if (Os.isFamily(Os.FAMILY_WINDOWS)) "gradlew.bat" else "gradlew",
215+
)
216+
.also { it.setExecutable(true) }
217+
.absolutePath
218+
args = gradleTasks
219+
println("exec: ${this.commandLine.joinToString(separator = " ")}")
220+
}.apply { println("ExecResult: $this") }
221+
}
222+
}
223+
}
224+
225+
tasks.register("ciEmulatorJobsMatrixSetup") {
226+
doLast {
227+
EmulatorJobsMatrix().createMatrixJsonFile(rootProject = rootProject)
228+
}
229+
}
230+
231+
tasks.register("ciSdkManagerLicenses") {
232+
doLast {
233+
val sdkDirPath = getAndroidSdkPath(rootDir = rootDir)
234+
getSdkmanagerFile(rootDir = rootDir)?.let { sdkmanagerFile ->
235+
val yesInputStream = object : InputStream() {
236+
private val yesString = "y\n"
237+
private var counter = 0
238+
override fun read(): Int = yesString[counter % 2].also { counter++ }.code
239+
}
240+
exec {
241+
executable = sdkmanagerFile.absolutePath
242+
args = listOf("--list", "--sdk_root=$sdkDirPath")
243+
println("exec: ${this.commandLine.joinToString(separator = " ")}")
244+
}.apply { println("ExecResult: $this") }
245+
exec {
246+
executable = sdkmanagerFile.absolutePath
247+
args = listOf("--licenses", "--sdk_root=$sdkDirPath")
248+
standardInput = yesInputStream
249+
println("exec: ${this.commandLine.joinToString(separator = " ")}")
250+
}.apply { println("ExecResult: $this") }
251+
}
252+
}
253+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
plugins {
2+
`kotlin-dsl`
3+
}
4+
5+
dependencies {
6+
compileOnly(libs.android.gradle.plugin)
7+
compileOnly(libs.gson)
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
dependencyResolutionManagement {
2+
repositories {
3+
google()
4+
mavenCentral()
5+
}
6+
versionCatalogs {
7+
create("libs") {
8+
from(files("../gradle/libs.versions.toml"))
9+
}
10+
}
11+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import com.google.gson.GsonBuilder
2+
import org.gradle.api.Project
3+
import java.io.File
4+
import java.util.Properties
5+
6+
class EmulatorJobsMatrix {
7+
8+
private val gson by lazy {
9+
GsonBuilder()
10+
.disableHtmlEscaping()
11+
.setPrettyPrinting()
12+
.create()
13+
}
14+
15+
fun createMatrixJsonFile(rootProject: Project) {
16+
val taskList = getTaskList(rootProject = rootProject).map { it.joinToString(separator = " ") }
17+
val matrix = mapOf("gradle_tasks" to taskList)
18+
val jsonText = gson.toJson(matrix)
19+
rootProject.layout.buildDirectory.asFile.get().also { buildDir ->
20+
buildDir.mkdirs()
21+
File(buildDir, "emulator_jobs_matrix.json").writeText(jsonText)
22+
}
23+
}
24+
25+
fun getTaskList(rootProject: Project): List<List<String>> =
26+
rootProject.subprojects.filter { subProject ->
27+
File(subProject.projectDir, "src${File.separator}androidInstrumentedTest").exists()
28+
}.map { subProject ->
29+
"${subProject.path}:gradleManagedDeviceDebugAndroidTest"
30+
}.map { taskName ->
31+
mutableListOf(taskName).also {
32+
it.add("--no-parallel")
33+
it.add("--max-workers=1")
34+
it.add("-Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect")
35+
it.add("-Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true")
36+
}.also {
37+
if (!true.toString().equals(other = System.getenv("CI"), ignoreCase = true)) {
38+
it.add("--enable-display")
39+
}
40+
}
41+
}
42+
}
43+
44+
fun getAndroidSdkPath(rootDir: File): String? =
45+
Properties().apply {
46+
val propertiesFile = File(rootDir, "local.properties")
47+
if (propertiesFile.exists()) {
48+
load(propertiesFile.inputStream())
49+
}
50+
}.getProperty("sdk.dir").let { propertiesSdkDirPath ->
51+
(propertiesSdkDirPath ?: System.getenv("ANDROID_HOME"))
52+
}
53+
54+
fun getSdkmanagerFile(rootDir: File): File? =
55+
getAndroidSdkPath(rootDir = rootDir)?.let { sdkDirPath ->
56+
println("sdkDirPath: $sdkDirPath")
57+
val files = File(sdkDirPath).walk().filter { file ->
58+
file.path.contains("cmdline-tools") && file.path.endsWith("sdkmanager")
59+
}
60+
files.forEach { println("walk: ${it.absolutePath}") }
61+
val sdkmanagerFile = files.firstOrNull()
62+
println("sdkmanagerFile: $sdkmanagerFile")
63+
sdkmanagerFile
64+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import com.android.build.api.dsl.ManagedVirtualDevice
2+
import com.android.build.api.dsl.TestOptions
3+
import org.gradle.kotlin.dsl.create
4+
5+
fun TestOptions.configureTestOptions() {
6+
unitTests {
7+
isIncludeAndroidResources = true
8+
all { test: org.gradle.api.tasks.testing.Test ->
9+
test.testLogging {
10+
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
11+
events = setOf(
12+
org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,
13+
org.gradle.api.tasks.testing.logging.TestLogEvent.SKIPPED,
14+
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
15+
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_OUT,
16+
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR,
17+
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED
18+
)
19+
}
20+
}
21+
}
22+
animationsDisabled = true
23+
emulatorSnapshots {
24+
enableForTestFailures = false
25+
}
26+
managedDevices.devices.create<ManagedVirtualDevice>("gradleManagedDevice") {
27+
device = "Pixel 2"
28+
apiLevel = 33
29+
systemImageSource = "google-atd"
30+
require64Bit = true
31+
}
32+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
plugins {
2+
}

firebase-analytics/build.gradle.kts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ plugins {
1010
id("com.android.library")
1111
kotlin("native.cocoapods")
1212
kotlin("multiplatform")
13+
id("testOptionsConvention")
1314
}
1415

1516
android {
@@ -29,11 +30,7 @@ android {
2930
targetCompatibility = JavaVersion.VERSION_11
3031
}
3132

32-
testOptions {
33-
unitTests.apply {
34-
isIncludeAndroidResources = true
35-
}
36-
}
33+
testOptions.configureTestOptions()
3734
packaging {
3835
resources.pickFirsts.add("META-INF/kotlinx-serialization-core.kotlin_module")
3936
resources.pickFirsts.add("META-INF/AL2.0")

firebase-app/build.gradle.kts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ plugins {
1111
id("com.android.library")
1212
kotlin("native.cocoapods")
1313
kotlin("multiplatform")
14+
id("testOptionsConvention")
1415
}
1516

1617
android {
@@ -30,11 +31,7 @@ android {
3031
targetCompatibility = JavaVersion.VERSION_11
3132
}
3233

33-
testOptions {
34-
unitTests.apply {
35-
isIncludeAndroidResources = true
36-
}
37-
}
34+
testOptions.configureTestOptions()
3835
packaging {
3936
resources.pickFirsts.add("META-INF/kotlinx-serialization-core.kotlin_module")
4037
resources.pickFirsts.add("META-INF/AL2.0")

firebase-auth/build.gradle.kts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ plugins {
1010
id("com.android.library")
1111
kotlin("multiplatform")
1212
kotlin("native.cocoapods")
13-
//id("com.quittle.android-emulator") version "0.2.0"
13+
id("testOptionsConvention")
1414
}
1515

1616
android {
@@ -30,11 +30,7 @@ android {
3030
targetCompatibility = JavaVersion.VERSION_11
3131
}
3232

33-
testOptions {
34-
unitTests.apply {
35-
isIncludeAndroidResources = true
36-
}
37-
}
33+
testOptions.configureTestOptions()
3834
packaging {
3935
resources.pickFirsts.add("META-INF/kotlinx-serialization-core.kotlin_module")
4036
resources.pickFirsts.add("META-INF/AL2.0")
@@ -45,19 +41,6 @@ android {
4541
}
4642
}
4743

48-
// Optional configuration
49-
//androidEmulator {
50-
// emulator {
51-
// name("givlive_emulator")
52-
// sdkVersion(28)
53-
// abi("x86_64")
54-
// includeGoogleApis(true) // Defaults to false
55-
//
56-
// }
57-
// headless(false)
58-
// logEmulatorOutput(false)
59-
//}
60-
6144
val supportIosTarget = project.property("skipIosTarget") != "true"
6245

6346
kotlin {

firebase-common-internal/build.gradle.kts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ plugins {
1010
id("com.android.library")
1111
kotlin("multiplatform")
1212
kotlin("plugin.serialization")
13+
id("testOptionsConvention")
1314
}
1415

1516
android {
@@ -28,11 +29,7 @@ android {
2829
targetCompatibility = JavaVersion.VERSION_11
2930
}
3031

31-
testOptions {
32-
unitTests.apply {
33-
isIncludeAndroidResources = true
34-
}
35-
}
32+
testOptions.configureTestOptions()
3633

3734
packaging {
3835
resources.pickFirsts.add("META-INF/kotlinx-serialization-core.kotlin_module")

firebase-common/build.gradle.kts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ plugins {
1010
id("com.android.library")
1111
kotlin("multiplatform")
1212
kotlin("plugin.serialization")
13+
id("testOptionsConvention")
1314
}
1415

1516
android {
@@ -28,11 +29,7 @@ android {
2829
targetCompatibility = JavaVersion.VERSION_11
2930
}
3031

31-
testOptions {
32-
unitTests.apply {
33-
isIncludeAndroidResources = true
34-
}
35-
}
32+
testOptions.configureTestOptions()
3633

3734
packaging {
3835
resources.pickFirsts.add("META-INF/kotlinx-serialization-core.kotlin_module")

0 commit comments

Comments
 (0)