@@ -23,6 +23,8 @@ import com.intellij.codeInsight.FileModificationService
23
23
import com.intellij.ide.fileTemplates.FileTemplateManager
24
24
import com.intellij.ide.fileTemplates.FileTemplateUtil
25
25
import com.intellij.ide.fileTemplates.JavaTemplateUtil
26
+ import com.intellij.openapi.application.runReadAction
27
+ import com.intellij.openapi.application.runWriteAction
26
28
import com.intellij.openapi.command.WriteCommandAction.runWriteCommandAction
27
29
import com.intellij.openapi.command.executeCommand
28
30
import com.intellij.openapi.components.service
@@ -44,39 +46,62 @@ import com.intellij.psi.codeStyle.JavaCodeStyleManager
44
46
import com.intellij.psi.search.GlobalSearchScopesCore
45
47
import com.intellij.testIntegration.TestIntegrationUtils
46
48
import com.intellij.util.IncorrectOperationException
49
+ import com.intellij.util.concurrency.AppExecutorUtil
47
50
import com.intellij.util.io.exists
48
51
import com.siyeh.ig.psiutils.ImportUtils
52
+ import java.nio.file.Path
49
53
import java.nio.file.Paths
54
+ import java.util.concurrent.CountDownLatch
55
+ import java.util.concurrent.TimeUnit
50
56
import org.jetbrains.kotlin.asJava.classes.KtUltraLightClass
51
57
import org.jetbrains.kotlin.idea.core.ShortenReferences
52
58
import org.jetbrains.kotlin.idea.core.getPackage
53
59
import org.jetbrains.kotlin.idea.core.util.toPsiDirectory
54
60
import org.jetbrains.kotlin.idea.util.ImportInsertHelperImpl
61
+ import org.jetbrains.kotlin.idea.util.application.invokeLater
55
62
import org.jetbrains.kotlin.name.FqName
56
63
import org.jetbrains.kotlin.psi.KtClass
57
64
import org.jetbrains.kotlin.psi.KtNamedFunction
58
65
import org.jetbrains.kotlin.psi.KtPsiFactory
59
66
import org.jetbrains.kotlin.psi.psiUtil.endOffset
60
67
import org.jetbrains.kotlin.psi.psiUtil.startOffset
61
68
import org.jetbrains.kotlin.scripting.resolve.classId
69
+ import org.utbot.framework.plugin.api.util.UtContext
70
+ import org.utbot.framework.plugin.api.util.withUtContext
62
71
import org.utbot.intellij.plugin.error.showErrorDialogLater
63
72
import org.utbot.intellij.plugin.ui.GenerateTestsModel
64
73
import org.utbot.intellij.plugin.ui.SarifReportNotifier
65
74
import org.utbot.intellij.plugin.ui.TestReportUrlOpeningListener
66
75
import org.utbot.intellij.plugin.ui.TestsReportNotifier
67
76
import org.utbot.intellij.plugin.ui.packageName
77
+ import org.utbot.intellij.plugin.generator.TestGenerator.Target.THREAD_POOL
78
+ import org.utbot.intellij.plugin.generator.TestGenerator.Target.EDT_LATER
79
+ import org.utbot.intellij.plugin.generator.TestGenerator.Target.READ_ACTION
80
+ import org.utbot.intellij.plugin.generator.TestGenerator.Target.WRITE_ACTION
68
81
69
82
object TestGenerator {
70
- fun generateTests (model : GenerateTestsModel , testCases : Map <PsiClass , List <UtTestCase >>) {
71
- runWriteCommandAction(model.project, " Generate tests with UtBot" , null , {
72
- generateTestsInternal(model, testCases)
73
- })
83
+ private enum class Target {THREAD_POOL , READ_ACTION , WRITE_ACTION , EDT_LATER }
84
+
85
+ private fun run (target : Target , runnable : Runnable ) {
86
+ UtContext .currentContext()?.let {
87
+ when (target) {
88
+ THREAD_POOL -> AppExecutorUtil .getAppExecutorService().submit{
89
+ withUtContext(it) {
90
+ runnable.run ()
91
+ }
92
+ }
93
+ READ_ACTION -> runReadAction { withUtContext(it) { runnable.run () } }
94
+ WRITE_ACTION -> runWriteAction { withUtContext(it) { runnable.run () } }
95
+ EDT_LATER -> invokeLater { withUtContext(it) { runnable.run () } }
96
+ }
97
+ } ? : error(" No context in thread ${Thread .currentThread()} " )
74
98
}
75
99
76
- private fun generateTestsInternal (model : GenerateTestsModel , testCasesByClass : Map <PsiClass , List <UtTestCase >>) {
100
+ fun generateTests (model : GenerateTestsModel , testCasesByClass : Map <PsiClass , List <UtTestCase >>) {
77
101
val baseTestDirectory = model.testSourceRoot?.toPsiDirectory(model.project)
78
102
? : return
79
103
val allTestPackages = getPackageDirectories(baseTestDirectory)
104
+ val latch = CountDownLatch (testCasesByClass.size)
80
105
81
106
for (srcClass in testCasesByClass.keys) {
82
107
val testCases = testCasesByClass[srcClass] ? : continue
@@ -86,18 +111,37 @@ object TestGenerator {
86
111
val testDirectory = allTestPackages[classPackageName] ? : baseTestDirectory
87
112
val testClass = createTestClass(srcClass, testDirectory, model) ? : continue
88
113
val file = testClass.containingFile
89
-
90
- addTestMethodsAndSaveReports(testClass, file, testCases, model)
114
+ runWriteCommandAction(model.project, " Generate tests with UtBot" , null , {
115
+ try {
116
+ addTestMethodsAndSaveReports(testClass, file, testCases, model, latch)
117
+ } catch (e: IncorrectOperationException ) {
118
+ showCreatingClassError(model.project, createTestClassName(srcClass))
119
+ }
120
+ })
91
121
} catch (e: IncorrectOperationException ) {
92
122
showCreatingClassError(model.project, createTestClassName(srcClass))
93
123
}
94
124
}
125
+ run (READ_ACTION ) {
126
+ val sarifReportsPath = model.testModule.getOrCreateSarifReportsPath(model.testSourceRoot)
127
+ run (THREAD_POOL ) {
128
+ waitForCountDown(latch, model, sarifReportsPath)
129
+ }
130
+ }
131
+ }
95
132
96
- mergeSarifReports(model)
133
+ private fun waitForCountDown (latch : CountDownLatch , model : GenerateTestsModel , sarifReportsPath : Path ) {
134
+ try {
135
+ if (! latch.await(5 , TimeUnit .SECONDS )) {
136
+ run (THREAD_POOL ) { waitForCountDown(latch, model, sarifReportsPath) }
137
+ } else {
138
+ mergeSarifReports(model, sarifReportsPath)
139
+ }
140
+ } catch (ignored: InterruptedException ) {
141
+ }
97
142
}
98
143
99
- private fun mergeSarifReports (model : GenerateTestsModel ) {
100
- val sarifReportsPath = model.testModule.getOrCreateSarifReportsPath(model.testSourceRoot)
144
+ private fun mergeSarifReports (model : GenerateTestsModel , sarifReportsPath : Path ) {
101
145
val sarifReports = sarifReportsPath.toFile()
102
146
.walkTopDown()
103
147
.filter { it.extension == " sarif" }
@@ -183,6 +227,7 @@ object TestGenerator {
183
227
file : PsiFile ,
184
228
testCases : List <UtTestCase >,
185
229
model : GenerateTestsModel ,
230
+ reportsCountDown : CountDownLatch ,
186
231
) {
187
232
val selectedMethods = TestIntegrationUtils .extractClassMethods(testClass, false )
188
233
val testFramework = model.testFramework
@@ -213,32 +258,48 @@ object TestGenerator {
213
258
when (generator) {
214
259
is ModelBasedTestCodeGenerator -> {
215
260
val editor = CodeInsightUtil .positionCursorAtLBrace(testClass.project, file, testClass)
216
- val testsCodeWithTestReport = generator.generateAsStringWithTestReport(testCases)
217
- val generatedTestsCode = testsCodeWithTestReport.generatedCode
261
+ // TODO Use PsiDocumentManager.getInstance(model.project).getDocument(file)
262
+ // if we don't want to open _all_ new files with tests in editor one-by-one
263
+ run (THREAD_POOL ) {
264
+ val testsCodeWithTestReport = generator.generateAsStringWithTestReport(testCases)
265
+ val generatedTestsCode = testsCodeWithTestReport.generatedCode
266
+ run (EDT_LATER ) {
267
+ run (WRITE_ACTION ) {
268
+ unblockDocument(testClass.project, editor.document)
269
+ // TODO: JIRA:1246 - display warnings if we rewrite the file
270
+ executeCommand(testClass.project, " Insert Generated Tests" ) {
271
+ editor.document.setText(generatedTestsCode)
272
+ }
273
+ unblockDocument(testClass.project, editor.document)
274
+
275
+ // after committing the document the `testClass` is invalid in PsiTree,
276
+ // so we have to reload it from the corresponding `file`
277
+ val testClassUpdated = (file as PsiClassOwner ).classes.first() // only one class in the file
278
+
279
+ // reformatting before creating reports due to
280
+ // SarifReport requires the final version of the generated tests code
281
+ runWriteCommandAction(testClassUpdated.project, " UtBot tests reformatting" , null , {
282
+ reformat(model, file, testClassUpdated)
283
+ })
284
+ unblockDocument(testClassUpdated.project, editor.document)
285
+
286
+ // uploading formatted code
287
+ val testsCodeWithTestReportFormatted =
288
+ testsCodeWithTestReport.copy(generatedCode = file.text)
289
+
290
+ // creating and saving reports
291
+ saveSarifAndTestReports(
292
+ testClassUpdated,
293
+ testCases,
294
+ model,
295
+ testsCodeWithTestReportFormatted,
296
+ reportsCountDown
297
+ )
218
298
219
- unblockDocument(testClass.project, editor.document)
220
- // TODO: JIRA:1246 - display warnings if we rewrite the file
221
- executeCommand(testClass.project, " Insert Generated Tests" ) {
222
- editor.document.setText(generatedTestsCode)
299
+ unblockDocument(testClassUpdated.project, editor.document)
300
+ }
301
+ }
223
302
}
224
- unblockDocument(testClass.project, editor.document)
225
-
226
- // after committing the document the `testClass` is invalid in PsiTree,
227
- // so we have to reload it from the corresponding `file`
228
- val testClassUpdated = (file as PsiClassOwner ).classes.first() // only one class in the file
229
-
230
- // reformatting before creating reports due to
231
- // SarifReport requires the final version of the generated tests code
232
- reformat(model, file, testClassUpdated)
233
- unblockDocument(testClassUpdated.project, editor.document)
234
-
235
- // uploading formatted code
236
- val testsCodeWithTestReportFormatted = testsCodeWithTestReport.copy(generatedCode = file.text)
237
-
238
- // creating and saving reports
239
- saveSarifAndTestReports(testClassUpdated, testCases, model, testsCodeWithTestReportFormatted)
240
-
241
- unblockDocument(testClassUpdated.project, editor.document)
242
303
}
243
304
else -> TODO (" Only model based code generator supported, but got: ${generator::class } " )
244
305
}
@@ -265,7 +326,8 @@ object TestGenerator {
265
326
testClass : PsiClass ,
266
327
testCases : List <UtTestCase >,
267
328
model : GenerateTestsModel ,
268
- testsCodeWithTestReport : TestsCodeWithTestReport
329
+ testsCodeWithTestReport : TestsCodeWithTestReport ,
330
+ reportsCountDown : CountDownLatch
269
331
) {
270
332
val project = model.project
271
333
val generatedTestsCode = testsCodeWithTestReport.generatedCode
@@ -282,6 +344,8 @@ object TestGenerator {
282
344
message = " Cannot save Sarif report via generated tests: error occurred '${e.message} '" ,
283
345
title = " Failed to save Sarif report"
284
346
)
347
+ } finally {
348
+ reportsCountDown.countDown()
285
349
}
286
350
287
351
try {
0 commit comments