]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11492 Second analysis of a long-lived branch is using wrong leak version
authorDuarte Meneses <duarte.meneses@sonarsource.com>
Tue, 8 Jan 2019 13:29:37 +0000 (14:29 +0100)
committerSonarTech <sonartech@sonarsource.com>
Tue, 8 Jan 2019 19:21:05 +0000 (20:21 +0100)
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepTest.java
sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java
sonar-plugin-api/src/main/java/org/sonar/api/utils/DateUtils.java

index 3d6c5e271a55e5a51d85c13acfa6fa54c0761cd9..9070f7a2ddf61aac278a0b8d3a412237688fb89a 100644 (file)
  */
 package org.sonar.ce.task.projectanalysis.step;
 
+import java.time.Duration;
+import java.time.Instant;
 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;
@@ -129,7 +130,7 @@ public class LoadPeriodsStep implements ComputationStep {
     if (days != null) {
       return resolveByDays(dbSession, projectUuid, days, propertyValue);
     }
-    Date date = parseDate(propertyValue);
+    Instant date = parseDate(propertyValue);
     if (date != null) {
       return resolveByDate(dbSession, projectUuid, date, propertyValue);
     }
@@ -141,12 +142,12 @@ public class LoadPeriodsStep implements ComputationStep {
 
     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) {
+      if (versions.size() == 1) {
+        return resolvePreviousVersionWithOnlyOneExistingVersion(dbSession, projectUuid);
+      }
       return resolvePreviousVersion(dbSession, analysisProjectVersion, versions, mostRecentVersion);
     }
 
@@ -154,10 +155,10 @@ public class LoadPeriodsStep implements ComputationStep {
   }
 
   @CheckForNull
-  private static Date parseDate(String propertyValue) {
+  private static Instant parseDate(String propertyValue) {
     try {
       LocalDate localDate = LocalDate.parse(propertyValue);
-      return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
+      return 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");
@@ -171,24 +172,23 @@ public class LoadPeriodsStep implements ComputationStep {
     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)));
+    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 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)));
+  private Optional<Period> resolveByDate(DbSession dbSession, String projectUuid, Instant date, String propertyValue) {
+    Instant now = Instant.ofEpochMilli(system2.now());
+    checkPeriodProperty(date.compareTo(now) <= 0, propertyValue,
+      "date is in the future (now: '%s')", supplierToString(() -> 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))
+    LOG.debug("Resolving new code period by date: {}", supplierToString(() -> logDate(date)));
+    Optional<Period> period = findFirstSnapshot(dbSession, createCommonQuery(projectUuid).setCreatedAfter(date.toEpochMilli()).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)));
-
+    checkPeriodProperty(period.isPresent(), propertyValue, "No analysis found created after date '%s'", supplierToString(() -> logDate(date)));
     return period;
   }
 
@@ -205,15 +205,8 @@ public class LoadPeriodsStep implements ComputationStep {
     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);
+  private Optional<Period> resolvePreviousVersionWithOnlyOneExistingVersion(DbSession dbSession, String projectUuid) {
     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);
   }
 
@@ -234,7 +227,7 @@ public class LoadPeriodsStep implements ComputationStep {
     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)));
+      "version is none of the existing ones: %s", supplierToString(() -> toVersions(versions)));
 
     return newPeriod(dbSession, LEAK_PERIOD_MODE_VERSION, version.get());
   }
@@ -253,7 +246,7 @@ public class LoadPeriodsStep implements ComputationStep {
     return Arrays.toString(versions.stream().map(EventDto::getName).toArray(String[]::new));
   }
 
-  public static Object async(Supplier<String> s) {
+  private static Object supplierToString(Supplier<String> s) {
     return new Object() {
       @Override
       public String toString() {
@@ -268,7 +261,7 @@ public class LoadPeriodsStep implements ComputationStep {
 
   private static void checkPeriodProperty(boolean test, String propertyValue, String testDescription, Object... args) {
     if (!test) {
-      LOG.debug("Invalid code period '{}': {}", propertyValue, async(() -> format(testDescription, args)));
+      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));
@@ -295,13 +288,15 @@ public class LoadPeriodsStep implements ComputationStep {
     }
   }
 
-  private static SnapshotDto findNearestSnapshotToTargetDate(List<SnapshotDto> snapshots, Long targetDate) {
-    long bestDistance = Long.MAX_VALUE;
+  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) {
-      long distance = Math.abs(snapshot.getCreatedAt() - targetDate);
-      if (distance <= bestDistance) {
-        bestDistance = distance;
+      Instant createdAt = Instant.ofEpochMilli(snapshot.getCreatedAt());
+      Duration duration = Duration.between(targetDate, createdAt).abs();
+      if (bestDuration == null || duration.compareTo(bestDuration) <= 0) {
+        bestDuration = duration;
         nearest = snapshot;
       }
     }
@@ -312,11 +307,7 @@ public class LoadPeriodsStep implements ComputationStep {
     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)));
+  private static String logDate(Instant instant) {
+    return DateUtils.formatDate(instant.truncatedTo(ChronoUnit.SECONDS));
   }
 }
index d62086ef42eca1eb3faed895db98de37a3b044fb..8ac7cb76f37f9e9470e767baefdbb876767f50c6 100644 (file)
@@ -28,6 +28,7 @@ import java.util.Arrays;
 import java.util.Date;
 import java.util.Random;
 import java.util.stream.Stream;
+import javax.annotation.Nullable;
 import org.apache.commons.lang.RandomStringUtils;
 import org.junit.Before;
 import org.junit.Rule;
@@ -138,19 +139,15 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     settings.setProperty("sonar.leak.period", textDate);
     underTest.execute(new TestComputationStepContext());
 
-    Period period = periodsHolder.getPeriod();
-    assertThat(period).isNotNull();
-    assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_DATE);
-    assertThat(period.getModeParameter()).isEqualTo(textDate);
-    assertThat(period.getSnapshotDate()).isEqualTo(analysis.getCreatedAt());
-    assertThat(period.getAnalysisUuid()).isEqualTo(analysis.getUuid());
+    assertPeriod(LEAK_PERIOD_MODE_DATE, textDate, analysis.getCreatedAt(), analysis.getUuid());
   }
 
   @Test
   public void ignore_unprocessed_snapshots() {
     OrganizationDto organization = dbTester.organizations().insert();
     ComponentDto project = dbTester.components().insertPrivateProject(organization);
-    SnapshotDto analysis1 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setStatus(STATUS_UNPROCESSED).setCreatedAt(1226379600000L).setLast(false));// 2008-11-11
+    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()));
@@ -162,13 +159,7 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     settings.setProperty("sonar.leak.period", "100");
     underTest.execute(new TestComputationStepContext());
 
-    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());
-
+    assertPeriod(LEAK_PERIOD_MODE_DAYS, "100", analysis2.getCreatedAt(), analysis2.getUuid());
     verifyDebugLogs("Resolving new code period by 100 days: 2008-08-22");
   }
 
@@ -190,12 +181,7 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     underTest.execute(new TestComputationStepContext());
 
     // Return analysis from given date 2008-11-22
-    Period period = periodsHolder.getPeriod();
-    assertThat(period).isNotNull();
-    assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_DATE);
-    assertThat(period.getModeParameter()).isEqualTo(textDate);
-    assertThat(period.getSnapshotDate()).isEqualTo(analysis4.getCreatedAt());
-    assertThat(period.getAnalysisUuid()).isEqualTo(analysis4.getUuid());
+    assertPeriod(LEAK_PERIOD_MODE_DATE, textDate, analysis4.getCreatedAt(), analysis4.getUuid());
 
     verifyDebugLogs("Resolving new code period by date: 2008-11-22");
   }
@@ -218,13 +204,7 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     underTest.execute(new TestComputationStepContext());
 
     // Analysis form 2008-11-20
-    Period period = periodsHolder.getPeriod();
-    assertThat(period).isNotNull();
-    assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_DATE);
-    assertThat(period.getModeParameter()).isEqualTo(date);
-    assertThat(period.getSnapshotDate()).isEqualTo(analysis3.getCreatedAt());
-    assertThat(period.getAnalysisUuid()).isEqualTo(analysis3.getUuid());
-
+    assertPeriod(LEAK_PERIOD_MODE_DATE, date, analysis3.getCreatedAt(), analysis3.getUuid());
     verifyDebugLogs("Resolving new code period by date: 2008-11-13");
   }
 
@@ -415,12 +395,7 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     underTest.execute(new TestComputationStepContext());
 
     // return analysis from 2008-11-20
-    Period period = periodsHolder.getPeriod();
-    assertThat(period).isNotNull();
-    assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_DAYS);
-    assertThat(period.getModeParameter()).isEqualTo("10");
-    assertThat(period.getSnapshotDate()).isEqualTo(analysis3.getCreatedAt());
-    assertThat(period.getAnalysisUuid()).isEqualTo(analysis3.getUuid());
+    assertPeriod(LEAK_PERIOD_MODE_DAYS, "10", analysis3.getCreatedAt(), analysis3.getUuid());
 
     assertThat(logTester.getLogs()).hasSize(1);
     assertThat(logTester.getLogs(LoggerLevel.DEBUG))
@@ -448,12 +423,7 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     underTest.execute(new TestComputationStepContext());
 
     // Analysis form 2008-11-12
-    Period period = periodsHolder.getPeriod();
-    assertThat(period).isNotNull();
-    assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_PREVIOUS_VERSION);
-    assertThat(period.getModeParameter()).isEqualTo("1.0");
-    assertThat(period.getSnapshotDate()).isEqualTo(analysis2.getCreatedAt());
-    assertThat(period.getAnalysisUuid()).isEqualTo(analysis2.getUuid());
+    assertPeriod(LEAK_PERIOD_MODE_PREVIOUS_VERSION, "1.0", analysis2.getCreatedAt(), analysis2.getUuid());
 
     verifyDebugLogs("Resolving new code period by previous version: 1.0");
   }
@@ -476,12 +446,7 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     underTest.execute(new TestComputationStepContext());
 
     // Analysis form 2008-11-11
-    Period period = periodsHolder.getPeriod();
-    assertThat(period).isNotNull();
-    assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_PREVIOUS_VERSION);
-    assertThat(period.getModeParameter()).isEqualTo("0.9");
-    assertThat(period.getSnapshotDate()).isEqualTo(analysis1.getCreatedAt());
-    assertThat(period.getAnalysisUuid()).isEqualTo(analysis1.getUuid());
+    assertPeriod(LEAK_PERIOD_MODE_PREVIOUS_VERSION, "0.9", analysis1.getCreatedAt(), analysis1.getUuid());
   }
 
   @Test
@@ -498,12 +463,7 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     settings.setProperty("sonar.leak.period", "previous_version");
     underTest.execute(new TestComputationStepContext());
 
-    Period period = periodsHolder.getPeriod();
-    assertThat(period).isNotNull();
-    assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_PREVIOUS_VERSION);
-    assertThat(period.getModeParameter()).isNull();
-    assertThat(period.getSnapshotDate()).isEqualTo(analysis1.getCreatedAt());
-    assertThat(period.getAnalysisUuid()).isEqualTo(analysis1.getUuid());
+    assertPeriod(LEAK_PERIOD_MODE_PREVIOUS_VERSION, null, analysis1.getCreatedAt(), analysis1.getUuid());
 
     verifyDebugLogs("Resolving first analysis as new code period as there is only one existing version");
   }
@@ -521,13 +481,7 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     settings.setProperty("sonar.leak.period", "previous_version");
     underTest.execute(new TestComputationStepContext());
 
-    Period period = periodsHolder.getPeriod();
-    assertThat(period).isNotNull();
-    assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_PREVIOUS_VERSION);
-    assertThat(period.getModeParameter()).isNull();
-    assertThat(period.getSnapshotDate()).isEqualTo(analysis.getCreatedAt());
-    assertThat(period.getAnalysisUuid()).isEqualTo(analysis.getUuid());
-
+    assertPeriod(LEAK_PERIOD_MODE_PREVIOUS_VERSION, null, analysis.getCreatedAt(), analysis.getUuid());
     verifyDebugLogs("Resolving first analysis as new code period as there is only one existing version");
   }
 
@@ -551,14 +505,38 @@ public class LoadPeriodsStepTest extends BaseStepTest {
     underTest.execute(new TestComputationStepContext());
 
     // Analysis form 2008-11-11
+    assertPeriod(LEAK_PERIOD_MODE_VERSION, "1.0", analysis2.getCreatedAt(), analysis2.getUuid());
+    verifyDebugLogs("Resolving new code period by version: 1.0");
+  }
+
+  /**
+   * SONAR-11492
+   */
+  @Test
+  public void feed_period_by_version_with_only_one_existing_version() {
+    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(true)); // 2008-11-11
+    dbTester.events().insertEvent(newEvent(analysis1).setName("0.9").setCategory(CATEGORY_VERSION));
+    when(system2Mock.now()).thenReturn(november30th2008.getTime());
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
+    setupRoot(project, "0.9");
+
+    settings.setProperty("sonar.leak.period", "0.9");
+    underTest.execute(new TestComputationStepContext());
+
+    // Analysis form 2008-11-11
+    assertPeriod(LEAK_PERIOD_MODE_VERSION, "0.9", analysis1.getCreatedAt(), analysis1.getUuid());
+    verifyDebugLogs("Resolving new code period by version: 0.9");
+  }
+
+  private void assertPeriod(String mode, @Nullable String modeParameter, long snapshotDate, String analysisUuid) {
     Period period = periodsHolder.getPeriod();
     assertThat(period).isNotNull();
-    assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_VERSION);
-    assertThat(period.getModeParameter()).isEqualTo("1.0");
-    assertThat(period.getSnapshotDate()).isEqualTo(analysis2.getCreatedAt());
-    assertThat(period.getAnalysisUuid()).isEqualTo(analysis2.getUuid());
-
-    verifyDebugLogs("Resolving new code period by version: 1.0");
+    assertThat(period.getMode()).isEqualTo(mode);
+    assertThat(period.getModeParameter()).isEqualTo(modeParameter);
+    assertThat(period.getSnapshotDate()).isEqualTo(snapshotDate);
+    assertThat(period.getAnalysisUuid()).isEqualTo(analysisUuid);
   }
 
   private void verifyDebugLogs(String log, String... otherLogs) {
index 80ebf6729941ce7f262a7e0b7ca709cd075bef46..c30758dc89501ece08007daea634294e8671b38e 100644 (file)
@@ -33,7 +33,6 @@ import static org.sonar.api.PropertyType.BOOLEAN;
 public class CorePropertyDefinitions {
 
   public static final String LEAK_PERIOD = "sonar.leak.period";
-  public static final String LEAK_PERIOD_MODE_PREVIOUS_ANALYSIS = "previous_analysis";
   public static final String LEAK_PERIOD_MODE_DATE = "date";
   public static final String LEAK_PERIOD_MODE_VERSION = "version";
   public static final String LEAK_PERIOD_MODE_DAYS = "days";
index 53e8ec65c10591188102e588298f743f3871761b..aa9a8bea9434e627a71d6c5504719df3b8c4fecc 100644 (file)
@@ -54,6 +54,15 @@ public final class DateUtils {
     return d.toInstant().atZone(ZoneId.systemDefault()).toLocalDate().toString();
   }
 
+  /**
+   * Warning: relies on default timezone!
+   *
+   * @since 7.6
+   */
+  public static String formatDate(Instant d) {
+    return d.atZone(ZoneId.systemDefault()).toLocalDate().toString();
+  }
+
   /**
    * Warning: relies on default timezone!
    */
@@ -94,6 +103,7 @@ public final class DateUtils {
 
   /**
    * Return a date at the start of day.
+   *
    * @param s string in format {@link #DATE_FORMAT}
    * @throws SonarException when string cannot be parsed
    */
@@ -218,8 +228,9 @@ public final class DateUtils {
 
   /**
    * Warning: may rely on default timezone!
-   * @throws IllegalArgumentException if stringDate is not a correctly formed date or datetime
+   *
    * @return the datetime, {@code null} if stringDate is null
+   * @throws IllegalArgumentException if stringDate is not a correctly formed date or datetime
    * @since 6.1
    */
   @CheckForNull
@@ -241,7 +252,8 @@ public final class DateUtils {
 
   /**
    * Warning: may rely on default timezone!
-   * @see #parseDateOrDateTime(String) 
+   *
+   * @see #parseDateOrDateTime(String)
    */
   @CheckForNull
   public static Date parseStartingDateOrDateTime(@Nullable String stringDate) {
@@ -251,9 +263,10 @@ public final class DateUtils {
   /**
    * Return the datetime if @param stringDate is a datetime, date + 1 day if stringDate is a date.
    * So '2016-09-01' would return a date equivalent to '2016-09-02T00:00:00+0000' in GMT (Warning: relies on default timezone!)
-   * @see #parseDateOrDateTime(String)
-   * @throws IllegalArgumentException if stringDate is not a correctly formed date or datetime
+   *
    * @return the datetime, {@code null} if stringDate is null
+   * @throws IllegalArgumentException if stringDate is not a correctly formed date or datetime
+   * @see #parseDateOrDateTime(String)
    * @since 6.1
    */
   @CheckForNull
@@ -277,14 +290,21 @@ public final class DateUtils {
    * Adds a number of days to a date returning a new object.
    * The original date object is unchanged.
    *
-   * @param date  the date, not null
-   * @param numberOfDays  the amount to add, may be negative
+   * @param date         the date, not null
+   * @param numberOfDays the amount to add, may be negative
    * @return the new date object with the amount added
    */
   public static Date addDays(Date date, int numberOfDays) {
     return Date.from(date.toInstant().plus(numberOfDays, ChronoUnit.DAYS));
   }
 
+  /**
+   * @since 7.6
+   */
+  public static Instant addDays(Instant instant, int numberOfDays) {
+    return instant.plus(numberOfDays, ChronoUnit.DAYS);
+  }
+
   @CheckForNull
   public static Date truncateToSeconds(@Nullable Date d) {
     if (d == null) {