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++) {
*/
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;
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;
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
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();
}
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;
- }
}
--- /dev/null
+/*
+ * 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);
+ }
+}
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;
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;
ComponentIndex.class,
ComponentIndexer.class,
LiveMeasureModule.class,
+ ComponentViewerJsonWriter.class,
FavoriteModule.class,
// source
HtmlSourceDecorator.class,
+ LinesJsonWriter.class,
SourceService.class,
SourcesWs.class,
org.sonar.server.source.ws.ShowAction.class,
+ IssueSnippetsAction.class,
LinesAction.class,
HashAction.class,
RawAction.class,
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;
/**
* 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) {
.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));
}
--- /dev/null
+/*
+ * 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);
+ }
+
+ }
+}
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;
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;
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;
}
@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>" +
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();
}
}
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();
- }
-
}
--- /dev/null
+/*
+ * 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();
+ }
+}
@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>" +
@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() {
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;
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());
@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());
--- /dev/null
+/*
+ * 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();
+ }
+
+}
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);
}
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;
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() {
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();
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);
}
}
--- /dev/null
+{
+ "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
+ }
+ ]
+ }
+}
--- /dev/null
+{
+ "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
+ }
+ ]
+ }
+}
--- /dev/null
+{
+ "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
+ }
+ ]
+ }
+}
--- /dev/null
+{
+ "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
+ }
+ ]
+ }
+}