Skip to content

Prune branches with unexpected mocks #1889

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 12 commits into from
Mar 13, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -1149,24 +1149,33 @@ open class TypeParameters(val parameters: List<ClassId> = emptyList())
class WildcardTypeParameter : TypeParameters(emptyList())

/**
* Additional data describing user project.
*/
interface ApplicationContext

/**
* A context to use when no additional data is required.
* A context to use when no specific data is required.
*
* @param mockFrameworkInstalled shows if we have installed framework dependencies
* @param staticsMockingIsConfigured shows if we have installed static mocking tools
*/
object EmptyApplicationContext: ApplicationContext
open class StandardApplicationContext(
val mockFrameworkInstalled: Boolean = true,
val staticsMockingIsConfigured: Boolean = true,
) {
init {
if (!mockFrameworkInstalled) {
require(!staticsMockingIsConfigured) { "Static mocking cannot be used without mock framework" }
}
}
}

/**
* Data we get from Spring application context
* to manage engine and code generator behaviour.
*
* @param beanQualifiedNames describes fqn of injected classes
*/
data class SpringApplicationContext(
class SpringApplicationContext(
mockInstalled: Boolean,
staticsMockingIsConfigured: Boolean,
val beanQualifiedNames: List<String> = emptyList(),
): ApplicationContext {
): StandardApplicationContext(mockInstalled, staticsMockingIsConfigured) {
private val springInjectedClasses: List<ClassId> by lazy {
beanQualifiedNames.map { fqn -> utContext.classLoader.loadClass(fqn).id }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,33 @@ package org.utbot.examples.mock
import org.utbot.framework.plugin.api.MockStrategyApi
import org.junit.jupiter.api.Test
import org.utbot.testcheckers.eq
import org.utbot.testing.DoNotCalculate
import org.utbot.testing.UtValueTestCaseChecker
import org.utbot.testing.atLeast

internal class CommonMocksExampleTest: UtValueTestCaseChecker(testClass = CommonMocksExample::class) {

//TODO: coverage values here require further investigation by experts

@Test
fun testMockInterfaceWithoutImplementors() {
fun testMockInterfaceWithoutImplementorsWithNoMocksStrategy() {
checkMocks(
CommonMocksExample::mockInterfaceWithoutImplementors,
eq(1),
{ v, mocks, _ -> v == null && mocks.isEmpty() },
mockStrategy = MockStrategyApi.NO_MOCKS,
coverage = atLeast(75),
)
}

@Test
fun testMockInterfaceWithoutImplementorsWithMockingStrategy() {
checkMocks(
CommonMocksExample::mockInterfaceWithoutImplementors,
eq(2),
{ v, mocks, _ -> v == null && mocks.isEmpty() },
{ _, mocks, _ -> mocks.singleOrNull() != null },
coverage = DoNotCalculate
mockStrategy = MockStrategyApi.OTHER_CLASSES,
coverage = atLeast(75),
)
}

Expand All @@ -27,7 +42,7 @@ internal class CommonMocksExampleTest: UtValueTestCaseChecker(testClass = Common
{ fst, _, mocks, _ -> fst == null && mocks.isEmpty() },
{ _, _, mocks, _ -> mocks.isEmpty() }, // should be changed to not null fst when 1449 will be finished
mockStrategy = MockStrategyApi.OTHER_PACKAGES,
coverage = DoNotCalculate
coverage = atLeast(75)
)
}

Expand All @@ -42,7 +57,7 @@ internal class CommonMocksExampleTest: UtValueTestCaseChecker(testClass = Common
// node == node.next
// node.next.value == node.value + 1
mockStrategy = MockStrategyApi.OTHER_CLASSES,
coverage = DoNotCalculate
coverage = atLeast(13)
)
}

Expand All @@ -53,7 +68,7 @@ internal class CommonMocksExampleTest: UtValueTestCaseChecker(testClass = Common
eq(1),
{ r -> r == -420 },
mockStrategy = MockStrategyApi.OTHER_CLASSES,
coverage = DoNotCalculate
coverage = atLeast(70),
)
}

Expand All @@ -63,7 +78,7 @@ internal class CommonMocksExampleTest: UtValueTestCaseChecker(testClass = Common
CommonMocksExample::mocksForNullOfDifferentTypes,
eq(1),
mockStrategy = MockStrategyApi.OTHER_PACKAGES,
coverage = DoNotCalculate
coverage = atLeast(75)
)
}
}
78 changes: 71 additions & 7 deletions utbot-framework/src/main/kotlin/org/utbot/engine/Mocks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import kotlinx.collections.immutable.persistentListOf
import org.utbot.common.nameOfPackage
import org.utbot.engine.types.OBJECT_TYPE
import org.utbot.engine.util.mockListeners.MockListenerController
import org.utbot.framework.plugin.api.StandardApplicationContext
import org.utbot.framework.plugin.api.util.isInaccessibleViaReflection
import soot.BooleanType
import soot.RefType
Expand Down Expand Up @@ -128,6 +129,36 @@ data class UtStaticMethodMockInfo(
val methodId: MethodId
) : UtMockInfo(methodId.classId, addr)

/**
* A wrapper for [ObjectValue] to store additional info.
*/
sealed class MockedObjectInfo {
abstract val value: ObjectValue?
}

object NoMock: MockedObjectInfo() {
override val value: ObjectValue? = null
}

/**
* Represents a mock that occurs when mock strategy allows it
* or when an object type requires always requires mocking.
*
* See [Mocker.mockAlways] for more details.
*/
class ExpectedMock(objectValue: ObjectValue): MockedObjectInfo() {
override val value: ObjectValue = objectValue
}

/**
* Represents a mock that occurs when it is not allowed.
* E.g. mock framework is not installed or
* mock strategy is [MockStrategy.NO_MOCKS] and class is not in [Mocker.mockAlways] set.
*/
class UnexpectedMock(objectValue: ObjectValue): MockedObjectInfo() {
override val value: ObjectValue = objectValue
}

/**
* Service to mock things. Knows mock strategy, class under test and class hierarchy.
*/
Expand All @@ -137,21 +168,30 @@ class Mocker(
private val hierarchy: Hierarchy,
chosenClassesToMockAlways: Set<ClassId>,
internal val mockListenerController: MockListenerController? = null,
private val applicationContext: StandardApplicationContext,
) {
private val mocksAreDesired: Boolean = strategy != MockStrategy.NO_MOCKS

/**
* Creates mocked instance of the [type] using mock info if it should be mocked by the mocker,
* otherwise returns null.
* Creates mocked instance (if it should be mocked by the mocker) of the [type] using [mockInfo]
* otherwise returns [NoMock].
*
* @see shouldMock
*/
fun mock(type: RefType, mockInfo: UtMockInfo): ObjectValue? =
if (shouldMock(type, mockInfo)) createMockObject(type, mockInfo) else null
fun mock(type: RefType, mockInfo: UtMockInfo): MockedObjectInfo {
val objectValue = if (shouldMock(type, mockInfo)) createMockObject(type, mockInfo) else null
return construct(objectValue, mockInfo)
}

/**
* Creates mocked instance of the [type] using mock info. Unlike to [mock], it does not
* check anything and always returns the constructed mock.
* Unlike to [mock], unconditionally creates a mocked instance of the [type] using [mockInfo].
*/
fun forceMock(type: RefType, mockInfo: UtMockInfo): ObjectValue = createMockObject(type, mockInfo)
fun forceMock(type: RefType, mockInfo: UtMockInfo): MockedObjectInfo {
mockListenerController?.onShouldMock(strategy, mockInfo)

val objectValue = createMockObject(type, mockInfo)
return construct(objectValue, mockInfo)
}

/**
* Checks if Engine should mock objects of particular type with current mock strategy and mock type.
Expand All @@ -177,6 +217,30 @@ class Mocker(
}
}

/**
* Constructs [MockedObjectInfo]: enriches given [mockedValue] with an information if mocking is expected or not.
*/
private fun construct(mockedValue: ObjectValue?, mockInfo: UtMockInfo): MockedObjectInfo {
if (mockedValue == null) {
return NoMock
}

val mockingIsPossible = when (mockInfo) {
is UtFieldMockInfo,
is UtObjectMockInfo -> applicationContext.mockFrameworkInstalled
is UtNewInstanceMockInfo,
is UtStaticMethodMockInfo,
is UtStaticObjectMockInfo -> applicationContext.staticsMockingIsConfigured
}
val mockingIsForcedAndPossible = mockAlways(mockedValue.type) && mockingIsPossible

return if (mocksAreDesired || mockingIsForcedAndPossible) {
ExpectedMock(mockedValue)
} else {
UnexpectedMock(mockedValue)
}
}

private fun checkIfShouldMock(
type: RefType,
mockInfo: UtMockInfo
Expand Down
89 changes: 63 additions & 26 deletions utbot-framework/src/main/kotlin/org/utbot/engine/Traverser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,11 @@ import org.utbot.framework.UtSettings
import org.utbot.framework.UtSettings.maximizeCoverageUsingReflection
import org.utbot.framework.UtSettings.preferredCexOption
import org.utbot.framework.UtSettings.substituteStaticsWithSymbolicVariable
import org.utbot.framework.plugin.api.ApplicationContext
import org.utbot.framework.plugin.api.StandardApplicationContext
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 org.utbot.framework.plugin.api.SpringApplicationContext
import org.utbot.framework.plugin.api.classId
import org.utbot.framework.plugin.api.id
import org.utbot.framework.plugin.api.util.executable
Expand Down Expand Up @@ -240,7 +239,7 @@ class Traverser(
internal val typeResolver: TypeResolver,
private val globalGraph: InterProceduralUnitGraph,
private val mocker: Mocker,
private val applicationContext: ApplicationContext?,
private val applicationContext: StandardApplicationContext?,
) : UtContextInitializer() {

private val visitedStmts: MutableSet<Stmt> = mutableSetOf()
Expand Down Expand Up @@ -277,8 +276,6 @@ class Traverser(

internal val objectCounter = ObjectCounter(TypeRegistry.objectCounterInitialValue)



private fun findNewAddr(insideStaticInitializer: Boolean): UtAddrExpression {
val newAddr = objectCounter.createNewAddr()
// return negative address for objects created inside static initializer
Expand Down Expand Up @@ -1383,8 +1380,17 @@ class Traverser(

queuedSymbolicStateUpdates += MemoryUpdate(addrToMockInfo = persistentHashMapOf(addr to mockInfo))

val mockedObject = mocker.mock(type, mockInfo)
val mockedObjectInfo = mocker.mock(type, mockInfo)

if (mockedObjectInfo is UnexpectedMock) {
// if mock occurs, but it is unexpected due to some reasons
// (e.g. we do not have mock framework installed),
// we can only generate a test that uses null value for mocked object
queuedSymbolicStateUpdates += nullEqualityConstraint.asHardConstraint()
return mockedObjectInfo.value
}

val mockedObject = mockedObjectInfo.value
if (mockedObject != null) {
queuedSymbolicStateUpdates += MemoryUpdate(mockInfos = persistentListOf(MockInfoEnriched(mockInfo)))

Expand Down Expand Up @@ -1470,7 +1476,24 @@ class Traverser(
}

val mockInfo = mockInfoGenerator.generate(addr)
val mockedObject = mocker.forceMock(type, mockInfoGenerator.generate(addr))
val mockedObjectInfo = mocker.forceMock(type, mockInfoGenerator.generate(addr))

val mockedObject: ObjectValue = when (mockedObjectInfo) {
is NoMock -> error("Value must be mocked after the force mock")
is ExpectedMock -> mockedObjectInfo.value
is UnexpectedMock -> {
// if mock occurs, but it is unexpected due to some reasons
// (e.g. we do not have mock framework installed),
// we can only generate a test that uses null value for mocked object
queuedSymbolicStateUpdates += nullEqualityConstraint.asHardConstraint()

mockedObjectInfo.value
}
}

if (mockedObjectInfo is UnexpectedMock) {
return mockedObject
}

queuedSymbolicStateUpdates += MemoryUpdate(mockInfos = persistentListOf(MockInfoEnriched(mockInfo)))

Expand Down Expand Up @@ -2675,7 +2698,7 @@ class Traverser(
.map { (method, implementationClass, possibleTypes) ->
val typeStorage = typeResolver.constructTypeStorage(implementationClass, possibleTypes)
val mockInfo = memory.mockInfoByAddr(instance.addr)
val mockedObject = mockInfo?.let {
val mockedObjectInfo = mockInfo?.let {
// TODO rewrite to fix JIRA:1611
val type = Scene.v().getSootClass(mockInfo.classId.name).type
val ancestorTypes = typeResolver.findOrConstructAncestorsIncludingTypes(type)
Expand All @@ -2684,28 +2707,42 @@ class Traverser(
} else {
it.copyWithClassId(classId = implementationClass.id)
}
mocker.mock(implementationClass, updatedMockInfo)
}

if (mockedObject == null) {
// Above we might get implementationClass that has to be substituted.
// For example, for a call "Collection.size()" such classes will be produced.
val wrapperOrInstance = wrapper(implementationClass, instance.addr)
?: instance.copy(typeStorage = typeStorage)

val typeConstraint = typeRegistry.typeConstraint(instance.addr, wrapperOrInstance.typeStorage)
val constraints = setOf(typeConstraint.isOrNullConstraint())
mocker.mock(implementationClass, updatedMockInfo)
} ?: NoMock

// TODO add memory updated for types JIRA:1523
when (mockedObjectInfo) {
is NoMock -> {
// Above we might get implementationClass that has to be substituted.
// For example, for a call "Collection.size()" such classes will be produced.
val wrapperOrInstance = wrapper(implementationClass, instance.addr)
?: instance.copy(typeStorage = typeStorage)

InvocationTarget(wrapperOrInstance, method, constraints)
} else {
val typeConstraint = typeRegistry.typeConstraint(mockedObject.addr, mockedObject.typeStorage)
val constraints = setOf(typeConstraint.isOrNullConstraint())
val typeConstraint = typeRegistry.typeConstraint(instance.addr, wrapperOrInstance.typeStorage)
val constraints = setOf(typeConstraint.isOrNullConstraint())

// TODO add memory updated for types JIRA:1523
// TODO isMock????
InvocationTarget(mockedObject, method, constraints)
// TODO add memory updated for types JIRA:1523
InvocationTarget(wrapperOrInstance, method, constraints)
}
is ExpectedMock -> {
val mockedObject = mockedObjectInfo.value
val typeConstraint = typeRegistry.typeConstraint(mockedObject.addr, mockedObject.typeStorage)
val constraints = setOf(typeConstraint.isOrNullConstraint())

// TODO add memory updated for types JIRA:1523
// TODO isMock????
InvocationTarget(mockedObject, method, constraints)
}
/*
Currently, it is unclear how this could happen.
Perhaps, the answer is somewhere in the following situation:
you have an interface with an abstract method `foo`, and it has an abstract inheritor with the implementation of the method,
but this inheritor doesn't have any concrete inheritors. It looks like in this case we would mock this instance
(because it doesn't have any possible concrete type), but it is impossible since either this class cannot present
in possible types of the object on which we call `foo` (since they contain only concrete types),
or this class would be already mocked (since it doesn't contain any concrete implementors).
*/
is UnexpectedMock -> unreachableBranch("If it ever happens, it should be investigated")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class UtBotSymbolicEngine(
dependencyPaths: String,
val mockStrategy: MockStrategy = NO_MOCKS,
chosenClassesToMockAlways: Set<ClassId>,
applicationContext: ApplicationContext?,
applicationContext: StandardApplicationContext,
private val solverTimeoutInMillis: Int = checkSolverTimeoutMillis
) : UtContextInitializer() {
private val graph = methodUnderTest.sootMethod.jimpleBody().apply {
Expand All @@ -129,7 +129,8 @@ class UtBotSymbolicEngine(
classUnderTest,
hierarchy,
chosenClassesToMockAlways,
MockListenerController(controller)
MockListenerController(controller),
applicationContext = applicationContext,
)

fun attachMockListener(mockListener: MockListener) = mocker.mockListenerController?.attach(mockListener)
Expand Down
Loading