diff --git a/src/main/kotlin/org/jetbrains/research/testspark/Util.kt b/src/main/kotlin/org/jetbrains/research/testspark/DataFilesUtil.kt similarity index 62% rename from src/main/kotlin/org/jetbrains/research/testspark/Util.kt rename to src/main/kotlin/org/jetbrains/research/testspark/DataFilesUtil.kt index b0c69d423..3665138bd 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/Util.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/DataFilesUtil.kt @@ -4,7 +4,7 @@ import com.intellij.openapi.util.io.FileUtilRt import java.io.File import java.util.Locale -class Util { +class DataFilesUtil { companion object { fun makeTmp() { val sep = File.separatorChar @@ -24,6 +24,26 @@ class Util { } } + fun cleanFolder(path: String) { + val folder = File(path) + + if (!folder.exists()) return + + if (folder.isDirectory) { + val files = folder.listFiles() + if (files != null) { + for (file in files) { + if (file.isDirectory) { + cleanFolder(file.absolutePath) + } else { + file.delete() + } + } + } + } + folder.delete() + } + val classpathSeparator: Char get() { var sep = ':' diff --git a/src/main/kotlin/org/jetbrains/research/testspark/data/TestGenerationData.kt b/src/main/kotlin/org/jetbrains/research/testspark/data/TestGenerationData.kt index 03a5c0015..b6383513a 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/data/TestGenerationData.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/data/TestGenerationData.kt @@ -1,6 +1,5 @@ package org.jetbrains.research.testspark.data -import org.jetbrains.research.testspark.editor.Workspace import org.jetbrains.research.testspark.tools.llm.test.TestCaseGeneratedByLLM /** @@ -18,13 +17,6 @@ class TestGenerationData { var runWith: String = "" var otherInfo: String = "" - // Maps a workspace file to the test generation jobs that were triggered on it. - // Currently, the file key is represented by its presentableUrl - var testGenerationResults: HashMap> = HashMap() - - // Maps a test generation job id to its corresponding test job information - var pendingTestResults: HashMap = HashMap() - // changing parameters with a large prompt var polyDepthReducing: Int = 0 var inputParamsDepthReducing: Int = 0 @@ -43,8 +35,6 @@ class TestGenerationData { packageLine = "" runWith = "" otherInfo = "" - testGenerationResults.clear() - pendingTestResults.clear() polyDepthReducing = 0 inputParamsDepthReducing = 0 compilableTestCases.clear() diff --git a/src/main/kotlin/org/jetbrains/research/testspark/display/ButtonCreator.kt b/src/main/kotlin/org/jetbrains/research/testspark/display/IconButtonCreator.kt similarity index 100% rename from src/main/kotlin/org/jetbrains/research/testspark/display/ButtonCreator.kt rename to src/main/kotlin/org/jetbrains/research/testspark/display/IconButtonCreator.kt diff --git a/src/main/kotlin/org/jetbrains/research/testspark/display/TestCasePanelFactory.kt b/src/main/kotlin/org/jetbrains/research/testspark/display/TestCasePanelFactory.kt index 59903cb81..b40e6d01c 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/display/TestCasePanelFactory.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/display/TestCasePanelFactory.kt @@ -19,13 +19,12 @@ import com.intellij.util.ui.JBUI import org.jetbrains.research.testspark.TestSparkBundle import org.jetbrains.research.testspark.TestSparkLabelsBundle import org.jetbrains.research.testspark.data.TestCase -import org.jetbrains.research.testspark.editor.Workspace -import org.jetbrains.research.testspark.services.COVERAGE_SELECTION_TOGGLE_TOPIC import org.jetbrains.research.testspark.services.ErrorService import org.jetbrains.research.testspark.services.JavaClassBuilderService import org.jetbrains.research.testspark.services.LLMChatService +import org.jetbrains.research.testspark.services.ReportLockingService import org.jetbrains.research.testspark.services.TestCaseDisplayService -import org.jetbrains.research.testspark.services.TestCoverageCollectorService +import org.jetbrains.research.testspark.services.TestStorageProcessingService import org.jetbrains.research.testspark.services.TestsExecutionResultService import org.jetbrains.research.testspark.tools.llm.test.TestSuiteGeneratedByLLM import org.jetbrains.research.testspark.tools.processStopped @@ -42,6 +41,7 @@ import javax.swing.FocusManager import javax.swing.JButton import javax.swing.JCheckBox import javax.swing.JLabel +import javax.swing.JOptionPane import javax.swing.JPanel import javax.swing.JTextField import javax.swing.ScrollPaneConstants @@ -52,7 +52,7 @@ import javax.swing.border.MatteBorder class TestCasePanelFactory( private val project: Project, private val testCase: TestCase, - private val editor: Editor, + editor: Editor, private val checkbox: JCheckBox, ) { private val panel = JPanel() @@ -69,8 +69,12 @@ class TestCasePanelFactory( private var allRequestsNumber = 1 private var currentRequestNumber = 1 + private val testCaseCodeToListOfCoveredLines: HashMap> = hashMapOf() + private val dimensionSize = 7 + private var isRemoved = false + // Add an editor to modify the test source code private val languageTextField = LanguageTextField( Language.findLanguageByID("JAVA"), @@ -227,7 +231,7 @@ class TestCasePanelFactory( val buttonsPanel = JPanel() buttonsPanel.layout = BoxLayout(buttonsPanel, BoxLayout.X_AXIS) buttonsPanel.add(Box.createRigidArea(Dimension(checkbox.preferredSize.width, checkbox.preferredSize.height))) - runTestButton.isEnabled = false + runTestButton.isEnabled = true buttonsPanel.add(runTestButton) loadingLabel.isVisible = false buttonsPanel.add(loadingLabel) @@ -242,7 +246,17 @@ class TestCasePanelFactory( panel.add(requestPanel) panel.add(buttonsPanel) - runTestButton.addActionListener { runTest() } + runTestButton.addActionListener { + val choice = JOptionPane.showConfirmDialog( + null, + TestSparkBundle.message("runCautionMessage"), + TestSparkBundle.message("confirmationTitle"), + JOptionPane.OK_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE, + ) + + if (choice == JOptionPane.OK_OPTION) runTest() + } resetButton.addActionListener { reset() } resetToLastRunButton.addActionListener { resetToLastRun() } removeButton.addActionListener { remove() } @@ -315,7 +329,7 @@ class TestCasePanelFactory( * Updates the user interface based on the provided code. */ private fun updateUI() { - updateTestCase() + updateTestCaseInformation() val lastRunCode = lastRunCodes[currentRequestNumber - 1] languageTextField.editor!!.markupModel.removeAllHighlighters() @@ -323,7 +337,7 @@ class TestCasePanelFactory( resetButton.isEnabled = testCase.testCode != initialCodes[currentRequestNumber - 1] resetToLastRunButton.isEnabled = testCase.testCode != lastRunCode - val error = getError(testCase.id, testCase.testCode) + val error = getError() if (error.isNullOrBlank()) { project.service().addCurrentPassedTest(testCase.id) } else { @@ -351,6 +365,15 @@ class TestCasePanelFactory( // select checkbox checkbox.isSelected = true + + if (testCaseCodeToListOfCoveredLines.containsKey(testCase.testCode)) { + testCase.coveredLines = testCaseCodeToListOfCoveredLines[testCase.testCode]!! + } else { + testCase.coveredLines = setOf() + } + + project.service().updateTestCase(testCase) + project.service().updateUI() } /** @@ -401,17 +424,6 @@ class TestCasePanelFactory( .getTestMethodNameFromClassWithTestCase(testCase.testName, code) testCase.testCode = code - // run new code - project.service().updateTestCase( - project.service() - .updateDataWithTestCase( - "${project.service().getClassFromTestCaseCode(testCase.testCode)}.java", - testCase.id, - testCase.testName, - code, - ), - ) - // update numbers allRequestsNumber++ currentRequestNumber = allRequestsNumber @@ -437,30 +449,29 @@ class TestCasePanelFactory( * label in the test case upper panel, removes all highlighters from the language text field, * and updates the UI. */ - private fun runTest() { + fun runTest() { + if (isRemoved) return + if (!runTestButton.isEnabled) return + loadingLabel.isVisible = true runTestButton.isEnabled = false - resetToLastRunButton.isEnabled = false - languageTextField.editor!!.markupModel.removeAllHighlighters() SwingUtilities.invokeLater { - project.service().updateTestCase( - project.service() - .updateDataWithTestCase( - "${project.service().getClassFromTestCaseCode(testCase.testCode)}.java", - testCase.id, - testCase.testName, - testCase.testCode, - ), - ) - updateBorder() - updateErrorLabel() + val newTestCase = project.service() + .processNewTestCase( + "${project.service().getClassFromTestCaseCode(testCase.testCode)}.java", + testCase.id, + testCase.testName, + testCase.testCode, + ) + testCase.coveredLines = newTestCase.coveredLines - lastRunCodes[currentRequestNumber - 1] = testCase.testCode + testCaseCodeToListOfCoveredLines[testCase.testCode] = testCase.coveredLines + lastRunCodes[currentRequestNumber - 1] = testCase.testCode loadingLabel.isVisible = false - project.service().updateUI() + updateUI() } } @@ -476,25 +487,10 @@ class TestCasePanelFactory( private fun reset() { WriteCommandAction.runWriteCommandAction(project) { languageTextField.document.setText(initialCodes[currentRequestNumber - 1]) - updateBorder() - project.service().updateTestCase(testCase) - resetButton.isEnabled = false - if (getError(testCase.id, testCase.testCode)!!.isBlank()) { - project.service().addPassedTest(testCase.id, testCase.testCode) - } else { - project.service() - .addFailedTest(testCase.id, testCase.testCode, errorLabel.toolTipText) - } - resetToLastRunButton.isEnabled = false - runTestButton.isEnabled = false - updateErrorLabel() - languageTextField.editor!!.markupModel.removeAllHighlighters() - currentCodes[currentRequestNumber - 1] = testCase.testCode lastRunCodes[currentRequestNumber - 1] = testCase.testCode - updateTestCase() - project.service().updateUI() + updateUI() } } @@ -503,18 +499,10 @@ class TestCasePanelFactory( */ private fun resetToLastRun() { WriteCommandAction.runWriteCommandAction(project) { - val code = lastRunCodes[currentRequestNumber - 1] - languageTextField.document.setText(code) - resetToLastRunButton.isEnabled = false - runTestButton.isEnabled = false - updateBorder() - updateErrorLabel() - languageTextField.editor!!.markupModel.removeAllHighlighters() - + languageTextField.document.setText(lastRunCodes[currentRequestNumber - 1]) currentCodes[currentRequestNumber - 1] = testCase.testCode - updateTestCase() - project.service().updateUI() + updateUI() } } @@ -525,21 +513,28 @@ class TestCasePanelFactory( * and updating the UI. */ private fun remove() { - // Remove the highlighting of the test - project.messageBus.syncPublisher(COVERAGE_SELECTION_TOGGLE_TOPIC) - .testGenerationResult(testCase.id, false, editor) - // Remove the test case from the cache project.service().removeTestCase(testCase.testName) + runTestButton.isEnabled = false + isRemoved = true + + project.service().removeTestCase(testCase) project.service().updateUI() } + /** + * Determines if the "Run" button is enabled. + * + * @return true if the "Run" button is enabled, false otherwise. + */ + fun isRunEnabled() = runTestButton.isEnabled + /** * Updates the border of the languageTextField based on the provided test name and text. */ private fun updateBorder() { - languageTextField.border = getBorder(testCase.id, testCase.testCode) + languageTextField.border = getBorder() } /** @@ -549,8 +544,7 @@ class TestCasePanelFactory( * @param testCaseCode the code of the test case * @return the error message for the test case */ - private fun getError(testCaseId: Int, testCaseCode: String) = - project.service().getError(testCaseId, testCaseCode) + fun getError() = project.service().getError(testCase.id, testCase.testCode) /** * Returns the border for a given test case. @@ -558,9 +552,9 @@ class TestCasePanelFactory( * @param testCaseId the id of the test case * @return the border for the test case */ - private fun getBorder(testCaseId: Int, testCaseCode: String): Border { + private fun getBorder(): Border { val size = 3 - return when (getError(testCaseId, testCaseCode)) { + return when (getError()) { null -> JBUI.Borders.empty() "" -> MatteBorder(size, size, size, size, JBColor.GREEN) else -> MatteBorder(size, size, size, size, JBColor.RED) @@ -574,7 +568,6 @@ class TestCasePanelFactory( */ private fun createRunTestButton(): JButton { val runTestButton = JButton(TestSparkLabelsBundle.defaultValue("run"), TestSparkIcons.runTest) - runTestButton.isEnabled = false runTestButton.isOpaque = false runTestButton.isContentAreaFilled = false runTestButton.isBorderPainted = true @@ -588,7 +581,6 @@ class TestCasePanelFactory( */ private fun switchToAnotherCode() { languageTextField.document.setText(currentCodes[currentRequestNumber - 1]) - updateTestCase() updateUI() } @@ -642,7 +634,7 @@ class TestCasePanelFactory( /** * Updates the current test case with the specified test name and test code. */ - private fun updateTestCase() { + private fun updateTestCaseInformation() { testCase.testName = project.service() .getTestMethodNameFromClassWithTestCase(testCase.testName, languageTextField.document.text) diff --git a/src/main/kotlin/org/jetbrains/research/testspark/display/TestSparkIcons.kt b/src/main/kotlin/org/jetbrains/research/testspark/display/TestSparkIcons.kt index 7ae3caf14..8311cc31e 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/display/TestSparkIcons.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/display/TestSparkIcons.kt @@ -60,6 +60,9 @@ object TestSparkIcons { @JvmField val documentation = IconLoader.getIcon("/icons/documentation.svg", javaClass) + @JvmField + val settings = IconLoader.getIcon("/icons/settings.svg", javaClass) + @JvmField val pluginIcon = IconLoader.getIcon("/META-INF/pluginIcon.svg", javaClass) } diff --git a/src/main/kotlin/org/jetbrains/research/testspark/display/TopButtonsPanelFactory.kt b/src/main/kotlin/org/jetbrains/research/testspark/display/TopButtonsPanelFactory.kt index 7098d4566..9be757dc0 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/display/TopButtonsPanelFactory.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/display/TopButtonsPanelFactory.kt @@ -5,7 +5,6 @@ import com.intellij.openapi.project.Project import org.jetbrains.research.testspark.TestSparkBundle import org.jetbrains.research.testspark.TestSparkLabelsBundle import org.jetbrains.research.testspark.services.TestCaseDisplayService -import org.jetbrains.research.testspark.services.TestsExecutionResultService import java.awt.Dimension import javax.swing.Box import javax.swing.BoxLayout @@ -16,6 +15,7 @@ import javax.swing.JOptionPane import javax.swing.JPanel class TopButtonsPanelFactory(private val project: Project) { + private var runAllButton: JButton = createRunAllTestButton() private var selectAllButton: JButton = createButton(TestSparkIcons.selectAll, TestSparkLabelsBundle.defaultValue("selectAllTip")) private var unselectAllButton: JButton = @@ -29,6 +29,8 @@ class TopButtonsPanelFactory(private val project: Project) { private val testsPassedText: String = "${TestSparkLabelsBundle.defaultValue("testsPassed")}: %d/%d" private var testsPassedLabel: JLabel = JLabel(testsPassedText) + private val testCasePanelFactories = arrayListOf() + fun getPanel(): JPanel { val panel = JPanel() panel.layout = BoxLayout(panel, BoxLayout.X_AXIS) @@ -38,6 +40,7 @@ class TopButtonsPanelFactory(private val project: Project) { panel.add(Box.createRigidArea(Dimension(10, 0))) panel.add(testsSelectedLabel) panel.add(Box.createHorizontalGlue()) + panel.add(runAllButton) panel.add(selectAllButton) panel.add(unselectAllButton) panel.add(removeAllButton) @@ -45,6 +48,7 @@ class TopButtonsPanelFactory(private val project: Project) { selectAllButton.addActionListener { toggleAllCheckboxes(true) } unselectAllButton.addActionListener { toggleAllCheckboxes(false) } removeAllButton.addActionListener { removeAllTestCases() } + runAllButton.addActionListener { runAllTestCases() } return panel } @@ -53,6 +57,12 @@ class TopButtonsPanelFactory(private val project: Project) { * Updates the labels. */ fun updateTopLabels() { + var numberOfPassedTests = 0 + for (testCasePanelFactory in testCasePanelFactories) { + if (testCasePanelFactory.getError() == "") { + numberOfPassedTests++ + } + } testsSelectedLabel.text = String.format( testsSelectedText, project.service().getTestsSelected(), @@ -61,10 +71,22 @@ class TopButtonsPanelFactory(private val project: Project) { testsPassedLabel.text = String.format( testsPassedText, - project.service() - .getTestCasePanels().size - project.service().size(), + numberOfPassedTests, project.service().getTestCasePanels().size, ) + runAllButton.isEnabled = false + for (testCasePanelFactory in testCasePanelFactories) { + runAllButton.isEnabled = runAllButton.isEnabled || testCasePanelFactory.isRunEnabled() + } + } + + /** + * Sets the array of TestCasePanelFactory objects. + * + * @param testCasePanelFactories The ArrayList containing the TestCasePanelFactory objects to be set. + */ + fun setTestCasePanelFactoriesArray(testCasePanelFactories: ArrayList) { + this.testCasePanelFactories.addAll(testCasePanelFactories) } /** @@ -100,4 +122,42 @@ class TopButtonsPanelFactory(private val project: Project) { project.service().clear() } + + /** + * Executes all test cases. + * + * This method presents a caution message to the user and asks for confirmation before executing the test cases. + * If the user confirms, it iterates through each test case panel factory and runs the corresponding test. + */ + private fun runAllTestCases() { + val choice = JOptionPane.showConfirmDialog( + null, + TestSparkBundle.message("runCautionMessage"), + TestSparkBundle.message("confirmationTitle"), + JOptionPane.OK_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE, + ) + + if (choice == JOptionPane.CANCEL_OPTION) return + + runAllButton.isEnabled = false + + for (testCasePanelFactory in testCasePanelFactories) { + // todo use doClick() function after removing JOptionPane + testCasePanelFactory.runTest() + } + } + + /** + * Creates a JButton for running all tests. + * + * @return a JButton for running all tests + */ + private fun createRunAllTestButton(): JButton { + val runTestButton = JButton(TestSparkLabelsBundle.defaultValue("runAll"), TestSparkIcons.runTest) + runTestButton.isOpaque = false + runTestButton.isContentAreaFilled = false + runTestButton.isBorderPainted = true + return runTestButton + } } diff --git a/src/main/kotlin/org/jetbrains/research/testspark/editor/Workspace.kt b/src/main/kotlin/org/jetbrains/research/testspark/editor/Workspace.kt index a3a660ca8..447e2f48a 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/editor/Workspace.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/editor/Workspace.kt @@ -1,36 +1,17 @@ package org.jetbrains.research.testspark.editor -import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.Service import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.EditorFactory -import com.intellij.openapi.editor.event.DocumentEvent -import com.intellij.openapi.editor.event.DocumentListener -import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiClass -import org.jetbrains.research.testspark.data.Report -import org.jetbrains.research.testspark.data.TestCase +import org.jetbrains.research.testspark.DataFilesUtil import org.jetbrains.research.testspark.data.TestGenerationData -import org.jetbrains.research.testspark.services.COVERAGE_SELECTION_TOGGLE_TOPIC -import org.jetbrains.research.testspark.services.CoverageSelectionToggleListener import org.jetbrains.research.testspark.services.CoverageVisualisationService import org.jetbrains.research.testspark.services.ErrorService import org.jetbrains.research.testspark.services.TestCaseDisplayService import org.jetbrains.research.testspark.services.TestsExecutionResultService -import org.jetbrains.research.testspark.tools.evosuite.validation.VALIDATION_RESULT_TOPIC -import org.jetbrains.research.testspark.tools.evosuite.validation.ValidationResultListener -import org.jetbrains.research.testspark.tools.evosuite.validation.Validator -import java.io.File /** * Workspace state service @@ -40,55 +21,21 @@ import java.io.File * */ @Service(Service.Level.PROJECT) -class Workspace(private val project: Project) : Disposable { - data class TestJobInfo( - val fileUrl: String, - var targetUnit: String, - val modificationTS: Long, - val jobId: String, - val targetClassPath: String, - ) - - class TestJob( - val info: TestJobInfo, - var report: Report, - val selectedTests: HashSet, - ) { - private fun getSelectedTests(): List { - return report.testCaseList.filter { selectedTests.contains(it.key) }.map { it.value } - } - - fun getSelectedLines(): HashSet { - val lineSet: HashSet = HashSet() - getSelectedTests().map { lineSet.addAll(it.coveredLines) } - return lineSet - } - - fun updateReport(report: Report) { - this.report = report - } - } - +class Workspace { var editor: Editor? = null - var testJob: TestJob? = null // The class path of the project. var projectClassPath: String? = null - var testResultDirectory: String? = null - var testResultName: String? = null // The path to save the generated test results. var resultPath: String? = null // The base directory of the project. var baseDir: String? = null - var vFile: VirtualFile? = null // The URL of the file being tested. var fileUrl: String? = null - // The modification stamp of the file being tested. - var modificationStamp: Long? = null var cutPsiClass: PsiClass? = null // The module to cut. @@ -97,77 +44,8 @@ class Workspace(private val project: Project) : Disposable { // The fully qualified name of the class being tested. var classFQN: String? = null - private val log = Logger.getInstance(this.javaClass) - - private var listenerDisposable: Disposable? = null - var testGenerationData = TestGenerationData() - var key: TestJobInfo? = null - - init { - val connection = project.messageBus.connect() - - // Set event listener for coverage visualization toggles for specific methods. - // These are triggered whenever the user toggles a test case's checkbox. - connection.subscribe( - COVERAGE_SELECTION_TOGGLE_TOPIC, - object : CoverageSelectionToggleListener { - override fun testGenerationResult(testId: Int, selected: Boolean, editor: Editor) { - val vFile = vFileForDocument(editor.document) ?: return - val fileKey = vFile.presentableUrl - val testJob = testGenerationData.testGenerationResults[fileKey]?.last() ?: return - val modTs = editor.document.modificationStamp - - if (selected) { - testJob.selectedTests.add(testId) - } else { - testJob.selectedTests.remove(testId) - } - - // update coverage only if the modification timestamp is the same - if (testJob.info.modificationTS == modTs) { - updateCoverage(testJob.getSelectedLines(), testJob.selectedTests, testJob.report, editor) - } - } - }, - ) - - connection.subscribe( - VALIDATION_RESULT_TOPIC, - object : ValidationResultListener { - override fun validationResult(junitResult: Validator.JUnitResult) { - showValidationResult(junitResult) - } - }, - ) - - val disposable = - Disposer.newDisposable(ApplicationManager.getApplication(), "Workspace.myDocumentListenerDisposable") - - // Set event listener for document changes. These are triggered whenever the user changes - // the contents of the editor. - EditorFactory.getInstance().eventMulticaster.addDocumentListener( - object : DocumentListener { - override fun documentChanged(event: DocumentEvent) { - super.documentChanged(event) - val file = FileDocumentManager.getInstance().getFile(event.document) ?: return - val fileName = file.presentableUrl - val modTs = event.document.modificationStamp - - val testJob = lastTestGeneration(fileName) ?: return - - if (testJob.info.modificationTS != modTs) { - val editor = editorForVFile(file) - editor?.markupModel?.removeAllHighlighters() - } - } - }, - disposable, - ) - listenerDisposable = disposable - } - /** * Clears the given project's test-related data, including test case display, * error service, coverage visualization, and test generation data. @@ -179,172 +57,7 @@ class Workspace(private val project: Project) : Disposable { project.service().clear() project.service().clear() testGenerationData.clear() - project.service().cleanFolder(resultPath!!) + DataFilesUtil.cleanFolder(resultPath!!) project.service().clear() } - - /** - * Updates the state after the action of publishing results. - * - * @param testResultName the test result job id which was received - * @param testReport the generated test suite - * @param cacheLazyPipeline the runner that was instantiated but not used to create the test suite - * due to a cache hit, or null if there was a cache miss - * @return the test job that was generated - */ - fun receiveGenerationResult( - testResultName: String, - testReport: Report, - cachedJobKey: TestJobInfo? = null, - ): TestJobInfo { - val pendingJobKey = testGenerationData.pendingTestResults.remove(testResultName)!! - - val jobKey = cachedJobKey ?: pendingJobKey - - val resultsForFile = testGenerationData.testGenerationResults.getOrPut(jobKey.fileUrl) { ArrayList() } - val displayedSet = HashSet() - displayedSet.addAll(testReport.testCaseList.values.stream().map { it.id }.toList()) - - testJob = TestJob(jobKey, testReport, displayedSet) - resultsForFile.add(testJob!!) - - updateEditorForFileUrl(jobKey.fileUrl) - - project.service().cleanFolder(resultPath!!) - - if (editor != null) { - showReport() - } else { - log.info("No editor opened for received test result") - } - - return jobKey - } - - /** - * Utility function that returns the editor for a specific file url, - * in case it is opened in the IDE - */ - private fun updateEditorForFileUrl(fileUrl: String) { - val documentManager = FileDocumentManager.getInstance() - // https://intellij-support.jetbrains.com/hc/en-us/community/posts/360004480599/comments/360000703299 - FileEditorManager.getInstance(project).selectedEditors.map { it as TextEditor }.map { it.editor }.map { - val currentFile = documentManager.getFile(it.document) - if (currentFile != null) { - if (currentFile.presentableUrl == fileUrl) { - editor = it - } - } - } - } - - /** - * Utility function that returns the editor for a specific VirtualFile - * in case it is opened in the IDE - */ - private fun editorForVFile(file: VirtualFile): Editor? { - val documentManager = FileDocumentManager.getInstance() - FileEditorManager.getInstance(project).allEditors.map { it as TextEditor }.map { it.editor }.map { - val currentFile = documentManager.getFile(it.document) - if (currentFile != null) { - if (currentFile == file) { - return it - } - } - } - return null - } - - /** - * Utility function that returns the virtual file - * for a specific document instance - */ - private fun vFileForDocument(document: Document): VirtualFile? { - val documentManager = FileDocumentManager.getInstance() - return documentManager.getFile(document) - } - - fun updateTestCase(testCase: TestCase) { - val updatedReport = testJob!!.report - updatedReport.testCaseList.remove(testCase.id) - updatedReport.testCaseList[testCase.id] = testCase - updatedReport.normalized() - testJob!!.updateReport(updatedReport) - project.service().showCoverage(updatedReport, editor!!) - } - - /** - * Function that calls the services responsible for visualizing - * coverage and displaying the generated test cases. This - * is used whenever a new test generation result gets published. - */ - private fun showReport() { - project.service().showGeneratedTests(editor!!) - project.service().showCoverage(testJob!!.report, editor!!) - } - - /** - * Shows the validation result by marking failing test cases. - * - * @param validationResult The JUnit result of the validation. - */ - private fun showValidationResult(validationResult: Validator.JUnitResult) { - val testCaseDisplayService = project.service() - testCaseDisplayService.markFailingTestCases(validationResult.failedTestNames) - } - - /** - * Function used to update coverage visualization information. - * Overrides the current visualization state with the one provided. - * Wrapper over [CoverageVisualisationService.updateCoverage] - */ - private fun updateCoverage( - linesToCover: Set, - selectedTests: HashSet, - testCaseList: Report, - editor: Editor, - ) { - val visualizationService = project.service() - visualizationService.updateCoverage(linesToCover, selectedTests, testCaseList, editor) - } - - /** - * Retrieves the last test job from the test generation results for a given file. - * - * @param fileName The name of the file for which to retrieve the last test job. - * @return The last test job generated for the specified file, or null if no test job is found. - */ - private fun lastTestGeneration(fileName: String): TestJob? { - return testGenerationData.testGenerationResults[fileName]?.last() - } - - /** - * Disposes the listenerDisposable if it is not null. - */ - override fun dispose() { - listenerDisposable?.let { Disposer.dispose(it) } - } - - /** - * Clean data folder - */ - fun cleanFolder(path: String) { - val folder = File(path) - - if (!folder.exists()) return - - if (folder.isDirectory) { - val files = folder.listFiles() - if (files != null) { - for (file in files) { - if (file.isDirectory) { - cleanFolder(file.absolutePath) - } else { - file.delete() - } - } - } - } - folder.delete() - } } diff --git a/src/main/kotlin/org/jetbrains/research/testspark/listener/TestGenerationResultListenerImpl.kt b/src/main/kotlin/org/jetbrains/research/testspark/listener/TestGenerationResultListenerImpl.kt deleted file mode 100644 index c6c6dd27b..000000000 --- a/src/main/kotlin/org/jetbrains/research/testspark/listener/TestGenerationResultListenerImpl.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.jetbrains.research.testspark.listener - -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.project.Project -import org.jetbrains.research.testspark.data.Report -import org.jetbrains.research.testspark.editor.Workspace -import org.jetbrains.research.testspark.services.TestCaseCachingService -import org.jetbrains.research.testspark.tools.TestGenerationResultListener - -class TestGenerationResultListenerImpl(private val project: Project) : TestGenerationResultListener { - private val log = Logger.getInstance(this.javaClass) - - override fun testGenerationResult(testReport: Report, resultName: String, fileUrl: String) { - log.info("Received test result for $resultName") - val workspace = project.service() - - ApplicationManager.getApplication().invokeLater { - val jobInfo = workspace.receiveGenerationResult(resultName, testReport) - cacheGeneratedTestCases(testReport, fileUrl, jobInfo) - } - } - - /** - * Put the generated test cases into the cache. - * @param testReport the test report - * @param fileUrl the file url - * @param jobInfo the job info of the generated tests - */ - private fun cacheGeneratedTestCases(testReport: Report, fileUrl: String, jobInfo: Workspace.TestJobInfo) { - val cache = project.service() - cache.putIntoCache(fileUrl, testReport, jobInfo) - } -} diff --git a/src/main/kotlin/org/jetbrains/research/testspark/services/CoverageSelectionToggleListener.kt b/src/main/kotlin/org/jetbrains/research/testspark/services/CoverageSelectionToggleListener.kt deleted file mode 100644 index a93b415dd..000000000 --- a/src/main/kotlin/org/jetbrains/research/testspark/services/CoverageSelectionToggleListener.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.jetbrains.research.testspark.services - -import com.intellij.openapi.editor.Editor -import com.intellij.util.messages.Topic - -val COVERAGE_SELECTION_TOGGLE_TOPIC: Topic = Topic.create( - "COVERAGE_SELECTION_TOGGLE_TOPIC", - CoverageSelectionToggleListener::class.java, - Topic.BroadcastDirection.TO_PARENT, -) - -/** - * Topic interface for sending and receiving test results produced by evosuite - * - * Subscribers to this topic will receive a Report whenever the plugin triggers a test - * generation job with testspark.evosuite.Runner - */ -interface CoverageSelectionToggleListener { - fun testGenerationResult(testId: Int, selected: Boolean, editor: Editor) -} diff --git a/src/main/kotlin/org/jetbrains/research/testspark/services/CoverageVisualisationService.kt b/src/main/kotlin/org/jetbrains/research/testspark/services/CoverageVisualisationService.kt index f6c753699..7a4252457 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/services/CoverageVisualisationService.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/services/CoverageVisualisationService.kt @@ -15,6 +15,7 @@ import org.jetbrains.research.testspark.TestSparkLabelsBundle import org.jetbrains.research.testspark.TestSparkToolTipsBundle import org.jetbrains.research.testspark.coverage.CoverageRenderer import org.jetbrains.research.testspark.data.Report +import org.jetbrains.research.testspark.editor.Workspace import java.awt.Color import kotlin.math.roundToInt @@ -68,14 +69,17 @@ class CoverageVisualisationService(private val project: Project) { * Instantiates tab for coverage table and calls function to update coverage. * * @param testReport the generated tests summary - * @param editor editor whose contents tests were generated for */ - fun showCoverage(testReport: Report, editor: Editor) { + fun showCoverage(testReport: Report) { // Show toolWindow statistics fillToolWindowContents(testReport) createToolWindowTab() - updateCoverage(testReport.allCoveredLines, testReport.testCaseList.values.stream().map { it.id }.toList().toHashSet(), testReport, editor) + updateCoverage( + testReport.allCoveredLines, + testReport.testCaseList.values.stream().map { it.id }.toList().toHashSet(), + testReport, + ) } /** @@ -85,21 +89,23 @@ class CoverageVisualisationService(private val project: Project) { * @param linesToCover total set of lines to cover * @param testReport report used for gutter information * @param selectedTests hash set of selected test names - * @param editor editor instance where coverage should be updated */ fun updateCoverage( linesToCover: Set, selectedTests: HashSet, testReport: Report, - editor: Editor, ) { - currentHighlightedData = HighlightedData(linesToCover, selectedTests, testReport, editor) + currentHighlightedData = + HighlightedData(linesToCover, selectedTests, testReport, project.service().editor!!) clear() val settingsProjectState = project.service().state if (settingsProjectState.showCoverageCheckboxSelected) { - val color = JBColor(TestSparkToolTipsBundle.defaultValue("colorName"), Color(settingsProjectState.colorRed, settingsProjectState.colorGreen, settingsProjectState.colorBlue)) + val color = JBColor( + TestSparkToolTipsBundle.defaultValue("colorName"), + Color(settingsProjectState.colorRed, settingsProjectState.colorGreen, settingsProjectState.colorBlue) + ) val colorForLines = JBColor( TestSparkToolTipsBundle.defaultValue("colorName"), Color( @@ -135,8 +141,11 @@ class CoverageVisualisationService(private val project: Project) { for (i in linesToCover) { val line = i - 1 - val hl = - editor.markupModel.addLineHighlighter(line, HighlighterLayer.ADDITIONAL_SYNTAX, textAttribute) + val hl = project.service().editor!!.markupModel.addLineHighlighter( + line, + HighlighterLayer.ADDITIONAL_SYNTAX, + textAttribute + ) val testsCoveringLine = testReport.testCaseList.filter { x -> i in x.value.coveredLines && x.value.id in selectedTests } diff --git a/src/main/kotlin/org/jetbrains/research/testspark/services/ReportLockingService.kt b/src/main/kotlin/org/jetbrains/research/testspark/services/ReportLockingService.kt new file mode 100644 index 000000000..817c7b009 --- /dev/null +++ b/src/main/kotlin/org/jetbrains/research/testspark/services/ReportLockingService.kt @@ -0,0 +1,63 @@ +package org.jetbrains.research.testspark.services + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import org.jetbrains.research.testspark.data.Report +import org.jetbrains.research.testspark.data.TestCase +import org.jetbrains.research.testspark.editor.Workspace + +@Service(Service.Level.PROJECT) +class ReportLockingService(private val project: Project) { + // final report + private var report: Report? = null + + private val unselectedTestCases = HashMap() + + private val log = Logger.getInstance(this.javaClass) + + fun getReport() = report!! + + /** + * Updates the state after the action of publishing results. + * @param report the generated test suite + */ + fun receiveReport(report: Report) { + this.report = report + + project.service().updateEditorForFileUrl(project.service().testGenerationData.fileUrl) + + if (project.service().editor != null) { + project.service().displayTestCases() + project.service().showCoverage(report) + } else { + log.info("No editor opened for received test result") + } + } + + fun updateTestCase(testCase: TestCase) { + report!!.testCaseList.remove(testCase.id) + report!!.testCaseList[testCase.id] = testCase + report!!.normalized() + project.service().showCoverage(report!!) + } + + fun removeTestCase(testCase: TestCase) { + report!!.testCaseList.remove(testCase.id) + report!!.normalized() + project.service().showCoverage(report!!) + } + + fun unselectTestCase(testCaseId: Int) { + unselectedTestCases[testCaseId] = report!!.testCaseList[testCaseId]!! + removeTestCase(report!!.testCaseList[testCaseId]!!) + } + + fun selectTestCase(testCaseId: Int) { + report!!.testCaseList[testCaseId] = unselectedTestCases[testCaseId]!! + unselectedTestCases.remove(testCaseId) + report!!.normalized() + project.service().showCoverage(report!!) + } +} diff --git a/src/main/kotlin/org/jetbrains/research/testspark/services/RunCommandLineService.kt b/src/main/kotlin/org/jetbrains/research/testspark/services/RunCommandLineService.kt index e0b304219..a2d1f75db 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/services/RunCommandLineService.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/services/RunCommandLineService.kt @@ -2,7 +2,7 @@ package org.jetbrains.research.testspark.services import com.intellij.openapi.components.Service import com.intellij.openapi.project.Project -import org.jetbrains.research.testspark.Util +import org.jetbrains.research.testspark.DataFilesUtil import java.io.BufferedReader import java.io.InputStreamReader @@ -21,7 +21,7 @@ class RunCommandLineService(private val project: Project) { /** * Since Windows does not provide bash, use cmd or similar default command line interpreter */ - val process = if (Util.isWindows()) { + val process = if (DataFilesUtil.isWindows()) { ProcessBuilder() .command("cmd", "/c", cmd.joinToString(" ")) .redirectErrorStream(true) diff --git a/src/main/kotlin/org/jetbrains/research/testspark/services/StaticInvalidationService.kt b/src/main/kotlin/org/jetbrains/research/testspark/services/StaticInvalidationService.kt deleted file mode 100644 index 25e345b48..000000000 --- a/src/main/kotlin/org/jetbrains/research/testspark/services/StaticInvalidationService.kt +++ /dev/null @@ -1,173 +0,0 @@ -package org.jetbrains.research.testspark.services - -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service -import com.intellij.openapi.editor.Document -import com.intellij.openapi.project.Project -import com.intellij.psi.PsiClass -import com.intellij.psi.PsiCodeBlock -import com.intellij.psi.PsiDocumentManager -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiModifierList -import com.intellij.psi.PsiReferenceParameterList -import com.intellij.psi.PsiWhiteSpace -import com.intellij.refactoring.suggested.endOffset -import com.intellij.refactoring.suggested.startOffset -import org.jetbrains.research.testspark.actions.isMethodConcrete - -/** - * Service used to invalidate cache statically - * @param project - the project - */ -@Service(Service.Level.PROJECT) -class StaticInvalidationService(private val project: Project) { - - // HashMap of Class filePath -> (HashMap of Method Signature -> (Method Body, Covered Lines)) - private var savedMethods: HashMap, Set>>> = HashMap() - - /** - * Method to invalidate the changed parts of a cache. - * - * @param fileUrl the url of a file - * @param lines the lines to invalidate tests - */ - fun invalidateCacheLines(fileUrl: String, lines: Set) { - val cache = project.service() - for (line in lines) { - cache.invalidateFromCache(fileUrl, line + 1, line + 1) - } - } - - /** - * Checks for a file what lines have been modified - * @param file the file in question - * @return list of lines that have been modified - */ - fun invalidate(file: PsiFile): Set { - // Initialize list - val linesToDiscard: MutableSet = mutableSetOf() - // Get basic details - val filePath = file.virtualFile.presentableUrl - val classToValidate = file.children.filterIsInstance() - val doc: Document = PsiDocumentManager.getInstance(file.project).getDocument(file) ?: return setOf() - // Go over all classes in file - classToValidate.forEach { currentClass -> - val className = currentClass.name!! - val methods = currentClass.methods - val map: HashMap, Set>> = HashMap() - methods.forEach { - if (isMethodConcrete(it)) { - val startLine = doc.getLineNumber(it.startOffset) - val endLine = doc.getLineNumber(it.endOffset) - map[it.hierarchicalMethodSignature.toString()] = - Pair(recursePsiMethodBody(it.body!!), startLine.rangeTo(endLine).toSet()) - } - } - // validate each class - linesToDiscard.addAll(validateClass(filePath, map, className)) - } - - return linesToDiscard - } - - /** - * Checks if method has been changed since last test generation (no, if first run) - * Always updates the lines the method uses (lines can change due to whitespace, but method can still be valid) - * @param signature the method in question - * @param body list of the body-elements of the method, without whitespaces - * @param methods hashmap of previously tested methods and their bodies - * @return the lines of methods which have been changed (based on previous lines for method) - */ - private fun validateMethod( - signature: String, - body: Pair, Set>, - methods: HashMap, Set>>, - ): Set { - val savedBody = methods[signature] - - // if body doesn't exist, method seen first time - // if amount of elements in body different, method surely changed - if (savedBody == null) { - methods[signature] = body - return setOf() - } - if (body.first.size != savedBody.first.size) { - methods[signature] = body - return savedBody.second - } - // compare each element (no whitespace) - body.first.zip(savedBody.first).forEach { - if (!it.first.text.equals(it.second.text)) { - methods[signature] = body - return savedBody.second - } - } - methods[signature] = Pair(savedBody.first, body.second) - return setOf() - } - - /** - * Checks for class what methods within it have been modified - * @param filePath path where the class is located - * @param methods the methods of the class and the lines they cover - * @param className the name of the class (used for preciser hashing) - * @return the lines in the class that have been changed (based on previous lines for methods) - */ - private fun validateClass( - filePath: String, - methods: HashMap, Set>>, - className: String, - ): MutableSet { - val linesToDiscard = mutableSetOf() - - // get old methods - val methodsSaved = savedMethods.getOrPut("$filePath/$className") { methods } - // validate each method against old methods - methods.keys.forEach { - val changed = validateMethod(it, methods[it]!!, methodsSaved) - linesToDiscard.addAll(changed) - } - - return linesToDiscard - } - - /** - * Returns a list of PsiElements that are part of the method psi children. - * - * @param psiMethodBody the psiBody of the method - * @return the list of PsiElements - */ - private fun recursePsiMethodBody(psiMethodBody: PsiCodeBlock): ArrayList { - val psiList: ArrayList = arrayListOf() - for (psiStatement in psiMethodBody.statements) { - if (psiStatement.children.isEmpty()) { - psiList.add(psiStatement) - continue - } - for (psiElement in psiStatement.children) { - recurseTree(psiElement, psiList) - } - } - return psiList - } - - /** - * Append elements to the list of PsiElements - * - * @param psiElement the psi of the element - * @param psiList list to append psi elements - */ - private fun recurseTree(psiElement: PsiElement, psiList: ArrayList) { - if (psiElement is PsiWhiteSpace || psiElement is PsiReferenceParameterList || psiElement is PsiModifierList) { - return - } - if (psiElement.children.isEmpty()) { - psiList.add(psiElement) - return - } - for (psiElementChild in psiElement.children) { - recurseTree(psiElementChild, psiList) - } - } -} diff --git a/src/main/kotlin/org/jetbrains/research/testspark/services/TestCaseCachingService.kt b/src/main/kotlin/org/jetbrains/research/testspark/services/TestCaseCachingService.kt deleted file mode 100644 index 084862743..000000000 --- a/src/main/kotlin/org/jetbrains/research/testspark/services/TestCaseCachingService.kt +++ /dev/null @@ -1,341 +0,0 @@ -package org.jetbrains.research.testspark.services - -import com.intellij.openapi.components.Service -import com.intellij.openapi.diagnostic.Logger -import org.evosuite.utils.CompactTestCase -import org.jetbrains.research.testspark.data.Report -import org.jetbrains.research.testspark.data.TestCase -import org.jetbrains.research.testspark.editor.Workspace -import kotlin.streams.toList - -@Service(Service.Level.PROJECT) -class TestCaseCachingService { - private val log: Logger = Logger.getInstance(this.javaClass) - private val files = mutableMapOf() - private val filesLock = Object() - - /** - * Insert test cases into the cache. - * - * @param fileUrl the URL of the file that the test cases belong to - * @param report the report containing the test cases - * @param jobInfo the TestJobInfo for this report - */ - fun putIntoCache(fileUrl: String, report: Report, jobInfo: Workspace.TestJobInfo) { - log.info("Inserting ${report.testCaseList.size} test cases into cache for $fileUrl") - val file = getFileTestCaseCache(fileUrl) - file.putIntoCache(report, jobInfo) - } - - /** - * Retrieve test cases from the cache that cover at least one line in the specified range. - * - * @param fileUrl the URL of the file that the test cases belong to - * @param lineFrom the start of the range - * @param lineTo the end of the range - * @return a list of test cases - */ - fun retrieveFromCache(fileUrl: String, lineFrom: Int, lineTo: Int): List { - val fileTestCaseCache = getFileTestCaseCache(fileUrl) - val compactTestCasesList: List = fileTestCaseCache.retrieveFromCache(lineFrom, lineTo) - val result: MutableList = mutableListOf() - for ((index, compactTestCase: CompactTestCase) in compactTestCasesList.withIndex()) { - result.add(TestCase(index, compactTestCase)) - } - log.info("Retrieved ${result.size} test cases from cache for $fileUrl") - return result - } - - /** - * Invalidate test cases from the cache. - * - * @param fileUrl the URL of the file that the test cases belong to - * @param lineFrom the start of the range - * @param lineTo the end of the ranges - */ - fun invalidateFromCache(fileUrl: String, lineFrom: Int, lineTo: Int) { - val fileTestCaseCache = getFileTestCaseCache(fileUrl) - fileTestCaseCache.invalidateFromCache(lineFrom, lineTo) - } - - /** - * Invalidate test case from the cache. - * - * @param fileUrl the URL of the file that the test cases belong to - * @param testCode the code of the test case - */ - fun invalidateFromCache(fileUrl: String, testCode: String) { - val fileTestCaseCache = getFileTestCaseCache(fileUrl) - fileTestCaseCache.invalidateFromCache(testCode) - } - - /** - * Retrieve the TestJobInfo for the specified test. - * - * @param fileUrl the URL of the file that the test cases belong to - * @param testCode the code of the test case - */ - fun getTestJobInfo(fileUrl: String, testCode: String): Workspace.TestJobInfo? { - val fileTestCaseCache = getFileTestCaseCache(fileUrl) - return fileTestCaseCache.getTestJobInfo(testCode) - } - - /** - * Get the file test case cache for the specified file. - * - * @param fileUrl the URL of the file - * @return the file test case cache - */ - private fun getFileTestCaseCache(fileUrl: String): FileTestCaseCache { - synchronized(filesLock) { - return files.getOrPut(fileUrl) { FileTestCaseCache() } - } - } - - /** - * A data structure keeping track of the cached test cases in a file. - */ - private class FileTestCaseCache { - private val lines = mutableMapOf() - private val linesLock = Object() - - // Used for retrieving references of unique test cases - private val caseIndex = mutableMapOf() - private val caseIndexLock = Object() - - /** - * Insert test cases into the file cache. - * - * @param report the report containing the test cases - * @param jobInfo the TestJobInfo for this test report - */ - fun putIntoCache(report: Report, jobInfo: Workspace.TestJobInfo) { - report.testCaseList.values.forEach { testCase -> - val cachedCompactTestCase = CachedCompactTestCase.fromCompactTestCase(testCase, this, jobInfo) - - synchronized(caseIndexLock) { - // invalidate existing test with the same code if one exists - caseIndex[cachedCompactTestCase.testCode]?.invalidate() - - // save new test in index - caseIndex[cachedCompactTestCase.testCode] = cachedCompactTestCase - } - - testCase.coveredLines.forEach { lineNumber -> - val line: LineTestCaseCache = getLineTestCaseCache(lineNumber) - line.putIntoCache(cachedCompactTestCase) - } - } - } - - /** - * Retrieve test cases from the file cache that cover at least one line in the specified range. - * - * @param lineFrom the start of the range - * @param lineTo the end of the range - * @return the test cases that cover at least one line in the specified range - */ - fun retrieveFromCache(lineFrom: Int, lineTo: Int): List { - val result = mutableSetOf() - for (lineNumber in lineFrom..lineTo) { - val line: LineTestCaseCache = getLineTestCaseCache(lineNumber) - result.addAll(line.getTestCases()) - } - - return result.map { it.toCompactTestCase() } - } - - /** - * Invalidate test cases from cache. - * - * @param lineFrom the start of the range - * @param lineTo the end of the range - */ - fun invalidateFromCache(lineFrom: Int, lineTo: Int) { - for (lineNumber in lineFrom..lineTo) { - val tests: LineTestCaseCache = getLineTestCaseCache(lineNumber) - for (test in tests.getTestCases()) { - test.invalidate() - } - } - } - - /** - * Invalidate test cases from cache. - * - * @param testCode the code of the test case - */ - fun invalidateFromCache(testCode: String) { - synchronized(caseIndexLock) { - caseIndex[testCode]?.invalidate() - } - } - - /** - * Retrieve the TestJobInfo for the specified test. - * - * @param testCode the code of the test case - */ - fun getTestJobInfo(testCode: String): Workspace.TestJobInfo? { - synchronized(caseIndexLock) { - return caseIndex[testCode]?.jobInfo - } - } - - /** - * Get the line test case cache for the specified line. - * @return the line test case cache - */ - fun getLineTestCaseCache(lineNumber: Int): LineTestCaseCache { - synchronized(linesLock) { - return lines.getOrPut(lineNumber) { - // The line number is included here so that it can easily be modified - // in all instances when line numbers change - LineTestCaseCache(lineNumber) - } - } - } - - /** - * Remove a test case from the test case index. - * - * @param cachedCompactTestCase the test case to remove - */ - fun removeTestCaseFromIndex(cachedCompactTestCase: CachedCompactTestCase) { - synchronized(caseIndexLock) { - caseIndex.remove(cachedCompactTestCase.testCode) - } - } - } - - /** - * A data structure keeping track of the cached test cases in a line. - * Furthermore, this structure keeps track of the line number of a particular line as it changes. - * If the line number of this structure is updated, this will automatically be reflected in all - * cached test cases. - */ - private class LineTestCaseCache(var lineNumber: Int) { - private val testCases = mutableListOf() - private val testCasesLock = Object() - - /** - * Insert a test case into the line cache. - * - * @param testCase the test case to insert - */ - fun putIntoCache(testCase: CachedCompactTestCase) { - synchronized(testCasesLock) { - testCases.add(testCase) - } - } - - /** - * Get the test cases that cover this line. - * - * @return the test cases that cover this line - */ - fun getTestCases(): List { - synchronized(testCasesLock) { - return testCases.toList() - } - } - - /** - * Remove a test case from this line cache. - * - * @param cachedCompactTestCase the test case to remove - */ - fun removeTestCase(cachedCompactTestCase: CachedCompactTestCase) { - synchronized(testCasesLock) { - testCases.remove(cachedCompactTestCase) - } - } - } - - /** - * A data structure keeping track of a cached test case. - */ - private class CachedCompactTestCase( - val testName: String, - val testCode: String, - val jobInfo: Workspace.TestJobInfo, - private val coveredLines: Set, - private val fileTestCaseCache: FileTestCaseCache, - ) { - /** - * Convert this cached test case back to a compact test case. - * - * @return the compact test case - */ - fun toCompactTestCase(): CompactTestCase { - return CompactTestCase( - "$testName (from cache ${testCode.hashCode()})", - testCode, - coveredLines.map { it.lineNumber }.toSet(), - // empty mutation and branch coverage as this is not calculated dynamically - setOf(), - setOf(), - ) - } - - /** - * Remove this test case from the cache. - */ - fun invalidate() { - // Remove from index - fileTestCaseCache.removeTestCaseFromIndex(this) - - // Remove from line caches - this.coveredLines.forEach { - it.removeTestCase(this) - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as CachedCompactTestCase - - if (testName != other.testName) return false - if (testCode != other.testCode) return false - if (coveredLines != other.coveredLines) return false - - return true - } - - override fun hashCode(): Int { - var result = testName.hashCode() - result = 31 * result + testCode.hashCode() - result = 31 * result + coveredLines.hashCode() - return result - } - - companion object { - - /** - * Create a cached test case from a compact test case. - * - * @param testCase the compact test case - * @param fileTestCaseCache the file test case cache (in order to retrieve line cache references) - * @param jobInfo the TestJobInfo for this test - * @return the cached test case - */ - fun fromCompactTestCase( - testCase: TestCase, - fileTestCaseCache: FileTestCaseCache, - jobInfo: Workspace.TestJobInfo, - ): CachedCompactTestCase { - return CachedCompactTestCase( - testCase.testName, - testCase.testCode.replace("\r\n", "\n"), - jobInfo, - testCase.coveredLines.map { - fileTestCaseCache.getLineTestCaseCache(it) - }.toSet(), - fileTestCaseCache, - ) - } - } - } -} diff --git a/src/main/kotlin/org/jetbrains/research/testspark/services/TestCaseDisplayService.kt b/src/main/kotlin/org/jetbrains/research/testspark/services/TestCaseDisplayService.kt index 2ee016ed8..ef24bb674 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/services/TestCaseDisplayService.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/services/TestCaseDisplayService.kt @@ -1,14 +1,14 @@ package org.jetbrains.research.testspark.services -import com.intellij.coverage.CoverageSuitesBundle import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.components.Service import com.intellij.openapi.components.service -import com.intellij.openapi.editor.Editor import com.intellij.openapi.fileChooser.FileChooser import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VirtualFile @@ -27,10 +27,8 @@ import com.intellij.ui.content.Content import com.intellij.ui.content.ContentFactory import com.intellij.ui.content.ContentManager import com.intellij.util.containers.stream -import com.intellij.util.ui.JBUI import org.jetbrains.research.testspark.TestSparkLabelsBundle import org.jetbrains.research.testspark.TestSparkToolTipsBundle -import org.jetbrains.research.testspark.data.Report import org.jetbrains.research.testspark.display.TestCasePanelFactory import org.jetbrains.research.testspark.display.TopButtonsPanelFactory import org.jetbrains.research.testspark.editor.Workspace @@ -39,7 +37,6 @@ import java.awt.Color import java.awt.Dimension import java.io.File import java.util.Locale -import javax.swing.BorderFactory import javax.swing.Box import javax.swing.BoxLayout import javax.swing.JButton @@ -48,7 +45,6 @@ import javax.swing.JOptionPane import javax.swing.JPanel import javax.swing.JSeparator import javax.swing.SwingConstants -import javax.swing.border.Border @Service(Service.Level.PROJECT) class TestCaseDisplayService(private val project: Project) { @@ -68,13 +64,11 @@ class TestCaseDisplayService(private val project: Project) { ) private var testCasePanels: HashMap = HashMap() - private var originalTestCases: HashMap = HashMap() private var testsSelected: Int = 0 // Default color for the editors in the tool window private var defaultEditorColor: Color? = null - private var defaultBorder: Border? = null // Content Manager to be able to add / remove tabs from tool window private var contentManager: ContentManager? = null @@ -82,8 +76,6 @@ class TestCaseDisplayService(private val project: Project) { // Variable to keep reference to the coverage visualisation content private var content: Content? = null - private var currentJacocoCoverageBundle: CoverageSuitesBundle? = null - init { allTestCasePanel.layout = BoxLayout(allTestCasePanel, BoxLayout.Y_AXIS) mainPanel.layout = BorderLayout() @@ -98,61 +90,40 @@ class TestCaseDisplayService(private val project: Project) { applyButton.addActionListener { applyTests() } } - /** - * Sets the JaCoCo report for the coverage suites bundle. - * - * @param coverageSuitesBundle The coverage suites bundle to set the JaCoCo report for. - */ - fun setJacocoReport(coverageSuitesBundle: CoverageSuitesBundle) { - currentJacocoCoverageBundle = coverageSuitesBundle - } - - /** - * Creates the complete panel in the "Generated Tests" tab, - * and adds the "Generated Tests" tab to the sidebar tool window. - * - * @param editor editor instance where coverage should be - * visualized - */ - fun showGeneratedTests(editor: Editor) { - displayTestCases(project.service().testJob!!.report, editor) - createToolWindowTab() - } - /** * Fill the panel with the generated test cases. Remove all previously shown test cases. * Add Tests and their names to a List of pairs (used for highlighting) - * - * @param testReport The report from which each testcase should be displayed - * @param editor editor instance where coverage should be - * visualized */ - private fun displayTestCases(testReport: Report, editor: Editor) { + fun displayTestCases() { + val report = project.service().getReport() + val editor = project.service().editor!! + allTestCasePanel.removeAll() testCasePanels.clear() - originalTestCases.clear() addSeparator() - testReport.testCaseList.values.forEach { + // TestCasePanelFactories array + val testCasePanelFactories = arrayListOf() + + report.testCaseList.values.forEach { val testCase = it val testCasePanel = JPanel() testCasePanel.layout = BorderLayout() - // Fix Windows line separators - originalTestCases[testCase.testName] = testCase.testCode - // Add a checkbox to select the test val checkbox = JCheckBox() checkbox.isSelected = true checkbox.addItemListener { - project.messageBus.syncPublisher(COVERAGE_SELECTION_TOGGLE_TOPIC) - .testGenerationResult(testCase.id, checkbox.isSelected, editor) - // Update the number of selected tests testsSelected -= (1 - 2 * checkbox.isSelected.compareTo(false)) - topButtonsPanelFactory.updateTopLabels() + if (checkbox.isSelected) + project.service().selectTestCase(testCase.id) + else + project.service().unselectTestCase(testCase.id) + + updateUI() } testCasePanel.add(checkbox, BorderLayout.WEST) @@ -161,6 +132,8 @@ class TestCaseDisplayService(private val project: Project) { testCasePanel.add(testCasePanelFactory.getMiddlePanel(), BorderLayout.CENTER) testCasePanel.add(testCasePanelFactory.getBottomPanel(), BorderLayout.SOUTH) + testCasePanelFactories.add(testCasePanelFactory) + testCasePanel.add(Box.createRigidArea(Dimension(12, 0)), BorderLayout.EAST) // Add panel to parent panel @@ -173,6 +146,11 @@ class TestCaseDisplayService(private val project: Project) { testsSelected = testCasePanels.size topButtonsPanelFactory.updateTopLabels() } + + topButtonsPanelFactory.setTestCasePanelFactoriesArray(testCasePanelFactories) + topButtonsPanelFactory.updateTopLabels() + + createToolWindowTab() } /** @@ -260,25 +238,6 @@ class TestCaseDisplayService(private val project: Project) { }.start() } - /** - * Highlight tests failing dynamic validation - * - * @param names set of test names that fail - */ - fun markFailingTestCases(names: Set) { - for (testCase in testCasePanels) { - if (names.contains(testCase.key)) { - val editor = getEditor(testCase.key) ?: return - val highlightColor = JBColor(TestSparkToolTipsBundle.defaultValue("colorName"), Color(255, 0, 0, 90)) - defaultBorder = editor.border - editor.border = BorderFactory.createLineBorder(highlightColor, 3) - } else { - val editor = getEditor(testCase.key) ?: return - editor.border = JBUI.Borders.empty() - } - } - } - /** * Highlight a range of editors * @param names list of test names to pass to highlight function @@ -412,10 +371,6 @@ class TestCaseDisplayService(private val project: Project) { appendTestsToClass(testCaseComponents, psiClass!!, psiJavaFile!!) } - // The scheduled tests will be submitted in the background - // (they will be checked every 5 minutes and also when the project is closed) - scheduleTelemetry(selectedTestCases) - // Remove the selected test cases from the cache and the tool window UI removeSelectedTestCases(selectedTestCasePanels) @@ -503,6 +458,23 @@ class TestCaseDisplayService(private val project: Project) { ) } + /** + * Utility function that returns the editor for a specific file url, + * in case it is opened in the IDE + */ + fun updateEditorForFileUrl(fileUrl: String) { + val documentManager = FileDocumentManager.getInstance() + // https://intellij-support.jetbrains.com/hc/en-us/community/posts/360004480599/comments/360000703299 + FileEditorManager.getInstance(project).selectedEditors.map { it as TextEditor }.map { it.editor }.map { + val currentFile = documentManager.getFile(it.document) + if (currentFile != null) { + if (currentFile.presentableUrl == fileUrl) { + project.service().editor = it + } + } + } + } + /** * Creates a new toolWindow tab for the coverage visualisation. */ @@ -563,13 +535,6 @@ class TestCaseDisplayService(private val project: Project) { * @param testCaseName the name of the test */ fun removeTestCase(testCaseName: String) { - // Remove the test from the cache - project.service() - .invalidateFromCache( - project.service().testGenerationData.fileUrl, - originalTestCases[testCaseName]!!, - ) - // Update the number of selected test cases if necessary if ((testCasePanels[testCaseName]!!.getComponent(0) as JCheckBox).isSelected) { testsSelected-- @@ -582,23 +547,6 @@ class TestCaseDisplayService(private val project: Project) { testCasePanels.remove(testCaseName) } - /** - * Schedules the telemetry for the selected and modified tests. - * - * @param selectedTestCases the test cases selected by the user - */ - private fun scheduleTelemetry(selectedTestCases: List) { - val telemetryService = project.service() - telemetryService.scheduleTestCasesForTelemetry( - selectedTestCases.map { - val modified = getEditor(it)!!.text - val original = originalTestCases[it]!! - - TestSparkTelemetryService.ModifiedTestCase(original, modified) - }.filter { it.modified != it.original }, - ) - } - /** * Updates the user interface of the tool window. * diff --git a/src/main/kotlin/org/jetbrains/research/testspark/services/TestCoverageCollectorService.kt b/src/main/kotlin/org/jetbrains/research/testspark/services/TestStorageProcessingService.kt similarity index 81% rename from src/main/kotlin/org/jetbrains/research/testspark/services/TestCoverageCollectorService.kt rename to src/main/kotlin/org/jetbrains/research/testspark/services/TestStorageProcessingService.kt index e57d1d712..41bda0ea5 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/services/TestCoverageCollectorService.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/services/TestStorageProcessingService.kt @@ -10,10 +10,11 @@ import com.intellij.openapi.roots.CompilerModuleExtension import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.roots.ProjectRootManager import com.intellij.openapi.util.io.FileUtilRt -import org.jetbrains.research.testspark.Util +import org.jetbrains.research.testspark.DataFilesUtil import org.jetbrains.research.testspark.data.TestCase import org.jetbrains.research.testspark.editor.Workspace import org.jetbrains.research.testspark.tools.getBuildPath +import org.jetbrains.research.testspark.tools.llm.test.TestCaseGeneratedByLLM import java.io.File import java.util.UUID import kotlin.collections.ArrayList @@ -21,7 +22,7 @@ import kotlin.io.path.Path import kotlin.io.path.createDirectories @Service(Service.Level.PROJECT) -class TestCoverageCollectorService(private val project: Project) { +class TestStorageProcessingService(private val project: Project) { private val sep = File.separatorChar private val id = UUID.randomUUID().toString() @@ -42,14 +43,12 @@ class TestCoverageCollectorService(private val project: Project) { */ private fun getPath(buildPath: String): String { // create the path for the command - val pluginsPath = PathManager.getPluginsPath() - val junitPath = "\"$pluginsPath${sep}TestSpark${sep}lib${sep}junit-4.13.jar\"" - val mockitoPath = "\"$pluginsPath${sep}TestSpark${sep}lib${sep}mockito-core-5.0.0.jar\"" - val hamcrestPath = "\"$pluginsPath${sep}TestSpark${sep}lib${sep}hamcrest-core-1.3.jar\"" - val byteBuddy = "\"$pluginsPath${sep}TestSpark${sep}lib${sep}byte-buddy-1.14.6.jar\"" - val byteBuddyAgent = "\"$pluginsPath${sep}TestSpark${sep}lib${sep}byte-buddy-agent-1.14.6.jar\"" - - val sep = Util.classpathSeparator + val junitPath = getLibrary("junit-4.13.jar") + val mockitoPath = getLibrary("mockito-core-5.0.0.jar") + val hamcrestPath = getLibrary("hamcrest-core-1.3.jar") + val byteBuddy = getLibrary("byte-buddy-1.14.6.jar") + val byteBuddyAgent = getLibrary("byte-buddy-agent-1.14.6.jar") + val sep = DataFilesUtil.classpathSeparator return "$junitPath${sep}$hamcrestPath${sep}$mockitoPath${sep}$byteBuddy${sep}$byteBuddyAgent${sep}$buildPath" } @@ -76,7 +75,7 @@ class TestCoverageCollectorService(private val project: Project) { // find the proper javac val javaCompile = File(javaHomeDirectory.path).walk() .filter { - val isCompilerName = if (Util.isWindows()) it.name.equals("javac.exe") else it.name.equals("javac") + val isCompilerName = if (DataFilesUtil.isWindows()) it.name.equals("javac.exe") else it.name.equals("javac") isCompilerName && it.isFile } .first() @@ -86,7 +85,7 @@ class TestCoverageCollectorService(private val project: Project) { arrayListOf( javaCompile.absolutePath, "-cp", - project.service().getPath(projectBuildPath), + getPath(projectBuildPath), path, ), ) @@ -100,6 +99,25 @@ class TestCoverageCollectorService(private val project: Project) { return Pair(File(classFilePath).exists(), errorMsg) } + /** + * Compiles the generated test file using the proper javac and returns a Pair + * indicating whether the compilation was successful and any error message encountered during compilation. + * + * @return A Pair containing a boolean indicating whether the compilation was successful + * and a String containing any error message encountered during compilation. + */ + fun compileTestCases(generatedTestCasesPaths: List, buildPath: String, testCases: MutableList): Boolean { + var result = false + for (index in generatedTestCasesPaths.indices) { + val compilable = compileCode(generatedTestCasesPaths[index], buildPath).first + result = result || compilable + if (compilable) { + project.service().testGenerationData.compilableTestCases.add(testCases[index]) + } + } + return result + } + /** * Save the generated tests to a specified directory. * @@ -109,7 +127,7 @@ class TestCoverageCollectorService(private val project: Project) { * @param testFileName The name of the test file. * @return The path where the generated tests are saved. */ - fun saveGeneratedTests(packageString: String, code: String, resultPath: String, testFileName: String): String { + fun saveGeneratedTest(packageString: String, code: String, resultPath: String, testFileName: String): String { // Generate the final path for the generated tests var generatedTestPath = "$resultPath${File.separatorChar}" packageString.split(".").forEach { directory -> @@ -136,7 +154,7 @@ class TestCoverageCollectorService(private val project: Project) { * @param generatedTestPackage The package where the generated test class is located. * @return An empty string if the test execution is successful, otherwise an error message. */ - fun createXmlFromJacoco( + private fun createXmlFromJacoco( className: String, dataFileName: String, testCaseName: String, @@ -146,13 +164,13 @@ class TestCoverageCollectorService(private val project: Project) { // find the proper javac val javaRunner = File(javaHomeDirectory.path).walk() .filter { - val isJavaName = if (Util.isWindows()) it.name.equals("java.exe") else it.name.equals("java") + val isJavaName = if (DataFilesUtil.isWindows()) it.name.equals("java.exe") else it.name.equals("java") isJavaName && it.isFile } .first() // JaCoCo libs - val jacocoAgentDir = project.service().getLibrary("jacocoagent.jar") - val jacocoCLIDir = project.service().getLibrary("jacococli.jar") + val jacocoAgentDir = getLibrary("jacocoagent.jar") + val jacocoCLIDir = getLibrary("jacococli.jar") val sourceRoots = ModuleRootManager.getInstance(project.service().cutModule!!).getSourceRoots(false) // unique name @@ -165,7 +183,7 @@ class TestCoverageCollectorService(private val project: Project) { javaRunner.absolutePath, "-javaagent:$jacocoAgentDir=destfile=$dataFileName.exec,append=false,includes=${project.service().classFQN}", "-cp", - "${project.service().getPath(projectBuildPath)}${project.service().getLibrary("JUnitRunner.jar")}${Util.classpathSeparator}$resultPath", + "${getPath(projectBuildPath)}${getLibrary("JUnitRunner.jar")}${DataFilesUtil.classpathSeparator}$resultPath", "org.jetbrains.research.SingleJUnitTestRunner", name, ), @@ -210,7 +228,7 @@ class TestCoverageCollectorService(private val project: Project) { * @param testCaseCode The test case code. * @param xmlFileName The XML file name to read data from. */ - fun getTestCaseFromXml( + private fun getTestCaseFromXml( testCaseId: Int, testCaseName: String, testCaseCode: String, @@ -258,7 +276,7 @@ class TestCoverageCollectorService(private val project: Project) { * @param testExecutionError error output (including the thrown stack trace) during the test execution. * @return a set of lines that are covered in CUT during the exception happening. */ - fun getExceptionData(testExecutionError: String): Pair> { + private fun getExceptionData(testExecutionError: String): Pair> { if (testExecutionError.isBlank()) { return Pair(false, emptySet()) } @@ -294,7 +312,7 @@ class TestCoverageCollectorService(private val project: Project) { * @param testCode new code of test * @param testName the name of the test */ - fun updateDataWithTestCase(fileName: String, testId: Int, testName: String, testCode: String): TestCase { + fun processNewTestCase(fileName: String, testId: Int, testName: String, testCode: String): TestCase { // get buildPath var buildPath: String = ProjectRootManager.getInstance(project).contentRoots.first().path if (project.service().state.buildPath.isEmpty()) { @@ -303,7 +321,7 @@ class TestCoverageCollectorService(private val project: Project) { } // save new test to file - val generatedTestPath: String = project.service().saveGeneratedTests( + val generatedTestPath: String = saveGeneratedTest( project.service().testGenerationData.packageLine, testCode, project.service().resultPath!!, @@ -311,14 +329,13 @@ class TestCoverageCollectorService(private val project: Project) { ) // compilation checking - val compilationResult = - project.service().compileCode(generatedTestPath, buildPath) + val compilationResult = compileCode(generatedTestPath, buildPath) if (!compilationResult.first) { project.service().addFailedTest(testId, testCode, compilationResult.second) } else { val dataFileName = "${project.service().resultPath!!}/jacoco-${fileName.split(".")[0]}" - val testExecutionError = project.service().createXmlFromJacoco( + val testExecutionError = createXmlFromJacoco( fileName.split(".")[0], dataFileName, testName, @@ -329,11 +346,11 @@ class TestCoverageCollectorService(private val project: Project) { if (!File("$dataFileName.xml").exists()) { project.service().addFailedTest(testId, testCode, testExecutionError) } else { - val testCase = project.service().getTestCaseFromXml( + val testCase = getTestCaseFromXml( testId, testName, testCode, - project.service().getExceptionData(testExecutionError).second, + getExceptionData(testExecutionError).second, "$dataFileName.xml", ) @@ -343,12 +360,12 @@ class TestCoverageCollectorService(private val project: Project) { project.service().addPassedTest(testId, testCode) } - project.service().cleanFolder(project.service().resultPath!!) + DataFilesUtil.cleanFolder(project.service().resultPath!!) return testCase } } - project.service().cleanFolder(project.service().resultPath!!) + DataFilesUtil.cleanFolder(project.service().resultPath!!) return TestCase(testId, testName, testCode, setOf(), setOf(), setOf()) } diff --git a/src/main/kotlin/org/jetbrains/research/testspark/settings/SettingsPluginConfigurable.kt b/src/main/kotlin/org/jetbrains/research/testspark/settings/SettingsPluginConfigurable.kt index ad3e938fb..a252409a2 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/settings/SettingsPluginConfigurable.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/settings/SettingsPluginConfigurable.kt @@ -88,7 +88,6 @@ class SettingsPluginConfigurable(val project: Project) : Configurable { currentHighlightedData.linesToCover, currentHighlightedData.selectedTests, currentHighlightedData.testReport, - currentHighlightedData.editor, ) } diff --git a/src/main/kotlin/org/jetbrains/research/testspark/tools/Manager.kt b/src/main/kotlin/org/jetbrains/research/testspark/tools/Manager.kt index fba73d396..70cb0c0fe 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/tools/Manager.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/tools/Manager.kt @@ -1,12 +1,14 @@ package org.jetbrains.research.testspark.tools import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger import com.intellij.util.concurrency.AppExecutorUtil import org.jetbrains.research.testspark.data.Report import org.jetbrains.research.testspark.editor.Workspace import org.jetbrains.research.testspark.services.ErrorService +import org.jetbrains.research.testspark.services.ReportLockingService import org.jetbrains.research.testspark.services.RunnerService import org.jetbrains.research.testspark.tools.evosuite.EvoSuite import org.jetbrains.research.testspark.tools.llm.Llm @@ -129,12 +131,9 @@ private class Display(private val event: AnActionEvent, private val numberOfUsed log.info("Found all $numberOfUsedTool generation results") - // sends result to Workspace - event.project!!.messageBus.syncPublisher(TEST_GENERATION_RESULT_TOPIC).testGenerationResult( - getMergeResult(numberOfUsedTool), - event.project!!.service().testGenerationData.resultName, - event.project!!.service().testGenerationData.fileUrl, - ) + ApplicationManager.getApplication().invokeLater { + event.project!!.service().receiveReport(getMergeResult(numberOfUsedTool)) + } break } diff --git a/src/main/kotlin/org/jetbrains/research/testspark/tools/Pipeline.kt b/src/main/kotlin/org/jetbrains/research/testspark/tools/Pipeline.kt index 9ef733d86..a4f3b029e 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/tools/Pipeline.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/tools/Pipeline.kt @@ -8,12 +8,12 @@ import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.roots.ProjectRootManager +import org.jetbrains.research.testspark.DataFilesUtil import org.jetbrains.research.testspark.TestSparkBundle -import org.jetbrains.research.testspark.Util import org.jetbrains.research.testspark.actions.getSurroundingClass import org.jetbrains.research.testspark.data.FragmentToTestData import org.jetbrains.research.testspark.editor.Workspace -import org.jetbrains.research.testspark.services.TestCoverageCollectorService +import org.jetbrains.research.testspark.services.TestStorageProcessingService import org.jetbrains.research.testspark.tools.template.generation.ProcessManager /** @@ -30,16 +30,9 @@ class Pipeline( init { project.service().projectClassPath = ProjectRootManager.getInstance(project).contentRoots.first().path - - project.service().testResultDirectory = project.service().testResultDirectory - project.service().testResultName = project.service().testResultName - project.service().resultPath = project.service().resultPath - - project.service().baseDir = "${project.service().testResultDirectory}${project.service().testResultName}-validation" - - project.service().vFile = e.dataContext.getData(CommonDataKeys.VIRTUAL_FILE)!! - project.service().fileUrl = project.service().vFile!!.presentableUrl - project.service().modificationStamp = project.service().vFile!!.modificationStamp + project.service().resultPath = project.service().resultPath + project.service().baseDir = "${project.service().testResultDirectory}${project.service().testResultName}-validation" + project.service().fileUrl = e.dataContext.getData(CommonDataKeys.VIRTUAL_FILE)!!.presentableUrl project.service().cutPsiClass = getSurroundingClass( e.dataContext.getData(CommonDataKeys.PSI_FILE)!!, @@ -48,17 +41,16 @@ class Pipeline( project.service().cutModule = ProjectFileIndex.getInstance(project).getModuleForFile(project.service().cutPsiClass!!.containingFile.virtualFile)!! project.service().classFQN = project.service().cutPsiClass!!.qualifiedName!! - project.service().key = getKey(project, project.service().classFQN!!) - Util.makeTmp() - Util.makeDir(project.service().baseDir!!) + DataFilesUtil.makeTmp() + DataFilesUtil.makeDir(project.service().baseDir!!) } /** * Builds the project and launches generation on a separate thread. */ fun runTestGeneration(processManager: ProcessManager, codeType: FragmentToTestData) { - clearDataBeforeTestGeneration(project, project.service().testResultName!!) + clearDataBeforeTestGeneration(project) val projectBuilder = ProjectBuilder(project) diff --git a/src/main/kotlin/org/jetbrains/research/testspark/tools/TestGenerationResultListener.kt b/src/main/kotlin/org/jetbrains/research/testspark/tools/TestGenerationResultListener.kt deleted file mode 100644 index 3ed85fe3f..000000000 --- a/src/main/kotlin/org/jetbrains/research/testspark/tools/TestGenerationResultListener.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.jetbrains.research.testspark.tools - -import com.intellij.util.messages.Topic -import org.jetbrains.research.testspark.data.Report - -val TEST_GENERATION_RESULT_TOPIC: Topic = Topic.create( - "TEST_GENERATION_RESULT_TOPIC", - TestGenerationResultListener::class.java, - Topic.BroadcastDirection.TO_PARENT, -) - -/** - * Topic interface for sending and receiving test results produced by evosuite - * - * Subscribers to this topic will receive a Report whenever the plugin triggers a test - * generation job with testspark.evosuite.Runner - */ -interface TestGenerationResultListener { - fun testGenerationResult(testReport: Report, resultName: String, fileUrl: String) -} diff --git a/src/main/kotlin/org/jetbrains/research/testspark/tools/ToolUtils.kt b/src/main/kotlin/org/jetbrains/research/testspark/tools/ToolUtils.kt index 4a3df6d76..4f45f6e68 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/tools/ToolUtils.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/tools/ToolUtils.kt @@ -6,13 +6,12 @@ import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.project.Project import com.intellij.openapi.roots.CompilerModuleExtension import com.intellij.openapi.roots.ModuleRootManager -import org.jetbrains.research.testspark.TestSparkBundle -import org.jetbrains.research.testspark.Util +import org.jetbrains.research.testspark.DataFilesUtil import org.jetbrains.research.testspark.data.Report import org.jetbrains.research.testspark.editor.Workspace import org.jetbrains.research.testspark.services.ErrorService import org.jetbrains.research.testspark.services.JavaClassBuilderService -import org.jetbrains.research.testspark.services.TestCoverageCollectorService +import org.jetbrains.research.testspark.services.TestStorageProcessingService import org.jetbrains.research.testspark.services.TestsExecutionResultService import java.io.File @@ -64,7 +63,7 @@ fun saveData( indicator: ProgressIndicator, ) { val workspace = project.service() - workspace.testGenerationData.resultName = project.service().testResultName!! + workspace.testGenerationData.resultName = project.service().testResultName!! workspace.testGenerationData.fileUrl = project.service().fileUrl!! workspace.testGenerationData.packageLine = packageLine workspace.testGenerationData.importsCode.addAll(importsCode) @@ -83,46 +82,16 @@ fun saveData( ) } - indicator.text = TestSparkBundle.message("testExecutionMessage") - - for (testCase in report.testCaseList.values) { - indicator.text = "Executing ${testCase.testName}" - project.service().updateDataWithTestCase( - "${project.service().getClassWithTestCaseName(testCase.testName)}.java", - testCase.id, - testCase.testName, - testCase.testCode, - ) - } - workspace.testGenerationData.testGenerationResultList.add(report) } -/** - * Retrieves the key for a test job in the workspace. - * - * @param classFQN The fully qualified name of the class associated with the test job. - * @return The test job information containing the provided parameters. - */ -fun getKey(project: Project, classFQN: String): Workspace.TestJobInfo = - Workspace.TestJobInfo( - project.service().fileUrl!!, - classFQN, - project.service().modificationStamp!!, - project.service().testResultName!!, - project.service().projectClassPath!!, - ) - /** * Clears the data before test generation for a specific test result. * * @param project The project for which the test generation data needs to be cleared. - * @param testResultName The name of the test result for which the data needs to be cleared. */ -fun clearDataBeforeTestGeneration(project: Project, testResultName: String) { - val workspace = project.service() - workspace.clear(project) - workspace.testGenerationData.pendingTestResults[testResultName] = project.service().key!! +fun clearDataBeforeTestGeneration(project: Project) { + project.service().clear(project) } /** @@ -137,7 +106,7 @@ fun getBuildPath(project: Project): String { for (module in ModuleManager.getInstance(project).modules) { val compilerOutputPath = CompilerModuleExtension.getInstance(module)?.compilerOutputPath - compilerOutputPath?.let { buildPath += compilerOutputPath.path.plus(Util.classpathSeparator.toString()) } + compilerOutputPath?.let { buildPath += compilerOutputPath.path.plus(DataFilesUtil.classpathSeparator.toString()) } // Include extra libraries in classpath val librariesPaths = ModuleRootManager.getInstance(module).orderEntries().librariesOnly().pathsList.pathList for (lib in librariesPaths) { @@ -158,7 +127,7 @@ fun getBuildPath(project: Project): String { continue } - buildPath += lib.plus(Util.classpathSeparator.toString()) + buildPath += lib.plus(DataFilesUtil.classpathSeparator.toString()) } } return buildPath diff --git a/src/main/kotlin/org/jetbrains/research/testspark/tools/evosuite/generation/EvoSuiteProcessManager.kt b/src/main/kotlin/org/jetbrains/research/testspark/tools/evosuite/generation/EvoSuiteProcessManager.kt index 8e986dc0b..7b0583ff9 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/tools/evosuite/generation/EvoSuiteProcessManager.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/tools/evosuite/generation/EvoSuiteProcessManager.kt @@ -24,7 +24,6 @@ import org.jetbrains.research.testspark.tools.evosuite.SettingsArguments import org.jetbrains.research.testspark.tools.evosuite.error.EvoSuiteErrorManager import org.jetbrains.research.testspark.tools.getBuildPath import org.jetbrains.research.testspark.tools.getImportsCodeFromTestSuiteCode -import org.jetbrains.research.testspark.tools.getKey import org.jetbrains.research.testspark.tools.getPackageFromTestSuiteCode import org.jetbrains.research.testspark.tools.processStopped import org.jetbrains.research.testspark.tools.saveData @@ -97,7 +96,6 @@ class EvoSuiteProcessManager( val command = when (codeType.type!!) { CodeType.CLASS -> SettingsArguments(projectClassPath, projectPath, resultName, classFQN, baseDir).build() CodeType.METHOD -> { - project.service().key = getKey(project, "$classFQN#${codeType.objectDescription}") SettingsArguments(projectClassPath, projectPath, resultName, classFQN, baseDir).forMethod(codeType.objectDescription).build() } diff --git a/src/main/kotlin/org/jetbrains/research/testspark/tools/evosuite/validation/TestCaseEditor.kt b/src/main/kotlin/org/jetbrains/research/testspark/tools/evosuite/validation/TestCaseEditor.kt deleted file mode 100644 index f4df45e5a..000000000 --- a/src/main/kotlin/org/jetbrains/research/testspark/tools/evosuite/validation/TestCaseEditor.kt +++ /dev/null @@ -1,157 +0,0 @@ -package org.jetbrains.research.testspark.tools.evosuite.validation - -import com.github.javaparser.JavaParser -import com.github.javaparser.StaticJavaParser -import com.github.javaparser.ast.Modifier -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration -import com.github.javaparser.ast.body.MethodDeclaration -import com.github.javaparser.ast.expr.NormalAnnotationExpr -import com.github.javaparser.ast.expr.SimpleName -import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr -import com.github.javaparser.ast.stmt.BlockStmt -import com.github.javaparser.ast.visitor.ModifierVisitor -import com.github.javaparser.ast.visitor.Visitable -import com.github.javaparser.ast.visitor.VoidVisitorAdapter -import com.intellij.openapi.diagnostic.Logger - -/** - * This class edits the test suite by setting the modified body of each test if it has been modified. It also removes scaffolding. - */ -class TestCaseEditor( - private val text: String, - private val activeTestList: HashMap, -) { - private val log: Logger = Logger.getInstance(this.javaClass) - - class TestCaseReplacer( - private val activeTestList: HashMap>, - ) : ModifierVisitor() { - private val log: Logger = Logger.getInstance(this.javaClass) - - private val activeTestNames = - activeTestList.entries.associate { - it.value.second to it.key - } - - override fun visit(n: ClassOrInterfaceDeclaration?, arg: Void?): Visitable { - n ?: return super.visit(n, arg) - - val methods = n.methods.map { it.nameAsString }.toSet() - - for (entry in activeTestList) { - if (!methods.contains(entry.value.second)) { - log.info("Test ${entry.key} in cache but not in file, inserting") - val methodDeclaration = n.addMethod(entry.value.second, Modifier.Keyword.PUBLIC) - methodDeclaration.setBody(entry.value.first) - val testAnnotation = NormalAnnotationExpr() - testAnnotation.setName("Test") - testAnnotation.addPair("timeout", "4000") - methodDeclaration.addAnnotation(testAnnotation) - } - } - - return super.visit(n, arg) - } - - override fun visit(n: MethodDeclaration?, arg: Void?): Visitable { - val name = n?.name!! - val testName = name.toString() - - if (activeTestNames.contains(testName)) { - val modifiedBody = activeTestList[activeTestNames[testName]] - if (modifiedBody != null) { - log.info("Test case modified $testName") - n.setBody(modifiedBody.first) - } else { - log.info("Test case not modified $testName") - } - } else { - log.info("Test case not selected by user $testName") - n.remove() - } - - return super.visit(n, arg) - } - } - - class BodyExtractor : VoidVisitorAdapter>>() { - override fun visit(n: MethodDeclaration?, arg: ArrayList>) { - super.visit(n, arg) - - val body = n?.body?.get() - if (body != null) { - arg.add(Pair(body, n.nameAsString)) - } - } - } - - class ScaffoldRemover : ModifierVisitor() { - - // removes @EvoRunnerParameters(..) - override fun visit(n: NormalAnnotationExpr?, arg: Void?): Visitable { - n ?: return super.visit(n, arg) - if (n.nameAsString.contains("EvoRunnerParameters")) { - n.remove() - } - return super.visit(n, arg) - } - - // removes @RunWith(EvoRunner.class) - override fun visit(n: SingleMemberAnnotationExpr?, arg: Void?): Visitable { - n?.remove() - return super.visit(n, arg) - } - - // Removes extension of scaffolding - // Changes class name - override fun visit(n: ClassOrInterfaceDeclaration?, arg: Void?): Visitable { - val scaffoldClass = n?.extendedTypes?.get(0) ?: return super.visit(n, arg) - scaffoldClass.remove() - val baseName = n.nameAsString - n.name = SimpleName("${baseName}_Cov") - return super.visit(n, arg) - } - } - - fun edit(): String { - val parser = JavaParser() - val unit = parser.parse(text).result.get() - - val map = HashMap>() - - for (edit in activeTestList) { - // hack needed to make java parser parse a method - val code = "package p; public class c {${edit.value}}" - val parsedModified = StaticJavaParser.parse(code) - val extractor = BodyExtractor() - val body = ArrayList>() - extractor.visit(parsedModified, body) - - if (body.isEmpty()) continue - - val blockStmt = body[0] - map[edit.key] = blockStmt - } - - val replacer = TestCaseReplacer(map) - replacer.visit(unit, null) - - val result = unit.toString() - log.debug("EDITED TEST SUITE:\n$result") - - return result - } - - fun editRemoveScaffold(testClass: String): String { - val parser = JavaParser() - val unit = parser.parse(testClass).result.get() - - val remover = ScaffoldRemover() - remover.visit(unit, null) - - val result = unit.toString() - log.debug("EDITED TEST SUITE NO SCAFFOLD:\n$result") - - return result - } -} diff --git a/src/main/kotlin/org/jetbrains/research/testspark/tools/evosuite/validation/ValidationResultListener.kt b/src/main/kotlin/org/jetbrains/research/testspark/tools/evosuite/validation/ValidationResultListener.kt deleted file mode 100644 index b63375687..000000000 --- a/src/main/kotlin/org/jetbrains/research/testspark/tools/evosuite/validation/ValidationResultListener.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.jetbrains.research.testspark.tools.evosuite.validation - -import com.intellij.util.messages.Topic - -val VALIDATION_RESULT_TOPIC: Topic = Topic.create( - "VALIDATION_RESULT_TOPIC", - ValidationResultListener::class.java, - Topic.BroadcastDirection.TO_PARENT, -) - -/** - * Topic interface for sending and receiving results of test validation - * - * Subscribers to this topic will receive a validation result whenever the user triggers test - * generation validation - */ -interface ValidationResultListener { - fun validationResult(junitResult: Validator.JUnitResult) -} diff --git a/src/main/kotlin/org/jetbrains/research/testspark/tools/evosuite/validation/ValidationToolWindowFactory.kt b/src/main/kotlin/org/jetbrains/research/testspark/tools/evosuite/validation/ValidationToolWindowFactory.kt deleted file mode 100644 index 91f695f77..000000000 --- a/src/main/kotlin/org/jetbrains/research/testspark/tools/evosuite/validation/ValidationToolWindowFactory.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.jetbrains.research.testspark.tools.evosuite.validation - -import com.intellij.openapi.project.Project -import com.intellij.openapi.wm.ToolWindow -import com.intellij.openapi.wm.ToolWindowFactory - -/** - * This class is responsible for creating the tabs and the UI of the tool window corresponding to dynamic test validation. - */ -class ValidationToolWindowFactory : ToolWindowFactory { - /** - * Initialises the UI of the tool window. - */ - override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { - } -} diff --git a/src/main/kotlin/org/jetbrains/research/testspark/tools/evosuite/validation/Validator.kt b/src/main/kotlin/org/jetbrains/research/testspark/tools/evosuite/validation/Validator.kt deleted file mode 100644 index 30e61ebcd..000000000 --- a/src/main/kotlin/org/jetbrains/research/testspark/tools/evosuite/validation/Validator.kt +++ /dev/null @@ -1,389 +0,0 @@ -package org.jetbrains.research.testspark.tools.evosuite.validation - -import com.github.javaparser.ParseProblemException -import com.intellij.coverage.CoverageDataManager -import com.intellij.coverage.CoverageRunner -import com.intellij.coverage.CoverageSuite -import com.intellij.coverage.CoverageSuitesBundle -import com.intellij.coverage.DefaultCoverageFileProvider -import com.intellij.execution.configurations.GeneralCommandLine -import com.intellij.execution.filters.TextConsoleBuilderFactory -import com.intellij.execution.process.CapturingProcessAdapter -import com.intellij.execution.process.OSProcessHandler -import com.intellij.notification.NotificationGroupManager -import com.intellij.notification.NotificationType -import com.intellij.openapi.application.PathManager -import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.progress.Task -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Comparing -import com.intellij.openapi.util.io.FileUtilRt -import com.intellij.openapi.vfs.LocalFileSystem -import com.intellij.openapi.vfs.VirtualFile -import org.jetbrains.research.testspark.TestSparkBundle -import org.jetbrains.research.testspark.editor.Workspace -import org.jetbrains.research.testspark.services.SettingsApplicationService -import org.jetbrains.research.testspark.services.TestCaseDisplayService -import org.jetbrains.research.testspark.tools.ProjectBuilder -import java.io.File -import java.io.FileWriter -import java.nio.charset.Charset -import java.util.regex.Pattern -import javax.tools.JavaCompiler -import javax.tools.ToolProvider - -/** - * Class for validating and calculating the coverage of an optionally - * edited set of test cases. - * - * @param tests a map of test names and their code - */ -class Validator( - private val project: Project, - private val testJobInfo: Workspace.TestJobInfo, - private val tests: HashMap, // test name, test code -) { - private val logger: Logger = Logger.getInstance(this.javaClass) - private val settingsState = SettingsApplicationService.getInstance().state!! - private val junitTimeout: Long = 12000000 // TODO: Source from config - - private val sep = File.separatorChar - private val pathSep = File.pathSeparatorChar - - fun validateSuite() { - val jobName = testJobInfo.jobId - - logger.info("Validating test suite $jobName") - - val fqn = testJobInfo.targetUnit.split('#').first() - val targetFqn = "${fqn}_ESTest" - - val targetProjectCP = testJobInfo.targetClassPath - - val pluginsPath = PathManager.getPluginsPath() - - val junitPath = "$pluginsPath${sep}TestSpark${sep}lib${sep}junit-4.13.jar" - val standaloneRuntimePath = "$pluginsPath${sep}TestSpark${sep}lib${sep}standalone-runtime.jar" - val hamcrestPath = "$pluginsPath${sep}TestSpark${sep}lib${sep}hamcrest-core-1.3.jar" - - val testValidationRoot = "${FileUtilRt.getTempDirectory()}${sep}testSparkResults$sep$jobName-validation" - val testValidationDirectory = "$testValidationRoot${sep}evosuite-tests" - val validationDir = File(testValidationDirectory) - - // TODO: Implement classpath builder - val dependenciesClasspath = "${junitPath}$pathSep$standaloneRuntimePath$pathSep$hamcrestPath" - val classpath = "${targetProjectCP}${pathSep}$dependenciesClasspath${pathSep}$testValidationDirectory" - - if (!validationDir.exists()) { - logger.error("Validation dir does not exist! - $testValidationDirectory") - return - } - logger.info("Rebuilding user project...") - val projectBuilder = ProjectBuilder(project) - - ProgressManager.getInstance() - .run(object : Task.Backgroundable(project, TestSparkBundle.message("validationCompilation")) { - override fun run(indicator: ProgressIndicator) { - try { - projectBuilder.runBuild(indicator) - - val compilationFiles = - setupCompilationFiles(testValidationDirectory, targetFqn) - ?: return - - logger.info("Compiling tests...") - val successfulCompilation = compileTests(classpath, compilationFiles) - - if (!successfulCompilation) { - logger.warn("Compilation failed") - showTestsCompilationFailed() - return - } - logger.info("Compilation successful!") - logger.info("Executing tests...") - indicator.text = TestSparkBundle.message("validationRunning") - - runTests(indicator, classpath, targetFqn) - runTestsWithCoverage(indicator, classpath, targetFqn, testValidationRoot) - indicator.stop() - } catch (e: Exception) { - e.printStackTrace() - } - } - - override fun onFinished() { - super.onFinished() -// TODO implement function in TestCaseDisplayService -// project.service().makeValidatedButtonAvailable() - } - }) - } - - private fun setupCompilationFiles( - testValidationDirectory: String, - targetFqn: String, - ): List? { - val baseClassName = "$testValidationDirectory$sep${targetFqn.replace('.', sep)}" - // flush test edits to file - val testsPath = "$baseClassName.java" - val testsFile = File(testsPath) - - val editor = TestCaseEditor(testsFile.readText(), tests) - val editedTests: String? - - try { - editedTests = editor.edit() - val testsFileWriter = FileWriter(testsFile, false) - testsFileWriter.write(editedTests) - testsFileWriter.close() - logger.trace("Flushed tests to $testsPath") - } catch (e: ParseProblemException) { - logger.warn("Parsing tests failed - $e") - showTestsParsingFailed() - return null - } - - val testsCovPath = "${baseClassName}_Cov.java" - val testsCov = File(testsCovPath) - val testsCovWriter = FileWriter(testsCov, false) - val testsNoScaffold = editor.editRemoveScaffold(editedTests) - - testsCovWriter.write(testsNoScaffold) - testsCovWriter.close() - - val scaffoldPath = "${baseClassName}_scaffolding.java" - val scaffoldFile = File(scaffoldPath) - - return listOf(scaffoldFile, testsFile, testsCov) - } - - /** - * Compiles the provided test files with the provided classpath - */ - private fun compileTests(classpath: String, files: List): Boolean { - logger.trace("Compiling with classpath $classpath") - - val optionList: List = listOf("-classpath", classpath) - - val compiler: JavaCompiler = ToolProvider.getSystemJavaCompiler() - - val fileManager = compiler.getStandardFileManager(null, null, null) - - val compilationUnits = fileManager.getJavaFileObjectsFromFiles(files) - - val task = compiler.getTask( - null, - fileManager, - null, - optionList, - null, - compilationUnits, - ) - - val compiled = task.call() - fileManager.close() - - return compiled - } - - /** - * Runs the compiled tests - * - * @param indicator the progress indicator - */ - private fun runTests( - indicator: ProgressIndicator, - classpath: String, - testFqn: String, - ) { - indicator.isIndeterminate = false - - // construct command - val cmd = ArrayList() - cmd.add(settingsState.javaPath) - cmd.add("-cp") - cmd.add(classpath) - cmd.add("org.junit.runner.JUnitCore") - cmd.add(testFqn) - - val cmdString = cmd.fold(String()) { acc, e -> acc.plus(e).plus(" ") } - logger.info("Running junit tests with: $cmdString") - - val junitProcess = GeneralCommandLine(cmd) - junitProcess.charset = Charset.forName("UTF-8") - val handler = OSProcessHandler(junitProcess) - - // attach console listener for displaying information - val consoleBuilder = TextConsoleBuilderFactory.getInstance().createBuilder(project) - consoleBuilder.setViewer(true) - val console = consoleBuilder.console - console.attachToProcess(handler) - - val capturer = CapturingProcessAdapter() - // attach another listener for parsing process results - handler.addProcessListener(capturer) - handler.startNotify() - -// TODO uncomment after the validator fixing -// val manager: ToolWindowManager = ToolWindowManager.getInstance(project) -// -// ApplicationManager.getApplication().invokeLater { -// val window = manager.getToolWindow("TestSpark Validator")!! -// val contentManager: ContentManager = window.contentManager -// contentManager.removeAllContents(true) -// val content: Content = contentManager.factory.createContent( -// console.component, -// TestSparkLabelsBundle.defaultValue("junitRun"), -// false, -// ) -// contentManager.addContent(content) -// } - - // treat this as a join handle - handler.waitFor(junitTimeout) - - val output = capturer.output.stdout - - val junitResult = parseJunitResult(output) - showValidationResult(junitResult) - - project.messageBus.syncPublisher(VALIDATION_RESULT_TOPIC).validationResult(junitResult) - } - - /** - * Runs the compiled tests - * - * @param indicator the progress indicator - */ - private fun runTestsWithCoverage( - indicator: ProgressIndicator, - classpath: String, - testFqn: String, - testValidationRoot: String, - ) { - indicator.text = TestSparkBundle.message("calculatingCoverage") - - val pluginsPath = PathManager.getPluginsPath() - val jacocoPath = "$pluginsPath${sep}TestSpark${sep}lib${sep}jacocoagent.jar" - // construct command - val jacocoReportPath = "$testValidationRoot${sep}jacoco.exec" - // delete old report - File(jacocoReportPath).delete() - val cmd = ArrayList() - cmd.add(settingsState.javaPath) - cmd.add("-javaagent:$jacocoPath=destfile=$jacocoReportPath") - cmd.add("-cp") - cmd.add(classpath) - cmd.add("org.junit.runner.JUnitCore") - cmd.add("${testFqn}_Cov") - - val cmdString = cmd.fold(String()) { acc, e -> acc.plus(e).plus(" ") } - logger.info("Running tests for coverage with: $cmdString") - - val junitProcess = GeneralCommandLine(cmd) - junitProcess.charset = Charset.forName("UTF-8") - val handler = OSProcessHandler(junitProcess) - - val cap = CapturingProcessAdapter() - // attach another listener for parsing process results - handler.addProcessListener(cap) - handler.startNotify() - - // treat this as a join handle - handler.waitFor(junitTimeout) - - val manager = CoverageDataManager.getInstance(project) - val virtualFile = LocalFileSystem.getInstance().findFileByPath(jacocoReportPath)!! - val coverageRunner = getCoverageRunner(virtualFile) - - val coverageSuite: CoverageSuite = manager - .addExternalCoverageSuite( - virtualFile.name, - virtualFile.timeStamp, - coverageRunner!!, - DefaultCoverageFileProvider(virtualFile.path), - ) - - val testCaseDisplayService = project.service() - val coverageSuitesBundle = CoverageSuitesBundle(coverageSuite) - testCaseDisplayService.setJacocoReport(coverageSuitesBundle) -// TODO uncomment after the validator fixing -// testCaseDisplayService.toggleJacocoButton.isEnabled = true - } - - private fun getCoverageRunner(file: VirtualFile): CoverageRunner? { - for (runner in CoverageRunner.EP_NAME.extensionList) { - for (extension in runner.dataFileExtensions) { - if (Comparing.strEqual(file.extension, extension)) return runner - } - } - return null - } - - /** - * Method to show notification that the tests cannot be compiled. - */ - private fun showTestsCompilationFailed() { - NotificationGroupManager.getInstance().getNotificationGroup("Test Validation Error").createNotification( - TestSparkBundle.message("compilationFailedNotificationTitle"), - TestSparkBundle.message("compilationFailedNotificationText"), - NotificationType.ERROR, - ).notify(project) - } - - /** - * Method to show notification that the tests cannot be parsed. - */ - private fun showTestsParsingFailed() { - NotificationGroupManager.getInstance().getNotificationGroup("Test Validation Error").createNotification( - TestSparkBundle.message("compilationFailedNotificationTitle"), - TestSparkBundle.message("compilationFailedNotificationText"), - NotificationType.ERROR, - ).notify(project) - } - - /** - * Method to show validation results - */ - private fun showValidationResult(junitResult: JUnitResult) { - val passed = junitResult.totalTests - junitResult.failedTests - NotificationGroupManager.getInstance().getNotificationGroup("Validation Result").createNotification( - TestSparkBundle.message("validationResult"), - "$passed/${junitResult.totalTests}", - NotificationType.INFORMATION, - ).notify(project) - } - - data class JUnitResult(val totalTests: Int, val failedTests: Int, val failedTestNames: Set) - - companion object { - fun parseJunitResult(cap: String): JUnitResult { - val output = cap.trimEnd() - val resultString = output.substring(output.lastIndexOf("\n")).trim() - - if (resultString.startsWith("OK")) { - val successMatcher = Pattern.compile("(\\d+)").matcher(resultString) - successMatcher.find() - val total = successMatcher.group().toInt() - return JUnitResult(total, 0, emptySet()) - } else { - val failMatcher = Pattern.compile("\\d+").matcher(resultString) - failMatcher.find() - val total = failMatcher.group().toInt() - failMatcher.find() - val failed = failMatcher.group().toInt() - val failedCaseMatcher = Pattern.compile("\\d\\) (\\w*)\\(.*\n").matcher(output) - val cases = mutableSetOf() - while (failedCaseMatcher.find()) { - val testName = failedCaseMatcher.group(1).trim() - cases.add(testName) - } - - return JUnitResult(total, failed, cases) - } - } - } -} diff --git a/src/main/kotlin/org/jetbrains/research/testspark/tools/llm/generation/LLMProcessManager.kt b/src/main/kotlin/org/jetbrains/research/testspark/tools/llm/generation/LLMProcessManager.kt index eb13b4a9f..f64d705f6 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/tools/llm/generation/LLMProcessManager.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/tools/llm/generation/LLMProcessManager.kt @@ -5,22 +5,22 @@ import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.project.Project import org.jetbrains.research.testspark.TestSparkBundle -import org.jetbrains.research.testspark.data.CodeType import org.jetbrains.research.testspark.data.FragmentToTestData import org.jetbrains.research.testspark.data.Report +import org.jetbrains.research.testspark.data.TestCase import org.jetbrains.research.testspark.editor.Workspace import org.jetbrains.research.testspark.services.ErrorService import org.jetbrains.research.testspark.services.JavaClassBuilderService import org.jetbrains.research.testspark.services.LLMChatService import org.jetbrains.research.testspark.services.SettingsProjectService -import org.jetbrains.research.testspark.services.TestCoverageCollectorService +import org.jetbrains.research.testspark.services.TestStorageProcessingService import org.jetbrains.research.testspark.tools.getBuildPath import org.jetbrains.research.testspark.tools.getImportsCodeFromTestSuiteCode -import org.jetbrains.research.testspark.tools.getKey import org.jetbrains.research.testspark.tools.getPackageFromTestSuiteCode import org.jetbrains.research.testspark.tools.isPromptLengthWithinLimit import org.jetbrains.research.testspark.tools.llm.SettingsArguments import org.jetbrains.research.testspark.tools.llm.error.LLMErrorManager +import org.jetbrains.research.testspark.tools.llm.test.TestCaseGeneratedByLLM import org.jetbrains.research.testspark.tools.llm.test.TestSuiteGeneratedByLLM import org.jetbrains.research.testspark.tools.processStopped import org.jetbrains.research.testspark.tools.saveData @@ -69,13 +69,6 @@ class LLMProcessManager( return } - if (codeType.type == CodeType.METHOD) { - project.service().key = getKey( - project, - "${project.service().classFQN}#${codeType.objectDescription}", - ) - } - // update build path var buildPath = project.service().projectClassPath!! if (settingsProjectState.buildPath.isEmpty()) { @@ -93,7 +86,7 @@ class LLMProcessManager( var generatedTestsArePassing = false - var report: Report? = null + val report = Report() var requestsCount = 0 var warningMessage = "" @@ -137,7 +130,8 @@ class LLMProcessManager( // Empty response checking if (generatedTestSuite.testCases.isEmpty()) { warningMessage = TestSparkBundle.message("emptyResponse") - messageToPrompt = "You have provided an empty answer! Please answer my previous question with the same formats." + messageToPrompt = + "You have provided an empty answer! Please answer my previous question with the same formats." continue } @@ -148,7 +142,7 @@ class LLMProcessManager( } else { for (testCaseIndex in generatedTestSuite.testCases.indices) { generatedTestCasesPaths.add( - project.service().saveGeneratedTests( + project.service().saveGeneratedTest( generatedTestSuite.packageString, generatedTestSuite.toStringSingleTestCaseWithoutExpectedException(testCaseIndex), project.service().resultPath!!, @@ -158,7 +152,7 @@ class LLMProcessManager( } } - val generatedTestPath: String = project.service().saveGeneratedTests( + val generatedTestPath: String = project.service().saveGeneratedTest( generatedTestSuite.packageString, generatedTestSuite.toStringWithoutExpectedException(), project.service().resultPath!!, @@ -173,21 +167,18 @@ class LLMProcessManager( break } - // Collect coverage information for each generated test method and display it - val coverageCollector = TestCoverageCollector( - indicator, - project, - generatedTestCasesPaths, - File(generatedTestPath), - generatedTestSuite.getPrintablePackageString(), - buildPath, - if (!isLastIteration(requestsCount)) generatedTestSuite.testCases else project.service().testGenerationData.compilableTestCases.toMutableList() - ) + // Get test cases + val testCases: MutableList = + if (!isLastIteration(requestsCount)) { + generatedTestSuite.testCases + } else { + project.service().testGenerationData.compilableTestCases.toMutableList() + } - // compile the test file + // Compile the test file indicator.text = TestSparkBundle.message("compilationTestsChecking") - val separateCompilationResult = coverageCollector.compileTestCases() - val commonCompilationResult = project.service().compileCode(File(generatedTestPath).absolutePath, buildPath) + val separateCompilationResult = project.service().compileTestCases(generatedTestCasesPaths, buildPath, testCases) + val commonCompilationResult = project.service().compileCode(File(generatedTestPath).absolutePath, buildPath) if (!separateCompilationResult && !isLastIteration(requestsCount)) { log.info("Incorrect result: \n$generatedTestSuite") @@ -199,7 +190,10 @@ class LLMProcessManager( log.info("Result is compilable") generatedTestsArePassing = true - report = coverageCollector.collect() + + for (index in testCases.indices) { + report.testCaseList[index] = TestCase(index, testCases[index].name, testCases[index].toString(), setOf(), setOf(), setOf()) + } } if (processStopped(project, indicator)) return @@ -211,7 +205,7 @@ class LLMProcessManager( saveData( project, - report!!, + report, getPackageFromTestSuiteCode(generatedTestSuite.toString()), getImportsCodeFromTestSuiteCode(generatedTestSuite.toString(), project.service().classFQN!!), indicator, diff --git a/src/main/kotlin/org/jetbrains/research/testspark/tools/llm/generation/TestCoverageCollector.kt b/src/main/kotlin/org/jetbrains/research/testspark/tools/llm/generation/TestCoverageCollector.kt deleted file mode 100644 index d9bbc164a..000000000 --- a/src/main/kotlin/org/jetbrains/research/testspark/tools/llm/generation/TestCoverageCollector.kt +++ /dev/null @@ -1,115 +0,0 @@ -package org.jetbrains.research.testspark.tools.llm.generation - -import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.project.Project -import org.jetbrains.research.testspark.TestSparkBundle -import org.jetbrains.research.testspark.data.Report -import org.jetbrains.research.testspark.editor.Workspace -import org.jetbrains.research.testspark.services.TestCoverageCollectorService -import org.jetbrains.research.testspark.tools.llm.error.LLMErrorManager -import org.jetbrains.research.testspark.tools.llm.test.TestCaseGeneratedByLLM -import java.io.File - -/** - * The TestCoverageCollector class is responsible for collecting test coverage data and generating a report. - * - * @property indicator The progress indicator to display the current task progress. - * @property project The project associated with the test generation. - * @property generatedTestPaths The paths of the generated test files. - * @property generatedTestFile The generated test file. - * @property generatedTestPackage The package of the generated test file. - * @property projectBuildPath The path to the project build directory. - * @property testCases The list of test cases generated by the LLM. - */ -class TestCoverageCollector( - private val indicator: ProgressIndicator, - private val project: Project, - private val generatedTestPaths: List, - private val generatedTestFile: File, - private val generatedTestPackage: String, - private val projectBuildPath: String, - private val testCases: MutableList, -) { - private val log = Logger.getInstance(this::class.java) - - private val report = Report() - - /** - * Executes Jacoco on the compiled test file and collects the Jacoco results. - * - * @return The normalized Jacoco report. - */ - fun collect(): Report { - log.info("Test collection begins") - - // run Jacoco on the compiled test file - runJacoco() - - // collect the Jacoco results and return the report - return report.normalized() - } - - /** - * Compiles the generated test file using the proper javac and returns a Pair - * indicating whether the compilation was successful and any error message encountered during compilation. - * - * @return A Pair containing a boolean indicating whether the compilation was successful - * and a String containing any error message encountered during compilation. - */ - fun compileTestCases(): Boolean { - var result = false - for (index in generatedTestPaths.indices) { - val compilable = project.service().compileCode(generatedTestPaths[index], projectBuildPath).first - result = result || compilable - if (compilable) { - project.service().testGenerationData.compilableTestCases.add(testCases[index]) - } - } - return result - } - - /** - * Runs Jacoco to collect code coverage data and generate reports. - */ - private fun runJacoco() { - indicator.text = TestSparkBundle.message("runningJacoco") - - log.info("Running jacoco") - - // Execute each test method separately - for (index in generatedTestPaths.indices) { - val testCase = testCases[index] - - // name of .exec and .xml files - val dataFileName = "${project.service().resultPath!!}/jacoco-${testCase.name}" - - val testExecutionError = project.service().createXmlFromJacoco( - generatedTestPaths[index].split('/').last().split('.')[0], - dataFileName, - testCase.name, - projectBuildPath, - generatedTestPackage, - ) - - // check if XML report is produced - if (!File("$dataFileName.xml").exists()) { - LLMErrorManager().errorProcess("Something went wrong with generating Jacoco report.", project) - return - } - log.info("xml file exists") - - // save data to TestGenerationResult - indicator.text = TestSparkBundle.message("testCasesSaving") - report.testCaseList[index] = project.service().getTestCaseFromXml( - index, - testCase.name, - testCase.toString(), - project.service().getExceptionData(testExecutionError).second, - "$dataFileName.xml", - ) - } - project.service().cleanFolder(project.service().resultPath!!) - } -} diff --git a/src/main/kotlin/org/jetbrains/research/testspark/toolwindow/DescriptionTab.kt b/src/main/kotlin/org/jetbrains/research/testspark/toolwindow/DescriptionTab.kt index 1b4dae8a9..2b2d39f35 100644 --- a/src/main/kotlin/org/jetbrains/research/testspark/toolwindow/DescriptionTab.kt +++ b/src/main/kotlin/org/jetbrains/research/testspark/toolwindow/DescriptionTab.kt @@ -70,13 +70,13 @@ class DescriptionTab(private val project: Project) { } // Link to LLM settings - private val llmSettingsButton = JButton(TestSparkLabelsBundle.defaultValue("llmSettingsLink")) + private val llmSettingsButton = JButton(TestSparkLabelsBundle.defaultValue("llmSettingsLink"), TestSparkIcons.settings) // Link to EvoSuite settings - private val evoSuiteSettingsButton = JButton(TestSparkLabelsBundle.defaultValue("evoSuiteSettingsLink")) + private val evoSuiteSettingsButton = JButton(TestSparkLabelsBundle.defaultValue("evoSuiteSettingsLink"), TestSparkIcons.settings) // Link to open settings - private val settingsButton = JButton(TestSparkLabelsBundle.defaultValue("settingsLink")) + private val settingsButton = JButton(TestSparkLabelsBundle.defaultValue("settingsLink"), TestSparkIcons.settings) // Link to documentation private val documentationButton = JButton(TestSparkLabelsBundle.defaultValue("documentationLink"), TestSparkIcons.documentation) diff --git a/src/main/resources/defaults/Labels.properties b/src/main/resources/defaults/Labels.properties index 3a0a3b4b2..90e6a674b 100644 --- a/src/main/resources/defaults/Labels.properties +++ b/src/main/resources/defaults/Labels.properties @@ -56,6 +56,7 @@ branchCoverage=Branch coverage weakMutationCoverage=Weak mutation coverage !Test Display window run=Run +runAll=Run all send=Send applyButton=Apply to test suite testsSelected=Selected diff --git a/src/main/resources/icons/settings.svg b/src/main/resources/icons/settings.svg new file mode 100644 index 000000000..c30de8209 --- /dev/null +++ b/src/main/resources/icons/settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/icons/settings_dark.svg b/src/main/resources/icons/settings_dark.svg new file mode 100644 index 000000000..62f328167 --- /dev/null +++ b/src/main/resources/icons/settings_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/messages/TestSpark.properties b/src/main/resources/messages/TestSpark.properties index 74de7e5c2..5112a55ab 100644 --- a/src/main/resources/messages/TestSpark.properties +++ b/src/main/resources/messages/TestSpark.properties @@ -5,10 +5,7 @@ buildMessage=Building project searchMessage=Generating tests generatingTestNumber=Generating test # compilationTestsChecking=Compilation tests checking -runningJacoco=Running jacoco testCasesSaving=Test cases saving -calculatingCoverage=Calculating coverage -testExecutionMessage=Tests execution !Build error titles buildErrorTitle=Build error evosuiteErrorTitle=EvoSuite error @@ -20,12 +17,6 @@ confirmationTitle=Are You Sure? !RunnerService alreadyRunningNotificationTitle=Generation is already running alreadyRunningTextNotificationText=You can generate more tests after the current test generation is complete. -!Validator -validationCompilation=Validation: Compiling resources... -validationRunning=Validation: Running test suite... -validationResult=Validation: done -compilationFailedNotificationTitle=Test compilation failed -compilationFailedNotificationText=Check if your tests contain any syntax errors before running test validation. !EvoSuite error messages evosuiteErrorMessage=An exception occurred while executing EvoSuite: %s.\nPlease verify that you have set a valid path to a Java 11 binary in TestSpark Settings. evosuiteErrorCommon=EvoSuite process error: @@ -49,4 +40,7 @@ savingTestFileIssue=Plugin cannot save the test file promptReduction=The generated prompt is too long, plugin reduced Large Language Model parameters. !Build error messages commonBuildErrorMessage=Please make sure that IntelliJ can build your project without any issues or provide the correct build command in the settings -sendingFeedback=Sending your request to LLM \ No newline at end of file +sendingFeedback=Sending your request to LLM +testRunning=Test running +!Run caution message +runCautionMessage=By clicking "OK" you agree to run this code on your machine \ No newline at end of file diff --git a/src/test/kotlin/org/jetbrains/research/testspark/evosuite/validation/ValidatorTest.kt b/src/test/kotlin/org/jetbrains/research/testspark/evosuite/validation/ValidatorTest.kt deleted file mode 100644 index 409990a22..000000000 --- a/src/test/kotlin/org/jetbrains/research/testspark/evosuite/validation/ValidatorTest.kt +++ /dev/null @@ -1,43 +0,0 @@ -package org.jetbrains.research.testspark.evosuite.validation - -import org.jetbrains.research.testspark.tools.evosuite.validation.Validator -import org.junit.jupiter.api.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class ValidatorTest { - @Test - fun testMatchFail() { - val result = "Time: 0.443\n" + - "There were 3 failures:\n" + - "1) testNotStuff(demo.TestClass_ESTest)\n" + - "java.lang.AssertionError\n" + - "2) testComplexTerribleMethodReturningPositive(demo.TestClass_ESTest)\n" + - "java.lang.AssertionError: expected:<2> but was:<1>\n" + - "3) testDoStuff(demo.TestClass_ESTest)\n" + - "java.lang.AssertionError\n" + - "\n" + - "FAILURES!!!\n" + - "Tests run: 7, Failures: 3\n" + - "\n" - - val res = Validator.parseJunitResult(result) - - assertEquals(7, res.totalTests) - assertEquals(3, res.failedTests) - } - - @Test - fun testMatchOK() { - val result = ".......\n" + - "Time: 0.505\n" + - "\n" + - "OK (7 tests)" - - val res = Validator.parseJunitResult(result) - - assertEquals(7, res.totalTests) - assertEquals(0, res.failedTests) - assertTrue(res.failedTestNames.isEmpty()) - } -} diff --git a/src/test/kotlin/org/jetbrains/research/testspark/services/CoverageVisualisationServiceTest.kt b/src/test/kotlin/org/jetbrains/research/testspark/services/CoverageVisualisationServiceTest.kt index 0c19ba621..e6af9722a 100644 --- a/src/test/kotlin/org/jetbrains/research/testspark/services/CoverageVisualisationServiceTest.kt +++ b/src/test/kotlin/org/jetbrains/research/testspark/services/CoverageVisualisationServiceTest.kt @@ -91,7 +91,7 @@ class CoverageVisualisationServiceTest : LightJavaCodeInsightFixtureTestCase() { // } fun testCreateToolWindowTabTestSingleContent() { - coverageVisualisationService.showCoverage(Report(CompactReport(TestGenerationResultImpl())), myEditor) + coverageVisualisationService.showCoverage(Report(CompactReport(TestGenerationResultImpl()))) val toolWindow = ToolWindowManager.getInstance(project).getToolWindow("TestSpark")!! // Verify only 1 content is created @@ -99,7 +99,7 @@ class CoverageVisualisationServiceTest : LightJavaCodeInsightFixtureTestCase() { } fun testCreateToolWindowTabTestContent() { - coverageVisualisationService.showCoverage(Report(CompactReport(TestGenerationResultImpl())), myEditor) + coverageVisualisationService.showCoverage(Report(CompactReport(TestGenerationResultImpl()))) val toolWindow = ToolWindowManager.getInstance(project).getToolWindow("TestSpark")!! val content = toolWindow.contentManager.getContent(0)!! assertThat(content.displayName).isEqualTo("Coverage") diff --git a/src/test/kotlin/org/jetbrains/research/testspark/services/TestCaseCachingServicePropertyBasedTest.kt b/src/test/kotlin/org/jetbrains/research/testspark/services/TestCaseCachingServicePropertyBasedTest.kt deleted file mode 100644 index 97acae361..000000000 --- a/src/test/kotlin/org/jetbrains/research/testspark/services/TestCaseCachingServicePropertyBasedTest.kt +++ /dev/null @@ -1,108 +0,0 @@ -package org.jetbrains.research.testspark.services - -import com.intellij.util.containers.map2Array -import net.jqwik.api.Arbitraries -import net.jqwik.api.Arbitrary -import net.jqwik.api.Combinators -import net.jqwik.api.ForAll -import net.jqwik.api.Property -import net.jqwik.api.Provide -import net.jqwik.api.RandomDistribution -import net.jqwik.api.lifecycle.BeforeTry -import org.assertj.core.api.Assertions.assertThat -import org.evosuite.result.TestGenerationResultImpl -import org.evosuite.utils.CompactReport -import org.jetbrains.research.testspark.data.Report -import org.jetbrains.research.testspark.data.TestCase -import org.jetbrains.research.testspark.editor.Workspace -import org.jetbrains.research.testspark.services.TestCaseCachingServiceTest.Companion.createPair -import org.jetbrains.research.testspark.services.TestCaseCachingServiceTest.Companion.createTriple -import org.junit.jupiter.api.TestInstance -import java.lang.Integer.max -import java.lang.Integer.min - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class TestCaseCachingServicePropertyBasedTest { - - private lateinit var testCaseCachingService: TestCaseCachingService - - private val testJobInfo = Workspace.TestJobInfo("", "", 0, "", "") - - @BeforeTry - fun setUp() { - testCaseCachingService = TestCaseCachingService() - } - - @Property - fun singleFileMultipleLines( - @ForAll("compactTestCaseGenerator") testCases: List, - @ForAll("lineRangeGenerator") lineRange: Pair, - ) { - val lowerBound = lineRange.first - val upperBound = lineRange.second - - val report = Report(CompactReport(TestGenerationResultImpl())) - report.testCaseList = HashMap(testCases.associate { createPair(it) }) - val file = "file" - - testCaseCachingService.putIntoCache(file, report, testJobInfo) - - val actual = testCaseCachingService.retrieveFromCache(file, lowerBound, upperBound) - val expected = testCases.filter { it.coveredLines.any { b -> b in lowerBound..upperBound } } - - assertThat(actual) - .extracting>> { - createTriple(it) - } - .containsExactlyInAnyOrder( - *expected.map2Array { createTriple(it) }, - ) - } - - @Provide - private fun compactTestCaseGenerator(): Arbitrary> { - val lineNumberArbitrary = lineNumberGenerator() - - val testNameArbitrary = Arbitraries.strings() - .withCharRange('a', 'z') - .withCharRange('A', 'Z') - .withCharRange('0', '9') - .ofMinLength(1) - .ofMaxLength(32) - - val testCodeArbitrary = Arbitraries.strings() - .ofMinLength(1) - - var index = 0 - - val compactTestCaseArbitrary = Combinators.combine( - testNameArbitrary, - testCodeArbitrary, - lineNumberArbitrary.set(), - ) - .`as` { name, code, lineNumbers -> - TestCase(index++, name, code, lineNumbers, setOf(), setOf()) - } - .list() - .uniqueElements { it.testName } - .uniqueElements { it.testCode } - - return compactTestCaseArbitrary - } - - @Provide - fun lineNumberGenerator(): Arbitrary = Arbitraries.integers() - .between(1, 10000) - .shrinkTowards(1) - .withDistribution(RandomDistribution.gaussian(0.1)) - - @Provide - fun lineRangeGenerator(): Arbitrary> = lineNumberGenerator() - .list() - .ofSize(2) - .map { - val l1 = it[0] - val l2 = it[1] - Pair(min(l1, l2), max(l1, l2)) - } -} diff --git a/src/test/kotlin/org/jetbrains/research/testspark/services/TestCaseCachingServiceTest.kt b/src/test/kotlin/org/jetbrains/research/testspark/services/TestCaseCachingServiceTest.kt deleted file mode 100644 index 5a934b331..000000000 --- a/src/test/kotlin/org/jetbrains/research/testspark/services/TestCaseCachingServiceTest.kt +++ /dev/null @@ -1,462 +0,0 @@ -package org.jetbrains.research.testspark.services - -import org.assertj.core.api.Assertions.assertThat -import org.evosuite.result.TestGenerationResultImpl -import org.evosuite.utils.CompactReport -import org.jetbrains.research.testspark.data.Report -import org.jetbrains.research.testspark.data.TestCase -import org.jetbrains.research.testspark.editor.Workspace -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class TestCaseCachingServiceTest { - - private lateinit var testCaseCachingService: TestCaseCachingService - - private val testJobInfo = Workspace.TestJobInfo("", "", 0, "", "") - - @BeforeEach - fun setUp() { - testCaseCachingService = TestCaseCachingService() - } - - @Test - fun singleFileSingleLine() { - val report = Report(CompactReport(TestGenerationResultImpl())) - val test1 = TestCase(0, "a", "aa", setOf(1, 2), setOf(), setOf()) - val test2 = TestCase(1, "b", "bb", setOf(2, 3), setOf(), setOf()) - report.testCaseList = hashMapOf( - createPair(test1), - createPair(test2), - ) - - val file = "file" - - testCaseCachingService.putIntoCache(file, report, testJobInfo) - - val result = testCaseCachingService.retrieveFromCache(file, 2, 2) - - assertThat(result) - .extracting>> { - createTriple(it) - } - .containsExactlyInAnyOrder( - createTriple(test1), - createTriple(test2), - ) - } - - @Test - fun addingNewTestWithSameCodeInvalidatesPreviousOne() { - val report = Report(CompactReport(TestGenerationResultImpl())) - val test1 = TestCase(0, "a", "aa", setOf(1, 2), setOf(), setOf()) - val test2 = TestCase(1, "b", "bb", setOf(2, 3), setOf(), setOf()) - report.testCaseList = hashMapOf( - createPair(test1), - createPair(test2), - ) - val report1a = Report(CompactReport(TestGenerationResultImpl())) - val test1a = TestCase(0, "a2", "aa", setOf(1, 2, 3), setOf(), setOf()) - report1a.testCaseList = hashMapOf( - createPair(test1a), - ) - - val file = "file" - - testCaseCachingService.putIntoCache(file, report, testJobInfo) - testCaseCachingService.putIntoCache(file, report1a, testJobInfo) - - val result = testCaseCachingService.retrieveFromCache(file, 2, 2) - - assertThat(result) - .extracting>> { - createTriple(it) - } - .containsExactlyInAnyOrder( - createTriple(test1a), - createTriple(test2), - ) - } - - @Test - fun invalidateSingleFileSingleLine() { - val report = Report(CompactReport(TestGenerationResultImpl())) - val test1 = TestCase(0, "a", "aa", setOf(1, 2), setOf(), setOf()) - val test2 = TestCase(1, "b", "bb", setOf(2, 3), setOf(), setOf()) - report.testCaseList = hashMapOf( - createPair(test1), - createPair(test2), - ) - - val file = "file" - - testCaseCachingService.putIntoCache(file, report, testJobInfo) - - testCaseCachingService.invalidateFromCache(file, 1, 1) - - val result = testCaseCachingService.retrieveFromCache(file, 2, 2) - - assertThat(result) - .extracting>> { - createTriple(it) - } - .containsExactlyInAnyOrder( - createTriple(test2), - ) - } - - @Test - fun invalidateSingleTest() { - val report = Report(CompactReport(TestGenerationResultImpl())) - val test1 = TestCase(0, "a", "aa", setOf(1, 2), setOf(), setOf()) - val test2 = TestCase(1, "b", "bb", setOf(2, 3), setOf(), setOf()) - report.testCaseList = hashMapOf( - createPair(test1), - createPair(test2), - ) - - val file = "file" - - testCaseCachingService.putIntoCache(file, report, testJobInfo) - - testCaseCachingService.invalidateFromCache(file, test2.testCode) - - val result = testCaseCachingService.retrieveFromCache(file, 2, 2) - - assertThat(result) - .extracting>> { - createTriple(it) - } - .containsExactlyInAnyOrder( - createTriple(test1), - ) - } - - @Test - fun invalidateNonexistentSingleTest() { - val report = Report(CompactReport(TestGenerationResultImpl())) - val test1 = TestCase(0, "a", "aa", setOf(1, 2), setOf(), setOf()) - val test2 = TestCase(1, "b", "bb", setOf(2, 3), setOf(), setOf()) - report.testCaseList = hashMapOf( - createPair(test1), - createPair(test2), - ) - - val file = "file" - - testCaseCachingService.putIntoCache(file, report, testJobInfo) - testCaseCachingService.invalidateFromCache(file, "invaid") - - val result = testCaseCachingService.retrieveFromCache(file, 2, 2) - - assertThat(result) - .extracting>> { - createTriple(it) - } - .containsExactlyInAnyOrder( - createTriple(test1), - createTriple(test2), - ) - } - - @Test - fun singleFileMultipleLines() { - val report = Report(CompactReport(TestGenerationResultImpl())) - val test1 = TestCase(0, "a", "aa", setOf(1, 2), setOf(), setOf()) - val test2 = TestCase(1, "b", "bb", setOf(2, 3), setOf(), setOf()) - val test3 = TestCase(2, "c", "cc", setOf(1, 4), setOf(), setOf()) - val test4 = TestCase(3, "d", "dd", setOf(8), setOf(), setOf()) - val test5 = TestCase(4, "e", "ee", setOf(11), setOf(), setOf()) - report.testCaseList = hashMapOf( - createPair(test1), - createPair(test2), - createPair(test3), - createPair(test4), - createPair(test5), - ) - - val file = "file" - - testCaseCachingService.putIntoCache(file, report, testJobInfo) - - val result = testCaseCachingService.retrieveFromCache(file, 4, 10) - - assertThat(result) - .extracting>> { - createTriple(it) - } - .containsExactlyInAnyOrder( - createTriple(test3), - createTriple(test4), - ) - } - - @Test - fun invalidateSingleFileMultipleLines() { - val report = Report(CompactReport(TestGenerationResultImpl())) - val test1 = TestCase(0, "a", "aa", setOf(1, 2), setOf(), setOf()) - val test2 = TestCase(1, "b", "bb", setOf(2, 3), setOf(), setOf()) - val test3 = TestCase(2, "c", "cc", setOf(1, 4), setOf(), setOf()) - val test4 = TestCase(3, "d", "dd", setOf(8), setOf(), setOf()) - val test5 = TestCase(4, "e", "ee", setOf(11), setOf(), setOf()) - report.testCaseList = hashMapOf( - createPair(test1), - createPair(test2), - createPair(test3), - createPair(test4), - createPair(test5), - ) - - val file = "file" - - testCaseCachingService.putIntoCache(file, report, testJobInfo) - - testCaseCachingService.invalidateFromCache(file, 3, 9) - - val result = testCaseCachingService.retrieveFromCache(file, 1, 11) - - assertThat(result) - .extracting>> { - createTriple(it) - } - .containsExactlyInAnyOrder( - createTriple(test1), - createTriple(test5), - ) - } - - @Test - fun multipleFiles() { - val report = Report(CompactReport(TestGenerationResultImpl())) - val test1 = TestCase(0, "a", "aa", setOf(1, 2), setOf(), setOf()) - val test2 = TestCase(1, "b", "bb", setOf(2, 3), setOf(), setOf()) - val test3 = TestCase(2, "c", "cc", setOf(1, 4), setOf(), setOf()) - val test4 = TestCase(3, "d", "dd", setOf(8), setOf(), setOf()) - val test5 = TestCase(4, "e", "ee", setOf(11), setOf(), setOf()) - report.testCaseList = hashMapOf( - createPair(test1), - createPair(test2), - createPair(test3), - createPair(test4), - createPair(test5), - ) - - val report2 = Report(CompactReport(TestGenerationResultImpl())) - report2.testCaseList = hashMapOf( - createPair(TestCase(0, "0a", "aa", setOf(1, 2), setOf(), setOf())), - createPair(TestCase(1, "0b", "bb", setOf(2, 3), setOf(), setOf())), - createPair(TestCase(2, "0c", "cc", setOf(1, 4), setOf(), setOf())), - createPair(TestCase(3, "0d", "dd", setOf(8), setOf(), setOf())), - createPair(TestCase(4, "0e", "ee", setOf(11), setOf(), setOf())), - ) - - val file = "file" - - testCaseCachingService.putIntoCache(file, report, testJobInfo) - testCaseCachingService.putIntoCache("file 2", report2, testJobInfo) - - val result = testCaseCachingService.retrieveFromCache(file, 4, 10) - - assertThat(result) - .extracting>> { - createTriple(it) - } - .containsExactlyInAnyOrder( - createTriple(test3), - createTriple(test4), - ) - } - - @Test - fun multipleFilesMultipleInsertions() { - val report = Report(CompactReport(TestGenerationResultImpl())) - val test1 = TestCase(0, "a", "aa", setOf(1, 2), setOf(), setOf()) - val test2 = TestCase(1, "b", "bb", setOf(2, 3), setOf(), setOf()) - val test3 = TestCase(2, "c", "cc", setOf(1, 4), setOf(), setOf()) - val test5 = TestCase(3, "e", "ee", setOf(11), setOf(), setOf()) - report.testCaseList = hashMapOf( - createPair(test1), - createPair(test2), - createPair(test3), - createPair(test5), - ) - - val report2 = Report(CompactReport(TestGenerationResultImpl())) - report2.testCaseList = hashMapOf( - createPair(TestCase(0, "0a", "aa", setOf(1, 2), setOf(), setOf())), - createPair(TestCase(1, "0b", "bb", setOf(2, 3), setOf(), setOf())), - createPair(TestCase(2, "0c", "cc", setOf(1, 4), setOf(), setOf())), - createPair(TestCase(3, "0d", "dd", setOf(8), setOf(), setOf())), - createPair(TestCase(4, "0e", "ee", setOf(11), setOf(), setOf())), - ) - - val report3 = Report(CompactReport(TestGenerationResultImpl())) - val test4 = TestCase(0, "d", "dd", setOf(8), setOf(), setOf()) - report3.testCaseList = hashMapOf( - createPair(test4), - ) - - val file = "file" - - testCaseCachingService.putIntoCache(file, report, testJobInfo) - testCaseCachingService.putIntoCache("file 2", report2, testJobInfo) - testCaseCachingService.putIntoCache(file, report3, testJobInfo) - - val result = testCaseCachingService.retrieveFromCache(file, 4, 10) - - assertThat(result) - .extracting>> { - createTriple(it) - } - .containsExactlyInAnyOrder( - createTriple(test3), - createTriple(test4), - ) - } - - @Test - fun testCoversMultipleLinesInRange() { - val report = Report(CompactReport(TestGenerationResultImpl())) - val test1 = TestCase(0, "a", "aa", setOf(4, 5), setOf(), setOf()) - report.testCaseList = hashMapOf( - createPair(test1), - ) - - val file = "file" - - testCaseCachingService.putIntoCache(file, report, testJobInfo) - - val result = testCaseCachingService.retrieveFromCache(file, 1, 10) - - assertThat(result) - .extracting>> { - createTriple(it) - } - .containsExactlyInAnyOrder( - createTriple(test1), - ) - } - - @Test - fun nonexistentFile() { - val report = Report(CompactReport(TestGenerationResultImpl())) - val test1 = TestCase(0, "a", "aa", setOf(1, 2), setOf(), setOf()) - val test2 = TestCase(1, "b", "bb", setOf(2, 3), setOf(), setOf()) - report.testCaseList = hashMapOf( - createPair(test1), - createPair(test2), - ) - - testCaseCachingService.putIntoCache("aa", report, testJobInfo) - - val result = testCaseCachingService.retrieveFromCache("bb", 2, 2) - - assertThat(result) - .isEmpty() - } - - @Test - fun noMatchingTests() { - val report = Report(CompactReport(TestGenerationResultImpl())) - val test1 = TestCase(0, "a", "aa", setOf(1, 2), setOf(), setOf()) - val test2 = TestCase(1, "b", "bb", setOf(2, 3), setOf(), setOf()) - report.testCaseList = hashMapOf( - createPair(test1), - createPair(test2), - ) - - val file = "file" - - testCaseCachingService.putIntoCache(file, report, testJobInfo) - - val result = testCaseCachingService.retrieveFromCache(file, 4, 50) - - assertThat(result) - .isEmpty() - } - - @Test - fun invalidateNoMatchingTests() { - val report = Report(CompactReport(TestGenerationResultImpl())) - val test1 = TestCase(0, "a", "aa", setOf(1, 2), setOf(), setOf()) - val test2 = TestCase(1, "b", "bb", setOf(2, 3), setOf(), setOf()) - report.testCaseList = hashMapOf( - createPair(test1), - createPair(test2), - ) - - val file = "file" - - testCaseCachingService.putIntoCache(file, report, testJobInfo) - testCaseCachingService.invalidateFromCache(file, 4, 50) - val result = testCaseCachingService.retrieveFromCache(file, 1, 50) - - assertThat(result) - .extracting>> { - createTriple(it) - } - .containsExactlyInAnyOrder( - createTriple(test1), - createTriple(test2), - ) - } - - @Test - fun invalidInputLines() { - val report = Report(CompactReport(TestGenerationResultImpl())) - val test1 = TestCase(0, "a", "aa", setOf(1, 2), setOf(), setOf()) - val test2 = TestCase(1, "b", "bb", setOf(2, 3), setOf(), setOf()) - report.testCaseList = hashMapOf( - createPair(test1), - createPair(test2), - ) - - val file = "file" - - testCaseCachingService.putIntoCache(file, report, testJobInfo) - - val result = testCaseCachingService.retrieveFromCache(file, 4, 1) - - assertThat(result) - .isEmpty() - } - - @Test - fun invalidateInvalidInputLines() { - val report = Report(CompactReport(TestGenerationResultImpl())) - val test1 = TestCase(0, "a", "aa", setOf(1, 2), setOf(), setOf()) - val test2 = TestCase(1, "b", "bb", setOf(2, 3), setOf(), setOf()) - report.testCaseList = hashMapOf( - createPair(test1), - createPair(test2), - ) - - val file = "file" - - testCaseCachingService.putIntoCache(file, report, testJobInfo) - testCaseCachingService.invalidateFromCache(file, 4, 1) - - val result = testCaseCachingService.retrieveFromCache(file, 1, 50) - - assertThat(result) - .extracting>> { - createTriple(it) - } - .containsExactlyInAnyOrder( - createTriple(test1), - createTriple(test2), - ) - } - - companion object { - fun createPair(testCase: TestCase): Pair { - return Pair(testCase.id, testCase) - } - - fun createTriple(testCase: TestCase): Triple> { - return Triple(testCase.testName.split(' ')[0], testCase.testCode, testCase.coveredLines) - } - } -}