aboutsummaryrefslogtreecommitdiffstats
path: root/config-model-api
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2022-01-07 10:56:41 +0100
committerMartin Polden <mpolden@mpolden.no>2022-01-07 14:43:37 +0100
commitae843dd9bd28be5c8445adf10dc7c0a6a69f2d97 (patch)
tree7d7c5b2fc602c54e3d634d0a0c07ab13d0ab1981 /config-model-api
parente11472ccb26601fe74004a2400b44ea000c88d6f (diff)
Support date range in block window
Diffstat (limited to 'config-model-api')
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java40
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/TimeWindow.java116
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java32
-rw-r--r--config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java51
-rw-r--r--config-model-api/src/test/java/com/yahoo/config/application/api/TimeWindowTest.java137
5 files changed, 307 insertions, 69 deletions
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java
index 23a5dce3c25..67ddb9ef83c 100644
--- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java
+++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java
@@ -6,13 +6,16 @@ import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.RegionName;
+import java.time.Duration;
import java.time.Instant;
+import java.time.temporal.ChronoUnit;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* The deployment spec for an application instance
@@ -21,6 +24,9 @@ import java.util.stream.Collectors;
*/
public class DeploymentInstanceSpec extends DeploymentSpec.Steps {
+ /** The maximum number of consecutive days Vespa upgrades are allowed to be blocked */
+ private static final int maxUpgradeBlockingDays = 21;
+
/** The name of the instance this step deploys */
private final InstanceName name;
@@ -40,7 +46,8 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps {
Optional<String> globalServiceId,
Optional<AthenzService> athenzService,
Notifications notifications,
- List<Endpoint> endpoints) {
+ List<Endpoint> endpoints,
+ Instant now) {
super(steps);
this.name = name;
this.upgradePolicy = upgradePolicy;
@@ -52,6 +59,7 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps {
this.endpoints = List.copyOf(endpoints);
validateZones(new HashSet<>(), new HashSet<>(), this);
validateEndpoints(steps(), globalServiceId, this.endpoints);
+ validateChangeBlockers(changeBlockers, now);
}
public InstanceName name() { return name; }
@@ -109,6 +117,36 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps {
}
}
+ private void validateChangeBlockers(List<DeploymentSpec.ChangeBlocker> changeBlockers, Instant now) {
+ // Find all possible dates an upgrade block window can start
+ Stream<Instant> blockingFrom = changeBlockers.stream()
+ .filter(blocker -> blocker.blocksVersions())
+ .map(blocker -> blocker.window())
+ .map(window -> window.dateRange().start()
+ .map(date -> date.atStartOfDay(window.zone())
+ .toInstant())
+ .orElse(now))
+ .distinct();
+ if (!blockingFrom.allMatch(this::canUpgradeWithinDeadline)) {
+ throw new IllegalArgumentException("Cannot block Vespa upgrades for longer than " +
+ maxUpgradeBlockingDays + " consecutive days");
+ }
+ }
+
+ /** Returns whether this allows upgrade within deadline, relative to given instant */
+ private boolean canUpgradeWithinDeadline(Instant instant) {
+ instant = instant.truncatedTo(ChronoUnit.HOURS);
+ Duration step = Duration.ofHours(1);
+ Duration max = Duration.ofDays(maxUpgradeBlockingDays);
+ for (Instant current = instant; !canUpgradeAt(current); current = current.plus(step)) {
+ Duration blocked = Duration.between(instant, current);
+ if (blocked.compareTo(max) > 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/** Returns the upgrade policy of this, which is defaultPolicy if none is specified */
public DeploymentSpec.UpgradePolicy upgradePolicy() { return upgradePolicy; }
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/TimeWindow.java b/config-model-api/src/main/java/com/yahoo/config/application/api/TimeWindow.java
index 5a2b3a10fe1..14deb5d5720 100644
--- a/config-model-api/src/main/java/com/yahoo/config/application/api/TimeWindow.java
+++ b/config-model-api/src/main/java/com/yahoo/config/application/api/TimeWindow.java
@@ -4,20 +4,23 @@ package com.yahoo.config.application.api;
import java.time.DateTimeException;
import java.time.DayOfWeek;
import java.time.Instant;
+import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
+import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
+import java.util.Optional;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
- * This class represents a window of time for selected hours on selected days.
+ * This class represents a window of time for selected hours, days and dates.
*
* @author mpolden
*/
@@ -26,11 +29,20 @@ public class TimeWindow {
private final List<DayOfWeek> days;
private final List<Integer> hours;
private final ZoneId zone;
+ private final LocalDateRange dateRange;
- private TimeWindow(List<DayOfWeek> days, List<Integer> hours, ZoneId zone) {
+ private TimeWindow(List<DayOfWeek> days, List<Integer> hours, ZoneId zone, LocalDateRange dateRange) {
this.days = Objects.requireNonNull(days).stream().distinct().sorted().collect(Collectors.toUnmodifiableList());
this.hours = Objects.requireNonNull(hours).stream().distinct().sorted().collect(Collectors.toUnmodifiableList());
this.zone = Objects.requireNonNull(zone);
+ this.dateRange = Objects.requireNonNull(dateRange);
+ if (days.isEmpty()) throw new IllegalArgumentException("At least one day must be specified");
+ if (hours.isEmpty()) throw new IllegalArgumentException("At least one hour must be specified");
+ for (var day : days) {
+ if (!dateRange.includes(day)) {
+ throw new IllegalArgumentException("Invalid day: " + dateRange + " does not contain " + day);
+ }
+ }
}
/** Returns days in this time window */
@@ -46,10 +58,17 @@ public class TimeWindow {
/** Returns the time zone of this time window */
public ZoneId zone() { return zone; }
+ /** Returns the date range of this time window applies to */
+ public LocalDateRange dateRange() {
+ return dateRange;
+ }
+
/** Returns whether the given instant is in this time window */
public boolean includes(Instant instant) {
LocalDateTime dt = LocalDateTime.ofInstant(instant, zone);
- return days.contains(dt.getDayOfWeek()) && hours.contains(dt.getHour());
+ return days.contains(dt.getDayOfWeek()) &&
+ hours.contains(dt.getHour()) &&
+ dateRange.includes(dt.toLocalDate());
}
@Override
@@ -59,15 +78,19 @@ public class TimeWindow {
" on " + days.stream().map(DayOfWeek::name)
.map(String::toLowerCase)
.collect(Collectors.toList()) +
- " in " + zone;
+ " in time zone " + zone + " and " + dateRange.toString();
}
/** Parse a time window from the given day, hour and time zone specification */
- public static TimeWindow from(String daySpec, String hourSpec, String zoneSpec) {
- List<DayOfWeek> days = parse(daySpec, TimeWindow::parseDays);
- List<Integer> hours = parse(hourSpec, TimeWindow::parseHours);
- ZoneId zone = zoneFrom(zoneSpec);
- return new TimeWindow(days, hours, zone);
+ public static TimeWindow from(String daySpec, String hourSpec, String zoneSpec, String dateStart, String dateEnd) {
+ List<DayOfWeek> days = daySpec.isEmpty()
+ ? List.of(DayOfWeek.values()) // All days by default
+ : parse(daySpec, TimeWindow::parseDays);
+ List<Integer> hours = hourSpec.isEmpty()
+ ? IntStream.rangeClosed(0, 23).boxed().collect(Collectors.toList()) // All hours by default
+ : parse(hourSpec, TimeWindow::parseHours);
+ ZoneId zone = zoneFrom(zoneSpec.isEmpty() ? "UTC" : zoneSpec);
+ return new TimeWindow(days, hours, zone, LocalDateRange.from(dateStart, dateEnd));
}
/** Parse a specification, e.g. "1,4-5", using the given value parser */
@@ -97,7 +120,7 @@ public class TimeWindow {
endInclusive));
}
return IntStream.rangeClosed(start, end).boxed()
- .collect(Collectors.toList());
+ .collect(Collectors.toList());
}
/** Returns a list of all days occurring between startInclusive and endInclusive */
@@ -109,16 +132,16 @@ public class TimeWindow {
endInclusive));
}
return IntStream.rangeClosed(start.getValue(), end.getValue()).boxed()
- .map(DayOfWeek::of)
- .collect(Collectors.toList());
+ .map(DayOfWeek::of)
+ .collect(Collectors.toList());
}
/** Parse day of week from string */
private static DayOfWeek dayFrom(String day) {
return Arrays.stream(DayOfWeek.values())
- .filter(dayOfWeek -> day.length() >= 3 && dayOfWeek.name().toLowerCase().startsWith(day))
- .findFirst()
- .orElseThrow(() -> new IllegalArgumentException("Invalid day '" + day + "'"));
+ .filter(dayOfWeek -> day.length() >= 3 && dayOfWeek.name().toLowerCase().startsWith(day))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("Invalid day '" + day + "'"));
}
/** Parse hour from string */
@@ -139,4 +162,67 @@ public class TimeWindow {
}
}
+ /** A range of local dates, which may be unbounded */
+ public static class LocalDateRange {
+
+ private final Optional<LocalDate> start;
+ private final Optional<LocalDate> end;
+
+ private LocalDateRange(Optional<LocalDate> start, Optional<LocalDate> end) {
+ this.start = Objects.requireNonNull(start);
+ this.end = Objects.requireNonNull(end);
+ if (start.isPresent() && end.isPresent() && start.get().isAfter(end.get())) {
+ throw new IllegalArgumentException("Invalid date range: start date " + start.get() +
+ " is after end date " + end.get());
+ }
+ }
+
+ /** Returns the starting date of this (inclusive), if any */
+ public Optional<LocalDate> start() {
+ return start;
+ }
+
+ /** Returns the ending date of this (inclusive), if any */
+ public Optional<LocalDate> end() {
+ return end;
+ }
+
+ /** Returns whether this contains the given day */
+ private boolean includes(DayOfWeek day) {
+ if (start.isEmpty() || end.isEmpty()) return true;
+ for (LocalDate date = start.get(); !date.isAfter(end.get()); date = date.plusDays(1)) {
+ if (date.getDayOfWeek() == day) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Returns whether includes the given date */
+ private boolean includes(LocalDate date) {
+ if (start.isPresent() && date.isBefore(start.get())) return false;
+ if (end.isPresent() && date.isAfter(end.get())) return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "date range [" + start.map(LocalDate::toString).orElse("any date") +
+ ", " + end.map(LocalDate::toString).orElse("any date") + "]";
+ }
+
+ private static LocalDateRange from(String start, String end) {
+ try {
+ return new LocalDateRange(optionalDate(start), optionalDate(end));
+ } catch (DateTimeParseException e) {
+ throw new IllegalArgumentException("Could not parse date range '" + start + "' and '" + end + "'", e);
+ }
+ }
+
+ private static Optional<LocalDate> optionalDate(String date) {
+ return Optional.of(date).filter(s -> !s.isEmpty()).map(LocalDate::parse);
+ }
+
+ }
+
}
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java
index 493a72b2018..8f866654d56 100644
--- a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java
+++ b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java
@@ -27,7 +27,9 @@ import org.w3c.dom.Node;
import java.io.IOException;
import java.io.Reader;
+import java.time.Clock;
import java.time.Duration;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -67,22 +69,28 @@ public class DeploymentSpecXmlReader {
private static final String testerFlavorAttribute = "tester-flavor";
private final boolean validate;
+ private final Clock clock;
private final List<DeprecatedElement> deprecatedElements = new ArrayList<>();
- /** Creates a validating reader */
+ /**
+ * Create a deployment spec reader
+ * @param validate true to validate the input, false to accept any input which can be unambiguously parsed
+ * @param clock clock to use when validating time constraints
+ */
+ public DeploymentSpecXmlReader(boolean validate, Clock clock) {
+ this.validate = validate;
+ this.clock = clock;
+ }
+
public DeploymentSpecXmlReader() {
this(true);
}
- /**
- * Creates a deployment spec reader
- *
- * @param validate true to validate the input, false to accept any input which can be unambiguously parsed
- */
public DeploymentSpecXmlReader(boolean validate) {
- this.validate = validate;
+ this(validate, Clock.systemUTC());
}
+ /** Reads a deployment spec from given reader */
public DeploymentSpec read(Reader reader) {
try {
return read(IOUtils.readAll(reader));
@@ -169,6 +177,7 @@ public class DeploymentSpecXmlReader {
List<Endpoint> endpoints = readEndpoints(instanceTag, Optional.of(instanceNameString), steps);
// Build and return instances with these values
+ Instant now = clock.instant();
return Arrays.stream(instanceNameString.split(","))
.map(name -> name.trim())
.map(name -> new DeploymentInstanceSpec(InstanceName.from(name),
@@ -179,7 +188,8 @@ public class DeploymentSpecXmlReader {
globalServiceId.asOptional(),
athenzService,
notifications,
- endpoints))
+ endpoints,
+ now))
.collect(Collectors.toList());
}
@@ -430,9 +440,11 @@ public class DeploymentSpecXmlReader {
String daySpec = tag.getAttribute("days");
String hourSpec = tag.getAttribute("hours");
String zoneSpec = tag.getAttribute("time-zone");
- if (zoneSpec.isEmpty()) zoneSpec = "UTC"; // default
+ String dateStart = tag.getAttribute("from-date");
+ String dateEnd = tag.getAttribute("to-date");
+
return new DeploymentSpec.ChangeBlocker(blockRevisions, blockVersions,
- TimeWindow.from(daySpec, hourSpec, zoneSpec));
+ TimeWindow.from(daySpec, hourSpec, zoneSpec, dateStart, dateEnd));
}
/** Returns true if the given value is "true", or if it is missing */
diff --git a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java
index 43ccc34284f..2fa2ba83291 100644
--- a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java
+++ b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java
@@ -2,12 +2,15 @@
package com.yahoo.config.application.api;
import com.google.common.collect.ImmutableSet;
+import com.yahoo.config.application.api.xml.DeploymentSpecXmlReader;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.RegionName;
+import com.yahoo.test.ManualClock;
import org.junit.Test;
import java.io.StringReader;
+import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
@@ -771,6 +774,7 @@ public class DeploymentSpecTest {
" <instance id='default'>" +
" <block-change revision='false' days='mon,tue' hours='15-16'/>" +
" <block-change days='sat' hours='10' time-zone='CET'/>" +
+ " <block-change days='mon-sun' hours='0-23' time-zone='CET' from-date='2022-01-01' to-date='2022-01-15'/>" +
" <prod>" +
" <region active='true'>us-west-1</region>" +
" </prod>" +
@@ -778,7 +782,7 @@ public class DeploymentSpecTest {
"</deployment>"
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
- assertEquals(2, spec.requireInstance("default").changeBlocker().size());
+ assertEquals(3, spec.requireInstance("default").changeBlocker().size());
assertTrue(spec.requireInstance("default").changeBlocker().get(0).blocksVersions());
assertFalse(spec.requireInstance("default").changeBlocker().get(0).blocksRevisions());
assertEquals(ZoneId.of("UTC"), spec.requireInstance("default").changeBlocker().get(0).window().zone());
@@ -795,6 +799,8 @@ public class DeploymentSpecTest {
assertTrue(spec.requireInstance("default").canUpgradeAt(Instant.parse("2017-09-23T09:15:30.00Z")));
assertFalse(spec.requireInstance("default").canUpgradeAt(Instant.parse("2017-09-23T08:15:30.00Z"))); // 10 in CET
assertTrue(spec.requireInstance("default").canUpgradeAt(Instant.parse("2017-09-23T10:15:30.00Z")));
+
+ assertFalse(spec.requireInstance("default").canUpgradeAt(Instant.parse("2022-01-15T16:00:00.00Z")));
}
@Test
@@ -812,11 +818,13 @@ public class DeploymentSpecTest {
DeploymentSpec spec = DeploymentSpec.fromXml(r);
- String inheritedChangeBlocker = "change blocker revision=false version=true window=time window for hour(s) [15, 16] on [monday, tuesday] in UTC";
+ String inheritedChangeBlocker = "change blocker revision=false version=true window=time window for hour(s) " +
+ "[15, 16] on [monday, tuesday] in time zone UTC and date range [any date, any date]";
assertEquals(2, spec.requireInstance("instance1").changeBlocker().size());
assertEquals(inheritedChangeBlocker, spec.requireInstance("instance1").changeBlocker().get(0).toString());
- assertEquals("change blocker revision=true version=true window=time window for hour(s) [10] on [saturday] in CET",
+ assertEquals("change blocker revision=true version=true window=time window for hour(s) [10] on " +
+ "[saturday] in time zone CET and date range [any date, any date]",
spec.requireInstance("instance1").changeBlocker().get(1).toString());
assertEquals(1, spec.requireInstance("instance2").changeBlocker().size());
@@ -1269,10 +1277,45 @@ public class DeploymentSpecTest {
spec.requireInstance("main").endpoints());
}
+ @Test
+ public void disallowExcessiveUpgradeBlocking() {
+ List<String> specs = List.of(
+ "<deployment>\n" +
+ " <block-change/>\n" +
+ "</deployment>",
+
+ "<deployment>\n" +
+ " <block-change days=\"mon-wed\"/>\n" +
+ " <block-change days=\"tue-sun\"/>\n" +
+ "</deployment>",
+
+ "<deployment>\n" +
+ " <block-change to-date=\"2023-01-01\"/>\n" +
+ "</deployment>",
+
+ // Convoluted example of blocking too long
+ "<deployment>\n" +
+ " <block-change days=\"sat-sun\"/>\n" +
+ " <block-change days=\"mon-fri\" hours=\"0-10\" from-date=\"2023-01-01\" to-date=\"2023-01-15\"/>\n" +
+ " <block-change days=\"mon-fri\" hours=\"11-23\" from-date=\"2023-01-01\" to-date=\"2023-01-15\"/>\n" +
+ " <block-change from-date=\"2023-01-14\" to-date=\"2023-01-31\"/>" +
+ "</deployment>"
+ );
+ ManualClock clock = new ManualClock();
+ clock.setInstant(Instant.parse("2022-01-05T15:00:00.00Z"));
+ for (var spec : specs) {
+ assertInvalid(spec, "Cannot block Vespa upgrades for longer than 21 consecutive days", clock);
+ }
+ }
+
private static void assertInvalid(String deploymentSpec, String errorMessagePart) {
+ assertInvalid(deploymentSpec, errorMessagePart, new ManualClock());
+ }
+
+ private static void assertInvalid(String deploymentSpec, String errorMessagePart, Clock clock) {
if (errorMessagePart.isEmpty()) throw new IllegalArgumentException("Message part must be non-empty");
try {
- DeploymentSpec.fromXml(deploymentSpec);
+ new DeploymentSpecXmlReader(true, clock).read(deploymentSpec);
fail("Expected exception");
} catch (IllegalArgumentException e) {
assertTrue("\"" + e.getMessage() + "\" contains \"" + errorMessagePart + "\"",
diff --git a/config-model-api/src/test/java/com/yahoo/config/application/api/TimeWindowTest.java b/config-model-api/src/test/java/com/yahoo/config/application/api/TimeWindowTest.java
index 98a53dfd3df..a76f156ebb1 100644
--- a/config-model-api/src/test/java/com/yahoo/config/application/api/TimeWindowTest.java
+++ b/config-model-api/src/test/java/com/yahoo/config/application/api/TimeWindowTest.java
@@ -3,7 +3,11 @@ package com.yahoo.config.application.api;
import org.junit.Test;
+import java.time.DayOfWeek;
import java.time.Instant;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
import static java.time.DayOfWeek.FRIDAY;
import static java.time.DayOfWeek.MONDAY;
@@ -11,7 +15,6 @@ import static java.time.DayOfWeek.SATURDAY;
import static java.time.DayOfWeek.THURSDAY;
import static java.time.DayOfWeek.TUESDAY;
import static java.time.DayOfWeek.WEDNESDAY;
-import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -25,71 +28,106 @@ public class TimeWindowTest {
@Test
public void includesInstant() {
{
- TimeWindow tw = TimeWindow.from("mon", "10,11", "UTC");
+ TimeWindow tw = TimeWindow.from("mon", "10,11", "UTC", "", "");
Instant i0 = Instant.parse("2017-09-17T11:15:30.00Z"); // Wrong day
Instant i1 = Instant.parse("2017-09-18T09:15:30.00Z"); // Wrong hour
Instant i2 = Instant.parse("2017-09-18T10:15:30.00Z");
Instant i3 = Instant.parse("2017-09-18T11:15:30.00Z");
Instant i4 = Instant.parse("2017-09-18T12:15:30.00Z"); // Wrong hour
Instant i5 = Instant.parse("2017-09-19T11:15:30.00Z"); // Wrong day
-
- assertFalse("Instant " + i0 + " is not in window", tw.includes(i0));
- assertFalse("Instant " + i1 + " is not in window", tw.includes(i1));
- assertTrue("Instant " + i2 + " is in window", tw.includes(i2));
- assertTrue("Instant " + i3 + " is in window", tw.includes(i3));
- assertFalse("Instant " + i4 + " is not in window", tw.includes(i4));
- assertFalse("Instant " + i5 + " is not in window", tw.includes(i5));
+ assertOutside(tw, i0);
+ assertOutside(tw, i1);
+ assertInside(tw, i2);
+ assertInside(tw, i3);
+ assertOutside(tw, i4);
+ assertOutside(tw, i5);
}
{
- TimeWindow tw = TimeWindow.from("mon", "12,13", "CET");
+ TimeWindow tw = TimeWindow.from("mon", "12,13", "CET", "", "");
Instant i0 = Instant.parse("2017-09-17T11:15:30.00Z");
Instant i1 = Instant.parse("2017-09-18T09:15:30.00Z");
Instant i2 = Instant.parse("2017-09-18T10:15:30.00Z"); // Including offset this matches hour 12
Instant i3 = Instant.parse("2017-09-18T11:15:30.00Z"); // Including offset this matches hour 13
Instant i4 = Instant.parse("2017-09-18T12:15:30.00Z");
Instant i5 = Instant.parse("2017-09-19T11:15:30.00Z");
- assertFalse("Instant " + i0 + " is not in window", tw.includes(i0));
- assertFalse("Instant " + i1 + " is not in window", tw.includes(i1));
- assertTrue("Instant " + i2 + " is in window", tw.includes(i2));
- assertTrue("Instant " + i3 + " is in window", tw.includes(i3));
- assertFalse("Instant " + i4 + " is not in window", tw.includes(i4));
- assertFalse("Instant " + i5 + " is not in window", tw.includes(i5));
+ assertOutside(tw, i0);
+ assertOutside(tw, i1);
+ assertInside(tw, i2);
+ assertInside(tw, i3);
+ assertOutside(tw, i4);
+ assertOutside(tw, i5);
+ }
+ {
+ TimeWindow tw = TimeWindow.from("mon-sun", "0-23", "CET", "2022-01-15", "2022-02-15");
+ Instant i0 = Instant.parse("2022-01-14T12:00:00.00Z"); // Before window
+ Instant i1 = Instant.parse("2022-01-14T23:00:00.00Z"); // Inside window because of time zone offset
+ Instant i2 = Instant.parse("2022-02-05T12:00:00.00Z");
+ Instant i3 = Instant.parse("2022-02-14T23:00:00.00Z");
+ Instant i4 = Instant.parse("2022-02-15T23:00:00.00Z"); // After window because of time zone offset
+ Instant i5 = Instant.parse("2022-02-16T12:00:00.00Z"); // After window
+ assertOutside(tw, i0);
+ assertInside(tw, i1);
+ assertInside(tw, i2);
+ assertInside(tw, i3);
+ assertOutside(tw, i4);
+ assertOutside(tw, i5);
+
+ TimeWindow tw2 = TimeWindow.from("sun", "1", "CET", "2022-01-01", "2022-01-02");
+ Instant i6 = Instant.parse("2022-01-01T00:00:00.00Z"); // Wrong day
+ Instant i7 = Instant.parse("2022-01-02T01:00:00.00Z"); // Wrong hour because of time zone offset
+ Instant i8 = Instant.parse("2022-01-02T00:00:00.00Z");
+ assertOutside(tw2, i6);
+ assertOutside(tw2, i7);
+ assertInside(tw2, i8);
+
+ TimeWindow tw3 = TimeWindow.from("", "", "CET", "2022-01-02", "");
+ Instant i9 = Instant.parse("2022-02-15T00:00:00.00Z");
+ assertOutside(tw3, i6);
+ assertInside(tw3, i7);
+ assertInside(tw3, i8);
+ assertInside(tw3, i9);
}
}
@Test
public void validWindows() {
{
- TimeWindow fz = TimeWindow.from("fri", "8,17-19", "UTC");
- assertEquals(asList(FRIDAY), fz.days());
- assertEquals(asList(8, 17, 18, 19), fz.hours());
+ TimeWindow tw = TimeWindow.from("fri", "8,17-19", "UTC", "", "");
+ assertEquals(List.of(FRIDAY), tw.days());
+ assertEquals(List.of(8, 17, 18, 19), tw.hours());
}
{
- TimeWindow fz = TimeWindow.from("sat,", "8,17-19", "UTC");
- assertEquals(asList(SATURDAY), fz.days());
- assertEquals(asList(8, 17, 18, 19), fz.hours());
+ TimeWindow tw = TimeWindow.from("sat,", "8,17-19", "UTC", "", "");
+ assertEquals(List.of(SATURDAY), tw.days());
+ assertEquals(List.of(8, 17, 18, 19), tw.hours());
}
{
- TimeWindow fz = TimeWindow.from("tue,sat", "0,3,7,10", "UTC");
- assertEquals(asList(TUESDAY, SATURDAY), fz.days());
- assertEquals(asList(0, 3, 7, 10), fz.hours());
+ TimeWindow tw = TimeWindow.from("tue,sat", "0,3,7,10", "UTC", "", "");
+ assertEquals(List.of(TUESDAY, SATURDAY), tw.days());
+ assertEquals(List.of(0, 3, 7, 10), tw.hours());
}
{
- TimeWindow fz = TimeWindow.from("mon,wed-thu", "0,17-19", "UTC");
- assertEquals(asList(MONDAY, WEDNESDAY, THURSDAY), fz.days());
- assertEquals(asList(0, 17, 18, 19), fz.hours());
+ TimeWindow tw = TimeWindow.from("mon,wed-thu", "0,17-19", "UTC", "", "");
+ assertEquals(List.of(MONDAY, WEDNESDAY, THURSDAY), tw.days());
+ assertEquals(List.of(0, 17, 18, 19), tw.hours());
+ }
+ { // Empty results in default values
+ TimeWindow tw = TimeWindow.from("", "", "", "", "");
+ assertEquals(List.of(DayOfWeek.values()), tw.days());
+ assertEquals(IntStream.rangeClosed(0, 23).boxed().collect(Collectors.toList()), tw.hours());
+ assertEquals("UTC", tw.zone().getId());
}
{
// Full day names is allowed
- TimeWindow fz = TimeWindow.from("monday,wednesday-thursday", "0,17-19", "UTC");
- assertEquals(asList(MONDAY, WEDNESDAY, THURSDAY), fz.days());
- assertEquals(asList(0, 17, 18, 19), fz.hours());
+ TimeWindow tw = TimeWindow.from("monday,wednesday-thursday", "0,17-19", "UTC", "", "");
+ assertEquals(List.of(MONDAY, WEDNESDAY, THURSDAY), tw.days());
+ assertEquals(List.of(0, 17, 18, 19), tw.hours());
}
{
// Duplicate day and overlapping range is allowed
- TimeWindow fz = TimeWindow.from("mon,wed-thu,mon", "3,1-4", "UTC");
- assertEquals(asList(MONDAY, WEDNESDAY, THURSDAY), fz.days());
- assertEquals(asList(1, 2, 3, 4), fz.hours());
+ TimeWindow tw = TimeWindow.from("mon,wed-thu,mon", "3,1-4", "UTC", "", "");
+ assertEquals(List.of(MONDAY, WEDNESDAY, THURSDAY), tw.days());
+ assertEquals(List.of(1, 2, 3, 4), tw.hours());
}
}
@@ -99,7 +137,6 @@ public class TimeWindowTest {
assertInvalidZone("foo", "Invalid time zone 'foo'");
// Malformed day input
- assertInvalidDays("", "Invalid day ''");
assertInvalidDays("foo-", "Invalid range 'foo-'");
assertInvalidDays("foo", "Invalid day 'foo'");
assertInvalidDays("f", "Invalid day 'f'");
@@ -107,16 +144,29 @@ public class TimeWindowTest {
assertInvalidDays("fri-tue", "Invalid day range 'fri-tue'");
// Malformed hour input
- assertInvalidHours("", "Invalid hour ''");
assertInvalidHours("24", "Invalid hour '24'");
assertInvalidHours("-1-9", "Invalid range '-1-9'");
// Window crossing day boundary is disallowed
assertInvalidHours("23-1", "Invalid hour range '23-1'");
+
+ // Invalid date range
+ assertInvalidDateRange("", "foo", "bar", "Could not parse date range 'foo' and 'bar'");
+ assertInvalidDateRange("", "2022-01-15", "2022-01-01", "Invalid date range: start date 2022-01-15 is after end date 2022-01-01");
+ assertInvalidDateRange("wed", "2022-01-06", "2022-01-09", "Invalid day: date range [2022-01-06, 2022-01-09] does not contain WEDNESDAY");
+ assertInvalidDateRange("mon-sun", "2022-01-03", "2022-01-07", "Invalid day: date range [2022-01-03, 2022-01-07] does not contain SATURDAY");
+ }
+
+ private static void assertOutside(TimeWindow window, Instant instant) {
+ assertFalse("Instant " + instant + " is not in window", window.includes(instant));
+ }
+
+ private static void assertInside(TimeWindow window, Instant instant) {
+ assertTrue("Instant " + instant + " is in window", window.includes(instant));
}
private static void assertInvalidZone(String zoneSpec, String exceptionMessage) {
try {
- TimeWindow.from("mon", "1", zoneSpec);
+ TimeWindow.from("mon", "1", zoneSpec, "", "");
fail("Expected exception");
} catch (IllegalArgumentException e) {
assertEquals(exceptionMessage, e.getMessage());
@@ -125,7 +175,7 @@ public class TimeWindowTest {
private static void assertInvalidDays(String daySpec, String exceptionMessage) {
try {
- TimeWindow.from(daySpec, "1", "UTC");
+ TimeWindow.from(daySpec, "1", "UTC", "", "");
fail("Expected exception");
} catch (IllegalArgumentException e) {
assertEquals(exceptionMessage, e.getMessage());
@@ -134,11 +184,20 @@ public class TimeWindowTest {
private static void assertInvalidHours(String hourSpec, String exceptionMessage) {
try {
- TimeWindow.from("mon", hourSpec, "UTC");
+ TimeWindow.from("mon", hourSpec, "UTC", "", "");
fail("Expected exception");
} catch (IllegalArgumentException e) {
assertEquals(exceptionMessage, e.getMessage());
}
}
+ private static void assertInvalidDateRange(String daySpec, String startDate, String endDate, String message) {
+ try {
+ TimeWindow.from(daySpec, "", "UTC", startDate, endDate);
+ fail("Expected exception");
+ } catch (IllegalArgumentException e) {
+ assertEquals(message, e.getMessage());
+ }
+ }
+
}