diff --git a/.github/workflows/gradle-build.yml b/.github/workflows/gradle-build.yml new file mode 100644 index 0000000..bafd14f --- /dev/null +++ b/.github/workflows/gradle-build.yml @@ -0,0 +1,24 @@ +name: build +on: + push: + branches: + - "master" +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'adopt' + + - name: Build with Gradle + run: ./gradlew build + + - name: Test with Gradle + run: ./gradlew test + + - name: Pitest with Gradle + run: ./gradlew pitest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..49e9065 --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# These are some examples of commonly ignored file patterns. +# You should customize this list as applicable to your project. +# Learn more about .gitignore: +# https://www.atlassian.com/git/tutorials/saving-changes/gitignore + +# Node artifact files +node_modules/ +dist/ + +# Compiled Java class files +*.class + +# Compiled Python bytecode +*.py[cod] + +# Log files +*.log + +# Package files +*.jar + +# Maven +target/ +dist/ + +# JetBrains IDE +.idea/ + +# Unit test reports +TEST*.xml + +# Generated by MacOS +.DS_Store + +# Generated by Windows +Thumbs.db + +# Applications +*.app +*.exe +*.war + +# Large media files +*.mp4 +*.tiff +*.avi +*.flv +*.mov +*.wmv + +# gradle +build/ +.gradle/ +!gradle/wrapper/gradle-wrapper.jar diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..0ce36bf --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,66 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and +You must cause any modified files to carry prominent notices stating that You changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + + +Copyright [2022] [valantic CEC Schweiz AG] + +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 + + http://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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f825e9a --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +# Mutation Tester +This Plugin enables mutation testing in your IDE based on pitest.org. + +![build workflow](https://github.com/valantic/mutation-tester/actions/workflows/gradle-build.yml/badge.svg) +[![Software License](https://img.shields.io/badge/license-Apache%202-orange.svg?style=flat-square)](LICENSE.md) +[![HitCount](http://hits.dwyl.com/valantic/mutation-tester.svg?style=flat-square)](http://hits.dwyl.com/valantic/mutation-tester) + +This package is developed by [valantic CEC Schweiz](https://www.valantic.com/). + + +## About +Mutation Testing is also called Fault-based testing strategy as it involves creating a fault in the program and it is a type of White Box Testing which is mainly used for Unit Testing. +When the application code changes, it should produce different results and cause the unit tests to fail. If a unit test does not fail in this situation, it may indicate an issue with the test suite. + +This kind of testing helps you to produce better testing results, not only based on line coverage but also on mutation coverage. If less of your code can mutate, you can be sure to have a higher code quality. + +This Plugin will help you as developer to run mutation tests easy and fast in your IDE without wasting time in configurations. + + +## Installation +You can download the plugin from our [github(https://github.com/valantic/mutation-tester)]. +Next open your IntelliJ and install the plugin from disk, as seen below. +It is recommended to restart your IDE. +![](src/main/resources/sample/install-disk.png) + + +### IntelliJ Marketplace +The Plugin will be published to the Intellij Marketplace. This should be avaible in the store after our beta testing (est. 2022-04). + + +## Run the plugin +You can simply create a new RunConfigurations with Mutation Tester +![](src/main/resources/sample/create-configuration.png) + +This Plugin will save you a lot of time. You don't need to create everytime new configurations by hand to test your new unit test. +With this tool you can automatically create configuration for your given test, as seen below by just executing the test in the run context of the unit test. + +![](src/main/resources/sample/run-in-class.png) + +![](src/main/resources/sample/right-click-in-class.png) + +You can also select packages in your test directory and test all classes in this package with all the tests in same named package. + +![](src/main/resources/sample/run-context-menu.png) + +If you still need to adjust the configuration you can do this easily. Each run will create a RunConfiguration you can simply modify. + +![](src/main/resources/sample/run-as.png) + + +## Configuration +If you need to adjust the defualt configuration, you can just go into your RunConfiguration and adjust the default settings. + +![](src/main/resources/sample/advanced-settings.png) + +In the tab settings the values of targetTests, targetClasses, reportDir, sourceDirs and mutators can not be empty. Otherwise the tests will not run. in the advanced tab you can easily modify default behaviour. + +![](src/main/resources/sample/settings-1.png) + +![](src/main/resources/sample/settings-2.png) + +You can get more informations about the configuration fields [here](https://pitest.org/quickstart/commandline/). + + +## Copyright and Licensing Information +See [LICENSE.md](LICENSE.md) for the complete License. + +Copyright [2022] [valantic CEC Schweiz AG] + +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 + +``` +http://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. + + +## Contact Information +For any issue with plugin please use the our issueboard on github. + +For any information about our company go to valantic.com. + + +## Credit and Acknowledgments +This Plugin is based on the commandline tools from pitest.org. +So special credits go out to henry@pitest.org for developing such an amazing tool. + +Also the Icon (mutation.png) is based on an icon from flaticon by Freepik. +Gen Icons erstellt von Freepik - Flaticon + + +## Roadmap +- [ ] refactoring +- [ ] raise test coverage +- [ ] testing badges (test and pitest) +- [ ] fix beta issues +- [ ] add to IntelliJ Marketplace +- [ ] cross module testing +- [ ] caching diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..7562544 --- /dev/null +++ b/build.gradle @@ -0,0 +1,70 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +plugins { + id "java" + id "org.jetbrains.intellij" version "$intellijPluginVersion" + id 'info.solidsoft.pitest' version "$pitestGradleVersion" +} + +group "com.valantic" +version "$pluginVersion" + +sourceCompatibility = "$javaSourceCompatibility" + +repositories { + mavenCentral() +} + +// See https://github.com/JetBrains/gradle-intellij-plugin/ +intellij { + version = "$intellijVersion" + plugins = ["JUnit", "com.intellij.java"] +} + +pitest { + timestampedReports = false +} + +dependencies { + // https://mvnrepository.com/artifact/org.pitest/pitest + implementation group: "org.pitest", name: "pitest", version: "$pitestVersion" + + // https://mvnrepository.com/artifact/org.pitest/pitest-entry + implementation group: "org.pitest", name: "pitest-entry", version: "$pitestVersion" + + // https://mvnrepository.com/artifact/org.pitest/pitest-command-line + implementation group: "org.pitest", name: "pitest-command-line", version: "$pitestVersion" + + // https://mvnrepository.com/artifact/org.mockito/mockito-core + testImplementation group: "org.mockito", name: "mockito-core", version: "$jUnitVersion" + + // https://mvnrepository.com/artifact/org.mockito/mockito-inline + testImplementation group: "org.mockito", name: "mockito-inline", version: "$jUnitVersion" +} + +buildSearchableOptions { + enabled = false +} + +patchPluginXml { + version = project.version +} + +publishPlugin { + channels = ["beta"] +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..d77f3be --- /dev/null +++ b/gradle.properties @@ -0,0 +1,7 @@ +pluginVersion=0.1.0 +javaSourceCompatibility=1.8 +intellijPluginVersion=1.3.0 +intellijVersion=2021.2.3 +pitestVersion=1.7.4 +pitestGradleVersion=1.7.0 +jUnitVersion=4.1.0 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 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..41dfb87 --- /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-7.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..744e882 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or 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 UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$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 "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /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/settings.gradle b/settings.gradle new file mode 100644 index 0000000..33157e5 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,18 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +rootProject.name = 'Mutation Tester' diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/action/MutationAction.java b/src/main/java/com/valantic/intellij/plugin/mutation/action/MutationAction.java new file mode 100644 index 0000000..9911f2d --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/action/MutationAction.java @@ -0,0 +1,242 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.action; + +import com.intellij.execution.ExecutionManager; +import com.intellij.execution.RunManager; +import com.intellij.execution.executors.DefaultRunExecutor; +import com.intellij.execution.runners.ExecutionEnvironmentBuilder; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.LangDataKeys; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiJavaFile; +import com.intellij.psi.impl.file.PsiJavaDirectoryImpl; +import com.intellij.psi.util.PsiTreeUtil; +import com.valantic.intellij.plugin.mutation.configuration.MutationConfiguration; +import com.valantic.intellij.plugin.mutation.configuration.MutationConfigurationFactory; +import com.valantic.intellij.plugin.mutation.configuration.MutationConfigurationType; +import com.valantic.intellij.plugin.mutation.configuration.option.MutationConfigurationOptions; +import com.valantic.intellij.plugin.mutation.constants.MutationConstants; +import com.valantic.intellij.plugin.mutation.icons.Icons; +import com.valantic.intellij.plugin.mutation.localization.Messages; +import com.valantic.intellij.plugin.mutation.services.Services; +import com.valantic.intellij.plugin.mutation.services.impl.PsiService; +import com.valantic.intellij.plugin.mutation.services.impl.UtilService; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; +import java.util.stream.Stream; + + +/** + * created by fabian.huesig on 2022-02-01 + */ +public class MutationAction extends AnAction { + + private static final String PSI_DATA_DIR = "psi.Element.array"; + private static final String PSI_DATA_FILE = "psi.File"; + private static final String PROJECT_VIEW_POPUP = "ProjectViewPopup"; + + private String targetClass; + private String targetTest; + + private PsiService psiService = Services.getService(PsiService.class); + private UtilService utilService = Services.getService(UtilService.class); + + public MutationAction() { + super(Messages.getMessage("action.com.valantic.intellij.plugin.mutation.action.MutationAction.text"), + Messages.getMessage("action.com.valantic.intellij.plugin.mutation.action.MutationAction.description"), Icons.MUTATIONx16); + } + + @Override + public void update(@NotNull AnActionEvent event) { + if (PROJECT_VIEW_POPUP.equals(event.getPlace())) { + utilService.allowSlowOperations(() -> { + final PsiElement psiElement = event.getData(LangDataKeys.PSI_ELEMENT); + final PsiClass psiClass = Optional.ofNullable(psiElement) + .filter(PsiJavaDirectoryImpl.class::isInstance) + .map(PsiJavaDirectoryImpl.class::cast) + .map(element -> PsiTreeUtil.findChildOfType(element, PsiClass.class)) + .orElseGet(() -> getPsiClass(psiElement)); + Optional.of(event) + .map(AnActionEvent::getPresentation) + .ifPresent(presentation -> presentation.setEnabled(psiClass != null && psiService.isTestClass(psiClass))); + }); + } + } + + private PsiClass getPsiClass(final PsiElement psiElement) { + return Optional.ofNullable(psiElement) + .filter(PsiClass.class::isInstance) + .map(PsiClass.class::cast) + .orElse(null); + } + + public static MutationAction[] getSingletonActions() { + return getSingletonActions(null, null); + } + + public static MutationAction[] getSingletonActions(final String targetClass, final String targetTest) { + MutationAction action = new MutationAction(); + Optional.ofNullable(targetClass) + .ifPresent(action::setTargetClass); + Optional.ofNullable(targetTest) + .ifPresent(action::setTargetTest); + return new MutationAction[]{action}; + } + + private void setEventTargetTestIfNotExists(final AnActionEvent event) { + final DataContext dataContext = event.getDataContext(); + final String selectedTargetTest = Optional.ofNullable(dataContext) + .map(this::getSelectedTestDir) + .map(psiService::resolvePackageNameForDir) + .map(packageName -> packageName + MutationConstants.PACKAGE_WILDCARD_SUFFIX) + .orElseGet(() -> getSelectedFile(dataContext)); + if (StringUtils.isNotEmpty(selectedTargetTest)) { + this.targetTest = selectedTargetTest; + if (selectedTargetTest.endsWith(MutationConstants.WILDCARD_SUFFIX)) { + this.targetClass = selectedTargetTest; + } + } + } + + private String getSelectedFile(final DataContext dataContext) { + return Optional.of(PSI_DATA_FILE) + .map(dataContext::getData) + .filter(PsiJavaFile.class::isInstance) + .map(PsiJavaFile.class::cast) + .map(psiJavaFile -> psiJavaFile.getPackageName() + MutationConstants.PACKAGE_SEPARATOR + psiJavaFile.getName().split(MutationConstants.JAVA_FILE_SUFFIX_REGEX)[0]) + .orElse(StringUtils.EMPTY); + } + + private PsiJavaDirectoryImpl getSelectedTestDir(final DataContext dataContext) { + return Optional.of(PSI_DATA_DIR) + .map(dataContext::getData) + .filter(PsiElement[].class::isInstance) + .map(PsiElement[].class::cast) + .map(Arrays::stream) + .flatMap(Stream::findAny) + .filter(PsiJavaDirectoryImpl.class::isInstance) + .map(PsiJavaDirectoryImpl.class::cast) + .orElse(null); + } + + @Override + public void actionPerformed(final @NotNull AnActionEvent event) { + final Project project = event.getProject(); + setEventTargetTestIfNotExists(event); + final MutationConfiguration mutationConfiguration = getOrCreateMutationConfiguration(project); + Optional.of(mutationConfiguration) + .map(MutationConfiguration::getPitRunConfigurationOptions) + .ifPresent(options -> { + if (StringUtils.isEmpty(options.getSourceDirs())) { + options.setSourceDirs(project.getBasePath()); + } + Optional.ofNullable(this.targetTest) + .ifPresent(options::setTargetTests); + Optional.ofNullable(this.targetClass) + .ifPresent(options::setTargetClasses); + psiService.updateModule(project, options.getTargetTests(), mutationConfiguration.getConfigurationModule()); + }); + Optional.ofNullable(DefaultRunExecutor.getRunExecutorInstance()) + .map(executor -> ExecutionEnvironmentBuilder.createOrNull(executor, mutationConfiguration)) + .ifPresent(builder -> ExecutionManager.getInstance(project).restartRunProfile(builder.build())); + } + + /** + * get or create pitRunConfiguration. Search for Existing mutationConfiguration + * and if not exists create a new with default values. + * + * @param project + * @return existing or new mutationConfiguration + */ + private MutationConfiguration getOrCreateMutationConfiguration(final Project project) { + final Optional optionalPitRunConfiguration = getOptionalPitConfiguration(project); + if (optionalPitRunConfiguration.isPresent()) { + return optionalPitRunConfiguration.get(); + } + return createPitRunConfiguration(project); + } + + private Optional getOptionalPitConfiguration(final Project project) { + final String targetTestClassName = psiService.getClassName(this.targetTest); + return Optional.ofNullable(project) + .map(RunManager::getInstance) + .map(RunManager::getAllConfigurationsList) + .orElseGet(Collections::emptyList) + .stream() + .filter(MutationConfiguration.class::isInstance) + .map(MutationConfiguration.class::cast) + .filter(mutationConfiguration -> Optional.of(mutationConfiguration) + .map(MutationConfiguration::getPitRunConfigurationOptions) + .map(MutationConfigurationOptions::getTargetTests) + .filter(StringUtils::isNotEmpty) + .filter(targetTestClassName::equals) + .isPresent()) + .findFirst(); + } + + private MutationConfiguration createPitRunConfiguration(final Project project) { + final MutationConfigurationFactory factory = getOrCreateConfigurationFactory(project); + final MutationConfiguration mutationConfiguration = new MutationConfiguration(project, factory, getMutationConfigurationName()); + final RunManager runManager = RunManager.getInstance(project); + Optional.ofNullable(runManager.createConfiguration(mutationConfiguration, factory)) + .ifPresent(runManager::addConfiguration); + return mutationConfiguration; + } + + private String getMutationConfigurationName() { + return Optional.ofNullable(this.targetTest) + .filter(targetTestClass -> targetTestClass.endsWith(MutationConstants.PACKAGE_SEPARATOR + MutationConstants.WILDCARD_SUFFIX)) + .map(targetTestClass -> targetTestClass.split(MutationConstants.WILDCARD_SUFFIX_REGEX)[0]) + .orElseGet(() -> psiService.getClassName(this.targetTest)); + } + + private MutationConfigurationFactory getOrCreateConfigurationFactory(final Project project) { + return Optional.ofNullable(project) + .map(RunManager::getInstance) + .map(RunManager::getAllConfigurationsList) + .orElseGet(Collections::emptyList) + .stream() + .filter(MutationConfiguration.class::isInstance) + .map(MutationConfiguration.class::cast) + .map(MutationConfiguration::getFactory) + .filter(MutationConfigurationFactory.class::isInstance) + .map(MutationConfigurationFactory.class::cast) + .findFirst() + .orElseGet(() -> { + final MutationConfigurationType configurationType = new MutationConfigurationType(); + return new MutationConfigurationFactory(configurationType); + }); + } + + public void setTargetClass(String targetClass) { + this.targetClass = targetClass; + } + + public void setTargetTest(String targetTest) { + this.targetTest = targetTest; + } +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/commandline/MutationCommandLineState.java b/src/main/java/com/valantic/intellij/plugin/mutation/commandline/MutationCommandLineState.java new file mode 100644 index 0000000..8272ff2 --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/commandline/MutationCommandLineState.java @@ -0,0 +1,233 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.commandline; + +import com.intellij.execution.DefaultExecutionResult; +import com.intellij.execution.ExecutionException; +import com.intellij.execution.ExecutionResult; +import com.intellij.execution.Executor; +import com.intellij.execution.configurations.JavaCommandLineState; +import com.intellij.execution.configurations.JavaParameters; +import com.intellij.execution.configurations.JavaRunConfigurationModule; +import com.intellij.execution.configurations.ParametersList; +import com.intellij.execution.process.ProcessAdapter; +import com.intellij.execution.process.ProcessEvent; +import com.intellij.execution.process.ProcessHandler; +import com.intellij.execution.runners.ExecutionEnvironment; +import com.intellij.execution.runners.ProgramRunner; +import com.intellij.execution.ui.ConsoleView; +import com.intellij.execution.util.JavaParametersUtil; +import com.intellij.ide.browsers.OpenUrlHyperlinkInfo; +import com.intellij.util.PathUtil; +import com.valantic.intellij.plugin.mutation.action.MutationAction; +import com.valantic.intellij.plugin.mutation.configuration.MutationConfiguration; +import com.valantic.intellij.plugin.mutation.configuration.option.MutationConfigurationOptions; +import com.valantic.intellij.plugin.mutation.constants.MutationConstants; +import com.valantic.intellij.plugin.mutation.localization.Messages; +import com.valantic.intellij.plugin.mutation.services.Services; +import com.valantic.intellij.plugin.mutation.services.impl.ModuleService; +import com.valantic.intellij.plugin.mutation.services.impl.PsiService; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.junit.runners.JUnit4; +import org.pitest.boot.HotSwapAgent; +import org.pitest.mutationtest.commandline.MutationCoverageReport; +import org.pitest.mutationtest.config.PluginServices; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Optional; + + +/** + * created by fabian.huesig on 2022-02-01 + */ +public class MutationCommandLineState extends JavaCommandLineState { + + private static final String DATE_FORMAT = "yyyy-MM-dd-HH-mm-ss"; + private static final String INDEX_FILE = "index.html"; + private static final String MAIN_CLASS = "org.pitest.mutationtest.commandline.MutationCoverageReport"; + + private String creationTime; + private JavaRunConfigurationModule module; + private MutationConfigurationOptions options; + + private PsiService psiService = Services.getService(PsiService.class); + private ModuleService moduleService = Services.getService(ModuleService.class); + + public MutationCommandLineState(final ExecutionEnvironment environment) { + super(environment); + Optional.of(environment) + .map(ExecutionEnvironment::getRunProfile) + .filter(MutationConfiguration.class::isInstance) + .map(MutationConfiguration.class::cast) + .ifPresent(mutationConfiguration -> { + this.creationTime = new SimpleDateFormat(DATE_FORMAT).format(new Date()); + this.module = getConfigurationModule(mutationConfiguration); + this.options = mutationConfiguration.getPitRunConfigurationOptions(); + psiService.updateModule(mutationConfiguration.getProject(), this.options.getTargetTests(), this.module); + }); + } + + private JavaRunConfigurationModule getConfigurationModule(MutationConfiguration mutationConfiguration) { + return Optional.ofNullable(mutationConfiguration) + .map(MutationConfiguration::getConfigurationModule) + .orElseGet(moduleService::createConfigurationModule); + } + + @NotNull + @Override + public ExecutionResult execute(@NotNull final Executor executor, @NotNull final ProgramRunner runner) throws ExecutionException { + final ConsoleView consoleView = createConsole(executor); + final ProcessHandler processHandler = startProcess(); + consoleView.attachToProcess(processHandler); + processHandler.addProcessListener(new ProcessAdapter() { + @Override + public void processWillTerminate(final ProcessEvent event, final boolean willBeDestroyed) { + Optional.of(options) + .map(MutationConfigurationOptions::getReportDir) + .map(MutationCommandLineState.this::getReport) + .map(reportPath -> { + if (Boolean.valueOf(options.getTimestampedReports())) { + return reportPath; + } + return reportPath + MutationConstants.PATH_SEPARATOR + INDEX_FILE; + }) + .map(OpenUrlHyperlinkInfo::new) + .ifPresent(openUrlHyperlinkInfo -> consoleView.printHyperlink(Messages.getMessage("report.hyperlink.text"), openUrlHyperlinkInfo)); + } + }); + return new DefaultExecutionResult(consoleView, processHandler, MutationAction.getSingletonActions()); + } + + /** + * Setting JavaParameters to run org.pitest.mutationtest.commandline.MutationCoverageReport. + * Parameters will be set from @see {@link MutationConfigurationOptions} + * + * @return + * @throws ExecutionException + */ + @Override + protected JavaParameters createJavaParameters() throws ExecutionException { + final JavaParameters javaParameters = new JavaParameters(); + javaParameters.setMainClass(MAIN_CLASS); + JavaParametersUtil.configureModule(module, javaParameters, JavaParameters.JDK_AND_CLASSES_AND_TESTS, null); + Optional.of(javaParameters) + .map(JavaParameters::getProgramParametersList) + .ifPresent(this::populateParameterList); + Optional.ofNullable(javaParameters) + .map(JavaParameters::getClassPath) + .ifPresent(pathsList -> { + pathsList.addFirst(PathUtil.getJarPathForClass(HotSwapAgent.class)); + pathsList.addFirst(PathUtil.getJarPathForClass(MutationCoverageReport.class)); + pathsList.addFirst(PathUtil.getJarPathForClass(PluginServices.class)); + pathsList.addFirst(PathUtil.getJarPathForClass(JUnit4.class)); + }); + return javaParameters; + } + + + /** + * Path of report. Uses configured reportDir and creates a subdirectory based on timestamp. + * + * @return reportDir + timestampFolder + */ + public String getReport(final String reportDir) { + return Optional.of(reportDir) + .map(path -> path.replaceFirst(MutationConstants.TRAILING_SLASH_REGEX, StringUtils.EMPTY)) + .map(StringBuilder::new) + .map(stringBuilder -> stringBuilder.append(MutationConstants.PATH_SEPARATOR)) + .map(stringBuilder -> stringBuilder.append(creationTime)) + .map(StringBuilder::toString) + .orElse(reportDir); + } + + private void populateParameterList(final ParametersList parametersList) { + parametersList.add("--targetClasses", options.getTargetClasses()); + parametersList.add("--targetTests", options.getTargetTests()); + parametersList.add("--reportDir", getReport(options.getReportDir())); + parametersList.add("--sourceDirs", options.getSourceDirs()); + parametersList.add("--mutators", options.getMutators()); + parametersList.add("--timeoutConst", options.getTimeoutConst()); + parametersList.add("--outputFormats", options.getOutputFormats()); + parametersList.add(String.format("--timestampedReports=%s", options.getTimestampedReports())); + parametersList.add(String.format("--includeLaunchClasspath=%s", options.getIncludeLaunchClasspath())); + parametersList.add(String.format("--verbose=%s", options.getVerbose())); + parametersList.add(String.format("--failWhenNoMutations=%s", options.getFailWhenNoMutations())); + if (StringUtils.isNotEmpty(options.getDependencyDistance())) { + parametersList.add("--dependencyDistance", options.getDependencyDistance()); + } + if (StringUtils.isNotEmpty(options.getThreads())) { + parametersList.add("--threads", options.getThreads()); + } + if (StringUtils.isNotEmpty(options.getExcludedMethods())) { + parametersList.add("--excludedMethods", options.getExcludedMethods()); + } + if (StringUtils.isNotEmpty(options.getExcludedClasses())) { + parametersList.add("--excludedClasses", options.getExcludedClasses()); + } + if (StringUtils.isNotEmpty(options.getExcludedTests())) { + parametersList.add("--excludedTests", options.getExcludedTests()); + } + if (StringUtils.isNotEmpty(options.getAvoidCallsTo())) { + parametersList.add("--avoidCallsTo", options.getAvoidCallsTo()); + } + if (StringUtils.isNotEmpty(options.getTimeoutFactor())) { + parametersList.add("--timeoutFactor", options.getTimeoutFactor()); + } + if (StringUtils.isNotEmpty(options.getMaxMutationsPerClass())) { + parametersList.add("--maxMutationsPerClass", options.getMaxMutationsPerClass()); + } + if (StringUtils.isNotEmpty(options.getJvmArgs())) { + parametersList.add("--jvmArgs", options.getJvmArgs()); + } + if (StringUtils.isNotEmpty(options.getJvmPath())) { + parametersList.add("--jvmPath", options.getJvmPath()); + } + if (StringUtils.isNotEmpty(options.getClassPath())) { + parametersList.add("--classPath", options.getClassPath()); + } + if (StringUtils.isNotEmpty(options.getMutableCodePaths())) { + parametersList.add("--mutableCodePaths", options.getMutableCodePaths()); + } + if (StringUtils.isNotEmpty(options.getTestPlugin())) { + parametersList.add("--testPlugin", options.getTestPlugin()); + } + if (StringUtils.isNotEmpty(options.getIncludedGroups())) { + parametersList.add("--includedGroups", options.getIncludedGroups()); + } + if (StringUtils.isNotEmpty(options.getExcludedGroups())) { + parametersList.add("--excludedGroups", options.getExcludedGroups()); + } + if (StringUtils.isNotEmpty(options.getDetectInlinedCode())) { + parametersList.add("--detectInlinedCode", options.getDetectInlinedCode()); + } + if (StringUtils.isNotEmpty(options.getMutationThreshold())) { + parametersList.add("--mutationThreshold", options.getMutationThreshold()); + } + if (StringUtils.isNotEmpty(options.getCoverageThreshold())) { + parametersList.add("--coverageThreshold", options.getCoverageThreshold()); + } + if (StringUtils.isNotEmpty(options.getHistoryInputLocation())) { + parametersList.add("--historyInputLocation", options.getHistoryInputLocation()); + } + if (StringUtils.isNotEmpty(options.getHistoryOutputLocation())) { + parametersList.add("--historyOutputLocation", options.getHistoryOutputLocation()); + } + } +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/configuration/MutationConfiguration.java b/src/main/java/com/valantic/intellij/plugin/mutation/configuration/MutationConfiguration.java new file mode 100644 index 0000000..18810a1 --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/configuration/MutationConfiguration.java @@ -0,0 +1,201 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.configuration; + +import com.intellij.execution.Executor; +import com.intellij.execution.JavaRunConfigurationBase; +import com.intellij.execution.ShortenCommandLine; +import com.intellij.execution.configurations.ConfigurationFactory; +import com.intellij.execution.configurations.JavaRunConfigurationModule; +import com.intellij.execution.configurations.ModuleBasedConfigurationOptions; +import com.intellij.execution.configurations.RunConfiguration; +import com.intellij.execution.configurations.RunProfileState; +import com.intellij.execution.runners.ExecutionEnvironment; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.options.SettingsEditor; +import com.intellij.openapi.project.Project; +import com.valantic.intellij.plugin.mutation.commandline.MutationCommandLineState; +import com.valantic.intellij.plugin.mutation.configuration.option.MutationConfigurationOptions; +import com.valantic.intellij.plugin.mutation.editor.MutationSettingsEditor; +import com.valantic.intellij.plugin.mutation.services.Services; +import com.valantic.intellij.plugin.mutation.services.impl.ModuleService; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Map; +import java.util.Optional; + + +/** + * created by fabian.huesig on 2022-02-01 + */ +public class MutationConfiguration extends JavaRunConfigurationBase { + private String vmParameters; + private boolean alternativeJrePathEnabled; + private String alternativeJrePath; + private String programParameters; + private String workingDir; + private Map envs; + private boolean passParentEnvs; + private ShortenCommandLine shortenCommandLine; + + private ModuleService moduleService = Services.getService(ModuleService.class); + + public MutationConfiguration(final Project project, final ConfigurationFactory factory, final String name) { + super(name, new JavaRunConfigurationModule(project, Boolean.TRUE), factory); + } + + public MutationConfigurationOptions getPitRunConfigurationOptions() { + return Optional.of(getOptions()) + .filter(MutationConfigurationOptions.class::isInstance) + .map(MutationConfigurationOptions.class::cast) + .orElse(null); + } + + @NotNull + @Override + protected Class getDefaultOptionsClass() { + return MutationConfigurationOptions.class; + } + + @NotNull + @Override + public SettingsEditor getConfigurationEditor() { + return new MutationSettingsEditor(); + } + + @Nullable + @Override + public RunProfileState getState(@NotNull Executor executor, @NotNull ExecutionEnvironment executionEnvironment) { + return new MutationCommandLineState(executionEnvironment); + } + + @Override + public Collection getValidModules() { + return moduleService.getModules(getProject()); + } + + @Override + public String getVMParameters() { + return vmParameters; + } + + @Override + public void setVMParameters(@Nullable String vmParameters) { + this.vmParameters = vmParameters; + } + + @Override + public boolean isAlternativeJrePathEnabled() { + return alternativeJrePathEnabled; + } + + @Override + public void setAlternativeJrePathEnabled(boolean alternativeJrePathEnabled) { + this.alternativeJrePathEnabled = alternativeJrePathEnabled; + } + + @Override + public @Nullable String getAlternativeJrePath() { + return alternativeJrePath; + } + + @Override + public void setAlternativeJrePath(@Nullable String alternativeJrePath) { + this.alternativeJrePath = alternativeJrePath; + } + + @Override + public @Nullable String getProgramParameters() { + return programParameters; + } + + @Override + public void setProgramParameters(@Nullable String programParameters) { + this.programParameters = programParameters; + } + + @Override + public @Nullable String getWorkingDirectory() { + return workingDir; + } + + @Override + public void setWorkingDirectory(@Nullable String workingDir) { + this.workingDir = workingDir; + } + + @Override + public @NotNull Map getEnvs() { + return envs; + } + + @Override + public void setEnvs(@NotNull Map envs) { + this.envs = envs; + } + + @Override + public boolean isPassParentEnvs() { + return passParentEnvs; + } + + @Override + public void setPassParentEnvs(boolean passParentEnvs) { + this.passParentEnvs = passParentEnvs; + } + + @Override + public @Nullable ShortenCommandLine getShortenCommandLine() { + return shortenCommandLine; + } + + @Override + public void setShortenCommandLine(@Nullable ShortenCommandLine shortenCommandLine) { + this.shortenCommandLine = shortenCommandLine; + } + + /** + * not used + */ + @Override + public void checkConfiguration() { + // empty + } + + /** + * not used + * + * @return null + */ + @Override + public @Nullable String getRunClass() { + return null; + } + + /** + * not used + * + * @return null + */ + @Override + public @Nullable String getPackage() { + return null; + } +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/configuration/MutationConfigurationFactory.java b/src/main/java/com/valantic/intellij/plugin/mutation/configuration/MutationConfigurationFactory.java new file mode 100644 index 0000000..8c353bf --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/configuration/MutationConfigurationFactory.java @@ -0,0 +1,57 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.configuration; + +import com.valantic.intellij.plugin.mutation.configuration.option.MutationConfigurationOptions; +import com.valantic.intellij.plugin.mutation.localization.Messages; +import com.intellij.execution.configurations.ConfigurationFactory; +import com.intellij.execution.configurations.ConfigurationType; +import com.intellij.execution.configurations.RunConfiguration; +import com.intellij.openapi.components.BaseState; +import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + + +/** + * created by fabian.huesig on 2022-02-01 + */ +public class MutationConfigurationFactory extends ConfigurationFactory { + + public MutationConfigurationFactory(ConfigurationType type) { + super(type); + } + + @Override + public @NotNull String getId() { + return MutationConfigurationType.ID; + } + + @NotNull + @Override + public RunConfiguration createTemplateConfiguration(@NotNull Project project) { + return new MutationConfiguration(project, this, Messages.getMessage("plugin.name")); + } + + @Nullable + @Override + public Class getOptionsClass() { + return MutationConfigurationOptions.class; + } + +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/configuration/MutationConfigurationType.java b/src/main/java/com/valantic/intellij/plugin/mutation/configuration/MutationConfigurationType.java new file mode 100644 index 0000000..d3cbe11 --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/configuration/MutationConfigurationType.java @@ -0,0 +1,63 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.configuration; + +import com.valantic.intellij.plugin.mutation.icons.Icons; +import com.valantic.intellij.plugin.mutation.localization.Messages; +import com.intellij.execution.configurations.ConfigurationFactory; +import com.intellij.execution.configurations.ConfigurationType; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + + +/** + * created by fabian.huesig on 2022-02-01 + */ +public class MutationConfigurationType implements ConfigurationType { + + public static final String ID = "MutationConfiguration"; + + @NotNull + @Override + public String getDisplayName() { + return Messages.getMessage("plugin.name"); + } + + @Override + public String getConfigurationTypeDescription() { + return Messages.getMessage("plugin.description"); + } + + @Override + public Icon getIcon() { + return Icons.MUTATIONx16; + } + + @NotNull + @Override + public String getId() { + return ID; + } + + @Override + public ConfigurationFactory[] getConfigurationFactories() { + return new ConfigurationFactory[]{new MutationConfigurationFactory(this)}; + } + +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/configuration/option/MutationConfigurationOptions.java b/src/main/java/com/valantic/intellij/plugin/mutation/configuration/option/MutationConfigurationOptions.java new file mode 100644 index 0000000..238521e --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/configuration/option/MutationConfigurationOptions.java @@ -0,0 +1,505 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.configuration.option; + +import com.intellij.execution.configurations.ModuleBasedConfigurationOptions; +import com.intellij.openapi.components.StoredProperty; +import org.apache.commons.lang3.StringUtils; + + +/** + * + * created by fabian.huesig on 2022-02-01 + *

+ * Pit mutation run configuration options. + * Based on @see Pit Commandline Quick Start + */ +public class MutationConfigurationOptions extends ModuleBasedConfigurationOptions { + private static final String DEFAULT_VALUE_TIMEOUT_CONST = "4000"; + private static final String DEFAULT_VALUE_OUTPUT_FORMATS = "HTML"; + private static final String DEFAULT_VALUE_MUTATORS = "DEFAULTS"; + private static final String DEFAULT_VALUE_TIMESTAMPED_REPORTS = "false"; + private static final String DEFAULT_REPORT_DIR = System.getProperty("java.io.tmpdir"); + private static final String DEFAULT_VALUE_INCLUDE_LAUNCH_CLASSPATH = "true"; + private static final String DEFAULT_VALUE_VERBOSE = "false"; + private static final String DEFAULT_VALUE_TIMEOUT_FACTOR = "1.25"; + private static final String DEFAULT_VALUE_MAX_MUTATIONS_PER_CLASS = "0"; + private static final String DEFAULT_VALUE_FAIL_WHEN_NO_MUTATIONS = "true"; + private static final String DEFAULT_VALUE_TEST_PLUGIN = "junit"; + private static final String DEFAULT_VALUE_EXCLUDED_CLASSES = "*Test*"; + + /** + * The classes to be mutated. This is expressed as a comma separated list of globs. + *

+ * For example + * «com.mycompany.*» or «com.mycompany.package.*, com.mycompany.packageB.Foo, com.partner.*» + */ + private final StoredProperty targetClasses = string(StringUtils.EMPTY).provideDelegate(this, "targetClasses"); + + /** + * A comma separated list of globs can be supplied to this parameter to limit the tests available to be run. + * If this parameter is not supplied then any test fixture that matched targetClasses may be used, it is + * however recommended that this parameter is always explicitly set. + *

+ * This parameter can be used to point PIT to a top level suite or suites. Custom suites such as + * + * @see ClassPathSuite are supported. Tests found via these suites can also be limited by the distance filter. + */ + private final StoredProperty targetTests = string(StringUtils.EMPTY).provideDelegate(this, "targetTests"); + + /** + * Output directory for the reports + */ + private final StoredProperty reportDir = string(DEFAULT_REPORT_DIR).provideDelegate(this, "reportDir"); + + /** + * Source directories + */ + private final StoredProperty sourceDirs = string(StringUtils.EMPTY).provideDelegate(this, "sourceDirs"); + + /** + * List of mutations as group or comma separated list of. + * + * @see mutators + */ + private final StoredProperty mutators = string(DEFAULT_VALUE_MUTATORS).provideDelegate(this, "mutators"); + + /** + * Constant amount of additional time to allow a test to run for (after the application of the timeoutFactor) before considering it to be stuck in an infinite loop. + * Defaults to 4000 + */ + private final StoredProperty timeoutConst = string(DEFAULT_VALUE_TIMEOUT_CONST).provideDelegate(this, "timeoutConst"); + + /** + * Comma separated list of formats in which to write mutation results as the mutations are analysed. Supported formats are HTML, XML, CSV. + * Defaults to HTML. + */ + private final StoredProperty outputFormats = string(DEFAULT_VALUE_OUTPUT_FORMATS).provideDelegate(this, "outputFormats"); + + /** + * PIT will create a date and time stamped folder for its output each time it is run. + * This can can make automation difficult, so the behaviour is by default suppressed by passing --timestampedReports=false. + */ + private final StoredProperty timestampedReports = string(DEFAULT_VALUE_TIMESTAMPED_REPORTS).provideDelegate(this, "timestampedReports"); + + /** + * Indicates if the PIT should try to mutate classes on the classpath with which it was launched. If not supplied this flag defaults to true. + * If set to false only classes found on the paths specified by the –classPath option will be considered. + */ + private final StoredProperty includeLaunchClasspath = string(DEFAULT_VALUE_INCLUDE_LAUNCH_CLASSPATH).provideDelegate(this, "includeLaunchClasspath"); + + /** + * PIT can optionally apply an additional filter to the supplied tests, such that only tests a certain distance from a mutated class will be considered for running. e.g A test that directly calls + * a method on a mutated class has a distance of 0 , a test that calls a method on a class that uses the mutee as an implementation detail has a distance of 1 etc. + * This filter will not work for tests that utilise classes via interfaces, reflection or other methods where the dependencies between classes cannot be determined from the byte code. + * The distance filter is particularly useful when performing a targeted mutation test of a subset of classes within a large project as it avoids the overheads of calculating the times and + * coverage of tests that cannot exercise the mutees. + */ + private final StoredProperty dependencyDistance = string(StringUtils.EMPTY).provideDelegate(this, "dependencyDistance"); + + /** + * The number of threads to use when mutation testing. + */ + private final StoredProperty threads = string(StringUtils.EMPTY).provideDelegate(this, "threads"); + + /** + * List of globs to match against method names. Methods matching the globs will be excluded from mutation. + */ + private final StoredProperty excludedMethods = string(StringUtils.EMPTY).provideDelegate(this, "excludedMethods"); + + /** + * List of globs to match against class names. Matching classes will be excluded from mutation. + * Prior to release 1.3.0 tests matching this filter were also excluded from being run. + * From 1.3.0 onwards tests are excluded with the excludedTests parameter. + */ + private final StoredProperty excludedClasses = string(DEFAULT_VALUE_EXCLUDED_CLASSES).provideDelegate(this, "excludedClasses"); + + /** + * List of globs to match against test class names. + * Matching tests will not be run (note if a test suite includes an excluded class, then it will “leak” back in). + */ + private final StoredProperty excludedTests = string(StringUtils.EMPTY).provideDelegate(this, "excludedTests"); + + /** + * List of packages and classes which are to be considered outside the scope of mutation. Any lines of code containing calls to these classes will not be mutated. + * If a list is not explicitly supplied then PIT will default to a list of common logging packages as follows: + * - java.util.logging + * - org.apache.log4j + * - org.slf4j + * - org.apache.commons.logging + * If the feature FLOGCALL is disabled, this parameter is ignored and logging calls are also mutated. + */ + private final StoredProperty avoidCallsTo = string(StringUtils.EMPTY).provideDelegate(this, "avoidCallsTo"); + + /** + * Output verbose logging. Defaults to off/false. + */ + private final StoredProperty verbose = string(DEFAULT_VALUE_VERBOSE).provideDelegate(this, "verbose"); + /** + * A factor to apply to the normal runtime of a test when considering if it is stuck in an infinite loop. + * Defaults to 1.25 + */ + private final StoredProperty timeoutFactor = string(DEFAULT_VALUE_TIMEOUT_FACTOR).provideDelegate(this, "timeoutFactor"); + + /** + * The maximum number of mutations to create per class. Use 0 or -ve number to set no limit. + */ + private final StoredProperty maxMutationsPerClass = string(DEFAULT_VALUE_MAX_MUTATIONS_PER_CLASS).provideDelegate(this, "maxMutationsPerClass"); + + /** + * Argument string to use when PIT launches child processes. + * This is most commonly used to increase the amount of memory available to the process, but may be used to pass any valid JVM argument. + */ + private final StoredProperty jvmArgs = string(StringUtils.EMPTY).provideDelegate(this, "jvmArgs"); + + /** + * The path to tha java executable to be used to launch test with. + * If none is supplied defaults to the one pointed to by JAVA_HOME. + */ + private final StoredProperty jvmPath = string(StringUtils.EMPTY).provideDelegate(this, "jvmPath"); + + /** + * Whether to throw an error when no mutations found. + * Defaults to true + */ + private final StoredProperty failWhenNoMutations = string(DEFAULT_VALUE_FAIL_WHEN_NO_MUTATIONS).provideDelegate(this, "failWhenNoMutations"); + + /** + * Comma separated list (yes comma separated - this is admittedly a bit weird for a classpath) of additional classpath entries to use when looking for tests and mutable code. + * These will be used in addition to the classpath with which PIT is launched. + */ + private final StoredProperty classPath = string(StringUtils.EMPTY).provideDelegate(this, "classPath"); + + /** + * List of classpaths which should be considered to contain mutable code. + * If your build maintains separate output directories for tests and production classes this parameter should be set to your code output directory in order to avoid mutating test helper classes + * etc. + */ + private final StoredProperty mutableCodePaths = string(StringUtils.EMPTY).provideDelegate(this, "mutableCodePaths"); + + /** + * Plugin to use to run tests. Defaults to junit. + */ + private final StoredProperty testPlugin = string(DEFAULT_VALUE_TEST_PLUGIN).provideDelegate(this, "testPlugin"); + + /** + * Comma separated list of TestNG groups/JUnit categories to include in mutation analysis. Note that only class level categories are supported. + */ + private final StoredProperty includedGroups = string(StringUtils.EMPTY).provideDelegate(this, "includedGroups"); + + /** + * Comma separated list of TestNG groups/JUnit categories to exclude from mutation analysis. Note that only class level categories are supported. + */ + private final StoredProperty excludedGroups = string(StringUtils.EMPTY).provideDelegate(this, "excludedGroups"); + + /** + * Enabled by default since 0.29. + * Flag to indicate if PIT should attempt to detect the inlined code generated by the java compiler in order to implement finally blocks. Each copy of the inlined code would normally be mutated + * separately, resulting in multiple identical looking mutations. When inlined code detection is enabled PIT will attempt to spot inlined code and create only a single mutation that mutates all + * affected instructions simultaneously. + * The algorithm cannot easily distinguish between inlined copies of code, and genuine duplicate instructions on the same line within a finally block. + * In the case of any doubt PIT will act cautiously and assume that the code is not inlined. + * This will be detected as two separate inlined instructions: + * + * finally { + * int++; + * int++; + * } + * + * But this will look confusing so PIT will assume no in-lining is taking place. + * + * finally { + * int++; int++; + * } + * + * This sort of pattern might not be common with integer addition, but things like string concatenation are likely to produce multiple similar instructions on the same line. + */ + private final StoredProperty detectInlinedCode = string(StringUtils.EMPTY).provideDelegate(this, "detectInlinedCode"); + + /** + * Mutation score threshold below which the build will fail. This is an integer percent (0-100) that represents the fraction of killed mutations out of all mutations. + * Please bear in mind that your build may contain equivalent mutations. Careful thought must therefore be given when selecting a threshold. + */ + private final StoredProperty mutationThreshold = string(StringUtils.EMPTY).provideDelegate(this, "mutationThreshold"); + + /** + * Line coverage threshold below which the build will fail. This is an integer percent (0-100) that represents the fraction of the project covered by the tests. + */ + private final StoredProperty coverageThreshold = string(StringUtils.EMPTY).provideDelegate(this, "coverageThreshold"); + + /** + * Line coverage threshold below which the build will fail. This is an integer percent (0-100) that represents the fraction of the project covered by the tests. + */ + private final StoredProperty historyInputLocation = string(StringUtils.EMPTY).provideDelegate(this, "historyInputLocation"); + + /** + * Path to write history information for incremental analysis. May be the same as historyInputLocation. + */ + private final StoredProperty historyOutputLocation = string(StringUtils.EMPTY).provideDelegate(this, "historyOutputLocation"); + + + // getter & setter + + public String getTargetClasses() { + return targetClasses.getValue(this); + } + + public void setTargetClasses(String targetClasses) { + this.targetClasses.setValue(this, targetClasses); + } + + public String getTargetTests() { + return targetTests.getValue(this); + } + + public void setTargetTests(String targetTests) { + this.targetTests.setValue(this, targetTests); + } + + public String getReportDir() { + return reportDir.getValue(this); + } + + public void setReportDir(String reportDir) { + this.reportDir.setValue(this, reportDir); + } + + public String getSourceDirs() { + return sourceDirs.getValue(this); + } + + public void setSourceDirs(String sourceDirs) { + this.sourceDirs.setValue(this, sourceDirs); + } + + public String getMutators() { + return mutators.getValue(this); + } + + public void setMutators(String mutators) { + this.mutators.setValue(this, mutators); + } + + public String getTimeoutConst() { + return String.valueOf(timeoutConst.getValue(this)); + } + + public void setTimeoutConst(String timeoutConst) { + this.timeoutConst.setValue(this, timeoutConst); + } + + public String getOutputFormats() { + return outputFormats.getValue(this); + } + + public void setOutputFormats(String outputFormats) { + this.outputFormats.setValue(this, outputFormats); + } + + public String getTimestampedReports() { + return String.valueOf(timestampedReports.getValue(this)); + } + + public void setTimestampedReports(String timestampedReports) { + this.timestampedReports.setValue(this, timestampedReports); + } + + public String getIncludeLaunchClasspath() { + return includeLaunchClasspath.getValue(this); + } + + public void setIncludeLaunchClasspath(String includeLaunchClasspath) { + this.includeLaunchClasspath.setValue(this, includeLaunchClasspath); + } + + public String getDependencyDistance() { + return dependencyDistance.getValue(this); + } + + public void setDependencyDistance(String dependencyDistance) { + this.dependencyDistance.setValue(this, dependencyDistance); + } + + public String getThreads() { + return threads.getValue(this); + } + + public void setThreads(String threads) { + this.threads.setValue(this, threads); + } + + public String getExcludedMethods() { + return excludedMethods.getValue(this); + } + + public void setExcludedMethods(String excludedMethods) { + this.excludedMethods.setValue(this, excludedMethods); + } + + public String getExcludedClasses() { + return excludedClasses.getValue(this); + } + + public void setExcludedClasses(String excludedClasses) { + this.excludedClasses.setValue(this, excludedClasses); + } + + public String getExcludedTests() { + return excludedTests.getValue(this); + } + + public void setExcludedTests(String excludedTests) { + this.excludedTests.setValue(this, excludedTests); + } + + public String getAvoidCallsTo() { + return avoidCallsTo.getValue(this); + } + + public void setAvoidCallsTo(String avoidCallsTo) { + this.avoidCallsTo.setValue(this, avoidCallsTo); + } + + public String getVerbose() { + return verbose.getValue(this); + } + + public void setVerbose(String verbose) { + this.verbose.setValue(this, verbose); + } + + public String getTimeoutFactor() { + return timeoutFactor.getValue(this); + } + + public void setTimeoutFactor(String timeoutFactor) { + this.timeoutFactor.setValue(this, timeoutFactor); + } + + public String getMaxMutationsPerClass() { + return maxMutationsPerClass.getValue(this); + } + + public void setMaxMutationsPerClass(String maxMutationsPerClass) { + this.maxMutationsPerClass.setValue(this, maxMutationsPerClass); + } + + public String getJvmArgs() { + return jvmArgs.getValue(this); + } + + public void setJvmArgs(String jvmArgs) { + this.jvmArgs.setValue(this, jvmArgs); + } + + public String getJvmPath() { + return jvmPath.getValue(this); + } + + public void setJvmPath(String jvmPath) { + this.jvmPath.setValue(this, jvmPath); + } + + public String getFailWhenNoMutations() { + return failWhenNoMutations.getValue(this); + } + + public void setFailWhenNoMutations(String failWhenNoMutations) { + this.failWhenNoMutations.setValue(this, failWhenNoMutations); + } + + public String getClassPath() { + return classPath.getValue(this); + } + + public void setClassPath(String classPath) { + this.classPath.setValue(this, classPath); + } + + public String getMutableCodePaths() { + return mutableCodePaths.getValue(this); + } + + public void setMutableCodePaths(String mutableCodePaths) { + this.mutableCodePaths.setValue(this, mutableCodePaths); + } + + public String getTestPlugin() { + return testPlugin.getValue(this); + } + + public void setTestPlugin(String testPlugin) { + this.testPlugin.setValue(this, testPlugin); + } + + public String getIncludedGroups() { + return includedGroups.getValue(this); + } + + public void setIncludedGroups(String includedGroups) { + this.includedGroups.setValue(this, includedGroups); + } + + public String getExcludedGroups() { + return excludedGroups.getValue(this); + } + + public void setExcludedGroups(String excludedGroups) { + this.excludedGroups.setValue(this, excludedGroups); + } + + public String getDetectInlinedCode() { + return detectInlinedCode.getValue(this); + } + + public void setDetectInlinedCode(String detectInlinedCode) { + this.detectInlinedCode.setValue(this, detectInlinedCode); + } + + public String getMutationThreshold() { + return mutationThreshold.getValue(this); + } + + public void setMutationThreshold(String mutationThreshold) { + this.mutationThreshold.setValue(this, mutationThreshold); + } + + public String getCoverageThreshold() { + return coverageThreshold.getValue(this); + } + + public void setCoverageThreshold(String coverageThreshold) { + this.coverageThreshold.setValue(this, coverageThreshold); + } + + public String getHistoryInputLocation() { + return historyInputLocation.getValue(this); + } + + public void setHistoryInputLocation(String historyInputLocation) { + this.historyInputLocation.setValue(this, historyInputLocation); + } + + public String getHistoryOutputLocation() { + return historyOutputLocation.getValue(this); + } + + public void setHistoryOutputLocation(String historyOutputLocation) { + this.historyOutputLocation.setValue(this, historyOutputLocation); + } +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/constants/MutationConstants.java b/src/main/java/com/valantic/intellij/plugin/mutation/constants/MutationConstants.java new file mode 100644 index 0000000..85756a1 --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/constants/MutationConstants.java @@ -0,0 +1,34 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.constants; + +/** + * created by fabian.huesig on 2022-02-01 + */ +public interface MutationConstants { + + String PACKAGE_SEPARATOR = "."; + String PATH_SEPARATOR = "/"; + String TRAILING_SLASH_REGEX = "/*$"; + String JAVA_FILE_SUFFIX_REGEX = "\\.java"; + String WILDCARD_SUFFIX_REGEX = "\\.\\*"; + String PACKAGE_WILDCARD_SUFFIX = ".*"; + String TEST_CLASS_SUFFIX = "Test"; + String WILDCARD_SUFFIX = "*"; + +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/editor/MutationSettingsEditor.form b/src/main/java/com/valantic/intellij/plugin/mutation/editor/MutationSettingsEditor.form new file mode 100644 index 0000000..85378df --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/editor/MutationSettingsEditor.form @@ -0,0 +1,501 @@ + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/editor/MutationSettingsEditor.java b/src/main/java/com/valantic/intellij/plugin/mutation/editor/MutationSettingsEditor.java new file mode 100644 index 0000000..1db29c9 --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/editor/MutationSettingsEditor.java @@ -0,0 +1,348 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.editor; + +import java.util.Optional; + +import javax.swing.JComponent; +import javax.swing.JPanel; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +import com.valantic.intellij.plugin.mutation.configuration.MutationConfiguration; +import com.valantic.intellij.plugin.mutation.configuration.option.MutationConfigurationOptions; +import com.valantic.intellij.plugin.mutation.enums.Mutations; +import com.intellij.openapi.fileChooser.FileChooserDescriptor; +import com.intellij.openapi.options.SettingsEditor; +import com.intellij.openapi.ui.ComboBox; +import com.intellij.openapi.ui.LabeledComponent; +import com.intellij.openapi.ui.TextFieldWithBrowseButton; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.EditorTextField; +import com.intellij.ui.InsertPathAction; + + +/** + * created by fabian.huesig on 2022-02-01 + */ +public class MutationSettingsEditor extends SettingsEditor +{ + private JPanel jPanel; + private LabeledComponent targetClasses; + private LabeledComponent targetTests; + private LabeledComponent reportDir; + private LabeledComponent sourceDirs; + private LabeledComponent> mutators; + + // advanced + private LabeledComponent timeoutConst; + private LabeledComponent outputFormats; + private LabeledComponent> timestampedReports; + private LabeledComponent> includeLaunchClasspath; + private LabeledComponent dependencyDistance; + private LabeledComponent threads; + private LabeledComponent excludedMethods; + private LabeledComponent excludedClasses; + private LabeledComponent excludedTests; + private LabeledComponent avoidCallsTo; + private LabeledComponent> verbose; + private LabeledComponent timeoutFactor; + private LabeledComponent maxMutationsPerClass; + private LabeledComponent jvmArgs; + private LabeledComponent jvmPath; + private LabeledComponent> failWhenNoMutations; + private LabeledComponent classPath; + private LabeledComponent mutableCodePaths; + private LabeledComponent testPlugin; + private LabeledComponent includedGroups; + private LabeledComponent excludedGroups; + private LabeledComponent detectInlinedCode; + private LabeledComponent mutationThreshold; + private LabeledComponent coverageThreshold; + private LabeledComponent historyInputLocation; + private LabeledComponent historyOutputLocation; + + @Override + protected void resetEditorFrom(MutationConfiguration mutationConfiguration) + { + Optional.of(mutationConfiguration) + .map(MutationConfiguration::getPitRunConfigurationOptions) + .ifPresent(this::resetFields); + } + + @Override + protected void applyEditorTo(@NotNull MutationConfiguration mutationConfiguration) + { + Optional.of(mutationConfiguration) + .map(MutationConfiguration::getPitRunConfigurationOptions) + .ifPresent(this::applyValuesToOptions); + } + + @NotNull + @Override + protected JComponent createEditor() + { + return jPanel; + } + + /** + * called by the form to create the neccessary fields and components + */ + private void createUIComponents() + { + targetClasses = new LabeledComponent<>(); + targetTests = new LabeledComponent<>(); + reportDir = new LabeledComponent<>(); + sourceDirs = new LabeledComponent<>(); + mutators = new LabeledComponent<>(); + + // advanced + timeoutConst = new LabeledComponent<>(); + outputFormats = new LabeledComponent<>(); + timestampedReports = new LabeledComponent<>(); + includeLaunchClasspath = new LabeledComponent<>(); + dependencyDistance = new LabeledComponent<>(); + threads = new LabeledComponent<>(); + excludedMethods = new LabeledComponent<>(); + excludedClasses = new LabeledComponent<>(); + excludedTests = new LabeledComponent<>(); + avoidCallsTo = new LabeledComponent<>(); + verbose = new LabeledComponent<>(); + timeoutFactor = new LabeledComponent<>(); + maxMutationsPerClass = new LabeledComponent<>(); + jvmArgs = new LabeledComponent<>(); + jvmPath = new LabeledComponent<>(); + failWhenNoMutations = new LabeledComponent<>(); + classPath = new LabeledComponent<>(); + mutableCodePaths = new LabeledComponent<>(); + testPlugin = new LabeledComponent<>(); + includedGroups = new LabeledComponent<>(); + excludedGroups = new LabeledComponent<>(); + detectInlinedCode = new LabeledComponent<>(); + mutationThreshold = new LabeledComponent<>(); + coverageThreshold = new LabeledComponent<>(); + historyInputLocation = new LabeledComponent<>(); + historyOutputLocation = new LabeledComponent<>(); + + setUIComponents(); + } + + /** + * called by createUIComponents to set the components + */ + private void setUIComponents() + { + targetClasses.setComponent(new EditorTextField()); + targetTests.setComponent(new EditorTextField()); + reportDir.setComponent(new TextFieldWithBrowseButton()); + sourceDirs.setComponent(new TextFieldWithBrowseButton()); + mutators.setComponent(new ComboBox<>()); + + // advanced + timeoutConst.setComponent(new EditorTextField()); + outputFormats.setComponent(new EditorTextField()); + timestampedReports.setComponent(new ComboBox<>()); + includeLaunchClasspath.setComponent(new ComboBox<>()); + dependencyDistance.setComponent(new EditorTextField()); + threads.setComponent(new EditorTextField()); + excludedMethods.setComponent(new EditorTextField()); + excludedClasses.setComponent(new EditorTextField()); + excludedTests.setComponent(new EditorTextField()); + avoidCallsTo.setComponent(new EditorTextField()); + verbose.setComponent(new ComboBox<>()); + timeoutFactor.setComponent(new EditorTextField()); + maxMutationsPerClass.setComponent(new EditorTextField()); + jvmArgs.setComponent(new EditorTextField()); + jvmPath.setComponent(new EditorTextField()); + failWhenNoMutations.setComponent(new ComboBox<>()); + classPath.setComponent(new EditorTextField()); + mutableCodePaths.setComponent(new EditorTextField()); + testPlugin.setComponent(new EditorTextField()); + includedGroups.setComponent(new EditorTextField()); + excludedGroups.setComponent(new EditorTextField()); + detectInlinedCode.setComponent(new EditorTextField()); + mutationThreshold.setComponent(new EditorTextField()); + coverageThreshold.setComponent(new EditorTextField()); + historyInputLocation.setComponent(new EditorTextField()); + historyOutputLocation.setComponent(new EditorTextField()); + } + + /** + * applies the values submitted by the form to the options + * + * @param options + */ + private void applyValuesToOptions(final MutationConfigurationOptions options) + { + options.setTargetClasses(targetClasses.getComponent().getText()); + options.setTargetTests(targetTests.getComponent().getText()); + options.setReportDir(reportDir.getComponent().getText()); + options.setSourceDirs(sourceDirs.getComponent().getText()); + options.setMutators(mutators.getComponent().getSelectedItem().toString()); + + // advanced + options.setTimeoutConst(timeoutConst.getComponent().getText()); + options.setOutputFormats(outputFormats.getComponent().getText()); + options.setTimestampedReports(timestampedReports.getComponent().getSelectedItem().toString()); + options.setIncludeLaunchClasspath(includeLaunchClasspath.getComponent().getSelectedItem().toString()); + options.setDependencyDistance(dependencyDistance.getComponent().getText()); + options.setThreads(threads.getComponent().getText()); + options.setExcludedMethods(excludedMethods.getComponent().getText()); + options.setExcludedClasses(excludedClasses.getComponent().getText()); + options.setExcludedTests(excludedTests.getComponent().getText()); + options.setAvoidCallsTo(avoidCallsTo.getComponent().getText()); + options.setVerbose(verbose.getComponent().getSelectedItem().toString()); + options.setTimeoutFactor(timeoutFactor.getComponent().getText()); + options.setMaxMutationsPerClass(maxMutationsPerClass.getComponent().getText()); + options.setJvmArgs(jvmArgs.getComponent().getText()); + options.setJvmPath(jvmPath.getComponent().getText()); + options.setFailWhenNoMutations(failWhenNoMutations.getComponent().getSelectedItem().toString()); + options.setClassPath(classPath.getComponent().getText()); + options.setMutableCodePaths(mutableCodePaths.getComponent().getText()); + options.setTestPlugin(testPlugin.getComponent().getText()); + options.setIncludedGroups(includedGroups.getComponent().getText()); + options.setExcludedGroups(excludedGroups.getComponent().getText()); + options.setDetectInlinedCode(detectInlinedCode.getComponent().getText()); + options.setMutationThreshold(mutationThreshold.getComponent().getText()); + options.setCoverageThreshold(coverageThreshold.getComponent().getText()); + options.setHistoryInputLocation(historyInputLocation.getComponent().getText()); + options.setHistoryOutputLocation(historyOutputLocation.getComponent().getText()); + } + + /** + * reset the editor fields to the values from the provided options. + * + * @param options + */ + private void resetFields(final MutationConfigurationOptions options) + { + resetTextFieldWithBrowseButton(reportDir, options.getReportDir(), null); + resetTextFieldWithBrowseButton(sourceDirs, options.getSourceDirs(), null); + resetTextField(targetClasses, options.getTargetClasses()); + resetTextField(targetTests, options.getTargetTests()); + resetTextField(timeoutConst, options.getTimeoutConst()); + resetTextField(outputFormats, options.getOutputFormats()); + resetTextField(dependencyDistance, options.getDependencyDistance()); + resetTextField(threads, options.getThreads()); + resetTextField(excludedMethods, options.getExcludedMethods()); + resetTextField(excludedClasses, options.getExcludedClasses()); + resetTextField(excludedTests, options.getExcludedTests()); + resetTextField(avoidCallsTo, options.getAvoidCallsTo()); + resetTextField(timeoutFactor, options.getTimeoutFactor()); + resetTextField(maxMutationsPerClass, options.getMaxMutationsPerClass()); + resetTextField(jvmArgs, options.getJvmArgs()); + resetTextField(jvmPath, options.getJvmPath()); + resetTextField(classPath, options.getClassPath()); + resetTextField(mutableCodePaths, options.getMutableCodePaths()); + resetTextField(testPlugin, options.getTestPlugin()); + resetTextField(includedGroups, options.getIncludedGroups()); + resetTextField(excludedGroups, options.getExcludedGroups()); + resetTextField(detectInlinedCode, options.getDetectInlinedCode()); + resetTextField(mutationThreshold, options.getMutationThreshold()); + resetTextField(coverageThreshold, options.getCoverageThreshold()); + resetTextField(historyInputLocation, options.getHistoryInputLocation()); + resetTextField(historyOutputLocation, options.getHistoryOutputLocation()); + resetBooleanComboBox(timestampedReports, options.getTimestampedReports()); + resetBooleanComboBox(includeLaunchClasspath, options.getIncludeLaunchClasspath()); + resetBooleanComboBox(verbose, options.getVerbose()); + resetBooleanComboBox(failWhenNoMutations, options.getFailWhenNoMutations()); + Optional.of(mutators).map(LabeledComponent::getComponent).ifPresent(component -> { + component.setEditable(Boolean.TRUE); + component.setSelectedItem(options.getMutators()); + component.addItem(Mutations.DEFAULTS.getValue()); + component.addItem(Mutations.ALL.getValue()); + component.addItem(Mutations.STRONGER.getValue()); + component.addItem(Mutations.OLD_DEFAULTS.getValue()); + }); + } + + /** + * reset the editor combobox fields to the values from the provided options. Is only applicable for boolean values + * + * @param comboBoxField + * @param value + */ + private void resetBooleanComboBox(final LabeledComponent> comboBoxField, final String value) + { + Optional.of(comboBoxField).map(LabeledComponent::getComponent).ifPresent(component -> { + component.addItem(Boolean.FALSE.toString()); + component.addItem(Boolean.TRUE.toString()); + component.setSelectedItem(value); + }); + } + + /** + * reset the editor text field with browse button to the values from the provided options. + * + * @param textFieldWithBrowseButton + * @param text + * @param allowedSuffix + */ + private void resetTextFieldWithBrowseButton(final LabeledComponent textFieldWithBrowseButton, final String text, final String allowedSuffix) + { + Optional.of(textFieldWithBrowseButton).map(LabeledComponent::getComponent).ifPresent(component -> { + component.setText(text); + addPathListener(component, allowedSuffix); + }); + } + + /** + * reset the editor text fields to the values from the provided options. + * + * @param textField + * @param text + */ + private void resetTextField(final LabeledComponent textField, final String text) + { + Optional.of(textField).map(LabeledComponent::getComponent).ifPresent(component -> { + component.setText(text); + }); + } + + /** + * adds path listener to textFieldWithBrowseButton. + * If allowed suffix is not null, the path listener allows access to files. + * Otherwise only directories are allowed. + * + * @param textFieldWithBrowseButton + * @param allowedSuffix + */ + private void addPathListener(final TextFieldWithBrowseButton textFieldWithBrowseButton, final String allowedSuffix) + { + FileChooserDescriptor fileChooserDescriptor; + if (StringUtils.isNotEmpty(allowedSuffix)) + { + fileChooserDescriptor = new FileChooserDescriptor(Boolean.TRUE, Boolean.TRUE, Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, Boolean.FALSE) + { + @Override + public boolean isFileSelectable(VirtualFile file) + { + return file.getName().endsWith(allowedSuffix); + } + }; + } + else + { + fileChooserDescriptor = new FileChooserDescriptor(Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, Boolean.FALSE); + } + textFieldWithBrowseButton.addBrowseFolderListener(null, null, null, fileChooserDescriptor); + InsertPathAction.addTo(textFieldWithBrowseButton.getTextField(), fileChooserDescriptor); + } + +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/enums/Mutations.java b/src/main/java/com/valantic/intellij/plugin/mutation/enums/Mutations.java new file mode 100644 index 0000000..5b218eb --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/enums/Mutations.java @@ -0,0 +1,38 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.enums; + +/** + * created by fabian.huesig on 2022-02-01 + */ +public enum Mutations +{ + DEFAULTS("DEFAULTS"), ALL("ALL"), STRONGER("STRONGER"), OLD_DEFAULTS("OLD_DEFAULTS"); + + private String value; + + Mutations(String value) + { + this.value = value; + } + + public String getValue() + { + return this.value; + } +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/icons/Icons.java b/src/main/java/com/valantic/intellij/plugin/mutation/icons/Icons.java new file mode 100644 index 0000000..43dd582 --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/icons/Icons.java @@ -0,0 +1,48 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.icons; + +import javax.swing.Icon; +import javax.swing.ImageIcon; +import java.util.Optional; + + +/** + * created by fabian.huesig on 2022-02-01 + */ +public interface Icons { + + Icon MUTATION = createPNGImageIcon("/icons/mutation/mutation.png"); + Icon MUTATIONx40 = createPNGImageIcon("/icons/mutation/mutationx40.png"); + Icon MUTATIONx16 = createPNGImageIcon("/icons/mutation/mutationx16.png"); + Icon MUTATIONx13 = createPNGImageIcon("/icons/mutation/mutationx13.png"); + Icon MUTATIONx12 = createPNGImageIcon("/icons/mutation/mutationx12.png"); + + Icon MUTATION_DISABLED = createPNGImageIcon("/icons/mutation/disabled/mutation-disabled.png"); + Icon MUTATION_DISABLEDx40 = createPNGImageIcon("/icons/mutation/disabled/mutation-disabledx40.png"); + Icon MUTATION_DISABLEDx16 = createPNGImageIcon("/icons/mutation/disabled/mutation-disabledx16.png"); + Icon MUTATION_DISABLEDx13 = createPNGImageIcon("/icons/mutation/disabled/mutation-disabledx13.png"); + Icon MUTATION_DISABLEDx12 = createPNGImageIcon("/icons/mutation/disabled/mutation-disabledx12.png"); + + static ImageIcon createPNGImageIcon(String path) { + return Optional.ofNullable(path) + .map(Icons.class::getResource) + .map(ImageIcon::new) + .orElse(null); + } +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/linemarker/MutationTestRunLineMarkerContributor.java b/src/main/java/com/valantic/intellij/plugin/mutation/linemarker/MutationTestRunLineMarkerContributor.java new file mode 100644 index 0000000..a0f5edb --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/linemarker/MutationTestRunLineMarkerContributor.java @@ -0,0 +1,65 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.linemarker; + +import com.valantic.intellij.plugin.mutation.action.MutationAction; +import com.valantic.intellij.plugin.mutation.icons.Icons; +import com.valantic.intellij.plugin.mutation.services.Services; +import com.valantic.intellij.plugin.mutation.services.impl.PsiService; +import com.valantic.intellij.plugin.mutation.services.impl.UtilService; +import com.intellij.execution.lineMarker.RunLineMarkerContributor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiIdentifier; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +/** + * created by fabian.huesig on 2022-02-01 + */ +public class MutationTestRunLineMarkerContributor extends RunLineMarkerContributor { + + private static final String EXECUTION_BUNDLE_MESSAGE_KEY = "run.text"; + + private String targetClass; + private String targetTest; + + private UtilService utilService = Services.getService(UtilService.class); + private PsiService psiService = Services.getService(PsiService.class); + + @Nullable + @Override + public Info getInfo(@NotNull PsiElement element) { + final Info[] info = new Info[1]; + Optional.of(element).filter(PsiIdentifier.class::isInstance).map(PsiIdentifier.class::cast) + .map(PsiIdentifier::getParent).filter(PsiClass.class::isInstance).map(PsiClass.class::cast) + .filter(psiService::isTestClass).ifPresent(psiClass -> { + this.targetTest = psiService.determineTargetTest(psiClass); + this.targetClass = psiService.determineTargetClass(this.targetTest, psiClass); + info[0] = getInfo(); + }); + return info[0]; + } + + private Info getInfo() { + return new Info(Icons.MUTATIONx12, MutationAction.getSingletonActions(targetClass, targetTest), + element -> utilService.executionMessage(EXECUTION_BUNDLE_MESSAGE_KEY)); + } +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/localization/Messages.java b/src/main/java/com/valantic/intellij/plugin/mutation/localization/Messages.java new file mode 100644 index 0000000..d723864 --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/localization/Messages.java @@ -0,0 +1,46 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.localization; + +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.Optional; +import java.util.ResourceBundle; + + +/** + * created by fabian.huesig on 2022-02-01 + */ +public interface Messages { + String BASE_PACKAGE = "messages.MessageBundle"; + + static String getMessage(final String key) { + return Optional.ofNullable(key) + .map(Messages::getValue) + .orElse(key); + } + + static String getValue(final String key) { + try { + return ResourceBundle.getBundle(BASE_PACKAGE, Locale.ENGLISH).getString(key); + } catch (MissingResourceException e) { + // ignore exception + return key; + } + } +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/search/ProjectJavaFileSearchScope.java b/src/main/java/com/valantic/intellij/plugin/mutation/search/ProjectJavaFileSearchScope.java new file mode 100644 index 0000000..a019bdc --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/search/ProjectJavaFileSearchScope.java @@ -0,0 +1,63 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.search; + +import com.valantic.intellij.plugin.mutation.services.Services; +import com.valantic.intellij.plugin.mutation.services.impl.ProjectService; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ProjectFileIndex; +import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.search.GlobalSearchScope; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + + +/** + * created by fabian.huesig on 2022-02-01 + */ +public class ProjectJavaFileSearchScope extends GlobalSearchScope { + private ProjectFileIndex index; + private ProjectService projectService = Services.getService(ProjectService.class); + + public ProjectJavaFileSearchScope(@Nullable Project project) { + super(project); + Optional.of(project) + .map(projectService::getProjectRootManager) + .map(ProjectRootManager::getFileIndex) + .ifPresent(fileIndex -> this.index = fileIndex); + } + + @Override + public boolean isSearchInModuleContent(@NotNull Module module) { + return Boolean.FALSE; + } + + @Override + public boolean isSearchInLibraries() { + return Boolean.FALSE; + } + + @Override + public boolean contains(@NotNull VirtualFile file) { + return this.index.isInSourceContent(file); + } +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/services/Services.java b/src/main/java/com/valantic/intellij/plugin/mutation/services/Services.java new file mode 100644 index 0000000..c9b32a6 --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/services/Services.java @@ -0,0 +1,29 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.services; + +import com.intellij.openapi.application.ApplicationManager; + +/** + * created by fabian.huesig on 2022-02-01 + */ +public interface Services { + static T getService(Class clazz) { + return ApplicationManager.getApplication().getService(clazz); + } +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/services/impl/ClassNameService.java b/src/main/java/com/valantic/intellij/plugin/mutation/services/impl/ClassNameService.java new file mode 100644 index 0000000..035286a --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/services/impl/ClassNameService.java @@ -0,0 +1,35 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.services.impl; + +import com.intellij.openapi.components.Service; +import com.intellij.openapi.project.Project; +import com.intellij.psi.impl.search.AllClassesSearchExecutor; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.util.Processor; + +/** + * created by fabian.huesig on 2022-02-01 + */ +@Service +public final class ClassNameService { + + public boolean processClassNames(final Project project, final GlobalSearchScope searchScope, Processor processor) { + return AllClassesSearchExecutor.processClassNames(project, searchScope, processor); + } +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/services/impl/ModuleService.java b/src/main/java/com/valantic/intellij/plugin/mutation/services/impl/ModuleService.java new file mode 100644 index 0000000..6ce1f68 --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/services/impl/ModuleService.java @@ -0,0 +1,61 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.services.impl; + +import com.intellij.execution.configurations.JavaRunConfigurationModule; +import com.intellij.openapi.components.Service; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.module.ModuleUtil; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiFile; +import com.valantic.intellij.plugin.mutation.configuration.MutationConfiguration; +import com.valantic.intellij.plugin.mutation.configuration.MutationConfigurationFactory; +import com.valantic.intellij.plugin.mutation.configuration.MutationConfigurationType; +import com.valantic.intellij.plugin.mutation.services.Services; + +import java.util.Arrays; +import java.util.Collection; + +/** + * created by fabian.huesig on 2022-02-01 + */ +@Service +public final class ModuleService { + + private ProjectService projectService = Services.getService(ProjectService.class); + + public Collection getModules(final Project project) { + return Arrays.asList(getModuleManager(project).getModules()); + } + + public Module findModule(final PsiFile psiFile) { + return ModuleUtil.findModuleForFile(psiFile); + } + + public ModuleManager getModuleManager(final Project project) { + return ModuleManager.getInstance(project); + } + + public JavaRunConfigurationModule createConfigurationModule() { + MutationConfigurationType mutationConfigurationType = new MutationConfigurationType(); + MutationConfigurationFactory mutationConfigurationFactory = new MutationConfigurationFactory(mutationConfigurationType); + MutationConfiguration mutationConfiguration = new MutationConfiguration(projectService.getCurrentProject(), mutationConfigurationFactory, "name"); + return mutationConfiguration.getConfigurationModule(); + } +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/services/impl/ProjectService.java b/src/main/java/com/valantic/intellij/plugin/mutation/services/impl/ProjectService.java new file mode 100644 index 0000000..7574a22 --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/services/impl/ProjectService.java @@ -0,0 +1,58 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.services.impl; + +import com.valantic.intellij.plugin.mutation.search.ProjectJavaFileSearchScope; +import com.intellij.openapi.components.Service; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; +import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.psi.search.GlobalSearchScope; + +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * created by fabian.huesig on 2022-02-01 + */ +@Service +public final class ProjectService { + + /** + * gets current project + * + * @return + */ + public Project getCurrentProject() { + return Optional.of(ProjectManager.getInstance()) + .map(ProjectManager::getOpenProjects) + .map(Arrays::stream) + .flatMap(Stream::findFirst) + .orElse(null); + } + + public ProjectRootManager getProjectRootManager(final Project project) { + return ProjectRootManager.getInstance(project); + } + + public GlobalSearchScope getJavaFileProjectSearchScope(final Project project) { + return ProjectJavaFileSearchScope.projectScope(project); + } + +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/services/impl/PsiService.java b/src/main/java/com/valantic/intellij/plugin/mutation/services/impl/PsiService.java new file mode 100644 index 0000000..4187d1e --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/services/impl/PsiService.java @@ -0,0 +1,211 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.services.impl; + +import com.intellij.codeInsight.TestFrameworks; +import com.intellij.execution.configurations.JavaRunConfigurationModule; +import com.intellij.openapi.components.Service; +import com.intellij.openapi.project.Project; +import com.intellij.psi.JavaPsiFacade; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiJavaFile; +import com.intellij.psi.PsiPackage; +import com.intellij.psi.impl.file.PsiJavaDirectoryImpl; +import com.valantic.intellij.plugin.mutation.constants.MutationConstants; +import com.valantic.intellij.plugin.mutation.search.ProjectJavaFileSearchScope; +import com.valantic.intellij.plugin.mutation.services.Services; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.Optional; + + +/** + * created by fabian.huesig on 2022-02-01 + */ +@Service +public final class PsiService { + + private ClassNameService classNameService = Services.getService(ClassNameService.class); + private ModuleService moduleService = Services.getService(ModuleService.class); + private UtilService utilService = Services.getService(UtilService.class); + private ProjectService projectService = Services.getService(ProjectService.class); + + /** + * checks if class exists with the provided class name. + * + * @param fullyQualifiedClassName + * @return does class exists + */ + public boolean doesClassExists(final String fullyQualifiedClassName) { + final boolean[] exists = new boolean[1]; + final Project project = projectService.getCurrentProject(); + classNameService.processClassNames(project, projectService.getJavaFileProjectSearchScope(project), name -> { + if (getClassName(fullyQualifiedClassName).equals(name)) { + exists[0] = Boolean.TRUE; + return Boolean.FALSE; + } + return Boolean.TRUE; + + }); + return exists[0]; + } + + /** + * check if provided psiClass is a test class + * + * @param psiClass + * @return is test class + */ + public boolean isTestClass(PsiClass psiClass) { + return TestFrameworks.getInstance().isTestClass(psiClass); + } + + /** + * get class name and removes packages from name + * + * @return class name + */ + public String getClassName(final String fullyQualifiedClassName) { + return Optional.of(fullyQualifiedClassName) + .filter(className -> className.contains(MutationConstants.PACKAGE_SEPARATOR)) + .map(className -> StringUtils.substringAfterLast(className, MutationConstants.PACKAGE_SEPARATOR)) + .orElse(fullyQualifiedClassName); + } + + /** + * updates the module needed for java command line state. + * This can change in a multi module project depending of the used module. + * Determines the correct moule based by package of the test + * + * @param project + */ + public void updateModule(final Project project, final String qualifiedName, final JavaRunConfigurationModule configurationModule) { + utilService.allowSlowOperations(() -> Optional.of(JavaPsiFacade.getInstance(project)) + .map(javaPsiFacade -> getPsiClass(javaPsiFacade, qualifiedName, project)) + .map(PsiClass::getContainingFile) + .map(moduleService::findModule) + .ifPresent(module -> configurationModule.setModule(module))); + } + + /** + * either it is a list of classes, a wildcard package or a single test class, + * this method will return the base package of the primary used test. + * + * @param psiClass + * @return + */ + public String determineTargetTest(final PsiClass psiClass) { + return Optional.of(psiClass) + .map(PsiClass::getQualifiedName) + .filter(this::doesClassExists) + .orElse(StringUtils.EMPTY); + } + + /** + * get target class name based in provided string targetTest. + * If class does not exists, package will be returned based on psiClass. + * + * @param targetTest + * @param psiClass + * @return targetclass with fully package or package blob + */ + public String determineTargetClass(final String targetTest, final PsiClass psiClass) { + return Optional.ofNullable(targetTest) + .map(testClassName -> StringUtils.removeEnd(testClassName, MutationConstants.TEST_CLASS_SUFFIX)) + .filter(this::doesClassExists) + .orElseGet(() -> Optional.of(psiClass) + .map(PsiClass::getContainingFile) + .filter(PsiJavaFile.class::isInstance) + .map(PsiJavaFile.class::cast) + .map(PsiJavaFile::getPackageName) + .map(packageName -> packageName + MutationConstants.PACKAGE_WILDCARD_SUFFIX) + .orElse(StringUtils.EMPTY)); + } + + /** + * res + * + * @param dir + * @return + */ + public String resolvePackageNameForDir(PsiJavaDirectoryImpl dir) { + final String dirName = dir.getName(); + String classPackageName = Optional.of(dir.getChildren()) + .map(this::getAnyChildPsiClass) + .map(PsiElement::getContainingFile) + .filter(PsiJavaFile.class::isInstance) + .map(PsiJavaFile.class::cast) + .map(PsiJavaFile::getPackageName) + .orElse(StringUtils.EMPTY); + if (StringUtils.isNotEmpty(classPackageName)) { + String basePackageName = classPackageName.split(MutationConstants.PACKAGE_SEPARATOR + dirName + MutationConstants.PACKAGE_SEPARATOR)[0]; + return basePackageName + MutationConstants.PACKAGE_SEPARATOR + dirName; + } + return classPackageName; + } + + /** + * get recurisvly first child which is a PsiClass + * + * @param children + * @return + */ + private PsiClass getAnyChildPsiClass(PsiElement[] children) { + final PsiClass[] psiClass = new PsiClass[1]; + Optional optionalPsiClass = Arrays.stream(children) + .filter(PsiClass.class::isInstance) + .map(PsiClass.class::cast) + .findFirst(); + if (!optionalPsiClass.isPresent()) { + Arrays.stream(children).forEach(child -> { + if (ArrayUtils.isNotEmpty(child.getChildren())) + psiClass[0] = getAnyChildPsiClass(child.getChildren()); + if (psiClass[0] != null) { + return; + } + }); + } else { + psiClass[0] = optionalPsiClass.get(); + } + return psiClass[0]; + } + + /** + * get PsiClass for qualified name in the given project. + * + * @param javaPsiFacade + * @param qualifiedName + * @param project + * @return PsiClass + */ + private PsiClass getPsiClass(final JavaPsiFacade javaPsiFacade, final String qualifiedName, final Project project) { + if (qualifiedName.endsWith(MutationConstants.PACKAGE_SEPARATOR + MutationConstants.WILDCARD_SUFFIX)) { + PsiPackage psiPackage = Optional.of(qualifiedName.split(MutationConstants.WILDCARD_SUFFIX_REGEX)[0]) + .map(javaPsiFacade::findPackage) + .orElse(null); + if (psiPackage != null) { + return Arrays.stream(psiPackage.getClasses()).findAny().orElse(null); + } + } + return javaPsiFacade.findClass(qualifiedName, new ProjectJavaFileSearchScope(project)); + } + +} diff --git a/src/main/java/com/valantic/intellij/plugin/mutation/services/impl/UtilService.java b/src/main/java/com/valantic/intellij/plugin/mutation/services/impl/UtilService.java new file mode 100644 index 0000000..2dd6263 --- /dev/null +++ b/src/main/java/com/valantic/intellij/plugin/mutation/services/impl/UtilService.java @@ -0,0 +1,41 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.services.impl; + +import com.intellij.execution.ExecutionBundle; +import com.intellij.openapi.components.Service; +import com.intellij.util.SlowOperations; +import com.intellij.util.ThrowableRunnable; + + +/** + * created by fabian.huesig on 2022-02-01 + */ +@Service +public final class UtilService { + + @SuppressWarnings("unchecked") + public void allowSlowOperations(final ThrowableRunnable throwableRunnable) { + SlowOperations.allowSlowOperations(throwableRunnable); + } + + public String executionMessage(final String messageKey) { + return ExecutionBundle.message(messageKey); + } + +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000..2b03934 --- /dev/null +++ b/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,75 @@ + + + + com.valantic.intellij.plugin.mutation + + + Mutation Tester + + + com.intellij.modules.platform + com.intellij.modules.java + JUnit + + + + pitest.org commandline. + This Plugin lets you run single classes for mutation testing and provides you with the ability to configure everything in the IDE based on pitest-commandline configurations. +
+ More information are provided in the README.md with screenshots or here on our github. +
+ You can report any Issues in our github +
+ Mutation Testing is a type of software testing in which certain statements of + the source code are changed/mutated to check if the test cases are able to + find errors in source code. The goal of Mutation Testing is ensuring the + quality of test cases in terms of robustness that it should fail the mutated + source code. +
+ See pitest.org + for more information about mutation testing. +
+ created by valantic CEC Schweiz AG (valantic.com) + ]]> +
+ + +
  • 0.1.0 2022-02: Published Closed Beta Version with primary focused on SAP Commerce
  • + + ]]> +
    + + + valantic - github + + messages.MessageBundle + + + + + + + + + + +
    diff --git a/src/main/resources/META-INF/pluginIcon.svg b/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 0000000..1e094c8 --- /dev/null +++ b/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/src/main/resources/icons/mutation/disabled/mutation-disabled.png b/src/main/resources/icons/mutation/disabled/mutation-disabled.png new file mode 100644 index 0000000..4714e75 Binary files /dev/null and b/src/main/resources/icons/mutation/disabled/mutation-disabled.png differ diff --git a/src/main/resources/icons/mutation/disabled/mutation-disabledx12.png b/src/main/resources/icons/mutation/disabled/mutation-disabledx12.png new file mode 100644 index 0000000..2b1ab05 Binary files /dev/null and b/src/main/resources/icons/mutation/disabled/mutation-disabledx12.png differ diff --git a/src/main/resources/icons/mutation/disabled/mutation-disabledx13.png b/src/main/resources/icons/mutation/disabled/mutation-disabledx13.png new file mode 100644 index 0000000..0722901 Binary files /dev/null and b/src/main/resources/icons/mutation/disabled/mutation-disabledx13.png differ diff --git a/src/main/resources/icons/mutation/disabled/mutation-disabledx16.png b/src/main/resources/icons/mutation/disabled/mutation-disabledx16.png new file mode 100644 index 0000000..d57866b Binary files /dev/null and b/src/main/resources/icons/mutation/disabled/mutation-disabledx16.png differ diff --git a/src/main/resources/icons/mutation/disabled/mutation-disabledx40.png b/src/main/resources/icons/mutation/disabled/mutation-disabledx40.png new file mode 100644 index 0000000..8929a15 Binary files /dev/null and b/src/main/resources/icons/mutation/disabled/mutation-disabledx40.png differ diff --git a/src/main/resources/icons/mutation/mutation.md b/src/main/resources/icons/mutation/mutation.md new file mode 100644 index 0000000..e3799f3 --- /dev/null +++ b/src/main/resources/icons/mutation/mutation.md @@ -0,0 +1,2 @@ +mutation.png and all variations are based on an icon by Freepik from flaticon. +Gen Icons erstellt von Freepik - Flaticon diff --git a/src/main/resources/icons/mutation/mutation.png b/src/main/resources/icons/mutation/mutation.png new file mode 100755 index 0000000..4cf9f3c Binary files /dev/null and b/src/main/resources/icons/mutation/mutation.png differ diff --git a/src/main/resources/icons/mutation/mutationx12.png b/src/main/resources/icons/mutation/mutationx12.png new file mode 100755 index 0000000..44dd780 Binary files /dev/null and b/src/main/resources/icons/mutation/mutationx12.png differ diff --git a/src/main/resources/icons/mutation/mutationx13.png b/src/main/resources/icons/mutation/mutationx13.png new file mode 100755 index 0000000..0f8b0ff Binary files /dev/null and b/src/main/resources/icons/mutation/mutationx13.png differ diff --git a/src/main/resources/icons/mutation/mutationx16.png b/src/main/resources/icons/mutation/mutationx16.png new file mode 100755 index 0000000..2428982 Binary files /dev/null and b/src/main/resources/icons/mutation/mutationx16.png differ diff --git a/src/main/resources/icons/mutation/mutationx40.png b/src/main/resources/icons/mutation/mutationx40.png new file mode 100755 index 0000000..7381a56 Binary files /dev/null and b/src/main/resources/icons/mutation/mutationx40.png differ diff --git a/src/main/resources/icons/mutation/psd/mutation.psd b/src/main/resources/icons/mutation/psd/mutation.psd new file mode 100755 index 0000000..ae321f1 Binary files /dev/null and b/src/main/resources/icons/mutation/psd/mutation.psd differ diff --git a/src/main/resources/messages/MessageBundle.properties b/src/main/resources/messages/MessageBundle.properties new file mode 100644 index 0000000..82a08d3 --- /dev/null +++ b/src/main/resources/messages/MessageBundle.properties @@ -0,0 +1,73 @@ +action.com.valantic.intellij.plugin.mutation.action.MutationAction.text=mutate tests +action.com.valantic.intellij.plugin.mutation.action.MutationAction.description=Mutate tests for this testclass +pit.settings.editor.component.text.historyoutputlocation=historyOutputLocation +pit.settings.editor.component.text.historyinputlocation=historyInputLocation +pit.settings.editor.component.text.coveragethreshold=coverageThreshold +pit.settings.editor.component.text.mutationthreshold=mutationThreshold +pit.settings.editor.component.text.detectinlinedcode=detectInlinedCode +pit.settings.editor.component.text.excludedgroups=excludedGroups +pit.settings.editor.component.text.includedgroups=includedGroups +pit.settings.editor.component.text.testplugin=testPlugin +pit.settings.editor.component.text.mutablecodepaths=mutableCodePaths +pit.settings.editor.component.text.classpath=classPath +pit.settings.editor.component.text.failwhennomutations=failWhenNoMutations +pit.settings.editor.component.text.jvmpath=jvmPath +pit.settings.editor.component.text.jvmargs=jvmArgs +pit.settings.editor.component.text.maxmutationsperclass=maxMutationsPerClass +pit.settings.editor.component.text.timeoutfactor=timeoutFactor +pit.settings.editor.component.text.verbose=verbose +pit.settings.editor.component.text.avoidcallsto=avoidCallsTo +pit.settings.editor.component.text.excludedtests=excludedTests +pit.settings.editor.component.text.excludedclasses=excludedClasses +pit.settings.editor.component.text.excludedmethods=excludedMethods +pit.settings.editor.component.text.threads=threads +pit.settings.editor.component.text.dependencydistance=dependencyDistance +pit.settings.editor.component.text.includelaunchclasspath=includeLaunchClasspath +pit.settings.editor.component.text.outputformats=outputFormats +pit.settings.editor.component.text.timeoutconst=timeoutConst +pit.settings.editor.component.text.timestampedreports=timestampedReports +pit.settings.editor.component.text.mutators=mutators +pit.settings.editor.component.text.sourcedirs=sourceDirs +pit.settings.editor.component.text.reportdir=reportDir +pit.settings.editor.component.text.targettests=targetTests +pit.settings.editor.component.text.targetclasses=targetClasses +pit.settings.editor.component.tooltiptext.targetclasses=The classes to be mutated. This is expressed as a comma separated list of globs.

    For example com.mycompany.* or com.mycompany.package.*, com.mycompany.packageB.Foo, com.partner.* +pit.settings.editor.component.tooltiptext.targettests=A comma separated list of globs can be supplied to this parameter to limit the tests available to be run. If this parameter is not supplied then any test fixture that matched targetClasses may be used, it is however recommended that this parameter is always explicitly set. This parameter can be used to point PIT to a top level suite or suites. Custom suites such as ClassPathSuite are supported. Tests found via these suites can also be limited by the distance filter (see below). +pit.settings.editor.component.tooltiptext.reportdir=Output directory for the reports +pit.settings.editor.component.tooltiptext.sourcedirs=Source directories +pit.settings.editor.component.tooltiptext.mutators=List of mutations as group or comma separated list of mutators. See https://pitest.org/quickstart/mutators +pit.settings.editor.component.tooltiptext.timestampedreports=By default PIT will create a date and time stamped folder for its output each time it is run. This can can make automation difficult, so the behaviour can be suppressed by passing --timestampedReports=false. +pit.settings.editor.component.tooltiptext.timeoutconst=Constant amount of additional time to allow a test to run for (after the application of the timeoutFactor) before considering it to be stuck in an infinite loop. Defaults to 4000 +pit.settings.editor.component.tooltiptext.outputformats=Comma separated list of formats in which to write mutation results as the mutations are analysed. Supported formats are HTML, XML, CSV. Defaults to HTML. +pit.settings.editor.component.tooltiptext.includelaunchclasspath=Indicates if the PIT should try to mutate classes on the classpath with which it was launched. If not supplied this flag defaults to true. If set to false only classes found on the paths specified by the ?classPath option will be considered. +pit.settings.editor.component.tooltiptext.dependencydistance=PIT can optionally apply an additional filter to the supplied tests, such that only tests a certain distance from a mutated class will be considered for running. e.g A test that directly calls a method on a mutated class has a distance of 0 , a test that calls a method on a class that uses the mutee as an implementation detail has a distance of 1 etc. This filter will not work for tests that utilise classes via interfaces, reflection or other methods where the dependencies between classes cannot be determined from the byte code. The distance filter is particularly useful when performing a targeted mutation test of a subset of classes within a large project as it avoids the overheads of calculating the times and coverage of tests that cannot exercise the mutees. +pit.settings.editor.component.tooltiptext.threads=The number of threads to use when mutation testing. +pit.settings.editor.component.tooltiptext.excludedmethods=List of globs to match against method names. Methods matching the globs will be excluded from mutation. +pit.settings.editor.component.tooltiptext.excludedclasses=List of globs to match against class names. Matching classes will be excluded from mutation. Prior to release 1.3.0 tests matching this filter were also excluded from being run. From 1.3.0 onwards tests are excluded with the excludedTests parameter. +pit.settings.editor.component.tooltiptext.excludedtests=List of globs to match against test class names. Matching tests will not be run (note if a test suite includes an excluded class, then it will ?leak? back in). +pit.settings.editor.component.tooltiptext.avoidcallsto=List of packages and classes which are to be considered outside the scope of mutation. Any lines of code containing calls to these classes will not be mutated. If a list is not explicitly supplied then PIT will default to a list of common logging packages as follows java.util.logging org.apache.log4j org.slf4j org.apache.commons.logging If the feature FLOGCALL is disabled, this parameter is ignored and logging calls are also mutated. +pit.settings.editor.component.tooltiptext.verbose=Output verbose logging. Defaults to off/false. +pit.settings.editor.component.tooltiptext.timeoutfactor=A factor to apply to the normal runtime of a test when considering if it is stuck in an infinite loop. Defaults to 1.25 +pit.settings.editor.component.tooltiptext.maxmutationsperclass=The maximum number of mutations to create per class. Use 0 or -ve number to set no limit. +pit.settings.editor.component.tooltiptext.jvmargs=Argument string to use when PIT launches child processes. This is most commonly used to increase the amount of memory available to the process, but may be used to pass any valid JVM argument. +pit.settings.editor.component.tooltiptext.jvmpath=The path to tha java executable to be used to launch test with. If none is supplied defaults to the one pointed to by JAVA_HOME. +pit.settings.editor.component.tooltiptext.failwhennomutations=Whether to throw an error when no mutations found. Defaults to true +pit.settings.editor.component.tooltiptext.classpath=Comma separated list (yes comma separated - this is admittedly a bit weird for a classpath) of additional classpath entries to use when looking for tests and mutable code. These will be used in addition to the classpath with which PIT is launched. +pit.settings.editor.component.tooltiptext.mutablecodepaths=List of classpaths which should be considered to contain mutable code. If your build maintains separate output directories for tests and production classes this parameter should be set to your code output directory in order to avoid mutating test helper classes etc. If no mutableCodePath is supplied PIT will default to considering anything not defined within a jar or zip file as being a candidate for mutation. PIT will always attempt not to mutate test classes even if they are defined on a mutable path. +pit.settings.editor.component.tooltiptext.testplugin=Plugin to use to run tests. Defaults to junit. +pit.settings.editor.component.tooltiptext.includedgroups=Comma separated list of TestNG groups/JUnit categories to include in mutation analysis. Note that only class level categories are supported. +pit.settings.editor.component.tooltiptext.excludedgroups=Comma separated list of TestNG groups/JUnit categories to exclude from mutation analysis. Note that only class level categories are supported. +pit.settings.editor.component.tooltiptext.detectinlinedcode=Enabled by default since 0.29. Flag to indicate if PIT should attempt to detect the inlined code generated by the java compiler in order to implement finally blocks. Each copy of the inlined code would normally be mutated separately, resulting in multiple identical looking mutations. When inlined code detection is enabled PIT will attempt to spot inlined code and create only a single mutation that mutates all affected instructions simultaneously. The algorithm cannot easily distinguish between inlined copies of code, and genuine duplicate instructions on the same line within a finally block. In the case of any doubt PIT will act cautiously and assume that the code is not inlined. This will be detected as two separate inlined instructions finally { int++; int++; } But this will look confusing so PIT will assume no in-lining is taking place. finally { int++; int++; } This sort of pattern might not be common with integer addition, but things like string concatenation are likely to produce multiple similar instructions on the same line. +pit.settings.editor.component.tooltiptext.mutationthreshold=Mutation score threshold below which the build will fail. This is an integer percent (0-100) that represents the fraction of killed mutations out of all mutations. Please bear in mind that your build may contain equivalent mutations. Careful thought must therefore be given when selecting a threshold. +pit.settings.editor.component.tooltiptext.coveragethreshold=Line coverage threshold below which the build will fail. This is an integer percent (0-100) that represents the fraction of the project covered by the tests. +pit.settings.editor.component.tooltiptext.historyinputlocation=Path to a file containing history information for incremental analysis. +pit.settings.editor.component.tooltiptext.historyoutputlocation=Path to write history information for incremental analysis. May be the same as historyInputLocation. +pit.settings.editor.tab.advanced.text=Advanced settings for pitest command line. You can see the tool tip information if you hover over the label.\nThese are advanced settings. Change these only if you know what you are doing.\nFor any further information read pitest.org documentation. +pit.settings.editor.tab.settings.text=Modify these settings to run the pitest command line. You can see the tool tip information if you hover over the label.\nFor any further information read https://pitest.org/quickstart/commandline +pit.settings.editor.tab.title.advanced=advanced +pit.settings.editor.tab.title.settings=settings +pit.settings.editor.tab.tooltip.advanced=Here are advanced settings. Read https://pitest.org documentation first +pit.settings.editor.tab.tooltip.settings=You can modify these settings. Read https://pitest.org documentation first +plugin.description=Pit run configuration type +plugin.name=Mutation Testing +report.hyperlink.text=Show report diff --git a/src/main/resources/sample/advanced-settings.png b/src/main/resources/sample/advanced-settings.png new file mode 100644 index 0000000..3bff407 Binary files /dev/null and b/src/main/resources/sample/advanced-settings.png differ diff --git a/src/main/resources/sample/create-configuration.png b/src/main/resources/sample/create-configuration.png new file mode 100644 index 0000000..24ef62f Binary files /dev/null and b/src/main/resources/sample/create-configuration.png differ diff --git a/src/main/resources/sample/install-disk.png b/src/main/resources/sample/install-disk.png new file mode 100644 index 0000000..d916bb0 Binary files /dev/null and b/src/main/resources/sample/install-disk.png differ diff --git a/src/main/resources/sample/right-click-in-class.png b/src/main/resources/sample/right-click-in-class.png new file mode 100644 index 0000000..631bc9e Binary files /dev/null and b/src/main/resources/sample/right-click-in-class.png differ diff --git a/src/main/resources/sample/run-as.png b/src/main/resources/sample/run-as.png new file mode 100644 index 0000000..4160c01 Binary files /dev/null and b/src/main/resources/sample/run-as.png differ diff --git a/src/main/resources/sample/run-context-menu.png b/src/main/resources/sample/run-context-menu.png new file mode 100644 index 0000000..8cc2c9d Binary files /dev/null and b/src/main/resources/sample/run-context-menu.png differ diff --git a/src/main/resources/sample/run-in-class.png b/src/main/resources/sample/run-in-class.png new file mode 100644 index 0000000..9e6cdc4 Binary files /dev/null and b/src/main/resources/sample/run-in-class.png differ diff --git a/src/main/resources/sample/settings-1.png b/src/main/resources/sample/settings-1.png new file mode 100644 index 0000000..5e845e9 Binary files /dev/null and b/src/main/resources/sample/settings-1.png differ diff --git a/src/main/resources/sample/settings-2.png b/src/main/resources/sample/settings-2.png new file mode 100644 index 0000000..d49c040 Binary files /dev/null and b/src/main/resources/sample/settings-2.png differ diff --git a/src/test/java/com/valantic/intellij/plugin/mutation/icons/IconsTest.java b/src/test/java/com/valantic/intellij/plugin/mutation/icons/IconsTest.java new file mode 100644 index 0000000..1fbd0bd --- /dev/null +++ b/src/test/java/com/valantic/intellij/plugin/mutation/icons/IconsTest.java @@ -0,0 +1,43 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.icons; + +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; + + +/** + * created by fabian.huesig on 2022-02-01 + */ +public class IconsTest { + @Test + public void staticIcons() { + assertNotNull(Icons.MUTATION); + assertNotNull(Icons.MUTATIONx12); + assertNotNull(Icons.MUTATIONx13); + assertNotNull(Icons.MUTATIONx16); + assertNotNull(Icons.MUTATIONx40); + assertNotNull(Icons.MUTATION_DISABLED); + assertNotNull(Icons.MUTATION_DISABLEDx12); + assertNotNull(Icons.MUTATION_DISABLEDx13); + assertNotNull(Icons.MUTATION_DISABLEDx16); + assertNotNull(Icons.MUTATION_DISABLEDx40); + } + +} diff --git a/src/test/java/com/valantic/intellij/plugin/mutation/localization/MessagesTest.java b/src/test/java/com/valantic/intellij/plugin/mutation/localization/MessagesTest.java new file mode 100644 index 0000000..1026052 --- /dev/null +++ b/src/test/java/com/valantic/intellij/plugin/mutation/localization/MessagesTest.java @@ -0,0 +1,37 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.localization; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + + +/** + * created by fabian.huesig on 2022-02-01 + */ +public class MessagesTest +{ + + @Test + public void getMessages() + { + assertEquals("test value", Messages.getMessage("key.test")); + assertEquals("key.does.not.exist", Messages.getMessage("key.does.not.exist")); + } +} diff --git a/src/test/java/com/valantic/intellij/plugin/mutation/services/ServicesTest.java b/src/test/java/com/valantic/intellij/plugin/mutation/services/ServicesTest.java new file mode 100644 index 0000000..e69cda2 --- /dev/null +++ b/src/test/java/com/valantic/intellij/plugin/mutation/services/ServicesTest.java @@ -0,0 +1,64 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.services; + +import com.intellij.openapi.application.Application; +import com.intellij.openapi.application.ApplicationManager; +import com.valantic.intellij.plugin.mutation.services.impl.UtilService; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockedStatic; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + + +/** + * created by fabian.huesig on 2022-02-01 + */ +@RunWith(MockitoJUnitRunner.class) +public class ServicesTest { + private MockedStatic applicationManagerMockedStatic; + + @Before + public void setUp() { + applicationManagerMockedStatic = mockStatic(ApplicationManager.class); + } + + @Test + public void testGetService() { + Application application = mock(Application.class); + UtilService utilService = mock(UtilService.class); + applicationManagerMockedStatic.when(ApplicationManager::getApplication).thenReturn(application); + when(application.getService(UtilService.class)).thenReturn(utilService); + + assertSame(utilService, Services.getService(UtilService.class)); + } + + @After + public void tearDown() { + applicationManagerMockedStatic.close(); + } +} + + diff --git a/src/test/java/com/valantic/intellij/plugin/mutation/services/impl/PsiServiceTest.java b/src/test/java/com/valantic/intellij/plugin/mutation/services/impl/PsiServiceTest.java new file mode 100644 index 0000000..5fcb0b8 --- /dev/null +++ b/src/test/java/com/valantic/intellij/plugin/mutation/services/impl/PsiServiceTest.java @@ -0,0 +1,93 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.services.impl; + +import com.intellij.openapi.project.Project; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.util.Processor; +import com.valantic.intellij.plugin.mutation.services.Services; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * created by fabian.huesig on 2022-02-01 + */ +@RunWith(MockitoJUnitRunner.class) +public class PsiServiceTest { + + private PsiService underTest; + + @Mock + private ClassNameService classNameService; + @Mock + private ModuleService moduleService; + @Mock + private UtilService utilService; + @Mock + private ProjectService projectService; + + private MockedStatic servicesMockedStatic; + + @Before + public void setUp() { + servicesMockedStatic = mockStatic(Services.class); + servicesMockedStatic.when(() -> Services.getService(ClassNameService.class)).thenReturn(classNameService); + servicesMockedStatic.when(() -> Services.getService(ModuleService.class)).thenReturn(moduleService); + servicesMockedStatic.when(() -> Services.getService(UtilService.class)).thenReturn(utilService); + servicesMockedStatic.when(() -> Services.getService(ProjectService.class)).thenReturn(projectService); + underTest = new PsiService(); + } + + + @Test + public void testDoesClassExists() { + final String fullyQualifiedClassName = "fullyQualifiedClassName"; + final Project project = mock(Project.class); + final GlobalSearchScope searchScope = mock(GlobalSearchScope.class); + final ArgumentCaptor> processorArgumentCaptor = ArgumentCaptor.forClass(Processor.class); + + when(projectService.getCurrentProject()).thenReturn(project); + when(projectService.getJavaFileProjectSearchScope(project)).thenReturn(searchScope); + + underTest.doesClassExists(fullyQualifiedClassName); + + verify(classNameService).processClassNames(eq(project), eq(searchScope), processorArgumentCaptor.capture()); + assertFalse(processorArgumentCaptor.getValue().process("fullyQualifiedClassName")); + assertTrue(processorArgumentCaptor.getValue().process("anyOtherGivenName")); + } + + @After + public void tearDown() { + servicesMockedStatic.close(); + } + +} diff --git a/src/test/java/com/valantic/intellij/plugin/mutation/services/impl/UtilServiceTest.java b/src/test/java/com/valantic/intellij/plugin/mutation/services/impl/UtilServiceTest.java new file mode 100644 index 0000000..a5196bb --- /dev/null +++ b/src/test/java/com/valantic/intellij/plugin/mutation/services/impl/UtilServiceTest.java @@ -0,0 +1,83 @@ +/* + * Copyright [2022] [valantic CEC Schweiz AG] + * + * 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 + * + * http://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. + * + * Written by Fabian Hüsig , February, 2022 + */ +package com.valantic.intellij.plugin.mutation.services.impl; + +import com.intellij.execution.ExecutionBundle; +import com.intellij.util.SlowOperations; +import com.intellij.util.ThrowableRunnable; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +/** + * created by fabian.huesig on 2022-02-01 + */ +@RunWith(MockitoJUnitRunner.class) +public class UtilServiceTest { + + @InjectMocks + private UtilService underTest; + + private MockedStatic slowOperationsMockedStatic; + private MockedStatic executionBundleMockedStatic; + + @Before + public void setUp() { + slowOperationsMockedStatic = Mockito.mockStatic(SlowOperations.class); + executionBundleMockedStatic = Mockito.mockStatic(ExecutionBundle.class); + } + + @Test + public void testAllowSlowOperations() { + final ThrowableRunnable throwableRunnable = mock(ThrowableRunnable.class); + slowOperationsMockedStatic.when(() -> SlowOperations.allowSlowOperations(throwableRunnable)).thenAnswer((Answer) invocation -> null); + + underTest.allowSlowOperations(throwableRunnable); + + slowOperationsMockedStatic.verify(() -> SlowOperations.allowSlowOperations(throwableRunnable)); + } + + @Test + public void testExecutionMessage() { + final String messageKey = "messageKey"; + + executionBundleMockedStatic.when(() -> ExecutionBundle.message(messageKey)).thenReturn("messageValue"); + + final String result = underTest.executionMessage(messageKey); + + executionBundleMockedStatic.verify(() -> ExecutionBundle.message(messageKey)); + assertEquals("messageValue", result); + } + + + @After + public void tearDown() { + slowOperationsMockedStatic.close(); + executionBundleMockedStatic.close(); + } + +} diff --git a/src/test/resources/messages/MessageBundle.properties b/src/test/resources/messages/MessageBundle.properties new file mode 100644 index 0000000..75f80fb --- /dev/null +++ b/src/test/resources/messages/MessageBundle.properties @@ -0,0 +1 @@ +key.test=test value