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 |