github.com/rigado/snapd@v2.42.5-go-mod+incompatible/timeutil/schedule_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package timeutil_test
    21  
    22  import (
    23  	"strings"
    24  	"testing"
    25  	"time"
    26  
    27  	. "gopkg.in/check.v1"
    28  
    29  	"github.com/snapcore/snapd/timeutil"
    30  )
    31  
    32  func Test(t *testing.T) { TestingT(t) }
    33  
    34  type timeutilSuite struct{}
    35  
    36  var _ = Suite(&timeutilSuite{})
    37  
    38  func (ts *timeutilSuite) TestClock(c *C) {
    39  	td := timeutil.Clock{Hour: 23, Minute: 59}
    40  	c.Check(td.Add(time.Minute), Equals, timeutil.Clock{Hour: 0, Minute: 0})
    41  
    42  	td = timeutil.Clock{Hour: 5, Minute: 34}
    43  	c.Check(td.Add(time.Minute), Equals, timeutil.Clock{Hour: 5, Minute: 35})
    44  
    45  	td = timeutil.Clock{Hour: 10, Minute: 1}
    46  	c.Check(td.Sub(timeutil.Clock{Hour: 10, Minute: 0}), Equals, time.Minute)
    47  
    48  	td = timeutil.Clock{Hour: 23, Minute: 0}
    49  	c.Check(td.Add(time.Hour), Equals, timeutil.Clock{Hour: 0, Minute: 0})
    50  	c.Check(td.Add(2*time.Hour), Equals, timeutil.Clock{Hour: 1, Minute: 0})
    51  	c.Check(td.Sub(timeutil.Clock{Hour: 1, Minute: 0}), Equals, 22*time.Hour)
    52  	c.Check(td.Sub(timeutil.Clock{Hour: 0, Minute: 0}), Equals, 23*time.Hour)
    53  
    54  	td = timeutil.Clock{Hour: 1, Minute: 0}
    55  	c.Check(td.Sub(timeutil.Clock{Hour: 23, Minute: 0}), Equals, -2*time.Hour)
    56  	c.Check(td.Sub(timeutil.Clock{Hour: 1, Minute: 0}), Equals, time.Duration(0))
    57  
    58  	td = timeutil.Clock{Hour: 0, Minute: 0}
    59  	c.Check(td.Sub(timeutil.Clock{Hour: 23, Minute: 0}), Equals, -1*time.Hour)
    60  	c.Check(td.Sub(timeutil.Clock{Hour: 1, Minute: 0}), Equals, -23*time.Hour)
    61  }
    62  
    63  func (ts *timeutilSuite) TestParseClock(c *C) {
    64  	for _, t := range []struct {
    65  		timeStr      string
    66  		hour, minute int
    67  		errStr       string
    68  	}{
    69  		{"8:59", 8, 59, ""},
    70  		{"08:59", 8, 59, ""},
    71  		{"12:00", 12, 0, ""},
    72  		{"xx", 0, 0, `cannot parse "xx"`},
    73  		{"11:61", 0, 0, `cannot parse "11:61"`},
    74  		{"25:00", 0, 0, `cannot parse "25:00"`},
    75  	} {
    76  		ti, err := timeutil.ParseClock(t.timeStr)
    77  		if t.errStr != "" {
    78  			c.Check(err, ErrorMatches, t.errStr)
    79  		} else {
    80  			c.Check(err, IsNil)
    81  			c.Check(ti.Hour, Equals, t.hour)
    82  			c.Check(ti.Minute, Equals, t.minute)
    83  		}
    84  	}
    85  }
    86  
    87  func (ts *timeutilSuite) TestScheduleString(c *C) {
    88  	for _, t := range []struct {
    89  		sched timeutil.Schedule
    90  		str   string
    91  	}{
    92  		{
    93  			timeutil.Schedule{
    94  				ClockSpans: []timeutil.ClockSpan{
    95  					{Start: timeutil.Clock{Hour: 13, Minute: 41}, End: timeutil.Clock{Hour: 14, Minute: 59}}},
    96  			},
    97  			"13:41-14:59",
    98  		}, {
    99  			timeutil.Schedule{
   100  				ClockSpans: []timeutil.ClockSpan{
   101  					{Start: timeutil.Clock{Hour: 13, Minute: 41}, End: timeutil.Clock{Hour: 14, Minute: 59}},
   102  				},
   103  				WeekSpans: []timeutil.WeekSpan{
   104  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Monday}}},
   105  			},
   106  			"mon,13:41-14:59",
   107  		}, {
   108  			timeutil.Schedule{
   109  				ClockSpans: []timeutil.ClockSpan{
   110  					{Start: timeutil.Clock{Hour: 13, Minute: 41}, End: timeutil.Clock{Hour: 14, Minute: 59}, Spread: true}},
   111  			},
   112  			"13:41~14:59",
   113  		}, {
   114  			timeutil.Schedule{
   115  				ClockSpans: []timeutil.ClockSpan{
   116  					{Start: timeutil.Clock{Hour: 6}, End: timeutil.Clock{Hour: 6}}},
   117  				WeekSpans: []timeutil.WeekSpan{
   118  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Friday}}},
   119  			},
   120  			"mon-fri,06:00",
   121  		}, {
   122  			timeutil.Schedule{
   123  				ClockSpans: []timeutil.ClockSpan{
   124  					{Start: timeutil.Clock{Hour: 6}, End: timeutil.Clock{Hour: 6}},
   125  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 14}, Spread: true, Split: 2}},
   126  				WeekSpans: []timeutil.WeekSpan{
   127  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Friday}},
   128  					{Start: timeutil.Week{Weekday: time.Saturday}, End: timeutil.Week{Weekday: time.Saturday}}},
   129  			},
   130  			"mon-fri,sat,06:00,09:00~14:00/2",
   131  		}, {
   132  			timeutil.Schedule{
   133  				ClockSpans: []timeutil.ClockSpan{
   134  					{Start: timeutil.Clock{Hour: 6}, End: timeutil.Clock{Hour: 6}}},
   135  				WeekSpans: []timeutil.WeekSpan{
   136  					{Start: timeutil.Week{Weekday: time.Monday, Pos: 1}, End: timeutil.Week{Weekday: time.Friday, Pos: 1}}},
   137  			},
   138  			"mon1-fri1,06:00",
   139  		}, {
   140  			timeutil.Schedule{
   141  				ClockSpans: []timeutil.ClockSpan{
   142  					{Start: timeutil.Clock{Hour: 6}, End: timeutil.Clock{Hour: 6}}},
   143  				WeekSpans: []timeutil.WeekSpan{
   144  					{Start: timeutil.Week{Weekday: time.Monday, Pos: 5},
   145  						End: timeutil.Week{Weekday: time.Monday, Pos: 5}}},
   146  			},
   147  			"mon5,06:00",
   148  		}, {
   149  			timeutil.Schedule{
   150  				WeekSpans: []timeutil.WeekSpan{
   151  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Monday}}},
   152  			},
   153  			"mon",
   154  		}, {
   155  			timeutil.Schedule{
   156  				ClockSpans: []timeutil.ClockSpan{
   157  					{Start: timeutil.Clock{Hour: 6}, End: timeutil.Clock{Hour: 9}, Spread: true, Split: 2}},
   158  			},
   159  			"06:00~09:00/2",
   160  		},
   161  	} {
   162  		c.Check(t.sched.String(), Equals, t.str)
   163  	}
   164  }
   165  
   166  func (ts *timeutilSuite) TestParseLegacySchedule(c *C) {
   167  	for _, t := range []struct {
   168  		in       string
   169  		expected []*timeutil.Schedule
   170  		errStr   string
   171  	}{
   172  		// invalid
   173  		{"", nil, `cannot parse "": not a valid interval`},
   174  		{"invalid-11:00", nil, `cannot parse "invalid": not a valid time`},
   175  		{"9:00-11:00/invalid", nil, `cannot parse "invalid": not a valid interval`},
   176  		{"09:00-25:00", nil, `cannot parse "25:00": not a valid time`},
   177  		{"09:00-24:30", nil, `cannot parse "24:30": not a valid time`},
   178  
   179  		// valid
   180  		{"9:00-11:00", []*timeutil.Schedule{
   181  			{ClockSpans: []timeutil.ClockSpan{
   182  				{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}, Spread: true}}},
   183  		}, ""},
   184  		{"9:00-11:00/20:00-22:00", []*timeutil.Schedule{
   185  			{ClockSpans: []timeutil.ClockSpan{
   186  				{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}, Spread: true}}},
   187  			{ClockSpans: []timeutil.ClockSpan{
   188  				{Start: timeutil.Clock{Hour: 20}, End: timeutil.Clock{Hour: 22}, Spread: true}}},
   189  		}, ""},
   190  	} {
   191  		c.Logf("trying: %v", t)
   192  		schedule, err := timeutil.ParseLegacySchedule(t.in)
   193  		if t.errStr != "" {
   194  			c.Check(err, ErrorMatches, t.errStr, Commentf("%q returned unexpected error: %s", t.in, err))
   195  		} else {
   196  			c.Check(err, IsNil, Commentf("%q returned error: %s", t.in, err))
   197  			c.Check(schedule, DeepEquals, t.expected, Commentf("%q failed", t.in))
   198  		}
   199  
   200  	}
   201  }
   202  
   203  func parse(c *C, s string) (time.Duration, time.Duration) {
   204  	l := strings.Split(s, "-")
   205  	c.Assert(l, HasLen, 2)
   206  	a, err := time.ParseDuration(l[0])
   207  	c.Assert(err, IsNil)
   208  	b, err := time.ParseDuration(l[1])
   209  	c.Assert(err, IsNil)
   210  	return a, b
   211  }
   212  
   213  const (
   214  	maxDuration = 60 * 24 * time.Hour
   215  )
   216  
   217  func (ts *timeutilSuite) TestLegacyScheduleNext(c *C) {
   218  	const shortForm = "2006-01-02 15:04"
   219  
   220  	for _, t := range []struct {
   221  		schedule string
   222  		last     string
   223  		now      string
   224  		next     string
   225  	}{
   226  		{
   227  			// daily schedule, missed one window
   228  			// -> run next daily window
   229  			schedule: "9:00-11:00/21:00-23:00",
   230  			last:     "2017-02-05 22:00",
   231  			now:      "2017-02-06 20:00",
   232  			next:     "1h-3h",
   233  		},
   234  		{
   235  			// daily schedule, used one window
   236  			// -> run next daily window
   237  			schedule: "9:00-11:00/21:00-23:00",
   238  			last:     "2017-02-06 10:00",
   239  			now:      "2017-02-06 20:00",
   240  			next:     "1h-3h",
   241  		},
   242  		{
   243  			// daily schedule, missed all todays windows
   244  			// run tomorrow
   245  			schedule: "9:00-11:00/21:00-22:00",
   246  			last:     "2017-02-04 21:30",
   247  			now:      "2017-02-06 23:00",
   248  			next:     "10h-12h",
   249  		},
   250  		{
   251  			// single daily schedule, already updated today
   252  			schedule: "9:00-11:00",
   253  			last:     "2017-02-06 09:30",
   254  			now:      "2017-02-06 10:00",
   255  			next:     "23h-25h",
   256  		},
   257  		{
   258  			// single daily schedule, already updated today
   259  			// (at exactly the edge)
   260  			schedule: "9:00-11:00",
   261  			last:     "2017-02-06 09:00",
   262  			now:      "2017-02-06 09:00",
   263  			next:     "24h-26h",
   264  		},
   265  		{
   266  			// single daily schedule, last update a day ago
   267  			// now is within the update window so randomize
   268  			// (run within remaining time delta)
   269  			schedule: "9:00-11:00",
   270  			last:     "2017-02-05 09:30",
   271  			now:      "2017-02-06 10:00",
   272  			next:     "0-55m",
   273  		},
   274  		{
   275  			// multi daily schedule, already updated today
   276  			schedule: "9:00-11:00/21:00-22:00",
   277  			last:     "2017-02-06 21:30",
   278  			now:      "2017-02-06 23:00",
   279  			next:     "10h-12h",
   280  		},
   281  		{
   282  			// daily schedule, very small window
   283  			schedule: "9:00-9:03",
   284  			last:     "2017-02-05 09:02",
   285  			now:      "2017-02-06 08:58",
   286  			next:     "2m-5m",
   287  		},
   288  		{
   289  			// daily schedule, zero window
   290  			schedule: "9:00-9:00",
   291  			last:     "2017-02-05 09:02",
   292  			now:      "2017-02-06 08:58",
   293  			next:     "2m-2m",
   294  		},
   295  	} {
   296  		last, err := time.ParseInLocation(shortForm, t.last, time.Local)
   297  		c.Assert(err, IsNil)
   298  
   299  		fakeNow, err := time.ParseInLocation(shortForm, t.now, time.Local)
   300  		c.Assert(err, IsNil)
   301  		restorer := timeutil.MockTimeNow(func() time.Time {
   302  			return fakeNow
   303  		})
   304  		defer restorer()
   305  
   306  		sched, err := timeutil.ParseLegacySchedule(t.schedule)
   307  		c.Assert(err, IsNil)
   308  		minDist, maxDist := parse(c, t.next)
   309  
   310  		next := timeutil.Next(sched, last, maxDuration)
   311  		c.Check(next >= minDist && next <= maxDist, Equals, true, Commentf("invalid  distance for schedule %q with last refresh %q, now %q, expected %v, got %v", t.schedule, t.last, t.now, t.next, next))
   312  	}
   313  
   314  }
   315  
   316  func (ts *timeutilSuite) TestParseSchedule(c *C) {
   317  	for _, t := range []struct {
   318  		in       string
   319  		expected []*timeutil.Schedule
   320  		errStr   string
   321  	}{
   322  		// invalid
   323  		{"", nil, `cannot parse "": not a valid fragment`},
   324  		{"invalid-11:00", nil, `cannot parse "invalid-11:00": not a valid time`},
   325  		{"9:00-11:00/invalid", nil, `cannot parse "9:00-11:00/invalid": not a valid interval`},
   326  		{"9:00-11:00/0", nil, `cannot parse "9:00-11:00/0": not a valid interval`},
   327  		{"09:00-25:00", nil, `cannot parse "09:00-25:00": not a valid time`},
   328  		{"09:00-24:30", nil, `cannot parse "09:00-24:30": not a valid time`},
   329  		{"mon-01:00", nil, `cannot parse "mon-01:00": not a valid time`},
   330  		{"9:00-mon@11:00", nil, `cannot parse "9:00-mon@11:00": not a valid time`},
   331  		{"9:00,mon", nil, `cannot parse "mon": invalid schedule fragment`},
   332  		{"mon~wed", nil, `cannot parse "mon~wed": "mon~wed" is not a valid weekday`},
   333  		{"mon--wed", nil, `cannot parse "mon--wed": invalid week span`},
   334  		{"mon-wed/2,,9:00", nil, `cannot parse "mon-wed/2": "wed/2" is not a valid weekday`},
   335  		{"mon..wed", nil, `cannot parse "mon..wed": "mon..wed" is not a valid weekday`},
   336  		{"mon9,9:00", nil, `cannot parse "mon9": "mon9" is not a valid weekday`},
   337  		{"mon0,9:00", nil, `cannot parse "mon0": "mon0" is not a valid weekday`},
   338  		{"mon5-mon1,9:00", nil, `cannot parse "mon5-mon1": unsupported schedule`},
   339  		{"mon-mon2,9:00", nil, `cannot parse "mon-mon2": week number must be present for both weekdays or neither`},
   340  		{"mon%,9:00", nil, `cannot parse "mon%": "mon%" is not a valid weekday`},
   341  		{"foo2,9:00", nil, `cannot parse "foo2": "foo2" is not a valid weekday`},
   342  		{"9:00---11:00", nil, `cannot parse "9:00---11:00": not a valid time`},
   343  		{"9:00-11:00/3/3/3", nil, `cannot parse "9:00-11:00/3/3/3": not a valid interval`},
   344  		{"9:00-11:00///3", nil, `cannot parse "9:00-11:00///3": not a valid interval`},
   345  		{"9:00-9:00-10:00/3", nil, `cannot parse "9:00-9:00-10:00/3": not a valid time`},
   346  		{"9:00,,,9:00-10:00/3", nil, `cannot parse ",9:00-10:00/3": not a valid fragment`},
   347  		{",,,", nil, `cannot parse "": not a valid fragment`},
   348  		{",,", nil, `cannot parse "": not a valid fragment`},
   349  		{":", nil, `cannot parse ":": not a valid time`},
   350  		{"-", nil, `cannot parse "-": "" is not a valid weekday`},
   351  		{"-/4", nil, `cannot parse "-/4": "" is not a valid weekday`},
   352  		{"~/4", nil, `cannot parse "~/4": "~/4" is not a valid weekday`},
   353  		// valid
   354  		{
   355  			in: "9:00-11:00",
   356  			expected: []*timeutil.Schedule{{
   357  				ClockSpans: []timeutil.ClockSpan{
   358  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}}}}},
   359  		}, {
   360  			in: "9:00-11:00/2",
   361  			expected: []*timeutil.Schedule{{
   362  				ClockSpans: []timeutil.ClockSpan{
   363  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}, Split: 2}}}},
   364  		}, {
   365  			in: "mon,9:00-11:00",
   366  			expected: []*timeutil.Schedule{{
   367  				ClockSpans: []timeutil.ClockSpan{
   368  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}}},
   369  				WeekSpans: []timeutil.WeekSpan{
   370  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Monday}}},
   371  			}},
   372  		}, {
   373  			in: "fri,mon,9:00-11:00",
   374  			expected: []*timeutil.Schedule{{
   375  				ClockSpans: []timeutil.ClockSpan{
   376  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}}},
   377  				WeekSpans: []timeutil.WeekSpan{
   378  					{Start: timeutil.Week{Weekday: time.Friday}, End: timeutil.Week{Weekday: time.Friday}},
   379  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Monday}}},
   380  			}},
   381  		}, {
   382  			in: "9:00-11:00,,20:00-22:00",
   383  			expected: []*timeutil.Schedule{{
   384  				ClockSpans: []timeutil.ClockSpan{
   385  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}}},
   386  			}, {
   387  				ClockSpans: []timeutil.ClockSpan{
   388  					{Start: timeutil.Clock{Hour: 20}, End: timeutil.Clock{Hour: 22}}}},
   389  			},
   390  		}, {
   391  			in: "mon,9:00-11:00,,wed,22:00-23:00",
   392  			expected: []*timeutil.Schedule{{
   393  				ClockSpans: []timeutil.ClockSpan{
   394  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}}},
   395  				WeekSpans: []timeutil.WeekSpan{
   396  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Monday}}},
   397  			}, {
   398  				ClockSpans: []timeutil.ClockSpan{
   399  					{Start: timeutil.Clock{Hour: 22}, End: timeutil.Clock{Hour: 23}}},
   400  				WeekSpans: []timeutil.WeekSpan{
   401  					{Start: timeutil.Week{Weekday: time.Wednesday}, End: timeutil.Week{Weekday: time.Wednesday}}},
   402  			}},
   403  		}, {
   404  			in: "mon,9:00,10:00,14:00,15:00",
   405  			expected: []*timeutil.Schedule{{
   406  				ClockSpans: []timeutil.ClockSpan{
   407  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 9}},
   408  					{Start: timeutil.Clock{Hour: 10}, End: timeutil.Clock{Hour: 10}},
   409  					{Start: timeutil.Clock{Hour: 14}, End: timeutil.Clock{Hour: 14}},
   410  					{Start: timeutil.Clock{Hour: 15}, End: timeutil.Clock{Hour: 15}}},
   411  				WeekSpans: []timeutil.WeekSpan{
   412  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Monday}}},
   413  			}},
   414  		}, {
   415  			in: "mon,wed",
   416  			expected: []*timeutil.Schedule{{
   417  				WeekSpans: []timeutil.WeekSpan{
   418  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Monday}},
   419  					{Start: timeutil.Week{Weekday: time.Wednesday}, End: timeutil.Week{Weekday: time.Wednesday}}},
   420  			}},
   421  		}, {
   422  			// same as above
   423  			in: "mon,,wed",
   424  			expected: []*timeutil.Schedule{{
   425  				WeekSpans: []timeutil.WeekSpan{
   426  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Monday}}},
   427  			}, {
   428  				WeekSpans: []timeutil.WeekSpan{
   429  					{Start: timeutil.Week{Weekday: time.Wednesday}, End: timeutil.Week{Weekday: time.Wednesday}}}},
   430  			},
   431  		}, {
   432  			// but not the same as this one
   433  			in: "mon-wed",
   434  			expected: []*timeutil.Schedule{{
   435  				WeekSpans: []timeutil.WeekSpan{
   436  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Wednesday}}},
   437  			}},
   438  		}, {
   439  			in: "mon-wed,fri,9:00-11:00/2",
   440  			expected: []*timeutil.Schedule{{
   441  				ClockSpans: []timeutil.ClockSpan{
   442  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}, Split: 2},
   443  				},
   444  				WeekSpans: []timeutil.WeekSpan{
   445  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Wednesday}},
   446  					{Start: timeutil.Week{Weekday: time.Friday}, End: timeutil.Week{Weekday: time.Friday}},
   447  				},
   448  			}},
   449  		}, {
   450  			in: "9:00~11:00",
   451  			expected: []*timeutil.Schedule{{
   452  				ClockSpans: []timeutil.ClockSpan{
   453  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}, Spread: true}},
   454  			}},
   455  		}, {
   456  			in: "9:00",
   457  			expected: []*timeutil.Schedule{{
   458  				ClockSpans: []timeutil.ClockSpan{
   459  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 9}}},
   460  			}},
   461  		}, {
   462  			in: "mon1,9:00",
   463  			expected: []*timeutil.Schedule{{
   464  				ClockSpans: []timeutil.ClockSpan{
   465  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 9}}},
   466  				WeekSpans: []timeutil.WeekSpan{
   467  					{Start: timeutil.Week{Weekday: time.Monday, Pos: 1}, End: timeutil.Week{Weekday: time.Monday, Pos: 1}}},
   468  			}},
   469  		}, {
   470  			in: "00:00-24:00",
   471  			expected: []*timeutil.Schedule{{
   472  				ClockSpans: []timeutil.ClockSpan{
   473  					{Start: timeutil.Clock{Hour: 0}, End: timeutil.Clock{Hour: 24}}},
   474  			}},
   475  		}, {
   476  			in: "23:00-01:00",
   477  			expected: []*timeutil.Schedule{{
   478  				ClockSpans: []timeutil.ClockSpan{
   479  					{Start: timeutil.Clock{Hour: 23}, End: timeutil.Clock{Hour: 1}},
   480  				},
   481  			}},
   482  		}, {
   483  			in: "fri-mon",
   484  			expected: []*timeutil.Schedule{{
   485  				WeekSpans: []timeutil.WeekSpan{
   486  					{Start: timeutil.Week{Weekday: time.Friday}, End: timeutil.Week{Weekday: time.Monday}}},
   487  			}},
   488  		},
   489  	} {
   490  		c.Logf("trying %+v", t)
   491  		schedule, err := timeutil.ParseSchedule(t.in)
   492  		if t.errStr != "" {
   493  			c.Check(err, ErrorMatches, t.errStr, Commentf("%q returned unexpected error: %s", t.in, err))
   494  		} else {
   495  			c.Check(err, IsNil, Commentf("%q returned error: %s", t.in, err))
   496  			c.Check(schedule, DeepEquals, t.expected, Commentf("%q failed", t.in))
   497  		}
   498  	}
   499  }
   500  
   501  func (ts *timeutilSuite) TestScheduleNext(c *C) {
   502  	const shortForm = "2006-01-02 15:04"
   503  
   504  	for _, t := range []struct {
   505  		schedule   string
   506  		last       string
   507  		now        string
   508  		next       string
   509  		randomized bool
   510  	}{
   511  		{
   512  			schedule: "mon,10:00,,fri,15:00",
   513  			// sun 22:00
   514  			last: "2017-02-05 22:00",
   515  			// mon 9:00
   516  			now:  "2017-02-06 9:00",
   517  			next: "1h-1h",
   518  		}, {
   519  			// first monday of the month, at 10:00
   520  			schedule: "mon1,10:00",
   521  			// Sun 22:00
   522  			last: "2017-02-05 22:00",
   523  			// Mon 9:00
   524  			now:  "2017-02-06 9:00",
   525  			next: "1h-1h",
   526  		}, {
   527  			// first Monday of the month, at 10:00
   528  			schedule: "mon1,10:00",
   529  			// first Monday of the month, 10:00
   530  			last: "2017-02-06 10:00",
   531  			// first Monday of the month, 11:00, right after
   532  			// 'previous first Monday' run
   533  			now: "2017-02-06 11:00",
   534  			// expecting March, 6th, 10:00, 27 days and 23 hours
   535  			// from now
   536  			next: "671h-671h",
   537  		}, {
   538  			// second Monday of the month, at 10:00
   539  			schedule: "mon2,10:00",
   540  			// first Monday of the month, 10:00
   541  			last: "2017-02-06 10:00",
   542  			// first Monday of the month, 11:00, right after
   543  			// 'previous first Monday' run
   544  			now: "2017-02-06 11:00",
   545  			// expecting February, 13, 10:00, 6 days and 23 hours
   546  			// from now
   547  			next: "167h-167h",
   548  		}, {
   549  			// last Monday of the month, at 10:00
   550  			schedule: "mon5,10:00",
   551  			// first Monday of the month, 10:00
   552  			last: "2017-02-06 10:00",
   553  			// first Monday of the month, 11:00, right after
   554  			// 'previous first Monday' run
   555  			now: "2017-02-06 11:00",
   556  			// expecting February, 27th, 10:00, 20 days and 23 hours
   557  			// from now
   558  			next: "503h-503h",
   559  		}, {
   560  			// from the first Monday of the month to the second Tuesday of
   561  			// the month, at 10:00
   562  			schedule: "mon1-tue2,10:00",
   563  			// Monday, 10:00
   564  			last: "2017-02-06 10:00",
   565  			// Tuesday, the day after the first Monday of the month
   566  			now: "2017-02-07 11:00",
   567  			// expecting to run the next day at 10:00
   568  			next: "23h-23h",
   569  		}, {
   570  			// from the first Monday of the month to the second Tuesday of
   571  			// the month, at 10:00
   572  			schedule: "mon1-tue2,10:00",
   573  			last:     "2017-02-01 10:00",
   574  			// Sunday, 10:00
   575  			now: "2017-02-05 10:00",
   576  			// expecting to run the next day at 10:00
   577  			next: "24h-24h",
   578  		}, {
   579  			// from the first Monday of the month to the second Tuesday of
   580  			// the month, at 10:00
   581  			schedule: "mon1-tue2,10:00",
   582  			// Tuesday, 10:00
   583  			last: "2017-02-14 22:00",
   584  			// Thursday, 10:00
   585  			now: "2017-02-16 10:00",
   586  			// expecting to run in 18 days
   587  			next: "432h-432h",
   588  		}, {
   589  			// from the first Monday of the month to the second Tuesday of
   590  			// the month, at 10:00
   591  			schedule: "mon1-tue2,10:00",
   592  			// Sunday, 22:00
   593  			last: "2017-02-05 22:00",
   594  			// first Monday of the month
   595  			now: "2017-02-06 11:00",
   596  			// expecting to run the next day at 10:00
   597  			next: "23h-23h",
   598  		}, {
   599  			// from the first Monday of the month to the second Tuesday of
   600  			// the month, at 10:00
   601  			schedule: "mon1-tue2,10:00-12:00",
   602  			// Sunday, 22:00
   603  			last: "2017-02-05 22:00",
   604  			// first Monday of the month, within the update window
   605  			now: "2017-02-06 11:00",
   606  			// expecting to run now
   607  			next: "0h-0h",
   608  		}, {
   609  			// from the first Monday of the month to the second Tuesday of
   610  			// the month, at 10:00
   611  			schedule: "mon1-tue2,10:00~12:00",
   612  			// Sunday, 22:00
   613  			last: "2017-02-05 22:00",
   614  			// first Monday of the month, within the update window
   615  			now: "2017-02-06 11:00",
   616  			// expecting to run now
   617  			next: "0h-1h",
   618  			// since we're in update window we'll run now regardless
   619  			// of 'spreading'
   620  			randomized: false,
   621  		}, {
   622  			schedule:   "mon,10:00~12:00,,fri,15:00",
   623  			last:       "2017-02-05 22:00",
   624  			now:        "2017-02-06 9:00",
   625  			next:       "1h-3h",
   626  			randomized: true,
   627  		}, {
   628  			schedule: "mon,10:00-12:00,,fri,15:00",
   629  			last:     "2017-02-06 12:00",
   630  			// tue 12:00
   631  			now: "2017-02-07 12:00",
   632  			// 3 days and 3 hours from now
   633  			next: "75h-75h",
   634  		}, {
   635  			// randomized between 10:00 and 12:00
   636  			schedule: "mon,10:00~12:00",
   637  			// sun 22:00
   638  			last: "2017-02-05 22:00",
   639  			// mon 9:00
   640  			now:        "2017-02-06 9:00",
   641  			next:       "1h-3h",
   642  			randomized: true,
   643  		}, {
   644  			// Friday to Monday, 10am
   645  			schedule: "fri-mon,10:00",
   646  			// sun 22:00
   647  			last: "2017-02-05 22:00",
   648  			// mon 9:00
   649  			now:  "2017-02-06 9:00",
   650  			next: "1h-1h",
   651  		}, {
   652  			// Friday to Monday, 10am
   653  			schedule: "fri-mon,10:00",
   654  			// mon 10:00
   655  			last: "2017-02-06 10:00",
   656  			// mon 10:00
   657  			now: "2017-02-06 10:00",
   658  			// 4 days from now
   659  			next: "96h-96h",
   660  		}, {
   661  			// Wednesday to Friday, 10am
   662  			schedule: "wed-fri,10:00",
   663  			// mon 10:00
   664  			last: "2017-02-06 10:00",
   665  			// mon 10:00
   666  			now: "2017-02-06 10:00",
   667  			// 2 days from now
   668  			next: "48h-48h",
   669  		}, {
   670  			// randomized, once a day
   671  			schedule: "0:00~24:00",
   672  			// sun 22:00
   673  			last: "2017-02-05 22:00",
   674  			// mon 9:00
   675  			now:        "2017-02-05 23:00",
   676  			next:       "1h-25h",
   677  			randomized: true,
   678  		}, {
   679  			// randomized, once a day
   680  			schedule: "0:00~24:00",
   681  			// mon 10:00
   682  			last: "2017-02-06 10:00",
   683  			// mon 11:00
   684  			now: "2017-02-06 11:00",
   685  			// sometime the next day
   686  			next:       "13h-37h",
   687  			randomized: true,
   688  		}, {
   689  			// during the night, 23:00-1:00
   690  			schedule: "23:00~1:00",
   691  			// mon 10:00
   692  			last: "2017-02-06 10:00",
   693  			// mon 11:00
   694  			now: "2017-02-06 22:00",
   695  			// sometime over the night
   696  			next:       "1h-3h",
   697  			randomized: true,
   698  		}, {
   699  			// during the night, 23:00-1:00
   700  			schedule: "23:00~1:00",
   701  			// Mon 23:00
   702  			last: "2017-02-06 23:00",
   703  			// Tue 0:00
   704  			now: "2017-02-07 00:00",
   705  			// sometime over the night
   706  			next:       "23h-25h",
   707  			randomized: true,
   708  		}, {
   709  			// twice between 9am and 11am
   710  			schedule: "9:00-11:00/2",
   711  			// last attempt at the beginning of window
   712  			last: "2017-02-06 9:00",
   713  			// sometime between 10am and 11am
   714  			now:  "2017-02-06 9:30",
   715  			next: "30m-90m",
   716  		}, {
   717  			// 2 ranges
   718  			schedule: "9:00-10:00,10:00-11:00",
   719  			// last attempt at the beginning of window
   720  			last: "2017-02-06 9:01",
   721  			// next one at 10am
   722  			now:  "2017-02-06 9:30",
   723  			next: "30m-30m",
   724  		}, {
   725  			// twice, at 9am and at 2pm
   726  			schedule: "9:00,14:00",
   727  			// last right after scheduled time window
   728  			last: "2017-02-06 9:01",
   729  			// next one at 2pm
   730  			now:  "2017-02-06 9:30",
   731  			next: "270m-270m",
   732  		}, {
   733  			// 2 ranges, reversed order in spec
   734  			schedule: "10:00~11:00,9:00-10:00",
   735  			// last attempt at the beginning of window
   736  			last: "2017-02-06 9:01",
   737  			// sometime between 10am and 11am
   738  			now:        "2017-02-06 9:30",
   739  			next:       "30m-90m",
   740  			randomized: true,
   741  		}, {
   742  			// first Wednesday at 13:00
   743  			schedule: "wed1,13:00",
   744  			now:      "2018-07-30 9:00",
   745  			// yesterday
   746  			last: "2018-07-29 13:00",
   747  			// next one on 2018-08-01 13:00
   748  			next: "52h-52h",
   749  		},
   750  	} {
   751  		c.Logf("trying %+v", t)
   752  
   753  		last, err := time.ParseInLocation(shortForm, t.last, time.Local)
   754  		c.Assert(err, IsNil)
   755  
   756  		fakeNow, err := time.ParseInLocation(shortForm, t.now, time.Local)
   757  		c.Assert(err, IsNil)
   758  		restorer := timeutil.MockTimeNow(func() time.Time {
   759  			return fakeNow
   760  		})
   761  		defer restorer()
   762  
   763  		sched, err := timeutil.ParseSchedule(t.schedule)
   764  		c.Assert(err, IsNil)
   765  
   766  		// keep track of previous result for tests where event time is
   767  		// randomized
   768  		previous := time.Duration(0)
   769  		calls := 2
   770  
   771  		for i := 0; i < calls; i++ {
   772  			next := timeutil.Next(sched, last, maxDuration)
   773  			if t.randomized {
   774  				c.Check(next, Not(Equals), previous)
   775  			} else if previous != 0 {
   776  				// not randomized and not the first run
   777  				c.Check(next, Equals, previous)
   778  			}
   779  
   780  			c.Logf("next: %v", next)
   781  			minDist, maxDist := parse(c, t.next)
   782  
   783  			c.Check(next >= minDist && next <= maxDist,
   784  				Equals, true,
   785  				Commentf("invalid  distance for schedule %q with last refresh %q, now %q, expected %v, got %v, date %s",
   786  					t.schedule, t.last, t.now, t.next, next, fakeNow.Add(next)))
   787  			previous = next
   788  		}
   789  	}
   790  }
   791  
   792  func (ts *timeutilSuite) TestScheduleIncludes(c *C) {
   793  	const shortForm = "2006-01-02 15:04:05"
   794  
   795  	for _, t := range []struct {
   796  		schedule  string
   797  		now       string
   798  		expecting bool
   799  	}{
   800  		{
   801  			schedule: "mon,10:00,,fri,15:00",
   802  			// mon 9:00
   803  			now:       "2017-02-06 9:00:00",
   804  			expecting: false,
   805  		}, {
   806  			// first monday of the month, at 10:00
   807  			schedule: "mon1,10:00",
   808  			// Mon 10:00:00
   809  			now:       "2017-02-06 10:00:00",
   810  			expecting: true,
   811  		}, {
   812  			// first monday of the month, at 10:00
   813  			schedule: "mon1,10:00",
   814  			// Mon 10:00:45
   815  			now:       "2017-02-06 10:00:45",
   816  			expecting: true,
   817  		}, {
   818  			// first monday of the month, at 10:00
   819  			schedule: "mon1,10:00",
   820  			// Mon 10:01
   821  			now:       "2017-02-06 10:01:00",
   822  			expecting: false,
   823  		}, {
   824  			// last Monday of the month, at 10:00
   825  			schedule: "mon5,10:00-11:00",
   826  			// first Monday of the month, 11:00, right after
   827  			// 'previous first Monday' run
   828  			now:       "2017-02-27 10:59:20",
   829  			expecting: true,
   830  		}, {
   831  			// from first Monday of the month to the second Tuesday of
   832  			// the month, at 10:00 to 12:00
   833  			schedule: "mon1-tue2,10:00-12:00",
   834  			// Thursday, 11:10
   835  			now:       "2017-02-09 11:10:00",
   836  			expecting: true,
   837  		}, {
   838  			// from first Monday of the month to the second Tuesday of
   839  			// the month, at 10:00 to 12:00
   840  			schedule: "mon1-tue2,10:00~12:00",
   841  			// Thursday, 11:10
   842  			now:       "2017-02-02 11:10:00",
   843  			expecting: false,
   844  		}, {
   845  			// from first Monday of the month to the second Tuesday of
   846  			// the month, at 10:00 to 12:00
   847  			schedule: "mon1-tue2,10:00~12:00",
   848  			// Monday, 11:10
   849  			now:       "2017-02-06 11:10:00",
   850  			expecting: true,
   851  		}, {
   852  			// from first Monday of the month to the second Tuesday of
   853  			// the month, at 10:00 to 12:00
   854  			schedule: "mon1-tue2,10:00~12:00",
   855  			// Thursday, 11:10
   856  			now:       "2017-02-16 11:10:00",
   857  			expecting: false,
   858  		}, {
   859  			// from first Monday of the month to the second Tuesday of
   860  			// the month, at 10:00 to 12:00
   861  			schedule: "mon1-tue2,10:00~12:00",
   862  			// Thursday, 11:10
   863  			now:       "2017-02-16 11:10:00",
   864  			expecting: false,
   865  		}, {
   866  			// from first Monday of the month to the second Tuesday of
   867  			// the month, at 10:00 to 12:00
   868  			schedule: "mon1-tue2,10:00~12:00",
   869  			// Thursday, 11:10
   870  			now:       "2017-02-09 11:10:00",
   871  			expecting: true,
   872  		}, {
   873  			// from first Tuesday of the month to the second Monday of
   874  			// the month, at 10:00 to 12:00
   875  			schedule: "tue1-mon2,10:00~12:00",
   876  			// Thursday, 11:10
   877  			now:       "2017-02-09 11:10:00",
   878  			expecting: true,
   879  		}, {
   880  			// from 4th Monday of the month to the last Wednesday of
   881  			// the month, at 10:00 to 12:00
   882  			schedule: "mon4-wed5,10:00~12:00",
   883  			// Schedule ends up being Feb 20 - Feb 22 2017
   884  			now:       "2017-03-01 11:10:00",
   885  			expecting: false,
   886  		}, {
   887  			// from 4th Monday of the month to the last Wednesday of
   888  			// the month, at 10:00 to 12:00
   889  			schedule: "mon4-wed5,10:00~12:00",
   890  			// Schedule ends up being Feb 20 - Feb 22 2017
   891  			now:       "2017-02-23 11:10:00",
   892  			expecting: false,
   893  		}, {
   894  			// from last Monday of the month to the second Tuesday of
   895  			// the month, at 10:00
   896  			schedule: "mon1-tue2,10:00~12:00",
   897  			// Sunday, 11:10
   898  			now:       "2017-02-05 11:10:00",
   899  			expecting: false,
   900  		}, {
   901  			// twice between 9am and 11am
   902  			schedule:  "9:00-11:00/2",
   903  			now:       "2017-02-06 10:30:00",
   904  			expecting: true,
   905  		}, {
   906  			schedule:  "9:00-10:00,10:00-11:00",
   907  			now:       "2017-02-06 10:30:00",
   908  			expecting: true,
   909  		}, {
   910  			// every day, 23:59
   911  			schedule:  "23:59",
   912  			now:       "2017-02-06 23:59:59",
   913  			expecting: true,
   914  		}, {
   915  			// 2 ranges, reversed order in spec
   916  			schedule: "10:00~11:00,9:00-10:00",
   917  			// sometime between 10am and 11am
   918  			now:       "2017-02-06 9:30:00",
   919  			expecting: true,
   920  		},
   921  	} {
   922  		c.Logf("trying %+v", t)
   923  
   924  		now, err := time.ParseInLocation(shortForm, t.now, time.Local)
   925  		c.Assert(err, IsNil)
   926  
   927  		sched, err := timeutil.ParseSchedule(t.schedule)
   928  		c.Assert(err, IsNil)
   929  
   930  		c.Check(timeutil.Includes(sched, now), Equals, t.expecting,
   931  			Commentf("unexpected result for schedule %v and time %v", t.schedule, now))
   932  	}
   933  }
   934  
   935  func (ts *timeutilSuite) TestClockSpans(c *C) {
   936  	const shortForm = "2006-01-02 15:04:05"
   937  
   938  	for _, t := range []struct {
   939  		clockspan  string
   940  		flattenend []string
   941  	}{
   942  		{
   943  			clockspan:  "23:00-01:00/2",
   944  			flattenend: []string{"23:00-00:00", "00:00-01:00"},
   945  		}, {
   946  			clockspan:  "23:00-01:00/4",
   947  			flattenend: []string{"23:00-23:30", "23:30-00:00", "00:00-00:30", "00:30-01:00"},
   948  		},
   949  	} {
   950  		c.Logf("trying %+v", t)
   951  		spans, err := timeutil.ParseClockSpan(t.clockspan)
   952  		c.Assert(err, IsNil)
   953  
   954  		spanStrings := make([]string, len(t.flattenend))
   955  		flattened := spans.ClockSpans()
   956  		c.Assert(flattened, HasLen, len(t.flattenend))
   957  		for i := range flattened {
   958  			spanStrings[i] = flattened[i].String()
   959  		}
   960  
   961  		c.Assert(spanStrings, DeepEquals, t.flattenend)
   962  	}
   963  }
   964  
   965  func (ts *timeutilSuite) TestWeekSpans(c *C) {
   966  	const shortForm = "2006-01-02"
   967  
   968  	//     July 2018            August 2018
   969  	// Su Mo Tu We Th Fr Sa  Su Mo Tu We Th Fr Sa
   970  	//  1  2  3  4  5  6  7            1  2  3  4
   971  	//  8  9 10 11 12 13 14   5  6  7  8  9 10 11
   972  	// 15 16 17 18 19 20 21  12 13 14 15 16 17 18
   973  	// 22 23 24 25 26 27 28  19 20 21 22 23 24 25
   974  	// 29 30 31              26 27 28 29 30 31
   975  
   976  	for _, t := range []struct {
   977  		week  string
   978  		when  string
   979  		match bool
   980  	}{
   981  		{
   982  			// first Wednesday
   983  			week:  "wed1",
   984  			when:  "2018-08-01",
   985  			match: true,
   986  		}, {
   987  			// first Wednesday
   988  			week: "wed1",
   989  			// actually 2nd Wednesday
   990  			when:  "2018-08-08",
   991  			match: false,
   992  		}, {
   993  			// second Wednesday
   994  			week:  "wed2",
   995  			when:  "2018-08-08",
   996  			match: true,
   997  		}, {
   998  			// first Tuesday
   999  			week:  "tue1",
  1000  			when:  "2018-08-07",
  1001  			match: true,
  1002  		}, {
  1003  			// first Sunday
  1004  			week:  "sun1",
  1005  			when:  "2018-07-01",
  1006  			match: true,
  1007  		}, {
  1008  			// last Tuesday
  1009  			week:  "tue5",
  1010  			when:  "2018-07-31",
  1011  			match: true,
  1012  		}, {
  1013  			// last Tuesday
  1014  			week:  "tue5",
  1015  			when:  "2018-07-24",
  1016  			match: false,
  1017  		}, {
  1018  			// last Thursday
  1019  			week:  "thu5",
  1020  			when:  "2018-07-26",
  1021  			match: true,
  1022  		},
  1023  	} {
  1024  		c.Logf("trying %+v", t)
  1025  		ws, err := timeutil.ParseWeekSpan(t.week)
  1026  		c.Assert(err, IsNil)
  1027  
  1028  		when, err := time.ParseInLocation(shortForm, t.when, time.Local)
  1029  		c.Assert(err, IsNil)
  1030  		c.Logf("when: %v %s", when, when.Weekday())
  1031  
  1032  		c.Check(ws.Match(when), Equals, t.match)
  1033  	}
  1034  }