Skip to content

Support creation and persistence of valid entities #2475

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,13 @@ openblasVersion=0.3.10-1.5.4
arpackNgVersion=3.7.0-1.5.4
commonsLoggingVersion=1.2
commonsIOVersion=2.11.0
javaxVersion=2.2
jakartaVersion=3.1.0

# use latest Java 8 compaitable Spring and Spring Boot versions
springVersion=5.3.28
springBootVersion=2.7.13
springSecurityVersion=5.8.5

# configuration for build server
#
Expand Down
105 changes: 105 additions & 0 deletions utbot-core/src/main/kotlin/org/utbot/common/DynamicProperties.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.utbot.common

import java.util.*

/**
* @param TOwner used purely to make type system enforce the use of properties with correct receiver,
* e.g. if property `NotEmptyTypeFlag` is defined for `FuzzedType` it can't be used on `CgContext`.
*
* **See also:** [this post](https://stackoverflow.com/a/58219723/10536125).
*/
interface DynamicProperty<TOwner, T>

data class InitialisedDynamicProperty<TOwner, T>(
val property: DynamicProperty<TOwner, T>,
val value: T
)

fun <TOwner, T> DynamicProperty<TOwner, T>.withValue(value: T) =
InitialisedDynamicProperty(this, value)

interface DynamicProperties<TOwner> {
val entries: Set<InitialisedDynamicProperty<TOwner, *>>

operator fun <T> get(property: DynamicProperty<TOwner, T>): T?
fun <T> getValue(property: DynamicProperty<TOwner, T>): T
operator fun contains(property: DynamicProperty<TOwner, *>): Boolean

/**
* Two instances of [DynamicProperties] implementations are equal iff their [entries] are equal
*/
override fun equals(other: Any?): Boolean
override fun hashCode(): Int
}

interface MutableDynamicProperties<TOwner> : DynamicProperties<TOwner> {
operator fun <T> set(property: DynamicProperty<TOwner, T>, value: T)
}

fun <TOwner> Iterable<InitialisedDynamicProperty<TOwner, *>>.toMutableDynamicProperties(): MutableDynamicProperties<TOwner> =
DynamicPropertiesImpl<TOwner>().also { properties ->
forEach { properties.add(it) }
}

fun <TOwner> Iterable<InitialisedDynamicProperty<TOwner, *>>.toDynamicProperties(): DynamicProperties<TOwner> =
toMutableDynamicProperties()

fun <TOwner> mutableDynamicPropertiesOf(
vararg initialisedDynamicProperties: InitialisedDynamicProperty<TOwner, *>
): MutableDynamicProperties<TOwner> = initialisedDynamicProperties.asIterable().toMutableDynamicProperties()

fun <TOwner> dynamicPropertiesOf(
vararg initialisedDynamicProperties: InitialisedDynamicProperty<TOwner, *>
): DynamicProperties<TOwner> = initialisedDynamicProperties.asIterable().toDynamicProperties()

fun <TOwner> DynamicProperties<TOwner>.withoutProperty(property: DynamicProperty<TOwner, *>): DynamicProperties<TOwner> =
entries.filterNot { it.property == property }.toMutableDynamicProperties()

operator fun <TOwner> DynamicProperties<TOwner>.plus(other: DynamicProperties<TOwner>): DynamicProperties<TOwner> =
(entries + other.entries).toMutableDynamicProperties()

class DynamicPropertiesImpl<TOwner> : MutableDynamicProperties<TOwner> {
/**
* Actual type of [properties] should be `Map<DynamicProperty<TOwner, T>, T>`, but
* such type is not representable withing kotlin type system, hence unchecked casts are
* used later.
*/
private val properties = IdentityHashMap<DynamicProperty<TOwner, *>, Any?>()
override val entries: Set<InitialisedDynamicProperty<TOwner, *>>
get() = properties.mapTo(mutableSetOf()) { unsafeInitializedDynamicProperty(it.key, it.value) }

@Suppress("UNCHECKED_CAST")
private fun <T> unsafeInitializedDynamicProperty(property: DynamicProperty<TOwner, T>, value: Any?) =
InitialisedDynamicProperty(property, value as T)

@Suppress("UNCHECKED_CAST")
override fun <T> get(property: DynamicProperty<TOwner, T>): T? =
properties[property] as T?

@Suppress("UNCHECKED_CAST")
override fun <T> getValue(property: DynamicProperty<TOwner, T>): T =
properties.getValue(property) as T

override fun <T> set(property: DynamicProperty<TOwner, T>, value: T) {
properties[property] = value
}

override fun contains(property: DynamicProperty<TOwner, *>): Boolean =
property in properties

fun <T> add(initialisedDynamicProperty: InitialisedDynamicProperty<TOwner, T>) =
set(initialisedDynamicProperty.property, initialisedDynamicProperty.value)

override fun toString(): String {
return "DynamicPropertiesImpl(properties=$properties)"
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is DynamicProperties<*>) return false
return entries == other.entries
}

override fun hashCode(): Int =
properties.hashCode()
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

package org.utbot.framework.plugin.api

import mu.KotlinLogging
import org.utbot.common.FileUtil
import org.utbot.common.isDefaultValue
import org.utbot.common.withToStringThreadLocalReentrancyGuard
Expand Down Expand Up @@ -778,15 +777,27 @@ object UtSpringContextModel : UtCustomModel(
override fun hashCode(): Int = 0
}

class UtSpringEntityManagerModel : UtCustomModel(
id = null,
classId = SpringModelUtils.entityManagerClassIds.first(),
modelName = "entityManager"
) {
// NOTE that overriding equals is required just because without it
// we will lose equality for objects after deserialization
override fun equals(other: Any?): Boolean = other is UtSpringEntityManagerModel

override fun hashCode(): Int = 0
}

data class SpringRepositoryId(
val repositoryBeanName: String,
val repositoryClassId: ClassId,
val entityClassId: ClassId,
)

class UtSpringMockMvcResultActionsModel(
id: Int?,
origin: UtCompositeModel?,
data class UtSpringMockMvcResultActionsModel(
override val id: Int?,
override val origin: UtCompositeModel?,
val status: Int,
val errorMessage: String?,
val contentAsString: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package org.utbot.framework.plugin.api.util
import org.utbot.common.tryLoadClass
import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.MethodId
import org.utbot.framework.plugin.api.SpringRepositoryId
import org.utbot.framework.plugin.api.UtArrayModel
import org.utbot.framework.plugin.api.UtAssembleModel
import org.utbot.framework.plugin.api.UtExecutableCallModel
Expand All @@ -16,7 +15,7 @@ object SpringModelUtils {
val autowiredClassId = ClassId("org.springframework.beans.factory.annotation.Autowired")

val applicationContextClassId = ClassId("org.springframework.context.ApplicationContext")
val crudRepositoryClassId = ClassId("org.springframework.data.repository.CrudRepository")
val repositoryClassId = ClassId("org.springframework.data.repository.Repository")

val springBootTestClassId = ClassId("org.springframework.boot.test.context.SpringBootTest")

Expand All @@ -25,6 +24,7 @@ object SpringModelUtils {
val transactionalClassId = ClassId("org.springframework.transaction.annotation.Transactional")
val autoConfigureTestDbClassId = ClassId("org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase")
val autoConfigureMockMvcClassId = ClassId("org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc")
val withMockUserClassId = ClassId("org.springframework.security.test.context.support.WithMockUser")

val runWithClassId = ClassId("org.junit.runner.RunWith")
val springRunnerClassId = ClassId("org.springframework.test.context.junit4.SpringRunner")
Expand All @@ -39,13 +39,52 @@ object SpringModelUtils {
val activeProfilesClassId = ClassId("org.springframework.test.context.ActiveProfiles")
val contextConfigurationClassId = ClassId("org.springframework.test.context.ContextConfiguration")

private fun getClassIdFromEachAvailablePackage(
packages: List<String>,
classNameFromPackage: String
): List<ClassId> = packages.map { ClassId("$it.$classNameFromPackage") }
.filter { utContext.classLoader.tryLoadClass(it.name) != null }

// most likely only one persistent library is on the classpath, but we need to be able to work with either of them
private val persistentLibraries = listOf("javax.persistence", "jakarta.persistence")
private fun persistentClassIds(simpleName: String) = persistentLibraries.map { ClassId("$it.$simpleName") }
private fun persistentClassIds(simpleName: String) = getClassIdFromEachAvailablePackage(persistentLibraries, simpleName)

val entityClassIds get() = persistentClassIds("Entity")
val generatedValueClassIds get() = persistentClassIds("GeneratedValue")
val idClassIds get() = persistentClassIds("Id")
val persistenceContextClassIds get() = persistentClassIds("PersistenceContext")
val entityManagerClassIds get() = persistentClassIds("EntityManager")

val persistMethodIdOrNull: MethodId?
get() {
return MethodId(
classId = entityManagerClassIds.firstOrNull() ?: return null,
name = "persist",
returnType = voidClassId,
parameters = listOf(objectClassId),
bypassesSandbox = true // TODO may be we can use some alternative sandbox that has more permissions
)
}

val detachMethodIdOrNull: MethodId?
get() {
return MethodId(
classId = entityManagerClassIds.firstOrNull() ?: return null,
name = "detach",
returnType = voidClassId,
parameters = listOf(objectClassId),
bypassesSandbox = true // TODO may be we can use some alternative sandbox that has more permissions
)
}

private val validationLibraries = listOf("jakarta.validation.constraints")
private fun validationClassIds(simpleName: String) = getClassIdFromEachAvailablePackage(validationLibraries, simpleName)
.filter { utContext.classLoader.tryLoadClass(it.name) != null }

val notEmptyClassIds get() = validationClassIds("NotEmpty")
val notBlankClassIds get() = validationClassIds("NotBlank")
val emailClassIds get() = validationClassIds("Email")

val entityClassIds = persistentClassIds("Entity")
val generatedValueClassIds = persistentClassIds("GeneratedValue")

private val getBeanMethodId = MethodId(
classId = applicationContextClassId,
Expand All @@ -55,15 +94,6 @@ object SpringModelUtils {
bypassesSandbox = true // TODO may be we can use some alternative sandbox that has more permissions
)

private val saveMethodId = MethodId(
classId = crudRepositoryClassId,
name = "save",
returnType = Any::class.id,
parameters = listOf(Any::class.id),
bypassesSandbox = true // TODO may be we can use some alternative sandbox that has more permissions
)


fun createBeanModel(beanName: String, id: Int, classId: ClassId) = UtAssembleModel(
id = id,
classId = classId,
Expand All @@ -76,16 +106,6 @@ object SpringModelUtils {
modificationsChainProvider = { mutableListOf() }
)

fun createSaveCallModel(repositoryId: SpringRepositoryId, id: Int, entityModel: UtModel) = UtExecutableCallModel(
instance = createBeanModel(
beanName = repositoryId.repositoryBeanName,
id = id,
classId = repositoryId.repositoryClassId,
),
executable = saveMethodId,
params = listOf(entityModel)
)

fun UtModel.isAutowiredFromContext(): Boolean =
this is UtAssembleModel && this.instantiationCall.instance is UtSpringContextModel

Expand Down Expand Up @@ -291,7 +311,7 @@ object SpringModelUtils {
)
)

// TODO support @RequestParam, @RequestHeader, @CookieValue, @RequestAttribute
// TODO #2462 (support @RequestParam, @RequestHeader, @CookieValue, @RequestAttribute, ...)
return addContentToRequestBuilderModel(methodId, arguments, requestBuilderModel, idGenerator)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ class SpringSpecificInformation(
val thisInstanceDependentMocks: TypedModelWrappers,
val thisInstanceDependentSpies: TypedModelWrappers,
val autowiredFromContextModels: TypedModelWrappers,
val entityManagerModels: TypedModelWrappers,
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import org.utbot.framework.plugin.api.UtLambdaModel
import org.utbot.framework.plugin.api.UtModel
import org.utbot.framework.plugin.api.UtNullModel
import org.utbot.framework.plugin.api.UtPrimitiveModel
import org.utbot.framework.plugin.api.UtSpringEntityManagerModel
import org.utbot.framework.plugin.api.UtVoidModel
import org.utbot.framework.plugin.api.isMockModel
import org.utbot.framework.plugin.api.UtStatementCallModel
Expand Down Expand Up @@ -55,13 +56,15 @@ class SpringTestClassModelBuilder(val context: CgContext) :
.filterNotNull()
.forEach { model ->
thisInstanceModels += model.wrap()
thisInstancesDependentModels += collectDependentModels(model)

thisInstancesDependentModels += collectImmediateDependentModels(
model,
skipModificationChains = true
)
}

(execution.stateBefore.parameters + execution.stateBefore.thisInstance)
.filterNotNull()
.forEach { model -> stateBeforeDependentModels += collectAutowiredModels(model) }
.forEach { model -> stateBeforeDependentModels += collectRecursively(model) }
}
}
}
Expand All @@ -82,15 +85,19 @@ class SpringTestClassModelBuilder(val context: CgContext) :
val autowiredFromContextModels =
stateBeforeDependentModels.filterTo(HashSet()) { it.model.isAutowiredFromContext() }

val entityManagerModels =
stateBeforeDependentModels.filterTo(HashSet()) { it.model is UtSpringEntityManagerModel }

return SpringSpecificInformation(
thisInstanceModels.groupByClassId(),
dependentMockModels.groupByClassId(),
dependentSpyModels.groupByClassId(),
autowiredFromContextModels.groupByClassId(),
entityManagerModels.groupByClassId(),
)
}

private fun collectAutowiredModels(model: UtModel): Set<UtModelWrapper> {
private fun collectRecursively(model: UtModel): Set<UtModelWrapper> {
val allDependentModels = mutableSetOf<UtModelWrapper>()

collectRecursively(model, allDependentModels)
Expand All @@ -102,10 +109,12 @@ class SpringTestClassModelBuilder(val context: CgContext) :
if(!allDependentModels.add(model.wrap())){
return
}
collectDependentModels(model).forEach { collectRecursively(it.model, allDependentModels) }
collectImmediateDependentModels(model, skipModificationChains = false).forEach {
collectRecursively(it.model, allDependentModels)
}
}

private fun collectDependentModels(model: UtModel): Set<UtModelWrapper> {
private fun collectImmediateDependentModels(model: UtModel, skipModificationChains: Boolean): Set<UtModelWrapper> {
val dependentModels = mutableSetOf<UtModelWrapper>()

when (model) {
Expand Down Expand Up @@ -134,7 +143,7 @@ class SpringTestClassModelBuilder(val context: CgContext) :
model.instantiationCall.instance?.let { dependentModels.add(it.wrap()) }
model.instantiationCall.params.forEach { dependentModels.add(it.wrap()) }

if(model.isAutowiredFromContext()) {
if(!skipModificationChains) {
model.modificationsChain.forEach { stmt ->
stmt.instance?.let { dependentModels.add(it.wrap()) }
when (stmt) {
Expand Down
Loading