From 3dbe4d8a58cd80f3af74f8e13276f25b8619cbcd Mon Sep 17 00:00:00 2001 From: Danylo Biliaiev Date: Fri, 14 Feb 2025 15:01:34 +0100 Subject: [PATCH] General Refactor (#440) * Move class-level fields in TestSparkAction to actionPerformed Because this may lead to resource leaks. Closes #242 * Remove redundant qualifier names in TestSparkAction.kt * Extract the TestSparkActionWindow class into a separate file Extract the TestSparkActionWindow class into a separate file for better modularity and maintainability. This separation improves code readability and simplifies the structure of TestSparkAction by isolating UI logic. It will also reduce potential git conflicts. * Add intelliJPlatform folder to gitignore * Remove the irrelevant TODO in TestSparkActionWindow.kt If the psiHelper is not available, we'll get a NullPointerException automatically, so we don't need to throw anything manually. It's the responsibility of the caller class to make sure that the window is shown only when the dataContext returns some data and a psiHelper instance can be retrieved. * Refactor TestCompilerFactory to resolve issue #324 Use the provided javaSDKHomePath or derive it from the project's instance. If can't derive throw an exception. * Resolve ktlint issues --- .gitignore | 1 + .../testspark/actions/TestSparkAction.kt | 408 +----------------- .../actions/TestSparkActionWindow.kt | 402 +++++++++++++++++ .../tools/factories/TestCompilerFactory.kt | 29 +- 4 files changed, 418 insertions(+), 422 deletions(-) create mode 100644 src/main/kotlin/org/jetbrains/research/testspark/actions/TestSparkActionWindow.kt diff --git a/.gitignore b/.gitignore index aed022b20..8291da049 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ lib/evosuite-*.jar lib/JUnitRunner.jar **/evosuite-tests/ *.DS_Store +.intellijPlatform/ # Test projects src/test/resources/project/.idea/ diff --git a/src/main/kotlin/org/jetbrains/research/testspark/actions/TestSparkAction.kt b/src/main/kotlin/org/jetbrains/research/testspark/actions/TestSparkAction.kt index 5f537ac89..e519ef8bd 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/actions/TestSparkAction.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/actions/TestSparkAction.kt @@ -1,53 +1,14 @@ package org.jetbrains.research.testspark.actions -import com.intellij.notification.NotificationGroupManager -import com.intellij.notification.NotificationType import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.project.Project -import com.intellij.ui.components.JBScrollPane -import com.intellij.util.ui.FormBuilder import org.jetbrains.research.testspark.actions.controllers.TestGenerationController import org.jetbrains.research.testspark.actions.controllers.VisibilityController -import org.jetbrains.research.testspark.actions.evosuite.EvoSuitePanelBuilder -import org.jetbrains.research.testspark.actions.llm.LLMSampleSelectorBuilder -import org.jetbrains.research.testspark.actions.llm.LLMSetupPanelBuilder -import org.jetbrains.research.testspark.actions.template.PanelBuilder -import org.jetbrains.research.testspark.bundles.plugin.PluginLabelsBundle -import org.jetbrains.research.testspark.bundles.plugin.PluginMessagesBundle -import org.jetbrains.research.testspark.core.test.data.CodeType import org.jetbrains.research.testspark.display.TestSparkDisplayManager -import org.jetbrains.research.testspark.display.TestSparkIcons -import org.jetbrains.research.testspark.langwrappers.PsiHelper import org.jetbrains.research.testspark.langwrappers.PsiHelperProvider -import org.jetbrains.research.testspark.services.EvoSuiteSettingsService -import org.jetbrains.research.testspark.services.LLMSettingsService -import org.jetbrains.research.testspark.settings.evosuite.EvoSuiteSettingsState -import org.jetbrains.research.testspark.settings.llm.LLMSettingsState import org.jetbrains.research.testspark.tools.TestsExecutionResultManager -import org.jetbrains.research.testspark.tools.evosuite.EvoSuite -import org.jetbrains.research.testspark.tools.kex.Kex -import org.jetbrains.research.testspark.tools.llm.Llm -import org.jetbrains.research.testspark.tools.template.Tool -import java.awt.BorderLayout -import java.awt.CardLayout -import java.awt.Component -import java.awt.Dimension -import java.awt.Font -import java.awt.Toolkit -import java.awt.event.WindowAdapter -import java.awt.event.WindowEvent -import javax.swing.Box -import javax.swing.BoxLayout -import javax.swing.ButtonGroup -import javax.swing.JButton -import javax.swing.JFrame -import javax.swing.JLabel -import javax.swing.JPanel -import javax.swing.JRadioButton -import javax.swing.SwingConstants /** * Represents an action to be performed in the TestSpark plugin. @@ -56,12 +17,6 @@ import javax.swing.SwingConstants * It creates a dialog wrapper and displays it when the associated action is performed. */ class TestSparkAction : AnAction() { - // Controllers - private val visibilityController = VisibilityController() - private val testGenerationController = TestGenerationController() - - private val testSparkDisplayManager = TestSparkDisplayManager() - private val testsExecutionResultManager = TestsExecutionResultManager() /** * Handles the action performed event. @@ -73,7 +28,13 @@ class TestSparkAction : AnAction() { * This parameter is required. */ override fun actionPerformed(e: AnActionEvent) { - TestSparkActionWindow(e, visibilityController, testGenerationController, testSparkDisplayManager, testsExecutionResultManager) + TestSparkActionWindow( + e = e, + visibilityController = VisibilityController(), + testGenerationController = TestGenerationController(), + testSparkDisplayManager = TestSparkDisplayManager(), + testsExecutionResultManager = TestsExecutionResultManager(), + ) } /** @@ -93,360 +54,5 @@ class TestSparkAction : AnAction() { e.presentation.isEnabledAndVisible = (psiHelper != null) && psiHelper.availableForGeneration(e) } - /** - * Class representing the TestSparkActionWindow. - * - * @property e The AnActionEvent object. - */ - class TestSparkActionWindow( - private val e: AnActionEvent, - private val visibilityController: VisibilityController, - private val testGenerationController: TestGenerationController, - private val testSparkDisplayManager: TestSparkDisplayManager, - private val testsExecutionResultManager: TestsExecutionResultManager, - ) : - JFrame("TestSpark") { - private val project: Project = e.project!! - - private val llmSettingsState: LLMSettingsState - get() = project.getService(LLMSettingsService::class.java).state - private val evoSuiteSettingsState: EvoSuiteSettingsState - get() = project.getService(EvoSuiteSettingsService::class.java).state - - private val llmButton = JRadioButton("${Llm().name}") - private val evoSuiteButton = JRadioButton("${EvoSuite().name}") - private val kexButton = JRadioButton("${Kex().name}") - private val testGeneratorButtonGroup = ButtonGroup() - private val kexForLineCodeTypeErrMsg = JLabel() // The error displayed when if kex and line code type are chosen - - private val psiHelper: PsiHelper - get() { - val file = e.dataContext.getData(CommonDataKeys.PSI_FILE)!! - val psiHelper = PsiHelperProvider.getPsiHelper(file) - if (psiHelper == null) { - // TODO exception - } - return psiHelper!! - } - - private val codeTypes = psiHelper.getCurrentListOfCodeTypes(e) - private val caretOffset: Int = e.dataContext.getData(CommonDataKeys.CARET)?.caretModel?.primaryCaret!!.offset - private val fileUrl = e.dataContext.getData(CommonDataKeys.VIRTUAL_FILE)!!.presentableUrl - - private val codeTypeButtons: MutableList> = mutableListOf() - private val codeTypeButtonGroup = ButtonGroup() - - private val nextButton = JButton(PluginLabelsBundle.get("next")) - - private val cardLayout = CardLayout() - private val llmSetupPanelFactory = LLMSetupPanelBuilder(e, project) - private val llmSampleSelectorFactory = LLMSampleSelectorBuilder(project, psiHelper.language) - private val evoSuitePanelFactory = EvoSuitePanelBuilder(project) - - init { - if (!visibilityController.isVisible) { - visibilityController.isVisible = true - isVisible = true - - val panel = JPanel(cardLayout) - - panel.add(getMainPanel(), "1") - panel.add(createCardPanel(evoSuitePanelFactory), "2") - panel.add(createCardPanel(llmSetupPanelFactory), "3") - - panel.add( - JBScrollPane( - createCardPanel(llmSampleSelectorFactory), - JBScrollPane.VERTICAL_SCROLLBAR_ALWAYS, - JBScrollPane.HORIZONTAL_SCROLLBAR_NEVER, - ), - "4", - ) - - addListeners(panel) - - add(panel) - - pack() - - val dimension: Dimension = Toolkit.getDefaultToolkit().screenSize - val x = (dimension.width - size.width) / 2 - val y = (dimension.height - size.height) / 2 - setLocation(x, y) - } else { - NotificationGroupManager.getInstance() - .getNotificationGroup("Generation Error") - .createNotification( - PluginMessagesBundle.get("generationWindowWarningTitle"), - PluginMessagesBundle.get("generationWindowWarningMessage"), - NotificationType.WARNING, - ) - .notify(e.project) - } - } - - private fun createCardPanel(toolPanelBuilder: PanelBuilder): JPanel { - val cardPanel = JPanel(BorderLayout()) - cardPanel.add(toolPanelBuilder.getTitlePanel(), BorderLayout.NORTH) - cardPanel.add(toolPanelBuilder.getMiddlePanel(), BorderLayout.CENTER) - cardPanel.add(toolPanelBuilder.getBottomPanel(), BorderLayout.SOUTH) - - return cardPanel - } - - /** - * Returns the main panel for the test generator UI. - * This panel contains options for selecting the test generator and the code type. - * It also includes a button for proceeding to the next step. - * - * @return the main panel for the test generator UI - */ - private fun getMainPanel(): JPanel { - val panelTitle = JPanel() - val textTitle = JLabel("Welcome to TestSpark!") - textTitle.font = Font("Monochrome", Font.BOLD, 20) - panelTitle.add(JLabel(TestSparkIcons.pluginIcon)) - panelTitle.add(textTitle) - - if (Llm().appliedForLanguage(psiHelper.language)) testGeneratorButtonGroup.add(llmButton) - if (EvoSuite().appliedForLanguage(psiHelper.language)) testGeneratorButtonGroup.add(evoSuiteButton) - if (Kex().appliedForLanguage(psiHelper.language)) testGeneratorButtonGroup.add(kexButton) - - val testGeneratorPanel = JPanel() - testGeneratorPanel.add(JLabel("Select the test generator:")) - for (button in testGeneratorButtonGroup.elements) testGeneratorPanel.add(button) - if (testGeneratorButtonGroup.elements.toList().size == 1) { - // A single button is selected by default - testGeneratorButtonGroup.elements.toList()[0].isSelected = true - } - - for ((codeType, codeTypeName) in codeTypes) { - val button = JRadioButton(codeTypeName) - codeTypeButtons.add(codeType to button) - codeTypeButtonGroup.add(button) - } - - val codesToTestPanel = JPanel() - codesToTestPanel.add(JLabel("Select the code type:")) - if (codeTypeButtons.size == 1) { - // A single button is selected by default - codeTypeButtons[0].second.isSelected = true - } - for ((_, button) in codeTypeButtons) codesToTestPanel.add(button) - - val middlePanel = FormBuilder.createFormBuilder() - .setFormLeftIndent(10) - .addComponent( - testGeneratorPanel, - 10, - ) - .addComponent( - codesToTestPanel, - 10, - ) - .panel - - val nextButtonPanel = JPanel() - nextButtonPanel.layout = BoxLayout(nextButtonPanel, BoxLayout.Y_AXIS) - nextButton.isEnabled = false - nextButton.alignmentX = Component.CENTER_ALIGNMENT - kexForLineCodeTypeErrMsg.alignmentX = Component.CENTER_ALIGNMENT - kexForLineCodeTypeErrMsg.horizontalAlignment = SwingConstants.CENTER - nextButtonPanel.add(kexForLineCodeTypeErrMsg) - nextButtonPanel.add(Box.createVerticalStrut(10)) // Add some space between label and button - nextButtonPanel.add(nextButton) - updateNextButton() - - val cardPanel = JPanel(BorderLayout()) - cardPanel.add(panelTitle, BorderLayout.NORTH) - cardPanel.add(middlePanel, BorderLayout.CENTER) - cardPanel.add(nextButtonPanel, BorderLayout.SOUTH) - - return cardPanel - } - - /** - * Adds listeners to various components in the given panel. - * - * @param panel the JPanel to add listeners to - */ - private fun addListeners(panel: JPanel) { - addWindowListener(object : WindowAdapter() { - override fun windowClosing(e: WindowEvent?) { - visibilityController.isVisible = false - } - }) - - llmButton.addActionListener { - updateNextButton() - } - - evoSuiteButton.addActionListener { - updateNextButton() - } - - kexButton.addActionListener { - updateNextButton() - } - - for ((_, button) in codeTypeButtons) { - button.addActionListener { - llmSetupPanelFactory.setPromptEditorType(button.text) - updateNextButton() - } - } - - nextButton.addActionListener { - if (llmButton.isSelected && !llmSettingsState.llmSetupCheckBoxSelected && !llmSettingsState.provideTestSamplesCheckBoxSelected) { - startLLMGeneration() - } else if (llmButton.isSelected && !llmSettingsState.llmSetupCheckBoxSelected) { - cardLayout.next(panel) - cardLayout.next(panel) - cardLayout.next(panel) - pack() - } else if (llmButton.isSelected) { - cardLayout.next(panel) - cardLayout.next(panel) - pack() - } else if (kexButton.isSelected) { - startKexGeneration() - } else if (evoSuiteButton.isSelected && !evoSuiteSettingsState.evosuiteSetupCheckBoxSelected) { - startEvoSuiteGeneration() - } else { - cardLayout.next(panel) - pack() - } - } - - evoSuitePanelFactory.getBackButton().addActionListener { - cardLayout.previous(panel) - pack() - } - - llmSetupPanelFactory.getBackButton().addActionListener { - cardLayout.previous(panel) - cardLayout.previous(panel) - pack() - } - - llmSetupPanelFactory.getFinishedButton().addActionListener { - llmSetupPanelFactory.applyUpdates() - if (llmSettingsState.provideTestSamplesCheckBoxSelected) { - cardLayout.next(panel) - } else { - startLLMGeneration() - } - } - - llmSampleSelectorFactory.getAddButton().addActionListener { - size = Dimension(width, 500) - } - - llmSampleSelectorFactory.getBackButton().addActionListener { - if (llmSettingsState.llmSetupCheckBoxSelected) { - cardLayout.previous(panel) - } else { - cardLayout.previous(panel) - cardLayout.previous(panel) - cardLayout.previous(panel) - } - pack() - } - - llmSampleSelectorFactory.getFinishedButton().addActionListener { - llmSampleSelectorFactory.applyUpdates() - startLLMGeneration() - } - - evoSuitePanelFactory.getFinishedButton().addActionListener { - evoSuitePanelFactory.applyUpdates() - startEvoSuiteGeneration() - } - } - - private fun startUnitTestGenerationTool(tool: Tool) { - if (!testGenerationController.isGeneratorRunning(project)) { - val testSamplesCode = llmSampleSelectorFactory.getTestSamplesCode() - - for ((codeType, button) in codeTypeButtons) { - if (button.isSelected) { - when (codeType) { - CodeType.CLASS -> tool.generateTestsForClass( - project, - psiHelper, - caretOffset, - fileUrl, - testSamplesCode, - testGenerationController, - testSparkDisplayManager, - testsExecutionResultManager, - ) - CodeType.METHOD -> tool.generateTestsForMethod( - project, - psiHelper, - caretOffset, - fileUrl, - testSamplesCode, - testGenerationController, - testSparkDisplayManager, - testsExecutionResultManager, - ) - CodeType.LINE -> tool.generateTestsForLine( - project, - psiHelper, - caretOffset, - fileUrl, - testSamplesCode, - testGenerationController, - testSparkDisplayManager, - testsExecutionResultManager, - ) - } - break - } - } - } - - visibilityController.isVisible = false - dispose() - } - - private fun startKexGeneration() = startUnitTestGenerationTool(tool = Kex()) - private fun startEvoSuiteGeneration() = startUnitTestGenerationTool(tool = EvoSuite()) - private fun startLLMGeneration() = startUnitTestGenerationTool(tool = Llm()) - - /** - * Updates the state of the "Next" button based on the selected options. - * The "Next" button is enabled only if a test generator button (llmButton or evoSuiteButton) and at least one - * code type button (from codeTypeButtons) are selected. - * - * This method should be called whenever the mentioned above buttons are clicked. - */ - private fun updateNextButton() { - val isTestGeneratorButtonGroupSelected = llmButton.isSelected || evoSuiteButton.isSelected || kexButton.isSelected - val isCodeTypeButtonGroupSelected = codeTypeButtons.any { it.second.isSelected } - val kexForCodeLineType = - kexButton.isSelected && codeTypeButtons.any { (codeType, button) -> codeType == CodeType.LINE && button.isSelected } - if (kexForCodeLineType) { - kexForLineCodeTypeErrMsg.text = - "* Kex cannot generate tests for a single line. Please change your selection" - } else { - kexForLineCodeTypeErrMsg.text = "" - } - - nextButton.isEnabled = - isTestGeneratorButtonGroupSelected && isCodeTypeButtonGroupSelected && !kexForCodeLineType - - if ((llmButton.isSelected && !llmSettingsState.llmSetupCheckBoxSelected && !llmSettingsState.provideTestSamplesCheckBoxSelected) || - (evoSuiteButton.isSelected && !evoSuiteSettingsState.evosuiteSetupCheckBoxSelected) || - kexButton.isSelected - ) { - nextButton.text = PluginLabelsBundle.get("ok") - } else { - nextButton.text = PluginLabelsBundle.get("next") - } - } - } - override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT } diff --git a/src/main/kotlin/org/jetbrains/research/testspark/actions/TestSparkActionWindow.kt b/src/main/kotlin/org/jetbrains/research/testspark/actions/TestSparkActionWindow.kt new file mode 100644 index 000000000..a8ca7ac08 --- /dev/null +++ b/src/main/kotlin/org/jetbrains/research/testspark/actions/TestSparkActionWindow.kt @@ -0,0 +1,402 @@ +package org.jetbrains.research.testspark.actions + +import com.intellij.notification.NotificationGroupManager +import com.intellij.notification.NotificationType +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.project.Project +import com.intellij.ui.components.JBScrollPane +import com.intellij.util.ui.FormBuilder +import org.jetbrains.research.testspark.actions.controllers.TestGenerationController +import org.jetbrains.research.testspark.actions.controllers.VisibilityController +import org.jetbrains.research.testspark.actions.evosuite.EvoSuitePanelBuilder +import org.jetbrains.research.testspark.actions.llm.LLMSampleSelectorBuilder +import org.jetbrains.research.testspark.actions.llm.LLMSetupPanelBuilder +import org.jetbrains.research.testspark.actions.template.PanelBuilder +import org.jetbrains.research.testspark.bundles.plugin.PluginLabelsBundle +import org.jetbrains.research.testspark.bundles.plugin.PluginMessagesBundle +import org.jetbrains.research.testspark.core.test.data.CodeType +import org.jetbrains.research.testspark.display.TestSparkDisplayManager +import org.jetbrains.research.testspark.display.TestSparkIcons +import org.jetbrains.research.testspark.langwrappers.PsiHelper +import org.jetbrains.research.testspark.langwrappers.PsiHelperProvider +import org.jetbrains.research.testspark.services.EvoSuiteSettingsService +import org.jetbrains.research.testspark.services.LLMSettingsService +import org.jetbrains.research.testspark.settings.evosuite.EvoSuiteSettingsState +import org.jetbrains.research.testspark.settings.llm.LLMSettingsState +import org.jetbrains.research.testspark.tools.TestsExecutionResultManager +import org.jetbrains.research.testspark.tools.evosuite.EvoSuite +import org.jetbrains.research.testspark.tools.kex.Kex +import org.jetbrains.research.testspark.tools.llm.Llm +import org.jetbrains.research.testspark.tools.template.Tool +import java.awt.BorderLayout +import java.awt.CardLayout +import java.awt.Dimension +import java.awt.Font +import java.awt.Toolkit +import java.awt.event.WindowAdapter +import java.awt.event.WindowEvent +import javax.swing.Box +import javax.swing.BoxLayout +import javax.swing.ButtonGroup +import javax.swing.JButton +import javax.swing.JFrame +import javax.swing.JLabel +import javax.swing.JPanel +import javax.swing.JRadioButton +import javax.swing.SwingConstants + +/** + * Class representing the TestSparkActionWindow. + * + * @property e The AnActionEvent object. + */ +class TestSparkActionWindow( + private val e: AnActionEvent, + private val visibilityController: VisibilityController, + private val testGenerationController: TestGenerationController, + private val testSparkDisplayManager: TestSparkDisplayManager, + private val testsExecutionResultManager: TestsExecutionResultManager, +) : + JFrame("TestSpark") { + private val project: Project = e.project!! + + private val llmSettingsState: LLMSettingsState + get() = project.getService(LLMSettingsService::class.java).state + private val evoSuiteSettingsState: EvoSuiteSettingsState + get() = project.getService(EvoSuiteSettingsService::class.java).state + + private val llmButton = JRadioButton("${Llm().name}") + private val evoSuiteButton = JRadioButton("${EvoSuite().name}") + private val kexButton = JRadioButton("${Kex().name}") + private val testGeneratorButtonGroup = ButtonGroup() + private val kexForLineCodeTypeErrMsg = JLabel() // The error displayed when if kex and line code type are chosen + + private val psiHelper: PsiHelper + get() { + val file = e.dataContext.getData(CommonDataKeys.PSI_FILE)!! + val psiHelper = PsiHelperProvider.getPsiHelper(file) + return psiHelper!! + } + + private val codeTypes = psiHelper.getCurrentListOfCodeTypes(e) + private val caretOffset: Int = e.dataContext.getData(CommonDataKeys.CARET)?.caretModel?.primaryCaret!!.offset + private val fileUrl = e.dataContext.getData(CommonDataKeys.VIRTUAL_FILE)!!.presentableUrl + + private val codeTypeButtons: MutableList> = mutableListOf() + private val codeTypeButtonGroup = ButtonGroup() + + private val nextButton = JButton(PluginLabelsBundle.get("next")) + + private val cardLayout = CardLayout() + private val llmSetupPanelFactory = LLMSetupPanelBuilder(e, project) + private val llmSampleSelectorFactory = LLMSampleSelectorBuilder(project, psiHelper.language) + private val evoSuitePanelFactory = EvoSuitePanelBuilder(project) + + init { + if (!visibilityController.isVisible) { + visibilityController.isVisible = true + isVisible = true + + val panel = JPanel(cardLayout) + + panel.add(getMainPanel(), "1") + panel.add(createCardPanel(evoSuitePanelFactory), "2") + panel.add(createCardPanel(llmSetupPanelFactory), "3") + + panel.add( + JBScrollPane( + createCardPanel(llmSampleSelectorFactory), + JBScrollPane.VERTICAL_SCROLLBAR_ALWAYS, + JBScrollPane.HORIZONTAL_SCROLLBAR_NEVER, + ), + "4", + ) + + addListeners(panel) + + add(panel) + + pack() + + val dimension: Dimension = Toolkit.getDefaultToolkit().screenSize + val x = (dimension.width - size.width) / 2 + val y = (dimension.height - size.height) / 2 + setLocation(x, y) + } else { + NotificationGroupManager.getInstance() + .getNotificationGroup("Generation Error") + .createNotification( + PluginMessagesBundle.get("generationWindowWarningTitle"), + PluginMessagesBundle.get("generationWindowWarningMessage"), + NotificationType.WARNING, + ) + .notify(e.project) + } + } + + private fun createCardPanel(toolPanelBuilder: PanelBuilder): JPanel { + val cardPanel = JPanel(BorderLayout()) + cardPanel.add(toolPanelBuilder.getTitlePanel(), BorderLayout.NORTH) + cardPanel.add(toolPanelBuilder.getMiddlePanel(), BorderLayout.CENTER) + cardPanel.add(toolPanelBuilder.getBottomPanel(), BorderLayout.SOUTH) + + return cardPanel + } + + /** + * Returns the main panel for the test generator UI. + * This panel contains options for selecting the test generator and the code type. + * It also includes a button for proceeding to the next step. + * + * @return the main panel for the test generator UI + */ + private fun getMainPanel(): JPanel { + val panelTitle = JPanel() + val textTitle = JLabel("Welcome to TestSpark!") + textTitle.font = Font("Monochrome", Font.BOLD, 20) + panelTitle.add(JLabel(TestSparkIcons.pluginIcon)) + panelTitle.add(textTitle) + + if (Llm().appliedForLanguage(psiHelper.language)) testGeneratorButtonGroup.add(llmButton) + if (EvoSuite().appliedForLanguage(psiHelper.language)) testGeneratorButtonGroup.add(evoSuiteButton) + if (Kex().appliedForLanguage(psiHelper.language)) testGeneratorButtonGroup.add(kexButton) + + val testGeneratorPanel = JPanel() + testGeneratorPanel.add(JLabel("Select the test generator:")) + for (button in testGeneratorButtonGroup.elements) testGeneratorPanel.add(button) + if (testGeneratorButtonGroup.elements.toList().size == 1) { + // A single button is selected by default + testGeneratorButtonGroup.elements.toList()[0].isSelected = true + } + + for ((codeType, codeTypeName) in codeTypes) { + val button = JRadioButton(codeTypeName) + codeTypeButtons.add(codeType to button) + codeTypeButtonGroup.add(button) + } + + val codesToTestPanel = JPanel() + codesToTestPanel.add(JLabel("Select the code type:")) + if (codeTypeButtons.size == 1) { + // A single button is selected by default + codeTypeButtons[0].second.isSelected = true + } + for ((_, button) in codeTypeButtons) codesToTestPanel.add(button) + + val middlePanel = FormBuilder.createFormBuilder() + .setFormLeftIndent(10) + .addComponent( + testGeneratorPanel, + 10, + ) + .addComponent( + codesToTestPanel, + 10, + ) + .panel + + val nextButtonPanel = JPanel() + nextButtonPanel.layout = BoxLayout(nextButtonPanel, BoxLayout.Y_AXIS) + nextButton.isEnabled = false + nextButton.alignmentX = CENTER_ALIGNMENT + kexForLineCodeTypeErrMsg.alignmentX = CENTER_ALIGNMENT + kexForLineCodeTypeErrMsg.horizontalAlignment = SwingConstants.CENTER + nextButtonPanel.add(kexForLineCodeTypeErrMsg) + nextButtonPanel.add(Box.createVerticalStrut(10)) // Add some space between label and button + nextButtonPanel.add(nextButton) + updateNextButton() + + val cardPanel = JPanel(BorderLayout()) + cardPanel.add(panelTitle, BorderLayout.NORTH) + cardPanel.add(middlePanel, BorderLayout.CENTER) + cardPanel.add(nextButtonPanel, BorderLayout.SOUTH) + + return cardPanel + } + + /** + * Adds listeners to various components in the given panel. + * + * @param panel the JPanel to add listeners to + */ + private fun addListeners(panel: JPanel) { + addWindowListener(object : WindowAdapter() { + override fun windowClosing(e: WindowEvent?) { + visibilityController.isVisible = false + } + }) + + llmButton.addActionListener { + updateNextButton() + } + + evoSuiteButton.addActionListener { + updateNextButton() + } + + kexButton.addActionListener { + updateNextButton() + } + + for ((_, button) in codeTypeButtons) { + button.addActionListener { + llmSetupPanelFactory.setPromptEditorType(button.text) + updateNextButton() + } + } + + nextButton.addActionListener { + if (llmButton.isSelected && !llmSettingsState.llmSetupCheckBoxSelected && !llmSettingsState.provideTestSamplesCheckBoxSelected) { + startLLMGeneration() + } else if (llmButton.isSelected && !llmSettingsState.llmSetupCheckBoxSelected) { + cardLayout.next(panel) + cardLayout.next(panel) + cardLayout.next(panel) + pack() + } else if (llmButton.isSelected) { + cardLayout.next(panel) + cardLayout.next(panel) + pack() + } else if (kexButton.isSelected) { + startKexGeneration() + } else if (evoSuiteButton.isSelected && !evoSuiteSettingsState.evosuiteSetupCheckBoxSelected) { + startEvoSuiteGeneration() + } else { + cardLayout.next(panel) + pack() + } + } + + evoSuitePanelFactory.getBackButton().addActionListener { + cardLayout.previous(panel) + pack() + } + + llmSetupPanelFactory.getBackButton().addActionListener { + cardLayout.previous(panel) + cardLayout.previous(panel) + pack() + } + + llmSetupPanelFactory.getFinishedButton().addActionListener { + llmSetupPanelFactory.applyUpdates() + if (llmSettingsState.provideTestSamplesCheckBoxSelected) { + cardLayout.next(panel) + } else { + startLLMGeneration() + } + } + + llmSampleSelectorFactory.getAddButton().addActionListener { + size = Dimension(width, 500) + } + + llmSampleSelectorFactory.getBackButton().addActionListener { + if (llmSettingsState.llmSetupCheckBoxSelected) { + cardLayout.previous(panel) + } else { + cardLayout.previous(panel) + cardLayout.previous(panel) + cardLayout.previous(panel) + } + pack() + } + + llmSampleSelectorFactory.getFinishedButton().addActionListener { + llmSampleSelectorFactory.applyUpdates() + startLLMGeneration() + } + + evoSuitePanelFactory.getFinishedButton().addActionListener { + evoSuitePanelFactory.applyUpdates() + startEvoSuiteGeneration() + } + } + + private fun startUnitTestGenerationTool(tool: Tool) { + if (!testGenerationController.isGeneratorRunning(project)) { + val testSamplesCode = llmSampleSelectorFactory.getTestSamplesCode() + + for ((codeType, button) in codeTypeButtons) { + if (button.isSelected) { + when (codeType) { + CodeType.CLASS -> tool.generateTestsForClass( + project, + psiHelper, + caretOffset, + fileUrl, + testSamplesCode, + testGenerationController, + testSparkDisplayManager, + testsExecutionResultManager, + ) + + CodeType.METHOD -> tool.generateTestsForMethod( + project, + psiHelper, + caretOffset, + fileUrl, + testSamplesCode, + testGenerationController, + testSparkDisplayManager, + testsExecutionResultManager, + ) + + CodeType.LINE -> tool.generateTestsForLine( + project, + psiHelper, + caretOffset, + fileUrl, + testSamplesCode, + testGenerationController, + testSparkDisplayManager, + testsExecutionResultManager, + ) + } + break + } + } + } + + visibilityController.isVisible = false + dispose() + } + + private fun startKexGeneration() = startUnitTestGenerationTool(tool = Kex()) + private fun startEvoSuiteGeneration() = startUnitTestGenerationTool(tool = EvoSuite()) + private fun startLLMGeneration() = startUnitTestGenerationTool(tool = Llm()) + + /** + * Updates the state of the "Next" button based on the selected options. + * The "Next" button is enabled only if a test generator button (llmButton or evoSuiteButton) and at least one + * code type button (from codeTypeButtons) are selected. + * + * This method should be called whenever the mentioned above buttons are clicked. + */ + private fun updateNextButton() { + val isTestGeneratorButtonGroupSelected = + llmButton.isSelected || evoSuiteButton.isSelected || kexButton.isSelected + val isCodeTypeButtonGroupSelected = codeTypeButtons.any { it.second.isSelected } + val kexForCodeLineType = + kexButton.isSelected && codeTypeButtons.any { (codeType, button) -> codeType == CodeType.LINE && button.isSelected } + if (kexForCodeLineType) { + kexForLineCodeTypeErrMsg.text = + "* Kex cannot generate tests for a single line. Please change your selection" + } else { + kexForLineCodeTypeErrMsg.text = "" + } + + nextButton.isEnabled = + isTestGeneratorButtonGroupSelected && isCodeTypeButtonGroupSelected && !kexForCodeLineType + + if ((llmButton.isSelected && !llmSettingsState.llmSetupCheckBoxSelected && !llmSettingsState.provideTestSamplesCheckBoxSelected) || + (evoSuiteButton.isSelected && !evoSuiteSettingsState.evosuiteSetupCheckBoxSelected) || + kexButton.isSelected + ) { + nextButton.text = PluginLabelsBundle.get("ok") + } else { + nextButton.text = PluginLabelsBundle.get("next") + } + } +} diff --git a/src/main/kotlin/org/jetbrains/research/testspark/tools/factories/TestCompilerFactory.kt b/src/main/kotlin/org/jetbrains/research/testspark/tools/factories/TestCompilerFactory.kt index 025761b24..62a5d14ea 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/tools/factories/TestCompilerFactory.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/tools/factories/TestCompilerFactory.kt @@ -17,20 +17,17 @@ object TestCompilerFactory { project: Project, junitVersion: JUnitVersion, language: SupportedLanguage, - javaHomeDirectory: String? = null, + javaSDKHomePath: String = findJavaSDKHomePath(project) ): TestCompiler { val libraryPaths = LibraryPathsProvider.getTestCompilationLibraryPaths() val junitLibraryPaths = LibraryPathsProvider.getJUnitLibraryPaths(junitVersion) - // TODO add the warning window that for Java we always need the javaHomeDirectoryPath return when (language) { SupportedLanguage.Java -> { - val javaSDKHomePath = findJavaSDKHomePath(javaHomeDirectory, project) JavaTestCompiler(libraryPaths, junitLibraryPaths, javaSDKHomePath) } SupportedLanguage.Kotlin -> { // Kotlinc relies on java to compile kotlin files. - val javaSDKHomePath = findJavaSDKHomePath(javaHomeDirectory, project) // kotlinc should be under `[kotlinSDKHomeDirectory]/bin/kotlinc` val kotlinSDKHomeDirectory = KotlinPluginLayout.kotlinc.absolutePath KotlinTestCompiler(libraryPaths, junitLibraryPaths, kotlinSDKHomeDirectory, javaSDKHomePath) @@ -41,26 +38,16 @@ object TestCompilerFactory { /** * Finds the home path of the Java SDK. * - * @param javaHomeDirectory The directory where Java SDK is installed. If null, the project's configured SDK path is used. * @param project The project for which the Java SDK home path is being determined. * @return The home path of the Java SDK. * @throws JavaSDKMissingException If no Java SDK is configured for the project. */ - private fun findJavaSDKHomePath( - javaHomeDirectory: String?, - project: Project, - ): String { - val javaSDKHomePath = - javaHomeDirectory - ?: ProjectRootManager - .getInstance(project) - .projectSdk - ?.homeDirectory - ?.path - - if (javaSDKHomePath == null) { - throw JavaSDKMissingException(LLMMessagesBundle.get("javaSdkNotConfigured")) - } - return javaSDKHomePath + private fun findJavaSDKHomePath(project: Project): String { + return ProjectRootManager + .getInstance(project) + .projectSdk + ?.homeDirectory + ?.path + ?: (throw JavaSDKMissingException(LLMMessagesBundle.get("javaSdkNotConfigured"))) } }