From d605cb21be3873053d0cb01fd69d5312e7f8b1b8 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Sun, 26 May 2024 22:19:52 +0530 Subject: [PATCH] fix(lsp/java): add benchmarking module and more unit tests for JavaJarModelBuilder --- .idea/compiler.xml | 1 + build.gradle.kts | 1 + common/build.gradle.kts | 21 +- gradle/libs.versions.toml | 4 + .../lsp/java/indexing/JavaClassCRUDTest.kt | 34 +-- .../lsp/java/indexing/JavaIndexerTest.kt | 32 +- .../lsp/java/indexing/ModelBuilder.kt | 52 ++++ .../lsp/java/indexing/SharedModelCRUDTest.kt | 278 ++++++++++++++++++ .../androidide/lsp/java/indexing/crudExts.kt | 72 +++++ .../lsp/java/indexing/JavaIndexer.kt | 2 +- ...ModelBuilder.kt => JavaJarModelBuilder.kt} | 4 +- .../lsp/java/indexing/models/ApiInfo.kt | 60 +++- .../lsp/java/indexing/models/ICloneable.kt | 27 ++ .../lsp/java/indexing/models/JavaConstant.kt | 39 ++- .../lsp/java/indexing/models/JavaType.kt | 6 +- .../models/annotationElementValues.kt | 120 +++++++- .../itsaky/androidide/lsp/java/JavaLSPTest.kt | 2 +- .../java/indexing/AndroidJarModelingRule.kt | 4 +- .../lsp/java/indexing/JavaIndexWorkerText.kt | 2 +- .../itsaky/androidide/lsp/xml/XMLLSPTest.kt | 2 +- settings.gradle.kts | 1 + .../db/utils/realmDictionaryExts.kt | 60 ++++ testing/android/build.gradle.kts | 13 +- .../android/rules/AbstractAndroidTestRule.kt | 37 +++ .../testing/android/rules/RealmDBTestRule.kt | 61 ++++ .../testing/android/util/NoOpStatement.kt | 30 ++ testing/benchmark/.gitignore | 1 + .../benchmark/benchmark-proguard-rules.pro | 37 +++ testing/benchmark/build.gradle.kts | 70 +++++ .../src/androidTest/AndroidManifest.xml | 18 ++ .../lsp/java/JavaIndexingWorkerBenchmark.kt | 53 ++-- testing/common/build.gradle.kts | 3 +- testing/lsp/build.gradle.kts | 6 +- .../lsp/{api => testing}/LSPTest.kt | 3 +- .../util/ReflectiveLogListener.kt | 2 +- 35 files changed, 1069 insertions(+), 89 deletions(-) create mode 100644 lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/ModelBuilder.kt create mode 100644 lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/SharedModelCRUDTest.kt create mode 100644 lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/crudExts.kt rename lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/{JavaIndexModelBuilder.kt => JavaJarModelBuilder.kt} (99%) create mode 100644 lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/ICloneable.kt create mode 100644 subprojects/database/src/main/java/com/itsaky/androidide/db/utils/realmDictionaryExts.kt create mode 100644 testing/android/src/main/java/com/itsaky/androidide/testing/android/rules/AbstractAndroidTestRule.kt create mode 100644 testing/android/src/main/java/com/itsaky/androidide/testing/android/rules/RealmDBTestRule.kt create mode 100644 testing/android/src/main/java/com/itsaky/androidide/testing/android/util/NoOpStatement.kt create mode 100644 testing/benchmark/.gitignore create mode 100644 testing/benchmark/benchmark-proguard-rules.pro create mode 100644 testing/benchmark/build.gradle.kts create mode 100644 testing/benchmark/src/androidTest/AndroidManifest.xml rename lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/IndexingHelper.kt => testing/benchmark/src/androidTest/java/com/itsaky/androidide/benchmark/lsp/java/JavaIndexingWorkerBenchmark.kt (50%) rename testing/lsp/src/main/java/com/itsaky/androidide/lsp/{api => testing}/LSPTest.kt (98%) rename testing/lsp/src/main/java/com/itsaky/androidide/lsp/{api => testing}/util/ReflectiveLogListener.kt (93%) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 972818729..f279c3492 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -113,6 +113,7 @@ + diff --git a/build.gradle.kts b/build.gradle.kts index 731884cec..e4861ea61 100755 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -35,6 +35,7 @@ plugins { alias(libs.plugins.maven.publish) apply false alias(libs.plugins.gradle.publish) apply false alias(libs.plugins.protobuf) apply false + alias(libs.plugins.benchmark) apply false } buildscript { diff --git a/common/build.gradle.kts b/common/build.gradle.kts index d8146a0df..82dbd987d 100755 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,8 +1,25 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + import com.itsaky.androidide.build.config.BuildConfig plugins { - id("com.android.library") - id("kotlin-android") + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) } android { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 492109f44..10e5ba5a1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,6 +19,8 @@ markwon = "4.6.2" maven-publish-plugin = "0.27.0" logback = "1.5.3" realm = "10.18.0" +benchmark = "1.2.4" +benchmark-junit4 = "1.2.4" [libraries] @@ -166,6 +168,7 @@ android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } nav-safe-args-gradle-plugin = { module = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "androidx-navigation" } maven-publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven-publish-plugin" } +androidx-benchmark-junit4 = { group = "androidx.benchmark", name = "benchmark-junit4", version.ref = "benchmark-junit4" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } @@ -175,3 +178,4 @@ kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } maven-publish = { id = "com.vanniktech.maven.publish.base", version.ref = "maven-publish-plugin" } gradle-publish = { id = "com.gradle.plugin-publish", version = "1.2.1" } protobuf = { id = "com.google.protobuf", version = "0.9.4" } +benchmark = { id = "androidx.benchmark", version.ref = "benchmark" } diff --git a/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaClassCRUDTest.kt b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaClassCRUDTest.kt index 04888981b..9c4b8e2d5 100644 --- a/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaClassCRUDTest.kt +++ b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaClassCRUDTest.kt @@ -20,12 +20,13 @@ package com.itsaky.androidide.lsp.java.indexing import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.itsaky.androidide.lsp.java.indexing.models.JavaClass -import com.itsaky.androidide.lsp.java.indexing.models.JavaField -import com.itsaky.androidide.lsp.java.indexing.models.JavaMethod +import com.itsaky.androidide.lsp.java.indexing.models.JavaIndexingRealmModule import com.itsaky.androidide.lsp.java.indexing.models.JavaType +import com.itsaky.androidide.testing.android.rules.RealmDBTestRule import io.realm.RealmList import io.realm.RealmObject import openjdk.tools.classfile.AccessFlags +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -35,33 +36,18 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class JavaClassCRUDTest { + @Rule + @JvmField + val dbTestRule = RealmDBTestRule(JavaIndexingRealmModule()) + @Test fun testSimpleJavaClassCRUD() { - IndexingHelper("java-class-create").doWithRealm { - val klass = JavaClass.newInstance( - fqn = "com/itsaky/androidide/indexing/TestClass", - name = "TestClass", - packageName = "com/itsaky/androidide/indexing", - accessFlags = AccessFlags.ACC_PUBLIC or AccessFlags.ACC_FINAL, - superClassFqn = "java/lang/Object", - superInterfacesFqn = RealmList(), - fields = RealmList().apply { - add(JavaField.newField("someString", JavaType.STRING, AccessFlags.ACC_PRIVATE)) - }, - methods = RealmList().apply { - add( - JavaMethod.newInstance( - name = "getSomeString", - paramsTypes = RealmList(), - returnType = JavaType.STRING, - accessFlags = AccessFlags.ACC_PUBLIC - ) - ) - }) + dbTestRule.withDb("java-class-CRUD") { + val klass = ModelBuilder.createJavaClass() // CREATE executeTransaction { - insert(klass) + it.insert(klass) } var dbKlass: JavaClass? = null diff --git a/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexerTest.kt b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexerTest.kt index b9b556611..ead2695ff 100644 --- a/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexerTest.kt +++ b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexerTest.kt @@ -17,11 +17,18 @@ package com.itsaky.androidide.lsp.java.indexing +import android.content.Context +import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.blankj.utilcode.util.ConvertUtils +import com.google.common.truth.Truth +import com.itsaky.androidide.lsp.java.indexing.models.JavaIndexingRealmModule +import com.itsaky.androidide.testing.android.rules.RealmDBTestRule import io.realm.RealmModel +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import java.io.File /** * @author Akash Yadav @@ -29,16 +36,31 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class JavaIndexerTest { + @Rule + @JvmField + val dbTestRule = RealmDBTestRule(JavaIndexingRealmModule()) + + val androidJar: File by lazy { + val context = ApplicationProvider.getApplicationContext() + val file = File.createTempFile("ajar", null, context.cacheDir) + context.assets.open("android.jar").use { asset -> + Truth.assertThat(asset).isNotNull() + file.outputStream().buffered().use { out -> + asset.copyTo(out) + out.flush() + } + } + file + } + @Test fun testSimpleAndroidJarIndexingDurationCheck() { - val helper = IndexingHelper("android-jar-classes.realm") - val worker = JavaIndexModelBuilder(helper.androidJar) + val worker = JavaJarModelBuilder(androidJar) val batches = mutableMapOf, MutableList>() - val totalDuration = 0L var dbWriteDuration = 0L - helper.doWithRealm { + dbTestRule.withDb("android-jar-classes") { val totalStart = System.currentTimeMillis() worker.consumeTypes { type -> val batched = batches.computeIfAbsent(type.javaClass) { mutableListOf() } @@ -68,7 +90,7 @@ class JavaIndexerTest { println( "Took ${dbWriteDuration}ms to write android.jar (${ ConvertUtils.byte2FitMemorySize( - helper.androidJar.length() + androidJar.length() ) }) classes to Realm" ) diff --git a/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/ModelBuilder.kt b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/ModelBuilder.kt new file mode 100644 index 000000000..1f1c65953 --- /dev/null +++ b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/ModelBuilder.kt @@ -0,0 +1,52 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.lsp.java.indexing + +import com.itsaky.androidide.lsp.java.indexing.models.JavaClass +import com.itsaky.androidide.lsp.java.indexing.models.JavaField +import com.itsaky.androidide.lsp.java.indexing.models.JavaMethod +import com.itsaky.androidide.lsp.java.indexing.models.JavaType +import io.realm.RealmList +import openjdk.tools.classfile.AccessFlags + +/** + * @author Akash Yadav + */ +object ModelBuilder { + + fun createJavaClass() = JavaClass.newInstance( + fqn = "com/itsaky/androidide/indexing/TestClass", + name = "TestClass", + packageName = "com/itsaky/androidide/indexing", + accessFlags = AccessFlags.ACC_PUBLIC or AccessFlags.ACC_FINAL, + superClassFqn = "java/lang/Object", + superInterfacesFqn = RealmList(), + fields = RealmList().apply { + add(JavaField.newField("someString", JavaType.STRING, AccessFlags.ACC_PRIVATE)) + }, + methods = RealmList().apply { + add( + JavaMethod.newInstance( + name = "getSomeString", + paramsTypes = RealmList(), + returnType = JavaType.STRING, + accessFlags = AccessFlags.ACC_PUBLIC + ) + ) + }) +} \ No newline at end of file diff --git a/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/SharedModelCRUDTest.kt b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/SharedModelCRUDTest.kt new file mode 100644 index 000000000..4b12169cf --- /dev/null +++ b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/SharedModelCRUDTest.kt @@ -0,0 +1,278 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.lsp.java.indexing + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import com.itsaky.androidide.lsp.java.indexing.models.AnnotationAnnotationElementValue +import com.itsaky.androidide.lsp.java.indexing.models.ApiInfo +import com.itsaky.androidide.lsp.java.indexing.models.ArrayAnnotationElementValue +import com.itsaky.androidide.lsp.java.indexing.models.ClassAnnotationElementValue +import com.itsaky.androidide.lsp.java.indexing.models.EnumAnnotationElementValue +import com.itsaky.androidide.lsp.java.indexing.models.IAnnotationElementValue +import com.itsaky.androidide.lsp.java.indexing.models.JavaConstant +import com.itsaky.androidide.lsp.java.indexing.models.JavaIndexingRealmModule +import com.itsaky.androidide.lsp.java.indexing.models.JavaType +import com.itsaky.androidide.lsp.java.indexing.models.PrimitiveAnnotationElementValue +import com.itsaky.androidide.testing.android.rules.RealmDBTestRule +import io.realm.RealmAny +import io.realm.RealmDictionary +import io.realm.RealmList +import io.realm.exceptions.RealmPrimaryKeyConstraintException +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * @author Akash Yadav + */ +@RunWith(AndroidJUnit4::class) +class SharedModelCRUDTest { + + @Rule + @JvmField + val dbTestRule = RealmDBTestRule(JavaIndexingRealmModule()) + + @Test + fun testApiInfoCreate() { + dbTestRule.withDb("api-info") { + val apiInfo = ApiInfo.newInstance(since = 1, deprecatedIn = 21, removedIn = 23) + assertInsertUnique(apiInfo) + + val apiInfo2 = apiInfo.clone().update(removedIn = 26) + assertInsertion(allowUpdate = true, apiInfo, apiInfo2) { models -> + assertThat(models).hasSize(2) + assertThat(models).containsExactly(apiInfo, apiInfo2) + } + } + } + + @Test(expected = RealmPrimaryKeyConstraintException::class) + fun testApiInfoDuplicationError() { + dbTestRule.withDb("api-info-duplication-error") { + val apiInfo = ApiInfo.newInstance(since = 1, deprecatedIn = 21, removedIn = 23) + assertInsertSingle(apiInfo) + assertInsertSingle(apiInfo) + } + } + + @Test + fun testConstantCreate() { + dbTestRule.withDb("java-constant-create") { + val constant = + JavaConstant.newInstance(kind = JavaType.KIND_INT, value = RealmAny.valueOf(1.toInt())) + val constant2 = + JavaConstant.newInstance(kind = JavaType.KIND_INT, value = RealmAny.valueOf(2.toInt())) + + assertInsertUnique(constant) + + assertInsertion(allowUpdate = true, constant, constant2) { models -> + assertThat(models).hasSize(2) + assertThat(models).containsExactly(constant, constant2) + } + } + } + + @Test(expected = RealmPrimaryKeyConstraintException::class) + fun testConstantDuplicationError() { + dbTestRule.withDb("java-constant-duplication-error") { + val constant = + JavaConstant.newInstance(kind = JavaType.KIND_INT, value = RealmAny.valueOf(1.toInt())) + assertInsertSingle(constant) + assertInsertSingle(constant) + } + } + + @Test + fun testJavaTypeCreate() { + dbTestRule.withDb("java-type-create") { + assertInsertUnique(JavaType.INT) + + assertInsertion(JavaType.STRING, JavaType.OBJECT) { models -> + assertThat(models).hasSize(3) + assertThat(models).containsExactly(JavaType.INT, JavaType.STRING, JavaType.OBJECT) + } + } + } + + @Test(expected = RealmPrimaryKeyConstraintException::class) + fun testJavaTypeDuplicationError() { + dbTestRule.withDb("java-type-duplication-error") { + assertInsertSingle(JavaType.OBJECT) + assertInsertSingle(JavaType.OBJECT) + } + } + + @Test + fun testPrimitiveAnnotationValueCreate() { + dbTestRule.withDb("annotation-value-primitive") { + val prim1 = PrimitiveAnnotationElementValue.newInstance( + IAnnotationElementValue.KIND_INT, + JavaConstant.newInstance(JavaType.KIND_INT, RealmAny.valueOf(1.toInt())) + ) + val prim2 = PrimitiveAnnotationElementValue.newInstance( + IAnnotationElementValue.KIND_STRING, + JavaConstant.newInstance(JavaType.KIND_REF, RealmAny.valueOf("Something something")) + ) + + assertInsertUnique(prim1) + + assertInsertion(allowUpdate = true, prim1, prim2) { models -> + assertThat(models).hasSize(2) + assertThat(models).containsExactly(prim1, prim2) + } + } + } + + @Test(expected = RealmPrimaryKeyConstraintException::class) + fun testPrimitiveAnnotationValueDuplicationError() { + dbTestRule.withDb("annotation-value-primitive-duplication") { + val prim1 = PrimitiveAnnotationElementValue.newInstance( + IAnnotationElementValue.KIND_INT, + JavaConstant.newInstance(JavaType.KIND_INT, RealmAny.valueOf(1.toInt())) + ) + assertInsertSingle(prim1) + assertInsertSingle(prim1) + } + } + + @Test + fun testArrayAnnotationValueCreate() { + dbTestRule.withDb("annotation-value-array") { + val obj1 = ArrayAnnotationElementValue.newInstance( + RealmList() + ) + val obj2 = ArrayAnnotationElementValue.newInstance( + RealmList().apply { + add( + RealmAny.valueOf( + PrimitiveAnnotationElementValue.newInstance( + IAnnotationElementValue.KIND_STRING, + JavaConstant.newInstance(JavaType.KIND_REF, RealmAny.valueOf("Something something")) + ) + ) + ) + + RealmAny.valueOf( + PrimitiveAnnotationElementValue.newInstance( + IAnnotationElementValue.KIND_INT, + JavaConstant.newInstance(JavaType.KIND_INT, RealmAny.valueOf(123.toInt())) + ) + ) + } + ) + + assertInsertUnique(obj1) + + assertInsertion(allowUpdate = true, obj1, obj2) { models -> + assertThat(models).hasSize(2) + assertThat(models).containsExactly(obj1, obj2) + } + } + } + + @Test(expected = RealmPrimaryKeyConstraintException::class) + fun testArrayAnnotationValueDuplicationError() { + dbTestRule.withDb("annotation-value-array-duplication") { + val obj1 = ArrayAnnotationElementValue.newInstance( + RealmList().apply { + add( + RealmAny.valueOf( + PrimitiveAnnotationElementValue.newInstance( + IAnnotationElementValue.KIND_STRING, + JavaConstant.newInstance(JavaType.KIND_REF, RealmAny.valueOf("Something something")) + ) + ) + ) + + RealmAny.valueOf( + PrimitiveAnnotationElementValue.newInstance( + IAnnotationElementValue.KIND_INT, + JavaConstant.newInstance(JavaType.KIND_INT, RealmAny.valueOf(123.toInt())) + ) + ) + } + ) + assertInsertSingle(obj1) + assertInsertSingle(obj1) + } + } + + @Test + fun testClassAnnotationValueCreate() { + dbTestRule.withDb("annotation-value-class") { + val obj1 = ClassAnnotationElementValue.newInstance( + JavaType.newInstance("com/example/SomeType", JavaType.KIND_REF) + ) + val obj2 = ClassAnnotationElementValue.newInstance( + JavaType.newInstance("com/example/SomeType", JavaType.KIND_REF, arrayDims = 5) + ) + + assertInsertUnique(obj1) + + assertInsertion(allowUpdate = true, obj1, obj2) { models -> + assertThat(models).hasSize(2) + assertThat(models).containsExactly(obj1, obj2) + } + } + } + + @Test(expected = RealmPrimaryKeyConstraintException::class) + fun testClassAnnotationValueDuplicationError() { + dbTestRule.withDb("annotation-value-class-duplication") { + val obj1 = ClassAnnotationElementValue.newInstance( + JavaType.newInstance("com/example/SomeType", JavaType.KIND_REF) + ) + assertInsertSingle(obj1) + assertInsertSingle(obj1) + } + } + + @Test + fun testEnumAnnotationValueCreate() { + dbTestRule.withDb("annotation-value-enum") { + val obj1 = EnumAnnotationElementValue.newInstance( + "SomeEnum_ENTRY", + JavaType.newInstance("com/example/SomeEnum", JavaType.KIND_REF), + ) + val obj2 = EnumAnnotationElementValue.newInstance( + "SomeEnum_ENTRY_2", + JavaType.newInstance("com/example/SomeEnum", JavaType.KIND_REF), + ) + + assertInsertUnique(obj1) + + assertInsertion(allowUpdate = true, obj1, obj2) { models -> + assertThat(models).hasSize(2) + assertThat(models).containsExactly(obj1, obj2) + } + } + } + + @Test(expected = RealmPrimaryKeyConstraintException::class) + fun testEnumAnnotationValueDuplicationError() { + dbTestRule.withDb("annotation-value-enum-duplication") { + val obj1 = EnumAnnotationElementValue.newInstance( + "SomeEnum_ENTRY", + JavaType.newInstance("com/example/SomeEnum", JavaType.KIND_REF), + ) + assertInsertSingle(obj1) + assertInsertSingle(obj1) + } + } +} \ No newline at end of file diff --git a/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/crudExts.kt b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/crudExts.kt new file mode 100644 index 000000000..afe8a35c0 --- /dev/null +++ b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/crudExts.kt @@ -0,0 +1,72 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.lsp.java.indexing + +import com.google.common.truth.Truth.assertThat +import io.realm.Realm +import io.realm.RealmModel +import io.realm.RealmResults + +inline fun Realm.assertInsertion( + vararg models: T, + crossinline verify: Realm.(models: RealmResults) -> Unit +) { + assertInsertion(false, *models, verify = verify) +} + +inline fun Realm.assertInsertion( + allowUpdate: Boolean = false, + vararg models: T, + crossinline verify: Realm.(models: RealmResults) -> Unit +) { + val modelList = models.toList() + assertThat(modelList).isNotEmpty() + + executeTransaction { + if (allowUpdate) { + it.insertOrUpdate(modelList) + } else { + it.insert(modelList) + } + } + + executeTransaction { + verify(it.where(modelList[0].javaClass).findAll()) + } +} + +fun Realm.assertInsertSingle(model: RealmModel) { + assertInsertion(model) { models -> + assertThat(models).isNotNull() + assertThat(models).isNotEmpty() + assertThat(models).hasSize(1) + assertThat(models[0]).isEqualTo(model) + } +} + +fun Realm.assertInsertUnique(model: RealmModel) { + assertInsertSingle(model) + assertInsertion(true, model) { models -> + assertThat(models).isNotNull() + assertThat(models).isNotEmpty() + assertThat(models).hasSize(1) + assertThat(models[0]).isEqualTo(model) + } +} + + diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexer.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexer.kt index 36879434e..09d8f7fef 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexer.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexer.kt @@ -45,7 +45,7 @@ class JavaIndexer( override fun index(index: IIndex, params: Params): Collection> { return jarPaths.map { jar -> coroutineScope.async { - val worker = JavaIndexModelBuilder(jar) + val worker = JavaJarModelBuilder(jar) // worker.index(index) } } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexModelBuilder.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaJarModelBuilder.kt similarity index 99% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexModelBuilder.kt rename to lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaJarModelBuilder.kt index 9501a88b6..1629b2187 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexModelBuilder.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaJarModelBuilder.kt @@ -60,11 +60,11 @@ import java.io.File import java.util.zip.ZipFile /** - * [JavaIndexModelBuilder] builds models for Java classes in a JAR file. + * [JavaJarModelBuilder] builds models for Java classes in a JAR file. * * @author Akash Yadav */ -class JavaIndexModelBuilder( +class JavaJarModelBuilder( private val jar: File ) { diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/ApiInfo.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/ApiInfo.kt index 98a7b4eae..855f0e2a3 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/ApiInfo.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/ApiInfo.kt @@ -31,7 +31,7 @@ import io.realm.annotations.Required */ @RealmClass -open class ApiInfo : ISharedJavaIndexable { +open class ApiInfo : ISharedJavaIndexable, ICloneable { @Required @PrimaryKey @@ -39,18 +39,70 @@ open class ApiInfo : ISharedJavaIndexable { override var id: Int? = null @RealmField("since") - var since: Int = 1 + private var since: Int = 1 @RealmField("deprecatedIn") - var deprecatedIn: Int = 0 + private var deprecatedIn: Int = 0 @RealmField("removedIn") - var removedIn: Int = 0 + private var removedIn: Int = 0 override fun computeId() { this.id = Objects.hashCode(this.since, this.removedIn, this.deprecatedIn) } + /** + * Update the API information from the given [ApiInfo]. + */ + fun update(apiInfo: ApiInfo) = apply { + this.since = apiInfo.since + this.deprecatedIn = apiInfo.deprecatedIn + this.removedIn = apiInfo.removedIn + this.computeId() + } + + /** + * Update the API information with the given parameters. + */ + fun update( + since: Int = this.since, + deprecatedIn: Int = this.deprecatedIn, + removedIn: Int = this.removedIn + ) = apply { + this.since = since + this.deprecatedIn = deprecatedIn + this.removedIn = removedIn + this.computeId() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ApiInfo) return false + + if (id != other.id) return false + if (since != other.since) return false + if (deprecatedIn != other.deprecatedIn) return false + if (removedIn != other.removedIn) return false + + return true + } + + override fun hashCode(): Int { + var result = id ?: 0 + result = 31 * result + since + result = 31 * result + deprecatedIn + result = 31 * result + removedIn + return result + } + + override fun toString(): String { + return "ApiInfo(id=$id, since=$since, deprecatedIn=$deprecatedIn, removedIn=$removedIn)" + } + + override fun clone(): ApiInfo { + return newInstance(since, deprecatedIn, removedIn) + } + companion object { @JvmStatic fun newInstance(since: Int, deprecatedIn: Int, removedIn: Int): ApiInfo { diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/ICloneable.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/ICloneable.kt new file mode 100644 index 000000000..19b64f1ea --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/ICloneable.kt @@ -0,0 +1,27 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.lsp.java.indexing.models + +/** + * Anything that can be cloned. + * + * @author Akash Yadav + */ +interface ICloneable { + fun clone(): ICloneable +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaConstant.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaConstant.kt index b00017ab7..49fa155f4 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaConstant.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaConstant.kt @@ -43,10 +43,10 @@ open class JavaConstant : ISharedJavaIndexable { override var id: Int? = null @RealmField("kind") - var kind: Short = JavaType.KIND_UNKNOWN + private var kind: Short = JavaType.KIND_UNKNOWN @RealmField("value") - var value: RealmAny? = null + private var value: RealmAny? = null companion object { fun newInstance(kind: Short, value: RealmAny): JavaConstant { @@ -58,6 +58,19 @@ open class JavaConstant : ISharedJavaIndexable { } } + fun update(kind: Short = this.kind, value: RealmAny? = this.value) { + this.kind = kind + this.value = value + this.computeId() + } + + fun update(src: JavaConstant) { + this.kind = src.kind + this.value = src.value + this.computeId() + } + + override fun computeId() { this.id = Objects.hashCode(this.kind, this.kind, this.value) } @@ -111,4 +124,26 @@ open class JavaConstant : ISharedJavaIndexable { } else null } } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is JavaConstant) return false + + if (id != other.id) return false + if (kind != other.kind) return false + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + var result = id ?: 0 + result = 31 * result + kind + result = 31 * result + (value?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "JavaConstant(id=$id, kind=$kind, value=$value)" + } } \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaType.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaType.kt index fb1d3b033..5bb3d94ec 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaType.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaType.kt @@ -29,7 +29,7 @@ import io.realm.annotations.Required */ @RealmClass -open class JavaType : ISharedJavaIndexable { +open class JavaType : ISharedJavaIndexable, ICloneable { @Required @PrimaryKey @@ -115,6 +115,10 @@ open class JavaType : ISharedJavaIndexable { return name == CLASS.name } + override fun clone(): JavaType { + return newInstance(name = name, kind = kind, arrayDims = arrayDims) + } + override fun computeId() { this.id = Objects.hashCode(this.kind, this.name, this.arrayDims) } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/annotationElementValues.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/annotationElementValues.kt index d80fc5b68..d4ebadc40 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/annotationElementValues.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/annotationElementValues.kt @@ -18,6 +18,8 @@ package com.itsaky.androidide.lsp.java.indexing.models import com.google.common.base.Objects +import com.itsaky.androidide.db.utils.isEqualTo +import com.itsaky.androidide.db.utils.hash import io.realm.RealmAny import io.realm.RealmDictionary import io.realm.RealmList @@ -66,6 +68,28 @@ open class PrimitiveAnnotationElementValue : IAnnotationElementValue { this.id = Objects.hashCode(this.kind, this.value) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is PrimitiveAnnotationElementValue) return false + + if (id != other.id) return false + if (kind != other.kind) return false + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + var result = id ?: 0 + result = 31 * result + kind + result = 31 * result + (value?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "PrimitiveAnnotationElementValue(id=$id, kind=$kind, value=$value)" + } + companion object { @JvmStatic fun newInstance(kind: Byte, value: JavaConstant): PrimitiveAnnotationElementValue { @@ -99,6 +123,30 @@ open class EnumAnnotationElementValue : IAnnotationElementValue { this.id = Objects.hashCode(this.kind, this.name, this.type) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is EnumAnnotationElementValue) return false + + if (id != other.id) return false + if (kind != other.kind) return false + if (name != other.name) return false + if (type != other.type) return false + + return true + } + + override fun hashCode(): Int { + var result = id ?: 0 + result = 31 * result + kind + result = 31 * result + (name?.hashCode() ?: 0) + result = 31 * result + (type?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "EnumAnnotationElementValue(id=$id, kind=$kind, name=$name, type=$type)" + } + companion object { @JvmStatic fun newInstance(name: String, type: JavaType): EnumAnnotationElementValue { @@ -136,6 +184,30 @@ open class ArrayAnnotationElementValue : IAnnotationElementValue { this.id = Objects.hashCode(this.kind, this.numValues, this.values) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ArrayAnnotationElementValue) return false + + if (id != other.id) return false + if (kind != other.kind) return false + if (numValues != other.numValues) return false + if (values != other.values) return false + + return true + } + + override fun hashCode(): Int { + var result = id ?: 0 + result = 31 * result + kind + result = 31 * result + numValues + result = 31 * result + (values?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "ArrayAnnotationElementValue(id=$id, kind=$kind, numValues=$numValues, values=$values)" + } + companion object { @JvmStatic fun newInstance(values: RealmList): ArrayAnnotationElementValue { @@ -167,6 +239,28 @@ open class ClassAnnotationElementValue : IAnnotationElementValue { this.id = Objects.hashCode(this.kind, this.type) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ClassAnnotationElementValue) return false + + if (id != other.id) return false + if (kind != other.kind) return false + if (type != other.type) return false + + return true + } + + override fun hashCode(): Int { + var result = id ?: 0 + result = 31 * result + kind + result = 31 * result + (type?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "ClassAnnotationElementValue(id=$id, kind=$kind, type=$type)" + } + companion object { @JvmStatic fun newInstance(type: JavaType): ClassAnnotationElementValue { @@ -201,7 +295,31 @@ open class AnnotationAnnotationElementValue : IAnnotationElementValue { override fun computeId() { // RealmDictionary seems to not override hashCode() - this.id = Objects.hashCode(this.kind, this.type, this.values?.toMap()?.hashCode()) + this.id = Objects.hashCode(this.kind, this.type, this.values?.hash()) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is AnnotationAnnotationElementValue) return false + + if (id != other.id) return false + if (kind != other.kind) return false + if (type != other.type) return false + if (!values.isEqualTo(other)) return false + + return true + } + + override fun hashCode(): Int { + var result = id ?: 0 + result = 31 * result + kind + result = 31 * result + (type?.hashCode() ?: 0) + result = 31 * result + (values?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "AnnotationAnnotationElementValue(id=$id, kind=$kind, type=$type, values=$values)" } companion object { diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/JavaLSPTest.kt b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/JavaLSPTest.kt index 3e5fb5be4..a0d81735b 100644 --- a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/JavaLSPTest.kt +++ b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/JavaLSPTest.kt @@ -18,7 +18,7 @@ package com.itsaky.androidide.lsp.java import com.itsaky.androidide.lsp.api.ILanguageServerRegistry -import com.itsaky.androidide.lsp.api.LSPTest +import com.itsaky.androidide.lsp.testing.LSPTest import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService import com.itsaky.androidide.projects.util.findAppModule import org.junit.Before diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/AndroidJarModelingRule.kt b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/AndroidJarModelingRule.kt index b46ba07a0..a0517cbf2 100644 --- a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/AndroidJarModelingRule.kt +++ b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/AndroidJarModelingRule.kt @@ -39,9 +39,9 @@ class AndroidJarModelingRule : TestRule { .toFile() } - private val androidJarIndexer: JavaIndexModelBuilder by lazy { + private val androidJarIndexer: JavaJarModelBuilder by lazy { assertThat(androidJar.exists()).isTrue() - JavaIndexModelBuilder(androidJar) + JavaJarModelBuilder(androidJar) } val types: List> by lazy { diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexWorkerText.kt b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexWorkerText.kt index dad68efca..22d550569 100644 --- a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexWorkerText.kt +++ b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexWorkerText.kt @@ -42,7 +42,7 @@ import java.lang.reflect.Modifier * @author Akash Yadav */ @RunWith(RobolectricTestRunner::class) -class JavaIndexModelBuilderTest { +class JavaJarModelBuilderTest { @Rule @JvmField diff --git a/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/XMLLSPTest.kt b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/XMLLSPTest.kt index 6d9bc0691..0715633b4 100644 --- a/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/XMLLSPTest.kt +++ b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/XMLLSPTest.kt @@ -18,7 +18,7 @@ package com.itsaky.androidide.lsp.xml import com.itsaky.androidide.lookup.Lookup import com.itsaky.androidide.lsp.api.ILanguageServerRegistry -import com.itsaky.androidide.lsp.api.LSPTest +import com.itsaky.androidide.lsp.testing.LSPTest import com.itsaky.androidide.projects.api.ModuleProject import com.itsaky.androidide.projects.util.findAppModule import com.itsaky.androidide.xml.resources.ResourceTableRegistry diff --git a/settings.gradle.kts b/settings.gradle.kts index aa594273b..c61874f29 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -156,6 +156,7 @@ include( ":termux:termux-shared", ":termux:termux-view", ":testing:android", + ":testing:benchmark", ":testing:common", ":testing:lsp", ":testing:tooling", diff --git a/subprojects/database/src/main/java/com/itsaky/androidide/db/utils/realmDictionaryExts.kt b/subprojects/database/src/main/java/com/itsaky/androidide/db/utils/realmDictionaryExts.kt new file mode 100644 index 000000000..4408fc98d --- /dev/null +++ b/subprojects/database/src/main/java/com/itsaky/androidide/db/utils/realmDictionaryExts.kt @@ -0,0 +1,60 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.db.utils + +import io.realm.RealmDictionary + +/** + * Check if two RealmDictionaries are equal. + */ +fun RealmDictionary?.isEqualTo(other: Any?): Boolean { + // implementation taken from java/util/AbstractMap from JDK 21 + if (other == this) return true + if (other == null || this == null) return false + if (other !is RealmDictionary<*>) return false + if (this.size != other.size) return false + + try { + for (entry in this.entries) { + val key = entry.key + val value = entry.value + if (value == null) { + if (!(other[key] == null && other.containsKey(key))) { + return false + } + } else { + if (value != other[key]) { + return false + } + } + } + } catch (e: ClassCastException) { + return false + } catch (e: NullPointerException) { + return false + } + + return true +} + +/** + * Generate the hash code for a RealmDictionary. + */ +fun RealmDictionary.hash(): Int { + return this.entries.sumOf { it.hashCode() } +} \ No newline at end of file diff --git a/testing/android/build.gradle.kts b/testing/android/build.gradle.kts index 017429b4d..5a392befa 100644 --- a/testing/android/build.gradle.kts +++ b/testing/android/build.gradle.kts @@ -18,8 +18,8 @@ import com.itsaky.androidide.build.config.BuildConfig plugins { - id("com.android.library") - kotlin("android") + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) } android { @@ -38,6 +38,12 @@ configurations.api { } dependencies { + + // compileOnly so that we can provide the APIs for the related test rules + // if a module needs to use those APIs, it needs to explicitly add the database module as a + // dependency + compileOnly(projects.subprojects.database) + api(libs.google.protobuf) api(libs.tests.androidx.espresso.core) api(libs.tests.androidx.espresso.contrib) @@ -52,9 +58,8 @@ dependencies { exclude("org.jetbrains.kotlin") } - api(projects.testing.common) - api(projects.buildInfo) api(projects.common) api(projects.shared) + api(projects.testing.common) } \ No newline at end of file diff --git a/testing/android/src/main/java/com/itsaky/androidide/testing/android/rules/AbstractAndroidTestRule.kt b/testing/android/src/main/java/com/itsaky/androidide/testing/android/rules/AbstractAndroidTestRule.kt new file mode 100644 index 000000000..0dd4bc671 --- /dev/null +++ b/testing/android/src/main/java/com/itsaky/androidide/testing/android/rules/AbstractAndroidTestRule.kt @@ -0,0 +1,37 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.testing.android.rules + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.itsaky.androidide.utils.Environment +import org.junit.rules.TestRule + +/** + * Abstract class for Android testing rules. + * + * @author Akash Yadav + */ +abstract class AbstractAndroidTestRule : TestRule { + init { + Environment.init(ApplicationProvider.getApplicationContext()) + } + + protected val context: Context + get() = ApplicationProvider.getApplicationContext() +} \ No newline at end of file diff --git a/testing/android/src/main/java/com/itsaky/androidide/testing/android/rules/RealmDBTestRule.kt b/testing/android/src/main/java/com/itsaky/androidide/testing/android/rules/RealmDBTestRule.kt new file mode 100644 index 000000000..b92033626 --- /dev/null +++ b/testing/android/src/main/java/com/itsaky/androidide/testing/android/rules/RealmDBTestRule.kt @@ -0,0 +1,61 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.testing.android.rules + +import com.itsaky.androidide.db.IRealmProvider +import com.itsaky.androidide.testing.android.util.NoOpStatement +import io.realm.Realm +import io.realm.log.LogLevel +import io.realm.log.RealmLog +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Rule for Realm DB tests. + * + * @author Akash Yadav + */ +class RealmDBTestRule( + val baseModule: Any? = null, + vararg val additionalModules: Array, +) : AbstractAndroidTestRule() { + + inline fun withDb(dbName: String, deleteDbAfterTest: Boolean = true, action: Realm.() -> Unit) { + val name = dbName.replace(IRealmProvider.PATH_SEPARATOR, '-') + val realm = IRealmProvider.instance().get("/indexing/java/$name") { + baseModule?.let { baseModule -> + modules(baseModule, *additionalModules) + } + } + + try { + realm.action() + } finally { + if (deleteDbAfterTest) { + realm.configuration.realmDirectory.deleteRecursively() + } + } + } + + override fun apply(base: Statement?, description: Description?): Statement { + Realm.init(context.applicationContext) + RealmLog.setLevel(LogLevel.ALL) + base?.evaluate() + return NoOpStatement() + } +} \ No newline at end of file diff --git a/testing/android/src/main/java/com/itsaky/androidide/testing/android/util/NoOpStatement.kt b/testing/android/src/main/java/com/itsaky/androidide/testing/android/util/NoOpStatement.kt new file mode 100644 index 000000000..dafae85e1 --- /dev/null +++ b/testing/android/src/main/java/com/itsaky/androidide/testing/android/util/NoOpStatement.kt @@ -0,0 +1,30 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.testing.android.util + +import org.junit.runners.model.Statement + +/** + * A no-op statement. + * + * @author Akash Yadav + */ +class NoOpStatement : Statement() { + override fun evaluate() { + } +} \ No newline at end of file diff --git a/testing/benchmark/.gitignore b/testing/benchmark/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/testing/benchmark/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/testing/benchmark/benchmark-proguard-rules.pro b/testing/benchmark/benchmark-proguard-rules.pro new file mode 100644 index 000000000..e4061d222 --- /dev/null +++ b/testing/benchmark/benchmark-proguard-rules.pro @@ -0,0 +1,37 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +-dontobfuscate + +-ignorewarnings + +-keepattributes *Annotation* + +-dontnote junit.framework.** +-dontnote junit.runner.** + +-dontwarn androidx.test.** +-dontwarn org.junit.** +-dontwarn org.hamcrest.** +-dontwarn com.squareup.javawriter.JavaWriter + +-keepclasseswithmembers @org.junit.runner.RunWith public class * \ No newline at end of file diff --git a/testing/benchmark/build.gradle.kts b/testing/benchmark/build.gradle.kts new file mode 100644 index 000000000..596b71512 --- /dev/null +++ b/testing/benchmark/build.gradle.kts @@ -0,0 +1,70 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +import com.itsaky.androidide.build.config.BuildConfig + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.benchmark) +} + +android { + namespace = "${BuildConfig.packageName}.benchmark" + + sourceSets { + getByName("androidTest") { + assets.srcDirs(rootProject.file("subprojects/framework-stubs/libs")) + } + } + + defaultConfig { + testInstrumentationRunner = "androidx.benchmark.junit4.AndroidBenchmarkRunner" + } + + testBuildType = "release" + + buildTypes { + debug { + // Since isDebuggable can"t be modified by gradle for library modules, + // it must be done in a manifest - see src/androidTest/AndroidManifest.xml + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "benchmark-proguard-rules.pro" + ) + } + + release { + isDefault = true + } + } +} + +dependencies { + androidTestImplementation(libs.tests.androidx.test.runner) + androidTestImplementation(libs.tests.androidx.junit) + androidTestImplementation(libs.tests.junit) + androidTestImplementation(libs.tests.google.truth) + androidTestImplementation(libs.androidx.benchmark.junit4) + + androidTestImplementation(projects.common) + androidTestImplementation(projects.lsp.java) + androidTestImplementation(projects.shared) + androidTestImplementation(projects.subprojects.database) + androidTestImplementation(projects.subprojects.indexing) +} \ No newline at end of file diff --git a/testing/benchmark/src/androidTest/AndroidManifest.xml b/testing/benchmark/src/androidTest/AndroidManifest.xml new file mode 100644 index 000000000..c48372a2a --- /dev/null +++ b/testing/benchmark/src/androidTest/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/IndexingHelper.kt b/testing/benchmark/src/androidTest/java/com/itsaky/androidide/benchmark/lsp/java/JavaIndexingWorkerBenchmark.kt similarity index 50% rename from lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/IndexingHelper.kt rename to testing/benchmark/src/androidTest/java/com/itsaky/androidide/benchmark/lsp/java/JavaIndexingWorkerBenchmark.kt index 1c16d3ddb..799e6aa00 100644 --- a/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/IndexingHelper.kt +++ b/testing/benchmark/src/androidTest/java/com/itsaky/androidide/benchmark/lsp/java/JavaIndexingWorkerBenchmark.kt @@ -15,32 +15,34 @@ * along with AndroidIDE. If not, see . */ -package com.itsaky.androidide.lsp.java.indexing +package com.itsaky.androidide.benchmark.lsp.java import android.content.Context +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth -import com.itsaky.androidide.db.IRealmProvider -import com.itsaky.androidide.lsp.java.indexing.models.JavaIndexingRealmModule -import com.itsaky.androidide.utils.Environment -import io.realm.Realm -import io.realm.log.LogLevel -import io.realm.log.RealmLog +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import com.itsaky.androidide.lsp.java.indexing.JavaJarModelBuilder +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith import java.io.File /** * @author Akash Yadav */ -class IndexingHelper( - private val dbName: String -) { - private val context: Context - get() = ApplicationProvider.getApplicationContext() +@RunWith(AndroidJUnit4::class) +class JavaIndexingWorkerBenchmark { + + @get:Rule + val benchmarkRule = BenchmarkRule() val androidJar: File by lazy { + val context = ApplicationProvider.getApplicationContext() val file = File.createTempFile("ajar", null, context.cacheDir) context.assets.open("android.jar").use { asset -> - Truth.assertThat(asset).isNotNull() + assertThat(asset).isNotNull() file.outputStream().buffered().use { out -> asset.copyTo(out) out.flush() @@ -49,24 +51,11 @@ class IndexingHelper( file } - fun doWithRealm(deleteDbAfterAction: Boolean = true, action: Realm.() -> Unit) { - Realm.init(context.applicationContext) - RealmLog.setLevel(LogLevel.ALL) - - if (Environment.REALM_DB_DIR == null) { - Environment.init(context) - } - - val dbName = dbName.replace(IRealmProvider.PATH_SEPARATOR, '-') - val realm = IRealmProvider.instance().get("/indexing/java/$dbName") { - modules(JavaIndexingRealmModule()) - } - try { - realm.action() - } finally { - if (deleteDbAfterAction) { - realm.configuration.realmDirectory.deleteRecursively() - } + @Test + fun benchmarkJavaIndexingWorker() { + val worker = JavaJarModelBuilder(androidJar) + benchmarkRule.measureRepeated { + worker.buildTypes() } } } \ No newline at end of file diff --git a/testing/common/build.gradle.kts b/testing/common/build.gradle.kts index 7dff8466d..8fc16e0ec 100644 --- a/testing/common/build.gradle.kts +++ b/testing/common/build.gradle.kts @@ -23,10 +23,11 @@ plugins { } dependencies { - implementation(project(":subprojects:tooling-api-impl")) kapt(libs.google.auto.service) implementation(libs.google.auto.service.annotations) api(libs.tests.junit) api(projects.logger) + + implementation(projects.subprojects.toolingApiImpl) } \ No newline at end of file diff --git a/testing/lsp/build.gradle.kts b/testing/lsp/build.gradle.kts index 18e4cb012..d9a04fe5a 100644 --- a/testing/lsp/build.gradle.kts +++ b/testing/lsp/build.gradle.kts @@ -18,8 +18,8 @@ import com.itsaky.androidide.build.config.BuildConfig plugins { - id("com.android.library") - id("kotlin-android") + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) } android { @@ -37,8 +37,8 @@ dependencies { api(projects.common) api(projects.eventbusAndroid) api(projects.lsp.api) - api(projects.testing.common) + api(projects.testing.android) api(projects.testing.tooling) api(projects.testing.unit) } \ No newline at end of file diff --git a/testing/lsp/src/main/java/com/itsaky/androidide/lsp/api/LSPTest.kt b/testing/lsp/src/main/java/com/itsaky/androidide/lsp/testing/LSPTest.kt similarity index 98% rename from testing/lsp/src/main/java/com/itsaky/androidide/lsp/api/LSPTest.kt rename to testing/lsp/src/main/java/com/itsaky/androidide/lsp/testing/LSPTest.kt index 4f8b385bb..31bf17e2c 100644 --- a/testing/lsp/src/main/java/com/itsaky/androidide/lsp/api/LSPTest.kt +++ b/testing/lsp/src/main/java/com/itsaky/androidide/lsp/testing/LSPTest.kt @@ -15,7 +15,7 @@ * along with AndroidIDE. If not, see . */ -package com.itsaky.androidide.lsp.api +package com.itsaky.androidide.lsp.testing import android.content.Context import com.google.common.truth.Truth.assertThat @@ -27,6 +27,7 @@ import com.itsaky.androidide.eventbus.events.editor.DocumentOpenEvent import com.itsaky.androidide.eventbus.events.file.FileDeletionEvent import com.itsaky.androidide.eventbus.events.file.FileRenameEvent import com.itsaky.androidide.lookup.Lookup +import com.itsaky.androidide.lsp.api.ILanguageServerRegistry import com.itsaky.androidide.managers.PreferenceManager import com.itsaky.androidide.models.Position import com.itsaky.androidide.models.Range diff --git a/testing/lsp/src/main/java/com/itsaky/androidide/lsp/api/util/ReflectiveLogListener.kt b/testing/lsp/src/main/java/com/itsaky/androidide/lsp/testing/util/ReflectiveLogListener.kt similarity index 93% rename from testing/lsp/src/main/java/com/itsaky/androidide/lsp/api/util/ReflectiveLogListener.kt rename to testing/lsp/src/main/java/com/itsaky/androidide/lsp/testing/util/ReflectiveLogListener.kt index 72099f158..9af78cc96 100644 --- a/testing/lsp/src/main/java/com/itsaky/androidide/lsp/api/util/ReflectiveLogListener.kt +++ b/testing/lsp/src/main/java/com/itsaky/androidide/lsp/testing/util/ReflectiveLogListener.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with AndroidIDE. If not, see . */ -package com.itsaky.androidide.lsp.api.util +package com.itsaky.androidide.lsp.testing.util import com.itsaky.androidide.utils.ILogger import com.itsaky.androidide.utils.ILogger.Level