Skip to content

Commit 90ee166

Browse files
committed
Add Android Native emulator tests
1 parent c4ebf69 commit 90ee166

12 files changed

Lines changed: 289 additions & 2 deletions

File tree

.github/workflows/CI.yml

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
uses: actions/setup-java@v4
2424
with:
2525
distribution: 'zulu'
26-
java-version: 11
26+
java-version: 17
2727

2828
- name: Check API Compatibility
2929
if: matrix.os == 'macos-latest'
@@ -40,7 +40,7 @@ jobs:
4040
if: matrix.os == 'ubuntu-latest'
4141
run: >
4242
./gradlew check --stacktrace
43-
-PKMP_TARGETS="ANDROID,ANDROID_ARM32,ANDROID_ARM64,ANDROID_X64,ANDROID_X86,JVM,JS,LINUX_ARM64,LINUX_X64,WASM_JS,WASM_WASI"
43+
-PKMP_TARGETS="ANDROID_ARM32,ANDROID_ARM64,ANDROID_X64,ANDROID_X86,JVM,JS,LINUX_ARM64,LINUX_X64,WASM_JS,WASM_WASI"
4444
4545
- name: Run Windows Tests
4646
if: matrix.os == 'windows-latest'
@@ -85,3 +85,74 @@ jobs:
8585
with:
8686
name: benchmark-report-${{ matrix.os }}
8787
path: '**/build/reports/benchmarks/**'
88+
89+
android-check:
90+
strategy:
91+
fail-fast: false
92+
matrix:
93+
include:
94+
- api-level: 21
95+
arch: x86_64
96+
- api-level: 22
97+
arch: x86_64
98+
- api-level: 23
99+
arch: x86_64
100+
- api-level: 24
101+
arch: x86_64
102+
- api-level: 25
103+
arch: x86
104+
- api-level: 26
105+
arch: x86_64
106+
- api-level: 27
107+
arch: x86_64
108+
- api-level: 28
109+
arch: x86_64
110+
- api-level: 29
111+
arch: x86
112+
- api-level: 30
113+
arch: x86_64
114+
- api-level: 31
115+
arch: x86_64
116+
- api-level: 32
117+
arch: x86_64
118+
- api-level: 33
119+
arch: x86_64
120+
- api-level: 34
121+
arch: x86_64
122+
- api-level: 35
123+
arch: x86_64
124+
runs-on: ubuntu-latest
125+
steps:
126+
- name: Checkout Repository
127+
uses: actions/checkout@v4
128+
129+
- name: Enable KVM
130+
run: |
131+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
132+
sudo udevadm control --reload-rules
133+
sudo udevadm trigger --name-match=kvm
134+
135+
- name: Validate Gradle Wrapper
136+
uses: gradle/actions/wrapper-validation@v3
137+
138+
- name: Setup JDK
139+
uses: actions/setup-java@v4
140+
with:
141+
distribution: 'zulu'
142+
java-version: 17
143+
144+
- name: Run Android Instrumented Tests
145+
uses: reactivecircus/android-emulator-runner@v2
146+
with:
147+
emulator-boot-timeout: 300 # 5 minutes
148+
api-level: ${{ matrix.api-level }}
149+
arch: ${{ matrix.arch }}
150+
script: ./gradlew connectedCheck -PKMP_TARGETS="ANDROID,ANDROID_ARM32,ANDROID_ARM64,ANDROID_X64,ANDROID_X86,JVM"
151+
152+
- name: Upload Test Reports
153+
uses: actions/upload-artifact@v4
154+
if: ${{ always() }}
155+
with:
156+
name: test-report-android-${{ matrix.api-level }}-${{ matrix.arch }}
157+
path: '**/build/reports/androidTests/**'
158+
retention-days: 1

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin
1818
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension
1919

2020
plugins {
21+
alias(libs.plugins.android.library) apply(false)
2122
alias(libs.plugins.benchmark) apply(false)
2223
alias(libs.plugins.binary.compat)
2324
alias(libs.plugins.cklib) apply(false)

gradle.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
22
org.gradle.parallel=true
33
org.gradle.caching=true
44

5+
android.useAndroidX=true
6+
android.enableJetifier=true
7+
58
kotlin.code.style=official
69
kotlin.mpp.applyDefaultHierarchyTemplate=false
710
kotlin.mpp.enableCInteropCommonization=true

gradle/libs.versions.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
[versions]
2+
androidx-test-core = "1.6.1"
3+
androidx-test-runner = "1.6.2"
4+
5+
gradle-android = "8.7.3"
26
gradle-benchmark = "0.4.13"
37
gradle-binary-compat = "0.17.0"
48
gradle-cklib = "0.3.3"
@@ -7,6 +11,7 @@ gradle-kmp-configuration = "0.4.0"
711
gradle-kotlin = "2.1.10"
812
gradle-publish-maven = "0.30.0"
913

14+
kmp-process = "0.2.1"
1015
kotlincrypto-error = "0.3.0"
1116

1217
[libraries]
@@ -18,9 +23,13 @@ gradle-publish-maven = { module = "com.vanniktech:gradle-maven-publish-pl
1823
kotlincrypto-error = { module = "org.kotlincrypto:error", version.ref = "kotlincrypto-error" }
1924

2025
# tests & tooling
26+
androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" }
27+
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" }
2128
benchmark-runtime = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "gradle-benchmark" }
29+
kmp-process = { module = "io.matthewnelson.kmp-process:process", version.ref = "kmp-process" }
2230

2331
[plugins]
32+
android-library = { id = "com.android.library", version.ref = "gradle-android" }
2433
benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "gradle-benchmark" }
2534
binary-compat = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "gradle-binary-compat" }
2635
cklib = { id = "co.touchlab.cklib", version.ref = "gradle-cklib" }

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ if (CHECK_PUBLICATION != null) {
2424

2525
include(":benchmarks")
2626
include(":sample")
27+
include(":test-android")
2728
}

test-android/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
build/
2+
src/androidInstrumentedTest/jniLibs/

test-android/api/test-android.api

Whitespace-only changes.

test-android/api/test-android.klib.api

Whitespace-only changes.

test-android/build.gradle.kts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright (c) 2025 KotlinCrypto
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
**/
16+
import com.android.build.gradle.tasks.MergeSourceSetFolders
17+
18+
plugins {
19+
id("configuration")
20+
}
21+
22+
repositories { google() }
23+
24+
kmpConfiguration {
25+
configure {
26+
val jniLibsDir = projectDir
27+
.resolve("src")
28+
.resolve("androidInstrumentedTest")
29+
.resolve("jniLibs")
30+
31+
project.tasks.all {
32+
if (name != "clean") return@all
33+
doLast { jniLibsDir.deleteRecursively() }
34+
}
35+
36+
androidLibrary {
37+
android {
38+
buildToolsVersion = "34.0.0"
39+
compileSdk = 34
40+
namespace = "org.kotlincrypto.random.test.android"
41+
42+
defaultConfig {
43+
minSdk = 21
44+
45+
testInstrumentationRunnerArguments["disableAnalytics"] = true.toString()
46+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
47+
}
48+
49+
packaging.jniLibs.useLegacyPackaging = true
50+
51+
sourceSets["androidTest"].jniLibs.srcDir(jniLibsDir)
52+
}
53+
54+
sourceSetTestInstrumented {
55+
dependencies {
56+
implementation(libs.androidx.test.core)
57+
implementation(libs.androidx.test.runner)
58+
implementation(libs.kmp.process)
59+
}
60+
}
61+
62+
kotlinJvmTarget = JavaVersion.VERSION_1_8
63+
compileSourceCompatibility = JavaVersion.VERSION_1_8
64+
compileTargetCompatibility = JavaVersion.VERSION_1_8
65+
}
66+
67+
common {
68+
sourceSetTest {
69+
dependencies {
70+
implementation(kotlin("test"))
71+
}
72+
}
73+
}
74+
75+
kotlin {
76+
if (!project.plugins.hasPlugin("com.android.base")) return@kotlin
77+
78+
try {
79+
project.evaluationDependsOn(":library:crypto-rand")
80+
} catch (_: Throwable) {}
81+
82+
val cryptoRandProject = project(":library:crypto-rand")
83+
84+
val cryptoRandBuildDir = cryptoRandProject
85+
.layout
86+
.buildDirectory
87+
.asFile.get()
88+
89+
val nativeTestBinariesTasks = listOf(
90+
"Arm32" to "armeabi-v7a",
91+
"Arm64" to "arm64-v8a",
92+
"X64" to "x86_64",
93+
"X86" to "x86",
94+
).mapNotNull { (arch, abi) ->
95+
val nativeTestBinariesTask = cryptoRandProject
96+
.tasks
97+
.findByName("androidNative${arch}TestBinaries")
98+
?: return@mapNotNull null
99+
100+
val abiDir = jniLibsDir.resolve(abi)
101+
if (!abiDir.exists() && !abiDir.mkdirs()) throw RuntimeException("mkdirs[$abiDir]")
102+
103+
val testExecutable = cryptoRandBuildDir
104+
.resolve("bin")
105+
.resolve("androidNative$arch")
106+
.resolve("debugTest")
107+
.resolve("test.kexe")
108+
109+
nativeTestBinariesTask.doLast {
110+
testExecutable.copyTo(abiDir.resolve("libTestExec.so"), overwrite = true)
111+
}
112+
113+
nativeTestBinariesTask
114+
}
115+
116+
project.tasks.withType(MergeSourceSetFolders::class.java) {
117+
if (name != "mergeDebugAndroidTestJniLibFolders") return@withType
118+
nativeTestBinariesTasks.forEach { task -> this.dependsOn(task) }
119+
}
120+
}
121+
}
122+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright (c) 2025 KotlinCrypto
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
**/
16+
package org.kotlincrypto.random.test.android
17+
18+
import android.app.Application
19+
import androidx.test.core.app.ApplicationProvider
20+
import io.matthewnelson.kmp.file.toFile
21+
import io.matthewnelson.kmp.process.Process
22+
import kotlin.test.Test
23+
import kotlin.test.assertEquals
24+
25+
class AndroidNativeTest {
26+
27+
private val ctx = ApplicationProvider.getApplicationContext<Application>().applicationContext
28+
private val nativeLibraryDir = ctx.applicationInfo.nativeLibraryDir.toFile().absoluteFile
29+
30+
@Test
31+
fun givenAndroidNative_whenExecuteTestBinary_thenIsSuccessful() {
32+
val out = Process.Builder(executable = nativeLibraryDir.resolve("libTestExec.so"))
33+
.output {
34+
timeoutMillis = 5_000
35+
maxBuffer = Int.MAX_VALUE / 2
36+
}
37+
38+
assertEquals(0, out.processInfo.exitCode, out.stdout)
39+
println(out.stdout)
40+
println(out.stderr)
41+
}
42+
}

0 commit comments

Comments
 (0)