diff --git a/src/funcTest/groovy/info/solidsoft/gradle/pitest/functional/AbstractPitestFunctionalSpec.groovy b/src/funcTest/groovy/info/solidsoft/gradle/pitest/functional/AbstractPitestFunctionalSpec.groovy index 642f2985..93aa0734 100644 --- a/src/funcTest/groovy/info/solidsoft/gradle/pitest/functional/AbstractPitestFunctionalSpec.groovy +++ b/src/funcTest/groovy/info/solidsoft/gradle/pitest/functional/AbstractPitestFunctionalSpec.groovy @@ -67,6 +67,21 @@ abstract class AbstractPitestFunctionalSpec extends IntegrationSpec { """.stripIndent() } + protected void writeFailingPitTest(String packageDotted = 'gradle.pitest.test.hello', File baseDir = getProjectDir()) { + String path = 'src/test/java/' + packageDotted.replace('.', '/') + '/HelloPitTest.java' + File javaFile = createFile(path, baseDir) + javaFile << """package ${packageDotted}; + import org.junit.Test; + import static org.junit.Assert.assertEquals; + + public class HelloPitTest { + @Test public void shouldReturnInputNumber() { + assertEquals(1, new HelloPit().returnInputNumber(5)); + } + } + """.stripIndent() + } + protected void assertStdOutOrStdErrContainsGivenText(ExecutionResult result, String textToContain) { //TODO: Simplify if possible - standardOutput for Gradle <5 and standardError for Gradle 5+ assert result.standardOutput.contains(textToContain) || result.standardError.contains(textToContain) diff --git a/src/funcTest/groovy/info/solidsoft/gradle/pitest/functional/PitestPluginGeneralFunctionalSpec.groovy b/src/funcTest/groovy/info/solidsoft/gradle/pitest/functional/PitestPluginGeneralFunctionalSpec.groovy index 48edacad..b8b5a1a1 100644 --- a/src/funcTest/groovy/info/solidsoft/gradle/pitest/functional/PitestPluginGeneralFunctionalSpec.groovy +++ b/src/funcTest/groovy/info/solidsoft/gradle/pitest/functional/PitestPluginGeneralFunctionalSpec.groovy @@ -78,6 +78,93 @@ class PitestPluginGeneralFunctionalSpec extends AbstractPitestFunctionalSpec { historyOutputLocation.size() } + void "report URI is not printed if test suite is not green"() { + given: + buildFile << getBasicGradlePitestConfig() + and: + writeHelloPitClass() + writeFailingPitTest() + when: + ExecutionResult result = runTasksWithFailure('pitest') + then: + result.wasExecuted(':pitest') + !result.getStandardOutput().contains('index.html') + } + + void "timestamped report URI is printed if pitest run fails due to unmet thresholds"() { + given: + buildFile << getBasicGradlePitestConfig() + buildFile << """ + pitest { + mutationThreshold = 101 + } + """.stripIndent() + and: + writeHelloPitClass() + writeHelloPitTest() + when: + ExecutionResult result = runTasksWithFailure('pitest') + then: + result.wasExecuted(':pitest') + result.getStandardOutput().find(/pitest.[0-9]{12}.index\.html/) + } + + void "timestamped report URI is not printed if turned off and pitest run fails due to unmet thresholds"() { + given: + buildFile << getBasicGradlePitestConfig() + buildFile << """ + pitest { + mutationThreshold = 101 + printReportUri = 'never' + } + """.stripIndent() + and: + writeHelloPitClass() + writeHelloPitTest() + when: + ExecutionResult result = runTasksWithFailure('pitest') + then: + result.wasExecuted(':pitest') + !result.getStandardOutput().find(/pitest.[0-9]{12}.index\.html/) + } + + void "timestamped report URI is printed if always requested and pitest run passes"() { + given: + buildFile << getBasicGradlePitestConfig() + buildFile << """ + pitest { + printReportUri = 'always' + } + """.stripIndent() + and: + writeHelloPitClass() + writeHelloPitTest() + when: + ExecutionResult result = runTasksSuccessfully('pitest') + then: + result.wasExecuted(':pitest') + result.getStandardOutput().find(/pitest.[0-9]{12}.index\.html/) + } + + void "non-timestamped report URI is printed if always requested and pitest run passes"() { + given: + buildFile << getBasicGradlePitestConfig() + buildFile << """ + pitest { + printReportUri = 'always' + timestampedReports = false + } + """.stripIndent() + and: + writeHelloPitClass() + writeHelloPitTest() + when: + ExecutionResult result = runTasksSuccessfully('pitest') + then: + result.wasExecuted(':pitest') + result.getStandardOutput().contains(quoteBackslashesInWindowsPath(new File('reports/pitest/index.html'))) + } + void "pass additional configured parameters that cannot be test with ProjectBuilder"() { given: buildFile << getBasicGradlePitestConfig() @@ -114,7 +201,7 @@ class PitestPluginGeneralFunctionalSpec extends AbstractPitestFunctionalSpec { private String quoteBackslashesInWindowsPath(File file) { //There is problem with backslash within '' or "" while running this test on Windows: "unexpected char" - return file.absolutePath.replaceAll('\\\\', '\\\\\\\\') + return file.toString().replaceAll('\\\\', '\\\\\\\\') } } diff --git a/src/main/groovy/info/solidsoft/gradle/pitest/PitestPlugin.groovy b/src/main/groovy/info/solidsoft/gradle/pitest/PitestPlugin.groovy index cbf1d7d8..8cf86291 100644 --- a/src/main/groovy/info/solidsoft/gradle/pitest/PitestPlugin.groovy +++ b/src/main/groovy/info/solidsoft/gradle/pitest/PitestPlugin.groovy @@ -201,6 +201,7 @@ class PitestPlugin implements Plugin { task.maxSurviving.set(extension.maxSurviving) task.useClasspathJar.set(extension.useClasspathJar) task.features.set(extension.features) + task.printReportUri.set(extension.printReportUri) configurePropertiesWithProblematicTypesForGradle5(task) } diff --git a/src/main/groovy/info/solidsoft/gradle/pitest/PitestPluginExtension.groovy b/src/main/groovy/info/solidsoft/gradle/pitest/PitestPluginExtension.groovy index 123ee9f2..4ba19896 100644 --- a/src/main/groovy/info/solidsoft/gradle/pitest/PitestPluginExtension.groovy +++ b/src/main/groovy/info/solidsoft/gradle/pitest/PitestPluginExtension.groovy @@ -212,6 +212,12 @@ class PitestPluginExtension { @Incubating final ListProperty fileExtensionsToFilter + /** + * Controls when the report URI will be printed. Valid values are: 'never', 'on-failure' (default), + * or 'always' + */ + final Property printReportUri + PitestPluginExtension(Project project) { ObjectFactory of = project.objects Project p = project @@ -262,6 +268,7 @@ class PitestPluginExtension { useClasspathJar = of.property(Boolean) features = nullListPropertyOf(p, String) fileExtensionsToFilter = nullListPropertyOf(p, String) + printReportUri = of.property(String) } void setReportDir(File reportDir) { diff --git a/src/main/groovy/info/solidsoft/gradle/pitest/PitestTask.groovy b/src/main/groovy/info/solidsoft/gradle/pitest/PitestTask.groovy index cd31e67d..6ead57ef 100644 --- a/src/main/groovy/info/solidsoft/gradle/pitest/PitestTask.groovy +++ b/src/main/groovy/info/solidsoft/gradle/pitest/PitestTask.groovy @@ -38,6 +38,7 @@ import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.options.Option +import org.gradle.process.ExecResult /** * Gradle task implementation for Pitest. @@ -241,6 +242,10 @@ class PitestTask extends JavaExec { @Optional List overriddenTargetTests //should be Set or SetProperty but it's not supported in Gradle as of 5.6.1 + @Input + @Optional + final Property printReportUri + PitestTask() { //setting during execution doesn't work in 6.4+: //The value for task ':pitest' property 'mainClass' is final and cannot be changed any further. @@ -294,6 +299,7 @@ class PitestTask extends JavaExec { useAdditionalClasspathFile = of.property(Boolean) additionalClasspathFile = of.fileProperty() features = of.listProperty(String) + printReportUri = of.property(String) } @Input @@ -319,12 +325,61 @@ class PitestTask extends JavaExec { return jvmPath.isPresent() ? jvmPath.asFile.get().absolutePath : null } + @SuppressWarnings('UnnecessarySetter') @Override void exec() { args = argumentsForPit() jvmArgs = ((List) getMainProcessJvmArgs().getOrNull() ?: getJvmArgs()) classpath = getLaunchClasspath() + + /* + * As it's up to Pitest to write the report somewhere in the report dir if + * timestamped reports are allowed, it is necessary to figure out which one + * is the latest one. + */ + Set previousReportDirs = [] + if (timestampedReports.getOrElse(true)) { + previousReportDirs = listTimestampedReportDirs(getReportDir().asFile.get()) + } + + setIgnoreExitValue(true) super.exec() + ExecResult execResult = getExecutionResult().get() + + printReportUriIfNecessary(execResult.exitValue, previousReportDirs) + + execResult.assertNormalExitValue() + } + + private void printReportUriIfNecessary(int exitValue, Set previousReportDirs) { + String printReportUri = this.printReportUri.getOrElse('on-failure') + + if ((exitValue != 0 && + printReportUri in ['on-failure', 'always']) + || printReportUri in ['always']) { + File indexFile = resolveReportIndexFile(previousReportDirs) + + if (indexFile.exists()) { + getLogger().warn('See the Pitest report at: ' + + new URI('file', '', indexFile.toURI().getPath(), (String) null, (String) null)) + } + } + } + + private File resolveReportIndexFile(Set previousReportDirs) { + File actualReportDir + + if (timestampedReports.getOrElse(true)) { + actualReportDir = (listTimestampedReportDirs(getReportDir().asFile.get()) - previousReportDirs).first() + } else { + actualReportDir = getReportDir().asFile.get() + } + + return new File(actualReportDir, 'index.html') + } + + private static Set listTimestampedReportDirs(File reportDir) { + return reportDir.listFiles().findAll { file -> file.isDirectory() }.toSet() } private List argumentsForPit() {