Skip to content

Commit

Permalink
Support mock strategies and type replacement in Spring unit test fuzz…
Browse files Browse the repository at this point in the history
…ing (#2561)

* Support mock strategies and type replacement in Spring unit test fuzzing

* Avoid mocking replaced types

* Only use `InjectMockValueProvider` for `thisInstance`

* Disallow non-mocks when mock can be used

* Move all value provider decorators to one package

* Improve adding of `ReplacedFuzzedTypeFlag`

* Make `map` and `except` apply transformer/filter to ALL value providers

* Avoid using `anyObjectValueProvider` that is removed in #2583

* Make `AnyObjectValueProvider` not extend any interfaces, remove unrelated comment

* Fix compilation after rebase
  • Loading branch information
IlyaMuravjov authored Sep 19, 2023
1 parent dd787d0 commit 36c23ff
Show file tree
Hide file tree
Showing 19 changed files with 218 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1397,7 +1397,7 @@ class Traverser(
// from Spring bean definitions, for example), we can just create a symbolic object
// with hard constraint on the mentioned type.
val replacedClassId = when (typeReplacer.typeReplacementMode) {
KnownImplementor -> typeReplacer.replaceTypeIfNeeded(type)
KnownImplementor -> typeReplacer.replaceTypeIfNeeded(type.id)
AnyImplementor,
NoImplementors -> null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,6 @@ class UtBotSymbolicEngine(
* Run fuzzing flow.
*
* @param until is used by fuzzer to cancel all tasks if the current time is over this value
* @param transform provides model values for a method
*/
fun fuzzing(until: Long = Long.MAX_VALUE) = flow {
val isFuzzable = methodUnderTest.parameters.all { classId ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package org.utbot.framework.context

import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.TypeReplacementMode
import soot.RefType

interface TypeReplacer {
/**
Expand All @@ -14,5 +13,5 @@ interface TypeReplacer {
* Finds a type to replace the original abstract type
* if it is guided with some additional information.
*/
fun replaceTypeIfNeeded(type: RefType): ClassId?
fun replaceTypeIfNeeded(classId: ClassId): ClassId?
}
Original file line number Diff line number Diff line change
@@ -1,41 +1,51 @@
package org.utbot.framework.context.custom

import org.utbot.framework.context.JavaFuzzingContext
import org.utbot.framework.plugin.api.UtNullModel
import org.utbot.framework.plugin.api.ExecutableId
import org.utbot.fuzzer.FuzzedType
import org.utbot.fuzzing.JavaValueProvider
import org.utbot.fuzzing.providers.MapValueProvider
import org.utbot.fuzzing.Seed
import org.utbot.fuzzing.providers.AnyDepthNullValueProvider
import org.utbot.fuzzing.providers.AnyObjectValueProvider
import org.utbot.fuzzing.spring.unit.MockValueProvider
import org.utbot.fuzzing.providers.NullValueProvider
import org.utbot.fuzzing.providers.ObjectValueProvider
import org.utbot.fuzzing.providers.StringValueProvider
import org.utbot.fuzzing.spring.decorators.filterSeeds
import org.utbot.fuzzing.spring.decorators.filterTypes
import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult

/**
* Makes fuzzer mock all types that don't have *specific* [JavaValueProvider],
* like [MapValueProvider] or [StringValueProvider].
* Makes fuzzer to use mocks in accordance with [mockPredicate].
*
* NOTE: the caller is responsible for providing some *specific* [JavaValueProvider]
* that can create values for class under test (otherwise it will be mocked),
* [ObjectValueProvider] and [NullValueProvider] do not count as *specific*.
* NOTE:
* - fuzzer won't mock types, that have *specific* value providers
* (i.e. ones that do not implement [AnyObjectValueProvider])
* - fuzzer may still resort to mocks despite [mockPredicate] and *specific*
* value providers if it can't create other non-null values or at runtime
*/
fun JavaFuzzingContext.mockAllTypesWithoutSpecificValueProvider() =
MockingJavaFuzzingContext(delegateContext = this)
fun JavaFuzzingContext.useMocks(mockPredicate: (FuzzedType) -> Boolean) =
MockingJavaFuzzingContext(delegateContext = this, mockPredicate)

class MockingJavaFuzzingContext(
val delegateContext: JavaFuzzingContext
val delegateContext: JavaFuzzingContext,
val mockPredicate: (FuzzedType) -> Boolean,
) : JavaFuzzingContext by delegateContext {
private val mockValueProvider = MockValueProvider(delegateContext.idGenerator)

override val valueProvider: JavaValueProvider =
// NOTE: we first remove `NullValueProvider` from `delegateContext.valueProvider` and then
// add it back as a part of our `withFallback` so it has the same priority as
// `mockValueProvider`, otherwise mocks will never be used where `null` can be used.

delegateContext.valueProvider
.except { it is NullValueProvider }
.except { it is ObjectValueProvider }
// NOTE: we first remove `AnyObjectValueProvider` and `NullValueProvider` from `delegateContext.valueProvider`
// and then add them back as a part of our `withFallback` so they have the same priority as
// `mockValueProvider`, otherwise mocks will never be used where `null` or new object can be used.
.except { it is AnyObjectValueProvider }
.withFallback(
mockValueProvider
.with(NullValueProvider)
mockValueProvider.filterTypes(mockPredicate)
.with(
delegateContext.valueProvider
.filterTypes { !mockPredicate(it) }
.filterSeeds { (it as? Seed.Simple)?.value?.model !is UtNullModel }
)
.withFallback(mockValueProvider.with(AnyDepthNullValueProvider))
)

override fun handleFuzzedConcreteExecutionResult(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ package org.utbot.framework.context.simple
import org.utbot.framework.context.TypeReplacer
import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.TypeReplacementMode
import soot.RefType

class SimpleTypeReplacer : TypeReplacer {
override val typeReplacementMode: TypeReplacementMode = TypeReplacementMode.AnyImplementor

override fun replaceTypeIfNeeded(type: RefType): ClassId? = null
override fun replaceTypeIfNeeded(classId: ClassId): ClassId? = null
}
25 changes: 10 additions & 15 deletions utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Providers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -92,26 +92,16 @@ fun interface ValueProvider<T, R, D : Description<T>> {
}

/**
* Removes `anotherValueProviders<T, R` from current one.
* Removes providers matching [filter] from the current one.
*/
fun except(filter: (ValueProvider<T, R, D>) -> Boolean): ValueProvider<T, R, D> {
return if (this is Combined) {
Combined(providers.map { it.unwrapIfFallback() }.filterNot(filter))
} else {
Combined(if (filter(unwrapIfFallback())) emptyList() else listOf(this))
}
}
fun except(filter: (ValueProvider<T, R, D>) -> Boolean): ValueProvider<T, R, D> =
map { if (filter(it)) Combined(emptyList()) else it }

/**
* Applies [transform] for current provider
*/
fun map(transform: (ValueProvider<T, R, D>) -> ValueProvider<T, R, D>): ValueProvider<T, R, D> {
return if (this is Combined) {
Combined(providers.map(transform))
} else {
transform(this)
}
}
fun map(transform: (ValueProvider<T, R, D>) -> ValueProvider<T, R, D>): ValueProvider<T, R, D> =
transform(this)

/**
* Uses fallback value provider in case when 'this' one failed to generate any value.
Expand Down Expand Up @@ -167,6 +157,8 @@ fun interface ValueProvider<T, R, D : Description<T>> {
}
}

override fun map(transform: (ValueProvider<T, R, D>) -> ValueProvider<T, R, D>): ValueProvider<T, R, D> =
transform(Fallback(provider.map(transform), fallback.map(transform)))
}

/**
Expand Down Expand Up @@ -200,6 +192,9 @@ fun interface ValueProvider<T, R, D : Description<T>> {
}
}
}

override fun map(transform: (ValueProvider<T, R, D>) -> ValueProvider<T, R, D>): ValueProvider<T, R, D> =
transform(Combined(providers.map { it.map(transform) }))
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ internal fun FuzzedType.traverseHierarchy(typeCache: MutableMap<Type, FuzzedType
* @param type to be resolved
* @param cache is used to store same [FuzzedType] for same java types
*/
internal fun toFuzzerType(type: Type, cache: MutableMap<Type, FuzzedType>): FuzzedType {
fun toFuzzerType(type: Type, cache: MutableMap<Type, FuzzedType>): FuzzedType {
return toFuzzerType(
type = type,
classId = { t -> toClassId(t, cache) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,16 @@ fun anyObjectValueProvider(idGenerator: IdentityPreservingIdGenerator<Int>) =
ovp.withFallback(AbstractsObjectValueProvider(idGenerator))
}

/**
* Marker interface that shows that this [JavaValueProvider] can potentially provide values of
* arbitrary types, unlike type-specific value providers that were designed to provide values of
* few specific popular types (e.g. `List`, `String`, etc.).
*/
interface AnyObjectValueProvider

class ObjectValueProvider(
val idGenerator: IdGenerator<Int>,
) : JavaValueProvider {
) : JavaValueProvider, AnyObjectValueProvider {

override fun accept(type: FuzzedType) = !isIgnored(type.classId)

Expand Down Expand Up @@ -140,7 +147,7 @@ class ObjectValueProvider(
}

@Suppress("unused")
object NullValueProvider : JavaValueProvider {
object NullValueProvider : JavaValueProvider, AnyObjectValueProvider {

override fun enrich(description: FuzzedDescription, type: FuzzedType, scope: Scope) {
// any value in static function is ok to fuzz
Expand Down Expand Up @@ -170,7 +177,7 @@ object NullValueProvider : JavaValueProvider {
*
* Intended to be used as a last fallback.
*/
object AnyDepthNullValueProvider : JavaValueProvider {
object AnyDepthNullValueProvider : JavaValueProvider, AnyObjectValueProvider {

override fun accept(type: FuzzedType) = type.classId.isRefType

Expand All @@ -185,7 +192,7 @@ object AnyDepthNullValueProvider : JavaValueProvider {
*/
class AbstractsObjectValueProvider(
val idGenerator: IdGenerator<Int>,
) : JavaValueProvider {
) : JavaValueProvider, AnyObjectValueProvider {

override fun accept(type: FuzzedType) = type.classId.isRefType && !isKnownTypes(type.classId)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.utbot.fuzzing.providers
package org.utbot.fuzzing.spring.decorators

import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.UtAssembleModel
Expand All @@ -9,8 +9,8 @@ import org.utbot.fuzzer.FuzzedValue
import org.utbot.fuzzing.FuzzedDescription
import org.utbot.fuzzing.JavaValueProvider
import org.utbot.fuzzing.Routine
import org.utbot.fuzzing.Scope
import org.utbot.fuzzing.Seed
import org.utbot.fuzzing.providers.findMethodsToModifyWith

/**
* Value provider that is a buddy for another provider
Expand All @@ -22,8 +22,11 @@ import org.utbot.fuzzing.Seed
*/
class ModifyingWithMethodsProviderWrapper(
private val classUnderTest: ClassId,
private val delegate: JavaValueProvider
) : JavaValueProvider by delegate {
delegate: JavaValueProvider
) : ValueProviderDecorator<FuzzedType, FuzzedValue, FuzzedDescription>(delegate) {

override fun wrap(provider: JavaValueProvider): JavaValueProvider =
ModifyingWithMethodsProviderWrapper(classUnderTest, provider)

override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence<Seed<FuzzedType, FuzzedValue>> =
delegate
Expand All @@ -50,9 +53,4 @@ class ModifyingWithMethodsProviderWrapper(
)
} else seed
}

override fun enrich(description: FuzzedDescription, type: FuzzedType, scope: Scope) =
delegate.enrich(description, type, scope)

override fun accept(type: FuzzedType): Boolean = delegate.accept(type)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package org.utbot.fuzzing.spring
package org.utbot.fuzzing.spring.decorators

import org.utbot.common.toDynamicProperties
import org.utbot.fuzzer.FuzzedType
import org.utbot.fuzzer.FuzzedValue
import org.utbot.fuzzing.FuzzedDescription
import org.utbot.fuzzing.JavaValueProvider
import org.utbot.fuzzing.Routine
import org.utbot.fuzzing.Scope
import org.utbot.fuzzing.Seed
import org.utbot.fuzzing.spring.FuzzedTypeProperty
import org.utbot.fuzzing.spring.addProperties
import org.utbot.fuzzing.spring.properties

/**
* @see preserveProperties
Expand All @@ -25,14 +27,14 @@ interface PreservableFuzzedTypeProperty<T> : FuzzedTypeProperty<T>
fun JavaValueProvider.preserveProperties() : JavaValueProvider =
PropertyPreservingValueProvider(this)

class PropertyPreservingValueProvider(private val delegateProvider: JavaValueProvider) : JavaValueProvider {
override fun enrich(description: FuzzedDescription, type: FuzzedType, scope: Scope) =
delegateProvider.enrich(description, type, scope)

override fun accept(type: FuzzedType): Boolean = delegateProvider.accept(type)
class PropertyPreservingValueProvider(
delegate: JavaValueProvider
) : ValueProviderDecorator<FuzzedType, FuzzedValue, FuzzedDescription>(delegate) {
override fun wrap(provider: JavaValueProvider): JavaValueProvider =
provider.preserveProperties()

override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence<Seed<FuzzedType, FuzzedValue>> {
val delegateSeeds = delegateProvider.generate(description, type)
val delegateSeeds = delegate.generate(description, type)

val preservedProperties = type.properties.entries
.filter { it.property is PreservableFuzzedTypeProperty }
Expand Down Expand Up @@ -67,10 +69,4 @@ class PropertyPreservingValueProvider(private val delegateProvider: JavaValuePro
}
}
}

override fun map(transform: (JavaValueProvider) -> JavaValueProvider): JavaValueProvider =
delegateProvider.map(transform).preserveProperties()

override fun except(filter: (JavaValueProvider) -> Boolean): JavaValueProvider =
delegateProvider.except(filter).preserveProperties()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.utbot.fuzzing.spring.decorators

import org.utbot.fuzzing.Description
import org.utbot.fuzzing.Seed
import org.utbot.fuzzing.ValueProvider

fun <T, R, D : Description<T>> ValueProvider<T, R, D>.filterSeeds(predicate: (Seed<T, R>) -> Boolean) =
SeedFilteringValueProvider(delegate = this, predicate)

class SeedFilteringValueProvider<T, R, D : Description<T>>(
delegate: ValueProvider<T, R, D>,
private val predicate: (Seed<T, R>) -> Boolean
) : ValueProviderDecorator<T, R, D>(delegate) {
override fun wrap(provider: ValueProvider<T, R, D>): ValueProvider<T, R, D> =
provider.filterSeeds(predicate)

override fun generate(description: D, type: T): Sequence<Seed<T, R>> =
delegate.generate(description, type).filter(predicate)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.utbot.fuzzing.spring.decorators

import org.utbot.fuzzing.Description
import org.utbot.fuzzing.ValueProvider

fun <T, R, D : Description<T>> ValueProvider<T, R, D>.filterTypes(predicate: (T) -> Boolean) =
TypeFilteringValueProvider(delegate = this, predicate)

class TypeFilteringValueProvider<T, R, D : Description<T>>(
delegate: ValueProvider<T, R, D>,
private val predicate: (T) -> Boolean
) : ValueProviderDecorator<T, R, D>(delegate) {
override fun wrap(provider: ValueProvider<T, R, D>): ValueProvider<T, R, D> =
provider.filterTypes(predicate)

override fun accept(type: T): Boolean =
predicate(type) && super.accept(type)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.utbot.fuzzing.spring.decorators

import org.utbot.fuzzing.Description
import org.utbot.fuzzing.Scope
import org.utbot.fuzzing.Seed
import org.utbot.fuzzing.ValueProvider

fun <T, R, D : Description<T>> ValueProvider<T, R, D>.replaceTypes(typeReplacer: (D, T) -> T) =
TypeReplacingValueProvider(delegate = this, typeReplacer)

class TypeReplacingValueProvider<T, R, D : Description<T>>(
delegate: ValueProvider<T, R, D>,
private val typeReplacer: (D, T) -> T
) : ValueProviderDecorator<T, R, D>(delegate) {
override fun wrap(provider: ValueProvider<T, R, D>): ValueProvider<T, R, D> =
provider.replaceTypes(typeReplacer)

override fun enrich(description: D, type: T, scope: Scope) =
super.enrich(description, typeReplacer(description, type), scope)

override fun accept(type: T): Boolean = true

override fun generate(description: D, type: T): Sequence<Seed<T, R>> =
if (super.accept(typeReplacer(description, type)))
super.generate(description, typeReplacer(description, type))
else
emptySequence()
}
Loading

0 comments on commit 36c23ff

Please sign in to comment.