Skip to content

Commit e2ffdfb

Browse files
Fix windows scheduler flaky tests (#533)
* refactor: enhance task scheduling with flexible time options * fix: resolve flaky tests by ensuring consistent task creation timestamps * test: improve logging for calendar and time triggers in task creation tests * fix: streamline task deletion and improve error handling
1 parent cc4cec9 commit e2ffdfb

7 files changed

Lines changed: 196 additions & 65 deletions

File tree

schtasks/schtasks.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,20 +109,20 @@ func listRegisteredTasks() ([]byte, error) {
109109
return stdout.Bytes(), err
110110
}
111111

112-
func deleteTask(taskName string) (string, error) {
112+
func deleteTask(taskName string) error {
113113
taskName, err := sanitizeTaskName(taskName)
114114
if err != nil {
115-
return "", err
115+
return err
116116
}
117117
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
118118
cmd := exec.CommandContext(context.TODO(), binaryPath, "/delete", "/f", "/tn", taskName)
119-
cmd.Stdout = stdout
119+
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
120120
cmd.Stderr = stderr
121121
err = cmd.Run()
122122
if err != nil {
123-
return "", schTasksError(stderr.String())
123+
return schTasksError(stderr.String())
124124
}
125-
return stdout.String(), nil
125+
return nil
126126
}
127127

128128
// readTaskInfo returns the raw output from querying the task name (via schtasks.exe)

schtasks/task.go

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ type Task struct {
3030
Principals Principals `xml:"Principals"`
3131
Settings Settings `xml:"Settings"`
3232
Actions Actions `xml:"Actions"`
33+
fromNow time.Time `xml:"-"`
3334
}
3435

35-
func NewTask() Task {
36+
func NewTask(options ...TaskOption) Task {
3637
var userID string
3738
if currentUser, err := user.Current(); err == nil {
3839
userID = currentUser.Uid
@@ -42,7 +43,7 @@ func NewTask() Task {
4243
Version: taskSchemaVersion,
4344
Xmlns: taskSchema,
4445
RegistrationInfo: RegistrationInfo{
45-
Date: time.Now().Format(dateFormat),
46+
Date: time.Now().Format(dateFormat), // this will be overridden by setFromNow if needed
4647
Author: constants.ApplicationName,
4748
},
4849
Principals: Principals{
@@ -69,6 +70,11 @@ func NewTask() Task {
6970
Actions: Actions{
7071
Context: author,
7172
},
73+
fromNow: time.Now(), // default will be overridden by setFromNow if needed
74+
}
75+
76+
for _, option := range options {
77+
option.apply(&task)
7278
}
7379
return task
7480
}
@@ -105,9 +111,14 @@ func (t *Task) AddSchedules(schedules []*calendar.Event) {
105111
}
106112
}
107113

114+
func (t *Task) setFromNow(fromNow time.Time) {
115+
t.fromNow = fromNow
116+
t.RegistrationInfo.Date = fromNow.Format(dateFormat)
117+
}
118+
108119
func (t *Task) addTimeTrigger(triggerOnce time.Time) {
109120
timeTrigger := TimeTrigger{
110-
StartBoundary: triggerOnce.Format(dateFormat),
121+
StartBoundary: &triggerOnce,
111122
}
112123
if t.Triggers.TimeTrigger == nil {
113124
t.Triggers.TimeTrigger = []TimeTrigger{timeTrigger}
@@ -125,7 +136,7 @@ func (t *Task) addCalendarTrigger(trigger CalendarTrigger) {
125136
}
126137

127138
func (t *Task) addDailyTrigger(schedule *calendar.Event) {
128-
start := schedule.Next(time.Now())
139+
start := schedule.Next(t.fromNow)
129140
// get all recurrences in the same day
130141
recurrences := schedule.GetAllInBetween(start, start.Add(24*time.Hour))
131142
if len(recurrences) == 0 {
@@ -135,7 +146,7 @@ func (t *Task) addDailyTrigger(schedule *calendar.Event) {
135146
// Is it only once a day?
136147
if len(recurrences) == 1 {
137148
t.addCalendarTrigger(CalendarTrigger{
138-
StartBoundary: recurrences[0].Format(dateFormat),
149+
StartBoundary: &recurrences[0],
139150
ScheduleByDay: &ScheduleByDay{
140151
DaysInterval: 1,
141152
},
@@ -149,7 +160,7 @@ func (t *Task) addDailyTrigger(schedule *calendar.Event) {
149160
// case with regular repetition
150161
interval := period.NewOf(compactDifferences[0])
151162
t.addCalendarTrigger(CalendarTrigger{
152-
StartBoundary: start.Format(dateFormat),
163+
StartBoundary: &start,
153164
ScheduleByDay: &ScheduleByDay{
154165
DaysInterval: 1,
155166
},
@@ -166,9 +177,9 @@ func (t *Task) addDailyTrigger(schedule *calendar.Event) {
166177
return
167178
}
168179
// install them all
169-
for _, recurrence := range recurrences {
180+
for i := range recurrences {
170181
t.addCalendarTrigger(CalendarTrigger{
171-
StartBoundary: recurrence.Format(dateFormat),
182+
StartBoundary: &recurrences[i],
172183
ScheduleByDay: &ScheduleByDay{
173184
DaysInterval: 1,
174185
},
@@ -177,7 +188,7 @@ func (t *Task) addDailyTrigger(schedule *calendar.Event) {
177188
}
178189

179190
func (t *Task) addWeeklyTrigger(schedule *calendar.Event) {
180-
start := schedule.Next(time.Now())
191+
start := schedule.Next(t.fromNow)
181192
// get all recurrences in the same day
182193
recurrences := schedule.GetAllInBetween(start, start.Add(24*time.Hour))
183194
if len(recurrences) == 0 {
@@ -187,7 +198,7 @@ func (t *Task) addWeeklyTrigger(schedule *calendar.Event) {
187198
// Is it only once per 24h?
188199
if len(recurrences) == 1 {
189200
t.addCalendarTrigger(CalendarTrigger{
190-
StartBoundary: recurrences[0].Format(dateFormat),
201+
StartBoundary: &recurrences[0],
191202
ScheduleByWeek: &ScheduleByWeek{
192203
WeeksInterval: 1,
193204
DaysOfWeek: convertWeekdays(schedule.WeekDay.GetRangeValues()),
@@ -202,7 +213,7 @@ func (t *Task) addWeeklyTrigger(schedule *calendar.Event) {
202213
// case with regular repetition
203214
interval := period.NewOf(compactDifferences[0])
204215
t.addCalendarTrigger(CalendarTrigger{
205-
StartBoundary: start.Format(dateFormat),
216+
StartBoundary: &start,
206217
ScheduleByWeek: &ScheduleByWeek{
207218
WeeksInterval: 1,
208219
DaysOfWeek: convertWeekdays(schedule.WeekDay.GetRangeValues()),
@@ -220,9 +231,9 @@ func (t *Task) addWeeklyTrigger(schedule *calendar.Event) {
220231
return
221232
}
222233
// install them all
223-
for _, recurrence := range recurrences {
234+
for i := range recurrences {
224235
t.addCalendarTrigger(CalendarTrigger{
225-
StartBoundary: recurrence.Format(dateFormat),
236+
StartBoundary: &recurrences[i],
226237
ScheduleByWeek: &ScheduleByWeek{
227238
WeeksInterval: 1,
228239
DaysOfWeek: convertWeekdays(schedule.WeekDay.GetRangeValues()),
@@ -232,7 +243,7 @@ func (t *Task) addWeeklyTrigger(schedule *calendar.Event) {
232243
}
233244

234245
func (t *Task) addMonthlyTrigger(schedule *calendar.Event) {
235-
start := schedule.Next(time.Now())
246+
start := schedule.Next(t.fromNow)
236247
// get all recurrences in the same day
237248
recurrences := schedule.GetAllInBetween(start, start.Add(24*time.Hour))
238249
if len(recurrences) == 0 {
@@ -245,14 +256,14 @@ func (t *Task) addMonthlyTrigger(schedule *calendar.Event) {
245256
return
246257
}
247258
// install them all
248-
for _, recurrence := range recurrences {
259+
for i := range recurrences {
249260
if schedule.WeekDay.HasValue() && schedule.Day.HasValue() {
250261
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())
251262
return
252263
}
253264
if schedule.WeekDay.HasValue() {
254265
t.addCalendarTrigger(CalendarTrigger{
255-
StartBoundary: recurrence.Format(dateFormat),
266+
StartBoundary: &recurrences[i],
256267
ScheduleByMonthDayOfWeek: &ScheduleByMonthDayOfWeek{
257268
DaysOfWeek: convertWeekdays(schedule.WeekDay.GetRangeValues()),
258269
Weeks: AllWeeks,
@@ -262,7 +273,7 @@ func (t *Task) addMonthlyTrigger(schedule *calendar.Event) {
262273
continue
263274
}
264275
t.addCalendarTrigger(CalendarTrigger{
265-
StartBoundary: recurrence.Format(dateFormat),
276+
StartBoundary: &recurrences[i],
266277
ScheduleByMonth: &ScheduleByMonth{
267278
DaysOfMonth: convertDaysOfMonth(schedule.Day.GetRangeValues()),
268279
Months: convertMonths(schedule.Month.GetRangeValues()),

schtasks/task_options.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//go:build windows
2+
3+
package schtasks
4+
5+
import "time"
6+
7+
type TaskOption interface {
8+
apply(t *Task)
9+
}
10+
11+
type WithFromNowOption struct {
12+
now time.Time
13+
}
14+
15+
func WithFromNow(now time.Time) WithFromNowOption {
16+
return WithFromNowOption{now: now}
17+
}
18+
19+
func (w WithFromNowOption) apply(t *Task) {
20+
t.setFromNow(w.now)
21+
}

schtasks/taskscheduler.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"slices"
2929
"strings"
3030
"text/tabwriter"
31+
"time"
3132

3233
"github.com/creativeprojects/clog"
3334
"github.com/creativeprojects/resticprofile/calendar"
@@ -48,13 +49,12 @@ func Create(config *Config, schedules []*calendar.Event, permission Permission)
4849
taskPath := getTaskPath(config.ProfileName, config.CommandName)
4950
if slices.Contains(list, taskPath) {
5051
clog.Debugf("task %q already exists: deleting before creating", taskPath)
51-
_, err = deleteTask(taskPath)
52+
err = deleteTask(taskPath)
5253
if err != nil {
5354
return fmt.Errorf("cannot delete existing task to replace it: %w", err)
5455
}
5556
}
56-
57-
task := createTaskDefinition(config, schedules)
57+
task := createTaskDefinition(config, schedules, time.Time{})
5858
task.RegistrationInfo.URI = taskPath
5959

6060
switch config.RunLevel {
@@ -112,8 +112,7 @@ func Create(config *Config, schedules []*calendar.Event, permission Permission)
112112
// Delete a task
113113
func Delete(title, subtitle string) error {
114114
taskName := getTaskPath(title, subtitle)
115-
_, err := deleteTask(taskName)
116-
return err
115+
return deleteTask(taskName)
117116
}
118117

119118
// Status returns the status of a task
@@ -181,8 +180,12 @@ func getTaskPath(profileName, commandName string) string {
181180
return fmt.Sprintf("%s%s %s", tasksPathPrefix, profileName, commandName)
182181
}
183182

184-
func createTaskDefinition(config *Config, schedules []*calendar.Event) Task {
185-
task := NewTask()
183+
func createTaskDefinition(config *Config, schedules []*calendar.Event, from time.Time) Task {
184+
options := make([]TaskOption, 0, 1)
185+
if !from.IsZero() {
186+
options = append(options, WithFromNow(from))
187+
}
188+
task := NewTask(options...)
186189
task.RegistrationInfo.Description = config.JobDescription
187190
task.Settings.StartWhenAvailable = config.StartWhenAvailable
188191
task.AddExecAction(ExecAction{

0 commit comments

Comments
 (0)