Skip to content

Commit 1ac3a04

Browse files
Nikita Vlaevnikitavlaev
Nikita Vlaev
authored andcommitted
Force mocking without mock framework installed fix
Added mock listeners for engine. Added force mock listener. Added url handler for urls like "#utbot/...". Added message with link to mock framework configuration to test reports if force mocking happened.
1 parent 17126eb commit 1ac3a04

File tree

15 files changed

+248
-60
lines changed

15 files changed

+248
-60
lines changed

utbot-framework/src/main/kotlin/org/utbot/engine/Mocks.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import java.util.concurrent.atomic.AtomicInteger
1818
import kotlin.reflect.KFunction2
1919
import kotlin.reflect.KFunction5
2020
import kotlinx.collections.immutable.persistentListOf
21+
import org.utbot.engine.util.mockListeners.MockListenerController
2122
import soot.BooleanType
2223
import soot.RefType
2324
import soot.Scene
@@ -133,7 +134,8 @@ class Mocker(
133134
private val strategy: MockStrategy,
134135
private val classUnderTest: ClassId,
135136
private val hierarchy: Hierarchy,
136-
chosenClassesToMockAlways: Set<ClassId>
137+
chosenClassesToMockAlways: Set<ClassId>,
138+
internal val mockListenerController: MockListenerController? = null,
137139
) {
138140
/**
139141
* Creates mocked instance of the [type] using mock info if it should be mocked by the mocker,
@@ -164,6 +166,11 @@ class Mocker(
164166
* For others, if mock is not a new instance mock, asks mock strategy for decision.
165167
*/
166168
fun shouldMock(
169+
type: RefType,
170+
mockInfo: UtMockInfo,
171+
): Boolean = checkIfShouldMock(type, mockInfo).also { if (it) mockListenerController?.onShouldMock(strategy) }
172+
173+
private fun checkIfShouldMock(
167174
type: RefType,
168175
mockInfo: UtMockInfo
169176
): Boolean {

utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@ import kotlinx.collections.immutable.persistentSetOf
66
import kotlinx.collections.immutable.toPersistentList
77
import kotlinx.collections.immutable.toPersistentMap
88
import kotlinx.collections.immutable.toPersistentSet
9-
import kotlinx.coroutines.currentCoroutineContext
109
import kotlinx.coroutines.CancellationException
1110
import kotlinx.coroutines.Job
11+
import kotlinx.coroutines.currentCoroutineContext
12+
import kotlinx.coroutines.ensureActive
1213
import kotlinx.coroutines.flow.Flow
1314
import kotlinx.coroutines.flow.FlowCollector
1415
import kotlinx.coroutines.flow.flow
1516
import kotlinx.coroutines.flow.onCompletion
1617
import kotlinx.coroutines.flow.onStart
1718
import kotlinx.coroutines.isActive
19+
import kotlinx.coroutines.job
1820
import kotlinx.coroutines.yield
1921
import mu.KotlinLogging
2022
import org.utbot.analytics.EngineAnalyticsContext
@@ -103,6 +105,8 @@ import org.utbot.engine.symbolic.asHardConstraint
103105
import org.utbot.engine.symbolic.asSoftConstraint
104106
import org.utbot.engine.symbolic.asAssumption
105107
import org.utbot.engine.symbolic.asUpdate
108+
import org.utbot.engine.util.mockListeners.MockListener
109+
import org.utbot.engine.util.mockListeners.MockListenerController
106110
import org.utbot.engine.util.statics.concrete.associateEnumSootFieldsWithConcreteValues
107111
import org.utbot.engine.util.statics.concrete.isEnumAffectingExternalStatics
108112
import org.utbot.engine.util.statics.concrete.isEnumValuesFieldName
@@ -330,7 +334,7 @@ class UtBotSymbolicEngine(
330334

331335
private val classUnderTest: ClassId = methodUnderTest.clazz.id
332336

333-
private val mocker: Mocker = Mocker(mockStrategy, classUnderTest, hierarchy, chosenClassesToMockAlways)
337+
private val mocker: Mocker = Mocker(mockStrategy, classUnderTest, hierarchy, chosenClassesToMockAlways, MockListenerController(controller))
334338

335339
private val statesForConcreteExecution: MutableList<ExecutionState> = mutableListOf()
336340

@@ -541,6 +545,10 @@ class UtBotSymbolicEngine(
541545
} else {
542546
traverseStmt(currentStmt)
543547
}
548+
549+
// Here job can be cancelled from within traverse, e.g. by using force mocking without Mockito.
550+
// So we need to make it throw CancelledException by method below:
551+
currentCoroutineContext().job.ensureActive()
544552
} catch (ex: Throwable) {
545553
environment.state.close()
546554

@@ -2521,6 +2529,8 @@ class UtBotSymbolicEngine(
25212529
)
25222530
}
25232531

2532+
fun attachMockListener(mockListener: MockListener) = mocker.mockListenerController?.attach(mockListener)
2533+
25242534
private fun staticInvoke(invokeExpr: JStaticInvokeExpr): List<MethodResult> {
25252535
val parameters = resolveParameters(invokeExpr.args, invokeExpr.method.parameterTypes)
25262536
val result = mockMakeSymbolic(invokeExpr) ?: mockStaticMethod(invokeExpr.method, parameters)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.utbot.engine.util.mockListeners
2+
import org.utbot.engine.EngineController
3+
import org.utbot.engine.MockStrategy
4+
import org.utbot.engine.util.mockListeners.exceptions.ForceMockCancellationException
5+
6+
/**
7+
* Listener for mocker events in [org.utbot.engine.UtBotSymbolicEngine].
8+
* If forced mock happened, cancels the engine job.
9+
*
10+
* Supposed to be created only if Mockito is not installed.
11+
*/
12+
class ForceMockListener: MockListener {
13+
var forceMockHappened = false
14+
private set
15+
16+
override fun onShouldMock(controller: EngineController, strategy: MockStrategy) {
17+
// If force mocking happened -- сancel engine job
18+
controller.job?.cancel(ForceMockCancellationException())
19+
forceMockHappened = true
20+
}
21+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.utbot.engine.util.mockListeners
2+
3+
import org.utbot.engine.EngineController
4+
import org.utbot.engine.MockStrategy
5+
6+
/**
7+
* Listener that can be attached using [MockListenerController] to mocker in [org.utbot.engine.UtBotSymbolicEngine].
8+
*/
9+
interface MockListener {
10+
fun onShouldMock(controller: EngineController, strategy: MockStrategy)
11+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.utbot.engine.util.mockListeners
2+
3+
import org.utbot.engine.EngineController
4+
import org.utbot.engine.MockStrategy
5+
6+
/**
7+
* Controller that allows to attach listeners to mocker in [org.utbot.engine.UtBotSymbolicEngine].
8+
*/
9+
class MockListenerController(private val controller: EngineController) {
10+
val listeners = mutableListOf<MockListener>()
11+
12+
fun attach(listener: MockListener) {
13+
listeners += listener
14+
}
15+
16+
fun onShouldMock(strategy: MockStrategy) {
17+
listeners.map { it.onShouldMock(controller, strategy) }
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.utbot.engine.util.mockListeners.exceptions
2+
3+
import kotlinx.coroutines.CancellationException
4+
5+
/**
6+
* Exception used in [org.utbot.engine.util.mockListeners.ForceMockListener].
7+
*/
8+
class ForceMockCancellationException: CancellationException("Forced mocks without Mockito")

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ data class TestsGenerationReport(
188188
val classUnderTest: KClass<*>
189189
get() = executables.firstOrNull()?.clazz ?: error("No executables found in test report")
190190

191+
// Initial message is generated lazily to avoid evaluation of classUnderTest
192+
var initialMessage: () -> String = { "Unit tests for $classUnderTest were generated successfully." }
193+
191194
fun addMethodErrors(testCase: UtTestCase, errors: Map<String, Int>) {
192195
this.errors[testCase.method] = errors
193196
}
@@ -212,7 +215,8 @@ data class TestsGenerationReport(
212215
}
213216

214217
override fun toString(): String = buildString {
215-
appendHtmlLine("Unit tests for $classUnderTest were generated successfully.")
218+
appendHtmlLine(initialMessage())
219+
appendHtmlLine()
216220
val testMethodsStatistic = executables.map { it.countTestMethods() }
217221
val errors = executables.map { it.countErrors() }
218222
val overallTestMethods = testMethodsStatistic.sumBy { it.count }

utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/UtBotTestCaseGenerator.kt

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ object UtBotTestCaseGenerator : TestCaseGenerator {
5858
private val logger = KotlinLogging.logger {}
5959
private val timeoutLogger = KotlinLogging.logger(logger.name + ".timeout")
6060

61+
lateinit var configureEngine: (UtBotSymbolicEngine) -> Unit
6162
lateinit var isCanceled: () -> Boolean
6263

6364
//properties to save time on soot initialization
@@ -71,8 +72,23 @@ object UtBotTestCaseGenerator : TestCaseGenerator {
7172
classpath: String?,
7273
dependencyPaths: String,
7374
isCanceled: () -> Boolean
75+
) = init(
76+
buildDir,
77+
classpath,
78+
dependencyPaths,
79+
configureEngine = {},
80+
isCanceled
81+
)
82+
83+
fun init(
84+
buildDir: Path,
85+
classpath: String?,
86+
dependencyPaths: String,
87+
configureEngine: (UtBotSymbolicEngine) -> Unit,
88+
isCanceled: () -> Boolean
7489
) {
7590
this.isCanceled = isCanceled
91+
this.configureEngine = configureEngine
7692
if (isCanceled()) return
7793

7894
checkFrameworkDependencies(dependencyPaths)
@@ -268,13 +284,15 @@ object UtBotTestCaseGenerator : TestCaseGenerator {
268284
//yield one to
269285
yield()
270286

271-
generate(createSymbolicEngine(
287+
val engine: UtBotSymbolicEngine = createSymbolicEngine(
272288
controller,
273289
method,
274290
mockStrategy,
275291
chosenClassesToMockAlways,
276292
executionTimeEstimator
277-
)).collect {
293+
).apply(configureEngine)
294+
295+
generate(engine).collect {
278296
when (it) {
279297
is UtExecution -> method2executions.getValue(method) += it
280298
is UtError -> method2errors.getValue(method).merge(it.description, 1, Int::plus)
@@ -364,7 +382,6 @@ object UtBotTestCaseGenerator : TestCaseGenerator {
364382
val executions = mutableListOf<UtExecution>()
365383
val errors = mutableMapOf<String, Int>()
366384

367-
368385
runIgnoringCancellationException {
369386
runBlockingWithCancellationPredicate(isCanceled) {
370387
generateAsync(EngineController(), method, mockStrategy).collect {

utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerator.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import com.intellij.openapi.diagnostic.Logger
1414
import com.intellij.openapi.project.Project
1515
import com.intellij.psi.PsiMethod
1616
import com.intellij.refactoring.util.classMembers.MemberInfo
17+
import org.utbot.engine.UtBotSymbolicEngine
1718
import java.nio.file.Path
1819
import java.nio.file.Paths
1920
import kotlin.reflect.KClass
@@ -32,14 +33,15 @@ class CodeGenerator(
3233
buildDir: String,
3334
classpath: String,
3435
pluginJarsPath: String,
35-
isCanceled: () -> Boolean
36+
configureEngine: (UtBotSymbolicEngine) -> Unit = {},
37+
isCanceled: () -> Boolean,
3638
) {
3739
init {
3840
UtSettings.testMinimizationStrategyType = TestSelectionStrategyType.COVERAGE_STRATEGY
3941
}
4042

41-
private val generator = project.service<Settings>().testCasesGenerator.apply {
42-
init(Paths.get(buildDir), classpath, pluginJarsPath, isCanceled)
43+
val generator = (project.service<Settings>().testCasesGenerator as UtBotTestCaseGenerator).apply {
44+
init(Paths.get(buildDir), classpath, pluginJarsPath, configureEngine, isCanceled)
4345
}
4446

4547
private val settingsState = project.service<Settings>().state
@@ -49,7 +51,7 @@ class CodeGenerator(
4951
fun generateForSeveralMethods(methods: List<UtMethod<*>>, timeout:Long = UtSettings.utBotGenerationTimeoutInMillis): List<UtTestCase> {
5052
logger.info("Tests generating parameters $settingsState")
5153

52-
return (generator as UtBotTestCaseGenerator)
54+
return generator
5355
.generateForSeveralMethods(methods, mockStrategy, chosenClassesToMockAlways, methodsGenerationTimeout = timeout)
5456
.map { it.summarize(searchDirectory) }
5557
}

utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/TestGenerator.kt

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ import org.utbot.framework.plugin.api.UtTestCase
1515
import org.utbot.intellij.plugin.sarif.SarifReportIdea
1616
import org.utbot.intellij.plugin.sarif.SourceFindingStrategyIdea
1717
import org.utbot.intellij.plugin.settings.Settings
18-
import org.utbot.intellij.plugin.ui.GenerateTestsModel
19-
import org.utbot.intellij.plugin.ui.SarifReportNotifier
20-
import org.utbot.intellij.plugin.ui.TestsReportNotifier
21-
import org.utbot.intellij.plugin.ui.packageName
2218
import org.utbot.intellij.plugin.ui.utils.getOrCreateSarifReportsPath
2319
import org.utbot.intellij.plugin.ui.utils.getOrCreateTestResourcesPath
2420
import org.utbot.sarif.SarifReport
@@ -63,6 +59,11 @@ import org.jetbrains.kotlin.psi.psiUtil.endOffset
6359
import org.jetbrains.kotlin.psi.psiUtil.startOffset
6460
import org.jetbrains.kotlin.scripting.resolve.classId
6561
import org.utbot.intellij.plugin.error.showErrorDialogLater
62+
import org.utbot.intellij.plugin.ui.GenerateTestsModel
63+
import org.utbot.intellij.plugin.ui.SarifReportNotifier
64+
import org.utbot.intellij.plugin.ui.TestReportUrlOpeningListener
65+
import org.utbot.intellij.plugin.ui.TestsReportNotifier
66+
import org.utbot.intellij.plugin.ui.packageName
6667

6768
object TestGenerator {
6869
fun generateTests(model: GenerateTestsModel, testCases: Map<PsiClass, List<UtTestCase>>) {
@@ -317,6 +318,17 @@ object TestGenerator {
317318
VfsUtil.createDirectories(parent.toString())
318319
resultedReportedPath.toFile().writeText(testsCodeWithTestReport.testsGenerationReport.getFileContent())
319320

321+
if (model.forceMockHappened) {
322+
testsCodeWithTestReport.testsGenerationReport.apply {
323+
initialMessage = {
324+
"""
325+
Unit tests for $classUnderTest were generated partially.<br>
326+
<b>Warning</b>: Some test cases were ignored, because no mocking framework is installed in the project.<br>
327+
Better results could be achieved by <a href="${TestReportUrlOpeningListener.prefix}${TestReportUrlOpeningListener.mockitoSuffix}">installing mocking framework</a>.
328+
""".trimIndent()
329+
}
330+
}
331+
}
320332
val notifyMessage = buildString {
321333
appendHtmlLine(testsCodeWithTestReport.testsGenerationReport.toString())
322334
appendHtmlLine()
@@ -334,7 +346,7 @@ object TestGenerator {
334346
""".trimIndent()
335347
appendHtmlLine(savedFileMessage)
336348
}
337-
TestsReportNotifier.notify(notifyMessage)
349+
TestsReportNotifier.notify(notifyMessage, model.project, model.testModule)
338350
}
339351

340352
@Suppress("unused")
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.utbot.intellij.plugin.ui
2+
3+
import com.intellij.openapi.module.Module
4+
import com.intellij.openapi.project.Project
5+
import com.intellij.openapi.roots.DependencyScope
6+
import com.intellij.openapi.roots.ExternalLibraryDescriptor
7+
import com.intellij.openapi.roots.JavaProjectModelModificationService
8+
import com.intellij.openapi.ui.Messages
9+
import org.jetbrains.concurrency.Promise
10+
import org.utbot.framework.plugin.api.MockFramework
11+
import org.utbot.intellij.plugin.ui.utils.LibrarySearchScope
12+
import org.utbot.intellij.plugin.ui.utils.findFrameworkLibrary
13+
import org.utbot.intellij.plugin.ui.utils.parseVersion
14+
15+
fun createMockFrameworkNotificationDialog(title: String) = Messages.showYesNoDialog(
16+
"""Mock framework ${MockFramework.MOCKITO.displayName} is not installed into current module.
17+
|Would you like to install it now?""".trimMargin(),
18+
title,
19+
"Yes",
20+
"No",
21+
Messages.getQuestionIcon(),
22+
)
23+
24+
fun configureMockFramework(project: Project, module: Module) {
25+
val selectedMockFramework = MockFramework.MOCKITO
26+
27+
val libraryInProject =
28+
findFrameworkLibrary(project, module, selectedMockFramework, LibrarySearchScope.Project)
29+
val versionInProject = libraryInProject?.libraryName?.parseVersion()
30+
31+
selectedMockFramework.isInstalled = true
32+
addDependency(project, module, mockitoCoreLibraryDescriptor(versionInProject))
33+
.onError { selectedMockFramework.isInstalled = false }
34+
}
35+
36+
/**
37+
* Adds the dependency for selected framework via [JavaProjectModelModificationService].
38+
*
39+
* Note that version restrictions will be applied only if they are present on target machine
40+
* Otherwise latest release version will be installed.
41+
*/
42+
fun addDependency(project: Project, module: Module, libraryDescriptor: ExternalLibraryDescriptor): Promise<Void> {
43+
return JavaProjectModelModificationService
44+
.getInstance(project)
45+
//this method returns JetBrains internal Promise that is difficult to deal with, but it is our way
46+
.addDependency(module, libraryDescriptor, DependencyScope.TEST)
47+
}

0 commit comments

Comments
 (0)