mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
Closes https://github.com/coder/coder/issues/12612 The problem in the linked issue was caused due to a mismatch of when the Web UI tooltip shows up (2 hours before an autostop requirement) and the leeway in the `autostop_requirement` algorithm (workspace builds must be 1 hour before an autostop requirement to skip them). Now, restarting your workspace whilst the tooltip is showing will skip the upcoming autostop requirement. This also could have been fixed by only showing the tooltip one hour before the autostop requirement, but it looks like 1 hour was chosen arbitrarily, and it doesn't hurt to give users more time to skip the autostop.
688 lines
25 KiB
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 2h leeway logic.
|
|
now: time.Date(2023, 1, 1, 21, 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.WorkspaceTable{
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|