diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 1495567b..7c1058ee 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -21,10 +21,10 @@ assignees: "" ## Definition of done (DoD) - All acceptance criteria are met. -- Work products are uploaded to the Github repository. +- Work products are uploaded to the GitHub repository. - A pull request is created for each related branch. - The work products in the pull requests are reviewed. -- Github CI Workflow passes for the branches +- GitHub CI Workflow passes for the branches - The corresponding branches are merged and closed. - The bill of materials section of the planning documents is updated. - Tests are written for the added features, if suitable diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6a2389ec..2690d339 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,13 +3,13 @@ # SPDX-License-Identifier: MIT --- -name: ktlint +name: super-linter on: pull_request: jobs: - lint: + super-linter: runs-on: ubuntu-latest permissions: @@ -31,3 +31,5 @@ jobs: VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VALIDATE_HTML: false + FILTER_REGEX_EXCLUDE: ".*src/test/resources.*" diff --git a/Deliverables/sprint-07/feature_board.jpg b/Deliverables/sprint-07/feature_board.jpg new file mode 100644 index 00000000..60772a3f Binary files /dev/null and b/Deliverables/sprint-07/feature_board.jpg differ diff --git a/Deliverables/sprint-07/planning_documents.pdf b/Deliverables/sprint-07/planning_documents.pdf new file mode 100644 index 00000000..409cf235 Binary files /dev/null and b/Deliverables/sprint-07/planning_documents.pdf differ diff --git a/pitmutationmate-override-plugin/plugin/src/test/groovy/io/github/amosproj/pitmutationmate/override/communication/UdpMessagingServiceSpec.groovy b/pitmutationmate-override-plugin/plugin/src/test/groovy/io/github/amosproj/pitmutationmate/override/communication/UdpMessagingServiceSpec.groovy index 1614d652..b4f5c5a4 100644 --- a/pitmutationmate-override-plugin/plugin/src/test/groovy/io/github/amosproj/pitmutationmate/override/communication/UdpMessagingServiceSpec.groovy +++ b/pitmutationmate-override-plugin/plugin/src/test/groovy/io/github/amosproj/pitmutationmate/override/communication/UdpMessagingServiceSpec.groovy @@ -6,7 +6,6 @@ package io.github.amosproj.pitmutationmate.override.communication import io.github.amosproj.pitmutationmate.override.communicaiton.UdpMessagingService import spock.lang.Specification -import spock.lang.Unroll /** * UdpMessagingServiceSpec @@ -19,7 +18,7 @@ class UdpMessagingServiceSpec extends Specification { def "Test UDP client sends messages correctly"() { given: - def port = 50001 + def port = findAvailablePort() def client = new UdpMessagingService(port: port) def mockServer = new MockUDPServer(port) mockServer.startServer() @@ -50,10 +49,10 @@ Duis eget erat ipsum. V""".trim() when: for (message in testMessages) { client.sendMessage(message) - Thread.sleep(100) } then: + Thread.sleep(100) mockServer.stopServer() mockServer.thread.join() for (message in expectedMessages) { @@ -61,4 +60,21 @@ Duis eget erat ipsum. V""".trim() } } + private int findAvailablePort() { + def minPortNumber = 49152 + def maxPortNumber = 65535 + + for (port in minPortNumber..maxPortNumber) { + try { + def socket = new DatagramSocket(port) + socket.close() + return port + } catch (Exception ignored) { + // Port is not available, continue searching + } + } + + throw new IllegalStateException("No available port found in the dynamic range") + } + } diff --git a/pitmutationmate/build.gradle.kts b/pitmutationmate/build.gradle.kts index 188c5647..4fc6ffd6 100644 --- a/pitmutationmate/build.gradle.kts +++ b/pitmutationmate/build.gradle.kts @@ -16,12 +16,14 @@ dependencies { implementation("org.pitest:pitest-command-line:1.7.0") implementation("org.junit.jupiter:junit-jupiter:5.8.1") implementation("org.junit.jupiter:junit-jupiter:5.8.1") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") // https://mvnrepository.com/artifact/org.jfree/jfreechart implementation("org.jfree:jfreechart:1.0.19") // https://mvnrepository.com/artifact/jfree/jcommon implementation("org.jfree:jcommon:1.0.24") + // https://mvnrepository.com/artifact/org.mockito/mockito-core + testImplementation("org.mockito:mockito-core:5.8.0") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") } group = "com.amos.pitmutationmate" diff --git a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/MutationTestToolWindowFactory.kt b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/MutationTestToolWindowFactory.kt index 85de683a..00e2fc3f 100644 --- a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/MutationTestToolWindowFactory.kt +++ b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/MutationTestToolWindowFactory.kt @@ -2,6 +2,7 @@ // SPDX-FileCopyrightText: 2023 package com.amos.pitmutationmate.pitmutationmate +import com.amos.pitmutationmate.pitmutationmate.reporting.XMLParser import com.amos.pitmutationmate.pitmutationmate.visualization.BarGraph import com.amos.pitmutationmate.pitmutationmate.visualization.LatestPiTestReport import com.amos.pitmutationmate.pitmutationmate.visualization.LineGraph @@ -10,11 +11,19 @@ import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory +import com.intellij.ui.components.JBLabel import com.intellij.ui.content.ContentFactory +import javax.swing.JPanel internal class MutationTestToolWindowFactory : ToolWindowFactory, DumbAware { override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { - val latestPiTestReport = ContentFactory.getInstance().createContent(LatestPiTestReport(), "Latest Result", false) + // TODO: fetch most recent results to display (e.g. when opening up the editor and previous Pitest runs are saved) + val lastCoverageReport: XMLParser.CoverageReport? = null + val latestPiTestReport = if (lastCoverageReport == null) { + ContentFactory.getInstance().createContent(displayErrorMessage(), "Latest Result", false) + } else { + ContentFactory.getInstance().createContent(LatestPiTestReport(lastCoverageReport), "Latest Result", false) + } val table = ContentFactory.getInstance().createContent(JTreeTable(), "Mutationtest Coverage", false) val lineChart = ContentFactory.getInstance().createContent(LineGraph(), "Line Chart", false) val barChart = ContentFactory.getInstance().createContent(BarGraph(), "Bar Chart", false) @@ -24,4 +33,19 @@ internal class MutationTestToolWindowFactory : ToolWindowFactory, DumbAware { toolWindow.contentManager.addContent(lineChart) toolWindow.contentManager.addContent(barChart) } + + fun updateReport(toolWindow: ToolWindow, newCoverageReport: XMLParser.CoverageReport) { + toolWindow.contentManager.findContent("Latest Result").component = LatestPiTestReport(newCoverageReport) + } + + private fun displayErrorMessage(): JPanel { + val panel = JPanel() + + // Displaying an error message in the panel + val errorMessage = "No results to display yet." + val errorLabel = JBLabel(errorMessage) + panel.add(errorLabel) + + return panel + } } diff --git a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/actions/ContextMenuAction.kt b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/actions/ContextMenuAction.kt index bc3e5f46..097139b3 100644 --- a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/actions/ContextMenuAction.kt +++ b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/actions/ContextMenuAction.kt @@ -3,35 +3,82 @@ package com.amos.pitmutationmate.pitmutationmate.actions +import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.diagnostic.Logger +import com.intellij.psi.PsiClass import com.intellij.psi.PsiClassOwner +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.psi.KtClass class ContextMenuAction : RunConfigurationAction() { + private val logger = Logger.getInstance(ContextMenuAction::class.java) override fun actionPerformed(e: AnActionEvent) { + val editor = e.getData(CommonDataKeys.EDITOR) val psiFile = e.getData(CommonDataKeys.PSI_FILE) - println("ContextMenuAction: actionPerformed for file $psiFile") - val psiClasses = (psiFile as PsiClassOwner).classes - val fqns = mutableListOf() - for (psiClass in psiClasses) { - val fqn = psiClass.qualifiedName - if (fqn != null) { - fqns.add(fqn) - println("ContextMenuAction: detected class '$fqn'") + if (e.place == "EditorPopup") { + logger.info("ContextMenuAction: actionPerformed in EditorPopup for file $psiFile") + val psiElement = psiFile?.findElementAt(editor?.caretModel!!.offset) + val selectedClass = findEnclosingClass(psiElement) + if (selectedClass != null) { + var classFQN = "" + if (selectedClass is PsiClass) { + classFQN = selectedClass.qualifiedName.toString() + } + if (selectedClass is KtClass) { + classFQN = selectedClass.fqName.toString() + } + + logger.info("ContextMenuAction: selected class is $classFQN.") + updateAndExecuteRunConfig(classFQN, e.project!!, editor) } } - val editor = e.getData(CommonDataKeys.EDITOR) - updateAndExecuteRunConfig(fqns.first(), e.project!!, editor) + if (e.place == "ProjectViewPopup") { + logger.info("ContextMenuAction: actionPerformed in ProjectViewPopup for file $psiFile") + val psiClasses = (psiFile as PsiClassOwner).classes + var classFQNs: String = "" + for (psiClass in psiClasses) { + val fqn = psiClass.qualifiedName + if (fqn != null) { + classFQNs = if (classFQNs != "") { + "$classFQNs,$fqn" + } else { + fqn + } + } + } + logger.info("ContextMenuAction: selected classes are $classFQNs.") + updateAndExecuteRunConfig(classFQNs, e.project!!, editor) + } } override fun update(e: AnActionEvent) { - val file: VirtualFile? = e.dataContext.getData("virtualFile") as VirtualFile? - val shouldEnable: Boolean = checkCondition(file) + val shouldEnable: Boolean = checkCondition(e) e.presentation.isEnabled = shouldEnable } - private fun checkCondition(file: VirtualFile?): Boolean { - return file != null && (file.name.endsWith(".java") || file.name.endsWith(".kt")) + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.BGT + } + + private fun checkCondition(e: AnActionEvent): Boolean { + val psiFile = e.getData(CommonDataKeys.PSI_FILE) + val validFile = psiFile != null && (psiFile.name.endsWith(".java") || psiFile.name.endsWith(".kt")) + if (e.place == "EditorPopup") { + val editor = e.getData(CommonDataKeys.EDITOR) + val psiElement = psiFile?.findElementAt(editor?.caretModel!!.offset) + val validClass = (findEnclosingClass(psiElement) != null) + return validFile && validClass + } + return validFile + } + + private fun findEnclosingClass(psiElement: PsiElement?): PsiElement? { + var currentElement: PsiElement? = psiElement + while (currentElement != null && currentElement !is PsiClass && currentElement !is KtClass) { + currentElement = currentElement.parent + } + return currentElement } } diff --git a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/actions/GutterMarker.kt b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/actions/GutterMarker.kt index d83a6203..41bc0671 100644 --- a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/actions/GutterMarker.kt +++ b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/actions/GutterMarker.kt @@ -7,17 +7,24 @@ import com.intellij.execution.lineMarker.RunLineMarkerContributor import com.intellij.icons.AllIcons import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.psi.KtClass import javax.swing.Icon class GutterMarker : RunLineMarkerContributor() { override fun getInfo(psielement: PsiElement): Info? { + val gutterIcon: Icon = AllIcons.General.ArrowRight if (psielement is PsiClass) { - val gutterIcon: Icon = AllIcons.General.ArrowRight val toolTipProvider: (PsiElement) -> String = { _ -> "Run PIT MutationMate on '${psielement.name}'" } val fqn = psielement.qualifiedName val action: Array = arrayOf(fqn?.let { GutterAction(it) }) return Info(gutterIcon, action, toolTipProvider) } + if (psielement is KtClass) { + val toolTipProvider: (PsiElement) -> String = { _ -> "Run PIT MutationMate on '${psielement.name}'" } + val fqn = psielement.fqName.toString() + val action: Array = arrayOf(GutterAction(fqn)) + return Info(gutterIcon, action, toolTipProvider) + } return null } } diff --git a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/actions/HoverAction.kt b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/actions/HoverAction.kt new file mode 100644 index 00000000..74ff2959 --- /dev/null +++ b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/actions/HoverAction.kt @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2023 Tim Herzig + +package com.amos.pitmutationmate.pitmutationmate.actions + +import com.amos.pitmutationmate.pitmutationmate.reporting.XMLParser +import com.intellij.codeInsight.hint.HintManager +import com.intellij.codeInsight.hint.HintManagerImpl +import com.intellij.codeInsight.hint.HintUtil +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.EditorMouseEvent +import com.intellij.openapi.editor.event.EditorMouseListener +import com.intellij.openapi.editor.event.EditorMouseMotionListener +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiFile +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.ui.LightweightHint +import com.intellij.util.ui.accessibility.AccessibleContextUtil +import java.awt.Point +import javax.swing.JComponent + +class HoverAction(private val editor: Editor, private val result: XMLParser.ResultData) { + fun addHoverAction() { + this.editor.addEditorMouseListener(MouseClick()) +// TODO: Decide on which action suits the plugin the best --> when choice is made refactor to only use one +// this.editor.addEditorMouseMotionListener(MouseMotion()) + } + + inner class MouseMotion : EditorMouseMotionListener { + override fun mouseMoved(event: EditorMouseEvent) { + showHoverMessage(event.mouseEvent.point) + } + } + + inner class MouseClick : EditorMouseListener { + override fun mouseClicked(event: EditorMouseEvent) { + showHoverMessage(event.mouseEvent.point) + } + } + + private fun buildHoverMessage(point: Point): String? { + val project: Project = this.editor.project ?: return null + val psiFile: PsiFile = PsiDocumentManager.getInstance(project).getPsiFile(this.editor.document) ?: return null + + val offset: Int = this.editor.visualPositionToOffset(this.editor.xyToVisualPosition(point)) + val line: Int = this.editor.yToVisualLine(point.y) + 1 + PsiTreeUtil.findElementOfClassAtOffset(psiFile, offset, psiFile.javaClass, false) + + for (r in this.result.mutationResults) { + if (r.lineNumber == line) { + val color: String = if (r.detected) "dark-green" else "dark-pink" + return "PiTest: selected offset: $offset, selected line: $line \n" + + "The color of this line is $color" + } + } + + return null + } + + fun showHoverMessage(point: Point) { + val message: String = buildHoverMessage(point) ?: return + val hintManager: HintManagerImpl = HintManagerImpl.getInstanceImpl() + val label: JComponent = HintUtil.createInformationLabel(message, null, null, null) + AccessibleContextUtil.setName(label, "PiTest") + val hint = LightweightHint(label) + val p: Point = HintManagerImpl.getHintPosition(hint, this.editor, this.editor.xyToVisualPosition(point), 1) + val flags: Int = HintManager.HIDE_BY_ANY_KEY or HintManager.HIDE_BY_TEXT_CHANGE or HintManager.HIDE_BY_SCROLLING + hintManager.showEditorHint(hint, this.editor, p, flags, 0, true, 1) + } +} diff --git a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/actions/RunConfigurationAction.kt b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/actions/RunConfigurationAction.kt index 2020f764..468a9b37 100644 --- a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/actions/RunConfigurationAction.kt +++ b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/actions/RunConfigurationAction.kt @@ -3,15 +3,20 @@ package com.amos.pitmutationmate.pitmutationmate.actions +import com.amos.pitmutationmate.pitmutationmate.MutationTestToolWindowFactory import com.amos.pitmutationmate.pitmutationmate.configuration.RunConfiguration import com.amos.pitmutationmate.pitmutationmate.configuration.RunConfigurationType import com.amos.pitmutationmate.pitmutationmate.reporting.XMLListener +import com.amos.pitmutationmate.pitmutationmate.reporting.XMLParser import com.intellij.execution.ExecutorRegistry import com.intellij.execution.ProgramRunnerUtil import com.intellij.execution.RunManager import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindow +import com.intellij.openapi.wm.ToolWindowManager +import org.jetbrains.kotlin.idea.gradleTooling.get import java.nio.file.Paths abstract class RunConfigurationAction : AnAction() { @@ -34,8 +39,26 @@ abstract class RunConfigurationAction : AnAction() { if (editor != null) { // TODO: use actual XML report directories. This currently uses a placeholder test folder val dir = Paths.get("build", "reports", "pitest", "test", "mutations.xml") - var xmlListener = XMLListener(dir, editor) + val xmlListener = XMLListener(dir, editor) xmlListener.listen() + val ha: HoverAction = HoverAction(editor, xmlListener.getResult()) + ha.addHoverAction() + } + + // Update visualisation with mock results + // TODO: replace this by real results extracted by the HTMLParser + val toolWindow: ToolWindow? = ToolWindowManager.getInstance(project).getToolWindow("Pitest") + val mutationTestToolWindowFactorySingleton = MutationTestToolWindowFactory() + val coverageReport: XMLParser.CoverageReport = XMLParser.CoverageReport( + lineCoveragePercentage = 80, + lineCoverageTextRatio = "160/200", + mutationCoveragePercentage = 50, + mutationCoverageTextRatio = "100/200", + testStrengthPercentage = 40, + testStrengthTextRatio = "80/200" + ) + if (toolWindow != null) { + mutationTestToolWindowFactorySingleton.updateReport(toolWindow, coverageReport) } } } diff --git a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/BasePitestExecutor.kt b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/BasePitestExecutor.kt index b4265028..4e3832b4 100644 --- a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/BasePitestExecutor.kt +++ b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/BasePitestExecutor.kt @@ -21,13 +21,13 @@ abstract class BasePitestExecutor { fun executeTask( project: Project, executable: String?, - taskName: String?, + overrideTaskName: String?, classFQN: String? ): ProcessHandler { val messagingServer = project.service() messagingServer.startServer(classFQN) // Start the UDP server - val commandLine = buildCommandLine(executable, taskName, project.basePath!!, classFQN, messagingServer.port) + val commandLine = buildCommandLine(executable, overrideTaskName, project.basePath!!, classFQN, messagingServer.port) log.debug("BasePitestExecutor: executeTask: commandLine: $commandLine") val processHandler = createProcessHandler(commandLine) processHandler.addProcessListener(object : ProcessAdapter() { @@ -42,7 +42,7 @@ abstract class BasePitestExecutor { abstract fun buildCommandLine( executable: String?, - taskName: String?, + overrideTaskName: String?, projectDir: String, classFQN: String?, port: Int diff --git a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/GradleTaskExecutor.kt b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/GradleTaskExecutor.kt index 99a60376..0ca3e9c7 100644 --- a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/GradleTaskExecutor.kt +++ b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/GradleTaskExecutor.kt @@ -4,41 +4,47 @@ package com.amos.pitmutationmate.pitmutationmate.execution import com.intellij.execution.configurations.GeneralCommandLine -import com.intellij.openapi.util.SystemInfo import java.io.File class GradleTaskExecutor : BasePitestExecutor() { - private var taskName: String = "pitest" - private var gradleExecutable: String? = null - private val windowsGradleExecutable = "gradlew.bat" - private val unixGradleExecutable = "./gradlew" + companion object { + const val PITEST_TASK_NAME = "pitest" + const val WINDOWS_SHELL_EXECUTABLE = "cmd" + const val WINDOWS_FIRST_PARAMETER = "/c" + const val WINDOWS_GRADLE_EXECUTABLE = "gradlew.bat" + const val UNIX_SHELL_EXECUTABLE = "/usr/bin/env" + const val UNIX_FIRST_PARAMETER = "sh" + const val UNIX_GRADLE_EXECUTABLE = "./gradlew" + } + private var systemInfoProvider: SystemInfoProvider = SystemInfo() override fun buildCommandLine( executable: String?, - taskName: String?, + overrideTaskName: String?, projectDir: String, classFQN: String?, port: Int ): GeneralCommandLine { val commandLine = GeneralCommandLine() + var gradleExecutable: String? = executable + var taskName: String? = PITEST_TASK_NAME - this.gradleExecutable = executable - if (!taskName.isNullOrEmpty()) { - this.taskName = taskName!! + if (!overrideTaskName.isNullOrEmpty()) { + taskName = overrideTaskName } - if (SystemInfo.isWindows) { - if (this.gradleExecutable.isNullOrEmpty()) { - this.gradleExecutable = windowsGradleExecutable + if (systemInfoProvider.isWindows()) { + if (gradleExecutable.isNullOrEmpty()) { + gradleExecutable = WINDOWS_GRADLE_EXECUTABLE } - commandLine.exePath = "cmd" - commandLine.addParameters("/c", this.gradleExecutable, this.taskName) + commandLine.exePath = WINDOWS_SHELL_EXECUTABLE + commandLine.addParameters(WINDOWS_FIRST_PARAMETER, gradleExecutable, taskName) } else { - if (this.gradleExecutable.isNullOrEmpty()) { - this.gradleExecutable = unixGradleExecutable + if (gradleExecutable.isNullOrEmpty()) { + gradleExecutable = UNIX_GRADLE_EXECUTABLE } - commandLine.exePath = "/usr/bin/env" - commandLine.addParameters("sh", this.gradleExecutable, this.taskName) + commandLine.exePath = UNIX_SHELL_EXECUTABLE + commandLine.addParameters(UNIX_FIRST_PARAMETER, gradleExecutable, taskName) } commandLine.addParameters(getPitestOverrideParameters(classFQN, port)) diff --git a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/MavenTaskExecutor.kt b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/MavenTaskExecutor.kt index f39cd1bb..9a06520e 100644 --- a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/MavenTaskExecutor.kt +++ b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/MavenTaskExecutor.kt @@ -12,7 +12,7 @@ class MavenTaskExecutor : BasePitestExecutor() { override fun buildCommandLine( executable: String?, - taskName: String?, + overrideTaskName: String?, projectDir: String, classFQN: String?, port: Int @@ -23,8 +23,8 @@ class MavenTaskExecutor : BasePitestExecutor() { this.mavenExecutable = executable!! } - if (!taskName.isNullOrEmpty()) { - this.taskName = taskName!! + if (!overrideTaskName.isNullOrEmpty()) { + this.taskName = overrideTaskName!! } commandLine.exePath = mavenExecutable diff --git a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/SystemInfo.kt b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/SystemInfo.kt new file mode 100644 index 00000000..7b2cfaf7 --- /dev/null +++ b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/SystemInfo.kt @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2023 Lennart Heimbs + +package com.amos.pitmutationmate.pitmutationmate.execution + +import com.intellij.openapi.util.SystemInfo + +interface SystemInfoProvider { + fun isWindows(): Boolean +} + +class SystemInfo : SystemInfoProvider { + override fun isWindows(): Boolean { + return SystemInfo.isWindows + } +} diff --git a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/reporting/XMLListener.kt b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/reporting/XMLListener.kt index f6eb908d..39ea6011 100644 --- a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/reporting/XMLListener.kt +++ b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/reporting/XMLListener.kt @@ -18,9 +18,13 @@ class XMLListener(private var dir: Path, private var editor: Editor) { displayResults() } + fun getResult(): XMLParser.ResultData { + return this.result + } + private fun loadResults() { val parser: XMLParser = XMLParser() - result = parser.loadResultsFromXmlReport(this.dir.toString()) + this.result = parser.loadResultsFromXmlReport(this.dir.toString()) } fun displayResults() { diff --git a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/reporting/XMLParser.kt b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/reporting/XMLParser.kt index 335fc023..653ef1cb 100644 --- a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/reporting/XMLParser.kt +++ b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/reporting/XMLParser.kt @@ -102,6 +102,8 @@ class XMLParser { } data class ResultData( + // placeholder field for coverage report results to be displayed in visualisation + val coverageReport: CoverageReport? = null, val mutationResults: MutableList = mutableListOf() ) { fun addMutationResult(mutationResult: MutationResult) { @@ -129,4 +131,13 @@ class XMLParser { val killingTest: String, val description: String ) + + data class CoverageReport( + val lineCoveragePercentage: Int, + val lineCoverageTextRatio: String, + val mutationCoveragePercentage: Int, + val mutationCoverageTextRatio: String, + val testStrengthPercentage: Int, + val testStrengthTextRatio: String + ) } diff --git a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/visualization/LatestPiTestReport.kt b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/visualization/LatestPiTestReport.kt index 7fe98f1d..150fe94c 100644 --- a/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/visualization/LatestPiTestReport.kt +++ b/pitmutationmate/src/main/kotlin/com/amos/pitmutationmate/pitmutationmate/visualization/LatestPiTestReport.kt @@ -4,6 +4,7 @@ package com.amos.pitmutationmate.pitmutationmate.visualization +import com.amos.pitmutationmate.pitmutationmate.reporting.XMLParser import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBScrollPane import com.intellij.ui.table.JBTable @@ -18,12 +19,21 @@ import javax.swing.ListSelectionModel import javax.swing.table.DefaultTableCellRenderer import javax.swing.table.DefaultTableModel -class LatestPiTestReport : JPanel() { +class LatestPiTestReport( + coverageReport: XMLParser.CoverageReport = XMLParser.CoverageReport( + lineCoveragePercentage = 0, + lineCoverageTextRatio = "", + mutationCoveragePercentage = 0, + mutationCoverageTextRatio = "", + testStrengthPercentage = 0, + testStrengthTextRatio = "" + ) +) : JPanel() { init { - val lineCoverageBar = CustomProgressBar(30, "1/5") - val mutationCoverageBar = CustomProgressBar(50, "3000/30000") - val testStrengthBar = CustomProgressBar(93, "200/2000") + val lineCoverageBar = CustomProgressBar(coverageReport.lineCoveragePercentage, coverageReport.lineCoverageTextRatio) + val mutationCoverageBar = CustomProgressBar(coverageReport.mutationCoveragePercentage, coverageReport.mutationCoverageTextRatio) + val testStrengthBar = CustomProgressBar(coverageReport.testStrengthPercentage, coverageReport.testStrengthTextRatio) val data = arrayOf( arrayOf(getLabel("Class Name"), getLabel("Test.java")), diff --git a/pitmutationmate/src/test/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/GradleTaskExecutorTest.kt b/pitmutationmate/src/test/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/GradleTaskExecutorTest.kt new file mode 100644 index 00000000..d3901a67 --- /dev/null +++ b/pitmutationmate/src/test/kotlin/com/amos/pitmutationmate/pitmutationmate/execution/GradleTaskExecutorTest.kt @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2023 Lennart Heimbs + +package com.amos.pitmutationmate.pitmutationmate.execution + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +/** + * Tests for GradleTaskExecutor + * + * @see GradleTaskExecutor + */ +class GradleTaskExecutorTest { + @Mock + lateinit var systemInfo: SystemInfoProvider // Assuming SystemInfo is a dependency + + @InjectMocks + lateinit var gradleTaskExecutor: GradleTaskExecutor + + @BeforeEach + fun setup() { + MockitoAnnotations.openMocks(this) + } + + @Test + fun `test buildCommandLine for Windows`() { + `when`(systemInfo.isWindows()).thenReturn(true) + + val commandLine = gradleTaskExecutor.buildCommandLine( + null, + "clean", + "/path/to/project", + "com.example.Class", + 8080 + ) + + assertEquals(GradleTaskExecutor.WINDOWS_SHELL_EXECUTABLE, commandLine.exePath) + assertEquals(GradleTaskExecutor.WINDOWS_FIRST_PARAMETER, commandLine.parametersList.parameters[0]) + assertEquals(GradleTaskExecutor.WINDOWS_GRADLE_EXECUTABLE, commandLine.parametersList.parameters[1]) + } + + @Test + fun `test buildCommandLine for Unix`() { + `when`(systemInfo.isWindows()).thenReturn(false) + + val commandLine = gradleTaskExecutor.buildCommandLine( + null, + "clean", + "/path/to/project", + "com.example.Class", + 8080 + ) + + assertEquals(GradleTaskExecutor.UNIX_SHELL_EXECUTABLE, commandLine.exePath) + assertEquals(GradleTaskExecutor.UNIX_FIRST_PARAMETER, commandLine.parametersList.parameters[0]) + assertEquals(GradleTaskExecutor.UNIX_GRADLE_EXECUTABLE, commandLine.parametersList.parameters[1]) + } + + @Test + fun `test buildCommandLine without taskName uses default taskName`() { + `when`(systemInfo.isWindows()).thenReturn(true) + + val commandLine = gradleTaskExecutor.buildCommandLine( + null, + null, + "/path/to/project", + "com.example.Class", + 8080 + ) + + assertEquals(GradleTaskExecutor.PITEST_TASK_NAME, commandLine.parametersList.parameters[2]) + } + + @Test + fun `test buildCommandLine with taskName uses given taskName`() { + `when`(systemInfo.isWindows()).thenReturn(true) + + val taskName = "test123" + val commandLine = gradleTaskExecutor.buildCommandLine( + null, + taskName, + "/path/to/project", + "com.example.Class", + 8080 + ) + + assertEquals(taskName, commandLine.parametersList.parameters[2]) + } + + @Test + fun `test buildCommandLine without classFQDN does not add targetClass override`() { + `when`(systemInfo.isWindows()).thenReturn(true) + + val commandLine = gradleTaskExecutor.buildCommandLine( + null, + "clean", + "/path/to/project", + null, + 8080 + ) + + for (parameter in commandLine.parametersList.parameters) { + assertFalse(parameter.contains("-Dpitmutationmate.override.targetClasses")) + } + } + + @Test + fun `test buildCommandLine with classFQDN adds targetClass override`() { + `when`(systemInfo.isWindows()).thenReturn(true) + + val classFQN = "com.example.Class" + val commandLine = gradleTaskExecutor.buildCommandLine( + null, + "clean", + "/path/to/project", + classFQN, + 8080 + ) + + assertTrue( + commandLine.parametersList.parameters.contains("-Dpitmutationmate.override.targetClasses=$classFQN") + ) + } +}