coder/coderd/schedule/autostop_test.go

688 lines
25 KiB
Go

package schedule_test
import (
"context"
"database/sql"
"fmt"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/schedule"
"github.com/coder/coder/v2/coderd/schedule/cron"
"github.com/coder/coder/v2/testutil"
)
func TestCalculateAutoStop(t *testing.T) {
t.Parallel()
now := time.Now()
chicago, err := time.LoadLocation("America/Chicago")
require.NoError(t, err, "loading chicago time location")
// pastDateNight is 9:45pm on a wednesday
pastDateNight := time.Date(2024, 2, 14, 21, 45, 0, 0, chicago)
// Wednesday the 8th of February 2023 at midnight. This date was
// specifically chosen as it doesn't fall on a applicable week for both
// fortnightly and triweekly autostop requirements.
wednesdayMidnightUTC := time.Date(2023, 2, 8, 0, 0, 0, 0, time.UTC)
sydneyQuietHours := "CRON_TZ=Australia/Sydney 0 0 * * *"
sydneyLoc, err := time.LoadLocation("Australia/Sydney")
require.NoError(t, err)
// 10pm on Friday the 10th of February 2023 in Sydney.
fridayEveningSydney := time.Date(2023, 2, 10, 22, 0, 0, 0, sydneyLoc)
// 12am on Saturday the 11th of February2023 in Sydney.
saturdayMidnightSydney := time.Date(2023, 2, 11, 0, 0, 0, 0, sydneyLoc)
t.Log("now", now)
t.Log("wednesdayMidnightUTC", wednesdayMidnightUTC)
t.Log("fridayEveningSydney", fridayEveningSydney)
t.Log("saturdayMidnightSydney", saturdayMidnightSydney)
dstIn := time.Date(2023, 10, 1, 2, 0, 0, 0, sydneyLoc) // 1 hour backward
dstInQuietHours := "CRON_TZ=Australia/Sydney 30 2 * * *" // never
// The expected behavior is that we will pick the next time that falls on
// quiet hours after the DST transition. In this case, it will be the same
// time the next day.
dstInQuietHoursExpectedTime := time.Date(2023, 10, 2, 2, 30, 0, 0, sydneyLoc)
beforeDstIn := time.Date(2023, 10, 1, 0, 0, 0, 0, sydneyLoc)
saturdayMidnightAfterDstIn := time.Date(2023, 10, 7, 0, 0, 0, 0, sydneyLoc)
// Wednesday after DST starts.
duringDst := time.Date(2023, 10, 4, 0, 0, 0, 0, sydneyLoc)
saturdayMidnightAfterDuringDst := saturdayMidnightAfterDstIn
dstOut := time.Date(2024, 4, 7, 3, 0, 0, 0, sydneyLoc) // 1 hour forward
dstOutQuietHours := "CRON_TZ=Australia/Sydney 30 3 * * *" // twice
dstOutQuietHoursExpectedTime := time.Date(2024, 4, 7, 3, 30, 0, 0, sydneyLoc) // in reality, this is the first occurrence
beforeDstOut := time.Date(2024, 4, 7, 0, 0, 0, 0, sydneyLoc)
saturdayMidnightAfterDstOut := time.Date(2024, 4, 13, 0, 0, 0, 0, sydneyLoc)
t.Log("dstIn", dstIn)
t.Log("beforeDstIn", beforeDstIn)
t.Log("saturdayMidnightAfterDstIn", saturdayMidnightAfterDstIn)
t.Log("dstOut", dstOut)
t.Log("beforeDstOut", beforeDstOut)
t.Log("saturdayMidnightAfterDstOut", saturdayMidnightAfterDstOut)
cases := []struct {
name string
now time.Time
wsAutostart string
templateAutoStart schedule.TemplateAutostartRequirement
templateAllowAutostop bool
templateDefaultTTL time.Duration
templateAutostopRequirement schedule.TemplateAutostopRequirement
userQuietHoursSchedule string
// workspaceTTL is usually copied from the template's TTL when the
// workspace is made, so it takes precedence unless
// templateAllowAutostop is false.
workspaceTTL time.Duration
// expectedDeadline is copied from expectedMaxDeadline if unset.
expectedDeadline time.Time
expectedMaxDeadline time.Time
errContains string
}{
{
name: "OK",
now: now,
templateAllowAutostop: true,
templateDefaultTTL: 0,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
workspaceTTL: 0,
expectedDeadline: time.Time{},
expectedMaxDeadline: time.Time{},
},
{
name: "Delete",
now: now,
templateAllowAutostop: true,
templateDefaultTTL: 0,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
workspaceTTL: 0,
expectedDeadline: time.Time{},
expectedMaxDeadline: time.Time{},
},
{
name: "WorkspaceTTL",
now: now,
templateAllowAutostop: true,
templateDefaultTTL: 0,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
workspaceTTL: time.Hour,
expectedDeadline: now.Add(time.Hour),
expectedMaxDeadline: time.Time{},
},
{
name: "TemplateDefaultTTLIgnored",
now: now,
templateAllowAutostop: true,
templateDefaultTTL: time.Hour,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
workspaceTTL: 0,
expectedDeadline: time.Time{},
expectedMaxDeadline: time.Time{},
},
{
name: "WorkspaceTTLOverridesTemplateDefaultTTL",
now: now,
templateAllowAutostop: true,
templateDefaultTTL: 2 * time.Hour,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
workspaceTTL: time.Hour,
expectedDeadline: now.Add(time.Hour),
expectedMaxDeadline: time.Time{},
},
{
name: "TemplateBlockWorkspaceTTL",
now: now,
templateAllowAutostop: false,
templateDefaultTTL: 3 * time.Hour,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
workspaceTTL: 4 * time.Hour,
expectedDeadline: now.Add(3 * time.Hour),
expectedMaxDeadline: time.Time{},
},
{
name: "TemplateAutostopRequirement",
now: wednesdayMidnightUTC,
templateAllowAutostop: true,
templateDefaultTTL: 0,
userQuietHoursSchedule: sydneyQuietHours,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: 0b00100000, // Saturday
Weeks: 0, // weekly
},
workspaceTTL: 0,
// expectedDeadline is copied from expectedMaxDeadline.
expectedMaxDeadline: saturdayMidnightSydney.In(time.UTC),
},
{
name: "TemplateAutostopRequirement1HourSkip",
now: saturdayMidnightSydney.Add(-59 * time.Minute),
templateAllowAutostop: true,
templateDefaultTTL: 0,
userQuietHoursSchedule: sydneyQuietHours,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: 0b00100000, // Saturday
Weeks: 1, // 1 also means weekly
},
workspaceTTL: 0,
// expectedDeadline is copied from expectedMaxDeadline.
expectedMaxDeadline: saturdayMidnightSydney.Add(7 * 24 * time.Hour).In(time.UTC),
},
{
// The next autostop requirement should be skipped if the
// workspace is started within 1 hour of it.
name: "TemplateAutostopRequirementDaily",
now: fridayEveningSydney,
templateAllowAutostop: true,
templateDefaultTTL: 0,
userQuietHoursSchedule: sydneyQuietHours,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: 0b01111111, // daily
Weeks: 0, // all weeks
},
workspaceTTL: 0,
// expectedDeadline is copied from expectedMaxDeadline.
expectedMaxDeadline: saturdayMidnightSydney.In(time.UTC),
},
{
name: "TemplateAutostopRequirementFortnightly/Skip",
now: wednesdayMidnightUTC,
templateAllowAutostop: true,
templateDefaultTTL: 0,
userQuietHoursSchedule: sydneyQuietHours,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: 0b00100000, // Saturday
Weeks: 2, // every 2 weeks
},
workspaceTTL: 0,
// expectedDeadline is copied from expectedMaxDeadline.
expectedMaxDeadline: saturdayMidnightSydney.AddDate(0, 0, 7).In(time.UTC),
},
{
name: "TemplateAutostopRequirementFortnightly/NoSkip",
now: wednesdayMidnightUTC.AddDate(0, 0, 7),
templateAllowAutostop: true,
templateDefaultTTL: 0,
userQuietHoursSchedule: sydneyQuietHours,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: 0b00100000, // Saturday
Weeks: 2, // every 2 weeks
},
workspaceTTL: 0,
// expectedDeadline is copied from expectedMaxDeadline.
expectedMaxDeadline: saturdayMidnightSydney.AddDate(0, 0, 7).In(time.UTC),
},
{
name: "TemplateAutostopRequirementTriweekly/Skip",
now: wednesdayMidnightUTC,
templateAllowAutostop: true,
templateDefaultTTL: 0,
userQuietHoursSchedule: sydneyQuietHours,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: 0b00100000, // Saturday
Weeks: 3, // every 3 weeks
},
workspaceTTL: 0,
// expectedDeadline is copied from expectedMaxDeadline.
// The next triweekly autostop requirement happens next week
// according to the epoch.
expectedMaxDeadline: saturdayMidnightSydney.AddDate(0, 0, 7).In(time.UTC),
},
{
name: "TemplateAutostopRequirementTriweekly/NoSkip",
now: wednesdayMidnightUTC.AddDate(0, 0, 7),
templateAllowAutostop: true,
templateDefaultTTL: 0,
userQuietHoursSchedule: sydneyQuietHours,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: 0b00100000, // Saturday
Weeks: 3, // every 3 weeks
},
workspaceTTL: 0,
// expectedDeadline is copied from expectedMaxDeadline.
expectedMaxDeadline: saturdayMidnightSydney.AddDate(0, 0, 7).In(time.UTC),
},
{
name: "TemplateAutostopRequirementOverridesWorkspaceTTL",
// now doesn't have to be UTC, but it helps us ensure that
// timezones are compared correctly in this test.
now: fridayEveningSydney.In(time.UTC),
templateAllowAutostop: true,
templateDefaultTTL: 0,
userQuietHoursSchedule: sydneyQuietHours,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: 0b00100000, // Saturday
Weeks: 0, // weekly
},
workspaceTTL: 3 * time.Hour,
// expectedDeadline is copied from expectedMaxDeadline.
expectedMaxDeadline: saturdayMidnightSydney.In(time.UTC),
},
{
name: "TemplateAutostopRequirementOverridesTemplateDefaultTTL",
now: fridayEveningSydney.In(time.UTC),
templateAllowAutostop: true,
templateDefaultTTL: 3 * time.Hour,
userQuietHoursSchedule: sydneyQuietHours,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: 0b00100000, // Saturday
Weeks: 0, // weekly
},
workspaceTTL: 0,
// expectedDeadline is copied from expectedMaxDeadline.
expectedMaxDeadline: saturdayMidnightSydney.In(time.UTC),
},
{
name: "TimeBeforeEpoch",
// The epoch is 2023-01-02 in each timezone. We set the time to
// 1 second before 11pm the previous day, as this is the latest time
// we allow due to our 1h leeway logic.
now: time.Date(2023, 1, 1, 22, 59, 59, 0, sydneyLoc),
templateAllowAutostop: true,
templateDefaultTTL: 0,
userQuietHoursSchedule: sydneyQuietHours,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: 0b00100000, // Saturday
Weeks: 2, // every fortnight
},
workspaceTTL: 0,
errContains: "coder server system clock is incorrect",
},
{
name: "DaylightSavings/OK",
now: duringDst,
templateAllowAutostop: true,
templateDefaultTTL: 0,
userQuietHoursSchedule: sydneyQuietHours,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: 0b00100000, // Saturday
Weeks: 1, // weekly
},
workspaceTTL: 0,
// expectedDeadline is copied from expectedMaxDeadline.
expectedMaxDeadline: saturdayMidnightAfterDuringDst,
},
{
name: "DaylightSavings/SwitchMidWeek/In",
now: beforeDstIn,
templateAllowAutostop: true,
templateDefaultTTL: 0,
userQuietHoursSchedule: sydneyQuietHours,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: 0b00100000, // Saturday
Weeks: 1, // weekly
},
workspaceTTL: 0,
// expectedDeadline is copied from expectedMaxDeadline.
expectedMaxDeadline: saturdayMidnightAfterDstIn,
},
{
name: "DaylightSavings/SwitchMidWeek/Out",
now: beforeDstOut,
templateAllowAutostop: true,
templateDefaultTTL: 0,
userQuietHoursSchedule: sydneyQuietHours,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: 0b00100000, // Saturday
Weeks: 1, // weekly
},
workspaceTTL: 0,
// expectedDeadline is copied from expectedMaxDeadline.
expectedMaxDeadline: saturdayMidnightAfterDstOut,
},
{
name: "DaylightSavings/QuietHoursFallsOnDstSwitch/In",
now: beforeDstIn.Add(-24 * time.Hour),
templateAllowAutostop: true,
templateDefaultTTL: 0,
userQuietHoursSchedule: dstInQuietHours,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: 0b01000000, // Sunday
Weeks: 1, // weekly
},
workspaceTTL: 0,
// expectedDeadline is copied from expectedMaxDeadline.
expectedMaxDeadline: dstInQuietHoursExpectedTime,
},
{
name: "DaylightSavings/QuietHoursFallsOnDstSwitch/Out",
now: beforeDstOut.Add(-24 * time.Hour),
templateAllowAutostop: true,
templateDefaultTTL: 0,
userQuietHoursSchedule: dstOutQuietHours,
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: 0b01000000, // Sunday
Weeks: 1, // weekly
},
workspaceTTL: 0,
// expectedDeadline is copied from expectedMaxDeadline.
expectedMaxDeadline: dstOutQuietHoursExpectedTime,
},
{
// A user expects this workspace to be online from 9am -> 9pm.
// So if a deadline is going to land in the middle of this range,
// we should bump it to the end.
// This is already done on `ActivityBumpWorkspace`, but that requires
// activity on the workspace.
name: "AutostopCrossAutostartBorder",
// Starting at 9:45pm, with the autostart at 9am.
now: pastDateNight,
templateAllowAutostop: false,
templateDefaultTTL: time.Hour * 12,
workspaceTTL: time.Hour * 12,
// At 9am every morning
wsAutostart: "CRON_TZ=America/Chicago 0 9 * * *",
// No quiet hours
templateAutoStart: schedule.TemplateAutostartRequirement{
// Just allow all days of the week
DaysOfWeek: 0b01111111,
},
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
userQuietHoursSchedule: "",
expectedDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 21, 0, 0, 0, chicago),
expectedMaxDeadline: time.Time{},
errContains: "",
},
{
// Same as AutostopCrossAutostartBorder, but just misses the autostart.
name: "AutostopCrossMissAutostartBorder",
// Starting at 8:45pm, with the autostart at 9am.
now: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day(), 20, 30, 0, 0, chicago),
templateAllowAutostop: false,
templateDefaultTTL: time.Hour * 12,
workspaceTTL: time.Hour * 12,
// At 9am every morning
wsAutostart: "CRON_TZ=America/Chicago 0 9 * * *",
// No quiet hours
templateAutoStart: schedule.TemplateAutostartRequirement{
// Just allow all days of the week
DaysOfWeek: 0b01111111,
},
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
userQuietHoursSchedule: "",
expectedDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 8, 30, 0, 0, chicago),
expectedMaxDeadline: time.Time{},
errContains: "",
},
{
// Same as AutostopCrossAutostartBorderMaxEarlyDeadline with max deadline to limit it.
// The autostop deadline is before the autostart threshold.
name: "AutostopCrossAutostartBorderMaxEarlyDeadline",
// Starting at 9:45pm, with the autostart at 9am.
now: pastDateNight,
templateAllowAutostop: false,
templateDefaultTTL: time.Hour * 12,
workspaceTTL: time.Hour * 12,
// At 9am every morning
wsAutostart: "CRON_TZ=America/Chicago 0 9 * * *",
// No quiet hours
templateAutoStart: schedule.TemplateAutostartRequirement{
// Just allow all days of the week
DaysOfWeek: 0b01111111,
},
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
// Autostop every day
DaysOfWeek: 0b01111111,
Weeks: 0,
},
// 6am quiet hours
userQuietHoursSchedule: "CRON_TZ=America/Chicago 0 6 * * *",
expectedDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 6, 0, 0, 0, chicago),
expectedMaxDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 6, 0, 0, 0, chicago),
errContains: "",
},
{
// Same as AutostopCrossAutostartBorder with max deadline to limit it.
// The autostop deadline is after autostart threshold.
// So the deadline is > 12 hours, but stops at the max deadline.
name: "AutostopCrossAutostartBorderMaxDeadline",
// Starting at 9:45pm, with the autostart at 9am.
now: pastDateNight,
templateAllowAutostop: false,
templateDefaultTTL: time.Hour * 12,
workspaceTTL: time.Hour * 12,
// At 9am every morning
wsAutostart: "CRON_TZ=America/Chicago 0 9 * * *",
// No quiet hours
templateAutoStart: schedule.TemplateAutostartRequirement{
// Just allow all days of the week
DaysOfWeek: 0b01111111,
},
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
// Autostop every day
DaysOfWeek: 0b01111111,
Weeks: 0,
},
// 11am quiet hours, yea this is werid case.
userQuietHoursSchedule: "CRON_TZ=America/Chicago 0 11 * * *",
expectedDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 11, 0, 0, 0, chicago),
expectedMaxDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 11, 0, 0, 0, chicago),
errContains: "",
},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
ctx := testutil.Context(t, testutil.WaitLong)
templateScheduleStore := schedule.MockTemplateScheduleStore{
GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (schedule.TemplateScheduleOptions, error) {
return schedule.TemplateScheduleOptions{
UserAutostartEnabled: false,
UserAutostopEnabled: c.templateAllowAutostop,
DefaultTTL: c.templateDefaultTTL,
AutostopRequirement: c.templateAutostopRequirement,
AutostartRequirement: c.templateAutoStart,
}, nil
},
}
userQuietHoursScheduleStore := schedule.MockUserQuietHoursScheduleStore{
GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (schedule.UserQuietHoursScheduleOptions, error) {
if c.userQuietHoursSchedule == "" {
return schedule.UserQuietHoursScheduleOptions{
Schedule: nil,
}, nil
}
sched, err := cron.Daily(c.userQuietHoursSchedule)
if !assert.NoError(t, err) {
return schedule.UserQuietHoursScheduleOptions{}, err
}
return schedule.UserQuietHoursScheduleOptions{
Schedule: sched,
UserSet: false,
}, nil
},
}
org := dbgen.Organization(t, db, database.Organization{})
user := dbgen.User(t, db, database.User{
QuietHoursSchedule: c.userQuietHoursSchedule,
})
template := dbgen.Template(t, db, database.Template{
Name: "template",
Provisioner: database.ProvisionerTypeEcho,
OrganizationID: org.ID,
CreatedBy: user.ID,
})
err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
ID: template.ID,
UpdatedAt: dbtime.Now(),
AllowUserAutostart: c.templateAllowAutostop,
AutostopRequirementDaysOfWeek: int16(c.templateAutostopRequirement.DaysOfWeek),
AutostopRequirementWeeks: c.templateAutostopRequirement.Weeks,
})
require.NoError(t, err)
template, err = db.GetTemplateByID(ctx, template.ID)
require.NoError(t, err)
workspaceTTL := sql.NullInt64{}
if c.workspaceTTL != 0 {
workspaceTTL = sql.NullInt64{
Int64: int64(c.workspaceTTL),
Valid: true,
}
}
autostart := sql.NullString{}
if c.wsAutostart != "" {
autostart = sql.NullString{
String: c.wsAutostart,
Valid: true,
}
}
workspace := dbgen.Workspace(t, db, database.Workspace{
TemplateID: template.ID,
OrganizationID: org.ID,
OwnerID: user.ID,
Ttl: workspaceTTL,
AutostartSchedule: autostart,
})
autostop, err := schedule.CalculateAutostop(ctx, schedule.CalculateAutostopParams{
Database: db,
TemplateScheduleStore: templateScheduleStore,
UserQuietHoursScheduleStore: userQuietHoursScheduleStore,
Now: c.now,
Workspace: workspace,
WorkspaceAutostart: c.wsAutostart,
})
if c.errContains != "" {
require.Error(t, err)
require.ErrorContains(t, err, c.errContains)
return
}
require.NoError(t, err)
// If the max deadline is set, the deadline should also be set.
// Default to the max deadline if the deadline is not set.
if c.expectedDeadline.IsZero() {
c.expectedDeadline = c.expectedMaxDeadline
}
if c.expectedDeadline.IsZero() {
require.True(t, autostop.Deadline.IsZero())
} else {
require.WithinDuration(t, c.expectedDeadline, autostop.Deadline, 15*time.Second, "deadline does not match expected")
}
if c.expectedMaxDeadline.IsZero() {
require.True(t, autostop.MaxDeadline.IsZero())
} else {
require.WithinDuration(t, c.expectedMaxDeadline, autostop.MaxDeadline, 15*time.Second, "max deadline does not match expected")
require.GreaterOrEqual(t, autostop.MaxDeadline.Unix(), autostop.Deadline.Unix(), "max deadline is smaller than deadline")
}
})
}
}
func TestFindWeek(t *testing.T) {
t.Parallel()
timezones := []string{
"UTC",
"America/Los_Angeles",
"America/New_York",
"Europe/Dublin",
"Europe/London",
"Europe/Paris",
"Asia/Kolkata", // India (UTC+5:30)
"Asia/Tokyo",
"Australia/Sydney",
"Australia/Brisbane",
}
for _, tz := range timezones {
tz := tz
t.Run("Loc/"+tz, func(t *testing.T) {
t.Parallel()
loc, err := time.LoadLocation(tz)
require.NoError(t, err)
now := time.Now().In(loc)
currentWeek, err := schedule.WeeksSinceEpoch(now)
require.NoError(t, err)
diffMonday := now.Weekday() - time.Monday
if now.Weekday() == time.Sunday {
// Sunday is 0, but Monday is the first day of the week in the
// code.
diffMonday = 6
}
currentWeekMondayExpected := now.AddDate(0, 0, -int(diffMonday))
require.Equal(t, time.Monday, currentWeekMondayExpected.Weekday())
y, m, d := currentWeekMondayExpected.Date()
// Change to midnight.
currentWeekMondayExpected = time.Date(y, m, d, 0, 0, 0, 0, loc)
currentWeekMonday, err := schedule.GetMondayOfWeek(now.Location(), currentWeek)
require.NoError(t, err)
require.Equal(t, time.Monday, currentWeekMonday.Weekday())
require.Equal(t, currentWeekMondayExpected, currentWeekMonday)
t.Log("now", now)
t.Log("currentWeek", currentWeek)
t.Log("currentMonday", currentWeekMonday)
// Loop through every single Monday and Sunday for the next 100
// years and make sure the week calculations are correct.
for i := int64(1); i < 52*100; i++ {
msg := fmt.Sprintf("week %d", i)
monday := currentWeekMonday.AddDate(0, 0, int(i*7))
y, m, d := monday.Date()
monday = time.Date(y, m, d, 0, 0, 0, 0, loc)
require.Equal(t, monday.Weekday(), time.Monday, msg)
t.Log(msg, "monday", monday)
week, err := schedule.WeeksSinceEpoch(monday)
require.NoError(t, err, msg)
require.Equal(t, currentWeek+i, week, msg)
gotMonday, err := schedule.GetMondayOfWeek(monday.Location(), week)
require.NoError(t, err, msg)
require.Equal(t, monday, gotMonday, msg)
// Check that we get the same week number for late Sunday.
sunday := time.Date(y, m, d+6, 23, 59, 59, 0, loc)
require.Equal(t, sunday.Weekday(), time.Sunday, msg)
t.Log(msg, "sunday", sunday)
week, err = schedule.WeeksSinceEpoch(sunday)
require.NoError(t, err, msg)
require.Equal(t, currentWeek+i, week, msg)
}
})
}
}