瀏覽代碼

Feature/dm/migrate new code period (#2017)

* SONAR-12396 List new code periods for all branches with effective current values

* SONAR-12347 Migrate old definitions of leak period
tags/8.0
Duarte Meneses 4 年之前
父節點
當前提交
d650e5eeb4
共有 27 個檔案被更改,包括 1241 行新增478 行删除
  1. 2
    0
      server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
  2. 190
    0
      server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/NewCodePeriodResolver.java
  3. 7
    155
      server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java
  4. 7
    5
      server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepTest.java
  5. 6
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/newcodeperiod/NewCodePeriodDao.java
  6. 4
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/newcodeperiod/NewCodePeriodDto.java
  7. 3
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/newcodeperiod/NewCodePeriodMapper.java
  8. 0
    1
      server/sonar-db-dao/src/main/java/org/sonar/db/newcodeperiod/NewCodePeriodType.java
  9. 8
    0
      server/sonar-db-dao/src/main/resources/org/sonar/db/newcodeperiod/NewCodePeriodMapper.xml
  10. 4
    4
      server/sonar-db-dao/src/test/java/org/sonar/db/newcodeperiod/NewCodePeriodDtoTest.java
  11. 3
    3
      server/sonar-db-dao/src/test/java/org/sonar/db/purge/PurgeCommandsTest.java
  12. 218
    25
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v80/PopulateNewCodePeriodTable.java
  13. 200
    12
      server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v80/PopulateNewCodePeriodTableTest.java
  14. 60
    1
      server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v80/PopulateNewCodePeriodTableTest/schema.sql
  15. 152
    0
      server/sonar-server/src/main/java/org/sonar/server/newcodeperiod/ws/ListAction.java
  16. 4
    13
      server/sonar-server/src/main/java/org/sonar/server/newcodeperiod/ws/SetAction.java
  17. 1
    3
      server/sonar-server/src/main/java/org/sonar/server/newcodeperiod/ws/ShowAction.java
  18. 23
    0
      server/sonar-server/src/main/java/org/sonar/server/newcodeperiod/ws/package-info.java
  19. 0
    0
      server/sonar-server/src/main/resources/org/sonar/server/setting/ws/list_new_code_period-example.json
  20. 319
    0
      server/sonar-server/src/test/java/org/sonar/server/newcodeperiod/ws/ListActionTest.java
  21. 3
    32
      server/sonar-server/src/test/java/org/sonar/server/newcodeperiod/ws/SetActionTest.java
  22. 6
    6
      server/sonar-server/src/test/java/org/sonar/server/newcodeperiod/ws/ShowActionTest.java
  23. 9
    7
      server/sonar-server/src/test/java/org/sonar/server/newcodeperiod/ws/UnsetActionTest.java
  24. 0
    58
      sonar-ws/src/main/java/org/sonarqube/ws/client/settings/DeleteNewCodePeriodRequest.java
  25. 0
    58
      sonar-ws/src/main/java/org/sonarqube/ws/client/settings/ShowNewCodePeriodRequest.java
  26. 0
    86
      sonar-ws/src/main/java/org/sonarqube/ws/client/settings/UpdateNewCodePeriodRequest.java
  27. 12
    9
      sonar-ws/src/main/protobuf/ws-newcodeperiods.proto

+ 2
- 0
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java 查看文件

@@ -101,6 +101,7 @@ import org.sonar.ce.task.projectanalysis.measure.MeasureToMeasureDto;
import org.sonar.ce.task.projectanalysis.metric.MetricModule;
import org.sonar.ce.task.projectanalysis.notification.NotificationFactory;
import org.sonar.ce.task.projectanalysis.organization.DefaultOrganizationLoader;
import org.sonar.ce.task.projectanalysis.period.NewCodePeriodResolver;
import org.sonar.ce.task.projectanalysis.period.PeriodHolderImpl;
import org.sonar.ce.task.projectanalysis.qualitygate.EvaluationResultTextConverterImpl;
import org.sonar.ce.task.projectanalysis.qualitygate.QualityGateHolderImpl;
@@ -285,6 +286,7 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop
BranchPersisterImpl.class,
SiblingsIssuesLoader.class,
SiblingsIssueMerger.class,
NewCodePeriodResolver.class,

// filemove
ScoreMatrixDumperImpl.class,

+ 190
- 0
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/NewCodePeriodResolver.java 查看文件

@@ -0,0 +1,190 @@
/*
* 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.ce.task.projectanalysis.period;

import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import org.sonar.api.utils.DateUtils;
import org.sonar.api.utils.MessageException;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.SnapshotDto;
import org.sonar.db.component.SnapshotQuery;
import org.sonar.db.event.EventDto;
import org.sonar.db.newcodeperiod.NewCodePeriodDto;
import org.sonar.db.newcodeperiod.NewCodePeriodParser;
import org.sonar.db.newcodeperiod.NewCodePeriodType;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.lang.String.format;
import static org.sonar.db.component.SnapshotDto.STATUS_PROCESSED;
import static org.sonar.db.component.SnapshotQuery.SORT_FIELD.BY_DATE;
import static org.sonar.db.component.SnapshotQuery.SORT_ORDER.ASC;

public class NewCodePeriodResolver {
private static final Logger LOG = Loggers.get(NewCodePeriodResolver.class);

private final DbClient dbClient;

public NewCodePeriodResolver(DbClient dbClient) {
this.dbClient = dbClient;
}

public Period resolve(DbSession dbSession, String branchUuid, NewCodePeriodDto newCodePeriodDto, long referenceDate, String projectVersion) {
return toPeriod(newCodePeriodDto.getType(), newCodePeriodDto.getValue(), dbSession, projectVersion, branchUuid, referenceDate);
}

private Period toPeriod(NewCodePeriodType type, @Nullable String value, DbSession dbSession, String projectVersion, String rootUuid, long referenceDate) {
switch (type) {
case NUMBER_OF_DAYS:
Integer days = NewCodePeriodParser.parseDays(value);
checkNotNullValue(value, type);
return resolveByDays(dbSession, rootUuid, days, value, referenceDate);
case PREVIOUS_VERSION:
return resolveByPreviousVersion(dbSession, rootUuid, projectVersion);
case SPECIFIC_ANALYSIS:
checkNotNullValue(value, type);
return resolveBySpecificAnalysis(dbSession, rootUuid, value);
default:
throw new IllegalStateException("Unexpected type: " + type);
}
}

private Period resolveBySpecificAnalysis(DbSession dbSession, String rootUuid, String value) {
SnapshotDto baseline = dbClient.snapshotDao().selectByUuid(dbSession, value)
.filter(t -> t.getComponentUuid().equals(rootUuid))
.orElseThrow(() -> new IllegalStateException("Analysis '" + value + "' of project '" + rootUuid
+ "' defined as the baseline does not exist"));
LOG.debug("Resolving new code period with a specific analysis");
return newPeriod(NewCodePeriodType.SPECIFIC_ANALYSIS, value, Instant.ofEpochMilli(baseline.getCreatedAt()));
}

private Period resolveByPreviousVersion(DbSession dbSession, String projectUuid, String projectVersion) {
List<EventDto> versions = dbClient.eventDao().selectVersionsByMostRecentFirst(dbSession, projectUuid);
if (versions.isEmpty()) {
return findOldestAnalysis(dbSession, projectUuid);
}

String mostRecentVersion = Optional.ofNullable(versions.iterator().next().getName())
.orElseThrow(() -> new IllegalStateException("selectVersionsByMostRecentFirst returned a DTO which didn't have a name"));

if (versions.size() == 1) {
return findOldestAnalysis(dbSession, projectUuid);
}
return resolvePreviousVersion(dbSession, projectVersion, versions, mostRecentVersion);
}

private Period resolveByDays(DbSession dbSession, String rootUuid, Integer days, String value, long referenceDate) {
checkPeriodProperty(days > 0, value, "number of days is <= 0");
List<SnapshotDto> snapshots = dbClient.snapshotDao().selectAnalysesByQuery(dbSession, createCommonQuery(rootUuid)
.setCreatedBefore(referenceDate).setSort(BY_DATE, ASC));

ensureNotOnFirstAnalysis(!snapshots.isEmpty());
Instant targetDate = DateUtils.addDays(Instant.ofEpochMilli(referenceDate), -days);
LOG.debug("Resolving new code period by {} days: {}", days, supplierToString(() -> logDate(targetDate)));
SnapshotDto snapshot = findNearestSnapshotToTargetDate(snapshots, targetDate);
return newPeriod(NewCodePeriodType.NUMBER_OF_DAYS, String.valueOf((int) days), Instant.ofEpochMilli(snapshot.getCreatedAt()));
}

private Period resolvePreviousVersion(DbSession dbSession, String currentVersion, List<EventDto> versions, String mostRecentVersion) {
EventDto previousVersion = versions.get(currentVersion.equals(mostRecentVersion) ? 1 : 0);
LOG.debug("Resolving new code period by previous version: {}", previousVersion.getName());
return newPeriod(dbSession, previousVersion);
}

private Period findOldestAnalysis(DbSession dbSession, String projectUuid) {
LOG.debug("Resolving first analysis as new code period as there is only one existing version");
Optional<Period> period = dbClient.snapshotDao().selectOldestSnapshot(dbSession, projectUuid)
.map(dto -> newPeriod(NewCodePeriodType.PREVIOUS_VERSION, null, Instant.ofEpochMilli(dto.getCreatedAt())));
ensureNotOnFirstAnalysis(period.isPresent());
return period.get();
}

private Period newPeriod(DbSession dbSession, EventDto previousVersion) {
Optional<Period> period = dbClient.snapshotDao().selectByUuid(dbSession, previousVersion.getAnalysisUuid())
.map(dto -> newPeriod(NewCodePeriodType.PREVIOUS_VERSION, dto.getProjectVersion(), Instant.ofEpochMilli(dto.getCreatedAt())));
if (!period.isPresent()) {
throw new IllegalStateException(format("Analysis '%s' for version event '%s' has been deleted",
previousVersion.getAnalysisUuid(), previousVersion.getName()));
}
return period.get();
}

private static Period newPeriod(NewCodePeriodType type, @Nullable String value, Instant instant) {
return new Period(type.name(), value, instant.toEpochMilli());
}

private static Object supplierToString(Supplier<String> s) {
return new Object() {
@Override
public String toString() {
return s.get();
}
};
}

private static SnapshotQuery createCommonQuery(String projectUuid) {
return new SnapshotQuery().setComponentUuid(projectUuid).setStatus(STATUS_PROCESSED);
}

private static SnapshotDto findNearestSnapshotToTargetDate(List<SnapshotDto> snapshots, Instant targetDate) {
// FIXME shouldn't this be the first analysis after targetDate?
Duration bestDuration = null;
SnapshotDto nearest = null;
for (SnapshotDto snapshot : snapshots) {
Instant createdAt = Instant.ofEpochMilli(snapshot.getCreatedAt());
Duration duration = Duration.between(targetDate, createdAt).abs();
if (bestDuration == null || duration.compareTo(bestDuration) <= 0) {
bestDuration = duration;
nearest = snapshot;
}
}
return nearest;
}

private static void checkPeriodProperty(boolean test, String propertyValue, String testDescription, Object... args) {
if (!test) {
LOG.debug("Invalid code period '{}': {}", propertyValue, supplierToString(() -> format(testDescription, args)));
throw MessageException.of(format("Invalid new code period. '%s' is not one of: " +
"integer > 0, date before current analysis j, \"previous_version\", or version string that exists in the project' \n" +
"Please contact a project administrator to correct this setting", propertyValue));
}
}

private static void ensureNotOnFirstAnalysis(boolean expression) {
checkState(expression, "Attempting to resolve period while no analysis exist for project");
}

private static void checkNotNullValue(@Nullable String value, NewCodePeriodType type) {
checkNotNull(value, "Value can't be null with type %s", type);
}

private static String logDate(Instant instant) {
return DateUtils.formatDate(instant.truncatedTo(ChronoUnit.SECONDS));
}
}

+ 7
- 155
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java 查看文件

@@ -19,39 +19,19 @@
*/
package org.sonar.ce.task.projectanalysis.step;

import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import org.sonar.api.utils.DateUtils;
import org.sonar.api.utils.MessageException;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
import org.sonar.ce.task.projectanalysis.period.NewCodePeriodResolver;
import org.sonar.ce.task.projectanalysis.period.Period;
import org.sonar.ce.task.projectanalysis.period.PeriodHolder;
import org.sonar.ce.task.projectanalysis.period.PeriodHolderImpl;
import org.sonar.ce.task.step.ComputationStep;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.SnapshotDto;
import org.sonar.db.component.SnapshotQuery;
import org.sonar.db.event.EventDto;
import org.sonar.db.newcodeperiod.NewCodePeriodDao;
import org.sonar.db.newcodeperiod.NewCodePeriodDto;
import org.sonar.db.newcodeperiod.NewCodePeriodParser;
import org.sonar.db.newcodeperiod.NewCodePeriodType;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.lang.String.format;
import static org.sonar.db.component.SnapshotDto.STATUS_PROCESSED;
import static org.sonar.db.component.SnapshotQuery.SORT_FIELD.BY_DATE;
import static org.sonar.db.component.SnapshotQuery.SORT_ORDER.ASC;

/**
* Populates the {@link PeriodHolder}
@@ -62,21 +42,22 @@ import static org.sonar.db.component.SnapshotQuery.SORT_ORDER.ASC;
* - If a snapshot is found, a period is set to the repository, otherwise fail with MessageException
*/
public class LoadPeriodsStep implements ComputationStep {
private static final Logger LOG = Loggers.get(LoadPeriodsStep.class);

private final AnalysisMetadataHolder analysisMetadataHolder;
private final NewCodePeriodDao newCodePeriodDao;
private final TreeRootHolder treeRootHolder;
private final PeriodHolderImpl periodsHolder;
private final DbClient dbClient;
private final NewCodePeriodResolver resolver;

public LoadPeriodsStep(AnalysisMetadataHolder analysisMetadataHolder, NewCodePeriodDao newCodePeriodDao, TreeRootHolder treeRootHolder,
PeriodHolderImpl periodsHolder, DbClient dbClient) {
PeriodHolderImpl periodsHolder, DbClient dbClient, NewCodePeriodResolver resolver) {
this.analysisMetadataHolder = analysisMetadataHolder;
this.newCodePeriodDao = newCodePeriodDao;
this.treeRootHolder = treeRootHolder;
this.periodsHolder = periodsHolder;
this.dbClient = dbClient;
this.resolver = resolver;
}

@Override
@@ -101,8 +82,9 @@ public class LoadPeriodsStep implements ComputationStep {
() -> getProjectSetting(dbSession, projectUuid),
() -> getGlobalSetting(dbSession));

Period period = dto.map(d -> toPeriod(d.getType(), d.getValue(), dbSession, projectVersion, branchUuid))
.orElseGet(() -> toPeriod(NewCodePeriodType.PREVIOUS_VERSION, null, dbSession, projectVersion, branchUuid));
long analysisDate = analysisMetadataHolder.getAnalysisDate();
Period period = dto.map(d -> resolver.resolve(dbSession, branchUuid, d, analysisDate, projectVersion))
.orElseGet(() -> resolver.resolve(dbSession, branchUuid, NewCodePeriodDto.defaultInstance(), analysisDate, projectVersion));
periodsHolder.setPeriod(period);
}
}
@@ -129,137 +111,7 @@ public class LoadPeriodsStep implements ComputationStep {
return newCodePeriodDao.selectGlobal(dbSession);
}

private Period toPeriod(NewCodePeriodType type, @Nullable String value, DbSession dbSession, String analysisProjectVersion, String rootUuid) {
switch (type) {
case NUMBER_OF_DAYS:
Integer days = NewCodePeriodParser.parseDays(value);
checkNotNullValue(value, type);
return resolveByDays(dbSession, rootUuid, days, value);
case PREVIOUS_VERSION:
return resolveByPreviousVersion(dbSession, rootUuid, analysisProjectVersion);
case SPECIFIC_ANALYSIS:
checkNotNullValue(value, type);
return resolveBySpecificAnalysis(dbSession, rootUuid, value);
default:
throw new IllegalStateException("Unexpected type: " + type);
}
}

private String getProjectBranchUuid() {
return analysisMetadataHolder.getProject().getUuid();
}

private Period resolveBySpecificAnalysis(DbSession dbSession, String rootUuid, String value) {
SnapshotDto baseline = dbClient.snapshotDao().selectByUuid(dbSession, value)
.filter(t -> t.getComponentUuid().equals(rootUuid))
.orElseThrow(() -> new IllegalStateException("Analysis '" + value + "' of project '" + rootUuid
+ "' defined as the baseline does not exist"));
LOG.debug("Resolving new code period with a specific analysis");
return newPeriod(NewCodePeriodType.SPECIFIC_ANALYSIS, value, Instant.ofEpochMilli(baseline.getCreatedAt()));
}

private Period resolveByPreviousVersion(DbSession dbSession, String projectUuid, String analysisProjectVersion) {
List<EventDto> versions = dbClient.eventDao().selectVersionsByMostRecentFirst(dbSession, projectUuid);
if (versions.isEmpty()) {
return findOldestAnalysis(dbSession, projectUuid);
}

String mostRecentVersion = Optional.ofNullable(versions.iterator().next().getName())
.orElseThrow(() -> new IllegalStateException("selectVersionsByMostRecentFirst returned a DTO which didn't have a name"));

if (versions.size() == 1) {
return findOldestAnalysis(dbSession, projectUuid);
}
return resolvePreviousVersion(dbSession, analysisProjectVersion, versions, mostRecentVersion);
}

private Period resolveByDays(DbSession dbSession, String rootUuid, Integer days, String propertyValue) {
checkPeriodProperty(days > 0, propertyValue, "number of days is <= 0");
long analysisDate = analysisMetadataHolder.getAnalysisDate();
List<SnapshotDto> snapshots = dbClient.snapshotDao().selectAnalysesByQuery(dbSession, createCommonQuery(rootUuid)
.setCreatedBefore(analysisDate).setSort(BY_DATE, ASC));

ensureNotOnFirstAnalysis(!snapshots.isEmpty());
Instant targetDate = DateUtils.addDays(Instant.ofEpochMilli(analysisDate), -days);
LOG.debug("Resolving new code period by {} days: {}", days, supplierToString(() -> logDate(targetDate)));
SnapshotDto snapshot = findNearestSnapshotToTargetDate(snapshots, targetDate);
return newPeriod(NewCodePeriodType.NUMBER_OF_DAYS, String.valueOf((int) days), Instant.ofEpochMilli(snapshot.getCreatedAt()));
}

private Period resolvePreviousVersion(DbSession dbSession, String currentVersion, List<EventDto> versions, String mostRecentVersion) {
EventDto previousVersion = versions.get(currentVersion.equals(mostRecentVersion) ? 1 : 0);
LOG.debug("Resolving new code period by previous version: {}", previousVersion.getName());
return newPeriod(dbSession, previousVersion);
}

private Period findOldestAnalysis(DbSession dbSession, String projectUuid) {
LOG.debug("Resolving first analysis as new code period as there is only one existing version");
Optional<Period> period = dbClient.snapshotDao().selectOldestSnapshot(dbSession, projectUuid)
.map(dto -> newPeriod(NewCodePeriodType.PREVIOUS_VERSION, null, Instant.ofEpochMilli(dto.getCreatedAt())));
ensureNotOnFirstAnalysis(period.isPresent());
return period.get();
}

private Period newPeriod(DbSession dbSession, EventDto previousVersion) {
Optional<Period> period = dbClient.snapshotDao().selectByUuid(dbSession, previousVersion.getAnalysisUuid())
.map(dto -> newPeriod(NewCodePeriodType.PREVIOUS_VERSION, dto.getProjectVersion(), Instant.ofEpochMilli(dto.getCreatedAt())));
if (!period.isPresent()) {
throw new IllegalStateException(format("Analysis '%s' for version event '%s' has been deleted",
previousVersion.getAnalysisUuid(), previousVersion.getName()));
}
return period.get();
}

private static Period newPeriod(NewCodePeriodType type, @Nullable String value, Instant instant) {
return new Period(type.name(), value, instant.toEpochMilli());
}

private static Object supplierToString(Supplier<String> s) {
return new Object() {
@Override
public String toString() {
return s.get();
}
};
}

private static SnapshotQuery createCommonQuery(String projectUuid) {
return new SnapshotQuery().setComponentUuid(projectUuid).setStatus(STATUS_PROCESSED);
}

private static SnapshotDto findNearestSnapshotToTargetDate(List<SnapshotDto> snapshots, Instant targetDate) {
// FIXME shouldn't this be the first analysis after targetDate?
Duration bestDuration = null;
SnapshotDto nearest = null;
for (SnapshotDto snapshot : snapshots) {
Instant createdAt = Instant.ofEpochMilli(snapshot.getCreatedAt());
Duration duration = Duration.between(targetDate, createdAt).abs();
if (bestDuration == null || duration.compareTo(bestDuration) <= 0) {
bestDuration = duration;
nearest = snapshot;
}
}
return nearest;
}

private static void checkPeriodProperty(boolean test, String propertyValue, String testDescription, Object... args) {
if (!test) {
LOG.debug("Invalid code period '{}': {}", propertyValue, supplierToString(() -> format(testDescription, args)));
throw MessageException.of(format("Invalid new code period. '%s' is not one of: " +
"integer > 0, date before current analysis j, \"previous_version\", or version string that exists in the project' \n" +
"Please contact a project administrator to correct this setting", propertyValue));
}
}

private static void ensureNotOnFirstAnalysis(boolean expression) {
checkState(expression, "Attempting to resolve period while no analysis exist for project");
}

private static void checkNotNullValue(@Nullable String value, NewCodePeriodType type) {
checkNotNull(value, "Value can't be null with type %s", type);
}

private static String logDate(Instant instant) {
return DateUtils.formatDate(instant.truncatedTo(ChronoUnit.SECONDS));
}
}

+ 7
- 5
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepTest.java 查看文件

@@ -44,6 +44,7 @@ import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
import org.sonar.ce.task.projectanalysis.component.Component;
import org.sonar.ce.task.projectanalysis.component.ReportComponent;
import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
import org.sonar.ce.task.projectanalysis.period.NewCodePeriodResolver;
import org.sonar.ce.task.projectanalysis.period.Period;
import org.sonar.ce.task.projectanalysis.period.PeriodHolderImpl;
import org.sonar.ce.task.step.ComputationStep;
@@ -86,9 +87,10 @@ public class LoadPeriodsStepTest extends BaseStepTest {
private PeriodHolderImpl periodsHolder = new PeriodHolderImpl();
private System2 system2Mock = mock(System2.class);
private NewCodePeriodDao dao = new NewCodePeriodDao(system2Mock, new SequenceUuidFactory());
private NewCodePeriodResolver newCodePeriodResolver = new NewCodePeriodResolver(dbTester.getDbClient());
private ZonedDateTime analysisDate = ZonedDateTime.of(2019, 3, 20, 5, 30, 40, 0, ZoneId.systemDefault());

private LoadPeriodsStep underTest = new LoadPeriodsStep(analysisMetadataHolder, dao, treeRootHolder, periodsHolder, dbTester.getDbClient());
private LoadPeriodsStep underTest = new LoadPeriodsStep(analysisMetadataHolder, dao, treeRootHolder, periodsHolder, dbTester.getDbClient(), newCodePeriodResolver);

private OrganizationDto organization;
private ComponentDto project;
@@ -373,7 +375,7 @@ public class LoadPeriodsStepTest extends BaseStepTest {

@DataProvider
public static Object[][] zeroOrLess() {
return new Object[][] {
return new Object[][]{
{0},
{-1 - new Random().nextInt(30)}
};
@@ -381,7 +383,7 @@ public class LoadPeriodsStepTest extends BaseStepTest {

@DataProvider
public static Object[][] stringConsideredAsVersions() {
return new Object[][] {
return new Object[][]{
{randomAlphabetic(5)},
{"1,3"},
{"1.3"},
@@ -393,7 +395,7 @@ public class LoadPeriodsStepTest extends BaseStepTest {

@DataProvider
public static Object[][] projectVersionNullOrNot() {
return new Object[][] {
return new Object[][]{
{null},
{randomAlphabetic(15)},
};
@@ -401,7 +403,7 @@ public class LoadPeriodsStepTest extends BaseStepTest {

@DataProvider
public static Object[][] anyValidLeakPeriodSettingValue() {
return new Object[][] {
return new Object[][]{
// days
{"100"},
// previous_version keyword

+ 6
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/newcodeperiod/NewCodePeriodDao.java 查看文件

@@ -19,6 +19,7 @@
*/
package org.sonar.db.newcodeperiod;

import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;
import org.sonar.api.utils.System2;
@@ -76,6 +77,11 @@ public class NewCodePeriodDao implements Dao {
return Optional.ofNullable(mapper(dbSession).selectByProject(projectUuid));
}

public List<NewCodePeriodDto> selectAllByProject(DbSession dbSession, String projectUuid) {
requireNonNull(projectUuid, "Project uuid must be specified.");
return mapper(dbSession).selectAllByProject(projectUuid);
}

public Optional<NewCodePeriodDto> selectByBranch(DbSession dbSession, String projectUuid, String branchUuid) {
requireNonNull(projectUuid, "Project uuid must be specified.");
requireNonNull(branchUuid, "Branch uuid must be specified.");

+ 4
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/newcodeperiod/NewCodePeriodDto.java 查看文件

@@ -30,6 +30,10 @@ public class NewCodePeriodDto {
private long updatedAt;
private long createdAt;

public static NewCodePeriodDto defaultInstance() {
return new NewCodePeriodDto().setType(NewCodePeriodType.PREVIOUS_VERSION);
}

public long getCreatedAt() {
return createdAt;
}

+ 3
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/newcodeperiod/NewCodePeriodMapper.java 查看文件

@@ -19,6 +19,7 @@
*/
package org.sonar.db.newcodeperiod;

import java.util.List;
import java.util.Optional;
import org.apache.ibatis.annotations.Param;

@@ -39,4 +40,6 @@ public interface NewCodePeriodMapper {
NewCodePeriodDto selectByBranch(@Param("projectUuid") String projectUuid, @Param("branchUuid") String branchUuid);

long countByProjectAnalysis(String projectAnalysisUuid);

List<NewCodePeriodDto> selectAllByProject(String projectUuid);
}

+ 0
- 1
server/sonar-db-dao/src/main/java/org/sonar/db/newcodeperiod/NewCodePeriodType.java 查看文件

@@ -22,6 +22,5 @@ package org.sonar.db.newcodeperiod;
public enum NewCodePeriodType {
PREVIOUS_VERSION,
NUMBER_OF_DAYS,
DATE,
SPECIFIC_ANALYSIS
}

+ 8
- 0
server/sonar-db-dao/src/main/resources/org/sonar/db/newcodeperiod/NewCodePeriodMapper.xml 查看文件

@@ -79,6 +79,14 @@
AND ncp.branch_uuid is null
</select>

<select id="selectAllByProject" parameterType="map" resultType="org.sonar.db.newcodeperiod.NewCodePeriodDto">
SELECT
<include refid="newCodePeriodMapperColumns"/>
FROM new_code_periods ncp
WHERE
ncp.project_uuid=#{projectUuid, jdbcType=VARCHAR}
</select>

<select id="selectByBranch" parameterType="map" resultType="org.sonar.db.newcodeperiod.NewCodePeriodDto">
SELECT
<include refid="newCodePeriodMapperColumns"/>

+ 4
- 4
server/sonar-db-dao/src/test/java/org/sonar/db/newcodeperiod/NewCodePeriodDtoTest.java 查看文件

@@ -39,15 +39,15 @@ public class NewCodePeriodDtoTest {
.setBranchUuid("branchUuid")
.setCreatedAt(currentTime)
.setUpdatedAt(currentTime)
.setType(NewCodePeriodType.DATE)
.setValue("2018-01-02");
.setType(NewCodePeriodType.NUMBER_OF_DAYS)
.setValue("1");

assertThat(newCodePeriodDto.getUuid()).isEqualTo("uuid");
assertThat(newCodePeriodDto.getProjectUuid()).isEqualTo("projectUuid");
assertThat(newCodePeriodDto.getBranchUuid()).isEqualTo("branchUuid");
assertThat(newCodePeriodDto.getCreatedAt()).isEqualTo(currentTime);
assertThat(newCodePeriodDto.getUpdatedAt()).isEqualTo(currentTime);
assertThat(newCodePeriodDto.getType()).isEqualTo(NewCodePeriodType.DATE);
assertThat(newCodePeriodDto.getValue()).isEqualTo("2018-01-02");
assertThat(newCodePeriodDto.getType()).isEqualTo(NewCodePeriodType.NUMBER_OF_DAYS);
assertThat(newCodePeriodDto.getValue()).isEqualTo("1");
}
}

+ 3
- 3
server/sonar-db-dao/src/test/java/org/sonar/db/purge/PurgeCommandsTest.java 查看文件

@@ -563,7 +563,7 @@ public class PurgeCommandsTest {
dbTester.newCodePeriods().insert(NewCodePeriodType.PREVIOUS_VERSION, null);

//project settings
dbTester.newCodePeriods().insert(project.uuid(), NewCodePeriodType.DATE, "2019-01-01");
dbTester.newCodePeriods().insert(project.uuid(), NewCodePeriodType.NUMBER_OF_DAYS, "20");

//branch settings
dbTester.newCodePeriods().insert(project.uuid(), branch.getUuid(), NewCodePeriodType.NUMBER_OF_DAYS, "1");
@@ -586,7 +586,7 @@ public class PurgeCommandsTest {
dbTester.newCodePeriods().insert(NewCodePeriodType.PREVIOUS_VERSION, null);

//project settings
dbTester.newCodePeriods().insert(project.uuid(), NewCodePeriodType.DATE, "2019-01-01");
dbTester.newCodePeriods().insert(project.uuid(), NewCodePeriodType.NUMBER_OF_DAYS, "20");

//branch settings
dbTester.newCodePeriods().insert(project.uuid(), branch.getUuid(), NewCodePeriodType.NUMBER_OF_DAYS, "1");
@@ -609,7 +609,7 @@ public class PurgeCommandsTest {
dbTester.newCodePeriods().insert(NewCodePeriodType.PREVIOUS_VERSION, null);

//project settings
dbTester.newCodePeriods().insert(project.uuid(), NewCodePeriodType.DATE, "2019-01-01");
dbTester.newCodePeriods().insert(project.uuid(), NewCodePeriodType.NUMBER_OF_DAYS, "20");

//branch settings
dbTester.newCodePeriods().insert(project.uuid(), branch.getUuid(), NewCodePeriodType.NUMBER_OF_DAYS, "1");

+ 218
- 25
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v80/PopulateNewCodePeriodTable.java 查看文件

@@ -20,14 +20,30 @@
package org.sonar.server.platform.db.migration.version.v80;

import java.sql.SQLException;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.sonar.api.utils.System2;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.core.util.UuidFactory;
import org.sonar.db.Database;
import org.sonar.server.platform.db.migration.step.DataChange;
import org.sonar.server.platform.db.migration.step.Upsert;

public class PopulateNewCodePeriodTable extends DataChange {
private static final Logger LOG = Loggers.get(PopulateNewCodePeriodTable.class);

private static final String TYPE_PREVIOUS_VERSION = "PREVIOUS_VERSION";
private static final String TYPE_NUMBER_OF_DAYS = "NUMBER_OF_DAYS";
private static final String TYPE_SPECIFIC_ANALYSIS = "SPECIFIC_ANALYSIS";

private static final String LEAK_PERIOD_PROP_KEY = "sonar.leak.period";

private final UuidFactory uuidFactory;
private final System2 system2;
@@ -40,41 +56,132 @@ public class PopulateNewCodePeriodTable extends DataChange {

@Override
protected void execute(Context context) throws SQLException {
List<ProjectBranchCodePeriod> projectBranchCodePeriods = context.prepareSelect(
"select pb.uuid, pb.project_uuid, pb.manual_baseline_analysis_uuid from project_branches pb where pb.manual_baseline_analysis_uuid is not null")
List<String> branchUuidsAlreadyMigrated = populateFromManualBaselines(context);
populateFromSettings(context, branchUuidsAlreadyMigrated);
}

private List<String> populateFromManualBaselines(Context context) throws SQLException {
List<ProjectBranchManualBaselineDto> projectBranchManualBaselines = context.prepareSelect(
"SELECT pb.uuid, pb.project_uuid, pb.manual_baseline_analysis_uuid FROM project_branches pb WHERE pb.manual_baseline_analysis_uuid IS NOT NULL")
.list(row -> {
String branchUuid = row.getString(1);
String projectUuid = row.getString(2);
String manualBaselineAnalysisUuid = row.getString(3);
return new ProjectBranchCodePeriod(branchUuid, projectUuid, manualBaselineAnalysisUuid);
return new ProjectBranchManualBaselineDto(branchUuid, projectUuid, manualBaselineAnalysisUuid);
});

if (!projectBranchManualBaselines.isEmpty()) {
populateWithManualBaselines(context, projectBranchManualBaselines);
}

return projectBranchManualBaselines.stream()
.map(pb -> pb.branchUuid)
.collect(Collectors.toList());
}

private void populateFromSettings(Context context, List<String> branchUuidsAlreadyMigrated) throws SQLException {
// migrate global setting
String globalSetting = context
.prepareSelect("SELECT props.text_value FROM properties props WHERE props.prop_key = '" + LEAK_PERIOD_PROP_KEY + "' AND props.resource_id IS NULL")
.get(r -> r.getString(1));
if (globalSetting != null) {
populateGlobalSetting(context, globalSetting);
}

List<BranchLeakPeriodPropertyDto> projectLeakPeriodProperties = new ArrayList<>();
context.prepareSelect(
"SELECT projs.uuid, projs.main_branch_project_uuid, props.text_value "
+ "FROM properties props INNER JOIN projects projs ON props.resource_id = projs.id "
+ "WHERE props.prop_key = '" + LEAK_PERIOD_PROP_KEY + "' AND props.resource_id IS NOT NULL ")
.scroll(row -> {
String branchUuid = row.getString(1);
if (branchUuidsAlreadyMigrated.contains(branchUuid)) {
return;
}
String mainBranchUuid = row.getString(2);
String value = row.getString(3);
if (mainBranchUuid == null) {
mainBranchUuid = branchUuid;
}
projectLeakPeriodProperties.add(new BranchLeakPeriodPropertyDto(branchUuid, mainBranchUuid, value));
});

if (!projectBranchCodePeriods.isEmpty()) {
populateProjectBranchCodePeriods(context, projectBranchCodePeriods);
populateWithSettings(context, projectLeakPeriodProperties);
}

private void populateGlobalSetting(Context context, String value) {
Optional<TypeValuePair> typeValue = tryParse(context, null, value);
typeValue.ifPresent(tp -> {
try (Upsert upsertQuery = prepareUpsertNewCodePeriodQuery(context)) {
long currentTime = system2.now();
insert(upsertQuery, null, null, tp.type, tp.value, currentTime);
upsertQuery
.execute()
.commit();
} catch (SQLException e) {
LOG.warn("Failed to migrate the global property for the new code period", e);
}
});
}

private void populateWithSettings(Context context, List<BranchLeakPeriodPropertyDto> projectLeakPeriodProperties) throws SQLException {
try (Upsert upsertQuery = prepareUpsertNewCodePeriodQuery(context)) {
long currentTime = system2.now();
boolean commit = false;

for (BranchLeakPeriodPropertyDto branchLeakPeriodProperty : projectLeakPeriodProperties) {
Optional<TypeValuePair> typeValueOpt = tryParse(context, branchLeakPeriodProperty.branchUuid, branchLeakPeriodProperty.value);
if (!typeValueOpt.isPresent()) {
continue;
}

TypeValuePair typeValue = typeValueOpt.get();

String branchUuid = null;
if (!branchLeakPeriodProperty.isMainBranch() || TYPE_SPECIFIC_ANALYSIS.equals(typeValue.type)) {
branchUuid = branchLeakPeriodProperty.branchUuid;
}

insert(upsertQuery, branchLeakPeriodProperty.mainBranchUuid, branchUuid, typeValue.type, typeValue.value, currentTime);
commit = true;
}

if (commit) {
upsertQuery
.execute()
.commit();
}
}
}

private void populateProjectBranchCodePeriods(Context context, List<ProjectBranchCodePeriod> projectBranchCodePeriods) throws SQLException {
Upsert insertQuery = prepareInsertProjectQualityGateQuery(context);
for (ProjectBranchCodePeriod projectBranchCodePeriod : projectBranchCodePeriods) {
long currenTime = system2.now();
insertQuery
.setString(1, uuidFactory.create())
.setString(2, projectBranchCodePeriod.projectUuid)
.setString(3, projectBranchCodePeriod.branchUuid)
.setString(4, "SPECIFIC_ANALYSIS")
.setString(5, projectBranchCodePeriod.manualBaselineAnalysisUuid)
.setLong(6, currenTime)
.setLong(7, currenTime)
.addBatch();
private void populateWithManualBaselines(Context context, List<ProjectBranchManualBaselineDto> projectBranchManualBaselines) throws SQLException {
try (Upsert upsertQuery = prepareUpsertNewCodePeriodQuery(context)) {
long currentTime = system2.now();

for (ProjectBranchManualBaselineDto projectBranchManualBaseline : projectBranchManualBaselines) {
insert(upsertQuery, projectBranchManualBaseline.projectUuid, projectBranchManualBaseline.branchUuid, TYPE_SPECIFIC_ANALYSIS,
projectBranchManualBaseline.manualBaselineAnalysisUuid, currentTime);
}
upsertQuery
.execute()
.commit();
}
insertQuery
.execute()
.commit();
}

private static Upsert prepareInsertProjectQualityGateQuery(Context context) throws SQLException {
return context.prepareUpsert("insert into new_code_periods(" +
private void insert(Upsert upsert, @Nullable String projectUuid, @Nullable String branchUuid, String type, @Nullable String value, long currentTime) throws SQLException {
upsert
.setString(1, uuidFactory.create())
.setString(2, projectUuid)
.setString(3, branchUuid)
.setString(4, type)
.setString(5, value)
.setLong(6, currentTime)
.setLong(7, currentTime)
.addBatch();
}

private static Upsert prepareUpsertNewCodePeriodQuery(Context context) throws SQLException {
return context.prepareUpsert("INSERT INTO new_code_periods(" +
"uuid, " +
"project_uuid," +
"branch_uuid," +
@@ -85,16 +192,102 @@ public class PopulateNewCodePeriodTable extends DataChange {
") VALUES (?, ?, ?, ?, ?, ?, ?)");
}

private static class ProjectBranchCodePeriod {
private static Optional<TypeValuePair> tryParse(Context context, @Nullable String branchUuid, String value) {
try {
if (value.equals("previous_version")) {
return Optional.of(new TypeValuePair(TYPE_PREVIOUS_VERSION, null));
}

try {
Integer.parseInt(value);
return Optional.of(new TypeValuePair(TYPE_NUMBER_OF_DAYS, value));
} catch (NumberFormatException e) {
// ignore
}

if (branchUuid == null) {
return Optional.empty();
}

try {
LocalDate localDate = LocalDate.parse(value);
Optional<String> snapshot = findFirstSnapshot(context, branchUuid, localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli());
return snapshot.map(uuid -> new TypeValuePair(TYPE_SPECIFIC_ANALYSIS, uuid));
} catch (DateTimeParseException e) {
// ignore
}

List<EventDto> versions = getVersionsByMostRecentFirst(context, branchUuid);
Optional<EventDto> versionMatch = versions.stream().filter(v -> v.version.equals(value)).findFirst();
return versionMatch.map(e -> new TypeValuePair(TYPE_SPECIFIC_ANALYSIS, e.analysisUuid));
} catch (SQLException e) {
LOG.warn("Failed to migrate a property for the new code period", e);
return Optional.empty();
}
}

private static List<EventDto> getVersionsByMostRecentFirst(Context context, String branchUuid) throws SQLException {
// TODO in LoadPeriodsStep we do a join with snapshots and see if the analysis is still unprocessed. Do we need to do it here?
return context.prepareSelect("SELECT name, analysis_uuid FROM events "
+ "WHERE component_uuid = '" + branchUuid + "' AND category = 'Version' "
+ "ORDER BY created_at DESC")
.list(r -> new EventDto(r.getString(1), r.getString(2)));
}

private static Optional<String> findFirstSnapshot(Context context, String branchUuid, long date) throws SQLException {
String analysisUuid = context.prepareSelect("SELECT uuid FROM snapshots "
+ "WHERE component_uuid = '" + branchUuid + "' AND created_at > " + date + " AND status = 'P' "
+ "ORDER BY created_at ASC")
.get(r -> r.getString(1));
return Optional.ofNullable(analysisUuid);
}

private static class TypeValuePair {
private final String type;
@Nullable
private final String value;

private TypeValuePair(String type, @Nullable String value) {
this.type = type;
this.value = value;
}
}

private static class EventDto {
private final String version;
private final String analysisUuid;

private EventDto(String version, String analysisUuid) {
this.version = version;
this.analysisUuid = analysisUuid;
}
}

private static class ProjectBranchManualBaselineDto {
private final String branchUuid;
private final String projectUuid;
private final String manualBaselineAnalysisUuid;

ProjectBranchCodePeriod(String branchUuid, String projectUuid, String manualBaselineAnalysisUuid) {
ProjectBranchManualBaselineDto(String branchUuid, String projectUuid, String manualBaselineAnalysisUuid) {
this.branchUuid = branchUuid;
this.projectUuid = projectUuid;
this.manualBaselineAnalysisUuid = manualBaselineAnalysisUuid;
}
}

private static class BranchLeakPeriodPropertyDto {
private final String branchUuid;
private final String mainBranchUuid;
private final String value;

BranchLeakPeriodPropertyDto(String branchUuid, String mainBranchUuid, String value) {
this.branchUuid = branchUuid;
this.mainBranchUuid = mainBranchUuid;
this.value = value;
}

private boolean isMainBranch() {
return mainBranchUuid.equals(branchUuid);
}
}
}

+ 200
- 12
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v80/PopulateNewCodePeriodTableTest.java 查看文件

@@ -20,14 +20,23 @@
package org.sonar.server.platform.db.migration.version.v80;

import java.sql.SQLException;
import org.junit.Assert;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.api.resources.Scopes;
import org.sonar.api.utils.System2;
import org.sonar.core.util.UuidFactoryImpl;
import org.sonar.db.CoreDbTester;

import static java.lang.String.valueOf;
import static org.assertj.core.api.Assertions.assertThat;

public class PopulateNewCodePeriodTableTest {
private static final String NEW_CODE_PERIODS_TABLE_NAME = "new_code_periods";
private static final String PROJECT_BRANCHES_TABLE_NAME = "project_branches";
@@ -35,7 +44,6 @@ public class PopulateNewCodePeriodTableTest {

@Rule
public CoreDbTester dbTester = CoreDbTester.createForSchema(PopulateNewCodePeriodTableTest.class, "schema.sql");

@Rule
public ExpectedException expectedException = ExpectedException.none();

@@ -43,31 +51,211 @@ public class PopulateNewCodePeriodTableTest {

@Test
public void copy_manual_baseline_analysis_to_new_code_period_table() throws SQLException {
for (long i = 1; i <= NUMBER_OF_PROJECT_BRANCHES_TO_INSERT; i++) {
insertProjectBranch(i);
for (long i = 0; i < NUMBER_OF_PROJECT_BRANCHES_TO_INSERT; i++) {
insertMainBranch(i, true);
}

underTest.execute();
assertThat(dbTester.countRowsOfTable(NEW_CODE_PERIODS_TABLE_NAME)).isEqualTo(10);

int propertiesCount = dbTester.countRowsOfTable(NEW_CODE_PERIODS_TABLE_NAME);
Assert.assertEquals(10, propertiesCount);
for (int i = 0; i < NUMBER_OF_PROJECT_BRANCHES_TO_INSERT; i++) {
assertNewCodePeriod(i, "pb-uuid-" + i, "pb-uuid-" + i, "SPECIFIC_ANALYSIS", "mba-uuid" + i);
}

//should not fail if executed twice
underTest.execute();
}

private void insertProjectBranch(long counter) {
@Test
public void do_nothing_if_cant_find_matching_analysis() throws SQLException {
insertMainBranch(0, false);
insertMainBranch(1, false);

insertProperty(0, "2.0");
insertProperty(0, "2019-04-05");

underTest.execute();
assertThat(dbTester.countRowsOfTable(NEW_CODE_PERIODS_TABLE_NAME)).isEqualTo(0);
}

@Test
public void do_nothing_if_cant_migrate_global_property() throws SQLException {
insertProperty(null, "2.0");

underTest.execute();
assertThat(dbTester.countRowsOfTable(NEW_CODE_PERIODS_TABLE_NAME)).isEqualTo(0);
}

@Test
public void migrate_project_property_set_to_previous_version() throws SQLException {
insertMainBranch(0, true);
insertMainBranch(1, false);
// no property defined for it
insertMainBranch(2, false);

// doesn't get copied since there is a manual baseline taking precedence
insertProperty(0, "20");
insertProperty(1, "previous_version");
// doesn't exist
insertProperty(3, "previous_version");

underTest.execute();

assertThat(dbTester.countRowsOfTable(NEW_CODE_PERIODS_TABLE_NAME)).isEqualTo(2);
assertNewCodePeriod(0, "pb-uuid-" + 0, "pb-uuid-" + 0, "SPECIFIC_ANALYSIS", "mba-uuid" + 0);
assertNewCodePeriod(1, "pb-uuid-" + 1, null, "PREVIOUS_VERSION", null);
}

@Test
public void migrate_project_property_set_to_number_of_days() throws SQLException {
insertMainBranch(0, false);
insertBranch(0, 1, false);
insertProperty(1, "20");

underTest.execute();

assertThat(dbTester.countRowsOfTable(NEW_CODE_PERIODS_TABLE_NAME)).isEqualTo(1);
assertNewCodePeriod(0, "pb-uuid-" + 0, "pb-uuid-" + 1, "NUMBER_OF_DAYS", "20");
}

@Test
public void migrate_branch_property_set_to_number_of_days() throws SQLException {
insertMainBranch(0, false);
insertProperty(0, "20");

underTest.execute();

assertThat(dbTester.countRowsOfTable(NEW_CODE_PERIODS_TABLE_NAME)).isEqualTo(1);
assertNewCodePeriod(0, "pb-uuid-" + 0, null, "NUMBER_OF_DAYS", "20");
}

@Test
public void migrate_global_property_set_to_number_of_days() throws SQLException {
insertProperty(null, "20");

underTest.execute();

assertThat(dbTester.countRowsOfTable(NEW_CODE_PERIODS_TABLE_NAME)).isEqualTo(1);
assertNewCodePeriod(0, null, null, "NUMBER_OF_DAYS", "20");
}


@Test
public void migrate_project_property_set_to_version() throws SQLException {
insertMainBranch(0, false);
insertProperty(0, "2.0");

insertVersionEvent(0, 10L, "1.0");
insertVersionEvent(0, 20L, "2.0");
insertVersionEvent(0, 30L, "3.0");
insertVersionEvent(0, 40L, "2.0");
insertVersionEvent(0, 50L, "3.0");

underTest.execute();

assertThat(dbTester.countRowsOfTable(NEW_CODE_PERIODS_TABLE_NAME)).isEqualTo(1);
// we don't support specific analysis for projects, so we set it for the branch
assertNewCodePeriod(0, "pb-uuid-" + 0, "pb-uuid-" + 0, "SPECIFIC_ANALYSIS", "analysis-40");
}

@Test
public void migrate_project_property_set_to_date() throws SQLException {
insertMainBranch(0, false);
insertProperty(0, "2019-04-05");

long reference = LocalDate.parse("2019-04-05").atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
insertSnapshot(100, 0, reference - 100);
insertSnapshot(200, 0, reference + 100);
insertSnapshot(300, 0, reference + 200);

underTest.execute();

assertThat(dbTester.countRowsOfTable(NEW_CODE_PERIODS_TABLE_NAME)).isEqualTo(1);
// we don't support specific analysis for projects, so we set it for the branch
assertNewCodePeriod(0, "pb-uuid-" + 0, "pb-uuid-" + 0, "SPECIFIC_ANALYSIS", "snapshot-200");
}

private void assertNewCodePeriod(int row, @Nullable String projectUuid, @Nullable String branchUuid, String type, @Nullable String value) {
Optional<Map<String, Object>> r = dbTester.select("SELECT PROJECT_UUID, BRANCH_UUID, TYPE, VALUE FROM " + NEW_CODE_PERIODS_TABLE_NAME)
.stream()
.skip(row)
.findFirst();

assertThat(r).isPresent();

assertThat(r.get().get("PROJECT_UUID")).isEqualTo(projectUuid);
assertThat(r.get().get("BRANCH_UUID")).isEqualTo(branchUuid);
assertThat(r.get().get("TYPE")).isEqualTo(type);
assertThat(r.get().get("VALUE")).isEqualTo(value);
}

private void insertBranch(long mainBranchUid, long uid, boolean withBaseLine) {
String manualBaseline = withBaseLine ? "mba-uuid" + uid : null;

String mainBranchProjectUuid = mainBranchUid == uid ? null : "pb-uuid-" + mainBranchUid;
insertComponent(uid, "pb-uuid-" + uid, mainBranchProjectUuid);

dbTester.executeInsert(
PROJECT_BRANCHES_TABLE_NAME,
"UUID", "pb-uuid-" + counter,
"PROJECT_UUID", "pb-uuid-" + counter,
"KEE", "pb-key-" + counter,
"UUID", "pb-uuid-" + uid,
"PROJECT_UUID", "pb-uuid-" + mainBranchUid,
"KEE", "pb-key-" + uid,
"KEY_TYPE", "TSR",
"BRANCH_TYPE", "LONG",
"MERGE_BRANCH_UUID", "mb-uuid-" + counter,
"MANUAL_BASELINE_ANALYSIS_UUID", "mba-uuid" + counter,
"MERGE_BRANCH_UUID", "mb-uuid-" + mainBranchUid,
"MANUAL_BASELINE_ANALYSIS_UUID", manualBaseline,
"CREATED_AT", System2.INSTANCE.now(),
"UPDATED_AT", System2.INSTANCE.now()
);
}

private void insertSnapshot(int uid, int branchUid, long creationDate) {
dbTester.executeInsert(
"SNAPSHOTS",
"UUID", "snapshot-" + uid,
"COMPONENT_UUID", "pb-uuid-" + branchUid,
"STATUS", "P",
"CREATED_AT", creationDate
);
}

private void insertProperty(@Nullable Integer uid, String value) {
dbTester.executeInsert(
"PROPERTIES",
"PROP_KEY", "sonar.leak.period",
"RESOURCE_ID", uid,
"USER_ID", null,
"IS_EMPTY", false,
"TEXT_VALUE", value,
"CLOB_VALUE", null,
"CREATED_AT", System2.INSTANCE.now()
);
}

private void insertComponent(long id, String uuid, @Nullable String mainBranchProjectUuid) {
dbTester.executeInsert(
"projects",
"ID", valueOf(id),
"UUID", uuid,
"ROOT_UUID", uuid,
"PROJECT_UUID", uuid,
"MAIN_BRANCH_PROJECT_UUID", mainBranchProjectUuid,
"SCOPE", Scopes.PROJECT,
"QUALIFIER", "TRK",
"NAME", "name-" + id);
}

private void insertVersionEvent(long id, long createdAt, String version) {
dbTester.executeInsert(
"events",
"ANALYSIS_UUID", "analysis-" + createdAt,
"NAME", version,
"COMPONENT_UUID", "pb-uuid-" + id,
"CATEGORY", "Version",
"CREATED_AT", createdAt);
}

private void insertMainBranch(long uid, boolean withBaseLine) {
insertBranch(uid, uid, withBaseLine);
}
}

+ 60
- 1
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v80/PopulateNewCodePeriodTableTest/schema.sql 查看文件

@@ -19,9 +19,68 @@ CREATE TABLE "NEW_CODE_PERIODS" (
"PROJECT_UUID" VARCHAR(40),
"BRANCH_UUID" VARCHAR(40),
"TYPE" VARCHAR(30) NOT NULL,
"VALUE" VARCHAR(40) NOT NULL,
"VALUE" VARCHAR(40),
"UPDATED_AT" BIGINT NOT NULL,
"CREATED_AT" BIGINT NOT NULL,

CONSTRAINT "PK_NEW_CODE_PERIOD" PRIMARY KEY ("UUID")
);

CREATE TABLE "PROPERTIES" (
"ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
"PROP_KEY" VARCHAR(512) NOT NULL,
"RESOURCE_ID" INTEGER,
"USER_ID" INTEGER,
"IS_EMPTY" BOOLEAN NOT NULL,
"TEXT_VALUE" VARCHAR(4000),
"CLOB_VALUE" CLOB,
"CREATED_AT" BIGINT
);
CREATE INDEX "PROPERTIES_KEY" ON "PROPERTIES" ("PROP_KEY");

CREATE TABLE "PROJECTS" (
"ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
"KEE" VARCHAR(400),
"UUID" VARCHAR(50) NOT NULL,
"ROOT_UUID" VARCHAR(50),
"PROJECT_UUID" VARCHAR(50) NOT NULL,
"MODULE_UUID" VARCHAR(50),
"MODULE_UUID_PATH" VARCHAR(1500),
"MAIN_BRANCH_PROJECT_UUID" VARCHAR(50),
"NAME" VARCHAR(2000),
"TAGS" VARCHAR(500),
"ENABLED" BOOLEAN NOT NULL DEFAULT TRUE,
"SCOPE" VARCHAR(3),
"QUALIFIER" VARCHAR(10)
);
CREATE UNIQUE INDEX "PROJECTS_KEE" ON "PROJECTS" ("KEE");
CREATE INDEX "PROJECTS_ROOT_UUID" ON "PROJECTS" ("ROOT_UUID");
CREATE UNIQUE INDEX "PROJECTS_UUID" ON "PROJECTS" ("UUID");
CREATE INDEX "PROJECTS_PROJECT_UUID" ON "PROJECTS" ("PROJECT_UUID");
CREATE INDEX "PROJECTS_MODULE_UUID" ON "PROJECTS" ("MODULE_UUID");
CREATE INDEX "PROJECTS_QUALIFIER" ON "PROJECTS" ("QUALIFIER");

CREATE TABLE "EVENTS" (
"ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
"NAME" VARCHAR(400),
"ANALYSIS_UUID" VARCHAR(50) NOT NULL,
"COMPONENT_UUID" VARCHAR(50) NOT NULL,
"CATEGORY" VARCHAR(50),
"CREATED_AT" BIGINT NOT NULL,
"DESCRIPTION" VARCHAR(4000),
"EVENT_DATA" VARCHAR(4000)
);
CREATE INDEX "EVENTS_ANALYSIS" ON "EVENTS" ("ANALYSIS_UUID");
CREATE INDEX "EVENTS_COMPONENT_UUID" ON "EVENTS" ("COMPONENT_UUID");

CREATE TABLE "SNAPSHOTS" (
"ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
"UUID" VARCHAR(50) NOT NULL,
"CREATED_AT" BIGINT,
"BUILD_DATE" BIGINT,
"COMPONENT_UUID" VARCHAR(50) NOT NULL,
"STATUS" VARCHAR(4) NOT NULL DEFAULT 'U',
"ISLAST" BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX "SNAPSHOT_COMPONENT" ON "SNAPSHOTS" ("COMPONENT_UUID");
CREATE UNIQUE INDEX "ANALYSES_UUID" ON "SNAPSHOTS" ("UUID");

+ 152
- 0
server/sonar-server/src/main/java/org/sonar/server/newcodeperiod/ws/ListAction.java 查看文件

@@ -0,0 +1,152 @@
/*
* 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.newcodeperiod.ws;

import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
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.web.UserRole;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.newcodeperiod.NewCodePeriodDao;
import org.sonar.db.newcodeperiod.NewCodePeriodDto;
import org.sonar.db.newcodeperiod.NewCodePeriodType;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.user.UserSession;
import org.sonarqube.ws.NewCodePeriods;
import org.sonarqube.ws.NewCodePeriods.ListWSResponse;

import static org.sonar.core.util.stream.MoreCollectors.toList;
import static org.sonar.db.component.BranchType.LONG;
import static org.sonar.server.ws.WsUtils.writeProtobuf;
import static org.sonarqube.ws.NewCodePeriods.ShowWSResponse.newBuilder;

public class ListAction implements NewCodePeriodsWsAction {
private static final String PARAM_PROJECT = "project";

private final DbClient dbClient;
private final UserSession userSession;
private final ComponentFinder componentFinder;
private final NewCodePeriodDao newCodePeriodDao;

public ListAction(DbClient dbClient, UserSession userSession, ComponentFinder componentFinder, NewCodePeriodDao newCodePeriodDao) {
this.dbClient = dbClient;
this.userSession = userSession;
this.componentFinder = componentFinder;
this.newCodePeriodDao = newCodePeriodDao;
}

@Override
public void define(WebService.NewController context) {
WebService.NewAction action = context.createAction("list")
.setDescription("List the New Code Periods for all long lived branches in a project.<br>" +
"Requires the permission to browse the project")
.setSince("8.0")
.setResponseExample(getClass().getResource("list-example.json"))
.setHandler(this);

action.createParam(PARAM_PROJECT)
.setRequired(true)
.setDescription("Project key");
}

@Override
public void handle(Request request, Response response) throws Exception {
String projectKey = request.mandatoryParam(PARAM_PROJECT);

try (DbSession dbSession = dbClient.openSession(false)) {
ComponentDto project = componentFinder.getByKey(dbSession, projectKey);
userSession.checkComponentPermission(UserRole.ADMIN, project);
Collection<BranchDto> branches = dbClient.branchDao().selectByComponent(dbSession, project).stream()
.filter(b -> b.getBranchType() == LONG)
.collect(toList());
Map<String, InheritedNewCodePeriod> newCodePeriodByBranchUuid = newCodePeriodDao
.selectAllByProject(dbSession, project.uuid())
.stream()
.collect(Collectors.toMap(NewCodePeriodDto::getBranchUuid, dto -> new InheritedNewCodePeriod(dto, dto.getBranchUuid() == null)));

InheritedNewCodePeriod projectDefault = newCodePeriodByBranchUuid.getOrDefault(null,
newCodePeriodDao.selectGlobal(dbSession)
.map(dto -> new InheritedNewCodePeriod(dto, true))
.orElse(new InheritedNewCodePeriod(NewCodePeriodDto.defaultInstance(), true))
);

ListWSResponse.Builder builder = ListWSResponse.newBuilder();
for (BranchDto branch : branches) {
InheritedNewCodePeriod inherited = newCodePeriodByBranchUuid.getOrDefault(branch.getUuid(), projectDefault);
builder.addNewCodePeriods(
build(projectKey, branch.getKey(), inherited.getType(), inherited.getValue(), inherited.inherited));
}

writeProtobuf(builder.build(), request, response);
}
}

private NewCodePeriods.ShowWSResponse build(String projectKey, String branchKey, NewCodePeriodType newCodePeriodType, @Nullable String value, boolean inherited) {
NewCodePeriods.ShowWSResponse.Builder builder = newBuilder()
.setType(convertType(newCodePeriodType))
.setInherited(inherited)
.setBranchKey(branchKey)
.setProjectKey(projectKey);

if (value != null) {
builder.setValue(value);
}

return builder.build();
}

private static NewCodePeriods.NewCodePeriodType convertType(NewCodePeriodType type) {
switch (type) {
case NUMBER_OF_DAYS:
return NewCodePeriods.NewCodePeriodType.NUMBER_OF_DAYS;
case PREVIOUS_VERSION:
return NewCodePeriods.NewCodePeriodType.PREVIOUS_VERSION;
case SPECIFIC_ANALYSIS:
return NewCodePeriods.NewCodePeriodType.SPECIFIC_ANALYSIS;
default:
throw new IllegalStateException("Unexpected type: " + type);
}
}

private static class InheritedNewCodePeriod {
NewCodePeriodDto newCodePeriod;
boolean inherited;

InheritedNewCodePeriod(NewCodePeriodDto newCodePeriod, boolean inherited) {
this.newCodePeriod = newCodePeriod;
this.inherited = inherited;
}

NewCodePeriodType getType() {
return newCodePeriod.getType();
}

String getValue() {
return newCodePeriod.getValue();
}
}
}

+ 4
- 13
server/sonar-server/src/main/java/org/sonar/server/newcodeperiod/ws/SetAction.java 查看文件

@@ -20,7 +20,7 @@
package org.sonar.server.newcodeperiod.ws;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import java.util.EnumSet;
import java.util.Locale;
import java.util.Set;
import javax.annotation.Nullable;
@@ -44,7 +44,6 @@ import org.sonar.server.user.UserSession;

import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
import static org.sonar.db.newcodeperiod.NewCodePeriodType.DATE;
import static org.sonar.db.newcodeperiod.NewCodePeriodType.NUMBER_OF_DAYS;
import static org.sonar.db.newcodeperiod.NewCodePeriodType.PREVIOUS_VERSION;
import static org.sonar.db.newcodeperiod.NewCodePeriodType.SPECIFIC_ANALYSIS;
@@ -55,9 +54,9 @@ public class SetAction implements NewCodePeriodsWsAction {
private static final String PARAM_PROJECT = "project";
private static final String PARAM_TYPE = "type";
private static final String PARAM_VALUE = "value";
private static final Set<NewCodePeriodType> OVERALL_TYPES = ImmutableSet.of(PREVIOUS_VERSION, NUMBER_OF_DAYS);
private static final Set<NewCodePeriodType> PROJECT_TYPES = ImmutableSet.of(DATE, PREVIOUS_VERSION, NUMBER_OF_DAYS);
private static final Set<NewCodePeriodType> BRANCH_TYPES = ImmutableSet.of(DATE, PREVIOUS_VERSION, NUMBER_OF_DAYS, SPECIFIC_ANALYSIS);
private static final Set<NewCodePeriodType> OVERALL_TYPES = EnumSet.of(PREVIOUS_VERSION, NUMBER_OF_DAYS);
private static final Set<NewCodePeriodType> PROJECT_TYPES = EnumSet.of(PREVIOUS_VERSION, NUMBER_OF_DAYS);
private static final Set<NewCodePeriodType> BRANCH_TYPES = EnumSet.of(PREVIOUS_VERSION, NUMBER_OF_DAYS, SPECIFIC_ANALYSIS);

private final DbClient dbClient;
private final UserSession userSession;
@@ -146,14 +145,6 @@ public class SetAction implements NewCodePeriodsWsAction {
throw new IllegalArgumentException("Failed to parse number of days: " + value);
}
break;
case DATE:
requireValue(type, value);
try {
dto.setValue(NewCodePeriodParser.parseDate(value).toString());
} catch (Exception e) {
throw new IllegalArgumentException("Failed to parse date: " + value);
}
break;
case SPECIFIC_ANALYSIS:
requireValue(type, value);
SnapshotDto analysis = getAnalysis(dbSession, value, projectBranch, branch);

+ 1
- 3
server/sonar-server/src/main/java/org/sonar/server/newcodeperiod/ws/ShowAction.java 查看文件

@@ -37,12 +37,12 @@ import org.sonar.server.component.ComponentFinder;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.user.UserSession;
import org.sonarqube.ws.NewCodePeriods;
import org.sonarqube.ws.NewCodePeriods.ShowWSResponse;

import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
import static org.sonar.server.component.ComponentFinder.ParamNames.PROJECT_ID_AND_KEY;
import static org.sonar.server.ws.WsUtils.writeProtobuf;
import static org.sonarqube.ws.NewCodePeriods.*;

public class ShowAction implements NewCodePeriodsWsAction {
private static final String PARAM_BRANCH = "branch";
@@ -154,8 +154,6 @@ public class ShowAction implements NewCodePeriodsWsAction {
switch (type) {
case NUMBER_OF_DAYS:
return NewCodePeriods.NewCodePeriodType.NUMBER_OF_DAYS;
case DATE:
return NewCodePeriods.NewCodePeriodType.DATE;
case PREVIOUS_VERSION:
return NewCodePeriods.NewCodePeriodType.PREVIOUS_VERSION;
case SPECIFIC_ANALYSIS:

+ 23
- 0
server/sonar-server/src/main/java/org/sonar/server/newcodeperiod/ws/package-info.java 查看文件

@@ -0,0 +1,23 @@
/*
* 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.
*/
@ParametersAreNonnullByDefault
package org.sonar.server.newcodeperiod.ws;

import javax.annotation.ParametersAreNonnullByDefault;

+ 0
- 0
server/sonar-server/src/main/resources/org/sonar/server/setting/ws/list_new_code_period-example.json 查看文件


+ 319
- 0
server/sonar-server/src/test/java/org/sonar/server/newcodeperiod/ws/ListActionTest.java 查看文件

@@ -0,0 +1,319 @@
/*
* 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.newcodeperiod.ws;

import java.util.Optional;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.System2;
import org.sonar.api.web.UserRole;
import org.sonar.core.util.UuidFactoryFast;
import org.sonar.db.DbClient;
import org.sonar.db.DbTester;
import org.sonar.db.component.BranchType;
import org.sonar.db.component.ComponentDbTester;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.newcodeperiod.NewCodePeriodDao;
import org.sonar.db.newcodeperiod.NewCodePeriodDbTester;
import org.sonar.db.newcodeperiod.NewCodePeriodDto;
import org.sonar.db.newcodeperiod.NewCodePeriodType;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.component.TestComponentFinder;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.WsActionTester;
import org.sonarqube.ws.NewCodePeriods;
import org.sonarqube.ws.NewCodePeriods.ListWSResponse;
import org.sonarqube.ws.NewCodePeriods.ShowWSResponse;

import static org.assertj.core.api.Assertions.assertThat;

public class ListActionTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Rule
public UserSessionRule userSession = UserSessionRule.standalone();
@Rule
public DbTester db = DbTester.create(System2.INSTANCE);

private ComponentDbTester componentDb = new ComponentDbTester(db);
private DbClient dbClient = db.getDbClient();
private ComponentFinder componentFinder = TestComponentFinder.from(db);
private NewCodePeriodDao dao = new NewCodePeriodDao(System2.INSTANCE, UuidFactoryFast.getInstance());
private NewCodePeriodDbTester tester = new NewCodePeriodDbTester(db);
private ListAction underTest = new ListAction(dbClient, userSession, componentFinder, dao);
private WsActionTester ws = new WsActionTester(underTest);

@Test
public void test_definition() {
WebService.Action definition = ws.getDef();

assertThat(definition.key()).isEqualTo("list");
assertThat(definition.isInternal()).isFalse();
assertThat(definition.since()).isEqualTo("8.0");
assertThat(definition.isPost()).isFalse();

assertThat(definition.params()).extracting(WebService.Param::key).containsOnly("project");
assertThat(definition.param("project").isRequired()).isTrue();
}

@Test
public void throw_NFE_if_project_not_found() {
expectedException.expect(NotFoundException.class);
expectedException.expectMessage("Component key 'unknown' not found");

ws.newRequest()
.setParam("project", "unknown")
.execute();
}

@Test
public void throw_FE_if_no_project_permission() {
ComponentDto project = componentDb.insertMainBranch();
expectedException.expect(ForbiddenException.class);
expectedException.expectMessage("Insufficient privileges");

ws.newRequest()
.setParam("project", project.getKey())
.execute();
}

@Test
public void list_only_LLB() {
ComponentDto project = componentDb.insertMainBranch();

createBranches(project, 5, BranchType.LONG);
createBranches(project, 3, BranchType.SHORT);

logInAsProjectAdministrator(project);

ListWSResponse response = ws.newRequest()
.setParam("project", project.getKey())
.executeProtobuf(ListWSResponse.class);

assertThat(response).isNotNull();
assertThat(response.getNewCodePeriodsCount()).isEqualTo(6);
assertThat(response.getNewCodePeriodsList()).extracting(ShowWSResponse::getBranchKey)
.contains("master", "LONG_0", "LONG_1", "LONG_2", "LONG_3", "LONG_4");

//check if global default is set
assertThat(response.getNewCodePeriodsList()).extracting(ShowWSResponse::getType)
.contains(NewCodePeriods.NewCodePeriodType.PREVIOUS_VERSION);
}

@Test
public void list_inherited_global_settings() {
ComponentDto project = componentDb.insertMainBranch();
tester.insert(new NewCodePeriodDto().setType(NewCodePeriodType.SPECIFIC_ANALYSIS).setValue("uuid"));

createBranches(project, 5, BranchType.LONG);

logInAsProjectAdministrator(project);

ListWSResponse response = ws.newRequest()
.setParam("project", project.getKey())
.executeProtobuf(ListWSResponse.class);

assertThat(response).isNotNull();
assertThat(response.getNewCodePeriodsCount()).isEqualTo(6);
assertThat(response.getNewCodePeriodsList()).extracting(ShowWSResponse::getBranchKey)
.contains("master", "LONG_0", "LONG_1", "LONG_2", "LONG_3", "LONG_4");

//check if global default is set
assertThat(response.getNewCodePeriodsList()).extracting(ShowWSResponse::getType)
.contains(NewCodePeriods.NewCodePeriodType.SPECIFIC_ANALYSIS);
assertThat(response.getNewCodePeriodsList()).extracting(ShowWSResponse::getValue)
.contains("uuid");
assertThat(response.getNewCodePeriodsList()).extracting(ShowWSResponse::getInherited)
.contains(true);
}

@Test
public void list_inherited_project_settings() {
ComponentDto projectWithOwnSettings = componentDb.insertMainBranch();
ComponentDto projectWithGlobalSettings = componentDb.insertMainBranch();
tester.insert(new NewCodePeriodDto()
.setType(NewCodePeriodType.SPECIFIC_ANALYSIS)
.setValue("global_uuid"));
tester.insert(new NewCodePeriodDto()
.setProjectUuid(projectWithOwnSettings.uuid())
.setType(NewCodePeriodType.SPECIFIC_ANALYSIS)
.setValue("project_uuid"));

createBranches(projectWithOwnSettings, 5, BranchType.LONG);

logInAsProjectAdministrator(projectWithOwnSettings, projectWithGlobalSettings);

ListWSResponse response = ws.newRequest()
.setParam("project", projectWithOwnSettings.getKey())
.executeProtobuf(ListWSResponse.class);

//verify project with project level settings
assertThat(response).isNotNull();
assertThat(response.getNewCodePeriodsCount()).isEqualTo(6);
assertThat(response.getNewCodePeriodsList()).extracting(ShowWSResponse::getBranchKey)
.contains("master", "LONG_0", "LONG_1", "LONG_2", "LONG_3", "LONG_4");

//check if project setting is set
assertThat(response.getNewCodePeriodsList()).extracting(ShowWSResponse::getType)
.contains(NewCodePeriods.NewCodePeriodType.SPECIFIC_ANALYSIS);
assertThat(response.getNewCodePeriodsList()).extracting(ShowWSResponse::getValue)
.containsOnly("project_uuid");
assertThat(response.getNewCodePeriodsList()).extracting(ShowWSResponse::getInherited)
.containsOnly(true);

//verify project with global level settings
response = ws.newRequest()
.setParam("project", projectWithGlobalSettings.getKey())
.executeProtobuf(ListWSResponse.class);

assertThat(response).isNotNull();
assertThat(response.getNewCodePeriodsCount()).isEqualTo(1);
assertThat(response.getNewCodePeriodsList()).extracting(ShowWSResponse::getBranchKey)
.containsOnly("master");

//check if global setting is set
assertThat(response.getNewCodePeriodsList()).extracting(ShowWSResponse::getType)
.contains(NewCodePeriods.NewCodePeriodType.SPECIFIC_ANALYSIS);
assertThat(response.getNewCodePeriodsList()).extracting(ShowWSResponse::getValue)
.contains("global_uuid");
assertThat(response.getNewCodePeriodsList()).extracting(ShowWSResponse::getInherited)
.containsOnly(true);
}

@Test
public void list_branch_and_inherited_global_settings() {
ComponentDto project = componentDb.insertMainBranch();
ComponentDto branchWithOwnSettings = componentDb.insertProjectBranch(project, branchDto -> branchDto.setKey("OWN_SETTINGS"));
componentDb.insertProjectBranch(project, branchDto -> branchDto.setKey("GLOBAL_SETTINGS"));

tester.insert(new NewCodePeriodDto()
.setType(NewCodePeriodType.SPECIFIC_ANALYSIS)
.setValue("global_uuid"));

tester.insert(new NewCodePeriodDto()
.setProjectUuid(project.uuid())
.setBranchUuid(branchWithOwnSettings.uuid())
.setType(NewCodePeriodType.SPECIFIC_ANALYSIS)
.setValue("branch_uuid"));

logInAsProjectAdministrator(project);

ListWSResponse response = ws.newRequest()
.setParam("project", project.getKey())
.executeProtobuf(ListWSResponse.class);

assertThat(response).isNotNull();
assertThat(response.getNewCodePeriodsCount()).isEqualTo(3);
assertThat(response.getNewCodePeriodsList()).extracting(ShowWSResponse::getBranchKey)
.contains("master", "OWN_SETTINGS", "GLOBAL_SETTINGS");

Optional<ShowWSResponse> ownSettings = response.getNewCodePeriodsList().stream()
.filter(s -> !s.getInherited())
.findFirst();

assertThat(ownSettings).isNotNull();
assertThat(ownSettings).isNotEmpty();
assertThat(ownSettings.get().getProjectKey()).isEqualTo(project.getKey());
assertThat(ownSettings.get().getBranchKey()).isEqualTo("OWN_SETTINGS");
assertThat(ownSettings.get().getType()).isEqualTo(NewCodePeriods.NewCodePeriodType.SPECIFIC_ANALYSIS);
assertThat(ownSettings.get().getValue()).isEqualTo("branch_uuid");
assertThat(ownSettings.get().getInherited()).isFalse();

//check if global default is set
assertThat(response.getNewCodePeriodsList())
.filteredOn(ShowWSResponse::getInherited)
.extracting(ShowWSResponse::getType)
.contains(NewCodePeriods.NewCodePeriodType.SPECIFIC_ANALYSIS);
assertThat(response.getNewCodePeriodsList())
.filteredOn(ShowWSResponse::getInherited)
.extracting(ShowWSResponse::getValue)
.contains("global_uuid");
}

@Test
public void list_branch_and_inherited_project_settings() {
ComponentDto project = componentDb.insertMainBranch();
ComponentDto branchWithOwnSettings = componentDb.insertProjectBranch(project, branchDto -> branchDto.setKey("OWN_SETTINGS"));
componentDb.insertProjectBranch(project, branchDto -> branchDto.setKey("PROJECT_SETTINGS"));

tester.insert(new NewCodePeriodDto()
.setType(NewCodePeriodType.SPECIFIC_ANALYSIS)
.setValue("global_uuid"));

tester.insert(new NewCodePeriodDto()
.setProjectUuid(project.uuid())
.setType(NewCodePeriodType.SPECIFIC_ANALYSIS)
.setValue("project_uuid"));

tester.insert(new NewCodePeriodDto()
.setProjectUuid(project.uuid())
.setBranchUuid(branchWithOwnSettings.uuid())
.setType(NewCodePeriodType.SPECIFIC_ANALYSIS)
.setValue("branch_uuid"));

logInAsProjectAdministrator(project);

ListWSResponse response = ws.newRequest()
.setParam("project", project.getKey())
.executeProtobuf(ListWSResponse.class);

assertThat(response).isNotNull();
assertThat(response.getNewCodePeriodsCount()).isEqualTo(3);
assertThat(response.getNewCodePeriodsList()).extracting(ShowWSResponse::getBranchKey)
.contains("master", "OWN_SETTINGS", "PROJECT_SETTINGS");

Optional<ShowWSResponse> ownSettings = response.getNewCodePeriodsList().stream()
.filter(s -> !s.getInherited())
.findFirst();

assertThat(ownSettings).isNotNull();
assertThat(ownSettings).isNotEmpty();
assertThat(ownSettings.get().getProjectKey()).isEqualTo(project.getKey());
assertThat(ownSettings.get().getBranchKey()).isEqualTo("OWN_SETTINGS");
assertThat(ownSettings.get().getType()).isEqualTo(NewCodePeriods.NewCodePeriodType.SPECIFIC_ANALYSIS);
assertThat(ownSettings.get().getValue()).isEqualTo("branch_uuid");
assertThat(ownSettings.get().getInherited()).isFalse();

//check if global default is set
assertThat(response.getNewCodePeriodsList())
.filteredOn(ShowWSResponse::getInherited)
.extracting(ShowWSResponse::getType)
.contains(NewCodePeriods.NewCodePeriodType.SPECIFIC_ANALYSIS);
assertThat(response.getNewCodePeriodsList())
.filteredOn(ShowWSResponse::getInherited)
.extracting(ShowWSResponse::getValue)
.contains("project_uuid");
}

private void createBranches(ComponentDto project, int numberOfBranches, BranchType branchType) {
for (int branchCount = 0; branchCount < numberOfBranches; branchCount++) {
String branchKey = String.format("%s_%d", branchType.name(), branchCount);
componentDb.insertProjectBranch(project, branchDto -> branchDto.setKey(branchKey).setBranchType(branchType));
}
}

private void logInAsProjectAdministrator(ComponentDto... project) {
userSession.logIn().addProjectPermission(UserRole.ADMIN, project);
}
}

+ 3
- 32
server/sonar-server/src/test/java/org/sonar/server/newcodeperiod/ws/SetActionTest.java 查看文件

@@ -102,10 +102,10 @@ public class SetActionTest {
@Test
public void throw_IAE_if_type_is_invalid_for_global() {
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("Invalid type 'DATE'. Overall setting can only be set with types: [PREVIOUS_VERSION, NUMBER_OF_DAYS]");
expectedException.expectMessage("Invalid type 'SPECIFIC_ANALYSIS'. Overall setting can only be set with types: [PREVIOUS_VERSION, NUMBER_OF_DAYS]");

ws.newRequest()
.setParam("type", "date")
.setParam("type", "specific_analysis")
.execute();
}

@@ -115,7 +115,7 @@ public class SetActionTest {
logInAsProjectAdministrator(project);

expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("Invalid type 'SPECIFIC_ANALYSIS'. Projects can only be set with types: [DATE, PREVIOUS_VERSION, NUMBER_OF_DAYS]");
expectedException.expectMessage("Invalid type 'SPECIFIC_ANALYSIS'. Projects can only be set with types: [PREVIOUS_VERSION, NUMBER_OF_DAYS]");

ws.newRequest()
.setParam("project", project.getKey())
@@ -123,20 +123,6 @@ public class SetActionTest {
.execute();
}

// validation of value
@Test
public void throw_IAE_if_no_value_for_date() {
ComponentDto project = componentDb.insertPublicProject();
logInAsProjectAdministrator(project);
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("New Code Period type 'DATE' requires a value");

ws.newRequest()
.setParam("project", project.getKey())
.setParam("type", "date")
.execute();
}

@Test
public void throw_IAE_if_no_value_for_days() {
ComponentDto project = componentDb.insertMainBranch();
@@ -165,21 +151,6 @@ public class SetActionTest {
.execute();
}

@Test
public void throw_IAE_if_date_is_invalid() {
ComponentDto project = componentDb.insertMainBranch();
logInAsProjectAdministrator(project);
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("Failed to parse date: unknown");

ws.newRequest()
.setParam("project", project.getKey())
.setParam("type", "date")
.setParam("branch", "master")
.setParam("value", "unknown")
.execute();
}

@Test
public void throw_IAE_if_days_is_invalid() {
ComponentDto project = componentDb.insertMainBranch();

+ 6
- 6
server/sonar-server/src/test/java/org/sonar/server/newcodeperiod/ws/ShowActionTest.java 查看文件

@@ -173,15 +173,15 @@ public class ShowActionTest {
tester.insert(new NewCodePeriodDto()
.setProjectUuid(project.uuid())
.setBranchUuid(branch.uuid())
.setType(NewCodePeriodType.DATE)
.setValue("2018-04-05"));
.setType(NewCodePeriodType.NUMBER_OF_DAYS)
.setValue("1"));

ShowWSResponse response = ws.newRequest()
.setParam("project", project.getKey())
.setParam("branch", "branch")
.executeProtobuf(ShowWSResponse.class);

assertResponse(response, project.getKey(), "branch", NewCodePeriods.NewCodePeriodType.DATE, "2018-04-05", false);
assertResponse(response, project.getKey(), "branch", NewCodePeriods.NewCodePeriodType.NUMBER_OF_DAYS, "1", false);
}

@Test
@@ -206,15 +206,15 @@ public class ShowActionTest {

tester.insert(new NewCodePeriodDto()
.setProjectUuid(project.uuid())
.setType(NewCodePeriodType.DATE)
.setValue("2018-04-05"));
.setType(NewCodePeriodType.NUMBER_OF_DAYS)
.setValue("1"));

ShowWSResponse response = ws.newRequest()
.setParam("project", project.getKey())
.setParam("branch", "branch")
.executeProtobuf(ShowWSResponse.class);

assertResponse(response, project.getKey(), "branch", NewCodePeriods.NewCodePeriodType.DATE, "2018-04-05", true);
assertResponse(response, project.getKey(), "branch", NewCodePeriods.NewCodePeriodType.NUMBER_OF_DAYS, "1", true);
}

@Test

+ 9
- 7
server/sonar-server/src/test/java/org/sonar/server/newcodeperiod/ws/UnsetActionTest.java 查看文件

@@ -173,20 +173,22 @@ public class UnsetActionTest {

@Test
public void delete_project_period_twice() {
ComponentDto project = componentDb.insertMainBranch();
db.newCodePeriods().insert(project.uuid(), null, NewCodePeriodType.SPECIFIC_ANALYSIS, "uuid");
ComponentDto project1 = componentDb.insertMainBranch();
ComponentDto project2 = componentDb.insertMainBranch();
db.newCodePeriods().insert(project1.uuid(), null, NewCodePeriodType.SPECIFIC_ANALYSIS, "uuid1");
db.newCodePeriods().insert(project2.uuid(), null, NewCodePeriodType.SPECIFIC_ANALYSIS, "uuid2");

logInAsProjectAdministrator(project);
logInAsProjectAdministrator(project1);
ws.newRequest()
.setParam("project", project.getKey())
.setParam("project", project1.getKey())
.execute();
assertTableEmpty();
assertTableContainsOnly(project2.uuid(), null, NewCodePeriodType.SPECIFIC_ANALYSIS, "uuid2");

ws.newRequest()
.setParam("project", project.getKey())
.setParam("project", project1.getKey())
.execute();

assertTableEmpty();
assertTableContainsOnly(project2.uuid(), null, NewCodePeriodType.SPECIFIC_ANALYSIS, "uuid2");
}

@Test

+ 0
- 58
sonar-ws/src/main/java/org/sonarqube/ws/client/settings/DeleteNewCodePeriodRequest.java 查看文件

@@ -1,58 +0,0 @@
/*
* 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.sonarqube.ws.client.settings;

import java.util.List;
import javax.annotation.Generated;

/**
* This is part of the internal API.
* This is a POST request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/settings/delete_new_code_period">Further information about this action online (including a response example)</a>
* @since 8.0
*/
@Generated("sonar-ws-generator")
public class DeleteNewCodePeriodRequest {

private String branch;
private String project;

/**
*/
public DeleteNewCodePeriodRequest setBranch(String branch) {
this.branch = branch;
return this;
}

public String getBranch() {
return branch;
}

/**
*/
public DeleteNewCodePeriodRequest setProject(String project) {
this.project = project;
return this;
}

public String getProject() {
return project;
}
}

+ 0
- 58
sonar-ws/src/main/java/org/sonarqube/ws/client/settings/ShowNewCodePeriodRequest.java 查看文件

@@ -1,58 +0,0 @@
/*
* 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.sonarqube.ws.client.settings;

import java.util.List;
import javax.annotation.Generated;

/**
* This is part of the internal API.
* This is a POST request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/settings/show_new_code_period">Further information about this action online (including a response example)</a>
* @since 8.0
*/
@Generated("sonar-ws-generator")
public class ShowNewCodePeriodRequest {

private String branch;
private String project;

/**
*/
public ShowNewCodePeriodRequest setBranch(String branch) {
this.branch = branch;
return this;
}

public String getBranch() {
return branch;
}

/**
*/
public ShowNewCodePeriodRequest setProject(String project) {
this.project = project;
return this;
}

public String getProject() {
return project;
}
}

+ 0
- 86
sonar-ws/src/main/java/org/sonarqube/ws/client/settings/UpdateNewCodePeriodRequest.java 查看文件

@@ -1,86 +0,0 @@
/*
* 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.sonarqube.ws.client.settings;

import javax.annotation.Generated;

/**
* This is part of the internal API.
* This is a POST request.
*
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/settings/update_new_code_period">Further information about this action online (including a response example)</a>
* @since 8.0
*/
@Generated("sonar-ws-generator")
public class UpdateNewCodePeriodRequest {

private String branch;
private String project;
private String type;
private String value;

/**
*
*/
public UpdateNewCodePeriodRequest setBranch(String branch) {
this.branch = branch;
return this;
}

public String getBranch() {
return branch;
}

/**
*
*/
public UpdateNewCodePeriodRequest setProject(String project) {
this.project = project;
return this;
}

public String getProject() {
return project;
}

/**
* This is a mandatory parameter.
*/
public UpdateNewCodePeriodRequest setType(String type) {
this.type = type;
return this;
}

public String getType() {
return type;
}

/**
*
*/
public UpdateNewCodePeriodRequest setValue(String value) {
this.value = value;
return this;
}

public String getValue() {
return value;
}
}

+ 12
- 9
sonar-ws/src/main/protobuf/ws-newcodeperiods.proto 查看文件

@@ -16,27 +16,30 @@
// along with this program; if not, write to the Free Software Foundation,
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

syntax = "proto2";
syntax = "proto3";

package sonarqube.ws.batch;

option java_package = "org.sonarqube.ws";
option java_outer_classname = "NewCodePeriods";

option optimize_for = SPEED;

// WS api/new_code_periods/show
message ShowWSResponse {
optional string projectKey = 1;
optional string branchKey = 2;
required NewCodePeriodType type = 3;
optional string value = 4;
required bool inherited = 5;
string projectKey = 1;
string branchKey = 2;
NewCodePeriodType type = 3;
string value = 4;
bool inherited = 5;
}

// WS api/new_code_periods/list
message ListWSResponse {
repeated ShowWSResponse newCodePeriods = 1;
}

enum NewCodePeriodType {
PREVIOUS_VERSION = 0;
NUMBER_OF_DAYS = 1;
DATE = 2;
SPECIFIC_ANALYSIS = 3;
SPECIFIC_ANALYSIS = 2;
}

Loading…
取消
儲存