Commit c62162f9 authored by Birger Schmidt's avatar Birger Schmidt

refactor OffPeakTimePreriods with timezone support

- Move the .In(location) call to inside of InPeriod().
  Then the GetCurrentTime() function will be responsible only for
  returning a current time for purpose of altering this time in tests.
- Update the timeSince calculation to use the time returned by
  GetCurrentTime() instead of internal time.Now().
- Update the comparison to check if the timeSince is inside of the
  closed interval [time-1s, time+1s] instead of the open interval
  (time-1s, time+1s).
- additional Tests for:
  - user uses the Europe/Berlin timezone business hours,
  - user uses UTC as Runner's host timezone,
  - user defines OffPeakPeriod representing the 10-18 hours each day.
  TimePeriod is initialized with above data (so period having the 10-18
  hours definition and Europe/Berlin timezone). The test should confirm
  that the InPeriod() call should return:
  - TRUE for the 2017-01-01 16:30:00 UTC
    (no DST time in Europe/Berlin, so the time is +1h to the UTC time -
    17:30 - which is inside of the time period),
  - FALSE for the 2017-07-01 16:30:00 UTC
    (DST time in Europe/Berlin, so the time is +2h to the UTC time -
    18:30 - which is outside of the time period).
parent 96a75bc8
......@@ -262,13 +262,18 @@ func (c *DockerMachine) isOffPeak() bool {
c.CompileOffPeakPeriods()
}
return c.offPeakTimePeriods != nil && c.offPeakTimePeriods.InPeriod()
return c.offPeakTimePeriods != nil && c.offPeakTimePeriods.InPeriod(c.OffPeakTimezone)
}
func (c *DockerMachine) CompileOffPeakPeriods() (err error) {
c.offPeakTimePeriods, err = timeperiod.TimePeriods(c.OffPeakPeriods, c.OffPeakTimezone)
c.offPeakTimePeriods, err = timeperiod.TimePeriods(c.OffPeakPeriods)
if err != nil {
err = errors.New(fmt.Sprint("Invalid OffPeakPeriods value: ", err))
return
}
_, err = time.LoadLocation(c.OffPeakTimezone)
if err != nil {
err = errors.New(fmt.Sprint("Invalid OffPeakTimeZone value: ", err))
}
return
......
package timeperiod
import (
"fmt"
"time"
"github.com/gorhill/cronexpr"
......@@ -11,11 +12,23 @@ type TimePeriod struct {
GetCurrentTime func() time.Time
}
func (t *TimePeriod) InPeriod() bool {
func (t *TimePeriod) InPeriod(timezone string) bool {
// if not set, default to system setting (the empty string would mean UTC)
if timezone == "" {
timezone = "Local"
}
location, err := time.LoadLocation(timezone)
if err != nil {
// I don't want this function to return an error code.
// The validity of the input should already be checked on config load.
// But to be sure and able to test, we have this here.
panic(fmt.Sprint("Invalid OffPeakTimeZone value: ", err))
}
now := t.GetCurrentTime().In(location)
for _, expression := range t.expressions {
nextIn := expression.Next(t.GetCurrentTime())
timeSince := time.Since(nextIn)
if timeSince < time.Second && timeSince > -time.Second {
nextIn := expression.Next(now)
timeSince := now.Sub(nextIn)
if timeSince <= time.Second && timeSince >= -time.Second {
return true
}
}
......@@ -23,7 +36,7 @@ func (t *TimePeriod) InPeriod() bool {
return false
}
func TimePeriods(periods []string, timezone string) (*TimePeriod, error) {
func TimePeriods(periods []string) (*TimePeriod, error) {
var expressions []*cronexpr.Expression
for _, period := range periods {
......@@ -35,18 +48,9 @@ func TimePeriods(periods []string, timezone string) (*TimePeriod, error) {
expressions = append(expressions, expression)
}
// if not set, default to system setting (the empty string would mean UTC)
if timezone == "" {
timezone = "Local"
}
location, err := time.LoadLocation(timezone)
if err != nil {
return nil, err
}
timePeriod := &TimePeriod{
expressions: expressions,
GetCurrentTime: func() time.Time { return time.Now().In(location) },
GetCurrentTime: func() time.Time { return time.Now() },
}
return timePeriod, nil
......
......@@ -18,65 +18,63 @@ var daysOfWeek = map[time.Weekday]string{
time.Sunday: "sun",
}
func newTimePeriods(t *testing.T, timezone string) (time.Time, *TimePeriod) {
location, err := time.LoadLocation(timezone)
now := time.Now().In(location)
func newTimePeriods(t *testing.T) (time.Time, *TimePeriod) {
now := time.Now()
minute := now.Minute()
hour := now.Hour()
day := now.Weekday()
dayName := daysOfWeek[day]
periodPattern := fmt.Sprintf("* %d %d * * %s *", minute, hour, dayName)
timePeriods, err := TimePeriods([]string{periodPattern}, timezone)
timePeriods, err := TimePeriods([]string{periodPattern})
assert.NoError(t, err)
return now, timePeriods
}
func TestWrongTimezone(t *testing.T) {
_, err := TimePeriods([]string{}, "NoValidTimezone/String")
assert.Error(t, err)
func TestOffPeakPeriod(t *testing.T) {
timePeriods, _ := TimePeriods([]string{"* * 10-17 * * * *"})
timePeriods.GetCurrentTime = func() time.Time {
return time.Date(2017, time.January, 1, 16, 30, 0, 0, time.UTC)
}
assert.True(t, timePeriods.InPeriod("Europe/Berlin"), "2017-01-01 16:30:00 UTC (no DST time in Europe/Berlin, so the time is +1h to UTC = 17:30 - which is inside of '* * 10-17 * * * *')")
timePeriods.GetCurrentTime = func() time.Time {
return time.Date(2017, time.July, 1, 16, 30, 0, 0, time.UTC)
}
assert.False(t, timePeriods.InPeriod("Europe/Berlin"), "2017-07-01 16:30:00 UTC (DST time in Europe/Berlin, so the time is +2h to UTC = 18:30 - which is outside of '* * 10-17 * * * *')")
}
func TestTimezone(t *testing.T) {
// make sure timezones are respected and make no difference
_, timePeriods := newTimePeriods(t, "Local")
assert.WithinDuration(t, timePeriods.GetCurrentTime(), time.Now(), 1*time.Second)
_, timePeriods = newTimePeriods(t, "America/New_York")
assert.WithinDuration(t, timePeriods.GetCurrentTime(), time.Now(), 1*time.Second)
_, timePeriods = newTimePeriods(t, "Australia/Sydney")
assert.WithinDuration(t, timePeriods.GetCurrentTime(), time.Now(), 1*time.Second)
// make sure timezones are calculated correctly
location, _ := time.LoadLocation("Australia/Sydney")
assert.Equal(t, timePeriods.GetCurrentTime().Format("3:04PM"), time.Now().In(location).Format("3:04PM"))
func TestWrongTimezone(t *testing.T) {
timePeriods, _ := TimePeriods([]string{})
assert.Panics(t, func() {
_ = timePeriods.InPeriod("NoValidTimezone/String")
}, "Calling InPeriod(\"NoValidTimezone/String\") should panic")
}
func TestInPeriod(t *testing.T) {
now, timePeriods := newTimePeriods(t, "")
now, timePeriods := newTimePeriods(t)
timePeriods.GetCurrentTime = func() time.Time {
return now
}
assert.True(t, timePeriods.InPeriod())
assert.True(t, timePeriods.InPeriod("Local"))
}
func TestPeriodOut(t *testing.T) {
now, timePeriods := newTimePeriods(t, "")
now, timePeriods := newTimePeriods(t)
timePeriods.GetCurrentTime = func() time.Time {
return now.Add(time.Hour * 48)
}
assert.False(t, timePeriods.InPeriod())
assert.False(t, timePeriods.InPeriod("Local"))
now, timePeriods = newTimePeriods(t, "")
now, timePeriods = newTimePeriods(t)
timePeriods.GetCurrentTime = func() time.Time {
return now.Add(time.Hour * 4)
}
assert.False(t, timePeriods.InPeriod())
assert.False(t, timePeriods.InPeriod("Local"))
now, timePeriods = newTimePeriods(t, "")
now, timePeriods = newTimePeriods(t)
timePeriods.GetCurrentTime = func() time.Time {
return now.Add(time.Minute * 4)
}
assert.False(t, timePeriods.InPeriod())
assert.False(t, timePeriods.InPeriod("Local"))
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment