Skip to content

Commit

Permalink
feat: created new plugin PitestAggregatorPlugin for aggregation proce…
Browse files Browse the repository at this point in the history
…ss for multi-project

resolves: gh-80
  • Loading branch information
MikeSafonov committed Dec 14, 2020
1 parent ddb995f commit f706426
Show file tree
Hide file tree
Showing 11 changed files with 424 additions and 4 deletions.
32 changes: 29 additions & 3 deletions README.md
Expand Up @@ -168,10 +168,36 @@ subprojects {
}
}
```
It is possible to aggregate pitest report for multi-module project using plugin `info.solidsoft.pitest.aggregator` and
task `pitestReportAggregate`. Root project must be properly configured to use `pitestReportAggregate` :

Currently the plugin [does not provide](https://github.com/szpak/gradle-pitest-plugin/issues/80) an aggregated report for
multi-module project. A report for each module has to be browsed separately. Alternatively a
[PIT plugin for Sonar](https://github.com/SonarCommunity/sonar-pitest) can be used to get aggregated results.

```groovy
//in root project configuration
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'info.solidsoft.gradle.pitest:gradle-pitest-plugin:1.5.2'
}
}
apply plugin: 'info.solidsoft.pitest.aggregator' // to 'pitestReportAggregate' appear
subprojects {
apply plugin: 'info.solidsoft.pitest'
pitest {
// export mutations.xml and line coverage for aggregation
outputFormats = ["XML"]
exportLineCoverage = true
...
}
}
```
After execution command `pitest pitestReportAggregate` aggregated report created by PIT will be placed in
`${PROJECT_DIR}/build/reports/pitest` directory.

## Integration tests in separate subproject

Expand Down
1 change: 1 addition & 0 deletions build.gradle
Expand Up @@ -34,6 +34,7 @@ sourceSets {

dependencies {
implementation localGroovy()
implementation "org.pitest:pitest-aggregator:1.5.2"

testImplementation('org.spockframework:spock-core:2.0-M3-groovy-2.5') {
exclude group: 'org.codehaus.groovy'
Expand Down
Expand Up @@ -2,18 +2,64 @@ package info.solidsoft.gradle.pitest.functional

import groovy.transform.CompileDynamic
import nebula.test.functional.ExecutionResult
import spock.util.environment.RestoreSystemProperties

import java.nio.file.Paths

@CompileDynamic
class AcceptanceTestsInSeparateSubprojectFunctionalSpec extends AbstractPitestFunctionalSpec {

private String htmlReport = null

void "should mutate production code in another subproject"() {
given:
copyResources("testProjects/multiproject", "")
when:
ExecutionResult result = runTasksSuccessfully('pitest')
ExecutionResult result = runTasks('pitest')
then:
!result.standardError.contains("Build failed with an exception")
!result.failure
result.wasExecuted(':itest:pitest')
result.getStandardOutput().contains('Generated 4 mutations Killed 3 (75%)')
}

@RestoreSystemProperties
void "should aggregate report from subproject"() {
given:
copyResources("testProjects/multiproject", "")
when:
ExecutionResult result = runTasks('pitest', 'pitestReportAggregate', '-c', 'settings-report.gradle')
then:
!result.standardError.contains("Build failed with an exception")
!result.failure
result.wasExecuted(':shared:pitest')
result.wasExecuted(':for-report:pitest')
result.wasExecuted(':pitestReportAggregate')
and:
result.getStandardOutput().contains('Aggregating pitest reports')
result.getStandardOutput().contains("Aggregated report ${getOutputReportPath()}")
fileExists("build/reports/pitest/index.html")
and:
assertHtmlContains("<h1>Pit Test Coverage Report</h1>")
assertHtmlContains("<th>Number of Classes</th>")
assertHtmlContains("<th>Line Coverage</th>")
assertHtmlContains("<th>Mutation Coverage</th>")
assertHtmlContains("<td>2</td>")
assertHtmlContains("<td>95% ")
assertHtmlContains("<td>40% ")
assertHtmlContains("<td><a href=\"./pitest.sample.multimodule.forreport/index.html\">pitest.sample.multimodule.forreport</a></td>")
assertHtmlContains("<td><a href=\"./pitest.sample.multimodule.shared/index.html\">pitest.sample.multimodule.shared</a></td>")
}

private void assertHtmlContains(String content) {
if (htmlReport == null) {
htmlReport = new File(projectDir, "build/reports/pitest/index.html").text
}
assert htmlReport.contains(content)
}

private String getOutputReportPath() {
return Paths.get(projectDir.absolutePath, "build", "reports", "pitest", "index.html").toString()
}

}
@@ -0,0 +1,44 @@
buildscript {
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
//Local/current version of the plugin should be put on a classpath earlier to override that plugin version
// classpath 'info.solidsoft.gradle.pitest:gradle-pitest-plugin:1.1.3-SNAPSHOT'
}
}

apply plugin: "info.solidsoft.pitest.aggregator"

subprojects {
apply plugin: 'java'

repositories {
mavenCentral()
}

dependencies {
testImplementation 'junit:junit:4.12'
}

version = '1.0'
group = 'pitest.sample.multimodule'
}

[":shared", ":for-report"].each { subprojectName ->
configure(project(subprojectName)) {
apply plugin: "info.solidsoft.pitest"

dependencies {
implementation 'org.slf4j:slf4j-api:1.7.25'
implementation 'org.slf4j:slf4j-nop:1.7.25'
}

pitest {
outputFormats = ["HTML","XML"]
timestampedReports = false
exportLineCoverage = true
}
}
}
@@ -0,0 +1,31 @@
package pitest.sample.multimodule.forreport;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ForReport {
private final Logger logger = LoggerFactory.getLogger(getClass());

private String name;

public ForReport(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String notUsed() {
return "notUsed";
}

public String readProperty() {
logger.info("to fail on broken dependency");
return "important";
}
}
@@ -0,0 +1,20 @@
package pitest.sample.multimodule.forreport;

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class ForReportTest {
@Test
public void testName() {
ForReport shared = new ForReport("testname1");
assertEquals("testname1", shared.getName());
shared.setName("testname2");
assertEquals("testname2", shared.getName());
}

@Test
public void testCallLogger() {
new ForReport("P").readProperty();
}
}
@@ -0,0 +1,3 @@
include "shared", "for-report"

rootProject.buildFileName = "build-report.gradle"
@@ -0,0 +1,181 @@
package info.solidsoft.gradle.pitest

import groovy.transform.CompileStatic
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Provider
import org.gradle.api.reporting.ReportingExtension
import org.gradle.api.tasks.Classpath
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.SkipWhenEmpty
import org.gradle.api.tasks.TaskAction
import org.pitest.aggregate.ReportAggregator
import org.pitest.mutationtest.config.DirectoryResultOutputStrategy
import org.pitest.mutationtest.config.UndatedReportDirCreationStrategy

import java.util.stream.Collectors

/**
* Task to aggregate pitest report
*/
@CompileStatic
class AggregateReportTask extends DefaultTask {

private static final String MUTATION_FILE_NAME = "mutations.xml";
private static final String LINE_COVERAGE_FILE_NAME = "linecoverage.xml";

@OutputDirectory
final DirectoryProperty reportDir

@OutputFile
final RegularFileProperty reportFile

@SkipWhenEmpty
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
final ConfigurableFileCollection sourceDirs

@SkipWhenEmpty
@InputFiles
@Classpath
final ConfigurableFileCollection additionalClasspath

@SkipWhenEmpty
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
final ConfigurableFileCollection mutationFiles

@SkipWhenEmpty
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
final ConfigurableFileCollection lineCoverageFiles

AggregateReportTask() {
reportDir = project.objects.directoryProperty()
reportDir.set(new File(getReportBaseDirectory(), PitestPlugin.PITEST_REPORT_DIRECTORY_NAME))

reportFile = project.objects.fileProperty()
reportFile.set(reportDir.file("index.html"))

Set<PitestTask> pitestTasks = getAllPitestTasks()

sourceDirs = project.objects.fileCollection()
sourceDirs.from = collectSourceDirs(pitestTasks)

additionalClasspath = project.objects.fileCollection()
additionalClasspath.from = collectClasspathDirs(pitestTasks)

Set<Project> projectsWithPitest = getProjectsWithPitestPlugin()

mutationFiles = project.objects.fileCollection()
mutationFiles.from = collectMutationFiles(projectsWithPitest)

lineCoverageFiles = project.objects.fileCollection()
lineCoverageFiles.from = collectLineCoverageFiles(projectsWithPitest)
}

@TaskAction
void aggregate() {
logger.info("Aggregating pitest reports")

ReportAggregator.Builder builder = ReportAggregator.builder()

lineCoverageFiles.each { file -> builder.addLineCoverageFile(file) }
mutationFiles.each { file -> builder.addMutationResultsFile(file) }
sourceDirs.each { file -> builder.addSourceCodeDirectory(file) }
additionalClasspath.each { file -> builder.addCompiledCodeDirectory(file) }

ReportAggregator aggregator = builder.resultOutputStrategy(new DirectoryResultOutputStrategy(
reportDir.asFile.get().absolutePath,
new UndatedReportDirCreationStrategy()))
.build()
aggregator.aggregateReport()

logger.info("Aggregated report ${reportFile.asFile.get().absolutePath}")
}

private static Set<Provider<File>> collectMutationFiles(Set<Project> pitestProjects) {
return pitestProjects.stream()
.map { prj -> prj.extensions.getByType(PitestPluginExtension) }
.map { extension -> extension.reportDir.file(MUTATION_FILE_NAME) }
.collect(Collectors.toSet())
}

private static Set<Provider<File>> collectLineCoverageFiles(Set<Project> pitestProjects) {
return pitestProjects.stream()
.map { prj -> prj.extensions.getByType(PitestPluginExtension) }
.map { extension -> extension.reportDir.file(LINE_COVERAGE_FILE_NAME) }
.collect(Collectors.toSet())
}

DirectoryProperty getReportDir() {
return reportDir
}

ConfigurableFileCollection getAdditionalClasspath() {
return additionalClasspath
}

ConfigurableFileCollection getMutationFiles() {
return mutationFiles
}

ConfigurableFileCollection getLineCoverageFiles() {
return lineCoverageFiles
}

ConfigurableFileCollection getSourceDirs() {
return sourceDirs
}

private File getReportBaseDirectory() {
// if Java plugin configured on root project
if (project.extensions.findByType(ReportingExtension)) {
return project.extensions.getByType(ReportingExtension).baseDir
}
return new File(project.buildDir, "reports")
}

private Set<Project> getProjectsWithPitestPlugin() {
Set<Project> projects = [] as Set
if (isRootPitestConfigured()) {
projects.add(project)
}
projects.addAll(getSubprojectsWithPitest())
return projects
}

private boolean isRootPitestConfigured() {
return project.plugins.hasPlugin(PitestPlugin.PLUGIN_ID) && project.extensions.findByType(PitestPluginExtension)
}

private Set<Project> getSubprojectsWithPitest() {
return project.subprojects.findAll { prj -> prj.plugins.hasPlugin(PitestPlugin.PLUGIN_ID) }
}

private Set<PitestTask> getAllPitestTasks() {
return project.getTasksByName(PitestPlugin.PITEST_TASK_NAME, true) as Set<PitestTask>
}

private Set<Provider<Set<File>>> collectSourceDirs(Set<PitestTask> pitestTasks) {
return pitestTasks.stream()
.map { task -> task.sourceDirs }
.map { cfc -> project.provider { cfc.files.findAll { f -> f.isDirectory() } } }
.collect(Collectors.toSet())
}

private Set<Provider<Set<File>>> collectClasspathDirs(Set<PitestTask> pitestTasks) {
return pitestTasks.stream()
.map { task -> task.additionalClasspath }
.map { cfc -> project.provider { cfc.files.findAll { f -> f.isDirectory() } } }
.collect(Collectors.toSet())
}

}

0 comments on commit f706426

Please sign in to comment.