diff --git a/.github/check-md-links.json b/.github/check-md-links.json new file mode 100644 index 000000000..76af7fd96 --- /dev/null +++ b/.github/check-md-links.json @@ -0,0 +1,10 @@ +{ + "httpHeaders": [ + { + "urls": ["https://github.com/", "https://guides.github.com/", "https://help.github.com/", "https://docs.github.com/"], + "headers": { + "Accept-Encoding": "zstd, br, gzip, deflate" + } + } + ] +} diff --git a/.github/workflows/assign-pr.yml b/.github/workflows/assign-pr.yml new file mode 100644 index 000000000..64ffa369f --- /dev/null +++ b/.github/workflows/assign-pr.yml @@ -0,0 +1,11 @@ +name: 'Auto Assign PR' + +on: pull_request_target + +jobs: + add-reviews: + runs-on: ubuntu-latest + steps: + - uses: kentaro-m/auto-assign-action@v1.2.4 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/check-md-links.yml b/.github/workflows/check-md-links.yml index 7914d1c57..a06f66992 100644 --- a/.github/workflows/check-md-links.yml +++ b/.github/workflows/check-md-links.yml @@ -1,4 +1,4 @@ -name: Check Markdown links +name: 'Check Markdown links' on: push @@ -9,4 +9,5 @@ jobs: - uses: actions/checkout@v3 - uses: gaurav-nelson/github-action-markdown-link-check@v1 with: - check-modified-files-only: 'yes' + check-modified-files-only: 'no' + config-file: '.github/check-md-links.json' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..020ae810b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,51 @@ +name: "CodeQL" + +on: + push: + branches: + - master + - main + pull_request: + branches: + - master + - main + schedule: + - cron: "32 3 * * 0" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ java ] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 11 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index b0b286a4e..c537e7244 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -4,12 +4,13 @@ on: push: branches: - master + - main pull_request: jobs: coverage: - runs-on: [ubuntu-latest] + runs-on: ubuntu-latest name: Coverage on Ubuntu steps: @@ -20,14 +21,10 @@ jobs: distribution: 'temurin' java-version: '11' check-latest: true - cache: 'maven' - name: Generate coverage with JaCoCo - env: - BROWSER: firefox-container - run: mvn -V --color always -ntp clean verify jacoco:prepare-agent test integration-test jacoco:report --file plugin/pom.xml '-Dgpg.skip' - + run: mvn -V --color always -ntp clean verify --file pom.xml '-Dgpg.skip' - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: + files: 'target/site/jacoco/jacoco.xml' token: ${{secrets.CODECOV_TOKEN}} - file: ./plugin/target/site/jacoco/jacoco.xml diff --git a/.github/workflows/enforce-labels.yml b/.github/workflows/enforce-labels.yml new file mode 100644 index 000000000..e38c08cf4 --- /dev/null +++ b/.github/workflows/enforce-labels.yml @@ -0,0 +1,14 @@ +name: Enforce PR labels + +on: + pull_request: + types: [unlabeled] +jobs: + enforce-label: + runs-on: ubuntu-latest + steps: + - uses: yogevbd/enforce-label-action@2.1.0 + with: + REQUIRED_LABELS_ANY: "bug,feature,enhancement,deprecated,removed,tests,documentation,internal,dependencies" + REQUIRED_LABELS_ANY_DESCRIPTION: "Maintainer needs to assign at least one label before merge" + BANNED_LABELS: "banned" diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml index e2f3e9f07..a0779e7a5 100644 --- a/.github/workflows/sync-labels.yml +++ b/.github/workflows/sync-labels.yml @@ -3,8 +3,10 @@ on: push: branches: - master + - main paths: - .github/labels.yml + - .github/workflows/sync-labels.yml jobs: build: diff --git a/Jenkinsfile b/Jenkinsfile index e01eea722..8bf9c6305 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -5,8 +5,8 @@ def configurations = [ buildPlugin(failFast: false, configurations: configurations, timeout: 90, checkstyle: [qualityGates: [[threshold: 1, type: 'NEW', unstable: true]], - filters:[includePackage('io.jenkins.plugins.coverage.model')]], + filters:[includePackage('io.jenkins.plugins.coverage.metrics')]], pmd: [qualityGates: [[threshold: 1, type: 'NEW', unstable: true]], - filters:[includePackage('io.jenkins.plugins.coverage.model')]], + filters:[includePackage('io.jenkins.plugins.coverage.metrics')]], spotbugs: [qualityGates: [[threshold: 1, type: 'NEW', unstable: true]], - filters:[includePackage('io.jenkins.plugins.coverage.model')]]) + filters:[includePackage('io.jenkins.plugins.coverage.metrics')]]) diff --git a/README.md b/README.md index 9044261f9..f87801159 100644 --- a/README.md +++ b/README.md @@ -5,34 +5,32 @@ [![Jenkins](https://ci.jenkins.io/job/Plugins/job/code-coverage-api-plugin/job/master/badge/icon?subject=Jenkins%20CI)](https://ci.jenkins.io/job/Plugins/job/code-coverage-api-plugin/job/master/) [![GitHub Actions](https://github.com/jenkinsci/code-coverage-api-plugin/workflows/GitHub%20CI/badge.svg?branch=master)](https://github.com/jenkinsci/code-coverage-api-plugin/actions) [![Codecov](https://codecov.io/gh/jenkinsci/code-coverage-api/branch/master/graph/badge.svg)](https://codecov.io/gh/jenkinsci/code-coverage-api-plugin/branch/master) +[![CodeQL](https://github.com/jenkinsci/code-coverage-api/workflows/CodeQL/badge.svg?branch=master)](https://github.com/jenkinsci/code-coverage-api/actions/workflows/codeql.yml) +The Jenkins code coverage plug-in collects reports of code coverage or mutation coverage tools. It has support for the following report formats: -This Jenkins plugin integrates and publishes multiple coverage report types. -It has been developed since [GSoC 2018](https://jenkins.io/projects/gsoc/2018/code-coverage-api-plugin/). +- [JaCoCo](https://www.jacoco.org/jacoco) +- [Cobertura](https://cobertura.github.io/cobertura/) +- [PIT](https://pitest.org/) -* [Supported coverage formats](#Supported-Coverage-Formats) -* [Release notes](#Release-Notes) -* [Features](#Features) -* [Usage](#Usage) +If your coverage tool is not yet supported by the code coverage plugin you can provide a pull request for the [Coverage Model](https://github.com/uhafner/coverage-model/pulls). +The plugin publishes a report of the code coverage and mutation coverage in your build, so you can navigate to a summary report from the main build page. From there you can also dive into the details: +- tree charts that show the distribution of coverage by type (line, instruction, branch, method, class, etc.) +- tabular listing of all files with their coverage +- source code of the files with the coverage highlighted +- trend charts of the coverage over time -## Supported Coverage Formats -#### Embedded -- [JaCoCo](https://www.jacoco.org/jacoco/trunk/doc/) -- [Istanbul](https://istanbul.js.org/) - [Cobertura Reporter](https://istanbul.js.org/docs/advanced/alternative-reporters/#cobertura) -- [Cobertura](http://cobertura.github.io/cobertura/) +This project was part of [GSoC 2018](https://jenkins.io/projects/gsoc/2018/code-coverage-api-plugin/). -#### Other plugins as an Extension of Code Coverage API plugin -- [llvm-cov](https://github.com/llvm-mirror/clang/blob/master/docs/SourceBasedCodeCoverage.rst) ([llvm-cov plugin](https://github.com/jenkinsci/llvm-cov-plugin)) -- [OpenCover](https://github.com/OpenCover/opencover) ([OpenCover Plugin](https://github.com/jenkinsci/opencover-plugin)) +## Features -## Release Notes -See the [GitHub Releases](https://github.com/jenkinsci/code-coverage-api-plugin/releases). +The code coverage plug-in provides the following features when added as a post build action (or step) +to a job: -## Features -* **Coverage analysis of whole projects and pull requests:** - * complete code (Project Coverage) - * code changes (Change Coverage) +* **Coverage analysis of projects and pull requests:** + * all project code (Project Coverage) + * code changes (Modified Lines or Files Coverage) * coverage changes created by changed test cases (Indirect Coverage Changes) * **Modernized coverage report visualization:** * Coverage overview and trend @@ -61,131 +59,133 @@ See the [GitHub Releases](https://github.com/jenkinsci/code-coverage-api-plugin/ ## Usage -### 1. Configure your coverage tool to generate reports - -#### Cobertura based coverage - -Configure Maven to generate Cobertura coverage reports: -```xml - - - - org.codehaus.mojo - cobertura-maven-plugin - 2.7 - - - xml - - - - - - package - - cobertura - - - - - - -``` -More information about [Cobertura](http://cobertura.github.io/cobertura/). - -#### JaCoCo based coverage - -Configure Maven to generate JaCoCo coverage reports: - -```xml - - - - org.jacoco - jacoco-maven-plugin - 0.8.1 - - - - prepare-agent - - - - report - package - - report - - - - - - -``` -More Information about [JaCoCo](https://www.jacoco.org/jacoco/trunk/doc/). +:exclamation: The plugin does not run the code coverage, it just visualizes the results reported by such tools. +You still need to enable and configure the code coverage tool in your build file or Jenkinsfile. -#### llvm-cov based coverage +### Supported project types -Use llvm-cov to generate JSON format report: -``` -$ llvm-cov export -instr-profile /path/to/foo.profdata /path/to/foo -``` -More Information [llvm-cov](https://github.com/llvm-mirror/clang/blob/master/docs/SourceBasedCodeCoverage.rst#exporting-coverage-data). +The Warnings Next Generation plugin supports the following Jenkins project types: -### 2. (Optional) Install Jenkins plugins which implement Code Coverage API plugin (cobertura-plugin, llvm-cov-plugin). -### 3. Enable "Publish Coverage Report" publisher in the Post-build Actions. -### 4. Add your coverage tool adapter and specify reports path. -![alt text](./images/config-add-adapter.png "Add coverage adapter") -### 5. (Optional) Use the [forensics-api](https://github.com/jenkinsci/forensics-api-plugin) plugin to discover the reference build that is used to compute a delta report of the coverage results. -### 6. (Optional) Specify Thresholds of each metrics in global or adapter level. -### 7. (Optional) Specify Source code storing level to enable source code navigation. -![alt text](./images/config.png "Config") +- Freestyle Project +- Maven Project +- Scripted Pipeline (sequential and parallel steps) +- Declarative Pipeline (sequential and parallel steps) +- Multi-branch Pipeline -## Pipeline example -We also support pipeline configuration, you can generate pipeline code in Jenkins Snippet Generator. +### Freestyle project -```groovy +Enable the "Record code coverage results" publisher in the Post-build Actions section of your job. Select at least one coverage tool and specify the path to the report file. If you do not specify a path, the plugin will search for the report file in the workspace using the default pattern of the tool. -publishCoverage adapters: [jacocoAdapter('target/site/jacoco/jacoco.xml')] +The plugin can compare the coverage results of the current build with the results of a reference build. The reference build will be discovered using the [forensics-api](https://github.com/jenkinsci/forensics-api-plugin) plugin. -``` -You can also use `jacoco` instead of `jacocoAdapter` if you didn't install Jacoco-Plugin. +### Pipeline example + +We also support pipeline configuration, you can generate pipeline code in Jenkins' Snippet Generator. -##### Parallel Pipeline Support -We support parallel pipeline. You can call the Code Coverage API plugin in different branches like this: ```groovy -node { - parallel firstBranch: { - publishCoverage adapters: [jacocoAdapter('target/site/jacoco/jacoco.xml')] -}, secondBranch: { - publishCoverage adapters: [jacocoAdapter('jacoco.xml')] - } -} -``` -##### Reports Combining Support -You can add tag on publishCoverage and Code Coverage API plugin will combine reports have same tag: -``` -node { - parallel firstBranch: { - publishCoverage adapters: [jacocoAdapter('target/site/jacoco/jacoco.xml')], tag: ‘t’ -}, secondBranch: { - publishCoverage adapters: [jacocoAdapter('jacoco.xml')], tag: ‘t’ - } -} -``` -##### Merging Reports -There is also a possibility to merge multiple reports (e.g. from multiple xml files) into one using the `mergeToOneReport` option with an ant-style path pattern. -All reports found by the adapter will then be combined into a single report: +recordCoverage(tools: [[parser: 'JACOCO']], + id: 'jacoco', name: 'JaCoCo Coverage', + sourceCodeRetention: 'EVERY_BUILD', + qualityGates: [ + [threshold: 60.0, metric: 'LINE', baseline: 'PROJECT', unstable: true], + [threshold: 60.0, metric: 'BRANCH', baseline: 'PROJECT', unstable: true]]) ``` -publishCoverage adapters: [jacocoAdapter(mergeToOneReport: true, path: '**/*.xml')] + +## Remote API + +We provide a remote API to retrieve coverage data, using the following URL: `https://[jenkins-url]/job/[job-name]/[build-number]/coverage/api/json?pretty=true`. + +Example output: +```json +{ + "_class" : "io.jenkins.plugins.coverage.metrics.steps.CoverageApi", + "modifiedFilesDelta" : { + "branch" : "+1.72%", + "class" : "-3.54%", + "complexity" : "-236", + "complexity-density" : "+0.47%", + "file" : "+0.00%", + "instruction" : "+0.16%", + "line" : "-0.48%", + "loc" : "-482", + "method" : "+1.23%", + "module" : "+0.00%", + "package" : "+0.00%" + }, + "modifiedFilesStatistics" : { + "branch" : "83.91%", + "class" : "93.33%", + "complexity" : "392", + "complexity-density" : "+50.19%", + "file" : "100.00%", + "instruction" : "88.19%", + "line" : "87.96%", + "loc" : "781", + "method" : "86.18%", + "module" : "100.00%", + "package" : "100.00%" + }, + "modifiedLinesDelta" : { + "branch" : "+8.95%", + "file" : "+0.00%", + "line" : "+3.85%", + "loc" : "-610", + "module" : "+0.00%", + "package" : "+0.00%" + }, + "modifiedLinesStatistics" : { + "branch" : "92.86%", + "file" : "100.00%", + "line" : "91.81%", + "loc" : "171", + "module" : "100.00%", + "package" : "100.00%" + }, + "projectDelta" : { + "branch" : "+4.43%", + "class" : "+2.94%", + "complexity" : "-8", + "complexity-density" : "+1.28%", + "file" : "+4.00%", + "instruction" : "+2.59%", + "line" : "+3.37%", + "loc" : "-50", + "method" : "+1.28%", + "module" : "+0.00%", + "package" : "+0.00%" + }, + "projectStatistics" : { + "branch" : "82.19%", + "class" : "96.88%", + "complexity" : "628", + "complexity-density" : "+49.72%", + "file" : "100.00%", + "instruction" : "88.03%", + "line" : "88.44%", + "loc" : "1263", + "method" : "84.94%", + "module" : "100.00%", + "package" : "100.00%" + }, + "qualityGates" : { + "overallResult" : "SUCCESS", + "resultItems" : [ + { + "qualityGate" : "Overall project - Line Coverage", + "result" : "SUCCESS", + "threshold" : 60.0, + "value" : "88.44%" + }, + { + "qualityGate" : "Overall project - Branch Coverage", + "result" : "SUCCESS", + "threshold" : 60.0, + "value" : "82.19%" + } + ] + }, + "referenceBuild" : "coverage-model-history #10" +} ``` -## REST API -We provide a REST API to retrieve coverage data: -- Coverage result: `…​/{buildNumber}/coverage/…​/result/api/\{json|xml\}?depth={number}` -- Trend result: `…​/{buildNumber}/coverage/…​/trend/api/\{json|xml\}?depth={number}` -- Coverage result of last build: `…​/{buildNumber}/coverage/…​/last/result/api/\{json|xml\}?depth={number}` -- Trend result of last build: `…​/{buildNumber}/coverage/…​/last/trend/api/\{json|xml\}?depth={number}` - -Note: The larger the number, the deeper of coverage information can be retrieved. diff --git a/plugin/pom.xml b/plugin/pom.xml index 7520e4a0c..ca65bc385 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -5,7 +5,7 @@ org.jvnet.hudson.plugins analysis-pom - 5.37.0 + 6.2.0 @@ -19,7 +19,7 @@ https://github.com/jenkinsci/code-coverage-api-plugin - 3.6.0 + 4.0.0 -SNAPSHOT jenkinsci/code-coverage-api-plugin @@ -30,11 +30,23 @@ 2.36.1 2.10.1 1.15.4 - 1.28.0-2 1.11 1.17.6 1.81 - 5.4.0-1 + 2.9.0 + + 0.18.0 + 1.29.0-3 + 2.0.0 + + 3.6.3-1 + 3.1.0 + 5.2.2-1 + 6.3.0-1 + 1.13.3-1 + 5.4.0-2 + + 2.0.0 @@ -58,6 +70,41 @@ + + edu.hm.hafner + coverage-model + ${coverage-model.version} + + + edu.hm.hafner + codingstyle + + + com.github.spotbugs + spotbugs-annotations + + + com.google.errorprone + error_prone_annotations + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-text + + + one.util + streamex + + + javax.annotation + javax.annotation-api + + + io.jenkins.plugins ionicons-api @@ -95,6 +142,14 @@ org.jenkins-ci.plugins.workflow workflow-multibranch + + org.jenkins-ci.plugins.workflow + workflow-api + + + org.jenkins-ci.plugins.workflow + workflow-step-api + org.jenkins-ci.plugins jackson2-api @@ -115,27 +170,32 @@ io.jenkins.plugins bootstrap5-api - 5.2.0-3 + ${bootstrap5-api.version} io.jenkins.plugins jquery3-api + ${jquery3-api.version} io.jenkins.plugins data-tables-api + ${data-tables-api.version} io.jenkins.plugins forensics-api + ${forensics-api.version} io.jenkins.plugins plugin-util-api + ${plugin-util-api.version} io.jenkins.plugins font-awesome-api + ${font-awesome-api.version} io.jenkins.plugins @@ -160,8 +220,40 @@ plugin-util-api tests test + ${plugin-util-api.version} + + + org.xmlunit + xmlunit-core + ${xmlunit.version} + test + + + net.bytebuddy + byte-buddy + + + jakarta.xml.bind + jakarta.xml.bind-api + + + + + org.xmlunit + xmlunit-assertj + ${xmlunit.version} + test + + + net.bytebuddy + byte-buddy + + + jakarta.xml.bind + jakarta.xml.bind-api + + - net.javacrumbs.json-unit json-unit-assertj @@ -174,11 +266,6 @@ - - org.jenkins-ci.plugins.workflow - workflow-step-api - test - org.jenkins-ci.plugins.workflow workflow-cps @@ -199,11 +286,6 @@ workflow-durable-task-step test - - org.jenkins-ci.plugins.workflow - workflow-api - test - org.jenkins-ci.plugins.workflow workflow-support @@ -238,7 +320,7 @@ io.jenkins.plugins git-forensics - 1.9.0 + ${git-forensics.version} test @@ -302,11 +384,14 @@ assertj-assertions-generator-maven-plugin + io.jenkins.plugins.coverage.metrics io.jenkins.plugins.coverage.model + .*ITest.* + .*CoverageXmlStream.* - io.jenkins.plugins.coverage.model + io.jenkins.plugins.coverage.metrics diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/CoverageElementInitializer.java b/plugin/src/main/java/io/jenkins/plugins/coverage/CoverageElementInitializer.java index d05ba9c94..efd640e7f 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/CoverageElementInitializer.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/CoverageElementInitializer.java @@ -3,11 +3,13 @@ import hudson.DescriptorExtensionList; import hudson.init.InitMilestone; import hudson.init.Initializer; + import io.jenkins.plugins.coverage.adapter.CoverageAdapter; import io.jenkins.plugins.coverage.adapter.CoverageAdapterDescriptor; import io.jenkins.plugins.coverage.adapter.CoverageReportAdapterDescriptor; import io.jenkins.plugins.coverage.targets.CoverageElementRegister; +@SuppressWarnings("unchecked") public class CoverageElementInitializer { @Initializer(after = InitMilestone.PLUGINS_STARTED) diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/CoveragePublisher.java b/plugin/src/main/java/io/jenkins/plugins/coverage/CoveragePublisher.java index eee9fbc55..3668c52ad 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/CoveragePublisher.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/CoveragePublisher.java @@ -46,10 +46,11 @@ import io.jenkins.plugins.coverage.source.DefaultSourceFileResolver; import io.jenkins.plugins.coverage.source.SourceFileResolver; import io.jenkins.plugins.coverage.threshold.Threshold; -import io.jenkins.plugins.prism.CharsetValidation; import io.jenkins.plugins.prism.SourceCodeDirectory; import io.jenkins.plugins.util.JenkinsFacade; +import io.jenkins.plugins.util.ValidationUtilities; +@SuppressWarnings("unchecked") public class CoveragePublisher extends Recorder implements SimpleBuildStep { private static final String CHECKS_DEFAULT_NAME = "Code Coverage"; private List adapters = new LinkedList<>(); @@ -81,6 +82,7 @@ public class CoveragePublisher extends Recorder implements SimpleBuildStep { public CoveragePublisher() { } + @SuppressWarnings("deprecation") @Override public void perform(@NonNull final Run run, @NonNull final FilePath workspace, @NonNull final Launcher launcher, @NonNull final TaskListener listener) @@ -337,7 +339,7 @@ protected Object readResolve() { public static final class CoveragePublisherDescriptor extends BuildStepDescriptor { private static final JenkinsFacade JENKINS = new JenkinsFacade(); - private final CharsetValidation validation = new CharsetValidation(); + private final ValidationUtilities validation = new ValidationUtilities(); public CoveragePublisherDescriptor() { super(CoveragePublisher.class); diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/CoverageSeriesBuilder.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/CoverageSeriesBuilder.java new file mode 100644 index 000000000..4788b6600 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/CoverageSeriesBuilder.java @@ -0,0 +1,42 @@ +package io.jenkins.plugins.coverage.metrics.charts; + +import java.util.HashMap; +import java.util.Map; + +import edu.hm.hafner.coverage.Coverage; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.echarts.line.SeriesBuilder; + +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics; + +/** + * Builds one x-axis point for the series of a line chart showing the line and branch coverage of a project. + * + * @author Ullrich Hafner + */ +public class CoverageSeriesBuilder extends SeriesBuilder { + static final String LINE_COVERAGE = "line"; + static final String BRANCH_COVERAGE = "branch"; + static final String MUTATION_COVERAGE = "mutation"; + + @Override + protected Map computeSeries(final CoverageStatistics statistics) { + Map series = new HashMap<>(); + + series.put(LINE_COVERAGE, getRoundedPercentage(statistics, Metric.LINE)); + if (statistics.containsValue(Baseline.PROJECT, Metric.BRANCH)) { + series.put(BRANCH_COVERAGE, getRoundedPercentage(statistics, Metric.BRANCH)); + } + if (statistics.containsValue(Baseline.PROJECT, Metric.MUTATION)) { + series.put(MUTATION_COVERAGE, getRoundedPercentage(statistics, Metric.MUTATION)); + } + return series; + } + + private double getRoundedPercentage(final CoverageStatistics statistics, final Metric metric) { + Coverage coverage = (Coverage) statistics.getValue(Baseline.PROJECT, metric) + .orElse(Coverage.nullObject(metric)); + return (coverage.getCoveredPercentage().toDouble() / 100.0) * 100.0; + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/CoverageTrendChart.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/CoverageTrendChart.java new file mode 100644 index 000000000..a2f2ae4d0 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/CoverageTrendChart.java @@ -0,0 +1,68 @@ +package io.jenkins.plugins.coverage.metrics.charts; + +import edu.hm.hafner.echarts.BuildResult; +import edu.hm.hafner.echarts.ChartModelConfiguration; +import edu.hm.hafner.echarts.JacksonFacade; +import edu.hm.hafner.echarts.Palette; +import edu.hm.hafner.echarts.line.LineSeries; +import edu.hm.hafner.echarts.line.LineSeries.FilledMode; +import edu.hm.hafner.echarts.line.LineSeries.StackedMode; +import edu.hm.hafner.echarts.line.LinesChartModel; +import edu.hm.hafner.echarts.line.LinesDataSet; + +import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics; +import io.jenkins.plugins.coverage.metrics.model.Messages; + +/** + * Builds the Java side model for a trend chart showing the line and branch coverage of a project. The number of builds + * to consider is controlled by a {@link ChartModelConfiguration} instance. The created model object can be serialized + * to JSON (e.g., using the {@link JacksonFacade}) and can be used 1:1 as ECharts configuration object in the + * corresponding JS file. + * + * @author Ullrich Hafner + * @see JacksonFacade + */ +public class CoverageTrendChart { + /** + * Creates the chart for the specified results. + * + * @param results + * the forensics results to render - these results must be provided in descending order, i.e. the current * + * build is the head of the list, then the previous builds, and so on + * @param configuration + * the chart configuration to be used + * + * @return the chart model, ready to be serialized to JSON + */ + public LinesChartModel create(final Iterable> results, + final ChartModelConfiguration configuration) { + CoverageSeriesBuilder builder = new CoverageSeriesBuilder(); + LinesDataSet dataSet = builder.createDataSet(configuration, results); + + LinesChartModel model = new LinesChartModel(dataSet); + if (dataSet.isNotEmpty()) { + LineSeries lineSeries = new LineSeries(Messages.Metric_LINE(), + Palette.GREEN.getNormal(), StackedMode.SEPARATE_LINES, FilledMode.FILLED, + dataSet.getSeries(CoverageSeriesBuilder.LINE_COVERAGE)); + model.addSeries(lineSeries); + model.useContinuousRangeAxis(); + model.setRangeMax(100); + model.setRangeMin(dataSet.getMinimumValue()); + + addSecondSeries(dataSet, model, Messages.Metric_BRANCH(), CoverageSeriesBuilder.BRANCH_COVERAGE); + addSecondSeries(dataSet, model, Messages.Metric_MUTATION(), CoverageSeriesBuilder.MUTATION_COVERAGE); + } + return model; + } + + private static void addSecondSeries(final LinesDataSet dataSet, final LinesChartModel model, + final String name, final String seriesId) { + if (dataSet.containsSeries(seriesId)) { + LineSeries branchSeries = new LineSeries(name, + Palette.GREEN.getHover(), StackedMode.SEPARATE_LINES, FilledMode.FILLED, + dataSet.getSeries(seriesId)); + + model.addSeries(branchSeries); + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/TreeMapNodeConverter.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/TreeMapNodeConverter.java new file mode 100644 index 000000000..9fcb5f80c --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/TreeMapNodeConverter.java @@ -0,0 +1,101 @@ +package io.jenkins.plugins.coverage.metrics.charts; + +import java.util.Optional; + +import edu.hm.hafner.coverage.Coverage; +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.ModuleNode; +import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.echarts.ItemStyle; +import edu.hm.hafner.echarts.Label; +import edu.hm.hafner.echarts.LabeledTreeMapNode; +import edu.hm.hafner.echarts.TreeMapNode; + +import io.jenkins.plugins.coverage.metrics.color.ColorProvider; +import io.jenkins.plugins.coverage.metrics.color.ColorProvider.DisplayColors; +import io.jenkins.plugins.coverage.metrics.color.CoverageLevel; +import io.jenkins.plugins.coverage.metrics.model.ElementFormatter; + +/** + * Converts a tree of {@link Node coverage nodes} to a corresponding tree of + * {@link TreeMapNode ECharts tree map nodes}. + * + * @author Ullrich Hafner + */ +public class TreeMapNodeConverter { + private static final ElementFormatter FORMATTER = new ElementFormatter(); + + /** + * Converts a coverage tree of {@link Node nodes} to an ECharts tree map of {@link TreeMapNode}. + * + * @param node + * The root node of the tree to be converted + * @param metric + * The coverage metric that should be represented (line and branch coverage are available) + * @param colorProvider + * Provides the colors to be used for highlighting the tree nodes + * + * @return the converted tree map representation + */ + public LabeledTreeMapNode toTreeChartModel(final Node node, final Metric metric, final ColorProvider colorProvider) { + var tree = mergePackages(node); + LabeledTreeMapNode root = toTreeMapNode(tree, metric, colorProvider).orElse( + new LabeledTreeMapNode(node.getPath(), node.getName())); + for (LabeledTreeMapNode child : root.getChildren()) { + child.collapseEmptyPackages(); + } + + return root; + } + + private Node mergePackages(final Node node) { + if (node instanceof ModuleNode) { + ModuleNode copy = (ModuleNode) node.copyTree(); + copy.splitPackages(); + return copy; + } + return node; + } + + private Optional toTreeMapNode(final Node node, final Metric metric, + final ColorProvider colorProvider) { + var value = node.getValue(metric); + if (value.isPresent()) { + var rootValue = value.get(); + if (rootValue instanceof Coverage) { + return Optional.of(createCoverageTree((Coverage) rootValue, colorProvider, node, metric)); + } + } + + return Optional.empty(); + } + + private LabeledTreeMapNode createCoverageTree(final Coverage coverage, final ColorProvider colorProvider, final Node node, + final Metric metric) { + double coveragePercentage = coverage.getCoveredPercentage().toDouble(); + + DisplayColors colors = CoverageLevel.getDisplayColorsOfCoverageLevel(coveragePercentage, colorProvider); + String lineColor = colors.getLineColorAsRGBHex(); + String fillColor = colors.getFillColorAsRGBHex(); + + Label label = new Label(true, lineColor); + Label upperLabel = new Label(true, lineColor); + + if (node instanceof FileNode) { + return new LabeledTreeMapNode(node.getPath(), node.getName(), new ItemStyle(fillColor), label, upperLabel, + String.valueOf(coverage.getTotal()), FORMATTER.getTooltip(coverage)); + } + + ItemStyle packageStyle = new ItemStyle(fillColor, fillColor, 4); + LabeledTreeMapNode treeNode = new LabeledTreeMapNode(node.getPath(), node.getName(), packageStyle, label, upperLabel, + String.valueOf(coverage.getTotal()), FORMATTER.getTooltip(coverage)); + + node.getChildren().stream() + .map(n -> toTreeMapNode(n, metric, colorProvider)) + .flatMap(Optional::stream) + .forEach(treeNode::insertNode); + + return treeNode; + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/package-info.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/package-info.java new file mode 100644 index 000000000..b4ed85415 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/package-info.java @@ -0,0 +1,8 @@ +/** + * Contains logic and models for visualizing code coverage charts. + */ +@DefaultAnnotation(NonNull.class) +package io.jenkins.plugins.coverage.metrics.charts; + +import edu.umd.cs.findbugs.annotations.DefaultAnnotation; +import edu.umd.cs.findbugs.annotations.NonNull; diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/ColorId.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/ColorId.java new file mode 100644 index 000000000..05689f8b5 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/ColorId.java @@ -0,0 +1,20 @@ +package io.jenkins.plugins.coverage.metrics.color; + +/** + * Provides IDs for colors which are used within this plugin in order to separate the color palette from the logic. + * + * @author Florian Orendi + */ +public enum ColorId { + INSUFFICIENT, + VERY_BAD, + BAD, + INADEQUATE, + AVERAGE, + GOOD, + VERY_GOOD, + EXCELLENT, + + BLACK, + WHITE +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/ColorProvider.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/ColorProvider.java new file mode 100644 index 000000000..f3a4a7d13 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/ColorProvider.java @@ -0,0 +1,242 @@ +package io.jenkins.plugins.coverage.metrics.color; + +import java.awt.*; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Loads a color palette and provides these colors and operations on them. The colors are provided as a tuple of fill + * color and line color, mapped by the id of the fill color. + * + * @author Florian Orendi + */ +public class ColorProvider { + + /** + * Default color that is provided if no color is found in order to guarantee a proper colorization. + */ + public static final DisplayColors DEFAULT_COLOR = new DisplayColors(Color.black, Color.white); + + static final String BLEND_COLOR_ERROR_MESSAGE = "Color weights have to be greater or equal to zero"; + + /** + * The available {@link DisplayColors display colors} are mapped by the {@link ColorId id} of the fill color. + */ + private final Map availableColors; + + /** + * Creates a color provider which uses the passed colors. Each color entry contains a background and a fitting text + * color. + * + * @param colorMapping + * The color mapping to be used + */ + ColorProvider(final Map colorMapping) { + availableColors = new HashMap<>(colorMapping); + } + + /** + * Blends two colors. + * + * @param color1 + * The first color + * @param color2 + * The second color + * + * @return the blended color + */ + public static Color blendColors(final Color color1, final Color color2) { + return blendWeightedColors(color1, color2, 1, 1); + } + + /** + * Blends two colors using weights that have to be greater then zero. + * + * @param color1 + * The first color + * @param color2 + * The second color + * @param weight1 + * The weight of the first color + * @param weight2 + * The weight of the second color + * + * @return the blended color + */ + public static Color blendWeightedColors(@NonNull final Color color1, @NonNull final Color color2, + final double weight1, final double weight2) { + if (weight1 >= 0 && weight2 >= 0) { + final double total = weight1 + weight2; + final int r = (int) ((color1.getRed() * weight1 + color2.getRed() * weight2) / total); + final int g = (int) ((color1.getGreen() * weight1 + color2.getGreen() * weight2) / total); + final int b = (int) ((color1.getBlue() * weight1 + color2.getBlue() * weight2) / total); + final int a = (int) ((color1.getAlpha() * weight1 + color2.getAlpha() * weight2) / total); + return new Color(r, g, b, a); + } + throw new IllegalArgumentException(BLEND_COLOR_ERROR_MESSAGE); + } + + /** + * Provides the RGBA hex string of the passed color. + * + * @param color + * The {@link Color} + * @param alpha + * The alpha value within the range [0;255] + * + * @return the color as a hex string + */ + public static String colorAsRGBAHex(final Color color, final int alpha) { + return String.format("#%02X%02X%02X%02X", color.getRed(), color.getGreen(), color.getBlue(), alpha); + } + + /** + * Provides the RGB hex string of the passed color. + * + * @param color + * The {@link Color} + * + * @return the color as a hex string + */ + public static String colorAsRGBHex(final Color color) { + return String.format("#%02X%02X%02X", color.getRed(), color.getGreen(), color.getBlue()); + } + + /** + * Returns the {@link DisplayColors display colors} for the passed id. + * + * @param colorId + * The ID of the fill color + * + * @return the display colors or the {@link #DEFAULT_COLOR default color} if no color has been found + */ + public DisplayColors getDisplayColorsOf(final ColorId colorId) { + if (containsColorId(colorId)) { + return availableColors.get(colorId); + } + return DEFAULT_COLOR; + } + + /** + * Checks whether the provider contains {@link DisplayColors display colors} for the passed id. + * + * @param colorId + * The color id that should be checked + * + * @return {@code true} whether the id is available, else {@code false} + */ + public boolean containsColorId(final ColorId colorId) { + return availableColors.containsKey(colorId); + } + + /** + * Gets the blended {@link DisplayColors display colors} in dependence of the passed weights for each colors. + * + * @param weightFirst + * The weight of the first colors + * @param weightSecond + * The weight of the second colors + * @param first + * The first display colors + * @param second + * The second display colors + * + * @return the blended display colors or the {@link #DEFAULT_COLOR} if one color has not been found + */ + public DisplayColors getBlendedDisplayColors(final double weightFirst, final double weightSecond, + final ColorId first, final ColorId second) { + if (containsColorId(first) && containsColorId(second)) { + DisplayColors firstColor = getDisplayColorsOf(first); + DisplayColors secondColor = getDisplayColorsOf(second); + Color lineColor; + if (weightFirst > weightSecond) { + lineColor = firstColor.lineColor; + } + else { + lineColor = secondColor.lineColor; + } + Color fillColor = blendWeightedColors(firstColor.fillColor, secondColor.fillColor, weightFirst, + weightSecond); + return new DisplayColors(lineColor, fillColor); + } + return DEFAULT_COLOR; + } + + /** + * Wraps the fill color and the line color that should be used in order to visualize coverage values. + * + * @author Florian Orendi + */ + public static class DisplayColors { + + private final Color lineColor; + private final Color fillColor; + + /** + * Creates a wrapper for the colors used for displaying values. + * + * @param lineColor + * The used line color + * @param fillColor + * The used fill color + */ + public DisplayColors(final Color lineColor, final Color fillColor) { + this.lineColor = lineColor; + this.fillColor = fillColor; + } + + public Color getLineColor() { + return lineColor; + } + + public Color getFillColor() { + return fillColor; + } + + /** + * Gets the fill color with the passed alpha value. Using a low alpha value might require using another line + * color than the provided {@link #lineColor}. + * + * @param alpha + * The color alpha + * + * @return the hex code which contains the alpha value + */ + public String getFillColorAsRGBAHex(final int alpha) { + return colorAsRGBAHex(fillColor, alpha); + } + + public String getLineColorAsRGBHex() { + return colorAsRGBHex(lineColor); + } + + public String getFillColorAsRGBHex() { + return colorAsRGBHex(fillColor); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DisplayColors that = (DisplayColors) o; + return Objects.equals(lineColor, that.lineColor) && Objects.equals(fillColor, that.fillColor); + } + + @Override + public int hashCode() { + return Objects.hash(lineColor, fillColor); + } + + @Override + public String toString() { + return String.format("%s - %s", lineColor, fillColor); + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/ColorProviderFactory.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/ColorProviderFactory.java new file mode 100644 index 000000000..bfb930c46 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/ColorProviderFactory.java @@ -0,0 +1,127 @@ +package io.jenkins.plugins.coverage.metrics.color; + +import java.awt.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import io.jenkins.plugins.coverage.metrics.color.ColorProvider.DisplayColors; + +/** + * Provides factory methods for creating different {@link ColorProvider color providers}. + * + * @author Florian Orendi + */ +public class ColorProviderFactory { + + private ColorProviderFactory() { + // prevents initialization + } + + /** + * Creates a {@link ColorProvider color provider} which uses the internal + * {@link CoverageColorPalette color palette}. + * + * @return the created color provider + */ + public static ColorProvider createDefaultColorProvider() { + return new ColorProvider(getDefaultColors()); + } + + /** + * Creates a {@link ColorProvider color provider} which uses the set Jenkins colors. Required color keys are: + * '--green', '--light-green', '--yellow', '--light-yellow', '--orange', '--light-orange', '--red', '--light-red' - + * see {@link CoverageColorJenkinsId}. If colors are missing, the internal default colors are used - see + * {@link CoverageColorPalette}. + * + * @param colors + * Maps {@link CoverageColorJenkinsId jenkins color IDs} + * + * @return the created color provider + */ + public static ColorProvider createColorProvider(final Map colors) { + if (!colors.keySet().equals(CoverageColorJenkinsId.getAll()) || !verifyHexCodes(colors.values())) { + return createDefaultColorProvider(); + } + Map colorMap = new HashMap<>(); + // TODO: use dynamic text color (not provided yet) + colorMap.put(ColorId.INSUFFICIENT, + createDisplayColor(colors.get(CoverageColorJenkinsId.RED.getJenkinsColorId()), "#ffffff")); + colorMap.put(ColorId.VERY_BAD, + createDisplayColor(colors.get(CoverageColorJenkinsId.LIGHT_RED.getJenkinsColorId()), "#ffffff")); + colorMap.put(ColorId.BAD, + createDisplayColor(colors.get(CoverageColorJenkinsId.ORANGE.getJenkinsColorId()), "#000000")); + colorMap.put(ColorId.INADEQUATE, + createDisplayColor(colors.get(CoverageColorJenkinsId.LIGHT_ORANGE.getJenkinsColorId()), "#000000")); + colorMap.put(ColorId.AVERAGE, + createDisplayColor(colors.get(CoverageColorJenkinsId.YELLOW.getJenkinsColorId()), "#000000")); + colorMap.put(ColorId.GOOD, + createDisplayColor(colors.get(CoverageColorJenkinsId.LIGHT_YELLOW.getJenkinsColorId()), "#000000")); + colorMap.put(ColorId.VERY_GOOD, + createDisplayColor(colors.get(CoverageColorJenkinsId.LIGHT_GREEN.getJenkinsColorId()), "#000000")); + colorMap.put(ColorId.EXCELLENT, + createDisplayColor(colors.get(CoverageColorJenkinsId.GREEN.getJenkinsColorId()), "#ffffff")); + colorMap.put(ColorId.BLACK, createDisplayColor(CoverageColorPalette.BLACK)); + colorMap.put(ColorId.WHITE, createDisplayColor(CoverageColorPalette.WHITE)); + return new ColorProvider(colorMap); + } + + /** + * Loads the internally usable {@link CoverageColorPalette color palette}. This can be also used as a fallback. + * + * @return the default color mapping + */ + private static Map getDefaultColors() { + return Arrays.stream(CoverageColorPalette.values()) + .collect(Collectors.toMap(CoverageColorPalette::getColorId, ColorProviderFactory::createDisplayColor)); + } + + /** + * Verifies that all passed strings are color hex codes. + * + * @param hexCodes + * The strings to be investigated + * + * @return {@code true} if all strings are hex codes + */ + private static boolean verifyHexCodes(final Collection hexCodes) { + Pattern hexPattern = Pattern.compile("^#[A-Fa-f0-9]{6}$"); + for (String hex : hexCodes) { + if (!hexPattern.matcher(hex).matches()) { + return false; + } + } + return true; + } + + /** + * Creates a pair of {@link DisplayColors display colors} from the {@link CoverageColorPalette}. + * + * @param color + * The passed palette color + * + * @return the created display color + */ + private static DisplayColors createDisplayColor(final CoverageColorPalette color) { + return new DisplayColors(color.getLineColor(), color.getFillColor()); + } + + /** + * Creates a pair of {@link DisplayColors display colors} from the passed hex colors. + * + * @param backgroundColorHex + * The hex code of the background color + * @param textColorHex + * The hex code of the text color + * + * @return the created display color + */ + private static DisplayColors createDisplayColor(final String backgroundColorHex, final String textColorHex) { + Color backgroundColor = Color.decode(backgroundColorHex); + Color textColor = Color.decode(textColorHex); + return new DisplayColors(textColor, backgroundColor); + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/ColorScheme.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/ColorScheme.java new file mode 100644 index 000000000..b364f35e3 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/ColorScheme.java @@ -0,0 +1,22 @@ +package io.jenkins.plugins.coverage.metrics.color; + +/** + * Represents different types of color schemes that can be selected in order to load the matching color palette. + * + * @author Florian Orendi + */ +public enum ColorScheme { + + /** + * The default colors. + */ + DEFAULT, + /** + * Colors which are used if the dark mode is activated. + */ + DARK_MODE, + /** + * Colors which are usable in case of color blindness. + */ + COLOR_BLINDNESS +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/CoverageChangeLevel.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/CoverageChangeLevel.java new file mode 100644 index 000000000..40db598d2 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/CoverageChangeLevel.java @@ -0,0 +1,72 @@ +package io.jenkins.plugins.coverage.metrics.color; + +import edu.umd.cs.findbugs.annotations.NonNull; + +import io.jenkins.plugins.coverage.metrics.color.ColorProvider.DisplayColors; + +/** + * Provides the colorization for different coverage change levels. + * + * @author Florian Orendi + */ +public enum CoverageChangeLevel { + + INCREASE_5(5.0, ColorId.EXCELLENT), + INCREASE_2(2.0, ColorId.VERY_GOOD), + EQUALS(0.0, ColorId.AVERAGE), + DECREASE_2(-2.0, ColorId.INADEQUATE), + DECREASE_5(-5.0, ColorId.BAD), + DECREASE_10(-10.0, ColorId.VERY_BAD), + DECREASE_20(-20.0, ColorId.INSUFFICIENT), + NA(-100.0, ColorId.WHITE); + + private final double change; + private final ColorId colorizationId; + + CoverageChangeLevel(final double change, final ColorId colorizationId) { + this.change = change; + this.colorizationId = colorizationId; + } + + /** + * Gets the {@link DisplayColors display colors} for representing the passed coverage change. If the change is + * placed between two levels, the fill colors are blended. + * + * @param coverageDifference + * The coverage change + * @param colorProvider + * The {@link ColorProvider color provider} to be used + * + * @return the display colors + */ + public static DisplayColors getDisplayColorsOfCoverageChange(final double coverageDifference, + @NonNull final ColorProvider colorProvider) { + for (int i = 0; i < values().length - 1; i++) { + CoverageChangeLevel level = values()[i]; + if (coverageDifference >= level.change) { + if (i == 0) { + return colorProvider.getDisplayColorsOf(level.colorizationId); + } + double distanceLevel = coverageDifference - level.change; + if (distanceLevel == 0) { + return colorProvider.getDisplayColorsOf(level.colorizationId); + } + CoverageChangeLevel upperLevel = values()[i - 1]; + double distanceUpper = upperLevel.change - coverageDifference; + return colorProvider.getBlendedDisplayColors( + distanceLevel, distanceUpper, + upperLevel.colorizationId, + level.colorizationId); + } + } + return colorProvider.getDisplayColorsOf(NA.colorizationId); + } + + public double getChange() { + return change; + } + + public ColorId getColorizationId() { + return colorizationId; + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/CoverageChangeTendency.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/CoverageChangeTendency.java new file mode 100644 index 000000000..4bc18382b --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/CoverageChangeTendency.java @@ -0,0 +1,56 @@ +package io.jenkins.plugins.coverage.metrics.color; + +import edu.umd.cs.findbugs.annotations.NonNull; + +import io.jenkins.plugins.coverage.metrics.color.ColorProvider.DisplayColors; + +/** + * Provides the colorization for different coverage change tendencies. + * + * @author Florian Orendi + */ +public enum CoverageChangeTendency { + + INCREASED(ColorId.EXCELLENT), + EQUALS(ColorId.AVERAGE), + DECREASED(ColorId.INSUFFICIENT), + NA(ColorId.WHITE); + + private final ColorId colorizationId; + + CoverageChangeTendency(final ColorId colorizationId) { + this.colorizationId = colorizationId; + } + + /** + * Provides the {@link DisplayColors display colors} which match with the passed coverage change tendency. + * + * @param change + * The coverage change + * @param colorProvider + * The {@link ColorProvider color provider} to be used + * + * @return the matching change level + */ + public static DisplayColors getDisplayColorsForTendency(final Double change, + @NonNull final ColorProvider colorProvider) { + ColorId colorId; + if (change == null || change.isNaN()) { + colorId = NA.colorizationId; + } + else if (change > 0) { + colorId = INCREASED.colorizationId; + } + else if (change < 0) { + colorId = DECREASED.colorizationId; + } + else { + colorId = EQUALS.colorizationId; + } + return colorProvider.getDisplayColorsOf(colorId); + } + + public ColorId getColorizationId() { + return colorizationId; + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/CoverageColorJenkinsId.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/CoverageColorJenkinsId.java new file mode 100644 index 000000000..ec4bed30f --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/CoverageColorJenkinsId.java @@ -0,0 +1,39 @@ +package io.jenkins.plugins.coverage.metrics.color; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Contains color IDs which represent the keys of a JSON object that is dynamically filled with the currently set + * Jenkins colors. + * + * @author Florian Orendi + */ +public enum CoverageColorJenkinsId { + + GREEN("--green"), + LIGHT_GREEN("--light-green"), + YELLOW("--yellow"), + LIGHT_YELLOW("--light-yellow"), + ORANGE("--orange"), + LIGHT_ORANGE("--light-orange"), + RED("--red"), + LIGHT_RED("--light-red"); + + private final String jenkinsColorId; + + CoverageColorJenkinsId(final String colorId) { + this.jenkinsColorId = colorId; + } + + public String getJenkinsColorId() { + return jenkinsColorId; + } + + public static Set getAll() { + return Arrays.stream(values()) + .map(id -> id.jenkinsColorId) + .collect(Collectors.toSet()); + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/CoverageColorPalette.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/CoverageColorPalette.java new file mode 100644 index 000000000..a0bbf143e --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/CoverageColorPalette.java @@ -0,0 +1,50 @@ +package io.jenkins.plugins.coverage.metrics.color; + +import java.awt.*; + +/** + * Provides a color palette which can be used as a plugin internal fallback if no other color schemes have been defined. + * The defined colors correspond to the Jenkins Design + * Library. + * + * @author Florian Orendi + */ +public enum CoverageColorPalette { + + WHITE(ColorId.WHITE, new Color(255, 255, 255), new Color(0, 0, 0)), + BLACK(ColorId.BLACK, new Color(0, 0, 0), new Color(255, 255, 255)), + + RED(ColorId.INSUFFICIENT, new Color(230, 0, 31), new Color(255, 255, 255)), + LIGHT_RED(ColorId.VERY_BAD, new Color(255, 77, 101), new Color(255, 255, 255)), + + ORANGE(ColorId.BAD, new Color(254, 130, 10), new Color(0, 0, 0)), + LIGHT_ORANGE(ColorId.INADEQUATE, new Color(254, 182, 112), new Color(0, 0, 0)), + + YELLOW(ColorId.AVERAGE, new Color(255, 204, 0), new Color(0, 0, 0)), + LIGHT_YELLOW(ColorId.GOOD, new Color(255, 224, 102), new Color(0, 0, 0)), + + LIGHT_GREEN(ColorId.VERY_GOOD, new Color(75, 223, 124), new Color(0, 0, 0)), + GREEN(ColorId.EXCELLENT, new Color(30, 166, 75), new Color(255, 255, 255)); + + private final ColorId colorId; + private final Color fillColor; + private final Color lineColor; + + CoverageColorPalette(final ColorId colorId, final Color fillColor, final Color lineColor) { + this.colorId = colorId; + this.fillColor = fillColor; + this.lineColor = lineColor; + } + + public ColorId getColorId() { + return colorId; + } + + public Color getFillColor() { + return fillColor; + } + + public Color getLineColor() { + return lineColor; + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/CoverageLevel.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/CoverageLevel.java new file mode 100644 index 000000000..a96f4bff2 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/CoverageLevel.java @@ -0,0 +1,91 @@ +package io.jenkins.plugins.coverage.metrics.color; + +import edu.umd.cs.findbugs.annotations.NonNull; + +import io.jenkins.plugins.coverage.metrics.color.ColorProvider.DisplayColors; + +/** + * Provides the colorization for different coverage levels. + * + * @author Florian Orendi + */ +public enum CoverageLevel { + + LVL_95(95.0, ColorId.EXCELLENT), + LVL_90(90.0, ColorId.VERY_GOOD), + LVL_80(85.0, ColorId.GOOD), + LVL_75(80.0, ColorId.AVERAGE), + LVL_70(70.0, ColorId.INADEQUATE), + LVL_60(60.0, ColorId.BAD), + LVL_50(50.0, ColorId.VERY_BAD), + LVL_0(0.0, ColorId.INSUFFICIENT), + NA(-1.0, ColorId.WHITE); + + private final double level; + private final ColorId colorizationId; + + CoverageLevel(final double level, final ColorId colorizationId) { + this.level = level; + this.colorizationId = colorizationId; + } + + /** + * Gets the {@link DisplayColors display colors} for representing the passed coverage amount. If the value is placed + * between two levels, the fill colors are blended. + * + * @param coveragePercentage + * The coverage percentage + * @param colorProvider + * The {@link ColorProvider color provider} to be used + * + * @return the display colors + */ + public static DisplayColors getDisplayColorsOfCoverageLevel(final double coveragePercentage, + @NonNull final ColorProvider colorProvider) { + if (coveragePercentage >= 0) { + return getBlendedColors(coveragePercentage, colorProvider); + } + return colorProvider.getDisplayColorsOf(NA.colorizationId); + } + + /** + * Gets the blended {@link DisplayColors display colors} for representing the passed coverage amount. + * + * @param coveragePercentage + * The coverage percentage + * @param colorProvider + * The {@link ColorProvider color provider} to be used + * + * @return the blended display colors + */ + private static DisplayColors getBlendedColors(final double coveragePercentage, + @NonNull final ColorProvider colorProvider) { + for (int i = 0; i < values().length - 1; i++) { + CoverageLevel level = values()[i]; + if (coveragePercentage >= level.level) { + if (i == 0) { + return colorProvider.getDisplayColorsOf(level.colorizationId); + } + double distanceLevel = coveragePercentage - level.level; + if (distanceLevel == 0) { + return colorProvider.getDisplayColorsOf(level.colorizationId); + } + CoverageLevel upperLevel = values()[i - 1]; + double distanceUpper = upperLevel.level - coveragePercentage; + return colorProvider.getBlendedDisplayColors( + distanceLevel, distanceUpper, + upperLevel.colorizationId, + level.colorizationId); + } + } + return colorProvider.getDisplayColorsOf(NA.colorizationId); + } + + public double getLevel() { + return level; + } + + public ColorId getColorizationId() { + return colorizationId; + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/package-info.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/package-info.java new file mode 100644 index 000000000..9d98b9cd7 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/color/package-info.java @@ -0,0 +1,8 @@ +/** + * Provides colors and operations on them to be used in order to visualize coverage. + */ +@DefaultAnnotation(NonNull.class) +package io.jenkins.plugins.coverage.metrics.color; + +import edu.umd.cs.findbugs.annotations.DefaultAnnotation; +import edu.umd.cs.findbugs.annotations.NonNull; diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/Baseline.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/Baseline.java new file mode 100644 index 000000000..709440405 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/Baseline.java @@ -0,0 +1,85 @@ +package io.jenkins.plugins.coverage.metrics.model; + +import java.util.function.BiFunction; + +import org.jvnet.localizer.Localizable; + +import io.jenkins.plugins.coverage.metrics.color.ColorProvider; +import io.jenkins.plugins.coverage.metrics.color.ColorProvider.DisplayColors; +import io.jenkins.plugins.coverage.metrics.color.CoverageChangeTendency; +import io.jenkins.plugins.coverage.metrics.color.CoverageLevel; + +/** + * The baseline for the code coverage computation. + */ +public enum Baseline { + /** + * Coverage of the whole project. This is an absolute value that might not change much from build to build. + */ + PROJECT(Messages._Baseline_PROJECT(), "overview", CoverageLevel::getDisplayColorsOfCoverageLevel), + /** + * Difference between the project coverages of the current build and the reference build. Teams can use this delta + * value to ensure that the coverage will not decrease. + */ + PROJECT_DELTA(Messages._Baseline_PROJECT_DELTA(), "overview", + CoverageChangeTendency::getDisplayColorsForTendency), + /** + * Coverage of the modified lines (e.g., within the modified lines of a pull or merge request) will focus on new or + * modified code only. + */ + MODIFIED_LINES(Messages._Baseline_MODIFIED_LINES(), "modifiedLinesCoverage", CoverageLevel::getDisplayColorsOfCoverageLevel), + /** + * Difference between the project coverage and the modified lines coverage of the current build. Teams can use this delta + * value to ensure that the coverage of pull requests is better than the whole project coverage. + */ + MODIFIED_LINES_DELTA(Messages._Baseline_MODIFIED_LINES_DELTA(), "modifiedLinesCoverage", + CoverageChangeTendency::getDisplayColorsForTendency), + /** + * Coverage of the modified files (e.g., within the files that have been touched in a pull or merge request) will + * focus on new or modified code only. + */ + MODIFIED_FILES(Messages._Baseline_MODIFIED_FILES(), "modifiedFilesCoverage", CoverageLevel::getDisplayColorsOfCoverageLevel), + /** + * Difference between the project coverage and the modified file coverage of the current build. Teams can use this delta + * value to ensure that the coverage of pull requests is better than the whole project coverage. + */ + MODIFIED_FILES_DELTA(Messages._Baseline_MODIFIED_FILES_DELTA(), "modifiedFilesCoverage", CoverageChangeTendency::getDisplayColorsForTendency), + /** + * Indirect changes of the overall code coverage that are not part of the changed code. These changes might occur, + * if new tests will be added without touching the underlying code under test. + */ + INDIRECT(Messages._Baseline_INDIRECT(), "indirectCoverage", CoverageLevel::getDisplayColorsOfCoverageLevel); + + private final Localizable title; + private final String url; + private final BiFunction colorMapper; + + Baseline(final Localizable title, final String url, + final BiFunction colorMapper) { + this.title = title; + this.url = url; + this.colorMapper = colorMapper; + } + + public String getTitle() { + return title.toString(); + } + + public String getUrl() { + return "#" + url; + } + + /** + * Returns the display colors to use render a value of this baseline. + * + * @param value + * the value to render + * @param colorProvider + * the color provider to use + * + * @return the display colors to use + */ + public DisplayColors getDisplayColors(final double value, final ColorProvider colorProvider) { + return colorMapper.apply(value, colorProvider); + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/CoverageStatistics.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/CoverageStatistics.java new file mode 100644 index 000000000..893506e84 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/CoverageStatistics.java @@ -0,0 +1,116 @@ +package io.jenkins.plugins.coverage.metrics.model; + +import java.util.List; +import java.util.Map.Entry; +import java.util.NavigableMap; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.math.Fraction; + +import edu.hm.hafner.coverage.FractionValue; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Value; + +/** + * Represents the different mappings of coverage metric and baseline to actual values. + */ +public class CoverageStatistics { + private final List projectValueMapping; + private final NavigableMap projectDelta; + private final List changeValueMapping; + private final NavigableMap changeDelta; + private final List fileValueMapping; + private final NavigableMap fileDelta; + + /** + * Creates a new instance of {@link CoverageStatistics}. + * + * @param projectValueMapping + * mapping of metrics to values for {@link Baseline#PROJECT} + * @param projectDeltaMapping + * mapping of metrics to delta values for {@link Baseline#PROJECT_DELTA} + * @param modifiedLinesValueMapping + * mapping of metrics to values for {@link Baseline#MODIFIED_LINES} + * @param modifiedLinesDeltaMapping + * mapping of metrics to delta values for {@link Baseline#MODIFIED_LINES_DELTA} + * @param modifiedFilesValueMapping + * mapping of metrics to values for {@link Baseline#MODIFIED_FILES} + * @param modifiedFilesDeltaMapping + * mapping of metrics to delta values for {@link Baseline#MODIFIED_FILES_DELTA} + */ + public CoverageStatistics( + final List projectValueMapping, + final NavigableMap projectDeltaMapping, + final List modifiedLinesValueMapping, + final NavigableMap modifiedLinesDeltaMapping, + final List modifiedFilesValueMapping, + final NavigableMap modifiedFilesDeltaMapping) { + this.projectValueMapping = List.copyOf(projectValueMapping); + this.changeValueMapping = List.copyOf(modifiedLinesValueMapping); + this.fileValueMapping = List.copyOf(modifiedFilesValueMapping); + + this.projectDelta = asValueMap(projectDeltaMapping); + this.changeDelta = asValueMap(modifiedLinesDeltaMapping); + this.fileDelta = asValueMap(modifiedFilesDeltaMapping); + } + + private static NavigableMap asValueMap(final NavigableMap projectDelta) { + return projectDelta.entrySet().stream().collect( + Collectors.toMap(Entry::getKey, e -> new FractionValue(e.getKey(), e.getValue()), (o1, o2) -> o1, + TreeMap::new)); + } + + /** + * Returns the value for the specified baseline and metric. + * + * @param baseline + * the baseline of the value + * @param metric + * the metric of the value + * + * @return the value, if available + */ + public Optional getValue(final Baseline baseline, final Metric metric) { + if (baseline == Baseline.PROJECT) { + return Value.findValue(metric, projectValueMapping); + } + if (baseline == Baseline.MODIFIED_FILES) { + return Value.findValue(metric, fileValueMapping); + } + if (baseline == Baseline.MODIFIED_LINES) { + return Value.findValue(metric, changeValueMapping); + } + if (baseline == Baseline.PROJECT_DELTA) { + return getValue(metric, projectDelta); + } + if (baseline == Baseline.MODIFIED_LINES_DELTA) { + return getValue(metric, changeDelta); + } + if (baseline == Baseline.MODIFIED_FILES_DELTA) { + return getValue(metric, fileDelta); + } + + throw new NoSuchElementException("No such baseline: " + baseline); + } + + private Optional getValue(final Metric metric, final NavigableMap mapping) { + return Optional.ofNullable(mapping.get(metric)); + } + + /** + * Returns whether a value for the specified metric and baseline is available. + * + * @param baseline + * the baseline of the value + * @param metric + * the metric of the value + * + * @return {@code true}, if a value is available, {@code false} otherwise + */ + public boolean containsValue(final Baseline baseline, final Metric metric) { + return getValue(baseline, metric).isPresent(); + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/ElementFormatter.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/ElementFormatter.java new file mode 100644 index 000000000..17ee81d24 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/ElementFormatter.java @@ -0,0 +1,535 @@ +package io.jenkins.plugins.coverage.metrics.model; + +import java.util.List; +import java.util.Locale; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.Fraction; + +import edu.hm.hafner.coverage.Coverage; +import edu.hm.hafner.coverage.FractionValue; +import edu.hm.hafner.coverage.IntegerValue; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.coverage.Percentage; +import edu.hm.hafner.coverage.Value; + +import hudson.Functions; +import hudson.util.ListBoxModel; + +import io.jenkins.plugins.coverage.metrics.color.ColorProvider; +import io.jenkins.plugins.coverage.metrics.color.ColorProvider.DisplayColors; +import io.jenkins.plugins.coverage.metrics.color.ColorProviderFactory; + +/** + * A localized formatter for coverages, metrics, baselines, etc. + * + * @author Florian Orendi + */ +@SuppressWarnings("PMD.GodClass") +public final class ElementFormatter { + private static final Fraction HUNDRED = Fraction.getFraction("100.0"); + private static final String NO_COVERAGE_AVAILABLE = "-"; + private static final Pattern PERCENTAGE = Pattern.compile("\\d+(\\.\\d+)?%"); + + /** + * Formats a generic value using a specific rendering method. The type of the given {@link Value} instance is used + * to select the best matching rendering method. This non-object-oriented approach is required since the + * {@link Value} instances are provided by a library that is not capable of localizing these values for the user. + * + * @param value + * the value to format + * + * @return the formatted value as plain text + */ + public String getTooltip(final Value value) { + return formatValueWithMetric(value) + " (" + formatAdditionalInformation(value) + ")"; + } + + /** + * Formats a generic value using a specific rendering method. The type of the given {@link Value} instance is used + * to select the best matching rendering method. This non-object-oriented approach is required since the + * {@link Value} instances are provided by a library that is not capable of localizing these values for the user. + * + * @param value + * the value to format + * + * @return the formatted value as plain text + */ + public String format(final Value value) { + return format(value, Functions.getCurrentLocale()); + } + + /** + * Formats a generic value using a specific rendering method. The type of the given {@link Value} instance is used + * to select the best matching rendering method. This non-object-oriented approach is required since the + * {@link Value} instances are provided by a library that is not capable of localizing these values for the user. + * + * @param value + * the value to format + * @param locale + * the locale to use to render the values + * + * @return the formatted value as plain text + */ + public String format(final Value value, final Locale locale) { + if (value instanceof Coverage) { + return formatPercentage((Coverage) value, locale); + } + if (value instanceof IntegerValue) { + return String.valueOf(((IntegerValue) value).getValue()); + } + if (value instanceof FractionValue) { + return formatDelta(((FractionValue) value).getFraction(), value.getMetric(), locale); + } + return value.toString(); + } + + /** + * Formats a generic value using a specific rendering method. The type of the given {@link Value} instance is used + * to select the best matching rendering method. This non-object-oriented approach is required since the + * {@link Value} instances are provided by a library that is not capable of localizing these values for the user. + * + * @param value + * the value to format + * + * @return the formatted value as plain text + */ + public String formatDetails(final Value value) { + return formatDetails(value, Functions.getCurrentLocale()); + } + + /** + * Formats a generic value using a specific rendering method. The type of the given {@link Value} instance is used + * to select the best matching rendering method. This non-object-oriented approach is required since the + * {@link Value} instances are provided by a library that is not capable of localizing these values for the user. + * + * @param value + * the value to format + * @param locale + * the locale to use to render the values + * + * @return the formatted value as plain text + */ + public String formatDetails(final Value value, final Locale locale) { + if (value instanceof Coverage) { + var coverage = (Coverage) value; + return formatPercentage(coverage, locale) + + formatRatio(coverage.getCovered(), coverage.getTotal()); + } + if (value instanceof IntegerValue) { + return String.valueOf(((IntegerValue) value).getValue()); + } + if (value instanceof FractionValue) { + return String.format(locale, "%.2f%%", ((FractionValue) value).getFraction().doubleValue()); + } + return value.toString(); + } + + /** + * Formats additional information for a generic value using a specific rendering method. This information can be + * added as a tooltip. The type of the given {@link Value} instance is used to select the best matching rendering + * method. This non-object-oriented approach is required since the {@link Value} instances are provided by a library + * that is not capable of localizing these values for the user. + * + * @param value + * the value to format + * + * @return the formatted value as plain text + */ + public String formatAdditionalInformation(final Value value) { + if (value instanceof Coverage) { + var coverage = (Coverage) value; + if (coverage.isSet()) { + if (coverage.getMetric() == Metric.MUTATION) { + return formatCoverage(coverage, Messages.Metric_MUTATION_Killed(), + Messages.Metric_MUTATION_Survived()); + } + else { + return formatCoverage(coverage, Messages.Metric_Coverage_Covered(), + Messages.Metric_Coverage_Missed()); + } + } + return StringUtils.EMPTY; + } + return StringUtils.EMPTY; + } + + private static String formatCoverage(final Coverage coverage, final String coveredText, final String missedText) { + return String.format("%s: %d - %s: %d", coveredText, coverage.getCovered(), + missedText, coverage.getMissed()); + } + + /** + * Returns whether the value should be rendered by using a color badge. + * + * @param value + * the value to render + * + * @return {@code true} if the value should be rendered by using a color badge, {@code false} otherwise + */ + public boolean showColors(final Value value) { + return value instanceof Coverage; + } + + /** + * Provides the colors to render a given coverage percentage. + * + * @param baseline + * the baseline to show + * @param value + * the value to format + * + * @return the display colors to use + */ + public DisplayColors getDisplayColors(final Baseline baseline, final Value value) { + var defaultColorProvider = ColorProviderFactory.createDefaultColorProvider(); + if (value instanceof Coverage) { + return baseline.getDisplayColors(((Coverage) value).getCoveredPercentage().toDouble(), + defaultColorProvider); + } + else if (value instanceof FractionValue) { + return baseline.getDisplayColors(((FractionValue) value).getFraction().doubleValue(), defaultColorProvider); + } + return ColorProvider.DEFAULT_COLOR; + } + + /** + * Returns a formatted and localized String representation of the specified value (without metric). + * + * @param value + * the value to format + * + * @return the value formatted as a string + */ + @SuppressWarnings("unused") // Called by jelly view + public String formatValue(final Value value) { + return formatDetails(value, Functions.getCurrentLocale()); + } + + /** + * Returns a formatted and localized String representation of the specified value prefixed with the metric name. + * + * @param value + * the value to format + * + * @return the value formatted as a string + */ + @SuppressWarnings("unused") // Called by jelly view + public String formatValueWithMetric(final Value value) { + return getDisplayName(value.getMetric()) + ": " + + format(value, Functions.getCurrentLocale()); + } + + /** + * Transforms percentages with a ',' decimal separator to a representation using a '.' in order to use the + * percentage for styling HTML tags. + * + * @param percentage + * The text representation of a percentage + * + * @return the formatted percentage string + */ + public String getBackgroundColorFillPercentage(final String percentage) { + String formattedPercentage = percentage.replace(",", "."); + if (PERCENTAGE.matcher(formattedPercentage).matches()) { + return formattedPercentage; + } + return "100%"; + } + + /** + * Returns the fill percentage for the specified value. + * + * @param value + * the value to format + * + * @return the percentage string + */ + @SuppressWarnings("unused") // Called by jelly view + public String getBackgroundColorFillPercentage(final Value value) { + if (value instanceof Coverage) { + return format(value, Locale.ENGLISH); + } + return "100%"; + } + + private String formatRatio(final int covered, final int total) { + if (total > 0) { + return String.format(" (%d/%d)", covered, total); + } + return StringUtils.EMPTY; + } + + /** + * Formats a coverage as a percentage number. The shown value is multiplied by 100 and rounded by two decimals. + * + * @param coverage + * the coverage to format + * @param locale + * the locale to use to render the values + * + * @return the formatted percentage as plain text + */ + public String formatPercentage(final Coverage coverage, final Locale locale) { + if (coverage.isSet()) { + return formatPercentage(coverage.getCoveredPercentage(), locale); + } + return NO_COVERAGE_AVAILABLE; + } + + /** + * Formats a fraction in the interval [0, 1] as a percentage number. The shown value is multiplied by 100 and + * rounded by two decimals. + * + * @param fraction + * the fraction to format (in the interval [0, 1]) + * @param locale + * the locale to use to render the values + * + * @return the formatted percentage as plain text + */ + private String formatPercentage(final Percentage fraction, final Locale locale) { + return String.format(locale, "%.2f%%", fraction.toDouble()); + } + + /** + * Formats a coverage given by covered and total elements as a percentage number. The shown value is multiplied by + * 100 and * rounded by two decimals. + * + * @param covered + * the number of covered items + * @param total + * the number of total items + * @param locale + * the locale to use to render the values + * + * @return the formatted percentage as plain text + */ + public String formatPercentage(final int covered, final int total, final Locale locale) { + return formatPercentage(Percentage.valueOf(covered, total), locale); + } + + /** + * Formats a delta percentage to its plain text representation with a leading sign and rounds the value to two + * decimals. + * + * @param fraction + * the value of the delta + * @param metric + * the metric of the value + * @param locale + * the locale to use to render the values + * + * @return the formatted delta percentage as plain text with a leading sign + */ + public String formatDelta(final Fraction fraction, final Metric metric, final Locale locale) { + if (metric.equals(Metric.COMPLEXITY) || metric.equals(Metric.LOC)) { // TODO: move to metric? + return String.format(locale, "%+d", fraction.intValue()); + } + return String.format(locale, "%+.2f%%", fraction.multiplyBy(HUNDRED).doubleValue()); + } + + /** + * Returns a localized human-readable name for the specified metric. + * + * @param metric + * the metric to get the name for + * + * @return the display name + */ + @SuppressWarnings("PMD.CyclomaticComplexity") + public String getDisplayName(final Metric metric) { + switch (metric) { + case CONTAINER: + return Messages.Metric_CONTAINER(); + case MODULE: + return Messages.Metric_MODULE(); + case PACKAGE: + return Messages.Metric_PACKAGE(); + case FILE: + return Messages.Metric_FILE(); + case CLASS: + return Messages.Metric_CLASS(); + case METHOD: + return Messages.Metric_METHOD(); + case LINE: + return Messages.Metric_LINE(); + case BRANCH: + return Messages.Metric_BRANCH(); + case INSTRUCTION: + return Messages.Metric_INSTRUCTION(); + case MUTATION: + return Messages.Metric_MUTATION(); + case COMPLEXITY: + return Messages.Metric_COMPLEXITY(); + case COMPLEXITY_DENSITY: + return Messages.Metric_COMPLEXITY_DENSITY(); + case LOC: + return Messages.Metric_LOC(); + default: + throw new NoSuchElementException("No display name found for metric " + metric); + } + } + + /** + * Gets the display names of the existing {@link Metric coverage metrics}, sorted by the metrics ordinal. + * + * @return the sorted metric display names + */ + public List getSortedCoverageDisplayNames() { + return Metric.getCoverageMetrics().stream() + .map(this::getDisplayName) + .collect(Collectors.toList()); + } + + /** + * Formats a stream of values to their display representation by using the given locale. + * + * @param values + * The values to be formatted + * @param locale + * The locale to be used for formatting + * + * @return the formatted values in the origin order of the stream + */ + public List getFormattedValues(final Stream values, final Locale locale) { + return values.map(value -> formatDetails(value, locale)).collect(Collectors.toList()); + } + + /** + * Returns a stream of {@link Coverage} values for the given root node sorted by the metric ordinal. + * + * @param coverage + * The coverage root node + * + * @return a stream containing the existent coverage values + */ + public Stream getSortedCoverageValues(final Node coverage) { + return Metric.getCoverageMetrics() + .stream() + .map(m -> m.getValueFor(coverage)) + .flatMap(Optional::stream) + .filter(value -> value instanceof Coverage) + .map(Coverage.class::cast); + } + + /** + * Returns a localized human-readable label for the specified metric. + * + * @param metric + * the metric to get the label for + * + * @return the display name + */ + @SuppressWarnings("PMD.CyclomaticComplexity") + public String getLabel(final Metric metric) { + switch (metric) { + case CONTAINER: + return Messages.Metric_Short_CONTAINER(); + case MODULE: + return Messages.Metric_Short_MODULE(); + case PACKAGE: + return Messages.Metric_Short_PACKAGE(); + case FILE: + return Messages.Metric_Short_FILE(); + case CLASS: + return Messages.Metric_Short_CLASS(); + case METHOD: + return Messages.Metric_Short_METHOD(); + case LINE: + return Messages.Metric_Short_LINE(); + case BRANCH: + return Messages.Metric_Short_BRANCH(); + case INSTRUCTION: + return Messages.Metric_Short_INSTRUCTION(); + case MUTATION: + return Messages.Metric_Short_MUTATION(); + case COMPLEXITY: + return Messages.Metric_Short_COMPLEXITY(); + case COMPLEXITY_DENSITY: + return Messages.Metric_Short_COMPLEXITY_DENSITY(); + case LOC: + return Messages.Metric_Short_LOC(); + default: + throw new NoSuchElementException("No label found for metric " + metric); + } + } + + /** + * Returns a localized human-readable name for the specified baseline. + * + * @param baseline + * the baseline to get the name for + * + * @return the display name + */ + public String getDisplayName(final Baseline baseline) { + switch (baseline) { + case PROJECT: + return Messages.Baseline_PROJECT(); + case MODIFIED_LINES: + return Messages.Baseline_MODIFIED_LINES(); + case MODIFIED_FILES: + return Messages.Baseline_MODIFIED_FILES(); + case PROJECT_DELTA: + return Messages.Baseline_PROJECT_DELTA(); + case MODIFIED_LINES_DELTA: + return Messages.Baseline_MODIFIED_LINES_DELTA(); + case MODIFIED_FILES_DELTA: + return Messages.Baseline_MODIFIED_FILES_DELTA(); + default: + throw new NoSuchElementException("No display name found for baseline " + baseline); + } + } + + /** + * Returns all available metrics as a {@link ListBoxModel}. + * + * @return the metrics in a {@link ListBoxModel} + */ + public ListBoxModel getMetricItems() { + ListBoxModel options = new ListBoxModel(); + add(options, Metric.MODULE); + add(options, Metric.PACKAGE); + add(options, Metric.FILE); + add(options, Metric.CLASS); + add(options, Metric.METHOD); + add(options, Metric.LINE); + add(options, Metric.BRANCH); + add(options, Metric.INSTRUCTION); + add(options, Metric.MUTATION); + add(options, Metric.COMPLEXITY); + add(options, Metric.LOC); + return options; + } + + private void add(final ListBoxModel options, final Metric metric) { + options.add(getDisplayName(metric), metric.name()); + } + + /** + * Returns all available baselines as a {@link ListBoxModel}. + * + * @return the baselines in a {@link ListBoxModel} + */ + public ListBoxModel getBaselineItems() { + ListBoxModel options = new ListBoxModel(); + add(options, Baseline.PROJECT); + add(options, Baseline.MODIFIED_LINES); + add(options, Baseline.MODIFIED_FILES); + add(options, Baseline.PROJECT_DELTA); + add(options, Baseline.MODIFIED_LINES_DELTA); + add(options, Baseline.MODIFIED_FILES_DELTA); + return options; + } + + private void add(final ListBoxModel options, final Baseline baseline) { + options.add(getDisplayName(baseline), baseline.name()); + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/package-info.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/package-info.java new file mode 100644 index 000000000..fb5d4a13a --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/package-info.java @@ -0,0 +1,8 @@ +/** + * Contains models to format coverages and aggregate values. + */ +@DefaultAnnotation(NonNull.class) +package io.jenkins.plugins.coverage.metrics.model; + +import edu.umd.cs.findbugs.annotations.DefaultAnnotation; +import edu.umd.cs.findbugs.annotations.NonNull; diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeFacade.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeFacade.java new file mode 100644 index 000000000..b475a7e4d --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeFacade.java @@ -0,0 +1,323 @@ +package io.jenkins.plugins.coverage.metrics.source; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.parser.Parser; +import org.jsoup.select.Elements; + +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.util.FilteredLog; + +import hudson.FilePath; +import hudson.model.Run; +import hudson.util.TextFile; + +/** + * Facade to the source code file structure in Jenkins build folder. Access of those files should be done using an + * instance of this class only. + * + * @author Ullrich Hafner + * @author Florian Orendi + */ +public class SourceCodeFacade { + /** Toplevel directory in the build folder of the controller that contains the zipped source files. */ + static final String COVERAGE_SOURCES_DIRECTORY = "coverage-sources"; + static final String COVERAGE_SOURCES_ZIP = "coverage-sources.zip"; + static final int MAX_FILENAME_LENGTH = 245; // Windows has limitations on long file names + static final String ZIP_FILE_EXTENSION = ".zip"; + + static String sanitizeFilename(final String inputName) { + return StringUtils.right(inputName.replaceAll("[^a-zA-Z0-9-_.]", "_"), MAX_FILENAME_LENGTH); + } + + /** + * Reads the contents of the source file of the given file into a String. + * + * @param buildResults + * Jenkins directory for build results + * @param id + * if of the coverage results + * @param path + * relative path to the coverage node base filename of the coverage node + * + * @return the file content as String + */ + public String read(final File buildResults, final String id, final String path) + throws IOException, InterruptedException { + Path tempDir = Files.createTempDirectory(COVERAGE_SOURCES_DIRECTORY); + FilePath unzippedSourcesDir = new FilePath(tempDir.toFile()); + try { + FilePath inputZipFile = new FilePath(createFileInBuildFolder(buildResults, id, path)); + inputZipFile.unzip(unzippedSourcesDir); + String actualPaintedSourceFileName = StringUtils.removeEnd(sanitizeFilename(path), ZIP_FILE_EXTENSION); + File sourceFile = tempDir.resolve(actualPaintedSourceFileName).toFile(); + + return new TextFile(sourceFile).read(); + } + finally { + unzippedSourcesDir.deleteRecursive(); + } + } + + /** + * Returns whether the source code is available for the specified source file. + * + * @param buildResults + * Jenkins directory for build results + * @param id + * if of the coverage results + * @param path + * relative path to the source code filename name + * + * @return the file content as String + */ + public boolean canRead(final File buildResults, final String id, final String path) { + return createFileInBuildFolder(buildResults, id, path).canRead(); + } + + /** + * Checks whether any source files has been stored. Even if it is wanted, there might have been errors which cause + * the absence of any source files. + * + * @param buildResults + * Jenkins directory for build results + * @param id + * id of the coverage results + * + * @return {@code true} whether source files has been stored, else {@code false} + */ + public boolean hasStoredSourceCode(final File buildResults, final String id) { + File sourceFolder = new File(buildResults, COVERAGE_SOURCES_DIRECTORY); + File elementFolder = new File(sourceFolder, id); + File[] files = elementFolder.listFiles(); + return files != null && files.length > 0; + } + + String getCoverageSourcesDirectory() { + return COVERAGE_SOURCES_DIRECTORY; + } + + /** + * Copies the zipped source files from the agent to the controller and unpacks them in the coverage-sources folder + * of the current build. + * + * @param build + * the build with the coverage result + * @param workspace + * the workspace on the agent that created the ZIP file + * @param log + * the log + * + * @throws InterruptedException + * in case the user terminated the job + */ + void copySourcesToBuildFolder(final Run build, final FilePath workspace, final FilteredLog log) + throws InterruptedException { + try { + FilePath buildFolder = new FilePath(build.getRootDir()).child(COVERAGE_SOURCES_DIRECTORY); + FilePath buildZip = buildFolder.child(COVERAGE_SOURCES_ZIP); + workspace.child(COVERAGE_SOURCES_ZIP).copyTo(buildZip); + log.logInfo("-> extracting..."); + buildZip.unzip(buildFolder); + buildZip.delete(); + log.logInfo("-> done"); + } + catch (IOException exception) { + log.logException(exception, "Can't copy zipped sources from agent to controller"); + } + } + + /** + * Returns a file to a source file in Jenkins' build folder. Note that the file might not exist. + * + * @param buildResults + * Jenkins directory for build results + * @param id + * if of the coverage results + * @param path + * relative path to the coverage node base filename of the coverage node + * + * @return the file + */ + File createFileInBuildFolder(final File buildResults, final String id, final String path) { + File sourceFolder = new File(buildResults, COVERAGE_SOURCES_DIRECTORY); + File elementFolder = new File(sourceFolder, id); + + return new File(elementFolder, sanitizeFilename(path) + ZIP_FILE_EXTENSION); + } + + /** + * Filters the sourcecode coverage highlighting for analyzing the modified lines coverage only. + * + * @param content + * The original HTML content + * @param fileNode + * The {@link FileNode node} which represents the coverage of the file + * + * @return the filtered HTML sourcecode view + */ + public String calculateModifiedLinesCoverageSourceCode(final String content, final FileNode fileNode) { + Set lines = fileNode.getLinesWithCoverage(); + lines.retainAll(fileNode.getModifiedLines()); + Set linesAsText = lines.stream().map(String::valueOf).collect(Collectors.toSet()); + Document doc = Jsoup.parse(content, Parser.xmlParser()); + int maxLine = Integer.parseInt(Objects.requireNonNull( + doc.select("tr").last()).select("a").text()); + Map linesMapping = calculateLineMapping(lines, maxLine); + Elements elements = doc.select("tr"); + for (Element element : elements) { + String line = element.select("td > a").text(); + if (linesMapping.containsKey(line)) { + if (linesMapping.get(line)) { + changeCodeToSkipLine(element); + } + else if (!linesAsText.contains(line)) { + element.removeClass(element.className()); + element.addClass("noCover"); + Objects.requireNonNull(element.select("td.hits").first()).text(""); + } + } + else { + element.remove(); + } + } + return doc.html(); + } + + /** + * Filters the sourcecode coverage highlighting for analyzing indirect coverage changes only. + * + * @param content + * The original HTML content + * @param fileNode + * The {@link FileNode node} which represents the coverage of the file + * + * @return the filtered HTML sourcecode view + */ + public String calculateIndirectCoverageChangesSourceCode(final String content, final FileNode fileNode) { + Map lines = fileNode.getIndirectCoverageChanges(); + Map indirectCoverageChangesAsText = lines.entrySet().stream() + .collect(Collectors + .toMap(entry -> String.valueOf(entry.getKey()), entry -> String.valueOf(entry.getValue()))); + Document doc = Jsoup.parse(content, Parser.xmlParser()); + int maxLine = Integer.parseInt(Objects.requireNonNull( + doc.select("tr").last()).select("a").text()); + Map linesMapping = calculateLineMapping(lines.keySet(), maxLine); + doc.select("tr").forEach(element -> { + String line = element.select("td > a").text(); + if (linesMapping.containsKey(line)) { + colorIndirectCoverageChangeLine(element, line, linesMapping, indirectCoverageChangesAsText); + } + else { + element.remove(); + } + }); + return doc.html(); + } + + /** + * Highlights a line to be a skip line which represents a bunch of not visible lines. + * + * @param element + * The HTML element which represents the line + */ + private void changeCodeToSkipLine(final Element element) { + element.removeClass(element.className()); + element.addClass("coverSkip"); + Objects.requireNonNull(element.select("td.line").first()).text(".."); + Objects.requireNonNull(element.select("td.hits").first()).text(""); + Objects.requireNonNull(element.select("td.code").first()).text(""); + } + + /** + * Colors one line within the indirect coverage changes code view. + * + * @param element + * The HTML element which represents the line + * @param line + * The line number + * @param linesMapping + * The mapping which classifies how the line should be treated + * @param indirectCoverageChangesAsText + * The indirect coverage changes mapping + */ + private void colorIndirectCoverageChangeLine(final Element element, final String line, + final Map linesMapping, final Map indirectCoverageChangesAsText) { + if (linesMapping.get(line)) { + changeCodeToSkipLine(element); + } + else if (indirectCoverageChangesAsText.containsKey(line)) { + element.removeClass(element.className()); + String hits = indirectCoverageChangesAsText.get(line); + if (hits.startsWith("-")) { + element.addClass("coverNone"); + } + else { + element.addClass("coverFull"); + } + Objects.requireNonNull(element.select("td.hits").first()).text(hits); + } + else { + element.removeClass(element.className()); + element.addClass("noCover"); + Objects.requireNonNull(element.select("td.hits").first()).text(""); + } + } + + /** + * Calculates a mapping of lines which should be shown. The mapping contains the passed line intervals surrounded by + * +-3 lines each. + * + * @param lines + * The lines which build the line intervals to be shown + * @param maxLine + * The maximum line number + * + * @return the line mapping as a map with the line number text as key and {@code true} if the line should be marked + * as a filling line, {@code false} if the line shows code + */ + private Map calculateLineMapping(final Set lines, final int maxLine) { + SortedSet linesWithSurroundings = new TreeSet<>(lines); + lines.forEach(line -> { + for (int i = 1; i <= 3; i++) { + linesWithSurroundings.add(line + i); + linesWithSurroundings.add(line - i); + } + }); + List sortedLines = linesWithSurroundings.stream() + .filter(line -> line >= 1 && line <= maxLine) + .collect(Collectors.toList()); + SortedMap linesMapping = new TreeMap<>(); + for (int i = 0; i < sortedLines.size(); i++) { + int line = sortedLines.get(i); + linesMapping.put(String.valueOf(line), false); + if (i < sortedLines.size() - 1 && line + 1 != sortedLines.get(i + 1)) { + linesMapping.put(String.valueOf(line + 1), true); + } + } + int highestLine = sortedLines.get(sortedLines.size() - 1); + if (sortedLines.get(0) > 1) { + linesMapping.put("1", true); + } + if (highestLine < maxLine) { + linesMapping.put(String.valueOf(highestLine + 1), true); + } + return linesMapping; + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceCodePainter.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceCodePainter.java new file mode 100644 index 000000000..965982b7a --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceCodePainter.java @@ -0,0 +1,390 @@ +package io.jenkins.plugins.coverage.metrics.source; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.io.FileUtils; + +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.coverage.Mutation; +import edu.hm.hafner.util.FilteredLog; +import edu.umd.cs.findbugs.annotations.NonNull; + +import hudson.FilePath; +import hudson.model.Run; +import hudson.remoting.VirtualChannel; +import jenkins.MasterToSlaveFileCallable; + +import io.jenkins.plugins.prism.FilePermissionEnforcer; +import io.jenkins.plugins.prism.PermittedSourceCodeDirectory; +import io.jenkins.plugins.prism.PrismConfiguration; +import io.jenkins.plugins.prism.SourceCodeRetention; +import io.jenkins.plugins.prism.SourceDirectoryFilter; +import io.jenkins.plugins.util.ValidationUtilities; + +/** + * Highlights the code coverage information in all source code files. This process is executed on the agent node that + * has all source files checked out. + */ +public class SourceCodePainter { + private final Run build; + private final FilePath workspace; + private final String id; + + /** + * Creates a painter for the passed build, using the passed properties. + * + * @param build + * The build which processes the source code + * @param workspace + * The workspace which contains the source code files + * @param id + * the ID of the coverage results - each ID will store the files in a sepearate directory + */ + public SourceCodePainter(@NonNull final Run build, @NonNull final FilePath workspace, final String id) { + this.build = build; + this.workspace = workspace; + this.id = id; + } + + /** + * Processes the source code painting. + * + * @param files + * the files to paint + * @param sourceDirectories + * the source directories that have been configured in the associated job + * @param sourceCodeEncoding + * the encoding of the source code files + * @param sourceCodeRetention + * the source code retention strategy + * @param log + * The log + * + * @throws InterruptedException + * if the painting process has been interrupted + */ + public void processSourceCodePainting(final List files, final Set sourceDirectories, + final String sourceCodeEncoding, + final SourceCodeRetention sourceCodeRetention, final FilteredLog log) + throws InterruptedException { + SourceCodeFacade sourceCodeFacade = new SourceCodeFacade(); + if (sourceCodeRetention != SourceCodeRetention.NEVER) { + var paintedFiles = files.stream() + .map(PaintedNode::new) + .collect(Collectors.toList()); + log.logInfo("Painting %d source files on agent", paintedFiles.size()); + + paintFilesOnAgent(paintedFiles, sourceDirectories, sourceCodeEncoding, log); + log.logInfo("Copying painted sources from agent to build folder"); + + sourceCodeFacade.copySourcesToBuildFolder(build, workspace, log); + } + sourceCodeRetention.cleanup(build, sourceCodeFacade.getCoverageSourcesDirectory(), log); + } + + private void paintFilesOnAgent(final List paintedFiles, + final Set requestedSourceDirectories, + final String sourceCodeEncoding, final FilteredLog log) throws InterruptedException { + try { + Set permittedSourceDirectories = PrismConfiguration.getInstance() + .getSourceDirectories() + .stream() + .map(PermittedSourceCodeDirectory::getPath) + .collect(Collectors.toSet()); + + var painter = new AgentCoveragePainter(paintedFiles, permittedSourceDirectories, + requestedSourceDirectories, sourceCodeEncoding, id); + FilteredLog agentLog = workspace.act(painter); + log.merge(agentLog); + } + catch (IOException exception) { + log.logException(exception, "Can't paint and zip sources on the agent"); + } + } + + /** + * Paints source code files on the agent using the recorded coverage information. All files are stored as zipped + * HTML files that contain the painted source code. In the last step all zipped source files are aggregated into a + * single archive to simplify copying to the controller. + */ + static class AgentCoveragePainter extends MasterToSlaveFileCallable { + private static final long serialVersionUID = 3966282357309568323L; + + private final List paintedFiles; + private final Set permittedSourceDirectories; + + private final Set requestedSourceDirectories; + private final String sourceCodeEncoding; + + private final String directory; + + /** + * Creates a new instance of {@link AgentCoveragePainter}. + * + * @param paintedFiles + * the model for the file painting for each coverage node + * @param permittedSourceDirectories + * the permitted source code directories (in Jenkins global configuration) + * @param requestedSourceDirectories + * the requested relative and absolute source directories (in the step configuration) + * @param sourceCodeEncoding + * the encoding of the source code files + * @param directory + * the subdirectory where the source files will be stored in + */ + AgentCoveragePainter(final List paintedFiles, + final Set permittedSourceDirectories, final Set requestedSourceDirectories, + final String sourceCodeEncoding, final String directory) { + super(); + + this.paintedFiles = paintedFiles; + this.permittedSourceDirectories = permittedSourceDirectories; + this.requestedSourceDirectories = requestedSourceDirectories; + this.sourceCodeEncoding = sourceCodeEncoding; + this.directory = directory; + } + + @Override + public FilteredLog invoke(final File workspaceFile, final VirtualChannel channel) { + FilteredLog log = new FilteredLog("Errors during source code painting:"); + Set sourceDirectories = filterSourceDirectories(workspaceFile, log); + if (sourceDirectories.isEmpty()) { + log.logInfo("Searching for source code files in root of workspace '%s'", workspaceFile); + } + else if (sourceDirectories.size() == 1) { + log.logInfo("Searching for source code files in '%s'", sourceDirectories.iterator().next()); + } + else { + log.logInfo("Searching for source code files in:", workspaceFile); + sourceDirectories.forEach(dir -> log.logInfo("-> %s", dir)); + } + FilePath workspace = new FilePath(workspaceFile); + + try { + FilePath outputFolder = workspace.child(directory); + outputFolder.mkdirs(); + + Path temporaryFolder = Files.createTempDirectory(directory); + + int count = paintedFiles.parallelStream() + .mapToInt(file -> paintSource(file, workspace, temporaryFolder, sourceDirectories, log)) + .sum(); + + if (count == paintedFiles.size()) { + log.logInfo("-> finished painting successfully"); + } + else { + log.logInfo("-> finished painting (%d files have been painted, %d files failed)", + count, paintedFiles.size() - count); + } + + FilePath zipFile = workspace.child(SourceCodeFacade.COVERAGE_SOURCES_ZIP); + outputFolder.zip(zipFile); + log.logInfo("-> zipping sources from folder '%s' as '%s'", outputFolder, zipFile); + + deleteFolder(temporaryFolder.toFile(), log); + } + catch (IOException exception) { + log.logException(exception, + "Cannot create temporary directory in folder '%s' for the painted source files", workspace); + } + catch (InterruptedException exception) { + log.logException(exception, + "Processing has been interrupted: skipping zipping of source files", workspace); + } + + return log; + } + + private Charset getCharset() { + return new ValidationUtilities().getCharset(sourceCodeEncoding); + } + + private Set filterSourceDirectories(final File workspace, final FilteredLog log) { + SourceDirectoryFilter filter = new SourceDirectoryFilter(); + return filter.getPermittedSourceDirectories(workspace.getAbsolutePath(), + permittedSourceDirectories, requestedSourceDirectories, log); + } + + private int paintSource(final PaintedNode fileNode, final FilePath workspace, final Path temporaryFolder, + final Set sourceSearchDirectories, final FilteredLog log) { + String relativePathIdentifier = fileNode.getPath(); + FilePath paintedFilesDirectory = workspace.child(directory); + return findSourceFile(workspace, relativePathIdentifier, sourceSearchDirectories, log) + .map(resolvedPath -> paint(fileNode, relativePathIdentifier, resolvedPath, + paintedFilesDirectory, temporaryFolder, getCharset(), log)) + .orElse(0); + } + + private int paint(final PaintedNode paint, final String relativePathIdentifier, final FilePath resolvedPath, + final FilePath paintedFilesDirectory, final Path temporaryFolder, + final Charset charset, final FilteredLog log) { + String sanitizedFileName = SourceCodeFacade.sanitizeFilename(relativePathIdentifier); + FilePath zipOutputPath = paintedFilesDirectory.child( + sanitizedFileName + SourceCodeFacade.ZIP_FILE_EXTENSION); + try { + Path paintedFilesFolder = Files.createTempDirectory(temporaryFolder, directory); + Path fullSourcePath = paintedFilesFolder.resolve(sanitizedFileName); + try (BufferedWriter output = Files.newBufferedWriter(fullSourcePath)) { + List lines = Files.readAllLines(Paths.get(resolvedPath.getRemote()), charset); + new SourceToHtml().paintSourceCodeWithCoverageInformation(paint, output, lines); + } + new FilePath(fullSourcePath.toFile()).zip(zipOutputPath); + FileUtils.deleteDirectory(paintedFilesFolder.toFile()); + return 1; + } + catch (IOException | InterruptedException exception) { + log.logException(exception, "Can't write coverage paint of '%s' to zipped source file '%s'", + relativePathIdentifier, zipOutputPath); + return 0; + } + } + + private Optional findSourceFile(final FilePath workspace, final String fileName, + final Set sourceDirectories, final FilteredLog log) { + try { + FilePath absolutePath = new FilePath(new File(fileName)); + if (absolutePath.exists()) { + return enforcePermissionFor(absolutePath, workspace, sourceDirectories, log); + } + + FilePath relativePath = workspace.child(fileName); + if (relativePath.exists()) { + return enforcePermissionFor(relativePath, workspace, sourceDirectories, log); + } + + for (String sourceFolder : sourceDirectories) { + FilePath sourcePath = workspace.child(sourceFolder).child(fileName); + if (sourcePath.exists()) { + return enforcePermissionFor(sourcePath, workspace, sourceDirectories, log); + } + } + + log.logError("Source file '%s' not found", fileName); + } + catch (InvalidPathException | IOException | InterruptedException exception) { + log.logException(exception, "No valid path in coverage node: '%s'", fileName); + } + return Optional.empty(); + } + + private Optional enforcePermissionFor(final FilePath absolutePath, final FilePath workspace, + final Set sourceDirectories, final FilteredLog log) { + FilePermissionEnforcer enforcer = new FilePermissionEnforcer(); + if (enforcer.isInWorkspace(absolutePath.getRemote(), workspace, sourceDirectories)) { + return Optional.of(absolutePath); + } + log.logError("Skipping coloring of file: %s (not part of workspace or permitted source code folders)", + absolutePath.getRemote()); + return Optional.empty(); + } + + /** + * Deletes a folder. + * + * @param folder + * The directory to be deleted + * @param log + * The log + */ + private void deleteFolder(final File folder, final FilteredLog log) { + if (folder.isDirectory()) { + try { + FileUtils.deleteDirectory(folder); + } + catch (IOException e) { + log.logError("The folder '%s' could not be deleted", + folder.getAbsolutePath()); + } + } + } + } + + /** + * Provides all required information for a {@link FileNode} so that its source code can be rendered in HTML. + */ + static class PaintedNode implements Serializable { + private static final long serialVersionUID = -6044649044983631852L; + + private enum Type { + MUTATION, + COVERAGE + } + + private final String path; + private final int[] linesToPaint; + private final int[] coveredPerLine; + private final int[] missedPerLine; + private final int[] survivedPerLine; + private final int[] killedPerLine; + + PaintedNode(final FileNode file) { + path = file.getPath(); + + linesToPaint = file.getLinesWithCoverage().stream().mapToInt(i -> i).toArray(); + coveredPerLine = file.getCoveredCounters(); + missedPerLine = file.getMissedCounters(); + + survivedPerLine = new int[linesToPaint.length]; + killedPerLine = new int[linesToPaint.length]; + + for (Mutation mutation : file.getMutations()) { + if (mutation.hasSurvived()) { + survivedPerLine[findLine(mutation.getLine())]++; + } + else if (mutation.isKilled()) { + killedPerLine[findLine(mutation.getLine())]++; + } + } + } + + public String getPath() { + return path; + } + + public boolean isPainted(final int line) { + return findLine(line) >= 0; + } + + private int findLine(final int line) { + return Arrays.binarySearch(linesToPaint, line); + } + + public int getCovered(final int line) { + return getCounter(line, coveredPerLine); + } + + public int getMissed(final int line) { + return getCounter(line, missedPerLine); + } + + public int getSurvived(final int line) { + return getCounter(line, survivedPerLine); + } + + public int getKilled(final int line) { + return getCounter(line, killedPerLine); + } + + private int getCounter(final int line, final int[] counters) { + var index = findLine(line); + if (index >= 0) { + return counters[index]; + } + return 0; + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceToHtml.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceToHtml.java new file mode 100644 index 000000000..2e73f5ace --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceToHtml.java @@ -0,0 +1,120 @@ +package io.jenkins.plugins.coverage.metrics.source; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; + +import io.jenkins.plugins.coverage.metrics.source.SourceCodePainter.PaintedNode; + +/** + * Converts a source code file into an HTML document. The HTML document contains the source code with highlighted + * coverage information. + * + * @author Ullrich Hafner + */ +public class SourceToHtml { + void paintSourceCodeWithCoverageInformation(final PaintedNode paint, final BufferedWriter output, final List lines) + throws IOException { + for (int line = 0; line < lines.size(); line++) { + paintLine(line + 1, lines.get(line), paint, output); + } + } + + // TODO rewrite using j2html and prism.js + private void paintLine(final int line, final String content, final PaintedNode paint, + final BufferedWriter output) throws IOException { + if (paint.isPainted(line)) { + int covered = paint.getCovered(line); + int missed = paint.getMissed(line); + + int survived = paint.getSurvived(line); + int killed = paint.getKilled(line); + + output.write("\n"); + output.write("" + line + "\n"); + + String display; + + if (survived + killed > 0) { + display = String.format("%d/%d", killed, survived + killed); + } + else if (covered + missed > 1) { + display = String.format("%d/%d", covered, covered + missed); + } + else { + display = String.valueOf(covered); + } + + output.write("" + display + "\n"); + + } + else { + output.write("\n"); + output.write("" + line + "\n"); + output.write("\n"); + } + output.write("" + + content.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\n", "") + .replace("\r", "") + .replace(" ", + " ") + .replace("\t", "        ") + "\n"); + output.write("\n"); + } + + private String selectColor(final int covered, final int missed, final int survived) { + // TODO: what colors should be used for the different cases: Mutations or Branches? + if (covered == 0) { + return "coverNone"; + } + else if (missed == 0 && survived == 0) { + return "coverFull"; + } + else { + return "coverPart"; + } + } + + private String getTooltip(final PaintedNode paint, + final int missed, final int covered, final int survived, final int killed) { + var tooltip = getTooltipValue(paint, missed, covered, survived, killed); + if (StringUtils.isBlank(tooltip)) { + return StringUtils.EMPTY; + } + return "tooltip=\"" + tooltip + "\""; + } + + // TODO: Extract into classes so that we can paint the mutations as well + private String getTooltipValue(final PaintedNode paint, + final int missed, final int covered, final int survived, final int killed) { + + if (survived + killed > 1) { + return String.format("Mutations survived: %d, mutations killed: %d", survived, killed); + } + if (survived == 1) { + return "One survived mutation"; + } + if (killed == 1) { + return "One killed mutation"; + } + + if (covered + missed > 1) { + if (missed == 0) { + return "All branches covered"; + } + return String.format("Partially covered, branch coverage: %d/%d", covered, covered + missed); + } + else if (covered == 1) { + return "Covered at least once"; + } + else { + return "Not covered"; + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceViewModel.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceViewModel.java new file mode 100644 index 000000000..a9f9031a9 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceViewModel.java @@ -0,0 +1,83 @@ +package io.jenkins.plugins.coverage.metrics.source; + +import java.io.IOException; + +import org.apache.commons.lang3.exception.ExceptionUtils; + +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.coverage.Node; + +import hudson.model.ModelObject; +import hudson.model.Run; + +/** + * Server side model that provides the data for the source code view of the coverage results. The layout of the + * associated view is defined corresponding jelly view 'index.jelly'. + * + * @author Ullrich Hafner + */ +public class SourceViewModel implements ModelObject { + private static final SourceCodeFacade SOURCE_CODE_FACADE = new SourceCodeFacade(); + + private final Run owner; + private final String id; + private final FileNode fileNode; + + /** + * Creates a new source view model instance. + * + * @param owner + * the owner of this view + * @param id + * the ID that is used to store the coverage sources + * @param fileNode + * the selected file node of the coverage tree + */ + public SourceViewModel(final Run owner, final String id, final FileNode fileNode) { + this.owner = owner; + this.id = id; + this.fileNode = fileNode; + } + + public Run getOwner() { + return owner; + } + + public Node getNode() { + return fileNode; + } + + + /** + * Returns the source file rendered in HTML. + * + * @return the colored source code as HTML document + */ + @SuppressWarnings("unused") // Called by jelly view + public String getSourceFileContent() { + try { + return SOURCE_CODE_FACADE.read(getOwner().getRootDir(), id, getNode().getPath()); + } + catch (IOException | InterruptedException exception) { + return ExceptionUtils.getStackTrace(exception); + } + } + + /** + * Returns whether the source file is available in Jenkins build folder. + * + * @param coverageNode + * The {@link Node} which is checked if there is a source file available + * + * @return {@code true} if the source file is available, {@code false} otherwise + */ + @SuppressWarnings("unused") // Called by jelly view + public boolean isSourceFileAvailable(final Node coverageNode) { + return SOURCE_CODE_FACADE.canRead(getOwner().getRootDir(), id, coverageNode.getPath()); + } + + @Override + public String getDisplayName() { + return Messages.Coverage_Title(getNode().getName()); + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/package-info.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/package-info.java new file mode 100644 index 000000000..4109ce802 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/package-info.java @@ -0,0 +1,8 @@ +/** + * Contains logic and models for visualizing the colored HTML source code files. + */ +@DefaultAnnotation(NonNull.class) +package io.jenkins.plugins.coverage.metrics.source; + +import edu.umd.cs.findbugs.annotations.DefaultAnnotation; +import edu.umd.cs.findbugs.annotations.NonNull; diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangesTableModel.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangesTableModel.java new file mode 100644 index 000000000..1375a4391 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangesTableModel.java @@ -0,0 +1,84 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +import edu.hm.hafner.coverage.Coverage; +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Node; + +import hudson.Functions; + +import io.jenkins.plugins.coverage.metrics.color.ColorProvider; +import io.jenkins.plugins.datatables.DetailedCell; + +/** + * A base class for coverage table models that handle the changes to a result of a reference build. + */ +abstract class ChangesTableModel extends CoverageTableModel { + private final Node changeRoot; + + ChangesTableModel(final String id, final Node root, final Node changeRoot, + final RowRenderer renderer, final ColorProvider colorProvider) { + super(id, root, renderer, colorProvider); + + this.changeRoot = changeRoot; + } + + @Override + public List getRows() { + Locale browserLocale = Functions.getCurrentLocale(); + return changeRoot.getAllFileNodes().stream() + .map(file -> createRow(file, browserLocale)) + .collect(Collectors.toList()); + } + + abstract CoverageRow createRow(FileNode file, Locale browserLocale); + + FileNode getOriginalNode(final FileNode fileNode) { + return getRoot().getAllFileNodes().stream() + .filter(node -> node.getPath().equals(fileNode.getPath()) + && node.getName().equals(fileNode.getName())) + .findFirst() + .orElse(fileNode); // return this as fallback to prevent exceptions + } + + /** + * UI row model for the changes rows of a table. + */ + static class ChangesRow extends CoverageRow { + private final FileNode originalFile; + + ChangesRow(final FileNode originalFile, final FileNode changedFileNode, + final Locale browserLocale, final RowRenderer renderer, final ColorProvider colorProvider) { + super(changedFileNode, browserLocale, renderer, colorProvider); + + this.originalFile = originalFile; + } + + FileNode getOriginalFile() { + return originalFile; + } + + @Override + public DetailedCell getLineCoverageDelta() { + return createColoredModifiedLinesCoverageDeltaColumn(Metric.LINE); + } + + @Override + public DetailedCell getBranchCoverageDelta() { + return createColoredModifiedLinesCoverageDeltaColumn(Metric.BRANCH); + } + + DetailedCell createColoredModifiedLinesCoverageDeltaColumn(final Metric metric) { + Coverage modifiedLinesCoverage = getFile().getTypedValue(metric, Coverage.nullObject(metric)); + if (modifiedLinesCoverage.isSet()) { + return createColoredCoverageDeltaColumn(metric, + modifiedLinesCoverage.delta(originalFile.getTypedValue(metric, Coverage.nullObject(metric)))); + } + return NO_COVERAGE; + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CodeDeltaCalculator.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CodeDeltaCalculator.java new file mode 100644 index 000000000..57f4a56ca --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CodeDeltaCalculator.java @@ -0,0 +1,293 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.util.FilteredLog; + +import one.util.streamex.StreamEx; + +import hudson.FilePath; +import hudson.model.Run; +import hudson.model.TaskListener; + +import io.jenkins.plugins.forensics.delta.Delta; +import io.jenkins.plugins.forensics.delta.DeltaCalculatorFactory; +import io.jenkins.plugins.forensics.delta.FileChanges; +import io.jenkins.plugins.forensics.delta.FileEditType; + +/** + * Calculates the code delta between a Jenkins build and a reference build. + * + * @author Florian Orendi + */ +class CodeDeltaCalculator { + static final String AMBIGUOUS_PATHS_ERROR = + "Failed to map SCM paths with coverage report paths due to ambiguous fully qualified names"; + static final String AMBIGUOUS_OLD_PATHS_ERROR = + "Failed to map SCM paths from the reference with coverage report paths from the reference " + + "due to ambiguous fully qualified names"; + + static final String CODE_DELTA_TO_COVERAGE_DATA_MISMATCH_ERROR_TEMPLATE = + "Unexpected behavior detected when comparing coverage data with the code delta " + + "- there are ambiguous paths when comparing new with former file paths: "; + + static final String EMPTY_OLD_PATHS_WARNING = "File renamings have been detected for files which have not been " + + "part of the reference coverage report. These files are skipped when calculating the coverage deltas:"; + + private final Run build; + private final FilePath workspace; + private final TaskListener listener; + private final String scm; + + /** + * Creates a code delta calculator for a specific build. + * + * @param build + * The build + * @param workspace + * The workspace + * @param listener + * The listener + * @param scm + * The selected SCM + */ + CodeDeltaCalculator(final Run build, final FilePath workspace, + final TaskListener listener, final String scm) { + this.build = build; + this.workspace = workspace; + this.listener = listener; + this.scm = scm; + } + + /** + * Calculates the code delta between the {@link #build} and the passed reference build. + * + * @param referenceBuild + * The reference build + * @param log + * The log + * + * @return the {@link Delta code delta} as Optional if existent, else an empty Optional + */ + public Optional calculateCodeDeltaToReference(final Run referenceBuild, final FilteredLog log) { + return DeltaCalculatorFactory + .findDeltaCalculator(scm, build, workspace, listener, log) + .calculateDelta(build, referenceBuild, scm, log); + } + + /** + * Gets all code changes which are relevant for the coverage (added, renamed and modified files). + * + * @param delta + * The calculated code {@link Delta} + * + * @return the relevant code changes + */ + public Set getCoverageRelevantChanges(final Delta delta) { + return delta.getFileChangesMap().values().stream() + .filter(fileChange -> fileChange.getFileEditType().equals(FileEditType.MODIFY) + || fileChange.getFileEditType().equals(FileEditType.ADD) + || fileChange.getFileEditType().equals(FileEditType.RENAME)) + .collect(Collectors.toSet()); + } + + /** + * Maps the passed {@link FileChanges code changes} to the corresponding fully qualified names as they are used by + * the coverage reporting tools - usually the fully qualified name of the file. + * + * @param changes + * the code changes + * @param root + * the root of the coverage tree + * @param log + * logger + * + * @return the created mapping of code changes + * @throws IllegalStateException + * when creating the mapping failed due to ambiguous paths + */ + public Map mapScmChangesToReportPaths( + final Set changes, final Node root, final FilteredLog log) throws IllegalStateException { + Set reportPaths = new HashSet<>(root.getFiles()); + Set scmPaths = changes.stream().map(FileChanges::getFileName).collect(Collectors.toSet()); + + Map pathMapping = getScmToReportPathMapping(scmPaths, reportPaths); + verifyScmToReportPathMapping(pathMapping, log); + + return changes.stream() + .filter(change -> reportPaths.contains(pathMapping.get(change.getFileName()))) + .collect(Collectors.toMap( + fileChange -> pathMapping.get(fileChange.getFileName()), Function.identity())); + + } + + /** + * Creates a mapping between the currently used coverage report paths and the corresponding paths that has been used + * for the same coverage nodes before the modifications. This affects only renamed and untouched / modified files + * without a renaming, since added files did not exist before and deleted files do not exist anymore. + * + * @param root + * The root of the coverage tree + * @param referenceRoot + * The root of the coverage tree from the reference build + * @param changes + * The {@link FileChanges changes}, mapped by the currently used coverage report path to which they + * correspond to + * @param log + * The log + * + * @return the created mapping whose keys are the currently used paths and whose values are the paths before the + * modifications + * @throws IllegalStateException + * if the SCM path mapping is ambiguous + */ + public Map createOldPathMapping(final Node root, final Node referenceRoot, + final Map changes, final FilteredLog log) + throws IllegalStateException { + Set oldReportPaths = new HashSet<>(referenceRoot.getFiles()); + // mapping between reference and current file paths which initially contains the SCM paths with renamings + Map oldPathMapping = changes.entrySet().stream() + .filter(entry -> FileEditType.RENAME.equals(entry.getValue().getFileEditType())) + .collect(Collectors.toMap(Entry::getKey, entry -> entry.getValue().getOldFileName())); + // the SCM paths and the coverage report paths from the reference + Map oldScmToOldReportPathMapping + = getScmToReportPathMapping(oldPathMapping.values(), oldReportPaths); + + // replacing the old SCM paths with the old report paths + Set newReportPathsWithRename = oldPathMapping.keySet(); + oldPathMapping.forEach((reportPath, oldScmPath) -> { + String oldReportPath = oldScmToOldReportPathMapping.get(oldScmPath); + oldPathMapping.replace(reportPath, oldReportPath); + }); + if (!newReportPathsWithRename.equals(oldPathMapping.keySet())) { + throw new IllegalStateException(AMBIGUOUS_OLD_PATHS_ERROR); + } + + // adding the paths, which exist in both trees and contain no changes, to the mapping + root.getFiles().stream() + .filter(file -> !oldPathMapping.containsKey(file) && oldReportPaths.contains(file)) + .forEach(file -> oldPathMapping.put(file, file)); + + removeMissingReferences(oldPathMapping, log); + verifyOldPathMapping(oldPathMapping, log); + + return oldPathMapping; + } + + /** + * Creates a mapping between SCM paths and the corresponding coverage report paths. + * + * @param scmPaths + * The SCM paths + * @param reportPaths + * The coverage report paths + * + * @return the created mapping with the SCM path as key + */ + private Map getScmToReportPathMapping( + final Collection scmPaths, final Collection reportPaths) { + Map pathMapping = new HashMap<>(); + for (String scmPath : scmPaths) { + reportPaths.stream() + .filter(scmPath::endsWith) + .max(Comparator.comparingInt(String::length)) + .map(match -> { + pathMapping.put(scmPath, match); + return match; + }) + .orElseGet(() -> pathMapping.put(scmPath, "")); + } + return pathMapping; + } + + /** + * Verifies the passed mapping between SCM and coverage report paths. + * + * @param pathMapping + * The path mapping + * @param log + * The log + * + * @throws IllegalStateException + * when ambiguous paths has been detected + */ + private void verifyScmToReportPathMapping(final Map pathMapping, final FilteredLog log) + throws IllegalStateException { + List notEmptyValues = pathMapping.values().stream() + .filter(path -> !path.isEmpty()) + .collect(Collectors.toList()); + if (notEmptyValues.size() != new HashSet<>(notEmptyValues).size()) { + throw new IllegalStateException(AMBIGUOUS_PATHS_ERROR); + } + log.logInfo("Successfully mapped SCM paths to coverage report paths"); + } + + /** + * Verifies that all reference coverage report files of the passed path mapping exist. Found empty paths which + * represent missing reference nodes will be removed from the mapping since these entries are not usable. + * + * @param oldPathMapping + * The file path mapping to be verified and adjusted + * @param log + * The log + */ + private void removeMissingReferences(final Map oldPathMapping, final FilteredLog log) { + Set pathsWithEmptyReferences = oldPathMapping.entrySet().stream() + .filter(entry -> entry.getValue().isEmpty()) + .map(Entry::getKey) + .collect(Collectors.toSet()); + if (!pathsWithEmptyReferences.isEmpty()) { + pathsWithEmptyReferences.forEach(oldPathMapping::remove); // remove entries which represent missing reference files + String skippedFiles = pathsWithEmptyReferences.stream() + .limit(20) // prevent log overflows + .collect(Collectors.joining("," + System.lineSeparator())); + log.logInfo(EMPTY_OLD_PATHS_WARNING + System.lineSeparator() + skippedFiles); + } + } + + /** + * Verifies that the mapping between the file paths of the current build and the former file paths of the reference + * builds are clearly assigned to each other. This is done to prevent an unexpected behavior triggered by a third + * party library in case that the code delta does not match with the coverage data. + * + * @param oldPathMapping + * The file path mapping + * @param log + * The log + * + * @throws IllegalStateException + * when the mapping is ambiguous + */ + static void verifyOldPathMapping(final Map oldPathMapping, final FilteredLog log) + throws IllegalStateException { + Set duplicates = StreamEx.of(oldPathMapping.values()) + .distinct(2) + .collect(Collectors.toSet()); + + Map duplicateEntries = oldPathMapping.entrySet().stream() + .filter(entry -> duplicates.contains(entry.getValue())) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + + if (!duplicates.isEmpty()) { + String mismatches = duplicateEntries.entrySet().stream() + .limit(20) // prevent log overflows + .map(entry -> String.format("new: '%s' - former: '%s'", entry.getKey(), entry.getValue())) + .collect(Collectors.joining("," + System.lineSeparator())); + String errorMessage = + CODE_DELTA_TO_COVERAGE_DATA_MISMATCH_ERROR_TEMPLATE + System.lineSeparator() + mismatches; + throw new IllegalStateException(errorMessage); + } + log.logInfo("Successfully verified that the coverage data matches with the code delta"); + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageApi.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageApi.java new file mode 100644 index 000000000..19e5a72a9 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageApi.java @@ -0,0 +1,174 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.Collection; +import java.util.Locale; +import java.util.NavigableMap; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import edu.hm.hafner.coverage.Metric; + +import org.kohsuke.stapler.export.Exported; +import org.kohsuke.stapler.export.ExportedBean; +import hudson.model.Result; + +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics; +import io.jenkins.plugins.coverage.metrics.model.ElementFormatter; +import io.jenkins.plugins.util.QualityGateResult; +import io.jenkins.plugins.util.QualityGateResult.QualityGateResultItem; + +/** + * Remote API to list the details of the coverage results. + * + * @author Ullrich Hafner + */ +@ExportedBean +public class CoverageApi { + private static final ElementFormatter FORMATTER = new ElementFormatter(); + private final CoverageStatistics statistics; + private final QualityGateResult qualityGateResult; + private final String referenceBuild; + + CoverageApi(final CoverageStatistics statistics, final QualityGateResult qualityGateResult, + final String referenceBuild) { + this.statistics = statistics; + this.qualityGateResult = qualityGateResult; + this.referenceBuild = referenceBuild; + } + + @Exported(inline = true) + public QualityGateResultApi getQualityGates() { + return new QualityGateResultApi(qualityGateResult); + } + + @Exported + public String getReferenceBuild() { + return referenceBuild; + } + + /** + * Returns the statistics for the project coverage. + * + * @return a mapping of metrics to their values (only metrics with a value are included) + */ + @Exported(inline = true) + public NavigableMap getProjectStatistics() { + return mapToStrings(Baseline.PROJECT); + } + + /** + * Returns the delta values for the project coverage. + * + * @return a mapping of metrics to their values (only metrics with a value are included) + */ + @Exported(inline = true) + public NavigableMap getProjectDelta() { + return mapToStrings(Baseline.PROJECT_DELTA); + } + + /** + * Returns the statistics for the coverage of modified files. + * + * @return a mapping of metrics to their values (only metrics with a value are included) + */ + @Exported(inline = true) + public NavigableMap getModifiedFilesStatistics() { + return mapToStrings(Baseline.MODIFIED_FILES); + } + + /** + * Returns the delta values for the modified files coverage. + * + * @return a mapping of metrics to their delta values (only metrics with a value are included) + */ + @Exported(inline = true) + public NavigableMap getModifiedFilesDelta() { + return mapToStrings(Baseline.MODIFIED_FILES_DELTA); + } + + /** + * Returns the statistics for the coverage of modified lines. + * + * @return a mapping of metrics to their values (only metrics with a value are included) + */ + @Exported(inline = true) + public NavigableMap getModifiedLinesStatistics() { + return mapToStrings(Baseline.MODIFIED_LINES); + } + + /** + * Returns the delta values for the modified lines coverage. + * + * @return a mapping of metrics to their delta values (only metrics with a value are included) + */ + @Exported(inline = true) + public NavigableMap getModifiedLinesDelta() { + return mapToStrings(Baseline.MODIFIED_LINES_DELTA); + } + + private NavigableMap mapToStrings(final Baseline baseline) { + var values = new TreeMap(); + + for (Metric metric : Metric.values()) { + statistics.getValue(baseline, metric) + .ifPresent(value -> values.put(metric.toTagName(), FORMATTER.format(value, Locale.ENGLISH))); + } + + return values; + } + + /** + * Remote API to list the overview of the quality gate evaluation. + */ + @ExportedBean + public static class QualityGateResultApi { + private final QualityGateResult qualityGateResult; + + QualityGateResultApi(final QualityGateResult qualityGateResult) { + this.qualityGateResult = qualityGateResult; + } + + @Exported(inline = true) + public Result getOverallResult() { + return qualityGateResult.getOverallStatus().getResult(); + } + + @Exported(inline = true) + public Collection getResultItems() { + return qualityGateResult.getResultItems().stream().map(QualityGateItemApi::new).collect(Collectors.toList()); + } + } + + /** + * Remote API to show the content of an individual quality gate item. + */ + @ExportedBean + public static class QualityGateItemApi { + private final QualityGateResultItem item; + + QualityGateItemApi(final QualityGateResultItem item) { + this.item = item; + } + + @Exported + public String getQualityGate() { + return item.getQualityGate().getName(); + } + + @Exported + public double getThreshold() { + return item.getQualityGate().getThreshold(); + } + + @Exported(inline = true) + public Result getResult() { + return item.getStatus().getResult(); + } + + @Exported + public String getValue() { + return item.getActualValue(); + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildAction.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildAction.java new file mode 100644 index 000000000..53385a535 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildAction.java @@ -0,0 +1,538 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.ArrayList; +import java.util.List; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.Fraction; + +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.coverage.Value; +import edu.hm.hafner.echarts.ChartModelConfiguration; +import edu.hm.hafner.echarts.JacksonFacade; +import edu.hm.hafner.util.FilteredLog; +import edu.hm.hafner.util.VisibleForTesting; +import edu.umd.cs.findbugs.annotations.NonNull; + +import org.kohsuke.stapler.StaplerProxy; +import hudson.Functions; +import hudson.model.Run; + +import io.jenkins.plugins.coverage.metrics.charts.CoverageTrendChart; +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics; +import io.jenkins.plugins.coverage.metrics.model.ElementFormatter; +import io.jenkins.plugins.coverage.metrics.steps.CoverageXmlStream.MetricFractionMapConverter; +import io.jenkins.plugins.coverage.model.Messages; +import io.jenkins.plugins.echarts.GenericBuildActionIterator.BuildActionIterable; +import io.jenkins.plugins.forensics.reference.ReferenceBuild; +import io.jenkins.plugins.util.AbstractXmlStream; +import io.jenkins.plugins.util.BuildAction; +import io.jenkins.plugins.util.JenkinsFacade; +import io.jenkins.plugins.util.QualityGateResult; + +import static hudson.model.Run.*; + +/** + * Controls the life cycle of the coverage results in a job. This action persists the results of a build and displays a + * summary on the build page. The actual visualization of the results is defined in the matching {@code summary.jelly} + * file. This action also provides access to the coverage details: these are rendered using a new view instance. + * + * @author Ullrich Hafner + */ +@SuppressWarnings({"PMD.GodClass", "checkstyle:ClassDataAbstractionCoupling", "checkstyle:ClassFanOutComplexity"}) +public final class CoverageBuildAction extends BuildAction implements StaplerProxy { + private static final long serialVersionUID = -6023811049340671399L; + + private static final ElementFormatter FORMATTER = new ElementFormatter(); + private static final String NO_REFERENCE_BUILD = "-"; + + private final String id; + private final String name; + + private final String referenceBuildId; + + private final QualityGateResult qualityGateResult; + + private final String icon; + // FIXME: Rethink if we need a separate result object that stores all data? + private final FilteredLog log; + + /** The aggregated values of the result for the root of the tree. */ + private final List projectValues; + + /** The delta of this build's coverages with respect to the reference build. */ + private final NavigableMap difference; + + /** The coverages filtered by changed lines of the associated change request. */ + private final List modifiedLinesCoverage; + + /** The delta of the coverages of the associated change request with respect to the reference build. */ + private final NavigableMap modifiedLinesCoverageDifference; + + /** The coverage of the modified lines. */ + private final List modifiedFilesCoverage; + + /** The coverage delta of the modified lines. */ + private final NavigableMap modifiedFilesCoverageDifference; + + /** The indirect coverage changes of the associated change request with respect to the reference build. */ + private final List indirectCoverageChanges; + + static { + CoverageXmlStream.registerConverters(XSTREAM2); + XSTREAM2.registerLocalConverter(CoverageBuildAction.class, "difference", + new MetricFractionMapConverter()); + XSTREAM2.registerLocalConverter(CoverageBuildAction.class, "modifiedLinesCoverageDifference", + new MetricFractionMapConverter()); + } + + /** + * Creates a new instance of {@link CoverageBuildAction}. + * + * @param owner + * the associated build that created the statistics + * @param id + * ID (URL) of the results + * @param optionalName + * optional name that overrides the default name of the results + * @param icon + * name of the icon that should be used in actions and views + * @param result + * the coverage tree as result to persist with this action + * @param qualityGateResult + * status of the quality gates + * @param log + * the logging statements of the recording step + */ + public CoverageBuildAction(final Run owner, final String id, final String optionalName, final String icon, + final Node result, final QualityGateResult qualityGateResult, final FilteredLog log) { + this(owner, id, optionalName, icon, result, qualityGateResult, log, NO_REFERENCE_BUILD, + new TreeMap<>(), List.of(), new TreeMap<>(), List.of(), new TreeMap<>(), List.of()); + } + + /** + * Creates a new instance of {@link CoverageBuildAction}. + * + * @param owner + * the associated build that created the statistics + * @param id + * ID (URL) of the results + * @param optionalName + * optional name that overrides the default name of the results + * @param icon + * name of the icon that should be used in actions and views + * @param result + * the coverage tree as result to persist with this action + * @param qualityGateResult + * status of the quality gates + * @param log + * the logging statements of the recording step + * @param referenceBuildId + * the ID of the reference build + * @param delta + * delta of this build's coverages with respect to the reference build + * @param modifiedLinesCoverage + * the coverages filtered by changed lines of the associated change request + * @param modifiedLinesCoverageDifference + * the delta of the coverages of the associated change request with respect to the reference build + * @param indirectCoverageChanges + * the indirect coverage changes of the associated change request with respect to the reference build + */ + @SuppressWarnings("checkstyle:ParameterNumber") + public CoverageBuildAction(final Run owner, final String id, final String optionalName, final String icon, + final Node result, final QualityGateResult qualityGateResult, final FilteredLog log, + final String referenceBuildId, + final NavigableMap delta, + final List modifiedLinesCoverage, + final NavigableMap modifiedLinesCoverageDifference, + final List modifiedFilesCoverage, + final NavigableMap modifiedFilesCoverageDifference, + final List indirectCoverageChanges) { + this(owner, id, optionalName, icon, result, qualityGateResult, log, referenceBuildId, delta, + modifiedLinesCoverage, + modifiedLinesCoverageDifference, modifiedFilesCoverage, modifiedFilesCoverageDifference, + indirectCoverageChanges, + true); + } + + @VisibleForTesting + @SuppressWarnings("checkstyle:ParameterNumber") + CoverageBuildAction(final Run owner, final String id, final String name, final String icon, + final Node result, final QualityGateResult qualityGateResult, final FilteredLog log, + final String referenceBuildId, + final NavigableMap delta, + final List modifiedLinesCoverage, + final NavigableMap modifiedLinesCoverageDifference, + final List modifiedFilesCoverage, + final NavigableMap modifiedFilesCoverageDifference, + final List indirectCoverageChanges, + final boolean canSerialize) { + super(owner, result, false); + + this.id = id; + this.name = name; + this.icon = icon; + this.log = log; + + projectValues = result.aggregateValues(); + this.qualityGateResult = qualityGateResult; + difference = delta; + this.modifiedLinesCoverage = new ArrayList<>(modifiedLinesCoverage); + this.modifiedLinesCoverageDifference = modifiedLinesCoverageDifference; + this.modifiedFilesCoverage = new ArrayList<>(modifiedFilesCoverage); + this.modifiedFilesCoverageDifference = modifiedFilesCoverageDifference; + this.indirectCoverageChanges = new ArrayList<>(indirectCoverageChanges); + this.referenceBuildId = referenceBuildId; + + if (canSerialize) { + createXmlStream().write(owner.getRootDir().toPath().resolve(getBuildResultBaseName()), result); + } + } + + /** + * Returns the actual name of the tool. If no user defined name is given, then the default name is returned. + * + * @return the name + */ + private String getActualName() { + return StringUtils.defaultIfBlank(name, Messages.Coverage_Link_Name()); + } + + public FilteredLog getLog() { + return log; + } + + public QualityGateResult getQualityGateResult() { + return qualityGateResult; + } + + public ElementFormatter getFormatter() { + return FORMATTER; + } + + public CoverageStatistics getStatistics() { + return new CoverageStatistics(projectValues, difference, modifiedLinesCoverage, modifiedLinesCoverageDifference, + modifiedFilesCoverage, modifiedFilesCoverageDifference); + } + + /** + * Returns the supported baselines. + * + * @return all supported baselines + */ + @SuppressWarnings("unused") // Called by jelly view + public List getBaselines() { + return List.of(Baseline.PROJECT, Baseline.MODIFIED_FILES, Baseline.MODIFIED_LINES, Baseline.INDIRECT); + } + + /** + * Returns whether a delta metric for the specified metric exists. + * + * @param baseline + * the baseline to use + * + * @return {@code true} if a delta is available for the specified metric, {@code false} otherwise + */ + @SuppressWarnings("unused") // Called by jelly view + public boolean hasBaselineResult(final Baseline baseline) { + return !getValues(baseline).isEmpty(); + } + + /** + * Returns the associate delta baseline for the specified baseline. + * + * @param baseline + * the baseline to get the delta baseline for + * + * @return the delta baseline + * @throws NoSuchElementException + * if this baseline does not provide a delta baseline + */ + @SuppressWarnings("unused") // Called by jelly view + public Baseline getDeltaBaseline(final Baseline baseline) { + if (baseline == Baseline.PROJECT) { + return Baseline.PROJECT_DELTA; + } + if (baseline == Baseline.MODIFIED_LINES) { + return Baseline.MODIFIED_LINES_DELTA; + } + if (baseline == Baseline.MODIFIED_FILES) { + return Baseline.MODIFIED_FILES_DELTA; + } + throw new NoSuchElementException("No delta baseline for this baseline: " + baseline); + } + + /** + * Returns all available values for the specified baseline. + * + * @param baseline + * the baseline to get the values for + * + * @return the available values + * @throws NoSuchElementException + * if this baseline does not provide values + */ + // Called by jelly view + public List getAllValues(final Baseline baseline) { + return getValueStream(baseline).collect(Collectors.toList()); + } + + public NavigableMap getAllDeltas(final Baseline deltaBaseline) { + if (deltaBaseline == Baseline.PROJECT_DELTA) { + return difference; + } + else if (deltaBaseline == Baseline.MODIFIED_LINES_DELTA) { + return modifiedLinesCoverageDifference; + } + else if (deltaBaseline == Baseline.MODIFIED_FILES_DELTA) { + return modifiedFilesCoverageDifference; + } + throw new NoSuchElementException("No delta baseline: " + deltaBaseline); + } + + /** + * Returns all important values for the specified baseline. + * + * @param baseline + * the baseline to get the values for + * + * @return the available values + * @throws NoSuchElementException + * if this baseline does not provide values + */ + // Called by jelly view + public List getValues(final Baseline baseline) { + return filterImportantMetrics(getValueStream(baseline)); + } + + public Optional getValueForMetric(final Baseline baseline, final Metric metric) { + return getAllValues(baseline).stream() + .filter(value -> value.getMetric() == metric) + .findFirst(); + } + + private List filterImportantMetrics(final Stream values) { + return values.filter(v -> getMetricsForSummary().contains(v.getMetric())) + .collect(Collectors.toList()); + } + + private Stream getValueStream(final Baseline baseline) { + if (baseline == Baseline.PROJECT) { + return projectValues.stream(); + } + if (baseline == Baseline.MODIFIED_LINES) { + return modifiedLinesCoverage.stream(); + } + if (baseline == Baseline.MODIFIED_FILES) { + return modifiedFilesCoverage.stream(); + } + if (baseline == Baseline.INDIRECT) { + return indirectCoverageChanges.stream(); + } + throw new NoSuchElementException("No such baseline: " + baseline); + } + + /** + * Returns whether a delta metric for the specified baseline exists. + * + * @param baseline + * the baseline to use + * + * @return {@code true} if a delta is available for the specified baseline, {@code false} otherwise + */ + @SuppressWarnings("unused") // Called by jelly view + public boolean hasDelta(final Baseline baseline) { + return baseline == Baseline.PROJECT || baseline == Baseline.MODIFIED_LINES + || baseline == Baseline.MODIFIED_FILES; + } + + /** + * Returns whether a delta metric for the specified metric exists. + * + * @param baseline + * the baseline to use + * @param metric + * the metric to check + * + * @return {@code true} if a delta is available for the specified metric, {@code false} otherwise + */ + public boolean hasDelta(final Baseline baseline, final Metric metric) { + if (baseline == Baseline.PROJECT) { + return difference.containsKey(metric); + } + if (baseline == Baseline.MODIFIED_LINES) { + return modifiedLinesCoverageDifference.containsKey(metric) + && Set.of(Metric.BRANCH, Metric.LINE).contains(metric); + } + if (baseline == Baseline.MODIFIED_FILES) { + return modifiedFilesCoverageDifference.containsKey(metric) + && Set.of(Metric.BRANCH, Metric.LINE).contains(metric); + } + if (baseline == Baseline.INDIRECT) { + return false; + } + throw new NoSuchElementException("No such baseline: " + baseline); + } + + /** + * Returns whether a value for the specified metric exists. + * + * @param baseline + * the baseline to use + * @param metric + * the metric to check + * + * @return {@code true} if a value is available for the specified metric, {@code false} otherwise + */ + public boolean hasValue(final Baseline baseline, final Metric metric) { + return getAllValues(baseline).stream() + .anyMatch(v -> v.getMetric() == metric); + } + + /** + * Returns a formatted and localized String representation of the value for the specified metric (with respect to + * the given baseline). + * + * @param baseline + * the baseline to use + * @param metric + * the metric to get the delta for + * + * @return the formatted value + */ + public String formatValue(final Baseline baseline, final Metric metric) { + var value = getValueForMetric(baseline, metric); + return value.isPresent() ? FORMATTER.formatValue(value.get()) : Messages.Coverage_Not_Available(); + } + + /** + * Returns a formatted and localized String representation of the delta for the specified metric (with respect to + * the given baseline). + * + * @param baseline + * the baseline to use + * @param metric + * the metric to get the delta for + * + * @return the delta metric + */ + @SuppressWarnings("unused") // Called by jelly view + public String formatDelta(final Baseline baseline, final Metric metric) { + if (baseline == Baseline.PROJECT) { + if (hasDelta(baseline, metric)) { + return FORMATTER.formatDelta(difference.get(metric), metric, + Functions.getCurrentLocale()); + } + } + if (baseline == Baseline.MODIFIED_LINES) { + if (hasDelta(baseline, metric)) { + return FORMATTER.formatDelta(modifiedLinesCoverageDifference.get(metric), metric, + Functions.getCurrentLocale()); + } + } + if (baseline == Baseline.MODIFIED_FILES) { + if (hasDelta(baseline, metric)) { + return FORMATTER.formatDelta(modifiedFilesCoverageDifference.get(metric), metric, + Functions.getCurrentLocale()); + } + } + return Messages.Coverage_Not_Available(); + } + + /** + * Returns the visible metrics for the project summary. + * + * @return the metrics to be shown in the project summary + */ + @VisibleForTesting + NavigableSet getMetricsForSummary() { + return new TreeSet<>( + Set.of(Metric.LINE, Metric.LOC, Metric.BRANCH, Metric.COMPLEXITY_DENSITY, Metric.MUTATION)); + } + + /** + * Returns the possible reference build that has been used to compute the coverage delta. + * + * @return the reference build, if available + */ + public Optional> getReferenceBuild() { + if (NO_REFERENCE_BUILD.equals(referenceBuildId)) { + return Optional.empty(); + } + return new JenkinsFacade().getBuild(referenceBuildId); + } + + /** + * Renders the reference build as HTML link. + * + * @return the reference build + * @see #getReferenceBuild() + */ + @SuppressWarnings("unused") // Called by jelly view + public String getReferenceBuildLink() { + return ReferenceBuild.getReferenceBuildLink(referenceBuildId); + } + + @Override + protected AbstractXmlStream createXmlStream() { + return new CoverageXmlStream(); + } + + @Override + protected CoverageJobAction createProjectAction() { + return new CoverageJobAction(getOwner().getParent(), getUrlName(), name, icon); + } + + @Override + protected String getBuildResultBaseName() { + return String.format("%s.xml", id); + } + + @Override + public CoverageViewModel getTarget() { + return new CoverageViewModel(getOwner(), getUrlName(), name, getResult(), + getStatistics(), getQualityGateResult(), getReferenceBuildLink(), log, this::createChartModel); + } + + private String createChartModel(final String configuration) { + // FIXME: add without optional + var iterable = new BuildActionIterable<>(CoverageBuildAction.class, Optional.of(this), + action -> getUrlName().equals(action.getUrlName()), CoverageBuildAction::getStatistics); + return new JacksonFacade().toJson( + new CoverageTrendChart().create(iterable, ChartModelConfiguration.fromJson(configuration))); + } + + @NonNull + @Override + public String getIconFileName() { + return icon; + } + + @NonNull + @Override + public String getDisplayName() { + return getActualName(); + } + + @NonNull + @Override + public String getUrlName() { + return id; + } + + @Override + public String toString() { + return String.format("%s (%s): %s", getDisplayName(), getUrlName(), projectValues); + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisher.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisher.java new file mode 100644 index 000000000..aacf5d2e2 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisher.java @@ -0,0 +1,426 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.NavigableMap; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.math.Fraction; + +import edu.hm.hafner.coverage.Coverage; +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.util.VisibleForTesting; + +import hudson.Functions; +import hudson.model.TaskListener; + +import io.jenkins.plugins.checks.api.ChecksAnnotation; +import io.jenkins.plugins.checks.api.ChecksAnnotation.ChecksAnnotationBuilder; +import io.jenkins.plugins.checks.api.ChecksAnnotation.ChecksAnnotationLevel; +import io.jenkins.plugins.checks.api.ChecksConclusion; +import io.jenkins.plugins.checks.api.ChecksDetails; +import io.jenkins.plugins.checks.api.ChecksDetails.ChecksDetailsBuilder; +import io.jenkins.plugins.checks.api.ChecksOutput.ChecksOutputBuilder; +import io.jenkins.plugins.checks.api.ChecksPublisherFactory; +import io.jenkins.plugins.checks.api.ChecksStatus; +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.coverage.metrics.model.ElementFormatter; +import io.jenkins.plugins.coverage.metrics.steps.CoverageRecorder.ChecksAnnotationScope; +import io.jenkins.plugins.util.JenkinsFacade; +import io.jenkins.plugins.util.QualityGateStatus; + +/** + * Publishes coverage as Checks to SCM platforms. + * + * @author Florian Orendi + */ +class CoverageChecksPublisher { + private static final ElementFormatter FORMATTER = new ElementFormatter(); + + private final CoverageBuildAction action; + private final JenkinsFacade jenkinsFacade; + private final String checksName; + private final ChecksAnnotationScope annotationScope; + + CoverageChecksPublisher(final CoverageBuildAction action, final String checksName, + final ChecksAnnotationScope annotationScope) { + this(action, checksName, annotationScope, new JenkinsFacade()); + } + + @VisibleForTesting + CoverageChecksPublisher(final CoverageBuildAction action, + final String checksName, final ChecksAnnotationScope annotationScope, final JenkinsFacade jenkinsFacade) { + this.jenkinsFacade = jenkinsFacade; + this.action = action; + this.checksName = checksName; + this.annotationScope = annotationScope; + } + + /** + * Publishes the coverage report as Checks to SCM platforms. + * + * @param listener + * The task listener + */ + void publishCoverageReport(final TaskListener listener) { + var publisher = ChecksPublisherFactory.fromRun(action.getOwner(), listener); + publisher.publish(extractChecksDetails()); + } + + @VisibleForTesting + ChecksDetails extractChecksDetails() { + var output = new ChecksOutputBuilder() + .withTitle(getChecksTitle()) + .withSummary(getSummary()) + .withAnnotations(getAnnotations()) + .build(); + + return new ChecksDetailsBuilder() + .withName(checksName) + .withStatus(ChecksStatus.COMPLETED) + .withConclusion(getCheckConclusion(action.getQualityGateResult().getOverallStatus())) + .withDetailsURL(getCoverageReportBaseUrl()) + .withOutput(output) + .build(); + } + + private String getChecksTitle() { + return String.format("%s: %s", + FORMATTER.getDisplayName(Baseline.MODIFIED_LINES), + action.formatValue(Baseline.MODIFIED_LINES, Metric.LINE)); + } + + private String getSummary() { + var root = action.getResult(); + return getOverallCoverageSummary(root) + "\n\n" + + getQualityGatesSummary() + "\n\n" + + getProjectMetricsSummary(root); + } + + private List getAnnotations() { + if (annotationScope == ChecksAnnotationScope.SKIP) { + return List.of(); + } + + var tree = action.getResult(); + Node filtered; + if (annotationScope == ChecksAnnotationScope.ALL_LINES) { + filtered = tree; + } + else { + filtered = tree.filterByModifiedLines(); + } + + var annotations = new ArrayList(); + for (var fileNode : filtered.getAllFileNodes()) { + annotations.addAll(getMissingLines(fileNode)); + annotations.addAll(getPartiallyCoveredLines(fileNode)); + annotations.addAll(getSurvivedMutations(fileNode)); + } + return annotations; + } + + private Collection getMissingLines(final FileNode fileNode) { + var builder = createAnnotationBuilder(fileNode).withTitle("Not covered line"); + + return fileNode.getMissedLines().stream() + .map(line -> builder.withMessage("Line " + line + " is not covered by tests").withStartLine(line).build()) + .collect(Collectors.toList()); + } + + private Collection getSurvivedMutations(final FileNode fileNode) { + var builder = createAnnotationBuilder(fileNode).withTitle("Mutation survived"); + + return fileNode.getSurvivedMutations().entrySet().stream() + .map(entry -> builder.withMessage(createMutationMessage(entry.getKey(), entry.getValue())) + .withStartLine(entry.getKey()).build()) + .collect(Collectors.toList()); + } + + private String createMutationMessage(final int line, final int survived) { + if (survived == 1) { + return "One mutation survived in line " + line; + } + return String.format("%d mutations survived in line %d", survived, line); + } + + private Collection getPartiallyCoveredLines(final FileNode fileNode) { + var builder = createAnnotationBuilder(fileNode).withTitle("Partially covered line"); + + return fileNode.getPartiallyCoveredLines().entrySet().stream() + .map(entry -> builder.withMessage(createBranchMessage(entry.getKey(), entry.getValue())) + .withStartLine(entry.getKey()).build()) + .collect(Collectors.toList()); + } + + private String createBranchMessage(final int line, final int missed) { + if (missed == 1) { + return "Line " + line + " is only partially covered, one branch is missing"; + + } + return "Line " + line + " is only partially covered, %d branches are missing."; + } + + private ChecksAnnotationBuilder createAnnotationBuilder(final FileNode fileNode) { + return new ChecksAnnotationBuilder() + .withPath(fileNode.getPath()) + .withAnnotationLevel(ChecksAnnotationLevel.WARNING); + } + + private String getCoverageReportBaseUrl() { + return jenkinsFacade.getAbsoluteUrl(action.getOwner().getUrl(), action.getUrlName()); + } + + private String getOverallCoverageSummary(final Node root) { + String sectionHeader = getSectionHeader(2, Messages.Checks_Summary()); + + var modifiedFilesCoverageRoot = root.filterByModifiedFiles(); + var modifiedLinesCoverageRoot = root.filterByModifiedLines(); + var indirectlyChangedCoverage = root.filterByIndirectChanges(); + + var projectCoverageHeader = getBulletListItem(1, + formatText(TextFormat.BOLD, getUrlText(Baseline.PROJECT_DELTA.getTitle(), + getCoverageReportBaseUrl() + Baseline.PROJECT_DELTA.getUrl()))); + var modifiedFilesCoverageHeader = getBulletListItem(1, + formatText(TextFormat.BOLD, getUrlText(Baseline.MODIFIED_FILES_DELTA.getTitle(), + getCoverageReportBaseUrl() + Baseline.MODIFIED_FILES_DELTA.getUrl()))); + var modifiedLinesCoverageHeader = getBulletListItem(1, + formatText(TextFormat.BOLD, getUrlText(Baseline.MODIFIED_LINES_DELTA.getTitle(), + getCoverageReportBaseUrl() + Baseline.MODIFIED_LINES_DELTA.getUrl()))); + var indirectCoverageChangesHeader = getBulletListItem(1, + formatText(TextFormat.BOLD, getUrlText(Baseline.INDIRECT.getTitle(), + getCoverageReportBaseUrl() + Baseline.INDIRECT.getUrl()))); + + var projectCoverageLine = getBulletListItem(2, + formatCoverageForMetric(Metric.LINE, Baseline.PROJECT)); + var projectCoverageBranch = getBulletListItem(2, + formatCoverageForMetric(Metric.BRANCH, Baseline.PROJECT)); + var projectCoverageComplexity = getBulletListItem(2, formatRootValueOfMetric(root, Metric.COMPLEXITY_DENSITY)); + var projectCoverageLoc = getBulletListItem(2, formatRootValueOfMetric(root, Metric.LOC)); + + var modifiedFilesCoverageLine = getBulletListItem(2, + formatCoverageForMetric(Metric.LINE, Baseline.MODIFIED_FILES)); + var modifiedFilesCoverageBranch = getBulletListItem(2, + formatCoverageForMetric(Metric.BRANCH, Baseline.MODIFIED_FILES)); + var modifiedFilesCoverageComplexity = getBulletListItem(2, + formatRootValueOfMetric(modifiedFilesCoverageRoot, Metric.COMPLEXITY_DENSITY)); + var modifiedFilesCoverageLoc = getBulletListItem(2, + formatRootValueOfMetric(modifiedFilesCoverageRoot, Metric.LOC)); + + var modifiedLinesCoverageLine = getBulletListItem(2, + formatCoverageForMetric(Metric.LINE, Baseline.MODIFIED_LINES)); + var modifiedLinesCoverageBranch = getBulletListItem(2, + formatCoverageForMetric(Metric.BRANCH, Baseline.MODIFIED_LINES)); + var modifiedLinesCoverageLoc = getBulletListItem(2, + formatRootValueOfMetric(modifiedLinesCoverageRoot, Metric.LOC)); + + var indirectCoverageChangesLine = getBulletListItem(2, + formatCoverageForMetric(Metric.LINE, Baseline.INDIRECT)); + var indirectCoverageChangesBranch = getBulletListItem(2, + formatCoverageForMetric(Metric.BRANCH, Baseline.INDIRECT)); + var indirectCoverageChangesLoc = getBulletListItem(2, + formatRootValueOfMetric(indirectlyChangedCoverage, Metric.LOC)); + + return sectionHeader + + projectCoverageHeader + + projectCoverageLine + + projectCoverageBranch + + projectCoverageComplexity + + projectCoverageLoc + + modifiedFilesCoverageHeader + + modifiedFilesCoverageLine + + modifiedFilesCoverageBranch + + modifiedFilesCoverageComplexity + + modifiedFilesCoverageLoc + + modifiedLinesCoverageHeader + + modifiedLinesCoverageLine + + modifiedLinesCoverageBranch + + modifiedLinesCoverageLoc + + indirectCoverageChangesHeader + + indirectCoverageChangesLine + + indirectCoverageChangesBranch + + indirectCoverageChangesLoc; + } + + /** + * Checks overview regarding the quality gate status. + * + * @return the markdown string representing the status summary + */ + // TODO: expand with summary of status of each defined quality gate + private String getQualityGatesSummary() { + return getSectionHeader(2, + Messages.Checks_QualityGates(action.getQualityGateResult().getOverallStatus().name())); + } + + private String getProjectMetricsSummary(final Node result) { + String sectionHeader = getSectionHeader(2, Messages.Checks_ProjectOverview()); + + List coverageDisplayNames = FORMATTER.getSortedCoverageDisplayNames(); + String header = formatRow(coverageDisplayNames); + String headerSeparator = formatRow( + getTableSeparators(ColumnAlignment.CENTER, coverageDisplayNames.size())); + + String projectCoverageName = String.format("|%s **%s**", Icon.WHITE_CHECK_MARK.markdown, + FORMATTER.getDisplayName(Baseline.PROJECT)); + List projectCoverage = FORMATTER.getFormattedValues(FORMATTER.getSortedCoverageValues(result), + Functions.getCurrentLocale()); + String projectCoverageRow = projectCoverageName + formatRow(projectCoverage); + + String projectCoverageDeltaName = String.format("|%s **%s**", Icon.CHART_UPWARDS_TREND.markdown, + FORMATTER.getDisplayName(Baseline.PROJECT_DELTA)); + Collection projectCoverageDelta = formatCoverageDelta(Metric.getCoverageMetrics(), + action.getAllDeltas(Baseline.PROJECT_DELTA)); + String projectCoverageDeltaRow = + projectCoverageDeltaName + formatRow(projectCoverageDelta); + + return sectionHeader + + header + + headerSeparator + + projectCoverageRow + + projectCoverageDeltaRow; + } + + private String formatCoverageForMetric(final Metric metric, final Baseline baseline) { + return String.format("%s: %s / %s", FORMATTER.getDisplayName(metric), + action.formatValue(baseline, metric), action.formatDelta(baseline, metric)); + } + + private String formatRootValueOfMetric(final Node root, final Metric metric) { + var value = root.getValue(metric); + return value.map(FORMATTER::formatValueWithMetric) + .orElseGet(() -> FORMATTER.getDisplayName(metric) + ": " + Messages.Coverage_Not_Available()); + } + + private String formatText(final TextFormat format, final String text) { + switch (format) { + case BOLD: + return "**" + text + "**"; + case CURSIVE: + return "_" + text + "_"; + default: + return text; + } + } + + /** + * Formats the passed delta computation to a collection of its display representations, which is sorted by the + * metric ordinal. Also, a collection of required metrics is passed. This is used to fill not existent metrics which + * are required for the representation. Coverage deltas might not be existent if the reference does not contain a + * reference value of the metric. + * + * @param requiredMetrics + * The metrics which should be displayed + * @param deltas + * The delta calculation mapped by their metric + */ + private Collection formatCoverageDelta(final Collection requiredMetrics, + final NavigableMap deltas) { + var coverageDelta = new TreeMap(); + for (Metric metric : requiredMetrics) { + if (deltas.containsKey(metric)) { + var coverage = deltas.get(metric); + coverageDelta.putIfAbsent(metric, + FORMATTER.formatDelta(coverage, metric, Functions.getCurrentLocale()) + + getTrendIcon(coverage.doubleValue())); + } + else { + coverageDelta.putIfAbsent(metric, + FORMATTER.formatPercentage(Coverage.nullObject(metric), Functions.getCurrentLocale())); + } + } + return coverageDelta.values(); + } + + private String getTrendIcon(final double trend) { + if (trend > 0) { + return " " + Icon.ARROW_UP.markdown; + } + else if (trend < 0) { + return " " + Icon.ARROW_DOWN.markdown; + } + else { + return " " + Icon.ARROW_RIGHT.markdown; + } + } + + private List getTableSeparators(final ColumnAlignment alignment, final int count) { + switch (alignment) { + case LEFT: + return Collections.nCopies(count, ":---"); + case RIGHT: + return Collections.nCopies(count, "---:"); + case CENTER: + default: + return Collections.nCopies(count, ":---:"); + } + } + + private String getBulletListItem(final int level, final String text) { + int whitespaces = (level - 1) * 2; + return String.join("", Collections.nCopies(whitespaces, " ")) + "* " + text + "\n"; + } + + private String getUrlText(final String text, final String url) { + return String.format("[%s](%s)", text, url); + } + + private String formatRow(final Collection columns) { + StringBuilder row = new StringBuilder(); + for (Object column : columns) { + row.append(String.format("|%s", column)); + } + if (columns.size() > 0) { + row.append('|'); + } + row.append('\n'); + return row.toString(); + } + + private String getSectionHeader(final int level, final String text) { + return String.join("", Collections.nCopies(level, "#")) + " " + text + "\n\n"; + } + + private ChecksConclusion getCheckConclusion(final QualityGateStatus status) { + switch (status) { + case INACTIVE: + case PASSED: + return ChecksConclusion.SUCCESS; + case FAILED: + case WARNING: + return ChecksConclusion.FAILURE; + default: + throw new IllegalArgumentException("Unsupported quality gate status: " + status); + } + } + + private enum ColumnAlignment { + CENTER, + LEFT, + RIGHT + } + + private enum Icon { + WHITE_CHECK_MARK(":white_check_mark:"), + CHART_UPWARDS_TREND(":chart_with_upwards_trend:"), + ARROW_UP(":arrow_up:"), + ARROW_RIGHT(":arrow_right:"), + ARROW_DOWN(":arrow_down:"); + + private final String markdown; + + Icon(final String markdown) { + this.markdown = markdown; + } + } + + private enum TextFormat { + BOLD, + CURSIVE + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageJobAction.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageJobAction.java new file mode 100644 index 000000000..0c0af8b3e --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageJobAction.java @@ -0,0 +1,91 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.Optional; +import java.util.function.Predicate; + +import org.apache.commons.lang3.StringUtils; + +import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.echarts.ChartModelConfiguration; +import edu.hm.hafner.echarts.line.LinesChartModel; +import edu.umd.cs.findbugs.annotations.NonNull; + +import hudson.model.Job; + +import io.jenkins.plugins.coverage.metrics.charts.CoverageTrendChart; +import io.jenkins.plugins.echarts.ActionSelector; +import io.jenkins.plugins.echarts.GenericBuildActionIterator.BuildActionIterable; +import io.jenkins.plugins.echarts.TrendChartJobAction; + +/** + * Project level action for the coverage results. A job action displays a link on the side panel of a job that refers to + * the last build that contains coverage results (i.e. a {@link CoverageBuildAction} with a {@link Node} instance). This + * action also is responsible to render the historical trend via its associated 'floatingBox.jelly' view. + * + * @author Ullrich Hafner + */ +public class CoverageJobAction extends TrendChartJobAction { + private final String id; + private final String name; + private final String icon; + + CoverageJobAction(final Job owner, final String id, final String name, final String icon) { + super(owner, CoverageBuildAction.class); + + this.id = id; + this.name = name; + this.icon = icon; + } + + @Override + public String getIconFileName() { + return icon; + } + + @Override + public String getDisplayName() { + return StringUtils.defaultIfBlank(name, Messages.Coverage_Link_Name()); + } + + /** + * Returns a label for the trend chart. + * + * @return a label for the trend chart + */ + public String getTrendName() { + if (StringUtils.isBlank(name)) { + return Messages.Coverage_Trend_Default_Name(); + } + return Messages.Coverage_Trend_Name(name); + } + + @Override @NonNull + public String getUrlName() { + return id; + } + + public Job getProject() { + return getOwner(); + } + + public String getSearchUrl() { + return getUrlName(); + } + + @Override + protected LinesChartModel createChartModel(final String configuration) { + var iterable = new BuildActionIterable<>(CoverageBuildAction.class, getLatestAction(), + selectByUrl(), CoverageBuildAction::getStatistics); + + return new CoverageTrendChart().create(iterable, ChartModelConfiguration.fromJson(configuration)); + } + + @Override + public Optional getLatestAction() { + return new ActionSelector<>(CoverageBuildAction.class, selectByUrl()).findFirst(getOwner().getLastBuild()); + } + + private Predicate selectByUrl() { + return action -> getUrlName().equals(action.getUrlName()); + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn.java new file mode 100644 index 000000000..9a8924bdf --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn.java @@ -0,0 +1,297 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.List; +import java.util.Optional; + +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Value; +import edu.hm.hafner.util.VisibleForTesting; +import edu.umd.cs.findbugs.annotations.NonNull; + +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.verb.POST; +import org.jenkinsci.Symbol; +import hudson.Extension; +import hudson.Functions; +import hudson.model.AbstractProject; +import hudson.model.Item; +import hudson.model.Job; +import hudson.model.Run; +import hudson.util.ListBoxModel; +import hudson.views.ListViewColumn; +import hudson.views.ListViewColumnDescriptor; + +import io.jenkins.plugins.coverage.metrics.color.ColorProvider; +import io.jenkins.plugins.coverage.metrics.color.ColorProvider.DisplayColors; +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.coverage.metrics.model.ElementFormatter; +import io.jenkins.plugins.util.JenkinsFacade; + +/** + * Dashboard column model which represents coverage metrics of different coverage types. + * + * @author Florian Orendi + */ +public class CoverageMetricColumn extends ListViewColumn { + private static final ElementFormatter FORMATTER = new ElementFormatter(); + + private String columnName = Messages.Coverage_Column(); + private Metric metric = Metric.LINE; + private Baseline baseline = Baseline.PROJECT; + + /** + * Creates a new column. + */ + @DataBoundConstructor + public CoverageMetricColumn() { + super(); + } + + public ElementFormatter getFormatter() { + return FORMATTER; + } + + /** + * Sets the display name of the column. + * + * @param columnName + * the human-readable name of the column + */ + @DataBoundSetter + public void setColumnName(final String columnName) { + this.columnName = columnName; + } + + public String getColumnName() { + return columnName; + } + + /** + * Sets the baseline of the values that will be shown. + * + * @param baseline + * the baseline to use + */ + @DataBoundSetter + public void setBaseline(final Baseline baseline) { + this.baseline = baseline; + } + + public Baseline getBaseline() { + return baseline; + } + + /** + * Sets the metric of the values that will be shown. + * + * @param metric + * the metric to use + */ + @DataBoundSetter + public void setMetric(final Metric metric) { + this.metric = metric; + } + + public Metric getMetric() { + return metric; + } + + /** + * Returns all available values for the specified baseline. + * + * @param job + * the job in the current row + * + * @return the available values + */ + // Called by jelly view + public List getAllValues(final Job job) { + return findAction(job).map(a -> a.getAllValues(baseline)).orElse(List.of()); + } + + /** + * Returns a formatted and localized String representation of the specified value (without metric). + * + * @param value + * the value to format + * + * @return the value formatted as a string + */ + @SuppressWarnings("unused") // Called by jelly view + public String formatMetric(final Value value) { + return FORMATTER.getDisplayName(value.getMetric()); + } + + /** + * Returns a formatted and localized String representation of the specified value (without metric). + * + * @param value + * the value to format + * + * @return the value formatted as a string + */ + @SuppressWarnings("unused") // Called by jelly view + public String formatValue(final Value value) { + return FORMATTER.formatDetails(value, Functions.getCurrentLocale()); + } + + /** + * Provides a text which represents the coverage percentage of the selected coverage type and metric. + * + * @param job + * the job in the current row + * + * @return the coverage text + */ + public String getCoverageText(final Job job) { + Optional coverageValue = getCoverageValue(job); + if (coverageValue.isPresent()) { + return FORMATTER.format(coverageValue.get(), Functions.getCurrentLocale()); + } + return Messages.Coverage_Not_Available(); + } + + /** + * Provides the coverage value of the selected coverage type and metric. + * + * @param job + * the job in the current row + * + * @return the coverage percentage + */ + public Optional getCoverageValue(final Job job) { + return findAction(job).flatMap(action -> action.getStatistics().getValue(getBaseline(), metric)); + } + + private static Optional findAction(final Job job) { + var lastCompletedBuild = job.getLastCompletedBuild(); + if (lastCompletedBuild == null) { + return Optional.empty(); + } + return Optional.ofNullable(lastCompletedBuild.getAction(CoverageBuildAction.class)); + } + + /** + * Provides the line color for representing the passed coverage value. + * + * @param job + * the job in the current row + * @param coverage + * The coverage value as percentage + * + * @return the line color as hex string + */ + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public DisplayColors getDisplayColors(final Job job, final Optional coverage) { + if (coverage.isPresent() && hasCoverageAction(job)) { + return FORMATTER.getDisplayColors(baseline, coverage.get()); + } + return ColorProvider.DEFAULT_COLOR; + } + + /** + * Provides the relative URL which can be used for accessing the coverage report. + * + * @param job + * the job in the current row + * + * @return the relative URL or an empty string when there is no matching URL + */ + public String getRelativeCoverageUrl(final Job job) { + if (hasCoverageAction(job)) { + CoverageBuildAction action = job.getLastCompletedBuild().getAction(CoverageBuildAction.class); + return action.getUrlName() + "/" + baseline.getUrl(); + } + return ""; + } + + /** + * Transforms percentages with a ',' decimal separator to a representation using a '.' in order to use the + * percentage for styling HTML tags. + * + * @param percentage + * The text representation of a percentage + * + * @return the formatted percentage string + */ + public String getBackgroundColorFillPercentage(final String percentage) { + return FORMATTER.getBackgroundColorFillPercentage(percentage); + } + + /** + * Checks whether a {@link CoverageBuildAction} exists within the completed build. + * + * @param job + * the job in the current row + * + * @return {@code true} whether the action exists, else {@code false} + */ + private boolean hasCoverageAction(final Job job) { + Run lastCompletedBuild = job.getLastCompletedBuild(); + return lastCompletedBuild != null && !lastCompletedBuild.getActions(CoverageBuildAction.class).isEmpty(); + } + + /** + * Descriptor of the column. + */ + @Extension(optional = true) + @Symbol("coverageTotalsColumn") + public static class CoverageMetricColumnDescriptor extends ListViewColumnDescriptor { + /** + * Creates a new descriptor. + */ + @SuppressWarnings("unused") // Required for Jenkins Extensions + public CoverageMetricColumnDescriptor() { + this(new JenkinsFacade()); + } + + @VisibleForTesting + CoverageMetricColumnDescriptor(final JenkinsFacade jenkins) { + this.jenkins = jenkins; + } + + private final JenkinsFacade jenkins; + + @NonNull + @Override + public String getDisplayName() { + return Messages.Coverage_Column(); + } + + /** + * Returns a model with all {@link Metric metrics} that can be used in quality gates. + * + * @param project + * the project that is configured + * + * @return a model with all {@link Metric metrics}. + */ + @POST + @SuppressWarnings("unused") // used by Stapler view data binding + public ListBoxModel doFillMetricItems(@AncestorInPath final AbstractProject project) { + if (jenkins.hasPermission(Item.CONFIGURE, project)) { + return FORMATTER.getMetricItems(); + } + return new ListBoxModel(); + } + + /** + * Returns a model with all {@link Metric metrics} that can be used in quality gates. + * + * @param project + * the project that is configured + * + * @return a model with all {@link Metric metrics}. + */ + @POST + @SuppressWarnings("unused") // used by Stapler view data binding + public ListBoxModel doFillBaselineItems(@AncestorInPath final AbstractProject project) { + if (jenkins.hasPermission(Item.CONFIGURE, project)) { + return FORMATTER.getBaselineItems(); + } + return new ListBoxModel(); + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate.java new file mode 100644 index 000000000..4a0bd09d5 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate.java @@ -0,0 +1,144 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.util.VisibleForTesting; + +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.verb.POST; +import hudson.Extension; +import hudson.model.AbstractProject; +import hudson.model.Item; +import hudson.util.ListBoxModel; + +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.coverage.metrics.model.ElementFormatter; +import io.jenkins.plugins.util.JenkinsFacade; +import io.jenkins.plugins.util.QualityGate; + +/** + * Defines a quality gate based on a specific threshold of code coverage in the current build. After a build has been + * finished, a set of {@link CoverageQualityGate quality gates} will be evaluated and the overall quality gate status will be + * reported in Jenkins UI. + * + * @author Johannes Walter + */ +public class CoverageQualityGate extends QualityGate { + private static final long serialVersionUID = -397278599489426668L; + + private static final ElementFormatter FORMATTER = new ElementFormatter(); + + private final Metric metric; + private Baseline baseline = Baseline.PROJECT; + + /** + * Creates a new instance of {@link CoverageQualityGate}. + * + * @param threshold + * minimum or maximum value that triggers this quality gate + * @param metric + * the metric to compare + */ + @DataBoundConstructor + public CoverageQualityGate(final double threshold, final Metric metric) { + super(threshold); + + this.metric = metric; + } + + CoverageQualityGate(final double threshold, final Metric metric, + final Baseline baseline, final QualityGateCriticality criticality) { + this(threshold, metric); + + setBaseline(baseline); + setCriticality(criticality); + } + + /** + * Sets the baseline that will be used for the quality gate evaluation. + * + * @param baseline + * the baseline to use + */ + @DataBoundSetter + public final void setBaseline(final Baseline baseline) { + this.baseline = baseline; + } + + /** + * Returns a human-readable name of the quality gate. + * + * @return a human-readable name + */ + @Override + public String getName() { + return String.format("%s - %s", FORMATTER.getDisplayName(getBaseline()), + FORMATTER.getDisplayName(getMetric())); + } + + public Metric getMetric() { + return metric; + } + + public Baseline getBaseline() { + return baseline; + } + + /** + * Descriptor of the {@link CoverageQualityGate}. + */ + @Extension + public static class Descriptor extends QualityGateDescriptor { + private final JenkinsFacade jenkins; + + @VisibleForTesting + Descriptor(final JenkinsFacade jenkinsFacade) { + super(); + + jenkins = jenkinsFacade; + } + + /** + * Creates a new descriptor. + */ + @SuppressWarnings("unused") // Required for Jenkins Extensions + public Descriptor() { + this(new JenkinsFacade()); + } + + /** + * Returns a model with all {@link Metric metrics} that can be used in quality gates. + * + * @param project + * the project that is configured + * + * @return a model with all {@link Metric metrics}. + */ + @POST + @SuppressWarnings("unused") // used by Stapler view data binding + public ListBoxModel doFillMetricItems(@AncestorInPath final AbstractProject project) { + if (jenkins.hasPermission(Item.CONFIGURE, project)) { + return FORMATTER.getMetricItems(); + } + return new ListBoxModel(); + } + + /** + * Returns a model with all {@link Metric metrics} that can be used in quality gates. + * + * @param project + * the project that is configured + * + * @return a model with all {@link Metric metrics}. + */ + @POST + @SuppressWarnings("unused") // used by Stapler view data binding + public ListBoxModel doFillBaselineItems(@AncestorInPath final AbstractProject project) { + if (jenkins.hasPermission(Item.CONFIGURE, project)) { + return FORMATTER.getBaselineItems(); + } + return new ListBoxModel(); + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGateEvaluator.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGateEvaluator.java new file mode 100644 index 000000000..0faf54920 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGateEvaluator.java @@ -0,0 +1,42 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.Collection; +import java.util.Locale; + +import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics; +import io.jenkins.plugins.coverage.metrics.model.ElementFormatter; +import io.jenkins.plugins.util.QualityGateEvaluator; +import io.jenkins.plugins.util.QualityGateResult; +import io.jenkins.plugins.util.QualityGateStatus; + +/** + * Evaluates a given set of quality gates. + * + * @author Johannes Walter + */ +class CoverageQualityGateEvaluator extends QualityGateEvaluator { + private static final ElementFormatter FORMATTER = new ElementFormatter(); + private final CoverageStatistics statistics; + + CoverageQualityGateEvaluator(final Collection qualityGates, final CoverageStatistics statistics) { + super(qualityGates); + + this.statistics = statistics; + } + + @Override + protected void evaluate(final CoverageQualityGate qualityGate, final QualityGateResult result) { + var baseline = qualityGate.getBaseline(); + var possibleValue = statistics.getValue(baseline, qualityGate.getMetric()); + if (possibleValue.isPresent()) { + var actualValue = possibleValue.get(); + + var status = actualValue.isBelowThreshold( + qualityGate.getThreshold()) ? qualityGate.getStatus() : QualityGateStatus.PASSED; + result.add(qualityGate, status, FORMATTER.format(actualValue, Locale.ENGLISH)); + } + else { + result.add(qualityGate, qualityGate.getStatus(), "n/a"); + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder.java new file mode 100644 index 000000000..35c7c6198 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder.java @@ -0,0 +1,612 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; + +import edu.hm.hafner.coverage.ModuleNode; +import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.util.FilteredLog; +import edu.umd.cs.findbugs.annotations.NonNull; + +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.verb.POST; +import org.jenkinsci.Symbol; +import hudson.Extension; +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.AbstractBuild; +import hudson.model.AbstractProject; +import hudson.model.BuildListener; +import hudson.model.Item; +import hudson.model.Result; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.tasks.BuildStepDescriptor; +import hudson.tasks.BuildStepMonitor; +import hudson.tasks.Publisher; +import hudson.tasks.Recorder; +import hudson.tools.ToolDescriptor; +import hudson.util.ComboBoxModel; +import hudson.util.FormValidation; +import hudson.util.FormValidation.Kind; +import hudson.util.ListBoxModel; + +import io.jenkins.plugins.coverage.metrics.steps.CoverageTool.Parser; +import io.jenkins.plugins.prism.SourceCodeDirectory; +import io.jenkins.plugins.prism.SourceCodeRetention; +import io.jenkins.plugins.util.AgentFileVisitor.FileVisitorResult; +import io.jenkins.plugins.util.EnvironmentResolver; +import io.jenkins.plugins.util.JenkinsFacade; +import io.jenkins.plugins.util.LogHandler; +import io.jenkins.plugins.util.RunResultHandler; +import io.jenkins.plugins.util.StageResultHandler; +import io.jenkins.plugins.util.ValidationUtilities; + +/** + * A pipeline {@code Step} or Freestyle or Maven {@link Recorder} that reads and parses coverage results in a build and + * adds the results to the persisted build results. + *

+ * Stores the created issues in a {@link Node}. This result is then attached to the {@link Run build} by registering a + * {@link CoverageBuildAction}. + *

+ * + * @author Ullrich Hafner + */ +@SuppressWarnings("checkstyle:ClassFanOutComplexity") +public class CoverageRecorder extends Recorder { + static final String CHECKS_DEFAULT_NAME = "Code Coverage"; + + static final String DEFAULT_ID = "coverage"; + private static final ValidationUtilities VALIDATION_UTILITIES = new ValidationUtilities(); + /** The coverage report symbol from the Ionicons plugin. */ + private static final String ICON = "symbol-footsteps-outline plugin-ionicons-api"; + + private List tools = new ArrayList<>(); + private List qualityGates = new ArrayList<>(); + private String id = StringUtils.EMPTY; + private String name = StringUtils.EMPTY; + private boolean skipPublishingChecks = false; + private String checksName = StringUtils.EMPTY; + private ChecksAnnotationScope checksAnnotationScope = ChecksAnnotationScope.MODIFIED_LINES; + private boolean failOnError = false; + private boolean enabledForFailure = false; + private boolean skipSymbolicLinks = false; + private String scm = StringUtils.EMPTY; + private String sourceCodeEncoding = StringUtils.EMPTY; + private Set sourceDirectories = new HashSet<>(); + private SourceCodeRetention sourceCodeRetention = SourceCodeRetention.LAST_BUILD; + + /** + * Creates a new instance of {@link CoverageRecorder}. + */ + @DataBoundConstructor + public CoverageRecorder() { + super(); + + // empty constructor required for Stapler + } + + /** + * Sets the coverage tools that will scan files and create coverage reports. + * + * @param tools + * the coverage tools + */ + @DataBoundSetter + public void setTools(final List tools) { + this.tools = List.copyOf(tools); + } + + public List getTools() { + return tools; + } + + /** + * Defines the optional list of quality gates. + * + * @param qualityGates + * the quality gates + */ + @SuppressWarnings("unused") // used by Stapler view data binding + @DataBoundSetter + public void setQualityGates(final List qualityGates) { + this.qualityGates = List.copyOf(qualityGates); + } + + @SuppressWarnings("unused") // used by Stapler view data binding + public List getQualityGates() { + return qualityGates; + } + + /** + * Overrides the default ID of the results. The ID is used as URL of the results and as identifier in UI elements. + * If no ID is given, then the default ID "coverage". + * + * @param id + * the ID of the results + * + * @see ToolDescriptor#getId() + */ + @DataBoundSetter + public void setId(final String id) { + VALIDATION_UTILITIES.ensureValidId(id); + + this.id = id; + } + + public String getId() { + return id; + } + + /** + * Returns the actual ID of the results. If no user defined ID is given, then the default ID {@link #DEFAULT_ID} is + * returned. + * + * @return the ID + * @see #setId(String) + */ + public String getActualId() { + return StringUtils.defaultIfBlank(id, DEFAULT_ID); + } + + /** + * Overrides the name of the results. The name is used for all labels in the UI. If no name is given, then the + * default name is used. + * + * @param name + * the name of the results + * + * @see #getName() + */ + @DataBoundSetter + public void setName(final String name) { + this.name = name; + } + + public String getName() { + return StringUtils.defaultString(name); + } + + /** + * Sets whether publishing checks should be skipped or not. + * + * @param skipPublishingChecks + * {@code true} if publishing checks should be skipped, {@code false} otherwise + */ + @DataBoundSetter + public void setSkipPublishingChecks(final boolean skipPublishingChecks) { + this.skipPublishingChecks = skipPublishingChecks; + } + + public boolean isSkipPublishingChecks() { + return skipPublishingChecks; + } + + /** + * Changes the default name for the SCM checks report. + * + * @param checksName + * the name that should be used for the SCM checks report + */ + @DataBoundSetter + public void setChecksName(final String checksName) { + this.checksName = checksName; + } + + public String getChecksName() { + return StringUtils.defaultIfBlank(checksName, CHECKS_DEFAULT_NAME); + } + + /** + * Sets the scope of the annotations that should be published to SCM checks. + * + * @param checksAnnotationScope + * the scope to use + */ + @DataBoundSetter + public void setChecksAnnotationScope(final ChecksAnnotationScope checksAnnotationScope) { + this.checksAnnotationScope = checksAnnotationScope; + } + + public ChecksAnnotationScope getChecksAnnotationScope() { + return checksAnnotationScope; + } + + /** + * Specify if traversal of symbolic links will be skipped during directory scanning for coverage reports. + * + * @param skipSymbolicLinks + * if symbolic links should be skipped during directory scanning + */ + @DataBoundSetter + public void setSkipSymbolicLinks(final boolean skipSymbolicLinks) { + this.skipSymbolicLinks = skipSymbolicLinks; + } + + @SuppressWarnings({"unused", "PMD.BooleanGetMethodName"}) // called by Stapler + public boolean isSkipSymbolicLinks() { + return skipSymbolicLinks; + } + + /** + * Determines whether to fail the build on errors during the step of recording coverage reports. + * + * @param failOnError + * if {@code true} then the build will be failed on errors, {@code false} then errors are only reported in + * the UI + */ + @DataBoundSetter + @SuppressWarnings("unused") // Used by Stapler + public void setFailOnError(final boolean failOnError) { + this.failOnError = failOnError; + } + + public boolean isFailOnError() { + return failOnError; + } + + /** + * Returns whether recording should be enabled for failed builds as well. + * + * @param enabledForFailure + * {@code true} if recording should be enabled for failed builds as well, {@code false} if recording is + * enabled for successful or unstable builds only + */ + @DataBoundSetter + public void setEnabledForFailure(final boolean enabledForFailure) { + this.enabledForFailure = enabledForFailure; + } + + public boolean isEnabledForFailure() { + return enabledForFailure; + } + + /** + * Sets the encoding to use to read source files. + * + * @param sourceCodeEncoding + * the encoding, e.g. "ISO-8859-1" + */ + @DataBoundSetter + public void setSourceCodeEncoding(final String sourceCodeEncoding) { + this.sourceCodeEncoding = sourceCodeEncoding; + } + + public String getSourceCodeEncoding() { + return sourceCodeEncoding; + } + + /** + * Sets the paths to the directories that contain the source code. If not relative and thus not part of the + * workspace then these directories need to be added in Jenkins global configuration to prevent accessing of + * forbidden resources. + * + * @param sourceCodeDirectories + * directories containing the source code + */ + @DataBoundSetter + public void setSourceDirectories(final List sourceCodeDirectories) { + sourceDirectories = Set.copyOf(sourceCodeDirectories); + } + + public Set getSourceDirectories() { + return sourceDirectories; + } + + private Set getSourceDirectoriesPaths() { + Set paths = sourceDirectories.stream() + .map(SourceCodeDirectory::getPath) + .collect(Collectors.toSet()); + paths.add("src/main/java"); + return paths; + } + + /** + * Defines the retention strategy for source code files. + * + * @param sourceCodeRetention + * the retention strategy for source code files + */ + @DataBoundSetter + public void setSourceCodeRetention(final SourceCodeRetention sourceCodeRetention) { + this.sourceCodeRetention = sourceCodeRetention; + } + + public SourceCodeRetention getSourceCodeRetention() { + return sourceCodeRetention; + } + + /** + * Sets the SCM that should be used to find the reference build for. The reference recorder will select the SCM + * based on a substring comparison, there is no need to specify the full name. + * + * @param scm + * the ID of the SCM to use (a substring of the full ID) + */ + @DataBoundSetter + public void setScm(final String scm) { + this.scm = scm; + } + + public String getScm() { + return scm; + } + + @Override + public BuildStepMonitor getRequiredMonitorService() { + return BuildStepMonitor.NONE; + } + + @Override + public boolean perform(final AbstractBuild build, final Launcher launcher, final BuildListener listener) + throws InterruptedException, IOException { + FilePath workspace = build.getWorkspace(); + if (workspace == null) { + throw new IOException("No workspace found for " + build); + } + + perform(build, workspace, listener, new RunResultHandler(build)); + + return true; + } + + void perform(final Run run, final FilePath workspace, final TaskListener taskListener, + final StageResultHandler resultHandler) throws InterruptedException { + Result overallResult = run.getResult(); + LogHandler logHandler = new LogHandler(taskListener, "Coverage"); + if (enabledForFailure || overallResult == null || overallResult.isBetterOrEqualTo(Result.UNSTABLE)) { + FilteredLog log = new FilteredLog("Errors while recording code coverage:"); + log.logInfo("Recording coverage results"); + + var validation = VALIDATION_UTILITIES.validateId(getId()); + if (validation.kind != Kind.OK) { + failStage(resultHandler, logHandler, log, validation.getLocalizedMessage()); + } + if (tools.isEmpty()) { + failStage(resultHandler, logHandler, log, + "No tools defined that will record the coverage files"); + } + else { + perform(run, workspace, taskListener, resultHandler, log); + } + + } + else { + logHandler.log("Skipping execution of coverage recorder since overall result is '%s'", overallResult); + } + } + + private void perform(final Run run, final FilePath workspace, final TaskListener taskListener, + final StageResultHandler resultHandler, final FilteredLog log) throws InterruptedException { + List results = recordCoverageResults(run, workspace, taskListener, resultHandler, log); + + if (!results.isEmpty()) { + CoverageReporter reporter = new CoverageReporter(); + var action = reporter.publishAction(getActualId(), getName(), getIcon(), + Node.merge(results), run, workspace, taskListener, + getQualityGates(), + getScm(), getSourceDirectoriesPaths(), + getSourceCodeEncoding(), getSourceCodeRetention(), resultHandler); + if (!skipPublishingChecks) { + var checksPublisher = new CoverageChecksPublisher(action, getChecksName(), getChecksAnnotationScope()); + checksPublisher.publishCoverageReport(taskListener); + } + } + } + + private String getIcon() { + var icons = tools.stream().map(CoverageTool::getParser).map(Parser::getIcon).collect(Collectors.toSet()); + if (icons.size() == 1) { + return icons.iterator().next(); // unique icon + } + return ICON; + } + + private static void failStage(final StageResultHandler resultHandler, final LogHandler logHandler, + final FilteredLog log, final String message) { + log.logError(message); + resultHandler.setResult(Result.FAILURE, message); + logHandler.log(log); + } + + private List recordCoverageResults(final Run run, final FilePath workspace, final TaskListener taskListener, + final StageResultHandler resultHandler, final FilteredLog log) throws InterruptedException { + List results = new ArrayList<>(); + for (CoverageTool tool : tools) { + LogHandler toolHandler = new LogHandler(taskListener, tool.getDisplayName()); + Parser parser = tool.getParser(); + if (StringUtils.isBlank(tool.getPattern())) { + toolHandler.log("Using default pattern '%s' since user defined pattern is not set", + parser.getDefaultPattern()); + } + + String expandedPattern = expandPattern(run, tool.getActualPattern()); + if (!expandedPattern.equals(tool.getActualPattern())) { + log.logInfo("Expanding pattern '%s' to '%s'", tool.getActualPattern(), expandedPattern); + } + + try { + FileVisitorResult result = workspace.act( + new CoverageReportScanner(expandedPattern, "UTF-8", isSkipSymbolicLinks(), parser)); + log.merge(result.getLog()); + + var coverageResults = result.getResults(); + if (result.hasErrors()) { + if (isFailOnError()) { + var errorMessage = "Failing build due to some errors during recording of the coverage"; + log.logInfo(errorMessage); + resultHandler.setResult(Result.FAILURE, errorMessage); + } + else { + log.logInfo("Ignore errors and continue processing"); + } + } + results.addAll(coverageResults); + } + catch (IOException exception) { + log.logException(exception, "Exception while parsing with tool " + tool); + } + + toolHandler.log(log); + } + return results; + } + + private String expandPattern(final Run run, final String actualPattern) { + try { + EnvironmentResolver environmentResolver = new EnvironmentResolver(); + + return environmentResolver.expandEnvironmentVariables( + run.getEnvironment(TaskListener.NULL), actualPattern); + } + catch (IOException | InterruptedException ignore) { + return actualPattern; // fallback, no expansion + } + } + + @Override + public Descriptor getDescriptor() { + return (Descriptor) super.getDescriptor(); + } + + /** + * Descriptor for this step: defines the context and the UI elements. + */ + @Extension + @Symbol("recordCoverage") + public static class Descriptor extends BuildStepDescriptor { + private static final JenkinsFacade JENKINS = new JenkinsFacade(); + + @NonNull + @Override + public String getDisplayName() { + return Messages.Recorder_Name(); + } + + @Override + public boolean isApplicable(final Class jobType) { + return true; + } + + /** + * Returns a model with all {@link SourceCodeRetention} strategies. + * + * @param project + * the project that is configured + * + * @return a model with all {@link SourceCodeRetention} strategies. + */ + @POST + @SuppressWarnings("unused") // used by Stapler view data binding + public ListBoxModel doFillSourceCodeRetentionItems(@AncestorInPath final AbstractProject project) { + if (JENKINS.hasPermission(Item.CONFIGURE, project)) { + return SourceCodeRetention.fillItems(); + } + return new ListBoxModel(); + } + + /** + * Returns a model with all {@link ChecksAnnotationScope} scopes. + * + * @param project + * the project that is configured + * + * @return a model with all {@link ChecksAnnotationScope} scopes. + */ + @POST + @SuppressWarnings("unused") // used by Stapler view data binding + public ListBoxModel doFillChecksAnnotationScopeItems(@AncestorInPath final AbstractProject project) { + if (JENKINS.hasPermission(Item.CONFIGURE, project)) { + return ChecksAnnotationScope.fillItems(); + } + return new ListBoxModel(); + } + + /** + * Returns a model with all available charsets. + * + * @param project + * the project that is configured + * + * @return a model with all available charsets + */ + @POST + @SuppressWarnings("unused") // used by Stapler view data binding + public ComboBoxModel doFillSourceCodeEncodingItems(@AncestorInPath final AbstractProject project) { + if (JENKINS.hasPermission(Item.CONFIGURE, project)) { + return VALIDATION_UTILITIES.getAllCharsets(); + } + return new ComboBoxModel(); + } + + /** + * Performs on-the-fly validation on the character encoding. + * + * @param project + * the project that is configured + * @param sourceCodeEncoding + * the character encoding + * + * @return the validation result + */ + @POST + @SuppressWarnings("unused") // used by Stapler view data binding + public FormValidation doCheckSourceCodeEncoding(@AncestorInPath final AbstractProject project, + @QueryParameter final String sourceCodeEncoding) { + if (!JENKINS.hasPermission(Item.CONFIGURE, project)) { + return FormValidation.ok(); + } + + return VALIDATION_UTILITIES.validateCharset(sourceCodeEncoding); + } + + /** + * Performs on-the-fly validation of the ID. + * + * @param project + * the project that is configured + * @param id + * the ID of the tool + * + * @return the validation result + */ + @POST + public FormValidation doCheckId(@AncestorInPath final AbstractProject project, + @QueryParameter final String id) { + if (!JENKINS.hasPermission(Item.CONFIGURE, project)) { + return FormValidation.ok(); + } + + return VALIDATION_UTILITIES.validateId(id); + } + } + + /** + * Defines the scope of SCM checks annotations. + */ + enum ChecksAnnotationScope { + /** No annotations are created. */ + SKIP, + /** Only changed lines are annotated. */ + MODIFIED_LINES, + /** All lines are annotated. */ + ALL_LINES; + + static ListBoxModel fillItems() { + ListBoxModel items = new ListBoxModel(); + items.add(Messages.ChecksAnnotationScope_Skip(), ChecksAnnotationScope.SKIP.name()); + items.add(Messages.ChecksAnnotationScope_ModifiedLines(), ChecksAnnotationScope.MODIFIED_LINES.name()); + items.add(Messages.ChecksAnnotationScope_AllLines(), ChecksAnnotationScope.ALL_LINES.name()); + return items; + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageReportScanner.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageReportScanner.java new file mode 100644 index 000000000..a48e8ddc7 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageReportScanner.java @@ -0,0 +1,64 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +import edu.hm.hafner.coverage.CoverageParser; +import edu.hm.hafner.coverage.ModuleNode; +import edu.hm.hafner.util.FilteredLog; +import edu.hm.hafner.util.PathUtil; +import edu.hm.hafner.util.SecureXmlParserFactory.ParsingException; + +import io.jenkins.plugins.coverage.metrics.steps.CoverageTool.Parser; +import io.jenkins.plugins.util.AgentFileVisitor; + +/** + * Scans the workspace for coverage reports that match a specified Ant file pattern and parse these files with the + * specified parser. Creates a new {@link ModuleNode} for each parsed file. For files that cannot be read, an empty + * module node will be returned. + * + * @author Ullrich Hafner + */ +public class CoverageReportScanner extends AgentFileVisitor { + private static final long serialVersionUID = 6940864958150044554L; + + private static final PathUtil PATH_UTIL = new PathUtil(); + private final Parser parser; + + /** + * Creates a new instance of {@link CoverageReportScanner}. + * + * @param filePattern + * ant file-set pattern to scan for files to parse + * @param encoding + * encoding of the files to parse + * @param followSymbolicLinks + * if the scanner should traverse symbolic links + * @param parser + * the parser to use + */ + public CoverageReportScanner(final String filePattern, final String encoding, + final boolean followSymbolicLinks, final Parser parser) { + super(filePattern, encoding, followSymbolicLinks, true); + + this.parser = parser; + } + + @Override + protected Optional processFile(final Path file, final Charset charset, final FilteredLog log) { + try { + CoverageParser xmlParser = parser.createParser(); + ModuleNode node = xmlParser.parse(Files.newBufferedReader(file, charset), log); + log.logInfo("Successfully parsed file '%s'", PATH_UTIL.getAbsolutePath(file)); + node.aggregateValues().forEach(v -> log.logInfo("%s", v)); + return Optional.of(node); + } + catch (IOException | ParsingException exception) { + log.logException(exception, "Parsing of file '%s' failed due to an exception:", file); + return Optional.empty(); + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageReporter.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageReporter.java new file mode 100644 index 000000000..bc3ad321a --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageReporter.java @@ -0,0 +1,241 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.ArrayList; +import java.util.List; +import java.util.NavigableMap; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; + +import org.apache.commons.lang3.math.Fraction; + +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.coverage.Value; +import edu.hm.hafner.util.FilteredLog; +import edu.umd.cs.findbugs.annotations.CheckForNull; + +import hudson.FilePath; +import hudson.model.Run; +import hudson.model.TaskListener; + +import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics; +import io.jenkins.plugins.coverage.metrics.source.SourceCodePainter; +import io.jenkins.plugins.forensics.delta.Delta; +import io.jenkins.plugins.forensics.delta.FileChanges; +import io.jenkins.plugins.forensics.reference.ReferenceFinder; +import io.jenkins.plugins.prism.SourceCodeRetention; +import io.jenkins.plugins.util.LogHandler; +import io.jenkins.plugins.util.QualityGateResult; +import io.jenkins.plugins.util.StageResultHandler; + +/** + * Transforms the old model to the new model and invokes all steps that work on the new model. Currently, only the + * source code painting and copying has been moved to this new reporter class. + * + * @author Ullrich Hafner + */ +public class CoverageReporter { + @SuppressWarnings("checkstyle:ParameterNumber") + CoverageBuildAction publishAction(final String id, final String optionalName, final String icon, final Node rootNode, + final Run build, + final FilePath workspace, final TaskListener listener, final List qualityGates, + final String scm, final Set sourceDirectories, final String sourceCodeEncoding, + final SourceCodeRetention sourceCodeRetention, final StageResultHandler resultHandler) + throws InterruptedException { + FilteredLog log = new FilteredLog("Errors while reporting code coverage results:"); + + Optional possibleReferenceResult = getReferenceBuildAction(build, log); + + List filesToStore; + CoverageBuildAction action; + if (possibleReferenceResult.isPresent()) { + CoverageBuildAction referenceAction = possibleReferenceResult.get(); + Node referenceRoot = referenceAction.getResult(); + + log.logInfo("Calculating the code delta..."); + CodeDeltaCalculator codeDeltaCalculator = new CodeDeltaCalculator(build, workspace, listener, scm); + Optional delta = codeDeltaCalculator.calculateCodeDeltaToReference(referenceAction.getOwner(), log); + delta.ifPresent(value -> createDeltaReports(rootNode, log, referenceRoot, codeDeltaCalculator, value)); + + log.logInfo("Calculating coverage deltas..."); + + Node modifiedLinesCoverageRoot = rootNode.filterByModifiedLines(); + + NavigableMap modifiedLinesCoverageDelta; + List aggregatedModifiedFilesCoverage; + NavigableMap modifiedFilesCoverageDelta; + if (hasModifiedLinesCoverage(modifiedLinesCoverageRoot)) { + Node modifiedFilesCoverageRoot = rootNode.filterByModifiedFiles(); + aggregatedModifiedFilesCoverage = modifiedFilesCoverageRoot.aggregateValues(); + modifiedFilesCoverageDelta = modifiedFilesCoverageRoot.computeDelta(rootNode); + modifiedLinesCoverageDelta = modifiedLinesCoverageRoot.computeDelta(modifiedFilesCoverageRoot); + } + else { + modifiedLinesCoverageDelta = new TreeMap<>(); + aggregatedModifiedFilesCoverage = new ArrayList<>(); + modifiedFilesCoverageDelta = new TreeMap<>(); + if (rootNode.hasModifiedLines()) { + log.logInfo("No detected code changes affect the code coverage"); + } + } + + NavigableMap coverageDelta = rootNode.computeDelta(referenceRoot); + + QualityGateResult qualityGateResult = evaluateQualityGates(rootNode, log, + modifiedLinesCoverageRoot.aggregateValues(), modifiedLinesCoverageDelta, coverageDelta, + resultHandler, qualityGates); + + action = new CoverageBuildAction(build, id, optionalName, icon, rootNode, qualityGateResult, log, + referenceAction.getOwner().getExternalizableId(), coverageDelta, + modifiedLinesCoverageRoot.aggregateValues(), modifiedLinesCoverageDelta, + aggregatedModifiedFilesCoverage, modifiedFilesCoverageDelta, + rootNode.filterByIndirectChanges().aggregateValues()); + + if (sourceCodeRetention == SourceCodeRetention.MODIFIED) { + filesToStore = modifiedLinesCoverageRoot.getAllFileNodes(); + log.logInfo("-> Selecting %d modified files for source code painting", filesToStore.size()); + } + else { + filesToStore = rootNode.getAllFileNodes(); + } + } + else { + QualityGateResult qualityGateStatus = evaluateQualityGates(rootNode, log, + List.of(), new TreeMap<>(), new TreeMap<>(), resultHandler, qualityGates); + + action = new CoverageBuildAction(build, id, optionalName, icon, rootNode, qualityGateStatus, log); + filesToStore = rootNode.getAllFileNodes(); + } + + log.logInfo("Executing source code painting..."); + SourceCodePainter sourceCodePainter = new SourceCodePainter(build, workspace, id); + sourceCodePainter.processSourceCodePainting(filesToStore, sourceDirectories, + sourceCodeEncoding, sourceCodeRetention, log); + + log.logInfo("Finished coverage processing - adding the action to the build..."); + + LogHandler logHandler = new LogHandler(listener, "Coverage"); + logHandler.log(log); + + build.addAction(action); + return action; + } + + private void createDeltaReports(final Node rootNode, final FilteredLog log, final Node referenceRoot, + final CodeDeltaCalculator codeDeltaCalculator, final Delta delta) { + FileChangesProcessor fileChangesProcessor = new FileChangesProcessor(); + + try { + log.logInfo("Preprocessing code changes..."); + Set changes = codeDeltaCalculator.getCoverageRelevantChanges(delta); + var mappedChanges = codeDeltaCalculator.mapScmChangesToReportPaths(changes, rootNode, log); + var oldPathMapping = codeDeltaCalculator.createOldPathMapping(rootNode, referenceRoot, mappedChanges, log); + + log.logInfo("Obtaining code changes for files..."); + fileChangesProcessor.attachChangedCodeLines(rootNode, mappedChanges); + + log.logInfo("Obtaining indirect coverage changes..."); + fileChangesProcessor.attachIndirectCoveragesChanges(rootNode, referenceRoot, + mappedChanges, oldPathMapping); + + log.logInfo("Obtaining coverage delta for files..."); + fileChangesProcessor.attachFileCoverageDeltas(rootNode, referenceRoot, oldPathMapping); + } + catch (IllegalStateException exception) { + log.logError("An error occurred while processing code and coverage changes:"); + log.logError("-> Message: " + exception.getMessage()); + log.logError("-> Skipping calculating modified lines coverage, modified files coverage" + + " and indirect coverage changes"); + } + } + + private QualityGateResult evaluateQualityGates(final Node rootNode, final FilteredLog log, + final List modifiedLinesCoverageDistribution, + final NavigableMap modifiedLinesCoverageDelta, + final NavigableMap coverageDelta, final StageResultHandler resultHandler, + final List qualityGates) { + var statistics = new CoverageStatistics(rootNode.aggregateValues(), coverageDelta, + modifiedLinesCoverageDistribution, modifiedLinesCoverageDelta, List.of(), new TreeMap<>()); + CoverageQualityGateEvaluator evaluator = new CoverageQualityGateEvaluator(qualityGates, statistics); + var qualityGateStatus = evaluator.evaluate(); + if (qualityGateStatus.isInactive()) { + log.logInfo("No quality gates have been set - skipping"); + } + else { + log.logInfo("Evaluating quality gates"); + if (qualityGateStatus.isSuccessful()) { + log.logInfo("-> All quality gates have been passed"); + } + else { + var message = String.format("-> Some quality gates have been missed: overall result is %s", + qualityGateStatus.getOverallStatus().getResult()); + log.logInfo(message); + resultHandler.setResult(qualityGateStatus.getOverallStatus().getResult(), message); + } + log.logInfo("-> Details for each quality gate:"); + qualityGateStatus.getMessages().forEach(log::logInfo); + } + return qualityGateStatus; + } + + private boolean hasModifiedLinesCoverage(final Node modifiedLinesCoverageRoot) { + Optional lineCoverage = modifiedLinesCoverageRoot.getValue(Metric.LINE); + if (lineCoverage.isPresent()) { + if (((edu.hm.hafner.coverage.Coverage) lineCoverage.get()).isSet()) { + return true; + } + } + Optional branchCoverage = modifiedLinesCoverageRoot.getValue(Metric.BRANCH); + return branchCoverage.filter(value -> ((edu.hm.hafner.coverage.Coverage) value).isSet()).isPresent(); + } + + private Optional getReferenceBuildAction(final Run build, final FilteredLog log) { + log.logInfo("Obtaining action of reference build"); + + ReferenceFinder referenceFinder = new ReferenceFinder(); + Optional> reference = referenceFinder.findReference(build, log); + + Optional previousResult; + if (reference.isPresent()) { + Run referenceBuild = reference.get(); + log.logInfo("-> Using reference build '%s'", referenceBuild); + previousResult = getPreviousResult(reference.get()); + if (previousResult.isPresent()) { + Run fallbackBuild = previousResult.get().getOwner(); + if (!fallbackBuild.equals(referenceBuild)) { + log.logInfo("-> Reference build has no action, falling back to last build with action: '%s'", + fallbackBuild.getDisplayName()); + } + } + } + else { + previousResult = getPreviousResult(build.getPreviousBuild()); + previousResult.ifPresent(coverageBuildAction -> + log.logInfo("-> No reference build defined, falling back to previous build: '%s'", + coverageBuildAction.getOwner().getDisplayName())); + } + + if (previousResult.isEmpty()) { + log.logInfo("-> Found no reference result in reference build"); + + return Optional.empty(); + } + + CoverageBuildAction referenceAction = previousResult.get(); + log.logInfo("-> Found reference result in build '%s'", referenceAction.getOwner().getDisplayName()); + + return Optional.of(referenceAction); + } + + private Optional getPreviousResult(@CheckForNull final Run startSearch) { + for (Run build = startSearch; build != null; build = build.getPreviousBuild()) { + CoverageBuildAction action = build.getAction(CoverageBuildAction.class); + if (action != null) { + return Optional.of(action); + } + } + return Optional.empty(); + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageStep.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageStep.java new file mode 100644 index 000000000..a215ebf57 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageStep.java @@ -0,0 +1,478 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.verb.POST; +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.steps.Step; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.StepDescriptor; +import org.jenkinsci.plugins.workflow.steps.StepExecution; +import hudson.Extension; +import hudson.FilePath; +import hudson.model.AbstractProject; +import hudson.model.Item; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.tools.ToolDescriptor; +import hudson.util.ComboBoxModel; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; + +import io.jenkins.plugins.prism.SourceCodeDirectory; +import io.jenkins.plugins.prism.SourceCodeRetention; +import io.jenkins.plugins.util.AbstractExecution; +import io.jenkins.plugins.util.JenkinsFacade; +import io.jenkins.plugins.util.ValidationUtilities; + +import static io.jenkins.plugins.coverage.metrics.steps.CoverageRecorder.*; + +/** + * A pipeline {@code Step} that reads and parses coverage results in a build and adds the results to the persisted build + * results. This step only provides the entry point for pipelines, the actual computation is delegated to an associated + * Freestyle {@link CoverageRecorder} instance. + * + * @author Ullrich Hafner + */ +public class CoverageStep extends Step implements Serializable { + private static final long serialVersionUID = 34386077204781270L; + private static final ValidationUtilities VALIDATION_UTILITIES = new ValidationUtilities(); + + private List tools = new ArrayList<>(); + private List qualityGates = new ArrayList<>(); + private String id = StringUtils.EMPTY; + private String name = StringUtils.EMPTY; + private boolean skipPublishingChecks = false; + private String checksName = StringUtils.EMPTY; + private ChecksAnnotationScope checksAnnotationScope = ChecksAnnotationScope.MODIFIED_LINES; + private boolean failOnError = false; + private boolean enabledForFailure = false; + private boolean skipSymbolicLinks = false; + private String scm = StringUtils.EMPTY; + private String sourceCodeEncoding = StringUtils.EMPTY; + private Set sourceDirectories = new HashSet<>(); + private SourceCodeRetention sourceCodeRetention = SourceCodeRetention.LAST_BUILD; + + /** + * Creates a new instance of {@link CoverageStep}. + */ + @DataBoundConstructor + public CoverageStep() { + super(); + + // empty constructor required for Stapler + } + + @Override + public StepExecution start(final StepContext context) throws Exception { + return new Execution(context, this); + } + + /** + * Sets the coverage tools that will scan files and create coverage reports. + * + * @param tools + * the coverage tools + */ + @DataBoundSetter + public void setTools(final List tools) { + this.tools = List.copyOf(tools); + } + + public List getTools() { + return tools; + } + + /** + * Defines the optional list of quality gates. + * + * @param qualityGates + * the quality gates + */ + @SuppressWarnings("unused") // used by Stapler view data binding + @DataBoundSetter + public void setQualityGates(final List qualityGates) { + this.qualityGates = List.copyOf(qualityGates); + } + + public List getQualityGates() { + return qualityGates; + } + + /** + * Overrides the default ID of the results. The ID is used as URL of the results and as identifier in UI elements. + * If no ID is given, then the default ID "coverage". + * + * @param id + * the ID of the results + * + * @see ToolDescriptor#getId() + */ + @DataBoundSetter + public void setId(final String id) { + VALIDATION_UTILITIES.ensureValidId(id); + + this.id = id; + } + + public String getId() { + return id; + } + + /** + * Overrides the name of the results. The name is used for all labels in the UI. If no name is given, then the + * default name is used. + * + * @param name + * the name of the results + * + * @see #getName() + */ + @DataBoundSetter + public void setName(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + + /** + * Sets whether publishing checks should be skipped or not. + * + * @param skipPublishingChecks + * {@code true} if publishing checks should be skipped, {@code false} otherwise + */ + @DataBoundSetter + public void setSkipPublishingChecks(final boolean skipPublishingChecks) { + this.skipPublishingChecks = skipPublishingChecks; + } + + public boolean isSkipPublishingChecks() { + return skipPublishingChecks; + } + + /** + * Changes the default name for the SCM checks report. + * + * @param checksName + * the name that should be used for the SCM checks report + */ + @DataBoundSetter + public void setChecksName(final String checksName) { + this.checksName = checksName; + } + + public String getChecksName() { + return checksName; + } + + /** + * Sets the scope of the annotations that should be published to SCM checks. + * + * @param checksAnnotationScope + * the scope to use + */ + @DataBoundSetter + public void setChecksAnnotationScope(final ChecksAnnotationScope checksAnnotationScope) { + this.checksAnnotationScope = checksAnnotationScope; + } + + public ChecksAnnotationScope getChecksAnnotationScope() { + return checksAnnotationScope; + } + + /** + * Specify if traversal of symbolic links will be skipped during directory scanning for coverage reports. + * + * @param skipSymbolicLinks + * if symbolic links should be skipped during directory scanning + */ + @DataBoundSetter + public void setSkipSymbolicLinks(final boolean skipSymbolicLinks) { + this.skipSymbolicLinks = skipSymbolicLinks; + } + + public boolean isSkipSymbolicLinks() { + return skipSymbolicLinks; + } + + /** + * Determines whether to fail the build on errors during the step of recording coverage reports. + * + * @param failOnError + * if {@code true} then the build will be failed on errors, {@code false} then errors are only reported in + * the UI + */ + @DataBoundSetter + @SuppressWarnings("unused") // Used by Stapler + public void setFailOnError(final boolean failOnError) { + this.failOnError = failOnError; + } + + public boolean isFailOnError() { + return failOnError; + } + + /** + * Returns whether recording should be enabled for failed builds as well. + * + * @param enabledForFailure + * {@code true} if recording should be enabled for failed builds as well, {@code false} if recording is + * enabled for successful or unstable builds only + */ + @DataBoundSetter + public void setEnabledForFailure(final boolean enabledForFailure) { + this.enabledForFailure = enabledForFailure; + } + + public boolean isEnabledForFailure() { + return enabledForFailure; + } + + /** + * Sets the encoding to use to read source files. + * + * @param sourceCodeEncoding + * the encoding, e.g. "ISO-8859-1" + */ + @DataBoundSetter + public void setSourceCodeEncoding(final String sourceCodeEncoding) { + this.sourceCodeEncoding = sourceCodeEncoding; + } + + public String getSourceCodeEncoding() { + return sourceCodeEncoding; + } + + /** + * Sets the paths to the directories that contain the source code. If not relative and thus not part of the + * workspace then these directories need to be added in Jenkins global configuration to prevent accessing of + * forbidden resources. + * + * @param sourceCodeDirectories + * directories containing the source code + */ + @DataBoundSetter + public void setSourceDirectories(final List sourceCodeDirectories) { + sourceDirectories = Set.copyOf(sourceCodeDirectories); + } + + public Set getSourceDirectories() { + return sourceDirectories; + } + + /** + * Defines the retention strategy for source code files. + * + * @param sourceCodeRetention + * the retention strategy for source code files + */ + @DataBoundSetter + public void setSourceCodeRetention(final SourceCodeRetention sourceCodeRetention) { + this.sourceCodeRetention = sourceCodeRetention; + } + + public SourceCodeRetention getSourceCodeRetention() { + return sourceCodeRetention; + } + + /** + * Sets the SCM that should be used to find the reference build for. The reference recorder will select the SCM + * based on a substring comparison, there is no need to specify the full name. + * + * @param scm + * the ID of the SCM to use (a substring of the full ID) + */ + @DataBoundSetter + public void setScm(final String scm) { + this.scm = scm; + } + + public String getScm() { + return scm; + } + + /** + * Actually performs the execution of the associated step. + */ + @SuppressFBWarnings(value = "THROWS", justification = "false positive") + static class Execution extends AbstractExecution { + private static final long serialVersionUID = -2840020502160375407L; + private static final Void UNUSED = null; + + private final CoverageStep step; + + Execution(@NonNull final StepContext context, final CoverageStep step) { + super(context); + + this.step = step; + } + + @Override + @CheckForNull + protected Void run() throws IOException, InterruptedException { + var recorder = new CoverageRecorder(); + recorder.setTools(step.getTools()); + recorder.setQualityGates(step.getQualityGates()); + recorder.setId(step.getId()); + recorder.setName(step.getName()); + recorder.setSkipPublishingChecks(step.isSkipPublishingChecks()); + recorder.setChecksName(step.getChecksName()); + recorder.setChecksAnnotationScope(step.getChecksAnnotationScope()); + recorder.setFailOnError(step.isFailOnError()); + recorder.setEnabledForFailure(step.isEnabledForFailure()); + recorder.setScm(step.getScm()); + recorder.setSourceCodeEncoding(step.getSourceCodeEncoding()); + recorder.setSourceDirectories(List.copyOf(step.getSourceDirectories())); + recorder.setSourceCodeRetention(step.getSourceCodeRetention()); + + recorder.perform(getRun(), getWorkspace(), getTaskListener(), createStageResultHandler()); + + return UNUSED; + } + } + + /** + * Descriptor for this step: defines the context and the UI labels. + */ + @Extension + @SuppressWarnings("unused") // most methods are used by the corresponding jelly view + public static class Descriptor extends StepDescriptor { + private static final JenkinsFacade JENKINS = new JenkinsFacade(); + + @Override + public String getFunctionName() { + return "recordCoverage"; + } + + @NonNull + @Override + public String getDisplayName() { + return Messages.Recorder_Name(); + } + + @Override + public Set> getRequiredContext() { + return Set.of(FilePath.class, FlowNode.class, Run.class, TaskListener.class); + } + + @Override + public String argumentsToString(@NonNull final Map namedArgs) { + String formatted = super.argumentsToString(namedArgs); + if (formatted != null) { + return formatted; + } + return namedArgs.toString(); + } + + /** + * Returns a model with all {@link SourceCodeRetention} strategies. + * + * @param project + * the project that is configured + * + * @return a model with all {@link SourceCodeRetention} strategies. + */ + @POST + @SuppressWarnings("unused") // used by Stapler view data binding + public ListBoxModel doFillSourceCodeRetentionItems(@AncestorInPath final AbstractProject project) { + if (JENKINS.hasPermission(Item.CONFIGURE, project)) { + return SourceCodeRetention.fillItems(); + } + return new ListBoxModel(); + } + + /** + * Returns a model with all {@link ChecksAnnotationScope} scopes. + * + * @param project + * the project that is configured + * + * @return a model with all {@link ChecksAnnotationScope} scopes. + */ + @POST + @SuppressWarnings("unused") // used by Stapler view data binding + public ListBoxModel doFillChecksAnnotationScopeItems(@AncestorInPath final AbstractProject project) { + if (JENKINS.hasPermission(Item.CONFIGURE, project)) { + return ChecksAnnotationScope.fillItems(); + } + return new ListBoxModel(); + } + + + /** + * Returns a model with all available charsets. + * + * @param project + * the project that is configured + * + * @return a model with all available charsets + */ + @POST + @SuppressWarnings("unused") // used by Stapler view data binding + public ComboBoxModel doFillSourceCodeEncodingItems(@AncestorInPath final AbstractProject project) { + if (JENKINS.hasPermission(Item.CONFIGURE, project)) { + return VALIDATION_UTILITIES.getAllCharsets(); + } + return new ComboBoxModel(); + } + + /** + * Performs on-the-fly validation on the character encoding. + * + * @param project + * the project that is configured + * @param sourceCodeEncoding + * the character encoding + * + * @return the validation result + */ + @POST + @SuppressWarnings("unused") // used by Stapler view data binding + public FormValidation doCheckSourceCodeEncoding(@AncestorInPath final AbstractProject project, + @QueryParameter final String sourceCodeEncoding) { + if (!JENKINS.hasPermission(Item.CONFIGURE, project)) { + return FormValidation.ok(); + } + + return VALIDATION_UTILITIES.validateCharset(sourceCodeEncoding); + } + + /** + * Performs on-the-fly validation of the ID. + * + * @param project + * the project that is configured + * @param id + * the ID of the tool + * + * @return the validation result + */ + @POST + public FormValidation doCheckId(@AncestorInPath final AbstractProject project, + @QueryParameter final String id) { + if (!JENKINS.hasPermission(Item.CONFIGURE, project)) { + return FormValidation.ok(); + } + + return VALIDATION_UTILITIES.validateId(id); + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageTableModel.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageTableModel.java new file mode 100644 index 000000000..a5c355963 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageTableModel.java @@ -0,0 +1,383 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.math.Fraction; + +import edu.hm.hafner.coverage.Coverage; +import edu.hm.hafner.coverage.CyclomaticComplexity; +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.coverage.FractionValue; +import edu.hm.hafner.coverage.LinesOfCode; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Node; + +import hudson.Functions; + +import io.jenkins.plugins.coverage.metrics.color.ColorProvider; +import io.jenkins.plugins.coverage.metrics.color.ColorProvider.DisplayColors; +import io.jenkins.plugins.coverage.metrics.color.CoverageChangeTendency; +import io.jenkins.plugins.coverage.metrics.color.CoverageLevel; +import io.jenkins.plugins.coverage.metrics.model.ElementFormatter; +import io.jenkins.plugins.coverage.metrics.source.SourceCodeFacade; +import io.jenkins.plugins.datatables.DetailedCell; +import io.jenkins.plugins.datatables.TableColumn; +import io.jenkins.plugins.datatables.TableColumn.ColumnBuilder; +import io.jenkins.plugins.datatables.TableColumn.ColumnCss; +import io.jenkins.plugins.datatables.TableColumn.ColumnType; +import io.jenkins.plugins.datatables.TableConfiguration; +import io.jenkins.plugins.datatables.TableConfiguration.SelectStyle; +import io.jenkins.plugins.datatables.TableModel; + +import static j2html.TagCreator.*; + +/** + * UI table model for the coverage details table. + */ +class CoverageTableModel extends TableModel { + private static final int NO_COVERAGE_SORT = -1_000; + private static final SourceCodeFacade SOURCE_CODE_FACADE = new SourceCodeFacade(); + + /** + * The alpha value for colors to be used to highlight the coverage within the table view. + */ + private static final int TABLE_COVERAGE_COLOR_ALPHA = 80; + + static final DetailedCell NO_COVERAGE + = new DetailedCell<>(Messages.Coverage_Not_Available(), NO_COVERAGE_SORT); + + private final ColorProvider colorProvider; + private final Node root; + private final RowRenderer renderer; + private final String id; + + CoverageTableModel(final String id, final Node root, final RowRenderer renderer, final ColorProvider colors) { + super(); + + this.id = id; + this.root = root; + this.renderer = renderer; + colorProvider = colors; + } + + RowRenderer getRenderer() { + return renderer; + } + + @Override + public String getId() { + return id; + } + + @Override + public TableConfiguration getTableConfiguration() { + TableConfiguration tableConfiguration = new TableConfiguration(); + tableConfiguration.responsive(); + tableConfiguration.select(SelectStyle.SINGLE); + renderer.configureTable(tableConfiguration); + return tableConfiguration; + } + + @Override + public List getColumns() { + List columns = new ArrayList<>(); + + TableColumn fileHash = new ColumnBuilder().withHeaderLabel("Hash") + .withDataPropertyKey("fileHash") + .withHeaderClass(ColumnCss.HIDDEN) + .build(); + columns.add(fileHash); + TableColumn modified = new ColumnBuilder().withHeaderLabel("Modified") + .withDataPropertyKey("modified") + .withHeaderClass(ColumnCss.HIDDEN) + .build(); + columns.add(modified); + TableColumn fileName = new ColumnBuilder().withHeaderLabel(Messages.Column_File()) + .withDataPropertyKey("fileName") + .withDetailedCell() + .withResponsivePriority(1) + .build(); + columns.add(fileName); + TableColumn packageName = new ColumnBuilder().withHeaderLabel(Messages.Column_Package()) + .withDataPropertyKey("packageName") + .withResponsivePriority(50_000) + .build(); + columns.add(packageName); + + configureValueColumn("lineCoverage", Metric.LINE, Messages.Column_LineCoverage(), + Messages.Column_DeltaLineCoverage("Δ"), columns); + configureValueColumn("branchCoverage", Metric.BRANCH, Messages.Column_BranchCoverage(), + Messages.Column_DeltaBranchCoverage("Δ"), columns); + configureValueColumn("mutationCoverage", Metric.MUTATION, Messages.Column_MutationCoverage(), + Messages.Column_DeltaMutationCoverage("Δ"), columns); + TableColumn loc = new ColumnBuilder().withHeaderLabel(Messages.Column_LinesOfCode()) + .withDataPropertyKey("loc") + .withResponsivePriority(200) + .withType(ColumnType.NUMBER) + .build(); + columns.add(loc); + if (root.containsMetric(Metric.COMPLEXITY)) { + TableColumn complexity = new ColumnBuilder().withHeaderLabel(Messages.Column_Complexity()) + .withDataPropertyKey("complexity") + .withResponsivePriority(500) + .withType(ColumnType.NUMBER) + .build(); + columns.add(complexity); + } + if (root.containsMetric(Metric.COMPLEXITY_DENSITY)) { + TableColumn complexity = new ColumnBuilder().withHeaderLabel(Messages.Column_ComplexityDensity()) + .withDataPropertyKey("density") + .withDetailedCell() + .withResponsivePriority(700) + .withType(ColumnType.NUMBER) + .build(); + columns.add(complexity); + } + return columns; + } + + private void configureValueColumn(final String key, final Metric metric, final String headerLabel, + final String deltaHeaderLabel, final List columns) { + if (root.containsMetric(metric)) { + TableColumn lineCoverage = new ColumnBuilder().withHeaderLabel(headerLabel) + .withDataPropertyKey(key) + .withDetailedCell() + .withType(ColumnType.NUMBER) + .withResponsivePriority(1) + .build(); + columns.add(lineCoverage); + TableColumn lineCoverageDelta = new ColumnBuilder().withHeaderLabel(deltaHeaderLabel) + .withDataPropertyKey(key + "Delta") + .withDetailedCell() + .withType(ColumnType.NUMBER) + .withResponsivePriority(2) + .build(); + columns.add(lineCoverageDelta); + } + } + + @Override + public List getRows() { + Locale browserLocale = Functions.getCurrentLocale(); + return root.getAllFileNodes().stream() + .map(file -> new CoverageRow(file, browserLocale, renderer, colorProvider)) + .collect(Collectors.toList()); + } + + protected Node getRoot() { + return root; + } + + protected ColorProvider getColorProvider() { + return colorProvider; + } + + /** + * UI row model for the coverage details table. + */ + static class CoverageRow { + private static final String COVERAGE_COLUMN_OUTER = "coverage-cell-outer float-end"; + private static final String COVERAGE_COLUMN_INNER = "coverage-jenkins-cell-inner"; + private static final ElementFormatter FORMATTER = new ElementFormatter(); + private static final FractionValue ZERO_DENSITY = new FractionValue(Metric.COMPLEXITY_DENSITY, 0, 1); + private static final LinesOfCode ZERO_LOC = new LinesOfCode(0); + private static final CyclomaticComplexity ZERO_COMPLEXITY = new CyclomaticComplexity(0); + + private final FileNode file; + private final Locale browserLocale; + private final RowRenderer renderer; + private final ColorProvider colorProvider; + + CoverageRow(final FileNode file, final Locale browserLocale, final RowRenderer renderer, + final ColorProvider colors) { + this.file = file; + this.browserLocale = browserLocale; + this.renderer = renderer; + colorProvider = colors; + } + + public String getFileHash() { + return String.valueOf(file.getPath().hashCode()); + } + + public boolean getModified() { + return file.hasModifiedLines(); + } + + public DetailedCell getFileName() { + return new DetailedCell<>(renderer.renderFileName(file.getName(), file.getPath()), file.getName()); + } + + public String getPackageName() { + return file.getParentName(); + } + + public DetailedCell getLineCoverage() { + return createColoredCoverageColumn(getCoverageOfNode(Metric.LINE)); + } + + public DetailedCell getBranchCoverage() { + return createColoredCoverageColumn(getCoverageOfNode(Metric.BRANCH)); + } + + public DetailedCell getMutationCoverage() { + return createColoredCoverageColumn(getCoverageOfNode(Metric.MUTATION)); + } + + Coverage getCoverageOfNode(final Metric metric) { + return file.getTypedValue(metric, Coverage.nullObject(metric)); + } + + public DetailedCell getLineCoverageDelta() { + return createColoredFileCoverageDeltaColumn(Metric.LINE); + } + + public DetailedCell getBranchCoverageDelta() { + return createColoredFileCoverageDeltaColumn(Metric.BRANCH); + } + + public DetailedCell getMutationCoverageDelta() { + return createColoredFileCoverageDeltaColumn(Metric.MUTATION); + } + + public int getLoc() { + return file.getTypedValue(Metric.LOC, ZERO_LOC).getValue(); + } + + public int getComplexity() { + return file.getTypedValue(Metric.COMPLEXITY, ZERO_COMPLEXITY).getValue(); + } + + public DetailedCell getDensity() { + double complexityDensity = file.getTypedValue(Metric.COMPLEXITY_DENSITY, ZERO_DENSITY) + .getFraction() + .doubleValue(); + return new DetailedCell<>(String.format("%.2f", complexityDensity), complexityDensity); + } + + /** + * Creates a table cell which colorizes the shown coverage dependent on the coverage percentage. + * + * @param coverage + * the coverage of the element + * + * @return the new {@link DetailedCell} + */ + protected DetailedCell createColoredCoverageColumn(final Coverage coverage) { + if (coverage.isSet()) { + double percentage = coverage.getCoveredPercentage().toDouble(); + DisplayColors colors = CoverageLevel.getDisplayColorsOfCoverageLevel(percentage, colorProvider); + String cell = div() + .withClasses(COVERAGE_COLUMN_OUTER).with( + div().withClasses(COVERAGE_COLUMN_INNER) + .withStyle(String.format( + "background-image: linear-gradient(90deg, %s %f%%, transparent %f%%);", + colors.getFillColorAsRGBAHex(TABLE_COVERAGE_COLOR_ALPHA), + percentage, percentage)) + .attr("data-bs-toggle", "tooltip") + .attr("data-bs-placement", "top") + .withTitle(FORMATTER.formatAdditionalInformation(coverage)) + .withText(FORMATTER.formatPercentage(coverage, browserLocale))) + .render(); + return new DetailedCell<>(cell, percentage); + } + return NO_COVERAGE; + } + + /** + * Creates a table cell which colorizes the tendency of the shown coverage delta. + * + * @param metric + * the metric to use + * @param delta + * The coverage delta as percentage + * + * @return the created {@link DetailedCell} + */ + protected DetailedCell createColoredCoverageDeltaColumn(final Metric metric, final Fraction delta) { + double percentage = delta.doubleValue() * 100.0; + DisplayColors colors = CoverageChangeTendency.getDisplayColorsForTendency(percentage, colorProvider); + String cell = div().withClasses(COVERAGE_COLUMN_OUTER).with( + div().withClasses(COVERAGE_COLUMN_INNER) + .withStyle(String.format("background-color:%s;", colors.getFillColorAsRGBAHex( + TABLE_COVERAGE_COLOR_ALPHA))) + .withText(FORMATTER.formatDelta(delta, metric, browserLocale))) + .render(); + return new DetailedCell<>(cell, percentage); + } + + protected FileNode getFile() { + return file; + } + + /** + * Creates a colored column for visualizing the file coverage delta against a reference for the passed + * {@link Metric}. + * + * @param metric + * the coverage metric + * + * @return the created {@link DetailedCell} + */ + private DetailedCell createColoredFileCoverageDeltaColumn(final Metric metric) { + if (file.hasDelta(metric)) { + return createColoredCoverageDeltaColumn(metric, file.getDelta(metric)); + } + return NO_COVERAGE; + } + } + + /** + * Renders filenames with links. Selection will be handled by opening a new page using the provided link. + */ + static class LinkedRowRenderer implements RowRenderer { + private final File buildFolder; + private final String resultsId; + + LinkedRowRenderer(final File buildFolder, final String resultsId) { + this.buildFolder = buildFolder; + this.resultsId = resultsId; + } + + @Override + public void configureTable(final TableConfiguration tableConfiguration) { + // nothing required + } + + @Override + public String renderFileName(final String fileName, final String path) { + if (SOURCE_CODE_FACADE.canRead(buildFolder, resultsId, path)) { + return a().withHref(String.valueOf(path.hashCode())).withText(fileName).render(); + } + return fileName; + } + } + + /** + * Renders filenames without links. Selection will be handled using the table select events. + */ + static class InlineRowRenderer implements RowRenderer { + @Override + public void configureTable(final TableConfiguration tableConfiguration) { + tableConfiguration.select(SelectStyle.SINGLE); + } + + @Override + public String renderFileName(final String fileName, final String path) { + return fileName; + } + } + + /** + * Renders filenames in table cells. + */ + interface RowRenderer { + void configureTable(TableConfiguration tableConfiguration); + + String renderFileName(String fileName, String path); + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageTool.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageTool.java new file mode 100644 index 000000000..77f04b49d --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageTool.java @@ -0,0 +1,248 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.io.Serializable; + +import org.apache.commons.lang3.StringUtils; + +import edu.hm.hafner.coverage.CoverageParser; +import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.coverage.registry.ParserRegistry; +import edu.hm.hafner.util.VisibleForTesting; +import edu.umd.cs.findbugs.annotations.CheckForNull; + +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.verb.POST; +import org.jvnet.localizer.Localizable; +import hudson.Extension; +import hudson.model.AbstractDescribableImpl; +import hudson.model.AbstractProject; +import hudson.model.Descriptor; +import hudson.model.Item; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; + +import io.jenkins.plugins.prism.SourceCodeRetention; +import io.jenkins.plugins.util.JenkinsFacade; +import io.jenkins.plugins.util.ValidationUtilities; + +/** + * A coverage tool that can produce a {@link Node coverage tree} by parsing a given report file. + * + * @author Ullrich Hafner + */ +public class CoverageTool extends AbstractDescribableImpl implements Serializable { + private static final long serialVersionUID = -8612521458890553037L; + private static final ValidationUtilities VALIDATION_UTILITIES = new ValidationUtilities(); + + private JenkinsFacade jenkins = new JenkinsFacade(); + + private String pattern = StringUtils.EMPTY; + private Parser parser = Parser.JACOCO; + + /** + * Creates a new {@link io.jenkins.plugins.coverage.metrics.steps.CoverageTool}. + */ + @DataBoundConstructor + public CoverageTool() { + // empty for stapler + } + + CoverageTool(final Parser parser, final String pattern) { + this.pattern = pattern; + this.parser = parser; + } + + public Parser getParser() { + return parser; + } + + /** + * Sets the parser to be used to read the input files. + * + * @param parser the parser to use + */ + @DataBoundSetter + public void setParser(final Parser parser) { + this.parser = parser; + } + + @VisibleForTesting + void setJenkinsFacade(final JenkinsFacade jenkinsFacade) { + jenkins = jenkinsFacade; + } + + /** + * Called after de-serialization to retain backward compatibility. + * + * @return this + */ + protected Object readResolve() { + jenkins = new JenkinsFacade(); + + return this; + } + + /** + * Sets the Ant file-set pattern of files to work with. If the pattern is undefined then the console log is + * scanned. + * + * @param pattern + * the pattern to use + */ + @DataBoundSetter + public void setPattern(final String pattern) { + this.pattern = pattern; + } + + @CheckForNull + public String getPattern() { + return pattern; + } + + /** + * Returns the actual pattern to work with. If no user defined pattern is given, then the default pattern is + * returned. + * + * @return the name + * @see #setPattern(String) + */ + public String getActualPattern() { + return StringUtils.defaultIfBlank(pattern, parser.getDefaultPattern()); + } + + @Override + public String toString() { + return String.format("%s (pattern: %s)", getParser(), getActualPattern()); + } + + @Override + public CoverageToolDescriptor getDescriptor() { + return (CoverageToolDescriptor) jenkins.getDescriptorOrDie(getClass()); + } + + public String getDisplayName() { + return getParser().getDisplayName(); + } + + /** Descriptor for {@link io.jenkins.plugins.coverage.metrics.steps.CoverageTool}. **/ + @Extension + public static class CoverageToolDescriptor extends Descriptor { + private static final JenkinsFacade JENKINS = new JenkinsFacade(); + + /** + * Creates a new instance of {@link CoverageToolDescriptor}. + */ + public CoverageToolDescriptor() { + super(); + } + + /** + * Returns a model with all {@link SourceCodeRetention} strategies. + * + * @param project + * the project that is configured + * @return a model with all {@link SourceCodeRetention} strategies. + */ + @POST + public ListBoxModel doFillParserItems(@AncestorInPath final AbstractProject project) { + if (JENKINS.hasPermission(Item.CONFIGURE, project)) { + ListBoxModel options = new ListBoxModel(); + add(options, Parser.JACOCO); + add(options, Parser.COBERTURA); + add(options, Parser.PIT); + return options; + } + return new ListBoxModel(); + } + + private void add(final ListBoxModel options, final Parser parser) { + options.add(parser.getDisplayName(), parser.name()); + } + + /** + * Performs on-the-fly validation of the ID. + * + * @param project + * the project that is configured + * @param id + * the ID of the tool + * + * @return the validation result + */ + @POST + public FormValidation doCheckId(@AncestorInPath final AbstractProject project, + @QueryParameter final String id) { + if (!new JenkinsFacade().hasPermission(Item.CONFIGURE, project)) { + return FormValidation.ok(); + } + + return VALIDATION_UTILITIES.validateId(id); + } + + /** + * Returns an optional help text that can provide useful hints on how to configure the coverage tool so that the + * report files could be parsed by Jenkins. This help can be a plain text message or an HTML snippet. + * + * @return the help + */ + public String getHelp() { + return StringUtils.EMPTY; + } + + /** + * Returns an optional URL to the homepage of the coverage tool. + * + * @return the help + */ + public String getUrl() { + return StringUtils.EMPTY; + } + } + + /** + * Supported coverage parsers. + */ + public enum Parser { + COBERTURA(Messages._Parser_Cobertura(), "**/cobertura.xml", + "symbol-footsteps-outline plugin-ionicons-api"), + JACOCO(Messages._Parser_JaCoCo(), "**/jacoco.xml", + "symbol-footsteps-outline plugin-ionicons-api"), + PIT(Messages._Parser_PIT(), "**/mutations.xml", + "symbol-solid/virus-slash plugin-font-awesome-api"); + + private final Localizable displayName; + private final String defaultPattern; + private final String icon; + + Parser(final Localizable displayName, final String defaultPattern, + final String icon) { + this.displayName = displayName; + this.defaultPattern = defaultPattern; + this.icon = icon; + } + + public String getDisplayName() { + return displayName.toString(); + } + + public String getDefaultPattern() { + return defaultPattern; + } + + public String getIcon() { + return icon; + } + + /** + * Creates a new parser to read the report XML files into a Java object model of {@link Node} instances. + * + * @return the parser + */ + public CoverageParser createParser() { + return new ParserRegistry().getParser(name()); + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel.java new file mode 100644 index 000000000..f555147b2 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel.java @@ -0,0 +1,528 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NavigableSet; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import edu.hm.hafner.coverage.Coverage; +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.coverage.Percentage; +import edu.hm.hafner.echarts.LabeledTreeMapNode; +import edu.hm.hafner.util.FilteredLog; +import edu.umd.cs.findbugs.annotations.CheckForNull; + +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.bind.JavaScriptMethod; +import hudson.model.Api; +import hudson.model.ModelObject; +import hudson.model.Run; + +import io.jenkins.plugins.bootstrap5.MessagesViewModel; +import io.jenkins.plugins.coverage.metrics.charts.TreeMapNodeConverter; +import io.jenkins.plugins.coverage.metrics.color.ColorProvider; +import io.jenkins.plugins.coverage.metrics.color.ColorProviderFactory; +import io.jenkins.plugins.coverage.metrics.color.CoverageColorJenkinsId; +import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics; +import io.jenkins.plugins.coverage.metrics.model.ElementFormatter; +import io.jenkins.plugins.coverage.metrics.source.SourceCodeFacade; +import io.jenkins.plugins.coverage.metrics.source.SourceViewModel; +import io.jenkins.plugins.coverage.metrics.steps.CoverageTableModel.InlineRowRenderer; +import io.jenkins.plugins.coverage.metrics.steps.CoverageTableModel.LinkedRowRenderer; +import io.jenkins.plugins.coverage.metrics.steps.CoverageTableModel.RowRenderer; +import io.jenkins.plugins.datatables.DefaultAsyncTableContentProvider; +import io.jenkins.plugins.datatables.TableModel; +import io.jenkins.plugins.util.BuildResultNavigator; +import io.jenkins.plugins.util.QualityGateResult; + +/** + * Server side model that provides the data for the details view of the coverage results. The layout of the associated + * view is defined corresponding jelly view 'index.jelly'. + * + * @author Ullrich Hafner + * @author Florian Orendi + */ +@SuppressWarnings({"PMD.GodClass", "PMD.ExcessivePublicCount", "checkstyle:ClassDataAbstractionCoupling", "checkstyle:ClassFanOutComplexity"}) +public class CoverageViewModel extends DefaultAsyncTableContentProvider implements ModelObject { + private static final TreeMapNodeConverter TREE_MAP_NODE_CONVERTER = new TreeMapNodeConverter(); + private static final BuildResultNavigator NAVIGATOR = new BuildResultNavigator(); + private static final SourceCodeFacade SOURCE_CODE_FACADE = new SourceCodeFacade(); + + static final String ABSOLUTE_COVERAGE_TABLE_ID = "absolute-coverage-table"; + static final String MODIFIED_LINES_COVERAGE_TABLE_ID = "modified-lines-coverage-table"; + static final String MODIFIED_FILES_COVERAGE_TABLE_ID = "modified-files-coverage-table"; + static final String INDIRECT_COVERAGE_TABLE_ID = "indirect-coverage-table"; + private static final String INLINE_SUFFIX = "-inline"; + private static final String INFO_MESSAGES_VIEW_URL = "info"; + + private static final ElementFormatter FORMATTER = new ElementFormatter(); + private static final Set TREE_METRICS = Set.of(Metric.LINE, Metric.INSTRUCTION, Metric.BRANCH, Metric.MUTATION); + private final Run owner; + private final String optionalName; + private final CoverageStatistics statistics; + private final QualityGateResult qualityGateResult; + private final String referenceBuild; + private final FilteredLog log; + private final Node node; + private final String id; + + private final Node modifiedLinesCoverageTreeRoot; + private final Node modifiedFilesCoverageTreeRoot; + private final Node indirectCoverageChangesTreeRoot; + private final Function trendChartFunction; + + private ColorProvider colorProvider = ColorProviderFactory.createDefaultColorProvider(); + + @SuppressWarnings("checkstyle:ParameterNumber") + CoverageViewModel(final Run owner, final String id, final String optionalName, final Node node, + final CoverageStatistics statistics, final QualityGateResult qualityGateResult, + final String referenceBuild, final FilteredLog log, final Function trendChartFunction) { + super(); + + this.owner = owner; + + this.id = id; + this.optionalName = optionalName; + + this.node = node; + this.statistics = statistics; + this.qualityGateResult = qualityGateResult; + this.referenceBuild = referenceBuild; + + this.log = log; + + // initialize filtered coverage trees so that they will not be calculated multiple times + modifiedLinesCoverageTreeRoot = node.filterByModifiedLines(); + modifiedFilesCoverageTreeRoot = node.filterByModifiedFiles(); + indirectCoverageChangesTreeRoot = node.filterByIndirectChanges(); + this.trendChartFunction = trendChartFunction; + } + + public String getId() { + return id; + } + + public Run getOwner() { + return owner; + } + + public Node getNode() { + return node; + } + + public ElementFormatter getFormatter() { + return FORMATTER; + } + + /** + * Returns the value metrics that should be visualized in a tree map. + * + * @return the value metrics + */ + public NavigableSet getTreeMetrics() { + var valueMetrics = node.getValueMetrics(); + valueMetrics.retainAll(TREE_METRICS); + return valueMetrics; + } + + @Override + public String getDisplayName() { + if (StringUtils.isBlank(node.getName()) || "-".equals(node.getName())) { + if (StringUtils.isBlank(optionalName)) { + return Messages.Coverage_Link_Name(); + } + return optionalName; + } + if (StringUtils.isBlank(optionalName)) { + return Messages.Coverage_Title(node.getName()); + } + return String.format("%s: %s", optionalName, node.getName()); + } + + /** + * Gets the remote API for this action. Depending on the path, a different result is selected. + * + * @return the remote API + */ + public Api getApi() { + return new Api(new CoverageApi(statistics, qualityGateResult, referenceBuild)); + } + + /** + * Gets a set of color IDs which can be used to dynamically load the defined Jenkins colors. + * + * @return the available color IDs + */ + @JavaScriptMethod + @SuppressWarnings("unused") + public Set getJenkinsColorIDs() { + return CoverageColorJenkinsId.getAll(); + } + + /** + * Creates a new {@link ColorProvider} based on the passed color json string which contains the set Jenkins colors. + * + * @param colors + * The dynamically loaded Jenkins colors to be used for highlighting the coverage tree as json string + */ + @JavaScriptMethod + @SuppressWarnings("unused") + public void setJenkinsColors(final String colors) { + colorProvider = createColorProvider(colors); + } + + /** + * Parses the passed color json string to a {@link ColorProvider}. + * + * @param json + * The color json + * + * @return the created color provider + */ + private ColorProvider createColorProvider(final String json) { + try { + ObjectMapper mapper = new ObjectMapper(); + Map colorMapping = mapper.readValue(json, new ColorMappingType()); + return ColorProviderFactory.createColorProvider(colorMapping); + } + catch (JsonProcessingException e) { + return ColorProviderFactory.createDefaultColorProvider(); + } + } + + @JavaScriptMethod + public CoverageOverview getOverview() { + return new CoverageOverview(node); + } + + /** + * Returns the trend chart configuration. + * + * @param configuration + * JSON object to configure optional properties for the trend chart + * + * @return the trend chart model (converted to a JSON string) + */ + @JavaScriptMethod + @SuppressWarnings("unused") + public String getTrendChart(final String configuration) { + return trendChartFunction.apply(configuration); + } + + /** + * Returns the root of the tree of nodes for the ECharts treemap. This tree is used as model for the chart on the + * client side. + * + * @param coverageMetric + * the used coverage metric (line, branch, instruction, mutation) + * + * @return the tree of nodes for the ECharts treemap + */ + @JavaScriptMethod + @SuppressWarnings("unused") + public LabeledTreeMapNode getCoverageTree(final String coverageMetric) { + Metric metric = getCoverageMetricFromText(coverageMetric); + return TREE_MAP_NODE_CONVERTER.toTreeChartModel(getNode(), metric, colorProvider); + } + + /** + * Gets the {@link Metric} from a String representation used in the frontend. Only 'Line' and 'Branch' is possible. + * 'Line' is used as a default. + * + * @param text + * The coverage metric as String + * + * @return the coverage metric + */ + private Metric getCoverageMetricFromText(final String text) { + if (text.contains("line")) { + return Metric.LINE; + } + if (text.contains("branch")) { + return Metric.BRANCH; + } + if (text.contains("instruction")) { + return Metric.INSTRUCTION; + } + if (text.contains("mutation")) { + return Metric.MUTATION; + } + if (text.contains("loc")) { + return Metric.LOC; + } + if (text.contains("density")) { + return Metric.COMPLEXITY_DENSITY; + } + return Metric.COMPLEXITY; + } + + /** + * Returns the table model that matches with the passed table ID and shows the files along with the branch and line + * coverage. + * + * @param tableId + * ID of the table model + * + * @return the table model with the specified ID + */ + @Override + public TableModel getTableModel(final String tableId) { + RowRenderer renderer = createRenderer(tableId); + + String actualId = tableId.replace(INLINE_SUFFIX, StringUtils.EMPTY); + switch (actualId) { + case ABSOLUTE_COVERAGE_TABLE_ID: + return new CoverageTableModel(tableId, getNode(), renderer, colorProvider); + case MODIFIED_LINES_COVERAGE_TABLE_ID: + return new ModifiedLinesCoverageTableModel(tableId, getNode(), modifiedLinesCoverageTreeRoot, renderer, + colorProvider); + case INDIRECT_COVERAGE_TABLE_ID: + return new IndirectCoverageChangesTable(tableId, getNode(), indirectCoverageChangesTreeRoot, renderer, + colorProvider); + default: + throw new NoSuchElementException("No such table with id " + actualId); + } + } + + private RowRenderer createRenderer(final String tableId) { + RowRenderer renderer; + if (tableId.endsWith(INLINE_SUFFIX) && hasSourceCode()) { + renderer = new InlineRowRenderer(); + } + else { + renderer = new LinkedRowRenderer(getOwner().getRootDir(), getId()); + } + return renderer; + } + + /** + * Returns the URL for coverage results of the selected build. Based on the current URL, the new URL will be + * composed by replacing the current build number with the selected build number. + * + * @param selectedBuildDisplayName + * the selected build to open the new results for + * @param currentUrl + * the absolute URL to this details view results + * + * @return the URL to the results or an empty string if the results are not available + */ + @JavaScriptMethod + public String getUrlForBuild(final String selectedBuildDisplayName, final String currentUrl) { + return NAVIGATOR.getSameUrlForOtherBuild(owner, currentUrl, id, + selectedBuildDisplayName).orElse(StringUtils.EMPTY); + } + + /** + * Gets the source code of the file which is represented by the passed hash code. The coverage of the source code is + * highlighted by using HTML. Depending on the passed table ID, the source code is returned filtered with only the + * relevant lines of code. + * + * @param fileHash + * The hash code of the requested file + * @param tableId + * The ID of the source file table + * + * @return the highlighted source code + */ + @JavaScriptMethod + public String getSourceCode(final String fileHash, final String tableId) { + Optional targetResult + = getNode().findByHashCode(Metric.FILE, Integer.parseInt(fileHash)); + if (targetResult.isPresent()) { + try { + Node fileNode = targetResult.get(); + return readSourceCode(fileNode, tableId); + } + catch (IOException | InterruptedException exception) { + return ExceptionUtils.getStackTrace(exception); + } + } + return Messages.Coverage_Not_Available(); + } + + /** + * Reads the sourcecode corresponding to the passed {@link Node node} and filters the code dependent on the table + * ID. + * + * @param sourceNode + * The node + * @param tableId + * The table ID + * + * @return the sourcecode with highlighted coverage + * @throws IOException + * if reading failed + * @throws InterruptedException + * if reading failed + */ + private String readSourceCode(final Node sourceNode, final String tableId) + throws IOException, InterruptedException { + String content = ""; + File rootDir = getOwner().getRootDir(); + if (isSourceFileAvailable(sourceNode)) { + content = SOURCE_CODE_FACADE.read(rootDir, getId(), sourceNode.getPath()); + } + if (!content.isEmpty() && sourceNode instanceof FileNode) { + FileNode fileNode = (FileNode) sourceNode; + String cleanTableId = StringUtils.removeEnd(tableId, INLINE_SUFFIX); + if (MODIFIED_LINES_COVERAGE_TABLE_ID.equals(cleanTableId)) { + return SOURCE_CODE_FACADE.calculateModifiedLinesCoverageSourceCode(content, fileNode); + } + else if (INDIRECT_COVERAGE_TABLE_ID.equals(cleanTableId)) { + return SOURCE_CODE_FACADE.calculateIndirectCoverageChangesSourceCode(content, fileNode); + } + else { + return content; + } + } + return Messages.Coverage_Not_Available(); + } + + /** + * Checks whether source files are stored. + * + * @return {@code true} when source files are stored, {@code false} otherwise + */ + @JavaScriptMethod + public boolean hasSourceCode() { + return SOURCE_CODE_FACADE.hasStoredSourceCode(getOwner().getRootDir(), id); + } + + /** + * Checks whether modified lines coverage exists. + * + * @return {@code true} whether modified lines coverage exists, else {@code false} + */ + public boolean hasModifiedLinesCoverage() { + return !modifiedLinesCoverageTreeRoot.isEmpty(); + } + + /** + * Checks whether indirect coverage changes exist. + * + * @return {@code true} whether indirect coverage changes exist, else {@code false} + */ + public boolean hasIndirectCoverageChanges() { + return !indirectCoverageChangesTreeRoot.isEmpty(); + } + + /** + * Returns whether the source file is available in Jenkins build folder. + * + * @param coverageNode + * The {@link Node} which is checked if there is a source file available + * + * @return {@code true} if the source file is available, {@code false} otherwise + */ + public boolean isSourceFileAvailable(final Node coverageNode) { + return SOURCE_CODE_FACADE.canRead(getOwner().getRootDir(), id, coverageNode.getPath()); + } + + /** + * Returns a new sub-page for the selected link. + * + * @param link + * the link to identify the sub-page to show + * @param request + * Stapler request + * @param response + * Stapler response + * + * @return the new sub-page + */ + @SuppressWarnings("unused") // Called by jelly view + @CheckForNull + public Object getDynamic(final String link, final StaplerRequest request, final StaplerResponse response) { + if (INFO_MESSAGES_VIEW_URL.equals(link)) { + return new MessagesViewModel(getOwner(), Messages.MessagesViewModel_Title(), + log.getInfoMessages(), log.getErrorMessages()); + } + if (StringUtils.isNotEmpty(link)) { + try { + Optional targetResult + = getNode().findByHashCode(Metric.FILE, Integer.parseInt(link)); + if (targetResult.isPresent() && targetResult.get() instanceof FileNode) { + return new SourceViewModel(getOwner(), getId(), (FileNode) targetResult.get()); + } + } + catch (NumberFormatException exception) { + // ignore + } + } + return null; // fallback on broken URLs + } + + /** + * UI model for the coverage overview bar chart. Shows the coverage results for the different coverage metrics. + */ + public static class CoverageOverview { + private final Node coverage; + private static final ElementFormatter ELEMENT_FORMATTER = new ElementFormatter(); + + CoverageOverview(final Node coverage) { + this.coverage = coverage; + } + + public List getMetrics() { + return sortCoverages() + .map(Coverage::getMetric) + .map(ELEMENT_FORMATTER::getLabel) + .collect(Collectors.toList()); + } + + private Stream sortCoverages() { + return ELEMENT_FORMATTER.getSortedCoverageValues(coverage) + .filter(c -> c.getTotal() > 1); // ignore elements that have a total of 1 + } + + public List getCovered() { + return getCoverageCounter(Coverage::getCovered); + } + + public List getMissed() { + return getCoverageCounter(Coverage::getMissed); + } + + private List getCoverageCounter(final Function property) { + return sortCoverages().map(property).collect(Collectors.toList()); + } + + public List getCoveredPercentages() { + return getPercentages(Coverage::getCoveredPercentage); + } + + public List getMissedPercentages() { + return getPercentages(c -> Percentage.valueOf(c.getMissed(), c.getTotal())); + } + + private List getPercentages(final Function displayType) { + return sortCoverages().map(displayType) + .map(Percentage::toDouble) + .collect(Collectors.toList()); + } + } + + /** + * Used for parsing a Jenkins color mapping JSON string to a color map. + */ + private static final class ColorMappingType extends TypeReference> { + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageXmlStream.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageXmlStream.java new file mode 100644 index 000000000..8f1ac5140 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageXmlStream.java @@ -0,0 +1,287 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.AbstractMap.SimpleEntry; +import java.util.Arrays; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.Fraction; + +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; + +import edu.hm.hafner.coverage.ClassNode; +import edu.hm.hafner.coverage.ContainerNode; +import edu.hm.hafner.coverage.Coverage; +import edu.hm.hafner.coverage.CyclomaticComplexity; +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.coverage.FractionValue; +import edu.hm.hafner.coverage.LinesOfCode; +import edu.hm.hafner.coverage.MethodNode; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.ModuleNode; +import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.coverage.PackageNode; +import edu.hm.hafner.coverage.Value; + +import hudson.util.XStream2; + +import io.jenkins.plugins.util.AbstractXmlStream; + +/** + * Configures the XML stream for the coverage tree, which consists of {@link Node}s. + */ +class CoverageXmlStream extends AbstractXmlStream { + private static final Collector ARRAY_JOINER = Collectors.joining(", ", "[", "]"); + + private static String[] toArray(final String value) { + String cleanInput = StringUtils.removeEnd(StringUtils.removeStart(StringUtils.deleteWhitespace(value), "["), "]"); + + return StringUtils.split(cleanInput, ","); + } + + /** + * Creates an XML stream for {@link Node}. + */ + CoverageXmlStream() { + super(Node.class); + } + + @Override + protected void configureXStream(final XStream2 xStream) { + registerConverters(xStream); + + xStream.alias("container", ContainerNode.class); + xStream.alias("module", ModuleNode.class); + xStream.alias("package", PackageNode.class); + xStream.alias("file", FileNode.class); + xStream.alias("class", ClassNode.class); + xStream.alias("method", MethodNode.class); + + xStream.registerLocalConverter(FileNode.class, "coveredPerLine", new IntegerLineMapConverter()); + xStream.registerLocalConverter(FileNode.class, "missedPerLine", new IntegerLineMapConverter()); + xStream.registerLocalConverter(FileNode.class, "indirectCoverageChanges", new IntegerLineMapConverter()); + + xStream.registerLocalConverter(FileNode.class, "changedLines", new IntegerSetConverter()); + xStream.registerLocalConverter(FileNode.class, "coverageDelta", new MetricFractionMapConverter()); + } + + static void registerConverters(final XStream2 xStream) { + xStream.alias("metric", Metric.class); + + xStream.alias("coverage", Coverage.class); + xStream.addImmutableType(Coverage.class, false); + xStream.alias("complexity", CyclomaticComplexity.class); + xStream.addImmutableType(CyclomaticComplexity.class, false); + xStream.alias("loc", LinesOfCode.class); + xStream.addImmutableType(LinesOfCode.class, false); + xStream.alias("fraction", FractionValue.class); + xStream.addImmutableType(FractionValue.class, false); + + xStream.registerConverter(new FractionConverter()); + xStream.registerConverter(new SimpleConverter<>(Value.class, Value::serialize, Value::valueOf)); + xStream.registerConverter(new SimpleConverter<>(Metric.class, Metric::name, Metric::valueOf)); + } + + @Override + protected Node createDefaultValue() { + return new ModuleNode("Empty"); + } + + /** + * {@link Converter} for {@link Fraction} instances so that only the values will be serialized. After reading the + * values back from the stream, the string representation will be converted to an actual instance again. + */ + static final class FractionConverter implements Converter { + @SuppressWarnings("PMD.NullAssignment") + @Override + public void marshal(final Object source, final HierarchicalStreamWriter writer, + final MarshallingContext context) { + writer.setValue(source instanceof Fraction ? ((Fraction) source).toProperString() : null); + } + + @Override + public Object unmarshal(final HierarchicalStreamReader reader, final UnmarshallingContext context) { + return Fraction.getFraction(reader.getValue()); + } + + @Override + public boolean canConvert(final Class type) { + return type == Fraction.class; + } + } + + /** + * {@link Converter} for a {@link TreeMap} of coverage percentages per metric. Stores the mapping in the condensed + * format {@code metric1: numerator1/denominator1, metric2: numerator2/denominator2, ...}. + */ + static final class MetricFractionMapConverter extends TreeMapConverter { + @Override + protected Function, String> createMapEntry() { + return e -> String.format("%s: %s", e.getKey().name(), e.getValue().toProperString()); + } + + @Override + protected Entry createMapping(final String key, final String value) { + return entry(Metric.valueOf(key), Fraction.getFraction(value)); + } + } + + /** + * {@link Converter} for {@link Coverage} instances so that only the values will be serialized. After reading the + * values back from the stream, the string representation will be converted to an actual instance again. + * + * @param type of the objects that will be marshalled and unmarshalled + */ + public static class SimpleConverter implements Converter { + private final Class type; + private final Function marshaller; + private final Function unmarshaller; + + protected SimpleConverter(final Class type, final Function marshaller, final Function unmarshaller) { + this.type = type; + this.marshaller = marshaller; + this.unmarshaller = unmarshaller; + } + + @SuppressWarnings("PMD.NullAssignment") + @Override + public void marshal(final Object source, final HierarchicalStreamWriter writer, + final MarshallingContext context) { + writer.setValue(type.isInstance(source) ? marshaller.apply(type.cast(source)) : null); + } + + @Override + public final Object unmarshal(final HierarchicalStreamReader reader, final UnmarshallingContext context) { + return unmarshaller.apply(reader.getValue()); + } + + @Override + public final boolean canConvert(final Class clazz) { + return type.isAssignableFrom(clazz); + } + } + + /** + * {@link Converter} base class for {@link TreeMap} instance. Stores the mappings in a condensed format + * {@code key1: value1, key2: value2, ...}. + * + * @param + * the type of keys maintained by this map + * @param + * the type of mapped values + */ + abstract static class TreeMapConverter, V> implements Converter { + @Override + @SuppressWarnings({"PMD.NullAssignment", "unchecked"}) + public void marshal(final Object source, final HierarchicalStreamWriter writer, + final MarshallingContext context) { + writer.setValue(source instanceof NavigableMap ? marshal((NavigableMap) source) : null); + } + + String marshal(final SortedMap source) { + return source.entrySet() + .stream() + .map(createMapEntry()) + .collect(ARRAY_JOINER); + } + + @Override + public boolean canConvert(final Class type) { + return type == TreeMap.class; + } + + @Override + public NavigableMap unmarshal(final HierarchicalStreamReader reader, final UnmarshallingContext context) { + return unmarshal(reader.getValue()); + } + + NavigableMap unmarshal(final String value) { + NavigableMap map = new TreeMap<>(); + + for (String marshalledValue : toArray(value)) { + if (StringUtils.contains(marshalledValue, ":")) { + try { + Entry entry = createMapping( + StringUtils.substringBefore(marshalledValue, ':'), + StringUtils.substringAfter(marshalledValue, ':')); + map.put(entry.getKey(), entry.getValue()); + } + catch (IllegalArgumentException exception) { + // ignore + } + } + } + return map; + } + + protected abstract Function, String> createMapEntry(); + + protected abstract Map.Entry createMapping(String key, String value); + + protected SimpleEntry entry(final K key, final V value) { + return new SimpleEntry<>(key, value); + } + } + + /** + * {@link Converter} for a {@link SortedMap} of coverages per line. Stores the mapping in the condensed format + * {@code key1: covered1/missed1, key2: covered2/missed2, ...}. + */ + static final class IntegerLineMapConverter extends TreeMapConverter { + @Override + protected Function, String> createMapEntry() { + return e -> String.format("%d: %d", e.getKey(), e.getValue()); + } + + @Override + protected Entry createMapping(final String key, final String value) { + return entry(Integer.valueOf(key), Integer.valueOf(value)); + } + } + + /** + * {@link Converter} for a {@link TreeSet} of integers that serializes just the values. After + * reading the values back from the stream, the string representation will be converted to an actual instance + * again. + */ + static final class IntegerSetConverter implements Converter { + @SuppressWarnings({"PMD.NullAssignment", "unchecked"}) + @Override + public void marshal(final Object source, final HierarchicalStreamWriter writer, + final MarshallingContext context) { + writer.setValue(source instanceof TreeSet ? marshal((TreeSet) source) : null); + } + + String marshal(final Set lines) { + return lines.stream().map(String::valueOf).collect(ARRAY_JOINER); + } + + @Override + public NavigableSet unmarshal(final HierarchicalStreamReader reader, final UnmarshallingContext context) { + return unmarshal(reader.getValue()); + } + + NavigableSet unmarshal(final String value) { + return Arrays.stream(toArray(value)).map(Integer::valueOf).collect(Collectors.toCollection(TreeSet::new)); + } + + @Override + public boolean canConvert(final Class type) { + return type == TreeSet.class; + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/FileChangesProcessor.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/FileChangesProcessor.java new file mode 100644 index 000000000..bc5b62c7f --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/FileChangesProcessor.java @@ -0,0 +1,316 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Optional; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.coverage.Node; + +import io.jenkins.plugins.forensics.delta.Change; +import io.jenkins.plugins.forensics.delta.ChangeEditType; +import io.jenkins.plugins.forensics.delta.FileChanges; + +/** + * Calculates and attaches values to the {@link FileNode nodes} of the coverage tree which represent the changes + * concerning code and coverage. + * + * @author Florian Orendi + */ +public class FileChangesProcessor { + /** + * Attaches the changed code lines to the file nodes of the coverage tree. + * + * @param coverageNode + * The root node of the coverage tree + * @param codeChanges + * The code changes to be attached + */ + public void attachChangedCodeLines(final Node coverageNode, final Map codeChanges) { + Map nodePathMapping = coverageNode.getAllFileNodes().stream() + .collect(Collectors.toMap(Node::getPath, Function.identity())); + + codeChanges.forEach((path, fileChange) -> { + if (nodePathMapping.containsKey(path)) { + FileNode changedNode = nodePathMapping.get(path); + attachChanges(changedNode, fileChange.getChangesByType(ChangeEditType.INSERT)); + attachChanges(changedNode, fileChange.getChangesByType(ChangeEditType.REPLACE)); + } + }); + } + + /** + * Attaches a set of changes to a specific {@link FileNode node}. + * + * @param changedNode + * The node which contains code changes + * @param relevantChanges + * The relevant changes + */ + private void attachChanges(final FileNode changedNode, final Set relevantChanges) { + for (Change change : relevantChanges) { + for (int i = change.getFromLine(); i <= change.getToLine(); i++) { + changedNode.addModifiedLines(i); + } + } + } + + /** + * Attaches the delta between the total file coverage of all currently built files against the passed reference. The + * algorithm also covers renamed files. + * + * @param root + * The root of the coverage tree + * @param referenceNode + * The root of the reference coverage tree + * @param oldPathMapping + * A mapping between the report paths of the current and the reference coverage tree + */ + public void attachFileCoverageDeltas(final Node root, final Node referenceNode, + final Map oldPathMapping) { + Map fileNodes = getFileNodeMappingWithReferencePaths(root, oldPathMapping); + Map referenceFileNodes = getReferenceFileNodeMapping(fileNodes, referenceNode); + fileNodes.entrySet().stream() + .filter(entry -> referenceFileNodes.containsKey(entry.getKey())) + .forEach(entry -> attachFileCoverageDelta(entry.getValue(), referenceFileNodes.get(entry.getKey()))); + } + + /** + * Attaches the delta between the total coverage of a file against the same file from the reference build. + * + * @param fileNode + * The {@link FileNode node} which represents the total coverage of a file + * @param referenceNode + * The {@link FileNode reference node} which represents the coverage of the reference file + */ + private void attachFileCoverageDelta(final FileNode fileNode, final FileNode referenceNode) { + fileNode.computeDelta(referenceNode); + } + + /** + * Attaches all found indirect coverage changes within the coverage tree, compared to a reference tree. + * + * @param root + * The root of the tree in which indirect coverage changes are searched + * @param referenceNode + * The root of the reference tree + * @param codeChanges + * The code changes that has been applied between the two commits underlying the node and its reference + * @param oldPathMapping + * A mapping between the report paths of the current and the reference coverage tree + */ + public void attachIndirectCoveragesChanges(final Node root, final Node referenceNode, + final Map codeChanges, final Map oldPathMapping) { + Map fileNodes = getFileNodeMappingWithReferencePaths(root, oldPathMapping); + Map referenceFileNodes = getReferenceFileNodeMapping(fileNodes, referenceNode); + + for (Map.Entry entry : fileNodes.entrySet()) { + String referencePath = entry.getKey(); + FileNode fileNode = entry.getValue(); + Optional> referenceCoveragePerLine = + getReferenceCoveragePerLine(referenceFileNodes, referencePath); + if (referenceCoveragePerLine.isPresent()) { + SortedMap referenceCoverageMapping = new TreeMap<>(referenceCoveragePerLine.get()); + String currentPath = fileNode.getPath(); + if (codeChanges.containsKey(currentPath)) { + adjustedCoveragePerLine(referenceCoverageMapping, codeChanges.get(currentPath)); + } + attachIndirectCoverageChangeForFile(fileNode, referenceCoverageMapping); + } + } + } + + /** + * Attaches the indirect coverage changes for a specific file, represented by the specified {@link FileNode}. + * + * @param fileNode + * the file coverage node which represents the processed file + * @param referenceCoverageMapping + * a mapping which contains the coverage per line of the reference file + */ + private void attachIndirectCoverageChangeForFile(final FileNode fileNode, + final SortedMap referenceCoverageMapping) { + fileNode.getLinesWithCoverage().forEach(line -> { + if (!fileNode.hasModifiedLine(line) && referenceCoverageMapping.containsKey(line)) { + int referenceCovered = referenceCoverageMapping.get(line); + int covered = fileNode.getCoveredOfLine(line); + if (covered != referenceCovered) { + fileNode.addIndirectCoverageChange(line, covered - referenceCovered); + } + } + }); + } + + /** + * Gets the coverage, mapped by the line within a file, for a reference file, represented by its fully qualified + * name. + * + * @param references + * All possible reference + * @param fullyQualifiedName + * The fully qualified name of the file for which the coverage per line is required + * + * @return an Optional of the coverage mapping if existent, else an empty Optional + */ + private Optional> getReferenceCoveragePerLine( + final Map references, final String fullyQualifiedName) { + if (references.containsKey(fullyQualifiedName)) { + NavigableMap coveragePerLine = references.get(fullyQualifiedName).getCounters(); + if (!coveragePerLine.isEmpty()) { + return Optional.of(coveragePerLine); + } + } + return Optional.empty(); + } + + /** + * Adjusts a coverage-per-line mapping of a file before changes has been applied so that the coverage values can be + * compared to the coverage-per-line mapping after code changes within the file. + * + * @param coveragePerLine + * The coverage-per-line mapping of the file before the changes which should be adjusted + * @param fileChanges + * The applied code changes of the file + */ + private void adjustedCoveragePerLine(final SortedMap coveragePerLine, + final FileChanges fileChanges) { + List> coverages = transformCoveragePerLine(coveragePerLine, fileChanges); + + fileChanges.getChangesByType(ChangeEditType.DELETE).forEach(change -> { + for (int i = change.getChangedFromLine(); i <= change.getChangedToLine(); i++) { + coverages.get(i).clear(); + } + }); + + fileChanges.getChangesByType(ChangeEditType.INSERT).forEach(change -> { + List inserted = coverages.get(change.getChangedFromLine()); + int changedLinesNumber = change.getToLine() - change.getFromLine() + 1; + fillCoverageListWithNull(inserted, changedLinesNumber); + }); + + fileChanges.getChangesByType(ChangeEditType.REPLACE).forEach(change -> { + List replaced = coverages.get(change.getChangedFromLine()); + replaced.clear(); // coverage of replaced code is irrelevant + int changedLinesNumber = change.getToLine() - change.getFromLine() + 1; + fillCoverageListWithNull(replaced, changedLinesNumber); + for (int i = change.getChangedFromLine() + 1; i <= change.getChangedToLine(); i++) { + coverages.get(i).clear(); + } + }); + + List adjustedCoveragesList = coverages.stream() + .flatMap(Collection::stream) + .collect(Collectors.toList()); + + coveragePerLine.clear(); + for (int line = 1; line < adjustedCoveragesList.size(); line++) { + Integer coverage = adjustedCoveragesList.get(line); + if (coverage != null) { + coveragePerLine.put(line, coverage); + } + } + } + + /** + * Transforms a coverage-per-line mapping to a list representation which can be expanded without influencing the + * original line numbers. + * + * @param coveragePerLine + * The coverage-per-line mapping of the file before the changes which should be adjusted + * @param fileChanges + * The applied code changes of the file + * + * @return the list expandable list representation of the coverage-per-line mapping + */ + private List> transformCoveragePerLine( + final SortedMap coveragePerLine, final FileChanges fileChanges) { + List> coverages = coveragePerLine.values().stream() + .map(coverage -> new ArrayList<>(Collections.singletonList(coverage))) + .collect(Collectors.toList()); + + // the highest covered line might not be the highest line which contains changes + int maxLineNumber = coveragePerLine.lastKey(); + Optional highestLineNumber = fileChanges.getChanges().values().stream() + .flatMap(Set::stream) + .map(Change::getChangedToLine) + .max(Comparator.naturalOrder()); + if (highestLineNumber.isPresent() && highestLineNumber.get() > maxLineNumber) { + maxLineNumber = highestLineNumber.get(); + } + + IntStream.range(0, maxLineNumber + 1) + .filter(line -> !coveragePerLine.containsKey(line)) + .forEach(line -> { + if (line < coverages.size()) { + coverages.add(line, new ArrayList<>(Collections.singletonList(null))); + } + else { + coverages.add(new ArrayList<>(Collections.singletonList(null))); + } + }); + + return coverages; + } + + /** + * Gets all {@link FileNode file nodes} from the currently running build which also exist within the + * reference build and maps them by their fully qualified name from the reference. + * + * @param root + * the root node of the coverage tree of the currently running build + * @param oldPathMapping + * a mapping between the report fully qualified names of the current and the reference coverage tree + * + * @return the created node mapping whose keys are fully qualified names from the reference and which values are the + * corresponding nodes from the actual build + */ + private Map getFileNodeMappingWithReferencePaths( + final Node root, final Map oldPathMapping) { + return root.getAllFileNodes().stream() + .filter(node -> oldPathMapping.containsKey(node.getPath())) + .collect(Collectors.toMap(node -> oldPathMapping.get(node.getPath()), Function.identity())); + } + + /** + * Gets all {@link FileNode file nodes} from a reference coverage tree which also exist in the current + * coverage tree. The found nodes are mapped by their path. + * + * @param nodeMapping + * The file nodes of the current coverage tree, mapped by their paths + * @param referenceNode + * The root of the reference coverage tree + * + * @return the created node mapping + */ + private Map getReferenceFileNodeMapping( + final Map nodeMapping, final Node referenceNode) { + return referenceNode.getAllFileNodes().stream() + .filter(reference -> nodeMapping.containsKey(reference.getPath())) + .collect(Collectors.toMap(FileNode::getPath, Function.identity())); + } + + /** + * Adds {@code null} values to the passed list. + * + * @param coverageList + * The list which should be filled with {@code null} + * @param number + * The number of values to be inserted + */ + private void fillCoverageListWithNull(final List coverageList, final int number) { + for (int i = 0; i < number; i++) { + coverageList.add(null); + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/IndirectCoverageChangesTable.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/IndirectCoverageChangesTable.java new file mode 100644 index 000000000..561fa8bae --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/IndirectCoverageChangesTable.java @@ -0,0 +1,40 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.Locale; + +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.coverage.Node; + +import io.jenkins.plugins.coverage.metrics.color.ColorProvider; + +/** + * A coverage table model that handles the lines of code that have been indirectly changed with respect to a result of a + * reference build. + */ +class IndirectCoverageChangesTable extends ChangesTableModel { + IndirectCoverageChangesTable(final String id, final Node root, final Node changeRoot, + final RowRenderer renderer, final ColorProvider colorProvider) { + super(id, root, changeRoot, renderer, colorProvider); + } + + @Override + IndirectCoverageChangesRow createRow(final FileNode file, final Locale browserLocale) { + return new IndirectCoverageChangesRow( + getOriginalNode(file), file, browserLocale, getRenderer(), getColorProvider()); + } + + /** + * UI row model for the coverage details table of the indirect coverage changes. + */ + private static class IndirectCoverageChangesRow extends ChangesRow { + IndirectCoverageChangesRow(final FileNode originalFile, final FileNode changedFileNode, + final Locale browserLocale, final RowRenderer renderer, final ColorProvider colorProvider) { + super(originalFile, changedFileNode, browserLocale, renderer, colorProvider); + } + + @Override + public int getLoc() { + return getOriginalFile().getIndirectCoverageChanges().size(); + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedLinesCoverageTableModel.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedLinesCoverageTableModel.java new file mode 100644 index 000000000..4b0d75048 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedLinesCoverageTableModel.java @@ -0,0 +1,39 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.Locale; + +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.coverage.Node; + +import io.jenkins.plugins.coverage.metrics.color.ColorProvider; + +/** + * A coverage table model that handles the modified lines of a change with respect to a result of a reference build. + */ +class ModifiedLinesCoverageTableModel extends ChangesTableModel { + ModifiedLinesCoverageTableModel(final String id, final Node root, final Node changeRoot, + final RowRenderer renderer, final ColorProvider colorProvider) { + super(id, root, changeRoot, renderer, colorProvider); + } + + @Override + ModifiedLinesCoverageRow createRow(final FileNode file, final Locale browserLocale) { + return new ModifiedLinesCoverageRow(getOriginalNode(file), file, + browserLocale, getRenderer(), getColorProvider()); + } + + /** + * UI row model for the coverage details table of modified lines. + */ + private static class ModifiedLinesCoverageRow extends ChangesRow { + ModifiedLinesCoverageRow(final FileNode originalFile, final FileNode changedFileNode, + final Locale browserLocale, final RowRenderer renderer, final ColorProvider colorProvider) { + super(originalFile, changedFileNode, browserLocale, renderer, colorProvider); + } + + @Override + public int getLoc() { + return getFile().getCoveredAndModifiedLines().size(); + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/package-info.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/package-info.java new file mode 100644 index 000000000..aedb4c2bc --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/package-info.java @@ -0,0 +1,10 @@ +/** + * New coverage model that tries to replace all the existing functionality with a more versatile model. + * + * @author Ullrich Hafner + */ +@DefaultAnnotation(NonNull.class) +package io.jenkins.plugins.coverage.metrics.steps; + +import edu.umd.cs.findbugs.annotations.DefaultAnnotation; +import edu.umd.cs.findbugs.annotations.NonNull; diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/model/CodeDeltaCalculator.java b/plugin/src/main/java/io/jenkins/plugins/coverage/model/CodeDeltaCalculator.java index 9a85b1f19..7454c8add 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/model/CodeDeltaCalculator.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/model/CodeDeltaCalculator.java @@ -21,10 +21,10 @@ import hudson.model.TaskListener; import io.jenkins.plugins.coverage.model.exception.CodeDeltaException; +import io.jenkins.plugins.forensics.delta.Delta; import io.jenkins.plugins.forensics.delta.DeltaCalculatorFactory; -import io.jenkins.plugins.forensics.delta.model.Delta; -import io.jenkins.plugins.forensics.delta.model.FileChanges; -import io.jenkins.plugins.forensics.delta.model.FileEditType; +import io.jenkins.plugins.forensics.delta.FileChanges; +import io.jenkins.plugins.forensics.delta.FileEditType; /** * Calculates the code delta between a Jenkins build and a reference build. diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/model/CoverageReporter.java b/plugin/src/main/java/io/jenkins/plugins/coverage/model/CoverageReporter.java index bda488b01..9addc90b1 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/model/CoverageReporter.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/model/CoverageReporter.java @@ -17,8 +17,8 @@ import io.jenkins.plugins.coverage.model.exception.CodeDeltaException; import io.jenkins.plugins.coverage.model.visualization.code.SourceCodePainter; import io.jenkins.plugins.coverage.targets.CoverageResult; -import io.jenkins.plugins.forensics.delta.model.Delta; -import io.jenkins.plugins.forensics.delta.model.FileChanges; +import io.jenkins.plugins.forensics.delta.Delta; +import io.jenkins.plugins.forensics.delta.FileChanges; import io.jenkins.plugins.forensics.reference.ReferenceFinder; import io.jenkins.plugins.prism.SourceCodeRetention; import io.jenkins.plugins.util.LogHandler; @@ -225,7 +225,7 @@ private Optional getReferenceBuildAction(final Run bu coverageBuildAction.getOwner().getDisplayName())); } - if (!previousResult.isPresent()) { + if (previousResult.isEmpty()) { log.logInfo("-> Found no reference result in reference build"); return Optional.empty(); diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/model/FileChangesProcessor.java b/plugin/src/main/java/io/jenkins/plugins/coverage/model/FileChangesProcessor.java index 2e549bf2e..ae81dc292 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/model/FileChangesProcessor.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/model/FileChangesProcessor.java @@ -16,9 +16,9 @@ import org.apache.commons.lang3.math.Fraction; -import io.jenkins.plugins.forensics.delta.model.Change; -import io.jenkins.plugins.forensics.delta.model.ChangeEditType; -import io.jenkins.plugins.forensics.delta.model.FileChanges; +import io.jenkins.plugins.forensics.delta.Change; +import io.jenkins.plugins.forensics.delta.ChangeEditType; +import io.jenkins.plugins.forensics.delta.FileChanges; /** * Calculates and attaches values to the {@link FileCoverageNode nodes} of the coverage tree which represent the changes diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/model/visualization/code/SourceCodeFacade.java b/plugin/src/main/java/io/jenkins/plugins/coverage/model/visualization/code/SourceCodeFacade.java index e61739c8b..d6b638851 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/model/visualization/code/SourceCodeFacade.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/model/visualization/code/SourceCodeFacade.java @@ -40,9 +40,9 @@ import io.jenkins.plugins.coverage.model.CoverageNode; import io.jenkins.plugins.coverage.model.FileCoverageNode; import io.jenkins.plugins.coverage.targets.CoveragePaint; -import io.jenkins.plugins.prism.CharsetValidation; import io.jenkins.plugins.prism.FilePermissionEnforcer; import io.jenkins.plugins.prism.SourceDirectoryFilter; +import io.jenkins.plugins.util.ValidationUtilities; /** * Facade to the source code file structure in Jenkins build folder. Access of those files should be done using an @@ -421,7 +421,7 @@ else if (sourceDirectories.size() == 1) { } private Charset getCharset() { - return new CharsetValidation().getCharset(sourceCodeEncoding); + return new ValidationUtilities().getCharset(sourceCodeEncoding); } private Set filterSourceDirectories(final File workspace, final FilteredLog log) { diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/model/visualization/dashboard/CoverageColumn.java b/plugin/src/main/java/io/jenkins/plugins/coverage/model/visualization/dashboard/CoverageColumn.java index 47055e37d..2ed9bca32 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/model/visualization/dashboard/CoverageColumn.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/model/visualization/dashboard/CoverageColumn.java @@ -211,6 +211,10 @@ private boolean hasCoverageAction(final Job job) { */ @Extension(optional = true) public static class CoverageDescriptor extends ListViewColumnDescriptor { + @Override + public boolean shownByDefault() { + return false; + } @NonNull @Override diff --git a/plugin/src/main/resources/META-INF/hudson.remoting.ClassFilter b/plugin/src/main/resources/META-INF/hudson.remoting.ClassFilter index bf3363b1d..e0aad24f4 100644 --- a/plugin/src/main/resources/META-INF/hudson.remoting.ClassFilter +++ b/plugin/src/main/resources/META-INF/hudson.remoting.ClassFilter @@ -2,3 +2,29 @@ gnu.trove.impl.hash.THash gnu.trove.impl.hash.TIntHash gnu.trove.impl.hash.TPrimitiveHash gnu.trove.map.hash.TIntObjectHashMap + +org.apache.commons.lang3.math.Fraction + +edu.hm.hafner.coverage.ClassNode +edu.hm.hafner.coverage.ContainerNode +edu.hm.hafner.coverage.Coverage +edu.hm.hafner.coverage.CyclomaticComplexity +edu.hm.hafner.coverage.FileNode +edu.hm.hafner.coverage.FractionValue +edu.hm.hafner.coverage.IntegerValue +edu.hm.hafner.coverage.LinesOfCode +edu.hm.hafner.coverage.MethodNode +edu.hm.hafner.coverage.Metric +edu.hm.hafner.coverage.ModuleNode +edu.hm.hafner.coverage.Mutation +edu.hm.hafner.coverage.MutationStatus +edu.hm.hafner.coverage.Mutator +edu.hm.hafner.coverage.Node +edu.hm.hafner.coverage.PackageNode +edu.hm.hafner.coverage.Percentage +edu.hm.hafner.coverage.SafeFraction +edu.hm.hafner.coverage.Value + +java.util.ImmutableCollections$ListN +java.util.ImmutableCollections$SetN +java.util.ImmutableCollections$Set12 diff --git a/plugin/src/main/resources/coverage/configuration.jelly b/plugin/src/main/resources/coverage/configuration.jelly new file mode 100644 index 000000000..94448befa --- /dev/null +++ b/plugin/src/main/resources/coverage/configuration.jelly @@ -0,0 +1,76 @@ + + + + + Provides the configuration for the coverage recorder and step. + + + +
+ + +
+ +
+
+
+
+
+ + +
+ + +
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ diff --git a/plugin/src/main/resources/coverage/configuration.properties b/plugin/src/main/resources/coverage/configuration.properties new file mode 100644 index 000000000..980de9223 --- /dev/null +++ b/plugin/src/main/resources/coverage/configuration.properties @@ -0,0 +1,18 @@ +parser.title=Code Coverage Tools +parser.add=Add Tool + +qualityGates.add=Add Quality Gate +qualityGates.title=Quality gates +qualityGates.description=You can define an arbitrary number of quality gates that will be evaluated after a build. \ + If a quality gate is not passed then the build can be set to unstable or failed, respectively. + +title.id=Custom ID +title.name=Custom Name +skipPublishingChecks.title=Skip publishing of checks to SCM hosting platforms +checksName.title=Checks name +checksAnnotationScope.title=Select the scope of source code annotations +failOnError.title=Fail the build if errors have been reported during the execution +title.enabledForFailure=Enable recording for failed builds +title.skipSymbolicLinks=Skip symbolic links when searching for files +sourceCodeRetention.title=Source Code Retention Strategy + diff --git a/plugin/src/main/resources/coverage/coverage-summary.jelly b/plugin/src/main/resources/coverage/coverage-summary.jelly new file mode 100644 index 000000000..4693884db --- /dev/null +++ b/plugin/src/main/resources/coverage/coverage-summary.jelly @@ -0,0 +1,89 @@ + + + + + Provides a the summary for a given coverage baseline. + + The baseline to show. + + + + +
  • + + + ${it.getDeltaBaseline(baseline).title} + + + ${baseline.title} + + +
    + + + + +
    + + + + + + + + + + + + + + + +
    ${%Metric}${%Value}
    ${formatter.getDisplayName(value.metric)} + + + + + + ${formatter.format(value)} + + + +
    +
    + +
    + +
      + +
    • ${formatter.formatValueWithMetric(value)} + + + + + + + + + + + (${delta}) + +
    • +
      +
    +
    + +
  • +
    + +
    + diff --git a/plugin/src/main/resources/coverage/coverage-table.jelly b/plugin/src/main/resources/coverage/coverage-table.jelly index dc4f28985..90e99c319 100644 --- a/plugin/src/main/resources/coverage/coverage-table.jelly +++ b/plugin/src/main/resources/coverage/coverage-table.jelly @@ -1,34 +1,51 @@ - + Provides a table to render the file coverage nodes without the source code. - + The ID of the table. + + The title of the table. + + + The symbol (icon) of the table. + + + Determines whether to show the changed files filter toggle. + + +
    - - + + + + +
    - - + + + + +
    - -
    - +
    +
    -
    +
    @@ -36,7 +53,7 @@
    ${%Please select a file in the table to open the source code}
    -
    +
    @@ -49,8 +66,11 @@
    - - + + + + +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/Messages.properties b/plugin/src/main/resources/io/jenkins/plugins/coverage/Messages.properties index 33adc3609..ca9304d57 100644 --- a/plugin/src/main/resources/io/jenkins/plugins/coverage/Messages.properties +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/Messages.properties @@ -1,4 +1,4 @@ -CoveragePublisher.displayName=Publish Coverage Report +CoveragePublisher.displayName=Publish Coverage Report [deprecated] CoverageAction.displayName=Coverage Report CoverageProjectAction.displayName=Coverage Report CoverageProcessor.healthReportDescriptionTemplate=Coverage Healthy score is {0}% diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/cell-style.css b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/cell-style.css new file mode 100644 index 000000000..d5ad1e99a --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/cell-style.css @@ -0,0 +1,21 @@ +.coverage-cell-outer { + border: 1px solid var(--medium-grey); + border-radius: 10px; +} + +.coverage-cell-inner { /* Bootstrap table. */ + padding: 4px 7px; + border-radius: 10px; + min-width: 5.5em; /* All percentages should use same width. */ +} + +.coverage-jenkins-cell-inner { /* Jenkins table. */ + padding: 1px 5px; + border-radius: 10px; + min-width: 4.5em; /* All percentages should use same width. */ +} + +.coverage-cell-link:hover { + text-decoration: none; +} + diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/model/Messages.properties b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/model/Messages.properties new file mode 100644 index 000000000..374d8bc85 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/model/Messages.properties @@ -0,0 +1,40 @@ +Metric.CONTAINER=Container Coverage +Metric.MODULE=Module Coverage +Metric.PACKAGE=Package Coverage +Metric.FILE=File Coverage +Metric.CLASS=Class Coverage +Metric.METHOD=Method Coverage +Metric.INSTRUCTION=Instruction Coverage +Metric.LINE=Line Coverage +Metric.MUTATION=Mutation Coverage +Metric.BRANCH=Branch Coverage +Metric.COMPLEXITY=Cyclomatic Complexity +Metric.COMPLEXITY_DENSITY=Complexity Density +Metric.LOC=Lines of Code + +Metric.Short.CONTAINER=Container +Metric.Short.MODULE=Module +Metric.Short.PACKAGE=Package +Metric.Short.FILE=File +Metric.Short.CLASS=Class +Metric.Short.METHOD=Method +Metric.Short.INSTRUCTION=Instruction +Metric.Short.LINE=Line +Metric.Short.MUTATION=Mutation +Metric.Short.BRANCH=Branch +Metric.Short.COMPLEXITY=Complexity +Metric.Short.COMPLEXITY_DENSITY=Complexity Density +Metric.Short.LOC=LOC + +Metric.MUTATION.Killed=Killed +Metric.MUTATION.Survived=Survived +Metric.Coverage.Covered=Covered +Metric.Coverage.Missed=Missed + +Baseline.PROJECT=Overall project +Baseline.PROJECT_DELTA=Overall project (difference to reference job) +Baseline.MODIFIED_LINES=Modified code lines +Baseline.MODIFIED_LINES_DELTA=Modified code lines (difference to modified files) +Baseline.MODIFIED_FILES=Modified files +Baseline.MODIFIED_FILES_DELTA=Modified files (difference to overall project) +Baseline.INDIRECT=Indirect changes diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/source/Messages.properties b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/source/Messages.properties new file mode 100644 index 000000000..4e509ceda --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/source/Messages.properties @@ -0,0 +1 @@ +Coverage.Title=Coverage of ''{0}'' diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/source/SourceViewModel/index.jelly b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/source/SourceViewModel/index.jelly new file mode 100644 index 000000000..3023dea7c --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/source/SourceViewModel/index.jelly @@ -0,0 +1,46 @@ + + + + + + + + + +
    + +
    +
    + + + +
    + + +
    +
    +
    + +

    + ${%Source code is unavailable.} + ${%Some possible reasons are:} +

      +
    • ${%reason.1}
    • +
    • ${%reason.2}
    • +
    • ${%reason.3}
    • +
    +

    +
    +
    + +
    +
    +
    + +
    + + + +
    + +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/source/SourceViewModel/index.properties b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/source/SourceViewModel/index.properties new file mode 100644 index 000000000..be06b04d5 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/source/SourceViewModel/index.properties @@ -0,0 +1,11 @@ +reason.1=\ + You did not enable storing of source files (see parameter 'sourceFiles'). + +reason.2=\ + Code Coverage API plugin did not find the source files. + +reason.3=\ + You do not have sufficient permissions to view source files. + + + diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildAction/summary.jelly b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildAction/summary.jelly new file mode 100644 index 000000000..34d9d2ce3 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildAction/summary.jelly @@ -0,0 +1,26 @@ + + + + + +
    + + ${it.displayName} + +
      + + + + + +
    • + Reference build: + +
    • +
      +
      + +
    +
    +
    +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildAction/summary.properties b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildAction/summary.properties new file mode 100644 index 000000000..1f2538806 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildAction/summary.properties @@ -0,0 +1,4 @@ +icon.info.tooltip=Click to see all logging messages during recording. +project.title=Overall Project (with difference to reference) +change.title=Changed Lines (with difference to overall project) +indirect.title=Indirect Changes diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageJobAction/floatingBox.jelly b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageJobAction/floatingBox.jelly new file mode 100644 index 000000000..c5ac20a26 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageJobAction/floatingBox.jelly @@ -0,0 +1,4 @@ + + + + diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/column.jelly b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/column.jelly new file mode 100644 index 000000000..0ae6351a9 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/column.jelly @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    ${%Metric}${%Value}
    ${formatter.getDisplayName(value.metric)} + + + + + + ${formatter.format(value)} + + + +
    +
    + +
    + + + + + + + + +
    + + + ${coverageText} + + +
    + +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/columnHeader.jelly b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/columnHeader.jelly new file mode 100644 index 000000000..a7db87cb5 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/columnHeader.jelly @@ -0,0 +1,4 @@ + + + ${it.columnName} + diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/config.jelly b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/config.jelly new file mode 100644 index 000000000..b4c340b41 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/config.jelly @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/config.properties b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/config.properties new file mode 100644 index 000000000..d3d6a98ec --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/config.properties @@ -0,0 +1,3 @@ +title.columnName=Column Name +title.baseline=Baseline +title.metric=Metric diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/help-baseline.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/help-baseline.html new file mode 100644 index 000000000..febd55a89 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/help-baseline.html @@ -0,0 +1,37 @@ +
    + Select the baseline to be used for the code coverage computation of this quality gate. + + The following different baselines are supported: + +
    +
    Overall project - PROJECT
    +
    + Coverage of the whole project. This is an absolute value that might not change much from build to build. +
    +
    Overall project (difference to reference job) - PROJECT_DELTA
    +
    + Difference between the project coverages of the current build and the reference build. Teams can use this delta + value to ensure that the coverage will not decrease. +
    +
    Modified code lines - MODIFIED_LINES
    +
    + Coverage of the modified lines (e.g., within the modified lines of a pull or merge request) + will focus on new or modified code only. +
    +
    Modified code lines (difference to overall project) - MODIFIED_LINES_DELTA
    +
    + Difference between the project coverage and the modified lines coverage of the current build. Teams can use this delta + value to ensure that the coverage of pull requests is better than the whole project coverage. +
    +
    Modified files - MODIFIED_FILES
    +
    + Coverage of the modified files (e.g., within the files that have been touched in a pull or merge request) + will focus on new or modified code only. +
    +
    Modified files (difference to overall project) - MODIFIED_FILES_DELTA
    +
    + Difference between the project coverage and the modified files coverage of the current build. Teams can use this delta + value to ensure that the coverage of pull requests is better than the whole project coverage. +
    +
    +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/help-metric.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/help-metric.html new file mode 100644 index 000000000..6e5cd1375 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumn/help-metric.html @@ -0,0 +1,32 @@ +
    + Select the metric for the coverage results that are shown in this column. + + The following different metrics are supported: + +
    +
    MODULE
    +
    Covered and missed modules (given as percentage)
    +
    PACKAGE
    +
    Covered and missed packages - also used for namespaces or directories (given as percentage)
    +
    FILE
    +
    Covered and missed files (given as percentage)
    +
    CLASS
    +
    Covered and missed classes (given as percentage)
    +
    METHOD
    +
    Covered and missed methods (given as percentage)
    +
    LINE
    +
    Line coverage (given as percentage)
    +
    INSTRUCTION
    +
    Instruction coverage (given as percentage)
    +
    BRANCH
    +
    Branch coverage or decision coverage (given as percentage)
    +
    MUTATION
    +
    Mutation coverage (given as percentage)
    +
    COMPLEXITY
    +
    Cyclomatic complexity (given as absolute number)
    +
    COMPLEXITY_DENSITY
    +
    Cyclomatic complexity density (given as relation between cyclomatic complexity and lines of code)
    +
    LOC
    +
    Lines of code (given as absolute number)
    +
    +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/config.jelly b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/config.jelly new file mode 100644 index 000000000..1de4e6a86 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/config.jelly @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/config.properties b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/config.properties new file mode 100644 index 000000000..d12708170 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/config.properties @@ -0,0 +1,4 @@ +title.threshold=Threshold +title.baseline=Baseline +title.metric=Metric +title.warning=Stage or Build Result diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/help-baseline.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/help-baseline.html new file mode 100644 index 000000000..febd55a89 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/help-baseline.html @@ -0,0 +1,37 @@ +
    + Select the baseline to be used for the code coverage computation of this quality gate. + + The following different baselines are supported: + +
    +
    Overall project - PROJECT
    +
    + Coverage of the whole project. This is an absolute value that might not change much from build to build. +
    +
    Overall project (difference to reference job) - PROJECT_DELTA
    +
    + Difference between the project coverages of the current build and the reference build. Teams can use this delta + value to ensure that the coverage will not decrease. +
    +
    Modified code lines - MODIFIED_LINES
    +
    + Coverage of the modified lines (e.g., within the modified lines of a pull or merge request) + will focus on new or modified code only. +
    +
    Modified code lines (difference to overall project) - MODIFIED_LINES_DELTA
    +
    + Difference between the project coverage and the modified lines coverage of the current build. Teams can use this delta + value to ensure that the coverage of pull requests is better than the whole project coverage. +
    +
    Modified files - MODIFIED_FILES
    +
    + Coverage of the modified files (e.g., within the files that have been touched in a pull or merge request) + will focus on new or modified code only. +
    +
    Modified files (difference to overall project) - MODIFIED_FILES_DELTA
    +
    + Difference between the project coverage and the modified files coverage of the current build. Teams can use this delta + value to ensure that the coverage of pull requests is better than the whole project coverage. +
    +
    +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/help-criticality.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/help-criticality.html new file mode 100644 index 000000000..e2322e414 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/help-criticality.html @@ -0,0 +1,17 @@ +
    + When a quality gate has been missed, this property determines whether the result of the associated coverage stage will + be marked as unstable or failure. + + The following two enum values are possible: + +
    +
    UNSTABLE
    +
    + Set the build status to unstable if the quality gate has been missed. +
    +
    FAILURE
    +
    + Fail the build if the quality gate has been missed. +
    +
    +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/help-metric.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/help-metric.html new file mode 100644 index 000000000..6987cf777 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/help-metric.html @@ -0,0 +1,32 @@ +
    + Select the metric that should be evaluated for this quality gate. + + The following different metrics are supported: + +
    +
    MODULE
    +
    Covered and missed modules (given as percentage)
    +
    PACKAGE
    +
    Covered and missed packages - also used for namespaces or directories (given as percentage)
    +
    FILE
    +
    Covered and missed files (given as percentage)
    +
    CLASS
    +
    Covered and missed classes (given as percentage)
    +
    METHOD
    +
    Covered and missed methods (given as percentage)
    +
    LINE
    +
    Line coverage (given as percentage)
    +
    INSTRUCTION
    +
    Instruction coverage (given as percentage)
    +
    BRANCH
    +
    Branch coverage or decision coverage (given as percentage)
    +
    MUTATION
    +
    Mutation coverage (given as percentage)
    +
    COMPLEXITY
    +
    Cyclomatic complexity (given as absolute number)
    +
    COMPLEXITY_DENSITY
    +
    Cyclomatic complexity density (given as relation between cyclomatic complexity and lines of code)
    +
    LOC
    +
    Lines of code (given as absolute number)
    +
    +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/help-threshold.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/help-threshold.html new file mode 100644 index 000000000..63e34a4bf --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/help-threshold.html @@ -0,0 +1,4 @@ +
    + The threshold defines the minimum or maximum value (depends on the metric) of a coverage metric that is required to + pass or miss the quality gate. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/config.jelly b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/config.jelly new file mode 100644 index 000000000..c59017705 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/config.jelly @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-aggregatingResults.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-aggregatingResults.html new file mode 100644 index 000000000..55e9eef75 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-aggregatingResults.html @@ -0,0 +1,6 @@ +
    + By default, each static analysis result will be recorded as a separate result + that is presented as an individual Jenkins Action with separate UI and dashboard. If you rather prefer aggregation + of the results into a single result (i.e., single Jenkins Action), then activate this check box. You still can + see the distribution of issues grouped by static analysis tool in the UI. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-checksAnnotationScope.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-checksAnnotationScope.html new file mode 100644 index 000000000..737f1ec3f --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-checksAnnotationScope.html @@ -0,0 +1,22 @@ +
    + Select the scope of source code annotations in SCM checks. + + The following different scopes are supported: + +
    +
    SKIP - Skip annotations
    +
    + Do not publish any annotations, just report the coverage report summary. +
    +
    MODIFIED_LINES - Publish annotations for modified lines
    +
    + Publish only annotations for lines that have been changed (with respect to the reference build). + Teams can use these annotations to improve the quality of pull or merge requests. +
    +
    ALL_LINES - Publish annotations for all lines
    +
    + Publish annotations for existing and new code. There might be a lot of annotations depending on + your code coverage. +
    +
    +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-checksName.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-checksName.html new file mode 100644 index 000000000..b1d1ace7d --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-checksName.html @@ -0,0 +1,4 @@ +
    +If provided, and publishing checks enabled, the plugin will use this name when publishing results to corresponding +SCM hosting platforms. If not, the default name will be used. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-enabledForFailure.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-enabledForFailure.html new file mode 100644 index 000000000..10bac9a14 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-enabledForFailure.html @@ -0,0 +1,5 @@ +
    + This toggle determines if recording of code coverage results should be enabled for failed builds + as well. By default, code coverage results are only recorded for stable or unstable builds, but not for failed builds: + code coverage results might be inaccurate if the build failed. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-failOnError.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-failOnError.html new file mode 100644 index 000000000..c5deb289f --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-failOnError.html @@ -0,0 +1,6 @@ +
    + This toggle determines if the coverage plugin should fail the build whenever an error occurred during processing + of the coverage results. Several errors might occur: file pattern matches no files, source files + could not be copied, etc. By default, these errors are logged in a separate view but the build status will + not be altered. If you would rather like to fail the build on such errors, please tick this checkbox. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-healthy.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-healthy.html new file mode 100644 index 000000000..2d3dbbf93 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-healthy.html @@ -0,0 +1,5 @@ +
    + The healthy threshold defines the limit of warnings for a healthy result: A build is considered as 100% healthy + when the number of issues is less than the specified threshold. Values less or equal zero are ignored. + So if you want to have a healthy build (i.e. 100%) only for zero warnings, then set this field to 1. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-id.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-id.html new file mode 100644 index 000000000..51c78732f --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-id.html @@ -0,0 +1,8 @@ +
    + You can override the default ID (i.e., URL) that is used to publish the coverage results in this job. This ID is used as + link to the results, so choose a short and meaningful name. + Allowed elements are characters, digits, dashes and underscores (more precisely, + the ID must match the regular expression \p{Alnum}[\p{Alnum}-_]*). The chosen ID must be + unique in a job, i.e., it must not be already used by other results. + If you leave the ID field empty, then the built-in default ID "coverage" will be used to show the results. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-name.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-name.html new file mode 100644 index 000000000..f098b143a --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-name.html @@ -0,0 +1,5 @@ +
    + You can override the display name of the coverage results. This name is used in details views, trend captions, + hyperlinks, and checks titles. If you leave the name field empty, then the built-in default name + "Coverage Results" will be used. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-qualityGates.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-qualityGates.html new file mode 100644 index 000000000..e42f5a845 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-qualityGates.html @@ -0,0 +1,4 @@ +
    + Add one or more quality gates that will be checked right after the build. You can define for each quality gate which + metric and baseline will be used to select the value that will be compared with the threshold. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-scm.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-scm.html new file mode 100644 index 000000000..96d6a8dd7 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-scm.html @@ -0,0 +1,5 @@ +
    + Specify the key of your repository (substring is sufficient) if you are using multiple SCMs in your job. + When your job is composed of several SCM checkouts (modules, pipeline libraries, etc.) then Jenkins stores + all those repositories in an unsorted way. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-skipPublishingChecks.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-skipPublishingChecks.html new file mode 100644 index 000000000..b33aebe6b --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-skipPublishingChecks.html @@ -0,0 +1,6 @@ +
    + If this option is unchecked, then the plugin automatically publishes a summary of the coverage report to + corresponding SCM hosting platforms. For example, if you are using this feature for a GitHub organization project, + the coverage summary will be published to GitHub through the Checks API. If this operation slows down your build, + or you don't want to publish the summary to SCM platforms, you can use this option to deactivate this feature. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-skipSymbolicLinks.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-skipSymbolicLinks.html new file mode 100644 index 000000000..5624fcfb1 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-skipSymbolicLinks.html @@ -0,0 +1,4 @@ +
    + The coverage plugin will not traverse symbolic links while scanning for report or source code files + when this option is enabled. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-sourceCodeEncoding.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-sourceCodeEncoding.html new file mode 100644 index 000000000..9169dc549 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-sourceCodeEncoding.html @@ -0,0 +1,6 @@ +
    + In order to correctly show all your covered source code files in the detail views, + the plugin must open these files with the correct character encoding (UTF-8, ISO-8859-1, etc.). + If you leave this field empty then the default encoding of the platform will be used. This might work but + is not recommended. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-sourceCodeRetention.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-sourceCodeRetention.html new file mode 100644 index 000000000..ff2a6ab43 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-sourceCodeRetention.html @@ -0,0 +1,19 @@ +
    + Select the strategy that should be used to store the colored source code files. + Storing the affected source code files along with the coverage information (which lines have been covered, which + not) consumes a lot of space on your hard disk for large projects. So if your server has not enough free space + available to store the sources for all builds it might make more sense to store only the coverage results of the + last build. In this case, the plugin will automatically discard old results before the new sources will be stored. + If you do not need the source files at all you can deactivate the storing of source code files. + + The following options are supported: + +
    +
    NEVER
    +
    Never store source code files.
    +
    LAST_BUILD
    +
    Store source code files of the last build, delete older artifacts.
    +
    EVERY_BUILD
    +
    Store source code files for all builds, never delete those files automatically.
    +
    +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-sourceDirectories.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-sourceDirectories.html new file mode 100644 index 000000000..2c5b727a7 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-sourceDirectories.html @@ -0,0 +1,8 @@ +
    + Select additional folders that contain the source code files of the job. + Since the plugin also reads the affected source code files it needs to copy these files from the agent to the + controller. If these files are not part of the workspace (or checked out into a sub folder of the workspace) they + are not automatically found. So you can add one or more source code directories where the plugin tries to find + these files. Note, that due to security restrictions additional paths outside the workspace need to be registered + in Jenkins system configuration before they can be used here. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-tools.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-tools.html new file mode 100644 index 000000000..9f19a23c1 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-tools.html @@ -0,0 +1,4 @@ +
    + Select one of the supported coverage report formats. Most coverage tools support the + output to the Cobertura format, please look into the manual of your coverage tool to see if that format is supported. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/config.jelly b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/config.jelly new file mode 100644 index 000000000..c59017705 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/config.jelly @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-aggregatingResults.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-aggregatingResults.html new file mode 100644 index 000000000..55e9eef75 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-aggregatingResults.html @@ -0,0 +1,6 @@ +
    + By default, each static analysis result will be recorded as a separate result + that is presented as an individual Jenkins Action with separate UI and dashboard. If you rather prefer aggregation + of the results into a single result (i.e., single Jenkins Action), then activate this check box. You still can + see the distribution of issues grouped by static analysis tool in the UI. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-checksAnnotationScope.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-checksAnnotationScope.html new file mode 100644 index 000000000..737f1ec3f --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-checksAnnotationScope.html @@ -0,0 +1,22 @@ +
    + Select the scope of source code annotations in SCM checks. + + The following different scopes are supported: + +
    +
    SKIP - Skip annotations
    +
    + Do not publish any annotations, just report the coverage report summary. +
    +
    MODIFIED_LINES - Publish annotations for modified lines
    +
    + Publish only annotations for lines that have been changed (with respect to the reference build). + Teams can use these annotations to improve the quality of pull or merge requests. +
    +
    ALL_LINES - Publish annotations for all lines
    +
    + Publish annotations for existing and new code. There might be a lot of annotations depending on + your code coverage. +
    +
    +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-checksName.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-checksName.html new file mode 100644 index 000000000..b1d1ace7d --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-checksName.html @@ -0,0 +1,4 @@ +
    +If provided, and publishing checks enabled, the plugin will use this name when publishing results to corresponding +SCM hosting platforms. If not, the default name will be used. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-enabledForFailure.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-enabledForFailure.html new file mode 100644 index 000000000..10bac9a14 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-enabledForFailure.html @@ -0,0 +1,5 @@ +
    + This toggle determines if recording of code coverage results should be enabled for failed builds + as well. By default, code coverage results are only recorded for stable or unstable builds, but not for failed builds: + code coverage results might be inaccurate if the build failed. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-failOnError.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-failOnError.html new file mode 100644 index 000000000..c5deb289f --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-failOnError.html @@ -0,0 +1,6 @@ +
    + This toggle determines if the coverage plugin should fail the build whenever an error occurred during processing + of the coverage results. Several errors might occur: file pattern matches no files, source files + could not be copied, etc. By default, these errors are logged in a separate view but the build status will + not be altered. If you would rather like to fail the build on such errors, please tick this checkbox. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-healthy.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-healthy.html new file mode 100644 index 000000000..2d3dbbf93 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-healthy.html @@ -0,0 +1,5 @@ +
    + The healthy threshold defines the limit of warnings for a healthy result: A build is considered as 100% healthy + when the number of issues is less than the specified threshold. Values less or equal zero are ignored. + So if you want to have a healthy build (i.e. 100%) only for zero warnings, then set this field to 1. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-id.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-id.html new file mode 100644 index 000000000..51c78732f --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-id.html @@ -0,0 +1,8 @@ +
    + You can override the default ID (i.e., URL) that is used to publish the coverage results in this job. This ID is used as + link to the results, so choose a short and meaningful name. + Allowed elements are characters, digits, dashes and underscores (more precisely, + the ID must match the regular expression \p{Alnum}[\p{Alnum}-_]*). The chosen ID must be + unique in a job, i.e., it must not be already used by other results. + If you leave the ID field empty, then the built-in default ID "coverage" will be used to show the results. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-name.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-name.html new file mode 100644 index 000000000..f098b143a --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-name.html @@ -0,0 +1,5 @@ +
    + You can override the display name of the coverage results. This name is used in details views, trend captions, + hyperlinks, and checks titles. If you leave the name field empty, then the built-in default name + "Coverage Results" will be used. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-qualityGates.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-qualityGates.html new file mode 100644 index 000000000..e42f5a845 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-qualityGates.html @@ -0,0 +1,4 @@ +
    + Add one or more quality gates that will be checked right after the build. You can define for each quality gate which + metric and baseline will be used to select the value that will be compared with the threshold. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-scm.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-scm.html new file mode 100644 index 000000000..96d6a8dd7 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-scm.html @@ -0,0 +1,5 @@ +
    + Specify the key of your repository (substring is sufficient) if you are using multiple SCMs in your job. + When your job is composed of several SCM checkouts (modules, pipeline libraries, etc.) then Jenkins stores + all those repositories in an unsorted way. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-skipPublishingChecks.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-skipPublishingChecks.html new file mode 100644 index 000000000..b33aebe6b --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-skipPublishingChecks.html @@ -0,0 +1,6 @@ +
    + If this option is unchecked, then the plugin automatically publishes a summary of the coverage report to + corresponding SCM hosting platforms. For example, if you are using this feature for a GitHub organization project, + the coverage summary will be published to GitHub through the Checks API. If this operation slows down your build, + or you don't want to publish the summary to SCM platforms, you can use this option to deactivate this feature. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-skipSymbolicLinks.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-skipSymbolicLinks.html new file mode 100644 index 000000000..5624fcfb1 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-skipSymbolicLinks.html @@ -0,0 +1,4 @@ +
    + The coverage plugin will not traverse symbolic links while scanning for report or source code files + when this option is enabled. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-sourceCodeEncoding.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-sourceCodeEncoding.html new file mode 100644 index 000000000..9169dc549 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-sourceCodeEncoding.html @@ -0,0 +1,6 @@ +
    + In order to correctly show all your covered source code files in the detail views, + the plugin must open these files with the correct character encoding (UTF-8, ISO-8859-1, etc.). + If you leave this field empty then the default encoding of the platform will be used. This might work but + is not recommended. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-sourceCodeRetention.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-sourceCodeRetention.html new file mode 100644 index 000000000..ff2a6ab43 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-sourceCodeRetention.html @@ -0,0 +1,19 @@ +
    + Select the strategy that should be used to store the colored source code files. + Storing the affected source code files along with the coverage information (which lines have been covered, which + not) consumes a lot of space on your hard disk for large projects. So if your server has not enough free space + available to store the sources for all builds it might make more sense to store only the coverage results of the + last build. In this case, the plugin will automatically discard old results before the new sources will be stored. + If you do not need the source files at all you can deactivate the storing of source code files. + + The following options are supported: + +
    +
    NEVER
    +
    Never store source code files.
    +
    LAST_BUILD
    +
    Store source code files of the last build, delete older artifacts.
    +
    EVERY_BUILD
    +
    Store source code files for all builds, never delete those files automatically.
    +
    +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-sourceDirectories.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-sourceDirectories.html new file mode 100644 index 000000000..2c5b727a7 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-sourceDirectories.html @@ -0,0 +1,8 @@ +
    + Select additional folders that contain the source code files of the job. + Since the plugin also reads the affected source code files it needs to copy these files from the agent to the + controller. If these files are not part of the workspace (or checked out into a sub folder of the workspace) they + are not automatically found. So you can add one or more source code directories where the plugin tries to find + these files. Note, that due to security restrictions additional paths outside the workspace need to be registered + in Jenkins system configuration before they can be used here. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-tools.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-tools.html new file mode 100644 index 000000000..9f19a23c1 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-tools.html @@ -0,0 +1,4 @@ +
    + Select one of the supported coverage report formats. Most coverage tools support the + output to the Cobertura format, please look into the manual of your coverage tool to see if that format is supported. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageTool/config.jelly b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageTool/config.jelly new file mode 100644 index 000000000..e14851b21 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageTool/config.jelly @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageTool/config.properties b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageTool/config.properties new file mode 100644 index 000000000..f56588117 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageTool/config.properties @@ -0,0 +1,4 @@ +parser.title=Coverage Parser +title.pattern=Report File Pattern +description.pattern=Fileset ''includes'' syntax \ + specifying the coverage files to read, such as ''**/jacoco.xml''. diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageTool/help-parser.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageTool/help-parser.html new file mode 100644 index 000000000..34316974e --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageTool/help-parser.html @@ -0,0 +1,4 @@ +
    + Select the ID of the parser that should read and parse your report files - currently, parsers for + Cobertura (id = COBERTURA), JaCoCo (id = JACOCO), and PIT (id = PIT) are supported. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageTool/help-pattern.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageTool/help-pattern.html new file mode 100644 index 000000000..bb1624a0d --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageTool/help-pattern.html @@ -0,0 +1,6 @@ +
    + A pattern is defined by an Ant Fileset ''includes'' + setting that specifies the coverage report files to read. Multiple patterns can be separated by space or comma. + Note that such a pattern is resolved in Jenkins' workspace, so the paths must be relative only. + If no pattern is specified then the default pattern of the coverage tool will be used. +
    diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel/index.jelly b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel/index.jelly new file mode 100644 index 000000000..3c1e3e2dd --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel/index.jelly @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel/index.properties b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel/index.properties new file mode 100644 index 000000000..ef930187f --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel/index.properties @@ -0,0 +1,7 @@ +coverage.details.title=Coverage details by {0} +reason.1=\ + You did not enable storing of source files (see parameter 'sourceFiles'). +reason.2=\ + Code Coverage API plugin did not find the source files. +reason.3=\ + You do not have sufficient permissions to view source files. diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/Messages.properties b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/Messages.properties new file mode 100644 index 000000000..68f0622c7 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/Messages.properties @@ -0,0 +1,45 @@ +Recorder.Name=Record code coverage results + +Parser.Cobertura=Cobertura +Parser.JaCoCo=JaCoCo +Parser.PIT=PIT Mutation Testing + +Coverage.Not.Available=n/a +Coverage.Link.Name=Coverage Report +Coverage.Trend.Name={0} Trend +Coverage.Trend.Default.Name=Code Coverage Trend +Coverage.Title=Coverage of ''{0}'' +Coverage_Column=Coverage +Project_Coverage_Type=Project Coverage +Project_Coverage_Delta_Type=Project Coverage Delta +Change_Coverage_Type=Change Coverage +Change_Coverage_Delta_Type=Change Coverage Delta +Indirect_Coverage_Changes_Type=Indirect Coverage Changes + +QualityGate.Failure=Fail the build if the quality gate has been missed +QualityGate.Unstable=Set the build status to unstable if the quality gate has been missed + +Column.File=File +Column.Package=Package +Column.LineCoverage=Line +Column.DeltaLineCoverage=Line {0} +Column.BranchCoverage=Branch +Column.DeltaBranchCoverage=Branch {0} +Column.MutationCoverage=Mutation +Column.DeltaMutationCoverage=Mutation {0} +Column.LinesOfCode=LOC +Column.Complexity=Complexity +Column.ComplexityDensity=Complexity / LOC + +MessagesViewModel.Title=Code Coverage + +Checks.Summary=Coverage Report Overview +Checks.QualityGates=Quality Gates Summary - {0} +Checks.ProjectOverview=Project Coverage Summary +Checks.Annotation.Title=Missing Coverage +Checks.Annotation.Message.SingleLine=Changed line #L{0} is not covered by tests +Checks.Annotation.Message.MultiLine=Changed lines #L{0} - L{1} are not covered by tests + +ChecksAnnotationScope.Skip=Skip annotations +ChecksAnnotationScope.ModifiedLines=Publish annotations for modified lines +ChecksAnnotationScope.AllLines=Publish annotations for all lines diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/model/Messages.properties b/plugin/src/main/resources/io/jenkins/plugins/coverage/model/Messages.properties index d37256021..5ad49bde4 100644 --- a/plugin/src/main/resources/io/jenkins/plugins/coverage/model/Messages.properties +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/model/Messages.properties @@ -1,7 +1,7 @@ Coverage.Not.Available=n/a Coverage.Link.Name=Coverage Report Coverage.Title=Coverage of ''{0}'' -Coverage_Column=Coverage +Coverage_Column=Coverage [deprecated] Project_Coverage_Type=Project Coverage Project_Coverage_Delta_Type=Project Coverage Delta Change_Coverage_Type=Change Coverage diff --git a/plugin/src/main/webapp/js/charts.js b/plugin/src/main/webapp/js/charts.js index b07330b96..0a552431b 100644 --- a/plugin/src/main/webapp/js/charts.js +++ b/plugin/src/main/webapp/js/charts.js @@ -389,7 +389,7 @@ const CoverageChartGenerator = function ($) { * Event handler to resizes all charts. */ function redrawCharts() { - renderTrendChart(); // rerender since the configuration might have changed + renderTrendChart(); // re-render since the configuration might have changed resizeChartOf('#coverage-overview'); resizeChartOf('#project-line-coverage'); diff --git a/plugin/src/main/webapp/js/view-model.js b/plugin/src/main/webapp/js/view-model.js new file mode 100644 index 000000000..fe4300622 --- /dev/null +++ b/plugin/src/main/webapp/js/view-model.js @@ -0,0 +1,483 @@ +/* global jQuery3, viewProxy, echartsJenkinsApi, bootstrap5 */ + +const CoverageChartGenerator = function ($) { + var selectedTreeNode; + + function printPercentage(value, minimumFractionDigits = 2) { + return Number(value / 100.0).toLocaleString(undefined, {style: 'percent', minimumFractionDigits: minimumFractionDigits}); + } + + const openBuild = function (build) { + viewProxy.getUrlForBuild(build, window.location.href, function (buildUrl) { + if (buildUrl.responseJSON.startsWith('http')) { + window.location.assign(buildUrl.responseJSON); + } + }); + }; + + function getTextColor() { + return getComputedStyle(document.body).getPropertyValue('--text-color') || '#333'; + } + + /** + * Searches for a Jenkins color by a color id. + * + * @param jenkinsColors The available Jenkins colors + * @param id The color id + * @param defaultValue The default value if the id does not exist + * @param alpha The alpha value between [0;255] + * @returns {string} the hex code of the Jenkins color or, if not existent, the default value + */ + function getJenkinsColorById(jenkinsColors, id, defaultValue, alpha) { + const alphaHex = alpha.toString(16); + if (jenkinsColors.has(id)) { + const color = jenkinsColors.get(id); + if (color.match(/^#[a-fA-F0-9]{6}$/) !== null) { + return color + alphaHex; + } + } + return defaultValue + alphaHex; + } + + function createOverview(overview, id, jenkinsColors) { + const missedColor = getJenkinsColorById(jenkinsColors, "--red", "#ff4d65", 120); + const coveredColor = getJenkinsColorById(jenkinsColors, "--green", "#4bdf7c", 120); + + const summaryChartDiv = $('#' + id); + summaryChartDiv.height(overview.metrics.length * 31 + 150 + 'px'); + const summaryChart = echarts.init(summaryChartDiv[0]); + summaryChartDiv[0].echart = summaryChart; + + const textColor = getTextColor(); + + const summaryOption = { + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow' + }, + formatter: function (obj) { + if (Array.isArray(obj)) { + if (obj.length === 2) { + return ['' + obj[0].name + '', + obj[0].seriesName + ': ' + overview.covered[obj[0].dataIndex], + obj[1].seriesName + ': ' + overview.missed[obj[1].dataIndex], + printPercentage(overview.coveredPercentages[obj[0].dataIndex]) + + ].join('
    '); + } + else if (obj.length === 1) { + return '' + obj[0].name + '
    ' + + obj[0].seriesName + ': ' + + (obj[0].seriesName === 'Covered' ? + overview.covered[obj[0].dataIndex] + : overview.missed[obj[0].dataIndex]); + } + } + } + }, + legend: { + data: ['Covered', 'Missed'], + x: 'center', + y: 'top', + textStyle: { + color: textColor + } + }, + grid: { + left: '20', + right: '10', + bottom: '5', + top: '40', + containLabel: true + }, + xAxis: { + type: 'value', + axisLabel: { + formatter: function (value) { + return printPercentage(value, 0); + }, + color: textColor + } + }, + yAxis: [{ + type: 'category', + data: overview.metrics, + axisLine: { + show: false + }, + axisTick: { + show: false + }, + axisLabel: { + color: textColor + } + }, { + type: 'category', + data: overview.coveredPercentages, + position: 'right', + axisLabel: { + formatter: function (value, index) { + return printPercentage(overview.coveredPercentages[index]); + }, + color: textColor + }, + axisLine: { + show: false + }, + axisTick: { + show: false + } + + }], + series: [ + { + name: 'Covered', + type: 'bar', + stack: 'sum', + itemStyle: { + normal: { + color: coveredColor + } + }, + label: { + show: true, + position: 'insideLeft', + color: 'black', + formatter: function (obj) { + return overview.covered[obj.dataIndex]; + } + }, + data: overview.coveredPercentages + }, + { + name: 'Missed', + type: 'bar', + stack: 'sum', + itemStyle: { + normal: { + color: missedColor + } + }, + label: { + show: true, + position: 'insideRight', + color: 'black', + formatter: function (obj) { + return overview.missed[obj.dataIndex]; + } + }, + data: overview.missedPercentages + } + ] + }; + summaryChart.setOption(summaryOption); + summaryChart.resize(); + } + + function createFilesTreeMap(coverageTree, id, coverageMetric) { + function getLevelOption() { + return [ + { + itemStyle: { + borderWidth: 0, + gapWidth: 5, + }, + upperLabel: { + show: false + } + }, + { + itemStyle: { + gapWidth: 3, + } + }, + { + itemStyle: { + gapWidth: 1, + } + }, + { + itemStyle: { + gapWidth: 1, + } + }, + { + itemStyle: { + gapWidth: 1, + } + }, + { + itemStyle: { + gapWidth: 1, + } + }, + { + itemStyle: { + gapWidth: 1, + } + }, + { + itemStyle: { + gapWidth: 1, + } + }, + { + itemStyle: { + gapWidth: 1, + } + }, + { + itemStyle: { + gapWidth: 1, + } + }, + ]; + } + + const treeChartDiv = $('#' + id); + const treeChart = echarts.init(treeChartDiv[0]); + treeChartDiv[0].echart = treeChart; + + const formatUtil = echarts.format; + + const option = { + tooltip: { + formatter: function (info) { + const treePathInfo = info.treePathInfo; + const treePath = []; + for (let i = 2; i < treePathInfo.length; i++) { + treePath.push(treePathInfo[i].name); + } + selectedTreeNode = info.id; + const values = info.value; + const total = values[0]; + const tooltip = values[1]; + + const title = '
    ' + formatUtil.encodeHTML(treePath.join('.')) + '
    '; + if (total === 0) { + return [title, coverageMetric + ': n/a',].join(''); + } + return [title, tooltip].join(''); + } + }, + series: [ + { + name: coverageMetric, + type: 'treemap', + breadcrumb: { + itemStyle: { + color: '#A4A4A4' + }, + emphasis: { + itemStyle: { + opacity: 0.6 + }, + } + }, + width: '100%', + height: '100%', + top: 'top', + label: { + show: true, + formatter: '{b}', + }, + upperLabel: { + show: true, + height: 30, + }, + itemStyle: { + shadowColor: '#000', + shadowBlur: 3, + }, + levels: getLevelOption(), + data: [coverageTree] + } + ] + }; + treeChart.setOption(option); + treeChart.resize(); + } + + this.populateDetailsCharts = function (jenkinsColors) { + /** + * Activate the tab that has been visited the last time. If there is no such tab, highlight the first one. + * If the user selects the tab using an #anchor prefer this tab. + */ + function registerTabEvents() { + /** + * Activates the specified tab. + * + * @param {String} selector - selector of the tab + * @return true if the tab has been selected + */ + function selectTab(selector) { + const detailsTabs = $('#tab-details'); + const selectedTab = detailsTabs.find(selector); + + if (selectedTab.length !== 0) { + const tab = new bootstrap5.Tab(selectedTab[0]); + tab.show(); + + return true; + } + return false + } + + const selectedTabID = 'jenkins-coverage-activeTab'; + const url = document.location.toString(); + if (url.match('#')) { + window.location.hash = ''; + const tabName = url.split('#')[1]; + if (selectTab('a[data-bs-target="#' + tabName + '"]')) { + localStorage.setItem(selectedTabID, '#' + tabName); + } + } + else { + const activeTab = localStorage.getItem(selectedTabID); + if (activeTab) { + selectTab('a[data-bs-target="' + activeTab + '"]'); + } + } + if ($('#tab-details a.active').length === 0) { + selectTab('li:first-child a'); // fallback if all other options fail + } + + $('a[data-bs-toggle="tab"]').on('shown.bs.tab', function (e) { + window.location.hash = e.target.hash; + const activeTab = $(e.target).attr('data-bs-target'); + localStorage.setItem(selectedTabID, activeTab); + redrawCharts(); + }); + } + + /** + * Loads all chart JSON models via AJAX calls from the server and renders the corresponding echarts. + */ + // TODO: maybe it would make sense to render only visible charts + function initializeCharts() { + renderTrendChart(); + + viewProxy.getOverview(function (t) { + createOverview(t.responseObject(), 'coverage-overview', jenkinsColors); + }); + + $('.tree-chart').each(function () { + const id = $(this).attr('id'); + const name = $(this).attr('data-item-name'); + viewProxy.getCoverageTree(id, function (t) { + createFilesTreeMap(t.responseObject(), id, name); + }); + }); + } + + function resizeChartOf(selector) { + $(selector)[0].echart.resize(); + } + + function renderTrendChart() { + const configuration = JSON.stringify(echartsJenkinsApi.readFromLocalStorage('jenkins-echarts-chart-configuration-coverage-history')); + viewProxy.getTrendChart(configuration, function (t) { + echartsJenkinsApi.renderConfigurableZoomableTrendChart('coverage-trend', t.responseJSON, 'chart-configuration-coverage-history', openBuild); + resizeChartOf('#coverage-trend'); + }); + } + + /** + * Event handler to resizes all charts. + */ + function redrawCharts() { + renderTrendChart(); // re-render since the configuration might have changed + + resizeChartOf('#coverage-overview'); + + $('.tree-chart').each(function () { + $(this)[0].echart.resize(); + }); + } + + function registerTrendChartConfiguration() { + const trendConfigurationDialogId = 'chart-configuration-coverage-history'; + $('#' + trendConfigurationDialogId).on('hidden.bs.modal', function () { + redrawCharts(); + }); + } + + /** + * Initializes a selection listener for a datatable which loads the selected source code. + * + * @param {String} tableId The ID of the DataTable + */ + function initializeSourceCodeSelection(tableId) { + const datatable = $('#' + tableId + '-table-inline').DataTable(); + const sourceView = $('#' + tableId + '-source-file'); + const noFileSelectedBanner = $('#' + tableId + '-no-selection'); + const noSourceAvailableBanner = $('#' + tableId + '-no-source'); + + function showNoSelection() { + sourceView.hide(); + noSourceAvailableBanner.hide(); + noFileSelectedBanner.show(); + } + + function showNoSourceCode() { + sourceView.hide(); + noFileSelectedBanner.hide(); + noSourceAvailableBanner.show(); + } + + function showSourceCode() { + noFileSelectedBanner.hide(); + noSourceAvailableBanner.hide(); + sourceView.show(); + } + + showNoSelection(); + datatable.on('select', function (e, dt, type, indexes) { + if (type === 'row') { + showSourceCode(); + sourceView.html('Loading...'); + const rowData = datatable.rows(indexes).data().toArray(); + viewProxy.getSourceCode(rowData[0].fileHash, tableId + '-table', function (t) { + const sourceCode = t.responseObject(); + if (sourceCode === "n/a") { + showNoSourceCode(); + } + else { + sourceView.html(sourceCode); + } + }); + } + else { + showNoSelection(); + } + }) + datatable.on('deselect', function () { + showNoSelection(); + }); + } + + registerTrendChartConfiguration(); + registerTabEvents(); + + initializeCharts(); + + window.addEventListener('resize', function () { + redrawCharts(); + }); + + $(document).ready(function () { + initializeSourceCodeSelection('absolute-coverage'); + initializeSourceCodeSelection('change-coverage'); + initializeSourceCodeSelection('indirect-coverage'); + + $('input[name="changed"]').on('change', function () { + const showChanged = $(this).prop('checked'); + $('table.data-table').each(function () { + const table = $(this).DataTable(); + table.column(1).search(showChanged ? 'true' : '').draw(); + }); + }); + }); + } +}; diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/CoverageChecksPublisherTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/CoverageChecksPublisherTest.java index 32dbbc02a..738682e16 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/CoverageChecksPublisherTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/CoverageChecksPublisherTest.java @@ -25,6 +25,7 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +@SuppressWarnings("unchecked") public class CoverageChecksPublisherTest { private static final String JENKINS_BASE_URL = "http://127.0.0.1:8080"; private static final String COVERAGE_URL_NAME = "coverage"; @@ -59,14 +60,15 @@ public void shouldConstructChecksDetailsWithLineAndMethodCoverage() { .build(); Run build = mock(Run.class); - CoverageResult result = createCoverageResult((float)0.6, (float)0.4); + CoverageResult result = createCoverageResult((float) 0.6, (float) 0.4); when(result.getPreviousResult()).thenReturn(null); when(result.getOwner()).thenReturn(build); when(build.getUrl()).thenReturn(BUILD_LINK); when(build.getPreviousSuccessfulBuild()).thenReturn(null); - assertThat(new CoverageChecksPublisher(createActionWithDefaultHealthReport(result), createJenkins(), CHECKS_NAME) - .extractChecksDetails()) + assertThat( + new CoverageChecksPublisher(createActionWithDefaultHealthReport(result), createJenkins(), CHECKS_NAME) + .extractChecksDetails()) .usingRecursiveComparison() .isEqualTo(expectedDetails); } @@ -82,7 +84,8 @@ public void shouldConstructChecksDetailsWithIncreasedLineCoverageAndConditionalC .withTitle("Line: 50.00% (+10.00% against target branch). " + "Branch: 50.00% (+15.00% against target branch).") .withSummary("* ### [Target branch build](" + JENKINS_BASE_URL + "/" + TARGET_BUILD_LINK + ")\n" - + "* ### [Last successful build](" + JENKINS_BASE_URL + "/" + LAST_SUCCESSFUL_BUILD_LINK + ")\n" + + "* ### [Last successful build](" + JENKINS_BASE_URL + "/" + LAST_SUCCESSFUL_BUILD_LINK + + ")\n" + "## " + HEALTH_REPORT + ".") .withText("## Conditional\n* :white_check_mark: Coverage: 50%\n* :arrow_up: Trend: 20%\n" + "## Line\n* :white_check_mark: Coverage: 50%\n* :arrow_up: Trend: 10%\n") @@ -93,7 +96,8 @@ public void shouldConstructChecksDetailsWithIncreasedLineCoverageAndConditionalC .build()) .build(); - CoverageResult result = createCoverageResult((float)0.4, (float)0.3, (float)0.5, (float)0.5, TARGET_BUILD_LINK, + CoverageResult result = createCoverageResult((float) 0.4, (float) 0.3, (float) 0.5, (float) 0.5, + TARGET_BUILD_LINK, +10, +15); CoverageAction action = new CoverageAction(result); @@ -101,8 +105,9 @@ public void shouldConstructChecksDetailsWithIncreasedLineCoverageAndConditionalC when(localizable.toString()).thenReturn(HEALTH_REPORT); action.setHealthReport(new HealthReport(100, localizable)); - assertThat(new CoverageChecksPublisher(createActionWithDefaultHealthReport(result), createJenkins(), CHECKS_NAME) - .extractChecksDetails()) + assertThat( + new CoverageChecksPublisher(createActionWithDefaultHealthReport(result), createJenkins(), CHECKS_NAME) + .extractChecksDetails()) .usingRecursiveComparison() .isEqualTo(expectedDetails); } @@ -118,7 +123,8 @@ public void shouldConstructChecksDetailsWithDecreasedLineCoverageAndConditionalC .withTitle("Line: 50.00% (-10.00% against target branch). " + "Branch: 50.00% (-15.00% against target branch).") .withSummary("* ### [Target branch build](" + JENKINS_BASE_URL + "/" + TARGET_BUILD_LINK + ")\n" - + "* ### [Last successful build](" + JENKINS_BASE_URL + "/" + LAST_SUCCESSFUL_BUILD_LINK + ")\n" + + "* ### [Last successful build](" + JENKINS_BASE_URL + "/" + LAST_SUCCESSFUL_BUILD_LINK + + ")\n" + "## " + HEALTH_REPORT + ".") .withText("## Conditional\n* :white_check_mark: Coverage: 50%\n* :arrow_down: Trend: 15%\n" + "## Line\n* :white_check_mark: Coverage: 50%\n* :arrow_down: Trend: 10%\n") @@ -129,7 +135,8 @@ public void shouldConstructChecksDetailsWithDecreasedLineCoverageAndConditionalC .build()) .build(); - CoverageResult result = createCoverageResult((float)0.6, (float)0.7, (float)0.5, (float)0.5, TARGET_BUILD_LINK, + CoverageResult result = createCoverageResult((float) 0.6, (float) 0.7, (float) 0.5, (float) 0.5, + TARGET_BUILD_LINK, -10, -15); CoverageAction action = new CoverageAction(result); @@ -137,8 +144,9 @@ public void shouldConstructChecksDetailsWithDecreasedLineCoverageAndConditionalC when(localizable.toString()).thenReturn(HEALTH_REPORT); action.setHealthReport(new HealthReport(100, localizable)); - assertThat(new CoverageChecksPublisher(createActionWithDefaultHealthReport(result), createJenkins(), CHECKS_NAME) - .extractChecksDetails()) + assertThat( + new CoverageChecksPublisher(createActionWithDefaultHealthReport(result), createJenkins(), CHECKS_NAME) + .extractChecksDetails()) .usingRecursiveComparison() .isEqualTo(expectedDetails); } @@ -154,7 +162,8 @@ public void shouldConstructChecksDetailsWithUnchangedLineAndConditionalCoverage( .withTitle("Line: 60.00% (+0.00% against target branch). " + "Branch: 40.00% (+0.00% against target branch).") .withSummary("* ### [Target branch build](" + JENKINS_BASE_URL + "/" + TARGET_BUILD_LINK + ")\n" - + "* ### [Last successful build](" + JENKINS_BASE_URL + "/" + LAST_SUCCESSFUL_BUILD_LINK + ")\n" + + "* ### [Last successful build](" + JENKINS_BASE_URL + "/" + LAST_SUCCESSFUL_BUILD_LINK + + ")\n" + "## " + HEALTH_REPORT + ".") .withText("||Conditional|Line|\n" + "|:-:|:-:|:-:|\n" + @@ -163,7 +172,8 @@ public void shouldConstructChecksDetailsWithUnchangedLineAndConditionalCoverage( .build()) .build(); - CoverageResult result = createCoverageResult((float)0.6, (float)0.4, (float)0.6, (float)0.4, TARGET_BUILD_LINK, 0, + CoverageResult result = createCoverageResult((float) 0.6, (float) 0.4, (float) 0.6, (float) 0.4, + TARGET_BUILD_LINK, 0, 0); CoverageAction action = new CoverageAction(result); @@ -171,8 +181,9 @@ public void shouldConstructChecksDetailsWithUnchangedLineAndConditionalCoverage( when(localizable.toString()).thenReturn(HEALTH_REPORT); action.setHealthReport(new HealthReport(100, localizable)); - assertThat(new CoverageChecksPublisher(createActionWithDefaultHealthReport(result), createJenkins(), CHECKS_NAME) - .extractChecksDetails()) + assertThat( + new CoverageChecksPublisher(createActionWithDefaultHealthReport(result), createJenkins(), CHECKS_NAME) + .extractChecksDetails()) .usingRecursiveComparison() .isEqualTo(expectedDetails); } @@ -187,8 +198,10 @@ public void shouldUseLastSuccessfulBuildForLineCoverageIfNoTargetBranchIsCompare .withOutput(new ChecksOutputBuilder() .withTitle("Line: 60.00% (+10.00% against last successful build). " + "Branch: 40.00% (+10.00% against last successful build).") - .withSummary("* ### [Last successful build](" + JENKINS_BASE_URL + "/" + LAST_SUCCESSFUL_BUILD_LINK + ")\n" - + "## " + HEALTH_REPORT + ".") + .withSummary( + "* ### [Last successful build](" + JENKINS_BASE_URL + "/" + LAST_SUCCESSFUL_BUILD_LINK + + ")\n" + + "## " + HEALTH_REPORT + ".") .withText("## Conditional\n* :white_check_mark: Coverage: 40%\n* :arrow_up: Trend: 10%\n" + "## Line\n* :white_check_mark: Coverage: 60%\n* :arrow_up: Trend: 10%\n") .withText("||Conditional|Line|\n" + @@ -198,15 +211,16 @@ public void shouldUseLastSuccessfulBuildForLineCoverageIfNoTargetBranchIsCompare .build()) .build(); - CoverageResult result = createCoverageResult((float)0.5, (float)0.3, (float)0.6, (float)0.4, null, 0, -10); + CoverageResult result = createCoverageResult((float) 0.5, (float) 0.3, (float) 0.6, (float) 0.4, null, 0, -10); CoverageAction action = new CoverageAction(result); Localizable localizable = mock(Localizable.class); when(localizable.toString()).thenReturn(HEALTH_REPORT); action.setHealthReport(new HealthReport(100, localizable)); - assertThat(new CoverageChecksPublisher(createActionWithDefaultHealthReport(result), createJenkins(), CHECKS_NAME) - .extractChecksDetails()) + assertThat( + new CoverageChecksPublisher(createActionWithDefaultHealthReport(result), createJenkins(), CHECKS_NAME) + .extractChecksDetails()) .usingRecursiveComparison() .isEqualTo(expectedDetails); } @@ -232,7 +246,7 @@ public void shouldPublishFailedCheck() { .build(); Run build = mock(Run.class); - CoverageResult result = createCoverageResult((float)0.6, (float)0.4); + CoverageResult result = createCoverageResult((float) 0.6, (float) 0.4); when(result.getPreviousResult()).thenReturn(null); when(result.getOwner()).thenReturn(build); when(build.getUrl()).thenReturn(BUILD_LINK); @@ -291,8 +305,9 @@ public void shouldReportNoLineOrBranchCoverageInChecksTitle() { when(localizable.toString()).thenReturn(HEALTH_REPORT); action.setHealthReport(new HealthReport(100, localizable)); - assertThat(new CoverageChecksPublisher(createActionWithDefaultHealthReport(result), createJenkins(), CHECKS_NAME) - .extractChecksDetails().getOutput()) + assertThat( + new CoverageChecksPublisher(createActionWithDefaultHealthReport(result), createJenkins(), CHECKS_NAME) + .extractChecksDetails().getOutput()) .isPresent() .get() .hasFieldOrPropertyWithValue("title", Optional.of("No line or branch coverage has been computed.")); @@ -345,9 +360,11 @@ private CoverageAction createActionWithDefaultHealthReport(final CoverageResult private JenkinsFacade createJenkins() { JenkinsFacade jenkinsFacade = mock(JenkinsFacade.class); - when(jenkinsFacade.getAbsoluteUrl(BUILD_LINK, COVERAGE_URL_NAME)).thenReturn(JENKINS_BASE_URL + "/" + BUILD_LINK + COVERAGE_URL_NAME); + when(jenkinsFacade.getAbsoluteUrl(BUILD_LINK, COVERAGE_URL_NAME)).thenReturn( + JENKINS_BASE_URL + "/" + BUILD_LINK + COVERAGE_URL_NAME); when(jenkinsFacade.getAbsoluteUrl(TARGET_BUILD_LINK)).thenReturn(JENKINS_BASE_URL + "/" + TARGET_BUILD_LINK); - when(jenkinsFacade.getAbsoluteUrl(LAST_SUCCESSFUL_BUILD_LINK)).thenReturn(JENKINS_BASE_URL + "/" + LAST_SUCCESSFUL_BUILD_LINK); + when(jenkinsFacade.getAbsoluteUrl(LAST_SUCCESSFUL_BUILD_LINK)).thenReturn( + JENKINS_BASE_URL + "/" + LAST_SUCCESSFUL_BUILD_LINK); return jenkinsFacade; } diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/CoverageScriptedPipelineScriptBuilder.java b/plugin/src/test/java/io/jenkins/plugins/coverage/CoverageScriptedPipelineScriptBuilder.java index 922740064..d29b4df1f 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/CoverageScriptedPipelineScriptBuilder.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/CoverageScriptedPipelineScriptBuilder.java @@ -1,20 +1,22 @@ package io.jenkins.plugins.coverage; +import java.util.LinkedList; +import java.util.List; + +import org.jenkinsci.Symbol; import hudson.model.Descriptor; import hudson.model.Slave; + import io.jenkins.plugins.coverage.adapter.CoverageAdapter; import io.jenkins.plugins.coverage.adapter.CoverageReportAdapter; import io.jenkins.plugins.coverage.detector.AntPathReportDetector; import io.jenkins.plugins.coverage.threshold.Threshold; -import org.jenkinsci.Symbol; - -import java.util.LinkedList; -import java.util.List; +@SuppressWarnings("unchecked") public class CoverageScriptedPipelineScriptBuilder { - private List adapters = new LinkedList<>(); + private final List adapters = new LinkedList<>(); - private List globalThresholds = new LinkedList<>(); + private final List globalThresholds = new LinkedList<>(); private boolean failUnhealthy; private boolean failUnstable; @@ -32,17 +34,17 @@ public static CoverageScriptedPipelineScriptBuilder builder() { return new CoverageScriptedPipelineScriptBuilder(); } - public CoverageScriptedPipelineScriptBuilder addAdapter(CoverageAdapter adapter) { + public CoverageScriptedPipelineScriptBuilder addAdapter(final CoverageAdapter adapter) { adapters.add(adapter); return this; } - public CoverageScriptedPipelineScriptBuilder addGlobalThreshold(Threshold threshold) { + public CoverageScriptedPipelineScriptBuilder addGlobalThreshold(final Threshold threshold) { globalThresholds.add(threshold); return this; } - public CoverageScriptedPipelineScriptBuilder onAgent(Slave slave) { + public CoverageScriptedPipelineScriptBuilder onAgent(final Slave slave) { this.agent = slave; return this; } @@ -87,7 +89,7 @@ public String build() { return sb.toString(); } - private String generateSnippetForReportAdapter(CoverageReportAdapter adapter) { + private String generateSnippetForReportAdapter(final CoverageReportAdapter adapter) { Descriptor d = adapter.getDescriptor(); Class c = d.getClass(); @@ -107,7 +109,7 @@ private String generateSnippetForReportAdapter(CoverageReportAdapter adapter) { return ""; } - private String generateSnippetForThresholds(List thresholds) { + private String generateSnippetForThresholds(final List thresholds) { StringBuilder sb = new StringBuilder(); sb.append("["); @@ -127,27 +129,27 @@ private String generateSnippetForThresholds(List thresholds) { } - public CoverageScriptedPipelineScriptBuilder setFailUnhealthy(boolean failUnhealthy) { + public CoverageScriptedPipelineScriptBuilder setFailUnhealthy(final boolean failUnhealthy) { this.failUnhealthy = failUnhealthy; return this; } - public CoverageScriptedPipelineScriptBuilder setFailUnstable(boolean failUnstable) { + public CoverageScriptedPipelineScriptBuilder setFailUnstable(final boolean failUnstable) { this.failUnstable = failUnstable; return this; } - public CoverageScriptedPipelineScriptBuilder setFailNoReports(boolean failNoReports) { + public CoverageScriptedPipelineScriptBuilder setFailNoReports(final boolean failNoReports) { this.failNoReports = failNoReports; return this; } - public CoverageScriptedPipelineScriptBuilder setApplyThresholdRecursively(boolean applyThresholdRecursively) { + public CoverageScriptedPipelineScriptBuilder setApplyThresholdRecursively(final boolean applyThresholdRecursively) { this.applyThresholdRecursively = applyThresholdRecursively; return this; } - public CoverageScriptedPipelineScriptBuilder setEnableSourceFileResolver(boolean enableSourceFileResolver) { + public CoverageScriptedPipelineScriptBuilder setEnableSourceFileResolver(final boolean enableSourceFileResolver) { this.enableSourceFileResolver = enableSourceFileResolver; return this; } diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/AbstractCoverageITest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/AbstractCoverageITest.java new file mode 100644 index 000000000..0409b958e --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/AbstractCoverageITest.java @@ -0,0 +1,91 @@ +package io.jenkins.plugins.coverage.metrics; + +import java.io.IOException; +import java.util.List; +import java.util.function.Consumer; + +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import hudson.model.FreeStyleProject; + +import io.jenkins.plugins.coverage.metrics.steps.CoverageRecorder; +import io.jenkins.plugins.coverage.metrics.steps.CoverageTool.Parser; +import io.jenkins.plugins.util.IntegrationTestWithJenkinsPerSuite; + +/** + * Provides some helper methods to create different job types that will record code coverage results. + * + * @author Ullrich Hafner + */ +public abstract class AbstractCoverageITest extends IntegrationTestWithJenkinsPerSuite { + protected FreeStyleProject createFreestyleJob(final Parser parser, final String... fileNames) { + return createFreestyleJob(parser, i -> { }, fileNames); + } + + protected FreeStyleProject createFreestyleJob(final Parser parser, + final Consumer configuration, final String... fileNames) { + FreeStyleProject project = createFreeStyleProjectWithWorkspaceFiles(fileNames); + + addCoverageRecorder(project, parser, "**/*xml", configuration); + + return project; + } + + protected void addCoverageRecorder(final FreeStyleProject project, + final Parser parser, final String pattern) { + addCoverageRecorder(project, parser, pattern, i -> { }); + } + + void addCoverageRecorder(final FreeStyleProject project, + final Parser parser, final String pattern, final Consumer configuration) { + CoverageRecorder recorder = new CoverageRecorder(); + + var tool = new io.jenkins.plugins.coverage.metrics.steps.CoverageTool(); + tool.setParser(parser); + tool.setPattern(pattern); + recorder.setTools(List.of(tool)); + + configuration.accept(recorder); + + try { + project.getPublishersList().remove(CoverageRecorder.class); + } + catch (IOException exception) { + // ignore and continue + } + project.getPublishersList().add(recorder); + } + + protected WorkflowJob createPipeline(final Parser parser, final String... fileNames) { + WorkflowJob job = createPipelineWithWorkspaceFiles(fileNames); + + setPipelineScript(job, + "recordCoverage tools: [[parser: '" + parser.name() + "', pattern: '**/*xml']]"); + + return job; + } + + protected void setPipelineScript(final WorkflowJob job, final String recorderSnippet) { + job.setDefinition(new CpsFlowDefinition( + "node {\n" + + recorderSnippet + "\n" + + " }\n", true)); + } + + protected WorkflowJob createDeclarativePipeline(final Parser parser, final String... fileNames) { + WorkflowJob job = createPipelineWithWorkspaceFiles(fileNames); + + job.setDefinition(new CpsFlowDefinition("pipeline {\n" + + " agent any\n" + + " stages {\n" + + " stage('Test') {\n" + + " steps {\n" + + " recordCoverage(\n" + + " tools: [[parser: '" + parser.name() + "', pattern: '**/*xml']]" + + " )}\n" + + " }\n" + + " }\n" + + "}", true)); + return job; + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/AbstractCoverageTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/AbstractCoverageTest.java new file mode 100644 index 000000000..0f1fd9f5c --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/AbstractCoverageTest.java @@ -0,0 +1,91 @@ +package io.jenkins.plugins.coverage.metrics; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; +import java.util.NavigableMap; +import java.util.TreeMap; + +import org.apache.commons.lang3.math.Fraction; +import org.junitpioneer.jupiter.DefaultLocale; + +import edu.hm.hafner.coverage.Coverage.CoverageBuilder; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.coverage.Value; +import edu.hm.hafner.coverage.parser.JacocoParser; +import edu.hm.hafner.util.FilteredLog; +import edu.hm.hafner.util.ResourceTest; +import edu.hm.hafner.util.SecureXmlParserFactory.ParsingException; + +import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics; + +/** + * Base class for coverage tests that work on real coverage reports. + * + * @author Ullrich Hafner + */ +@DefaultLocale("en") +@SuppressWarnings("checkstyle:JavadocVariable") +public abstract class AbstractCoverageTest extends ResourceTest { + public static final String JACOCO_ANALYSIS_MODEL_FILE = "jacoco-analysis-model.xml"; + public static final int JACOCO_ANALYSIS_MODEL_COVERED = 5531; + public static final int JACOCO_ANALYSIS_MODEL_MISSED = 267; + public static final int JACOCO_ANALYSIS_MODEL_TOTAL + = JACOCO_ANALYSIS_MODEL_COVERED + JACOCO_ANALYSIS_MODEL_MISSED; + + public static final String JACOCO_CODING_STYLE_FILE = "jacoco-codingstyle.xml"; + public static final int JACOCO_CODING_STYLE_COVERED = 294; + public static final int JACOCO_CODING_STYLE_MISSED = 29; + public static final int JACOCO_CODING_STYLE_TOTAL + = JACOCO_CODING_STYLE_COVERED + JACOCO_CODING_STYLE_MISSED; + private final FilteredLog log = new FilteredLog("Errors"); + + /** + * Reads and parses a JaCoCo coverage report. + * + * @param fileName + * the name of the coverage report file + * + * @return the parsed coverage tree + */ + protected Node readJacocoResult(final String fileName) { + try { + var node = new JacocoParser().parse(Files.newBufferedReader(getResourceAsFile(fileName)), log); + node.splitPackages(); + return node; + } + catch (ParsingException | IOException exception) { + throw new AssertionError(exception); + } + } + + protected FilteredLog getLog() { + return log; + } + + /** + * Creates coverage statistics that can be used in test cases. + * + * @return the coverage statistics + */ + public static CoverageStatistics createStatistics() { + return new CoverageStatistics(fillValues(), fillDeltas(), + fillValues(), fillDeltas(), + fillValues(), fillDeltas()); + } + + private static List fillValues() { + var builder = new CoverageBuilder(); + return List.of(builder.setMetric(Metric.FILE).setCovered(3).setMissed(1).build(), + builder.setMetric(Metric.LINE).setCovered(2).setMissed(2).build(), + builder.setMetric(Metric.BRANCH).setCovered(9).setMissed(1).build()); + } + + private static NavigableMap fillDeltas() { + final NavigableMap deltaMapping = new TreeMap<>(); + deltaMapping.put(Metric.FILE, Fraction.getFraction(-10, 100)); + deltaMapping.put(Metric.LINE, Fraction.getFraction(5, 100)); + return deltaMapping; + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/PackageArchitectureTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/PackageArchitectureTest.java new file mode 100644 index 000000000..6985eefbb --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/PackageArchitectureTest.java @@ -0,0 +1,28 @@ +package io.jenkins.plugins.coverage.metrics; + +import java.net.URL; + +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.Configuration.*; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.*; + +/** + * Checks the package architecture of this plugin. + * + * @author Ullrich Hafner + */ +@SuppressWarnings("hideutilityclassconstructor") +@AnalyzeClasses(packages = "io.jenkins.plugins.coverage.metrics", importOptions = DoNotIncludeTests.class) +class PackageArchitectureTest { + private static final URL PACKAGE_DESIGN = PackageArchitectureTest.class.getResource("/design.puml"); + + @ArchTest + static final ArchRule ADHERES_TO_PACKAGE_DESIGN + = classes().should(adhereToPlantUmlDiagram(PACKAGE_DESIGN, + consideringOnlyDependenciesInAnyPackage("io.jenkins.plugins.coverage.metrics.."))); +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/PluginArchitectureTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/PluginArchitectureTest.java new file mode 100644 index 000000000..8134b2f95 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/PluginArchitectureTest.java @@ -0,0 +1,60 @@ +package io.jenkins.plugins.coverage.metrics; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import edu.hm.hafner.util.ArchitectureRules; + +import io.jenkins.plugins.util.PluginArchitectureRules; + +/** + * Checks several architecture rules for the Jenkins plugins. + * + * @author Ullrich Hafner + */ +@SuppressWarnings("hideutilityclassconstructor") +@AnalyzeClasses(packages = "io.jenkins.plugins.coverage.metrics") +class PluginArchitectureTest { + @ArchTest + static final ArchRule NO_EXCEPTIONS_WITH_NO_ARG_CONSTRUCTOR = ArchitectureRules.NO_EXCEPTIONS_WITH_NO_ARG_CONSTRUCTOR; + + @ArchTest + static final ArchRule NO_PUBLIC_TEST_CLASSES = ArchitectureRules.NO_PUBLIC_TEST_CLASSES; + + @ArchTest + static final ArchRule NO_PUBLIC_TEST_METHODS = ArchitectureRules.ONLY_PACKAGE_PRIVATE_TEST_METHODS; + + @ArchTest + static final ArchRule NO_TEST_API_CALLED = ArchitectureRules.NO_TEST_API_CALLED; + + @ArchTest + static final ArchRule NO_FORBIDDEN_ANNOTATION_USED = ArchitectureRules.NO_FORBIDDEN_ANNOTATION_USED; + + @ArchTest + static final ArchRule NO_FORBIDDEN_CLASSES_CALLED = ArchitectureRules.NO_FORBIDDEN_CLASSES_CALLED; + + @ArchTest + static final ArchRule ONLY_PACKAGE_PRIVATE_ARCHITECTURE_TESTS = ArchitectureRules.ONLY_PACKAGE_PRIVATE_ARCHITECTURE_TESTS; + + @ArchTest + static final ArchRule NO_JENKINS_INSTANCE_CALL = PluginArchitectureRules.NO_JENKINS_INSTANCE_CALL; + + @ArchTest + static final ArchRule NO_FORBIDDEN_PACKAGE_ACCESSED = PluginArchitectureRules.NO_FORBIDDEN_PACKAGE_ACCESSED; + + @ArchTest + static final ArchRule AJAX_PROXY_METHOD_MUST_BE_IN_PUBLIC_CLASS = PluginArchitectureRules.AJAX_PROXY_METHOD_MUST_BE_IN_PUBLIC_CLASS; + + @ArchTest + static final ArchRule DATA_BOUND_CONSTRUCTOR_MUST_BE_IN_PUBLIC_CLASS = PluginArchitectureRules.DATA_BOUND_CONSTRUCTOR_MUST_BE_IN_PUBLIC_CLASS; + + @ArchTest + static final ArchRule DATA_BOUND_SETTER_MUST_BE_IN_PUBLIC_CLASS = PluginArchitectureRules.DATA_BOUND_SETTER_MUST_BE_IN_PUBLIC_CLASS; + + @ArchTest + static final ArchRule USE_POST_FOR_VALIDATION_END_POINTS = PluginArchitectureRules.USE_POST_FOR_VALIDATION_END_POINTS; + + @ArchTest + static final ArchRule USE_POST_FOR_LIST_MODELS_RULE = PluginArchitectureRules.USE_POST_FOR_LIST_AND_COMBOBOX_FILL; +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/charts/CoverageSeriesBuilderTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/charts/CoverageSeriesBuilderTest.java new file mode 100644 index 000000000..526624c32 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/charts/CoverageSeriesBuilderTest.java @@ -0,0 +1,107 @@ +package io.jenkins.plugins.coverage.metrics.charts; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import edu.hm.hafner.coverage.Coverage; +import edu.hm.hafner.coverage.Coverage.CoverageBuilder; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.echarts.Build; +import edu.hm.hafner.echarts.BuildResult; +import edu.hm.hafner.echarts.ChartModelConfiguration; +import edu.hm.hafner.echarts.ChartModelConfiguration.AxisType; +import edu.hm.hafner.echarts.line.LinesChartModel; +import edu.hm.hafner.echarts.line.LinesDataSet; +import edu.hm.hafner.util.VisibleForTesting; + +import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Tests the class {@link CoverageSeriesBuilder}. + * + * @author Ullrich Hafner + */ +class CoverageSeriesBuilderTest { + @Test + void shouldHaveEmptyDataSetForEmptyIterator() { + CoverageSeriesBuilder builder = new CoverageSeriesBuilder(); + + LinesDataSet model = builder.createDataSet(createConfiguration(), new ArrayList<>()); + + assertThat(model.getDomainAxisSize()).isEqualTo(0); + assertThat(model.getDataSetIds()).isEmpty(); + } + + @Test + void shouldCreateChart() { + CoverageTrendChart trendChart = new CoverageTrendChart(); + + BuildResult smallLineCoverage = createResult(1, + new CoverageBuilder().setMetric(Metric.LINE).setCovered(1).setMissed(1).build(), + new CoverageBuilder().setMetric(Metric.BRANCH).setCovered(3).setMissed(1).build()); + + LinesChartModel lineCoverage = trendChart.create(Collections.singletonList(smallLineCoverage), + createConfiguration()); + verifySeriesDetails(lineCoverage); + + BuildResult smallBranchCoverage = createResult(1, + new CoverageBuilder().setMetric(Metric.LINE).setCovered(3).setMissed(1).build(), + new CoverageBuilder().setMetric(Metric.BRANCH).setCovered(1).setMissed(1).build()); + + LinesChartModel branchCoverage = trendChart.create(Collections.singletonList(smallBranchCoverage), + createConfiguration()); + verifySeriesDetails(branchCoverage); + } + + @VisibleForTesting + private BuildResult createResult(final int buildNumber, + final Coverage lineCoverage, final Coverage branchCoverage) { + var statistics = new CoverageStatistics( + List.of(lineCoverage, branchCoverage), Collections.emptyNavigableMap(), + Collections.emptyList(), Collections.emptyNavigableMap(), + Collections.emptyList(), Collections.emptyNavigableMap()); + Build build = new Build(buildNumber); + + return new BuildResult<>(build, statistics); + } + + private void verifySeriesDetails(final LinesChartModel lineCoverage) { + assertThat(lineCoverage.getBuildNumbers()).containsExactly(1); + assertThat(lineCoverage.getSeries()).hasSize(2); + assertThat(lineCoverage.getRangeMax()).isEqualTo(100.0); + assertThat(lineCoverage.getRangeMin()).isEqualTo(50.0); + } + + @Test + void shouldHaveTwoValuesForSingleBuild() { + CoverageSeriesBuilder builder = new CoverageSeriesBuilder(); + + BuildResult singleResult = createResult(1, + new CoverageBuilder().setMetric(Metric.LINE).setCovered(1).setMissed(1).build(), + new CoverageBuilder().setMetric(Metric.BRANCH).setCovered(3).setMissed(1).build()); + + LinesDataSet dataSet = builder.createDataSet(createConfiguration(), Collections.singletonList(singleResult)); + + assertThat(dataSet.getDomainAxisSize()).isEqualTo(1); + assertThat(dataSet.getDomainAxisLabels()).containsExactly("#1"); + + assertThat(dataSet.getDataSetIds()).containsExactlyInAnyOrder( + CoverageSeriesBuilder.LINE_COVERAGE, + CoverageSeriesBuilder.BRANCH_COVERAGE); + + assertThat(dataSet.getSeries(CoverageSeriesBuilder.LINE_COVERAGE)).containsExactly(50.0); + assertThat(dataSet.getSeries(CoverageSeriesBuilder.BRANCH_COVERAGE)).containsExactly(75.0); + } + + private ChartModelConfiguration createConfiguration() { + ChartModelConfiguration configuration = mock(ChartModelConfiguration.class); + when(configuration.getAxisType()).thenReturn(AxisType.BUILD); + return configuration; + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/charts/TreeMapNodeConverterTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/charts/TreeMapNodeConverterTest.java new file mode 100644 index 000000000..3be8940fe --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/charts/TreeMapNodeConverterTest.java @@ -0,0 +1,90 @@ +package io.jenkins.plugins.coverage.metrics.charts; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.echarts.LabeledTreeMapNode; + +import io.jenkins.plugins.coverage.metrics.AbstractCoverageTest; +import io.jenkins.plugins.coverage.metrics.color.ColorProvider; +import io.jenkins.plugins.coverage.metrics.color.ColorProviderFactory; +import io.jenkins.plugins.coverage.metrics.color.CoverageLevel; + +import static org.assertj.core.api.Assertions.*; + +/** + * Tests the class {@link TreeMapNodeConverter}. + * + * @author Ullrich Hafner + */ +class TreeMapNodeConverterTest extends AbstractCoverageTest { + + private static final ColorProvider COLOR_PROVIDER = ColorProviderFactory.createDefaultColorProvider(); + + @Test + void shouldConvertCodingStyleToTree() { + Node tree = readJacocoResult(JACOCO_CODING_STYLE_FILE); + + LabeledTreeMapNode root = new TreeMapNodeConverter().toTreeChartModel(tree, Metric.LINE, COLOR_PROVIDER); + assertThat(root.getName()).isEqualTo("Java coding style"); + + var overallCoverage = String.valueOf(JACOCO_CODING_STYLE_TOTAL); + assertThat(root.getValue()).contains(overallCoverage); + + var overallCoveragePercentage = 100.0 * JACOCO_CODING_STYLE_COVERED / JACOCO_CODING_STYLE_TOTAL; + assertThat(root.getItemStyle().getColor()).isEqualTo(getNodeColorAsRGBHex(overallCoveragePercentage)); + + assertThat(root.getChildren()).hasSize(1).element(0).satisfies( + node -> { + assertThat(node.getName()).isEqualTo("edu.hm.hafner.util"); + assertThat(node.getValue()).contains(overallCoverage); + assertThat(root.getItemStyle().getColor()).isEqualTo(getNodeColorAsRGBHex(overallCoveragePercentage)); + } + ); + } + + @Test + void shouldReadBranchCoverage() { + Node tree = readJacocoResult(JACOCO_ANALYSIS_MODEL_FILE); + + LabeledTreeMapNode root = new TreeMapNodeConverter().toTreeChartModel(tree, Metric.BRANCH, COLOR_PROVIDER); + + var nodes = aggregateChildren(root); + nodes.stream().filter(node -> node.getName().endsWith(".java")).forEach(node -> { + assertThat(node.getValue()).hasSize(2); + }); + } + + private List aggregateChildren(final LabeledTreeMapNode root) { + var children = root.getChildren(); + var subChildren = children.stream() + .map(this::aggregateChildren) + .flatMap(List::stream) + .collect(Collectors.toList()); + subChildren.addAll(children); + return subChildren; + } + + @Override + protected Node readJacocoResult(final String fileName) { + return super.readJacocoResult("../steps/" + fileName); + } + + /** + * Gets the matching fill color for the coverage percentage. + * + * @param coveredPercentage + * The coverage percentage + * + * @return the fill color as a hex string + */ + private String getNodeColorAsRGBHex(final Double coveredPercentage) { + return CoverageLevel + .getDisplayColorsOfCoverageLevel(coveredPercentage, COLOR_PROVIDER) + .getFillColorAsRGBHex(); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/ColorProviderFactoryTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/ColorProviderFactoryTest.java new file mode 100644 index 000000000..296372caa --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/ColorProviderFactoryTest.java @@ -0,0 +1,76 @@ +package io.jenkins.plugins.coverage.metrics.color; + +import java.awt.*; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static io.jenkins.plugins.coverage.metrics.color.CoverageColorJenkinsId.*; +import static org.assertj.core.api.Assertions.*; + +/** + * Test class for {@link ColorProviderFactory}. + * + * @author Florian Orendi + */ +class ColorProviderFactoryTest { + + private static final String TEST_COLOR_HEX = "#ffffff"; + private static final Color TEST_COLOR = Color.decode(TEST_COLOR_HEX); + + @Test + void shouldCreateDefaultColorProvider() { + ColorProvider colorProvider = ColorProviderFactory.createDefaultColorProvider(); + for (CoverageColorPalette color : CoverageColorPalette.values()) { + assertThat(colorProvider.containsColorId(color.getColorId())).isTrue(); + } + } + + @Test + void shouldCreateColorProviderWithJenkinsColors() { + Map colorMapping = createColorMapping(); + ColorProvider colorProvider = ColorProviderFactory.createColorProvider(colorMapping); + + for (CoverageColorPalette color : CoverageColorPalette.values()) { + assertThat(colorProvider.containsColorId(color.getColorId())).isTrue(); + if (!color.getColorId().equals(ColorId.BLACK) && !color.getColorId() + .equals(ColorId.WHITE)) { // skip set default color + assertThat(colorProvider.getDisplayColorsOf(color.getColorId())) + .satisfies(displayColor -> assertThat(displayColor.getFillColor()).isEqualTo(TEST_COLOR)); + } + } + } + + @Test + void shouldCreateDefaultColorProviderWithMissingJenkinsColorIds() { + Map colorMapping = createColorMapping(); + colorMapping.remove("--green"); + ColorProvider colorProvider = ColorProviderFactory.createColorProvider(colorMapping); + for (CoverageColorPalette color : CoverageColorPalette.values()) { + assertThat(colorProvider.containsColorId(color.getColorId())).isTrue(); + } + } + + @Test + void shouldCreateDefaultColorProviderWithoutHexColors() { + Map colorMapping = createColorMapping(); + colorMapping.replace("--green", "hsl(135deg, 75%, 55%)"); + ColorProvider colorProvider = ColorProviderFactory.createColorProvider(colorMapping); + for (CoverageColorPalette color : CoverageColorPalette.values()) { + assertThat(colorProvider.containsColorId(color.getColorId())).isTrue(); + } + } + + /** + * Creates a color mapping between the {@link CoverageColorJenkinsId jenkins color id} and the corresponding color + * hex code. + * + * @return the created mapping + */ + private Map createColorMapping() { + Map colorMapping = new HashMap<>(); + getAll().forEach(id -> colorMapping.put(id, TEST_COLOR_HEX)); + return colorMapping; + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/ColorProviderTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/ColorProviderTest.java new file mode 100644 index 000000000..7b00ddcd8 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/ColorProviderTest.java @@ -0,0 +1,99 @@ +package io.jenkins.plugins.coverage.metrics.color; + +import java.awt.*; + +import org.junit.jupiter.api.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; + +import static io.jenkins.plugins.coverage.metrics.color.ColorProvider.*; +import static org.assertj.core.api.Assertions.*; + +/** + * Test class for {@link ColorProvider}. + * + * @author Florian Orendi + */ +class ColorProviderTest { + + @Test + void shouldGetDisplayColorsOfId() { + ColorProvider colorProvider = createDefaultColorProvider(); + DisplayColors displayColors = colorProvider.getDisplayColorsOf(ColorId.EXCELLENT); + + assertThat(displayColors.getFillColor()).isEqualTo(CoverageColorPalette.GREEN.getFillColor()); + assertThat(displayColors.getLineColor()).isEqualTo(CoverageColorPalette.GREEN.getLineColor()); + assertThat(colorProvider.getDisplayColorsOf(null)).isEqualTo(DEFAULT_COLOR); + } + + @Test + void shouldCheckForExistentColorId() { + ColorProvider colorProvider = createDefaultColorProvider(); + assertThat(colorProvider.containsColorId(ColorId.EXCELLENT)).isTrue(); + assertThat(colorProvider.containsColorId(null)).isFalse(); + } + + @Test + void shouldGetBlendedDisplayColors() { + ColorProvider colorProvider = createDefaultColorProvider(); + + assertThat(colorProvider.getBlendedDisplayColors(1, 1, null, ColorId.EXCELLENT)) + .isEqualTo(DEFAULT_COLOR); + assertThat(colorProvider.getBlendedDisplayColors(1, 1, ColorId.EXCELLENT, null)) + .isEqualTo(DEFAULT_COLOR); + assertThat(colorProvider.getBlendedDisplayColors(1, 1, null, null)) + .isEqualTo(DEFAULT_COLOR); + assertThat(colorProvider.getBlendedDisplayColors(2, 1, ColorId.BLACK, ColorId.WHITE)) + .isEqualTo(new DisplayColors(new Color(0xFFFFFF), new Color(0x555555))); + assertThat(colorProvider.getBlendedDisplayColors(1, 2, ColorId.BLACK, ColorId.WHITE)) + .isEqualTo(new DisplayColors(new Color(0x000000), new Color(0xAAAAAA))); + } + + @Test + void shouldBlendColors() { + assertThat(blendColors(Color.yellow, Color.blue)).isEqualTo(new Color(127, 127, 127)); + } + + @Test + void shouldBlendWeightedColors() { + Color first = new Color(200, 200, 200); + Color second = new Color(0, 0, 0); + double firstWeight = 1; + double secondWeight = 3; + + assertThat(blendWeightedColors(first, second, firstWeight, secondWeight)) + .isEqualTo(new Color(50, 50, 50)); + assertThatThrownBy(() -> blendWeightedColors(first, second, -1, 1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(BLEND_COLOR_ERROR_MESSAGE); + assertThatThrownBy(() -> blendWeightedColors(first, second, 1, -1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(BLEND_COLOR_ERROR_MESSAGE); + } + + @Test + void shouldProvideColorAsHex() { + assertThat(colorAsRGBHex(Color.black)).isEqualTo("#000000"); + assertThat(colorAsRGBAHex(Color.black, 255)).isEqualTo("#000000FF"); + } + + @Test + void shouldProvideColorAsHexForDisplayColors() { + DisplayColors displayColors = new DisplayColors(Color.black, Color.white); + + assertThat(displayColors.getFillColorAsRGBAHex(255)).isEqualTo("#FFFFFFFF"); + assertThat(displayColors.getLineColorAsRGBHex()).isEqualTo("#000000"); + assertThat(displayColors.getFillColorAsRGBHex()).isEqualTo("#FFFFFF"); + } + + @Test + void shouldObeyEqualsContractForDisplayColors() { + EqualsVerifier.forClass(DisplayColors.class) + .usingGetClass() + .verify(); + } + + private ColorProvider createDefaultColorProvider() { + return ColorProviderFactory.createDefaultColorProvider(); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/CoverageChangeLevelTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/CoverageChangeLevelTest.java new file mode 100644 index 000000000..078431b9c --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/CoverageChangeLevelTest.java @@ -0,0 +1,48 @@ +package io.jenkins.plugins.coverage.metrics.color; + +import java.awt.*; + +import org.junit.jupiter.api.Test; + +import io.jenkins.plugins.coverage.metrics.color.ColorProvider.DisplayColors; + +import static org.assertj.core.api.Assertions.*; + +/** + * Test class for {@link CoverageChangeLevel}. + * + * @author Florian Orendi + */ +class CoverageChangeLevelTest { + + private static final ColorProvider COLOR_PROVIDER = ColorProviderFactory.createDefaultColorProvider(); + + @Test + void shouldHaveWorkingGetters() { + CoverageChangeLevel coverageChangeLevel = CoverageChangeLevel.INCREASE_2; + assertThat(coverageChangeLevel.getChange()).isEqualTo(2.0); + assertThat(coverageChangeLevel.getColorizationId()).isEqualTo(ColorId.VERY_GOOD); + } + + @Test + void shouldGetDisplayColorsOfCoveragePercentage() { + Color blendedLineColor = COLOR_PROVIDER.getDisplayColorsOf(ColorId.BLACK).getFillColor(); + Color blendedColorIncreased = ColorProvider.blendColors( + COLOR_PROVIDER.getDisplayColorsOf(CoverageChangeLevel.INCREASE_2.getColorizationId()).getFillColor(), + COLOR_PROVIDER.getDisplayColorsOf(CoverageChangeLevel.EQUALS.getColorizationId()).getFillColor()); + Color blendedColorDecreased = ColorProvider.blendColors( + COLOR_PROVIDER.getDisplayColorsOf(CoverageChangeLevel.DECREASE_2.getColorizationId()).getFillColor(), + COLOR_PROVIDER.getDisplayColorsOf(CoverageChangeLevel.EQUALS.getColorizationId()).getFillColor()); + + assertThat(CoverageChangeLevel.getDisplayColorsOfCoverageChange(1.0, COLOR_PROVIDER)) + .isEqualTo(new DisplayColors(blendedLineColor, blendedColorIncreased)); + assertThat(CoverageChangeLevel.getDisplayColorsOfCoverageChange(-1.0, COLOR_PROVIDER)) + .isEqualTo(new DisplayColors(blendedLineColor, blendedColorDecreased)); + assertThat(CoverageChangeLevel.getDisplayColorsOfCoverageChange(7.0, COLOR_PROVIDER)) + .isEqualTo(COLOR_PROVIDER.getDisplayColorsOf(ColorId.EXCELLENT)); + assertThat(CoverageChangeLevel.getDisplayColorsOfCoverageChange(-2.0, COLOR_PROVIDER)) + .isEqualTo(COLOR_PROVIDER.getDisplayColorsOf(ColorId.INADEQUATE)); + assertThat(CoverageChangeLevel.getDisplayColorsOfCoverageChange(-110.0, COLOR_PROVIDER)) + .isEqualTo(COLOR_PROVIDER.getDisplayColorsOf(ColorId.WHITE)); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/CoverageChangeTendencyTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/CoverageChangeTendencyTest.java new file mode 100644 index 000000000..120e352ab --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/CoverageChangeTendencyTest.java @@ -0,0 +1,32 @@ +package io.jenkins.plugins.coverage.metrics.color; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +/** + * Test class for {@link CoverageChangeTendency}. + * + * @author Florian Orendi + */ +class CoverageChangeTendencyTest { + + private static final ColorProvider COLOR_PROVIDER = ColorProviderFactory.createDefaultColorProvider(); + + @Test + void shouldGetDisplayColorsForTendency() { + assertThat(CoverageChangeTendency.getDisplayColorsForTendency(1.0, COLOR_PROVIDER)) + .isEqualTo(COLOR_PROVIDER.getDisplayColorsOf(ColorId.EXCELLENT)); + assertThat(CoverageChangeTendency.getDisplayColorsForTendency(0.0, COLOR_PROVIDER)) + .isEqualTo(COLOR_PROVIDER.getDisplayColorsOf(ColorId.AVERAGE)); + assertThat(CoverageChangeTendency.getDisplayColorsForTendency(-1.0, COLOR_PROVIDER)) + .isEqualTo(COLOR_PROVIDER.getDisplayColorsOf(ColorId.INSUFFICIENT)); + assertThat(CoverageChangeTendency.getDisplayColorsForTendency(null, COLOR_PROVIDER)) + .isEqualTo(COLOR_PROVIDER.getDisplayColorsOf(ColorId.WHITE)); + } + + @Test + void shouldGetColorizationId() { + assertThat(CoverageChangeTendency.INCREASED.getColorizationId()).isEqualTo(ColorId.EXCELLENT); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/CoverageColorJenkinsIdTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/CoverageColorJenkinsIdTest.java new file mode 100644 index 000000000..f6dd4667d --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/CoverageColorJenkinsIdTest.java @@ -0,0 +1,24 @@ +package io.jenkins.plugins.coverage.metrics.color; + +import org.junit.jupiter.api.Test; + +import static io.jenkins.plugins.coverage.metrics.color.CoverageColorJenkinsId.*; +import static org.assertj.core.api.Assertions.*; + +/** + * Test class for {@link CoverageColorJenkinsId}. + * + * @author Florian Orendi + */ +class CoverageColorJenkinsIdTest { + + @Test + void shouldGetAllIds() { + assertThat(getAll().size()).isEqualTo(values().length); + } + + @Test + void shouldGetColorId() { + assertThat(GREEN.getJenkinsColorId()).isEqualTo("--green"); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/CoverageLevelTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/CoverageLevelTest.java new file mode 100644 index 000000000..7623763bd --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/color/CoverageLevelTest.java @@ -0,0 +1,43 @@ +package io.jenkins.plugins.coverage.metrics.color; + +import java.awt.*; + +import org.junit.jupiter.api.Test; + +import io.jenkins.plugins.coverage.metrics.color.ColorProvider.DisplayColors; + +import static org.assertj.core.api.Assertions.*; + +/** + * Test class for {@link CoverageLevel}. + * + * @author Florian Orendi + */ +class CoverageLevelTest { + + private static final ColorProvider COLOR_PROVIDER = ColorProviderFactory.createDefaultColorProvider(); + + @Test + void shouldHaveWorkingGetters() { + CoverageLevel coverageLevel = CoverageLevel.LVL_0; + assertThat(coverageLevel.getLevel()).isEqualTo(0.0); + assertThat(coverageLevel.getColorizationId()).isEqualTo(ColorId.INSUFFICIENT); + } + + @Test + void shouldGetDisplayColorsOfCoveragePercentage() { + Color blendedColor = ColorProvider.blendColors( + COLOR_PROVIDER.getDisplayColorsOf(CoverageLevel.LVL_60.getColorizationId()).getFillColor(), + COLOR_PROVIDER.getDisplayColorsOf(CoverageLevel.LVL_70.getColorizationId()).getFillColor()); + + assertThat(CoverageLevel.getDisplayColorsOfCoverageLevel(65.0, COLOR_PROVIDER)) + .isEqualTo(new DisplayColors(COLOR_PROVIDER.getDisplayColorsOf(ColorId.BLACK).getFillColor(), + blendedColor)); + assertThat(CoverageLevel.getDisplayColorsOfCoverageLevel(96.0, COLOR_PROVIDER)) + .isEqualTo(COLOR_PROVIDER.getDisplayColorsOf(ColorId.EXCELLENT)); + assertThat(CoverageLevel.getDisplayColorsOfCoverageLevel(50.0, COLOR_PROVIDER)) + .isEqualTo(COLOR_PROVIDER.getDisplayColorsOf(ColorId.VERY_BAD)); + assertThat(CoverageLevel.getDisplayColorsOfCoverageLevel(-2.0, COLOR_PROVIDER)) + .isEqualTo(COLOR_PROVIDER.getDisplayColorsOf(ColorId.WHITE)); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/DockerAgentSourceCodeITest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/DockerAgentSourceCodeITest.java new file mode 100644 index 000000000..8ddaa81e5 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/DockerAgentSourceCodeITest.java @@ -0,0 +1,63 @@ +package io.jenkins.plugins.coverage.metrics.source; + +import java.io.IOException; + +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.MountableFile; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import hudson.model.Node; + +/** + * Verifies if source code copying and rendering works on Docker agents. + * + * @author Ullrich Hafner + */ +@Testcontainers(disabledWithoutDocker = true) +class DockerAgentSourceCodeITest extends SourceCodeITest { + private static final String SOURCES_IN_DOCKER_PATH = "/tmp/coverage"; + private static final String ACU_COBOL_PARSER_CONTAINER_PATH = SOURCES_IN_DOCKER_PATH + "/" + ACU_COBOL_PARSER_PACKAGE_PATH + ACU_COBOL_PARSER_FILE_NAME; + private static final String PATH_UTIL_CONTAINER_PATH = SOURCES_IN_DOCKER_PATH + "/" + PATH_UTIL_PACKAGE_PATH + PATH_UTIL_FILE_NAME; + + private static final String RESOURCES = "io/jenkins/plugins/coverage/metrics/source/"; + @Container @SuppressFBWarnings("BC") + private static final AgentContainer AGENT_CONTAINER = new AgentContainer() + .withCopyFileToContainer( + MountableFile.forClasspathResource(RESOURCES + ACU_COBOL_PARSER_SOURCE_FILE), + ACU_COBOL_PARSER_CONTAINER_PATH) + .withCopyFileToContainer( + MountableFile.forClasspathResource(RESOURCES + PATH_UTIL_SOURCE_FILE), + PATH_UTIL_CONTAINER_PATH); + + @Override + protected Node crateCoverageAgent() { + try { + Node agent = createDockerAgent(AGENT_CONTAINER); + agent.setLabelString(AGENT_LABEL); + return agent; + } + catch (IOException exception) { + throw new AssertionError(exception); + } + } + + @Override + protected String createExternalFolder() { + return SOURCES_IN_DOCKER_PATH; + } + + @Override + protected void copySourceFileToAgent(final String sourceDirectory, final Node localAgent, final WorkflowJob job) { + if (!sourceDirectory.startsWith(SOURCES_IN_DOCKER_PATH)) { + copySingleFileToAgentWorkspace(localAgent, job, + ACU_COBOL_PARSER_SOURCE_FILE, + createDestinationPath(sourceDirectory, ACU_COBOL_PARSER_PACKAGE_PATH, ACU_COBOL_PARSER_FILE_NAME)); + copySingleFileToAgentWorkspace(localAgent, job, + PATH_UTIL_SOURCE_FILE, + createDestinationPath(sourceDirectory, PATH_UTIL_PACKAGE_PATH, PATH_UTIL_FILE_NAME)); + } + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/LocalAgentSourceCodeITest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/LocalAgentSourceCodeITest.java new file mode 100644 index 000000000..5bb813dc9 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/LocalAgentSourceCodeITest.java @@ -0,0 +1,53 @@ +package io.jenkins.plugins.coverage.metrics.source; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +import edu.hm.hafner.util.PathUtil; + +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import hudson.model.Node; +import hudson.slaves.DumbSlave; + +/** + * Verifies if source code copying and rendering works on local dummy agents (see {@link DumbSlave}). + * + * @author Ullrich Hafner + */ +class LocalAgentSourceCodeITest extends SourceCodeITest { + private static final PathUtil PATH_UTIL = new PathUtil(); + + @Override + protected Node crateCoverageAgent() { + return createAgent(AGENT_LABEL); + } + + @Override + protected String createExternalFolder() throws IOException { + Path tempDirectory = Files.createTempDirectory("coverage"); + + createFile(tempDirectory, + ACU_COBOL_PARSER_PACKAGE_PATH, ACU_COBOL_PARSER_SOURCE_FILE, ACU_COBOL_PARSER_FILE_NAME); + createFile(tempDirectory, + PATH_UTIL_PACKAGE_PATH, PATH_UTIL_SOURCE_FILE, PATH_UTIL_FILE_NAME); + + return PATH_UTIL.getAbsolutePath(tempDirectory); + } + + private void createFile(final Path tempDirectory, + final String packagePath, final String sourceName, final String fileName) throws IOException { + Path sourceCodeDirectory = tempDirectory.resolve(packagePath); + Files.createDirectories(sourceCodeDirectory); + Files.copy(getResourceAsFile(sourceName), sourceCodeDirectory.resolve(fileName), StandardCopyOption.REPLACE_EXISTING); + } + + @Override + protected void copySourceFileToAgent(final String sourceDirectory, final Node localAgent, final WorkflowJob job) { + copySingleFileToAgentWorkspace(localAgent, job, ACU_COBOL_PARSER_SOURCE_FILE, createDestinationPath(sourceDirectory, + ACU_COBOL_PARSER_PACKAGE_PATH, ACU_COBOL_PARSER_FILE_NAME)); + copySingleFileToAgentWorkspace(localAgent, job, PATH_UTIL_SOURCE_FILE, createDestinationPath(sourceDirectory, + PATH_UTIL_PACKAGE_PATH, PATH_UTIL_FILE_NAME)); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeFacadeTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeFacadeTest.java new file mode 100644 index 000000000..b757075fe --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeFacadeTest.java @@ -0,0 +1,90 @@ +package io.jenkins.plugins.coverage.metrics.source; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.List; + +import org.jsoup.Jsoup; +import org.jsoup.parser.Parser; +import org.junit.jupiter.api.Test; + +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.util.ResourceTest; + +import static org.assertj.core.api.Assertions.*; + +/** + * Test class for {@link SourceCodeFacade}. + * + * @author Florian Orendi + */ +class SourceCodeFacadeTest extends ResourceTest { + private static final String WHOLE_SOURCE_CODE = "SourcecodeTest.html"; + private static final String MODIFIED_LINES_COVERAGE_SOURCE_CODE = "SourcecodeTestCC.html"; + private static final String INDIRECT_COVERAGE_SOURCE_CODE = "SourcecodeTestICC.html"; + + @Test + void shouldCalculateSourcecodeForModifiedLinesCoverage() throws IOException { + SourceCodeFacade sourceCodeFacade = createSourceCodeFacade(); + String originalHtml = readHtml(WHOLE_SOURCE_CODE); + FileNode node = createFileCoverageNode(); + + String requiredHtml = Jsoup.parse(readHtml(MODIFIED_LINES_COVERAGE_SOURCE_CODE), Parser.xmlParser()).html(); + + String modifiedLinesCoverageHtml = + sourceCodeFacade.calculateModifiedLinesCoverageSourceCode(originalHtml, node); + assertThat(modifiedLinesCoverageHtml).isEqualTo(requiredHtml); + } + + @Test + void shouldCalculateSourcecodeForIndirectCoverageChanges() throws IOException { + SourceCodeFacade sourceCodeFacade = createSourceCodeFacade(); + String originalHtml = readHtml(WHOLE_SOURCE_CODE); + FileNode node = createFileCoverageNode(); + + String requiredHtml = Jsoup.parse(readHtml(INDIRECT_COVERAGE_SOURCE_CODE), Parser.xmlParser()).html(); + + String modifiedLinesCoverageHtml = sourceCodeFacade.calculateIndirectCoverageChangesSourceCode(originalHtml, node); + assertThat(modifiedLinesCoverageHtml).isEqualTo(requiredHtml); + } + + /** + * Creates an instance of {@link SourceCodeFacade}. + * + * @return the created instance + */ + private SourceCodeFacade createSourceCodeFacade() { + return new SourceCodeFacade(); + } + + private FileNode createFileCoverageNode() { + FileNode file = new FileNode(""); + List lines = Arrays.asList(10, 11, 12, 16, 17, 18, 19); + for (Integer line : lines) { + file.addModifiedLines(line); + } + file.addIndirectCoverageChange(6, -1); + file.addIndirectCoverageChange(7, -1); + file.addIndirectCoverageChange(14, 1); + file.addIndirectCoverageChange(15, 1); + for (int i = 1; i <= 25; i++) { + file.addCounters(i, 1, 0); + } + return file; + } + + /** + * Reads a sourcecode HTML file for testing. + * + * @param name + * The name of the file + * + * @return the file content + * @throws IOException + * if reading failed + */ + private String readHtml(final String name) throws IOException { + return new String(Files.readAllBytes(getResourceAsFile(name))); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeITest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeITest.java new file mode 100644 index 000000000..9c7e4b876 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeITest.java @@ -0,0 +1,201 @@ +package io.jenkins.plugins.coverage.metrics.source; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import edu.hm.hafner.coverage.Coverage.CoverageBuilder; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Node; + +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import hudson.model.Run; + +import io.jenkins.plugins.coverage.metrics.AbstractCoverageITest; +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.coverage.metrics.steps.CoverageBuildAction; +import io.jenkins.plugins.prism.PermittedSourceCodeDirectory; +import io.jenkins.plugins.prism.PrismConfiguration; +import io.jenkins.plugins.prism.SourceCodeRetention; + +import static io.jenkins.plugins.prism.SourceCodeRetention.*; +import static org.assertj.core.api.Assertions.*; + +/** + * Verifies if source code copying and rendering works on agents. + * + * @author Ullrich Hafner + */ +abstract class SourceCodeITest extends AbstractCoverageITest { + private static final String ACU_COBOL_PARSER = "public class AcuCobolParser extends LookaheadParser {"; + private static final String PATH_UTIL = "public class PathUtil {"; + private static final String NO_SOURCE_CODE = "n/a"; + static final String ACU_COBOL_PARSER_FILE_NAME = "AcuCobolParser.java"; + static final String ACU_COBOL_PARSER_SOURCE_FILE = ACU_COBOL_PARSER_FILE_NAME + ".txt"; + static final String ACU_COBOL_PARSER_PACKAGE_PATH = "edu/hm/hafner/analysis/parser/"; + private static final String ACU_COBOL_PARSER_SOURCE_FILE_PATH = ACU_COBOL_PARSER_PACKAGE_PATH + ACU_COBOL_PARSER_FILE_NAME; + private static final String ACU_COBOL_PARSER_COVERAGE_REPORT = "jacoco-acu-cobol-parser.xml"; + private static final String PATH_UTIL_COVERAGE_REPORT = "jacoco-path-util.xml"; + static final String PATH_UTIL_FILE_NAME = "PathUtil.java"; + static final String PATH_UTIL_SOURCE_FILE = PATH_UTIL_FILE_NAME + ".txt"; + static final String PATH_UTIL_PACKAGE_PATH = "edu/hm/hafner/util/"; + private static final String PATH_UTIL_SOURCE_FILE_PATH = PATH_UTIL_PACKAGE_PATH + PATH_UTIL_FILE_NAME; + static final String AGENT_LABEL = "coverage-agent"; + + /** Verifies that the plugin reads source code from the workspace root. */ + @Test + void coveragePluginPipelineWithSourceCode() throws IOException { + runCoverageWithSourceCode(""); + } + + /** Verifies that the plugin reads source code in subdirectories of the workspace. */ + @Test + void coveragePluginPipelineWithSourceCodeInSubdirectory() throws IOException { + runCoverageWithSourceCode("sub-dir"); + } + + /** Verifies that the plugin reads source code in external but approved directories. */ + @Test + void coveragePluginPipelineWithSourceCodeInPermittedDirectory() throws IOException { + String directory = createExternalFolder(); + PrismConfiguration.getInstance().setSourceDirectories(List.of(new PermittedSourceCodeDirectory(directory))); + + Run externalDirectory = runCoverageWithSourceCode(directory); + assertThat(getConsoleLog(externalDirectory)) + .contains("Searching for source code files in:", "-> " + directory); + } + + /** Verifies that the plugin refuses source code in directories that are not approved in Jenkins' configuration. */ + @Test + void coveragePluginPipelineNotRegisteredSourceCodeDirectory() throws IOException { + var localAgent = crateCoverageAgent(); + String sourceDirectory = createExternalFolder(); + + WorkflowJob job = createPipeline(); + copySourceFileToAgent("ignore/", localAgent, job); + copyReports(localAgent, job); + + job.setDefinition(createPipelineWithSourceCode(EVERY_BUILD, sourceDirectory)); + + Run firstBuild = buildSuccessfully(job); + + assertThat(getConsoleLog(firstBuild)) + .contains("-> finished painting (0 files have been painted, 1 files failed)") + .contains(String.format( + "[-ERROR-] Removing source directory '%s' - it has not been approved in Jenkins' global configuration.", + sourceDirectory)); + + verifySourceCodeInBuild(firstBuild, NO_SOURCE_CODE, NO_SOURCE_CODE); // should be still available + localAgent.setLabelString(""); + } + + private Run runCoverageWithSourceCode(final String sourceDirectory) + throws IOException { + var localAgent = crateCoverageAgent(); + + WorkflowJob job = createPipeline(); + copyReports(localAgent, job); + copySourceFileToAgent(sourceDirectory, localAgent, job); + + // get the temporary directory - used by unit tests - to verify its content + File temporaryDirectory = new File(System.getProperty("java.io.tmpdir")); + assertThat(temporaryDirectory.exists()).isTrue(); + assertThat(temporaryDirectory.isDirectory()).isTrue(); + File[] temporaryFiles = temporaryDirectory.listFiles(); + + job.setDefinition(createPipelineWithSourceCode(EVERY_BUILD, sourceDirectory)); + Run firstBuild = buildSuccessfully(job); + assertThat(getConsoleLog(firstBuild)) + .contains("-> finished painting successfully"); + verifySourceCodeInBuild(firstBuild, ACU_COBOL_PARSER, PATH_UTIL); + + Run secondBuild = buildSuccessfully(job); + verifySourceCodeInBuild(secondBuild, ACU_COBOL_PARSER, PATH_UTIL); + verifySourceCodeInBuild(firstBuild, ACU_COBOL_PARSER, PATH_UTIL); // should be still available + + job.setDefinition(createPipelineWithSourceCode(LAST_BUILD, sourceDirectory)); + Run thirdBuild = buildSuccessfully(job); + verifySourceCodeInBuild(thirdBuild, ACU_COBOL_PARSER, PATH_UTIL); + verifySourceCodeInBuild(firstBuild, NO_SOURCE_CODE, NO_SOURCE_CODE); // should be still available + verifySourceCodeInBuild(secondBuild, NO_SOURCE_CODE, NO_SOURCE_CODE); // should be still available + + job.setDefinition(createPipelineWithSourceCode(NEVER, sourceDirectory)); + Run lastBuild = buildSuccessfully(job); + verifySourceCodeInBuild(lastBuild, NO_SOURCE_CODE, NO_SOURCE_CODE); + verifySourceCodeInBuild(firstBuild, NO_SOURCE_CODE, NO_SOURCE_CODE); // should be still available + verifySourceCodeInBuild(secondBuild, NO_SOURCE_CODE, NO_SOURCE_CODE); // should be still available + verifySourceCodeInBuild(thirdBuild, NO_SOURCE_CODE, NO_SOURCE_CODE); // should be still available + + assertThat(temporaryDirectory.listFiles()).isEqualTo(temporaryFiles); + + localAgent.setLabelString(""); + return firstBuild; + } + + private void copyReports(final hudson.model.Node localAgent, final WorkflowJob job) { + copySingleFileToAgentWorkspace(localAgent, job, ACU_COBOL_PARSER_COVERAGE_REPORT, ACU_COBOL_PARSER_COVERAGE_REPORT); + copySingleFileToAgentWorkspace(localAgent, job, PATH_UTIL_COVERAGE_REPORT, PATH_UTIL_COVERAGE_REPORT); + } + + private CpsFlowDefinition createPipelineWithSourceCode(final SourceCodeRetention sourceCodeRetention, + final String sourceDirectory) { + return new CpsFlowDefinition("node ('coverage-agent') {" + + " recordCoverage " + + " tools: [[parser: 'JACOCO', pattern: '" + ACU_COBOL_PARSER_COVERAGE_REPORT + "']], \n" + + " sourceCodeRetention: '" + sourceCodeRetention.name() + "', \n" + + " sourceCodeEncoding: 'UTF-8', \n" + + " sourceDirectories: [[path: '" + sourceDirectory + "']]\n" + + " recordCoverage id:'path', " + + " tools: [[parser: 'JACOCO', pattern: '" + PATH_UTIL_COVERAGE_REPORT + "']], \n" + + " sourceCodeRetention: '" + sourceCodeRetention.name() + "', \n" + + " sourceCodeEncoding: 'UTF-8', \n" + + " sourceDirectories: [[path: '" + sourceDirectory + "']]" + + "}", true); + } + + private void verifySourceCodeInBuild(final Run build, final String acuCobolParserSourceCodeSnippet, + final String pathUtilSourceCodeSnippet) { + System.out.println(getConsoleLog(build)); + + List actions = build.getActions(CoverageBuildAction.class); + var builder = new CoverageBuilder().setMetric(Metric.LINE).setMissed(0); + assertThat(actions).hasSize(2).satisfiesExactly( + action -> { + assertThat(action.getAllValues(Baseline.PROJECT)).contains(builder.setCovered(8).build()); + Optional fileNode = action.getResult().find(Metric.FILE, ACU_COBOL_PARSER_SOURCE_FILE_PATH); + assertThat(fileNode).isNotEmpty() + .hasValueSatisfying(node -> assertThat(node.getPath()).isEqualTo( + ACU_COBOL_PARSER_SOURCE_FILE_PATH)); + assertThat(action.getTarget().getSourceCode(String.valueOf(ACU_COBOL_PARSER_SOURCE_FILE_PATH.hashCode()), "coverage-table")) + .contains(acuCobolParserSourceCodeSnippet); + }, + action -> { + assertThat(action.getAllValues(Baseline.PROJECT)).contains(builder.setCovered(43).build()); + Optional fileNode = action.getResult().find(Metric.FILE, PATH_UTIL_SOURCE_FILE_PATH); + assertThat(fileNode).isNotEmpty() + .hasValueSatisfying(node -> assertThat(node.getPath()).isEqualTo( + PATH_UTIL_SOURCE_FILE_PATH)); + assertThat(action.getTarget().getSourceCode(String.valueOf(PATH_UTIL_SOURCE_FILE_PATH.hashCode()), "coverage-table")) + .contains(pathUtilSourceCodeSnippet); + }); + } + + String createDestinationPath(final String sourceDirectory, final String packagePath, final String fileName) { + if (sourceDirectory.isEmpty()) { + return packagePath + fileName; + } + else { + return sourceDirectory + "/" + packagePath + fileName; + } + } + + abstract hudson.model.Node crateCoverageAgent(); + + abstract String createExternalFolder() throws IOException; + + abstract void copySourceFileToAgent(String sourceDirectory, hudson.model.Node localAgent, WorkflowJob job); +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CodeDeltaCalculatorTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CodeDeltaCalculatorTest.java new file mode 100644 index 000000000..7a92e596a --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CodeDeltaCalculatorTest.java @@ -0,0 +1,302 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.util.FilteredLog; + +import hudson.FilePath; +import hudson.model.Run; +import hudson.model.TaskListener; + +import io.jenkins.plugins.forensics.delta.Delta; +import io.jenkins.plugins.forensics.delta.FileChanges; +import io.jenkins.plugins.forensics.delta.FileEditType; + +import static io.jenkins.plugins.coverage.metrics.steps.CodeDeltaCalculator.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Test class for {@link CodeDeltaCalculator}. + * + * @author Florian Orendi + */ +class CodeDeltaCalculatorTest { + private static final String LOG_NAME = "Errors while calculating changes mapping:"; + + private static final String EMPTY_PATH = ""; + + private static final String OLD_SCM_PATH_RENAME = + Paths.get("src", "main", "example", "Test.java").toString(); + private static final String OLD_REPORT_PATH_RENAME = + Paths.get("example", "Test.java").toString(); + + private static final String REPORT_PATH_ADD_1 = + Paths.get("test", "Test.java").toString(); + private static final String REPORT_PATH_ADD_2 = + Paths.get("package", "example", "test", "Test.java").toString(); + private static final String REPORT_PATH_MODIFY = + Paths.get("example", "test", "Test.java").toString(); + private static final String REPORT_PATH_RENAME = + Paths.get("example", "Test_Renamed.java").toString(); + + private static final String SCM_PATH_ADD_1 = + Paths.get("test", "Test.java").toString(); + private static final String SCM_PATH_ADD_2 = + Paths.get("src", "package", "example", "test", "Test.java").toString(); + private static final String SCM_PATH_MODIFY = + Paths.get("src", "main", "java", "example", "test", "Test.java").toString(); + private static final String SCM_PATH_DELETE = + Paths.get("src", "main", "example", "test", "Test.java").toString(); + private static final String SCM_PATH_RENAME = + Paths.get("src", "main", "example", "Test_Renamed.java").toString(); + + @Test + void shouldGetCoverageRelevantChanges() { + CodeDeltaCalculator codeDeltaCalculator = createCodeDeltaCalculator(); + Delta delta = createDeltaWithStubbedFileChanges(); + Map allChanges = delta.getFileChangesMap(); + + assertThat(codeDeltaCalculator.getCoverageRelevantChanges(delta)) + .containsExactlyInAnyOrder( + allChanges.get(SCM_PATH_ADD_1), + allChanges.get(SCM_PATH_ADD_2), + allChanges.get(SCM_PATH_MODIFY), + allChanges.get(SCM_PATH_RENAME) + ); + } + + @Test + void shouldMapScmChangesToReportPaths() throws IllegalStateException { + CodeDeltaCalculator codeDeltaCalculator = createCodeDeltaCalculator(); + Delta delta = createDeltaWithStubbedFileChanges(); + Set changes = codeDeltaCalculator.getCoverageRelevantChanges(delta); + Map changesMap = changes.stream() + .collect(Collectors.toMap(FileChanges::getFileName, Function.identity())); + Node tree = createStubbedCoverageTree(); + FilteredLog log = createFilteredLog(); + + Map should = new HashMap<>(); + should.put(REPORT_PATH_ADD_1, changesMap.get(SCM_PATH_ADD_1)); + should.put(REPORT_PATH_ADD_2, changesMap.get(SCM_PATH_ADD_2)); + should.put(REPORT_PATH_MODIFY, changesMap.get(SCM_PATH_MODIFY)); + should.put(REPORT_PATH_RENAME, changesMap.get(SCM_PATH_RENAME)); + + assertThat(codeDeltaCalculator.mapScmChangesToReportPaths(changes, tree, log)) + .containsExactlyInAnyOrderEntriesOf(should); + } + + @Test + void shouldCreateEmptyMappingWithoutChanges() throws IllegalStateException { + CodeDeltaCalculator codeDeltaCalculator = createCodeDeltaCalculator(); + Node tree = createStubbedCoverageTree(); + FilteredLog log = createFilteredLog(); + Set noChanges = new HashSet<>(); + + assertThat(codeDeltaCalculator.mapScmChangesToReportPaths(noChanges, tree, log)).isEmpty(); + } + + @Test + void shouldNotMapScmChangesWithAmbiguousPaths() throws IllegalStateException { + CodeDeltaCalculator codeDeltaCalculator = createCodeDeltaCalculator(); + FilteredLog log = createFilteredLog(); + + String path = "example"; + Set changes = createAmbiguousFileChanges(path); + + Node tree = mock(Node.class); + FileNode file1 = mock(FileNode.class); + when(file1.getPath()).thenReturn(path); + when(tree.getAllFileNodes()).thenReturn(List.of(file1)); + when(tree.getFiles()).thenReturn(Set.of(path)); + + assertThatThrownBy(() -> codeDeltaCalculator.mapScmChangesToReportPaths(changes, tree, log)) + .isInstanceOf(IllegalStateException.class) + .hasMessage(AMBIGUOUS_PATHS_ERROR); + } + + @Test + void shouldCreateOldPathMapping() throws IllegalStateException { + CodeDeltaCalculator codeDeltaCalculator = createCodeDeltaCalculator(); + FilteredLog log = createFilteredLog(); + Node tree = createStubbedCoverageTree(); + Node referenceTree = createStubbedReferenceCoverageTree(); + Map changes = new HashMap<>(); + changes.put(REPORT_PATH_MODIFY, createFileChanges(SCM_PATH_MODIFY, SCM_PATH_MODIFY, FileEditType.MODIFY)); + changes.put(REPORT_PATH_RENAME, createFileChanges(SCM_PATH_RENAME, OLD_SCM_PATH_RENAME, FileEditType.RENAME)); + + Map should = new HashMap<>(); + should.put(REPORT_PATH_MODIFY, REPORT_PATH_MODIFY); + should.put(REPORT_PATH_RENAME, OLD_REPORT_PATH_RENAME); + + assertThat(codeDeltaCalculator.createOldPathMapping(tree, referenceTree, changes, log)) + .containsExactlyInAnyOrderEntriesOf(should); + } + + @Test + void shouldNotCreateOldPathMappingWithMissingReferenceNodes() throws IllegalStateException { + CodeDeltaCalculator codeDeltaCalculator = createCodeDeltaCalculator(); + FilteredLog log = createFilteredLog(); + + Node tree = new FileNode(REPORT_PATH_RENAME); + Node referenceTree = new FileNode(REPORT_PATH_MODIFY); + Map changes = new HashMap<>(); + changes.put(REPORT_PATH_RENAME, createFileChanges(SCM_PATH_RENAME, OLD_SCM_PATH_RENAME, FileEditType.RENAME)); + + assertThat(codeDeltaCalculator.createOldPathMapping(tree, referenceTree, changes, log)).isEmpty(); + assertThat(log.getInfoMessages()).contains( + EMPTY_OLD_PATHS_WARNING + System.lineSeparator() + REPORT_PATH_RENAME + ); + } + + // checks the functionality to prevent exceptions in case of false calculated code deltas + @Test + void shouldNotCreateOldPathMappingWithCodeDeltaMismatches() { + CodeDeltaCalculator codeDeltaCalculator = createCodeDeltaCalculator(); + FilteredLog log = createFilteredLog(); + Node tree = createStubbedCoverageTree(); + Node referenceTree = createStubbedReferenceCoverageTree(); + + // two changes with the same former path + Map changes = new HashMap<>(); + changes.put(REPORT_PATH_RENAME, createFileChanges(SCM_PATH_RENAME, OLD_SCM_PATH_RENAME, FileEditType.RENAME)); + changes.put(REPORT_PATH_MODIFY, createFileChanges(REPORT_PATH_MODIFY, OLD_SCM_PATH_RENAME, FileEditType.RENAME)); + + assertThatThrownBy(() -> codeDeltaCalculator.createOldPathMapping(tree, referenceTree, changes, log)) + .isInstanceOf(IllegalStateException.class) + .hasMessageStartingWith(CODE_DELTA_TO_COVERAGE_DATA_MISMATCH_ERROR_TEMPLATE) + .hasMessageContainingAll( + String.format("new: '%s' - former: '%s',", REPORT_PATH_RENAME, OLD_REPORT_PATH_RENAME), + String.format("new: '%s' - former: '%s'", REPORT_PATH_MODIFY, OLD_REPORT_PATH_RENAME)); + } + + /** + * Creates an instance of {@link CodeDeltaCalculator}. + * + * @return the created instance + */ + private CodeDeltaCalculator createCodeDeltaCalculator() { + return new CodeDeltaCalculator(mock(Run.class), mock(FilePath.class), + mock(TaskListener.class), ""); + } + + private Delta createDeltaWithStubbedFileChanges() { + Delta delta = mock(Delta.class); + Map fileChanges = new HashMap<>(); + FileChanges fileChangesAdd1 = createFileChanges(SCM_PATH_ADD_1, EMPTY_PATH, FileEditType.ADD); + FileChanges fileChangesAdd2 = createFileChanges(SCM_PATH_ADD_2, EMPTY_PATH, FileEditType.ADD); + FileChanges fileChangesModify = createFileChanges(SCM_PATH_MODIFY, SCM_PATH_MODIFY, FileEditType.MODIFY); + FileChanges fileChangesDelete = createFileChanges(EMPTY_PATH, SCM_PATH_DELETE, FileEditType.DELETE); + FileChanges fileChangesRename = createFileChanges(SCM_PATH_RENAME, OLD_SCM_PATH_RENAME, FileEditType.RENAME); + fileChanges.put(fileChangesAdd1.getFileName(), fileChangesAdd1); + fileChanges.put(fileChangesAdd2.getFileName(), fileChangesAdd2); + fileChanges.put(fileChangesModify.getFileName(), fileChangesModify); + fileChanges.put(fileChangesDelete.getOldFileName(), fileChangesDelete); + fileChanges.put(fileChangesRename.getFileName(), fileChangesRename); + when(delta.getFileChangesMap()).thenReturn(fileChanges); + return delta; + } + + /** + * Creates a set of {@link FileChanges} with ambiguous paths. + * + * @param path + * The ambiguous path + * + * @return the set of changes + */ + private Set createAmbiguousFileChanges(final String path) { + FileChanges change1 = mock(FileChanges.class); + when(change1.getFileName()).thenReturn(Paths.get("src", path).toString()); + FileChanges change2 = mock(FileChanges.class); + when(change2.getFileName()).thenReturn(Paths.get("main", path).toString()); + Set changes = new HashSet<>(); + changes.add(change1); + changes.add(change2); + return changes; + } + + /** + * Creates a stub of {@link FileChanges}. + * + * @param filePath + * The file path + * @param oldFilePath + * The old file path before the modifications + * @param fileEditType + * The {@link FileEditType edit type} + * + * @return the created mock + */ + private FileChanges createFileChanges(final String filePath, final String oldFilePath, + final FileEditType fileEditType) { + FileChanges change = mock(FileChanges.class); + when(change.getFileEditType()).thenReturn(fileEditType); + when(change.getFileName()).thenReturn(filePath); + when(change.getOldFileName()).thenReturn(oldFilePath); + return change; + } + + /** + * Mocks a coverage tree which contains file nodes which represent {@link #REPORT_PATH_ADD_1}, {@link + * #REPORT_PATH_ADD_2} and {@link #REPORT_PATH_MODIFY}. + * + * @return the {@link Node root} of the tree + */ + private Node createStubbedCoverageTree() { + FileNode addFile1 = mock(FileNode.class); + when(addFile1.getPath()).thenReturn(REPORT_PATH_ADD_1); + FileNode addFile2 = mock(FileNode.class); + when(addFile2.getPath()).thenReturn(REPORT_PATH_ADD_2); + FileNode modifyFile = mock(FileNode.class); + when(modifyFile.getPath()).thenReturn(REPORT_PATH_MODIFY); + FileNode renameFile = mock(FileNode.class); + when(renameFile.getPath()).thenReturn(REPORT_PATH_RENAME); + Node root = mock(Node.class); + when(root.getAllFileNodes()).thenReturn(Arrays.asList(addFile1, addFile2, modifyFile, renameFile)); + var files = root.getAllFileNodes().stream().map(FileNode::getPath).collect(Collectors.toSet()); + when(root.getFiles()).thenReturn(files); + + return root; + } + + /** + * Mocks a reference coverage tree which contains file nodes which represent {@link #OLD_REPORT_PATH_RENAME} and + * {@link #REPORT_PATH_MODIFY}. + * + * @return the {@link Node root} of the tree + */ + private Node createStubbedReferenceCoverageTree() { + FileNode modifyFile = mock(FileNode.class); + when(modifyFile.getPath()).thenReturn(REPORT_PATH_MODIFY); + FileNode renameFile = mock(FileNode.class); + when(renameFile.getPath()).thenReturn(OLD_REPORT_PATH_RENAME); + Node root = mock(Node.class); + when(root.getAllFileNodes()).thenReturn(Arrays.asList(renameFile, modifyFile)); + var files = root.getAllFileNodes().stream().map(FileNode::getPath).collect(Collectors.toSet()); + when(root.getFiles()).thenReturn(files); + + return root; + } + + /** + * Creates a {@link FilteredLog}. + * + * @return the created log + */ + private FilteredLog createFilteredLog() { + return new FilteredLog(LOG_NAME); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageApiITest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageApiITest.java new file mode 100644 index 000000000..ea3ef812a --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageApiITest.java @@ -0,0 +1,104 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import edu.hm.hafner.coverage.Metric; + +import net.sf.json.JSONObject; + +import hudson.model.FreeStyleProject; +import hudson.model.Result; +import hudson.model.Run; + +import io.jenkins.plugins.coverage.metrics.AbstractCoverageITest; +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.coverage.metrics.steps.CoverageTool.Parser; +import io.jenkins.plugins.util.QualityGate.QualityGateCriticality; + +import static io.jenkins.plugins.coverage.metrics.AbstractCoverageTest.*; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; + +/** + * Tests the class {@link CoverageApi}. + * + * @author Ullrich Hafner + */ +class CoverageApiITest extends AbstractCoverageITest { + @Test + void shouldProvideRemoteApi() { + FreeStyleProject project = createFreestyleJob(Parser.JACOCO, JACOCO_ANALYSIS_MODEL_FILE); + + Run build = buildWithResult(project, Result.SUCCESS); + + var remoteApiResult = callRemoteApi(build); + assertThatJson(remoteApiResult) + .node("projectStatistics").isEqualTo("{\n" + + " \"branch\": \"88.28%\",\n" + + " \"complexity\": \"2558\",\n" + + " \"complexity-density\": \"+44.12%\",\n" + + " \"file\": \"99.67%\",\n" + + " \"instruction\": \"96.11%\",\n" + + " \"line\": \"95.39%\",\n" + + " \"loc\": \"5798\",\n" + + " \"method\": \"97.29%\",\n" + + " \"module\": \"100.00%\",\n" + + " \"package\": \"100.00%\"}"); + assertThatJson(remoteApiResult) + .node("modifiedFilesStatistics").isEqualTo("{}"); + assertThatJson(remoteApiResult) + .node("modifiedLinesStatistics").isEqualTo("{}"); + } + + @Test + void shouldShowQualityGatesInRemoteApi() { + var qualityGates = List.of(new CoverageQualityGate(100, Metric.LINE, Baseline.PROJECT, QualityGateCriticality.UNSTABLE)); + FreeStyleProject project = createFreestyleJob(Parser.JACOCO, r -> r.setQualityGates(qualityGates), JACOCO_ANALYSIS_MODEL_FILE); + + Run build = buildWithResult(project, Result.UNSTABLE); + + var remoteApiResult = callRemoteApi(build); + assertThatJson(remoteApiResult) + .node("qualityGates.overallResult").isEqualTo("UNSTABLE"); + assertThatJson(remoteApiResult) + .node("qualityGates.resultItems").isEqualTo("[{\n" + + " \"qualityGate\": \"Overall project - Line Coverage\",\n" + + " \"result\": \"UNSTABLE\",\n" + + " \"threshold\": 100.0,\n" + + " \"value\": \"95.39%\"\n" + + "}]\n"); + } + + @Test + void shouldShowDeltaInRemoteApi() { + FreeStyleProject project = createFreestyleJob(Parser.JACOCO, + JACOCO_ANALYSIS_MODEL_FILE, JACOCO_CODING_STYLE_FILE); + + buildSuccessfully(project); + // update parser pattern to pick only the coding style results + project.getPublishersList().get(CoverageRecorder.class).getTools().get(0).setPattern(JACOCO_CODING_STYLE_FILE); + Run secondBuild = buildSuccessfully(project); + + var remoteApiResult = callRemoteApi(secondBuild); + assertThatJson(remoteApiResult) + .node("projectDelta").isEqualTo("{\n" + + " \"branch\": \"+5.33%\",\n" + + " \"complexity\": \"-2558\",\n" + + " \"complexity-density\": \"+5.13%\",\n" + + " \"file\": \"-28.74%\",\n" + + " \"instruction\": \"-2.63%\",\n" + + " \"line\": \"-4.14%\",\n" + + " \"loc\": \"-5798\",\n" + + " \"method\": \"-2.06%\",\n" + + " \"module\": \"+0.00%\",\n" + + " \"package\": \"+0.00%\"\n" + + "}"); + assertThatJson(remoteApiResult).node("referenceBuild").asString() + .matches("test0 #1"); + } + + private JSONObject callRemoteApi(final Run build) { + return callJsonRemoteApi(build.getUrl() + "coverage/api/json").getJSONObject(); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildActionTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildActionTest.java new file mode 100644 index 000000000..7d9b2d635 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildActionTest.java @@ -0,0 +1,91 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.List; +import java.util.TreeMap; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.Fraction; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.DefaultLocale; + +import edu.hm.hafner.coverage.Coverage.CoverageBuilder; +import edu.hm.hafner.coverage.FractionValue; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.ModuleNode; +import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.util.FilteredLog; + +import hudson.model.FreeStyleBuild; + +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.util.QualityGateResult; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Tests the class {@link CoverageBuildAction}. + * + * @author Ullrich Hafner + */ +@DefaultLocale("en") +class CoverageBuildActionTest { + @Test + void shouldNotLoadResultIfCoverageValuesArePersistedInAction() { + Node module = new ModuleNode("module"); + + var coverageBuilder = new CoverageBuilder(); + var percent50 = coverageBuilder.setMetric(Metric.BRANCH).setCovered(1).setMissed(1).build(); + var percent80 = coverageBuilder.setMetric(Metric.LINE).setCovered(8).setMissed(2).build(); + + module.addValue(percent50); + module.addValue(percent80); + + var deltas = new TreeMap(); + var lineDelta = percent80.getCoveredPercentage().subtract(percent50.getCoveredPercentage()); + deltas.put(Metric.LINE, lineDelta); + var branchDelta = percent50.getCoveredPercentage().subtract(percent80.getCoveredPercentage()); + deltas.put(Metric.BRANCH, branchDelta); + + var coverages = List.of(percent50, percent80); + var action = spy(new CoverageBuildAction(mock(FreeStyleBuild.class), CoverageRecorder.DEFAULT_ID, + StringUtils.EMPTY, StringUtils.EMPTY, module, new QualityGateResult(), + createLog(), "-", deltas, coverages, deltas, coverages, deltas, coverages, false)); + + when(action.getResult()).thenThrow(new IllegalStateException("Result should not be accessed with getResult() when getting a coverage metric that is persisted in the build")); + + assertThat(action.getReferenceBuild()).isEmpty(); + + assertThat(action.getStatistics().getValue(Baseline.PROJECT, Metric.BRANCH)).hasValue(percent50); + assertThat(action.getStatistics().getValue(Baseline.PROJECT, Metric.LINE)).hasValue(percent80); + assertThat(action.getStatistics().getValue(Baseline.MODIFIED_LINES, Metric.BRANCH)).hasValue(percent50); + assertThat(action.getStatistics().getValue(Baseline.MODIFIED_LINES, Metric.LINE)).hasValue(percent80); + assertThat(action.getStatistics().getValue(Baseline.MODIFIED_FILES, Metric.BRANCH)).hasValue(percent50); + assertThat(action.getStatistics().getValue(Baseline.MODIFIED_FILES, Metric.LINE)).hasValue(percent80); + assertThat(action.getStatistics().getValue(Baseline.PROJECT_DELTA, Metric.LINE)) + .hasValue(new FractionValue(Metric.LINE, lineDelta)); + assertThat(action.getStatistics().getValue(Baseline.PROJECT_DELTA, Metric.BRANCH)) + .hasValue(new FractionValue(Metric.BRANCH, branchDelta)); + + assertThat(action.getAllValues(Baseline.PROJECT)).containsAll(coverages); + } + + private static CoverageBuildAction createEmptyAction(final Node module) { + return new CoverageBuildAction(mock(FreeStyleBuild.class), CoverageRecorder.DEFAULT_ID, + StringUtils.EMPTY, StringUtils.EMPTY, module, new QualityGateResult(), createLog(), "-", + new TreeMap<>(), List.of(), new TreeMap<>(), List.of(), new TreeMap<>(), List.of(), false); + } + + private static FilteredLog createLog() { + return new FilteredLog("Errors"); + } + + @Test + void shouldCreateViewModel() { + Node root = new ModuleNode("top-level"); + CoverageBuildAction action = createEmptyAction(root); + + assertThat(action.getTarget()).extracting(CoverageViewModel::getNode).isSameAs(root); + assertThat(action.getTarget()).extracting(CoverageViewModel::getOwner).isSameAs(action.getOwner()); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisherTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisherTest.java new file mode 100644 index 000000000..db3af08b6 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisherTest.java @@ -0,0 +1,133 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.Fraction; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junitpioneer.jupiter.DefaultLocale; + +import edu.hm.hafner.coverage.Coverage.CoverageBuilder; +import edu.hm.hafner.coverage.Metric; + +import hudson.model.Run; + +import io.jenkins.plugins.checks.api.ChecksAnnotation.ChecksAnnotationLevel; +import io.jenkins.plugins.checks.api.ChecksConclusion; +import io.jenkins.plugins.checks.api.ChecksDetails; +import io.jenkins.plugins.checks.api.ChecksOutput; +import io.jenkins.plugins.checks.api.ChecksStatus; +import io.jenkins.plugins.coverage.metrics.AbstractCoverageTest; +import io.jenkins.plugins.coverage.metrics.steps.CoverageRecorder.ChecksAnnotationScope; +import io.jenkins.plugins.util.JenkinsFacade; +import io.jenkins.plugins.util.QualityGateResult; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +@DefaultLocale("en") +class CoverageChecksPublisherTest extends AbstractCoverageTest { + private static final String JENKINS_BASE_URL = "http://127.0.0.1:8080"; + private static final String BUILD_LINK = "job/pipeline-coding-style/job/5"; + private static final String COVERAGE_ID = "coverage"; + private static final String REPORT_NAME = "Name"; + + @ParameterizedTest(name = "should create checks (scope = {0}, expected annotations = {1})") + @CsvSource({"SKIP, 0", "ALL_LINES, 36", "MODIFIED_LINES, 3"}) + void shouldCreateChecksReport(final ChecksAnnotationScope scope, final int expectedAnnotations) { + var publisher = new CoverageChecksPublisher(createCoverageBuildAction(), REPORT_NAME, scope, createJenkins()); + + var checkDetails = publisher.extractChecksDetails(); + + assertThat(checkDetails.getName()).isPresent().get().isEqualTo(REPORT_NAME); + assertThat(checkDetails.getStatus()).isEqualTo(ChecksStatus.COMPLETED); + assertThat(checkDetails.getConclusion()).isEqualTo(ChecksConclusion.SUCCESS); + assertThat(checkDetails.getDetailsURL()).isPresent() + .get() + .isEqualTo("http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage"); + assertThatDetailsAreCorrect(checkDetails, expectedAnnotations); + } + + private void assertThatDetailsAreCorrect(final ChecksDetails checkDetails, final int expectedAnnotations) { + assertThat(checkDetails.getOutput()).isPresent().get().satisfies(output -> { + assertThat(output.getTitle()).isPresent() + .get() + .isEqualTo("Modified code lines: 50.00% (1/2)"); + assertThat(output.getText()).isEmpty(); + assertChecksAnnotations(output, expectedAnnotations); + assertSummary(output); + }); + } + + private void assertSummary(final ChecksOutput checksOutput) throws IOException { + var expectedContent = Files.readString(getResourceAsFile("coverage-publisher-summary.md")); + assertThat(checksOutput.getSummary()).isPresent() + .get() + .isEqualTo(expectedContent); + } + + private void assertChecksAnnotations(final ChecksOutput checksOutput, final int expectedAnnotations) { + if (expectedAnnotations == 3) { + assertThat(checksOutput.getChecksAnnotations()).hasSize(expectedAnnotations).satisfiesExactly( + annotation -> { + assertThat(annotation.getTitle()).contains("Not covered line"); + assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); + assertThat(annotation.getPath()).contains("edu/hm/hafner/util/TreeStringBuilder.java"); + assertThat(annotation.getMessage()).contains("Line 61 is not covered by tests"); + assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(61); + }, + annotation -> { + assertThat(annotation.getTitle()).contains("Not covered line"); + assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); + assertThat(annotation.getPath()).contains("edu/hm/hafner/util/TreeStringBuilder.java"); + assertThat(annotation.getMessage()).contains("Line 62 is not covered by tests"); + assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(62); + }, + annotation -> { + assertThat(annotation.getTitle()).contains("Partially covered line"); + assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); + assertThat(annotation.getPath()).contains("edu/hm/hafner/util/TreeStringBuilder.java"); + assertThat(annotation.getMessage()).contains("Line 113 is only partially covered, one branch is missing"); + assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(113); + }); + } + else { + assertThat(checksOutput.getChecksAnnotations()).hasSize(expectedAnnotations); + } + } + + private JenkinsFacade createJenkins() { + JenkinsFacade jenkinsFacade = mock(JenkinsFacade.class); + when(jenkinsFacade.getAbsoluteUrl(BUILD_LINK, COVERAGE_ID)).thenReturn( + JENKINS_BASE_URL + "/" + BUILD_LINK + "/" + COVERAGE_ID); + return jenkinsFacade; + } + + private CoverageBuildAction createCoverageBuildAction() { + var testCoverage = new CoverageBuilder().setMetric(Metric.LINE) + .setCovered(1) + .setMissed(1) + .build(); + + var run = mock(Run.class); + when(run.getUrl()).thenReturn(BUILD_LINK); + var result = readJacocoResult("jacoco-codingstyle.xml"); + result.findFile("TreeStringBuilder.java") + .ifPresent(file -> { + assertThat(file.getMissedLines()).contains(61, 62); + assertThat(file.getPartiallyCoveredLines()).contains(entry(113, 1)); + file.addModifiedLines(61, 62, 113); + }); + + return new CoverageBuildAction(run, COVERAGE_ID, REPORT_NAME, StringUtils.EMPTY, result, + new QualityGateResult(), null, "refId", + new TreeMap<>(Map.of(Metric.LINE, Fraction.ONE_HALF, Metric.MODULE, Fraction.ONE_FIFTH)), + List.of(testCoverage), new TreeMap<>(Map.of(Metric.LINE, Fraction.ONE_HALF)), List.of(testCoverage), + new TreeMap<>(Map.of(Metric.LINE, Fraction.ONE_HALF)), List.of(testCoverage), false); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageJobActionTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageJobActionTest.java new file mode 100644 index 000000000..7041262ca --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageJobActionTest.java @@ -0,0 +1,108 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.io.IOException; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; + +import edu.hm.hafner.echarts.line.LinesChartModel; + +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; + +import static io.jenkins.plugins.coverage.metrics.AbstractCoverageTest.*; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Tests the class {@link CoverageJobAction}. + * + * @author Ullrich Hafner + */ +class CoverageJobActionTest { + + private static final String URL = "coverage"; + + @Test + void shouldIgnoreIndexIfNoActionFound() throws IOException { + FreeStyleProject job = mock(FreeStyleProject.class); + + CoverageJobAction action = createAction(job); + + assertThat(action.getProject()).isSameAs(job); + + StaplerResponse response = mock(StaplerResponse.class); + action.doIndex(mock(StaplerRequest.class), response); + + verifyNoInteractions(response); + } + + private static CoverageJobAction createAction(final FreeStyleProject job) { + return new CoverageJobAction(job, URL, "Coverage Results", StringUtils.EMPTY); + } + + @Test + void shouldNavigateToLastAction() throws IOException { + FreeStyleBuild build = mock(FreeStyleBuild.class); + + CoverageBuildAction action = createBuildAction(build); + + when(build.getActions(CoverageBuildAction.class)).thenReturn(List.of(action)); + when(build.getNumber()).thenReturn(15); + + FreeStyleProject job = mock(FreeStyleProject.class); + when(job.getLastBuild()).thenReturn(build); + when(job.getUrl()).thenReturn(URL); + + CoverageJobAction jobAction = createAction(job); + + StaplerResponse response = mock(StaplerResponse.class); + jobAction.doIndex(mock(StaplerRequest.class), response); + + verify(response).sendRedirect2("../15/coverage"); + } + + @Test + void shouldCreateTrendChartForLineAndBranchCoverage() { + FreeStyleBuild build = mock(FreeStyleBuild.class); + + CoverageBuildAction action = createBuildAction(build); + when(build.getActions(CoverageBuildAction.class)).thenReturn(List.of(action)); + when(action.getStatistics()).thenReturn(createStatistics()); + + int buildNumber = 15; + when(build.getNumber()).thenReturn(buildNumber); + when(build.getDisplayName()).thenReturn("#" + buildNumber); + + FreeStyleProject job = mock(FreeStyleProject.class); + when(job.getLastBuild()).thenReturn(build); + + CoverageJobAction jobAction = createAction(job); + + LinesChartModel chart = jobAction.createChartModel("{}"); + + assertThatJson(chart).node("buildNumbers").isArray().hasSize(1).containsExactly(buildNumber); + assertThatJson(chart).node("domainAxisLabels").isArray().hasSize(1).containsExactly("#15"); + assertThatJson(chart).node("series").isArray().hasSize(2); + + assertThatJson(chart.getSeries().get(0)).satisfies(series -> { + assertThatJson(series).node("name").isEqualTo("Line Coverage"); + assertThatJson(series).node("data").isArray().containsExactly("50.0"); + }); + assertThatJson(chart.getSeries().get(1)).satisfies(series -> { + assertThatJson(series).node("name").isEqualTo("Branch Coverage"); + assertThatJson(series).node("data").isArray().containsExactly("90.0"); + }); + } + + private CoverageBuildAction createBuildAction(final FreeStyleBuild build) { + CoverageBuildAction action = mock(CoverageBuildAction.class); + when(action.getOwner()).thenAnswer(i -> build); + when(action.getUrlName()).thenReturn(URL); + return action; + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumnTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumnTest.java new file mode 100644 index 000000000..0dce42dd8 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumnTest.java @@ -0,0 +1,235 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.awt.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.TreeMap; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.Fraction; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.DefaultLocale; + +import edu.hm.hafner.coverage.Coverage.CoverageBuilder; +import edu.hm.hafner.coverage.FractionValue; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Value; +import edu.hm.hafner.util.FilteredLog; +import edu.hm.hafner.util.VisibleForTesting; + +import hudson.model.Job; +import hudson.model.Run; + +import io.jenkins.plugins.coverage.metrics.AbstractCoverageTest; +import io.jenkins.plugins.coverage.metrics.color.ColorProvider; +import io.jenkins.plugins.coverage.metrics.color.ColorProviderFactory; +import io.jenkins.plugins.coverage.metrics.color.CoverageChangeTendency; +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.util.QualityGateResult; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Test class for {@link CoverageMetricColumn}. + * + * @author Florian Orendi + */ +@DefaultLocale("en") +class CoverageMetricColumnTest extends AbstractCoverageTest { + private static final String COLUMN_NAME = "Test Column"; + private static final Metric COVERAGE_METRIC = Metric.BRANCH; + + private static final ColorProvider COLOR_PROVIDER = ColorProviderFactory.createDefaultColorProvider(); + + /** + * Creates a stub for a {@link Job} that has the specified actions attached. + * + * @param actions + * The actions to attach, might be empty + * + * @return the created stub + */ + @VisibleForTesting + public static Job createJobWithActions(final CoverageBuildAction... actions) { + Job job = mock(Job.class); + Run build = createBuildWithActions(actions); + when(job.getLastCompletedBuild()).thenAnswer(a -> build); + return job; + } + + /** + * Creates a stub for a {@link Run} that has the specified actions attached. + * + * @param actions + * the actions to attach, might be empty + * + * @return the created stub + */ + @VisibleForTesting + public static Run createBuildWithActions(final CoverageBuildAction... actions) { + Run build = mock(Run.class); + when(build.getActions(CoverageBuildAction.class)).thenReturn(Arrays.asList(actions)); + if (actions.length > 0) { + when(build.getAction(CoverageBuildAction.class)).thenReturn(actions[0]); + } + return build; + } + + @Test + void shouldHaveWorkingDataGetters() { + CoverageMetricColumn column = createColumn(); + + assertThat(column.getColumnName()).isEqualTo(COLUMN_NAME); + assertThat(column.getBaseline()).isEqualTo(Baseline.PROJECT); + assertThat(column.getMetric()).isEqualTo(COVERAGE_METRIC); + assertThat(column.getRelativeCoverageUrl(mock(Job.class))).isEmpty(); + } + + @Test + void shouldProvideSelectedColumn() { + CoverageMetricColumn column = createColumn(); + Job job = createJobWithCoverageAction(); + + column.setBaseline(Baseline.PROJECT); + assertThat(column.getBaseline()).isEqualTo(Baseline.PROJECT); + assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#overview"); + + column.setBaseline(Baseline.PROJECT_DELTA); + assertThat(column.getBaseline()).isEqualTo(Baseline.PROJECT_DELTA); + assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#overview"); + + column.setBaseline(Baseline.MODIFIED_LINES); + assertThat(column.getBaseline()).isEqualTo(Baseline.MODIFIED_LINES); + assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#modifiedLinesCoverage"); + + column.setBaseline(Baseline.MODIFIED_LINES_DELTA); + assertThat(column.getBaseline()).isEqualTo(Baseline.MODIFIED_LINES_DELTA); + assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#modifiedLinesCoverage"); + + column.setBaseline(Baseline.MODIFIED_FILES); + assertThat(column.getBaseline()).isEqualTo(Baseline.MODIFIED_FILES); + assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#modifiedFilesCoverage"); + + column.setBaseline(Baseline.MODIFIED_FILES_DELTA); + assertThat(column.getBaseline()).isEqualTo(Baseline.MODIFIED_FILES_DELTA); + assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#modifiedFilesCoverage"); + + column.setBaseline(Baseline.INDIRECT); + assertThat(column.getBaseline()).isEqualTo(Baseline.INDIRECT); + assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#indirectCoverage"); + } + + @Test + void shouldProvideBackgroundColorFillPercentage() { + CoverageMetricColumn column = createColumn(); + + assertThat(column.getBackgroundColorFillPercentage("+5,0%")).isEqualTo("100%"); + assertThat(column.getBackgroundColorFillPercentage("+5.0%")).isEqualTo("100%"); + assertThat(column.getBackgroundColorFillPercentage("5,00%")).isEqualTo("5.00%"); + assertThat(column.getBackgroundColorFillPercentage("5.00%")).isEqualTo("5.00%"); + } + + @Test + void shouldShowNoResultIfBuild() { + CoverageMetricColumn column = createColumn(); + + Job job = mock(Job.class); + + assertThat(column.getCoverageText(job)).isEqualTo(Messages.Coverage_Not_Available()); + + Optional coverageValue = column.getCoverageValue(job); + assertThat(coverageValue).isEmpty(); + assertThat(column.getDisplayColors(job, Optional.empty())).isEqualTo(ColorProvider.DEFAULT_COLOR); + } + + @Test + void shouldShowNoResultIfNoAction() { + CoverageMetricColumn column = createColumn(); + + Job job = createJobWithActions(); + + assertThat(column.getCoverageText(job)).isEqualTo(Messages.Coverage_Not_Available()); + assertThat(column.getCoverageValue(job)).isEmpty(); + assertThat(column.getDisplayColors(job, Optional.empty())).isEqualTo(ColorProvider.DEFAULT_COLOR); + } + + @Test + void shouldShowNoResultForUnavailableMetric() { + CoverageMetricColumn column = createColumn(); + column.setMetric(Metric.MUTATION); + + Job job = createJobWithCoverageAction(); + + assertThat(column.getCoverageText(job)).isEqualTo(Messages.Coverage_Not_Available()); + assertThat(column.getCoverageValue(job)).isEmpty(); + + column.setBaseline(Baseline.PROJECT_DELTA); + + assertThat(column.getCoverageText(job)).isEqualTo(Messages.Coverage_Not_Available()); + assertThat(column.getCoverageValue(job)).isEmpty(); + } + + @Test + void shouldCalculateProjectCoverage() { + CoverageMetricColumn column = createColumn(); + + Job job = createJobWithCoverageAction(); + + assertThat(column.getCoverageText(job)).isEqualTo("93.97%"); + assertThat(column.getCoverageValue(job)) + .isNotEmpty() + .satisfies(coverage -> { + assertThat(coverage.get()).isEqualTo(new CoverageBuilder().setMetric(Metric.BRANCH).setCovered(109).setMissed(7).build()); + assertThat(column.getDisplayColors(job, coverage).getLineColor()) + .isEqualTo(Color.white); + }); + } + + @Test + void shouldCalculateProjectCoverageDelta() { + CoverageMetricColumn column = createColumn(); + column.setBaseline(Baseline.PROJECT_DELTA); + + Fraction coverageDelta = Fraction.getFraction(1, 20); + Job job = createJobWithCoverageAction(); + + assertThat(column.getCoverageText(job)).isEqualTo("+5.00%"); + assertThat(column.getCoverageValue(job)) + .isNotEmpty() + .satisfies(coverage -> { + assertThat(coverage.get()).isEqualTo(new FractionValue(Metric.BRANCH, coverageDelta)); + assertThat(column.getDisplayColors(job, coverage)) + .isEqualTo(COLOR_PROVIDER.getDisplayColorsOf( + CoverageChangeTendency.INCREASED.getColorizationId())); + }); + } + + private CoverageMetricColumn createColumn() { + CoverageMetricColumn column = new CoverageMetricColumn(); + column.setColumnName(COLUMN_NAME); + column.setBaseline(Baseline.PROJECT); + column.setMetric(COVERAGE_METRIC); + return column; + } + + private Job createJobWithCoverageAction() { + var node = readJacocoResult(JACOCO_CODING_STYLE_FILE); + var run = mock(Run.class); + var delta = new TreeMap(); + delta.put(Metric.BRANCH, Fraction.getFraction("0.05")); + CoverageBuildAction coverageBuildAction = + new CoverageBuildAction(run, "coverage", "Code Coverage", StringUtils.EMPTY, + node, new QualityGateResult(), new FilteredLog("Test"), + "-", delta, List.of(), new TreeMap<>(), List.of(), new TreeMap<>(), List.of(), false); + when(run.getAction(CoverageBuildAction.class)).thenReturn(coverageBuildAction); + when(run.getActions(CoverageBuildAction.class)).thenReturn(Collections.singletonList(coverageBuildAction)); + + var job = mock(Job.class); + when(job.getLastCompletedBuild()).thenReturn(run); + + return job; + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePluginITest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePluginITest.java new file mode 100644 index 000000000..1d5f0b31e --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePluginITest.java @@ -0,0 +1,358 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import edu.hm.hafner.coverage.Coverage; +import edu.hm.hafner.coverage.Coverage.CoverageBuilder; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Value; + +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import hudson.model.FreeStyleProject; +import hudson.model.Result; +import hudson.model.Run; +import jenkins.model.ParameterizedJobMixIn.ParameterizedJob; + +import io.jenkins.plugins.coverage.metrics.AbstractCoverageITest; +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.coverage.metrics.steps.CoverageTool.Parser; + +import static edu.hm.hafner.coverage.Metric.*; +import static io.jenkins.plugins.coverage.metrics.AbstractCoverageTest.*; +import static org.assertj.core.api.Assertions.*; + +/** + * Integration test for different JaCoCo, Cobertura, and PIT files. + */ +class CoveragePluginITest extends AbstractCoverageITest { + private static final String COBERTURA_HIGHER_COVERAGE_FILE = "cobertura-higher-coverage.xml"; + private static final int COBERTURA_COVERED_LINES = 7; + private static final int COBERTURA_MISSED_LINES = 0; + private static final String NO_FILES_FOUND_ERROR_MESSAGE = "[-ERROR-] No files found for pattern '**/*xml'. Configuration error?"; + + @Test + void shouldFailWithoutParserInFreestyleJob() { + FreeStyleProject project = createFreeStyleProject(); + + project.getPublishersList().add(new CoverageRecorder()); + + verifyNoParserError(project); + } + + @Test + void shouldFailWithoutParserInPipeline() { + WorkflowJob job = createPipeline(); + + setPipelineScript(job, "recordCoverage()"); + + verifyNoParserError(job); + } + + private void verifyNoParserError(final ParameterizedJob project) { + Run run = buildWithResult(project, Result.FAILURE); + + assertThat(getConsoleLog(run)).contains("[-ERROR-] No tools defined that will record the coverage files"); + } + + @EnumSource + @ParameterizedTest(name = "{index} => Freestyle job with parser {0}") + @DisplayName("Report error but do not fail build in freestyle job when no input files are found") + void shouldReportErrorWhenNoFilesHaveBeenFoundInFreestyleJob(final Parser parser) { + FreeStyleProject project = createFreestyleJob(parser); + + verifyLogMessageThatNoFilesFound(project); + } + + @EnumSource + @ParameterizedTest(name = "{index} => Pipeline with parser {0}") + @DisplayName("Report error but do not fail build in pipeline when no input files are found") + void shouldReportErrorWhenNoFilesHaveBeenFoundInPipeline(final Parser parser) { + WorkflowJob job = createPipeline(parser); + + verifyLogMessageThatNoFilesFound(job); + } + + private void verifyLogMessageThatNoFilesFound(final ParameterizedJob project) { + Run run = buildWithResult(project, Result.SUCCESS); + + assertThat(getConsoleLog(run)).contains(NO_FILES_FOUND_ERROR_MESSAGE, + "Ignore errors and continue processing"); + } + + @EnumSource + @ParameterizedTest(name = "{index} => Freestyle job with parser {0}") + @DisplayName("Report error and fail build in freestyle job when no input files are found") + void shouldFailBuildWhenNoFilesHaveBeenFoundInFreestyleJob(final Parser parser) { + FreeStyleProject project = createFreestyleJob(parser, r -> r.setFailOnError(true)); + + verifyFailureWhenNoFilesFound(project); + } + + @EnumSource + @ParameterizedTest(name = "{index} => Pipeline with parser {0}") + @DisplayName("Report error and fail build in pipeline when no input files are found") + void shouldFailBuildWhenNoFilesHaveBeenFoundInPipeline(final Parser parser) { + WorkflowJob job = createPipeline(); + + setPipelineScript(job, + "recordCoverage tools: [[parser: '" + parser.name() + "', pattern: '**/*xml']], " + + "failOnError: 'true'"); + + verifyFailureWhenNoFilesFound(job); + } + + private void verifyFailureWhenNoFilesFound(final ParameterizedJob project) { + Run run = buildWithResult(project, Result.FAILURE); + + assertThat(getConsoleLog(run)).contains(NO_FILES_FOUND_ERROR_MESSAGE, + "Failing build due to some errors during recording of the coverage"); + } + + @Test + void shouldRecordOneJacocoResultInFreestyleJob() { + FreeStyleProject project = createFreestyleJob(Parser.JACOCO, JACOCO_ANALYSIS_MODEL_FILE); + + verifyOneJacocoResult(project); + } + + @Test + void shouldRecordOneJacocoResultInPipeline() { + WorkflowJob job = createPipeline(Parser.JACOCO, JACOCO_ANALYSIS_MODEL_FILE); + + verifyOneJacocoResult(job); + } + + @Test + void shouldRecordOneJacocoResultInDeclarativePipeline() { + WorkflowJob job = createDeclarativePipeline(Parser.JACOCO, JACOCO_ANALYSIS_MODEL_FILE); + + verifyOneJacocoResult(job); + } + + private void verifyOneJacocoResult(final ParameterizedJob project) { + Run build = buildSuccessfully(project); + + verifyJaCoCoAction(build.getAction(CoverageBuildAction.class)); + } + + private static void verifyJaCoCoAction(final CoverageBuildAction coverageResult) { + assertThat(coverageResult.getAllValues(Baseline.PROJECT)).extracting(Value::getMetric) + .containsExactly(MODULE, + PACKAGE, + Metric.FILE, + Metric.CLASS, + METHOD, + LINE, + BRANCH, + INSTRUCTION, + COMPLEXITY, + COMPLEXITY_DENSITY, + LOC); + assertThat(coverageResult.getMetricsForSummary()) + .containsExactly(Metric.LINE, Metric.BRANCH, Metric.MUTATION, COMPLEXITY_DENSITY, Metric.LOC); + assertThat(coverageResult.getAllValues(Baseline.PROJECT)) + .contains(createLineCoverageBuilder() + .setCovered(JACOCO_ANALYSIS_MODEL_COVERED) + .setMissed(JACOCO_ANALYSIS_MODEL_TOTAL - JACOCO_ANALYSIS_MODEL_COVERED) + .build()); + } + + @Test + void shouldRecordTwoJacocoResultsInFreestyleJob() { + FreeStyleProject project = createFreestyleJob(Parser.JACOCO, + JACOCO_ANALYSIS_MODEL_FILE, JACOCO_CODING_STYLE_FILE); + verifyTwoJacocoResults(project); + } + + @Test + void shouldRecordTwoJacocoResultsInPipeline() { + WorkflowJob job = createPipeline(Parser.JACOCO, + JACOCO_ANALYSIS_MODEL_FILE, JACOCO_CODING_STYLE_FILE); + + verifyTwoJacocoResults(job); + } + + @Test + void shouldRecordTwoJacocoResultsInDeclarativePipeline() { + WorkflowJob job = createDeclarativePipeline(Parser.JACOCO, + JACOCO_ANALYSIS_MODEL_FILE, JACOCO_CODING_STYLE_FILE); + + verifyTwoJacocoResults(job); + } + + private void verifyTwoJacocoResults(final ParameterizedJob project) { + Run build = buildSuccessfully(project); + + CoverageBuildAction coverageResult = build.getAction(CoverageBuildAction.class); + assertThat(coverageResult.getAllValues(Baseline.PROJECT)) + .contains(createLineCoverageBuilder() + .setCovered(JACOCO_ANALYSIS_MODEL_COVERED + JACOCO_CODING_STYLE_COVERED) + .setMissed(JACOCO_ANALYSIS_MODEL_MISSED + JACOCO_CODING_STYLE_MISSED) + .build()); + } + + @Test + void shouldRecordOneCoberturaResultInFreestyleJob() { + FreeStyleProject project = createFreestyleJob(Parser.COBERTURA, COBERTURA_HIGHER_COVERAGE_FILE); + + verifyOneCoberturaResult(project); + } + + @Test + void shouldRecordOneCoberturaResultInPipeline() { + WorkflowJob job = createPipeline(Parser.COBERTURA, COBERTURA_HIGHER_COVERAGE_FILE); + + verifyOneCoberturaResult(job); + } + + @Test + void shouldRecordOneCoberturaResultInDeclarativePipeline() { + WorkflowJob job = createDeclarativePipeline(Parser.COBERTURA, COBERTURA_HIGHER_COVERAGE_FILE); + + verifyOneCoberturaResult(job); + } + + private void verifyOneCoberturaResult(final ParameterizedJob project) { + Run build = buildSuccessfully(project); + + verifyCoberturaAction(build.getAction(CoverageBuildAction.class)); + } + + private static void verifyCoberturaAction(final CoverageBuildAction coverageResult) { + assertThat(coverageResult.getAllValues(Baseline.PROJECT)) + .contains(new CoverageBuilder().setMetric(Metric.LINE).setCovered(COBERTURA_COVERED_LINES) + .setMissed(COBERTURA_MISSED_LINES) + .build()); + } + + @Test + void shouldRecordCoberturaAndJacocoResultsInFreestyleJob() { + FreeStyleProject project = createFreeStyleProjectWithWorkspaceFiles(JACOCO_ANALYSIS_MODEL_FILE, + COBERTURA_HIGHER_COVERAGE_FILE); + + CoverageRecorder recorder = new CoverageRecorder(); + + var cobertura = new io.jenkins.plugins.coverage.metrics.steps.CoverageTool(); + cobertura.setParser(Parser.COBERTURA); + cobertura.setPattern(COBERTURA_HIGHER_COVERAGE_FILE); + + var jacoco = new io.jenkins.plugins.coverage.metrics.steps.CoverageTool(); + jacoco.setParser(Parser.JACOCO); + jacoco.setPattern(JACOCO_ANALYSIS_MODEL_FILE); + + recorder.setTools(List.of(jacoco, cobertura)); + project.getPublishersList().add(recorder); + + verifyForOneCoberturaAndOneJacoco(project); + } + + @Test + void shouldRecordCoberturaAndJacocoResultsInPipeline() { + WorkflowJob job = createPipelineWithWorkspaceFiles(JACOCO_ANALYSIS_MODEL_FILE, COBERTURA_HIGHER_COVERAGE_FILE); + + setPipelineScript(job, + "recordCoverage tools: [" + + "[parser: 'COBERTURA', pattern: '" + COBERTURA_HIGHER_COVERAGE_FILE + "']," + + "[parser: 'JACOCO', pattern: '" + JACOCO_ANALYSIS_MODEL_FILE + "']" + + "]"); + + verifyForOneCoberturaAndOneJacoco(job); + } + + @Test + void shouldRecordCoberturaAndJacocoResultsInDeclarativePipeline() { + WorkflowJob job = createPipelineWithWorkspaceFiles(JACOCO_ANALYSIS_MODEL_FILE, COBERTURA_HIGHER_COVERAGE_FILE); + + job.setDefinition(new CpsFlowDefinition("pipeline {\n" + + " agent any\n" + + " stages {\n" + + " stage('Test') {\n" + + " steps {\n" + + " recordCoverage(tools: [\n" + + " [parser: 'COBERTURA', pattern: '" + COBERTURA_HIGHER_COVERAGE_FILE + "'],\n" + + " [parser: 'JACOCO', pattern: '" + JACOCO_ANALYSIS_MODEL_FILE + "']\n" + + " ])\n" + + " }\n" + + " }\n" + + " }\n" + + "}", true)); + + verifyForOneCoberturaAndOneJacoco(job); + } + + private void verifyForOneCoberturaAndOneJacoco(final ParameterizedJob project) { + Run build = buildSuccessfully(project); + + CoverageBuildAction coverageResult = build.getAction(CoverageBuildAction.class); + assertThat(coverageResult.getAllValues(Baseline.PROJECT)) + .contains(createLineCoverageBuilder() + .setCovered(JACOCO_ANALYSIS_MODEL_COVERED + COBERTURA_COVERED_LINES) + .setMissed(JACOCO_ANALYSIS_MODEL_MISSED) + .build()); + } + + @Test + void shouldRecordOnePitResultInFreestyleJob() { + FreeStyleProject project = createFreestyleJob(Parser.PIT, "mutations.xml"); + + verifyOnePitResult(project); + } + + private void verifyOnePitResult(final ParameterizedJob project) { + Run build = buildSuccessfully(project); + + CoverageBuildAction coverageResult = build.getAction(CoverageBuildAction.class); + assertThat(coverageResult.getAllValues(Baseline.PROJECT)) + .filteredOn(Value::getMetric, MUTATION) + .first() + .isInstanceOfSatisfying(Coverage.class, m -> { + assertThat(m.getCovered()).isEqualTo(222); + assertThat(m.getTotal()).isEqualTo(246); + }); + } + + private static CoverageBuilder createLineCoverageBuilder() { + return new CoverageBuilder().setMetric(Metric.LINE); + } + + @Test + void shouldRecordResultsWithDifferentId() { + WorkflowJob job = createPipelineWithWorkspaceFiles(JACOCO_ANALYSIS_MODEL_FILE, COBERTURA_HIGHER_COVERAGE_FILE); + + setPipelineScript(job, + "recordCoverage " + + "tools: [[parser: 'COBERTURA', pattern: '" + COBERTURA_HIGHER_COVERAGE_FILE + "']]," + + "id: 'cobertura', name: 'Cobertura Results'\n" + + "recordCoverage " + + "tools: [" + + "[parser: 'JACOCO', pattern: '" + JACOCO_ANALYSIS_MODEL_FILE + "']]," + + "id: 'jacoco', name: 'JaCoCo Results'\n"); + + Run build = buildSuccessfully(job); + + List coverageResult = build.getActions(CoverageBuildAction.class); + assertThat(coverageResult).hasSize(2); + + assertThat(coverageResult).element(0).satisfies( + a -> { + assertThat(a.getUrlName()).isEqualTo("cobertura"); + assertThat(a.getDisplayName()).isEqualTo("Cobertura Results"); + verifyCoberturaAction(a); + } + ); + assertThat(coverageResult).element(1).satisfies( + a -> { + assertThat(a.getUrlName()).isEqualTo("jacoco"); + assertThat(a.getDisplayName()).isEqualTo("JaCoCo Results"); + verifyJaCoCoAction(a); + }); + + // TODO: verify that two different trend charts are returned! + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGateEvaluatorTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGateEvaluatorTest.java new file mode 100644 index 000000000..5cf61091a --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGateEvaluatorTest.java @@ -0,0 +1,185 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import edu.hm.hafner.coverage.Metric; + +import io.jenkins.plugins.coverage.metrics.AbstractCoverageTest; +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.util.QualityGate.QualityGateCriticality; +import io.jenkins.plugins.util.QualityGateResult; +import io.jenkins.plugins.util.QualityGateStatus; + +import static io.jenkins.plugins.util.assertions.Assertions.*; + +class CoverageQualityGateEvaluatorTest extends AbstractCoverageTest { + @Test + void shouldBeInactiveIfGatesAreEmpty() { + CoverageQualityGateEvaluator evaluator = new CoverageQualityGateEvaluator(new ArrayList<>(), createStatistics()); + + QualityGateResult result = evaluator.evaluate(); + + assertThat(result).hasNoMessages().isInactive().isSuccessful().hasOverallStatus(QualityGateStatus.INACTIVE); + } + + @Test + void shouldPassForTooLowThresholds() { + Collection qualityGates = new ArrayList<>(); + + qualityGates.add(new CoverageQualityGate(0, Metric.FILE, Baseline.PROJECT, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(0, Metric.LINE, Baseline.PROJECT, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(0, Metric.FILE, Baseline.MODIFIED_LINES, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(0, Metric.LINE, Baseline.MODIFIED_LINES, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(0, Metric.FILE, Baseline.MODIFIED_FILES, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(0, Metric.LINE, Baseline.MODIFIED_FILES, QualityGateCriticality.UNSTABLE)); + + var minimum = -10; + qualityGates.add(new CoverageQualityGate(minimum, Metric.FILE, Baseline.PROJECT_DELTA, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(minimum, Metric.LINE, Baseline.PROJECT_DELTA, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(minimum, Metric.FILE, Baseline.MODIFIED_LINES_DELTA, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(minimum, Metric.LINE, Baseline.MODIFIED_LINES_DELTA, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(minimum, Metric.FILE, Baseline.MODIFIED_FILES_DELTA, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(minimum, Metric.LINE, Baseline.MODIFIED_FILES_DELTA, QualityGateCriticality.UNSTABLE)); + + CoverageQualityGateEvaluator evaluator = new CoverageQualityGateEvaluator(qualityGates, createStatistics()); + + assertThat(evaluator).isEnabled(); + + QualityGateResult result = evaluator.evaluate(); + + assertThat(result).hasOverallStatus(QualityGateStatus.PASSED).isSuccessful().isNotInactive().hasMessages( + "-> [Overall project - File Coverage]: ≪Success≫ - (Actual value: 75.00%, Quality gate: 0.00)", + "-> [Overall project - Line Coverage]: ≪Success≫ - (Actual value: 50.00%, Quality gate: 0.00)", + "-> [Modified code lines - File Coverage]: ≪Success≫ - (Actual value: 75.00%, Quality gate: 0.00)", + "-> [Modified code lines - Line Coverage]: ≪Success≫ - (Actual value: 50.00%, Quality gate: 0.00)", + "-> [Modified files - File Coverage]: ≪Success≫ - (Actual value: 75.00%, Quality gate: 0.00)", + "-> [Modified files - Line Coverage]: ≪Success≫ - (Actual value: 50.00%, Quality gate: 0.00)", + "-> [Overall project (difference to reference job) - File Coverage]: ≪Success≫ - (Actual value: -10.00%, Quality gate: -10.00)", + "-> [Overall project (difference to reference job) - Line Coverage]: ≪Success≫ - (Actual value: +5.00%, Quality gate: -10.00)", + "-> [Modified code lines (difference to modified files) - File Coverage]: ≪Success≫ - (Actual value: -10.00%, Quality gate: -10.00)", + "-> [Modified code lines (difference to modified files) - Line Coverage]: ≪Success≫ - (Actual value: +5.00%, Quality gate: -10.00)", + "-> [Modified files (difference to overall project) - File Coverage]: ≪Success≫ - (Actual value: -10.00%, Quality gate: -10.00)", + "-> [Modified files (difference to overall project) - Line Coverage]: ≪Success≫ - (Actual value: +5.00%, Quality gate: -10.00)"); + } + + @Test + void shouldReportUnstableIfBelowThreshold() { + Collection qualityGates = new ArrayList<>(); + + qualityGates.add(new CoverageQualityGate(76.0, Metric.FILE, Baseline.PROJECT, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(51.0, Metric.LINE, Baseline.PROJECT, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(76.0, Metric.FILE, Baseline.MODIFIED_LINES, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(51.0, Metric.LINE, Baseline.MODIFIED_LINES, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(76.0, Metric.FILE, Baseline.MODIFIED_FILES, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(51.0, Metric.LINE, Baseline.MODIFIED_FILES, QualityGateCriticality.UNSTABLE)); + + var minimum = 10; + qualityGates.add(new CoverageQualityGate(minimum, Metric.FILE, Baseline.PROJECT_DELTA, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(minimum, Metric.LINE, Baseline.PROJECT_DELTA, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(minimum, Metric.FILE, Baseline.MODIFIED_LINES_DELTA, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(minimum, Metric.LINE, Baseline.MODIFIED_LINES_DELTA, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(minimum, Metric.FILE, Baseline.MODIFIED_FILES_DELTA, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(minimum, Metric.LINE, Baseline.MODIFIED_FILES_DELTA, QualityGateCriticality.UNSTABLE)); + + CoverageQualityGateEvaluator evaluator = new CoverageQualityGateEvaluator(qualityGates, createStatistics()); + QualityGateResult result = evaluator.evaluate(); + + assertThat(result).hasOverallStatus(QualityGateStatus.WARNING).isNotSuccessful().isNotInactive().hasMessages( + "-> [Overall project - File Coverage]: ≪Unstable≫ - (Actual value: 75.00%, Quality gate: 76.00)", + "-> [Overall project - Line Coverage]: ≪Unstable≫ - (Actual value: 50.00%, Quality gate: 51.00)", + "-> [Modified code lines - File Coverage]: ≪Unstable≫ - (Actual value: 75.00%, Quality gate: 76.00)", + "-> [Modified code lines - Line Coverage]: ≪Unstable≫ - (Actual value: 50.00%, Quality gate: 51.00)", + "-> [Modified files - File Coverage]: ≪Unstable≫ - (Actual value: 75.00%, Quality gate: 76.00)", + "-> [Modified files - Line Coverage]: ≪Unstable≫ - (Actual value: 50.00%, Quality gate: 51.00)", + "-> [Overall project (difference to reference job) - File Coverage]: ≪Unstable≫ - (Actual value: -10.00%, Quality gate: 10.00)", + "-> [Overall project (difference to reference job) - Line Coverage]: ≪Unstable≫ - (Actual value: +5.00%, Quality gate: 10.00)", + "-> [Modified code lines (difference to modified files) - File Coverage]: ≪Unstable≫ - (Actual value: -10.00%, Quality gate: 10.00)", + "-> [Modified code lines (difference to modified files) - Line Coverage]: ≪Unstable≫ - (Actual value: +5.00%, Quality gate: 10.00)", + "-> [Modified files (difference to overall project) - File Coverage]: ≪Unstable≫ - (Actual value: -10.00%, Quality gate: 10.00)", + "-> [Modified files (difference to overall project) - Line Coverage]: ≪Unstable≫ - (Actual value: +5.00%, Quality gate: 10.00)"); + } + + @Test + void shouldReportFailureIfBelowThreshold() { + Collection qualityGates = new ArrayList<>(); + + qualityGates.add(new CoverageQualityGate(76.0, Metric.FILE, Baseline.PROJECT, QualityGateCriticality.FAILURE)); + qualityGates.add(new CoverageQualityGate(51.0, Metric.LINE, Baseline.PROJECT, QualityGateCriticality.FAILURE)); + qualityGates.add(new CoverageQualityGate(76.0, Metric.FILE, Baseline.MODIFIED_LINES, QualityGateCriticality.FAILURE)); + qualityGates.add(new CoverageQualityGate(51.0, Metric.LINE, Baseline.MODIFIED_LINES, QualityGateCriticality.FAILURE)); + qualityGates.add(new CoverageQualityGate(76.0, Metric.FILE, Baseline.MODIFIED_FILES, QualityGateCriticality.FAILURE)); + qualityGates.add(new CoverageQualityGate(51.0, Metric.LINE, Baseline.MODIFIED_FILES, QualityGateCriticality.FAILURE)); + + var minimum = 10; + qualityGates.add(new CoverageQualityGate(minimum, Metric.FILE, Baseline.PROJECT_DELTA, QualityGateCriticality.FAILURE)); + qualityGates.add(new CoverageQualityGate(minimum, Metric.LINE, Baseline.PROJECT_DELTA, QualityGateCriticality.FAILURE)); + qualityGates.add(new CoverageQualityGate(minimum, Metric.FILE, Baseline.MODIFIED_LINES_DELTA, QualityGateCriticality.FAILURE)); + qualityGates.add(new CoverageQualityGate(minimum, Metric.LINE, Baseline.MODIFIED_LINES_DELTA, QualityGateCriticality.FAILURE)); + qualityGates.add(new CoverageQualityGate(minimum, Metric.FILE, Baseline.MODIFIED_FILES_DELTA, QualityGateCriticality.FAILURE)); + qualityGates.add(new CoverageQualityGate(minimum, Metric.LINE, Baseline.MODIFIED_FILES_DELTA, QualityGateCriticality.FAILURE)); + + CoverageQualityGateEvaluator evaluator = new CoverageQualityGateEvaluator(qualityGates, createStatistics()); + + QualityGateResult result = evaluator.evaluate(); + + assertThat(result).hasOverallStatus(QualityGateStatus.FAILED).isNotSuccessful().isNotInactive().hasMessages( + "-> [Overall project - File Coverage]: ≪Failed≫ - (Actual value: 75.00%, Quality gate: 76.00)", + "-> [Overall project - Line Coverage]: ≪Failed≫ - (Actual value: 50.00%, Quality gate: 51.00)", + "-> [Modified code lines - File Coverage]: ≪Failed≫ - (Actual value: 75.00%, Quality gate: 76.00)", + "-> [Modified code lines - Line Coverage]: ≪Failed≫ - (Actual value: 50.00%, Quality gate: 51.00)", + "-> [Modified files - File Coverage]: ≪Failed≫ - (Actual value: 75.00%, Quality gate: 76.00)", + "-> [Modified files - Line Coverage]: ≪Failed≫ - (Actual value: 50.00%, Quality gate: 51.00)", + "-> [Overall project (difference to reference job) - File Coverage]: ≪Failed≫ - (Actual value: -10.00%, Quality gate: 10.00)", + "-> [Overall project (difference to reference job) - Line Coverage]: ≪Failed≫ - (Actual value: +5.00%, Quality gate: 10.00)", + "-> [Modified code lines (difference to modified files) - File Coverage]: ≪Failed≫ - (Actual value: -10.00%, Quality gate: 10.00)", + "-> [Modified code lines (difference to modified files) - Line Coverage]: ≪Failed≫ - (Actual value: +5.00%, Quality gate: 10.00)", + "-> [Modified files (difference to overall project) - File Coverage]: ≪Failed≫ - (Actual value: -10.00%, Quality gate: 10.00)", + "-> [Modified files (difference to overall project) - Line Coverage]: ≪Failed≫ - (Actual value: +5.00%, Quality gate: 10.00)"); + } + + @Test + void shouldOverwriteStatus() { + Collection qualityGates = new ArrayList<>(); + + qualityGates.add(new CoverageQualityGate(76.0, Metric.FILE, Baseline.PROJECT, QualityGateCriticality.UNSTABLE)); + qualityGates.add(new CoverageQualityGate(51.0, Metric.LINE, Baseline.PROJECT, QualityGateCriticality.FAILURE)); + + CoverageQualityGateEvaluator evaluator = new CoverageQualityGateEvaluator(qualityGates, createStatistics()); + assertThatStatusWillBeOverwritten(evaluator); + } + + @Test + void shouldFailIfValueIsNotFound() { + Collection qualityGates = new ArrayList<>(); + + qualityGates.add(new CoverageQualityGate(50.0, Metric.PACKAGE, Baseline.PROJECT, QualityGateCriticality.FAILURE)); + + CoverageQualityGateEvaluator evaluator = new CoverageQualityGateEvaluator(qualityGates, createStatistics()); + QualityGateResult result = evaluator.evaluate(); + assertThat(result).hasOverallStatus(QualityGateStatus.FAILED).isNotSuccessful().hasMessages( + "-> [Overall project - Package Coverage]: ≪Failed≫ - (Actual value: n/a, Quality gate: 50.00)"); + } + + @Test + void shouldAddAllQualityGates() { + + Collection qualityGates = List.of( + new CoverageQualityGate(76.0, Metric.FILE, Baseline.PROJECT, QualityGateCriticality.UNSTABLE), + new CoverageQualityGate(51.0, Metric.LINE, Baseline.PROJECT, QualityGateCriticality.FAILURE)); + + CoverageQualityGateEvaluator evaluator = new CoverageQualityGateEvaluator(qualityGates, createStatistics()); + + assertThatStatusWillBeOverwritten(evaluator); + } + + private static void assertThatStatusWillBeOverwritten(final CoverageQualityGateEvaluator evaluator) { + QualityGateResult result = evaluator.evaluate(); + assertThat(result).hasOverallStatus(QualityGateStatus.FAILED).isNotSuccessful().hasMessages( + "-> [Overall project - File Coverage]: ≪Unstable≫ - (Actual value: 75.00%, Quality gate: 76.00)", + "-> [Overall project - Line Coverage]: ≪Failed≫ - (Actual value: 50.00%, Quality gate: 51.00)"); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorderTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorderTest.java new file mode 100644 index 000000000..9d507d0a9 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorderTest.java @@ -0,0 +1,64 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import org.junit.jupiter.api.Test; + +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import hudson.model.Result; +import hudson.model.Run; + +import io.jenkins.plugins.util.IntegrationTestWithJenkinsPerSuite; + +import static org.assertj.core.api.Assertions.*; + +/** + * Tests the class {@link CoverageRecorder}. + * + * @author Ullrich Hafner + */ +class CoverageRecorderTest extends IntegrationTestWithJenkinsPerSuite { + @Test + void shouldIgnoreEmptyListOfFiles() { + WorkflowJob job = createPipeline(); + job.setDefinition(new CpsFlowDefinition( + "node {\n" + + " recordCoverage tools: [[parser: 'JACOCO']]\n" + + " }\n", true)); + + Run run = buildWithResult(job, Result.SUCCESS); + + assertThat(getConsoleLog(run)) + .contains("[JaCoCo] Using default pattern '**/jacoco.xml' since user defined pattern is not set", + "[-ERROR-] No files found for pattern '**/jacoco.xml'. Configuration error?") + .containsPattern("Searching for all files in '.*' that match the pattern '\\*\\*/jacoco.xml'") + .doesNotContain("Expanding pattern"); + } + + @Test + void shouldParseFileWithJaCoCo() { + WorkflowJob job = createPipeline(); + copyFilesToWorkspace(job, "jacoco.xml"); + job.setDefinition(new CpsFlowDefinition( + "node {\n" + + " recordCoverage tools: [[parser: 'JACOCO']]\n" + + " }\n", true)); + + Run run = buildWithResult(job, Result.SUCCESS); + + assertThat(getConsoleLog(run)) + .contains("[JaCoCo] Using default pattern '**/jacoco.xml' since user defined pattern is not set", + "[JaCoCo] -> found 1 file", + "[JaCoCo] MODULE: 100.00% (1/1)", + "[JaCoCo] PACKAGE: 100.00% (1/1)", + "[JaCoCo] FILE: 70.00% (7/10)", + "[JaCoCo] CLASS: 83.33% (15/18)", + "[JaCoCo] METHOD: 95.10% (97/102)", + "[JaCoCo] INSTRUCTION: 93.33% (1260/1350)", + "[JaCoCo] LINE: 91.02% (294/323)", + "[JaCoCo] BRANCH: 93.97% (109/116)", + "[JaCoCo] COMPLEXITY: 160") + .containsPattern("Searching for all files in '.*' that match the pattern '\\*\\*/jacoco.xml'") + .containsPattern("Successfully parsed file .*/jacoco.xml") + .doesNotContain("Expanding pattern"); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModelTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModelTest.java new file mode 100644 index 000000000..6c9cdfa2f --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModelTest.java @@ -0,0 +1,114 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.List; +import java.util.NoSuchElementException; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; + +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.coverage.ModuleNode; +import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.util.FilteredLog; + +import hudson.model.Run; + +import io.jenkins.plugins.coverage.metrics.AbstractCoverageTest; +import io.jenkins.plugins.util.QualityGateResult; + +import static io.jenkins.plugins.coverage.metrics.steps.CoverageViewModel.*; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Tests the class {@link CoverageViewModel}. + * + * @author Ullrich Hafner + * @author Florian Orendi + */ +@SuppressWarnings("PMD.TooManyStaticImports") +class CoverageViewModelTest extends AbstractCoverageTest { + @Test + void shouldReturnEmptySourceViewForExistingLinkButMissingSourceFile() { + CoverageViewModel model = createModelFromCodingStyleReport(); + + String hash = String.valueOf("PathUtil.java".hashCode()); + assertThat(model.getSourceCode(hash, ABSOLUTE_COVERAGE_TABLE_ID)).isEqualTo("n/a"); + assertThat(model.getSourceCode(hash, MODIFIED_LINES_COVERAGE_TABLE_ID)).isEqualTo("n/a"); + assertThat(model.getSourceCode(hash, INDIRECT_COVERAGE_TABLE_ID)).isEqualTo("n/a"); + } + + @Test + void shouldReportOverview() { + CoverageViewModel model = createModelFromCodingStyleReport(); + + CoverageOverview overview = model.getOverview(); + + var expectedMetrics = new String[] {"Package", "File", "Class", "Method", "Line", "Branch", "Instruction"}; + assertThat(overview.getMetrics()).containsExactly(expectedMetrics); + + var expectedCovered = List.of(4, 7, 15, 97, 294, 109, 1260); + assertThat(overview.getCovered()).containsExactlyElementsOf(expectedCovered); + ensureValidPercentages(overview.getCoveredPercentages()); + + var expectedMissed = List.of(0, 3, 3, 5, 29, 7, 90); + assertThat(overview.getMissed()).containsExactlyElementsOf(expectedMissed); + ensureValidPercentages(overview.getMissedPercentages()); + + assertThatJson(overview).node("metrics").isArray().containsExactly(expectedMetrics); + assertThatJson(overview).node("covered").isArray().containsExactlyElementsOf(expectedCovered); + assertThatJson(overview).node("missed").isArray().containsExactlyElementsOf(expectedMissed); + } + + private static void ensureValidPercentages(final List percentages) { + assertThat(percentages).allSatisfy(d -> + assertThat(d).isLessThanOrEqualTo(100.0).isGreaterThanOrEqualTo(0.0)); + } + + @Test + void shouldProvideIndirectCoverageChanges() { + Node node = createIndirectCoverageChangesNode(); + + CoverageViewModel model = createModel(node); + + assertThat(model.hasIndirectCoverageChanges()).isTrue(); + } + + private Node createIndirectCoverageChangesNode() { + var root = new ModuleNode("root"); + for (int file = 0; file < 5; file++) { + var fileNode = new FileNode("File-" + file); + + for (int line = 0; line < 2; line++) { + fileNode.addCounters(10 + line, 1, 1); + fileNode.addIndirectCoverageChange(10 + line, 2); + } + root.addChild(fileNode); + } + return root; + } + + @Test + void shouldProvideRightTableModelById() { + CoverageViewModel model = createModelFromCodingStyleReport(); + assertThat(model.getTableModel(MODIFIED_LINES_COVERAGE_TABLE_ID)).isInstanceOf(ModifiedLinesCoverageTableModel.class); + assertThat(model.getTableModel(INDIRECT_COVERAGE_TABLE_ID)).isInstanceOf(IndirectCoverageChangesTable.class); + assertThat(model.getTableModel(ABSOLUTE_COVERAGE_TABLE_ID)).isInstanceOf(CoverageTableModel.class); + + assertThatExceptionOfType(NoSuchElementException.class) + .isThrownBy(() -> model.getTableModel("wrong-id")); + } + + private CoverageViewModel createModelFromCodingStyleReport() { + var model = createModel(readJacocoResult("jacoco-codingstyle.xml")); + assertThat(model.getDisplayName()).contains("'Java coding style'"); + return model; + } + + private CoverageViewModel createModel(final Node node) { + return new CoverageViewModel(mock(Run.class), "id", StringUtils.EMPTY, + node, AbstractCoverageTest.createStatistics(), new QualityGateResult(), "-", new FilteredLog("Errors"), + i -> i); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageXmlStreamTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageXmlStreamTest.java new file mode 100644 index 000000000..77451c872 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageXmlStreamTest.java @@ -0,0 +1,251 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Path; +import java.util.List; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.Fraction; +import org.junit.jupiter.api.Test; +import org.xmlunit.builder.Input; + +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.coverage.Value; +import edu.hm.hafner.coverage.parser.JacocoParser; +import edu.hm.hafner.util.FilteredLog; +import edu.hm.hafner.util.SerializableTest; + +import hudson.XmlFile; +import hudson.model.FreeStyleBuild; +import hudson.util.XStream2; + +import io.jenkins.plugins.coverage.metrics.Assertions; +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.coverage.metrics.steps.CoverageXmlStream.IntegerLineMapConverter; +import io.jenkins.plugins.coverage.metrics.steps.CoverageXmlStream.IntegerSetConverter; +import io.jenkins.plugins.coverage.metrics.steps.CoverageXmlStream.MetricFractionMapConverter; +import io.jenkins.plugins.util.QualityGateResult; + +import static edu.hm.hafner.coverage.Metric.*; +import static org.assertj.core.api.BDDAssertions.*; +import static org.mockito.Mockito.*; +import static org.xmlunit.assertj.XmlAssert.assertThat; + +/** + * Tests the class {@link CoverageXmlStream}. + * + * @author Ullrich Hafner + */ +@SuppressWarnings("checkstyle:ClassDataAbstractionCoupling") +class CoverageXmlStreamTest extends SerializableTest { + private static final String ACTION_QUALIFIED_NAME = "io.jenkins.plugins.coverage.metrics.steps.CoverageBuildAction"; + private static final String EMPTY = "[]"; + + @Override + protected Node createSerializable() { + return new JacocoParser().parse(new InputStreamReader(asInputStream("jacoco-codingstyle.xml")), new FilteredLog("Errors")); + } + + @Test + void shouldSaveAndRestoreTree() throws IOException { + Path saved = createTempFile(); + Node convertedNode = createSerializable(); + + var xmlStream = new CoverageXmlStream(); + xmlStream.write(saved, convertedNode); + Node restored = xmlStream.read(saved); + + Assertions.assertThat(restored).usingRecursiveComparison().isEqualTo(convertedNode); + + var xml = Input.from(saved); + assertThat(xml).nodesByXPath("//module/values/*") + .hasSize(4).extractingText() + .containsExactly("INSTRUCTION: 1260/1350", + "BRANCH: 109/116", + "LINE: 294/323", + "COMPLEXITY: 160"); + assertThat(xml).nodesByXPath("//module/values/*") + .hasSize(4).extractingText() + .containsExactly("INSTRUCTION: 1260/1350", + "BRANCH: 109/116", + "LINE: 294/323", + "COMPLEXITY: 160"); + assertThat(xml).nodesByXPath("//file[./name = 'TreeStringBuilder.java']/values/*") + .hasSize(4).extractingText() + .containsExactly("INSTRUCTION: 229/233", "BRANCH: 17/18", "LINE: 51/53", "COMPLEXITY: 23"); + assertThat(xml).nodesByXPath("//file[./name = 'TreeStringBuilder.java']/coveredPerLine") + .hasSize(1).extractingText() + .containsExactly( + "[19: 1, 20: 1, 31: 1, 43: 1, 50: 1, 51: 1, 54: 1, 57: 1, 61: 0, 62: 0, 70: 1, 72: 1, 73: 1, 74: 1, 85: 2, 86: 1, 89: 1, 90: 2, 91: 1, 92: 2, 93: 2, 95: 1, 96: 1, 97: 1, 100: 1, 101: 1, 103: 1, 106: 1, 109: 1, 112: 1, 113: 1, 114: 1, 115: 1, 117: 1, 125: 2, 126: 1, 128: 1, 140: 1, 142: 1, 143: 1, 144: 1, 146: 1, 160: 1, 162: 2, 163: 2, 164: 1, 167: 1, 177: 1, 178: 2, 179: 1, 180: 1, 181: 1, 184: 1]"); + assertThat(xml).nodesByXPath("//file[./name = 'TreeStringBuilder.java']/missedPerLine") + .hasSize(1).extractingText() + .containsExactly( + "[19: 0, 20: 0, 31: 0, 43: 0, 50: 0, 51: 0, 54: 0, 57: 0, 61: 1, 62: 1, 70: 0, 72: 0, 73: 0, 74: 0, 85: 0, 86: 0, 89: 0, 90: 0, 91: 0, 92: 0, 93: 0, 95: 0, 96: 0, 97: 0, 100: 0, 101: 0, 103: 0, 106: 0, 109: 0, 112: 0, 113: 1, 114: 0, 115: 0, 117: 0, 125: 0, 126: 0, 128: 0, 140: 0, 142: 0, 143: 0, 144: 0, 146: 0, 160: 0, 162: 0, 163: 0, 164: 0, 167: 0, 177: 0, 178: 0, 179: 0, 180: 0, 181: 0, 184: 0]"); + } + + @Test + void shouldStoreActionCompactly() throws IOException { + Path saved = createTempFile(); + var xmlStream = new TestXmlStream(); + xmlStream.read(saved); + + var file = new XmlFile(xmlStream.getStream(), saved.toFile()); + file.write(createAction()); + + assertThat(Input.from(saved)).nodesByXPath("//" + ACTION_QUALIFIED_NAME + "/projectValues/*") + .hasSize(11).extractingText() + .containsExactly("MODULE: 1/1", + "PACKAGE: 1/1", + "FILE: 7/10", + "CLASS: 15/18", + "METHOD: 97/102", + "LINE: 294/323", + "BRANCH: 109/116", + "INSTRUCTION: 1260/1350", + "COMPLEXITY: 160", + "COMPLEXITY_DENSITY: 160/323", + "LOC: 323"); + + assertThat(Input.from(saved)).nodesByXPath("//" + ACTION_QUALIFIED_NAME + "/projectValues/coverage") + .hasSize(8).extractingText() + .containsExactly("MODULE: 1/1", + "PACKAGE: 1/1", + "FILE: 7/10", + "CLASS: 15/18", + "METHOD: 97/102", + "LINE: 294/323", + "BRANCH: 109/116", + "INSTRUCTION: 1260/1350"); + + var action = file.read(); + assertThat(action).isNotNull().isInstanceOfSatisfying(CoverageBuildAction.class, a -> + Assertions.assertThat(serializeValues(a)) + .containsExactly("MODULE: 1/1", + "PACKAGE: 1/1", + "FILE: 7/10", + "CLASS: 15/18", + "METHOD: 97/102", + "LINE: 294/323", + "BRANCH: 109/116", + "INSTRUCTION: 1260/1350", + "COMPLEXITY: 160", + "COMPLEXITY_DENSITY: 160/323", + "LOC: 323" + )); + } + + private static List serializeValues(final CoverageBuildAction a) { + return a.getAllValues(Baseline.PROJECT).stream() + .map(Value::serialize) + .collect(Collectors.toList()); + } + + @Test + void shouldConvertMetricMap2String() { + NavigableMap map = new TreeMap<>(); + + MetricFractionMapConverter converter = new MetricFractionMapConverter(); + + assertThat(converter.marshal(map)).isEqualTo(EMPTY); + + map.put(BRANCH, Fraction.getFraction(50, 100)); + assertThat(converter.marshal(map)).isEqualTo("[BRANCH: 50/100]"); + + map.put(LINE, Fraction.getFraction(3, 4)); + assertThat(converter.marshal(map)).isEqualTo("[LINE: 3/4, BRANCH: 50/100]"); + } + + @Test + void shouldConvertString2MetricMap() { + MetricFractionMapConverter converter = new MetricFractionMapConverter(); + + Assertions.assertThat(converter.unmarshal(EMPTY)).isEmpty(); + Fraction first = Fraction.getFraction(50, 100); + Assertions.assertThat(converter.unmarshal("[BRANCH: 50/100]")) + .containsExactly(entry(BRANCH, first)); + Assertions.assertThat(converter.unmarshal("[LINE: 3/4, BRANCH: 50/100]")) + .containsExactly(entry(LINE, Fraction.getFraction(3, 4)), + entry(BRANCH, first)); + } + + @Test + void shouldConvertIntegerMap2String() { + NavigableMap map = new TreeMap<>(); + + IntegerLineMapConverter converter = new IntegerLineMapConverter(); + + assertThat(converter.marshal(map)).isEqualTo(EMPTY); + + map.put(10, 20); + assertThat(converter.marshal(map)).isEqualTo("[10: 20]"); + + map.put(15, 25); + assertThat(converter.marshal(map)).isEqualTo("[10: 20, 15: 25]"); + } + + @Test + void shouldConvertString2IntegerMap() { + IntegerLineMapConverter converter = new IntegerLineMapConverter(); + + Assertions.assertThat(converter.unmarshal(EMPTY)).isEmpty(); + Assertions.assertThat(converter.unmarshal("[15: 25]")).containsExactly(entry(15, 25)); + Assertions.assertThat(converter.unmarshal("[15:25, 10: 20]")).containsExactly(entry(10, 20), entry(15, 25)); + } + + @Test + void shouldConvertIntegerSet2String() { + NavigableSet set = new TreeSet<>(); + + IntegerSetConverter converter = new IntegerSetConverter(); + + assertThat(converter.marshal(set)).isEqualTo(EMPTY); + + set.add(10); + assertThat(converter.marshal(set)).isEqualTo("[10]"); + + set.add(15); + assertThat(converter.marshal(set)).isEqualTo("[10, 15]"); + } + + @Test + void shouldConvertString2IntegerSet() { + IntegerSetConverter converter = new IntegerSetConverter(); + + Assertions.assertThat(converter.unmarshal(EMPTY)).isEmpty(); + Assertions.assertThat(converter.unmarshal("[15]")).containsExactly(15); + Assertions.assertThat(converter.unmarshal("[15, 20]")).containsExactly(15, 20); + } + + // TODO: Add content for the other baselines as well + CoverageBuildAction createAction() { + var tree = createSerializable(); + + return new CoverageBuildAction(mock(FreeStyleBuild.class), CoverageRecorder.DEFAULT_ID, StringUtils.EMPTY, + StringUtils.EMPTY, + tree, new QualityGateResult(), new FilteredLog("Test"), "-", + new TreeMap<>(), List.of(), new TreeMap<>(), List.of(), + new TreeMap<>(), List.of(), false); + } + + private static class TestXmlStream extends CoverageXmlStream { + private XStream2 xStream; + + @Override + protected void configureXStream(final XStream2 xStream2) { + super.configureXStream(xStream2); + + this.xStream = xStream2; + } + + public XStream2 getStream() { + return xStream; + } + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/DeltaComputationITest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/DeltaComputationITest.java new file mode 100644 index 000000000..76c0203e9 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/DeltaComputationITest.java @@ -0,0 +1,133 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import org.junit.jupiter.api.Test; + +import edu.hm.hafner.coverage.Coverage.CoverageBuilder; +import edu.hm.hafner.coverage.CyclomaticComplexity; +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.coverage.LinesOfCode; +import edu.hm.hafner.coverage.Node; + +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import hudson.model.FreeStyleProject; +import hudson.model.Run; + +import io.jenkins.plugins.coverage.metrics.AbstractCoverageITest; +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.coverage.metrics.steps.CoverageTool.Parser; + +import static edu.hm.hafner.coverage.Metric.*; +import static io.jenkins.plugins.coverage.metrics.AbstractCoverageTest.*; +import static org.assertj.core.api.Assertions.*; + +/** + * Integration test for delta computation of reference builds. + */ +class DeltaComputationITest extends AbstractCoverageITest { + @Test + void shouldComputeDeltaInFreestyleJob() { + FreeStyleProject project = createFreestyleJob(Parser.JACOCO, + JACOCO_ANALYSIS_MODEL_FILE, JACOCO_CODING_STYLE_FILE); + + Run firstBuild = buildSuccessfully(project); + verifyFirstBuild(firstBuild); + + // update parser pattern to pick only the coding style results + project.getPublishersList().get(CoverageRecorder.class).getTools().get(0).setPattern(JACOCO_CODING_STYLE_FILE); + + Run secondBuild = buildSuccessfully(project); + verifySecondBuild(secondBuild); + + verifyDeltaComputation(firstBuild, secondBuild); + } + + @Test + void shouldComputeDeltaInPipeline() { + WorkflowJob job = createPipeline(Parser.JACOCO, JACOCO_ANALYSIS_MODEL_FILE, JACOCO_CODING_STYLE_FILE); + + Run firstBuild = buildSuccessfully(job); + verifyFirstBuild(firstBuild); + + // update parser pattern to pick only the codingstyle results + setPipelineScript(job, + "recordCoverage tools: [[parser: 'JACOCO', pattern: '" + JACOCO_CODING_STYLE_FILE + "']]"); + + Run secondBuild = buildSuccessfully(job); + verifySecondBuild(secondBuild); + + verifyDeltaComputation(firstBuild, secondBuild); + } + + private static void verifyFirstBuild(final Run firstBuild) { + var action = firstBuild.getAction(CoverageBuildAction.class); + + var builder = new CoverageBuilder(); + assertThat(action.getAllValues(Baseline.PROJECT)).contains( + builder.setMetric(LINE) + .setCovered(JACOCO_ANALYSIS_MODEL_COVERED + JACOCO_CODING_STYLE_COVERED) + .setMissed(JACOCO_ANALYSIS_MODEL_MISSED + JACOCO_CODING_STYLE_MISSED) + .build(), + builder.setMetric(BRANCH) + .setCovered(1544 + 109) + .setMissed(1865 - (1544 + 109)) + .build(), + new LinesOfCode(JACOCO_ANALYSIS_MODEL_TOTAL + JACOCO_CODING_STYLE_TOTAL), + new CyclomaticComplexity(2718)); + } + + private static void verifySecondBuild(final Run secondBuild) { + var action = secondBuild.getAction(CoverageBuildAction.class); + + var builder = new CoverageBuilder(); + assertThat(action.getAllValues(Baseline.PROJECT)).contains( + builder.setMetric(LINE) + .setCovered(JACOCO_CODING_STYLE_COVERED) + .setMissed(JACOCO_CODING_STYLE_MISSED) + .build(), + builder.setMetric(BRANCH) + .setCovered(109) + .setMissed(7) + .build(), + new LinesOfCode(JACOCO_CODING_STYLE_TOTAL), + new CyclomaticComplexity(160)); + } + + /** + * Verifies the coverageComputation of the first and second build of the job. + * + * @param firstBuild + * of the project which is used as a reference + * @param secondBuild + * of the project + */ + private void verifyDeltaComputation(final Run firstBuild, final Run secondBuild) { + assertThat(secondBuild.getAction(CoverageBuildAction.class)).isNotNull(); + + CoverageBuildAction action = secondBuild.getAction(CoverageBuildAction.class); + + assertThat(action).isNotNull(); + assertThat(action.getReferenceBuild()) + .isPresent() + .satisfies(reference -> assertThat(reference.get()).isEqualTo(firstBuild)); + + assertThat(action.formatDelta(Baseline.PROJECT, LINE)).isEqualTo("-4.14%"); + assertThat(action.formatDelta(Baseline.PROJECT, BRANCH)).isEqualTo("+5.33%"); + assertThat(action.formatDelta(Baseline.PROJECT, LOC)).isEqualTo(String.valueOf(-JACOCO_ANALYSIS_MODEL_TOTAL)); + assertThat(action.formatDelta(Baseline.PROJECT, COMPLEXITY)).isEqualTo(String.valueOf(160 - 2718)); + + verifyModifiedLinesCoverage(action); + } + + /** + * Verifies the calculated modified lines coverage including the modified lines coverage delta and the code delta. This makes sure + * these metrics are set properly even if there are no code changes. + * + * @param action + * The created Jenkins action + */ + private void verifyModifiedLinesCoverage(final CoverageBuildAction action) { + Node root = action.getResult(); + assertThat(root).isNotNull(); + assertThat(root.getAllFileNodes()).flatExtracting(FileNode::getModifiedLines).isEmpty(); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/FileChangesProcessorTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/FileChangesProcessorTest.java new file mode 100644 index 000000000..ebdc5352e --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/FileChangesProcessorTest.java @@ -0,0 +1,158 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.AbstractMap.SimpleEntry; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.math.Fraction; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Node; + +import io.jenkins.plugins.coverage.metrics.AbstractCoverageTest; +import io.jenkins.plugins.forensics.delta.Change; +import io.jenkins.plugins.forensics.delta.ChangeEditType; +import io.jenkins.plugins.forensics.delta.FileChanges; +import io.jenkins.plugins.forensics.delta.FileEditType; + +import static org.assertj.core.api.Assertions.*; + +/** + * Test class for {@link FileChangesProcessor}. + * + * @author Florian Orendi + */ +class FileChangesProcessorTest extends AbstractCoverageTest { + private static final String TEST_FILE_1 = "Test1.java"; + private static final String TEST_FILE_2 = "Main.java"; + private static final String TEST_FILE_1_PATH = "test/example/" + TEST_FILE_1; + private static final String TEST_FILE_1_PATH_OLD = "test/example/old/" + TEST_FILE_1; + + /** + * A JaCoCo report which contains the code coverage of a test project before the {@link #CODE_CHANGES} has + * been inserted. + */ + private static final String TEST_REPORT_BEFORE = "file-changes-test-before.xml"; + /** + * A JaCoCo report which contains the code coverage of a test project after the {@link #CODE_CHANGES} has + * been inserted. + */ + private static final String TEST_REPORT_AFTER = "file-changes-test-after.xml"; + + /** + * The code changes that took place between the generation of {@link #TEST_REPORT_BEFORE} and {@link + * #TEST_REPORT_AFTER}. + */ + private static final Map CODE_CHANGES = new HashMap<>(); + + /** + * The mapping of the used paths between the generation of {@link #TEST_REPORT_BEFORE} and {@link + * #TEST_REPORT_AFTER}. + */ + private static final Map OLD_PATH_MAPPING = new HashMap<>(); + + /** + * Initializes a map with the inserted {@link #CODE_CHANGES}. + */ + @BeforeAll + static void initFileChanges() { + Change insert1 = new Change(ChangeEditType.INSERT, 4, 4, 5, 9); + Change insert2 = new Change(ChangeEditType.INSERT, 8, 8, 14, 18); + Change insert3 = new Change(ChangeEditType.INSERT, 25, 25, 33, 36); + Change replace = new Change(ChangeEditType.REPLACE, 10, 11, 20, 22); + Change delete = new Change(ChangeEditType.DELETE, 16, 19, 26, 26); + FileChanges fileChanges = new FileChanges(TEST_FILE_1_PATH, TEST_FILE_1_PATH_OLD, + "test", FileEditType.RENAME, new HashMap<>()); + fileChanges.addChange(insert1); + fileChanges.addChange(insert2); + fileChanges.addChange(insert3); + fileChanges.addChange(replace); + fileChanges.addChange(delete); + CODE_CHANGES.put(TEST_FILE_1_PATH, fileChanges); + CODE_CHANGES.put(TEST_FILE_2, + new FileChanges("empty", "empty", "", FileEditType.MODIFY, new HashMap<>())); + OLD_PATH_MAPPING.put(TEST_FILE_1_PATH, TEST_FILE_1_PATH_OLD); + } + + @Test + void shouldAttachChangesCodeLines() { + FileChangesProcessor fileChangesProcessor = createFileChangesProcessor(); + Node tree = readJacocoResult(TEST_REPORT_AFTER); + fileChangesProcessor.attachChangedCodeLines(tree, CODE_CHANGES); + + assertThat(tree.findByHashCode(Metric.FILE, TEST_FILE_1_PATH.hashCode())) + .isNotEmpty() + .satisfies(node -> assertThat(node.get()) + .isInstanceOfSatisfying(FileNode.class, f -> assertThat(f.getModifiedLines()) + .containsExactly( + 5, 6, 7, 8, 9, 14, 15, 16, 17, 18, 20, 21, 22, 33, 34, 35, 36))); + assertThat(tree.findByHashCode(Metric.FILE, TEST_FILE_2.hashCode())) + .isNotEmpty() + .satisfies(node -> assertThat(node.get()) + .isInstanceOfSatisfying(FileNode.class, f -> assertThat(f.getModifiedLines()) + .isEmpty())); + } + + @Test + void shouldAttachFileCoverageDelta() { + FileChangesProcessor fileChangesProcessor = createFileChangesProcessor(); + Node reference = readJacocoResult(TEST_REPORT_BEFORE); + Node tree = readJacocoResult(TEST_REPORT_AFTER); + fileChangesProcessor.attachFileCoverageDeltas(tree, reference, OLD_PATH_MAPPING); + + assertThat(tree.findByHashCode(Metric.FILE, TEST_FILE_1_PATH.hashCode())) + .isNotEmpty() + .satisfies(node -> { + assertThat(node.get()).isInstanceOf(FileNode.class); + verifyFileCoverageDeltaOfTestFile1((FileNode) node.get()); + }); + } + + /** + * Verifies the file coverage delta of {@link #TEST_FILE_1}. + * + * @param file + * The referencing coverage tree {@link FileNode node} + */ + private void verifyFileCoverageDeltaOfTestFile1(final FileNode file) { + assertThat(file.getName()).isEqualTo(TEST_FILE_1); + assertThat(file.getDelta(Metric.LINE)).isEqualTo(Fraction.getFraction(3, 117)); + assertThat(file.getDelta(Metric.BRANCH)).isEqualTo(Fraction.getFraction(3, 24)); + assertThat(file.getDelta(Metric.INSTRUCTION)).isEqualTo(Fraction.getFraction(90, 999)); + assertThat(file.getDelta(Metric.METHOD)).isEqualTo(Fraction.getFraction(-4, 30)); + assertThat(file.getDelta(Metric.CLASS)).isEqualTo(Fraction.ZERO); + assertThat(file.getDelta(Metric.FILE)).isEqualTo(Fraction.ZERO); + } + + @Test + void shouldAttachIndirectCoverageChanges() { + FileChangesProcessor fileChangesProcessor = createFileChangesProcessor(); + Node reference = readJacocoResult(TEST_REPORT_BEFORE); + Node tree = readJacocoResult(TEST_REPORT_AFTER); + fileChangesProcessor.attachIndirectCoveragesChanges(tree, reference, CODE_CHANGES, OLD_PATH_MAPPING); + + assertThat(tree.findByHashCode(Metric.FILE, TEST_FILE_1_PATH.hashCode())) + .isNotEmpty() + .satisfies(node -> { + assertThat(node.get()).isInstanceOf(FileNode.class); + FileNode file = (FileNode) node.get(); + assertThat(file.getIndirectCoverageChanges()).containsExactly( + new SimpleEntry<>(11, -1), + new SimpleEntry<>(29, -1), + new SimpleEntry<>(31, 1) + ); + }); + } + + /** + * Creates an instance of {@link FileChangesProcessor}. + * + * @return the created instance + */ + private FileChangesProcessor createFileChangesProcessor() { + return new FileChangesProcessor(); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/GitForensicsITest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/GitForensicsITest.java new file mode 100644 index 000000000..905acea54 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/GitForensicsITest.java @@ -0,0 +1,269 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import edu.hm.hafner.coverage.Coverage; +import edu.hm.hafner.coverage.Coverage.CoverageBuilder; +import edu.hm.hafner.coverage.FileNode; + +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.flow.FlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import hudson.model.FreeStyleProject; +import hudson.model.Node; +import hudson.model.Run; +import hudson.plugins.git.BranchSpec; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.extensions.impl.RelativeTargetDirectory; + +import io.jenkins.plugins.coverage.metrics.AbstractCoverageITest; +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.coverage.metrics.steps.CoverageTool.Parser; +import io.jenkins.plugins.prism.SourceCodeRetention; + +import static edu.hm.hafner.coverage.Metric.*; +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assumptions.*; + +/** + * Tests the integration of the Forensics API Plugin while using its Git implementation. + * + * @author Florian Orendi + */ +@Testcontainers(disabledWithoutDocker = true) +class GitForensicsITest extends AbstractCoverageITest { + /** The JaCoCo coverage report, generated for the commit {@link #COMMIT}. */ + private static final String JACOCO_FILE = "forensics_integration.xml"; + /** The JaCoCo coverage report, generated for the reference commit {@link #COMMIT_REFERENCE}. */ + private static final String JACOCO_REFERENCE_FILE = "forensics_integration_reference.xml"; + + private static final String COMMIT = "518eebd"; + private static final String COMMIT_REFERENCE = "fd43cd0"; + + private static final String REPOSITORY = "https://github.com/jenkinsci/forensics-api-plugin.git"; + + @Container + private static final AgentContainer AGENT_CONTAINER = new AgentContainer(); + + @ParameterizedTest(name = "Source code retention {0} should store {1} files") + @CsvSource({ + "EVERY_BUILD, 37", + "MODIFIED, 2" + }) + @DisplayName("Should compute delta report and store selected source files") + void shouldComputeDeltaInPipelineOnDockerAgent(final SourceCodeRetention sourceCodeRetention, + final int expectedNumberOfFilesToBeStored) { + assumeThat(isWindows()).as("Running on Windows").isFalse(); + + Node agent = createDockerAgent(AGENT_CONTAINER); + String node = "node('" + DOCKER_AGENT_NAME + "')"; + WorkflowJob project = createPipeline(); + copySingleFileToAgentWorkspace(agent, project, JACOCO_REFERENCE_FILE, JACOCO_REFERENCE_FILE); + copySingleFileToAgentWorkspace(agent, project, JACOCO_FILE, JACOCO_FILE); + + project.setDefinition(createPipelineForCommit(node, COMMIT_REFERENCE, JACOCO_REFERENCE_FILE)); + Run referenceBuild = buildSuccessfully(project); + verifyGitRepositoryForCommit(referenceBuild, COMMIT_REFERENCE); + + project.setDefinition(createPipelineForCommit(node, COMMIT, JACOCO_FILE, sourceCodeRetention)); + Run build = buildSuccessfully(project); + verifyGitRepositoryForCommit(build, COMMIT); + + System.out.println(getConsoleLog(build)); + verifyGitIntegration(build, referenceBuild); + + assertThat(getConsoleLog(build)).contains( + "[Coverage] -> 18 files contain changes", + "[Coverage] Painting " + expectedNumberOfFilesToBeStored + " source files on agent"); + } + + @Test + void shouldComputeDeltaInFreestyleJobOnDockerAgent() throws IOException { + assumeThat(isWindows()).as("Running on Windows").isFalse(); + + Node agent = createDockerAgent(AGENT_CONTAINER); + FreeStyleProject project = createFreestyleJob(Parser.JACOCO); + project.setAssignedNode(agent); + + configureGit(project, COMMIT_REFERENCE); + addCoverageRecorder(project, Parser.JACOCO, JACOCO_REFERENCE_FILE); + + copySingleFileToAgentWorkspace(agent, project, JACOCO_FILE, JACOCO_FILE); + copySingleFileToAgentWorkspace(agent, project, JACOCO_REFERENCE_FILE, JACOCO_REFERENCE_FILE); + + Run referenceBuild = buildSuccessfully(project); + + configureGit(project, COMMIT); + addCoverageRecorder(project, Parser.JACOCO, JACOCO_FILE); + + Run build = buildSuccessfully(project); + + verifyGitIntegration(build, referenceBuild); + } + + /** + * Verifies the Git repository for the commit with the passed ID. + * + * @param build + * The current build + * @param commit + * The commit ID + */ + private void verifyGitRepositoryForCommit(final Run build, final String commit) { + String consoleLog = getConsoleLog(build); + assertThat(consoleLog) + .contains("Recording commits of 'git " + REPOSITORY) + .contains("Checking out Revision " + commit); + } + + /** + * Verifies the Git integration. + * + * @param build + * The current build + * @param referenceBuild + * The reference build + */ + private void verifyGitIntegration(final Run build, final Run referenceBuild) { + CoverageBuildAction action = build.getAction(CoverageBuildAction.class); + assertThat(action).isNotNull(); + assertThat(action.getReferenceBuild()) + .isPresent() + .satisfies(reference -> + assertThat(reference.get().getExternalizableId()).isEqualTo( + referenceBuild.getExternalizableId())); + verifyCodeDelta(action); + verifyCoverage(action); + } + + /** + * Verifies the calculated coverage for the most important metrics line and branch coverage. + * + * @param action + * The created Jenkins action + */ + private void verifyCoverage(final CoverageBuildAction action) { + verifyOverallCoverage(action); + verifyModifiedLinesCoverage(action); + verifyIndirectCoverageChanges(action); + } + + /** + * Verifies the calculated overall coverage including the coverage delta. + * + * @param action + * The created Jenkins action + */ + private void verifyOverallCoverage(final CoverageBuildAction action) { + var builder = new CoverageBuilder(); + assertThat(action.getAllValues(Baseline.PROJECT)).contains( + builder.setMetric(LINE).setCovered(529).setMissed(408).build(), + builder.setMetric(BRANCH).setCovered(136).setMissed(94).build()); + } + + /** + * Verifies the calculated modified lines coverage including the modified lines coverage delta. + * + * @param action + * The created Jenkins action + */ + private void verifyModifiedLinesCoverage(final CoverageBuildAction action) { + var builder = new CoverageBuilder(); + assertThat(action.getAllValues(Baseline.MODIFIED_LINES)).contains( + builder.setMetric(LINE).setCovered(1).setMissed(1).build()); + } + + /** + * Verifies the calculated indirect coverage changes. + * + * @param action + * The created Jenkins action + */ + private void verifyIndirectCoverageChanges(final CoverageBuildAction action) { + assertThat(action.getAllValues(Baseline.INDIRECT)) + .filteredOn(coverage -> coverage.getMetric().equals(LINE)) + .first() + .isInstanceOfSatisfying(Coverage.class, coverage -> { + assertThat(coverage.getCovered()).isEqualTo(4); + assertThat(coverage.getMissed()).isEqualTo(0); + }); + assertThat(action.getAllValues(Baseline.INDIRECT)) + .filteredOn(coverage -> coverage.getMetric().equals(BRANCH)) + .isEmpty(); + } + + private void verifyCodeDelta(final CoverageBuildAction action) { + edu.hm.hafner.coverage.Node root = action.getResult(); + assertThat(root).isNotNull(); + + List modifiedFiles = root.getAllFileNodes().stream() + .filter(FileNode::hasModifiedLines) + .collect(Collectors.toList()); + assertThat(modifiedFiles).hasSize(4); + assertThat(modifiedFiles).extracting(FileNode::getName) + .containsExactlyInAnyOrder("MinerFactory.java", "RepositoryMinerStep.java", + "SimpleReferenceRecorder.java", "CommitDecoratorFactory.java"); + assertThat(modifiedFiles).flatExtracting(FileNode::getModifiedLines) + .containsExactlyInAnyOrder(15, 17, 63, 68, 80, 90, 130); + } + + /** + * Creates a {@link FlowDefinition} for a Jenkins pipeline which processes a JaCoCo coverage report. + * + * @param node + * The node + * @param commit + * The processed commit + * @param fileName + * The content of the processed JaCoCo report + * + * @return the created definition + */ + private FlowDefinition createPipelineForCommit(final String node, final String commit, final String fileName) { + return createPipelineForCommit(node, commit, fileName, SourceCodeRetention.EVERY_BUILD); + } + + /** + * Creates a {@link FlowDefinition} for a Jenkins pipeline which processes a JaCoCo coverage report. + * + * @param node + * The node + * @param commit + * The processed commit + * @param fileName + * The content of the processed JaCoCo report + * @param sourceCodeRetentionStrategy + * the source code retention strategy + * + * @return the created definition + */ + private FlowDefinition createPipelineForCommit(final String node, final String commit, final String fileName, + final SourceCodeRetention sourceCodeRetentionStrategy) { + return new CpsFlowDefinition(node + " {" + + " checkout([$class: 'GitSCM', " + + " branches: [[name: '" + commit + "' ]],\n" + + " userRemoteConfigs: [[url: '" + REPOSITORY + "']],\n" + + " extensions: [[$class: 'RelativeTargetDirectory', \n" + + " relativeTargetDir: 'checkout']]])\n" + + " recordCoverage tools: [[parser: 'JACOCO', pattern: '" + fileName + "']], " + + " sourceCodeRetention: '" + sourceCodeRetentionStrategy.name() + "'\n" + + "}", true); + } + + private void configureGit(final FreeStyleProject project, final String commit) throws IOException { + GitSCM scm = new GitSCM(GitSCM.createRepoList(REPOSITORY, null), + Collections.singletonList(new BranchSpec(commit)), null, null, + Collections.singletonList(new RelativeTargetDirectory("code-coverage-api"))); + project.setScm(scm); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/JobDslITest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/JobDslITest.java new file mode 100644 index 000000000..5d8fd25fa --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/JobDslITest.java @@ -0,0 +1,110 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import org.junit.jupiter.api.Test; + +import edu.hm.hafner.coverage.Metric; + +import hudson.model.Descriptor; +import hudson.model.FreeStyleProject; +import hudson.model.TopLevelItem; +import hudson.model.View; +import hudson.tasks.Publisher; +import hudson.util.DescribableList; +import hudson.views.ListViewColumn; + +import io.jenkins.plugins.casc.ConfigurationAsCode; +import io.jenkins.plugins.casc.ConfiguratorException; +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.coverage.metrics.steps.CoverageTool.Parser; +import io.jenkins.plugins.prism.SourceCodeDirectory; +import io.jenkins.plugins.prism.SourceCodeRetention; +import io.jenkins.plugins.util.IntegrationTestWithJenkinsPerTest; +import io.jenkins.plugins.util.QualityGate.QualityGateCriticality; + +import static io.jenkins.plugins.coverage.metrics.Assertions.*; + +/** + * Tests support for column and job configurations via the Job DSL Plugin. + * + * @author Ullrich Hafner + */ +class JobDslITest extends IntegrationTestWithJenkinsPerTest { + /** + * Creates a freestyle job from a YAML file and verifies that issue recorder finds warnings. + */ + @Test + void shouldCreateColumnFromYamlConfiguration() { + configureJenkins("column-metric-dsl.yaml"); + + View view = getJenkins().getInstance().getView("dsl-view"); + + assertThat(view).isNotNull(); + + assertThat(view.getColumns()) + .extracting(ListViewColumn::getColumnCaption) + .contains(new CoverageMetricColumn().getColumnCaption()); + + assertThat(view.getColumns()).first() + .isInstanceOfSatisfying(CoverageMetricColumn.class, + c -> assertThat(c) + .hasColumnCaption(Messages.Coverage_Column()) + .hasMetric(Metric.LINE)); + } + + /** + * Creates a freestyle job from a YAML file and verifies that issue recorder finds warnings. + */ + @Test + void shouldCreateFreestyleJobFromYamlConfiguration() { + configureJenkins("job-dsl.yaml"); + + TopLevelItem project = getJenkins().jenkins.getItem("dsl-freestyle-job"); + + assertThat(project).isNotNull(); + assertThat(project).isInstanceOf(FreeStyleProject.class); + + DescribableList> publishers = ((FreeStyleProject) project).getPublishersList(); + assertThat(publishers).hasSize(1); + + Publisher publisher = publishers.get(0); + assertThat(publisher).isInstanceOfSatisfying(CoverageRecorder.class, this::assertRecorderProperties); + } + + private void assertRecorderProperties(final CoverageRecorder recorder) { + assertThat(recorder.getTools()).hasSize(2).usingRecursiveFieldByFieldElementComparator() + .containsExactly( + new io.jenkins.plugins.coverage.metrics.steps.CoverageTool(Parser.JACOCO, "jacoco-pattern.*"), + new io.jenkins.plugins.coverage.metrics.steps.CoverageTool(Parser.COBERTURA, "cobertura-pattern.*")); + assertThat(recorder.getQualityGates()).hasSize(2).usingRecursiveFieldByFieldElementComparator() + .containsExactly( + new CoverageQualityGate(70.0, Metric.LINE, Baseline.PROJECT, QualityGateCriticality.UNSTABLE), + new CoverageQualityGate(80.0, Metric.BRANCH, Baseline.MODIFIED_LINES, QualityGateCriticality.FAILURE)); + assertThat(recorder.getSourceDirectories()).hasSize(2).extracting(SourceCodeDirectory::getPath) + .containsExactlyInAnyOrder("directory-1", "directory-2"); + assertThat(recorder) + .hasId("my-coverage") + .hasName("My Coverage") + .hasScm("my-git") + .hasSourceCodeEncoding("UTF-8") + .hasSourceCodeRetention(SourceCodeRetention.EVERY_BUILD) + .isEnabledForFailure() + .isFailOnError() + .isSkipPublishingChecks() + .isSkipSymbolicLinks(); + } + + /** + * Helper method to get jenkins configuration file. + * + * @param fileName + * file with configuration. + */ + private void configureJenkins(final String fileName) { + try { + ConfigurationAsCode.get().configure(getResourceAsFile(fileName).toUri().toString()); + } + catch (ConfiguratorException e) { + throw new AssertionError(e); + } + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/QualityGateITest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/QualityGateITest.java new file mode 100644 index 000000000..89909b10b --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/QualityGateITest.java @@ -0,0 +1,106 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.List; +import java.util.Objects; + +import org.junit.jupiter.api.Test; + +import edu.hm.hafner.coverage.Metric; + +import org.jenkinsci.plugins.workflow.actions.WarningAction; +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import hudson.model.FreeStyleProject; +import hudson.model.Result; +import hudson.model.Run; + +import io.jenkins.plugins.coverage.metrics.AbstractCoverageITest; +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.coverage.metrics.steps.CoverageTool.Parser; +import io.jenkins.plugins.util.QualityGate.QualityGateCriticality; +import io.jenkins.plugins.util.QualityGateStatus; + +import static io.jenkins.plugins.coverage.metrics.AbstractCoverageTest.*; +import static io.jenkins.plugins.util.assertions.Assertions.*; + +/** + * Integration tests with active quality gates. + */ +class QualityGateITest extends AbstractCoverageITest { + @Test + void shouldNotHaveQualityGate() { + WorkflowJob job = createPipeline(Parser.JACOCO, JACOCO_ANALYSIS_MODEL_FILE); + + Run build = buildWithResult(job, Result.SUCCESS); + + CoverageBuildAction coverageResult = build.getAction(CoverageBuildAction.class); + assertThat(coverageResult.getQualityGateResult()).hasOverallStatus(QualityGateStatus.INACTIVE); + } + + @Test + void shouldPassQualityGate() { + var qualityGates = List.of(new CoverageQualityGate(-100.0, Metric.LINE, Baseline.PROJECT, QualityGateCriticality.UNSTABLE)); + FreeStyleProject project = createFreestyleJob(Parser.JACOCO, r -> r.setQualityGates(qualityGates), JACOCO_ANALYSIS_MODEL_FILE); + + Run build = buildWithResult(project, Result.SUCCESS); + + CoverageBuildAction coverageResult = build.getAction(CoverageBuildAction.class); + assertThat(coverageResult.getQualityGateResult()).hasOverallStatus(QualityGateStatus.PASSED); + } + + @Test + void shouldFailQualityGateWithUnstable() { + var qualityGates = List.of(new CoverageQualityGate(100, Metric.LINE, Baseline.PROJECT, QualityGateCriticality.UNSTABLE)); + FreeStyleProject project = createFreestyleJob(Parser.JACOCO, r -> r.setQualityGates(qualityGates), JACOCO_ANALYSIS_MODEL_FILE); + + Run build = buildWithResult(project, Result.UNSTABLE); + + CoverageBuildAction coverageResult = build.getAction(CoverageBuildAction.class); + assertThat(coverageResult.getQualityGateResult()).hasOverallStatus(QualityGateStatus.WARNING); + } + + @Test + void shouldFailQualityGateWithFailure() { + var qualityGates = List.of(new CoverageQualityGate(100, Metric.LINE, Baseline.PROJECT, QualityGateCriticality.FAILURE)); + FreeStyleProject project = createFreestyleJob(Parser.JACOCO, r -> r.setQualityGates(qualityGates), JACOCO_ANALYSIS_MODEL_FILE); + + Run build = buildWithResult(project, Result.FAILURE); + + CoverageBuildAction coverageResult = build.getAction(CoverageBuildAction.class); + assertThat(coverageResult.getQualityGateResult()).hasOverallStatus(QualityGateStatus.FAILED); + } + + @Test + void shouldUseQualityGateInPipeline() { + WorkflowJob project = createPipelineWithWorkspaceFiles(JACOCO_ANALYSIS_MODEL_FILE); + + setPipelineScript(project, + "recordCoverage(" + + "tools: [[parser: '" + Parser.JACOCO.name() + "', pattern: '**/*xml']],\n" + + "qualityGates: [" + + " [threshold: 90.0, metric: 'LINE', baseline: 'PROJECT', criticality: 'UNSTABLE'], " + + " [threshold: 90.0, metric: 'BRANCH', baseline: 'PROJECT', criticality: 'UNSTABLE']])\n"); + + WorkflowRun build = (WorkflowRun)buildWithResult(project, Result.UNSTABLE); + + CoverageBuildAction coverageResult = build.getAction(CoverageBuildAction.class); + assertThat(coverageResult.getQualityGateResult()).hasOverallStatus(QualityGateStatus.WARNING); + + assertThat(coverageResult.getLog().getInfoMessages()).contains("Evaluating quality gates", + "-> Some quality gates have been missed: overall result is UNSTABLE", + "-> Details for each quality gate:", + "-> [Overall project - Line Coverage]: ≪Success≫ - (Actual value: 95.39%, Quality gate: 90.00)", + "-> [Overall project - Branch Coverage]: ≪Unstable≫ - (Actual value: 88.28%, Quality gate: 90.00)"); + + FlowNode flowNode = new DepthFirstScanner().findFirstMatch(build.getExecution(), + node -> "recordCoverage".equals(Objects.requireNonNull(node).getDisplayFunctionName())); + assertThat(flowNode).isNotNull(); + + WarningAction warningAction = flowNode.getPersistentAction(WarningAction.class); + assertThat(warningAction).isNotNull(); + assertThat(warningAction.getMessage()).isEqualTo( + "-> Some quality gates have been missed: overall result is UNSTABLE"); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CodeDeltaCalculatorTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CodeDeltaCalculatorTest.java index 27d889862..3663c08e6 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CodeDeltaCalculatorTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CodeDeltaCalculatorTest.java @@ -19,9 +19,9 @@ import hudson.model.TaskListener; import io.jenkins.plugins.coverage.model.exception.CodeDeltaException; -import io.jenkins.plugins.forensics.delta.model.Delta; -import io.jenkins.plugins.forensics.delta.model.FileChanges; -import io.jenkins.plugins.forensics.delta.model.FileEditType; +import io.jenkins.plugins.forensics.delta.Delta; +import io.jenkins.plugins.forensics.delta.FileChanges; +import io.jenkins.plugins.forensics.delta.FileEditType; import static io.jenkins.plugins.coverage.model.CodeDeltaCalculator.*; import static org.assertj.core.api.Assertions.*; diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageJobActionTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageJobActionTest.java index d6963f3e0..ab00edcc1 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageJobActionTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageJobActionTest.java @@ -15,7 +15,7 @@ import hudson.model.FreeStyleBuild; import hudson.model.FreeStyleProject; -import static io.jenkins.plugins.coverage.model.Assertions.*; +import static io.jenkins.plugins.coverage.metrics.Assertions.*; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; import static org.mockito.Mockito.*; diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageLeafTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageLeafTest.java index 26529470e..79fd88c86 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageLeafTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageLeafTest.java @@ -6,7 +6,7 @@ import io.jenkins.plugins.coverage.model.Coverage.CoverageBuilder; -import static io.jenkins.plugins.coverage.model.Assertions.*; +import static io.jenkins.plugins.coverage.metrics.Assertions.*; /** * Tests the class {@link CoverageLeaf}. diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageNodeTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageNodeTest.java index 93c888f8b..36bf445a2 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageNodeTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageNodeTest.java @@ -14,7 +14,7 @@ import io.jenkins.plugins.coverage.model.Coverage.CoverageBuilder; import io.jenkins.plugins.coverage.targets.CoverageResult; -import static io.jenkins.plugins.coverage.model.Assertions.*; +import static io.jenkins.plugins.coverage.metrics.Assertions.*; import static org.mockito.Mockito.*; /** diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoveragePercentageTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoveragePercentageTest.java index 3d9964650..4bd64688d 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoveragePercentageTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoveragePercentageTest.java @@ -7,9 +7,8 @@ import nl.jqno.equalsverifier.EqualsVerifier; -import static io.jenkins.plugins.coverage.model.Assertions.assertThat; +import static io.jenkins.plugins.coverage.metrics.Assertions.*; import static io.jenkins.plugins.coverage.model.CoveragePercentage.*; -import static org.assertj.core.api.Assertions.*; /** * Test class for {@link CoveragePercentage}. diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageResultTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageResultTest.java index f8bb16382..69888f314 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageResultTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageResultTest.java @@ -9,7 +9,7 @@ import io.jenkins.plugins.coverage.targets.CoverageResult; import io.jenkins.plugins.coverage.targets.Ratio; -import static io.jenkins.plugins.coverage.model.Assertions.*; +import static io.jenkins.plugins.coverage.metrics.Assertions.*; /** * TODO: Move to corresponding test class. diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageTest.java index 1e7283682..7b73c8e48 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageTest.java @@ -11,7 +11,7 @@ import io.jenkins.plugins.coverage.model.Coverage.CoverageBuilder; -import static io.jenkins.plugins.coverage.model.Assertions.*; +import static io.jenkins.plugins.coverage.metrics.Assertions.*; import static io.jenkins.plugins.coverage.model.Coverage.CoverageBuilder.*; /** diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageViewModelTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageViewModelTest.java index e41c1e333..680232010 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageViewModelTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageViewModelTest.java @@ -11,7 +11,7 @@ import io.jenkins.plugins.coverage.model.Coverage.CoverageBuilder; -import static io.jenkins.plugins.coverage.model.Assertions.*; +import static io.jenkins.plugins.coverage.metrics.Assertions.*; import static io.jenkins.plugins.coverage.model.CoverageViewModel.*; import static io.jenkins.plugins.coverage.model.testutil.CoverageStubs.*; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageXmlStreamTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageXmlStreamTest.java index ec98b9fc6..1aecbea29 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageXmlStreamTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/model/CoverageXmlStreamTest.java @@ -21,7 +21,7 @@ import io.jenkins.plugins.coverage.targets.CoverageElementRegister; import io.jenkins.plugins.coverage.targets.CoverageResult; -import static io.jenkins.plugins.coverage.model.Assertions.*; +import static io.jenkins.plugins.coverage.metrics.Assertions.*; /** * Tests the class {@link CoverageXmlStream}. diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/model/DeltaComputationVsReferenceBuildITest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/model/DeltaComputationVsReferenceBuildITest.java index a7c0786f9..88c9f6288 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/model/DeltaComputationVsReferenceBuildITest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/model/DeltaComputationVsReferenceBuildITest.java @@ -14,7 +14,7 @@ import io.jenkins.plugins.coverage.adapter.JacocoReportAdapter; import io.jenkins.plugins.util.IntegrationTestWithJenkinsPerSuite; -import static io.jenkins.plugins.coverage.model.Assertions.*; +import static io.jenkins.plugins.coverage.metrics.Assertions.*; import static io.jenkins.plugins.coverage.model.CoverageMetric.*; /** diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/model/FileChangesProcessorTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/model/FileChangesProcessorTest.java index 508384fd5..eaca23a01 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/model/FileChangesProcessorTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/model/FileChangesProcessorTest.java @@ -8,10 +8,10 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import io.jenkins.plugins.forensics.delta.model.Change; -import io.jenkins.plugins.forensics.delta.model.ChangeEditType; -import io.jenkins.plugins.forensics.delta.model.FileChanges; -import io.jenkins.plugins.forensics.delta.model.FileEditType; +import io.jenkins.plugins.forensics.delta.Change; +import io.jenkins.plugins.forensics.delta.ChangeEditType; +import io.jenkins.plugins.forensics.delta.FileChanges; +import io.jenkins.plugins.forensics.delta.FileEditType; import static org.assertj.core.api.Assertions.*; diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/model/GitForensicsITest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/model/GitForensicsITest.java index 082d01124..c1f3fed60 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/model/GitForensicsITest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/model/GitForensicsITest.java @@ -24,7 +24,7 @@ import io.jenkins.plugins.coverage.adapter.JacocoReportAdapter; import io.jenkins.plugins.util.IntegrationTestWithJenkinsPerSuite; -import static io.jenkins.plugins.coverage.model.Assertions.*; +import static io.jenkins.plugins.coverage.metrics.Assertions.*; import static io.jenkins.plugins.coverage.model.CoverageMetric.*; import static org.assertj.core.api.Assumptions.*; diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/model/JobDslITest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/model/JobDslITest.java index 1f812e94b..b6a7320a4 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/model/JobDslITest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/model/JobDslITest.java @@ -10,7 +10,7 @@ import io.jenkins.plugins.coverage.model.visualization.dashboard.CoverageColumn; import io.jenkins.plugins.util.IntegrationTestWithJenkinsPerTest; -import static io.jenkins.plugins.coverage.model.Assertions.*; +import static io.jenkins.plugins.coverage.metrics.Assertions.*; /** * Tests the Job DSL Plugin. diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/model/PluginArchitectureTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/model/PluginArchitectureTest.java index e09c8a548..b63ed55df 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/model/PluginArchitectureTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/model/PluginArchitectureTest.java @@ -35,7 +35,7 @@ class PluginArchitectureTest { static final ArchRule NO_FORBIDDEN_CLASSES_CALLED = ArchitectureRules.NO_FORBIDDEN_CLASSES_CALLED; @ArchTest - static final ArchRule NO_PUBLIC_ARCHITECTURE_TESTS = ArchitectureRules.NO_PUBLIC_ARCHITECTURE_TESTS; + static final ArchRule ONLY_PACKAGE_PRIVATE_ARCHITECTURE_TESTS = ArchitectureRules.ONLY_PACKAGE_PRIVATE_ARCHITECTURE_TESTS; @ArchTest static final ArchRule NO_JENKINS_INSTANCE_CALL = PluginArchitectureRules.NO_JENKINS_INSTANCE_CALL; @@ -57,5 +57,4 @@ class PluginArchitectureTest { @ArchTest static final ArchRule USE_POST_FOR_LIST_MODELS_RULE = PluginArchitectureRules.USE_POST_FOR_LIST_AND_COMBOBOX_FILL; - } diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/model/testutil/CoverageStubs.java b/plugin/src/test/java/io/jenkins/plugins/coverage/model/testutil/CoverageStubs.java index 6cfadafac..dcccb9a44 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/model/testutil/CoverageStubs.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/model/testutil/CoverageStubs.java @@ -63,7 +63,7 @@ public static BuildResult createResult(final int buildNumbe * * @return the created stub */ - @VisibleForTesting + @VisibleForTesting @SuppressWarnings("unchecked") public static CoverageBuildAction createCoverageBuildAction( final CoverageMetric coverageMetric, final Fraction coverageValue) { CoverageBuildAction action = mock(CoverageBuildAction.class); diff --git a/plugin/src/test/resources/design.puml b/plugin/src/test/resources/design.puml new file mode 100644 index 000000000..82ce678a6 --- /dev/null +++ b/plugin/src/test/resources/design.puml @@ -0,0 +1,28 @@ +@startuml + +skinparam componentStyle uml2 +skinparam component { + BorderColor #a0a0a0 + BackgroundColor #f8f8f8 +} + +[Steps] <<..metrics.steps>> +[Color] <<..metrics.color>> +[Source] <<..metrics.source>> +[Charts] <<..metrics.charts>> +[Model] <<..metrics.model>> + +[Steps] --> [Model] +[Steps] --> [Color] +[Steps] --> [Source] +[Steps] --> [Charts] + +[Charts] --> [Color] +[Charts] --> [Model] + +[Source] --> [Model] + +[Model] --> [Color] + + +@enduml diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/AcuCobolParser.java.txt b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/AcuCobolParser.java.txt new file mode 100644 index 000000000..8b14906d8 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/AcuCobolParser.java.txt @@ -0,0 +1,45 @@ +package edu.hm.hafner.analysis.parser; + +import java.util.Optional; +import java.util.regex.Matcher; + +import edu.hm.hafner.analysis.Issue; +import edu.hm.hafner.analysis.IssueBuilder; +import edu.hm.hafner.analysis.LookaheadParser; +import edu.hm.hafner.util.LookaheadStream; + +import static edu.hm.hafner.analysis.Categories.*; + +/** + * A parser for the Acu Cobol compile. + * + * @author jerryshea + */ +public class AcuCobolParser extends LookaheadParser { + private static final long serialVersionUID = -894639209290549425L; + + private static final String ACU_COBOL_WARNING_PATTERN = "^\\s*(\\[.*\\])?\\s*?(.*), line ([0-9]*): Warning: (.*)$"; + + /** + * Creates a new instance of {@link AcuCobolParser}. + */ + public AcuCobolParser() { + super(ACU_COBOL_WARNING_PATTERN); + } + + @Override + protected boolean isLineInteresting(final String line) { + return line.contains("Warning"); + } + + @Override + protected Optional createIssue(final Matcher matcher, final LookaheadStream lookahead, + final IssueBuilder builder) { + return builder.setFileName(matcher.group(2)) + .setLineStart(matcher.group(3)) + .setCategory(guessCategory(matcher.group(4))) + .setMessage(matcher.group(4)) + .buildOptional(); + } +} + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/PathUtil.java.txt b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/PathUtil.java.txt new file mode 100644 index 000000000..200112993 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/PathUtil.java.txt @@ -0,0 +1,277 @@ +package edu.hm.hafner.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; + +import edu.umd.cs.findbugs.annotations.CheckForNull; + +/** + * Utilities for {@link Path} instances. + * + * @author Ullrich Hafner + */ +public class PathUtil { + private static final String BACK_SLASH = "\\"; + private static final String SLASH = "/"; + private static final String DRIVE_LETTER_PREFIX = "^[a-z]:/.*"; + + /** + * Tests whether a file exists. + * + *

    + * Note that the result of this method is immediately outdated. If this method indicates the file exists then there + * is no guarantee that a subsequence access will succeed. Care should be taken when using this method in security + * sensitive applications. + *

    + * + * @param fileName + * the absolute path of the file + * + * @return {@code true} if the file exists; {@code false} if the file does not exist or its existence cannot be + * determined. + */ + public boolean exists(final String fileName) { + try { + return Files.exists(Paths.get(fileName)); + } + catch (IllegalArgumentException ignore) { + return false; + } + } + + /** + * Tests whether a file exists. + * + *

    + * Note that the result of this method is immediately outdated. If this method indicates the file exists then there + * is no guarantee that a subsequence access will succeed. Care should be taken when using this method in security + * sensitive applications. + *

    + * + * @param fileName + * the file name + * @param directory + * the directory that contains the file + * + * @return {@code true} if the file exists; {@code false} if the file does not exist or its existence cannot be + * determined. + */ + public boolean exists(final String fileName, final String directory) { + return exists(createAbsolutePath(directory, fileName)); + } + + /** + * Returns the string representation of the specified path. The path will be actually resolved in the file system + * and will be returned as fully qualified absolute path. In case of an error, i.e. if the file is not found, the + * provided {@code path} will be returned unchanged (but normalized using the UNIX path separator and upper case + * drive letter). + * + * @param path + * the path to get the absolute path for + * + * @return the absolute path + */ + public String getAbsolutePath(final String path) { + try { + return getAbsolutePath(Paths.get(path)); + } + catch (IllegalArgumentException ignored) { + return makeUnixPath(path); + } + } + + /** + * Returns the string representation of the specified path. The path will be actually resolved in the file system + * and will be returned as fully qualified absolute path. In case of an error, i.e. if the file is not found, the + * provided {@code path} will be returned unchanged (but normalized using the UNIX path separator and upper case + * drive letter). + * + * @param path + * the path to get the absolute path for + * + * @return the absolute path + */ + public String getAbsolutePath(final Path path) { + try { + return makeUnixPath(normalize(path).toString()); + } + catch (IOException | IllegalArgumentException ignored) { + return makeUnixPath(path.toString()); + } + } + + /** + * Returns the relative path of specified path with respect to the provided base directory. The given path will be + * actually resolved in the file system (which may lead to a different fully qualified absolute path). Then the base + * directory prefix will be removed (if possible). In case of an error, i.e., if the file is not found or could not + * be resolved in the parent, then the provided {@code path} will be returned unchanged (but normalized using the + * UNIX path separator and upper case drive letter). + * + * @param base + * the base directory that should be to get the absolute path for + * @param path + * the path to get the absolute path for + * + * @return the relative path + */ + public String getRelativePath(final Path base, final String path) { + try { + return getRelativePath(base, Paths.get(path)); + } + catch (IllegalArgumentException ignored) { + return makeUnixPath(path); + } + } + + /** + * Returns the relative path of specified path with respect to the provided base directory. The given path will be + * actually resolved in the file system (which may lead to a different fully qualified absolute path). Then the base + * directory prefix will be removed (if possible). In case of an error, i.e., if the file is not found or could not + * be resolved in the parent, then the provided {@code path} will be returned unchanged (but normalized using the + * UNIX path separator and upper case drive letter). + * + * @param base + * the base directory that should be to get the absolute path for + * @param path + * the path to get the absolute path for + * + * @return the relative path + */ + public String getRelativePath(final String base, final String path) { + try { + return getRelativePath(Paths.get(base), Paths.get(path)); + } + catch (IllegalArgumentException ignored) { + return makeUnixPath(path); + } + } + + /** + * Returns the relative path of specified path with respect to the provided base directory. The given path will be + * actually resolved in the file system (which may lead to a different fully qualified absolute path). Then the base + * directory prefix will be removed (if possible). In case of an error, i.e., if the file is not found or could not + * be resolved in the parent, then the provided {@code path} will be returned unchanged (but normalized using the + * UNIX path separator and upper case drive letter). + * + * @param base + * the base directory that should be to get the absolute path for + * @param path + * the path to get the absolute path for + * + * @return the relative path + */ + public String getRelativePath(final Path base, final Path path) { + try { + Path normalizedBase = normalize(base); + if (path.isAbsolute()) { + return makeUnixPath(normalizedBase.relativize(normalize(path)).toString()); + } + return makeUnixPath(normalizedBase.relativize(normalize(base.resolve(path))).toString()); + + } + catch (IOException | IllegalArgumentException ignored) { + // ignore and return the path as such + } + return makeUnixPath(path.toString()); + } + + /** + * Returns a normalized relative path of specified path. The given path will be actually resolved in the file system + * (which may lead to a different path). In case of an error, i.e., if the file is not found or could not be + * resolved in the parent, then the provided {@code path} will be returned unchanged (but normalized using the UNIX + * path separator and upper case drive letter). + * + * @param relative + * the path to get the normalized path for + * + * @return the normalized relative path + */ + public String getRelativePath(final Path relative) { + return makeUnixPath(relative.normalize().toString()); + } + + /** + * Returns a normalized relative path of specified path. The given path will be actually resolved in the file system + * (which may lead to a different path). In case of an error, i.e., if the file is not found or could not be + * resolved in the parent, then the provided {@code path} will be returned unchanged (but normalized using the UNIX + * path separator and upper case drive letter). + * + * @param relative + * the path to get the normalized path for + * + * @return the normalized relative path + */ + public String getRelativePath(final String relative) { + try { + return getRelativePath(Paths.get(relative)); + } + catch (IllegalArgumentException ignored) { + // ignore and return the path as such + } + return makeUnixPath(relative); + } + + /** + * Returns the absolute path of the specified file in the given directory. + * + * @param directory + * the directory that contains the file + * @param fileName + * the file name + * + * @return the absolute path + */ + public String createAbsolutePath(@CheckForNull final String directory, final String fileName) { + if (isAbsolute(fileName) || StringUtils.isBlank(directory)) { + return makeUnixPath(fileName); + } + String path = makeUnixPath(Objects.requireNonNull(directory)); + + String separator; + if (path.endsWith(SLASH)) { + separator = StringUtils.EMPTY; + } + else { + separator = SLASH; + } + + try { + String normalized = FilenameUtils.normalize(String.join(separator, path, fileName)); + return makeUnixPath(normalized == null ? fileName : normalized); + } + catch (IllegalArgumentException ignored) { + return makeUnixPath(fileName); + } + } + + /** + * Returns whether the specified file name is an absolute path. + * + * @param fileName + * the file name to test + * + * @return {@code true} if this path is an absolute path, {@code false} if a relative path + */ + public boolean isAbsolute(final String fileName) { + return FilenameUtils.getPrefixLength(fileName) > 0; + } + + private Path normalize(final Path path) throws IOException { + return path.toAbsolutePath().normalize().toRealPath(LinkOption.NOFOLLOW_LINKS); + } + + private String makeUnixPath(final String fileName) { + String unixStyle = fileName.replace(BACK_SLASH, SLASH); + if (unixStyle.matches(DRIVE_LETTER_PREFIX)) { + unixStyle = StringUtils.capitalize(unixStyle); + } + return unixStyle; + } +} diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/SourcecodeTest.html b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/SourcecodeTest.html new file mode 100644 index 000000000..82e8b5c35 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/SourcecodeTest.html @@ -0,0 +1,120 @@ + + 1 + + package test.example; + + + 2 + + + + + 3 + 1 + public class SourcecodeTest { + + + 4 + + + + + 5 + +     public void first() { + + + 6 + 0 +         System.out.println(); + + + 7 + 0 +     } + + + 8 + + + + + 9 + +     public void insertedTested() { + + + 10 + 1 +         System.out.println(); + + + 11 + 1 +     } + + + 12 + + + + + 13 + +     public void second() { + + + 14 + 1 +         System.out.println(); + + + 15 + 1 +     } + + + 16 + + + + + 17 + +     public void inserted() { + + + 18 + 0 +         System.out.println(); + + + 19 + 0 +     } + + + 20 + + + + + 21 + +     public void third() { + + + 22 + 0 +         System.out.println(); + + + 23 + 0 +     } + + + 24 + + } + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/SourcecodeTestCC.html b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/SourcecodeTestCC.html new file mode 100644 index 000000000..a98b581fe --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/SourcecodeTestCC.html @@ -0,0 +1,96 @@ + + .. + + + + + + + + + + 7 + +     } + + + 8 + + + + + 9 + +     public void insertedTested() { + + + 10 + 1 +         System.out.println(); + + + 11 + 1 +     } + + + 12 + + + + + 13 + +     public void second() { + + + 14 + +         System.out.println(); + + + 15 + +     } + + + 16 + + + + + 17 + +     public void inserted() { + + + 18 + 0 +         System.out.println(); + + + 19 + 0 +     } + + + 20 + + + + + 21 + +     public void third() { + + + 22 + +         System.out.println(); + + + .. + + + + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/SourcecodeTestICC.html b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/SourcecodeTestICC.html new file mode 100644 index 000000000..930820c63 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/SourcecodeTestICC.html @@ -0,0 +1,96 @@ + + .. + + + + + + 3 + + public class SourcecodeTest { + + + 4 + + + + + 5 + +     public void first() { + + + 6 + -1 +         System.out.println(); + + + 7 + -1 +     } + + + 8 + + + + + 9 + +     public void insertedTested() { + + + 10 + +         System.out.println(); + + + 11 + +     } + + + 12 + + + + + 13 + +     public void second() { + + + 14 + 1 +         System.out.println(); + + + 15 + 1 +     } + + + 16 + + + + + 17 + +     public void inserted() { + + + 18 + +         System.out.println(); + + + .. + + + + + + + + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/jacoco-acu-cobol-parser.xml b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/jacoco-acu-cobol-parser.xml new file mode 100644 index 000000000..4a0184921 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/jacoco-acu-cobol-parser.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/jacoco-path-util.xml b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/jacoco-path-util.xml new file mode 100644 index 000000000..309543aaa --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/jacoco-path-util.xml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/cobertura-higher-coverage.xml b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/cobertura-higher-coverage.xml new file mode 100644 index 000000000..f28634c42 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/cobertura-higher-coverage.xml @@ -0,0 +1,38 @@ + + + + + /Users/leobalter/dev/testing/solutions/3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/cobertura-lots-of-data.xml b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/cobertura-lots-of-data.xml new file mode 100644 index 000000000..4e6fa83fc --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/cobertura-lots-of-data.xml @@ -0,0 +1,4786 @@ + + + + + + C:/local/ant-coverage-example/src + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/cobertura-lower-coverage.xml b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/cobertura-lower-coverage.xml new file mode 100644 index 000000000..7d8aecbc3 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/cobertura-lower-coverage.xml @@ -0,0 +1,34 @@ + + + + + /Users/leobalter/dev/testing/solutions/3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/cobertura-npe.xml b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/cobertura-npe.xml new file mode 100644 index 000000000..c119a9a15 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/cobertura-npe.xml @@ -0,0 +1,166 @@ + + + + /CoverageTest.Service/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/column-metric-dsl.yaml b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/column-metric-dsl.yaml new file mode 100644 index 000000000..1560c3464 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/column-metric-dsl.yaml @@ -0,0 +1,19 @@ +jobs: + - script: > + listView("dsl-view") { + jobs { + regex("^bar") + } + recurse(true) + columns { + coverageMetricColumn() + status() + weather() + name() + lastSuccess() + lastFailure() + lastDuration() + testResult(1) + buildButton() + } + } diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/coverage-publisher-summary.md b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/coverage-publisher-summary.md new file mode 100644 index 000000000..b35ca8ef9 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/coverage-publisher-summary.md @@ -0,0 +1,32 @@ +## Coverage Report Overview + +* **[Overall project (difference to reference job)](http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage#overview)** + * Line Coverage: 91.02% (294/323) / +50.00% + * Branch Coverage: 93.97% (109/116) / n/a + * Complexity Density: +49.54% + * Lines of Code: 323 +* **[Modified files (difference to overall project)](http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage#modifiedFilesCoverage)** + * Line Coverage: 50.00% (1/2) / +50.00% + * Branch Coverage: n/a / n/a + * Complexity Density: +43.40% + * Lines of Code: 53 +* **[Modified code lines (difference to modified files)](http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage#modifiedLinesCoverage)** + * Line Coverage: 50.00% (1/2) / +50.00% + * Branch Coverage: n/a / n/a + * Lines of Code: 3 +* **[Indirect changes](http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage#indirectCoverage)** + * Line Coverage: 50.00% (1/2) / n/a + * Branch Coverage: n/a / n/a + * Lines of Code: n/a + + +## Quality Gates Summary - INACTIVE + + + +## Project Coverage Summary + +|Container Coverage|Module Coverage|Package Coverage|File Coverage|Class Coverage|Method Coverage|Line Coverage|Branch Coverage|Instruction Coverage| +|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| +|:white_check_mark: **Overall project**|100.00% (1/1)|100.00% (4/4)|70.00% (7/10)|83.33% (15/18)|95.10% (97/102)|91.02% (294/323)|93.97% (109/116)|93.33% (1260/1350)| +|:chart_with_upwards_trend: **Overall project (difference to reference job)**|-|+20.00% :arrow_up:|-|-|-|-|+50.00% :arrow_up:|-|-| diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/file-changes-test-after.xml b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/file-changes-test-after.xml new file mode 100644 index 000000000..1c1b89ba5 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/file-changes-test-after.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/file-changes-test-before.xml b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/file-changes-test-before.xml new file mode 100644 index 000000000..63a0fcdb1 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/file-changes-test-before.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/forensics_integration.xml b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/forensics_integration.xml new file mode 100644 index 000000000..e2a41d8a9 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/forensics_integration.xml @@ -0,0 +1,4199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/forensics_integration_reference.xml b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/forensics_integration_reference.xml new file mode 100644 index 000000000..f293038c6 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/forensics_integration_reference.xml @@ -0,0 +1,4299 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/jacoco-analysis-model.xml b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/jacoco-analysis-model.xml new file mode 100644 index 000000000..52c1b39a6 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/jacoco-analysis-model.xml @@ -0,0 +1,21645 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/jacoco-codingstyle.xml b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/jacoco-codingstyle.xml new file mode 100644 index 000000000..49915a685 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/jacoco-codingstyle.xml @@ -0,0 +1,1179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/jacoco.xml b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/jacoco.xml new file mode 100644 index 000000000..49915a685 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/jacoco.xml @@ -0,0 +1,1179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/job-dsl.yaml b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/job-dsl.yaml new file mode 100644 index 000000000..ab359b036 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/job-dsl.yaml @@ -0,0 +1,49 @@ +jobs: + - script: > + freeStyleJob('dsl-freestyle-job') { + publishers { + recordCoverage { + tools { + coverageTool { + parser('JACOCO') + pattern('jacoco-pattern.*') + } + coverageTool { + parser('COBERTURA') + pattern('cobertura-pattern.*') + } + } + qualityGates { + coverageQualityGate { + threshold(70) + metric('LINE') + baseline('PROJECT') + criticality('UNSTABLE') + } + coverageQualityGate { + threshold(80) + metric('BRANCH') + baseline('MODIFIED_LINES') + criticality('FAILURE') + } + } + id('my-coverage') + name('My Coverage') + enabledForFailure(true) + skipPublishingChecks(true) + failOnError(true) + skipSymbolicLinks(true) + scm('my-git') + sourceCodeEncoding('UTF-8') + sourceCodeRetention('EVERY_BUILD') + sourceDirectories { + sourceCodeDirectory { + path('directory-1') + } + sourceCodeDirectory { + path('directory-2') + } + } + } + } + }; diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/mutations.xml b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/mutations.xml new file mode 100644 index 000000000..28c48d76d --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/mutations.xml @@ -0,0 +1,3497 @@ + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + add + (Ledu/hm/hafner/coverage/CoverageNode;)V + 175 + NotExisting + 12 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldAddChildren()] + + removed call to edu/hm/hafner/coverage/CoverageNode::setParent + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + addAll + (Ljava/util/List;)V + 164 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 6 + 0 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithMultipleDots()] + + removed call to java/util/List::forEach + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + aggregateChildren + (Ledu/hm/hafner/coverage/CoverageMetric;)Ledu/hm/hafner/coverage/Coverage; + 284 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 19 + 3 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetMetricsDistributionAndPercentages()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::aggregateChildren + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + computeDelta + (Ledu/hm/hafner/coverage/CoverageNode;)Ljava/util/SortedMap; + 301 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 24 + 3 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldComputeDeltaWithOverflow()] + + removed call to java/util/SortedMap::forEach + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + computeDelta + (Ledu/hm/hafner/coverage/CoverageNode;)Ljava/util/SortedMap; + 304 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 28 + 4 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldComputeDeltaWithOverflow()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::computeDelta + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + copyEmpty + ()Ledu/hm/hafner/coverage/CoverageNode; + 483 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 10 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldCopyLeaf()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::copyEmpty + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + copyTree + ()Ledu/hm/hafner/coverage/CoverageNode; + 452 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 6 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldCopyLeaf()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::copyTree + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + copyTree + (Ledu/hm/hafner/coverage/CoverageNode;)Ledu/hm/hafner/coverage/CoverageNode; + 465 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 9 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldCopyLeaf()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + copyTree + (Ledu/hm/hafner/coverage/CoverageNode;)Ledu/hm/hafner/coverage/CoverageNode; + 466 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 14 + 2 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldCopyWithChild()] + + removed call to edu/hm/hafner/coverage/CoverageNode::setParent + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + copyTree + (Ledu/hm/hafner/coverage/CoverageNode;)Ledu/hm/hafner/coverage/CoverageNode; + 471 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 33 + 7 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldCopyWithChild()] + + removed call to java/util/stream/Stream::forEach + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + copyTree + (Ledu/hm/hafner/coverage/CoverageNode;)Ledu/hm/hafner/coverage/CoverageNode; + 472 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 43 + 10 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldCopyLeaf()] + + removed call to java/util/List::forEach + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + copyTree + (Ledu/hm/hafner/coverage/CoverageNode;)Ledu/hm/hafner/coverage/CoverageNode; + 474 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 47 + 11 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldCopyLeaf()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::copyTree + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + createChild + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageNode; + 499 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 22 + 7 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithMultipleDots()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + createChild + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageNode; + 505 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 43 + 11 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithMultipleDots()] + + removed call to edu/hm/hafner/coverage/CoverageNode::add + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + createChild + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageNode; + 500 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 26 + 8 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithMultipleDots()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::createChild + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + createChild + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageNode; + 506 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 47 + 12 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithMultipleDots()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::createChild + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + find + (Ledu/hm/hafner/coverage/CoverageMetric;Ljava/lang/String;)Ljava/util/Optional; + + 362 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 7 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldFindMetric()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + find + (Ledu/hm/hafner/coverage/CoverageMetric;Ljava/lang/String;)Ljava/util/Optional; + + 363 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 12 + 3 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldFindMetric()] + + replaced return value with Optional.empty for edu/hm/hafner/coverage/CoverageNode::find + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + find + (Ledu/hm/hafner/coverage/CoverageMetric;Ljava/lang/String;)Ljava/util/Optional; + + 365 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 34 + 8 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldFindMetric()] + + replaced return value with Optional.empty for edu/hm/hafner/coverage/CoverageNode::find + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + findByHashCode + (Ledu/hm/hafner/coverage/CoverageMetric;I)Ljava/util/Optional; + 382 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 7 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldFindMetric()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + findByHashCode + (Ledu/hm/hafner/coverage/CoverageMetric;I)Ljava/util/Optional; + 383 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 12 + 3 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldFindMetric()] + + replaced return value with Optional.empty for edu/hm/hafner/coverage/CoverageNode::findByHashCode + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + findByHashCode + (Ledu/hm/hafner/coverage/CoverageMetric;I)Ljava/util/Optional; + 385 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 34 + 8 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldFindMetric()] + + replaced return value with Optional.empty for edu/hm/hafner/coverage/CoverageNode::findByHashCode + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getAll + (Ledu/hm/hafner/coverage/CoverageMetric;)Ljava/util/List; + 345 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 40 + 9 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithSingleDot()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getAll + (Ledu/hm/hafner/coverage/CoverageMetric;)Ljava/util/List; + 340 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 15 + 2 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldThrowExceptionWithLeafMetric()] + + removed call to edu/hm/hafner/util/Ensure$BooleanCondition::isFalse + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getAll + (Ledu/hm/hafner/coverage/CoverageMetric;)Ljava/util/List; + 348 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 51 + 12 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithSingleDot()] + + replaced return value with Collections.emptyList for edu/hm/hafner/coverage/CoverageNode::getAll + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getChildren + ()Ljava/util/List; + 156 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 5 + 0 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldAddChildAndLeaf()] + + replaced return value with Collections.emptyList for edu/hm/hafner/coverage/CoverageNode::getChildren + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getCoverage + (Ledu/hm/hafner/coverage/CoverageMetric;)Ledu/hm/hafner/coverage/Coverage; + 272 + org.pitest.mutationtest.engine.gregor.mutators.ConditionalsBoundaryMutator + 45 + 11 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetModuleCoverage()] + + changed conditional boundary + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getCoverage + (Ledu/hm/hafner/coverage/CoverageMetric;)Ledu/hm/hafner/coverage/Coverage; + 265 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 11 + 2 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetMetricsDistributionAndPercentages()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getCoverage + (Ledu/hm/hafner/coverage/CoverageMetric;)Ledu/hm/hafner/coverage/Coverage; + 271 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 38 + 8 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetMetricsDistributionAndPercentages()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getCoverage + (Ledu/hm/hafner/coverage/CoverageMetric;)Ledu/hm/hafner/coverage/Coverage; + 272 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 45 + 11 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetMetricsDistributionAndPercentages()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getCoverage + (Ledu/hm/hafner/coverage/CoverageMetric;)Ledu/hm/hafner/coverage/Coverage; + 266 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 30 + 6 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetMetricsDistributionAndPercentages()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::getCoverage + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getCoverage + (Ledu/hm/hafner/coverage/CoverageMetric;)Ledu/hm/hafner/coverage/Coverage; + 273 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 51 + 13 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetMetricsDistributionAndPercentages()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::getCoverage + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getCoverage + (Ledu/hm/hafner/coverage/CoverageMetric;)Ledu/hm/hafner/coverage/Coverage; + 276 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 58 + 15 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetModuleCoverage()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::getCoverage + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getCoverage + (Ledu/hm/hafner/coverage/CoverageMetric;)Ledu/hm/hafner/coverage/Coverage; + 279 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 63 + 16 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetModuleCoverage()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::getCoverage + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getLeaves + ()Ljava/util/List; + 160 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 5 + 0 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldAddChildAndLeaf()] + + replaced return value with Collections.emptyList for edu/hm/hafner/coverage/CoverageNode::getLeaves + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getMetric + ()Ledu/hm/hafner/coverage/CoverageMetric; + 113 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 5 + 0 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldTextuallyRepresent()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::getMetric + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getMetrics + ()Ljava/util/NavigableSet; + 128 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 40 + 10 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetMetricsDistributionAndPercentages()] + + removed call to java/util/stream/Stream::forEach + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getMetrics + ()Ljava/util/NavigableSet; + 130 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 44 + 11 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetMetrics()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::getMetrics + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getMetricsDistribution + ()Ljava/util/NavigableMap; + 139 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 18 + 5 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetMetricsDistributionAndPercentages()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::getMetricsDistribution + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getMetricsPercentages + ()Ljava/util/NavigableMap; + 144 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 18 + 5 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetMetricsDistributionAndPercentages()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::getMetricsPercentages + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getName + ()Ljava/lang/String; + 152 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 5 + 0 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldTextuallyRepresent()] + + replaced return value with "" for edu/hm/hafner/coverage/CoverageNode::getName + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getParent + ()Ledu/hm/hafner/coverage/CoverageNode; + 71 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 5 + 0 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldThrowExceptionWithoutParent()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getParent + ()Ledu/hm/hafner/coverage/CoverageNode; + 74 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 18 + 3 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldAddChildren()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::getParent + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getParentName + ()Ljava/lang/String; + 216 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 5 + 0 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldAddChildren()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getParentName + ()Ljava/lang/String; + 222 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 31 + 5 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldAddChildren()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getParentName + ()Ljava/lang/String; + 222 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 36 + 8 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldAddChildren()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getParentName + ()Ljava/lang/String; + 223 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 43 + 10 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldAddChildren()] + + removed call to java/util/List::add + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getParentName + ()Ljava/lang/String; + 217 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 9 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldAddChildren()] + + replaced return value with "" for edu/hm/hafner/coverage/CoverageNode::getParentName + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + getParentName + ()Ljava/lang/String; + 225 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 56 + 13 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldAddChildren()] + + replaced return value with "" for edu/hm/hafner/coverage/CoverageNode::getParentName + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + hasParent + ()Z + 203 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 5 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldAddChildren()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + hasParent + ()Z + 203 + org.pitest.mutationtest.engine.gregor.mutators.returns.BooleanTrueReturnValsMutator + 13 + 4 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldAddChildren()] + + replaced boolean return with true for edu/hm/hafner/coverage/CoverageNode::hasParent + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + insertPackage + (Ledu/hm/hafner/coverage/CoverageNode;Ljava/util/Deque;)V + 489 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 17 + 3 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithMultipleDots()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + insertPackage + (Ledu/hm/hafner/coverage/CoverageNode;Ljava/util/Deque;)V + 490 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 23 + 4 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithMultipleDots()] + + removed call to edu/hm/hafner/coverage/CoverageNode::addAll + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + insertPackage + (Ledu/hm/hafner/coverage/CoverageNode;Ljava/util/Deque;)V + 493 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 31 + 6 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithMultipleDots()] + + removed call to edu/hm/hafner/coverage/CoverageNode::insertPackage + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + isRoot + ()Z + 194 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 5 + 0 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldAddChildren()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + isRoot + ()Z + 194 + org.pitest.mutationtest.engine.gregor.mutators.returns.BooleanTrueReturnValsMutator + 13 + 3 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldAddChildren()] + + replaced boolean return with true for edu/hm/hafner/coverage/CoverageNode::isRoot + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + lambda$aggregateChildren$4 + + (Ledu/hm/hafner/coverage/CoverageMetric;Ledu/hm/hafner/coverage/CoverageNode;)Ledu/hm/hafner/coverage/Coverage; + + 285 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 6 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetModuleCoverage()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::lambda$aggregateChildren$4 + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + lambda$copyTree$12 + (Ledu/hm/hafner/coverage/CoverageNode;)Ledu/hm/hafner/coverage/CoverageNode; + 470 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 6 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldCopyWithChild()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::lambda$copyTree$12 + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + lambda$find$7 + + (Ledu/hm/hafner/coverage/CoverageMetric;Ljava/lang/String;Ledu/hm/hafner/coverage/CoverageNode;)Ljava/util/Optional; + + 366 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 7 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldFindMetric()] + + replaced return value with Optional.empty for edu/hm/hafner/coverage/CoverageNode::lambda$find$7 + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + lambda$find$8 + (Ljava/util/Optional;)Ljava/util/stream/Stream; + 367 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 9 + 2 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldFindMetric()] + + replaced return value with Stream.empty for edu/hm/hafner/coverage/CoverageNode::lambda$find$8 + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + lambda$findByHashCode$10 + (Ljava/util/Optional;)Ljava/util/stream/Stream; + 387 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 9 + 2 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldFindMetric()] + + replaced return value with Stream.empty for + edu/hm/hafner/coverage/CoverageNode::lambda$findByHashCode$10 + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + lambda$findByHashCode$9 + + (Ledu/hm/hafner/coverage/CoverageMetric;ILedu/hm/hafner/coverage/CoverageNode;)Ljava/util/Optional; + + 386 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 7 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldFindMetric()] + + replaced return value with Optional.empty for + edu/hm/hafner/coverage/CoverageNode::lambda$findByHashCode$9 + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + lambda$getAll$6 + (Ledu/hm/hafner/coverage/CoverageMetric;Ledu/hm/hafner/coverage/CoverageNode;)Ljava/util/List; + + 343 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 6 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithSingleDot()] + + replaced return value with Collections.emptyList for + edu/hm/hafner/coverage/CoverageNode::lambda$getAll$6 + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + lambda$getCoverage$3 + + (Ledu/hm/hafner/coverage/CoverageMetric;Ledu/hm/hafner/coverage/CoverageLeaf;)Ledu/hm/hafner/coverage/Coverage; + + 267 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 6 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetMetricsDistributionAndPercentages()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::lambda$getCoverage$3 + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + lambda$getMetricsDistribution$0 + + (Ledu/hm/hafner/coverage/Coverage;Ledu/hm/hafner/coverage/Coverage;)Ledu/hm/hafner/coverage/Coverage; + + 140 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 4 + 0 + + replaced return value with null for + edu/hm/hafner/coverage/CoverageNode::lambda$getMetricsDistribution$0 + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + lambda$getMetricsPercentages$1 + (Ledu/hm/hafner/coverage/CoverageMetric;)Lorg/apache/commons/lang3/math/Fraction; + + 146 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 7 + 2 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetMetricsDistributionAndPercentages()] + + replaced return value with null for + edu/hm/hafner/coverage/CoverageNode::lambda$getMetricsPercentages$1 + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + lambda$getMetricsPercentages$2 + + (Lorg/apache/commons/lang3/math/Fraction;Lorg/apache/commons/lang3/math/Fraction;)Lorg/apache/commons/lang3/math/Fraction; + + 147 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 4 + 0 + + replaced return value with null for + edu/hm/hafner/coverage/CoverageNode::lambda$getMetricsPercentages$2 + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + lambda$splitPackages$11 + (Ledu/hm/hafner/coverage/CoverageNode;)Z + 428 + org.pitest.mutationtest.engine.gregor.mutators.returns.BooleanFalseReturnValsMutator + 7 + 2 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithMultipleDots()] + + replaced boolean return with false for edu/hm/hafner/coverage/CoverageNode::lambda$splitPackages$11 + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + lambda$splitPackages$11 + (Ledu/hm/hafner/coverage/CoverageNode;)Z + 428 + org.pitest.mutationtest.engine.gregor.mutators.returns.BooleanTrueReturnValsMutator + 7 + 2 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithoutPackageNodes()] + + replaced boolean return with true for edu/hm/hafner/coverage/CoverageNode::lambda$splitPackages$11 + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + matches + (Ledu/hm/hafner/coverage/CoverageMetric;I)Z + 416 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 7 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldFindMetric()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + matches + (Ledu/hm/hafner/coverage/CoverageMetric;I)Z + 419 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 19 + 4 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldFindMetric()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + matches + (Ledu/hm/hafner/coverage/CoverageMetric;I)Z + 419 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 24 + 7 + + edu.hm.hafner.coverage.PackageCoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.PackageCoverageNodeTest]/[method:shouldMatchPath()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + matches + (Ledu/hm/hafner/coverage/CoverageMetric;I)Z + 417 + org.pitest.mutationtest.engine.gregor.mutators.returns.BooleanTrueReturnValsMutator + 11 + 2 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldFindMetric()] + + replaced boolean return with true for edu/hm/hafner/coverage/CoverageNode::matches + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + matches + (Ledu/hm/hafner/coverage/CoverageMetric;I)Z + 419 + org.pitest.mutationtest.engine.gregor.mutators.returns.BooleanTrueReturnValsMutator + 34 + 10 + + edu.hm.hafner.coverage.PackageCoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.PackageCoverageNodeTest]/[method:shouldMatchPath()] + + replaced boolean return with true for edu/hm/hafner/coverage/CoverageNode::matches + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + matches + (Ledu/hm/hafner/coverage/CoverageMetric;Ljava/lang/String;)Z + 402 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 7 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldFindMetric()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + matches + (Ledu/hm/hafner/coverage/CoverageMetric;Ljava/lang/String;)Z + 402 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 12 + 3 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldFindMetric()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + matches + (Ledu/hm/hafner/coverage/CoverageMetric;Ljava/lang/String;)Z + 402 + org.pitest.mutationtest.engine.gregor.mutators.returns.BooleanTrueReturnValsMutator + 20 + 6 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldFindMetric()] + + replaced boolean return with true for edu/hm/hafner/coverage/CoverageNode::matches + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + mergePath + (Ljava/lang/String;)Ljava/lang/String; + 88 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 6 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldMergePath()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + mergePath + (Ljava/lang/String;)Ljava/lang/String; + 92 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 16 + 4 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldMergePath()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + mergePath + (Ljava/lang/String;)Ljava/lang/String; + 95 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 27 + 8 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldMergePath()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + mergePath + (Ljava/lang/String;)Ljava/lang/String; + 98 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 37 + 11 + + edu.hm.hafner.coverage.PackageCoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.PackageCoverageNodeTest]/[method:shouldMergePath()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + mergePath + (Ljava/lang/String;)Ljava/lang/String; + 96 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 31 + 9 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldMergePath()] + + replaced return value with "" for edu/hm/hafner/coverage/CoverageNode::mergePath + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + mergePath + (Ljava/lang/String;)Ljava/lang/String; + 99 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 41 + 12 + + edu.hm.hafner.coverage.PackageCoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.PackageCoverageNodeTest]/[method:shouldMergePath()] + + replaced return value with "" for edu/hm/hafner/coverage/CoverageNode::mergePath + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + mergePath + (Ljava/lang/String;)Ljava/lang/String; + 101 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 55 + 18 + + edu.hm.hafner.coverage.PackageCoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.PackageCoverageNodeTest]/[method:shouldMergePath()] + + replaced return value with "" for edu/hm/hafner/coverage/CoverageNode::mergePath + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + mergePath + (Ljava/lang/String;)Ljava/lang/String; + 104 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 60 + 19 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldMergePath()] + + replaced return value with "" for edu/hm/hafner/coverage/CoverageNode::mergePath + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + printCoverageFor + (Ledu/hm/hafner/coverage/CoverageMetric;)Ljava/lang/String; + 238 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 7 + 2 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetAndPrintLineCoverage()] + + replaced return value with "" for edu/hm/hafner/coverage/CoverageNode::printCoverageFor + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + printCoverageFor + (Ledu/hm/hafner/coverage/CoverageMetric;Ljava/util/Locale;)Ljava/lang/String; + 252 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 8 + 2 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetAndPrintLineCoverage()] + + replaced return value with "" for edu/hm/hafner/coverage/CoverageNode::printCoverageFor + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + saveSubtractFraction + + (Lorg/apache/commons/lang3/math/Fraction;Lorg/apache/commons/lang3/math/Fraction;)Lorg/apache/commons/lang3/math/Fraction; + + 323 + org.pitest.mutationtest.engine.gregor.mutators.MathMutator + 18 + 4 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldComputeDeltaWithOverflow()] + + Replaced double subtraction with addition + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + saveSubtractFraction + + (Lorg/apache/commons/lang3/math/Fraction;Lorg/apache/commons/lang3/math/Fraction;)Lorg/apache/commons/lang3/math/Fraction; + + 320 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 7 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldComputeDeltaWithOverflow()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::saveSubtractFraction + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + saveSubtractFraction + + (Lorg/apache/commons/lang3/math/Fraction;Lorg/apache/commons/lang3/math/Fraction;)Lorg/apache/commons/lang3/math/Fraction; + + 324 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 24 + 5 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldComputeDeltaWithOverflow()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageNode::saveSubtractFraction + + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + splitPackages + ()V + 434 + org.pitest.mutationtest.engine.gregor.mutators.ConditionalsBoundaryMutator + 59 + 16 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldKeepChildAfterSplit()] + + changed conditional boundary + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + splitPackages + ()V + 426 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 7 + 1 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithMultipleDots()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + splitPackages + ()V + 430 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 27 + 7 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithMultipleDots()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + splitPackages + ()V + 434 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 59 + 16 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithSingleDot()] + + negated conditional + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + splitPackages + ()V + 431 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 32 + 8 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithSingleDot()] + + removed call to java/util/List::clear + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + splitPackages + ()V + 436 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 73 + 19 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithMultipleDots()] + + removed call to edu/hm/hafner/coverage/CoverageNode::insertPackage + + + CoverageNode.java + edu.hm.hafner.coverage.CoverageNode + splitPackages + ()V + 439 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 82 + 21 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldSplitPackagesWithSingleDot()] + + removed call to edu/hm/hafner/coverage/CoverageNode::add + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + <init> + (Ljava/lang/String;)V + 54 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 29 + 1 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + removed call to edu/hm/hafner/parser/XmlParser::parseFile + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + endElement + (Ljavax/xml/stream/events/EndElement;)V + 251 + org.pitest.mutationtest.engine.gregor.mutators.ConditionalsBoundaryMutator + 75 + 16 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + changed conditional boundary + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + endElement + (Ljavax/xml/stream/events/EndElement;)V + 251 + org.pitest.mutationtest.engine.gregor.mutators.MathMutator + 74 + 16 + + Replaced integer addition with subtraction + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + endElement + (Ljavax/xml/stream/events/EndElement;)V + 251 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 75 + 16 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + negated conditional + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + endElement + (Ljavax/xml/stream/events/EndElement;)V + 249 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 67 + 15 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + removed call to edu/hm/hafner/coverage/CoverageNode::add + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + endElement + (Ljavax/xml/stream/events/EndElement;)V + 254 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 99 + 19 + + removed call to edu/hm/hafner/coverage/CoverageNode::add + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleClassElement + (Ljavax/xml/stream/events/StartElement;)V + 131 + org.pitest.mutationtest.engine.gregor.mutators.MathMutator + 32 + 6 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + Replaced integer subtraction with addition + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleClassElement + (Ljavax/xml/stream/events/StartElement;)V + 137 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 52 + 9 + + negated conditional + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleClassElement + (Ljavax/xml/stream/events/StartElement;)V + 148 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 67 + 12 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + negated conditional + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleClassElement + (Ljavax/xml/stream/events/StartElement;)V + 138 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 61 + 10 + + removed call to java/util/List::forEach + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleClassElement + (Ljavax/xml/stream/events/StartElement;)V + 150 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 79 + 14 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + removed call to edu/hm/hafner/coverage/FileCoverageNode::add + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleClassElement + (Ljavax/xml/stream/events/StartElement;)V + 151 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 85 + 15 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + removed call to edu/hm/hafner/coverage/PackageCoverageNode::add + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 181 + org.pitest.mutationtest.engine.gregor.mutators.ConditionalsBoundaryMutator + 53 + 15 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:testAmountOfLinenumberTolines()] + + changed conditional boundary + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 195 + org.pitest.mutationtest.engine.gregor.mutators.ConditionalsBoundaryMutator + 112 + 28 + + changed conditional boundary + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 209 + org.pitest.mutationtest.engine.gregor.mutators.ConditionalsBoundaryMutator + 163 + 40 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + changed conditional boundary + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 221 + org.pitest.mutationtest.engine.gregor.mutators.ConditionalsBoundaryMutator + 210 + 48 + + changed conditional boundary + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 210 + org.pitest.mutationtest.engine.gregor.mutators.MathMutator + 170 + 41 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + Replaced integer addition with subtraction + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 213 + org.pitest.mutationtest.engine.gregor.mutators.MathMutator + 180 + 42 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + Replaced integer addition with subtraction + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 222 + org.pitest.mutationtest.engine.gregor.mutators.MathMutator + 217 + 49 + + Replaced integer addition with subtraction + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 225 + org.pitest.mutationtest.engine.gregor.mutators.MathMutator + 228 + 50 + + Replaced integer subtraction with addition + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 225 + org.pitest.mutationtest.engine.gregor.mutators.MathMutator + 229 + 50 + + Replaced integer addition with subtraction + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 171 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 30 + 7 + + negated conditional + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 176 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 45 + 13 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + negated conditional + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 180 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 49 + 14 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + negated conditional + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 181 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 53 + 15 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:testAmountOfLinenumberTolines()] + + negated conditional + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 195 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 112 + 28 + + negated conditional + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 208 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 159 + 39 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + negated conditional + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 209 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 163 + 40 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + negated conditional + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 221 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 210 + 48 + + negated conditional + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + lambda$handleClassElement$0 + + (Ljava/lang/String;Ledu/hm/hafner/coverage/CoverageNode;Ljava/util/concurrent/atomic/AtomicBoolean;Ledu/hm/hafner/coverage/CoverageNode;)V + + 139 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 7 + 2 + + negated conditional + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + lambda$handleClassElement$0 + + (Ljava/lang/String;Ledu/hm/hafner/coverage/CoverageNode;Ljava/util/concurrent/atomic/AtomicBoolean;Ledu/hm/hafner/coverage/CoverageNode;)V + + 140 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 12 + 3 + + removed call to edu/hm/hafner/coverage/CoverageNode::add + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + lambda$handleClassElement$0 + + (Ljava/lang/String;Ledu/hm/hafner/coverage/CoverageNode;Ljava/util/concurrent/atomic/AtomicBoolean;Ledu/hm/hafner/coverage/CoverageNode;)V + + 141 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 17 + 4 + + removed call to java/util/concurrent/atomic/AtomicBoolean::set + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + parseConditionCoverage + (Ljavax/xml/stream/events/StartElement;)[Ljava/lang/String; + 272 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 23 + 4 + + replaced return value with null for edu/hm/hafner/parser/CoberturaParser::parseConditionCoverage + + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + startDocument + (Ljavax/xml/stream/events/XMLEvent;)V + 67 + org.pitest.mutationtest.engine.gregor.mutators.MathMutator + 20 + 3 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + Replaced integer subtraction with addition + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + startDocument + (Ljavax/xml/stream/events/XMLEvent;)V + 69 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 31 + 4 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + removed call to edu/hm/hafner/parser/CoberturaParser::setRootNode + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + startElement + (Ljavax/xml/stream/events/StartElement;)V + 87 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 75 + 21 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + removed call to edu/hm/hafner/coverage/CoverageNode::add + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + startElement + (Ljavax/xml/stream/events/StartElement;)V + 94 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 94 + 23 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + removed call to edu/hm/hafner/parser/CoberturaParser::handleClassElement + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + startElement + (Ljavax/xml/stream/events/StartElement;)V + 105 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 137 + 33 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + removed call to edu/hm/hafner/coverage/CoverageNode::add + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + startElement + (Ljavax/xml/stream/events/StartElement;)V + 107 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 143 + 34 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + removed call to edu/hm/hafner/coverage/CoverageNode::add + + + CoberturaParser.java + edu.hm.hafner.coverage.parser.CoberturaParser + startElement + (Ljavax/xml/stream/events/StartElement;)V + 112 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 157 + 36 + + edu.hm.hafner.parser.CoberturaParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.CoberturaParserTest]/[method:shouldConvertCoberturaBigToTree()] + + removed call to edu/hm/hafner/parser/CoberturaParser::handleLineElement + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + <init> + (Ljava/lang/String;ILedu/hm/hafner/coverage/CoverageMetric$MetricType;)V + 152 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 20 + 1 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + negated conditional + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + compareTo + (Ledu/hm/hafner/coverage/CoverageMetric;)I + 170 + org.pitest.mutationtest.engine.gregor.mutators.MathMutator + 7 + 0 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldSortMetrics()] + + Replaced integer subtraction with addition + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + compareTo + (Ledu/hm/hafner/coverage/CoverageMetric;)I + 170 + org.pitest.mutationtest.engine.gregor.mutators.returns.PrimitiveReturnsMutator + 8 + 0 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldSortMetrics()] + + replaced int return with 0 for edu/hm/hafner/coverage/CoverageMetric::compareTo + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + equalsIgnoreCase + (Ljava/lang/String;)Z + 108 + org.pitest.mutationtest.engine.gregor.mutators.returns.BooleanFalseReturnValsMutator + 7 + 2 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreatePredefinedMetrics()] + + replaced boolean return with false for edu/hm/hafner/coverage/CoverageMetric::equalsIgnoreCase + + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + equalsIgnoreCase + (Ljava/lang/String;)Z + 108 + org.pitest.mutationtest.engine.gregor.mutators.returns.BooleanTrueReturnValsMutator + 7 + 2 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + replaced boolean return with true for edu/hm/hafner/coverage/CoverageMetric::equalsIgnoreCase + + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + equalsIgnoreCase + (Ljava/lang/String;Ljava/lang/String;)Z + 134 + org.pitest.mutationtest.engine.gregor.mutators.returns.BooleanFalseReturnValsMutator + 8 + 3 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreatePredefinedMetrics()] + + replaced boolean return with false for edu/hm/hafner/coverage/CoverageMetric::equalsIgnoreCase + + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + equalsIgnoreCase + (Ljava/lang/String;Ljava/lang/String;)Z + 134 + org.pitest.mutationtest.engine.gregor.mutators.returns.BooleanTrueReturnValsMutator + 8 + 3 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + replaced boolean return with true for edu/hm/hafner/coverage/CoverageMetric::equalsIgnoreCase + + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + getAvailableCoverageMetrics + ()Ljava/util/List; + 96 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 42 + 1 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldGetAvailableMetrics()] + + replaced return value with Collections.emptyList for + edu/hm/hafner/coverage/CoverageMetric::getAvailableCoverageMetrics + + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + getName + ()Ljava/lang/String; + 156 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 5 + 0 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + replaced return value with "" for edu/hm/hafner/coverage/CoverageMetric::getName + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + isLeaf + ()Z + 160 + org.pitest.mutationtest.engine.gregor.mutators.returns.BooleanFalseReturnValsMutator + 5 + 0 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreatePredefinedMetrics()] + + replaced boolean return with false for edu/hm/hafner/coverage/CoverageMetric::isLeaf + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + isLeaf + ()Z + 160 + org.pitest.mutationtest.engine.gregor.mutators.returns.BooleanTrueReturnValsMutator + 5 + 0 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + replaced boolean return with true for edu/hm/hafner/coverage/CoverageMetric::isLeaf + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + normalize + (Ljava/lang/String;)Ljava/lang/String; + 138 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 7 + 2 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + replaced return value with "" for edu/hm/hafner/coverage/CoverageMetric::normalize + + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 60 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 6 + 1 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + negated conditional + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 60 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 10 + 3 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + negated conditional + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 63 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 22 + 6 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + negated conditional + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 66 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 33 + 9 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + negated conditional + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 69 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 44 + 12 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + negated conditional + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 72 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 55 + 15 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + negated conditional + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 75 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 66 + 18 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + negated conditional + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 78 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 77 + 21 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + negated conditional + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 81 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 88 + 24 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + negated conditional + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 81 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 92 + 26 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + negated conditional + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 84 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 104 + 29 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + negated conditional + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 61 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 15 + 4 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreatePredefinedMetrics()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageMetric::valueOf + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 64 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 26 + 7 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreatePredefinedMetrics()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageMetric::valueOf + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 67 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 37 + 10 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreatePredefinedMetrics()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageMetric::valueOf + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 70 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 48 + 13 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreatePredefinedMetrics()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageMetric::valueOf + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 73 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 59 + 16 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreatePredefinedMetrics()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageMetric::valueOf + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 76 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 70 + 19 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreatePredefinedMetrics()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageMetric::valueOf + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 79 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 81 + 22 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreatePredefinedMetrics()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageMetric::valueOf + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 82 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 97 + 27 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreatePredefinedMetrics()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageMetric::valueOf + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 85 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 108 + 30 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreatePredefinedMetrics()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageMetric::valueOf + + + CoverageMetric.java + edu.hm.hafner.coverage.Metric + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/coverage/CoverageMetric; + 87 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 117 + 32 + + edu.hm.hafner.coverage.CoverageMetricTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageMetricTest]/[method:shouldCreateNewMetric()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageMetric::valueOf + + + Coverage.java + edu.hm.hafner.coverage.Coverage + add + (Ledu/hm/hafner/coverage/Coverage;)Ledu/hm/hafner/coverage/Coverage; + 148 + org.pitest.mutationtest.engine.gregor.mutators.MathMutator + 9 + 1 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldCreatePercentages()] + + Replaced integer addition with subtraction + + + Coverage.java + edu.hm.hafner.coverage.Coverage + add + (Ledu/hm/hafner/coverage/Coverage;)Ledu/hm/hafner/coverage/Coverage; + 149 + org.pitest.mutationtest.engine.gregor.mutators.MathMutator + 16 + 2 + + edu.hm.hafner.coverage.CoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageNodeTest]/[method:shouldGetMetricsDistributionAndPercentages()] + + Replaced integer addition with subtraction + + + Coverage.java + edu.hm.hafner.coverage.Coverage + add + (Ledu/hm/hafner/coverage/Coverage;)Ledu/hm/hafner/coverage/Coverage; + 148 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 20 + 3 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldProvideNullObject()] + + replaced return value with null for edu/hm/hafner/coverage/Coverage::add + + + Coverage.java + edu.hm.hafner.coverage.Coverage + formatCoveredPercentage + ()Ljava/lang/String; + 74 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 6 + 2 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldProvideNullObject()] + + replaced return value with "" for edu/hm/hafner/coverage/Coverage::formatCoveredPercentage + + + + Coverage.java + edu.hm.hafner.coverage.Coverage + formatCoveredPercentage + (Ljava/util/Locale;)Ljava/lang/String; + 86 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 8 + 2 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldProvideNullObject()] + + replaced return value with "" for edu/hm/hafner/coverage/Coverage::formatCoveredPercentage + + + + Coverage.java + edu.hm.hafner.coverage.Coverage + formatMissedPercentage + ()Ljava/lang/String; + 117 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 6 + 2 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldProvideNullObject()] + + replaced return value with "" for edu/hm/hafner/coverage/Coverage::formatMissedPercentage + + + + Coverage.java + edu.hm.hafner.coverage.Coverage + formatMissedPercentage + (Ljava/util/Locale;)Ljava/lang/String; + 129 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 8 + 2 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldProvideNullObject()] + + replaced return value with "" for edu/hm/hafner/coverage/Coverage::formatMissedPercentage + + + + Coverage.java + edu.hm.hafner.coverage.Coverage + getCovered + ()I + 51 + org.pitest.mutationtest.engine.gregor.mutators.returns.PrimitiveReturnsMutator + 5 + 0 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldCreatePercentages()] + + replaced int return with 0 for edu/hm/hafner/coverage/Coverage::getCovered + + + Coverage.java + edu.hm.hafner.coverage.Coverage + getCoveredPercentage + ()Lorg/apache/commons/lang3/math/Fraction; + 60 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 5 + 1 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldProvideNullObject()] + + negated conditional + + + Coverage.java + edu.hm.hafner.coverage.Coverage + getCoveredPercentage + ()Lorg/apache/commons/lang3/math/Fraction; + 61 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 9 + 2 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldProvideNullObject()] + + replaced return value with null for edu/hm/hafner/coverage/Coverage::getCoveredPercentage + + + Coverage.java + edu.hm.hafner.coverage.Coverage + getCoveredPercentage + ()Lorg/apache/commons/lang3/math/Fraction; + 63 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 18 + 5 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldCreatePercentages()] + + replaced return value with null for edu/hm/hafner/coverage/Coverage::getCoveredPercentage + + + Coverage.java + edu.hm.hafner.coverage.Coverage + getMissed + ()I + 95 + org.pitest.mutationtest.engine.gregor.mutators.returns.PrimitiveReturnsMutator + 5 + 0 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldCreatePercentages()] + + replaced int return with 0 for edu/hm/hafner/coverage/Coverage::getMissed + + + Coverage.java + edu.hm.hafner.coverage.Coverage + getMissedPercentage + ()Lorg/apache/commons/lang3/math/Fraction; + 104 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 5 + 1 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldProvideNullObject()] + + negated conditional + + + Coverage.java + edu.hm.hafner.coverage.Coverage + getMissedPercentage + ()Lorg/apache/commons/lang3/math/Fraction; + 105 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 9 + 2 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldProvideNullObject()] + + replaced return value with null for edu/hm/hafner/coverage/Coverage::getMissedPercentage + + + Coverage.java + edu.hm.hafner.coverage.Coverage + getMissedPercentage + ()Lorg/apache/commons/lang3/math/Fraction; + 107 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 17 + 5 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldCreatePercentages()] + + replaced return value with null for edu/hm/hafner/coverage/Coverage::getMissedPercentage + + + Coverage.java + edu.hm.hafner.coverage.Coverage + getTotal + ()I + 162 + org.pitest.mutationtest.engine.gregor.mutators.MathMutator + 7 + 0 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldCreatePercentages()] + + Replaced integer addition with subtraction + + + Coverage.java + edu.hm.hafner.coverage.Coverage + getTotal + ()I + 162 + org.pitest.mutationtest.engine.gregor.mutators.returns.PrimitiveReturnsMutator + 8 + 0 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldCreatePercentages()] + + replaced int return with 0 for edu/hm/hafner/coverage/Coverage::getTotal + + + Coverage.java + edu.hm.hafner.coverage.Coverage + isSet + ()Z + 166 + org.pitest.mutationtest.engine.gregor.mutators.ConditionalsBoundaryMutator + 5 + 1 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldProvideNullObject()] + + changed conditional boundary + + + Coverage.java + edu.hm.hafner.coverage.Coverage + isSet + ()Z + 166 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 5 + 1 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldProvideNullObject()] + + negated conditional + + + Coverage.java + edu.hm.hafner.coverage.Coverage + isSet + ()Z + 166 + org.pitest.mutationtest.engine.gregor.mutators.returns.BooleanTrueReturnValsMutator + 13 + 4 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldProvideNullObject()] + + replaced boolean return with true for edu/hm/hafner/coverage/Coverage::isSet + + + Coverage.java + edu.hm.hafner.coverage.Coverage + printPercentage + (Ljava/util/Locale;Lorg/apache/commons/lang3/math/Fraction;)Ljava/lang/String; + + 133 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 5 + 1 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldProvideNullObject()] + + negated conditional + + + Coverage.java + edu.hm.hafner.coverage.Coverage + printPercentage + (Ljava/util/Locale;Lorg/apache/commons/lang3/math/Fraction;)Ljava/lang/String; + + 134 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 21 + 6 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldCreatePercentages()] + + replaced return value with "" for edu/hm/hafner/coverage/Coverage::printPercentage + + + + Coverage.java + edu.hm.hafner.coverage.Coverage + printPercentage + (Ljava/util/Locale;Lorg/apache/commons/lang3/math/Fraction;)Ljava/lang/String; + + 136 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 26 + 7 + + edu.hm.hafner.coverage.CoverageTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageTest]/[method:shouldProvideNullObject()] + + replaced return value with "" for edu/hm/hafner/coverage/Coverage::printPercentage + + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + <init> + (Ljava/lang/String;)V + 47 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 16 + 2 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldCreatePackageName()] + + removed call to edu/hm/hafner/parser/JacocoParser::parseFile + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + handleClassElement + (Ljavax/xml/stream/events/StartElement;)V + 137 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 25 + 6 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldThrowExceptionWhenObtainingAllBasicBlocks()] + + negated conditional + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + handleCounterElement + (Ljavax/xml/stream/events/StartElement;)V + 158 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 15 + 4 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + negated conditional + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + handleCounterElement + (Ljavax/xml/stream/events/StartElement;)V + 184 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 134 + 34 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + removed call to edu/hm/hafner/coverage/CoverageNode::add + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 199 + org.pitest.mutationtest.engine.gregor.mutators.ConditionalsBoundaryMutator + 44 + 15 + + changed conditional boundary + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 199 + org.pitest.mutationtest.engine.gregor.mutators.ConditionalsBoundaryMutator + 46 + 16 + + changed conditional boundary + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 205 + org.pitest.mutationtest.engine.gregor.mutators.ConditionalsBoundaryMutator + 75 + 23 + + changed conditional boundary + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 205 + org.pitest.mutationtest.engine.gregor.mutators.ConditionalsBoundaryMutator + 77 + 24 + + changed conditional boundary + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 199 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 44 + 15 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + negated conditional + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 199 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 46 + 16 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + negated conditional + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 205 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 75 + 23 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + negated conditional + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + handleLineElement + (Ljavax/xml/stream/events/StartElement;)V + 205 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 77 + 24 + + negated conditional + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + startDocument + (Ljavax/xml/stream/events/XMLEvent;)V + 54 + org.pitest.mutationtest.engine.gregor.mutators.MathMutator + 21 + 3 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldThrowExceptionWhenObtainingAllBasicBlocks()] + + Replaced integer subtraction with addition + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + startElement + (Ljavax/xml/stream/events/StartElement;)V + 72 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 108 + 34 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldNotSplitPackagesIfOnWrongHierarchyNode()] + + removed call to edu/hm/hafner/parser/JacocoParser::setRootNode + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + startElement + (Ljavax/xml/stream/events/StartElement;)V + 79 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 141 + 42 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldNotSplitPackagesIfOnWrongHierarchyNode()] + + removed call to edu/hm/hafner/coverage/CoverageNode::add + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + startElement + (Ljavax/xml/stream/events/StartElement;)V + 86 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 160 + 44 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldThrowExceptionWhenObtainingAllBasicBlocks()] + + removed call to edu/hm/hafner/parser/JacocoParser::handleClassElement + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + startElement + (Ljavax/xml/stream/events/StartElement;)V + 93 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 183 + 49 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldThrowExceptionWhenObtainingAllBasicBlocks()] + + removed call to edu/hm/hafner/coverage/CoverageNode::add + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + startElement + (Ljavax/xml/stream/events/StartElement;)V + 98 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 197 + 51 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + removed call to edu/hm/hafner/parser/JacocoParser::handleCounterElement + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + startElement + (Ljavax/xml/stream/events/StartElement;)V + 108 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 242 + 62 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + removed call to edu/hm/hafner/coverage/CoverageNode::add + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + startElement + (Ljavax/xml/stream/events/StartElement;)V + 111 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 252 + 64 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldCreatePackageName()] + + removed call to edu/hm/hafner/coverage/CoverageNode::add + + + JacocoParser.java + edu.hm.hafner.coverage.parser.JacocoParser + startElement + (Ljavax/xml/stream/events/StartElement;)V + 116 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 266 + 66 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + removed call to edu/hm/hafner/parser/JacocoParser::handleLineElement + + + FileCoverageNode.java + edu.hm.hafner.coverage.FileCoverageNode + copyEmpty + ()Ledu/hm/hafner/coverage/CoverageNode; + 86 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 8 + 2 + + edu.hm.hafner.coverage.FileNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.FileNodeTest]/[method:shouldCopyEmpty()] + + replaced return value with null for edu/hm/hafner/coverage/FileCoverageNode::copyEmpty + + + FileCoverageNode.java + edu.hm.hafner.coverage.FileCoverageNode + getCoveredBranchesCount + ()J + 74 + org.pitest.mutationtest.engine.gregor.mutators.returns.PrimitiveReturnsMutator + 17 + 4 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + replaced long return with 0 for edu/hm/hafner/coverage/FileCoverageNode::getCoveredBranchesCount + + + + FileCoverageNode.java + edu.hm.hafner.coverage.FileCoverageNode + getCoveredInstructionsCount + ()J + 52 + org.pitest.mutationtest.engine.gregor.mutators.returns.PrimitiveReturnsMutator + 17 + 4 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + replaced long return with 0 for edu/hm/hafner/coverage/FileCoverageNode::getCoveredInstructionsCount + + + + FileCoverageNode.java + edu.hm.hafner.coverage.FileCoverageNode + getLineNumberToBranchCoverage + ()Ljava/util/Map; + 28 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 5 + 0 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldThrowExceptionWhenObtainingAllBasicBlocks()] + + replaced return value with Collections.emptyMap for + edu/hm/hafner/coverage/FileCoverageNode::getLineNumberToBranchCoverage + + + + FileCoverageNode.java + edu.hm.hafner.coverage.FileCoverageNode + getLineNumberToInstructionCoverage + ()Ljava/util/Map; + 32 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 5 + 0 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldThrowExceptionWhenObtainingAllBasicBlocks()] + + replaced return value with Collections.emptyMap for + edu/hm/hafner/coverage/FileCoverageNode::getLineNumberToInstructionCoverage + + + + FileCoverageNode.java + edu.hm.hafner.coverage.FileCoverageNode + getMissedBranchesCount + ()J + 63 + org.pitest.mutationtest.engine.gregor.mutators.returns.PrimitiveReturnsMutator + 17 + 4 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + replaced long return with 0 for edu/hm/hafner/coverage/FileCoverageNode::getMissedBranchesCount + + + + FileCoverageNode.java + edu.hm.hafner.coverage.FileCoverageNode + getMissedInstructionsCount + ()J + 41 + org.pitest.mutationtest.engine.gregor.mutators.returns.PrimitiveReturnsMutator + 17 + 4 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + replaced long return with 0 for edu/hm/hafner/coverage/FileCoverageNode::getMissedInstructionsCount + + + + FileCoverageNode.java + edu.hm.hafner.coverage.FileCoverageNode + getPath + ()Ljava/lang/String; + 81 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 7 + 2 + + edu.hm.hafner.coverage.FileNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.FileNodeTest]/[method:shouldGetPath()] + + replaced return value with "" for edu/hm/hafner/coverage/FileCoverageNode::getPath + + + + FileCoverageNode.java + edu.hm.hafner.coverage.FileCoverageNode + lambda$getCoveredBranchesCount$3 + (Ledu/hm/hafner/coverage/CoverageLeaf;)I + 75 + org.pitest.mutationtest.engine.gregor.mutators.returns.PrimitiveReturnsMutator + 6 + 2 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + replaced int return with 0 for + edu/hm/hafner/coverage/FileCoverageNode::lambda$getCoveredBranchesCount$3 + + + + FileCoverageNode.java + edu.hm.hafner.coverage.FileCoverageNode + lambda$getCoveredInstructionsCount$1 + (Ledu/hm/hafner/coverage/CoverageLeaf;)I + 53 + org.pitest.mutationtest.engine.gregor.mutators.returns.PrimitiveReturnsMutator + 6 + 2 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + replaced int return with 0 for + edu/hm/hafner/coverage/FileCoverageNode::lambda$getCoveredInstructionsCount$1 + + + + FileCoverageNode.java + edu.hm.hafner.coverage.FileCoverageNode + lambda$getMissedBranchesCount$2 + (Ledu/hm/hafner/coverage/CoverageLeaf;)I + 64 + org.pitest.mutationtest.engine.gregor.mutators.returns.PrimitiveReturnsMutator + 6 + 2 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + replaced int return with 0 for + edu/hm/hafner/coverage/FileCoverageNode::lambda$getMissedBranchesCount$2 + + + + FileCoverageNode.java + edu.hm.hafner.coverage.FileCoverageNode + lambda$getMissedInstructionsCount$0 + (Ledu/hm/hafner/coverage/CoverageLeaf;)I + 42 + org.pitest.mutationtest.engine.gregor.mutators.returns.PrimitiveReturnsMutator + 6 + 2 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + replaced int return with 0 for + edu/hm/hafner/coverage/FileCoverageNode::lambda$getMissedInstructionsCount$0 + + + + XmlParser.java + edu.hm.hafner.coverage.parser.XmlParser + getRootNode + ()Ledu/hm/hafner/coverage/CoverageNode; + 24 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 4 + 0 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldThrowExceptionWhenObtainingAllBasicBlocks()] + + replaced return value with null for edu/hm/hafner/parser/XmlParser::getRootNode + + + XmlParser.java + edu.hm.hafner.coverage.parser.XmlParser + parseFile + (Ljava/lang/String;)V + 42 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 20 + 5 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldNotSplitPackagesIfOnWrongHierarchyNode()] + + negated conditional + + + XmlParser.java + edu.hm.hafner.coverage.parser.XmlParser + parseFile + (Ljava/lang/String;)V + 45 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 30 + 8 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldThrowExceptionWhenObtainingAllBasicBlocks()] + + negated conditional + + + XmlParser.java + edu.hm.hafner.coverage.parser.XmlParser + parseFile + (Ljava/lang/String;)V + 49 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 41 + 11 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldThrowExceptionWhenObtainingAllBasicBlocks()] + + negated conditional + + + XmlParser.java + edu.hm.hafner.coverage.parser.XmlParser + parseFile + (Ljava/lang/String;)V + 53 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 53 + 15 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldThrowExceptionWhenObtainingAllBasicBlocks()] + + negated conditional + + + XmlParser.java + edu.hm.hafner.coverage.parser.XmlParser + parseFile + (Ljava/lang/String;)V + 46 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 35 + 9 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + removed call to edu/hm/hafner/parser/XmlParser::startDocument + + + XmlParser.java + edu.hm.hafner.coverage.parser.XmlParser + parseFile + (Ljava/lang/String;)V + 50 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 47 + 13 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldThrowExceptionWhenObtainingAllBasicBlocks()] + + removed call to edu/hm/hafner/parser/XmlParser::startElement + + + XmlParser.java + edu.hm.hafner.coverage.parser.XmlParser + parseFile + (Ljava/lang/String;)V + 54 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 59 + 17 + + edu.hm.hafner.parser.JacocoParserTest.[engine:junit-jupiter]/[class:edu.hm.hafner.parser.JacocoParserTest]/[method:shouldSplitPackages()] + + removed call to edu/hm/hafner/parser/XmlParser::endElement + + + XmlParser.java + edu.hm.hafner.coverage.parser.XmlParser + parseFile + (Ljava/lang/String;)V + 59 + org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator + 75 + 20 + + removed call to java/lang/Exception::printStackTrace + + + CoverageLeaf.java + edu.hm.hafner.coverage.CoverageLeaf + getCoverage + ()Ledu/hm/hafner/coverage/Coverage; + 38 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 5 + 0 + + edu.hm.hafner.coverage.CoverageLeafTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageLeafTest]/[method:shouldCreateLeaf()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageLeaf::getCoverage + + + CoverageLeaf.java + edu.hm.hafner.coverage.CoverageLeaf + getCoverage + (Ledu/hm/hafner/coverage/CoverageMetric;)Ledu/hm/hafner/coverage/Coverage; + 50 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 7 + 1 + + edu.hm.hafner.coverage.CoverageLeafTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageLeafTest]/[method:shouldCreateLeaf()] + + negated conditional + + + CoverageLeaf.java + edu.hm.hafner.coverage.CoverageLeaf + getCoverage + (Ledu/hm/hafner/coverage/CoverageMetric;)Ledu/hm/hafner/coverage/Coverage; + 51 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 12 + 2 + + edu.hm.hafner.coverage.CoverageLeafTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageLeafTest]/[method:shouldCreateLeaf()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageLeaf::getCoverage + + + CoverageLeaf.java + edu.hm.hafner.coverage.CoverageLeaf + getCoverage + (Ledu/hm/hafner/coverage/CoverageMetric;)Ledu/hm/hafner/coverage/Coverage; + 53 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 17 + 3 + + edu.hm.hafner.coverage.CoverageLeafTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageLeafTest]/[method:shouldCreateLeaf()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageLeaf::getCoverage + + + CoverageLeaf.java + edu.hm.hafner.coverage.CoverageLeaf + getMetric + ()Ledu/hm/hafner/coverage/CoverageMetric; + 34 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 5 + 0 + + edu.hm.hafner.coverage.CoverageLeafTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.CoverageLeafTest]/[method:shouldCreateLeaf()] + + replaced return value with null for edu/hm/hafner/coverage/CoverageLeaf::getMetric + + + MethodCoverageNode.java + edu.hm.hafner.coverage.MethodCoverageNode + getLineNumber + ()I + 44 + org.pitest.mutationtest.engine.gregor.mutators.returns.PrimitiveReturnsMutator + 5 + 0 + + edu.hm.hafner.coverage.MethodCoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.MethodCoverageNodeTest]/[method:shouldGetValidLineNumber()] + + replaced int return with 0 for edu/hm/hafner/coverage/MethodCoverageNode::getLineNumber + + + MethodCoverageNode.java + edu.hm.hafner.coverage.MethodCoverageNode + hasValidLineNumber + ()Z + 40 + org.pitest.mutationtest.engine.gregor.mutators.ConditionalsBoundaryMutator + 5 + 0 + + edu.hm.hafner.coverage.MethodCoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.MethodCoverageNodeTest]/[method:shouldCheckInvalidLineNumber()] + + changed conditional boundary + + + MethodCoverageNode.java + edu.hm.hafner.coverage.MethodCoverageNode + hasValidLineNumber + ()Z + 40 + org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator + 5 + 0 + + edu.hm.hafner.coverage.MethodCoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.MethodCoverageNodeTest]/[method:shouldCheckInvalidLineNumber()] + + negated conditional + + + MethodCoverageNode.java + edu.hm.hafner.coverage.MethodCoverageNode + hasValidLineNumber + ()Z + 40 + org.pitest.mutationtest.engine.gregor.mutators.returns.BooleanTrueReturnValsMutator + 13 + 3 + + edu.hm.hafner.coverage.MethodCoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.MethodCoverageNodeTest]/[method:shouldCheckInvalidLineNumber()] + + replaced boolean return with true for edu/hm/hafner/coverage/MethodCoverageNode::hasValidLineNumber + + + + PackageCoverageNode.java + edu.hm.hafner.coverage.PackageCoverageNode + copyEmpty + ()Ledu/hm/hafner/coverage/CoverageNode; + 28 + org.pitest.mutationtest.engine.gregor.mutators.returns.NullReturnValsMutator + 8 + 2 + + edu.hm.hafner.coverage.PackageCoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.PackageCoverageNodeTest]/[method:shouldCopyEmpty()] + + replaced return value with null for edu/hm/hafner/coverage/PackageCoverageNode::copyEmpty + + + PackageCoverageNode.java + edu.hm.hafner.coverage.PackageCoverageNode + getPath + ()Ljava/lang/String; + 23 + org.pitest.mutationtest.engine.gregor.mutators.returns.EmptyObjectReturnValsMutator + 10 + 3 + + edu.hm.hafner.coverage.PackageCoverageNodeTest.[engine:junit-jupiter]/[class:edu.hm.hafner.coverage.PackageCoverageNodeTest]/[method:shouldMatchPath()] + + replaced return value with "" for edu/hm/hafner/coverage/PackageCoverageNode::getPath + + + diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/pit.xml b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/pit.xml new file mode 100755 index 000000000..3edb5f1f1 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/pit.xml @@ -0,0 +1,3668 @@ + + + MODULE + - + + + PACKAGE + edu.hm.hafner.util + + + FILE + PathUtil.java + + + CLASS + PathUtil + + + METHOD + createAbsolutePath + + + MUTATION: 7/7 + + + (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + 0 + + + METHOD + exists + + + MUTATION: 3/3 + + + (Ljava/lang/String;)Z + 0 + + + METHOD + exists + + + MUTATION: 2/2 + + + (Ljava/lang/String;Ljava/lang/String;)Z + 0 + + + METHOD + getAbsolutePath + + + MUTATION: 2/2 + + + (Ljava/lang/String;)Ljava/lang/String; + 0 + + + METHOD + getAbsolutePath + + + MUTATION: 2/2 + + + (Ljava/nio/file/Path;)Ljava/lang/String; + 0 + + + METHOD + getRelativePath + + + MUTATION: 2/2 + + + (Ljava/lang/String;)Ljava/lang/String; + 0 + + + METHOD + getRelativePath + + + MUTATION: 2/2 + + + (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + 0 + + + METHOD + getRelativePath + + + MUTATION: 1/1 + + + (Ljava/nio/file/Path;)Ljava/lang/String; + 0 + + + METHOD + getRelativePath + + + MUTATION: 2/2 + + + (Ljava/nio/file/Path;Ljava/lang/String;)Ljava/lang/String; + 0 + + + METHOD + getRelativePath + + + MUTATION: 4/4 + + + (Ljava/nio/file/Path;Ljava/nio/file/Path;)Ljava/lang/String; + 0 + + + METHOD + isAbsolute + + + MUTATION: 3/3 + + + (Ljava/lang/String;)Z + 0 + + + METHOD + makeUnixPath + + + MUTATION: 2/2 + + + (Ljava/lang/String;)Ljava/lang/String; + 0 + + + METHOD + normalize + + + MUTATION: 1/1 + + + (Ljava/nio/file/Path;)Ljava/nio/file/Path; + 0 + + + + + + + + LINE: 27/27 + + + [] + [] + [] + [] + [FILE: 0, CLASS: -1, METHOD: -1, LINE: 0, LOC: -16] + + + true + KILLED + 232 + NEGATE_CONDITIONALS + edu.hm.hafner.util.PathUtil + createAbsolutePath + (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + + + true + KILLED + 232 + NEGATE_CONDITIONALS + edu.hm.hafner.util.PathUtil + createAbsolutePath + (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + + + true + KILLED + 238 + NEGATE_CONDITIONALS + edu.hm.hafner.util.PathUtil + createAbsolutePath + (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + + + true + KILLED + 247 + NEGATE_CONDITIONALS + edu.hm.hafner.util.PathUtil + createAbsolutePath + (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + + + true + KILLED + 233 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + createAbsolutePath + (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + + + true + KILLED + 247 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + createAbsolutePath + (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + + + true + KILLED + 250 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + createAbsolutePath + (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + + + true + KILLED + 42 + FALSE_RETURNS + edu.hm.hafner.util.PathUtil + exists + (Ljava/lang/String;)Z + + + true + KILLED + 42 + TRUE_RETURNS + edu.hm.hafner.util.PathUtil + exists + (Ljava/lang/String;)Z + + + true + KILLED + 45 + TRUE_RETURNS + edu.hm.hafner.util.PathUtil + exists + (Ljava/lang/String;)Z + + + true + KILLED + 67 + FALSE_RETURNS + edu.hm.hafner.util.PathUtil + exists + (Ljava/lang/String;Ljava/lang/String;)Z + + + true + KILLED + 67 + TRUE_RETURNS + edu.hm.hafner.util.PathUtil + exists + (Ljava/lang/String;Ljava/lang/String;)Z + + + true + KILLED + 83 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + getAbsolutePath + (Ljava/lang/String;)Ljava/lang/String; + + + true + KILLED + 86 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + getAbsolutePath + (Ljava/lang/String;)Ljava/lang/String; + + + true + KILLED + 103 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + getAbsolutePath + (Ljava/nio/file/Path;)Ljava/lang/String; + + + true + KILLED + 106 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + getAbsolutePath + (Ljava/nio/file/Path;)Ljava/lang/String; + + + true + KILLED + 213 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + getRelativePath + (Ljava/lang/String;)Ljava/lang/String; + + + true + KILLED + 218 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + getRelativePath + (Ljava/lang/String;)Ljava/lang/String; + + + true + KILLED + 149 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + getRelativePath + (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + + + true + KILLED + 152 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + getRelativePath + (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + + + true + KILLED + 197 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + getRelativePath + (Ljava/nio/file/Path;)Ljava/lang/String; + + + true + KILLED + 126 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + getRelativePath + (Ljava/nio/file/Path;Ljava/lang/String;)Ljava/lang/String; + + + true + KILLED + 129 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + getRelativePath + (Ljava/nio/file/Path;Ljava/lang/String;)Ljava/lang/String; + + + true + KILLED + 173 + NEGATE_CONDITIONALS + edu.hm.hafner.util.PathUtil + getRelativePath + (Ljava/nio/file/Path;Ljava/nio/file/Path;)Ljava/lang/String; + + + true + KILLED + 174 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + getRelativePath + (Ljava/nio/file/Path;Ljava/nio/file/Path;)Ljava/lang/String; + + + true + KILLED + 176 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + getRelativePath + (Ljava/nio/file/Path;Ljava/nio/file/Path;)Ljava/lang/String; + + + true + KILLED + 182 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + getRelativePath + (Ljava/nio/file/Path;Ljava/nio/file/Path;)Ljava/lang/String; + + + true + KILLED + 263 + CONDITIONALS_BOUNDARY + edu.hm.hafner.util.PathUtil + isAbsolute + (Ljava/lang/String;)Z + + + true + KILLED + 263 + NEGATE_CONDITIONALS + edu.hm.hafner.util.PathUtil + isAbsolute + (Ljava/lang/String;)Z + + + true + KILLED + 263 + TRUE_RETURNS + edu.hm.hafner.util.PathUtil + isAbsolute + (Ljava/lang/String;)Z + + + true + KILLED + 272 + NEGATE_CONDITIONALS + edu.hm.hafner.util.PathUtil + makeUnixPath + (Ljava/lang/String;)Ljava/lang/String; + + + true + KILLED + 275 + EMPTY_RETURNS + edu.hm.hafner.util.PathUtil + makeUnixPath + (Ljava/lang/String;)Ljava/lang/String; + + + true + KILLED + 267 + NULL_RETURNS + edu.hm.hafner.util.PathUtil + normalize + (Ljava/nio/file/Path;)Ljava/nio/file/Path; + + + + + FILE + SecureXmlParserFactory.java + + + CLASS + SecureXmlParserFactory + + + METHOD + clearAttributes + + + MUTATION: 0/1 + + + (Ljavax/xml/parsers/DocumentBuilderFactory;)V + 0 + + + METHOD + clearAttributes + + + MUTATION: 0/1 + + + (Ljavax/xml/transform/TransformerFactory;)V + 0 + + + METHOD + configureSaxParserFactory + + + MUTATION: 0/4 + + + (Ljavax/xml/parsers/SAXParserFactory;)V + 0 + + + METHOD + createDocumentBuilder + + + MUTATION: 1/6 + + + ()Ljavax/xml/parsers/DocumentBuilder; + 0 + + + METHOD + createDocumentBuilderFactory + + + MUTATION: 1/1 + + + ()Ljavax/xml/parsers/DocumentBuilderFactory; + 0 + + + METHOD + createInputSource + + + MUTATION: 1/1 + + + (Ljava/io/Reader;Ljava/nio/charset/Charset;)Lorg/xml/sax/InputSource; + 0 + + + METHOD + createSaxParser + + + MUTATION: 1/3 + + + ()Ljavax/xml/parsers/SAXParser; + 0 + + + METHOD + createSaxParserFactory + + + MUTATION: 1/1 + + + ()Ljavax/xml/parsers/SAXParserFactory; + 0 + + + METHOD + createSecureInputFactory + + + MUTATION: 1/3 + + + ()Ljavax/xml/stream/XMLInputFactory; + 0 + + + METHOD + createTransformer + + + MUTATION: 1/2 + + + ()Ljavax/xml/transform/Transformer; + 0 + + + METHOD + createTransformerFactory + + + MUTATION: 1/1 + + + ()Ljavax/xml/transform/TransformerFactory; + 0 + + + METHOD + createXmlEventReader + + + MUTATION: 1/1 + + + (Ljava/io/Reader;)Ljavax/xml/stream/XMLEventReader; + 0 + + + METHOD + createXmlInputFactory + + + MUTATION: 1/1 + + + ()Ljavax/xml/stream/XMLInputFactory; + 0 + + + METHOD + createXmlStreamReader + + + MUTATION: 1/1 + + + (Ljava/io/Reader;)Ljavax/xml/stream/XMLStreamReader; + 0 + + + METHOD + parse + + + MUTATION: 1/1 + + + (Ljava/io/Reader;Ljava/nio/charset/Charset;Lorg/xml/sax/helpers/DefaultHandler;)V + 0 + + + METHOD + readDocument + + + MUTATION: 1/1 + + + (Ljava/io/Reader;Ljava/nio/charset/Charset;)Lorg/w3c/dom/Document; + 0 + + + METHOD + secureParser + + + MUTATION: 0/1 + + + (Ljavax/xml/parsers/SAXParser;)V + 0 + + + METHOD + setFeature + + + MUTATION: 0/1 + + + (Ljavax/xml/parsers/DocumentBuilderFactory;Ljava/lang/String;Z)V + 0 + + + METHOD + setFeatures + + + MUTATION: 0/2 + + + (Ljavax/xml/parsers/DocumentBuilderFactory;)V + 0 + + + + + + + CLASS + SecureXmlParserFactory$ParsingException + + + METHOD + createMessage + + + MUTATION: 0/1 + + + (Ljava/lang/Throwable;Ljava/lang/String;)Ljava/lang/String; + 0 + + + + + + + + LINE: 34/34 + + + [] + [] + [] + [] + [FILE: 0, CLASS: -1, METHOD: -23/25, LINE: 136/3094, LOC: -57] + + + false + SURVIVED + 127 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + clearAttributes + (Ljavax/xml/parsers/DocumentBuilderFactory;)V + + + false + SURVIVED + 138 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + clearAttributes + (Ljavax/xml/transform/TransformerFactory;)V + + + false + SURVIVED + 194 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + configureSaxParserFactory + (Ljavax/xml/parsers/SAXParserFactory;)V + + + false + SURVIVED + 195 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + configureSaxParserFactory + (Ljavax/xml/parsers/SAXParserFactory;)V + + + false + SURVIVED + 199 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + configureSaxParserFactory + (Ljavax/xml/parsers/SAXParserFactory;)V + + + false + SURVIVED + 207 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + configureSaxParserFactory + (Ljavax/xml/parsers/SAXParserFactory;)V + + + false + SURVIVED + 88 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + createDocumentBuilder + ()Ljavax/xml/parsers/DocumentBuilder; + + + false + SURVIVED + 89 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + createDocumentBuilder + ()Ljavax/xml/parsers/DocumentBuilder; + + + false + SURVIVED + 90 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + createDocumentBuilder + ()Ljavax/xml/parsers/DocumentBuilder; + + + false + SURVIVED + 91 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + createDocumentBuilder + ()Ljavax/xml/parsers/DocumentBuilder; + + + false + SURVIVED + 92 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + createDocumentBuilder + ()Ljavax/xml/parsers/DocumentBuilder; + + + true + KILLED + 94 + NULL_RETURNS + edu.hm.hafner.util.SecureXmlParserFactory + createDocumentBuilder + ()Ljavax/xml/parsers/DocumentBuilder; + + + true + KILLED + 103 + NULL_RETURNS + edu.hm.hafner.util.SecureXmlParserFactory + createDocumentBuilderFactory + ()Ljavax/xml/parsers/DocumentBuilderFactory; + + + true + KILLED + 308 + NULL_RETURNS + edu.hm.hafner.util.SecureXmlParserFactory + createInputSource + (Ljava/io/Reader;Ljava/nio/charset/Charset;)Lorg/xml/sax/InputSource; + + + false + SURVIVED + 154 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + createSaxParser + ()Ljavax/xml/parsers/SAXParser; + + + false + SURVIVED + 157 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + createSaxParser + ()Ljavax/xml/parsers/SAXParser; + + + true + KILLED + 158 + NULL_RETURNS + edu.hm.hafner.util.SecureXmlParserFactory + createSaxParser + ()Ljavax/xml/parsers/SAXParser; + + + true + KILLED + 167 + NULL_RETURNS + edu.hm.hafner.util.SecureXmlParserFactory + createSaxParserFactory + ()Ljavax/xml/parsers/SAXParserFactory; + + + false + SURVIVED + 251 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + createSecureInputFactory + ()Ljavax/xml/stream/XMLInputFactory; + + + false + SURVIVED + 252 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + createSecureInputFactory + ()Ljavax/xml/stream/XMLInputFactory; + + + true + KILLED + 253 + NULL_RETURNS + edu.hm.hafner.util.SecureXmlParserFactory + createSecureInputFactory + ()Ljavax/xml/stream/XMLInputFactory; + + + false + SURVIVED + 321 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + createTransformer + ()Ljavax/xml/transform/Transformer; + + + true + KILLED + 323 + NULL_RETURNS + edu.hm.hafner.util.SecureXmlParserFactory + createTransformer + ()Ljavax/xml/transform/Transformer; + + + true + KILLED + 332 + NULL_RETURNS + edu.hm.hafner.util.SecureXmlParserFactory + createTransformerFactory + ()Ljavax/xml/transform/TransformerFactory; + + + true + KILLED + 242 + NULL_RETURNS + edu.hm.hafner.util.SecureXmlParserFactory + createXmlEventReader + (Ljava/io/Reader;)Ljavax/xml/stream/XMLEventReader; + + + true + KILLED + 258 + NULL_RETURNS + edu.hm.hafner.util.SecureXmlParserFactory + createXmlInputFactory + ()Ljavax/xml/stream/XMLInputFactory; + + + true + KILLED + 225 + NULL_RETURNS + edu.hm.hafner.util.SecureXmlParserFactory + createXmlStreamReader + (Ljava/io/Reader;)Ljavax/xml/stream/XMLStreamReader; + + + true + KILLED + 278 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + parse + (Ljava/io/Reader;Ljava/nio/charset/Charset;Lorg/xml/sax/helpers/DefaultHandler;)V + + + true + KILLED + 300 + NULL_RETURNS + edu.hm.hafner.util.SecureXmlParserFactory + readDocument + (Ljava/io/Reader;Ljava/nio/charset/Charset;)Lorg/w3c/dom/Document; + + + false + SURVIVED + 179 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + secureParser + (Ljavax/xml/parsers/SAXParser;)V + + + false + SURVIVED + 117 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + setFeature + (Ljavax/xml/parsers/DocumentBuilderFactory;Ljava/lang/String;Z)V + + + false + SURVIVED + 108 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + setFeatures + (Ljavax/xml/parsers/DocumentBuilderFactory;)V + + + false + SURVIVED + 111 + VOID_METHOD_CALLS + edu.hm.hafner.util.SecureXmlParserFactory + setFeatures + (Ljavax/xml/parsers/DocumentBuilderFactory;)V + + + false + SURVIVED + 390 + EMPTY_RETURNS + edu.hm.hafner.util.SecureXmlParserFactory$ParsingException + createMessage + (Ljava/lang/Throwable;Ljava/lang/String;)Ljava/lang/String; + + + + + FILE + TreeStringBuilder.java + + + CLASS + TreeStringBuilder$Child + + + METHOD + commonPrefix + + + MUTATION: 5/5 + + + (Ljava/lang/String;Ljava/lang/String;)I + 0 + + + METHOD + dedup + + + MUTATION: 0/2 + + + (Ljava/util/Map;)V + 0 + + + METHOD + getNode + + + MUTATION: 1/1 + + + ()Ledu/hm/hafner/util/TreeString; + 0 + + + METHOD + intern + + + MUTATION: 11/11 + + + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeStringBuilder$Child; + 0 + + + METHOD + makeWritable + + + MUTATION: 1/1 + + + ()V + 0 + + + METHOD + split + + + MUTATION: 2/2 + + + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeStringBuilder$Child; + 0 + + + + + + + CLASS + TreeStringBuilder + + + METHOD + dedup + + + MUTATION: 0/1 + + + ()V + 0 + + + METHOD + getRoot + + + MUTATION: 1/1 + + + ()Ledu/hm/hafner/util/TreeStringBuilder$Child; + 0 + + + METHOD + intern + + + MUTATION: 1/1 + + + (Ledu/hm/hafner/util/TreeString;)Ledu/hm/hafner/util/TreeString; + 0 + + + METHOD + intern + + + MUTATION: 1/1 + + + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeString; + 0 + + + + + + + + LINE: 23/23 + + + [] + [] + [] + [] + [FILE: 0, CLASS: -1, METHOD: -13/14, LINE: 46/1219, LOC: -30] + + + true + KILLED + 164 + CONDITIONALS_BOUNDARY + edu.hm.hafner.util.TreeStringBuilder$Child + commonPrefix + (Ljava/lang/String;Ljava/lang/String;)I + + + true + KILLED + 164 + NEGATE_CONDITIONALS + edu.hm.hafner.util.TreeStringBuilder$Child + commonPrefix + (Ljava/lang/String;Ljava/lang/String;)I + + + true + KILLED + 165 + NEGATE_CONDITIONALS + edu.hm.hafner.util.TreeStringBuilder$Child + commonPrefix + (Ljava/lang/String;Ljava/lang/String;)I + + + true + KILLED + 166 + PRIMITIVE_RETURNS + edu.hm.hafner.util.TreeStringBuilder$Child + commonPrefix + (Ljava/lang/String;Ljava/lang/String;)I + + + true + KILLED + 169 + PRIMITIVE_RETURNS + edu.hm.hafner.util.TreeStringBuilder$Child + commonPrefix + (Ljava/lang/String;Ljava/lang/String;)I + + + false + SURVIVED + 179 + VOID_METHOD_CALLS + edu.hm.hafner.util.TreeStringBuilder$Child + dedup + (Ljava/util/Map;)V + + + false + SURVIVED + 181 + VOID_METHOD_CALLS + edu.hm.hafner.util.TreeStringBuilder$Child + dedup + (Ljava/util/Map;)V + + + true + KILLED + 186 + NULL_RETURNS + edu.hm.hafner.util.TreeStringBuilder$Child + getNode + ()Ledu/hm/hafner/util/TreeString; + + + true + KILLED + 94 + CONDITIONALS_BOUNDARY + edu.hm.hafner.util.TreeStringBuilder$Child + intern + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeStringBuilder$Child; + + + true + KILLED + 95 + CONDITIONALS_BOUNDARY + edu.hm.hafner.util.TreeStringBuilder$Child + intern + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeStringBuilder$Child; + + + true + KILLED + 87 + NEGATE_CONDITIONALS + edu.hm.hafner.util.TreeStringBuilder$Child + intern + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeStringBuilder$Child; + + + true + KILLED + 94 + NEGATE_CONDITIONALS + edu.hm.hafner.util.TreeStringBuilder$Child + intern + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeStringBuilder$Child; + + + true + KILLED + 95 + NEGATE_CONDITIONALS + edu.hm.hafner.util.TreeStringBuilder$Child + intern + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeStringBuilder$Child; + + + true + KILLED + 115 + NEGATE_CONDITIONALS + edu.hm.hafner.util.TreeStringBuilder$Child + intern + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeStringBuilder$Child; + + + true + KILLED + 91 + VOID_METHOD_CALLS + edu.hm.hafner.util.TreeStringBuilder$Child + intern + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeStringBuilder$Child; + + + true + KILLED + 88 + NULL_RETURNS + edu.hm.hafner.util.TreeStringBuilder$Child + intern + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeStringBuilder$Child; + + + true + KILLED + 105 + NULL_RETURNS + edu.hm.hafner.util.TreeStringBuilder$Child + intern + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeStringBuilder$Child; + + + true + KILLED + 108 + NULL_RETURNS + edu.hm.hafner.util.TreeStringBuilder$Child + intern + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeStringBuilder$Child; + + + true + KILLED + 119 + NULL_RETURNS + edu.hm.hafner.util.TreeStringBuilder$Child + intern + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeStringBuilder$Child; + + + true + KILLED + 127 + NEGATE_CONDITIONALS + edu.hm.hafner.util.TreeStringBuilder$Child + makeWritable + ()V + + + true + KILLED + 145 + VOID_METHOD_CALLS + edu.hm.hafner.util.TreeStringBuilder$Child + split + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeStringBuilder$Child; + + + true + KILLED + 148 + NULL_RETURNS + edu.hm.hafner.util.TreeStringBuilder$Child + split + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeStringBuilder$Child; + + + false + SURVIVED + 52 + VOID_METHOD_CALLS + edu.hm.hafner.util.TreeStringBuilder + dedup + ()V + + + true + KILLED + 59 + NULL_RETURNS + edu.hm.hafner.util.TreeStringBuilder + getRoot + ()Ledu/hm/hafner/util/TreeStringBuilder$Child; + + + true + KILLED + 45 + NULL_RETURNS + edu.hm.hafner.util.TreeStringBuilder + intern + (Ledu/hm/hafner/util/TreeString;)Ledu/hm/hafner/util/TreeString; + + + true + KILLED + 33 + NULL_RETURNS + edu.hm.hafner.util.TreeStringBuilder + intern + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeString; + + + + + FILE + LookaheadStream.java + + + CLASS + LookaheadStream + + + METHOD + close + + + MUTATION: 1/1 + + + ()V + 0 + + + METHOD + getFileName + + + MUTATION: 0/1 + + + ()Ljava/lang/String; + 0 + + + METHOD + getLine + + + MUTATION: 1/1 + + + ()I + 0 + + + METHOD + hasNext + + + MUTATION: 3/3 + + + ()Z + 0 + + + METHOD + hasNext + + + MUTATION: 6/6 + + + (Ljava/lang/String;)Z + 0 + + + METHOD + next + + + MUTATION: 4/4 + + + ()Ljava/lang/String; + 0 + + + METHOD + peekNext + + + MUTATION: 3/3 + + + ()Ljava/lang/String; + 0 + + + + + + + + LINE: 16/16 + + + [] + [] + [] + [] + [FILE: 0, CLASS: -1, METHOD: -1, LINE: 0, LOC: -15] + + + true + KILLED + 55 + VOID_METHOD_CALLS + edu.hm.hafner.util.LookaheadStream + close + ()V + + + false + SURVIVED + 50 + EMPTY_RETURNS + edu.hm.hafner.util.LookaheadStream + getFileName + ()Ljava/lang/String; + + + true + KILLED + 130 + PRIMITIVE_RETURNS + edu.hm.hafner.util.LookaheadStream + getLine + ()I + + + true + KILLED + 65 + NEGATE_CONDITIONALS + edu.hm.hafner.util.LookaheadStream + hasNext + ()Z + + + true + KILLED + 65 + NEGATE_CONDITIONALS + edu.hm.hafner.util.LookaheadStream + hasNext + ()Z + + + true + KILLED + 65 + TRUE_RETURNS + edu.hm.hafner.util.LookaheadStream + hasNext + ()Z + + + true + KILLED + 77 + NEGATE_CONDITIONALS + edu.hm.hafner.util.LookaheadStream + hasNext + (Ljava/lang/String;)Z + + + true + KILLED + 78 + NEGATE_CONDITIONALS + edu.hm.hafner.util.LookaheadStream + hasNext + (Ljava/lang/String;)Z + + + true + KILLED + 81 + VOID_METHOD_CALLS + edu.hm.hafner.util.LookaheadStream + hasNext + (Ljava/lang/String;)Z + + + true + KILLED + 84 + FALSE_RETURNS + edu.hm.hafner.util.LookaheadStream + hasNext + (Ljava/lang/String;)Z + + + true + KILLED + 79 + TRUE_RETURNS + edu.hm.hafner.util.LookaheadStream + hasNext + (Ljava/lang/String;)Z + + + true + KILLED + 84 + TRUE_RETURNS + edu.hm.hafner.util.LookaheadStream + hasNext + (Ljava/lang/String;)Z + + + true + KILLED + 115 + MATH + edu.hm.hafner.util.LookaheadStream + next + ()Ljava/lang/String; + + + true + KILLED + 117 + NEGATE_CONDITIONALS + edu.hm.hafner.util.LookaheadStream + next + ()Ljava/lang/String; + + + true + KILLED + 119 + EMPTY_RETURNS + edu.hm.hafner.util.LookaheadStream + next + ()Ljava/lang/String; + + + true + KILLED + 121 + EMPTY_RETURNS + edu.hm.hafner.util.LookaheadStream + next + ()Ljava/lang/String; + + + true + KILLED + 96 + NEGATE_CONDITIONALS + edu.hm.hafner.util.LookaheadStream + peekNext + ()Ljava/lang/String; + + + true + KILLED + 97 + VOID_METHOD_CALLS + edu.hm.hafner.util.LookaheadStream + peekNext + ()Ljava/lang/String; + + + true + KILLED + 99 + EMPTY_RETURNS + edu.hm.hafner.util.LookaheadStream + peekNext + ()Ljava/lang/String; + + + + + FILE + FilteredLog.java + + + CLASS + FilteredLog + + + METHOD + getErrorMessages + + + MUTATION: 1/1 + + + ()Ljava/util/List; + 0 + + + METHOD + getInfoMessages + + + MUTATION: 1/1 + + + ()Ljava/util/List; + 0 + + + METHOD + hasErrors + + + MUTATION: 2/2 + + + ()Z + 0 + + + METHOD + logError + + + MUTATION: 4/4 + + + (Ljava/lang/String;[Ljava/lang/Object;)V + 0 + + + METHOD + logException + + + MUTATION: 2/4 + + + (Ljava/lang/Exception;Ljava/lang/String;[Ljava/lang/Object;)V + 0 + + + METHOD + logSummary + + + MUTATION: 3/3 + + + ()V + 0 + + + METHOD + printTitle + + + MUTATION: 1/1 + + + ()V + 0 + + + METHOD + size + + + MUTATION: 1/1 + + + ()I + 0 + + + + + + + + LINE: 13/13 + + + [] + [] + [] + [] + [FILE: 0, CLASS: -1, METHOD: -1, LINE: 0, LOC: -24] + + + true + KILLED + 165 + EMPTY_RETURNS + edu.hm.hafner.util.FilteredLog + getErrorMessages + ()Ljava/util/List; + + + true + KILLED + 156 + EMPTY_RETURNS + edu.hm.hafner.util.FilteredLog + getInfoMessages + ()Ljava/util/List; + + + true + KILLED + 174 + NEGATE_CONDITIONALS + edu.hm.hafner.util.FilteredLog + hasErrors + ()Z + + + true + KILLED + 174 + TRUE_RETURNS + edu.hm.hafner.util.FilteredLog + hasErrors + ()Z + + + true + KILLED + 96 + CONDITIONALS_BOUNDARY + edu.hm.hafner.util.FilteredLog + logError + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 99 + MATH + edu.hm.hafner.util.FilteredLog + logError + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 96 + NEGATE_CONDITIONALS + edu.hm.hafner.util.FilteredLog + logError + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 94 + VOID_METHOD_CALLS + edu.hm.hafner.util.FilteredLog + logError + (Ljava/lang/String;[Ljava/lang/Object;)V + + + false + SURVIVED + 124 + CONDITIONALS_BOUNDARY + edu.hm.hafner.util.FilteredLog + logException + (Ljava/lang/Exception;Ljava/lang/String;[Ljava/lang/Object;)V + + + false + SURVIVED + 128 + MATH + edu.hm.hafner.util.FilteredLog + logException + (Ljava/lang/Exception;Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 124 + NEGATE_CONDITIONALS + edu.hm.hafner.util.FilteredLog + logException + (Ljava/lang/Exception;Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 122 + VOID_METHOD_CALLS + edu.hm.hafner.util.FilteredLog + logException + (Ljava/lang/Exception;Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 145 + CONDITIONALS_BOUNDARY + edu.hm.hafner.util.FilteredLog + logSummary + ()V + + + true + KILLED + 146 + MATH + edu.hm.hafner.util.FilteredLog + logSummary + ()V + + + true + KILLED + 145 + NEGATE_CONDITIONALS + edu.hm.hafner.util.FilteredLog + logSummary + ()V + + + true + KILLED + 103 + NEGATE_CONDITIONALS + edu.hm.hafner.util.FilteredLog + printTitle + ()V + + + true + KILLED + 137 + PRIMITIVE_RETURNS + edu.hm.hafner.util.FilteredLog + size + ()I + + + + + FILE + Ensure.java + + + CLASS + Ensure$ObjectCondition + + + METHOD + getValue + + + MUTATION: 2/2 + + + ()Ljava/lang/Object; + 0 + + + METHOD + isInstanceOf + + + MUTATION: 2/3 + + + (Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Object;)V + 0 + + + METHOD + isInstanceOf + + + MUTATION: 1/3 + + + (Ljava/lang/Class;[Ljava/lang/Class;)V + 0 + + + METHOD + isNotNull + + + MUTATION: 1/1 + + + ()V + 0 + + + METHOD + isNotNull + + + MUTATION: 4/4 + + + (Ljava/lang/String;[Ljava/lang/Object;)V + 0 + + + METHOD + isNull + + + MUTATION: 1/1 + + + ()V + 0 + + + METHOD + isNull + + + MUTATION: 2/2 + + + (Ljava/lang/String;[Ljava/lang/Object;)V + 0 + + + + + + + CLASS + Ensure$StringCondition + + + METHOD + isBlank + + + MUTATION: 6/7 + + + ()Z + 0 + + + METHOD + isNotBlank + + + MUTATION: 1/1 + + + ()V + 0 + + + METHOD + isNotBlank + + + MUTATION: 2/3 + + + (Ljava/lang/String;[Ljava/lang/Object;)V + 0 + + + METHOD + isNotEmpty + + + MUTATION: 1/1 + + + ()V + 0 + + + METHOD + isNotEmpty + + + MUTATION: 3/3 + + + (Ljava/lang/String;[Ljava/lang/Object;)V + 0 + + + + + + + CLASS + Ensure + + + METHOD + that + + + MUTATION: 0/1 + + + (Ljava/lang/Iterable;)Ledu/hm/hafner/util/Ensure$IterableCondition; + 0 + + + METHOD + that + + + MUTATION: 1/1 + + + (Ljava/lang/Object;[Ljava/lang/Object;)Ledu/hm/hafner/util/Ensure$ObjectCondition; + 0 + + + METHOD + that + + + MUTATION: 1/1 + + + (Ljava/lang/String;)Ledu/hm/hafner/util/Ensure$StringCondition; + 0 + + + METHOD + that + + + MUTATION: 1/1 + + + (Ljava/lang/Throwable;)Ledu/hm/hafner/util/Ensure$ExceptionCondition; + 0 + + + METHOD + that + + + MUTATION: 1/1 + + + (Ljava/util/Collection;)Ledu/hm/hafner/util/Ensure$CollectionCondition; + 0 + + + METHOD + that + + + MUTATION: 1/1 + + + (Z)Ledu/hm/hafner/util/Ensure$BooleanCondition; + 0 + + + METHOD + that + + + MUTATION: 1/1 + + + ([Ljava/lang/Object;)Ledu/hm/hafner/util/Ensure$ArrayCondition; + 0 + + + METHOD + thatStatementIsNeverReached + + + MUTATION: 1/1 + + + ()V + 0 + + + METHOD + thatStatementIsNeverReached + + + MUTATION: 1/1 + + + (Ljava/lang/String;[Ljava/lang/Object;)V + 0 + + + + + + + CLASS + Ensure$CollectionCondition + + + METHOD + contains + + + MUTATION: 1/1 + + + (Ljava/lang/Object;)V + 0 + + + METHOD + contains + + + MUTATION: 2/3 + + + (Ljava/lang/Object;Ljava/lang/String;[Ljava/lang/Object;)V + 0 + + + METHOD + doesNotContain + + + MUTATION: 1/1 + + + (Ljava/lang/Object;)V + 0 + + + METHOD + doesNotContain + + + MUTATION: 2/3 + + + (Ljava/lang/Object;Ljava/lang/String;[Ljava/lang/Object;)V + 0 + + + METHOD + getValue + + + MUTATION: 1/1 + + + ()Ljava/util/Collection; + 0 + + + + + + + CLASS + Ensure$ArrayCondition + + + METHOD + isNotEmpty + + + MUTATION: 1/1 + + + ()V + 0 + + + METHOD + isNotEmpty + + + MUTATION: 5/5 + + + (Ljava/lang/String;[Ljava/lang/Object;)V + 0 + + + + + + + CLASS + Ensure$BooleanCondition + + + METHOD + isFalse + + + MUTATION: 1/1 + + + ()V + 0 + + + METHOD + isFalse + + + MUTATION: 2/2 + + + (Ljava/lang/String;[Ljava/lang/Object;)V + 0 + + + METHOD + isTrue + + + MUTATION: 1/1 + + + ()V + 0 + + + METHOD + isTrue + + + MUTATION: 2/2 + + + (Ljava/lang/String;[Ljava/lang/Object;)V + 0 + + + + + + + CLASS + Ensure$IterableCondition + + + METHOD + isNotEmpty + + + MUTATION: 1/1 + + + ()V + 0 + + + METHOD + isNotEmpty + + + MUTATION: 3/5 + + + (Ljava/lang/String;[Ljava/lang/Object;)V + 0 + + + + + + + + LINE: 62/66 + + + [] + [] + [] + [] + [FILE: 0, CLASS: -1, METHOD: -42/45, LINE: 1150/8250, LOC: -59] + + + true + KILLED + 571 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$ObjectCondition + getValue + ()Ljava/lang/Object; + + + true + KILLED + 574 + NULL_RETURNS + edu.hm.hafner.util.Ensure$ObjectCondition + getValue + ()Ljava/lang/Object; + + + true + KILLED + 653 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$ObjectCondition + isInstanceOf + (Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Object;)V + + + false + SURVIVED + 651 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$ObjectCondition + isInstanceOf + (Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 654 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$ObjectCondition + isInstanceOf + (Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 627 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$ObjectCondition + isInstanceOf + (Ljava/lang/Class;[Ljava/lang/Class;)V + + + false + SURVIVED + 620 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$ObjectCondition + isInstanceOf + (Ljava/lang/Class;[Ljava/lang/Class;)V + + + false + NO_COVERAGE + 631 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$ObjectCondition + isInstanceOf + (Ljava/lang/Class;[Ljava/lang/Class;)V + + + true + KILLED + 540 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$ObjectCondition + isNotNull + ()V + + + true + KILLED + 558 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$ObjectCondition + isNotNull + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 562 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$ObjectCondition + isNotNull + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 559 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$ObjectCondition + isNotNull + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 563 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$ObjectCondition + isNotNull + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 584 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$ObjectCondition + isNull + ()V + + + true + KILLED + 603 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$ObjectCondition + isNull + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 604 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$ObjectCondition + isNull + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 488 + CONDITIONALS_BOUNDARY + edu.hm.hafner.util.Ensure$StringCondition + isBlank + ()Z + + + true + KILLED + 485 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$StringCondition + isBlank + ()Z + + + true + KILLED + 488 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$StringCondition + isBlank + ()Z + + + true + KILLED + 489 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$StringCondition + isBlank + ()Z + + + false + NO_COVERAGE + 486 + FALSE_RETURNS + edu.hm.hafner.util.Ensure$StringCondition + isBlank + ()Z + + + true + KILLED + 493 + FALSE_RETURNS + edu.hm.hafner.util.Ensure$StringCondition + isBlank + ()Z + + + true + KILLED + 490 + TRUE_RETURNS + edu.hm.hafner.util.Ensure$StringCondition + isBlank + ()Z + + + true + KILLED + 458 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$StringCondition + isNotBlank + ()V + + + true + KILLED + 478 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$StringCondition + isNotBlank + (Ljava/lang/String;[Ljava/lang/Object;)V + + + false + SURVIVED + 476 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$StringCondition + isNotBlank + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 479 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$StringCondition + isNotBlank + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 426 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$StringCondition + isNotEmpty + ()V + + + true + KILLED + 446 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$StringCondition + isNotEmpty + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 444 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$StringCondition + isNotEmpty + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 447 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$StringCondition + isNotEmpty + (Ljava/lang/String;[Ljava/lang/Object;)V + + + false + NO_COVERAGE + 81 + NULL_RETURNS + edu.hm.hafner.util.Ensure + that + (Ljava/lang/Iterable;)Ledu/hm/hafner/util/Ensure$IterableCondition; + + + true + KILLED + 68 + NULL_RETURNS + edu.hm.hafner.util.Ensure + that + (Ljava/lang/Object;[Ljava/lang/Object;)Ledu/hm/hafner/util/Ensure$ObjectCondition; + + + true + KILLED + 121 + NULL_RETURNS + edu.hm.hafner.util.Ensure + that + (Ljava/lang/String;)Ledu/hm/hafner/util/Ensure$StringCondition; + + + true + KILLED + 134 + NULL_RETURNS + edu.hm.hafner.util.Ensure + that + (Ljava/lang/Throwable;)Ledu/hm/hafner/util/Ensure$ExceptionCondition; + + + true + KILLED + 94 + NULL_RETURNS + edu.hm.hafner.util.Ensure + that + (Ljava/util/Collection;)Ledu/hm/hafner/util/Ensure$CollectionCondition; + + + true + KILLED + 50 + NULL_RETURNS + edu.hm.hafner.util.Ensure + that + (Z)Ledu/hm/hafner/util/Ensure$BooleanCondition; + + + true + KILLED + 108 + NULL_RETURNS + edu.hm.hafner.util.Ensure + that + ([Ljava/lang/Object;)Ledu/hm/hafner/util/Ensure$ArrayCondition; + + + true + KILLED + 141 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure + thatStatementIsNeverReached + ()V + + + true + KILLED + 156 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure + thatStatementIsNeverReached + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 283 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$CollectionCondition + contains + (Ljava/lang/Object;)V + + + true + KILLED + 305 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$CollectionCondition + contains + (Ljava/lang/Object;Ljava/lang/String;[Ljava/lang/Object;)V + + + false + SURVIVED + 303 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$CollectionCondition + contains + (Ljava/lang/Object;Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 306 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$CollectionCondition + contains + (Ljava/lang/Object;Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 320 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$CollectionCondition + doesNotContain + (Ljava/lang/Object;)V + + + true + KILLED + 342 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$CollectionCondition + doesNotContain + (Ljava/lang/Object;Ljava/lang/String;[Ljava/lang/Object;)V + + + false + SURVIVED + 340 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$CollectionCondition + doesNotContain + (Ljava/lang/Object;Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 343 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$CollectionCondition + doesNotContain + (Ljava/lang/Object;Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 270 + EMPTY_RETURNS + edu.hm.hafner.util.Ensure$CollectionCondition + getValue + ()Ljava/util/Collection; + + + true + KILLED + 371 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$ArrayCondition + isNotEmpty + ()V + + + true + KILLED + 392 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$ArrayCondition + isNotEmpty + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 397 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$ArrayCondition + isNotEmpty + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 390 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$ArrayCondition + isNotEmpty + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 393 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$ArrayCondition + isNotEmpty + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 398 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$ArrayCondition + isNotEmpty + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 705 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$BooleanCondition + isFalse + ()V + + + true + KILLED + 693 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$BooleanCondition + isFalse + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 694 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$BooleanCondition + isFalse + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 735 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$BooleanCondition + isTrue + ()V + + + true + KILLED + 723 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$BooleanCondition + isTrue + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 724 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$BooleanCondition + isTrue + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 219 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$IterableCondition + isNotEmpty + ()V + + + true + KILLED + 240 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$IterableCondition + isNotEmpty + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 242 + NEGATE_CONDITIONALS + edu.hm.hafner.util.Ensure$IterableCondition + isNotEmpty + (Ljava/lang/String;[Ljava/lang/Object;)V + + + false + SURVIVED + 238 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$IterableCondition + isNotEmpty + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 243 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$IterableCondition + isNotEmpty + (Ljava/lang/String;[Ljava/lang/Object;)V + + + false + NO_COVERAGE + 248 + VOID_METHOD_CALLS + edu.hm.hafner.util.Ensure$IterableCondition + isNotEmpty + (Ljava/lang/String;[Ljava/lang/Object;)V + + + + + FILE + TreeString.java + + + CLASS + TreeString + + + METHOD + <init> + + + MUTATION: 3/3 + + + (Ledu/hm/hafner/util/TreeString;Ljava/lang/String;)V + 0 + + + METHOD + dedup + + + MUTATION: 1/1 + + + (Ljava/util/Map;)V + 0 + + + METHOD + depth + + + MUTATION: 3/3 + + + ()I + 0 + + + METHOD + getLabel + + + MUTATION: 1/1 + + + ()Ljava/lang/String; + 0 + + + METHOD + getParent + + + MUTATION: 1/1 + + + ()Ledu/hm/hafner/util/TreeString; + 0 + + + METHOD + isBlank + + + MUTATION: 1/2 + + + ()Z + 0 + + + METHOD + split + + + MUTATION: 3/4 + + + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeString; + 0 + + + METHOD + valueOf + + + MUTATION: 1/1 + + + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeString; + 0 + + + + + + + + LINE: 14/14 + + + [] + [] + [] + [] + [FILE: 0, CLASS: -1, METHOD: -1, LINE: 0, LOC: -31] + + + true + KILLED + 47 + NEGATE_CONDITIONALS + edu.hm.hafner.util.TreeString + <init> + (Ledu/hm/hafner/util/TreeString;Ljava/lang/String;)V + + + true + KILLED + 47 + NEGATE_CONDITIONALS + edu.hm.hafner.util.TreeString + <init> + (Ledu/hm/hafner/util/TreeString;Ljava/lang/String;)V + + + true + KILLED + 48 + VOID_METHOD_CALLS + edu.hm.hafner.util.TreeString + <init> + (Ledu/hm/hafner/util/TreeString;Ljava/lang/String;)V + + + true + KILLED + 149 + NEGATE_CONDITIONALS + edu.hm.hafner.util.TreeString + dedup + (Ljava/util/Map;)V + + + true + KILLED + 98 + INCREMENTS + edu.hm.hafner.util.TreeString + depth + ()I + + + true + KILLED + 97 + NEGATE_CONDITIONALS + edu.hm.hafner.util.TreeString + depth + ()I + + + true + KILLED + 100 + PRIMITIVE_RETURNS + edu.hm.hafner.util.TreeString + depth + ()I + + + true + KILLED + 55 + EMPTY_RETURNS + edu.hm.hafner.util.TreeString + getLabel + ()Ljava/lang/String; + + + true + KILLED + 86 + NULL_RETURNS + edu.hm.hafner.util.TreeString + getParent + ()Ledu/hm/hafner/util/TreeString; + + + true + KILLED + 158 + FALSE_RETURNS + edu.hm.hafner.util.TreeString + isBlank + ()Z + + + false + SURVIVED + 158 + TRUE_RETURNS + edu.hm.hafner.util.TreeString + isBlank + ()Z + + + true + KILLED + 73 + MATH + edu.hm.hafner.util.TreeString + split + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeString; + + + false + SURVIVED + 71 + VOID_METHOD_CALLS + edu.hm.hafner.util.TreeString + split + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeString; + + + true + KILLED + 74 + VOID_METHOD_CALLS + edu.hm.hafner.util.TreeString + split + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeString; + + + true + KILLED + 80 + NULL_RETURNS + edu.hm.hafner.util.TreeString + split + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeString; + + + true + KILLED + 171 + NULL_RETURNS + edu.hm.hafner.util.TreeString + valueOf + (Ljava/lang/String;)Ledu/hm/hafner/util/TreeString; + + + + + FILE + ResourceExtractor.java + + + CLASS + ResourceExtractor + + + METHOD + <init> + + + MUTATION: 4/4 + + + (Ljava/lang/Class;Ljava/security/ProtectionDomain;)V + 0 + + + METHOD + extract + + + MUTATION: 3/3 + + + (Ljava/nio/file/Path;Ljava/lang/String;[Ljava/lang/String;)V + 0 + + + METHOD + getResourcePath + + + MUTATION: 1/1 + + + ()Ljava/lang/String; + 0 + + + METHOD + isReadingFromJarFile + + + MUTATION: 2/2 + + + ()Z + 0 + + + + + + + CLASS + ResourceExtractor$JarExtractor + + + METHOD + copy + + + MUTATION: 2/2 + + + (Ljava/nio/file/Path;Ljava/util/jar/JarFile;Ljava/util/jar/JarEntry;Ljava/lang/String;)V + 0 + + + METHOD + extractFiles + + + MUTATION: 4/4 + + + (Ljava/nio/file/Path;[Ljava/lang/String;)V + 0 + + + + + + + CLASS + ResourceExtractor$FolderExtractor + + + METHOD + extractFiles + + + MUTATION: 1/1 + + + (Ljava/nio/file/Path;[Ljava/lang/String;)V + 0 + + + + + + + CLASS + ResourceExtractor$Extractor + + + METHOD + getEntryPoint + + + MUTATION: 1/1 + + + ()Ljava/nio/file/Path; + 0 + + + + + + + + LINE: 17/17 + + + [] + [] + [] + [] + [FILE: 0, CLASS: -1, METHOD: -1, LINE: 85/1241, LOC: -56] + + + true + KILLED + 48 + NEGATE_CONDITIONALS + edu.hm.hafner.util.ResourceExtractor + <init> + (Ljava/lang/Class;Ljava/security/ProtectionDomain;)V + + + true + KILLED + 52 + NEGATE_CONDITIONALS + edu.hm.hafner.util.ResourceExtractor + <init> + (Ljava/lang/Class;Ljava/security/ProtectionDomain;)V + + + true + KILLED + 56 + NEGATE_CONDITIONALS + edu.hm.hafner.util.ResourceExtractor + <init> + (Ljava/lang/Class;Ljava/security/ProtectionDomain;)V + + + true + KILLED + 61 + NEGATE_CONDITIONALS + edu.hm.hafner.util.ResourceExtractor + <init> + (Ljava/lang/Class;Ljava/security/ProtectionDomain;)V + + + true + KILLED + 93 + MATH + edu.hm.hafner.util.ResourceExtractor + extract + (Ljava/nio/file/Path;Ljava/lang/String;[Ljava/lang/String;)V + + + true + KILLED + 89 + NEGATE_CONDITIONALS + edu.hm.hafner.util.ResourceExtractor + extract + (Ljava/nio/file/Path;Ljava/lang/String;[Ljava/lang/String;)V + + + true + KILLED + 95 + VOID_METHOD_CALLS + edu.hm.hafner.util.ResourceExtractor + extract + (Ljava/nio/file/Path;Ljava/lang/String;[Ljava/lang/String;)V + + + true + KILLED + 71 + EMPTY_RETURNS + edu.hm.hafner.util.ResourceExtractor + getResourcePath + ()Ljava/lang/String; + + + true + KILLED + 75 + FALSE_RETURNS + edu.hm.hafner.util.ResourceExtractor + isReadingFromJarFile + ()Z + + + true + KILLED + 75 + TRUE_RETURNS + edu.hm.hafner.util.ResourceExtractor + isReadingFromJarFile + ()Z + + + true + KILLED + 180 + NEGATE_CONDITIONALS + edu.hm.hafner.util.ResourceExtractor$JarExtractor + copy + (Ljava/nio/file/Path;Ljava/util/jar/JarFile;Ljava/util/jar/JarEntry;Ljava/lang/String;)V + + + true + KILLED + 184 + NEGATE_CONDITIONALS + edu.hm.hafner.util.ResourceExtractor$JarExtractor + copy + (Ljava/nio/file/Path;Ljava/util/jar/JarFile;Ljava/util/jar/JarEntry;Ljava/lang/String;)V + + + true + KILLED + 160 + NEGATE_CONDITIONALS + edu.hm.hafner.util.ResourceExtractor$JarExtractor + extractFiles + (Ljava/nio/file/Path;[Ljava/lang/String;)V + + + true + KILLED + 163 + NEGATE_CONDITIONALS + edu.hm.hafner.util.ResourceExtractor$JarExtractor + extractFiles + (Ljava/nio/file/Path;[Ljava/lang/String;)V + + + true + KILLED + 172 + NEGATE_CONDITIONALS + edu.hm.hafner.util.ResourceExtractor$JarExtractor + extractFiles + (Ljava/nio/file/Path;[Ljava/lang/String;)V + + + true + KILLED + 164 + VOID_METHOD_CALLS + edu.hm.hafner.util.ResourceExtractor$JarExtractor + extractFiles + (Ljava/nio/file/Path;[Ljava/lang/String;)V + + + true + KILLED + 129 + VOID_METHOD_CALLS + edu.hm.hafner.util.ResourceExtractor$FolderExtractor + extractFiles + (Ljava/nio/file/Path;[Ljava/lang/String;)V + + + true + KILLED + 109 + NULL_RETURNS + edu.hm.hafner.util.ResourceExtractor$Extractor + getEntryPoint + ()Ljava/nio/file/Path; + + + + + FILE + PrefixLogger.java + + + CLASS + PrefixLogger + + + METHOD + <init> + + + MUTATION: 1/1 + + + (Ljava/io/PrintStream;Ljava/lang/String;)V + 0 + + + METHOD + log + + + MUTATION: 1/1 + + + (Ljava/lang/String;[Ljava/lang/Object;)V + 0 + + + METHOD + logEachLine + + + MUTATION: 1/1 + + + (Ljava/util/Collection;)V + 0 + + + METHOD + print + + + MUTATION: 1/1 + + + (Ljava/lang/String;)V + 0 + + + + + + + + LINE: 4/4 + + + [] + [] + [] + [] + [FILE: 0, CLASS: -1, METHOD: -1, LINE: 0, LOC: -8] + + + true + KILLED + 26 + NEGATE_CONDITIONALS + edu.hm.hafner.util.PrefixLogger + <init> + (Ljava/io/PrintStream;Ljava/lang/String;)V + + + true + KILLED + 47 + VOID_METHOD_CALLS + edu.hm.hafner.util.PrefixLogger + log + (Ljava/lang/String;[Ljava/lang/Object;)V + + + true + KILLED + 57 + VOID_METHOD_CALLS + edu.hm.hafner.util.PrefixLogger + logEachLine + (Ljava/util/Collection;)V + + + true + KILLED + 61 + VOID_METHOD_CALLS + edu.hm.hafner.util.PrefixLogger + print + (Ljava/lang/String;)V + + + + + + + + + + + \ No newline at end of file