Skip to content

Commit 309be9f

Browse files
authored
Add support for nested classes to UI #649 (#726)
1 parent 9431dd8 commit 309be9f

File tree

10 files changed

+147
-104
lines changed

10 files changed

+147
-104
lines changed

utbot-core/src/main/kotlin/org/utbot/common/KClassUtil.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package org.utbot.common
22

33
import java.lang.reflect.InvocationTargetException
44
import java.lang.reflect.Method
5-
5+
import kotlin.reflect.KClass
66

77
val Class<*>.packageName: String get() = `package`?.name?:""
88

@@ -11,3 +11,6 @@ fun Method.invokeCatching(obj: Any?, args: List<Any?>) = try {
1111
} catch (e: InvocationTargetException) {
1212
Result.failure<Nothing>(e.targetException)
1313
}
14+
15+
val KClass<*>.allNestedClasses: List<KClass<*>>
16+
get() = listOf(this) + nestedClasses.flatMap { it.allNestedClasses }

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import org.jetbrains.kotlin.psi.psiUtil.startOffset
4444
import org.jetbrains.kotlin.scripting.resolve.classId
4545
import org.utbot.common.HTML_LINE_SEPARATOR
4646
import org.utbot.common.PathUtil.toHtmlLinkTag
47+
import org.utbot.common.allNestedClasses
4748
import org.utbot.common.appendHtmlLine
4849
import org.utbot.framework.codegen.Import
4950
import org.utbot.framework.codegen.ParametrizedTestSource
@@ -70,6 +71,7 @@ import org.utbot.intellij.plugin.ui.WarningTestsReportNotifier
7071
import org.utbot.intellij.plugin.ui.utils.getOrCreateSarifReportsPath
7172
import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater
7273
import org.utbot.intellij.plugin.util.RunConfigurationHelper
74+
import org.utbot.intellij.plugin.util.extractClassMethodsIncludingNested
7375
import org.utbot.intellij.plugin.util.signature
7476
import org.utbot.sarif.SarifReport
7577
import java.nio.file.Path
@@ -82,7 +84,11 @@ import org.utbot.intellij.plugin.util.IntelliJApiHelper.run
8284

8385
object CodeGenerationController {
8486

85-
fun generateTests(model: GenerateTestsModel, testSetsByClass: Map<PsiClass, List<UtMethodTestSet>>) {
87+
fun generateTests(
88+
model: GenerateTestsModel,
89+
testSetsByClass: Map<PsiClass, List<UtMethodTestSet>>,
90+
psi2KClass: Map<PsiClass, KClass<*>>
91+
) {
8692
val baseTestDirectory = model.testSourceRoot?.toPsiDirectory(model.project)
8793
?: return
8894
val allTestPackages = getPackageDirectories(baseTestDirectory)
@@ -98,9 +104,10 @@ object CodeGenerationController {
98104
val testDirectory = allTestPackages[classPackageName] ?: baseTestDirectory
99105
val testClass = createTestClass(srcClass, testDirectory, model) ?: continue
100106
val file = testClass.containingFile
107+
val cut = psi2KClass[srcClass] ?: error("Didn't find KClass instance for class ${srcClass.name}")
101108
runWriteCommandAction(model.project, "Generate tests with UtBot", null, {
102109
try {
103-
generateCodeAndReport(srcClass, testClass, file, testSets, model, latch, reports)
110+
generateCodeAndReport(srcClass, cut, testClass, file, testSets, model, latch, reports)
104111
testFiles.add(file)
105112
} catch (e: IncorrectOperationException) {
106113
showCreatingClassError(model.project, createTestClassName(srcClass))
@@ -240,15 +247,15 @@ object CodeGenerationController {
240247

241248
private fun generateCodeAndReport(
242249
srcClass: PsiClass,
250+
classUnderTest: KClass<*>,
243251
testClass: PsiClass,
244252
file: PsiFile,
245253
testSets: List<UtMethodTestSet>,
246254
model: GenerateTestsModel,
247255
reportsCountDown: CountDownLatch,
248256
reports: MutableList<TestsGenerationReport>,
249257
) {
250-
val classUnderTest = testSets.first().method.clazz
251-
val classMethods = TestIntegrationUtils.extractClassMethods(srcClass, false)
258+
val classMethods = srcClass.extractClassMethodsIncludingNested(false)
252259
val paramNames = DumbService.getInstance(model.project)
253260
.runReadActionInSmartMode(Computable { findMethodParamNames(classUnderTest, classMethods) })
254261

@@ -339,7 +346,7 @@ object CodeGenerationController {
339346

340347
private fun findMethodParamNames(clazz: KClass<*>, methods: List<MemberInfo>): Map<ExecutableId, List<String>> {
341348
val bySignature = methods.associate { it.signature() to it.paramNames() }
342-
return clazz.functions
349+
return clazz.allNestedClasses.flatMap { it.functions }
343350
.mapNotNull { method -> bySignature[method.signature()]?.let { params -> method.executableId to params } }
344351
.toMap()
345352
}

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

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,14 @@ import com.intellij.openapi.ui.Messages
1919
import com.intellij.openapi.util.Computable
2020
import com.intellij.openapi.util.text.StringUtil
2121
import com.intellij.psi.PsiClass
22-
import com.intellij.psi.SyntheticElement
22+
import com.intellij.psi.PsiMethod
2323
import com.intellij.refactoring.util.classMembers.MemberInfo
24-
import com.intellij.testIntegration.TestIntegrationUtils
2524
import com.intellij.util.concurrency.AppExecutorUtil
2625
import mu.KotlinLogging
2726
import org.jetbrains.kotlin.idea.util.module
2827
import org.utbot.analytics.EngineAnalyticsContext
2928
import org.utbot.analytics.Predictors
30-
import org.utbot.common.filterWhen
29+
import org.utbot.common.allNestedClasses
3130
import org.utbot.engine.util.mockListeners.ForceMockListener
3231
import org.utbot.framework.plugin.services.JdkInfoService
3332
import org.utbot.framework.UtSettings
@@ -56,11 +55,11 @@ import java.util.concurrent.TimeUnit
5655
import org.utbot.engine.util.mockListeners.ForceStaticMockListener
5756
import org.utbot.framework.PathSelectorType
5857
import org.utbot.framework.plugin.services.WorkingDirService
58+
import org.utbot.intellij.plugin.models.packageName
5959
import org.utbot.intellij.plugin.settings.Settings
60+
import org.utbot.intellij.plugin.util.extractClassMethodsIncludingNested
6061
import org.utbot.intellij.plugin.ui.utils.isBuildWithGradle
61-
import org.utbot.intellij.plugin.ui.utils.suitableTestSourceRoots
6262
import org.utbot.intellij.plugin.util.PluginWorkingDirProvider
63-
import org.utbot.intellij.plugin.util.isAbstract
6463
import kotlin.reflect.KClass
6564
import kotlin.reflect.full.functions
6665

@@ -71,16 +70,18 @@ object UtTestsDialogProcessor {
7170
fun createDialogAndGenerateTests(
7271
project: Project,
7372
srcClasses: Set<PsiClass>,
73+
extractMembersFromSrcClasses: Boolean,
7474
focusedMethod: MemberInfo?,
7575
) {
76-
createDialog(project, srcClasses, focusedMethod)?.let {
76+
createDialog(project, srcClasses, extractMembersFromSrcClasses, focusedMethod)?.let {
7777
if (it.showAndGet()) createTests(project, it.model)
7878
}
7979
}
8080

8181
private fun createDialog(
8282
project: Project,
8383
srcClasses: Set<PsiClass>,
84+
extractMembersFromSrcClasses: Boolean,
8485
focusedMethod: MemberInfo?,
8586
): GenerateTestsDialogWindow? {
8687
val srcModule = findSrcModule(srcClasses)
@@ -105,6 +106,7 @@ object UtTestsDialogProcessor {
105106
srcModule,
106107
testModules,
107108
srcClasses,
109+
extractMembersFromSrcClasses,
108110
if (focusedMethod != null) setOf(focusedMethod) else null,
109111
UtSettings.utBotGenerationTimeoutInMillis,
110112
)
@@ -145,6 +147,7 @@ object UtTestsDialogProcessor {
145147
val context = UtContext(classLoader)
146148

147149
val testSetsByClass = mutableMapOf<PsiClass, List<UtMethodTestSet>>()
150+
val psi2KClass = mutableMapOf<PsiClass, KClass<*>>()
148151
var processedClasses = 0
149152
val totalClasses = model.srcClasses.size
150153

@@ -159,18 +162,23 @@ object UtTestsDialogProcessor {
159162

160163
for (srcClass in model.srcClasses) {
161164
val methods = ReadAction.nonBlocking<List<UtMethod<*>>> {
162-
val clazz = classLoader.loadClass(srcClass.qualifiedName).kotlin
163-
val srcMethods =
164-
model.selectedMethods?.toList() ?: TestIntegrationUtils.extractClassMethods(
165-
srcClass,
166-
false
167-
)
168-
.filterWhen(UtSettings.skipTestGenerationForSyntheticMethods) {
169-
it.member !is SyntheticElement
170-
}
171-
.filterNot { it.isAbstract }
165+
val canonicalName = srcClass.canonicalName
166+
val clazz = classLoader.loadClass(canonicalName).kotlin
167+
psi2KClass[srcClass] = clazz
168+
169+
val srcMethods = if (model.extractMembersFromSrcClasses) {
170+
val chosenMethods = model.selectedMembers?.filter { it.member is PsiMethod } ?: listOf()
171+
val chosenNestedClasses = model.selectedMembers?.mapNotNull { it.member as? PsiClass } ?: listOf()
172+
chosenMethods + chosenNestedClasses.flatMap {
173+
it.extractClassMethodsIncludingNested(false)
174+
}
175+
} else {
176+
srcClass.extractClassMethodsIncludingNested(false)
177+
}
172178
DumbService.getInstance(project).runReadActionInSmartMode(Computable {
173-
findMethodsInClassMatchingSelected(clazz, srcMethods)
179+
clazz.allNestedClasses.flatMap {
180+
findMethodsInClassMatchingSelected(it, srcMethods)
181+
}
174182
})
175183
}.executeSynchronously()
176184

@@ -264,7 +272,7 @@ object UtTestsDialogProcessor {
264272

265273
invokeLater {
266274
withUtContext(context) {
267-
generateTests(model, testSetsByClass)
275+
generateTests(model, testSetsByClass, psi2KClass)
268276
}
269277
}
270278
}
@@ -273,6 +281,19 @@ object UtTestsDialogProcessor {
273281
}
274282
}
275283

284+
private val PsiClass.canonicalName: String
285+
get() {
286+
return if (packageName.isEmpty()) {
287+
qualifiedName?.replace(".", "$") ?: ""
288+
} else {
289+
val name = qualifiedName
290+
?.substringAfter("$packageName.")
291+
?.replace(".", "$")
292+
?: ""
293+
"$packageName.$name"
294+
}
295+
}
296+
276297
/**
277298
* Configures utbot-analytics models for the better path selection.
278299
*

utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ data class GenerateTestsModel(
2626
val srcModule: Module,
2727
val potentialTestModules: List<Module>,
2828
var srcClasses: Set<PsiClass>,
29-
var selectedMethods: Set<MemberInfo>?,
30-
var timeout:Long,
29+
val extractMembersFromSrcClasses: Boolean,
30+
var selectedMembers: Set<MemberInfo>?, // TODO: maybe we should make it not nullable?
31+
var timeout: Long,
3132
var generateWarningsForStaticMocking: Boolean = false,
3233
var fuzzingValue: Double = 0.05
3334
) {
@@ -36,6 +37,7 @@ data class GenerateTestsModel(
3637
var testModule: Module = potentialTestModules.firstOrNull() ?: error("Empty list of test modules in model")
3738

3839
var testSourceRoot: VirtualFile? = null
40+
3941
fun setSourceRootAndFindTestModule(newTestSourceRoot: VirtualFile?) {
4042
requireNotNull(newTestSourceRoot)
4143
testSourceRoot = newTestSourceRoot

utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt

Lines changed: 17 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,11 @@ import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile
3939
import com.intellij.openapi.wm.ToolWindowManager
4040
import com.intellij.psi.PsiClass
4141
import com.intellij.psi.PsiManager
42-
import com.intellij.psi.PsiMethod
43-
import com.intellij.psi.SyntheticElement
4442
import com.intellij.refactoring.PackageWrapper
4543
import com.intellij.refactoring.ui.MemberSelectionTable
4644
import com.intellij.refactoring.ui.PackageNameReferenceEditorCombo
4745
import com.intellij.refactoring.util.RefactoringUtil
4846
import com.intellij.refactoring.util.classMembers.MemberInfo
49-
import com.intellij.testIntegration.TestIntegrationUtils
5047
import com.intellij.ui.ColoredListCellRenderer
5148
import com.intellij.ui.ContextHelpLabel
5249
import com.intellij.ui.HyperlinkLabel
@@ -97,7 +94,6 @@ import org.jetbrains.concurrency.Promise
9794
import org.jetbrains.concurrency.thenRun
9895
import org.jetbrains.kotlin.asJava.classes.KtUltraLightClass
9996
import org.utbot.common.PathUtil.toPath
100-
import org.utbot.common.filterWhen
10197
import org.utbot.framework.UtSettings
10298
import org.utbot.framework.codegen.ForceStaticMocking
10399
import org.utbot.framework.codegen.Junit4
@@ -136,7 +132,7 @@ import org.utbot.intellij.plugin.ui.utils.parseVersion
136132
import org.utbot.intellij.plugin.ui.utils.testResourceRootTypes
137133
import org.utbot.intellij.plugin.ui.utils.testRootType
138134
import org.utbot.intellij.plugin.util.IntelliJApiHelper
139-
import org.utbot.intellij.plugin.util.isAbstract
135+
import org.utbot.intellij.plugin.util.extractFirstLevelMembers
140136

141137
private const val RECENTS_KEY = "org.utbot.recents"
142138

@@ -384,17 +380,14 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m
384380
private fun updateMembersTable() {
385381
val srcClasses = model.srcClasses
386382

387-
val items: List<MemberInfo>
388-
if (srcClasses.size == 1) {
389-
items = TestIntegrationUtils.extractClassMethods(srcClasses.single(), false)
390-
.filterWhen(UtSettings.skipTestGenerationForSyntheticMethods) { it.member !is SyntheticElement }
391-
.filterNot { it.isAbstract }
392-
updateMethodsTable(items)
383+
val items = if (model.extractMembersFromSrcClasses) {
384+
srcClasses.flatMap { it.extractFirstLevelMembers(false) }
393385
} else {
394-
items = srcClasses.map { MemberInfo(it) }
395-
updateClassesTable(items)
386+
srcClasses.map { MemberInfo(it) }
396387
}
397388

389+
checkMembers(items)
390+
membersTable.setMemberInfos(items)
398391
if (items.isEmpty()) isOKActionEnabled = false
399392

400393
// fix issue with MemberSelectionTable height, set it directly.
@@ -403,28 +396,14 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m
403396
membersTable.preferredScrollableViewportSize = size(-1, height)
404397
}
405398

406-
private fun updateMethodsTable(allMethods: List<MemberInfo>) {
407-
val selectedDisplayNames = model.selectedMethods?.map { it.displayName } ?: emptyList()
408-
val selectedMethods = if (selectedDisplayNames.isEmpty())
409-
allMethods
410-
else allMethods.filter { it.displayName in selectedDisplayNames }
399+
private fun checkMembers(allMembers: List<MemberInfo>) {
400+
val selectedDisplayNames = model.selectedMembers?.map { it.displayName } ?: emptyList()
401+
val selectedMembers = allMembers.filter { it.displayName in selectedDisplayNames }
411402

412-
if (selectedMethods.isEmpty()) {
413-
checkMembers(allMethods)
414-
} else {
415-
checkMembers(selectedMethods)
416-
}
417-
418-
membersTable.setMemberInfos(allMethods)
419-
}
420-
421-
private fun updateClassesTable(srcClasses: List<MemberInfo>) {
422-
checkMembers(srcClasses)
423-
membersTable.setMemberInfos(srcClasses)
403+
val methodsToCheck = selectedMembers.ifEmpty { allMembers }
404+
methodsToCheck.forEach { it.isChecked = true }
424405
}
425406

426-
private fun checkMembers(members: List<MemberInfo>) = members.forEach { it.isChecked = true }
427-
428407
private fun getTestRoot() : VirtualFile? {
429408
model.testSourceRoot?.let {
430409
if (it.isDirectory || it is FakeVirtualFile) return it
@@ -497,12 +476,12 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m
497476
if (testPackageField.text != SAME_PACKAGE_LABEL) testPackageField.text else ""
498477

499478
val selectedMembers = membersTable.selectedMemberInfos
500-
model.srcClasses = selectedMembers
501-
.mapNotNull { it.member as? PsiClass ?: it.member.containingClass }
502-
.toSet()
503-
504-
val selectedMethods = selectedMembers.filter { it.member is PsiMethod }.toSet()
505-
model.selectedMethods = if (selectedMethods.any()) selectedMethods else null
479+
if (!model.extractMembersFromSrcClasses) {
480+
model.srcClasses = selectedMembers
481+
.mapNotNull { it.member as? PsiClass }
482+
.toSet()
483+
}
484+
model.selectedMembers = selectedMembers.toSet()
506485

507486
model.testFramework = testFrameworks.item
508487
model.mockStrategy = mockStrategies.item

0 commit comments

Comments
 (0)