github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/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%,9:00", nil, `cannot parse "mon%": "mon%" is not a valid weekday`},
   340  		{"foo2,9:00", nil, `cannot parse "foo2": "foo2" is not a valid weekday`},
   341  		{"9:00---11:00", nil, `cannot parse "9:00---11:00": not a valid time`},
   342  		{"9:00-11:00/3/3/3", nil, `cannot parse "9:00-11:00/3/3/3": not a valid interval`},
   343  		{"9:00-11:00///3", nil, `cannot parse "9:00-11:00///3": not a valid interval`},
   344  		{"9:00-9:00-10:00/3", nil, `cannot parse "9:00-9:00-10:00/3": not a valid time`},
   345  		{"9:00,,,9:00-10:00/3", nil, `cannot parse ",9:00-10:00/3": not a valid fragment`},
   346  		{",,,", nil, `cannot parse "": not a valid fragment`},
   347  		{",,", nil, `cannot parse "": not a valid fragment`},
   348  		{":", nil, `cannot parse ":": not a valid time`},
   349  		{"-", nil, `cannot parse "-": "" is not a valid weekday`},
   350  		{"-/4", nil, `cannot parse "-/4": "" is not a valid weekday`},
   351  		{"~/4", nil, `cannot parse "~/4": "~/4" is not a valid weekday`},
   352  		// valid
   353  		{
   354  			in: "9:00-11:00",
   355  			expected: []*timeutil.Schedule{{
   356  				ClockSpans: []timeutil.ClockSpan{
   357  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}}}}},
   358  		}, {
   359  			in: "9:00-11:00/2",
   360  			expected: []*timeutil.Schedule{{
   361  				ClockSpans: []timeutil.ClockSpan{
   362  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}, Split: 2}}}},
   363  		}, {
   364  			in: "mon,9:00-11:00",
   365  			expected: []*timeutil.Schedule{{
   366  				ClockSpans: []timeutil.ClockSpan{
   367  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}}},
   368  				WeekSpans: []timeutil.WeekSpan{
   369  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Monday}}},
   370  			}},
   371  		}, {
   372  			in: "fri,mon,9:00-11:00",
   373  			expected: []*timeutil.Schedule{{
   374  				ClockSpans: []timeutil.ClockSpan{
   375  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}}},
   376  				WeekSpans: []timeutil.WeekSpan{
   377  					{Start: timeutil.Week{Weekday: time.Friday}, End: timeutil.Week{Weekday: time.Friday}},
   378  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Monday}}},
   379  			}},
   380  		}, {
   381  			in: "9:00-11:00,,20:00-22:00",
   382  			expected: []*timeutil.Schedule{{
   383  				ClockSpans: []timeutil.ClockSpan{
   384  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}}},
   385  			}, {
   386  				ClockSpans: []timeutil.ClockSpan{
   387  					{Start: timeutil.Clock{Hour: 20}, End: timeutil.Clock{Hour: 22}}}},
   388  			},
   389  		}, {
   390  			in: "mon,9:00-11:00,,wed,22:00-23:00",
   391  			expected: []*timeutil.Schedule{{
   392  				ClockSpans: []timeutil.ClockSpan{
   393  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}}},
   394  				WeekSpans: []timeutil.WeekSpan{
   395  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Monday}}},
   396  			}, {
   397  				ClockSpans: []timeutil.ClockSpan{
   398  					{Start: timeutil.Clock{Hour: 22}, End: timeutil.Clock{Hour: 23}}},
   399  				WeekSpans: []timeutil.WeekSpan{
   400  					{Start: timeutil.Week{Weekday: time.Wednesday}, End: timeutil.Week{Weekday: time.Wednesday}}},
   401  			}},
   402  		}, {
   403  			in: "mon,9:00,10:00,14:00,15:00",
   404  			expected: []*timeutil.Schedule{{
   405  				ClockSpans: []timeutil.ClockSpan{
   406  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 9}},
   407  					{Start: timeutil.Clock{Hour: 10}, End: timeutil.Clock{Hour: 10}},
   408  					{Start: timeutil.Clock{Hour: 14}, End: timeutil.Clock{Hour: 14}},
   409  					{Start: timeutil.Clock{Hour: 15}, End: timeutil.Clock{Hour: 15}}},
   410  				WeekSpans: []timeutil.WeekSpan{
   411  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Monday}}},
   412  			}},
   413  		}, {
   414  			in: "mon,wed",
   415  			expected: []*timeutil.Schedule{{
   416  				WeekSpans: []timeutil.WeekSpan{
   417  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Monday}},
   418  					{Start: timeutil.Week{Weekday: time.Wednesday}, End: timeutil.Week{Weekday: time.Wednesday}}},
   419  			}},
   420  		}, {
   421  			// same as above
   422  			in: "mon,,wed",
   423  			expected: []*timeutil.Schedule{{
   424  				WeekSpans: []timeutil.WeekSpan{
   425  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Monday}}},
   426  			}, {
   427  				WeekSpans: []timeutil.WeekSpan{
   428  					{Start: timeutil.Week{Weekday: time.Wednesday}, End: timeutil.Week{Weekday: time.Wednesday}}}},
   429  			},
   430  		}, {
   431  			// but not the same as this one
   432  			in: "mon-wed",
   433  			expected: []*timeutil.Schedule{{
   434  				WeekSpans: []timeutil.WeekSpan{
   435  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Wednesday}}},
   436  			}},
   437  		}, {
   438  			in: "mon-wed,fri,9:00-11:00/2",
   439  			expected: []*timeutil.Schedule{{
   440  				ClockSpans: []timeutil.ClockSpan{
   441  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}, Split: 2},
   442  				},
   443  				WeekSpans: []timeutil.WeekSpan{
   444  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Wednesday}},
   445  					{Start: timeutil.Week{Weekday: time.Friday}, End: timeutil.Week{Weekday: time.Friday}},
   446  				},
   447  			}},
   448  		}, {
   449  			in: "9:00~11:00",
   450  			expected: []*timeutil.Schedule{{
   451  				ClockSpans: []timeutil.ClockSpan{
   452  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 11}, Spread: true}},
   453  			}},
   454  		}, {
   455  			in: "9:00",
   456  			expected: []*timeutil.Schedule{{
   457  				ClockSpans: []timeutil.ClockSpan{
   458  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 9}}},
   459  			}},
   460  		}, {
   461  			in: "mon1,9:00",
   462  			expected: []*timeutil.Schedule{{
   463  				ClockSpans: []timeutil.ClockSpan{
   464  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 9}}},
   465  				WeekSpans: []timeutil.WeekSpan{
   466  					{Start: timeutil.Week{Weekday: time.Monday, Pos: 1}, End: timeutil.Week{Weekday: time.Monday, Pos: 1}}},
   467  			}},
   468  		}, {
   469  			in: "00:00-24:00",
   470  			expected: []*timeutil.Schedule{{
   471  				ClockSpans: []timeutil.ClockSpan{
   472  					{Start: timeutil.Clock{Hour: 0}, End: timeutil.Clock{Hour: 24}}},
   473  			}},
   474  		}, {
   475  			in: "23:00-01:00",
   476  			expected: []*timeutil.Schedule{{
   477  				ClockSpans: []timeutil.ClockSpan{
   478  					{Start: timeutil.Clock{Hour: 23}, End: timeutil.Clock{Hour: 1}},
   479  				},
   480  			}},
   481  		}, {
   482  			in: "fri-mon",
   483  			expected: []*timeutil.Schedule{{
   484  				WeekSpans: []timeutil.WeekSpan{
   485  					{Start: timeutil.Week{Weekday: time.Friday}, End: timeutil.Week{Weekday: time.Monday}}},
   486  			}},
   487  		}, {
   488  			in: "mon-mon2,9:00",
   489  			expected: []*timeutil.Schedule{{
   490  				ClockSpans: []timeutil.ClockSpan{
   491  					{Start: timeutil.Clock{Hour: 9}, End: timeutil.Clock{Hour: 9}}},
   492  				WeekSpans: []timeutil.WeekSpan{
   493  					{Start: timeutil.Week{Weekday: time.Monday}, End: timeutil.Week{Weekday: time.Monday, Pos: 2}}},
   494  			}},
   495  		},
   496  	} {
   497  		c.Logf("trying %+v", t)
   498  		schedule, err := timeutil.ParseSchedule(t.in)
   499  		if t.errStr != "" {
   500  			c.Check(err, ErrorMatches, t.errStr, Commentf("%q returned unexpected error: %s", t.in, err))
   501  		} else {
   502  			c.Check(err, IsNil, Commentf("%q returned error: %s", t.in, err))
   503  			c.Check(schedule, DeepEquals, t.expected, Commentf("%q failed", t.in))
   504  		}
   505  	}
   506  }
   507  
   508  func (ts *timeutilSuite) TestScheduleNext(c *C) {
   509  	const shortForm = "2006-01-02 15:04"
   510  
   511  	for _, t := range []struct {
   512  		schedule   string
   513  		last       string
   514  		now        string
   515  		next       string
   516  		randomized bool
   517  	}{
   518  		{
   519  			schedule: "mon,10:00,,fri,15:00",
   520  			// sun 22:00
   521  			last: "2017-02-05 22:00",
   522  			// mon 9:00
   523  			now:  "2017-02-06 9:00",
   524  			next: "1h-1h",
   525  		}, {
   526  			// first monday of the month, at 10:00
   527  			schedule: "mon1,10:00",
   528  			// Sun 22:00
   529  			last: "2017-02-05 22:00",
   530  			// Mon 9:00
   531  			now:  "2017-02-06 9:00",
   532  			next: "1h-1h",
   533  		}, {
   534  			// first Monday of the month, at 10:00
   535  			schedule: "mon1,10:00",
   536  			// first Monday of the month, 10:00
   537  			last: "2017-02-06 10:00",
   538  			// first Monday of the month, 11:00, right after
   539  			// 'previous first Monday' run
   540  			now: "2017-02-06 11:00",
   541  			// expecting March, 6th, 10:00, 27 days and 23 hours
   542  			// from now
   543  			next: "671h-671h",
   544  		}, {
   545  			// second Monday of the month, at 10:00
   546  			schedule: "mon2,10:00",
   547  			// first Monday of the month, 10:00
   548  			last: "2017-02-06 10:00",
   549  			// first Monday of the month, 11:00, right after
   550  			// 'previous first Monday' run
   551  			now: "2017-02-06 11:00",
   552  			// expecting February, 13, 10:00, 6 days and 23 hours
   553  			// from now
   554  			next: "167h-167h",
   555  		}, {
   556  			// last Monday of the month, at 10:00
   557  			schedule: "mon5,10:00",
   558  			// first Monday of the month, 10:00
   559  			last: "2017-02-06 10:00",
   560  			// first Monday of the month, 11:00, right after
   561  			// 'previous first Monday' run
   562  			now: "2017-02-06 11:00",
   563  			// expecting February, 27th, 10:00, 20 days and 23 hours
   564  			// from now
   565  			next: "503h-503h",
   566  		}, {
   567  			// (deprecated syntax, interpreted as mon1-tue)
   568  			// from the first Monday of the month to the second Tuesday of
   569  			// the month, at 10:00
   570  			schedule: "mon1-tue2,10:00",
   571  			// Monday, 10:00
   572  			last: "2017-02-06 10:00",
   573  			// Tuesday, the day after the first Monday of the month
   574  			now: "2017-02-07 11:00",
   575  			// expecting to run on 03.06.2017
   576  			next: "647h-647h",
   577  		}, {
   578  			// from the first Monday of the month to the following Tuesday of
   579  			// the month, at 10:00
   580  			schedule: "mon1-tue,10:00",
   581  			last:     "2017-02-01 10:00",
   582  			// Sunday, 10:00
   583  			now: "2017-02-05 10:00",
   584  			// expecting to run the next day at 10:00
   585  			next: "24h-24h",
   586  		}, {
   587  			// from the first Monday of the month to the following Tuesday of
   588  			// the month, at 10:00
   589  			schedule: "mon1-tue,10:00",
   590  			// Tuesday, 10:00
   591  			last: "2017-02-14 22:00",
   592  			// Thursday, 10:00
   593  			now: "2017-02-16 10:00",
   594  			// expecting to run in 18 days
   595  			next: "432h-432h",
   596  		}, {
   597  			// from the first Monday of the month to the following Tuesday of
   598  			// the month, at 10:00
   599  			schedule: "mon1-tue,10:00",
   600  			// Sunday, 22:00
   601  			last: "2017-02-05 22:00",
   602  			// first Monday of the month
   603  			now: "2017-02-06 11:00",
   604  			// expecting to run the next day at 10:00
   605  			next: "23h-23h",
   606  		}, {
   607  			// from the first Monday of the month to the following Tuesday of
   608  			// the month, at 10:00
   609  			schedule: "mon1-tue,10:00-12:00",
   610  			// Sunday, 22:00
   611  			last: "2017-02-05 22:00",
   612  			// first Monday of the month, within the update window
   613  			now: "2017-02-06 11:00",
   614  			// expecting to run now
   615  			next: "0h-0h",
   616  		}, {
   617  			// from the first Monday of the month to the following Tuesday of
   618  			// the month, at 10:00
   619  			schedule: "mon1-tue,10:00~12:00",
   620  			// Sunday, 22:00
   621  			last: "2017-02-05 22:00",
   622  			// first Monday of the month, within the update window
   623  			now: "2017-02-06 11:00",
   624  			// expecting to run now
   625  			next: "0h-1h",
   626  			// since we're in update window we'll run now regardless
   627  			// of 'spreading'
   628  			randomized: false,
   629  		}, {
   630  			schedule:   "mon,10:00~12:00,,fri,15:00",
   631  			last:       "2017-02-05 22:00",
   632  			now:        "2017-02-06 9:00",
   633  			next:       "1h-3h",
   634  			randomized: true,
   635  		}, {
   636  			schedule: "mon,10:00-12:00,,fri,15:00",
   637  			last:     "2017-02-06 12:00",
   638  			// tue 12:00
   639  			now: "2017-02-07 12:00",
   640  			// 3 days and 3 hours from now
   641  			next: "75h-75h",
   642  		}, {
   643  			// randomized between 10:00 and 12:00
   644  			schedule: "mon,10:00~12:00",
   645  			// sun 22:00
   646  			last: "2017-02-05 22:00",
   647  			// mon 9:00
   648  			now:        "2017-02-06 9:00",
   649  			next:       "1h-3h",
   650  			randomized: true,
   651  		}, {
   652  			// Friday to Monday, 10am
   653  			schedule: "fri-mon,10:00",
   654  			// sun 22:00
   655  			last: "2017-02-05 22:00",
   656  			// mon 9:00
   657  			now:  "2017-02-06 9:00",
   658  			next: "1h-1h",
   659  		}, {
   660  			// Friday to Monday, 10am
   661  			schedule: "fri-mon,10:00",
   662  			// mon 10:00
   663  			last: "2017-02-06 10:00",
   664  			// mon 10:00
   665  			now: "2017-02-06 10:00",
   666  			// 4 days from now
   667  			next: "96h-96h",
   668  		}, {
   669  			// Wednesday to Friday, 10am
   670  			schedule: "wed-fri,10:00",
   671  			// mon 10:00
   672  			last: "2017-02-06 10:00",
   673  			// mon 10:00
   674  			now: "2017-02-06 10:00",
   675  			// 2 days from now
   676  			next: "48h-48h",
   677  		}, {
   678  			// randomized, once a day
   679  			schedule: "0:00~24:00",
   680  			// sun 22:00
   681  			last: "2017-02-05 22:00",
   682  			// mon 9:00
   683  			now:        "2017-02-05 23:00",
   684  			next:       "1h-25h",
   685  			randomized: true,
   686  		}, {
   687  			// randomized, once a day
   688  			schedule: "0:00~24:00",
   689  			// mon 10:00
   690  			last: "2017-02-06 10:00",
   691  			// mon 11:00
   692  			now: "2017-02-06 11:00",
   693  			// sometime the next day
   694  			next:       "13h-37h",
   695  			randomized: true,
   696  		}, {
   697  			// during the night, 23:00-1:00
   698  			schedule: "23:00~1:00",
   699  			// mon 10:00
   700  			last: "2017-02-06 10:00",
   701  			// mon 11:00
   702  			now: "2017-02-06 22:00",
   703  			// sometime over the night
   704  			next:       "1h-3h",
   705  			randomized: true,
   706  		}, {
   707  			// during the night, 23:00-1:00
   708  			schedule: "23:00~1:00",
   709  			// Mon 23:00
   710  			last: "2017-02-06 23:00",
   711  			// Tue 0:00
   712  			now: "2017-02-07 00:00",
   713  			// sometime over the night
   714  			next:       "23h-25h",
   715  			randomized: true,
   716  		}, {
   717  			// twice between 9am and 11am
   718  			schedule: "9:00-11:00/2",
   719  			// last attempt at the beginning of window
   720  			last: "2017-02-06 9:00",
   721  			// sometime between 10am and 11am
   722  			now:  "2017-02-06 9:30",
   723  			next: "30m-90m",
   724  		}, {
   725  			// 2 ranges
   726  			schedule: "9:00-10:00,10:00-11:00",
   727  			// last attempt at the beginning of window
   728  			last: "2017-02-06 9:01",
   729  			// next one at 10am
   730  			now:  "2017-02-06 9:30",
   731  			next: "30m-30m",
   732  		}, {
   733  			// twice, at 9am and at 2pm
   734  			schedule: "9:00,14:00",
   735  			// last right after scheduled time window
   736  			last: "2017-02-06 9:01",
   737  			// next one at 2pm
   738  			now:  "2017-02-06 9:30",
   739  			next: "270m-270m",
   740  		}, {
   741  			// 2 ranges, reversed order in spec
   742  			schedule: "10:00~11:00,9:00-10:00",
   743  			// last attempt at the beginning of window
   744  			last: "2017-02-06 9:01",
   745  			// sometime between 10am and 11am
   746  			now:        "2017-02-06 9:30",
   747  			next:       "30m-90m",
   748  			randomized: true,
   749  		}, {
   750  			// first Wednesday at 13:00
   751  			schedule: "wed1,13:00",
   752  			now:      "2018-07-30 9:00",
   753  			// yesterday
   754  			last: "2018-07-29 13:00",
   755  			// next one on 2018-08-01 13:00
   756  			next: "52h-52h",
   757  		}, {
   758  			//   October 2019
   759  			// Su Mo Tu We Th Fr Sa
   760  			// 29 30| 1  2  3  4  5
   761  			//  6  7  8  9 10 11 12
   762  			// 13 14 15 16 17 18 19
   763  			// 20 21 22 23 24 25 26
   764  			// 27 28 29 30 31
   765  
   766  			// first Monday to the following Wednesday of the month, in Oct
   767  			// 2019, matches 07.10-09.10
   768  			schedule: "mon1-wed,9:00-13:00",
   769  			now:      "2019-09-30 9:00",
   770  			// yesterday
   771  			last: "2019-09-30 9:00",
   772  			// next one on 2019-10-07 9:00
   773  			next: "168h-168h",
   774  		}, {
   775  			// first Monday to the following Wednesday of the month, in Oct
   776  			// 2019, matches 30.09-04.10
   777  			schedule: "mon-fri1,9:00-13:00",
   778  			now:      "2019-09-29 9:00",
   779  			last:     "2019-09-29 9:00",
   780  			// next one on 2019-09-30 9:00
   781  			next: "24h-24h",
   782  		}, {
   783  			// most trivial case
   784  			schedule: "21:00-22:00",
   785  			now:      "2019-09-29 8:00",
   786  			last:     "2019-09-28 21:05",
   787  			// next one on 2019-09-29 at 21:00
   788  			next: "13h-13h",
   789  		},
   790  	} {
   791  		c.Logf("trying %+v", t)
   792  
   793  		last, err := time.ParseInLocation(shortForm, t.last, time.Local)
   794  		c.Assert(err, IsNil)
   795  
   796  		fakeNow, err := time.ParseInLocation(shortForm, t.now, time.Local)
   797  		c.Assert(err, IsNil)
   798  		restorer := timeutil.MockTimeNow(func() time.Time {
   799  			return fakeNow
   800  		})
   801  		defer restorer()
   802  
   803  		sched, err := timeutil.ParseSchedule(t.schedule)
   804  		c.Assert(err, IsNil)
   805  
   806  		// keep track of previous result for tests where event time is
   807  		// randomized
   808  		previous := time.Duration(0)
   809  		calls := 2
   810  
   811  		for i := 0; i < calls; i++ {
   812  			next := timeutil.Next(sched, last, maxDuration)
   813  			if t.randomized {
   814  				c.Check(next, Not(Equals), previous)
   815  			} else if previous != 0 {
   816  				// not randomized and not the first run
   817  				c.Check(next, Equals, previous)
   818  			}
   819  
   820  			c.Logf("next: %v", next)
   821  			minDist, maxDist := parse(c, t.next)
   822  
   823  			c.Check(next >= minDist && next <= maxDist,
   824  				Equals, true,
   825  				Commentf("invalid  distance for schedule %q with last refresh %q, now %q, expected %v, got %v, date %s",
   826  					t.schedule, t.last, t.now, t.next, next, fakeNow.Add(next)))
   827  			previous = next
   828  		}
   829  	}
   830  }
   831  
   832  func (ts *timeutilSuite) TestScheduleIncludes(c *C) {
   833  	const shortForm = "2006-01-02 15:04:05"
   834  
   835  	for _, t := range []struct {
   836  		schedule  string
   837  		now       string
   838  		expecting bool
   839  	}{
   840  		{
   841  			schedule: "mon,10:00,,fri,15:00",
   842  			// mon 9:00
   843  			now:       "2017-02-06 9:00:00",
   844  			expecting: false,
   845  		}, {
   846  			// first monday of the month, at 10:00
   847  			schedule: "mon1,10:00",
   848  			// Mon 10:00:00
   849  			now:       "2017-02-06 10:00:00",
   850  			expecting: true,
   851  		}, {
   852  			// first monday of the month, at 10:00
   853  			schedule: "mon1,10:00",
   854  			// Mon 10:00:45
   855  			now:       "2017-02-06 10:00:45",
   856  			expecting: true,
   857  		}, {
   858  			// first monday of the month, at 10:00
   859  			schedule: "mon1,10:00",
   860  			// Mon 10:01
   861  			now:       "2017-02-06 10:01:00",
   862  			expecting: false,
   863  		}, {
   864  			// last Monday of the month, at 10:00
   865  			schedule: "mon5,10:00-11:00",
   866  			// first Monday of the month, 11:00, right after
   867  			// 'previous first Monday' run
   868  			now:       "2017-02-27 10:59:20",
   869  			expecting: true,
   870  		}, {
   871  			// (deprecated syntax)
   872  			// from first Monday of the month to the second Tuesday of
   873  			// the month, at 10:00 to 12:00
   874  			schedule: "mon1-tue2,10:00-12:00",
   875  			// Thursday, 11:10
   876  			now:       "2017-02-09 11:10:00",
   877  			expecting: false,
   878  		}, {
   879  			// from first Monday of the month to the following Tuesday of
   880  			// the month, at 10:00 to 12:00
   881  			schedule: "mon1-tue,10:00~12:00",
   882  			// Thursday, 11:10
   883  			now:       "2017-02-02 11:10:00",
   884  			expecting: false,
   885  		}, {
   886  			// from first Monday of the month to the following Tuesday of
   887  			// the month, at 10:00 to 12:00
   888  			schedule: "mon1-tue,10:00~12:00",
   889  			// Monday, 11:10
   890  			now:       "2017-02-06 11:10:00",
   891  			expecting: true,
   892  		}, {
   893  			// from first Monday of the month to the following Tuesday of
   894  			// the month, at 10:00 to 12:00
   895  			schedule: "mon1-tue,10:00~12:00",
   896  			// Thursday, 11:10
   897  			now:       "2017-02-16 11:10:00",
   898  			expecting: false,
   899  		}, {
   900  			// from first Monday of the month to the following Tuesday of
   901  			// the month, at 10:00 to 12:00
   902  			schedule: "mon1-tue,10:00~12:00",
   903  			// Thursday, 11:10
   904  			now:       "2017-03-06 11:10:00",
   905  			expecting: true,
   906  		}, {
   907  			// from first Monday of the month to the following Tuesday of
   908  			// the month, at 10:00 to 12:00
   909  			schedule: "mon1-tue,10:00~12:00",
   910  			// Thursday, 11:10
   911  			now:       "2017-02-09 11:10:00",
   912  			expecting: false,
   913  		}, {
   914  			// from first Tuesday of the month to the following Monday of
   915  			// the month, at 10:00 to 12:00
   916  			schedule: "tue1-mon,10:00~12:00",
   917  			// Thursday, 11:10
   918  			now:       "2017-02-09 11:10:00",
   919  			expecting: true,
   920  		}, {
   921  			// (deprecated syntax)
   922  			// from 4th Monday of the month to the following Wednesday of
   923  			// the month, at 10:00 to 12:00
   924  			schedule: "mon4-wed5,10:00~12:00",
   925  			// Schedule ends up being Feb 27 - Mar 01 2017
   926  			now:       "2017-03-02 11:10:00",
   927  			expecting: false,
   928  		}, {
   929  			// from last Monday of the month to the following Wednesday of
   930  			// the month, at 10:00 to 12:00
   931  			schedule: "mon5-wed,10:00~12:00",
   932  			// Schedule ends up being Feb 27 - Mar 01 2017
   933  			now:       "2017-03-01 11:10:00",
   934  			expecting: true,
   935  		}, {
   936  			// from last Monday of the month to the following Wednesday of
   937  			// the month, at 10:00 to 12:00
   938  			schedule: "mon5-wed,10:00~12:00",
   939  			// Schedule ends up being Feb 27 - Mar 01 2017
   940  			now:       "2017-03-02 11:10:00",
   941  			expecting: false,
   942  		}, {
   943  			// (deprecated syntax)
   944  			// from last Monday of the month to the following Tuesday of
   945  			// the month, at 10:00
   946  			schedule: "mon1-tue2,10:00~12:00",
   947  			// Sunday, 11:10
   948  			now:       "2017-02-05 11:10:00",
   949  			expecting: false,
   950  		}, {
   951  			// twice between 9am and 11am
   952  			schedule:  "9:00-11:00/2",
   953  			now:       "2017-02-06 10:30:00",
   954  			expecting: true,
   955  		}, {
   956  			schedule:  "9:00-10:00,10:00-11:00",
   957  			now:       "2017-02-06 10:30:00",
   958  			expecting: true,
   959  		}, {
   960  			// every day, 23:59
   961  			schedule:  "23:59",
   962  			now:       "2017-02-06 23:59:59",
   963  			expecting: true,
   964  		}, {
   965  			// 2 ranges, reversed order in spec
   966  			schedule: "10:00~11:00,9:00-10:00",
   967  			// sometime between 10am and 11am
   968  			now:       "2017-02-06 9:30:00",
   969  			expecting: true,
   970  		}, {
   971  			schedule: "mon1-wed,9:00-10:00",
   972  			// Tue, 9:30
   973  			now:       "2019-10-08 9:30:00",
   974  			expecting: true,
   975  		}, {
   976  			schedule: "tue1,9:00-10:00",
   977  			// Tue, 9:30
   978  			now:       "2019-10-01 9:30:00",
   979  			expecting: true,
   980  		},
   981  	} {
   982  		c.Logf("trying %+v", t)
   983  
   984  		now, err := time.ParseInLocation(shortForm, t.now, time.Local)
   985  		c.Assert(err, IsNil)
   986  
   987  		sched, err := timeutil.ParseSchedule(t.schedule)
   988  		c.Assert(err, IsNil)
   989  
   990  		c.Check(timeutil.Includes(sched, now), Equals, t.expecting,
   991  			Commentf("unexpected result for schedule %v and time %v", t.schedule, now))
   992  	}
   993  }
   994  
   995  func (ts *timeutilSuite) TestClockSpans(c *C) {
   996  	const shortForm = "2006-01-02 15:04:05"
   997  
   998  	for _, t := range []struct {
   999  		clockspan  string
  1000  		flattenend []string
  1001  	}{
  1002  		{
  1003  			clockspan:  "23:00-01:00/2",
  1004  			flattenend: []string{"23:00-00:00", "00:00-01:00"},
  1005  		}, {
  1006  			clockspan:  "23:00-01:00/4",
  1007  			flattenend: []string{"23:00-23:30", "23:30-00:00", "00:00-00:30", "00:30-01:00"},
  1008  		},
  1009  	} {
  1010  		c.Logf("trying %+v", t)
  1011  		spans, err := timeutil.ParseClockSpan(t.clockspan)
  1012  		c.Assert(err, IsNil)
  1013  
  1014  		spanStrings := make([]string, len(t.flattenend))
  1015  		flattened := spans.ClockSpans()
  1016  		c.Assert(flattened, HasLen, len(t.flattenend))
  1017  		for i := range flattened {
  1018  			spanStrings[i] = flattened[i].String()
  1019  		}
  1020  
  1021  		c.Assert(spanStrings, DeepEquals, t.flattenend)
  1022  	}
  1023  }
  1024  
  1025  func (ts *timeutilSuite) TestWeekSpans(c *C) {
  1026  	const shortForm = "2006-01-02"
  1027  
  1028  	//     July 2018            August 2018
  1029  	// Su Mo Tu We Th Fr Sa  Su Mo Tu We Th Fr Sa
  1030  	//  1  2  3  4  5  6  7            1  2  3  4
  1031  	//  8  9 10 11 12 13 14   5  6  7  8  9 10 11
  1032  	// 15 16 17 18 19 20 21  12 13 14 15 16 17 18
  1033  	// 22 23 24 25 26 27 28  19 20 21 22 23 24 25
  1034  	// 29 30 31              26 27 28 29 30 31
  1035  
  1036  	for _, t := range []struct {
  1037  		week  string
  1038  		when  string
  1039  		match bool
  1040  	}{
  1041  		{
  1042  			// first Wednesday
  1043  			week:  "wed1",
  1044  			when:  "2018-08-01",
  1045  			match: true,
  1046  		}, {
  1047  			// first Wednesday
  1048  			week: "wed1",
  1049  			// actually 2nd Wednesday
  1050  			when:  "2018-08-08",
  1051  			match: false,
  1052  		}, {
  1053  			// second Wednesday
  1054  			week:  "wed2",
  1055  			when:  "2018-08-08",
  1056  			match: true,
  1057  		}, {
  1058  			// first Tuesday
  1059  			week:  "tue1",
  1060  			when:  "2018-08-07",
  1061  			match: true,
  1062  		}, {
  1063  			// first Sunday
  1064  			week:  "sun1",
  1065  			when:  "2018-07-01",
  1066  			match: true,
  1067  		}, {
  1068  			// last Tuesday
  1069  			week:  "tue5",
  1070  			when:  "2018-07-31",
  1071  			match: true,
  1072  		}, {
  1073  			// last Tuesday
  1074  			week:  "tue5",
  1075  			when:  "2018-07-24",
  1076  			match: false,
  1077  		}, {
  1078  			// last Thursday
  1079  			week:  "thu5",
  1080  			when:  "2018-07-26",
  1081  			match: true,
  1082  		}, {
  1083  			// using deprecated syntax
  1084  			// first Monday (06.08) to first Friday (03.08), see August calendar above
  1085  			// includes: 01.08-03.08 and 06.08-07.08
  1086  			week: "mon1-fri1",
  1087  			// Wednesday
  1088  			when:  "2018-08-01",
  1089  			match: false,
  1090  		}, {
  1091  			// using deprecated syntax
  1092  			// first Monday (06.08) to first Friday (03.08), see August calendar above
  1093  			week: "mon1-fri",
  1094  			// Tuesday
  1095  			when:  "2018-08-07",
  1096  			match: true,
  1097  		}, {
  1098  			// first Monday (06.08) to first Friday (03.08), see August calendar above
  1099  			week: "mon1-fri",
  1100  			// Thursday
  1101  			when:  "2018-08-08",
  1102  			match: true,
  1103  		}, {
  1104  			// second Monday (13.08) to second Friday (10.08), see August calendar above
  1105  			// includes: 13.08-14.08 and 08.08-10.08
  1106  			week: "mon2-fri",
  1107  			// Thursday
  1108  			when:  "2018-08-13",
  1109  			match: true,
  1110  		}, {
  1111  			// second Monday (13.08) to second Friday (10.08), see August calendar above
  1112  			week: "mon2-fri",
  1113  			// Thursday
  1114  			when:  "2018-08-13",
  1115  			match: true,
  1116  		}, {
  1117  			// first Friday (03.08) to the following Monday (06.08), see August calendar above
  1118  			// includes: 03.08-06.08
  1119  			week: "fri1-mon",
  1120  			// Saturday
  1121  			when:  "2018-08-04",
  1122  			match: true,
  1123  		}, {
  1124  			// first Friday (06.07) to the following Monday (09.07), see July calendar above
  1125  			// includes: 03.07-09.07
  1126  			week: "fri1-mon",
  1127  			// Sunday
  1128  			when:  "2018-07-08",
  1129  			match: true,
  1130  		}, {
  1131  			// first Friday (03.08) to the preceding Monday (30.07), see July. August calendar above
  1132  			// includes: 30.07-03.08
  1133  			week: "mon-fri1",
  1134  			// Saturday
  1135  			when:  "2018-08-01",
  1136  			match: true,
  1137  		}, {
  1138  			// first Friday (03.08) to the preceding Monday (30.07), see July. August calendar above
  1139  			// includes: 30.07-03.08
  1140  			week: "mon-fri1",
  1141  			// Saturday
  1142  			when:  "2018-07-30",
  1143  			match: true,
  1144  		}, {
  1145  			// 4th Friday (27.08) to the following Monday (02.08), see July. August calendar above
  1146  			// includes: 27.07-02.08
  1147  			week: "fri4-thu",
  1148  			// Saturday
  1149  			when:  "2018-08-01",
  1150  			match: true,
  1151  		}, {
  1152  			// using deprecated syntax
  1153  			// first Friday (06.07) to the following Monday (09.07), see July calendar above
  1154  			// includes: 03.07-09.07
  1155  			week: "fri1-mon1",
  1156  			// Sunday
  1157  			when:  "2018-07-08",
  1158  			match: true,
  1159  		}, {
  1160  			// first Friday (06.07) to the following Monday (09.07), see July calendar above
  1161  			// includes: 06.07-09.07
  1162  			week: "fri1-mon",
  1163  			// Sunday
  1164  			when:  "2018-07-15",
  1165  			match: false,
  1166  		}, {
  1167  			// last Monday (30.07) to the following Friday (03.07), see July calendar above
  1168  			// includes: 03.07-03.08
  1169  			week: "mon5-fri",
  1170  			// Sunday
  1171  			when:  "2018-07-31",
  1172  			match: true,
  1173  		}, {
  1174  			// last Friday (27.07) to the preceding Monday (23.07), see July calendar above
  1175  			// includes: 23.07-27.07
  1176  			week: "mon-fri5",
  1177  			// Sunday
  1178  			when:  "2018-07-28",
  1179  			match: false,
  1180  		}, {
  1181  			// last Friday (27.07) to the preceding Monday (23.07), see July calendar above
  1182  			// includes: 23.07-27.07
  1183  			week: "mon-fri5",
  1184  			// Sunday
  1185  			when:  "2018-07-25",
  1186  			match: true,
  1187  		}, {
  1188  			// first Monday (2.07) to the following Monday (9.07), see July calendar above
  1189  			// includes: 2.07-9.07
  1190  			week: "mon1-mon",
  1191  			// Tuesday
  1192  			when:  "2018-07-03",
  1193  			match: true,
  1194  		}, {
  1195  			week: "mon1-mon",
  1196  			// Monday (the farther edge of the span)
  1197  			when:  "2018-07-09",
  1198  			match: true,
  1199  		}, {
  1200  			week: "mon1-mon",
  1201  			// Tuesday
  1202  			when:  "2018-07-10",
  1203  			match: false,
  1204  		},
  1205  	} {
  1206  		c.Logf("trying %+v", t)
  1207  		ws, err := timeutil.ParseWeekSpan(t.week)
  1208  		c.Assert(err, IsNil)
  1209  
  1210  		when, err := time.ParseInLocation(shortForm, t.when, time.Local)
  1211  		c.Assert(err, IsNil)
  1212  		c.Logf("when: %v %s", when, when.Weekday())
  1213  
  1214  		c.Check(ws.Match(when), Equals, t.match)
  1215  	}
  1216  }
  1217  
  1218  func (ts *timeutilSuite) TestTimeZero(c *C) {
  1219  	// test with a zero time stamp to make sure that code does not do
  1220  	// anything silly
  1221  
  1222  	// zero time is: time is: 0001-01-01 00:00:00 +0000 UTC and ... Monday
  1223  	zero := time.Time{}
  1224  	c.Logf("time is: %v weekday: %v", zero, zero.Weekday())
  1225  
  1226  	for _, schedule := range []string{
  1227  		"mon-tue,0:00-12:00",
  1228  		"mon1-tue,0:00-12:00",
  1229  		"mon-tue1,0:00-12:00",
  1230  	} {
  1231  		c.Logf("trying: %v", schedule)
  1232  		sch, err := timeutil.ParseSchedule(schedule)
  1233  		c.Assert(err, IsNil)
  1234  
  1235  		c.Check(timeutil.Includes(sch, zero), Equals, true)
  1236  		c.Check(timeutil.Includes(sch, zero.Add(5*time.Hour)), Equals, true)
  1237  		// wednesday
  1238  		c.Check(timeutil.Includes(sch, zero.Add(2*24*time.Hour)), Equals, false)
  1239  	}
  1240  }
  1241  
  1242  func (ts *timeutilSuite) TestMonthNext(c *C) {
  1243  	const shortForm = "2006-01-02"
  1244  	for _, t := range []struct {
  1245  		when, next string
  1246  	}{
  1247  		{"2018-07-01", "2018-08-01"},
  1248  		{"2018-07-31", "2018-08-01"},
  1249  		{"2018-07-20", "2018-08-01"},
  1250  		{"2018-02-01", "2018-03-01"},
  1251  		{"2018-02-28", "2018-03-01"},
  1252  		{"2018-01-31", "2018-02-01"},
  1253  		// in 2020 Feb is 29 days
  1254  		{"2020-01-31", "2020-02-01"},
  1255  		{"2020-02-01", "2020-03-01"},
  1256  		{"2020-02-14", "2020-03-01"},
  1257  	} {
  1258  		when, err := time.ParseInLocation(shortForm, t.when, time.Local)
  1259  		c.Assert(err, IsNil)
  1260  		c.Logf("when: %v expecting: %v", when, t.next)
  1261  
  1262  		next := timeutil.MonthNext(when)
  1263  		c.Check(next.Format(shortForm), Equals, t.next)
  1264  	}
  1265  
  1266  }