diff --git a/README.md b/README.md index 2728176e9..9ea5ea32f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![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: +The Jenkins Code Coverage Plug-in collects reports of code coverage or mutation coverage tools. It has support for the following report formats: - [JaCoCo](https://www.jacoco.org/jacoco) - [Cobertura](https://cobertura.github.io/cobertura/) @@ -25,46 +25,66 @@ This project was part of [GSoC 2018](https://jenkins.io/projects/gsoc/2018/code- ## Features -The code coverage plug-in provides the following features when added as a post build action (or step) -to a job: +The code coverage plug-in provides the following features when added as a post build action (or step) to a job: -* **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 +* Coverage analysis of projects and pull requests: The plugin now computes and shows the absolute coverage of the project, the coverage of the modified files and the coverage of the modified lines, so you can see how the changes actually affect the code coverage. Additionally, the delta of these coverages with respect to the reference build are computed and the coverage changes created by changed test cases (indirect coverage changes). - ![alt text](./images/reportOverview_screen.PNG "Coverage overview and trend") + ![Coverage overview and trend](./images/summary.png) + +* Coverage overview and trend: + + ![Coverage overview and trend](./images/reportOverview_screen.PNG) - * Colored project coverage tree map for line and branch coverage +* Colored project coverage tree map for line, branch, instruction and mutation coverage: - ![alt text](./images/reportTree_screen.PNG "Colored project coverage tree map") + ![Colored project coverage tree map](./images/reportTree_screen.PNG) - * Source code navigation +* Source code navigation with a configuration option to store the source code files for all builds, for current build only, or for changed files only: - ![alt text](./images/reportFile_screen.PNG "Source code navigation") + ![Source code navigation](./images/reportFile_screen.PNG) - * Specific source code view for specifically analyzing the coverage of code changes (Change Coverage): +* Specific source code view for analyzing the coverage of changed code lines: - ![alt text](./images/reportCC_screen.PNG "Specific source code view for Change Coverage") + ![Specific source code view for Change Coverage](./images/reportCC_screen.PNG) - * Specific source code view for specifically analyzing the coverage after test changes (Indirect Coverage Changes): +* Specific source code view for analyzing the coverage changes that are a result of test changes (indirect coverage changes): - ![alt text](./images/reportICC_screen.PNG "Specific source code view for Indirect Coverage Changes") + ![Specific source code view for Indirect Coverage Changes](./images/reportICC_screen.PNG) + +* Customizable coverage overview for the Jenkins dashboard view and for build results: -* **Customizable coverage overview for the Jenkins dashboard view and for build results:** ![alt text](./images/dashboard_screen.PNG "Analysis overview for Jenkins dashboard") - ![alt text](./images/buildview_screen.PNG "Analysis overview for Jenkins build result") + +* Quality Gates: You can specify an arbitrary number of quality gates that allow to set the build to unstable or failed if the thresholds are not met. For each quality gate the metric (branch coverage, complexity, etc.) and the baseline (whole project, changed files, etc.) can be defined. + + ![Quality Gates](./images/quality-gates.png) + +* Cyclomatic Complexity and LOC metrics: Several coverage parsers support the measurement of cyclomatic complexity and lines of code. These metrics are now computed and recorded as well: + + ![Cyclomatic Complexity and LOC metrics](./images/all-metrics.png) + +* The recorder has been extended with a native step that is capable of setting the step status (unstable, failed, ok): + + ![Native step](./images/step.png) + +* GitHub checks report to show the detailed line and branch coverage results for pull request: + + ![Code Coverage Checks Overview](./images/jacoco-coverage-checks.png) + ![Code Coverage Checks Annotations](./images/jacoco-coverage-checks-annotations.png) + +* GitHub checks report to show the detailed line and mutation coverage results for pull request: + + ![Mutation Coverage Checks Overview](./images/pit-coverage-checks.png) + ![Mutation Coverage Checks Annotations](./images/pit-coverage-checks-annotations.png) + ## Usage -: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. +: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. :exclamation: ### Supported project types -The Warnings Next Generation plugin supports the following Jenkins project types: +The Code Coverage Plug-in supports the following Jenkins project types: - Freestyle Project - Maven Project diff --git a/etc/Jenkinsfile.analysis b/etc/Jenkinsfile.analysis new file mode 100644 index 000000000..d44a8d19f --- /dev/null +++ b/etc/Jenkinsfile.analysis @@ -0,0 +1,35 @@ +node('java-agent') { + stage ('Checkout') { + checkout scm + } + + stage ('Git mining') { + discoverGitReferenceBuild() + mineRepository() + gitDiffStat() + } + + stage ('Build, Test, and Static Analysis') { + withMaven(mavenLocalRepo: '/var/data/m2repository', mavenOpts: '-Xmx768m -Xms512m') { + sh 'mvn -V -e clean verify -Dmaven.test.failure.ignore -Dgpg.skip' + } + + recordIssues tools: [java(), javaDoc()], aggregatingResults: 'true', id: 'java', name: 'Java', filters:[excludeFile('.*Assert.java')] + recordIssues tool: errorProne(), healthy: 1, unhealthy: 20 + + junit testResults: '**/target/*-reports/TEST-*.xml' + recordCoverage tools: [[parser: 'JACOCO', pattern: '**/jacoco/jacoco.xml']], sourceCodeRetention: 'EVERY_BUILD', + qualityGates: [ [threshold: 90.0, metric: 'LINE', baseline: 'PROJECT', criticality: 'UNSTABLE']], + sourceDirectories: [[path: 'plugin/src/main/java']] + recordIssues tools: [checkStyle(pattern: 'target/checkstyle-result.xml'), + spotBugs(pattern: 'target/spotbugsXml.xml'), + pmdParser(pattern: 'target/pmd.xml'), + cpd(pattern: 'target/cpd.xml'), + revApi(pattern: 'target/revapi-result.json'), + taskScanner(highTags:'FIXME', normalTags:'TODO', includePattern: '**/*.java', excludePattern: 'target/**/*')] + } + + stage ('Collect Maven Warnings') { + recordIssues tool: mavenConsole() + } +} diff --git a/images/all-metrics.png b/images/all-metrics.png new file mode 100644 index 000000000..721cb8b90 Binary files /dev/null and b/images/all-metrics.png differ diff --git a/images/buildview_screen.PNG b/images/buildview_screen.PNG deleted file mode 100644 index 7aca1f2ba..000000000 Binary files a/images/buildview_screen.PNG and /dev/null differ diff --git a/images/config-add-adapter.png b/images/config-add-adapter.png deleted file mode 100644 index 1aea4098b..000000000 Binary files a/images/config-add-adapter.png and /dev/null differ diff --git a/images/config.png b/images/config.png deleted file mode 100644 index 73c0d4d31..000000000 Binary files a/images/config.png and /dev/null differ diff --git a/images/jacoco-coverage-checks-annotations.png b/images/jacoco-coverage-checks-annotations.png new file mode 100644 index 000000000..480cfa442 Binary files /dev/null and b/images/jacoco-coverage-checks-annotations.png differ diff --git a/images/jacoco-coverage-checks.png b/images/jacoco-coverage-checks.png new file mode 100644 index 000000000..e8dfebe0a Binary files /dev/null and b/images/jacoco-coverage-checks.png differ diff --git a/images/pit-coverage-checks-annotations.png b/images/pit-coverage-checks-annotations.png new file mode 100644 index 000000000..5bc267335 Binary files /dev/null and b/images/pit-coverage-checks-annotations.png differ diff --git a/images/pit-coverage-checks.png b/images/pit-coverage-checks.png new file mode 100644 index 000000000..b1c65fc43 Binary files /dev/null and b/images/pit-coverage-checks.png differ diff --git a/images/quality-gates.png b/images/quality-gates.png new file mode 100644 index 000000000..42175a5c2 Binary files /dev/null and b/images/quality-gates.png differ diff --git a/images/step.png b/images/step.png new file mode 100644 index 000000000..9b00c0cdf Binary files /dev/null and b/images/step.png differ diff --git a/images/summary.png b/images/summary.png new file mode 100644 index 000000000..68b827b70 Binary files /dev/null and b/images/summary.png differ diff --git a/img.png b/img.png new file mode 100644 index 000000000..882f1b858 Binary files /dev/null and b/img.png differ diff --git a/plugin/pom.xml b/plugin/pom.xml index 3d231fe3f..c3cdd0335 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -32,7 +32,7 @@ 1.17.6 1.83 - 0.21.0 + 0.22.0 2.0.0 1.29.0-4 1.7.8 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 index 9fcb5f80c..30b7156c7 100644 --- 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 @@ -40,8 +40,8 @@ public class TreeMapNodeConverter { */ 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())); + LabeledTreeMapNode root = toTreeMapNode(tree, metric, colorProvider) + .orElse(new LabeledTreeMapNode(getId(node), node.getName())); for (LabeledTreeMapNode child : root.getChildren()) { child.collapseEmptyPackages(); } @@ -49,6 +49,16 @@ public LabeledTreeMapNode toTreeChartModel(final Node node, final Metric metric, return root; } + private String getId(final Node node) { + String id = node.getName(); + if (node.isRoot()) { + return id; + } + else { + return getId(node.getParent()) + '/' + id; + } + } + private Node mergePackages(final Node node) { if (node instanceof ModuleNode) { ModuleNode copy = (ModuleNode) node.copyTree(); @@ -82,13 +92,14 @@ private LabeledTreeMapNode createCoverageTree(final Coverage coverage, final Col Label label = new Label(true, lineColor); Label upperLabel = new Label(true, lineColor); + var id = getId(node); if (node instanceof FileNode) { - return new LabeledTreeMapNode(node.getPath(), node.getName(), new ItemStyle(fillColor), label, upperLabel, + return new LabeledTreeMapNode(id, 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, + LabeledTreeMapNode treeNode = new LabeledTreeMapNode(id, node.getName(), packageStyle, label, upperLabel, String.valueOf(coverage.getTotal()), FORMATTER.getTooltip(coverage)); node.getChildren().stream() 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 index ff397e032..736cec8ef 100644 --- 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 @@ -12,7 +12,6 @@ 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; @@ -27,11 +26,7 @@ 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; /** @@ -51,7 +46,7 @@ public class SourceCodePainter { * @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 + * the ID of the coverage results - each ID will store the files in a separate directory */ public SourceCodePainter(@NonNull final Run build, @NonNull final FilePath workspace, final String id) { this.build = build; @@ -64,8 +59,6 @@ public SourceCodePainter(@NonNull final Run build, @NonNull final FilePath * * @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 @@ -76,7 +69,7 @@ public SourceCodePainter(@NonNull final Run build, @NonNull final FilePath * @throws InterruptedException * if the painting process has been interrupted */ - public void processSourceCodePainting(final List files, final Set sourceDirectories, + public void processSourceCodePainting(final List files, final String sourceCodeEncoding, final SourceCodeRetention sourceCodeRetention, final FilteredLog log) throws InterruptedException { @@ -87,7 +80,7 @@ public void processSourceCodePainting(final List files, final Set files, final Set 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); + var painter = new AgentCoveragePainter(paintedFiles, sourceCodeEncoding, id); FilteredLog agentLog = workspace.act(painter); log.merge(agentLog); } @@ -124,11 +109,7 @@ 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; /** @@ -136,23 +117,16 @@ static class AgentCoveragePainter extends MasterToSlaveFileCallable * * @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) { + AgentCoveragePainter(final List paintedFiles, final String sourceCodeEncoding, + final String directory) { super(); this.paintedFiles = paintedFiles; - this.permittedSourceDirectories = permittedSourceDirectories; - this.requestedSourceDirectories = requestedSourceDirectories; this.sourceCodeEncoding = sourceCodeEncoding; this.directory = directory; } @@ -160,17 +134,6 @@ static class AgentCoveragePainter extends MasterToSlaveFileCallable @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 { @@ -180,7 +143,7 @@ else if (sourceDirectories.size() == 1) { Path temporaryFolder = Files.createTempDirectory(directory); int count = paintedFiles.parallelStream() - .mapToInt(file -> paintSource(file, workspace, temporaryFolder, sourceDirectories, log)) + .mapToInt(file -> paintSource(file, workspace, temporaryFolder, log)) .sum(); if (count == paintedFiles.size()) { @@ -213,25 +176,19 @@ 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) { + private int paintSource(final PaintedNode fileNode, final FilePath workspace, + final Path temporaryFolder, final FilteredLog log) { String relativePathIdentifier = fileNode.getPath(); FilePath paintedFilesDirectory = workspace.child(directory); - return findSourceFile(workspace, relativePathIdentifier, sourceSearchDirectories, log) + return findSourceFile(workspace, relativePathIdentifier, 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) { + 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); @@ -254,23 +211,16 @@ private int paint(final PaintedNode paint, final String relativePathIdentifier, } private Optional findSourceFile(final FilePath workspace, final String fileName, - final Set sourceDirectories, final FilteredLog log) { + final FilteredLog log) { try { FilePath absolutePath = new FilePath(new File(fileName)); if (absolutePath.exists()) { - return enforcePermissionFor(absolutePath, workspace, sourceDirectories, log); + return Optional.of(absolutePath); } 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); - } + return Optional.of(relativePath); } log.logError("Source file '%s' not found", fileName); @@ -281,17 +231,6 @@ private Optional findSourceFile(final FilePath workspace, final String 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. * @@ -332,7 +271,7 @@ private enum Type { private final int[] killedPerLine; PaintedNode(final FileNode file) { - path = file.getPath(); + path = file.getRelativePath(); linesToPaint = file.getLinesWithCoverage().stream().mapToInt(i -> i).toArray(); coveredPerLine = file.getCoveredCounters(); 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 index a9f9031a9..1fbcebd59 100644 --- 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 @@ -5,7 +5,6 @@ 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; @@ -43,11 +42,10 @@ public SourceViewModel(final Run owner, final String id, final FileNode fi return owner; } - public Node getNode() { + public FileNode getNode() { return fileNode; } - /** * Returns the source file rendered in HTML. * @@ -56,7 +54,7 @@ public Node getNode() { @SuppressWarnings("unused") // Called by jelly view public String getSourceFileContent() { try { - return SOURCE_CODE_FACADE.read(getOwner().getRootDir(), id, getNode().getPath()); + return SOURCE_CODE_FACADE.read(getOwner().getRootDir(), id, getNode().getRelativePath()); } catch (IOException | InterruptedException exception) { return ExceptionUtils.getStackTrace(exception); @@ -66,14 +64,11 @@ public String getSourceFileContent() { /** * 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()); + public boolean isSourceFileAvailable() { + return SOURCE_CODE_FACADE.canRead(getOwner().getRootDir(), id, fileNode.getRelativePath()); } @Override 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 index 1375a4391..adf14a138 100644 --- 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 @@ -39,7 +39,7 @@ public List getRows() { FileNode getOriginalNode(final FileNode fileNode) { return getRoot().getAllFileNodes().stream() - .filter(node -> node.getPath().equals(fileNode.getPath()) + .filter(node -> node.getRelativePath().equals(fileNode.getRelativePath()) && node.getName().equals(fileNode.getName())) .findFirst() .orElse(fileNode); // return this as fallback to prevent exceptions 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 index 3730b0142..1993be0b6 100644 --- 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 @@ -150,7 +150,7 @@ private String getAnnotationSummary() { var filteredRoot = rootNode.filterByModifiedLines(); var modifiedFiles = filteredRoot.getAllFileNodes(); - var summary = new StringBuilder("Modified lines summary:\n"); + var summary = new StringBuilder("#### Summary for modified lines\n"); createTotalLinesSummary(modifiedFiles, summary); createLineCoverageSummary(modifiedFiles, summary); @@ -163,7 +163,7 @@ private String getAnnotationSummary() { } private void createTotalLinesSummary(final List modifiedFiles, final StringBuilder summary) { - var total = modifiedFiles.stream().map(FileNode::getModifiedLines).map(Set::size).count(); + var total = modifiedFiles.stream().map(FileNode::getModifiedLines).mapToInt(Set::size).sum(); if (total == 1) { summary.append("- 1 line has been modified"); } @@ -305,7 +305,7 @@ private String createBranchMessage(final int line, final int missed) { private ChecksAnnotationBuilder createAnnotationBuilder(final FileNode fileNode) { return new ChecksAnnotationBuilder() - .withPath(fileNode.getPath()) + .withPath(fileNode.getRelativePath()) .withAnnotationLevel(ChecksAnnotationLevel.WARNING); } 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 index d47665ce2..932718b06 100644 --- 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 @@ -6,6 +6,7 @@ import java.util.Optional; import java.util.Set; import java.util.TreeMap; +import java.util.stream.Collectors; import org.apache.commons.lang3.math.Fraction; @@ -14,6 +15,7 @@ import edu.hm.hafner.coverage.Node; import edu.hm.hafner.coverage.Value; import edu.hm.hafner.util.FilteredLog; +import edu.hm.hafner.util.TreeStringBuilder; import edu.umd.cs.findbugs.annotations.CheckForNull; import hudson.FilePath; @@ -36,6 +38,7 @@ * * @author Ullrich Hafner */ +@SuppressWarnings("checkstyle:ClassDataAbstractionCoupling") public class CoverageReporter { @SuppressWarnings("checkstyle:ParameterNumber") CoverageBuildAction publishAction(final String id, final String optionalName, final String icon, final Node rootNode, @@ -87,12 +90,6 @@ CoverageBuildAction publishAction(final String id, final String optionalName, fi 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()); @@ -100,18 +97,29 @@ CoverageBuildAction publishAction(final String id, final String optionalName, fi else { filesToStore = rootNode.getAllFileNodes(); } + + resolveAbsolutePaths(rootNode, workspace, sourceDirectories, log, filesToStore); + + action = new CoverageBuildAction(build, id, optionalName, icon, rootNode, qualityGateResult, log, + referenceAction.getOwner().getExternalizableId(), coverageDelta, + modifiedLinesCoverageRoot.aggregateValues(), modifiedLinesCoverageDelta, + aggregatedModifiedFilesCoverage, modifiedFilesCoverageDelta, + rootNode.filterByIndirectChanges().aggregateValues()); } 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(); + + resolveAbsolutePaths(rootNode, workspace, sourceDirectories, log, filesToStore); + + action = new CoverageBuildAction(build, id, optionalName, icon, rootNode, qualityGateStatus, log); } log.logInfo("Executing source code painting..."); SourceCodePainter sourceCodePainter = new SourceCodePainter(build, workspace, id); - sourceCodePainter.processSourceCodePainting(filesToStore, sourceDirectories, + sourceCodePainter.processSourceCodePainting(filesToStore, sourceCodeEncoding, sourceCodeRetention, log); log.logInfo("Finished coverage processing - adding the action to the build..."); @@ -123,6 +131,22 @@ CoverageBuildAction publishAction(final String id, final String optionalName, fi return action; } + private void resolveAbsolutePaths(final Node rootNode, final FilePath workspace, final Set sourceDirectories, + final FilteredLog log, final List filesToStore) throws InterruptedException { + log.logInfo("Resolving source code files..."); + var relativePaths = filesToStore.stream().map(FileNode::getRelativePath).collect(Collectors.toSet()); + var pathMapping = new PathResolver().resolvePaths(relativePaths, sourceDirectories, workspace, log); + + if (!pathMapping.isEmpty()) { + log.logInfo("Making paths of " + pathMapping.size() + " source code files relative to workspace root..."); + var builder = new TreeStringBuilder(); + rootNode.getAllFileNodes().stream() + .filter(file -> pathMapping.containsKey(file.getRelativePath())) + .forEach(file -> file.setRelativePath(builder.intern(pathMapping.get(file.getRelativePath())))); + builder.dedup(); + } + } + private void createDeltaReports(final Node rootNode, final FilteredLog log, final Node referenceRoot, final CodeDeltaCalculator codeDeltaCalculator, final Delta delta) { FileChangesProcessor fileChangesProcessor = new FileChangesProcessor(); 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 index 86313c803..a1355dbb0 100644 --- 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 @@ -215,7 +215,7 @@ public boolean isSkipSymbolicLinks() { } /** - * Determines whether to fail the build on errors during the step of recording coverage reports. + * Determines whether to fail the step 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 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 index 3ac9470c7..96aa98db5 100644 --- 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 @@ -209,7 +209,7 @@ static class CoverageRow { } public String getFileHash() { - return String.valueOf(file.getPath().hashCode()); + return String.valueOf(file.getRelativePath().hashCode()); } @SuppressWarnings("PMD.BooleanGetMethodName") @@ -218,7 +218,7 @@ public boolean getModified() { } public DetailedCell getFileName() { - return new DetailedCell<>(renderer.renderFileName(file.getName(), file.getPath()), file.getName()); + return new DetailedCell<>(renderer.renderFileName(file.getName(), file.getRelativePath()), file.getName()); } public String getPackageName() { 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 index adcfad6a1..01b2b9698 100644 --- 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 @@ -347,7 +347,7 @@ public String getSourceCode(final String fileHash, final String tableId) { if (targetResult.isPresent()) { try { Node fileNode = targetResult.get(); - return readSourceCode(fileNode, tableId); + return readSourceCode((FileNode)fileNode, tableId); } catch (IOException | InterruptedException exception) { return ExceptionUtils.getStackTrace(exception); @@ -371,21 +371,20 @@ public String getSourceCode(final String fileHash, final String tableId) { * @throws InterruptedException * if reading failed */ - private String readSourceCode(final Node sourceNode, final String tableId) + private String readSourceCode(final FileNode 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()); + content = SOURCE_CODE_FACADE.read(rootDir, getId(), sourceNode.getRelativePath()); } - if (!content.isEmpty() && sourceNode instanceof FileNode) { - FileNode fileNode = (FileNode) sourceNode; + if (!content.isEmpty()) { String cleanTableId = StringUtils.removeEnd(tableId, INLINE_SUFFIX); if (MODIFIED_LINES_COVERAGE_TABLE_ID.equals(cleanTableId)) { - return SOURCE_CODE_FACADE.calculateModifiedLinesCoverageSourceCode(content, fileNode); + return SOURCE_CODE_FACADE.calculateModifiedLinesCoverageSourceCode(content, sourceNode); } else if (INDIRECT_COVERAGE_TABLE_ID.equals(cleanTableId)) { - return SOURCE_CODE_FACADE.calculateIndirectCoverageChangesSourceCode(content, fileNode); + return SOURCE_CODE_FACADE.calculateIndirectCoverageChangesSourceCode(content, sourceNode); } else { return content; @@ -430,8 +429,8 @@ public boolean hasIndirectCoverageChanges() { * * @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()); + public boolean isSourceFileAvailable(final FileNode coverageNode) { + return SOURCE_CODE_FACADE.canRead(getOwner().getRootDir(), id, coverageNode.getRelativePath()); } /** 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 index bc5b62c7f..593e10fed 100644 --- 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 @@ -39,7 +39,7 @@ public class FileChangesProcessor { */ public void attachChangedCodeLines(final Node coverageNode, final Map codeChanges) { Map nodePathMapping = coverageNode.getAllFileNodes().stream() - .collect(Collectors.toMap(Node::getPath, Function.identity())); + .collect(Collectors.toMap(FileNode::getRelativePath, Function.identity())); codeChanges.forEach((path, fileChange) -> { if (nodePathMapping.containsKey(path)) { @@ -122,7 +122,7 @@ public void attachIndirectCoveragesChanges(final Node root, final Node reference getReferenceCoveragePerLine(referenceFileNodes, referencePath); if (referenceCoveragePerLine.isPresent()) { SortedMap referenceCoverageMapping = new TreeMap<>(referenceCoveragePerLine.get()); - String currentPath = fileNode.getPath(); + String currentPath = fileNode.getRelativePath(); if (codeChanges.containsKey(currentPath)) { adjustedCoveragePerLine(referenceCoverageMapping, codeChanges.get(currentPath)); } @@ -278,8 +278,8 @@ private List> transformCoveragePerLine( 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())); + .filter(node -> oldPathMapping.containsKey(node.getRelativePath())) + .collect(Collectors.toMap(node -> oldPathMapping.get(node.getRelativePath()), Function.identity())); } /** @@ -296,8 +296,8 @@ private Map getFileNodeMappingWithReferencePaths( 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())); + .filter(reference -> nodeMapping.containsKey(reference.getRelativePath())) + .collect(Collectors.toMap(FileNode::getRelativePath, Function.identity())); } /** diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/PathResolver.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/PathResolver.java new file mode 100644 index 000000000..c5b9d3000 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/PathResolver.java @@ -0,0 +1,255 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.nio.file.InvalidPathException; +import java.util.AbstractMap.SimpleEntry; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import edu.hm.hafner.util.FilteredLog; +import edu.hm.hafner.util.PathUtil; + +import hudson.FilePath; +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.SourceDirectoryFilter; + +/** + * Resolves source code files on the agent using the stored paths of the coverage reports. Since these paths are + * relative this resolver tries to find the absolute paths by guessing the prefix to the relative path. It also + * evaluates the defined source paths as prefixes when resolving the absolute paths. + */ +public class PathResolver { + /** + * Resolves source code files on the agent using the stored paths of the coverage reports. Since these paths are + * relative this resolver tries to find the absolute paths by guessing the prefix to the relative path. It also + * evaluates the defined source paths as prefixes when resolving the absolute paths. + * + * @param relativePaths + * the relative paths to map + * @param requestedSourceDirectories + * the requested relative and absolute source directories (in the step configuration) + * @param workspace + * the workspace that contains the source code files + * @param log + * the log to write to + * + * @return the resolved paths as mapping of relative to absolute paths + */ + public Map resolvePaths(final Set relativePaths, + final Set requestedSourceDirectories, + final FilePath workspace, final FilteredLog log) throws InterruptedException { + try { + Set permittedSourceDirectories = PrismConfiguration.getInstance() + .getSourceDirectories() + .stream() + .map(PermittedSourceCodeDirectory::getPath) + .collect(Collectors.toSet()); + + var resolver = new AgentPathResolver(relativePaths, permittedSourceDirectories, requestedSourceDirectories); + var agentLog = workspace.act(resolver); + log.merge(agentLog); + return agentLog.getResult(); + } + catch (IOException exception) { + log.logException(exception, "Can't resolve source files on agent"); + } + return Collections.emptyMap(); + } + + /** + * Resolves source code files on the agent using the stored paths of the coverage reports. Since these paths are + * relative this resolver tries to find the absolute paths by guessing the prefix to the relative path. It also + * evaluates the defined source paths as prefixes when resolving the absolute paths. + */ + static class AgentPathResolver extends MasterToSlaveFileCallable>> { + private static final long serialVersionUID = 3966282357309568323L; + private static final PathUtil PATH_UTIL = new PathUtil(); + + private final Set relativePaths; + private final Set permittedSourceDirectories; + private final Set requestedSourceDirectories; + + /** + * Creates a new instance of {@link AgentPathResolver}. + * + * @param relativePaths + * the relative paths to map + * @param permittedSourceDirectories + * the permitted source code directories (in Jenkins global configuration) + * @param requestedSourceDirectories + * the requested relative and absolute source directories (in the step configuration) + */ + AgentPathResolver(final Set relativePaths, + final Set permittedSourceDirectories, + final Set requestedSourceDirectories) { + super(); + + this.relativePaths = relativePaths; + this.permittedSourceDirectories = permittedSourceDirectories; + this.requestedSourceDirectories = requestedSourceDirectories; + } + + @Override + public RemoteResultWrapper> invoke( + final File workspaceFile, final VirtualChannel channel) { + FilteredLog log = new FilteredLog("-"); + + 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)); + } + + var workspace = new FilePath(workspaceFile); + var mapping = relativePaths.stream() + .map(path -> new SimpleEntry<>(path, locateSource(path, workspace, sourceDirectories, log))) + .filter(entry -> entry.getValue().isPresent()) + .collect(Collectors.toMap(Entry::getKey, entry -> entry.getValue().get())); + + if (mapping.size() == relativePaths.size()) { + log.logInfo("-> resolved absolute paths for all %d source files", mapping.size()); + } + else { + log.logInfo("-> finished resolving of absolute paths (found: %d, not found: %d)", + mapping.size(), relativePaths.size() - mapping.size()); + } + + var changedFilesMapping = mapping.entrySet() + .stream() + .filter(entry -> !entry.getKey().equals(entry.getValue())) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + var result = new RemoteResultWrapper<>(new HashMap<>(changedFilesMapping), "Errors during source path resolving:"); + result.merge(log); + return result; + } + + private Set filterSourceDirectories(final File workspace, final FilteredLog log) { + SourceDirectoryFilter filter = new SourceDirectoryFilter(); + return filter.getPermittedSourceDirectories(workspace.getAbsolutePath(), + permittedSourceDirectories, requestedSourceDirectories, log); + } + + private Optional locateSource(final String relativePath, final FilePath workspace, + final Set sourceSearchDirectories, final FilteredLog log) { + + try { + FilePath absolutePath = new FilePath(new File(relativePath)); + if (absolutePath.exists()) { + return enforcePermissionFor(absolutePath, workspace, sourceSearchDirectories, log); + } + + FilePath relativePathInWorkspace = workspace.child(relativePath); + if (relativePathInWorkspace.exists()) { + return enforcePermissionFor(relativePathInWorkspace, workspace, sourceSearchDirectories, log); + } + + for (String sourceFolder : sourceSearchDirectories) { + FilePath sourcePath = workspace.child(sourceFolder).child(relativePath); + if (sourcePath.exists()) { + return enforcePermissionFor(sourcePath, workspace, sourceSearchDirectories, log); + } + } + + log.logError("Source file '%s' not found", relativePath); + } + catch (InvalidPathException | IOException | InterruptedException exception) { + log.logException(exception, "No valid path in coverage node: '%s'", relativePath); + } + return Optional.empty(); + } + + private Optional enforcePermissionFor(final FilePath absolutePath, final FilePath workspace, + final Set sourceDirectories, final FilteredLog log) { + FilePermissionEnforcer enforcer = new FilePermissionEnforcer(); + var fileName = absolutePath.getRemote(); + if (enforcer.isInWorkspace(fileName, workspace, sourceDirectories)) { + if (isWithinWorkspace(fileName, workspace)) { + return Optional.of(PATH_UTIL.getRelativePath(workspace.getRemote(), fileName)); + } + else { + return Optional.of(PATH_UTIL.getAbsolutePath(fileName)); + } + } + log.logError("Skipping resolving of file: %s (not part of workspace or permitted source code folders)", + fileName); + return Optional.empty(); + } + + private boolean isWithinWorkspace(final String fileName, final FilePath workspace) { + var workspacePath = PATH_UTIL.getAbsolutePath(workspace.getRemote()); + return PATH_UTIL.getAbsolutePath(fileName).startsWith(workspacePath); + } + } + + /** + * A serializable result combined with a logger. Enables remote calls to return a result and a corresponding log. + * + * @param + * the type of the result + * + * @author Ullrich Hafner + */ + // FIXME: replace with plugin-util class once released + static final class RemoteResultWrapper extends FilteredLog { + private static final long serialVersionUID = -6411417555105688927L; + + private final T result; + + /** + * Creates a new instance of {@link RemoteResultWrapper}. + * + * @param result + * the wrapped result + * @param title + * the title of the error messages + */ + RemoteResultWrapper(final T result, final String title) { + super(title); + + this.result = result; + } + + public T getResult() { + return result; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + RemoteResultWrapper that = (RemoteResultWrapper) o; + return Objects.equals(result, that.result); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), result); + } + } +} diff --git a/plugin/src/main/resources/coverage/configuration.properties b/plugin/src/main/resources/coverage/configuration.properties index 980de9223..102bdf86f 100644 --- a/plugin/src/main/resources/coverage/configuration.properties +++ b/plugin/src/main/resources/coverage/configuration.properties @@ -11,7 +11,7 @@ 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 +failOnError.title=Fail the step 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/io/jenkins/plugins/coverage/CoveragePublisher/help-failUnhealthy.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/CoveragePublisher/help-failUnhealthy.html index 3d0e362ac..6cc0c29af 100644 --- a/plugin/src/main/resources/io/jenkins/plugins/coverage/CoveragePublisher/help-failUnhealthy.html +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/CoveragePublisher/help-failUnhealthy.html @@ -1 +1 @@ -Fail the build if coverage is lower than healthy threshold. \ No newline at end of file +Fail the step if coverage is lower than healthy threshold. 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 index 3023dea7c..dbc8862c8 100644 --- 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 @@ -13,7 +13,7 @@
- +
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 index be06b04d5..6f7147695 100644 --- 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 @@ -1,8 +1,8 @@ reason.1=\ - You did not enable storing of source files (see parameter 'sourceFiles'). + You did not enable storing of source files (see parameter 'sourceCodeRetention'). reason.2=\ - Code Coverage API plugin did not find the source files. + Code Coverage 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/CoverageQualityGate/help-criticality.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate/help-criticality.html index e2322e414..5eb583672 100644 --- 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 @@ -11,7 +11,7 @@
FAILURE
- Fail the build if the quality gate has been missed. + Fail the step if the quality gate has been missed.
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 index c5deb289f..ee8c20f36 100644 --- 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 @@ -1,6 +1,6 @@
- This toggle determines if the coverage plugin should fail the build whenever an error occurred during processing + This toggle determines if the coverage plugin should fail the step 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. + not be altered. If you would rather like to fail the step on such errors, please tick this checkbox.
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 index c5deb289f..ee8c20f36 100644 --- 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 @@ -1,6 +1,6 @@
- This toggle determines if the coverage plugin should fail the build whenever an error occurred during processing + This toggle determines if the coverage plugin should fail the step 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. + not be altered. If you would rather like to fail the step on such errors, please tick this checkbox.
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 index 7ac7b2960..0beb594c7 100644 --- 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 @@ -16,7 +16,7 @@ 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.Failure=Fail the step 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 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 index b757075fe..bb7ffd164 100644 --- 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 @@ -59,7 +59,7 @@ private SourceCodeFacade createSourceCodeFacade() { } private FileNode createFileCoverageNode() { - FileNode file = new FileNode(""); + FileNode file = new FileNode("", "path"); List lines = Arrays.asList(10, 11, 12, 16, 17, 18, 19); for (Integer line : lines) { file.addModifiedLines(line); 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 index b8b5492e2..0dd78efa6 100644 --- 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 @@ -2,14 +2,17 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Paths; 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.FileNode; import edu.hm.hafner.coverage.Metric; import edu.hm.hafner.coverage.Node; +import edu.hm.hafner.util.PathUtil; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; @@ -45,6 +48,7 @@ abstract class SourceCodeITest extends AbstractCoverageITest { 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"; + private static final PathUtil UTIL = new PathUtil(); /** Verifies that the plugin reads source code from the workspace root. */ @Test @@ -76,7 +80,8 @@ void coveragePluginPipelineNotRegisteredSourceCodeDirectory() throws IOException String sourceDirectory = createExternalFolder(); WorkflowJob job = createPipeline(); - copySourceFileToAgent("ignore/", localAgent, job); + var subFolder = "ignore/"; + copySourceFileToAgent(subFolder, localAgent, job); copyReports(localAgent, job); job.setDefinition(createPipelineWithSourceCode(EVERY_BUILD, sourceDirectory)); @@ -84,22 +89,23 @@ void coveragePluginPipelineNotRegisteredSourceCodeDirectory() throws IOException Run firstBuild = buildSuccessfully(job); assertThat(getConsoleLog(firstBuild)) + .contains("-> finished resolving of absolute paths (found: 0, not found: 1)") .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 + verifySourceCodeInBuild("", firstBuild, NO_SOURCE_CODE, NO_SOURCE_CODE); // should be still available localAgent.setLabelString(""); } - private Run runCoverageWithSourceCode(final String sourceDirectory) + private Run runCoverageWithSourceCode(final String sourceDir) throws IOException { var localAgent = crateCoverageAgent(); WorkflowJob job = createPipeline(); copyReports(localAgent, job); - copySourceFileToAgent(sourceDirectory, localAgent, job); + copySourceFileToAgent(sourceDir, localAgent, job); // get the temporary directory - used by unit tests - to verify its content File temporaryDirectory = new File(System.getProperty("java.io.tmpdir")); @@ -107,28 +113,29 @@ void coveragePluginPipelineNotRegisteredSourceCodeDirectory() throws IOException assertThat(temporaryDirectory.isDirectory()).isTrue(); File[] temporaryFiles = temporaryDirectory.listFiles(); - job.setDefinition(createPipelineWithSourceCode(EVERY_BUILD, sourceDirectory)); + job.setDefinition(createPipelineWithSourceCode(EVERY_BUILD, sourceDir)); Run firstBuild = buildSuccessfully(job); assertThat(getConsoleLog(firstBuild)) + .contains("-> resolved absolute paths for all 1 source files") .contains("-> finished painting successfully"); - verifySourceCodeInBuild(firstBuild, ACU_COBOL_PARSER, PATH_UTIL); + verifySourceCodeInBuild(sourceDir, 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 + verifySourceCodeInBuild(sourceDir, secondBuild, ACU_COBOL_PARSER, PATH_UTIL); + verifySourceCodeInBuild(sourceDir, firstBuild, ACU_COBOL_PARSER, PATH_UTIL); // should be still available - job.setDefinition(createPipelineWithSourceCode(LAST_BUILD, sourceDirectory)); + job.setDefinition(createPipelineWithSourceCode(LAST_BUILD, sourceDir)); 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 + verifySourceCodeInBuild(sourceDir, thirdBuild, ACU_COBOL_PARSER, PATH_UTIL); + verifySourceCodeInBuild(sourceDir, firstBuild, NO_SOURCE_CODE, NO_SOURCE_CODE); // should be still available + verifySourceCodeInBuild(sourceDir, secondBuild, NO_SOURCE_CODE, NO_SOURCE_CODE); // should be still available - job.setDefinition(createPipelineWithSourceCode(NEVER, sourceDirectory)); + job.setDefinition(createPipelineWithSourceCode(NEVER, sourceDir)); 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 + verifySourceCodeInBuild(sourceDir, lastBuild, NO_SOURCE_CODE, NO_SOURCE_CODE); + verifySourceCodeInBuild(sourceDir, firstBuild, NO_SOURCE_CODE, NO_SOURCE_CODE); // should be still available + verifySourceCodeInBuild(sourceDir, secondBuild, NO_SOURCE_CODE, NO_SOURCE_CODE); // should be still available + verifySourceCodeInBuild(sourceDir, thirdBuild, NO_SOURCE_CODE, NO_SOURCE_CODE); // should be still available assertThat(temporaryDirectory.listFiles()).isEqualTo(temporaryFiles); @@ -157,31 +164,37 @@ private CpsFlowDefinition createPipelineWithSourceCode(final SourceCodeRetention + "}", true); } - private void verifySourceCodeInBuild(final Run build, final String acuCobolParserSourceCodeSnippet, + private void verifySourceCodeInBuild(final String pathPrefix, final Run build, final String acuCobolParserSourceCodeSnippet, final String pathUtilSourceCodeSnippet) { 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")) + var relativePath = getRelativePath(pathPrefix, ACU_COBOL_PARSER_SOURCE_FILE_PATH); + Optional fileNode = action.getResult().find(Metric.FILE, relativePath); + assertThat(fileNode).isNotEmpty().get() + .isInstanceOfSatisfying(FileNode.class, + node -> assertThat(node.getRelativePath()).isEqualTo(relativePath)); + assertThat(action.getTarget().getSourceCode(String.valueOf(relativePath.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")) + var relativePath = getRelativePath(pathPrefix, PATH_UTIL_SOURCE_FILE_PATH); + Optional fileNode = action.getResult().find(Metric.FILE, relativePath); + assertThat(fileNode).isNotEmpty().get() + .isInstanceOfSatisfying(FileNode.class, + node -> assertThat(node.getRelativePath()).isEqualTo(relativePath)); + assertThat(action.getTarget().getSourceCode(String.valueOf(relativePath.hashCode()), "coverage-table")) .contains(pathUtilSourceCodeSnippet); }); } + private String getRelativePath(final String path, final String filePath) { + return UTIL.getRelativePath(Paths.get(path, filePath)); + } + String createDestinationPath(final String sourceDirectory, final String packagePath, final String fileName) { if (sourceDirectory.isEmpty()) { return packagePath + fileName; 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 index 7a92e596a..1c8724ede 100644 --- 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 @@ -118,7 +118,7 @@ void shouldNotMapScmChangesWithAmbiguousPaths() throws IllegalStateException { Node tree = mock(Node.class); FileNode file1 = mock(FileNode.class); - when(file1.getPath()).thenReturn(path); + when(file1.getRelativePath()).thenReturn(path); when(tree.getAllFileNodes()).thenReturn(List.of(file1)); when(tree.getFiles()).thenReturn(Set.of(path)); @@ -150,8 +150,8 @@ void shouldNotCreateOldPathMappingWithMissingReferenceNodes() throws IllegalStat CodeDeltaCalculator codeDeltaCalculator = createCodeDeltaCalculator(); FilteredLog log = createFilteredLog(); - Node tree = new FileNode(REPORT_PATH_RENAME); - Node referenceTree = new FileNode(REPORT_PATH_MODIFY); + Node tree = new FileNode("Test_Renamed.java", REPORT_PATH_RENAME); + Node referenceTree = new FileNode("Test.java", REPORT_PATH_MODIFY); Map changes = new HashMap<>(); changes.put(REPORT_PATH_RENAME, createFileChanges(SCM_PATH_RENAME, OLD_SCM_PATH_RENAME, FileEditType.RENAME)); @@ -257,16 +257,16 @@ private FileChanges createFileChanges(final String filePath, final String oldFil */ private Node createStubbedCoverageTree() { FileNode addFile1 = mock(FileNode.class); - when(addFile1.getPath()).thenReturn(REPORT_PATH_ADD_1); + when(addFile1.getRelativePath()).thenReturn(REPORT_PATH_ADD_1); FileNode addFile2 = mock(FileNode.class); - when(addFile2.getPath()).thenReturn(REPORT_PATH_ADD_2); + when(addFile2.getRelativePath()).thenReturn(REPORT_PATH_ADD_2); FileNode modifyFile = mock(FileNode.class); - when(modifyFile.getPath()).thenReturn(REPORT_PATH_MODIFY); + when(modifyFile.getRelativePath()).thenReturn(REPORT_PATH_MODIFY); FileNode renameFile = mock(FileNode.class); - when(renameFile.getPath()).thenReturn(REPORT_PATH_RENAME); + when(renameFile.getRelativePath()).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()); + var files = root.getAllFileNodes().stream().map(FileNode::getRelativePath).collect(Collectors.toSet()); when(root.getFiles()).thenReturn(files); return root; @@ -280,12 +280,12 @@ private Node createStubbedCoverageTree() { */ private Node createStubbedReferenceCoverageTree() { FileNode modifyFile = mock(FileNode.class); - when(modifyFile.getPath()).thenReturn(REPORT_PATH_MODIFY); + when(modifyFile.getRelativePath()).thenReturn(REPORT_PATH_MODIFY); FileNode renameFile = mock(FileNode.class); - when(renameFile.getPath()).thenReturn(OLD_REPORT_PATH_RENAME); + when(renameFile.getRelativePath()).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()); + var files = root.getAllFileNodes().stream().map(FileNode::getRelativePath).collect(Collectors.toSet()); when(root.getFiles()).thenReturn(files); return root; 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 index 1c2d3b276..bf3eb96c3 100644 --- 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 @@ -151,8 +151,8 @@ private static void verifyJaCoCoAction(final CoverageBuildAction coverageResult) Metric.BRANCH, Metric.INSTRUCTION, Metric.COMPLEXITY, - Metric.COMPLEXITY_DENSITY, Metric.COMPLEXITY_MAXIMUM, + Metric.COMPLEXITY_DENSITY, Metric.LOC); assertThat(coverageResult.getMetricsForSummary()) .containsExactly(Metric.LINE, Metric.BRANCH, Metric.MUTATION, Metric.COMPLEXITY_DENSITY, Metric.LOC); 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 index 6c9cdfa2f..09713a91e 100644 --- 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 @@ -78,7 +78,7 @@ void shouldProvideIndirectCoverageChanges() { private Node createIndirectCoverageChangesNode() { var root = new ModuleNode("root"); for (int file = 0; file < 5; file++) { - var fileNode = new FileNode("File-" + file); + var fileNode = new FileNode("File-" + file, "path"); for (int line = 0; line < 2; line++) { fileNode.addCounters(10 + line, 1, 1); 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 index 23d4e14e5..804cdf547 100644 --- 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 @@ -112,8 +112,8 @@ void shouldStoreActionCompactly() throws IOException { "BRANCH: 109/116", "INSTRUCTION: 1260/1350", "COMPLEXITY: 160", - "COMPLEXITY_DENSITY: 160/323", "COMPLEXITY_MAXIMUM: 6", + "COMPLEXITY_DENSITY: 160/323", "LOC: 323"); assertThat(Input.from(saved)).nodesByXPath("//" + ACTION_QUALIFIED_NAME + "/projectValues/coverage") @@ -130,11 +130,11 @@ void shouldStoreActionCompactly() throws IOException { 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", "COMPLEXITY_MAXIMUM: 6", "LOC: 323" + .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_MAXIMUM: 6", "COMPLEXITY_DENSITY: 160/323", + "LOC: 323" )); } diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/PathResolverTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/PathResolverTest.java new file mode 100644 index 000000000..1876e1d88 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/PathResolverTest.java @@ -0,0 +1,35 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; + +import io.jenkins.plugins.coverage.metrics.steps.PathResolver.RemoteResultWrapper; + +import static org.assertj.core.api.Assertions.*; + +class PathResolverTest { + @Nested + class RemoteResultWrapperTest { + @Test + void shouldCreateWrapper() { + var result = "result"; + + var wrapper = new RemoteResultWrapper<>(result, "title"); + + assertThat(wrapper.getResult()).isEqualTo(result); + + wrapper.logInfo("Hello %s", "World"); + assertThat(wrapper.getInfoMessages()).containsExactly("Hello World"); + } + + @Test + void shouldAdhereToEquals() { + EqualsVerifier.simple().forClass(RemoteResultWrapper.class) + .suppress(Warning.NULL_FIELDS) + .verify(); + } + } +} diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/coverage-publisher-summary.checks-expected-result b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/coverage-publisher-summary.checks-expected-result index 896556689..f89d9824e 100644 --- a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/coverage-publisher-summary.checks-expected-result +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/coverage-publisher-summary.checks-expected-result @@ -1,5 +1,6 @@ -Modified lines summary: -- 1 line has been modified +#### Summary for modified lines + +- 3 lines have been modified - 1 line is not covered - 1 line is covered only partially