Skip to content

Commit

Permalink
Run android emulator on ubuntu build agent. gradle-managed-devices
Browse files Browse the repository at this point in the history
…is used instead of reactivecircus/android-emulator-runner GitHub action task (#524)
  • Loading branch information
siarhei-luskanau authored Jun 19, 2024
1 parent 096f468 commit 90fba35
Show file tree
Hide file tree
Showing 24 changed files with 228 additions and 132 deletions.
61 changes: 28 additions & 33 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,46 +8,41 @@ on:
branches: [ master ]

jobs:
jobEmulatorMatrixSetup:
runs-on: ubuntu-latest
outputs:
emulator_jobs_matrix: ${{ steps.dataStep.outputs.emulator_jobs_matrix }}
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
cache: gradle
- name: Prepare the matrix JSON
run: ./gradlew ciEmulatorJobsMatrixSetup
- id: dataStep
run: echo "emulator_jobs_matrix=$(jq -c . < ./build/emulator_jobs_matrix.json)" >> $GITHUB_OUTPUT
build-android:
runs-on: macos-13
needs: jobEmulatorMatrixSetup
runs-on: ubuntu-latest
strategy:
matrix:
api-level: [ 34 ]
fail-fast: false
matrix: ${{ fromJson(needs.jobEmulatorMatrixSetup.outputs.emulator_jobs_matrix) }}
steps:
- uses: actions/checkout@v3
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Setup test environment
uses: ./.github/actions/setup_test_action
- name: AVD cache
uses: actions/cache@v3
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-${{ matrix.api-level }}-${{ runner.os }}-${{ runner.arch }}
- name: create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86_64
target: google_apis
avd-name: pixel6_API${{ matrix.api-level }}
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: false
script: echo "Generated AVD snapshot for caching."
- name: Apply Android licenses
run: ./gradlew ciSdkManagerLicenses
- name: Run Android Instrumented Tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86_64
target: google_apis
avd-name: pixel6_API${{ matrix.api-level }}
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: ./gradlew connectedAndroidTest
run: ./gradlew ${{ matrix.gradle_tasks }}
- name: Upload Android test artifact
uses: actions/upload-artifact@v3
if: failure()
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Project exclude paths
/.gradle/
/**/.gradle/
/**/build/
/.idea/
local.properties
Expand Down
50 changes: 50 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent
import java.io.InputStream

plugins {
alias(libs.plugins.android.application) apply false
Expand All @@ -9,6 +11,7 @@ plugins {
alias(libs.plugins.test.logger.plugin) apply false
alias(libs.plugins.ben.manes.versions) apply false
id("base")
id("testOptionsConvention")
}

val compileSdkVersion by extra(34)
Expand Down Expand Up @@ -201,3 +204,50 @@ tasks.withType<com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
reportfileName = "dependency-updates"
}
// check for latest dependencies - ./gradlew dependencyUpdates -Drevision=release

tasks.register("devRunEmulatorTests") {
doLast {
EmulatorJobsMatrix().getTaskList(rootProject = rootProject).forEach { gradleTasks ->
exec {
executable = File(
project.rootDir,
if (Os.isFamily(Os.FAMILY_WINDOWS)) "gradlew.bat" else "gradlew",
)
.also { it.setExecutable(true) }
.absolutePath
args = gradleTasks
println("exec: ${this.commandLine.joinToString(separator = " ")}")
}.apply { println("ExecResult: $this") }
}
}
}

tasks.register("ciEmulatorJobsMatrixSetup") {
doLast {
EmulatorJobsMatrix().createMatrixJsonFile(rootProject = rootProject)
}
}

tasks.register("ciSdkManagerLicenses") {
doLast {
val sdkDirPath = getAndroidSdkPath(rootDir = rootDir)
getSdkmanagerFile(rootDir = rootDir)?.let { sdkmanagerFile ->
val yesInputStream = object : InputStream() {
private val yesString = "y\n"
private var counter = 0
override fun read(): Int = yesString[counter % 2].also { counter++ }.code
}
exec {
executable = sdkmanagerFile.absolutePath
args = listOf("--list", "--sdk_root=$sdkDirPath")
println("exec: ${this.commandLine.joinToString(separator = " ")}")
}.apply { println("ExecResult: $this") }
exec {
executable = sdkmanagerFile.absolutePath
args = listOf("--licenses", "--sdk_root=$sdkDirPath")
standardInput = yesInputStream
println("exec: ${this.commandLine.joinToString(separator = " ")}")
}.apply { println("ExecResult: $this") }
}
}
}
8 changes: 8 additions & 0 deletions convention-plugin-test-option/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
plugins {
`kotlin-dsl`
}

dependencies {
compileOnly(libs.android.gradle.plugin)
compileOnly(libs.gson)
}
11 changes: 11 additions & 0 deletions convention-plugin-test-option/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import com.google.gson.GsonBuilder
import org.gradle.api.Project
import java.io.File
import java.util.Properties

class EmulatorJobsMatrix {

private val gson by lazy {
GsonBuilder()
.disableHtmlEscaping()
.setPrettyPrinting()
.create()
}

fun createMatrixJsonFile(rootProject: Project) {
val taskList = getTaskList(rootProject = rootProject).map { it.joinToString(separator = " ") }
val matrix = mapOf("gradle_tasks" to taskList)
val jsonText = gson.toJson(matrix)
rootProject.layout.buildDirectory.asFile.get().also { buildDir ->
buildDir.mkdirs()
File(buildDir, "emulator_jobs_matrix.json").writeText(jsonText)
}
}

fun getTaskList(rootProject: Project): List<List<String>> =
rootProject.subprojects.filter { subProject ->
File(subProject.projectDir, "src${File.separator}androidInstrumentedTest").exists()
}.map { subProject ->
"${subProject.path}:gradleManagedDeviceDebugAndroidTest"
}.map { taskName ->
mutableListOf(taskName).also {
it.add("--no-parallel")
it.add("--max-workers=1")
it.add("-Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect")
it.add("-Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true")
}.also {
if (!true.toString().equals(other = System.getenv("CI"), ignoreCase = true)) {
it.add("--enable-display")
}
}
}
}

fun getAndroidSdkPath(rootDir: File): String? =
Properties().apply {
val propertiesFile = File(rootDir, "local.properties")
if (propertiesFile.exists()) {
load(propertiesFile.inputStream())
}
}.getProperty("sdk.dir").let { propertiesSdkDirPath ->
(propertiesSdkDirPath ?: System.getenv("ANDROID_HOME"))
}

fun getSdkmanagerFile(rootDir: File): File? =
getAndroidSdkPath(rootDir = rootDir)?.let { sdkDirPath ->
println("sdkDirPath: $sdkDirPath")
val files = File(sdkDirPath).walk().filter { file ->
file.path.contains("cmdline-tools") && file.path.endsWith("sdkmanager")
}
files.forEach { println("walk: ${it.absolutePath}") }
val sdkmanagerFile = files.firstOrNull()
println("sdkmanagerFile: $sdkmanagerFile")
sdkmanagerFile
}
32 changes: 32 additions & 0 deletions convention-plugin-test-option/src/main/kotlin/TestOptionsConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import com.android.build.api.dsl.ManagedVirtualDevice
import com.android.build.api.dsl.TestOptions
import org.gradle.kotlin.dsl.create

fun TestOptions.configureTestOptions() {
unitTests {
isIncludeAndroidResources = true
all { test: org.gradle.api.tasks.testing.Test ->
test.testLogging {
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
events = setOf(
org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,
org.gradle.api.tasks.testing.logging.TestLogEvent.SKIPPED,
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_OUT,
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR,
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED
)
}
}
}
animationsDisabled = true
emulatorSnapshots {
enableForTestFailures = false
}
managedDevices.devices.create<ManagedVirtualDevice>("gradleManagedDevice") {
device = "Pixel 2"
apiLevel = 33
systemImageSource = "google-atd"
require64Bit = true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
plugins {
}
7 changes: 2 additions & 5 deletions firebase-analytics/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ plugins {
id("com.android.library")
kotlin("native.cocoapods")
kotlin("multiplatform")
id("testOptionsConvention")
}

android {
Expand All @@ -29,11 +30,7 @@ android {
targetCompatibility = JavaVersion.VERSION_11
}

testOptions {
unitTests.apply {
isIncludeAndroidResources = true
}
}
testOptions.configureTestOptions()
packaging {
resources.pickFirsts.add("META-INF/kotlinx-serialization-core.kotlin_module")
resources.pickFirsts.add("META-INF/AL2.0")
Expand Down
7 changes: 2 additions & 5 deletions firebase-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ plugins {
id("com.android.library")
kotlin("native.cocoapods")
kotlin("multiplatform")
id("testOptionsConvention")
}

android {
Expand All @@ -30,11 +31,7 @@ android {
targetCompatibility = JavaVersion.VERSION_11
}

testOptions {
unitTests.apply {
isIncludeAndroidResources = true
}
}
testOptions.configureTestOptions()
packaging {
resources.pickFirsts.add("META-INF/kotlinx-serialization-core.kotlin_module")
resources.pickFirsts.add("META-INF/AL2.0")
Expand Down
21 changes: 2 additions & 19 deletions firebase-auth/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ plugins {
id("com.android.library")
kotlin("multiplatform")
kotlin("native.cocoapods")
//id("com.quittle.android-emulator") version "0.2.0"
id("testOptionsConvention")
}

android {
Expand All @@ -30,11 +30,7 @@ android {
targetCompatibility = JavaVersion.VERSION_11
}

testOptions {
unitTests.apply {
isIncludeAndroidResources = true
}
}
testOptions.configureTestOptions()
packaging {
resources.pickFirsts.add("META-INF/kotlinx-serialization-core.kotlin_module")
resources.pickFirsts.add("META-INF/AL2.0")
Expand All @@ -45,19 +41,6 @@ android {
}
}

// Optional configuration
//androidEmulator {
// emulator {
// name("givlive_emulator")
// sdkVersion(28)
// abi("x86_64")
// includeGoogleApis(true) // Defaults to false
//
// }
// headless(false)
// logEmulatorOutput(false)
//}

val supportIosTarget = project.property("skipIosTarget") != "true"

kotlin {
Expand Down
7 changes: 2 additions & 5 deletions firebase-common-internal/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ plugins {
id("com.android.library")
kotlin("multiplatform")
kotlin("plugin.serialization")
id("testOptionsConvention")
}

android {
Expand All @@ -28,11 +29,7 @@ android {
targetCompatibility = JavaVersion.VERSION_11
}

testOptions {
unitTests.apply {
isIncludeAndroidResources = true
}
}
testOptions.configureTestOptions()

packaging {
resources.pickFirsts.add("META-INF/kotlinx-serialization-core.kotlin_module")
Expand Down
7 changes: 2 additions & 5 deletions firebase-common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ plugins {
id("com.android.library")
kotlin("multiplatform")
kotlin("plugin.serialization")
id("testOptionsConvention")
}

android {
Expand All @@ -28,11 +29,7 @@ android {
targetCompatibility = JavaVersion.VERSION_11
}

testOptions {
unitTests.apply {
isIncludeAndroidResources = true
}
}
testOptions.configureTestOptions()

packaging {
resources.pickFirsts.add("META-INF/kotlinx-serialization-core.kotlin_module")
Expand Down
Loading

0 comments on commit 90fba35

Please sign in to comment.