Browse Source

SONAR-11904 Create API endpoint to get snippets

tags/7.8
Duarte Meneses 5 years ago
parent
commit
f097b50098
18 changed files with 1400 additions and 226 deletions
  1. 18
    0
      server/sonar-db-dao/src/test/java/org/sonar/db/source/FileSourceTester.java
  2. 5
    124
      server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java
  3. 164
    0
      server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentViewerJsonWriter.java
  4. 6
    0
      server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
  5. 19
    1
      server/sonar-server/src/main/java/org/sonar/server/source/SourceService.java
  6. 178
    0
      server/sonar-server/src/main/java/org/sonar/server/source/ws/IssueSnippetsAction.java
  7. 5
    92
      server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesAction.java
  8. 119
    0
      server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesJsonWriter.java
  9. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/source/ws/ShowAction.java
  10. 4
    1
      server/sonar-server/src/test/java/org/sonar/server/component/ws/AppActionTest.java
  11. 16
    2
      server/sonar-server/src/test/java/org/sonar/server/source/SourceServiceTest.java
  12. 246
    0
      server/sonar-server/src/test/java/org/sonar/server/source/ws/IssueSnippetsActionTest.java
  13. 2
    1
      server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java
  14. 15
    4
      server/sonar-server/src/test/java/org/sonar/server/source/ws/SourcesWsTest.java
  15. 185
    0
      server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_close_to_each_other.json
  16. 240
    0
      server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_multiple_locations.json
  17. 86
    0
      server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_single_location.json
  18. 91
    0
      server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_with_measures.json

+ 18
- 0
server/sonar-db-dao/src/test/java/org/sonar/db/source/FileSourceTester.java View File

@@ -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++) {

+ 5
- 124
server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java View File

@@ -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;
}
}

+ 164
- 0
server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentViewerJsonWriter.java View File

@@ -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);
}
}

+ 6
- 0
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java View File

@@ -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,

+ 19
- 1
server/sonar-server/src/main/java/org/sonar/server/source/SourceService.java View File

@@ -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));
}

+ 178
- 0
server/sonar-server/src/main/java/org/sonar/server/source/ws/IssueSnippetsAction.java View File

@@ -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);
}

}
}

+ 5
- 92
server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesAction.java View File

@@ -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();
}

}

+ 119
- 0
server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesJsonWriter.java View File

@@ -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();
}
}

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/source/ws/ShowAction.java View File

@@ -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>" +

+ 4
- 1
server/sonar-server/src/test/java/org/sonar/server/component/ws/AppActionTest.java View File

@@ -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() {

+ 16
- 2
server/sonar-server/src/test/java/org/sonar/server/source/SourceServiceTest.java View File

@@ -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());

+ 246
- 0
server/sonar-server/src/test/java/org/sonar/server/source/ws/IssueSnippetsActionTest.java View File

@@ -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();
}

}

+ 2
- 1
server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java View File

@@ -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);
}

+ 15
- 4
server/sonar-server/src/test/java/org/sonar/server/source/ws/SourcesWsTest.java View File

@@ -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);
}
}

+ 185
- 0
server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_close_to_each_other.json View File

@@ -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
}
]
}
}

+ 240
- 0
server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_multiple_locations.json View File

@@ -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
}
]
}
}

+ 86
- 0
server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_single_location.json View File

@@ -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
}
]
}
}

+ 91
- 0
server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_with_measures.json View File

@@ -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
}
]
}
}

Loading…
Cancel
Save