From 2edd74c1426e366ca8d9092f3c10830bdd850ef5 Mon Sep 17 00:00:00 2001 From: IlyaMuravjov Date: Fri, 8 Sep 2023 12:13:15 +0300 Subject: [PATCH 1/2] Add executable generic parameter type resolver --- .../main/kotlin/org/utbot/fuzzer/TypeUtils.kt | 101 ++++++++++++++++++ .../org/utbot/fuzzing/spring/unit/Mocks.kt | 6 +- .../utbot/fuzzing/spring/utils/TypeUtils.kt | 46 -------- 3 files changed, 104 insertions(+), 49 deletions(-) create mode 100644 utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/TypeUtils.kt delete mode 100644 utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/utils/TypeUtils.kt diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/TypeUtils.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/TypeUtils.kt new file mode 100644 index 0000000000..2c1b908bb6 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/TypeUtils.kt @@ -0,0 +1,101 @@ +package org.utbot.fuzzer + +import com.google.common.reflect.TypeResolver +import com.google.common.reflect.TypeToken +import mu.KotlinLogging +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.util.constructor +import org.utbot.framework.plugin.api.util.executable +import org.utbot.framework.plugin.api.util.isArray +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.method +import java.lang.reflect.GenericArrayType +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.util.Optional + +private val logger = KotlinLogging.logger {} +private val loggedUnresolvedExecutables = mutableSetOf() + +val Type.typeToken: TypeToken<*> get() = TypeToken.of(this) + +val FuzzedType.jType: Type get() = toType(cache = mutableMapOf()) + +private fun FuzzedType.toType(cache: MutableMap): Type = cache.getOrPut(this) { + when { + generics.isEmpty() -> classId.jClass + classId.isArray && generics.size == 1 -> GenericArrayType { generics.single().toType(cache) } + else -> object : ParameterizedType { + override fun getActualTypeArguments(): Array = + generics.map { it.toType(cache) }.toTypedArray() + + override fun getRawType(): Type = + classId.jClass + + override fun getOwnerType(): Type? = null + } + } +} + +/** + * Returns fully parameterized type, e.g. for `Map` class + * `Map` type is returned, where `K` and `V` are type variables. + */ +fun Class<*>.toTypeParametrizedByTypeVariables(): Type = + if (typeParameters.isEmpty()) this + else object : ParameterizedType { + override fun getActualTypeArguments(): Array = + typeParameters.toList().toTypedArray() + + override fun getRawType(): Type = + this@toTypeParametrizedByTypeVariables + + override fun getOwnerType(): Type? = + declaringClass?.toTypeParametrizedByTypeVariables() + } + +/** + * Returns types of arguments that should be passed to [executableId] for it to return [neededType] and `null` iff + * [executableId] can't return [neededType] (e.g. if it returns `List` while `List` is needed). + * + * For example, if [executableId] is [Optional.of] and [neededType] is `Optional`, + * then one element list containing `String` type is returned. + */ +fun resolveParameterTypes( + executableId: ExecutableId, + neededType: Type +): List? { + return try { + val actualType = when (executableId) { + is MethodId -> executableId.method.genericReturnType + is ConstructorId -> executableId.constructor.declaringClass.toTypeParametrizedByTypeVariables() + } + + val neededClass = neededType.typeToken.rawType + val actualClass = actualType.typeToken.rawType + + if (!neededClass.isAssignableFrom(actualClass)) + return null + + @Suppress("UNCHECKED_CAST") + val actualSuperType = actualType.typeToken.getSupertype(neededClass as Class).type + val typeResolver = try { + TypeResolver().where(actualSuperType, neededType) + } catch (e: Exception) { + // TypeResolver.where() throws an exception when unification of actual & needed types fails + // e.g. when unifying Optional and Optional + return null + } + + executableId.executable.genericParameterTypes.map { + typeResolver.resolveType(it) + } + } catch (e: Exception) { + if (loggedUnresolvedExecutables.add(executableId)) + logger.error(e) { "Failed to resolve types for $executableId, using unresolved generic type" } + + executableId.executable.genericParameterTypes.toList() + } +} diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/unit/Mocks.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/unit/Mocks.kt index 85417456bf..1c848981bf 100644 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/unit/Mocks.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/unit/Mocks.kt @@ -18,9 +18,9 @@ import org.utbot.fuzzing.Routine import org.utbot.fuzzing.Scope import org.utbot.fuzzing.ScopeProperty import org.utbot.fuzzing.Seed -import org.utbot.fuzzing.spring.utils.jType -import org.utbot.fuzzing.spring.utils.toTypeParametrizedByTypeVariables -import org.utbot.fuzzing.spring.utils.typeToken +import org.utbot.fuzzer.jType +import org.utbot.fuzzer.toTypeParametrizedByTypeVariables +import org.utbot.fuzzer.typeToken import org.utbot.fuzzing.toFuzzerType val methodsToMockProperty = ScopeProperty>( diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/utils/TypeUtils.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/utils/TypeUtils.kt deleted file mode 100644 index ac1da0f200..0000000000 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/utils/TypeUtils.kt +++ /dev/null @@ -1,46 +0,0 @@ -package org.utbot.fuzzing.spring.utils - -import com.google.common.reflect.TypeToken -import org.utbot.framework.plugin.api.util.isArray -import org.utbot.framework.plugin.api.util.jClass -import org.utbot.fuzzer.FuzzedType -import java.lang.reflect.GenericArrayType -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type - -val Type.typeToken: TypeToken<*> get() = TypeToken.of(this) - -val FuzzedType.jType: Type get() = toType(cache = mutableMapOf()) - -private fun FuzzedType.toType(cache: MutableMap): Type = cache.getOrPut(this) { - when { - generics.isEmpty() -> classId.jClass - classId.isArray && generics.size == 1 -> GenericArrayType { generics.single().toType(cache) } - else -> object : ParameterizedType { - override fun getActualTypeArguments(): Array = - generics.map { it.toType(cache) }.toTypedArray() - - override fun getRawType(): Type = - classId.jClass - - override fun getOwnerType(): Type? = null - } - } -} - -/** - * Returns fully parameterized type, e.g. for `Map` class - * `Map` type is returned, where `K` and `V` are type variables. - */ -fun Class<*>.toTypeParametrizedByTypeVariables(): Type = - if (typeParameters.isEmpty()) this - else object : ParameterizedType { - override fun getActualTypeArguments(): Array = - typeParameters.toList().toTypedArray() - - override fun getRawType(): Type = - this@toTypeParametrizedByTypeVariables - - override fun getOwnerType(): Type? = - declaringClass?.toTypeParametrizedByTypeVariables() - } From 9c48d93e09e958e1bf58a48bf7559789ef7dc586 Mon Sep 17 00:00:00 2001 From: IlyaMuravjov Date: Fri, 8 Sep 2023 16:06:04 +0300 Subject: [PATCH 2/2] Add tests for `resolveParameterTypes` and improve its implementation --- .../main/kotlin/org/utbot/fuzzer/TypeUtils.kt | 7 + .../org/utbot/fuzzing/samples/StringList.java | 30 +++ .../kotlin/org/utbot/fuzzer/TypeUtilsTest.kt | 200 ++++++++++++++++++ 3 files changed, 237 insertions(+) create mode 100644 utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/StringList.java create mode 100644 utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzer/TypeUtilsTest.kt diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/TypeUtils.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/TypeUtils.kt index 2c1b908bb6..0eb097513a 100644 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/TypeUtils.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/TypeUtils.kt @@ -20,6 +20,8 @@ private val logger = KotlinLogging.logger {} private val loggedUnresolvedExecutables = mutableSetOf() val Type.typeToken: TypeToken<*> get() = TypeToken.of(this) +inline fun typeTokenOf(): TypeToken = object : TypeToken() {} +inline fun jTypeOf(): Type = typeTokenOf().type val FuzzedType.jType: Type get() = toType(cache = mutableMapOf()) @@ -89,6 +91,11 @@ fun resolveParameterTypes( return null } + // in some cases when bounded wildcards are involved TypeResolver.where() doesn't throw even though types are + // incompatible (e.g. when needed type is `List` while actual super type is `List`) + if (!typeResolver.resolveType(actualSuperType).typeToken.isSubtypeOf(neededType)) + return null + executableId.executable.genericParameterTypes.map { typeResolver.resolveType(it) } diff --git a/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/StringList.java b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/StringList.java new file mode 100644 index 0000000000..0cd91ecd37 --- /dev/null +++ b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/StringList.java @@ -0,0 +1,30 @@ +package org.utbot.fuzzing.samples; + +import java.util.*; + +public class StringList extends ArrayList { + public static StringList create() { + return new StringList(); + } + + public static List createAndUpcast() { + return new StringList(); + } + + public static List createListOfLists() { + return new ArrayList<>(); + } + + public static List> createListOfUpcastedLists() { + return new ArrayList<>(); + } + + public static List> createReadOnlyListOfReadOnlyLists() { + return new ArrayList<>(); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public static List> createListOfParametrizedLists(Optional elm) { + return Collections.singletonList(Collections.singletonList(elm.orElse(null))); + } +} diff --git a/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzer/TypeUtilsTest.kt b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzer/TypeUtilsTest.kt new file mode 100644 index 0000000000..d8e1df48b2 --- /dev/null +++ b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzer/TypeUtilsTest.kt @@ -0,0 +1,200 @@ +package org.utbot.fuzzer + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.util.executableId +import org.utbot.fuzzing.runBlockingWithContext +import org.utbot.fuzzing.samples.StringList +import java.io.File +import java.lang.reflect.Type +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.util.Optional + +class TypeUtilsTest { + @Test + fun `resolveParameterTypes works correctly with non-generic method`() = runBlockingWithContext { + val actualTypes = resolveParameterTypes(LocalDateTime::ofInstant.executableId, jTypeOf()) + val expectedTypes = listOf(jTypeOf(), jTypeOf()) + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with non-generic constructor`() = runBlockingWithContext { + val constructor = File::class.java.getConstructor(File::class.java, String::class.java) + val actualTypes = resolveParameterTypes(constructor.executableId, jTypeOf()) + val expectedTypes = listOf(jTypeOf(), jTypeOf()) + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with non-generic method and incompatible return type`() = runBlockingWithContext { + val actualTypes = resolveParameterTypes(LocalDateTime::ofInstant.executableId, jTypeOf()) + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with non-generic constructor and incompatible return type`() = runBlockingWithContext { + val constructor = File::class.java.getConstructor(File::class.java, String::class.java) + val actualTypes = resolveParameterTypes(constructor.executableId, jTypeOf()) + + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with simple generic method`() = runBlockingWithContext { + val method = Optional::class.java.getMethod("of", Object::class.java) + val actualTypes = resolveParameterTypes(method.executableId, jTypeOf>()) + val expectedTypes = listOf(jTypeOf()) + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with simple generic constructor`() = runBlockingWithContext { + val method = ArrayList::class.java.getConstructor(Collection::class.java) + val actualTypes = resolveParameterTypes(method.executableId, jTypeOf>()) + val expectedTypes = listOf(jTypeOf>()) + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with method returning type with incompatible type parameter`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes(StringList::createAndUpcast.executableId, jTypeOf>()) + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with method returning subtype with incompatible type parameter`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes(StringList::create.executableId, jTypeOf>()) + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with method returning type with incompatible upper bound type parameter`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes(StringList::createAndUpcast.executableId, jTypeOf>()) + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with method returning type with incompatible lower bounded type parameter`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes(StringList::createAndUpcast.executableId, jTypeOf>()) + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with method returning type with compatible type parameter`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes(StringList::createAndUpcast.executableId, jTypeOf>()) + val expectedTypes = emptyList() + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with method returning subtype with compatible type parameter`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes(StringList::create.executableId, jTypeOf>()) + val expectedTypes = emptyList() + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with method returning type with compatible upper bound type parameter`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes(StringList::createAndUpcast.executableId, jTypeOf>()) + val expectedTypes = emptyList() + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works with deep compatible bounded types`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes( + StringList::createListOfLists.executableId, + jTypeOf>>() + ) + val expectedTypes = emptyList() + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works with deep types incompatible at the top layer`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes( + StringList::createListOfLists.executableId, + jTypeOf>>() + ) + + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes works with deep compatible unbounded types`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes( + StringList::createListOfUpcastedLists.executableId, + jTypeOf>>() + ) + val expectedTypes = emptyList() + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works with deep types incompatible at the second layer`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes( + StringList::createListOfUpcastedLists.executableId, + jTypeOf>>() + ) + + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes works with methods returning compatible nested bounded types`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes( + StringList::createReadOnlyListOfReadOnlyLists.executableId, + jTypeOf>>() + ) + val expectedTypes = emptyList() + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works with methods returning incompatible nested bounded types`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes( + StringList::createReadOnlyListOfReadOnlyLists.executableId, + jTypeOf>>() + ) + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes resolves non-toplevel generics`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes( + StringList::class.java.getMethod("createListOfParametrizedLists", Optional::class.java).executableId, + jTypeOf>>>() + ) + val expectedTypes = listOf(jTypeOf>>()) + + assertEquals(expectedTypes, actualTypes) + } +} \ No newline at end of file