| 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..e0cbb00f23703687147f36bdd28229556a645b0f
|
| --- /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 finished), rewind the
|
| + // 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 is scheduled when we try again.
|
| + 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...
|
| + 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
|
| +}
|
|
|