Skip to content

Commit 9c0eb8a

Browse files
committed
Carefully consider different cases when generating an util class
1 parent 589d77d commit 9c0eb8a

File tree

2 files changed

+153
-62
lines changed

2 files changed

+153
-62
lines changed

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,5 +194,12 @@ sealed class UtilClassKind(
194194
const val UT_UTILS_PACKAGE_NAME = "org.utbot.runtime.utils"
195195
const val UT_UTILS_CLASS_NAME = "UtUtils"
196196
const val PACKAGE_DELIMITER = "."
197+
198+
/**
199+
* List of package names of UtUtils class.
200+
* See whole package name at [UT_UTILS_PACKAGE_NAME].
201+
*/
202+
val utilsPackages: List<String>
203+
get() = UT_UTILS_PACKAGE_NAME.split(PACKAGE_DELIMITER)
197204
}
198205
}

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

Lines changed: 146 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.intellij.ide.fileTemplates.FileTemplateUtil
77
import com.intellij.ide.fileTemplates.JavaTemplateUtil
88
import com.intellij.ide.highlighter.JavaFileType
99
import com.intellij.openapi.application.ApplicationManager
10+
import com.intellij.openapi.application.runReadAction
1011
import com.intellij.openapi.application.runWriteAction
1112
import com.intellij.openapi.command.WriteCommandAction.runWriteCommandAction
1213
import com.intellij.openapi.command.executeCommand
@@ -56,10 +57,7 @@ import org.utbot.framework.codegen.StaticImport
5657
import org.utbot.framework.codegen.model.CodeGenerator
5758
import org.utbot.framework.codegen.model.CodeGenerationResult
5859
import org.utbot.framework.codegen.model.UtilClassKind
59-
import org.utbot.framework.codegen.model.UtilClassKind.Companion.PACKAGE_DELIMITER
6060
import org.utbot.framework.codegen.model.UtilClassKind.Companion.UT_UTILS_CLASS_NAME
61-
import org.utbot.framework.codegen.model.UtilClassKind.Companion.UT_UTILS_PACKAGE_NAME
62-
import org.utbot.framework.codegen.model.UtilClassKind.UtUtilsWithMockito
6361
import org.utbot.framework.codegen.model.constructor.tree.TestsGenerationReport
6462
import org.utbot.framework.plugin.api.CodegenLanguage
6563
import org.utbot.framework.plugin.api.ExecutableId
@@ -91,11 +89,13 @@ import org.utbot.intellij.plugin.util.IntelliJApiHelper.run
9189

9290
object CodeGenerationController {
9391

94-
private class UtilMethodListener {
92+
private class UtilClassListener {
9593
var requiredUtilClassKind: UtilClassKind? = null
94+
var mockFrameworkUsed: Boolean = false
9695

9796
fun onTestClassGenerated(result: CodeGenerationResult) {
9897
requiredUtilClassKind = maxOfNullable(requiredUtilClassKind, result.utilClassKind)
98+
mockFrameworkUsed = maxOf(mockFrameworkUsed, result.mockFrameworkUsed)
9999
}
100100

101101
private fun <T : Comparable<T>> maxOfNullable(a: T?, b: T?): T? {
@@ -115,7 +115,7 @@ object CodeGenerationController {
115115

116116
val reports = mutableListOf<TestsGenerationReport>()
117117
val testFiles = mutableListOf<PsiFile>()
118-
val utilMethodListener = UtilMethodListener()
118+
val utilClassListener = UtilClassListener()
119119
for (srcClass in testSetsByClass.keys) {
120120
val testSets = testSetsByClass[srcClass] ?: continue
121121
try {
@@ -133,7 +133,7 @@ object CodeGenerationController {
133133
model,
134134
latch,
135135
reports,
136-
utilMethodListener
136+
utilClassListener
137137
)
138138
testFiles.add(testClassFile)
139139
} catch (e: IncorrectOperationException) {
@@ -148,25 +148,29 @@ object CodeGenerationController {
148148

149149
run(EDT_LATER) {
150150
waitForCountDown(latch, timeout = 100, timeUnit = TimeUnit.MILLISECONDS) {
151-
val utilClassKind = utilMethodListener.requiredUtilClassKind
152-
if (utilClassKind != null) {
153-
// create a directory to put utils class into
154-
val utilClassDirectory = createUtUtilSubdirectories(baseTestDirectory)
155-
156-
val language = model.codegenLanguage
157-
val utUtilsName = "$UT_UTILS_CLASS_NAME${language.extension}"
158-
159-
val utUtilsAlreadyExists = utilClassDirectory.findFile(utUtilsName) != null
160-
161-
// we generate and write an util class in one of the two cases:
162-
// - util file does not yet exist --> then we generate it, because it is required by generated tests
163-
// - utilClassKind is UtUtilsWithMockito --> then we generate utils class and add it to utils directory.
164-
// If utils file already exists, we overwrite it, because existing utils may be without Mockito,
165-
// and we want to make sure that the generated utils use Mockito.
166-
if (!utUtilsAlreadyExists || utilClassKind is UtUtilsWithMockito) {
167-
generateAndWriteUtilClass(utUtilsName, utilClassDirectory, model, utilClassKind)
168-
}
151+
val existingUtilClass = model.codegenLanguage.getUtilClassOrNull(baseTestDirectory)
152+
153+
val utilClassKind = utilClassListener.requiredUtilClassKind
154+
?: return@waitForCountDown // no util class needed
155+
156+
val utilClassExists = existingUtilClass != null
157+
val mockFrameworkNotUsed = !utilClassListener.mockFrameworkUsed
158+
159+
if (utilClassExists && mockFrameworkNotUsed) {
160+
// If util class already exists and mock framework is not used,
161+
// then existing util class is enough, and we don't need to generate a new one.
162+
// That's because both regular and mock versions of util class can work
163+
// with tests that do not use mocks, so we do not have to worry about
164+
// version of util class that we have at the moment.
165+
return@waitForCountDown
169166
}
167+
168+
createOrUpdateUtilClass(
169+
testDirectory = baseTestDirectory,
170+
utilClassKind = utilClassKind,
171+
existingUtilClass = existingUtilClass,
172+
model = model
173+
)
170174
}
171175
}
172176

@@ -198,55 +202,107 @@ object CodeGenerationController {
198202
}
199203

200204
/**
201-
* Create package directories if needed for UtUtils class.
202-
* Then generate and create a UtUtils class file in the utils package.
203-
* Also run reformatting for the generated class.
205+
* If [existingUtilClass] is null (no util class exists), then we create package directories for util class,
206+
* create util class itself, and put it into the corresponding directory.
207+
* Otherwise, we overwrite the existing util class with a new one.
208+
* This is necessary in case if existing util class has no mocks support, but the newly generated tests do use mocks.
209+
* So, we overwrite an util class with a new one that does support mocks.
210+
*
211+
* @param testDirectory root test directory where we will put our generated tests.
212+
* @param utilClassKind kind of util class required by the test class(es) that we generated.
213+
* @param existingUtilClass util class that already exists or null if it does not yet exist.
214+
* @param model [GenerateTestsModel] that contains some useful information for util class generation.
204215
*/
205-
private fun generateAndWriteUtilClass(
206-
utUtilsName: String,
207-
utilClassDirectory: PsiDirectory,
208-
model: GenerateTestsModel,
209-
utilClassKind: UtilClassKind
216+
private fun createOrUpdateUtilClass(
217+
testDirectory: PsiDirectory,
218+
utilClassKind: UtilClassKind,
219+
existingUtilClass: PsiFile?,
220+
model: GenerateTestsModel
210221
) {
211-
val psiDocumentManager = PsiDocumentManager.getInstance(model.project)
222+
val language = model.codegenLanguage
212223

213-
val utUtilsText = utilClassKind.getUtilClassText(model.codegenLanguage)
224+
val utUtilsFile = if (existingUtilClass == null) {
225+
// create a directory to put utils class into
226+
val utilClassDirectory = createUtUtilSubdirectories(testDirectory)
227+
// create util class file and put it into utils directory
228+
createNewUtilClass(utilClassDirectory, language, utilClassKind, model)
229+
} else {
230+
overwriteUtilClass(existingUtilClass, utilClassKind, model)
231+
}
232+
233+
val utUtilsClass = runReadAction {
234+
// there's only one class in the file
235+
(utUtilsFile as PsiClassOwner).classes.first()
236+
}
237+
238+
runWriteCommandAction(model.project, "UtBot util class reformatting", null, {
239+
reformat(model, utUtilsFile, utUtilsClass)
240+
})
241+
242+
val utUtilsDocument = PsiDocumentManager
243+
.getInstance(model.project)
244+
.getDocument(utUtilsFile) ?: error("Failed to get a Document for UtUtils file")
245+
246+
unblockDocument(model.project, utUtilsDocument)
247+
}
214248

215-
val existingUtUtilsDocument = utilClassDirectory
216-
.findFile(utUtilsName)
217-
?.let { psiDocumentManager.getDocument(it) }
249+
private fun overwriteUtilClass(
250+
existingUtilClass: PsiFile,
251+
utilClassKind: UtilClassKind,
252+
model: GenerateTestsModel
253+
): PsiFile {
254+
val utilsClassDocument = PsiDocumentManager
255+
.getInstance(model.project)
256+
.getDocument(existingUtilClass)
257+
?: error("Failed to get Document for UtUtils class PsiFile: ${existingUtilClass.name}")
218258

219-
val utUtilsFile = if (existingUtUtilsDocument != null) {
220-
executeCommand {
221-
existingUtUtilsDocument.setText(utUtilsText)
259+
val utUtilsText = utilClassKind.getUtilClassText(model.codegenLanguage)
260+
261+
run(EDT_LATER) {
262+
run(WRITE_ACTION) {
263+
unblockDocument(model.project, utilsClassDocument)
264+
executeCommand {
265+
utilsClassDocument.setText(utUtilsText)
266+
}
267+
unblockDocument(model.project, utilsClassDocument)
222268
}
223-
unblockDocument(model.project, existingUtUtilsDocument)
224-
psiDocumentManager.getPsiFile(existingUtUtilsDocument)
225-
} else {
226-
val utUtilsFile = PsiFileFactory.getInstance(model.project)
269+
}
270+
return existingUtilClass
271+
}
272+
273+
/**
274+
* This method creates an util class file and adds it into [utilClassDirectory].
275+
*
276+
* @param utilClassDirectory directory to put util class into.
277+
* @param language language of util class.
278+
* @param utilClassKind kind of util class required by the test class(es) that we generated.
279+
* @param model [GenerateTestsModel] that contains some useful information for util class generation.
280+
*/
281+
private fun createNewUtilClass(
282+
utilClassDirectory: PsiDirectory,
283+
language: CodegenLanguage,
284+
utilClassKind: UtilClassKind,
285+
model: GenerateTestsModel,
286+
): PsiFile {
287+
val utUtilsName = language.utilClassFileName
288+
289+
val utUtilsText = utilClassKind.getUtilClassText(model.codegenLanguage)
290+
291+
val utUtilsFile = runReadAction {
292+
PsiFileFactory.getInstance(model.project)
227293
.createFileFromText(
228294
utUtilsName,
229295
model.codegenLanguage.fileType,
230296
utUtilsText
231297
)
232-
// add the UtUtils class file into the utils directory
233-
runWriteCommandAction(model.project) {
234-
utilClassDirectory.add(utUtilsFile)
235-
}
236-
utUtilsFile
237298
}
238299

239-
// there's only one class in the file
240-
val utUtilsClass = (utUtilsFile as PsiClassOwner).classes.first()
241-
242-
runWriteCommandAction(model.project, "UtBot util class reformatting", null, {
243-
reformat(model, utUtilsFile, utUtilsClass)
244-
})
245-
246-
val utUtilsDocument = psiDocumentManager.getDocument(utUtilsFile)
247-
?: error("Failed to get a Document for UtUtils file")
300+
// add UtUtils class file into the utils directory
301+
runWriteCommandAction(model.project) {
302+
utilClassDirectory.add(utUtilsFile)
303+
}
248304

249-
unblockDocument(model.project, utUtilsDocument)
305+
return utUtilsFile
250306
}
251307

252308
/**
@@ -261,12 +317,40 @@ object CodeGenerationController {
261317
}
262318
}
263319

320+
private val CodegenLanguage.utilClassFileName: String
321+
get() = "$UT_UTILS_CLASS_NAME${this.extension}"
322+
323+
/**
324+
* @param testDirectory root test directory where we will put our generated tests.
325+
* @return directory for util class if it exists or null otherwise.
326+
*/
327+
private fun getUtilDirectoryOrNull(testDirectory: PsiDirectory): PsiDirectory? {
328+
val directoryNames = UtilClassKind.utilsPackages
329+
var currentDirectory = testDirectory
330+
for (name in directoryNames) {
331+
val subdirectory = runReadAction { currentDirectory.findSubdirectory(name) } ?: return null
332+
currentDirectory = subdirectory
333+
}
334+
return currentDirectory
335+
}
336+
337+
/**
338+
* @param testDirectory root test directory where we will put our generated tests.
339+
* @return file of util class if it exists or null otherwise.
340+
*/
341+
private fun CodegenLanguage.getUtilClassOrNull(testDirectory: PsiDirectory): PsiFile? {
342+
return runReadAction {
343+
val utilDirectory = getUtilDirectoryOrNull(testDirectory)
344+
utilDirectory?.findFile(this.utilClassFileName)
345+
}
346+
}
347+
264348
/**
265349
* Create all package directories for UtUtils class.
266350
* @return the innermost directory - utils from `org.utbot.runtime.utils`
267351
*/
268352
private fun createUtUtilSubdirectories(baseTestDirectory: PsiDirectory): PsiDirectory {
269-
val directoryNames = UT_UTILS_PACKAGE_NAME.split(PACKAGE_DELIMITER)
353+
val directoryNames = UtilClassKind.utilsPackages
270354
var currentDirectory = baseTestDirectory
271355
runWriteCommandAction(baseTestDirectory.project) {
272356
for (name in directoryNames) {
@@ -393,7 +477,7 @@ object CodeGenerationController {
393477
model: GenerateTestsModel,
394478
reportsCountDown: CountDownLatch,
395479
reports: MutableList<TestsGenerationReport>,
396-
utilMethodListener: UtilMethodListener
480+
utilClassListener: UtilClassListener
397481
) {
398482
val classUnderTest = testSets.first().method.clazz
399483
val classMethods = TestIntegrationUtils.extractClassMethods(srcClass, false)
@@ -422,7 +506,7 @@ object CodeGenerationController {
422506
// if we don't want to open _all_ new files with tests in editor one-by-one
423507
run(THREAD_POOL) {
424508
val codeGenerationResult = codeGenerator.generateAsStringWithTestReport(testSets)
425-
utilMethodListener.onTestClassGenerated(codeGenerationResult)
509+
utilClassListener.onTestClassGenerated(codeGenerationResult)
426510
val generatedTestsCode = codeGenerationResult.generatedCode
427511
run(EDT_LATER) {
428512
run(WRITE_ACTION) {

0 commit comments

Comments
 (0)