diff --git a/schtasks/schtasks.go b/schtasks/schtasks.go index 311100b5..aa9dcf26 100644 --- a/schtasks/schtasks.go +++ b/schtasks/schtasks.go @@ -109,20 +109,20 @@ func listRegisteredTasks() ([]byte, error) { return stdout.Bytes(), err } -func deleteTask(taskName string) (string, error) { +func deleteTask(taskName string) error { taskName, err := sanitizeTaskName(taskName) if err != nil { - return "", err + return err } stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{} cmd := exec.CommandContext(context.TODO(), binaryPath, "/delete", "/f", "/tn", taskName) - cmd.Stdout = stdout + cmd.Stdout = stdout // we're not actually interested in the output, but we need to capture it to avoid it being printed to the console cmd.Stderr = stderr err = cmd.Run() if err != nil { - return "", schTasksError(stderr.String()) + return schTasksError(stderr.String()) } - return stdout.String(), nil + return nil } // readTaskInfo returns the raw output from querying the task name (via schtasks.exe) diff --git a/schtasks/task.go b/schtasks/task.go index e11a6fcb..691ad6ce 100644 --- a/schtasks/task.go +++ b/schtasks/task.go @@ -30,9 +30,10 @@ type Task struct { Principals Principals `xml:"Principals"` Settings Settings `xml:"Settings"` Actions Actions `xml:"Actions"` + fromNow time.Time `xml:"-"` } -func NewTask() Task { +func NewTask(options ...TaskOption) Task { var userID string if currentUser, err := user.Current(); err == nil { userID = currentUser.Uid @@ -42,7 +43,7 @@ func NewTask() Task { Version: taskSchemaVersion, Xmlns: taskSchema, RegistrationInfo: RegistrationInfo{ - Date: time.Now().Format(dateFormat), + Date: time.Now().Format(dateFormat), // this will be overridden by setFromNow if needed Author: constants.ApplicationName, }, Principals: Principals{ @@ -69,6 +70,11 @@ func NewTask() Task { Actions: Actions{ Context: author, }, + fromNow: time.Now(), // default will be overridden by setFromNow if needed + } + + for _, option := range options { + option.apply(&task) } return task } @@ -105,9 +111,14 @@ func (t *Task) AddSchedules(schedules []*calendar.Event) { } } +func (t *Task) setFromNow(fromNow time.Time) { + t.fromNow = fromNow + t.RegistrationInfo.Date = fromNow.Format(dateFormat) +} + func (t *Task) addTimeTrigger(triggerOnce time.Time) { timeTrigger := TimeTrigger{ - StartBoundary: triggerOnce.Format(dateFormat), + StartBoundary: &triggerOnce, } if t.Triggers.TimeTrigger == nil { t.Triggers.TimeTrigger = []TimeTrigger{timeTrigger} @@ -125,7 +136,7 @@ func (t *Task) addCalendarTrigger(trigger CalendarTrigger) { } func (t *Task) addDailyTrigger(schedule *calendar.Event) { - start := schedule.Next(time.Now()) + start := schedule.Next(t.fromNow) // get all recurrences in the same day recurrences := schedule.GetAllInBetween(start, start.Add(24*time.Hour)) if len(recurrences) == 0 { @@ -135,7 +146,7 @@ func (t *Task) addDailyTrigger(schedule *calendar.Event) { // Is it only once a day? if len(recurrences) == 1 { t.addCalendarTrigger(CalendarTrigger{ - StartBoundary: recurrences[0].Format(dateFormat), + StartBoundary: &recurrences[0], ScheduleByDay: &ScheduleByDay{ DaysInterval: 1, }, @@ -149,7 +160,7 @@ func (t *Task) addDailyTrigger(schedule *calendar.Event) { // case with regular repetition interval := period.NewOf(compactDifferences[0]) t.addCalendarTrigger(CalendarTrigger{ - StartBoundary: start.Format(dateFormat), + StartBoundary: &start, ScheduleByDay: &ScheduleByDay{ DaysInterval: 1, }, @@ -166,9 +177,9 @@ func (t *Task) addDailyTrigger(schedule *calendar.Event) { return } // install them all - for _, recurrence := range recurrences { + for i := range recurrences { t.addCalendarTrigger(CalendarTrigger{ - StartBoundary: recurrence.Format(dateFormat), + StartBoundary: &recurrences[i], ScheduleByDay: &ScheduleByDay{ DaysInterval: 1, }, @@ -177,7 +188,7 @@ func (t *Task) addDailyTrigger(schedule *calendar.Event) { } func (t *Task) addWeeklyTrigger(schedule *calendar.Event) { - start := schedule.Next(time.Now()) + start := schedule.Next(t.fromNow) // get all recurrences in the same day recurrences := schedule.GetAllInBetween(start, start.Add(24*time.Hour)) if len(recurrences) == 0 { @@ -187,7 +198,7 @@ func (t *Task) addWeeklyTrigger(schedule *calendar.Event) { // Is it only once per 24h? if len(recurrences) == 1 { t.addCalendarTrigger(CalendarTrigger{ - StartBoundary: recurrences[0].Format(dateFormat), + StartBoundary: &recurrences[0], ScheduleByWeek: &ScheduleByWeek{ WeeksInterval: 1, DaysOfWeek: convertWeekdays(schedule.WeekDay.GetRangeValues()), @@ -202,7 +213,7 @@ func (t *Task) addWeeklyTrigger(schedule *calendar.Event) { // case with regular repetition interval := period.NewOf(compactDifferences[0]) t.addCalendarTrigger(CalendarTrigger{ - StartBoundary: start.Format(dateFormat), + StartBoundary: &start, ScheduleByWeek: &ScheduleByWeek{ WeeksInterval: 1, DaysOfWeek: convertWeekdays(schedule.WeekDay.GetRangeValues()), @@ -220,9 +231,9 @@ func (t *Task) addWeeklyTrigger(schedule *calendar.Event) { return } // install them all - for _, recurrence := range recurrences { + for i := range recurrences { t.addCalendarTrigger(CalendarTrigger{ - StartBoundary: recurrence.Format(dateFormat), + StartBoundary: &recurrences[i], ScheduleByWeek: &ScheduleByWeek{ WeeksInterval: 1, DaysOfWeek: convertWeekdays(schedule.WeekDay.GetRangeValues()), @@ -232,7 +243,7 @@ func (t *Task) addWeeklyTrigger(schedule *calendar.Event) { } func (t *Task) addMonthlyTrigger(schedule *calendar.Event) { - start := schedule.Next(time.Now()) + start := schedule.Next(t.fromNow) // get all recurrences in the same day recurrences := schedule.GetAllInBetween(start, start.Add(24*time.Hour)) if len(recurrences) == 0 { @@ -245,14 +256,14 @@ func (t *Task) addMonthlyTrigger(schedule *calendar.Event) { return } // install them all - for _, recurrence := range recurrences { + for i := range recurrences { if schedule.WeekDay.HasValue() && schedule.Day.HasValue() { clog.Warningf("task scheduler does not support a day of the month and a day of the week in the same trigger: %s", schedule.String()) return } if schedule.WeekDay.HasValue() { t.addCalendarTrigger(CalendarTrigger{ - StartBoundary: recurrence.Format(dateFormat), + StartBoundary: &recurrences[i], ScheduleByMonthDayOfWeek: &ScheduleByMonthDayOfWeek{ DaysOfWeek: convertWeekdays(schedule.WeekDay.GetRangeValues()), Weeks: AllWeeks, @@ -262,7 +273,7 @@ func (t *Task) addMonthlyTrigger(schedule *calendar.Event) { continue } t.addCalendarTrigger(CalendarTrigger{ - StartBoundary: recurrence.Format(dateFormat), + StartBoundary: &recurrences[i], ScheduleByMonth: &ScheduleByMonth{ DaysOfMonth: convertDaysOfMonth(schedule.Day.GetRangeValues()), Months: convertMonths(schedule.Month.GetRangeValues()), diff --git a/schtasks/task_options.go b/schtasks/task_options.go new file mode 100644 index 00000000..b9c1905f --- /dev/null +++ b/schtasks/task_options.go @@ -0,0 +1,21 @@ +//go:build windows + +package schtasks + +import "time" + +type TaskOption interface { + apply(t *Task) +} + +type WithFromNowOption struct { + now time.Time +} + +func WithFromNow(now time.Time) WithFromNowOption { + return WithFromNowOption{now: now} +} + +func (w WithFromNowOption) apply(t *Task) { + t.setFromNow(w.now) +} diff --git a/schtasks/taskscheduler.go b/schtasks/taskscheduler.go index 9de6edd1..06e1871c 100644 --- a/schtasks/taskscheduler.go +++ b/schtasks/taskscheduler.go @@ -28,6 +28,7 @@ import ( "slices" "strings" "text/tabwriter" + "time" "github.com/creativeprojects/clog" "github.com/creativeprojects/resticprofile/calendar" @@ -48,13 +49,12 @@ func Create(config *Config, schedules []*calendar.Event, permission Permission) taskPath := getTaskPath(config.ProfileName, config.CommandName) if slices.Contains(list, taskPath) { clog.Debugf("task %q already exists: deleting before creating", taskPath) - _, err = deleteTask(taskPath) + err = deleteTask(taskPath) if err != nil { return fmt.Errorf("cannot delete existing task to replace it: %w", err) } } - - task := createTaskDefinition(config, schedules) + task := createTaskDefinition(config, schedules, time.Time{}) task.RegistrationInfo.URI = taskPath switch config.RunLevel { @@ -112,8 +112,7 @@ func Create(config *Config, schedules []*calendar.Event, permission Permission) // Delete a task func Delete(title, subtitle string) error { taskName := getTaskPath(title, subtitle) - _, err := deleteTask(taskName) - return err + return deleteTask(taskName) } // Status returns the status of a task @@ -181,8 +180,12 @@ func getTaskPath(profileName, commandName string) string { return fmt.Sprintf("%s%s %s", tasksPathPrefix, profileName, commandName) } -func createTaskDefinition(config *Config, schedules []*calendar.Event) Task { - task := NewTask() +func createTaskDefinition(config *Config, schedules []*calendar.Event, from time.Time) Task { + options := make([]TaskOption, 0, 1) + if !from.IsZero() { + options = append(options, WithFromNow(from)) + } + task := NewTask(options...) task.RegistrationInfo.Description = config.JobDescription task.Settings.StartWhenAvailable = config.StartWhenAvailable task.AddExecAction(ExecAction{ diff --git a/schtasks/taskscheduler_test.go b/schtasks/taskscheduler_test.go index fd656c77..565ec6dc 100644 --- a/schtasks/taskscheduler_test.go +++ b/schtasks/taskscheduler_test.go @@ -125,98 +125,125 @@ func TestTaskSchedulerIntegration(t *testing.T) { fixtures := []struct { description string schedules []string + fromNow time.Time }{ { "only once", []string{"2020-01-02 03:04"}, + time.Time{}, }, // daily { "once every day", []string{"*-*-* 03:04"}, + time.Time{}, }, { "every hour", []string{"*-*-* *:04"}, + time.Time{}, }, { "every minute", []string{"*-*-* *:*"}, + time.Time{}, }, { - "every minute at 12", + "every minute at 12 (before 12)", []string{"*-*-* 12:*"}, + time.Date(2025, 7, 27, 11, 20, 0, 0, time.UTC), + }, + { + "every minute at 12 (after 12)", + []string{"*-*-* 12:*"}, + time.Date(2025, 7, 27, 13, 20, 0, 0, time.UTC), }, // daily - more than one { "three times a day", []string{"*-*-* 03..05:04"}, + time.Time{}, }, { "twice every hour", []string{"*-*-* *:04..05"}, + time.Time{}, }, // weekly { "once weekly", []string{"mon *-*-* 03:04"}, + time.Time{}, }, { "every hour on mondays", []string{strings.ToLower(fixedDay)[:3] + " *-*-* *:04"}, + time.Time{}, }, { "every minute on mondays", []string{strings.ToLower(fixedDay)[:3] + " *-*-* *:*"}, + time.Time{}, }, { "every minute at 12 on mondays", []string{"mon *-*-* 12:*"}, + time.Time{}, }, // more than once weekly { "twice weekly", []string{"mon *-*-* 03..04:04"}, + time.Time{}, }, { "twice mondays and tuesdays", []string{"mon,tue *-*-* 03:04..06"}, + time.Time{}, }, { "twice on fridays", []string{"fri *-*-* *:04..05"}, + time.Time{}, }, // monthly { "once monthly", []string{"*-01-* 03:04"}, + time.Time{}, }, { "every hour in january", []string{"*-01-* *:04"}, + time.Time{}, }, // monthly with weekdays { "mondays in January", []string{"mon *-01-* 03:04"}, + time.Time{}, }, { "every hour on Mondays in january", []string{"mon *-01-* *:04"}, + time.Time{}, }, // some days every month { "one day per month", []string{"*-*-0" + dayOfTheMonth + " 03:04"}, + time.Time{}, }, { "every hour on the 1st of each month", []string{"*-*-0" + dayOfTheMonth + " *:04"}, + time.Time{}, }, // more than once per month { "twice in one day per month", []string{"*-*-0" + dayOfTheMonth + " 03..04:04"}, + time.Time{}, }, } @@ -247,15 +274,16 @@ func TestTaskSchedulerIntegration(t *testing.T) { defer file.Close() taskPath := getTaskPath(config.ProfileName, config.CommandName) - sourceTask := createTaskDefinition(config, schedules) + sourceTask := createTaskDefinition(config, schedules, fixture.fromNow) sourceTask.RegistrationInfo.URI = taskPath err = createTaskFile(sourceTask, file) require.NoError(t, err) file.Close() - result, err := createTask(taskPath, file.Name(), "", "") - t.Log(result) + t.Logf("task contains %d time triggers and %d calendar triggers", len(sourceTask.Triggers.TimeTrigger), len(sourceTask.Triggers.CalendarTrigger)) + + _, err = createTask(taskPath, file.Name(), "", "") require.NoError(t, err) taskXML, err := exportTaskDefinition(taskPath) @@ -267,14 +295,16 @@ func TestTaskSchedulerIntegration(t *testing.T) { // no need for character conversion return input, nil } - readTask := &Task{} + readTask := Task{} err = decoder.Decode(&readTask) require.NoError(t, err) - assert.Equal(t, sourceTask, *readTask) + sourceTask.fromNow = time.Time{} // ignore fromNow in the source task + taskInUTC(&sourceTask) + taskInUTC(&readTask) + assert.Equal(t, sourceTask, readTask) - result, err = deleteTask(taskPath) - t.Log(result) + err = deleteTask(taskPath) require.NoError(t, err) }) } @@ -309,7 +339,7 @@ func TestStartWhenAvailableOption(t *testing.T) { defer file.Close() taskPath := getTaskPath(config.ProfileName, config.CommandName) - sourceTask := createTaskDefinition(config, schedules) + sourceTask := createTaskDefinition(config, schedules, time.Now()) sourceTask.RegistrationInfo.URI = taskPath // Verify StartWhenAvailable is set in source task @@ -323,7 +353,7 @@ func TestStartWhenAvailableOption(t *testing.T) { t.Log(result) require.NoError(t, err) defer func() { - _, _ = deleteTask(taskPath) + _ = deleteTask(taskPath) }() // Export and verify the task was created with StartWhenAvailable @@ -341,3 +371,21 @@ func TestStartWhenAvailableOption(t *testing.T) { assert.True(t, readTask.Settings.StartWhenAvailable, "StartWhenAvailable should be true in the created task") } + +func taskInUTC(task *Task) { + // Windows Task Scheduler is using the current timezone when loading dates into the XML definition. + // This is a workaround to ensure that the tests run consistently. + for i := range task.Triggers.TimeTrigger { + if task.Triggers.TimeTrigger[i].StartBoundary != nil { + *task.Triggers.TimeTrigger[i].StartBoundary = task.Triggers.TimeTrigger[i].StartBoundary.UTC() + } + } + for i := range task.Triggers.CalendarTrigger { + if task.Triggers.CalendarTrigger[i].StartBoundary != nil { + *task.Triggers.CalendarTrigger[i].StartBoundary = task.Triggers.CalendarTrigger[i].StartBoundary.UTC() + } + if task.Triggers.CalendarTrigger[i].EndBoundary != nil { + *task.Triggers.CalendarTrigger[i].EndBoundary = task.Triggers.CalendarTrigger[i].EndBoundary.UTC() + } + } +} diff --git a/schtasks/trigger.go b/schtasks/trigger.go index e855f314..d28a4d04 100644 --- a/schtasks/trigger.go +++ b/schtasks/trigger.go @@ -3,6 +3,8 @@ package schtasks import ( + "time" + "github.com/rickb777/period" ) @@ -13,14 +15,14 @@ type Triggers struct { type TimeTrigger struct { Enabled *bool `xml:"Enabled"` // indicates whether the trigger is enabled - StartBoundary string `xml:"StartBoundary"` + StartBoundary *time.Time `xml:"StartBoundary"` ExecutionTimeLimit *period.Period `xml:"ExecutionTimeLimit"` RandomDelay *period.Period `xml:"RandomDelay,omitempty"` // a delay time that is randomly added to the start time of the trigger } type CalendarTrigger struct { - StartBoundary string `xml:"StartBoundary,omitempty"` // the date and time when the trigger is activated - EndBoundary string `xml:"EndBoundary,omitempty"` // the date and time when the trigger is deactivated + StartBoundary *time.Time `xml:"StartBoundary,omitempty"` // the date and time when the trigger is activated + EndBoundary *time.Time `xml:"EndBoundary,omitempty"` // the date and time when the trigger is deactivated Repetition *RepetitionPattern `xml:"Repetition"` ExecutionTimeLimit *period.Period `xml:"ExecutionTimeLimit"` // the maximum amount of time that the task launched by this trigger is allowed to run Enabled *bool `xml:"Enabled"` // indicates whether the trigger is enabled diff --git a/schtasks/trigger_test.go b/schtasks/trigger_test.go index 76acb60f..41c9a7c9 100644 --- a/schtasks/trigger_test.go +++ b/schtasks/trigger_test.go @@ -32,145 +32,183 @@ func TestTriggerCreationFromXML(t *testing.T) { fixedDay = "Tuesday" } + timezone := `(Z|[+-]\d{2}:\d{2})` + fixtures := []struct { description string schedules []string expected string expectedMatchCount int + from time.Time }{ { "only once", []string{"2020-01-02 03:04"}, - `\s*2020-01-02T03:04:00\+00:00\s*`, + `\s*2020-01-02T03:04:00` + timezone + `\s*`, 1, + time.Time{}, }, // daily { "once every day", []string{"*-*-* 03:04"}, - `\s*\d{4}-\d{2}-\d{2}T03:04:00\+00:00\s*\s*1\s*\s*`, + `\s*\d{4}-\d{2}-\d{2}T03:04:00` + timezone + `\s*\s*1\s*\s*`, 1, + time.Date(2020, 1, 2, 3, 4, 0, 0, time.UTC), }, { "every hour", []string{"*-*-* *:04"}, - `\s*\d{4}-\d{2}-\d{2}T\d{2}:04:00\+00:00\s*\s*PT1H\s*PT23H\s*\s*\s*1\s*\s*`, + `\s*\d{4}-\d{2}-\d{2}T\d{2}:04:00` + timezone + `\s*\s*PT1H\s*PT23H\s*\s*\s*1\s*\s*`, 1, + time.Time{}, }, { "every minute", []string{"*-*-* *:*"}, - `\s*\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00\+00:00\s*\s*PT1M\s*P1D\s*\s*\s*1\s*\s*`, + `\s*\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00` + timezone + `\s*\s*PT1M\s*P1D\s*\s*\s*1\s*\s*`, 1, + time.Time{}, }, { - "every minute at 12", + "every minute at 12 (before 12)", []string{"*-*-* 12:*"}, - `\s*\d{4}-\d{2}-\d{2}T12:\d{2}:00\+00:00\s*\s*PT1M\s*PT59M\s*\s*\s*1\s*\s*`, + `\s*\d{4}-\d{2}-\d{2}T12:\d{2}:00` + timezone + `\s*\s*PT1M\s*PT59M\s*\s*\s*1\s*\s*`, 1, + time.Date(2025, 2, 27, 11, 20, 0, 0, time.UTC), }, + // { + // "every minute at 12", + // []string{"*-*-* 12:*"}, + // `\s*\d{4}-\d{2}-\d{2}T12:\d{2}:00` + timezone + `\s*\s*PT1M\s*PT59M\s*\s*\s*1\s*\s*`, + // 1, + // time.Date(2025, 2, 27, 12, 20, 0, 0, time.UTC), + // }, + // { + // "every minute at 12 (after 12)", + // []string{"*-*-* 12:*"}, + // `\s*\d{4}-\d{2}-\d{2}T13:\d{2}:00` + timezone + `\s*\s*PT1M\s*PT59M\s*\s*\s*1\s*\s*`, + // 1, + // time.Date(2025, 7, 27, 13, 20, 0, 0, time.UTC), + // }, // daily - more than one { "three times a day", []string{"*-*-* 03..05:04"}, - `\s*\d{4}-\d{2}-\d{2}T03:04:00\+00:00\s*\s*PT1H\s*PT2H\s*\s*\s*1\s*\s*`, + `\s*\d{4}-\d{2}-\d{2}T03:04:00` + timezone + `\s*\s*PT1H\s*PT2H\s*\s*\s*1\s*\s*`, 1, + time.Date(2026, 1, 2, 2, 0, 0, 0, time.UTC), }, { "twice every hour", []string{"*-*-* *:04..05"}, - `\s*\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00\+00:00\s*\s*1\s*\s*`, + `\s*\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00` + timezone + `\s*\s*1\s*\s*`, 48, + time.Time{}, }, // weekly { "once weekly", []string{"mon *-*-* 03:04"}, - `\s*\d{4}-\d{2}-\d{2}T03:04:00\+00:00\s*\s*1\s*\s*\s*\s*\s*`, + `\s*\d{4}-\d{2}-\d{2}T03:04:00` + timezone + `\s*\s*1\s*\s*\s*\s*\s*`, 1, + time.Time{}, }, { "every hour on mondays", []string{strings.ToLower(fixedDay)[:3] + " *-*-* *:04"}, - `\s*\d{4}-\d{2}-\d{2}T\d{2}:04:00\+00:00\s*\s*PT1H\s*PT23H\s*\s*\s*1\s*\s*<` + fixedDay + `>\s*\s*\s*`, + `\s*\d{4}-\d{2}-\d{2}T\d{2}:04:00` + timezone + `\s*\s*PT1H\s*PT23H\s*\s*\s*1\s*\s*<` + fixedDay + `>\s*\s*\s*`, 1, + time.Time{}, }, { "every minute on mondays", []string{strings.ToLower(fixedDay)[:3] + " *-*-* *:*"}, - `\s*\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00\+00:00\s*\s*PT1M\s*P1D\s*\s*\s*1\s*\s*<` + fixedDay + `>\s*\s*\s*`, + `\s*\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00` + timezone + `\s*\s*PT1M\s*P1D\s*\s*\s*1\s*\s*<` + fixedDay + `>\s*\s*\s*`, 1, + time.Time{}, }, { "every minute at 12 on mondays", []string{"mon *-*-* 12:*"}, - `\s*\d{4}-\d{2}-\d{2}T12:\d{2}:00\+00:00\s*\s*PT1M\s*PT59M\s*\s*\s*1\s*\s*\s*\s*\s*`, + `\s*\d{4}-\d{2}-\d{2}T12:\d{2}:00` + timezone + `\s*\s*PT1M\s*PT59M\s*\s*\s*1\s*\s*\s*\s*\s*`, 1, + time.Time{}, }, // more than once weekly { "twice weekly", []string{"mon *-*-* 03..04:04"}, - `\s*\d{4}-\d{2}-\d{2}T03:04:00\+00:00\s*\s*PT1H\s*PT1H\s*\s*\s*1\s*\s*\s*\s*\s*`, + `\s*\d{4}-\d{2}-\d{2}T03:04:00` + timezone + `\s*\s*PT1H\s*PT1H\s*\s*\s*1\s*\s*\s*\s*\s*`, 1, + time.Time{}, }, { "twice mondays and tuesdays", []string{"mon,tue *-*-* 03:04..06"}, - `\s*\d{4}-\d{2}-\d{2}T03:04:00\+00:00\s*\s*PT1M\s*PT2M\s*\s*\s*1\s*\s*\s*\s*\s*\s*`, + `\s*\d{4}-\d{2}-\d{2}T03:04:00` + timezone + `\s*\s*PT1M\s*PT2M\s*\s*\s*1\s*\s*\s*\s*\s*\s*`, 1, + time.Time{}, }, { "twice on fixed day", []string{strings.ToLower(fixedDay)[:3] + " *-*-* *:04..05"}, - `\s*\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00\+00:00\s*\s*1\s*\s*<` + fixedDay + `>\s*\s*\s*`, + `\s*\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00` + timezone + `\s*\s*1\s*\s*<` + fixedDay + `>\s*\s*\s*`, 48, + time.Time{}, }, // monthly { "once monthly", []string{"*-01-* 03:04"}, - `\s*\d{4}-01-\d{2}T03:04:00\+00:00\s*\s*\s*\s*\s*\s*` + generateEveryDayString() + `\s*\s*`, + `\s*\d{4}-01-\d{2}T03:04:00` + timezone + `\s*\s*\s*\s*\s*\s*` + generateEveryDayString() + `\s*\s*`, 1, + time.Time{}, }, { "every hour in january", []string{"*-01-* *:04"}, - `\s*\d{4}-01-\d{2}T\d{2}:04:00\+00:00\s*\s*\s*\s*\s*\s*` + generateEveryDayString() + `\s*\s*`, + `\s*\d{4}-01-\d{2}T\d{2}:04:00` + timezone + `\s*\s*\s*\s*\s*\s*` + generateEveryDayString() + `\s*\s*`, 24, + time.Time{}, }, // monthly with weekdays { "mondays in January", []string{"mon *-01-* 03:04"}, - `\s*\d{4}-01-\d{2}T03:04:00\+00:00\s*\s*\s*\s*\s*\s*1\s*2\s*3\s*4\s*Last\s*\s*\s*\s*\s*\s*`, + `\s*\d{4}-01-\d{2}T03:04:00` + timezone + `\s*\s*\s*\s*\s*\s*1\s*2\s*3\s*4\s*Last\s*\s*\s*\s*\s*\s*`, 1, + time.Time{}, }, { "every hour on Mondays in january", []string{"mon *-01-* *:04"}, - `\s*\d{4}-01-\d{2}T\d{2}:04:00\+00:00\s*\s*\s*\s*\s*\s*1\s*2\s*3\s*4\s*Last\s*\s*\s*\s*\s*\s*`, + `\s*\d{4}-01-\d{2}T\d{2}:04:00` + timezone + `\s*\s*\s*\s*\s*\s*1\s*2\s*3\s*4\s*Last\s*\s*\s*\s*\s*\s*`, 24, + time.Time{}, }, // // some days every month { "one day per month", []string{"*-*-0" + dayOfTheMonth + " 03:04"}, - `\s*\d{4}-\d{2}-0` + dayOfTheMonth + `T03:04:00\+00:00\s*\s*\s*` + generateEveryMonthString() + `\s*\s*` + dayOfTheMonth + `\s*\s*\s*`, + `\s*\d{4}-\d{2}-0` + dayOfTheMonth + `T03:04:00` + timezone + `\s*\s*\s*` + generateEveryMonthString() + `\s*\s*` + dayOfTheMonth + `\s*\s*\s*`, 1, + time.Time{}, }, { "every hour on the 1st of each month", []string{"*-*-0" + dayOfTheMonth + " *:04"}, - `\s*\d{4}-\d{2}-0` + dayOfTheMonth + `T\d{2}:04:00\+00:00\s*\s*\s*` + generateEveryMonthString() + `\s*\s*` + dayOfTheMonth + `\s*\s*\s*`, + `\s*\d{4}-\d{2}-0` + dayOfTheMonth + `T\d{2}:04:00` + timezone + `\s*\s*\s*` + generateEveryMonthString() + `\s*\s*` + dayOfTheMonth + `\s*\s*\s*`, 24, // 1 per hour + time.Time{}, }, // // more than once per month { "twice in one day per month", []string{"*-*-0" + dayOfTheMonth + " 03..04:04"}, - `\s*\d{4}-\d{2}-0` + dayOfTheMonth + `T\d{2}:04:00\+00:00\s*\s*\s*` + generateEveryMonthString() + `\s*\s*` + dayOfTheMonth + `\s*\s*\s*`, + `\s*\d{4}-\d{2}-0` + dayOfTheMonth + `T\d{2}:04:00` + timezone + `\s*\s*\s*` + generateEveryMonthString() + `\s*\s*` + dayOfTheMonth + `\s*\s*\s*`, 2, + time.Time{}, }, } @@ -196,7 +234,15 @@ func TestTriggerCreationFromXML(t *testing.T) { schedules[index] = event } buffer := &bytes.Buffer{} - task := createTaskDefinition(scheduleConfig, schedules) + task := createTaskDefinition(scheduleConfig, schedules, fixture.from) + + if len(task.Triggers.CalendarTrigger) > 0 { + t.Logf("calendar triggers %+v", task.Triggers.CalendarTrigger) + } + if len(task.Triggers.TimeTrigger) > 0 { + t.Logf("time triggers %+v", task.Triggers.TimeTrigger) + } + err = createTaskFile(task, buffer) require.NoError(t, err)