diff --git a/.gitignore b/.gitignore index 524f096..44578d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,42 @@ -# Compiled class file -*.class +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ -# Log file -*.log +### IntelliJ IDEA ### +.idea/** +.run/** +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ -# BlueJ files -*.ctxt +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ -# Mobile Tools for Java (J2ME) -.mtj.tmp/ +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar +### VS Code ### +.vscode/ -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -replay_pid* +### Mac OS ### +.DS_Store +# Project exclude paths +/.intellijPlatform/ \ No newline at end of file diff --git a/README.md b/README.md index a40b5ac..5b21952 100644 --- a/README.md +++ b/README.md @@ -1 +1,20 @@ -# pit4u \ No newline at end of file +# PIT4U + +PIT4U is a Plugin for IntelliJ to help you run mutation testing using [PIT](http://pitest.org). + +## Why? + +I created this plugin because the [reference plugin](https://github.com/mjedynak/pit-idea-plugin) seems to be abandoned, +and latest version is using deprecated APIs that is causing IntelliJ to freeze and report errors when using it. + +## Roadmap + +- [x] Create MVP +- [ ] Publish MVP +- [ ] Add context-aware action +- [ ] Fix validations in `Other params` table +- [ ] Parse results and show them in the IDE directly + +## Donations + +TBD \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..c039b4c --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,77 @@ +import org.jetbrains.intellij.platform.gradle.TestFrameworkType + +plugins { + id("java") + id("org.jetbrains.intellij.platform") version "2.1.0" +} + +group = "io.github.nahuel92" + +repositories { + mavenCentral() + + intellijPlatform { + defaultRepositories() + } +} + +intellijPlatform { + pluginConfiguration { + version = providers.gradleProperty("pluginVersion") + id = "io.github.nahuel92.pit4u" + name = "PIT4U" + version = "1.0.0" + description = "Plugin that allows you to run PIT mutation tests directly from your IDE" + changeNotes = "notes" + ideaVersion { + sinceBuild.set("232") + untilBuild.set("242.*") + } + } +} + +dependencies { + implementation("org.pitest:pitest:1.17.0") + implementation("org.pitest:pitest-junit5-plugin:1.2.1") + implementation("org.pitest:pitest-command-line:1.17.0") + implementation("org.pitest:pitest-entry:1.17.0") + intellijPlatform { + intellijIdeaCommunity("2024.2.3") + bundledPlugin("com.intellij.java") + bundledPlugin("org.jetbrains.idea.maven") + bundledPlugin("com.intellij.gradle") + + pluginVerifier() + zipSigner() + instrumentationTools() + + testFramework(TestFrameworkType.Platform) + } +} + +tasks { + withType { + sourceCompatibility = "21" + targetCompatibility = "21" + } + + patchPluginXml { + sinceBuild.set("232") + untilBuild.set("242.*") + pluginVersion.set("1.0-SNAPSHOT") + } + + signPlugin { + certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) + privateKey.set(System.getenv("PRIVATE_KEY")) + password.set(System.getenv("PRIVATE_KEY_PASSWORD")) + } + + publishPlugin { + token.set(System.getenv("PUBLISH_TOKEN")) + } + + generateManifest { + version.set("1.0-SNAPSHOT") + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..24630b3 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,6 @@ +# Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib +kotlin.stdlib.default.dependency=false +# Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html +org.gradle.configuration-cache=true +# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html +org.gradle.caching=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e1adfb4 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/icons/16px pluginIcon - raw.svg b/icons/16px pluginIcon - raw.svg new file mode 100644 index 0000000..7efebfd --- /dev/null +++ b/icons/16px pluginIcon - raw.svg @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/40px pluginIcon - raw.svg b/icons/40px pluginIcon - raw.svg new file mode 100644 index 0000000..27c651c --- /dev/null +++ b/icons/40px pluginIcon - raw.svg @@ -0,0 +1,289 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..22a1bce --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,8 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "pit4u" \ No newline at end of file diff --git a/src/main/java/io/github/nahuel92/pit4u/configuration/JavaParametersCreator.java b/src/main/java/io/github/nahuel92/pit4u/configuration/JavaParametersCreator.java new file mode 100644 index 0000000..76c21bf --- /dev/null +++ b/src/main/java/io/github/nahuel92/pit4u/configuration/JavaParametersCreator.java @@ -0,0 +1,83 @@ +package io.github.nahuel92.pit4u.configuration; + +import com.intellij.execution.CantRunException; +import com.intellij.execution.configurations.JavaParameters; +import com.intellij.execution.configurations.JavaRunConfigurationModule; +import com.intellij.execution.util.JavaParametersUtil; +import com.intellij.openapi.application.PathManager; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.module.ModuleUtil; +import com.intellij.openapi.project.Project; +import io.github.nahuel92.pit4u.gui.Pit4USettingsEditor; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +class JavaParametersCreator { + private static final List PIT_LIBS = getPitLibs(); + + public static JavaParameters create(final JavaRunConfigurationModule configurationModule, + final Project project, + final Pit4USettingsEditor pit4USettingsEditor) { + setModule(configurationModule, project); + final var javaParameters = new JavaParameters(); + addPitLibraries(javaParameters); + configureModules(project, javaParameters); + javaParameters.setWorkingDirectory(configurationModule.getProject().getBasePath()); + javaParameters.setMainClass("org.pitest.mutationtest.commandline.MutationCoverageReport"); + javaParameters.getProgramParametersList().add("--targetClasses", pit4USettingsEditor.getTargetClasses()); + javaParameters.getProgramParametersList().add("--targetTests", pit4USettingsEditor.getTargetTests()); + javaParameters.getProgramParametersList().add("--sourceDirs", pit4USettingsEditor.getSourceDirs()); + javaParameters.getProgramParametersList().add("--reportDir", pit4USettingsEditor.getReportDir()); + return javaParameters; + } + + private static List getPitLibs() { + final var pluginsPath = PathManager.getPluginsPath(); + final var pit4UPath = Path.of(pluginsPath).resolve("pit4u").resolve("lib"); + try (final var path = Files.walk(pit4UPath)) { + return path.filter(e -> { + final var name = e.getFileName().toString(); + return name.startsWith("pitest-"); + }) + .map(Path::toAbsolutePath) + .map(Path::toString) + .toList(); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + private static void setModule(final JavaRunConfigurationModule configurationModule, final Project project) { + if (configurationModule.getModule() == null && project.getProjectFile() != null) { + final var module = ModuleUtil.findModuleForFile(project.getProjectFile(), project); + configurationModule.setModule(module); + } + } + + private static void addPitLibraries(final JavaParameters javaParameters) { + PIT_LIBS.forEach(e -> javaParameters.getClassPath().addFirst(e)); + if (!javaParameters.getClassPath().getPathList().contains("unit-platform-launcher")) { + javaParameters.getClassPath().addFirst( + PIT_LIBS.getFirst().substring(0, PIT_LIBS.getFirst().indexOf("/lib")) + + "/lib/junit-platform-launcher-1.9.2.jar" + ); + } + } + + private static void configureModules(final Project project, final JavaParameters javaParameters) { + for (final var module : ModuleManager.getInstance(project).getModules()) { + try { + JavaParametersUtil.configureModule(module, + javaParameters, + JavaParameters.JDK_AND_CLASSES_AND_TESTS, + null + ); + } catch (final CantRunException e) { + throw new RuntimeException(e); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/nahuel92/pit4u/configuration/Pit4UConfigurationFactory.java b/src/main/java/io/github/nahuel92/pit4u/configuration/Pit4UConfigurationFactory.java new file mode 100644 index 0000000..fd63953 --- /dev/null +++ b/src/main/java/io/github/nahuel92/pit4u/configuration/Pit4UConfigurationFactory.java @@ -0,0 +1,32 @@ +package io.github.nahuel92.pit4u.configuration; + +import com.intellij.execution.configurations.ConfigurationFactory; +import com.intellij.execution.configurations.ConfigurationType; +import com.intellij.execution.configurations.RunConfiguration; +import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +class Pit4UConfigurationFactory extends ConfigurationFactory { + public Pit4UConfigurationFactory(@NotNull final ConfigurationType type) { + super(type); + } + + @Override + @NotNull + public String getId() { + return getType().getId(); + } + + @Override + public Icon getIcon(@NotNull final RunConfiguration configuration) { + return configuration.getIcon(); + } + + @Override + @NotNull + public RunConfiguration createTemplateConfiguration(@NotNull final Project project) { + return new Pit4URunConfiguration("PIT4U Configuration", project, this); + } +} diff --git a/src/main/java/io/github/nahuel92/pit4u/configuration/Pit4UConfigurationStore.java b/src/main/java/io/github/nahuel92/pit4u/configuration/Pit4UConfigurationStore.java new file mode 100644 index 0000000..5c590ef --- /dev/null +++ b/src/main/java/io/github/nahuel92/pit4u/configuration/Pit4UConfigurationStore.java @@ -0,0 +1,25 @@ +package io.github.nahuel92.pit4u.configuration; + +import io.github.nahuel92.pit4u.gui.Pit4USettingsEditor; +import org.jdom.Element; + +class Pit4UConfigurationStore { + private static final String TARGET_CLASSES = "targetClasses"; + private static final String TARGET_TESTS = "targetTests"; + private static final String SOURCE_DIRS = "sourceDirs"; + private static final String REPORT_DIR = "reportDir"; + + public static void readExternal(final Pit4USettingsEditor pit4USettingsEditor, final Element element) { + pit4USettingsEditor.setTargetClasses(element.getAttribute(TARGET_CLASSES).getValue()); + pit4USettingsEditor.setTargetTests(element.getAttribute(TARGET_TESTS).getValue()); + pit4USettingsEditor.setSourceDirs(element.getAttribute(SOURCE_DIRS).getValue()); + pit4USettingsEditor.setReportDir(element.getAttribute(REPORT_DIR).getValue()); + } + + public static void writeExternal(final Pit4USettingsEditor pit4USettingsEditor, final Element element) { + element.setAttribute(TARGET_CLASSES, pit4USettingsEditor.getTargetClasses()); + element.setAttribute(TARGET_TESTS, pit4USettingsEditor.getTargetTests()); + element.setAttribute(SOURCE_DIRS, pit4USettingsEditor.getSourceDirs()); + element.setAttribute(REPORT_DIR, pit4USettingsEditor.getReportDir()); + } +} diff --git a/src/main/java/io/github/nahuel92/pit4u/configuration/Pit4UConfigurationType.java b/src/main/java/io/github/nahuel92/pit4u/configuration/Pit4UConfigurationType.java new file mode 100644 index 0000000..d630609 --- /dev/null +++ b/src/main/java/io/github/nahuel92/pit4u/configuration/Pit4UConfigurationType.java @@ -0,0 +1,45 @@ +package io.github.nahuel92.pit4u.configuration; + +import com.intellij.execution.configurations.ConfigurationFactory; +import com.intellij.execution.configurations.ConfigurationType; +import com.intellij.openapi.util.IconLoader; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +class Pit4UConfigurationType implements ConfigurationType { + private static final Icon ICON = IconLoader.getIcon("/pit4u.svg", Pit4UConfigurationType.class); + private static final String ID = "Pit4UConfigurationType"; + + @Override + @NotNull + @Nls(capitalization = Nls.Capitalization.Title) + public String getDisplayName() { + return "PIT4U"; + } + + @Override + @Nls(capitalization = Nls.Capitalization.Sentence) + public String getConfigurationTypeDescription() { + return "PIT runner that assess your tests strength"; + } + + @Override + @NotNull + @NonNls + public String getId() { + return ID; + } + + @Override + public Icon getIcon() { + return ICON; + } + + @Override + public ConfigurationFactory[] getConfigurationFactories() { + return new ConfigurationFactory[]{new Pit4UConfigurationFactory(this)}; + } +} diff --git a/src/main/java/io/github/nahuel92/pit4u/configuration/Pit4URunConfiguration.java b/src/main/java/io/github/nahuel92/pit4u/configuration/Pit4URunConfiguration.java new file mode 100644 index 0000000..b5a369d --- /dev/null +++ b/src/main/java/io/github/nahuel92/pit4u/configuration/Pit4URunConfiguration.java @@ -0,0 +1,130 @@ +package io.github.nahuel92.pit4u.configuration; + +import com.intellij.execution.DefaultExecutionResult; +import com.intellij.execution.ExecutionException; +import com.intellij.execution.ExecutionResult; +import com.intellij.execution.Executor; +import com.intellij.execution.JavaRunConfigurationExtensionManager; +import com.intellij.execution.configurations.ConfigurationFactory; +import com.intellij.execution.configurations.JavaCommandLineState; +import com.intellij.execution.configurations.JavaParameters; +import com.intellij.execution.configurations.JavaRunConfigurationModule; +import com.intellij.execution.configurations.ModuleBasedConfiguration; +import com.intellij.execution.configurations.RunConfiguration; +import com.intellij.execution.configurations.RunProfileState; +import com.intellij.execution.filters.TextConsoleBuilderFactory; +import com.intellij.execution.process.OSProcessHandler; +import com.intellij.execution.process.ProcessAdapter; +import com.intellij.execution.process.ProcessEvent; +import com.intellij.execution.runners.ExecutionEnvironment; +import com.intellij.execution.runners.ProgramRunner; +import com.intellij.execution.ui.ConsoleView; +import com.intellij.ide.browsers.OpenUrlHyperlinkInfo; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.options.SettingsEditor; +import com.intellij.openapi.options.SettingsEditorGroup; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.WriteExternalException; +import io.github.nahuel92.pit4u.gui.Pit4USettingsEditor; +import org.jdom.Element; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; + +public class Pit4URunConfiguration extends ModuleBasedConfiguration + implements RunConfiguration { + private final Pit4USettingsEditor pit4USettingsEditor; + + protected Pit4URunConfiguration(final String name, final Project project, final ConfigurationFactory factory) { + super(name, new JavaRunConfigurationModule(project, true), factory); + pit4USettingsEditor = new Pit4USettingsEditor(project); + } + + @Override + @NotNull + public SettingsEditor getConfigurationEditor() { + final var group = new SettingsEditorGroup(); + group.addEditor("PIT4U", pit4USettingsEditor); + JavaRunConfigurationExtensionManager.getInstance().appendEditors(this, group); + return group; + } + + @Override + public Collection getValidModules() { + return List.of(ModuleManager.getInstance(getProject()).getModules()); + } + + @Override + @Nullable + public RunProfileState getState(@NotNull final Executor executor, @NotNull final ExecutionEnvironment environment) { + final var javaCommandLineState = new JavaCommandLineState(environment) { + private ConsoleView consoleView; + + @Override + protected JavaParameters createJavaParameters() { + return JavaParametersCreator.create(getConfigurationModule(), getProject(), pit4USettingsEditor); + } + + @Override + @NotNull + protected OSProcessHandler startProcess() throws ExecutionException { + final var osProcessHandler = super.startProcess(); + osProcessHandler.addProcessListener( + new ProcessAdapter() { + @Override + public void processTerminated(@NotNull final ProcessEvent event) { + final var reportLink = "file:///" + + Path.of(pit4USettingsEditor.getReportDir()).toAbsolutePath() + + "/index.html"; + consoleView.printHyperlink( + "Report ready, click to open it in your browser", + new OpenUrlHyperlinkInfo(reportLink) + ); + } + } + ); + return osProcessHandler; + } + + @Override + @NotNull + public ExecutionResult execute(@NotNull final Executor executor, + @NotNull final ProgramRunner runner) throws ExecutionException { + final var processHandler = startProcess(); + final var console = createConsole(executor); + if (console != null) { + console.attachToProcess(processHandler); + } + this.consoleView = console; + return new DefaultExecutionResult( + console, + processHandler, + createActions(console, processHandler, executor) + ); + } + }; + + javaCommandLineState.setConsoleBuilder( + TextConsoleBuilderFactory.getInstance().createBuilder(getProject()) + ); + return javaCommandLineState; + } + + @Override + public void readExternal(@NotNull final Element element) throws InvalidDataException { + super.readExternal(element); + Pit4UConfigurationStore.readExternal(pit4USettingsEditor, element); + } + + + @Override + public void writeExternal(@NotNull final Element element) throws WriteExternalException { + super.writeExternal(element); + Pit4UConfigurationStore.writeExternal(pit4USettingsEditor, element); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/nahuel92/pit4u/gui/Pit4USettingsEditor.form b/src/main/java/io/github/nahuel92/pit4u/gui/Pit4USettingsEditor.form new file mode 100644 index 0000000..470e786 --- /dev/null +++ b/src/main/java/io/github/nahuel92/pit4u/gui/Pit4USettingsEditor.form @@ -0,0 +1,98 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/main/java/io/github/nahuel92/pit4u/gui/Pit4USettingsEditor.java b/src/main/java/io/github/nahuel92/pit4u/gui/Pit4USettingsEditor.java new file mode 100644 index 0000000..22a8973 --- /dev/null +++ b/src/main/java/io/github/nahuel92/pit4u/gui/Pit4USettingsEditor.java @@ -0,0 +1,114 @@ +package io.github.nahuel92.pit4u.gui; + +import com.intellij.icons.AllIcons; +import com.intellij.ide.util.PackageChooserDialog; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; +import com.intellij.openapi.options.SettingsEditor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.TextFieldWithBrowseButton; +import io.github.nahuel92.pit4u.configuration.Pit4URunConfiguration; +import io.github.nahuel92.pit4u.gui.table.MyDialog; +import io.github.nahuel92.pit4u.gui.table.MyTableItem; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.event.ActionListener; +import java.util.stream.Collectors; + +public class Pit4USettingsEditor extends SettingsEditor { + private JPanel jPanel; + private TextFieldWithBrowseButton targetClasses; + private TextFieldWithBrowseButton targetTests; + private TextFieldWithBrowseButton sourceDir; + private TextFieldWithBrowseButton reportDir; + private TextFieldWithBrowseButton otherParams; + + public Pit4USettingsEditor(final Project project) { + addListener(this.targetClasses, "Select Target Classes Package", project); + addListener(this.targetTests, "Select Target Tests Package", project); + addListener(this.sourceDir, "Select Source Directory"); + addListener(this.reportDir, "Select Report Directory"); + this.otherParams.setButtonIcon(AllIcons.FileTypes.Archive); + this.otherParams.addActionListener(addListener()); + } + + public String getTargetClasses() { + return targetClasses.getText(); + } + + public void setTargetClasses(final String targetClasses) { + this.targetClasses.setText(targetClasses); + } + + public String getTargetTests() { + return targetTests.getText(); + } + + public void setTargetTests(final String targetTests) { + this.targetTests.setText(targetTests); + } + + public String getSourceDirs() { + return sourceDir.getText(); + } + + public void setSourceDirs(final String sourceDirs) { + this.sourceDir.setText(sourceDirs); + } + + public String getReportDir() { + return reportDir.getText(); + } + + public void setReportDir(final String reportDir) { + this.reportDir.setText(reportDir); + } + + @Override + @NotNull + protected JComponent createEditor() { + return jPanel; + } + + @Override + protected void resetEditorFrom(@NotNull final Pit4URunConfiguration s) { + } + + @Override + protected void applyEditorTo(@NotNull final Pit4URunConfiguration s) { + } + + private void addListener(final TextFieldWithBrowseButton field, final String title, final Project project) { + field.addActionListener(e -> { + final var packageChooser = new PackageChooserDialog(title, project); + if (packageChooser.showAndGet()) { + field.setText(packageChooser.getSelectedPackage().getQualifiedName() + ".*"); + } + }); + } + + private void addListener(final TextFieldWithBrowseButton field, final String title) { + field.addBrowseFolderListener( + title, + null, + null, + FileChooserDescriptorFactory.createSingleFolderDescriptor() + ); + } + + private ActionListener addListener() { + return e -> ApplicationManager.getApplication().invokeLater( + () -> { + final var myDialog = new MyDialog(); + if (myDialog.showAndGet()) { + final var otherParams1 = myDialog.getTableItems() + .stream() + .map(MyTableItem::toString) + .collect(Collectors.joining(" ")); + this.otherParams.setText(otherParams1); + } + } + ); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/nahuel92/pit4u/gui/table/MyCellEditor.java b/src/main/java/io/github/nahuel92/pit4u/gui/table/MyCellEditor.java new file mode 100644 index 0000000..3db6431 --- /dev/null +++ b/src/main/java/io/github/nahuel92/pit4u/gui/table/MyCellEditor.java @@ -0,0 +1,54 @@ +package io.github.nahuel92.pit4u.gui.table; + +import com.intellij.openapi.ui.ComboBox; + +import javax.swing.*; +import javax.swing.table.TableCellEditor; +import java.awt.*; + +class MyCellEditor extends AbstractCellEditor implements TableCellEditor { + private static final String[] OUTPUT_FORMATS = { + "HTML", + "XML", + "CSV", + "XML,CSV", + "HTML,XML", + "HTML,CSV", + "HTML,XML,CSV" + }; + private final ComboBox comboBox; + private final JTextField textField; + private final JCheckBox checkBox; + + public MyCellEditor() { + this.comboBox = new ComboBox<>(OUTPUT_FORMATS); + this.textField = new JTextField(); + this.checkBox = new JCheckBox(); + } + + @Override + public Object getCellEditorValue() { + if (checkBox.isShowing()) { + return checkBox.isSelected(); + } + if (comboBox.isShowing()) { + return comboBox.getSelectedItem(); + } + return textField.getText(); + } + + @Override + public Component getTableCellEditorComponent(final JTable table, final Object value, + boolean isSelected, int row, int column) { + if (row < 5) { + checkBox.setSelected((Boolean) value); + return checkBox; + } + if (row == 5) { + comboBox.setSelectedItem(value); + return comboBox; + } + textField.setText(value.toString()); + return textField; + } +} diff --git a/src/main/java/io/github/nahuel92/pit4u/gui/table/MyDialog.java b/src/main/java/io/github/nahuel92/pit4u/gui/table/MyDialog.java new file mode 100644 index 0000000..e108bfe --- /dev/null +++ b/src/main/java/io/github/nahuel92/pit4u/gui/table/MyDialog.java @@ -0,0 +1,45 @@ +package io.github.nahuel92.pit4u.gui.table; + +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.ui.table.TableView; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.util.List; + +public class MyDialog extends DialogWrapper { + private final MyTableModel myTableModel; + private final TableView> table; + + public MyDialog() { + super(true); + this.myTableModel = new MyTableModel(); + this.table = new TableView<>(myTableModel); + this.table.getColumnModel() + .getColumn(1) + .setCellEditor(new MyCellEditor()); + setTitle("Pit4U - Advanced Configuration"); + setSize(700, 600); + init(); + } + + @Override + @Nullable + protected JComponent createCenterPanel() { + final var defaultButton = new JButton("Reset Defaults"); + defaultButton.addActionListener(e -> myTableModel.restoreDefaultValues()); + + final var buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + buttonPanel.add(defaultButton); + + final var panel = new JPanel(new BorderLayout()); + panel.add(table, BorderLayout.CENTER); + panel.add(buttonPanel, BorderLayout.SOUTH); + return panel; + } + + public List> getTableItems() { + return myTableModel.getUpdatedItems(); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/nahuel92/pit4u/gui/table/MyTableItem.java b/src/main/java/io/github/nahuel92/pit4u/gui/table/MyTableItem.java new file mode 100644 index 0000000..963b95b --- /dev/null +++ b/src/main/java/io/github/nahuel92/pit4u/gui/table/MyTableItem.java @@ -0,0 +1,41 @@ +package io.github.nahuel92.pit4u.gui.table; + +public class MyTableItem { + protected final V defaultParameterValue; + private final String parameterName; + protected V parameterValue; + private boolean hasBeenUpdated; + + public MyTableItem(final String parameterName, final V defaultParameterValue) { + this.parameterName = parameterName; + this.defaultParameterValue = defaultParameterValue; + this.parameterValue = defaultParameterValue; + } + + public String getParameterName() { + return parameterName; + } + + public V getParameterValue() { + return parameterValue; + } + + @SuppressWarnings("unchecked") + public void setParameterValue(final Object newValue) { + this.parameterValue = (V) newValue; + this.hasBeenUpdated = true; + } + + public void resetDefaults() { + this.parameterValue = defaultParameterValue; + } + + public boolean hasBeenUpdated() { + return hasBeenUpdated && !defaultParameterValue.equals(parameterValue); + } + + @Override + public String toString() { + return parameterName + " \"" + parameterValue + "\""; + } +} diff --git a/src/main/java/io/github/nahuel92/pit4u/gui/table/MyTableModel.java b/src/main/java/io/github/nahuel92/pit4u/gui/table/MyTableModel.java new file mode 100644 index 0000000..cac472a --- /dev/null +++ b/src/main/java/io/github/nahuel92/pit4u/gui/table/MyTableModel.java @@ -0,0 +1,119 @@ +package io.github.nahuel92.pit4u.gui.table; + +import com.intellij.util.ui.ColumnInfo; +import com.intellij.util.ui.ListTableModel; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +class MyTableModel extends ListTableModel> { + private static final List> ADVANCED_ARGS = getAdvancedArgs(); + + public MyTableModel() { + super( + new ColumnInfo[]{new MyTableModel.ParameterColumnInfo(), new MyTableModel.ValueColumnInfo()}, + ADVANCED_ARGS + ); + } + + private static List> getAdvancedArgs() { + return List.of( + new MyTableItem<>("--detectInlinedCode", true), + new MyTableItem<>("--includeLaunchClasspath", true), + new MyTableItem<>("--failWhenNoMutations", true), + new MyTableItem<>("--timestampedReports", false), + new MyTableItem<>("--verbose", false), + + new MyTableItem<>("--outputFormats", "HTML"), + + new MyTableItem<>("--timeoutFactor", "1.25"), + + new MyTableItem<>("--timeoutConst", "4000"), + new MyTableItem<>("--threads", "1"), + new MyTableItem<>("--coverageThreshold", "0"), + new MyTableItem<>("--mutationThreshold", "0"), + + new MyTableItem<>("--avoidCallsTo", "java.util.logging,org.apache.log4j,org.slf4j,org.apache.commons.logging"), + new MyTableItem<>("--classPath", ""), + new MyTableItem<>("--includedGroups", ""), + new MyTableItem<>("--excludedGroups", ""), + new MyTableItem<>("--excludedMethods", ""), + new MyTableItem<>("--excludedTests", ""), + new MyTableItem<>("--historyInputLocation", ""), + new MyTableItem<>("--historyOutputLocation", ""), + new MyTableItem<>("--jvmArgs", ""), + new MyTableItem<>("--jvmPath", ""), + new MyTableItem<>("--mutableCodePaths", ""), + new MyTableItem<>("--mutators", "") + ); + } + + @Override + public int getRowCount() { + return this.getItems().size(); + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (columnIndex == 0) { + return this.getItems().get(rowIndex).getParameterName(); + } + if (columnIndex == 1) { + return this.getItems().get(rowIndex).getParameterValue(); + } + throw new IndexOutOfBoundsException(); + } + + @Override + public void setValueAt(final Object aValue, int rowIndex, int columnIndex) { + final var existingItem = ADVANCED_ARGS.get(rowIndex); + existingItem.setParameterValue(aValue); + fireTableCellUpdated(rowIndex, columnIndex); + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + return columnIndex == 1; + } + + public List> getUpdatedItems() { + return getItems() + .stream() + .filter(MyTableItem::hasBeenUpdated) + .toList(); + } + + public void restoreDefaultValues() { + getItems().forEach(MyTableItem::resetDefaults); + fireTableDataChanged(); + } + + private static class ParameterColumnInfo extends ColumnInfo, String> { + public ParameterColumnInfo() { + super("Parameter"); + } + + @Override + @Nullable + public String valueOf(final MyTableItem o) { + return o.getParameterName(); + } + } + + private static class ValueColumnInfo extends ColumnInfo, Object> { + public ValueColumnInfo() { + super("Value"); + } + + @Override + @Nullable + public Object valueOf(final MyTableItem o) { + return o.getParameterValue(); + } + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000..20a7320 --- /dev/null +++ b/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,36 @@ + + + + io.github.nahuel92.pit4u + + + Pit4U + + + YourCompany + + + + most HTML tags may be used + ]]> + + com.intellij.modules.java + org.jetbrains.idea.maven + com.intellij.gradle + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/pluginIcon.svg b/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 0000000..730f6ae --- /dev/null +++ b/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/src/main/resources/pit4u.svg b/src/main/resources/pit4u.svg new file mode 100644 index 0000000..c35da98 --- /dev/null +++ b/src/main/resources/pit4u.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + +