Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright 2017 The LUCI Authors. | |
| 2 // | |
| 3 // Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 // you may not use this file except in compliance with the License. | |
| 5 // You may obtain a copy of the License at | |
| 6 // | |
| 7 // http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 // | |
| 9 // Unless required by applicable law or agreed to in writing, software | |
| 10 // distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 // See the License for the specific language governing permissions and | |
| 13 // limitations under the License. | |
| 14 | |
| 15 package cron | |
| 16 | |
| 17 import ( | |
| 18 "testing" | |
| 19 "time" | |
| 20 | |
| 21 "github.com/luci/luci-go/scheduler/appengine/schedule" | |
| 22 . "github.com/smartystreets/goconvey/convey" | |
| 23 ) | |
| 24 | |
| 25 func TestMachine(t *testing.T) { | |
| 26 t.Parallel() | |
| 27 | |
| 28 at0min, _ := schedule.Parse("0 * * * *", 0) | |
| 29 at45min, _ := schedule.Parse("45 * * * *", 0) | |
| 30 each30min, _ := schedule.Parse("with 30m interval", 0) | |
| 31 each10min, _ := schedule.Parse("with 10m interval", 0) | |
| 32 never, _ := schedule.Parse("triggered", 0) | |
| 33 | |
| 34 Convey("Absolute schedule", t, func() { | |
| 35 tm := testMachine{ | |
| 36 Now: parseTime("00:15"), | |
| 37 Schedule: at0min, | |
| 38 } | |
| 39 | |
| 40 // Enabling the job schedules the first tick based on the schedu le. | |
| 41 err := tm.roll(func(m *Machine) error { | |
| 42 m.Enable() | |
| 43 return nil | |
| 44 }) | |
| 45 So(err, ShouldBeNil) | |
| 46 So(tm.Actions, ShouldResemble, []Action{ | |
| 47 TickLaterAction{ | |
| 48 When: parseTime("01:00"), | |
| 49 TickNonce: 1, | |
| 50 }, | |
| 51 }) | |
| 52 | |
| 53 // RewindIfNecessary does nothing, the tick is already set. | |
| 54 err = tm.roll(func(m *Machine) error { | |
| 55 m.RewindIfNecessary() | |
| 56 return nil | |
| 57 }) | |
| 58 So(err, ShouldBeNil) | |
| 59 So(tm.Actions, ShouldBeNil) | |
| 60 | |
| 61 // Early tick is ignored with an error. | |
| 62 tm.Now = parseTime("00:59") | |
| 63 err = tm.roll(func(m *Machine) error { return m.OnTimerTick(1) } ) | |
| 64 So(err.Error(), ShouldEqual, "tick happened 60.0 sec before it w as expected") | |
| 65 So(tm.Actions, ShouldEqual, nil) | |
| 66 | |
| 67 tm.Now = parseTime("01:01") // acceptable tick time (slightly la te)! | |
| 68 | |
| 69 // A tick with wrong nonce is silently skipped. | |
| 70 err = tm.roll(func(m *Machine) error { return m.OnTimerTick(123) }) | |
| 71 So(err, ShouldBeNil) | |
| 72 So(tm.Actions, ShouldEqual, nil) | |
| 73 | |
| 74 // The correct tick comes. Invocation is started and new tick is scheduled. | |
| 75 err = tm.roll(func(m *Machine) error { return m.OnTimerTick(1) } ) | |
| 76 So(err, ShouldBeNil) | |
| 77 So(tm.Actions, ShouldResemble, []Action{ | |
| 78 StartInvocationAction{}, | |
| 79 TickLaterAction{ | |
| 80 When: parseTime("02:00"), | |
| 81 TickNonce: 2, | |
| 82 }, | |
| 83 }) | |
| 84 | |
| 85 // Disabling the job. | |
| 86 err = tm.roll(func(m *Machine) error { | |
| 87 m.Disable() | |
| 88 return nil | |
| 89 }) | |
| 90 So(err, ShouldBeNil) | |
| 91 So(tm.Actions, ShouldBeNil) | |
| 92 | |
| 93 // It silently skips the tick now. | |
| 94 tm.Now = parseTime("02:00") | |
| 95 err = tm.roll(func(m *Machine) error { return m.OnTimerTick(2) } ) | |
| 96 So(err, ShouldBeNil) | |
| 97 So(tm.Actions, ShouldEqual, nil) | |
| 98 }) | |
| 99 | |
| 100 Convey("Relative schedule", t, func() { | |
| 101 tm := testMachine{ | |
| 102 Now: parseTime("00:00"), | |
| 103 Schedule: each30min, | |
| 104 } | |
| 105 | |
| 106 // Enabling the job schedules the first tick based on the schedu le. | |
| 107 err := tm.roll(func(m *Machine) error { | |
| 108 m.Enable() | |
| 109 return nil | |
| 110 }) | |
| 111 So(err, ShouldBeNil) | |
| 112 So(tm.Actions, ShouldResemble, []Action{ | |
| 113 TickLaterAction{ | |
| 114 When: parseTime("00:30"), | |
| 115 TickNonce: 1, | |
| 116 }, | |
| 117 }) | |
| 118 | |
| 119 // RewindIfNecessary does nothing, the tick is already set. | |
| 120 err = tm.roll(func(m *Machine) error { | |
| 121 m.RewindIfNecessary() | |
| 122 return nil | |
| 123 }) | |
| 124 So(err, ShouldBeNil) | |
| 125 So(tm.Actions, ShouldBeNil) | |
| 126 | |
| 127 // Tick arrives (slightly late). The invocation is started, but the next | |
| 128 // tick is _not_ set. | |
| 129 tm.Now = parseTime("00:31") | |
| 130 err = tm.roll(func(m *Machine) error { return m.OnTimerTick(1) } ) | |
| 131 So(err, ShouldBeNil) | |
| 132 So(tm.Actions, ShouldResemble, []Action{ | |
| 133 StartInvocationAction{}, | |
| 134 }) | |
| 135 | |
| 136 // Some time later (when invocation has presumably finishes), re wind the | |
|
tandrii(chromium)
2017/07/27 11:57:24
finisheD
Vadim Sh.
2017/07/27 17:10:21
Done.
| |
| 137 // clock. It sets a new tick 30min from now. | |
| 138 tm.Now = parseTime("00:40") | |
| 139 err = tm.roll(func(m *Machine) error { | |
| 140 m.RewindIfNecessary() | |
| 141 return nil | |
| 142 }) | |
| 143 So(err, ShouldBeNil) | |
| 144 So(tm.Actions, ShouldResemble, []Action{ | |
| 145 TickLaterAction{ | |
| 146 When: parseTime("01:10"), // 40min + 30min | |
| 147 TickNonce: 2, | |
| 148 }, | |
| 149 }) | |
| 150 }) | |
| 151 | |
| 152 Convey("Relative schedule, distant future", t, func() { | |
| 153 tm := testMachine{ | |
| 154 Now: parseTime("00:00"), | |
| 155 Schedule: never, | |
| 156 } | |
| 157 | |
| 158 // Enabling the job does nothing. | |
| 159 err := tm.roll(func(m *Machine) error { | |
| 160 m.Enable() | |
| 161 return nil | |
| 162 }) | |
| 163 So(err, ShouldBeNil) | |
| 164 So(tm.Actions, ShouldBeNil) | |
| 165 | |
| 166 // Rewinding does nothing. | |
| 167 err = tm.roll(func(m *Machine) error { | |
| 168 m.RewindIfNecessary() | |
| 169 return nil | |
| 170 }) | |
| 171 So(err, ShouldBeNil) | |
| 172 So(tm.Actions, ShouldBeNil) | |
| 173 | |
| 174 // Ticking does nothing. | |
| 175 err = tm.roll(func(m *Machine) error { return m.OnTimerTick(1) } ) | |
| 176 So(err, ShouldBeNil) | |
| 177 So(tm.Actions, ShouldBeNil) | |
| 178 }) | |
| 179 | |
| 180 Convey("Schedule changes", t, func() { | |
| 181 // Start with absolute. | |
| 182 tm := testMachine{ | |
| 183 Now: parseTime("00:00"), | |
| 184 Schedule: at0min, | |
| 185 } | |
| 186 | |
| 187 // The first tick is scheduled to 1h from now. | |
| 188 err := tm.roll(func(m *Machine) error { | |
| 189 m.Enable() | |
| 190 return nil | |
| 191 }) | |
| 192 So(err, ShouldBeNil) | |
| 193 So(tm.Actions, ShouldResemble, []Action{ | |
| 194 TickLaterAction{ | |
| 195 When: parseTime("01:00"), | |
| 196 TickNonce: 1, | |
| 197 }, | |
| 198 }) | |
| 199 | |
| 200 // 10 min later switch to the relative schedule. It reschedules the tick | |
| 201 // to 30 min since the _previous action_ (which was 'Enable'). | |
| 202 tm.Now = parseTime("00:10") | |
| 203 tm.Schedule = each30min | |
| 204 err = tm.roll(func(m *Machine) error { | |
| 205 m.OnScheduleChange() | |
| 206 return nil | |
| 207 }) | |
| 208 So(err, ShouldBeNil) | |
| 209 So(tm.Actions, ShouldResemble, []Action{ | |
| 210 TickLaterAction{ | |
| 211 When: parseTime("00:30"), | |
| 212 TickNonce: 2, | |
| 213 }, | |
| 214 }) | |
| 215 | |
| 216 // The operation is idempotent. No new tick are scheduled when w e try again. | |
|
tandrii(chromium)
2017/07/27 11:57:24
s/are/is
Vadim Sh.
2017/07/27 17:10:21
Done.
| |
| 217 tm.Now = parseTime("00:15") | |
| 218 err = tm.roll(func(m *Machine) error { | |
| 219 m.OnScheduleChange() | |
| 220 return nil | |
| 221 }) | |
| 222 So(err, ShouldBeNil) | |
| 223 So(tm.Actions, ShouldBeNil) | |
| 224 | |
| 225 // The scheduled tick comes. Since it is a relative schedule, no new tick | |
| 226 // is scheduled. | |
| 227 tm.Now = parseTime("00:30") | |
| 228 err = tm.roll(func(m *Machine) error { return m.OnTimerTick(2) } ) | |
| 229 So(err, ShouldBeNil) | |
| 230 So(tm.Actions, ShouldResemble, []Action{ | |
| 231 StartInvocationAction{}, | |
| 232 }) | |
| 233 | |
| 234 // Some time later we switch it to another relative schedule. No thing | |
| 235 // happens, since we are waiting for a rewind now anyway. | |
| 236 tm.Now = parseTime("00:40") | |
| 237 tm.Schedule = each10min | |
| 238 err = tm.roll(func(m *Machine) error { | |
| 239 m.OnScheduleChange() | |
| 240 return nil | |
| 241 }) | |
| 242 So(err, ShouldBeNil) | |
| 243 So(tm.Actions, ShouldBeNil) | |
| 244 | |
| 245 // Now we switch back to the absolute schedule. It schedules a n ew tick. | |
| 246 tm.Now = parseTime("01:30") | |
| 247 tm.Schedule = at0min | |
| 248 err = tm.roll(func(m *Machine) error { | |
| 249 m.OnScheduleChange() | |
| 250 return nil | |
| 251 }) | |
| 252 So(err, ShouldBeNil) | |
| 253 So(tm.Actions, ShouldResemble, []Action{ | |
| 254 TickLaterAction{ | |
| 255 When: parseTime("02:00"), | |
| 256 TickNonce: 3, | |
| 257 }, | |
| 258 }) | |
| 259 | |
| 260 // Changing the absolute schedule moves the tick accordingly. | |
| 261 tm.Schedule = at45min | |
| 262 err = tm.roll(func(m *Machine) error { | |
| 263 m.OnScheduleChange() | |
| 264 return nil | |
| 265 }) | |
| 266 So(err, ShouldBeNil) | |
| 267 So(tm.Actions, ShouldResemble, []Action{ | |
| 268 TickLaterAction{ | |
| 269 When: parseTime("01:45"), | |
| 270 TickNonce: 4, | |
| 271 }, | |
| 272 }) | |
| 273 | |
| 274 // Switching to 'triggered' schedule "disarms" the current tick by replacing | |
| 275 // it with "tick in the distant future". This doesn't emit any a ctions, | |
| 276 // since we can't actually schedule tick in the distant future. | |
| 277 tm.Schedule = never | |
| 278 err = tm.roll(func(m *Machine) error { | |
| 279 m.OnScheduleChange() | |
| 280 return nil | |
| 281 }) | |
| 282 So(err, ShouldBeNil) | |
| 283 So(tm.Actions, ShouldBeNil) | |
| 284 So(tm.State.LastTick, ShouldResemble, TickLaterAction{ | |
| 285 When: schedule.DistantFuture, | |
| 286 TickNonce: 5, | |
| 287 }) | |
| 288 | |
| 289 // Enabling back absolute schedule places a new tick. | |
| 290 tm.Now = parseTime("01:30") | |
| 291 tm.Schedule = at0min | |
| 292 err = tm.roll(func(m *Machine) error { | |
| 293 m.OnScheduleChange() | |
| 294 return nil | |
| 295 }) | |
| 296 So(err, ShouldBeNil) | |
| 297 So(tm.Actions, ShouldResemble, []Action{ | |
| 298 TickLaterAction{ | |
| 299 When: parseTime("02:00"), | |
| 300 TickNonce: 6, | |
| 301 }, | |
| 302 }) | |
| 303 | |
| 304 // Schedule changes do nothing to disabled jobs. | |
| 305 tm.Schedule = at45min | |
| 306 err = tm.roll(func(m *Machine) error { | |
| 307 m.Disable() | |
| 308 m.OnScheduleChange() | |
| 309 return nil | |
| 310 }) | |
| 311 So(err, ShouldBeNil) | |
| 312 So(tm.Actions, ShouldBeNil) | |
| 313 }) | |
| 314 | |
| 315 Convey("Petty code coverage", t, func() { | |
| 316 // Just to get 100% code coverage... | |
|
tandrii(chromium)
2017/07/27 11:57:24
:)
| |
| 317 So((StartInvocationAction{}).IsAction(), ShouldBeTrue) | |
| 318 So((TickLaterAction{}).IsAction(), ShouldBeTrue) | |
| 319 }) | |
| 320 } | |
| 321 | |
| 322 func parseTime(str string) time.Time { | |
| 323 t, err := time.Parse(time.RFC822, "01 Jan 17 "+str+" UTC") | |
| 324 if err != nil { | |
| 325 panic(err) | |
| 326 } | |
| 327 return t | |
| 328 } | |
| 329 | |
| 330 type testMachine struct { | |
| 331 State State | |
| 332 Schedule *schedule.Schedule | |
| 333 Now time.Time | |
| 334 Nonces int64 | |
| 335 Actions []Action | |
| 336 } | |
| 337 | |
| 338 func (t *testMachine) roll(cb func(*Machine) error) error { | |
| 339 m := Machine{ | |
| 340 Now: t.Now, | |
| 341 Schedule: t.Schedule, | |
| 342 Nonce: func() int64 { | |
| 343 t.Nonces++ | |
| 344 return t.Nonces | |
| 345 }, | |
| 346 State: t.State, | |
| 347 } | |
| 348 | |
| 349 if err := cb(&m); err != nil { | |
| 350 return err | |
| 351 } | |
| 352 | |
| 353 t.State = m.State | |
| 354 t.Actions = m.Actions | |
| 355 return nil | |
| 356 } | |
| OLD | NEW |