Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import edu.hm.hafner.echarts.LineSeries.StackedMode;
import edu.hm.hafner.echarts.LinesChartModel;

import java.util.Collections;
import java.util.Map;

import io.jenkins.plugins.analysis.core.util.AnalysisBuildResult;
import io.jenkins.plugins.echarts.JenkinsPalette;

Expand All @@ -16,6 +19,25 @@
* @author Ullrich Hafner
*/
public class ToolsTrendChart implements TrendChart {
private final Map<String, String> toolNames;

/**
* Creates a chart without tool name mappings. Tool IDs will be displayed directly.
*/
public ToolsTrendChart() {
this(Collections.emptyMap());
}

/**
* Creates a chart with tool name mappings.
*
* @param toolNames
* a map from tool IDs to human-readable names
*/
public ToolsTrendChart(final Map<String, String> toolNames) {
this.toolNames = toolNames;
}

@Override
public LinesChartModel create(final Iterable<? extends BuildResult<AnalysisBuildResult>> results,
final ChartModelConfiguration configuration) {
Expand All @@ -25,10 +47,11 @@ public LinesChartModel create(final Iterable<? extends BuildResult<AnalysisBuild
var model = new LinesChartModel(lineModel);

int index = 0;
for (String name : lineModel.getDataSetIds()) {
var lineSeries = new LineSeries(name, JenkinsPalette.chartColor(index).normal(),
for (String toolId : lineModel.getDataSetIds()) {
String displayName = toolNames.getOrDefault(toolId, toolId);
var lineSeries = new LineSeries(displayName, JenkinsPalette.chartColor(index).normal(),
StackedMode.SEPARATE_LINES, FilledMode.LINES);
lineSeries.addAll(lineModel.getSeries(name));
lineSeries.addAll(lineModel.getSeries(toolId));
model.addSeries(lineSeries);
index++;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ private LinesChartModel createChartModel(final ChartModelConfiguration configura
if (lastBuild == null) {
return new LinesChartModel();
}
return new ToolsTrendChart().create(new CompositeBuildResultsIterable(lastBuild), configuration);
var nameRegistry = ToolNameRegistry.fromBuild(lastBuild);
return new ToolsTrendChart(nameRegistry.asMap()).create(new CompositeBuildResultsIterable(lastBuild), configuration);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ private TrendChart selectChart(final String chartType) {
}
}
if (numberOfTools > 1) {
Run<?, ?> lastBuild = owner.getLastBuild();
if (lastBuild != null) {
return new ToolsTrendChart(ToolNameRegistry.fromBuild(lastBuild).asMap());
}
return new ToolsTrendChart();
}
else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package io.jenkins.plugins.analysis.core.model;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;

import hudson.model.Run;

/**
* Registry that maps tool IDs to their human-readable names. This is used to display tool names instead of IDs in
* trend charts and other visualizations.
*
* @author Akash Manna
*/
public class ToolNameRegistry {
private final Map<String, String> idToNameMap;

/**
* Creates an empty registry.
*/
public ToolNameRegistry() {
this(new HashMap<>());
}

/**
* Creates a registry with the given ID-to-name mapping.
*
* @param idToNameMap
* the mapping of tool IDs to names
*/
public ToolNameRegistry(final Map<String, String> idToNameMap) {
this.idToNameMap = new HashMap<>(idToNameMap);
}

/**
* Creates a registry from the {@link ResultAction}s of a build. Each action provides a tool ID and name, which
* are stored in the registry for later lookup. Names are HTML-escaped at creation time.
*
* @param build
* the build that contains the result actions
*
* @return a registry containing all tool IDs and HTML-escaped names from the build
*/
public static ToolNameRegistry fromBuild(final Run<?, ?> build) {
Map<String, String> mapping = new HashMap<>();
LabelProviderFactory factory = new LabelProviderFactory();
for (ResultAction action : build.getActions(ResultAction.class)) {
String id = action.getId();
String name = action.getName();
if (StringUtils.isBlank(name)) {
name = factory.create(id).getName();
}
mapping.put(id, StringEscapeUtils.escapeHtml4(name));
}
return new ToolNameRegistry(mapping);
}

/**
* Returns the human-readable name for a tool ID. If the ID is not registered, attempts to look up the name from
* the {@link LabelProviderFactory}. If that also fails, returns the ID itself. Names returned are already
* HTML-escaped.
*
* @param id
* the tool ID
*
* @return the HTML-escaped human-readable name, or the escaped ID if no name is found
*/
public String getName(final String id) {
if (idToNameMap.containsKey(id)) {
return idToNameMap.get(id);
}
var labelProvider = new LabelProviderFactory().create(id);
return StringEscapeUtils.escapeHtml4(labelProvider.getName());
}

/**
* Registers a tool ID with its corresponding name. The name will be HTML-escaped before storing.
*
* @param id
* the tool ID
* @param name
* the human-readable name
*/
public void register(final String id, final String name) {
idToNameMap.put(id, StringEscapeUtils.escapeHtml4(name));
}

/**
* Returns whether the registry contains a mapping for the given tool ID.
*
* @param id
* the tool ID
*
* @return {@code true} if the registry contains a mapping for the ID, {@code false} otherwise
*/
public boolean contains(final String id) {
return idToNameMap.containsKey(id);
}

/**
* Returns the number of registered tool IDs.
*
* @return the number of registered IDs
*/
public int size() {
return idToNameMap.size();
}

/**
* Returns the ID-to-name mapping as an immutable map. The names in the returned map are already HTML-escaped.
*
* @return an immutable map from tool IDs to HTML-escaped names
*/
public Map<String, String> asMap() {
return Collections.unmodifiableMap(idToNameMap);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.HashSet;
import java.util.List;

import io.jenkins.plugins.analysis.core.model.ToolNameRegistry;
import io.jenkins.plugins.analysis.core.util.AnalysisBuildResult;

import static io.jenkins.plugins.analysis.core.charts.BuildResultStubs.*;
Expand All @@ -31,15 +32,15 @@

@Test
void shouldCreateToolsChartForMultipleActions() {
var chart = new ToolsTrendChart();

List<BuildResult<AnalysisBuildResult>> compositeResults = new ArrayList<>();
compositeResults.add(new BuildResult<>(new Build(1), new CompositeBuildResult(List.of(
createAnalysisBuildResult(CHECK_STYLE, 1), createAnalysisBuildResult(SPOT_BUGS, 3)))));
compositeResults.add(new BuildResult<>(new Build(2), new CompositeBuildResult(List.of(
createAnalysisBuildResult(CHECK_STYLE, 2), createAnalysisBuildResult(SPOT_BUGS, 4)))));

var model = chart.create(compositeResults, new ChartModelConfiguration());

Check warning on line 43 in plugin/src/test/java/io/jenkins/plugins/analysis/core/charts/ToolsTrendChartTest.java

View check run for this annotation

ci.jenkins.io / CPD

CPD

LOW: Found duplicated code.
Raw output
<pre><code>var chart &#61; new ToolsTrendChart(); List&lt;BuildResult&lt;AnalysisBuildResult&gt;&gt; compositeResults &#61; new ArrayList&lt;&gt;(); compositeResults.add(new BuildResult&lt;&gt;(new Build(1), new CompositeBuildResult(List.of( createAnalysisBuildResult(CHECK_STYLE, 1), createAnalysisBuildResult(SPOT_BUGS, 3))))); compositeResults.add(new BuildResult&lt;&gt;(new Build(2), new CompositeBuildResult(List.of( createAnalysisBuildResult(CHECK_STYLE, 2), createAnalysisBuildResult(SPOT_BUGS, 4))))); var model &#61; chart.create(compositeResults, new ChartModelConfiguration());</code></pre>

verifySeries(model.getSeries().get(0), CHECK_STYLE, 1, 2);
verifySeries(model.getSeries().get(1), SPOT_BUGS, 3, 4);
Expand Down Expand Up @@ -94,4 +95,41 @@
int sizeWithoutDuplicate = new HashSet<>(list).size();
return list.size() > sizeWithoutDuplicate;
}

@Test
void shouldUseToolNamesInsteadOfIds() {
ToolNameRegistry registry = new ToolNameRegistry();
registry.register(CHECK_STYLE, "CheckStyle Warnings");
registry.register(SPOT_BUGS, "SpotBugs Issues");

var chart = new ToolsTrendChart(registry.asMap());

List<BuildResult<AnalysisBuildResult>> compositeResults = new ArrayList<>();
compositeResults.add(new BuildResult<>(new Build(1), new CompositeBuildResult(List.of(
createAnalysisBuildResult(CHECK_STYLE, 1), createAnalysisBuildResult(SPOT_BUGS, 3)))));
compositeResults.add(new BuildResult<>(new Build(2), new CompositeBuildResult(List.of(
createAnalysisBuildResult(CHECK_STYLE, 2), createAnalysisBuildResult(SPOT_BUGS, 4)))));

var model = chart.create(compositeResults, new ChartModelConfiguration());

Check warning on line 113 in plugin/src/test/java/io/jenkins/plugins/analysis/core/charts/ToolsTrendChartTest.java

View check run for this annotation

ci.jenkins.io / CPD

CPD

LOW: Found duplicated code.
Raw output
<pre><code>var chart &#61; new ToolsTrendChart(); List&lt;BuildResult&lt;AnalysisBuildResult&gt;&gt; compositeResults &#61; new ArrayList&lt;&gt;(); compositeResults.add(new BuildResult&lt;&gt;(new Build(1), new CompositeBuildResult(List.of( createAnalysisBuildResult(CHECK_STYLE, 1), createAnalysisBuildResult(SPOT_BUGS, 3))))); compositeResults.add(new BuildResult&lt;&gt;(new Build(2), new CompositeBuildResult(List.of( createAnalysisBuildResult(CHECK_STYLE, 2), createAnalysisBuildResult(SPOT_BUGS, 4))))); var model &#61; chart.create(compositeResults, new ChartModelConfiguration());</code></pre>

assertThatJson(model.getSeries().get(0)).node("name").isEqualTo("CheckStyle Warnings");
assertThatJson(model.getSeries().get(1)).node("name").isEqualTo("SpotBugs Issues");
}

@Test
void shouldEscapeHtmlInToolNames() {
ToolNameRegistry registry = new ToolNameRegistry();
registry.register("custom", "<script>alert('xss')</script>");

var chart = new ToolsTrendChart(registry.asMap());

List<BuildResult<AnalysisBuildResult>> results = new ArrayList<>();
results.add(new BuildResult<>(new Build(1), new CompositeBuildResult(List.of(
createAnalysisBuildResult("custom", 5)))));

var model = chart.create(results, new ChartModelConfiguration());

assertThatJson(model.getSeries().get(0)).node("name")
.isEqualTo("&lt;script&gt;alert('xss')&lt;/script&gt;");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package io.jenkins.plugins.analysis.core.model;

import org.junit.jupiter.api.Test;

import hudson.model.Run;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

/**
* Tests the class {@link ToolNameRegistry}.
*
* @author Akash Manna
*/
class ToolNameRegistryTest {
@Test
void shouldCreateEmptyRegistry() {
ToolNameRegistry registry = new ToolNameRegistry();

assertThat(registry.size()).isEqualTo(0);
assertThat(registry.contains("checkstyle")).isFalse();
}

@Test
void shouldRegisterAndRetrieveNames() {
ToolNameRegistry registry = new ToolNameRegistry();

registry.register("checkstyle", "CheckStyle Warnings");
registry.register("spotbugs", "SpotBugs");

assertThat(registry.size()).isEqualTo(2);
assertThat(registry.contains("checkstyle")).isTrue();
assertThat(registry.contains("spotbugs")).isTrue();
assertThat(registry.getName("checkstyle")).isEqualTo("CheckStyle Warnings");
assertThat(registry.getName("spotbugs")).isEqualTo("SpotBugs");
}

@Test
void shouldEscapeHtmlInNames() {
ToolNameRegistry registry = new ToolNameRegistry();

registry.register("custom", "<script>alert('xss')</script>");

assertThat(registry.getName("custom")).isEqualTo("&lt;script&gt;alert('xss')&lt;/script&gt;");
assertThat(registry.asMap().get("custom")).isEqualTo("&lt;script&gt;alert('xss')&lt;/script&gt;");
}

@Test
void shouldReturnEscapedIdForUnknownId() {
ToolNameRegistry registry = new ToolNameRegistry();

registry.register("unknown", "unknown");
assertThat(registry.getName("unknown")).isEqualTo("unknown");

registry.register("<script>", "<script>");
assertThat(registry.getName("<script>")).isEqualTo("&lt;script&gt;");
}

@Test
void shouldCreateRegistryFromBuild() {
Run<?, ?> build = mock(Run.class);

ResultAction checkstyleAction = mock(ResultAction.class);
when(checkstyleAction.getId()).thenReturn("checkstyle");
when(checkstyleAction.getName()).thenReturn("CheckStyle");

ResultAction spotbugsAction = mock(ResultAction.class);
when(spotbugsAction.getId()).thenReturn("spotbugs");
when(spotbugsAction.getName()).thenReturn("SpotBugs");

when(build.getActions(ResultAction.class)).thenReturn(java.util.List.of(checkstyleAction, spotbugsAction));

ToolNameRegistry registry = ToolNameRegistry.fromBuild(build);

assertThat(registry.size()).isEqualTo(2);
assertThat(registry.getName("checkstyle")).isEqualTo("CheckStyle");
assertThat(registry.getName("spotbugs")).isEqualTo("SpotBugs");
}

@Test
void shouldFallbackToIdForRegisteredIds() {
ToolNameRegistry registry = new ToolNameRegistry();

registry.register("checkstyle", "CheckStyle");
registry.register("unknownToolId", "Unknown Tool");

assertThat(registry.getName("checkstyle")).isEqualTo("CheckStyle");
assertThat(registry.getName("unknownToolId")).isEqualTo("Unknown Tool");
}
}
Loading