From 48221535d1ebc5d37f044e63dcfec2e9407aaa1b Mon Sep 17 00:00:00 2001 From: Ivan Volkov Date: Wed, 19 Oct 2022 23:07:46 +0300 Subject: [PATCH 1/2] Disable test generation for synthetic methods of data classes --- .../org/utbot/cli/GenerateTestsCommand.kt | 4 ++-- .../utbot/framework/plugin/api/util/IdUtil.kt | 3 +++ .../utbot/framework/util/SyntheticMethods.kt | 24 +++++++++++++++---- .../intellij/plugin/util/PsiClassHelper.kt | 12 ++++++++-- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsCommand.kt b/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsCommand.kt index b0b7101011..ed945f26a1 100644 --- a/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsCommand.kt +++ b/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsCommand.kt @@ -50,8 +50,8 @@ class GenerateTestsCommand : help = "Specifies source code file for a generated test" ) .required() - .check("Must exist and ends with *.java suffix") { - it.endsWith(".java") && Files.exists(Paths.get(it)) + .check("Must exist and end with .java or .kt suffix") { + (it.endsWith(".java") || it.endsWith(".kt")) && Files.exists(Paths.get(it)) } private val projectRoot by option( diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt index 0c6dec522c..c4e3efefc4 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt @@ -394,6 +394,9 @@ val ClassId.isIterableOrMap: Boolean val ClassId.isEnum: Boolean get() = jClass.isEnum +val ClassId.isData: Boolean + get() = kClass.isData + fun ClassId.findFieldByIdOrNull(fieldId: FieldId): Field? { if (isNotSubtypeOf(fieldId.declaringClass)) { return null diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/util/SyntheticMethods.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/util/SyntheticMethods.kt index ce66f79400..bc885c5747 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/util/SyntheticMethods.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/util/SyntheticMethods.kt @@ -1,14 +1,18 @@ package org.utbot.framework.util import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.util.humanReadableName +import org.utbot.framework.plugin.api.util.isData import org.utbot.framework.plugin.api.util.isEnum -fun isKnownSyntheticMethod(method: ExecutableId): Boolean = +fun isKnownSyntheticMethod(method: ExecutableId): Boolean { if (method.classId.isEnum) - method.humanReadableName.substringBefore('(') in KnownSyntheticMethodNames.enumSyntheticMethodNames - else - false + return method.name in KnownSyntheticMethodNames.enumSyntheticMethodNames + + if (method.classId.isData) + return KnownSyntheticMethodNames.dataClassSyntheticMethodNames.any { it.matches(method.name) } + + return false +} /** * Contains names of methods that are always autogenerated and thus it is unlikely that @@ -17,4 +21,14 @@ fun isKnownSyntheticMethod(method: ExecutableId): Boolean = private object KnownSyntheticMethodNames { /** List with names of enum methods that are autogenerated */ val enumSyntheticMethodNames = listOf("values", "valueOf") + + /** List with names of data classes methods that are autogenerated in Kotlin */ + val dataClassSyntheticMethodNames = listOf( + "equals", + "hashCode", + "toString", + "copy", + "copy\\\$default", + "component[1-9][0-9]*" + ).map { it.toRegex() } } \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/PsiClassHelper.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/PsiClassHelper.kt index 5b1f30e098..b8bb5a9584 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/PsiClassHelper.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/PsiClassHelper.kt @@ -9,6 +9,7 @@ import com.intellij.testIntegration.TestIntegrationUtils import org.jetbrains.kotlin.asJava.elements.KtLightMethod import org.jetbrains.kotlin.asJava.elements.isGetter import org.jetbrains.kotlin.asJava.elements.isSetter +import org.jetbrains.kotlin.psi.KtClass import org.utbot.common.filterWhen import org.utbot.framework.UtSettings @@ -22,8 +23,15 @@ private val PsiMember.isKotlinGetterOrSetter: Boolean return isGetter || isSetter } -private fun Iterable.filterTestableMethods(): List = this - .filterWhen(UtSettings.skipTestGenerationForSyntheticMethods) { it.member !is SyntheticElement } +// By now, we think that method in Kotlin is synthetic iff navigation to its declaration leads to its declaring class +// rather than the method itself (because synthetic methods don't have bodies that we can navigate to) +private val PsiMember.isSyntheticKotlinMethod: Boolean + get() = this is KtLightMethod && navigationElement is KtClass + +fun Iterable.filterTestableMethods(): List = this + .filterWhen(UtSettings.skipTestGenerationForSyntheticMethods) { + it.member !is SyntheticElement && !it.member.isSyntheticKotlinMethod + } .filterNot { it.member.isAbstract } .filterNot { it.member.isKotlinGetterOrSetter } From 0867c5a85e7a94516b0ab94e02a4163861628313 Mon Sep 17 00:00:00 2001 From: Ivan Volkov Date: Wed, 26 Oct 2022 17:29:25 +0300 Subject: [PATCH 2/2] Divided synthetic methods to "true-synthetic methods" and "implicitly declared methods" --- .../org/utbot/cli/GenerateTestsCommand.kt | 7 +++- .../kotlin/org/utbot/framework/UtSettings.kt | 5 ++- .../utbot/framework/plugin/api/util/IdUtil.kt | 1 - .../framework/plugin/api/util/ModifierUtil.kt | 4 ++ .../util/ImplicitlyDeclaredMethods.kt | 38 +++++++++++++++++++ .../utbot/framework/util/SyntheticMethods.kt | 34 ----------------- .../intellij/plugin/util/PsiClassHelper.kt | 10 ++--- .../main/kotlin/org/utbot/contest/Contest.kt | 7 +++- 8 files changed, 60 insertions(+), 46 deletions(-) create mode 100644 utbot-framework/src/main/kotlin/org/utbot/framework/util/ImplicitlyDeclaredMethods.kt delete mode 100644 utbot-framework/src/main/kotlin/org/utbot/framework/util/SyntheticMethods.kt diff --git a/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsCommand.kt b/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsCommand.kt index ed945f26a1..a6837ca80f 100644 --- a/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsCommand.kt +++ b/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsCommand.kt @@ -17,8 +17,9 @@ import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.UtMethodTestSet import org.utbot.framework.plugin.api.util.UtContext import org.utbot.framework.plugin.api.util.isAbstract +import org.utbot.framework.plugin.api.util.isSynthetic import org.utbot.framework.plugin.api.util.withUtContext -import org.utbot.framework.util.isKnownSyntheticMethod +import org.utbot.framework.util.isKnownImplicitlyDeclaredMethod import org.utbot.sarif.SarifReport import org.utbot.sarif.SourceFindingStrategyDefault import java.nio.file.Files @@ -97,7 +98,9 @@ class GenerateTestsCommand : withUtContext(UtContext(classLoader)) { val classIdUnderTest = ClassId(targetClassFqn) val targetMethods = classIdUnderTest.targetMethods() - .filterWhen(UtSettings.skipTestGenerationForSyntheticMethods) { !isKnownSyntheticMethod(it) } + .filterWhen(UtSettings.skipTestGenerationForSyntheticAndImplicitlyDeclaredMethods) { + !it.isSynthetic && !it.isKnownImplicitlyDeclaredMethod + } .filterNot { it.isAbstract } val testCaseGenerator = initializeGenerator(workingDirectory) diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt index dcd92c03b1..ad0abba414 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt @@ -3,6 +3,7 @@ package org.utbot.framework import com.jetbrains.rd.util.LogLevel import mu.KotlinLogging import org.utbot.common.AbstractSettings +import java.lang.reflect.Executable private val logger = KotlinLogging.logger {} /** @@ -361,9 +362,9 @@ object UtSettings : AbstractSettings( var singleSelector by getBooleanProperty(true) /** - * Flag that indicates whether tests for synthetic methods (values, valueOf in enums) should be generated, or not + * Flag that indicates whether tests for synthetic (see [Executable.isSynthetic]) and implicitly declared methods (like values, valueOf in enums) should be generated, or not */ - var skipTestGenerationForSyntheticMethods by getBooleanProperty(true) + var skipTestGenerationForSyntheticAndImplicitlyDeclaredMethods by getBooleanProperty(true) /** * Flag that indicates whether should we branch on and set static fields from trusted libraries or not. diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt index c4e3efefc4..f7fb77c0c7 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt @@ -1,6 +1,5 @@ package org.utbot.framework.plugin.api.util -import org.utbot.common.withAccessibility import org.utbot.framework.plugin.api.BuiltinClassId import org.utbot.framework.plugin.api.BuiltinConstructorId import org.utbot.framework.plugin.api.BuiltinMethodId diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/ModifierUtil.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/ModifierUtil.kt index 9a71ca82ac..bb40f5b7a4 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/ModifierUtil.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/ModifierUtil.kt @@ -3,6 +3,7 @@ package org.utbot.framework.plugin.api.util import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.ExecutableId import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.MethodId import java.lang.reflect.Modifier class ModifierFactory private constructor( @@ -77,6 +78,9 @@ val ExecutableId.isPackagePrivate: Boolean val ExecutableId.isAbstract: Boolean get() = Modifier.isAbstract(modifiers) +val ExecutableId.isSynthetic: Boolean + get() = (this is MethodId) && method.isSynthetic + // FieldIds val FieldId.isStatic: Boolean diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/util/ImplicitlyDeclaredMethods.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/util/ImplicitlyDeclaredMethods.kt new file mode 100644 index 0000000000..727f344abe --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/util/ImplicitlyDeclaredMethods.kt @@ -0,0 +1,38 @@ +package org.utbot.framework.util + +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.util.isData +import org.utbot.framework.plugin.api.util.isEnum + +/** + * Returns whether this method could be implicitly generated by compiler, or not. + * + * Note that here we can only judge this by method name and kind of class (data class, enum, etc). + * There seems to be no (at least, easy) way to check from bytecode if this method was actually overridden by user, + * so this function will return true even if the matching method is not autogenerated but written explicitly by user. + */ +val ExecutableId.isKnownImplicitlyDeclaredMethod: Boolean + get() = + when { + classId.isEnum -> name in KnownImplicitlyDeclaredMethods.enumImplicitlyDeclaredMethodNames + classId.isData -> KnownImplicitlyDeclaredMethods.dataClassImplicitlyDeclaredMethodNameRegexps.any { it.matches(name) } + else -> false + } + +/** + * Contains names of methods that are always autogenerated by compiler and thus it is unlikely that + * one would want to generate tests for them. + */ +private object KnownImplicitlyDeclaredMethods { + /** List with names of enum methods that are generated by compiler */ + val enumImplicitlyDeclaredMethodNames = listOf("values", "valueOf") + + /** List with regexps that match names of methods that are generated by Kotlin compiler for data classes */ + val dataClassImplicitlyDeclaredMethodNameRegexps = listOf( + "equals", + "hashCode", + "toString", + "copy", + "component[1-9][0-9]*" + ).map { it.toRegex() } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/util/SyntheticMethods.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/util/SyntheticMethods.kt deleted file mode 100644 index bc885c5747..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/util/SyntheticMethods.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.utbot.framework.util - -import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.util.isData -import org.utbot.framework.plugin.api.util.isEnum - -fun isKnownSyntheticMethod(method: ExecutableId): Boolean { - if (method.classId.isEnum) - return method.name in KnownSyntheticMethodNames.enumSyntheticMethodNames - - if (method.classId.isData) - return KnownSyntheticMethodNames.dataClassSyntheticMethodNames.any { it.matches(method.name) } - - return false -} - -/** - * Contains names of methods that are always autogenerated and thus it is unlikely that - * one would want to generate tests for them. - */ -private object KnownSyntheticMethodNames { - /** List with names of enum methods that are autogenerated */ - val enumSyntheticMethodNames = listOf("values", "valueOf") - - /** List with names of data classes methods that are autogenerated in Kotlin */ - val dataClassSyntheticMethodNames = listOf( - "equals", - "hashCode", - "toString", - "copy", - "copy\\\$default", - "component[1-9][0-9]*" - ).map { it.toRegex() } -} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/PsiClassHelper.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/PsiClassHelper.kt index b8bb5a9584..f02a4deb40 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/PsiClassHelper.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/PsiClassHelper.kt @@ -23,14 +23,14 @@ private val PsiMember.isKotlinGetterOrSetter: Boolean return isGetter || isSetter } -// By now, we think that method in Kotlin is synthetic iff navigation to its declaration leads to its declaring class -// rather than the method itself (because synthetic methods don't have bodies that we can navigate to) -private val PsiMember.isSyntheticKotlinMethod: Boolean +// By now, we think that method in Kotlin is autogenerated iff navigation to its declaration leads to its declaring class +// rather than the method itself (because such methods don't have bodies that we can navigate to) +private val PsiMember.isKotlinAutogeneratedMethod: Boolean get() = this is KtLightMethod && navigationElement is KtClass fun Iterable.filterTestableMethods(): List = this - .filterWhen(UtSettings.skipTestGenerationForSyntheticMethods) { - it.member !is SyntheticElement && !it.member.isSyntheticKotlinMethod + .filterWhen(UtSettings.skipTestGenerationForSyntheticAndImplicitlyDeclaredMethods) { + it.member !is SyntheticElement && !it.member.isKotlinAutogeneratedMethod } .filterNot { it.member.isAbstract } .filterNot { it.member.isKotlinGetterOrSetter } diff --git a/utbot-junit-contest/src/main/kotlin/org/utbot/contest/Contest.kt b/utbot-junit-contest/src/main/kotlin/org/utbot/contest/Contest.kt index fdd856ea0a..2bd9389f7f 100644 --- a/utbot-junit-contest/src/main/kotlin/org/utbot/contest/Contest.kt +++ b/utbot-junit-contest/src/main/kotlin/org/utbot/contest/Contest.kt @@ -29,7 +29,7 @@ import org.utbot.framework.plugin.api.util.jClass import org.utbot.framework.plugin.api.util.utContext import org.utbot.framework.plugin.api.util.withUtContext import org.utbot.framework.plugin.services.JdkInfoService -import org.utbot.framework.util.isKnownSyntheticMethod +import org.utbot.framework.util.isKnownImplicitlyDeclaredMethod import org.utbot.fuzzer.UtFuzzedExecution import org.utbot.instrumentation.ConcreteExecutor import org.utbot.instrumentation.ConcreteExecutorPool @@ -60,6 +60,7 @@ import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.yield +import org.utbot.framework.plugin.api.util.isSynthetic internal const val junitVersion = 4 private val logger = KotlinLogging.logger {} @@ -402,7 +403,9 @@ private fun prepareClass(javaClazz: Class<*>, methodNameFilter: String?): List