1
1
package org.utbot.intellij.plugin.generator
2
2
3
+ import org.utbot.common.HTML_LINE_SEPARATOR
4
+ import org.utbot.common.PathUtil.classFqnToPath
5
+ import org.utbot.common.PathUtil.toHtmlLinkTag
6
+ import org.utbot.common.appendHtmlLine
7
+ import org.utbot.framework.codegen.Import
8
+ import org.utbot.framework.codegen.ParametrizedTestSource
9
+ import org.utbot.framework.codegen.StaticImport
10
+ import org.utbot.framework.codegen.TestsCodeWithTestReport
11
+ import org.utbot.framework.codegen.model.ModelBasedTestCodeGenerator
12
+ import org.utbot.framework.codegen.model.constructor.tree.TestsGenerationReport
13
+ import org.utbot.framework.plugin.api.CodegenLanguage
14
+ import org.utbot.framework.plugin.api.UtTestCase
15
+ import org.utbot.intellij.plugin.sarif.SarifReportIdea
16
+ import org.utbot.intellij.plugin.sarif.SourceFindingStrategyIdea
17
+ import org.utbot.intellij.plugin.settings.Settings
18
+ import org.utbot.intellij.plugin.ui.utils.getOrCreateSarifReportsPath
19
+ import org.utbot.intellij.plugin.ui.utils.getOrCreateTestResourcesPath
20
+ import org.utbot.sarif.SarifReport
3
21
import com.intellij.codeInsight.CodeInsightUtil
4
22
import com.intellij.codeInsight.FileModificationService
5
23
import com.intellij.ide.fileTemplates.FileTemplateManager
6
24
import com.intellij.ide.fileTemplates.FileTemplateUtil
7
25
import com.intellij.ide.fileTemplates.JavaTemplateUtil
8
26
import com.intellij.openapi.application.ApplicationManager
27
+ import com.intellij.openapi.application.runReadAction
28
+ import com.intellij.openapi.application.runWriteAction
9
29
import com.intellij.openapi.command.WriteCommandAction.runWriteCommandAction
10
30
import com.intellij.openapi.command.executeCommand
11
31
import com.intellij.openapi.components.service
@@ -21,51 +41,59 @@ import com.intellij.psi.codeStyle.JavaCodeStyleManager
21
41
import com.intellij.psi.search.GlobalSearchScopesCore
22
42
import com.intellij.testIntegration.TestIntegrationUtils
23
43
import com.intellij.util.IncorrectOperationException
44
+ import com.intellij.util.concurrency.AppExecutorUtil
24
45
import com.intellij.util.io.exists
25
46
import com.siyeh.ig.psiutils.ImportUtils
47
+ import java.nio.file.Path
48
+ import java.nio.file.Paths
49
+ import java.util.concurrent.CountDownLatch
50
+ import java.util.concurrent.TimeUnit
26
51
import org.jetbrains.kotlin.asJava.classes.KtUltraLightClass
27
52
import org.jetbrains.kotlin.idea.core.ShortenReferences
28
53
import org.jetbrains.kotlin.idea.core.getPackage
29
54
import org.jetbrains.kotlin.idea.core.util.toPsiDirectory
30
55
import org.jetbrains.kotlin.idea.util.ImportInsertHelperImpl
56
+ import org.jetbrains.kotlin.idea.util.application.invokeLater
31
57
import org.jetbrains.kotlin.name.FqName
32
58
import org.jetbrains.kotlin.psi.KtClass
33
59
import org.jetbrains.kotlin.psi.KtNamedFunction
34
60
import org.jetbrains.kotlin.psi.KtPsiFactory
35
61
import org.jetbrains.kotlin.psi.psiUtil.endOffset
36
62
import org.jetbrains.kotlin.psi.psiUtil.startOffset
37
63
import org.jetbrains.kotlin.scripting.resolve.classId
38
- import org.utbot.common.HTML_LINE_SEPARATOR
39
- import org.utbot.common.PathUtil.classFqnToPath
40
- import org.utbot.common.PathUtil.toHtmlLinkTag
41
- import org.utbot.common.appendHtmlLine
42
- import org.utbot.framework.codegen.Import
43
- import org.utbot.framework.codegen.ParametrizedTestSource
44
- import org.utbot.framework.codegen.StaticImport
45
- import org.utbot.framework.codegen.TestsCodeWithTestReport
46
- import org.utbot.framework.codegen.model.ModelBasedTestCodeGenerator
47
- import org.utbot.framework.codegen.model.constructor.tree.TestsGenerationReport
48
- import org.utbot.framework.plugin.api.CodegenLanguage
49
- import org.utbot.framework.plugin.api.UtTestCase
64
+ import org.utbot.framework.plugin.api.util.UtContext
65
+ import org.utbot.framework.plugin.api.util.withUtContext
50
66
import org.utbot.intellij.plugin.error.showErrorDialogLater
51
- import org.utbot.intellij.plugin.sarif.SarifReportIdea
52
- import org.utbot.intellij.plugin.sarif.SourceFindingStrategyIdea
53
- import org.utbot.intellij.plugin.settings.Settings
54
- import org.utbot.intellij.plugin.ui.*
55
- import org.utbot.intellij.plugin.ui.utils.getOrCreateSarifReportsPath
56
- import org.utbot.intellij.plugin.ui.utils.getOrCreateTestResourcesPath
57
- import org.utbot.sarif.SarifReport
58
- import java.nio.file.Paths
67
+ import org.utbot.intellij.plugin.generator.TestGenerator.Target.*
68
+ import org.utbot.intellij.plugin.ui.GenerateTestsModel
69
+ import org.utbot.intellij.plugin.ui.SarifReportNotifier
70
+ import org.utbot.intellij.plugin.ui.TestReportUrlOpeningListener
71
+ import org.utbot.intellij.plugin.ui.TestsReportNotifier
72
+ import org.utbot.intellij.plugin.ui.packageName
59
73
60
74
object TestGenerator {
61
- fun generateTests (model : GenerateTestsModel , testCases : Map <PsiClass , List <UtTestCase >>) {
62
- generateTestsInternal(model, testCases)
75
+ private enum class Target {THREAD_POOL , READ_ACTION , WRITE_ACTION , EDT_LATER }
76
+
77
+ private fun run (target : Target , runnable : Runnable ) {
78
+ UtContext .currentContext()?.let {
79
+ when (target) {
80
+ THREAD_POOL -> AppExecutorUtil .getAppExecutorService().submit {
81
+ withUtContext(it) {
82
+ runnable.run ()
83
+ }
84
+ }
85
+ READ_ACTION -> runReadAction { withUtContext(it) { runnable.run () } }
86
+ WRITE_ACTION -> runWriteAction { withUtContext(it) { runnable.run () } }
87
+ EDT_LATER -> invokeLater { withUtContext(it) { runnable.run () } }
88
+ }
89
+ } ? : error(" No context in thread ${Thread .currentThread()} " )
63
90
}
64
91
65
- private fun generateTestsInternal (model : GenerateTestsModel , testCasesByClass : Map <PsiClass , List <UtTestCase >>) {
92
+ fun generateTests (model : GenerateTestsModel , testCasesByClass : Map <PsiClass , List <UtTestCase >>) {
66
93
val baseTestDirectory = model.testSourceRoot?.toPsiDirectory(model.project)
67
94
? : return
68
95
val allTestPackages = getPackageDirectories(baseTestDirectory)
96
+ val latch = CountDownLatch (testCasesByClass.size)
69
97
70
98
for (srcClass in testCasesByClass.keys) {
71
99
val testCases = testCasesByClass[srcClass] ? : continue
@@ -75,20 +103,37 @@ object TestGenerator {
75
103
val testDirectory = allTestPackages[classPackageName] ? : baseTestDirectory
76
104
val testClass = createTestClass(srcClass, testDirectory, model) ? : continue
77
105
val file = testClass.containingFile
78
-
79
106
runWriteCommandAction(model.project, " Generate tests with UtBot" , null , {
80
- addTestMethodsAndSaveReports(testClass, file, testCases, model)
107
+ try {
108
+ addTestMethodsAndSaveReports(testClass, file, testCases, model, latch)
109
+ } catch (e: IncorrectOperationException ) {
110
+ showCreatingClassError(model.project, createTestClassName(srcClass))
111
+ }
81
112
})
82
113
} catch (e: IncorrectOperationException ) {
83
114
showCreatingClassError(model.project, createTestClassName(srcClass))
84
115
}
85
116
}
117
+ run (READ_ACTION ) {
118
+ val sarifReportsPath = model.testModule.getOrCreateSarifReportsPath(model.testSourceRoot)
119
+ run (THREAD_POOL ) {
120
+ waitForCountDown(latch, model, sarifReportsPath)
121
+ }
122
+ }
123
+ }
86
124
87
- mergeSarifReports(model)
125
+ private fun waitForCountDown (latch : CountDownLatch , model : GenerateTestsModel , sarifReportsPath : Path ) {
126
+ try {
127
+ if (! latch.await(5 , TimeUnit .SECONDS )) {
128
+ run (THREAD_POOL ) { waitForCountDown(latch, model, sarifReportsPath) }
129
+ } else {
130
+ mergeSarifReports(model, sarifReportsPath)
131
+ }
132
+ } catch (ignored: InterruptedException ) {
133
+ }
88
134
}
89
135
90
- private fun mergeSarifReports (model : GenerateTestsModel ) {
91
- val sarifReportsPath = model.testModule.getOrCreateSarifReportsPath(model.testSourceRoot)
136
+ private fun mergeSarifReports (model : GenerateTestsModel , sarifReportsPath : Path ) {
92
137
val sarifReports = sarifReportsPath.toFile()
93
138
.walkTopDown()
94
139
.filter { it.extension == " sarif" }
@@ -177,6 +222,7 @@ object TestGenerator {
177
222
file : PsiFile ,
178
223
testCases : List <UtTestCase >,
179
224
model : GenerateTestsModel ,
225
+ reportsCountDown : CountDownLatch ,
180
226
) {
181
227
val selectedMethods = TestIntegrationUtils .extractClassMethods(testClass, false )
182
228
val testFramework = model.testFramework
@@ -207,33 +253,50 @@ object TestGenerator {
207
253
when (generator) {
208
254
is ModelBasedTestCodeGenerator -> {
209
255
val editor = CodeInsightUtil .positionCursorAtLBrace(testClass.project, file, testClass)
210
- val testsCodeWithTestReport = generator.generateAsStringWithTestReport(testCases)
211
- val generatedTestsCode = testsCodeWithTestReport.generatedCode
256
+ // TODO Use PsiDocumentManager.getInstance(model.project).getDocument(file)
257
+ // if we don't want to open _all_ new files with tests in editor one-by-one
258
+ run (THREAD_POOL ) {
259
+ val testsCodeWithTestReport = generator.generateAsStringWithTestReport(testCases)
260
+ val generatedTestsCode = testsCodeWithTestReport.generatedCode
261
+ run (EDT_LATER ) {
262
+ run (WRITE_ACTION ) {
263
+ unblockDocument(testClass.project, editor.document)
264
+ // TODO: JIRA:1246 - display warnings if we rewrite the file
265
+ executeCommand(testClass.project, " Insert Generated Tests" ) {
266
+ editor.document.setText(generatedTestsCode)
267
+ }
268
+ unblockDocument(testClass.project, editor.document)
269
+
270
+ // after committing the document the `testClass` is invalid in PsiTree,
271
+ // so we have to reload it from the corresponding `file`
272
+ val testClassUpdated = (file as PsiClassOwner ).classes.first() // only one class in the file
273
+
274
+ // reformatting before creating reports due to
275
+ // SarifReport requires the final version of the generated tests code
276
+ runWriteCommandAction(testClassUpdated.project, " UtBot tests reformatting" , null , {
277
+ reformat(model, file, testClassUpdated)
278
+ })
279
+ unblockDocument(testClassUpdated.project, editor.document)
280
+
281
+ // uploading formatted code
282
+ val testsCodeWithTestReportFormatted =
283
+ testsCodeWithTestReport.copy(generatedCode = file.text)
284
+
285
+ // creating and saving reports
286
+ saveSarifAndTestReports(
287
+ testClassUpdated,
288
+ testCases,
289
+ model,
290
+ testsCodeWithTestReportFormatted,
291
+ reportsCountDown
292
+ )
212
293
213
- unblockDocument(testClass.project, editor.document)
214
- // TODO: JIRA:1246 - display warnings if we rewrite the file
215
- executeCommand(testClass.project, " Insert Generated Tests" ) {
216
- editor.document.setText(generatedTestsCode)
294
+ unblockDocument(testClassUpdated.project, editor.document)
295
+ }
296
+ }
217
297
}
218
- unblockDocument(testClass.project, editor.document)
219
-
220
- // after committing the document the `testClass` is invalid in PsiTree,
221
- // so we have to reload it from the corresponding `file`
222
- val testClassUpdated = (file as PsiClassOwner ).classes.first() // only one class in the file
223
-
224
- // reformatting before creating reports due to
225
- // SarifReport requires the final version of the generated tests code
226
- reformat(model, file, testClassUpdated)
227
- unblockDocument(testClassUpdated.project, editor.document)
228
-
229
- // uploading formatted code
230
- val testsCodeWithTestReportFormatted = testsCodeWithTestReport.copy(generatedCode = file.text)
231
-
232
- // creating and saving reports
233
- saveSarifAndTestReports(testClassUpdated, testCases, model, testsCodeWithTestReportFormatted)
234
-
235
- unblockDocument(testClassUpdated.project, editor.document)
236
298
}
299
+ // Note that reportsCountDown.countDown() has to be called in every generator implementation to complete whole process
237
300
else -> TODO (" Only model based code generator supported, but got: ${generator::class } " )
238
301
}
239
302
}
@@ -259,7 +322,8 @@ object TestGenerator {
259
322
testClass : PsiClass ,
260
323
testCases : List <UtTestCase >,
261
324
model : GenerateTestsModel ,
262
- testsCodeWithTestReport : TestsCodeWithTestReport
325
+ testsCodeWithTestReport : TestsCodeWithTestReport ,
326
+ reportsCountDown : CountDownLatch
263
327
) {
264
328
val project = model.project
265
329
val generatedTestsCode = testsCodeWithTestReport.generatedCode
@@ -276,6 +340,8 @@ object TestGenerator {
276
340
message = " Cannot save Sarif report via generated tests: error occurred '${e.message} '" ,
277
341
title = " Failed to save Sarif report"
278
342
)
343
+ } finally {
344
+ reportsCountDown.countDown()
279
345
}
280
346
281
347
try {
0 commit comments