Skip to content

Commit c0af00d

Browse files
committed
Generate androidTest file executable by Junit4
1 parent 51a8ded commit c0af00d

File tree

3 files changed

+186
-26
lines changed

3 files changed

+186
-26
lines changed

plugin/main/src/kotlinx/benchmark/gradle/AndroidMultiplatformTasks.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ fun Project.processAndroidCompilation(target: KotlinJvmAndroidCompilation) {
1818
it.dependsOn("bundle${target.name.capitalize(Locale.getDefault())}Aar")
1919
it.doLast {
2020
unpackAndProcessAar(target)
21+
generateAndroidExecFile()
2122
}
2223
}
2324
}

plugin/main/src/kotlinx/benchmark/gradle/AndroidTasks.kt

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package kotlinx.benchmark.gradle
22

3+
import com.squareup.kotlinpoet.*
34
import kotlinx.benchmark.gradle.internal.KotlinxBenchmarkPluginInternalApi
45
import org.gradle.api.*
56
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation
@@ -31,6 +32,7 @@ fun Project.unpackAndProcessAar(target: KotlinJvmAndroidCompilation) {
3132
println("Class annotations for ${target.name}:")
3233

3334
classAnnotationsDescriptors.forEach { descriptor ->
35+
println("Package: ${descriptor.packageName}")
3436
println("Class: ${descriptor.name}")
3537
println(" Visibility: ${descriptor.visibility}")
3638
descriptor.annotations.forEach { annotation ->
@@ -67,4 +69,144 @@ fun Project.unpackAndProcessAar(target: KotlinJvmAndroidCompilation) {
6769
} else {
6870
println("AAR file not found")
6971
}
72+
}
73+
74+
@KotlinxBenchmarkPluginInternalApi
75+
fun Project.generateAndroidExecFile() {
76+
// TODO: It should be android template project directory
77+
val androidTestDir = File("E:\\Android\\AndroidProjects\\kotlin-qualification-task\\composeApp\\src\\androidTest\\kotlin")
78+
if (!androidTestDir.exists()) {
79+
androidTestDir.mkdirs()
80+
}
81+
82+
// TODO: It should be android template project build.gradle.kts file
83+
val buildGradleFile = File("E:\\Android\\AndroidProjects\\kotlin-qualification-task\\composeApp\\build.gradle.kts")
84+
val dependencyPaths = listOf(
85+
"${project.projectDir}\\build\\outputs\\unpacked-aar\\release\\classes.jar".replace("\\", "\\\\") to null,
86+
"androidx.benchmark:benchmark-junit4" to "1.2.4",
87+
"androidx.test.ext:junit-ktx" to "1.2.1",
88+
"junit:junit" to "4.13.2"
89+
)
90+
91+
updateAndroidDependencies(buildGradleFile, dependencyPaths)
92+
93+
val jarFile = JarFile(File("${project.projectDir}\\build\\outputs\\unpacked-aar\\release\\classes.jar"))
94+
val annotationProcessor = AnnotationProcessor()
95+
annotationProcessor.processJarFile(jarFile)
96+
jarFile.close()
97+
98+
val classDescriptors = annotationProcessor.getClassDescriptors()
99+
val fileSpecBuilder = FileSpec.builder("generated", "GeneratedCode")
100+
.addImport("androidx.test.ext.junit.runners", "AndroidJUnit4")
101+
.addImport("org.junit", "Test")
102+
.addImport("org.junit.runner", "RunWith")
103+
val typeSpecBuilder = TypeSpec.classBuilder("GeneratedCode")
104+
.addAnnotation(
105+
AnnotationSpec.builder(ClassName("org.junit.runner", "RunWith"))
106+
.addMember("%T::class", ClassName("androidx.test.ext.junit.runners", "AndroidJUnit4"))
107+
.build()
108+
)
109+
110+
val uniquePropertyNames = mutableSetOf<String>()
111+
112+
classDescriptors.forEach { descriptor ->
113+
if (descriptor.visibility == Visibility.PUBLIC && !descriptor.isAbstract) {
114+
val simpleClassName = descriptor.name
115+
val fullyQualifiedName = "${descriptor.packageName}.$simpleClassName"
116+
117+
var propertyName = "${descriptor.packageName.replace('.', '_')}_$simpleClassName".decapitalize()
118+
if (!propertyName.endsWith("Benchmark")) {
119+
propertyName += "Benchmark"
120+
}
121+
122+
while (!uniquePropertyNames.add(propertyName)) {
123+
propertyName += "_"
124+
}
125+
126+
val propertySpec = PropertySpec.builder(propertyName, ClassName.bestGuess(fullyQualifiedName))
127+
.initializer("%T()", ClassName.bestGuess(fullyQualifiedName))
128+
.build()
129+
typeSpecBuilder.addProperty(propertySpec)
130+
131+
val methodSpecBuilder = FunSpec.builder("benchmarkExecutor_${descriptor.packageName.replace('.', '_')}_$simpleClassName")
132+
.addAnnotation(ClassName("org.junit", "Test"))
133+
.addCode(buildBenchmarkTestFunctionCode(propertyName, descriptor.methods))
134+
135+
typeSpecBuilder.addFunction(methodSpecBuilder.build())
136+
}
137+
}
138+
139+
fileSpecBuilder.addType(typeSpecBuilder.build())
140+
fileSpecBuilder.build().writeTo(androidTestDir)
141+
}
142+
143+
private fun buildBenchmarkTestFunctionCode(propertyName: String, methods: List<MethodAnnotationsDescriptor>): CodeBlock {
144+
val builder = CodeBlock.builder()
145+
146+
val setUpMethod = methods.find { it.name == "setUp" }
147+
val tearDownMethod = methods.find { it.name == "tearDown" }
148+
val benchmarkMethods = methods.filter { it.name != "setUp" && it.name != "tearDown" && it.visibility == Visibility.PUBLIC }
149+
150+
setUpMethod?.let {
151+
builder.addStatement("$propertyName.${it.name}()")
152+
}
153+
154+
benchmarkMethods.forEach { method ->
155+
builder.addStatement("$propertyName.${method.name}()")
156+
}
157+
158+
tearDownMethod?.let {
159+
builder.addStatement("$propertyName.${it.name}()")
160+
}
161+
162+
return builder.build()
163+
}
164+
165+
private fun updateAndroidDependencies(buildGradleFile: File, dependencies: List<Pair<String, String?>>) {
166+
if (buildGradleFile.exists()) {
167+
val buildGradleContent = buildGradleFile.readText()
168+
169+
// Check if the android block exists
170+
if (buildGradleContent.contains("android {")) {
171+
// Check if the dependencies block inside android contains the dependencyString
172+
val androidBlockStart = buildGradleContent.indexOf("android {")
173+
val androidBlockEnd = buildGradleContent.lastIndexOf("}") + 1
174+
val androidBlockContent = buildGradleContent.substring(androidBlockStart, androidBlockEnd)
175+
176+
val newDependencies = dependencies.filterNot { (dependency, version) ->
177+
val dependencyString = version?.let { """$dependency:$version""" } ?: dependency
178+
androidBlockContent.contains(dependencyString)
179+
}
180+
if (newDependencies.isNotEmpty()) {
181+
// Add the dependencies inside the android { dependencies { ... } } block
182+
val updatedAndroidBlockContent = if (androidBlockContent.contains("dependencies {")) {
183+
val dependenciesBlockStart = androidBlockContent.indexOf("dependencies {")
184+
val dependenciesBlockEnd = androidBlockContent.indexOf("}", dependenciesBlockStart) + 1
185+
val dependenciesBlockContent =
186+
androidBlockContent.substring(dependenciesBlockStart, dependenciesBlockEnd)
187+
188+
val newDependenciesString = newDependencies.joinToString("\n ") { (dependency, version) ->
189+
version?.let { """androidTestImplementation("$dependency:$version")""" } ?: """androidTestImplementation(files("$dependency"))"""
190+
}
191+
androidBlockContent.replace(
192+
dependenciesBlockContent,
193+
dependenciesBlockContent.replace(
194+
"dependencies {",
195+
"dependencies {\n $newDependenciesString"
196+
)
197+
)
198+
} else {
199+
val newDependenciesString = newDependencies.joinToString("\n ") { (dependency, version) ->
200+
version?.let { """androidTestImplementation("$dependency:$version")""" } ?: """androidTestImplementation(files("$dependency"))"""
201+
}
202+
androidBlockContent.replace("{", "{\n dependencies {\n $newDependenciesString\n }\n")
203+
}
204+
205+
// Replace the old android block with the updated one
206+
val updatedBuildGradleContent =
207+
buildGradleContent.replace(androidBlockContent, updatedAndroidBlockContent)
208+
buildGradleFile.writeText(updatedBuildGradleContent)
209+
}
210+
}
211+
}
70212
}

plugin/main/src/kotlinx/benchmark/gradle/AnnotationProcessor.kt

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ data class AnnotationData(
1212
data class ClassAnnotationsDescriptor(
1313
val packageName: String,
1414
val name: String,
15-
val visibility: String,
15+
val visibility: Visibility,
1616
val isAbstract: Boolean,
1717
val annotations: List<AnnotationData>,
1818
val methods: List<MethodAnnotationsDescriptor>,
@@ -21,16 +21,20 @@ data class ClassAnnotationsDescriptor(
2121

2222
data class MethodAnnotationsDescriptor(
2323
val name: String,
24-
val visibility: String,
24+
val visibility: Visibility,
2525
val annotations: List<AnnotationData>
2626
)
2727

2828
data class FieldAnnotationsDescriptor(
2929
val name: String,
30-
val visibility: String,
30+
val visibility: Visibility,
3131
val annotations: List<AnnotationData>
3232
)
3333

34+
enum class Visibility {
35+
PUBLIC, PROTECTED, PRIVATE, PACKAGE_PRIVATE
36+
}
37+
3438
class AnnotationProcessor {
3539

3640
private val classAnnotationsDescriptors = mutableListOf<ClassAnnotationsDescriptor>()
@@ -58,13 +62,15 @@ class AnnotationProcessor {
5862
?.map { parseAnnotation(it) }
5963
?: emptyList()
6064

61-
val methodDescriptors = classNode.methods.map { methodNode ->
62-
val methodAnnotations = methodNode.visibleAnnotations
63-
?.filter { it.desc != "Lkotlin/Metadata;" }
64-
?.map { parseAnnotation(it) }
65-
?: emptyList()
66-
MethodAnnotationsDescriptor(methodNode.name, getVisibility(methodNode.access), methodAnnotations)
67-
}
65+
val methodDescriptors = classNode.methods
66+
.filterNot { it.name.startsWith("get") || it.name.startsWith("set") || it.name == "<init>" }
67+
.map { methodNode ->
68+
val methodAnnotations = methodNode.visibleAnnotations
69+
?.filter { it.desc != "Lkotlin/Metadata;" }
70+
?.map { parseAnnotation(it) }
71+
?: emptyList()
72+
MethodAnnotationsDescriptor(methodNode.name, getVisibility(methodNode.access), methodAnnotations)
73+
}
6874

6975
val fieldDescriptors = classNode.fields.map { fieldNode ->
7076
val fieldAnnotations = fieldNode.visibleAnnotations
@@ -75,9 +81,10 @@ class AnnotationProcessor {
7581
}
7682

7783
val packageName = classNode.name.substringBeforeLast('/', "").replace('/', '.')
84+
val className = classNode.name.substringAfterLast('/')
7885
val classDescriptor = ClassAnnotationsDescriptor(
7986
packageName,
80-
classNode.name.replace('/', '.').substringAfterLast('/'),
87+
className,
8188
getVisibility(classNode.access),
8289
isAbstract(classNode.access),
8390
classAnnotations,
@@ -136,28 +143,36 @@ class AnnotationProcessor {
136143
return sb.toString()
137144
}
138145

139-
private fun getVisibility(access: Int): String {
146+
private fun getVisibility(access: Int): Visibility {
140147
return when {
141-
(access and Opcodes.ACC_PUBLIC) != 0 -> "public"
142-
(access and Opcodes.ACC_PROTECTED) != 0 -> "protected"
143-
(access and Opcodes.ACC_PRIVATE) != 0 -> "private"
144-
else -> "package-private"
148+
(access and Opcodes.ACC_PUBLIC) != 0 -> Visibility.PUBLIC
149+
(access and Opcodes.ACC_PROTECTED) != 0 -> Visibility.PROTECTED
150+
(access and Opcodes.ACC_PRIVATE) != 0 -> Visibility.PRIVATE
151+
else -> Visibility.PACKAGE_PRIVATE
145152
}
146153
}
147154

148-
private fun getFieldVisibility(classNode: ClassNode, fieldNode: FieldNode): String {
155+
private fun getFieldVisibility(classNode: ClassNode, fieldNode: FieldNode): Visibility {
149156
val getterName = "get${fieldNode.name.capitalize()}"
150157
val setterName = "set${fieldNode.name.capitalize()}"
151158

152159
val getterMethod = classNode.methods.find { it.name == getterName }
153160
val setterMethod = classNode.methods.find { it.name == setterName }
154161

155-
return if (getterMethod != null && setterMethod != null) {
156-
val getterVisibility = getVisibility(getterMethod.access)
157-
val setterVisibility = getVisibility(setterMethod.access)
158-
if (getterVisibility == setterVisibility) getterVisibility else "package-private"
159-
} else {
160-
getVisibility(fieldNode.access)
162+
val getterVisibility = getterMethod?.let { getVisibility(it.access) }
163+
val setterVisibility = setterMethod?.let { getVisibility(it.access) }
164+
165+
return when {
166+
getterVisibility != null && setterVisibility != null -> {
167+
if (getterVisibility == setterVisibility) {
168+
getterVisibility
169+
} else {
170+
getVisibility(fieldNode.access)
171+
}
172+
}
173+
getterVisibility != null -> getterVisibility
174+
setterVisibility != null -> setterVisibility
175+
else -> getVisibility(fieldNode.access)
161176
}
162177
}
163178

@@ -169,9 +184,11 @@ class AnnotationProcessor {
169184
return classAnnotationsDescriptors
170185
}
171186

172-
fun getPublicClassNames(): List<Pair<String, String>> {
173-
return classAnnotationsDescriptors.filter { it.visibility == "public" && !it.isAbstract }
174-
.map { it.packageName to it.name }
187+
fun getMethodDescriptors(className: String): List<MethodAnnotationsDescriptor> {
188+
return classAnnotationsDescriptors.find { it.name == className }?.methods ?: emptyList()
175189
}
176190

191+
fun getFieldDescriptors(className: String): List<FieldAnnotationsDescriptor> {
192+
return classAnnotationsDescriptors.find { it.name == className }?.fields ?: emptyList()
193+
}
177194
}

0 commit comments

Comments
 (0)