golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/relui/schedule_test.go (about) 1 // Copyright 2022 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package relui 6 7 import ( 8 "context" 9 "database/sql" 10 "testing" 11 "time" 12 13 "github.com/google/go-cmp/cmp" 14 "github.com/google/go-cmp/cmp/cmpopts" 15 "github.com/robfig/cron/v3" 16 "golang.org/x/build/internal/relui/db" 17 ) 18 19 func mustParseSpec(t *testing.T, spec string) cron.Schedule { 20 t.Helper() 21 sched, err := cron.ParseStandard(spec) 22 if err != nil { 23 t.Fatalf("cron.ParseStandard(%q) = %q, wanted no error", spec, err) 24 } 25 return sched 26 } 27 28 func TestSchedulerCreate(t *testing.T) { 29 now := time.Now() 30 cases := []struct { 31 desc string 32 sched Schedule 33 workflowName string 34 params map[string]any 35 want db.Schedule 36 wantEntries []ScheduleEntry 37 wantErr bool 38 }{ 39 { 40 desc: "success: once", 41 sched: Schedule{Once: now.AddDate(1, 0, 0), Type: ScheduleOnce}, 42 workflowName: "echo", 43 params: map[string]any{"greeting": "hello", "farewell": "bye"}, 44 want: db.Schedule{ 45 WorkflowName: "echo", 46 WorkflowParams: sql.NullString{ 47 String: `{"farewell": "bye", "greeting": "hello"}`, 48 Valid: true, 49 }, 50 Once: now.AddDate(1, 0, 0), 51 CreatedAt: now, 52 UpdatedAt: now, 53 }, 54 wantEntries: []ScheduleEntry{ 55 {Entry: cron.Entry{ 56 Schedule: &RunOnce{next: now.UTC().AddDate(1, 0, 0)}, 57 Next: now.UTC().AddDate(1, 0, 0), 58 Job: &WorkflowSchedule{ 59 Schedule: db.Schedule{ 60 WorkflowName: "echo", 61 WorkflowParams: sql.NullString{ 62 String: `{"farewell": "bye", "greeting": "hello"}`, 63 Valid: true, 64 }, 65 Once: now.UTC().AddDate(1, 0, 0), 66 CreatedAt: now, 67 UpdatedAt: now, 68 }, 69 Params: map[string]any{"greeting": "hello", "farewell": "bye"}, 70 }, 71 }}, 72 }, 73 }, 74 { 75 desc: "success: cron", 76 sched: Schedule{Cron: "* * * * *", Type: ScheduleCron}, 77 workflowName: "echo", 78 params: map[string]any{"greeting": "hello", "farewell": "bye"}, 79 want: db.Schedule{ 80 WorkflowName: "echo", 81 WorkflowParams: sql.NullString{ 82 String: `{"farewell": "bye", "greeting": "hello"}`, 83 Valid: true, 84 }, 85 Spec: "* * * * *", 86 CreatedAt: now, 87 UpdatedAt: now, 88 }, 89 wantEntries: []ScheduleEntry{ 90 {Entry: cron.Entry{ 91 Schedule: mustParseSpec(t, "* * * * *"), 92 Next: now.Add(time.Minute), 93 Job: &WorkflowSchedule{ 94 Schedule: db.Schedule{ 95 WorkflowName: "echo", 96 WorkflowParams: sql.NullString{ 97 String: `{"farewell": "bye", "greeting": "hello"}`, 98 Valid: true, 99 }, 100 Spec: "* * * * *", 101 CreatedAt: now, 102 UpdatedAt: now, 103 }, 104 Params: map[string]any{"greeting": "hello", "farewell": "bye"}, 105 }, 106 }}, 107 }, 108 }, 109 { 110 desc: "error: invalid Schedule", 111 sched: Schedule{Type: ScheduleImmediate}, 112 workflowName: "echo", 113 params: map[string]any{"greeting": "hello", "farewell": "bye"}, 114 wantErr: true, 115 wantEntries: []ScheduleEntry{}, 116 }, 117 } 118 for _, c := range cases { 119 t.Run(c.desc, func(t *testing.T) { 120 ctx, cancel := context.WithCancel(context.Background()) 121 defer cancel() 122 p := testDB(ctx, t) 123 s := NewScheduler(p, NewWorker(NewDefinitionHolder(), p, &PGListener{DB: p})) 124 row, err := s.Create(ctx, c.sched, c.workflowName, c.params) 125 if (err != nil) != c.wantErr { 126 t.Fatalf("s.Create(_, %v, %q, %v) = %v, %v, wantErr: %t", c.sched, c.workflowName, c.params, row, err, c.wantErr) 127 } 128 if diff := cmp.Diff(c.want, row, cmpopts.EquateApproxTime(time.Minute), cmpopts.IgnoreFields(db.Schedule{}, "ID")); diff != "" { 129 t.Fatalf("s.Create() mismatch (-want +got):\n%s", diff) 130 } 131 got := s.Entries() 132 133 diffOpts := []cmp.Option{ 134 cmpopts.EquateApproxTime(time.Minute), 135 cmpopts.IgnoreFields(db.Schedule{}, "ID"), 136 cmpopts.IgnoreUnexported(RunOnce{}, WorkflowSchedule{}, time.Location{}), 137 cmpopts.IgnoreFields(ScheduleEntry{}, "ID", "LastRun.ID", "WrappedJob"), 138 } 139 if diff := cmp.Diff(c.wantEntries, got, diffOpts...); diff != "" { 140 t.Fatalf("s.Entries() mismatch (-want +got):\n%s", diff) 141 } 142 }) 143 } 144 } 145 146 func TestSchedulerResume(t *testing.T) { 147 now := time.Now() 148 cases := []struct { 149 desc string 150 scheds []db.CreateScheduleParams 151 want []ScheduleEntry 152 wantParams map[string]any 153 wantErr bool 154 }{ 155 { 156 desc: "success: once", 157 scheds: []db.CreateScheduleParams{ 158 { 159 WorkflowName: "echo", 160 WorkflowParams: sql.NullString{ 161 String: `{"farewell": "bye", "greeting": "hello"}`, 162 Valid: true, 163 }, 164 Once: now.UTC().AddDate(1, 0, 0), 165 CreatedAt: now, 166 UpdatedAt: now, 167 }, 168 }, 169 want: []ScheduleEntry{ 170 {Entry: cron.Entry{ 171 Schedule: &RunOnce{next: now.UTC().AddDate(1, 0, 0)}, 172 Next: now.UTC().AddDate(1, 0, 0), 173 Job: &WorkflowSchedule{ 174 Schedule: db.Schedule{ 175 WorkflowName: "echo", 176 WorkflowParams: sql.NullString{ 177 String: `{"farewell": "bye", "greeting": "hello"}`, 178 Valid: true, 179 }, 180 Once: now.UTC().AddDate(1, 0, 0), 181 CreatedAt: now, 182 UpdatedAt: now, 183 }, 184 Params: map[string]any{"greeting": "hello", "farewell": "bye"}, 185 }, 186 }}, 187 }, 188 }, 189 { 190 desc: "success: cron", 191 scheds: []db.CreateScheduleParams{ 192 { 193 WorkflowName: "echo", 194 WorkflowParams: sql.NullString{ 195 String: `{"farewell": "bye", "greeting": "hello"}`, 196 Valid: true, 197 }, 198 Spec: "* * * * *", 199 CreatedAt: now, 200 UpdatedAt: now, 201 }, 202 }, 203 want: []ScheduleEntry{ 204 {Entry: cron.Entry{ 205 Schedule: mustParseSpec(t, "* * * * *"), 206 Next: now.Add(time.Minute), 207 Job: &WorkflowSchedule{ 208 Schedule: db.Schedule{ 209 WorkflowName: "echo", 210 WorkflowParams: sql.NullString{ 211 String: `{"farewell": "bye", "greeting": "hello"}`, 212 Valid: true, 213 }, 214 Spec: "* * * * *", 215 CreatedAt: now, 216 UpdatedAt: now, 217 }, 218 Params: map[string]any{"greeting": "hello", "farewell": "bye"}, 219 }, 220 }}, 221 }, 222 }, 223 { 224 desc: "skip past RunOnce schedules", 225 scheds: []db.CreateScheduleParams{ 226 { 227 WorkflowName: "echo", 228 WorkflowParams: sql.NullString{ 229 String: `{"farewell": "bye", "greeting": "hello"}`, 230 Valid: true, 231 }, 232 Once: time.Now().AddDate(-1, 0, 0), 233 CreatedAt: now, 234 UpdatedAt: now, 235 }, 236 }, 237 want: []ScheduleEntry{}, 238 }, 239 } 240 for _, c := range cases { 241 t.Run(c.desc, func(t *testing.T) { 242 ctx, cancel := context.WithCancel(context.Background()) 243 defer cancel() 244 p := testDB(ctx, t) 245 q := db.New(p) 246 s := NewScheduler(p, NewWorker(NewDefinitionHolder(), p, &PGListener{DB: p})) 247 248 for _, csp := range c.scheds { 249 if _, err := q.CreateSchedule(ctx, csp); err != nil { 250 t.Fatalf("q.CreateSchedule(_, %#v) = _, %v, wanted no error", csp, err) 251 } 252 } 253 if err := s.Resume(ctx); err != nil { 254 t.Errorf("s.Resume() = %v, wanted no error", err) 255 } 256 got := s.Entries() 257 258 diffOpts := []cmp.Option{ 259 cmpopts.EquateApproxTime(time.Minute), 260 cmpopts.IgnoreFields(db.Schedule{}, "ID"), 261 cmpopts.IgnoreUnexported(RunOnce{}, WorkflowSchedule{}, time.Location{}), 262 cmpopts.IgnoreFields(ScheduleEntry{}, "ID", "LastRun.ID", "WrappedJob"), 263 } 264 if diff := cmp.Diff(c.want, got, diffOpts...); diff != "" { 265 t.Fatalf("s.Entries() mismatch (-want +got):\n%s", diff) 266 } 267 }) 268 } 269 } 270 271 func TestScheduleDelete(t *testing.T) { 272 now := time.Now() 273 cases := []struct { 274 desc string 275 sched Schedule 276 workflowName string 277 params map[string]any 278 wantErr bool 279 wantEntries []ScheduleEntry 280 want []db.Schedule 281 wantWorkflows []db.Workflow 282 }{ 283 { 284 desc: "success", 285 sched: Schedule{Once: now.AddDate(1, 0, 0), Type: ScheduleOnce}, 286 workflowName: "echo", 287 params: map[string]any{"greeting": "hello", "farewell": "bye"}, 288 wantEntries: []ScheduleEntry{}, 289 }, 290 } 291 for _, c := range cases { 292 t.Run(c.desc, func(t *testing.T) { 293 ctx, cancel := context.WithCancel(context.Background()) 294 defer cancel() 295 p := testDB(ctx, t) 296 q := db.New(p) 297 s := NewScheduler(p, NewWorker(NewDefinitionHolder(), p, &PGListener{DB: p})) 298 row, err := s.Create(ctx, c.sched, c.workflowName, c.params) 299 if err != nil { 300 t.Fatalf("s.Create(_, %v, %q, %v) = %v, %v, wanted no error", c.sched, c.workflowName, c.params, row, err) 301 } 302 // simulate a single run 303 wfid, err := s.w.StartWorkflow(ctx, c.workflowName, c.params, int(row.ID)) 304 if err != nil { 305 t.Fatalf("s.w.StartWorkflow(_, %q, %v, %d) = %q, %v, wanted no error", c.workflowName, c.params, row.ID, wfid.String(), err) 306 } 307 308 err = s.Delete(ctx, int(row.ID)) 309 if (err != nil) != c.wantErr { 310 t.Fatalf("s.Delete(%d) = %v, wantErr: %t", row.ID, err, c.wantErr) 311 } 312 313 entries := s.Entries() 314 diffOpts := []cmp.Option{ 315 cmpopts.EquateApproxTime(time.Minute), 316 cmpopts.IgnoreFields(db.Schedule{}, "ID"), 317 cmpopts.IgnoreUnexported(RunOnce{}, WorkflowSchedule{}, time.Location{}), 318 cmpopts.IgnoreFields(ScheduleEntry{}, "ID", "LastRun.ID", "WrappedJob"), 319 } 320 if c.sched.Type == ScheduleCron { 321 diffOpts = append(diffOpts, cmpopts.IgnoreFields(ScheduleEntry{}, "Next")) 322 } 323 if diff := cmp.Diff(c.wantEntries, entries, diffOpts...); diff != "" { 324 t.Errorf("s.Entries() mismatch (-want +got):\n%s", diff) 325 } 326 got, err := q.Schedules(ctx) 327 if err != nil { 328 t.Fatalf("q.Schedules() = %v, %v, wanted no error", got, err) 329 } 330 if diff := cmp.Diff(c.want, got, diffOpts...); diff != "" { 331 t.Errorf("q.Schedules() mismatch (-want +got):\n%s", diff) 332 } 333 wfs, err := q.Workflows(ctx) 334 if err != nil { 335 t.Fatalf("q.Workflows() = %v, %v, wanted no error", wfs, err) 336 } 337 if len(wfs) != 1 { 338 t.Errorf("len(q.Workflows()) = %d, wanted %d", len(wfs), 1) 339 } 340 for _, w := range wfs { 341 if w.ScheduleID.Int32 == row.ID { 342 t.Errorf("w.ScheduleID = %d, wanted != %d", w.ScheduleID.Int32, row.ID) 343 } 344 } 345 }) 346 } 347 }