]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10555 fail analysis if leak period is invalid
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Fri, 24 Aug 2018 11:58:10 +0000 (13:58 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 3 Oct 2018 07:28:22 +0000 (09:28 +0200)
12 files changed:
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PeriodResolver.java [deleted file]
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepTest.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/SnapshotDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/SnapshotMapper.java
server/sonar-db-dao/src/main/java/org/sonar/db/event/EventDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/event/EventMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/component/SnapshotMapper.xml
server/sonar-db-dao/src/main/resources/org/sonar/db/event/EventMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/component/SnapshotDaoTest.java
sonar-core/src/main/resources/org/sonar/l10n/core.properties
sonar-plugin-api/src/main/java/org/sonar/api/utils/log/LogAndArguments.java

index 7e11474941aa8d6cb27eca017c3669db4171d90b..8fe58c7ec6bfa75c2efa5337e6520d88acdbee91 100644 (file)
  */
 package org.sonar.ce.task.projectanalysis.step;
 
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Supplier;
 import javax.annotation.CheckForNull;
-import org.sonar.api.config.Configuration;
+import javax.annotation.Nullable;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.System2;
+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.Component;
-import org.sonar.ce.task.projectanalysis.component.ComponentVisitor;
 import org.sonar.ce.task.projectanalysis.component.ConfigurationRepository;
-import org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit;
-import org.sonar.ce.task.projectanalysis.component.DepthTraversalTypeAwareCrawler;
 import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
-import org.sonar.ce.task.projectanalysis.component.TypeAwareVisitorAdapter;
 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 static com.google.common.base.Preconditions.checkState;
+import static java.lang.String.format;
+import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD;
+import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD_MODE_DATE;
+import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD_MODE_DAYS;
+import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD_MODE_PREVIOUS_VERSION;
+import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD_MODE_VERSION;
+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}
@@ -42,62 +66,257 @@ import org.sonar.db.DbSession;
  * Here is how these periods are computed :
  * - Read the period property ${@link org.sonar.core.config.CorePropertyDefinitions#LEAK_PERIOD}
  * - Try to find the matching snapshots from the property
- * - If a snapshot is found, a period is set to the repository
+ * - 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 DbClient dbClient;
-  private final ConfigurationRepository configRepository;
   private final TreeRootHolder treeRootHolder;
   private final PeriodHolderImpl periodsHolder;
+  private final System2 system2;
+  private final DbClient dbClient;
+  private final ConfigurationRepository configRepository;
 
-  public LoadPeriodsStep(AnalysisMetadataHolder analysisMetadataHolder, DbClient dbClient,
-    ConfigurationRepository configurationRepository, TreeRootHolder treeRootHolder, PeriodHolderImpl periodsHolder) {
+  public LoadPeriodsStep(AnalysisMetadataHolder analysisMetadataHolder, TreeRootHolder treeRootHolder, PeriodHolderImpl periodsHolder,
+    System2 system2, DbClient dbClient, ConfigurationRepository configRepository) {
     this.analysisMetadataHolder = analysisMetadataHolder;
-    this.dbClient = dbClient;
-    this.configRepository = configurationRepository;
     this.treeRootHolder = treeRootHolder;
     this.periodsHolder = periodsHolder;
+    this.system2 = system2;
+    this.dbClient = dbClient;
+    this.configRepository = configRepository;
+  }
+
+  @Override
+  public String getDescription() {
+    return "Load new code period";
   }
 
   @Override
   public void execute(ComputationStep.Context context) {
     if (analysisMetadataHolder.isFirstAnalysis()) {
+      periodsHolder.setPeriod(null);
       return;
     }
 
-    new DepthTraversalTypeAwareCrawler(
-      new TypeAwareVisitorAdapter(CrawlerDepthLimit.PROJECT, ComponentVisitor.Order.PRE_ORDER) {
-        @Override
-        public void visitProject(Component project) {
-          execute(project);
-        }
-      }).visit(treeRootHolder.getRoot());
+    periodsHolder.setPeriod(resolvePeriod(treeRootHolder.getRoot()).orElse(null));
+  }
+
+  private Optional<Period> resolvePeriod(Component projectOrView) {
+    String currentVersion = projectOrView.getProjectAttributes().getVersion();
+    Optional<String> propertyValue = configRepository.getConfiguration().get(LEAK_PERIOD)
+      .filter(t -> !t.isEmpty());
+    checkPeriodProperty(propertyValue.isPresent(), "", "property is undefined or value is empty");
+
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      return resolve(dbSession, projectOrView.getUuid(), currentVersion, propertyValue.get());
+    }
   }
 
-  public void execute(Component projectOrView) {
+  public Optional<Period> resolve(String projectUuid, String analysisProjectVersion) {
+    Optional<String> propertyValue = configRepository.getConfiguration().get(LEAK_PERIOD)
+      .filter(t -> !t.isEmpty());
+    checkPeriodProperty(propertyValue.isPresent(), "", "property is undefined or value is empty");
+
     try (DbSession dbSession = dbClient.openSession(false)) {
-      periodsHolder.setPeriod(buildPeriod(projectOrView, dbSession));
+      return resolve(dbSession, projectUuid, analysisProjectVersion, propertyValue.get());
+    }
+  }
+
+  private Optional<Period> resolve(DbSession dbSession, String projectUuid, String analysisProjectVersion, String propertyValue) {
+    Integer days = parseDaysQuietly(propertyValue);
+    if (days != null) {
+      return resolveByDays(dbSession, projectUuid, days, propertyValue);
     }
+    Date date = parseDate(propertyValue);
+    if (date != null) {
+      return resolveByDate(dbSession, projectUuid, date, propertyValue);
+    }
+
+    List<EventDto> versions = dbClient.eventDao().selectVersionsByMostRecentFirst(dbSession, projectUuid);
+    if (versions.isEmpty()) {
+      return resolveWhenNoExistingVersion(dbSession, projectUuid, analysisProjectVersion, propertyValue);
+    }
+
+    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 resolveWhenOnlyOneExistingVersion(dbSession, projectUuid, mostRecentVersion, propertyValue);
+    }
+
+    boolean previousVersionPeriod = LEAK_PERIOD_MODE_PREVIOUS_VERSION.equals(propertyValue);
+    if (previousVersionPeriod) {
+      return resolvePreviousVersion(dbSession, analysisProjectVersion, versions, mostRecentVersion);
+    }
+
+    return resolveVersion(dbSession, versions, propertyValue);
   }
 
   @CheckForNull
-  private Period buildPeriod(Component projectOrView, DbSession session) {
-    PeriodResolver periodResolver = new PeriodResolver(dbClient, session, projectOrView.getUuid(), analysisMetadataHolder.getAnalysisDate(),
-      projectOrView.getProjectAttributes().getVersion());
-
-    Configuration config = configRepository.getConfiguration();
-    Period period = periodResolver.resolve(config);
-    // SONAR-4700 Add a past snapshot only if it exists
-    if (period != null) {
-      return period;
+  private static Date parseDate(String propertyValue) {
+    try {
+      LocalDate localDate = LocalDate.parse(propertyValue);
+      return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
+    } catch (DateTimeParseException e) {
+      boolean invalidDate = e.getCause() == null || e.getCause() == e || !e.getCause().getMessage().contains("Invalid date");
+      checkPeriodProperty(invalidDate, propertyValue, "Invalid date");
+      return null;
     }
-    return null;
   }
 
-  @Override
-  public String getDescription() {
-    return "Load new code period";
+  private Optional<Period> resolveByDays(DbSession dbSession, String projectUuid, 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(projectUuid).setCreatedBefore(analysisDate).setSort(BY_DATE, ASC));
+    ensureNotOnFirstAnalysis(!snapshots.isEmpty());
+
+    long targetDate = DateUtils.addDays(new Date(analysisDate), -days).getTime();
+    LOG.debug("Resolving new code period by {} days: {}", days, async(() -> logDate(targetDate)));
+    SnapshotDto snapshot = findNearestSnapshotToTargetDate(snapshots, targetDate);
+
+    return Optional.of(newPeriod(LEAK_PERIOD_MODE_DAYS, String.valueOf((int) days), snapshot));
+  }
+
+  private Optional<Period> resolveByDate(DbSession dbSession, String projectUuid, Date date, String propertyValue) {
+    long now = system2.now();
+    checkPeriodProperty(date.compareTo(new Date(now)) <= 0, propertyValue,
+      "date is in the future (now: '%s')", async(() -> logDate(now)));
+
+    LOG.debug("Resolving new code period by date: {}", async(() -> logDate(date)));
+    Optional<Period> period = findFirstSnapshot(dbSession, createCommonQuery(projectUuid).setCreatedAfter(date.getTime()).setSort(BY_DATE, ASC))
+      .map(dto -> newPeriod(LEAK_PERIOD_MODE_DATE, DateUtils.formatDate(date), dto));
+
+    checkPeriodProperty(period.isPresent(), propertyValue, "No analysis found created after date '%s'", async(() -> logDate(date)));
+
+    return period;
+  }
+
+  private Optional<Period> resolveWhenNoExistingVersion(DbSession dbSession, String projectUuid, String currentVersion, String propertyValue) {
+    LOG.debug("Resolving first analysis as new code period as there is no existing version");
+
+    boolean previousVersionPeriod = LEAK_PERIOD_MODE_PREVIOUS_VERSION.equals(propertyValue);
+    boolean currentVersionPeriod = currentVersion.equals(propertyValue);
+    checkPeriodProperty(previousVersionPeriod || currentVersionPeriod, propertyValue,
+      "No existing version. Property should be either '%s' or the current version '%s' (actual: '%s')",
+      LEAK_PERIOD_MODE_PREVIOUS_VERSION, currentVersion, propertyValue);
+
+    String periodMode = previousVersionPeriod ? LEAK_PERIOD_MODE_PREVIOUS_VERSION : LEAK_PERIOD_MODE_VERSION;
+    return findOldestAnalysis(dbSession, periodMode, projectUuid);
+  }
+
+  private Optional<Period> resolveWhenOnlyOneExistingVersion(DbSession dbSession, String projectUuid, String mostRecentVersion, String propertyValue) {
+    boolean previousVersionPeriod = LEAK_PERIOD_MODE_PREVIOUS_VERSION.equals(propertyValue);
+    LOG.debug("Resolving first analysis as new code period as there is only one existing version");
+
+    // only one existing version. Period must either be PREVIOUS_VERSION or the only valid version: the only existing one
+    checkPeriodProperty(previousVersionPeriod || propertyValue.equals(mostRecentVersion), propertyValue,
+      "Only one existing version, but period is neither %s nor this one version '%s' (actual: '%s')",
+      LEAK_PERIOD_MODE_PREVIOUS_VERSION, mostRecentVersion, propertyValue);
+
+    return findOldestAnalysis(dbSession, LEAK_PERIOD_MODE_PREVIOUS_VERSION, projectUuid);
+  }
+
+  private Optional<Period> findOldestAnalysis(DbSession dbSession, String periodMode, String projectUuid) {
+    Optional<Period> period = dbClient.snapshotDao().selectOldestSnapshot(dbSession, projectUuid)
+      .map(dto -> newPeriod(periodMode, null, dto));
+    ensureNotOnFirstAnalysis(period.isPresent());
+    return period;
+  }
+
+  private Optional<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, LEAK_PERIOD_MODE_PREVIOUS_VERSION, previousVersion);
+  }
+
+  private Optional<Period> resolveVersion(DbSession dbSession, List<EventDto> versions, String propertyValue) {
+    LOG.debug("Resolving new code period by version: {}", propertyValue);
+    Optional<EventDto> version = versions.stream().filter(t -> propertyValue.equals(t.getName())).findFirst();
+    checkPeriodProperty(version.isPresent(), propertyValue,
+      "version is none of the existing ones: %s", async(() -> toVersions(versions)));
+
+    return newPeriod(dbSession, LEAK_PERIOD_MODE_VERSION, version.get());
+  }
+
+  private Optional<Period> newPeriod(DbSession dbSession, String periodMode, EventDto previousVersion) {
+    Optional<Period> period = dbClient.snapshotDao().selectByUuid(dbSession, previousVersion.getAnalysisUuid())
+      .map(dto -> newPeriod(periodMode, previousVersion.getName(), dto));
+    if (!period.isPresent()) {
+      throw new IllegalStateException(format("Analysis '%s' for version event '%s' has been deleted",
+        previousVersion.getAnalysisUuid(), previousVersion.getName()));
+    }
+    return period;
+  }
+
+  private static String toVersions(List<EventDto> versions) {
+    return Arrays.toString(versions.stream().map(EventDto::getName).toArray(String[]::new));
+  }
+
+  public static Object async(Supplier<String> s) {
+    return new Object() {
+      @Override
+      public String toString() {
+        return s.get();
+      }
+    };
+  }
+
+  private static Period newPeriod(String mode, @Nullable String modeParameter, SnapshotDto dto) {
+    return new Period(mode, modeParameter, dto.getCreatedAt(), dto.getUuid());
+  }
+
+  private static void checkPeriodProperty(boolean test, String propertyValue, String testDescription, Object... args) {
+    if (!test) {
+      LOG.debug("Invalid code period '{}': {}", propertyValue, async(() -> 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 Optional<SnapshotDto> findFirstSnapshot(DbSession session, SnapshotQuery query) {
+    return dbClient.snapshotDao().selectAnalysesByQuery(session, query)
+      .stream()
+      .findFirst();
+  }
+
+  private static void ensureNotOnFirstAnalysis(boolean expression) {
+    checkState(expression, "Attempting to resolve period while no analysis exist for project");
+  }
+
+  @CheckForNull
+  private static Integer parseDaysQuietly(String property) {
+    try {
+      return Integer.parseInt(property);
+    } catch (NumberFormatException e) {
+      // Nothing to, it means that the property is not a number of days
+      return null;
+    }
+  }
+
+  private static SnapshotDto findNearestSnapshotToTargetDate(List<SnapshotDto> snapshots, Long targetDate) {
+    long bestDistance = Long.MAX_VALUE;
+    SnapshotDto nearest = null;
+    for (SnapshotDto snapshot : snapshots) {
+      long distance = Math.abs(snapshot.getCreatedAt() - targetDate);
+      if (distance <= bestDistance) {
+        bestDistance = distance;
+        nearest = snapshot;
+      }
+    }
+    return nearest;
+  }
+
+  private static SnapshotQuery createCommonQuery(String projectUuid) {
+    return new SnapshotQuery().setComponentUuid(projectUuid).setStatus(STATUS_PROCESSED);
+  }
+
+  private static String logDate(long date) {
+    return logDate(new Date(date));
+  }
+
+  private static String logDate(Date date1) {
+    return DateUtils.formatDate(Date.from(date1.toInstant().truncatedTo(ChronoUnit.SECONDS)));
   }
 }
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PeriodResolver.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PeriodResolver.java
deleted file mode 100644 (file)
index 0f62851..0000000
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.step;
-
-import java.time.temporal.ChronoUnit;
-import java.util.Date;
-import java.util.List;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-import org.apache.commons.lang.StringUtils;
-import org.sonar.api.config.Configuration;
-import org.sonar.api.utils.DateUtils;
-import org.sonar.api.utils.log.Logger;
-import org.sonar.api.utils.log.Loggers;
-import org.sonar.ce.task.projectanalysis.period.Period;
-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.ce.task.projectanalysis.period.Period;
-
-import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD;
-import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD_MODE_DATE;
-import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD_MODE_DAYS;
-import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD_MODE_PREVIOUS_VERSION;
-import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD_MODE_VERSION;
-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;
-import static org.sonar.db.component.SnapshotQuery.SORT_ORDER.DESC;
-
-public class PeriodResolver {
-  private static final Logger LOG = Loggers.get(PeriodResolver.class);
-
-  private final DbClient dbClient;
-  private final DbSession session;
-  private final String projectUuid;
-  private final long analysisDate;
-  @CheckForNull
-  private final String currentVersion;
-
-  public PeriodResolver(DbClient dbClient, DbSession session, String projectUuid, long analysisDate, @Nullable String currentVersion) {
-    this.dbClient = dbClient;
-    this.session = session;
-    this.projectUuid = projectUuid;
-    this.analysisDate = analysisDate;
-    this.currentVersion = currentVersion;
-  }
-
-  @CheckForNull
-  public Period resolve(Configuration config) {
-    String propertyValue = getPropertyValue(config);
-    if (StringUtils.isBlank(propertyValue)) {
-      return null;
-    }
-    Period period = resolve(propertyValue);
-    if (period == null && StringUtils.isNotBlank(propertyValue)) {
-      LOG.debug("Property " + LEAK_PERIOD + " is not valid: " + propertyValue);
-    }
-    return period;
-  }
-
-  @CheckForNull
-  private Period resolve(String property) {
-    Integer days = tryToResolveByDays(property);
-    if (days != null) {
-      return findByDays(days);
-    }
-    Date date = DateUtils.parseDateQuietly(property);
-    if (date != null) {
-      return findByDate(date);
-    }
-    if (StringUtils.equals(LEAK_PERIOD_MODE_PREVIOUS_VERSION, property)) {
-      return findByPreviousVersion();
-    }
-    return findByVersion(property);
-  }
-
-  private Period findByDate(Date date) {
-    SnapshotDto snapshot = findFirstSnapshot(session, createCommonQuery(projectUuid).setCreatedAfter(date.getTime()).setSort(BY_DATE, ASC));
-    if (snapshot == null) {
-      return null;
-    }
-    LOG.debug("Compare to date {} (analysis of {})", formatDate(date.getTime()), formatDate(snapshot.getCreatedAt()));
-    return new Period(LEAK_PERIOD_MODE_DATE, DateUtils.formatDate(date), snapshot.getCreatedAt(), snapshot.getUuid());
-  }
-
-  @CheckForNull
-  private Period findByDays(int days) {
-    List<SnapshotDto> snapshots = dbClient.snapshotDao().selectAnalysesByQuery(session, createCommonQuery(projectUuid).setCreatedBefore(analysisDate).setSort(BY_DATE, ASC));
-    long targetDate = DateUtils.addDays(new Date(analysisDate), -days).getTime();
-    SnapshotDto snapshot = findNearestSnapshotToTargetDate(snapshots, targetDate);
-    if (snapshot == null) {
-      return null;
-    }
-    LOG.debug("Compare over {} days ({}, analysis of {})", String.valueOf(days), formatDate(targetDate), formatDate(snapshot.getCreatedAt()));
-    return new Period(LEAK_PERIOD_MODE_DAYS, String.valueOf(days), snapshot.getCreatedAt(), snapshot.getUuid());
-  }
-
-  @CheckForNull
-  private Period findByPreviousVersion() {
-    if (currentVersion == null) {
-      return null;
-    }
-    List<SnapshotDto> snapshotDtos = dbClient.snapshotDao().selectPreviousVersionSnapshots(session, projectUuid, currentVersion);
-    if (snapshotDtos.isEmpty()) {
-      // If no previous version is found, the first analysis is returned
-      return findByFirstAnalysis();
-    }
-    SnapshotDto snapshotDto = snapshotDtos.get(0);
-    LOG.debug("Compare to previous version ({})", formatDate(snapshotDto.getCreatedAt()));
-    return new Period(LEAK_PERIOD_MODE_PREVIOUS_VERSION, snapshotDto.getVersion(), snapshotDto.getCreatedAt(), snapshotDto.getUuid());
-  }
-
-  @CheckForNull
-  private Period findByFirstAnalysis() {
-    SnapshotDto snapshotDto = dbClient.snapshotDao().selectOldestSnapshot(session, projectUuid);
-    if (snapshotDto == null) {
-      return null;
-    }
-    LOG.debug("Compare to first analysis ({})", formatDate(snapshotDto.getCreatedAt()));
-    return new Period(LEAK_PERIOD_MODE_PREVIOUS_VERSION, null, snapshotDto.getCreatedAt(), snapshotDto.getUuid());
-  }
-
-  @CheckForNull
-  private Period findByVersion(String version) {
-    SnapshotDto snapshot = findFirstSnapshot(session, createCommonQuery(projectUuid).setVersion(version).setSort(BY_DATE, DESC));
-    if (snapshot == null) {
-      return null;
-    }
-    LOG.debug("Compare to version ({}) ({})", version, formatDate(snapshot.getCreatedAt()));
-    return new Period(LEAK_PERIOD_MODE_VERSION, version, snapshot.getCreatedAt(), snapshot.getUuid());
-  }
-
-  @CheckForNull
-  private SnapshotDto findFirstSnapshot(DbSession session, SnapshotQuery query) {
-    List<SnapshotDto> snapshots = dbClient.snapshotDao().selectAnalysesByQuery(session, query);
-    if (!snapshots.isEmpty()) {
-      return snapshots.get(0);
-    }
-    return null;
-  }
-
-  @CheckForNull
-  private static Integer tryToResolveByDays(String property) {
-    try {
-      return Integer.parseInt(property);
-    } catch (NumberFormatException e) {
-      // Nothing to, it means that the property is not a number of days
-      return null;
-    }
-  }
-
-  @CheckForNull
-  private static SnapshotDto findNearestSnapshotToTargetDate(List<SnapshotDto> snapshots, Long targetDate) {
-    long bestDistance = Long.MAX_VALUE;
-    SnapshotDto nearest = null;
-    for (SnapshotDto snapshot : snapshots) {
-      long distance = Math.abs(snapshot.getCreatedAt() - targetDate);
-      if (distance <= bestDistance) {
-        bestDistance = distance;
-        nearest = snapshot;
-      }
-    }
-    return nearest;
-  }
-
-  private static SnapshotQuery createCommonQuery(String projectUuid) {
-    return new SnapshotQuery().setComponentUuid(projectUuid).setStatus(STATUS_PROCESSED);
-  }
-
-  private static String formatDate(long date) {
-    return DateUtils.formatDate(Date.from(new Date(date).toInstant().truncatedTo(ChronoUnit.SECONDS)));
-  }
-
-  private static String getPropertyValue(Configuration config) {
-    return config.get(LEAK_PERIOD).get();
-  }
-}
index adfb980b2d1ade7b16b93b4bc20eac9d26063687..a7cba5bc23e7b94cc7d2c1a55b8d01db87dc0e45 100644 (file)
  */
 package org.sonar.ce.task.projectanalysis.step;
 
+import com.tngtech.java.junit.dataprovider.DataProvider;
 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.text.ParseException;
 import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Random;
+import java.util.stream.Stream;
 import org.apache.commons.lang.RandomStringUtils;
 import org.junit.Before;
 import org.junit.Rule;
@@ -28,12 +35,12 @@ import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.utils.MessageException;
 import org.sonar.api.utils.System2;
+import org.sonar.api.utils.log.LogAndArguments;
 import org.sonar.api.utils.log.LogTester;
 import org.sonar.api.utils.log.LoggerLevel;
-import org.sonar.ce.task.projectanalysis.analysis.Analysis;
 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
-import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule;
 import org.sonar.ce.task.projectanalysis.component.Component;
 import org.sonar.ce.task.projectanalysis.component.ConfigurationRepository;
 import org.sonar.ce.task.projectanalysis.component.ReportComponent;
@@ -49,15 +56,19 @@ import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.SnapshotDto;
 import org.sonar.db.organization.OrganizationDto;
 
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD_MODE_DATE;
 import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD_MODE_DAYS;
 import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD_MODE_PREVIOUS_VERSION;
 import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD_MODE_VERSION;
+import static org.sonar.db.component.SnapshotDto.STATUS_PROCESSED;
 import static org.sonar.db.component.SnapshotDto.STATUS_UNPROCESSED;
 import static org.sonar.db.event.EventDto.CATEGORY_VERSION;
 import static org.sonar.db.event.EventTesting.newEvent;
@@ -72,18 +83,20 @@ public class LoadPeriodsStepTest extends BaseStepTest {
   @Rule
   public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
   @Rule
-  public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule();
-  @Rule
   public LogTester logTester = new LogTester();
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
 
+  private AnalysisMetadataHolder analysisMetadataHolder = mock(AnalysisMetadataHolder.class);
   private PeriodHolderImpl periodsHolder = new PeriodHolderImpl();
-  private DbClient dbClient = dbTester.getDbClient();
   private MapSettings settings = new MapSettings();
-  private ConfigurationRepository settingsRepository = mock(ConfigurationRepository.class);
+  private ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class);
+  private System2 system2Mock = mock(System2.class);
+
+  private LoadPeriodsStep underTest = new LoadPeriodsStep(analysisMetadataHolder, treeRootHolder, periodsHolder, system2Mock,
+    dbTester.getDbClient(), configurationRepository);
 
-  private LoadPeriodsStep underTest = new LoadPeriodsStep(analysisMetadataHolder, dbClient, settingsRepository, treeRootHolder, periodsHolder);
+  private Date november30th2008;
 
   @Override
   protected ComputationStep step() {
@@ -92,25 +105,24 @@ public class LoadPeriodsStepTest extends BaseStepTest {
 
   @Before
   public void setUp() throws Exception {
-    analysisMetadataHolder.setAnalysisDate(DATE_FORMAT.parse("2008-11-30").getTime());
-    analysisMetadataHolder.setBaseAnalysis(new Analysis.Builder().setUuid(RandomStringUtils.randomAlphabetic(15)).setId(888).setCreatedAt(5_666_777_888L).build());
+    november30th2008 = DATE_FORMAT.parse("2008-11-30");
   }
 
   @Test
-  public void no_period_on_first_analysis() {
-    DbClient dbClientMock = mock(DbClient.class);
-    ConfigurationRepository configurationRepositoryMock = mock(ConfigurationRepository.class);
+  public void no_period_on_first_analysis_and_execute_has_no_effect() {
     TreeRootHolder treeRootHolderMock = mock(TreeRootHolder.class);
     PeriodHolderImpl periodHolderMock = mock(PeriodHolderImpl.class);
-    AnalysisMetadataHolder analysisMetadataHolderMock = mock(AnalysisMetadataHolder.class);
-    when(analysisMetadataHolderMock.isFirstAnalysis()).thenReturn(true);
+    DbClient dbClientMock = mock(DbClient.class);
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(true);
 
-    LoadPeriodsStep underTest = new LoadPeriodsStep(analysisMetadataHolderMock, dbClientMock, configurationRepositoryMock, treeRootHolderMock, periodHolderMock);
+    LoadPeriodsStep underTest = new LoadPeriodsStep(analysisMetadataHolder, treeRootHolderMock, periodHolderMock, system2Mock, dbClientMock, configurationRepository);
 
     underTest.execute(new TestComputationStepContext());
 
-    verify(analysisMetadataHolderMock).isFirstAnalysis();
-    verifyZeroInteractions(dbClientMock, configurationRepositoryMock, treeRootHolderMock, periodHolderMock);
+    verify(analysisMetadataHolder).isFirstAnalysis();
+    verify(periodHolderMock).setPeriod(null);
+    verifyNoMoreInteractions(analysisMetadataHolder, periodHolderMock);
+    verifyZeroInteractions(treeRootHolderMock, system2Mock, dbClientMock, configurationRepository);
   }
 
   @Test
@@ -118,6 +130,8 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     OrganizationDto organization = dbTester.organizations().insert();
     ComponentDto project = dbTester.components().insertPrivateProject(organization);
     SnapshotDto analysis = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227934800000L)); // 2008-11-29
+    when(system2Mock.now()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
     setupRoot(project);
     String textDate = "2008-11-22";
 
@@ -132,43 +146,30 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     assertThat(period.getAnalysisUuid()).isEqualTo(analysis.getUuid());
   }
 
-  @Test
-  public void no_period_when_settings_match_no_analysis() {
-    OrganizationDto organization = dbTester.organizations().insert();
-    ComponentDto project = dbTester.components().insertPrivateProject(organization);
-    dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1226379600000L)); // 2008-11-29
-    setupRoot(project);
-
-    settings.setProperty("sonar.leak.period", "UNKNWOWN VERSION");
-    underTest.execute(new TestComputationStepContext());
-
-    assertThat(periodsHolder.hasPeriod()).isFalse();
-  }
-
-  @Test
-  public void no_period_when_settings_is_empty() {
-    OrganizationDto organization = dbTester.organizations().insert();
-    ComponentDto project = dbTester.components().insertPrivateProject(organization);
-    dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1226379600000L)); // 2008-11-29
-    setupRoot(project);
-
-    settings.setProperty("sonar.leak.period", "");
-    underTest.execute(new TestComputationStepContext());
-
-    assertThat(periodsHolder.hasPeriod()).isFalse();
-  }
-
   @Test
   public void ignore_unprocessed_snapshots() {
     OrganizationDto organization = dbTester.organizations().insert();
     ComponentDto project = dbTester.components().insertPrivateProject(organization);
-    dbTester.components().insertSnapshot(project, snapshot -> snapshot.setStatus(STATUS_UNPROCESSED).setCreatedAt(1226379600000L)); // 2008-11-29
+    SnapshotDto analysis1 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setStatus(STATUS_UNPROCESSED).setCreatedAt(1226379600000L).setLast(false));// 2008-11-11
+    SnapshotDto analysis2 = dbTester.components().insertSnapshot(project,
+      snapshot -> snapshot.setStatus(STATUS_PROCESSED).setVersion("not provided").setCreatedAt(1226379600000L).setLast(false));// 2008-11-29
+    dbTester.events().insertEvent(newEvent(analysis1).setName("not provided").setCategory(CATEGORY_VERSION).setDate(analysis1.getCreatedAt()));
+    dbTester.events().insertEvent(newEvent(analysis2).setName("not provided").setCategory(CATEGORY_VERSION).setDate(analysis2.getCreatedAt()));
+    when(analysisMetadataHolder.getAnalysisDate()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
     setupRoot(project);
 
     settings.setProperty("sonar.leak.period", "100");
     underTest.execute(new TestComputationStepContext());
 
-    assertThat(periodsHolder.hasPeriod()).isFalse();
+    Period period = periodsHolder.getPeriod();
+    assertThat(period).isNotNull();
+    assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_DAYS);
+    assertThat(period.getModeParameter()).isEqualTo("100");
+    assertThat(period.getSnapshotDate()).isEqualTo(analysis2.getCreatedAt());
+    assertThat(period.getAnalysisUuid()).isEqualTo(analysis2.getUuid());
+
+    verifyDebugLogs("Resolving new code period by 100 days: 2008-08-22");
   }
 
   @Test
@@ -180,6 +181,8 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     SnapshotDto analysis3 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227157200000L).setLast(false)); // 2008-11-20
     SnapshotDto analysis4 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227358680000L).setLast(false)); // 2008-11-22
     SnapshotDto analysis5 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227934800000L).setLast(true)); // 2008-11-29
+    when(system2Mock.now()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
     setupRoot(project);
 
     String textDate = "2008-11-22";
@@ -194,8 +197,7 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     assertThat(period.getSnapshotDate()).isEqualTo(analysis4.getCreatedAt());
     assertThat(period.getAnalysisUuid()).isEqualTo(analysis4.getUuid());
 
-    assertThat(logTester.logs()).hasSize(1);
-    assertThat(logTester.logs().get(0)).startsWith("Compare to date 2008-11-22 (analysis of ");
+    verifyDebugLogs("Resolving new code period by date: 2008-11-22");
   }
 
   @Test
@@ -207,6 +209,8 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     SnapshotDto analysis3 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227157200000L).setLast(false)); // 2008-11-20
     SnapshotDto analysis4 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227358680000L).setLast(false)); // 2008-11-22
     SnapshotDto analysis5 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227934800000L).setLast(true)); // 2008-11-29
+    when(system2Mock.now()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
     setupRoot(project);
 
     String date = "2008-11-13";
@@ -220,24 +224,182 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     assertThat(period.getModeParameter()).isEqualTo(date);
     assertThat(period.getSnapshotDate()).isEqualTo(analysis3.getCreatedAt());
     assertThat(period.getAnalysisUuid()).isEqualTo(analysis3.getUuid());
+
+    verifyDebugLogs("Resolving new code period by date: 2008-11-13");
   }
 
   @Test
-  public void no_period_by_date() {
+  public void fail_with_MessageException_if_period_is_date_after_today() {
     OrganizationDto organization = dbTester.organizations().insert();
     ComponentDto project = dbTester.components().insertPrivateProject(organization);
     dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227934800000L)); // 2008-11-29
+    when(system2Mock.now()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
     setupRoot(project);
+    String propertyValue = "2008-12-01";
+    settings.setProperty("sonar.leak.period", propertyValue);
+
+    verifyFailWithInvalidValueMessageException(propertyValue,
+      "Invalid code period '2008-12-01': date is in the future (now: '2008-11-30')");
+  }
+
+  @Test
+  public void fail_with_MessageException_if_date_does_not_exist() {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentDto project = dbTester.components().insertPrivateProject(organization);
+    dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227934800000L)); // 2008-11-29
+    when(system2Mock.now()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
+    setupRoot(project);
+    String propertyValue = "2008-11-31";
+    settings.setProperty("sonar.leak.period", propertyValue);
+
+    verifyFailWithInvalidValueMessageException(propertyValue,
+      "Invalid code period '2008-11-31': Invalid date");
+  }
+
+  @Test
+  public void fail_with_MessageException_if_period_is_today_but_no_analysis_today() {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentDto project = dbTester.components().insertPrivateProject(organization);
+    dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227934800000L)); // 2008-11-29
+    when(system2Mock.now()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
+    setupRoot(project);
+    String propertyValue = "2008-11-30";
+    settings.setProperty("sonar.leak.period", propertyValue);
+
+    verifyFailWithInvalidValueMessageException(propertyValue,
+      "Resolving new code period by date: 2008-11-30",
+      "Invalid code period '2008-11-30': No analysis found created after date '2008-11-30'");
+  }
+
+  @Test
+  @UseDataProvider("zeroOrLess")
+  public void fail_with_MessageException_if_period_is_0_or_less(int zeroOrLess) {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentDto project = dbTester.components().insertPrivateProject(organization);
+    when(system2Mock.now()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
+    setupRoot(project);
+    String propertyValue = String.valueOf(zeroOrLess);
+    settings.setProperty("sonar.leak.period", propertyValue);
+
+    verifyFailWithInvalidValueMessageException(propertyValue,
+      "Invalid code period '" + zeroOrLess + "': number of days is <= 0");
+  }
+
+  @DataProvider
+  public static Object[][] zeroOrLess() {
+    return new Object[][] {
+      {0},
+      {-1 - new Random().nextInt(30)}
+    };
+  }
+
+  @Test
+  @UseDataProvider("previousVersionOrVersion")
+  public void fail_with_ISE_if_not_firstAnalysis_but_no_snapshot_in_DB(String propertyValue) {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentDto project = dbTester.components().insertPrivateProject(organization);
+    when(system2Mock.now()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
+    setupRoot(project);
+    settings.setProperty("sonar.leak.period", "previous_version");
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Attempting to resolve period while no analysis exist");
 
-    // No analysis at and after this date
-    settings.setProperty("sonar.leak.period", "2008-11-30");
     underTest.execute(new TestComputationStepContext());
+  }
+
+  @DataProvider
+  public static Object[][] previousVersionOrVersion() {
+    return new Object[][] {
+      {"previous_version"},
+      {randomAlphabetic(3)}
+    };
+  }
 
-    assertThat(periodsHolder.hasPeriod()).isFalse();
+  @Test
+  @UseDataProvider("stringConsideredAsVersions")
+  public void fail_with_MessageException_if_string_is_not_an_existing_version_event(String propertyValue) {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentDto project = dbTester.components().insertPrivateProject(organization);
+    SnapshotDto analysis1 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1226379600000L).setVersion("0.9").setLast(false)); // 2008-11-11
+    SnapshotDto analysis2 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1226494680000L).setVersion("1.0").setLast(false)); // 2008-11-12
+    SnapshotDto analysis3 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227157200000L).setVersion("1.1").setLast(false)); // 2008-11-20
+    SnapshotDto analysis4 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227358680000L).setVersion("1.1").setLast(false)); // 2008-11-22
+    SnapshotDto analysis5 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227934800000L).setVersion("1.1").setLast(true)); // 2008-11-29
+    dbTester.events().insertEvent(newEvent(analysis1).setName("0.9").setCategory(CATEGORY_VERSION).setDate(analysis1.getCreatedAt()));
+    dbTester.events().insertEvent(newEvent(analysis2).setName("1.0").setCategory(CATEGORY_VERSION).setDate(analysis2.getCreatedAt()));
+    dbTester.events().insertEvent(newEvent(analysis5).setName("1.1").setCategory(CATEGORY_VERSION).setDate(analysis4.getCreatedAt()));
+    when(system2Mock.now()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
+    setupRoot(project);
+    settings.setProperty("sonar.leak.period", propertyValue);
+
+    try {
+      underTest.execute(new TestComputationStepContext());
+      fail("a Message Exception should have been thrown");
+    } catch (MessageException e) {
+      verifyInvalidValueMessage(e, propertyValue);
+      assertThat(logTester.getLogs()).hasSize(2);
+      assertThat(logTester.getLogs(LoggerLevel.DEBUG))
+        .hasSize(2)
+        .extracting(LogAndArguments::getFormattedMsg)
+        .contains("Invalid code period '" + propertyValue + "': version is none of the existing ones: [1.1, 1.0, 0.9]");
+    }
+  }
+
+  @DataProvider
+  public static Object[][] stringConsideredAsVersions() {
+    return new Object[][] {
+      {RandomStringUtils.randomAlphabetic(5)},
+      {"1,3"},
+      {"1.3"},
+      {"0 1"},
+      {"1-SNAPSHOT"},
+      {"01-12-2018"}, // unsupported date format
+    };
   }
 
   @Test
-  public void feed_period_by_days() {
+  public void fail_with_MessageException_if_property_does_not_exist() {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentDto project = dbTester.components().insertPrivateProject(organization);
+    when(system2Mock.now()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
+    setupRoot(project);
+
+    verifyFailWithInvalidValueMessageException("", "Invalid code period '': property is undefined or value is empty");
+  }
+
+  @Test
+  public void fail_with_MessageException_if_property_is_empty() {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentDto project = dbTester.components().insertPrivateProject(organization);
+    when(system2Mock.now()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
+    setupRoot(project);
+    String propertyValue = "";
+    settings.setProperty("sonar.leak.period", propertyValue);
+
+    verifyFailWithInvalidValueMessageException(propertyValue, "Invalid code period '': property is undefined or value is empty");
+  }
+
+  private void verifyFailWithInvalidValueMessageException(String propertyValue, String debugLog, String... otherDebugLogs) {
+    try {
+      underTest.execute(new TestComputationStepContext());
+      fail("a Message Exception should have been thrown");
+    } catch (MessageException e) {
+      verifyInvalidValueMessage(e, propertyValue);
+      verifyDebugLogs(debugLog, otherDebugLogs);
+    }
+  }
+
+  @Test
+  public void feed_period_by_days() throws ParseException {
     OrganizationDto organization = dbTester.organizations().insert();
     ComponentDto project = dbTester.components().insertPrivateProject(organization);
     SnapshotDto analysis1 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1226379600000L).setLast(false)); // 2008-11-11
@@ -245,6 +407,8 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     SnapshotDto analysis3 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227157200000L).setLast(false)); // 2008-11-20
     SnapshotDto analysis4 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227358680000L).setLast(false)); // 2008-11-22
     SnapshotDto analysis5 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227934800000L).setLast(true)); // 2008-11-29
+    when(analysisMetadataHolder.getAnalysisDate()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
     setupRoot(project);
 
     settings.setProperty("sonar.leak.period", "10");
@@ -258,20 +422,10 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     assertThat(period.getSnapshotDate()).isEqualTo(analysis3.getCreatedAt());
     assertThat(period.getAnalysisUuid()).isEqualTo(analysis3.getUuid());
 
-    assertThat(logTester.logs()).hasSize(1);
-    assertThat(logTester.logs().get(0)).startsWith("Compare over 10 days (2008-11-20, analysis of ");
-  }
-
-  @Test
-  public void no_period_by_days() {
-    OrganizationDto organization = dbTester.organizations().insert();
-    ComponentDto project = dbTester.components().insertPrivateProject(organization);
-    setupRoot(project);
-
-    settings.setProperty("sonar.leak.period", "0");
-    underTest.execute(new TestComputationStepContext());
-
-    assertThat(periodsHolder.hasPeriod()).isFalse();
+    assertThat(logTester.getLogs()).hasSize(1);
+    assertThat(logTester.getLogs(LoggerLevel.DEBUG))
+      .extracting(LogAndArguments::getFormattedMsg)
+      .containsOnly("Resolving new code period by 10 days: 2008-11-20");
   }
 
   @Test
@@ -286,6 +440,8 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     dbTester.events().insertEvent(newEvent(analysis1).setName("0.9").setCategory(CATEGORY_VERSION).setDate(analysis1.getCreatedAt()));
     dbTester.events().insertEvent(newEvent(analysis2).setName("1.0").setCategory(CATEGORY_VERSION).setDate(analysis2.getCreatedAt()));
     dbTester.events().insertEvent(newEvent(analysis5).setName("1.1").setCategory(CATEGORY_VERSION).setDate(analysis4.getCreatedAt()));
+    when(system2Mock.now()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
     setupRoot(project, "1.1");
 
     settings.setProperty("sonar.leak.period", "previous_version");
@@ -299,8 +455,7 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     assertThat(period.getSnapshotDate()).isEqualTo(analysis2.getCreatedAt());
     assertThat(period.getAnalysisUuid()).isEqualTo(analysis2.getUuid());
 
-    assertThat(logTester.logs(LoggerLevel.DEBUG)).hasSize(1);
-    assertThat(logTester.logs(LoggerLevel.DEBUG).get(0)).startsWith("Compare to previous version (");
+    verifyDebugLogs("Resolving new code period by previous version: 1.0");
   }
 
   @Test
@@ -313,6 +468,8 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     dbTester.events().insertEvent(newEvent(analysis1).setName("0.9").setCategory(CATEGORY_VERSION));
     // The "1.0" version was deleted from the history
     dbTester.events().insertEvent(newEvent(analysis3).setName("1.1").setCategory(CATEGORY_VERSION));
+    when(system2Mock.now()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
     setupRoot(project, "1.1");
 
     settings.setProperty("sonar.leak.period", "previous_version");
@@ -327,25 +484,15 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     assertThat(period.getAnalysisUuid()).isEqualTo(analysis1.getUuid());
   }
 
-  @Test
-  public void no_period_by_previous_version() {
-    OrganizationDto organization = dbTester.organizations().insert();
-    ComponentDto project = dbTester.components().insertPrivateProject(organization);
-    setupRoot(project);
-
-    settings.setProperty("sonar.leak.period", "previous_version");
-    underTest.execute(new TestComputationStepContext());
-
-    assertThat(periodsHolder.hasPeriod()).isFalse();
-  }
-
   @Test
   public void feed_period_by_previous_version_with_first_analysis_when_no_previous_version_found() {
     OrganizationDto organization = dbTester.organizations().insert();
     ComponentDto project = dbTester.components().insertPrivateProject(organization);
-    SnapshotDto analysis1 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1226379600000L).setVersion("0.9").setLast(false)); // 2008-11-11
-    SnapshotDto analysis5 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227934800000L).setVersion("1.1").setLast(true)); // 2008-11-29
-    dbTester.events().insertEvent(newEvent(analysis5).setName("1.1").setCategory(CATEGORY_VERSION));
+    SnapshotDto analysis1 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1226379600000L).setVersion("1.1").setLast(false)); // 2008-11-11
+    SnapshotDto analysis2 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1227934800000L).setVersion("1.1").setLast(true)); // 2008-11-29
+    dbTester.events().insertEvent(newEvent(analysis2).setName("1.1").setCategory(CATEGORY_VERSION));
+    when(system2Mock.now()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
     setupRoot(project, "1.1");
 
     settings.setProperty("sonar.leak.period", "previous_version");
@@ -357,6 +504,8 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     assertThat(period.getModeParameter()).isNull();
     assertThat(period.getSnapshotDate()).isEqualTo(analysis1.getCreatedAt());
     assertThat(period.getAnalysisUuid()).isEqualTo(analysis1.getUuid());
+
+    verifyDebugLogs("Resolving first analysis as new code period as there is only one existing version");
   }
 
   @Test
@@ -364,6 +513,9 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     OrganizationDto organization = dbTester.organizations().insert();
     ComponentDto project = dbTester.components().insertPrivateProject(organization);
     SnapshotDto analysis = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1226379600000L).setVersion("0.9").setLast(true)); // 2008-11-11
+    dbTester.events().insertEvent(newEvent(analysis).setName("0.9").setCategory(CATEGORY_VERSION));
+    when(system2Mock.now()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
     setupRoot(project, "1.1");
 
     settings.setProperty("sonar.leak.period", "previous_version");
@@ -375,6 +527,8 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     assertThat(period.getModeParameter()).isNull();
     assertThat(period.getSnapshotDate()).isEqualTo(analysis.getCreatedAt());
     assertThat(period.getAnalysisUuid()).isEqualTo(analysis.getUuid());
+
+    verifyDebugLogs("Resolving first analysis as new code period as there is only one existing version");
   }
 
   @Test
@@ -389,6 +543,8 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     dbTester.events().insertEvent(newEvent(analysis1).setName("0.9").setCategory(CATEGORY_VERSION));
     dbTester.events().insertEvent(newEvent(analysis2).setName("1.0").setCategory(CATEGORY_VERSION));
     dbTester.events().insertEvent(newEvent(analysis5).setName("1.1").setCategory(CATEGORY_VERSION));
+    when(system2Mock.now()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
     setupRoot(project, "1.1");
 
     settings.setProperty("sonar.leak.period", "1.0");
@@ -402,29 +558,29 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     assertThat(period.getSnapshotDate()).isEqualTo(analysis2.getCreatedAt());
     assertThat(period.getAnalysisUuid()).isEqualTo(analysis2.getUuid());
 
-    assertThat(logTester.logs()).hasSize(1);
-    assertThat(logTester.logs().get(0)).startsWith("Compare to version (1.0) (");
+    verifyDebugLogs("Resolving new code period by version: 1.0");
   }
 
-  @Test
-  public void no_period_by_version() {
-    OrganizationDto organization = dbTester.organizations().insert();
-    ComponentDto project = dbTester.components().insertPrivateProject(organization);
-    setupRoot(project);
-
-    settings.setProperty("sonar.leak.period", "0.8");
-    underTest.execute(new TestComputationStepContext());
-
-    assertThat(periodsHolder.hasPeriod()).isFalse();
+  private void verifyDebugLogs(String log, String... otherLogs) {
+    assertThat(logTester.getLogs()).hasSize(1 + otherLogs.length);
+    assertThat(logTester.getLogs(LoggerLevel.DEBUG))
+      .extracting(LogAndArguments::getFormattedMsg)
+      .containsOnly(Stream.concat(Stream.of(log), Arrays.stream(otherLogs)).toArray(String[]::new));
   }
 
   private void setupRoot(ComponentDto project) {
-    setupRoot(project, "1.1");
+    setupRoot(project, RandomStringUtils.randomAlphanumeric(3));
   }
 
   private void setupRoot(ComponentDto project, String version) {
     treeRootHolder.setRoot(ReportComponent.builder(Component.Type.PROJECT, 1).setUuid(project.uuid()).setKey(project.getKey()).setProjectVersion(version).build());
-    when(settingsRepository.getConfiguration()).thenReturn(settings.asConfig());
+    when(configurationRepository.getConfiguration()).thenReturn(settings.asConfig());
+  }
+
+  private static void verifyInvalidValueMessage(MessageException e, String propertyValue) {
+    assertThat(e).hasMessage("Invalid new code period. '" + propertyValue
+      + "' 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");
   }
 
 }
index ae2e55d4f54665478a74516c3bbdaa04a9d9a9a7..0001237348143853a10114f055a834596d7d2162 100644 (file)
@@ -83,17 +83,10 @@ public class SnapshotDao implements Dao {
     return dtos.get(0);
   }
 
-  /**
-   * Since this relies on tables EVENTS, this can return results only for root components (PROJECT, VIEW or DEVELOPER).
-   */
-  public List<SnapshotDto> selectPreviousVersionSnapshots(DbSession session, String componentUuid, String lastVersion) {
-    return mapper(session).selectPreviousVersionSnapshots(componentUuid, lastVersion);
-  }
-
-  @CheckForNull
-  public SnapshotDto selectOldestSnapshot(DbSession session, String componentUuid) {
-    List<SnapshotDto> snapshotDtos = mapper(session).selectOldestSnapshots(componentUuid, new RowBounds(0, 1));
-    return snapshotDtos.isEmpty() ? null : snapshotDtos.get(0);
+  public Optional<SnapshotDto> selectOldestSnapshot(DbSession session, String componentUuid) {
+    return mapper(session).selectOldestSnapshots(componentUuid, new RowBounds(0, 1))
+      .stream()
+      .findFirst();
   }
 
   /**
index 66eacc9af672b5e934df011c6a7f5c88b167c0d0..4636283747cc1d9d815b41b20458185adc05c4d4 100644 (file)
@@ -42,8 +42,6 @@ public interface SnapshotMapper {
 
   List<SnapshotDto> selectSnapshotsByQuery(@Param("query") SnapshotQuery query);
 
-  List<SnapshotDto> selectPreviousVersionSnapshots(@Param("componentUuid") String componentUuid, @Param("lastVersion") String lastVersion);
-
   List<SnapshotDto> selectOldestSnapshots(@Param("componentUuid") String componentUuid, RowBounds rowBounds);
 
   List<ViewsSnapshotDto> selectSnapshotBefore(@Param("componentUuid") String componentUuid, @Param("date") long date);
index dcb7d715110b112fd57f67e65157400bd76de79a..e01dbe42bd4277fdff865dbaee19e69fe23da154 100644 (file)
@@ -45,8 +45,12 @@ public class EventDao implements Dao {
     return executeLargeInputs(analyses, mapper(dbSession)::selectByAnalysisUuids);
   }
 
+  public List<EventDto> selectVersionsByMostRecentFirst(DbSession session, String componentUuid) {
+    return mapper(session).selectVersions(componentUuid);
+  }
+
   public EventDto insert(DbSession session, EventDto dto) {
-    session.getMapper(EventMapper.class).insert(dto);
+    mapper(session).insert(dto);
 
     return dto;
   }
index 244ff09244a019afb110f4135b28d363e3796757..0695fd4281e8a6e20534920236abe23b2342c223 100644 (file)
@@ -33,6 +33,8 @@ public interface EventMapper {
 
   List<EventDto> selectByAnalysisUuids(@Param("analysisUuids") List<String> list);
 
+  List<EventDto> selectVersions(@Param("componentUuid") String componentUuid);
+
   void insert(EventDto dto);
 
   void update(@Param("uuid") String uuid, @Param("name") @Nullable String name, @Param("description") @Nullable String description);
index 03f78b74776980057bea2cd260f7c7db477c18c3..d5e8bca83783dfb3c8759f56325f1ffe06d87599 100644 (file)
       s.created_at
   </select>
 
-  <select id="selectPreviousVersionSnapshots" parameterType="map" resultType="Snapshot">
-    SELECT
-    <include refid="snapshotColumns" />
-    FROM snapshots s
-    INNER JOIN events e ON s.uuid = e.analysis_uuid AND e.name &lt;&gt; #{lastVersion} AND e.category='Version'
-    <where>
-      s.component_uuid=#{componentUuid,jdbcType=VARCHAR}
-    </where>
-    ORDER BY e.event_date DESC
-  </select>
-
   <select id="selectOldestSnapshots" parameterType="map" resultType="Snapshot">
     SELECT
     <include refid="snapshotColumns" />
index bb7623140a1d5345a4548e034e601ed8076abfb0..4bab83a1b92c092a97dc0d8e634dfd934735399e 100644 (file)
     </where>
   </select>
 
+  <select id="selectVersions" resultType="Event" parameterType="map">
+    select
+      <include refid="eventColumns"/>
+    from events e
+    inner join snapshots s on
+      s.uuid = e.analysis_uuid
+      and s.status = 'P'
+      and s.component_uuid = #{componentUuid,jdbcType=VARCHAR}
+    where
+      e.category = 'Version'
+    order by
+      e.event_date desc
+  </select>
+
   <insert id="insert" parameterType="Event" keyColumn="id" useGeneratedKeys="true" keyProperty="id">
     INSERT INTO events (uuid, analysis_uuid, component_uuid, name, category, description, event_data, event_date, created_at)
     VALUES (
index a4a00f53aba5f8282ce469fa8b4ea2cb582106e3..4b4151cee5bf8119b4eb12309bac649546fba60e 100644 (file)
@@ -176,20 +176,6 @@ public class SnapshotDaoTest {
     underTest.selectAnalysisByQuery(db.getSession(), new SnapshotQuery());
   }
 
-  @Test
-  public void select_previous_version_snapshots() {
-    db.prepareDbUnit(getClass(), "select_previous_version_snapshots.xml");
-
-    List<SnapshotDto> snapshots = underTest.selectPreviousVersionSnapshots(db.getSession(), "ABCD", "1.2-SNAPSHOT");
-    assertThat(snapshots).hasSize(2);
-
-    SnapshotDto firstSnapshot = snapshots.get(0);
-    assertThat(firstSnapshot.getVersion()).isEqualTo("1.1");
-
-    // All snapshots are returned on an unknown version
-    assertThat(underTest.selectPreviousVersionSnapshots(db.getSession(), "ABCD", "UNKNOWN")).hasSize(3);
-  }
-
   @Test
   public void select_first_snapshots() {
     ComponentDto project = newPrivateProjectDto(db.getDefaultOrganization());
@@ -201,11 +187,11 @@ public class SnapshotDaoTest {
       newAnalysis(project).setCreatedAt(1L));
     dbSession.commit();
 
-    SnapshotDto dto = underTest.selectOldestSnapshot(dbSession, project.uuid());
-    assertThat(dto).isNotNull();
-    assertThat(dto.getCreatedAt()).isEqualTo(1L);
+    Optional<SnapshotDto> dto = underTest.selectOldestSnapshot(dbSession, project.uuid());
+    assertThat(dto).isNotEmpty();
+    assertThat(dto.get().getCreatedAt()).isEqualTo(1L);
 
-    assertThat(underTest.selectOldestSnapshot(dbSession, "blabla")).isNull();
+    assertThat(underTest.selectOldestSnapshot(dbSession, "blabla")).isEmpty();
   }
 
   @Test
index 76f5cb99b84da8b6ea37aaf6d0a8c2e562f2742d..88d2afea1456d899f8b8be43eb636dd7fc374eb6 100644 (file)
@@ -247,10 +247,6 @@ show_more=Show More
 show_all=Show All
 should_be_unique=Should be unique
 since_x=since {0}
-since_previous_analysis=since previous analysis
-since_previous_analysis_detailed=since previous analysis ({0})
-since_previous_analysis.short=\u0394 last
-since_previous_analysis_detailed.short=\u0394 last ({0})
 since_version=since version {0}
 since_version.short={0}
 since_version_detailed=since version {0} ({1})
index 5e4ad44a45bc8d2435234b07a59849871f936762..56aa05566c1fa0fb1c1d6bb05a65a68cc9023ac3 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.api.utils.log;
 
+import java.util.Arrays;
 import java.util.Optional;
 import javax.annotation.Nullable;
 
@@ -62,4 +63,13 @@ public final class LogAndArguments {
   public String getFormattedMsg() {
     return msg;
   }
+
+  @Override
+  public String toString() {
+    return "LogAndArguments{" +
+      "rawMsg='" + rawMsg + '\'' +
+      ", args=" + Arrays.toString(args) +
+      ", msg='" + msg + '\'' +
+      '}';
+  }
 }