Chromium Code Reviews| Index: scheduler/appengine/engine/cron/machine_test.go |
| diff --git a/scheduler/appengine/engine/cron/machine_test.go b/scheduler/appengine/engine/cron/machine_test.go |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..3320e235e4722398ba88c91f5f5208bfc885243c |
| --- /dev/null |
| +++ b/scheduler/appengine/engine/cron/machine_test.go |
| @@ -0,0 +1,356 @@ |
| +// Copyright 2017 The LUCI Authors. |
| +// |
| +// Licensed under the Apache License, Version 2.0 (the "License"); |
| +// you may not use this file except in compliance with the License. |
| +// You may obtain a copy of the License at |
| +// |
| +// http://www.apache.org/licenses/LICENSE-2.0 |
| +// |
| +// Unless required by applicable law or agreed to in writing, software |
| +// distributed under the License is distributed on an "AS IS" BASIS, |
| +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| +// See the License for the specific language governing permissions and |
| +// limitations under the License. |
| + |
| +package cron |
| + |
| +import ( |
| + "testing" |
| + "time" |
| + |
| + "github.com/luci/luci-go/scheduler/appengine/schedule" |
| + . "github.com/smartystreets/goconvey/convey" |
| +) |
| + |
| +func TestMachine(t *testing.T) { |
| + t.Parallel() |
| + |
| + at0min, _ := schedule.Parse("0 * * * *", 0) |
| + at45min, _ := schedule.Parse("45 * * * *", 0) |
| + each30min, _ := schedule.Parse("with 30m interval", 0) |
| + each10min, _ := schedule.Parse("with 10m interval", 0) |
| + never, _ := schedule.Parse("triggered", 0) |
| + |
| + Convey("Absolute schedule", t, func() { |
| + tm := testMachine{ |
| + Now: parseTime("00:15"), |
| + Schedule: at0min, |
| + } |
| + |
| + // Enabling the job schedules the first tick based on the schedule. |
| + err := tm.roll(func(m *Machine) error { |
| + m.Enable() |
| + return nil |
| + }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldResemble, []Action{ |
| + TickLaterAction{ |
| + When: parseTime("01:00"), |
| + TickNonce: 1, |
| + }, |
| + }) |
| + |
| + // RewindIfNecessary does nothing, the tick is already set. |
| + err = tm.roll(func(m *Machine) error { |
| + m.RewindIfNecessary() |
| + return nil |
| + }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldBeNil) |
| + |
| + // Early tick is ignored with an error. |
| + tm.Now = parseTime("00:59") |
| + err = tm.roll(func(m *Machine) error { return m.OnTimerTick(1) }) |
| + So(err.Error(), ShouldEqual, "tick happened 60.0 sec before it was expected") |
| + So(tm.Actions, ShouldEqual, nil) |
| + |
| + tm.Now = parseTime("01:01") // acceptable tick time (slightly late)! |
| + |
| + // A tick with wrong nonce is silently skipped. |
| + err = tm.roll(func(m *Machine) error { return m.OnTimerTick(123) }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldEqual, nil) |
| + |
| + // The correct tick comes. Invocation is started and new tick is scheduled. |
| + err = tm.roll(func(m *Machine) error { return m.OnTimerTick(1) }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldResemble, []Action{ |
| + StartInvocationAction{}, |
| + TickLaterAction{ |
| + When: parseTime("02:00"), |
| + TickNonce: 2, |
| + }, |
| + }) |
| + |
| + // Disabling the job. |
| + err = tm.roll(func(m *Machine) error { |
| + m.Disable() |
| + return nil |
| + }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldBeNil) |
| + |
| + // It silently skips the tick now. |
| + tm.Now = parseTime("02:00") |
| + err = tm.roll(func(m *Machine) error { return m.OnTimerTick(2) }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldEqual, nil) |
| + }) |
| + |
| + Convey("Relative schedule", t, func() { |
| + tm := testMachine{ |
| + Now: parseTime("00:00"), |
| + Schedule: each30min, |
| + } |
| + |
| + // Enabling the job schedules the first tick based on the schedule. |
| + err := tm.roll(func(m *Machine) error { |
| + m.Enable() |
| + return nil |
| + }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldResemble, []Action{ |
| + TickLaterAction{ |
| + When: parseTime("00:30"), |
| + TickNonce: 1, |
| + }, |
| + }) |
| + |
| + // RewindIfNecessary does nothing, the tick is already set. |
| + err = tm.roll(func(m *Machine) error { |
| + m.RewindIfNecessary() |
| + return nil |
| + }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldBeNil) |
| + |
| + // Tick arrives (slightly late). The invocation is started, but the next |
| + // tick is _not_ set. |
| + tm.Now = parseTime("00:31") |
| + err = tm.roll(func(m *Machine) error { return m.OnTimerTick(1) }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldResemble, []Action{ |
| + StartInvocationAction{}, |
| + }) |
| + |
| + // Some time later (when invocation has presumably finishes), rewind the |
|
tandrii(chromium)
2017/07/27 11:57:24
finisheD
Vadim Sh.
2017/07/27 17:10:21
Done.
|
| + // clock. It sets a new tick 30min from now. |
| + tm.Now = parseTime("00:40") |
| + err = tm.roll(func(m *Machine) error { |
| + m.RewindIfNecessary() |
| + return nil |
| + }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldResemble, []Action{ |
| + TickLaterAction{ |
| + When: parseTime("01:10"), // 40min + 30min |
| + TickNonce: 2, |
| + }, |
| + }) |
| + }) |
| + |
| + Convey("Relative schedule, distant future", t, func() { |
| + tm := testMachine{ |
| + Now: parseTime("00:00"), |
| + Schedule: never, |
| + } |
| + |
| + // Enabling the job does nothing. |
| + err := tm.roll(func(m *Machine) error { |
| + m.Enable() |
| + return nil |
| + }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldBeNil) |
| + |
| + // Rewinding does nothing. |
| + err = tm.roll(func(m *Machine) error { |
| + m.RewindIfNecessary() |
| + return nil |
| + }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldBeNil) |
| + |
| + // Ticking does nothing. |
| + err = tm.roll(func(m *Machine) error { return m.OnTimerTick(1) }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldBeNil) |
| + }) |
| + |
| + Convey("Schedule changes", t, func() { |
| + // Start with absolute. |
| + tm := testMachine{ |
| + Now: parseTime("00:00"), |
| + Schedule: at0min, |
| + } |
| + |
| + // The first tick is scheduled to 1h from now. |
| + err := tm.roll(func(m *Machine) error { |
| + m.Enable() |
| + return nil |
| + }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldResemble, []Action{ |
| + TickLaterAction{ |
| + When: parseTime("01:00"), |
| + TickNonce: 1, |
| + }, |
| + }) |
| + |
| + // 10 min later switch to the relative schedule. It reschedules the tick |
| + // to 30 min since the _previous action_ (which was 'Enable'). |
| + tm.Now = parseTime("00:10") |
| + tm.Schedule = each30min |
| + err = tm.roll(func(m *Machine) error { |
| + m.OnScheduleChange() |
| + return nil |
| + }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldResemble, []Action{ |
| + TickLaterAction{ |
| + When: parseTime("00:30"), |
| + TickNonce: 2, |
| + }, |
| + }) |
| + |
| + // The operation is idempotent. No new tick are scheduled when we try again. |
|
tandrii(chromium)
2017/07/27 11:57:24
s/are/is
Vadim Sh.
2017/07/27 17:10:21
Done.
|
| + tm.Now = parseTime("00:15") |
| + err = tm.roll(func(m *Machine) error { |
| + m.OnScheduleChange() |
| + return nil |
| + }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldBeNil) |
| + |
| + // The scheduled tick comes. Since it is a relative schedule, no new tick |
| + // is scheduled. |
| + tm.Now = parseTime("00:30") |
| + err = tm.roll(func(m *Machine) error { return m.OnTimerTick(2) }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldResemble, []Action{ |
| + StartInvocationAction{}, |
| + }) |
| + |
| + // Some time later we switch it to another relative schedule. Nothing |
| + // happens, since we are waiting for a rewind now anyway. |
| + tm.Now = parseTime("00:40") |
| + tm.Schedule = each10min |
| + err = tm.roll(func(m *Machine) error { |
| + m.OnScheduleChange() |
| + return nil |
| + }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldBeNil) |
| + |
| + // Now we switch back to the absolute schedule. It schedules a new tick. |
| + tm.Now = parseTime("01:30") |
| + tm.Schedule = at0min |
| + err = tm.roll(func(m *Machine) error { |
| + m.OnScheduleChange() |
| + return nil |
| + }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldResemble, []Action{ |
| + TickLaterAction{ |
| + When: parseTime("02:00"), |
| + TickNonce: 3, |
| + }, |
| + }) |
| + |
| + // Changing the absolute schedule moves the tick accordingly. |
| + tm.Schedule = at45min |
| + err = tm.roll(func(m *Machine) error { |
| + m.OnScheduleChange() |
| + return nil |
| + }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldResemble, []Action{ |
| + TickLaterAction{ |
| + When: parseTime("01:45"), |
| + TickNonce: 4, |
| + }, |
| + }) |
| + |
| + // Switching to 'triggered' schedule "disarms" the current tick by replacing |
| + // it with "tick in the distant future". This doesn't emit any actions, |
| + // since we can't actually schedule tick in the distant future. |
| + tm.Schedule = never |
| + err = tm.roll(func(m *Machine) error { |
| + m.OnScheduleChange() |
| + return nil |
| + }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldBeNil) |
| + So(tm.State.LastTick, ShouldResemble, TickLaterAction{ |
| + When: schedule.DistantFuture, |
| + TickNonce: 5, |
| + }) |
| + |
| + // Enabling back absolute schedule places a new tick. |
| + tm.Now = parseTime("01:30") |
| + tm.Schedule = at0min |
| + err = tm.roll(func(m *Machine) error { |
| + m.OnScheduleChange() |
| + return nil |
| + }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldResemble, []Action{ |
| + TickLaterAction{ |
| + When: parseTime("02:00"), |
| + TickNonce: 6, |
| + }, |
| + }) |
| + |
| + // Schedule changes do nothing to disabled jobs. |
| + tm.Schedule = at45min |
| + err = tm.roll(func(m *Machine) error { |
| + m.Disable() |
| + m.OnScheduleChange() |
| + return nil |
| + }) |
| + So(err, ShouldBeNil) |
| + So(tm.Actions, ShouldBeNil) |
| + }) |
| + |
| + Convey("Petty code coverage", t, func() { |
| + // Just to get 100% code coverage... |
|
tandrii(chromium)
2017/07/27 11:57:24
:)
|
| + So((StartInvocationAction{}).IsAction(), ShouldBeTrue) |
| + So((TickLaterAction{}).IsAction(), ShouldBeTrue) |
| + }) |
| +} |
| + |
| +func parseTime(str string) time.Time { |
| + t, err := time.Parse(time.RFC822, "01 Jan 17 "+str+" UTC") |
| + if err != nil { |
| + panic(err) |
| + } |
| + return t |
| +} |
| + |
| +type testMachine struct { |
| + State State |
| + Schedule *schedule.Schedule |
| + Now time.Time |
| + Nonces int64 |
| + Actions []Action |
| +} |
| + |
| +func (t *testMachine) roll(cb func(*Machine) error) error { |
| + m := Machine{ |
| + Now: t.Now, |
| + Schedule: t.Schedule, |
| + Nonce: func() int64 { |
| + t.Nonces++ |
| + return t.Nonces |
| + }, |
| + State: t.State, |
| + } |
| + |
| + if err := cb(&m); err != nil { |
| + return err |
| + } |
| + |
| + t.State = m.State |
| + t.Actions = m.Actions |
| + return nil |
| +} |