@@ -58,6 +58,24 @@ public class FileSourceTester { | |||
return dto; | |||
} | |||
@SafeVarargs | |||
public final FileSourceDto insertFileSource(ComponentDto file, int numLines, Consumer<FileSourceDto>... dtoPopulators) { | |||
FileSourceDto dto = new FileSourceDto() | |||
.setProjectUuid(file.projectUuid()) | |||
.setFileUuid(file.uuid()) | |||
.setSrcHash(randomAlphanumeric(50)) | |||
.setDataHash(randomAlphanumeric(50)) | |||
.setLineHashes(IntStream.range(0, numLines).mapToObj(String::valueOf).collect(MoreCollectors.toList())) | |||
.setRevision(randomAlphanumeric(100)) | |||
.setSourceData(newRandomData(numLines).build()) | |||
.setCreatedAt(new Date().getTime()) | |||
.setUpdatedAt(new Date().getTime()); | |||
Arrays.stream(dtoPopulators).forEach(c -> c.accept(dto)); | |||
db.getDbClient().fileSourceDao().insert(db.getSession(), dto); | |||
db.commit(); | |||
return dto; | |||
} | |||
private static DbFileSources.Data.Builder newRandomData(int numberOfLines) { | |||
DbFileSources.Data.Builder dataBuilder = DbFileSources.Data.newBuilder(); | |||
for (int i = 1; i <= numberOfLines; i++) { |
@@ -19,14 +19,6 @@ | |||
*/ | |||
package org.sonar.server.component.ws; | |||
import com.google.common.collect.Maps; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import java.util.Map; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import org.apache.commons.lang.BooleanUtils; | |||
import org.sonar.api.measures.Metric; | |||
import org.sonar.api.server.ws.Change; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.Response; | |||
@@ -36,26 +28,10 @@ import org.sonar.api.web.UserRole; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.measure.LiveMeasureDto; | |||
import org.sonar.db.metric.MetricDto; | |||
import org.sonar.db.property.PropertyDto; | |||
import org.sonar.db.property.PropertyQuery; | |||
import org.sonar.server.component.ComponentFinder; | |||
import org.sonar.server.user.UserSession; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
import static java.util.Arrays.asList; | |||
import static java.util.Collections.unmodifiableList; | |||
import static org.sonar.api.measures.CoreMetrics.COVERAGE; | |||
import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES_DENSITY; | |||
import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES_DENSITY_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.LINES; | |||
import static org.sonar.api.measures.CoreMetrics.LINES_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.TESTS; | |||
import static org.sonar.api.measures.CoreMetrics.TESTS_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.VIOLATIONS; | |||
import static org.sonar.api.measures.CoreMetrics.VIOLATIONS_KEY; | |||
import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01; | |||
import static org.sonar.server.component.ComponentFinder.ParamNames.COMPONENT_ID_AND_COMPONENT; | |||
import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_BRANCH; | |||
@@ -65,25 +41,20 @@ import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; | |||
import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001; | |||
public class AppAction implements ComponentsWsAction { | |||
private static final String PARAM_COMPONENT_ID = "componentId"; | |||
private static final String PARAM_COMPONENT = "component"; | |||
private static final List<String> METRIC_KEYS = unmodifiableList(asList( | |||
LINES_KEY, | |||
VIOLATIONS_KEY, | |||
COVERAGE_KEY, | |||
DUPLICATED_LINES_DENSITY_KEY, | |||
TESTS_KEY)); | |||
private final DbClient dbClient; | |||
private final UserSession userSession; | |||
private final ComponentFinder componentFinder; | |||
private final ComponentViewerJsonWriter componentViewerJsonWriter; | |||
public AppAction(DbClient dbClient, UserSession userSession, ComponentFinder componentFinder) { | |||
public AppAction(DbClient dbClient, UserSession userSession, ComponentFinder componentFinder, ComponentViewerJsonWriter componentViewerJsonWriter) { | |||
this.dbClient = dbClient; | |||
this.userSession = userSession; | |||
this.componentFinder = componentFinder; | |||
this.componentViewerJsonWriter = componentViewerJsonWriter; | |||
} | |||
@Override | |||
@@ -130,10 +101,9 @@ public class AppAction implements ComponentsWsAction { | |||
JsonWriter json = response.newJsonWriter(); | |||
json.beginObject(); | |||
Map<String, LiveMeasureDto> measuresByMetricKey = loadMeasuresGroupedByMetricKey(component, session); | |||
appendComponent(json, component, userSession, session); | |||
componentViewerJsonWriter.writeComponent(json, component, userSession, session); | |||
appendPermissions(json, userSession); | |||
appendMeasures(json, measuresByMetricKey); | |||
componentViewerJsonWriter.writeMeasures(json, component, session); | |||
json.endObject(); | |||
json.close(); | |||
} | |||
@@ -151,97 +121,8 @@ public class AppAction implements ComponentsWsAction { | |||
return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, request.mandatoryParam(PARAM_COMPONENT), branch, pullRequest); | |||
} | |||
private void appendComponent(JsonWriter json, ComponentDto component, UserSession userSession, DbSession session) { | |||
List<PropertyDto> propertyDtos = dbClient.propertiesDao().selectByQuery(PropertyQuery.builder() | |||
.setKey("favourite") | |||
.setComponentId(component.getId()) | |||
.setUserId(userSession.getUserId()) | |||
.build(), | |||
session); | |||
boolean isFavourite = propertyDtos.size() == 1; | |||
json.prop("key", component.getKey()); | |||
json.prop("uuid", component.uuid()); | |||
json.prop("path", component.path()); | |||
json.prop("name", component.name()); | |||
json.prop("longName", component.longName()); | |||
json.prop("q", component.qualifier()); | |||
ComponentDto parentModule = retrieveParentModuleIfNotCurrentComponent(component, session); | |||
ComponentDto project = dbClient.componentDao().selectOrFailByUuid(session, component.projectUuid()); | |||
// Do not display parent module if parent module and project are the same | |||
boolean displayParentModule = parentModule != null && !parentModule.uuid().equals(project.uuid()); | |||
json.prop("subProject", displayParentModule ? parentModule.getKey() : null); | |||
json.prop("subProjectName", displayParentModule ? parentModule.longName() : null); | |||
json.prop("project", project.getKey()); | |||
json.prop("projectName", project.longName()); | |||
String branch = project.getBranch(); | |||
if (branch != null) { | |||
json.prop("branch", branch); | |||
} | |||
String pullRequest = project.getPullRequest(); | |||
if (pullRequest != null) { | |||
json.prop("pullRequest", pullRequest); | |||
} | |||
json.prop("fav", isFavourite); | |||
} | |||
private static void appendPermissions(JsonWriter json, UserSession userSession) { | |||
json.prop("canMarkAsFavorite", userSession.isLoggedIn()); | |||
} | |||
private static void appendMeasures(JsonWriter json, Map<String, LiveMeasureDto> measuresByMetricKey) { | |||
json.name("measures").beginObject(); | |||
json.prop("lines", formatMeasure(measuresByMetricKey, LINES)); | |||
json.prop("coverage", formatMeasure(measuresByMetricKey, COVERAGE)); | |||
json.prop("duplicationDensity", formatMeasure(measuresByMetricKey, DUPLICATED_LINES_DENSITY)); | |||
json.prop("issues", formatMeasure(measuresByMetricKey, VIOLATIONS)); | |||
json.prop("tests", formatMeasure(measuresByMetricKey, TESTS)); | |||
json.endObject(); | |||
} | |||
private Map<String, LiveMeasureDto> loadMeasuresGroupedByMetricKey(ComponentDto component, DbSession dbSession) { | |||
List<MetricDto> metrics = dbClient.metricDao().selectByKeys(dbSession, METRIC_KEYS); | |||
Map<Integer, MetricDto> metricsById = Maps.uniqueIndex(metrics, MetricDto::getId); | |||
List<LiveMeasureDto> measures = dbClient.liveMeasureDao() | |||
.selectByComponentUuidsAndMetricIds(dbSession, Collections.singletonList(component.uuid()), metricsById.keySet()); | |||
return Maps.uniqueIndex(measures, m -> metricsById.get(m.getMetricId()).getKey()); | |||
} | |||
@CheckForNull | |||
private ComponentDto retrieveParentModuleIfNotCurrentComponent(ComponentDto componentDto, DbSession session) { | |||
final String moduleUuid = componentDto.moduleUuid(); | |||
if (moduleUuid == null || componentDto.uuid().equals(moduleUuid)) { | |||
return null; | |||
} | |||
return dbClient.componentDao().selectOrFailByUuid(session, moduleUuid); | |||
} | |||
@CheckForNull | |||
private static String formatMeasure(Map<String, LiveMeasureDto> measuresByMetricKey, Metric metric) { | |||
LiveMeasureDto measure = measuresByMetricKey.get(metric.getKey()); | |||
return formatMeasure(measure, metric); | |||
} | |||
private static String formatMeasure(@Nullable LiveMeasureDto measure, Metric metric) { | |||
if (measure == null) { | |||
return null; | |||
} | |||
Double value = getDoubleValue(measure, metric); | |||
if (value != null) { | |||
return Double.toString(value); | |||
} | |||
return null; | |||
} | |||
@CheckForNull | |||
private static Double getDoubleValue(LiveMeasureDto measure, Metric metric) { | |||
Double value = measure.getValue(); | |||
if (BooleanUtils.isTrue(metric.isOptimizedBestValue()) && value == null) { | |||
value = metric.getBestValue(); | |||
} | |||
return value; | |||
} | |||
} |
@@ -0,0 +1,164 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.component.ws; | |||
import com.google.common.collect.Maps; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import java.util.Map; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import org.apache.commons.lang.BooleanUtils; | |||
import org.sonar.api.measures.Metric; | |||
import org.sonar.api.utils.text.JsonWriter; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.measure.LiveMeasureDto; | |||
import org.sonar.db.metric.MetricDto; | |||
import org.sonar.db.property.PropertyDto; | |||
import org.sonar.db.property.PropertyQuery; | |||
import org.sonar.server.user.UserSession; | |||
import static java.util.Arrays.asList; | |||
import static java.util.Collections.unmodifiableList; | |||
import static org.sonar.api.measures.CoreMetrics.COVERAGE; | |||
import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES_DENSITY; | |||
import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES_DENSITY_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.LINES; | |||
import static org.sonar.api.measures.CoreMetrics.LINES_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.TESTS; | |||
import static org.sonar.api.measures.CoreMetrics.TESTS_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.VIOLATIONS; | |||
import static org.sonar.api.measures.CoreMetrics.VIOLATIONS_KEY; | |||
public class ComponentViewerJsonWriter { | |||
private static final List<String> METRIC_KEYS = unmodifiableList(asList( | |||
LINES_KEY, | |||
VIOLATIONS_KEY, | |||
COVERAGE_KEY, | |||
DUPLICATED_LINES_DENSITY_KEY, | |||
TESTS_KEY)); | |||
private final DbClient dbClient; | |||
public ComponentViewerJsonWriter(DbClient dbClient) { | |||
this.dbClient = dbClient; | |||
} | |||
public void writeComponentWithoutFav(JsonWriter json, ComponentDto component, DbSession session, boolean includeSubProject) { | |||
json.prop("key", component.getKey()); | |||
json.prop("uuid", component.uuid()); | |||
json.prop("path", component.path()); | |||
json.prop("name", component.name()); | |||
json.prop("longName", component.longName()); | |||
json.prop("q", component.qualifier()); | |||
ComponentDto project = dbClient.componentDao().selectOrFailByUuid(session, component.projectUuid()); | |||
if (includeSubProject) { | |||
ComponentDto parentModule = retrieveParentModuleIfNotCurrentComponent(component, session); | |||
// Do not display parent module if parent module and project are the same | |||
boolean displayParentModule = parentModule != null && !parentModule.uuid().equals(project.uuid()); | |||
json.prop("subProject", displayParentModule ? parentModule.getKey() : null); | |||
json.prop("subProjectName", displayParentModule ? parentModule.longName() : null); | |||
} | |||
json.prop("project", project.getKey()); | |||
json.prop("projectName", project.longName()); | |||
String branch = project.getBranch(); | |||
if (branch != null) { | |||
json.prop("branch", branch); | |||
} | |||
String pullRequest = project.getPullRequest(); | |||
if (pullRequest != null) { | |||
json.prop("pullRequest", pullRequest); | |||
} | |||
} | |||
public void writeComponent(JsonWriter json, ComponentDto component, UserSession userSession, DbSession session) { | |||
writeComponentWithoutFav(json, component, session, true); | |||
List<PropertyDto> propertyDtos = dbClient.propertiesDao().selectByQuery(PropertyQuery.builder() | |||
.setKey("favourite") | |||
.setComponentId(component.getId()) | |||
.setUserId(userSession.getUserId()) | |||
.build(), | |||
session); | |||
boolean isFavourite = propertyDtos.size() == 1; | |||
json.prop("fav", isFavourite); | |||
} | |||
public void writeMeasures(JsonWriter json, ComponentDto component, DbSession session) { | |||
Map<String, LiveMeasureDto> measuresByMetricKey = loadMeasuresGroupedByMetricKey(component, session); | |||
json.name("measures").beginObject(); | |||
json.prop("lines", formatMeasure(measuresByMetricKey, LINES)); | |||
json.prop("coverage", formatMeasure(measuresByMetricKey, COVERAGE)); | |||
json.prop("duplicationDensity", formatMeasure(measuresByMetricKey, DUPLICATED_LINES_DENSITY)); | |||
json.prop("issues", formatMeasure(measuresByMetricKey, VIOLATIONS)); | |||
json.prop("tests", formatMeasure(measuresByMetricKey, TESTS)); | |||
json.endObject(); | |||
} | |||
private Map<String, LiveMeasureDto> loadMeasuresGroupedByMetricKey(ComponentDto component, DbSession dbSession) { | |||
List<MetricDto> metrics = dbClient.metricDao().selectByKeys(dbSession, METRIC_KEYS); | |||
Map<Integer, MetricDto> metricsById = Maps.uniqueIndex(metrics, MetricDto::getId); | |||
List<LiveMeasureDto> measures = dbClient.liveMeasureDao() | |||
.selectByComponentUuidsAndMetricIds(dbSession, Collections.singletonList(component.uuid()), metricsById.keySet()); | |||
return Maps.uniqueIndex(measures, m -> metricsById.get(m.getMetricId()).getKey()); | |||
} | |||
@CheckForNull | |||
private static String formatMeasure(Map<String, LiveMeasureDto> measuresByMetricKey, Metric metric) { | |||
LiveMeasureDto measure = measuresByMetricKey.get(metric.getKey()); | |||
return formatMeasure(measure, metric); | |||
} | |||
private static String formatMeasure(@Nullable LiveMeasureDto measure, Metric metric) { | |||
if (measure == null) { | |||
return null; | |||
} | |||
Double value = getDoubleValue(measure, metric); | |||
if (value != null) { | |||
return Double.toString(value); | |||
} | |||
return null; | |||
} | |||
@CheckForNull | |||
private static Double getDoubleValue(LiveMeasureDto measure, Metric metric) { | |||
Double value = measure.getValue(); | |||
if (BooleanUtils.isTrue(metric.isOptimizedBestValue()) && value == null) { | |||
value = metric.getBestValue(); | |||
} | |||
return value; | |||
} | |||
@CheckForNull | |||
private ComponentDto retrieveParentModuleIfNotCurrentComponent(ComponentDto componentDto, DbSession session) { | |||
final String moduleUuid = componentDto.moduleUuid(); | |||
if (moduleUuid == null || componentDto.uuid().equals(moduleUuid)) { | |||
return null; | |||
} | |||
return dbClient.componentDao().selectOrFailByUuid(session, moduleUuid); | |||
} | |||
} |
@@ -49,6 +49,7 @@ import org.sonar.server.component.ComponentUpdater; | |||
import org.sonar.server.component.index.ComponentIndex; | |||
import org.sonar.server.component.index.ComponentIndexDefinition; | |||
import org.sonar.server.component.index.ComponentIndexer; | |||
import org.sonar.server.component.ws.ComponentViewerJsonWriter; | |||
import org.sonar.server.component.ws.ComponentsWsModule; | |||
import org.sonar.server.debt.DebtModelPluginRepository; | |||
import org.sonar.server.debt.DebtModelXMLExporter; | |||
@@ -186,7 +187,9 @@ import org.sonar.server.source.HtmlSourceDecorator; | |||
import org.sonar.server.source.SourceService; | |||
import org.sonar.server.source.ws.HashAction; | |||
import org.sonar.server.source.ws.IndexAction; | |||
import org.sonar.server.source.ws.IssueSnippetsAction; | |||
import org.sonar.server.source.ws.LinesAction; | |||
import org.sonar.server.source.ws.LinesJsonWriter; | |||
import org.sonar.server.source.ws.RawAction; | |||
import org.sonar.server.source.ws.ScmAction; | |||
import org.sonar.server.source.ws.SourcesWs; | |||
@@ -390,6 +393,7 @@ public class PlatformLevel4 extends PlatformLevel { | |||
ComponentIndex.class, | |||
ComponentIndexer.class, | |||
LiveMeasureModule.class, | |||
ComponentViewerJsonWriter.class, | |||
FavoriteModule.class, | |||
@@ -432,9 +436,11 @@ public class PlatformLevel4 extends PlatformLevel { | |||
// source | |||
HtmlSourceDecorator.class, | |||
LinesJsonWriter.class, | |||
SourceService.class, | |||
SourcesWs.class, | |||
org.sonar.server.source.ws.ShowAction.class, | |||
IssueSnippetsAction.class, | |||
LinesAction.class, | |||
HashAction.class, | |||
RawAction.class, |
@@ -20,6 +20,7 @@ | |||
package org.sonar.server.source; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.function.Function; | |||
import org.sonar.core.util.stream.MoreCollectors; | |||
import org.sonar.db.DbClient; | |||
@@ -43,15 +44,21 @@ public class SourceService { | |||
/** | |||
* Returns a range of lines as raw db data. User permission is not verified. | |||
* @param from starts from 1 | |||
* | |||
* @param from starts from 1 | |||
* @param toInclusive starts from 1, must be greater than or equal param {@code from} | |||
*/ | |||
public Optional<Iterable<DbFileSources.Line>> getLines(DbSession dbSession, String fileUuid, int from, int toInclusive) { | |||
return getLines(dbSession, fileUuid, from, toInclusive, Function.identity()); | |||
} | |||
public Optional<Iterable<DbFileSources.Line>> getLines(DbSession dbSession, String fileUuid, Set<Integer> lines) { | |||
return getLines(dbSession, fileUuid, lines, Function.identity()); | |||
} | |||
/** | |||
* Returns a range of lines as raw text. | |||
* | |||
* @see #getLines(DbSession, String, int, int) | |||
*/ | |||
public Optional<Iterable<String>> getLinesAsRawText(DbSession dbSession, String fileUuid, int from, int toInclusive) { | |||
@@ -76,6 +83,17 @@ public class SourceService { | |||
.collect(MoreCollectors.toList())); | |||
} | |||
private <E> Optional<Iterable<E>> getLines(DbSession dbSession, String fileUuid, Set<Integer> lines, Function<DbFileSources.Line, E> function) { | |||
FileSourceDto dto = dbClient.fileSourceDao().selectByFileUuid(dbSession, fileUuid); | |||
if (dto == null) { | |||
return Optional.empty(); | |||
} | |||
return Optional.of(dto.getSourceData().getLinesList().stream() | |||
.filter(line -> line.hasLine() && lines.contains(line.getLine())) | |||
.map(function) | |||
.collect(MoreCollectors.toList())); | |||
} | |||
private static void verifyLine(int line) { | |||
checkArgument(line >= 1, String.format("Line number must start at 1, got %d", line)); | |||
} |
@@ -0,0 +1,178 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.source.ws; | |||
import com.google.common.io.Resources; | |||
import java.util.Collections; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.TreeSet; | |||
import java.util.function.Supplier; | |||
import java.util.stream.Collectors; | |||
import java.util.stream.IntStream; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.api.utils.text.JsonWriter; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.component.SnapshotDto; | |||
import org.sonar.db.issue.IssueDto; | |||
import org.sonar.db.protobuf.DbCommons; | |||
import org.sonar.db.protobuf.DbFileSources; | |||
import org.sonar.db.protobuf.DbIssues; | |||
import org.sonar.server.component.ws.ComponentViewerJsonWriter; | |||
import org.sonar.server.issue.IssueFinder; | |||
import org.sonar.server.source.SourceService; | |||
import org.sonar.server.user.UserSession; | |||
public class IssueSnippetsAction implements SourcesWsAction { | |||
private final IssueFinder issueFinder; | |||
private final LinesJsonWriter linesJsonWriter; | |||
private final ComponentViewerJsonWriter componentViewerJsonWriter; | |||
private final SourceService sourceService; | |||
private final DbClient dbClient; | |||
public IssueSnippetsAction(SourceService sourceService, DbClient dbClient, IssueFinder issueFinder, LinesJsonWriter linesJsonWriter, | |||
ComponentViewerJsonWriter componentViewerJsonWriter) { | |||
this.sourceService = sourceService; | |||
this.dbClient = dbClient; | |||
this.issueFinder = issueFinder; | |||
this.linesJsonWriter = linesJsonWriter; | |||
this.componentViewerJsonWriter = componentViewerJsonWriter; | |||
} | |||
@Override | |||
public void define(WebService.NewController controller) { | |||
WebService.NewAction action = controller.createAction("issue_snippets") | |||
.setDescription("Get code snipets involved in an issue. Requires 'Browse' permission on the project<br/>") | |||
.setSince("7.8") | |||
.setInternal(true) | |||
.setResponseExample(Resources.getResource(getClass(), "example-show.json")) | |||
.setHandler(this); | |||
action | |||
.createParam("issueKey") | |||
.setRequired(true) | |||
.setDescription("Issue key") | |||
.setExampleValue("AU-Tpxb--iU5OvuD2FLy"); | |||
} | |||
@Override | |||
public void handle(Request request, Response response) throws Exception { | |||
String issueKey = request.mandatoryParam("issueKey"); | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
IssueDto issueDto = issueFinder.getByKey(dbSession, issueKey); | |||
Map<String, TreeSet<Integer>> linesPerComponent; | |||
Map<String, ComponentDto> componentsByUuid; | |||
DbIssues.Locations locations = issueDto.parseLocations(); | |||
if (locations != null && issueDto.getComponentUuid() != null) { | |||
linesPerComponent = getLinesPerComponent(issueDto.getComponentUuid(), locations); | |||
componentsByUuid = dbClient.componentDao().selectByUuids(dbSession, linesPerComponent.keySet()) | |||
.stream().collect(Collectors.toMap(ComponentDto::uuid, c -> c)); | |||
} else { | |||
componentsByUuid = Collections.emptyMap(); | |||
linesPerComponent = Collections.emptyMap(); | |||
} | |||
try (JsonWriter jsonWriter = response.newJsonWriter()) { | |||
jsonWriter.beginObject(); | |||
for (Map.Entry<String, TreeSet<Integer>> e : linesPerComponent.entrySet()) { | |||
ComponentDto componentDto = componentsByUuid.get(e.getKey()); | |||
if (componentDto != null) { | |||
processComponent(dbSession, jsonWriter, componentDto, e.getKey(), e.getValue()); | |||
} | |||
} | |||
jsonWriter.endObject(); | |||
} | |||
} | |||
} | |||
private void processComponent(DbSession dbSession, JsonWriter writer, ComponentDto componentDto, String fileUuid, Set<Integer> lines) { | |||
Optional<Iterable<DbFileSources.Line>> lineSourcesOpt = sourceService.getLines(dbSession, fileUuid, lines); | |||
if (!lineSourcesOpt.isPresent()) { | |||
return; | |||
} | |||
Supplier<Optional<Long>> periodDateSupplier = () -> dbClient.snapshotDao() | |||
.selectLastAnalysisByComponentUuid(dbSession, componentDto.projectUuid()) | |||
.map(SnapshotDto::getPeriodDate); | |||
Iterable<DbFileSources.Line> lineSources = lineSourcesOpt.get(); | |||
writer.name(componentDto.getKey()).beginObject(); | |||
writer.name("component").beginObject(); | |||
componentViewerJsonWriter.writeComponentWithoutFav(writer, componentDto, dbSession, false); | |||
componentViewerJsonWriter.writeMeasures(writer, componentDto, dbSession); | |||
writer.endObject(); | |||
linesJsonWriter.writeSource(lineSources, writer, true, periodDateSupplier); | |||
writer.endObject(); | |||
} | |||
private Map<String, TreeSet<Integer>> getLinesPerComponent(String componentUuid, DbIssues.Locations locations) { | |||
Map<String, TreeSet<Integer>> linesPerComponent = new HashMap<>(); | |||
if (locations.hasTextRange()) { | |||
// extra lines for the main location | |||
addTextRange(linesPerComponent, componentUuid, locations.getTextRange(), 9); | |||
} | |||
for (DbIssues.Flow flow : locations.getFlowList()) { | |||
for (DbIssues.Location l : flow.getLocationList()) { | |||
if (l.hasComponentId()) { | |||
addTextRange(linesPerComponent, l.getComponentId(), l.getTextRange(), 2); | |||
} else { | |||
addTextRange(linesPerComponent, componentUuid, l.getTextRange(), 2); | |||
} | |||
} | |||
} | |||
return linesPerComponent; | |||
} | |||
private static void addTextRange(Map<String, TreeSet<Integer>> linesPerComponent, String componentUuid, | |||
DbCommons.TextRange textRange, int numLinesAfterIssue) { | |||
int start = textRange.getStartLine() - 2; | |||
int end = textRange.getEndLine() + numLinesAfterIssue; | |||
TreeSet<Integer> lines = linesPerComponent.computeIfAbsent(componentUuid, c -> new TreeSet<>()); | |||
IntStream.rangeClosed(start, end).forEach(lines::add); | |||
// If two snippets in the same component are 3 lines apart of each other, include those 3 lines. | |||
Integer closestToStart = lines.lower(start); | |||
if (closestToStart != null && closestToStart >= start - 4) { | |||
IntStream.range(closestToStart + 1, start).forEach(lines::add); | |||
} | |||
Integer closestToEnd = lines.higher(end); | |||
if (closestToEnd != null && closestToEnd <= end + 4) { | |||
IntStream.range(end + 1, closestToEnd).forEach(lines::add); | |||
} | |||
} | |||
} |
@@ -21,14 +21,12 @@ package org.sonar.server.source.ws; | |||
import com.google.common.base.MoreObjects; | |||
import com.google.common.io.Resources; | |||
import java.util.Date; | |||
import java.util.Optional; | |||
import java.util.function.Supplier; | |||
import org.sonar.api.server.ws.Change; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.api.utils.DateUtils; | |||
import org.sonar.api.utils.text.JsonWriter; | |||
import org.sonar.api.web.UserRole; | |||
import org.sonar.db.DbClient; | |||
@@ -38,7 +36,6 @@ import org.sonar.db.component.SnapshotDto; | |||
import org.sonar.db.organization.OrganizationDto; | |||
import org.sonar.db.protobuf.DbFileSources; | |||
import org.sonar.server.component.ComponentFinder; | |||
import org.sonar.server.source.HtmlSourceDecorator; | |||
import org.sonar.server.source.SourceService; | |||
import org.sonar.server.user.UserSession; | |||
@@ -61,15 +58,15 @@ public class LinesAction implements SourcesWsAction { | |||
private final ComponentFinder componentFinder; | |||
private final SourceService sourceService; | |||
private final HtmlSourceDecorator htmlSourceDecorator; | |||
private final LinesJsonWriter linesJsonWriter; | |||
private final DbClient dbClient; | |||
private final UserSession userSession; | |||
public LinesAction(ComponentFinder componentFinder, DbClient dbClient, SourceService sourceService, | |||
HtmlSourceDecorator htmlSourceDecorator, UserSession userSession) { | |||
LinesJsonWriter linesJsonWriter, UserSession userSession) { | |||
this.componentFinder = componentFinder; | |||
this.sourceService = sourceService; | |||
this.htmlSourceDecorator = htmlSourceDecorator; | |||
this.linesJsonWriter = linesJsonWriter; | |||
this.dbClient = dbClient; | |||
this.userSession = userSession; | |||
} | |||
@@ -77,7 +74,7 @@ public class LinesAction implements SourcesWsAction { | |||
@Override | |||
public void define(WebService.NewController controller) { | |||
WebService.NewAction action = controller.createAction("lines") | |||
.setDescription("Show source code with line oriented info. Require See Source Code permission on file's project<br/>" + | |||
.setDescription("Show source code with line oriented info. Requires See Source Code permission on file's project<br/>" + | |||
"Each element of the result array is an object which contains:" + | |||
"<ol>" + | |||
"<li>Line number</li>" + | |||
@@ -153,7 +150,7 @@ public class LinesAction implements SourcesWsAction { | |||
Iterable<DbFileSources.Line> lines = checkFoundWithOptional(sourceService.getLines(dbSession, file.uuid(), from, to), "No source found for file '%s'", file.getDbKey()); | |||
try (JsonWriter json = response.newJsonWriter()) { | |||
json.beginObject(); | |||
writeSource(lines, json, isMemberOfOrganization(dbSession, file), periodDateSupplier); | |||
linesJsonWriter.writeSource(lines, json, isMemberOfOrganization(dbSession, file), periodDateSupplier); | |||
json.endObject(); | |||
} | |||
} | |||
@@ -179,88 +176,4 @@ public class LinesAction implements SourcesWsAction { | |||
checkRequest(componentKey != null, "The '%s' parameter is missing", PARAM_KEY); | |||
return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, branch, pullRequest); | |||
} | |||
private void writeSource(Iterable<DbFileSources.Line> lines, JsonWriter json, boolean filterScmAuthors, Supplier<Optional<Long>> periodDateSupplier) { | |||
Optional<Long> periodDate = null; | |||
json.name("sources").beginArray(); | |||
for (DbFileSources.Line line : lines) { | |||
json.beginObject() | |||
.prop("line", line.getLine()) | |||
.prop("code", htmlSourceDecorator.getDecoratedSourceAsHtml(line.getSource(), line.getHighlighting(), line.getSymbols())) | |||
.prop("scmRevision", line.getScmRevision()); | |||
if (!filterScmAuthors) { | |||
json.prop("scmAuthor", line.getScmAuthor()); | |||
} | |||
if (line.hasScmDate()) { | |||
json.prop("scmDate", DateUtils.formatDateTime(new Date(line.getScmDate()))); | |||
} | |||
Optional<Integer> lineHits = getLineHits(line); | |||
if (lineHits.isPresent()) { | |||
json.prop("utLineHits", lineHits.get()); | |||
json.prop("lineHits", lineHits.get()); | |||
} | |||
Optional<Integer> conditions = getConditions(line); | |||
if (conditions.isPresent()) { | |||
json.prop("utConditions", conditions.get()); | |||
json.prop("conditions", conditions.get()); | |||
} | |||
Optional<Integer> coveredConditions = getCoveredConditions(line); | |||
if (coveredConditions.isPresent()) { | |||
json.prop("utCoveredConditions", coveredConditions.get()); | |||
json.prop("coveredConditions", coveredConditions.get()); | |||
} | |||
json.prop("duplicated", line.getDuplicationCount() > 0); | |||
if (line.hasIsNewLine()) { | |||
json.prop("isNew", line.getIsNewLine()); | |||
} else { | |||
if (periodDate == null) { | |||
periodDate = periodDateSupplier.get(); | |||
} | |||
json.prop("isNew", periodDate.isPresent() && line.getScmDate() > periodDate.get()); | |||
} | |||
json.endObject(); | |||
} | |||
json.endArray(); | |||
} | |||
private static Optional<Integer> getLineHits(DbFileSources.Line line) { | |||
if (line.hasLineHits()) { | |||
return Optional.of(line.getLineHits()); | |||
} else if (line.hasDeprecatedOverallLineHits()) { | |||
return Optional.of(line.getDeprecatedOverallLineHits()); | |||
} else if (line.hasDeprecatedUtLineHits()) { | |||
return Optional.of(line.getDeprecatedUtLineHits()); | |||
} else if (line.hasDeprecatedItLineHits()) { | |||
return Optional.of(line.getDeprecatedItLineHits()); | |||
} | |||
return Optional.empty(); | |||
} | |||
private static Optional<Integer> getConditions(DbFileSources.Line line) { | |||
if (line.hasConditions()) { | |||
return Optional.of(line.getConditions()); | |||
} else if (line.hasDeprecatedOverallConditions()) { | |||
return Optional.of(line.getDeprecatedOverallConditions()); | |||
} else if (line.hasDeprecatedUtConditions()) { | |||
return Optional.of(line.getDeprecatedUtConditions()); | |||
} else if (line.hasDeprecatedItConditions()) { | |||
return Optional.of(line.getDeprecatedItConditions()); | |||
} | |||
return Optional.empty(); | |||
} | |||
private static Optional<Integer> getCoveredConditions(DbFileSources.Line line) { | |||
if (line.hasCoveredConditions()) { | |||
return Optional.of(line.getCoveredConditions()); | |||
} else if (line.hasDeprecatedOverallCoveredConditions()) { | |||
return Optional.of(line.getDeprecatedOverallCoveredConditions()); | |||
} else if (line.hasDeprecatedUtCoveredConditions()) { | |||
return Optional.of(line.getDeprecatedUtCoveredConditions()); | |||
} else if (line.hasDeprecatedItCoveredConditions()) { | |||
return Optional.of(line.getDeprecatedItCoveredConditions()); | |||
} | |||
return Optional.empty(); | |||
} | |||
} |
@@ -0,0 +1,119 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.source.ws; | |||
import java.util.Date; | |||
import java.util.Optional; | |||
import java.util.function.Supplier; | |||
import org.sonar.api.utils.DateUtils; | |||
import org.sonar.api.utils.text.JsonWriter; | |||
import org.sonar.db.protobuf.DbFileSources; | |||
import org.sonar.server.source.HtmlSourceDecorator; | |||
public class LinesJsonWriter { | |||
private final HtmlSourceDecorator htmlSourceDecorator; | |||
public LinesJsonWriter(HtmlSourceDecorator htmlSourceDecorator) { | |||
this.htmlSourceDecorator = htmlSourceDecorator; | |||
} | |||
public void writeSource(Iterable<DbFileSources.Line> lines, JsonWriter json, boolean filterScmAuthors, Supplier<Optional<Long>> periodDateSupplier) { | |||
Optional<Long> periodDate = null; | |||
json.name("sources").beginArray(); | |||
for (DbFileSources.Line line : lines) { | |||
json.beginObject() | |||
.prop("line", line.getLine()) | |||
.prop("code", htmlSourceDecorator.getDecoratedSourceAsHtml(line.getSource(), line.getHighlighting(), line.getSymbols())) | |||
.prop("scmRevision", line.getScmRevision()); | |||
if (!filterScmAuthors) { | |||
json.prop("scmAuthor", line.getScmAuthor()); | |||
} | |||
if (line.hasScmDate()) { | |||
json.prop("scmDate", DateUtils.formatDateTime(new Date(line.getScmDate()))); | |||
} | |||
Optional<Integer> lineHits = getLineHits(line); | |||
if (lineHits.isPresent()) { | |||
json.prop("utLineHits", lineHits.get()); | |||
json.prop("lineHits", lineHits.get()); | |||
} | |||
Optional<Integer> conditions = getConditions(line); | |||
if (conditions.isPresent()) { | |||
json.prop("utConditions", conditions.get()); | |||
json.prop("conditions", conditions.get()); | |||
} | |||
Optional<Integer> coveredConditions = getCoveredConditions(line); | |||
if (coveredConditions.isPresent()) { | |||
json.prop("utCoveredConditions", coveredConditions.get()); | |||
json.prop("coveredConditions", coveredConditions.get()); | |||
} | |||
json.prop("duplicated", line.getDuplicationCount() > 0); | |||
if (line.hasIsNewLine()) { | |||
json.prop("isNew", line.getIsNewLine()); | |||
} else { | |||
if (periodDate == null) { | |||
periodDate = periodDateSupplier.get(); | |||
} | |||
json.prop("isNew", periodDate.isPresent() && line.getScmDate() > periodDate.get()); | |||
} | |||
json.endObject(); | |||
} | |||
json.endArray(); | |||
} | |||
private static Optional<Integer> getLineHits(DbFileSources.Line line) { | |||
if (line.hasLineHits()) { | |||
return Optional.of(line.getLineHits()); | |||
} else if (line.hasDeprecatedOverallLineHits()) { | |||
return Optional.of(line.getDeprecatedOverallLineHits()); | |||
} else if (line.hasDeprecatedUtLineHits()) { | |||
return Optional.of(line.getDeprecatedUtLineHits()); | |||
} else if (line.hasDeprecatedItLineHits()) { | |||
return Optional.of(line.getDeprecatedItLineHits()); | |||
} | |||
return Optional.empty(); | |||
} | |||
private static Optional<Integer> getConditions(DbFileSources.Line line) { | |||
if (line.hasConditions()) { | |||
return Optional.of(line.getConditions()); | |||
} else if (line.hasDeprecatedOverallConditions()) { | |||
return Optional.of(line.getDeprecatedOverallConditions()); | |||
} else if (line.hasDeprecatedUtConditions()) { | |||
return Optional.of(line.getDeprecatedUtConditions()); | |||
} else if (line.hasDeprecatedItConditions()) { | |||
return Optional.of(line.getDeprecatedItConditions()); | |||
} | |||
return Optional.empty(); | |||
} | |||
private static Optional<Integer> getCoveredConditions(DbFileSources.Line line) { | |||
if (line.hasCoveredConditions()) { | |||
return Optional.of(line.getCoveredConditions()); | |||
} else if (line.hasDeprecatedOverallCoveredConditions()) { | |||
return Optional.of(line.getDeprecatedOverallCoveredConditions()); | |||
} else if (line.hasDeprecatedUtCoveredConditions()) { | |||
return Optional.of(line.getDeprecatedUtCoveredConditions()); | |||
} else if (line.hasDeprecatedItCoveredConditions()) { | |||
return Optional.of(line.getDeprecatedItCoveredConditions()); | |||
} | |||
return Optional.empty(); | |||
} | |||
} |
@@ -52,7 +52,7 @@ public class ShowAction implements SourcesWsAction { | |||
@Override | |||
public void define(WebService.NewController controller) { | |||
WebService.NewAction action = controller.createAction("show") | |||
.setDescription("Get source code. Require See Source Code permission on file's project<br/>" + | |||
.setDescription("Get source code. Requires See Source Code permission on file's project<br/>" + | |||
"Each element of the result array is composed of:" + | |||
"<ol>" + | |||
"<li>Line number</li>" + |
@@ -55,7 +55,10 @@ public class AppActionTest { | |||
@Rule | |||
public DbTester db = DbTester.create(); | |||
private WsActionTester ws = new WsActionTester(new AppAction(db.getDbClient(), userSession, TestComponentFinder.from(db))); | |||
private ComponentViewerJsonWriter componentViewerJsonWriter = new ComponentViewerJsonWriter(db.getDbClient()); | |||
private WsActionTester ws = new WsActionTester(new AppAction(db.getDbClient(), userSession, | |||
TestComponentFinder.from(db), componentViewerJsonWriter)); | |||
@Test | |||
public void file_info() { |
@@ -20,9 +20,11 @@ | |||
package org.sonar.server.source; | |||
import com.google.common.collect.Lists; | |||
import java.io.IOException; | |||
import java.util.Arrays; | |||
import java.util.HashSet; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
@@ -52,7 +54,7 @@ public class SourceServiceTest { | |||
SourceService underTest = new SourceService(dbTester.getDbClient(), htmlDecorator); | |||
@Before | |||
public void injectFakeLines() throws IOException { | |||
public void injectFakeLines() { | |||
FileSourceDto dto = new FileSourceDto(); | |||
dto.setFileUuid(FILE_UUID).setProjectUuid("PROJECT_UUID"); | |||
dto.setSourceData(FileSourceTesting.newFakeData(10).build()); | |||
@@ -62,6 +64,18 @@ public class SourceServiceTest { | |||
@Test | |||
public void get_range_of_lines() { | |||
Set<Integer> lineNumbers = new HashSet<>(Arrays.asList(1, 5, 6)); | |||
Optional<Iterable<DbFileSources.Line>> linesOpt = underTest.getLines(dbTester.getSession(), FILE_UUID, lineNumbers); | |||
assertThat(linesOpt.isPresent()).isTrue(); | |||
List<DbFileSources.Line> lines = Lists.newArrayList(linesOpt.get()); | |||
assertThat(lines).hasSize(3); | |||
assertThat(lines.get(0).getLine()).isEqualTo(1); | |||
assertThat(lines.get(1).getLine()).isEqualTo(5); | |||
assertThat(lines.get(2).getLine()).isEqualTo(6); | |||
} | |||
@Test | |||
public void get_set_of_lines() { | |||
Optional<Iterable<DbFileSources.Line>> linesOpt = underTest.getLines(dbTester.getSession(), FILE_UUID, 5, 7); | |||
assertThat(linesOpt.isPresent()).isTrue(); | |||
List<DbFileSources.Line> lines = Lists.newArrayList(linesOpt.get()); |
@@ -0,0 +1,246 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.source.ws; | |||
import java.util.Arrays; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import org.mockito.stubbing.Answer; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.issue.IssueDto; | |||
import org.sonar.db.metric.MetricDto; | |||
import org.sonar.db.organization.OrganizationDto; | |||
import org.sonar.db.protobuf.DbCommons; | |||
import org.sonar.db.protobuf.DbFileSources; | |||
import org.sonar.db.protobuf.DbIssues; | |||
import org.sonar.db.rule.RuleDefinitionDto; | |||
import org.sonar.db.source.FileSourceTester; | |||
import org.sonar.server.component.ws.ComponentViewerJsonWriter; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.issue.IssueFinder; | |||
import org.sonar.server.source.HtmlSourceDecorator; | |||
import org.sonar.server.source.SourceService; | |||
import org.sonar.server.source.index.FileSourceTesting; | |||
import org.sonar.server.tester.UserSessionRule; | |||
import org.sonar.server.ws.TestResponse; | |||
import org.sonar.server.ws.WsActionTester; | |||
import static org.mockito.ArgumentMatchers.anyString; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES_DENSITY_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.LINES_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.TECHNICAL_DEBT_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.TESTS_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.VIOLATIONS_KEY; | |||
import static org.sonar.api.web.UserRole.USER; | |||
import static org.sonar.db.component.ComponentTesting.newFileDto; | |||
public class IssueSnippetsActionTest { | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
@Rule | |||
public DbTester db = DbTester.create(System2.INSTANCE); | |||
@Rule | |||
public UserSessionRule userSession = UserSessionRule.standalone(); | |||
private DbClient dbClient = db.getDbClient(); | |||
private FileSourceTester fileSourceTester = new FileSourceTester(db); | |||
private ComponentDto project; | |||
private WsActionTester actionTester; | |||
@Before | |||
public void setUp() { | |||
OrganizationDto organization = db.organizations().insert(); | |||
project = db.components().insertPrivateProject(organization, "projectUuid"); | |||
HtmlSourceDecorator htmlSourceDecorator = mock(HtmlSourceDecorator.class); | |||
when(htmlSourceDecorator.getDecoratedSourceAsHtml(anyString(), anyString(), anyString())).then((Answer<String>) | |||
invocationOnMock -> "<p>" + invocationOnMock.getArguments()[0] + "</p>"); | |||
LinesJsonWriter linesJsonWriter = new LinesJsonWriter(htmlSourceDecorator); | |||
IssueFinder issueFinder = new IssueFinder(dbClient, userSession); | |||
ComponentViewerJsonWriter componentViewerJsonWriter = new ComponentViewerJsonWriter(dbClient); | |||
SourceService sourceService = new SourceService(dbClient, htmlSourceDecorator); | |||
actionTester = new WsActionTester(new IssueSnippetsAction(sourceService, dbClient, issueFinder, linesJsonWriter, componentViewerJsonWriter)); | |||
} | |||
@Test | |||
public void should_display_single_location_single_file() { | |||
ComponentDto file = insertFile(project, "file"); | |||
DbFileSources.Data fileSources = FileSourceTesting.newFakeData(10).build(); | |||
fileSourceTester.insertFileSource(file, 10, dto -> dto.setSourceData(fileSources)); | |||
userSession.logIn().addProjectPermission(USER, project, file); | |||
String issueKey = insertIssue(file, newLocation(file.uuid(), 5, 5)); | |||
TestResponse response = actionTester.newRequest().setParam("issueKey", issueKey).execute(); | |||
response.assertJson(getClass(), "issue_snippets_single_location.json"); | |||
} | |||
@Test | |||
public void should_add_measures_to_components() { | |||
ComponentDto file = insertFile(project, "file"); | |||
MetricDto lines = db.measures().insertMetric(m -> m.setKey(LINES_KEY)); | |||
db.measures().insertLiveMeasure(file, lines, m -> m.setValue(200d)); | |||
MetricDto duplicatedLines = db.measures().insertMetric(m -> m.setKey(DUPLICATED_LINES_DENSITY_KEY)); | |||
db.measures().insertLiveMeasure(file, duplicatedLines, m -> m.setValue(7.4)); | |||
MetricDto tests = db.measures().insertMetric(m -> m.setKey(TESTS_KEY)); | |||
db.measures().insertLiveMeasure(file, tests, m -> m.setValue(3d)); | |||
MetricDto technicalDebt = db.measures().insertMetric(m -> m.setKey(TECHNICAL_DEBT_KEY)); | |||
db.measures().insertLiveMeasure(file, technicalDebt, m -> m.setValue(182d)); | |||
MetricDto issues = db.measures().insertMetric(m -> m.setKey(VIOLATIONS_KEY)); | |||
db.measures().insertLiveMeasure(file, issues, m -> m.setValue(231d)); | |||
MetricDto coverage = db.measures().insertMetric(m -> m.setKey(COVERAGE_KEY)); | |||
db.measures().insertLiveMeasure(file, coverage, m -> m.setValue(95.4d)); | |||
DbFileSources.Data fileSources = FileSourceTesting.newFakeData(10).build(); | |||
fileSourceTester.insertFileSource(file, 10, dto -> dto.setSourceData(fileSources)); | |||
userSession.logIn().addProjectPermission(USER, project, file); | |||
String issueKey = insertIssue(file, newLocation(file.uuid(), 5, 5)); | |||
TestResponse response = actionTester.newRequest().setParam("issueKey", issueKey).execute(); | |||
response.assertJson(getClass(), "issue_snippets_with_measures.json"); | |||
} | |||
@Test | |||
public void issue_references_a_non_existing_component() { | |||
ComponentDto file = insertFile(project, "file"); | |||
ComponentDto file2 = newFileDto(project, null, "nonexisting"); | |||
DbFileSources.Data fileSources = FileSourceTesting.newFakeData(10).build(); | |||
fileSourceTester.insertFileSource(file, 10, dto -> dto.setSourceData(fileSources)); | |||
userSession.logIn().addProjectPermission(USER, project, file); | |||
String issueKey = insertIssue(file, newLocation(file2.uuid(), 5, 5)); | |||
TestResponse response = actionTester.newRequest().setParam("issueKey", issueKey).execute(); | |||
response.assertJson("{}"); | |||
} | |||
@Test | |||
public void no_code_to_display() { | |||
ComponentDto file = insertFile(project, "file"); | |||
userSession.logIn().addProjectPermission(USER, project, file); | |||
String issueKey = insertIssue(file, newLocation(file.uuid(), 5, 5)); | |||
TestResponse response = actionTester.newRequest().setParam("issueKey", issueKey).execute(); | |||
response.assertJson("{}"); | |||
} | |||
@Test | |||
public void fail_if_no_project_permission() { | |||
ComponentDto file = insertFile(project, "file"); | |||
String issueKey = insertIssue(file, newLocation(file.uuid(), 5, 5)); | |||
expectedException.expect(ForbiddenException.class); | |||
actionTester.newRequest().setParam("issueKey", issueKey).execute(); | |||
} | |||
@Test | |||
public void fail_if_issue_not_found() { | |||
ComponentDto file = insertFile(project, "file"); | |||
insertIssue(file, newLocation(file.uuid(), 5, 5)); | |||
userSession.logIn().addProjectPermission(USER, project, file); | |||
expectedException.expect(NotFoundException.class); | |||
expectedException.expectMessage("Issue with key 'invalid' does not exist"); | |||
actionTester.newRequest().setParam("issueKey", "invalid").execute(); | |||
} | |||
@Test | |||
public void fail_if_parameter_missing() { | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("The 'issueKey' parameter is missing"); | |||
actionTester.newRequest().execute(); | |||
} | |||
@Test | |||
public void should_display_multiple_locations_multiple_files() { | |||
ComponentDto file1 = insertFile(project, "file1"); | |||
ComponentDto file2 = insertFile(project, "file2"); | |||
DbFileSources.Data fileSources = FileSourceTesting.newFakeData(10).build(); | |||
fileSourceTester.insertFileSource(file1, 10, dto -> dto.setSourceData(fileSources)); | |||
fileSourceTester.insertFileSource(file2, 10, dto -> dto.setSourceData(fileSources)); | |||
userSession.logIn().addProjectPermission(USER, project, file1, file2); | |||
String issueKey1 = insertIssue(file1, newLocation(file1.uuid(), 5, 5), | |||
newLocation(file1.uuid(), 9, 9), newLocation(file2.uuid(), 1, 5)); | |||
TestResponse response = actionTester.newRequest().setParam("issueKey", issueKey1).execute(); | |||
response.assertJson(getClass(), "issue_snippets_multiple_locations.json"); | |||
} | |||
@Test | |||
public void should_connect_snippets_close_to_each_other() { | |||
ComponentDto file1 = insertFile(project, "file1"); | |||
DbFileSources.Data fileSources = FileSourceTesting.newFakeData(20).build(); | |||
fileSourceTester.insertFileSource(file1, 20, dto -> dto.setSourceData(fileSources)); | |||
userSession.logIn().addProjectPermission(USER, project, file1); | |||
// these two locations should get connected, making a single range 3-14 | |||
String issueKey1 = insertIssue(file1, newLocation(file1.uuid(), 5, 5), | |||
newLocation(file1.uuid(), 12, 12)); | |||
TestResponse response = actionTester.newRequest().setParam("issueKey", issueKey1).execute(); | |||
response.assertJson(getClass(), "issue_snippets_close_to_each_other.json"); | |||
} | |||
private DbIssues.Location newLocation(String fileUuid, int startLine, int endLine) { | |||
return DbIssues.Location.newBuilder() | |||
.setTextRange(DbCommons.TextRange.newBuilder().setStartLine(startLine).setEndLine(endLine).build()) | |||
.setComponentId(fileUuid) | |||
.build(); | |||
} | |||
private ComponentDto insertFile(ComponentDto project, String name) { | |||
ComponentDto file = newFileDto(project, null, name); | |||
db.components().insertComponents(file); | |||
return file; | |||
} | |||
private String insertIssue(ComponentDto file, DbIssues.Location... locations) { | |||
RuleDefinitionDto rule = db.rules().insert(); | |||
DbIssues.Flow flow = DbIssues.Flow.newBuilder().addAllLocation(Arrays.asList(locations)).build(); | |||
IssueDto issue = db.issues().insert(rule, project, file, i -> { | |||
i.setLocations(DbIssues.Locations.newBuilder().addFlow(flow).build()); | |||
i.setProjectUuid(project.uuid()); | |||
i.setLine(locations[0].getTextRange().getStartLine()); | |||
}); | |||
db.commit(); | |||
return issue.getKey(); | |||
} | |||
} |
@@ -73,9 +73,10 @@ public class LinesActionTest { | |||
HtmlSourceDecorator htmlSourceDecorator = mock(HtmlSourceDecorator.class); | |||
when(htmlSourceDecorator.getDecoratedSourceAsHtml(anyString(), anyString(), anyString())).then((Answer<String>) | |||
invocationOnMock -> "<p>" + invocationOnMock.getArguments()[0] + "</p>"); | |||
LinesJsonWriter linesJsonWriter = new LinesJsonWriter(htmlSourceDecorator); | |||
SourceService sourceService = new SourceService(db.getDbClient(), htmlSourceDecorator); | |||
wsTester = new WsTester(new SourcesWs( | |||
new LinesAction(TestComponentFinder.from(db), db.getDbClient(), sourceService, htmlSourceDecorator, userSession))); | |||
new LinesAction(TestComponentFinder.from(db), db.getDbClient(), sourceService, linesJsonWriter, userSession))); | |||
organization = db.organizations().insert(); | |||
privateProject = ComponentTesting.newPrivateProjectDto(organization); | |||
} |
@@ -24,7 +24,8 @@ import org.junit.Test; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.server.component.ComponentFinder; | |||
import org.sonar.server.source.HtmlSourceDecorator; | |||
import org.sonar.server.component.ws.ComponentViewerJsonWriter; | |||
import org.sonar.server.issue.IssueFinder; | |||
import org.sonar.server.source.SourceService; | |||
import org.sonar.server.tester.UserSessionRule; | |||
import org.sonar.server.ws.WsTester; | |||
@@ -38,9 +39,11 @@ public class SourcesWsTest { | |||
ShowAction showAction = new ShowAction(mock(SourceService.class), mock(DbClient.class), userSessionRule, mock(ComponentFinder.class)); | |||
RawAction rawAction = new RawAction(mock(DbClient.class), mock(SourceService.class), userSessionRule, mock(ComponentFinder.class)); | |||
LinesAction linesAction = new LinesAction(mock(ComponentFinder.class), mock(DbClient.class), mock(SourceService.class), mock(HtmlSourceDecorator.class), userSessionRule); | |||
LinesAction linesAction = new LinesAction(mock(ComponentFinder.class), mock(DbClient.class), mock(SourceService.class), mock(LinesJsonWriter.class), userSessionRule); | |||
HashAction hashAction = new HashAction(mock(DbClient.class), userSessionRule, mock(ComponentFinder.class)); | |||
WsTester tester = new WsTester(new SourcesWs(showAction, rawAction, linesAction, hashAction)); | |||
IssueSnippetsAction issueSnippetsAction = new IssueSnippetsAction(mock(SourceService.class), mock(DbClient.class), userSessionRule, mock(IssueFinder.class), | |||
mock(LinesJsonWriter.class), mock(ComponentViewerJsonWriter.class)); | |||
WsTester tester = new WsTester(new SourcesWs(showAction, rawAction, linesAction, hashAction, issueSnippetsAction)); | |||
@Test | |||
public void define_ws() { | |||
@@ -48,7 +51,7 @@ public class SourcesWsTest { | |||
assertThat(controller).isNotNull(); | |||
assertThat(controller.since()).isEqualTo("4.2"); | |||
assertThat(controller.description()).isNotEmpty(); | |||
assertThat(controller.actions()).hasSize(4); | |||
assertThat(controller.actions()).hasSize(5); | |||
WebService.Action show = controller.action("show"); | |||
assertThat(show).isNotNull(); | |||
@@ -81,5 +84,13 @@ public class SourcesWsTest { | |||
assertThat(hash.isInternal()).isTrue(); | |||
assertThat(hash.responseExampleAsString()).isNotEmpty(); | |||
assertThat(hash.params()).hasSize(1); | |||
WebService.Action issueSnippets = controller.action("issue_snippets"); | |||
assertThat(issueSnippets).isNotNull(); | |||
assertThat(issueSnippets.handler()).isSameAs(issueSnippetsAction); | |||
assertThat(issueSnippets.since()).isEqualTo("7.8"); | |||
assertThat(issueSnippets.isInternal()).isTrue(); | |||
assertThat(issueSnippets.responseExampleAsString()).isNotEmpty(); | |||
assertThat(issueSnippets.params()).hasSize(1); | |||
} | |||
} |
@@ -0,0 +1,185 @@ | |||
{ | |||
"FILE_KEY_file1": { | |||
"component": { | |||
"key": "FILE_KEY_file1", | |||
"uuid": "file1", | |||
"path": "null/NAME_file1", | |||
"name": "NAME_file1", | |||
"longName": "null/NAME_file1", | |||
"q": "FIL", | |||
"project": "KEY_projectUuid", | |||
"projectName": "LONG_NAME_projectUuid", | |||
"measures": {} | |||
}, | |||
"sources": [ | |||
{ | |||
"line": 3, | |||
"code": "\u003cp\u003eSOURCE_3\u003c/p\u003e", | |||
"scmRevision": "REVISION_3", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 3, | |||
"lineHits": 3, | |||
"utConditions": 4, | |||
"conditions": 4, | |||
"utCoveredConditions": 5, | |||
"coveredConditions": 5, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 4, | |||
"code": "\u003cp\u003eSOURCE_4\u003c/p\u003e", | |||
"scmRevision": "REVISION_4", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 4, | |||
"lineHits": 4, | |||
"utConditions": 5, | |||
"conditions": 5, | |||
"utCoveredConditions": 6, | |||
"coveredConditions": 6, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 5, | |||
"code": "\u003cp\u003eSOURCE_5\u003c/p\u003e", | |||
"scmRevision": "REVISION_5", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 5, | |||
"lineHits": 5, | |||
"utConditions": 6, | |||
"conditions": 6, | |||
"utCoveredConditions": 7, | |||
"coveredConditions": 7, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 6, | |||
"code": "\u003cp\u003eSOURCE_6\u003c/p\u003e", | |||
"scmRevision": "REVISION_6", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 6, | |||
"lineHits": 6, | |||
"utConditions": 7, | |||
"conditions": 7, | |||
"utCoveredConditions": 8, | |||
"coveredConditions": 8, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 7, | |||
"code": "\u003cp\u003eSOURCE_7\u003c/p\u003e", | |||
"scmRevision": "REVISION_7", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 7, | |||
"lineHits": 7, | |||
"utConditions": 8, | |||
"conditions": 8, | |||
"utCoveredConditions": 9, | |||
"coveredConditions": 9, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 8, | |||
"code": "\u003cp\u003eSOURCE_8\u003c/p\u003e", | |||
"scmRevision": "REVISION_8", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 8, | |||
"lineHits": 8, | |||
"utConditions": 9, | |||
"conditions": 9, | |||
"utCoveredConditions": 10, | |||
"coveredConditions": 10, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 9, | |||
"code": "\u003cp\u003eSOURCE_9\u003c/p\u003e", | |||
"scmRevision": "REVISION_9", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 9, | |||
"lineHits": 9, | |||
"utConditions": 10, | |||
"conditions": 10, | |||
"utCoveredConditions": 11, | |||
"coveredConditions": 11, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 10, | |||
"code": "\u003cp\u003eSOURCE_10\u003c/p\u003e", | |||
"scmRevision": "REVISION_10", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 10, | |||
"lineHits": 10, | |||
"utConditions": 11, | |||
"conditions": 11, | |||
"utCoveredConditions": 12, | |||
"coveredConditions": 12, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 11, | |||
"code": "\u003cp\u003eSOURCE_11\u003c/p\u003e", | |||
"scmRevision": "REVISION_11", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 11, | |||
"lineHits": 11, | |||
"utConditions": 12, | |||
"conditions": 12, | |||
"utCoveredConditions": 13, | |||
"coveredConditions": 13, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 12, | |||
"code": "\u003cp\u003eSOURCE_12\u003c/p\u003e", | |||
"scmRevision": "REVISION_12", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 12, | |||
"lineHits": 12, | |||
"utConditions": 13, | |||
"conditions": 13, | |||
"utCoveredConditions": 14, | |||
"coveredConditions": 14, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 13, | |||
"code": "\u003cp\u003eSOURCE_13\u003c/p\u003e", | |||
"scmRevision": "REVISION_13", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 13, | |||
"lineHits": 13, | |||
"utConditions": 14, | |||
"conditions": 14, | |||
"utCoveredConditions": 15, | |||
"coveredConditions": 15, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 14, | |||
"code": "\u003cp\u003eSOURCE_14\u003c/p\u003e", | |||
"scmRevision": "REVISION_14", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 14, | |||
"lineHits": 14, | |||
"utConditions": 15, | |||
"conditions": 15, | |||
"utCoveredConditions": 16, | |||
"coveredConditions": 16, | |||
"duplicated": true, | |||
"isNew": true | |||
} | |||
] | |||
} | |||
} |
@@ -0,0 +1,240 @@ | |||
{ | |||
"FILE_KEY_file2": { | |||
"component": { | |||
"key": "FILE_KEY_file2", | |||
"uuid": "file2", | |||
"path": "null/NAME_file2", | |||
"name": "NAME_file2", | |||
"longName": "null/NAME_file2", | |||
"q": "FIL", | |||
"project": "KEY_projectUuid", | |||
"projectName": "LONG_NAME_projectUuid" | |||
}, | |||
"sources": [ | |||
{ | |||
"line": 1, | |||
"code": "<p>SOURCE_1</p>", | |||
"scmRevision": "REVISION_1", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 1, | |||
"lineHits": 1, | |||
"utConditions": 2, | |||
"conditions": 2, | |||
"utCoveredConditions": 3, | |||
"coveredConditions": 3, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 2, | |||
"code": "<p>SOURCE_2</p>", | |||
"scmRevision": "REVISION_2", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 2, | |||
"lineHits": 2, | |||
"utConditions": 3, | |||
"conditions": 3, | |||
"utCoveredConditions": 4, | |||
"coveredConditions": 4, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 3, | |||
"code": "<p>SOURCE_3</p>", | |||
"scmRevision": "REVISION_3", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 3, | |||
"lineHits": 3, | |||
"utConditions": 4, | |||
"conditions": 4, | |||
"utCoveredConditions": 5, | |||
"coveredConditions": 5, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 4, | |||
"code": "<p>SOURCE_4</p>", | |||
"scmRevision": "REVISION_4", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 4, | |||
"lineHits": 4, | |||
"utConditions": 5, | |||
"conditions": 5, | |||
"utCoveredConditions": 6, | |||
"coveredConditions": 6, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 5, | |||
"code": "<p>SOURCE_5</p>", | |||
"scmRevision": "REVISION_5", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 5, | |||
"lineHits": 5, | |||
"utConditions": 6, | |||
"conditions": 6, | |||
"utCoveredConditions": 7, | |||
"coveredConditions": 7, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 6, | |||
"code": "<p>SOURCE_6</p>", | |||
"scmRevision": "REVISION_6", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 6, | |||
"lineHits": 6, | |||
"utConditions": 7, | |||
"conditions": 7, | |||
"utCoveredConditions": 8, | |||
"coveredConditions": 8, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 7, | |||
"code": "<p>SOURCE_7</p>", | |||
"scmRevision": "REVISION_7", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 7, | |||
"lineHits": 7, | |||
"utConditions": 8, | |||
"conditions": 8, | |||
"utCoveredConditions": 9, | |||
"coveredConditions": 9, | |||
"duplicated": true, | |||
"isNew": true | |||
} | |||
] | |||
}, | |||
"FILE_KEY_file1": { | |||
"component": { | |||
"key": "FILE_KEY_file1", | |||
"uuid": "file1", | |||
"path": "null/NAME_file1", | |||
"name": "NAME_file1", | |||
"longName": "null/NAME_file1", | |||
"q": "FIL", | |||
"project": "KEY_projectUuid", | |||
"projectName": "LONG_NAME_projectUuid" | |||
}, | |||
"sources": [ | |||
{ | |||
"line": 3, | |||
"code": "<p>SOURCE_3</p>", | |||
"scmRevision": "REVISION_3", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 3, | |||
"lineHits": 3, | |||
"utConditions": 4, | |||
"conditions": 4, | |||
"utCoveredConditions": 5, | |||
"coveredConditions": 5, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 4, | |||
"code": "<p>SOURCE_4</p>", | |||
"scmRevision": "REVISION_4", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 4, | |||
"lineHits": 4, | |||
"utConditions": 5, | |||
"conditions": 5, | |||
"utCoveredConditions": 6, | |||
"coveredConditions": 6, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 5, | |||
"code": "<p>SOURCE_5</p>", | |||
"scmRevision": "REVISION_5", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 5, | |||
"lineHits": 5, | |||
"utConditions": 6, | |||
"conditions": 6, | |||
"utCoveredConditions": 7, | |||
"coveredConditions": 7, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 6, | |||
"code": "<p>SOURCE_6</p>", | |||
"scmRevision": "REVISION_6", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 6, | |||
"lineHits": 6, | |||
"utConditions": 7, | |||
"conditions": 7, | |||
"utCoveredConditions": 8, | |||
"coveredConditions": 8, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 7, | |||
"code": "<p>SOURCE_7</p>", | |||
"scmRevision": "REVISION_7", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 7, | |||
"lineHits": 7, | |||
"utConditions": 8, | |||
"conditions": 8, | |||
"utCoveredConditions": 9, | |||
"coveredConditions": 9, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 8, | |||
"code": "<p>SOURCE_8</p>", | |||
"scmRevision": "REVISION_8", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 8, | |||
"lineHits": 8, | |||
"utConditions": 9, | |||
"conditions": 9, | |||
"utCoveredConditions": 10, | |||
"coveredConditions": 10, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 9, | |||
"code": "<p>SOURCE_9</p>", | |||
"scmRevision": "REVISION_9", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 9, | |||
"lineHits": 9, | |||
"utConditions": 10, | |||
"conditions": 10, | |||
"utCoveredConditions": 11, | |||
"coveredConditions": 11, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 10, | |||
"code": "<p>SOURCE_10</p>", | |||
"scmRevision": "REVISION_10", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 10, | |||
"lineHits": 10, | |||
"utConditions": 11, | |||
"conditions": 11, | |||
"utCoveredConditions": 12, | |||
"coveredConditions": 12, | |||
"duplicated": true, | |||
"isNew": true | |||
} | |||
] | |||
} | |||
} |
@@ -0,0 +1,86 @@ | |||
{ | |||
"FILE_KEY_file": { | |||
"component": { | |||
"key": "FILE_KEY_file", | |||
"uuid": "file", | |||
"path": "null/NAME_file", | |||
"name": "NAME_file", | |||
"longName": "null/NAME_file", | |||
"q": "FIL", | |||
"project": "KEY_projectUuid", | |||
"projectName": "LONG_NAME_projectUuid" | |||
}, | |||
"sources": [ | |||
{ | |||
"line": 3, | |||
"code": "\u003cp\u003eSOURCE_3\u003c/p\u003e", | |||
"scmRevision": "REVISION_3", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 3, | |||
"lineHits": 3, | |||
"utConditions": 4, | |||
"conditions": 4, | |||
"utCoveredConditions": 5, | |||
"coveredConditions": 5, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 4, | |||
"code": "\u003cp\u003eSOURCE_4\u003c/p\u003e", | |||
"scmRevision": "REVISION_4", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 4, | |||
"lineHits": 4, | |||
"utConditions": 5, | |||
"conditions": 5, | |||
"utCoveredConditions": 6, | |||
"coveredConditions": 6, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 5, | |||
"code": "\u003cp\u003eSOURCE_5\u003c/p\u003e", | |||
"scmRevision": "REVISION_5", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 5, | |||
"lineHits": 5, | |||
"utConditions": 6, | |||
"conditions": 6, | |||
"utCoveredConditions": 7, | |||
"coveredConditions": 7, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 6, | |||
"code": "\u003cp\u003eSOURCE_6\u003c/p\u003e", | |||
"scmRevision": "REVISION_6", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 6, | |||
"lineHits": 6, | |||
"utConditions": 7, | |||
"conditions": 7, | |||
"utCoveredConditions": 8, | |||
"coveredConditions": 8, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 7, | |||
"code": "\u003cp\u003eSOURCE_7\u003c/p\u003e", | |||
"scmRevision": "REVISION_7", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 7, | |||
"lineHits": 7, | |||
"utConditions": 8, | |||
"conditions": 8, | |||
"utCoveredConditions": 9, | |||
"coveredConditions": 9, | |||
"duplicated": true, | |||
"isNew": true | |||
} | |||
] | |||
} | |||
} |
@@ -0,0 +1,91 @@ | |||
{ | |||
"FILE_KEY_file": { | |||
"component": { | |||
"key": "FILE_KEY_file", | |||
"uuid": "file", | |||
"path": "null/NAME_file", | |||
"name": "NAME_file", | |||
"longName": "null/NAME_file", | |||
"q": "FIL", | |||
"project": "KEY_projectUuid", | |||
"projectName": "LONG_NAME_projectUuid", | |||
"measures": { | |||
"lines": "200.0", | |||
"coverage": "95.4", | |||
"duplicationDensity": "7.4" | |||
} | |||
}, | |||
"sources": [ | |||
{ | |||
"line": 3, | |||
"code": "\u003cp\u003eSOURCE_3\u003c/p\u003e", | |||
"scmRevision": "REVISION_3", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 3, | |||
"lineHits": 3, | |||
"utConditions": 4, | |||
"conditions": 4, | |||
"utCoveredConditions": 5, | |||
"coveredConditions": 5, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 4, | |||
"code": "\u003cp\u003eSOURCE_4\u003c/p\u003e", | |||
"scmRevision": "REVISION_4", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 4, | |||
"lineHits": 4, | |||
"utConditions": 5, | |||
"conditions": 5, | |||
"utCoveredConditions": 6, | |||
"coveredConditions": 6, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 5, | |||
"code": "\u003cp\u003eSOURCE_5\u003c/p\u003e", | |||
"scmRevision": "REVISION_5", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 5, | |||
"lineHits": 5, | |||
"utConditions": 6, | |||
"conditions": 6, | |||
"utCoveredConditions": 7, | |||
"coveredConditions": 7, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 6, | |||
"code": "\u003cp\u003eSOURCE_6\u003c/p\u003e", | |||
"scmRevision": "REVISION_6", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 6, | |||
"lineHits": 6, | |||
"utConditions": 7, | |||
"conditions": 7, | |||
"utCoveredConditions": 8, | |||
"coveredConditions": 8, | |||
"duplicated": true, | |||
"isNew": true | |||
}, | |||
{ | |||
"line": 7, | |||
"code": "\u003cp\u003eSOURCE_7\u003c/p\u003e", | |||
"scmRevision": "REVISION_7", | |||
"scmDate": "1974-10-02T21:40:00-0500", | |||
"utLineHits": 7, | |||
"lineHits": 7, | |||
"utConditions": 8, | |||
"conditions": 8, | |||
"utCoveredConditions": 9, | |||
"coveredConditions": 9, | |||
"duplicated": true, | |||
"isNew": true | |||
} | |||
] | |||
} | |||
} |