Skip to content

Ability to create and run tests at once #156 #670

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.intellij.ide.fileTemplates.FileTemplateManager
import com.intellij.ide.fileTemplates.FileTemplateUtil
import com.intellij.ide.fileTemplates.JavaTemplateUtil
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.command.WriteCommandAction.runWriteCommandAction
import com.intellij.openapi.command.executeCommand
Expand All @@ -29,14 +28,12 @@ import com.intellij.psi.search.GlobalSearchScopesCore
import com.intellij.refactoring.util.classMembers.MemberInfo
import com.intellij.testIntegration.TestIntegrationUtils
import com.intellij.util.IncorrectOperationException
import com.intellij.util.concurrency.AppExecutorUtil
import com.siyeh.ig.psiutils.ImportUtils
import org.jetbrains.kotlin.asJava.classes.KtUltraLightClass
import org.jetbrains.kotlin.idea.core.ShortenReferences
import org.jetbrains.kotlin.idea.core.getPackage
import org.jetbrains.kotlin.idea.core.util.toPsiDirectory
import org.jetbrains.kotlin.idea.util.ImportInsertHelperImpl
import org.jetbrains.kotlin.idea.util.application.invokeLater
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtNamedFunction
Expand All @@ -62,7 +59,6 @@ import org.utbot.framework.plugin.api.util.executableId
import org.utbot.framework.plugin.api.util.id
import org.utbot.framework.plugin.api.util.withUtContext
import org.utbot.framework.util.Conflict
import org.utbot.intellij.plugin.generator.CodeGenerationController.Target.*
import org.utbot.intellij.plugin.models.GenerateTestsModel
import org.utbot.intellij.plugin.models.packageName
import org.utbot.intellij.plugin.sarif.SarifReportIdea
Expand All @@ -74,16 +70,18 @@ import org.utbot.intellij.plugin.ui.TestsReportNotifier
import org.utbot.intellij.plugin.ui.WarningTestsReportNotifier
import org.utbot.intellij.plugin.ui.utils.getOrCreateSarifReportsPath
import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater
import org.utbot.intellij.plugin.util.RunConfigurationHelper
import org.utbot.intellij.plugin.util.signature
import org.utbot.sarif.SarifReport
import java.nio.file.Path
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.reflect.KClass
import kotlin.reflect.full.functions
import org.utbot.intellij.plugin.util.IntelliJApiHelper.Target.*
import org.utbot.intellij.plugin.util.IntelliJApiHelper.run

object CodeGenerationController {
private enum class Target { THREAD_POOL, READ_ACTION, WRITE_ACTION, EDT_LATER }

fun generateTests(model: GenerateTestsModel, testSetsByClass: Map<PsiClass, List<UtMethodTestSet>>) {
val baseTestDirectory = model.testSourceRoot?.toPsiDirectory(model.project)
Expand All @@ -92,6 +90,7 @@ object CodeGenerationController {
val latch = CountDownLatch(testSetsByClass.size)

val reports = mutableListOf<TestsGenerationReport>()
val testFiles = mutableListOf<PsiFile>()
for (srcClass in testSetsByClass.keys) {
val testSets = testSetsByClass[srcClass] ?: continue
try {
Expand All @@ -103,6 +102,7 @@ object CodeGenerationController {
runWriteCommandAction(model.project, "Generate tests with UtBot", null, {
try {
generateCodeAndReport(srcClass, testClass, file, testSets, model, latch, reports)
testFiles.add(file)
} catch (e: IncorrectOperationException) {
showCreatingClassError(model.project, createTestClassName(srcClass))
}
Expand Down Expand Up @@ -131,24 +131,12 @@ object CodeGenerationController {
}

mergeSarifReports(model, sarifReportsPath)
}
}
}
}

private fun run(target: Target, runnable: Runnable) {
UtContext.currentContext()?.let {
when (target) {
THREAD_POOL -> AppExecutorUtil.getAppExecutorService().submit {
withUtContext(it) {
runnable.run()
if (model.runGeneratedTestsWithCoverage) {
RunConfigurationHelper.runTestsWithCoverage(model, testFiles)
}
}
READ_ACTION -> runReadAction { withUtContext(it) { runnable.run() } }
WRITE_ACTION -> runWriteAction { withUtContext(it) { runnable.run() } }
EDT_LATER -> invokeLater { withUtContext(it) { runnable.run() } }
}
} ?: error("No context in thread ${Thread.currentThread()}")
}
}

private fun waitForCountDown(latch: CountDownLatch, action: Runnable) {
Expand Down Expand Up @@ -324,9 +312,9 @@ object CodeGenerationController {
testsCodeWithTestReportFormatted,
)

reportsCountDown.countDown()

unblockDocument(testClassUpdated.project, editor.document)

reportsCountDown.countDown()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.Computable
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiModifier
import com.intellij.psi.SyntheticElement
import com.intellij.refactoring.util.classMembers.MemberInfo
import com.intellij.testIntegration.TestIntegrationUtils
Expand All @@ -42,7 +41,7 @@ import org.utbot.intellij.plugin.ui.GenerateTestsDialogWindow
import org.utbot.intellij.plugin.ui.utils.jdkVersion
import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater
import org.utbot.intellij.plugin.ui.utils.testModule
import org.utbot.intellij.plugin.util.AndroidApiHelper
import org.utbot.intellij.plugin.util.IntelliJApiHelper
import org.utbot.intellij.plugin.util.PluginJdkPathProvider
import org.utbot.intellij.plugin.util.signature
import org.utbot.summary.summarize
Expand Down Expand Up @@ -213,11 +212,15 @@ object UtTestsDialogProcessor {
}.getOrDefault(listOf())

if (notEmptyCases.isEmpty()) {
showErrorDialogLater(
model.project,
errorMessage(className, secondsTimeout),
title = "Failed to generate unit tests for class $className"
)
if (model.srcClasses.size > 1) {
logger.error { "Failed to generate any tests cases for class $className" }
} else {
showErrorDialogLater(
model.project,
errorMessage(className, secondsTimeout),
title = "Failed to generate unit tests for class $className"
)
}
} else {
testSetsByClass[srcClass] = notEmptyCases
}
Expand Down Expand Up @@ -285,7 +288,7 @@ object UtTestsDialogProcessor {
val buildDir = CompilerPaths.getModuleOutputPath(srcModule, false) ?: return null
val pathsList = OrderEnumerator.orderEntries(srcModule).recursively().pathsList

val (classpath, classpathList) = if (AndroidApiHelper.isAndroidStudio()) {
val (classpath, classpathList) = if (IntelliJApiHelper.isAndroidStudio()) {
// Add $JAVA_HOME/jre/lib/rt.jar to path.
// This allows Soot to analyze real java instead of stub version in Android SDK on local machine.
pathsList.add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ data class GenerateTestsModel(
val isMultiPackage: Boolean by lazy {
srcClasses.map { it.packageName }.distinct().size != 1
}
var runGeneratedTestsWithCoverage : Boolean = false
}

val PsiClass.packageName: String get() = this.containingFile.containingDirectory.getPackage()?.qualifiedName ?: ""
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class Settings(val project: Project) : PersistentStateComponent<Settings.State>
var parametrizedTestSource: ParametrizedTestSource = ParametrizedTestSource.defaultItem,
var classesToMockAlways: Array<String> = Mocker.defaultSuperClassesToMockAlwaysNames.toTypedArray(),
var fuzzingValue: Double = 0.05,
var runGeneratedTestsWithCoverage : Boolean = false,
) {
constructor(model: GenerateTestsModel) : this(
codegenLanguage = model.codegenLanguage,
Expand All @@ -65,7 +66,8 @@ class Settings(val project: Project) : PersistentStateComponent<Settings.State>
forceStaticMocking = model.forceStaticMocking,
parametrizedTestSource = model.parametrizedTestSource,
classesToMockAlways = model.chosenClassesToMockAlways.mapTo(mutableSetOf()) { it.name }.toTypedArray(),
fuzzingValue = model.fuzzingValue
fuzzingValue = model.fuzzingValue,
runGeneratedTestsWithCoverage = model.runGeneratedTestsWithCoverage
)

override fun equals(other: Any?): Boolean {
Expand All @@ -86,6 +88,7 @@ class Settings(val project: Project) : PersistentStateComponent<Settings.State>
if (parametrizedTestSource != other.parametrizedTestSource) return false
if (!classesToMockAlways.contentEquals(other.classesToMockAlways)) return false
if (fuzzingValue != other.fuzzingValue) return false
if (runGeneratedTestsWithCoverage != other.runGeneratedTestsWithCoverage) return false

return true
}
Expand All @@ -102,6 +105,7 @@ class Settings(val project: Project) : PersistentStateComponent<Settings.State>
result = 31 * result + parametrizedTestSource.hashCode()
result = 31 * result + classesToMockAlways.contentHashCode()
result = 31 * result + fuzzingValue.hashCode()
result = 31 * result + if (runGeneratedTestsWithCoverage) 1 else 0

return result
}
Expand Down Expand Up @@ -138,6 +142,7 @@ class Settings(val project: Project) : PersistentStateComponent<Settings.State>
set(value) {
state.fuzzingValue = value.coerceIn(0.0, 1.0)
}
var runGeneratedTestsWithCoverage = state.runGeneratedTestsWithCoverage

fun setClassesToMockAlways(classesToMockAlways: List<String>) {
state.classesToMockAlways = classesToMockAlways.distinct().toTypedArray()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.ui.OptionAction
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.ui.popup.IconButton
import com.intellij.openapi.util.Computable
import com.intellij.openapi.util.text.TextWithMnemonic
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VfsUtilCore.urlToPath
Expand All @@ -40,7 +42,6 @@ import com.intellij.psi.PsiClass
import com.intellij.psi.PsiManager
import com.intellij.psi.PsiMethod
import com.intellij.psi.SyntheticElement
import com.intellij.psi.PsiModifier
import com.intellij.refactoring.PackageWrapper
import com.intellij.refactoring.ui.MemberSelectionTable
import com.intellij.refactoring.ui.PackageNameReferenceEditorCombo
Expand Down Expand Up @@ -75,11 +76,28 @@ import com.intellij.util.ui.JBUI.scale
import com.intellij.util.ui.JBUI.size
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.components.BorderLayoutPanel
import java.awt.BorderLayout
import java.awt.Color
import java.awt.event.ActionEvent
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.Objects
import java.util.concurrent.TimeUnit
import javax.swing.AbstractAction
import javax.swing.Action
import javax.swing.DefaultComboBoxModel
import javax.swing.JButton
import javax.swing.JComboBox
import javax.swing.JComponent
import javax.swing.JList
import javax.swing.JPanel
import kotlin.streams.toList
import org.jetbrains.concurrency.Promise
import org.jetbrains.concurrency.thenRun
import org.jetbrains.kotlin.asJava.classes.KtUltraLightClass
import org.utbot.common.filterWhen
import org.utbot.common.PathUtil.toPath
import org.utbot.common.filterWhen
import org.utbot.framework.UtSettings
import org.utbot.framework.codegen.ForceStaticMocking
import org.utbot.framework.codegen.Junit4
Expand Down Expand Up @@ -116,20 +134,7 @@ import org.utbot.intellij.plugin.ui.utils.kotlinTargetPlatform
import org.utbot.intellij.plugin.ui.utils.parseVersion
import org.utbot.intellij.plugin.ui.utils.testResourceRootTypes
import org.utbot.intellij.plugin.ui.utils.testRootType
import org.utbot.intellij.plugin.util.AndroidApiHelper
import java.awt.BorderLayout
import java.awt.Color
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
import java.util.concurrent.TimeUnit
import javax.swing.DefaultComboBoxModel
import javax.swing.JComboBox
import javax.swing.JComponent
import javax.swing.JList
import javax.swing.JPanel
import kotlin.streams.toList
import org.utbot.intellij.plugin.util.IntelliJApiHelper
import org.utbot.intellij.plugin.util.isAbstract

private const val RECENTS_KEY = "org.utbot.recents"
Expand All @@ -140,6 +145,9 @@ private const val WILL_BE_INSTALLED_LABEL = " (will be installed)"
private const val WILL_BE_CONFIGURED_LABEL = " (will be configured)"
private const val MINIMUM_TIMEOUT_VALUE_IN_SECONDS = 1

private const val ACTION_GENERATE = "Generate Tests"
private const val ACTION_GENERATE_AND_RUN = "Generate && Run" //Note that ampersand has to be escaped (doubled)

class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(model.project) {
companion object {
const val minSupportedSdkVersion = 8
Expand Down Expand Up @@ -213,6 +221,8 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m
twm.getToolWindow("Event Log")?.activate(null)
}

model.runGeneratedTestsWithCoverage = model.project.service<Settings>().runGeneratedTestsWithCoverage

init()
}

Expand Down Expand Up @@ -302,7 +312,7 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m
override fun createTitlePane(): JComponent? {
val sdkVersion = findSdkVersion()
//TODO:SAT-1571 investigate Android Studio specific sdk issues
if (sdkVersion?.feature in minSupportedSdkVersion..maxSupportedSdkVersion || AndroidApiHelper.isAndroidStudio()) return null
if (sdkVersion?.feature in minSupportedSdkVersion..maxSupportedSdkVersion || IntelliJApiHelper.isAndroidStudio()) return null
isOKActionEnabled = false
return SdkNotificationPanel(model, sdkVersion)
}
Expand Down Expand Up @@ -438,6 +448,45 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m
return null
}

class OKOptionAction(val testsModel: GenerateTestsModel, val okAction : Action) : AbstractAction(testsModel.getActionText()), OptionAction {
init {
putValue(DEFAULT_ACTION, java.lang.Boolean.TRUE)
putValue(FOCUSED_ACTION, java.lang.Boolean.TRUE)
}
private val generateAction = object : AbstractAction(ACTION_GENERATE) {
override fun actionPerformed(e: ActionEvent?) {
testsModel.runGeneratedTestsWithCoverage = false
updateButtonText(e)
}
}
private val generateAndRunAction = object : AbstractAction(ACTION_GENERATE_AND_RUN) {
override fun actionPerformed(e: ActionEvent?) {
testsModel.runGeneratedTestsWithCoverage = true
updateButtonText(e)
}
}

private fun updateButtonText(e: ActionEvent?) {
with(e?.source as JButton) {
text = TextWithMnemonic.parse(testsModel.getActionText()).dropMnemonic().text
testsModel.project.service<Settings>().runGeneratedTestsWithCoverage =
testsModel.runGeneratedTestsWithCoverage
repaint()
}
}

override fun actionPerformed(e: ActionEvent?) {
okAction.actionPerformed(e)
}

override fun getOptions(): Array<Action> {
if (testsModel.runGeneratedTestsWithCoverage) return arrayOf(generateAndRunAction, generateAction)
return arrayOf(generateAction, generateAndRunAction)
}
}

private val okOptionAction: OKOptionAction get() = OKOptionAction(model, super.getOKAction())
override fun getOKAction() = okOptionAction

override fun doOKAction() {
model.testPackageName =
Expand Down Expand Up @@ -964,6 +1013,9 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m
}
}

fun GenerateTestsModel.getActionText() : String =
if (this.runGeneratedTestsWithCoverage) ACTION_GENERATE_AND_RUN else ACTION_GENERATE

private fun ComboBox<CodeGenerationSettingItem>.setHelpTooltipTextChanger(helpLabel: JBLabel) {
addActionListener { event ->
val comboBox = event.source as ComboBox<*>
Expand Down

This file was deleted.

Loading