...

Source file src/github.com/prometheus/alertmanager/timeinterval/timeinterval_test.go

Documentation: github.com/prometheus/alertmanager/timeinterval

     1  // Copyright 2020 Prometheus Team
     2  // Licensed under the Apache License, Version 2.0 (the "License");
     3  // you may not use this file except in compliance with the License.
     4  // You may obtain a copy of the License at
     5  //
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package timeinterval
    15  
    16  import (
    17  	"encoding/json"
    18  	"reflect"
    19  	"testing"
    20  	"time"
    21  
    22  	"gopkg.in/yaml.v2"
    23  )
    24  
    25  var timeIntervalTestCases = []struct {
    26  	validTimeStrings   []string
    27  	invalidTimeStrings []string
    28  	timeInterval       TimeInterval
    29  }{
    30  	{
    31  		timeInterval: TimeInterval{},
    32  		validTimeStrings: []string{
    33  			"02 Jan 06 15:04 +0000",
    34  			"03 Jan 07 10:04 +0000",
    35  			"04 Jan 06 09:04 +0000",
    36  		},
    37  		invalidTimeStrings: []string{},
    38  	},
    39  	{
    40  		// 9am to 5pm, monday to friday
    41  		timeInterval: TimeInterval{
    42  			Times:    []TimeRange{{StartMinute: 540, EndMinute: 1020}},
    43  			Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}},
    44  		},
    45  		validTimeStrings: []string{
    46  			"04 May 20 15:04 +0000",
    47  			"05 May 20 10:04 +0000",
    48  			"09 Jun 20 09:04 +0000",
    49  		},
    50  		invalidTimeStrings: []string{
    51  			"03 May 20 15:04 +0000",
    52  			"04 May 20 08:59 +0000",
    53  			"05 May 20 05:00 +0000",
    54  		},
    55  	},
    56  	{
    57  		// Easter 2020
    58  		timeInterval: TimeInterval{
    59  			DaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: 4, End: 6}}},
    60  			Months:      []MonthRange{{InclusiveRange{Begin: 4, End: 4}}},
    61  			Years:       []YearRange{{InclusiveRange{Begin: 2020, End: 2020}}},
    62  		},
    63  		validTimeStrings: []string{
    64  			"04 Apr 20 15:04 +0000",
    65  			"05 Apr 20 00:00 +0000",
    66  			"06 Apr 20 23:05 +0000",
    67  		},
    68  		invalidTimeStrings: []string{
    69  			"03 May 18 15:04 +0000",
    70  			"03 Apr 20 23:59 +0000",
    71  			"04 Jun 20 23:59 +0000",
    72  			"06 Apr 19 23:59 +0000",
    73  			"07 Apr 20 00:00 +0000",
    74  		},
    75  	},
    76  	{
    77  		// Check negative days of month, last 3 days of each month
    78  		timeInterval: TimeInterval{
    79  			DaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: -3, End: -1}}},
    80  		},
    81  		validTimeStrings: []string{
    82  			"31 Jan 20 15:04 +0000",
    83  			"30 Jan 20 15:04 +0000",
    84  			"29 Jan 20 15:04 +0000",
    85  			"30 Jun 20 00:00 +0000",
    86  			"29 Feb 20 23:05 +0000",
    87  		},
    88  		invalidTimeStrings: []string{
    89  			"03 May 18 15:04 +0000",
    90  			"27 Jan 20 15:04 +0000",
    91  			"03 Apr 20 23:59 +0000",
    92  			"04 Jun 20 23:59 +0000",
    93  			"06 Apr 19 23:59 +0000",
    94  			"07 Apr 20 00:00 +0000",
    95  			"01 Mar 20 00:00 +0000",
    96  		},
    97  	},
    98  	{
    99  		// Check out of bound days are clamped to month boundaries
   100  		timeInterval: TimeInterval{
   101  			Months:      []MonthRange{{InclusiveRange{Begin: 6, End: 6}}},
   102  			DaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: -31, End: 31}}},
   103  		},
   104  		validTimeStrings: []string{
   105  			"30 Jun 20 00:00 +0000",
   106  			"01 Jun 20 00:00 +0000",
   107  		},
   108  		invalidTimeStrings: []string{
   109  			"31 May 20 00:00 +0000",
   110  			"1 Jul 20 00:00 +0000",
   111  		},
   112  	},
   113  	{
   114  		// Check alternative timezones can be used to compare times.
   115  		// AEST 9AM to 5PM, Monday to Friday.
   116  		timeInterval: TimeInterval{
   117  			Times:    []TimeRange{{StartMinute: 540, EndMinute: 1020}},
   118  			Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}},
   119  			Location: &Location{mustLoadLocation("Australia/Sydney")},
   120  		},
   121  		validTimeStrings: []string{
   122  			"06 Apr 21 13:00 +1000",
   123  		},
   124  		invalidTimeStrings: []string{
   125  			"06 Apr 21 13:00 +0000",
   126  		},
   127  	},
   128  	{
   129  		// Check an alternative timezone during daylight savings time.
   130  		timeInterval: TimeInterval{
   131  			Times:    []TimeRange{{StartMinute: 540, EndMinute: 1020}},
   132  			Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}},
   133  			Months:   []MonthRange{{InclusiveRange{Begin: 11, End: 11}}},
   134  			Location: &Location{mustLoadLocation("Australia/Sydney")},
   135  		},
   136  		validTimeStrings: []string{
   137  			"01 Nov 21 09:00 +1100",
   138  			"31 Oct 21 22:00 +0000",
   139  		},
   140  		invalidTimeStrings: []string{
   141  			"31 Oct 21 21:00 +0000",
   142  		},
   143  	},
   144  }
   145  
   146  var timeStringTestCases = []struct {
   147  	timeString  string
   148  	TimeRange   TimeRange
   149  	expectError bool
   150  }{
   151  	{
   152  		timeString:  "{'start_time': '00:00', 'end_time': '24:00'}",
   153  		TimeRange:   TimeRange{StartMinute: 0, EndMinute: 1440},
   154  		expectError: false,
   155  	},
   156  	{
   157  		timeString:  "{'start_time': '01:35', 'end_time': '17:39'}",
   158  		TimeRange:   TimeRange{StartMinute: 95, EndMinute: 1059},
   159  		expectError: false,
   160  	},
   161  	{
   162  		timeString:  "{'start_time': '09:35', 'end_time': '09:39'}",
   163  		TimeRange:   TimeRange{StartMinute: 575, EndMinute: 579},
   164  		expectError: false,
   165  	},
   166  	{
   167  		// Error: Begin and End times are the same
   168  		timeString:  "{'start_time': '17:31', 'end_time': '17:31'}",
   169  		TimeRange:   TimeRange{},
   170  		expectError: true,
   171  	},
   172  	{
   173  		// Error: End time out of range
   174  		timeString:  "{'start_time': '12:30', 'end_time': '24:01'}",
   175  		TimeRange:   TimeRange{},
   176  		expectError: true,
   177  	},
   178  	{
   179  		// Error: Start time greater than End time
   180  		timeString:  "{'start_time': '09:30', 'end_time': '07:41'}",
   181  		TimeRange:   TimeRange{},
   182  		expectError: true,
   183  	},
   184  	{
   185  		// Error: Start time out of range and greater than End time
   186  		timeString:  "{'start_time': '24:00', 'end_time': '17:41'}",
   187  		TimeRange:   TimeRange{},
   188  		expectError: true,
   189  	},
   190  	{
   191  		// Error: No range specified
   192  		timeString:  "{'start_time': '14:03'}",
   193  		TimeRange:   TimeRange{},
   194  		expectError: true,
   195  	},
   196  }
   197  
   198  var yamlUnmarshalTestCases = []struct {
   199  	in          string
   200  	intervals   []TimeInterval
   201  	contains    []string
   202  	excludes    []string
   203  	expectError bool
   204  	err         string
   205  }{
   206  	{
   207  		// Simple business hours test
   208  		in: `
   209  ---
   210  - weekdays: ['monday:friday']
   211    times:
   212      - start_time: '09:00'
   213        end_time: '17:00'
   214  `,
   215  		intervals: []TimeInterval{
   216  			{
   217  				Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}},
   218  				Times:    []TimeRange{{StartMinute: 540, EndMinute: 1020}},
   219  			},
   220  		},
   221  		contains: []string{
   222  			"08 Jul 20 09:00 +0000",
   223  			"08 Jul 20 16:59 +0000",
   224  		},
   225  		excludes: []string{
   226  			"08 Jul 20 05:00 +0000",
   227  			"08 Jul 20 08:59 +0000",
   228  		},
   229  		expectError: false,
   230  	},
   231  	{
   232  		// More advanced test with negative indices and ranges
   233  		in: `
   234  ---
   235    # Last week, excluding Saturday, of the first quarter of the year during business hours from 2020 to 2025 and 2030-2035
   236  - weekdays: ['monday:friday', 'sunday']
   237    months: ['january:march']
   238    days_of_month: ['-7:-1']
   239    years: ['2020:2025', '2030:2035']
   240    times:
   241      - start_time: '09:00'
   242        end_time: '17:00'
   243  `,
   244  		intervals: []TimeInterval{
   245  			{
   246  				Weekdays:    []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}, {InclusiveRange{Begin: 0, End: 0}}},
   247  				Times:       []TimeRange{{StartMinute: 540, EndMinute: 1020}},
   248  				Months:      []MonthRange{{InclusiveRange{1, 3}}},
   249  				DaysOfMonth: []DayOfMonthRange{{InclusiveRange{-7, -1}}},
   250  				Years:       []YearRange{{InclusiveRange{2020, 2025}}, {InclusiveRange{2030, 2035}}},
   251  			},
   252  		},
   253  		contains: []string{
   254  			"27 Jan 21 09:00 +0000",
   255  			"28 Jan 21 16:59 +0000",
   256  			"29 Jan 21 13:00 +0000",
   257  			"31 Mar 25 13:00 +0000",
   258  			"31 Mar 25 13:00 +0000",
   259  			"31 Jan 35 13:00 +0000",
   260  		},
   261  		excludes: []string{
   262  			"30 Jan 21 13:00 +0000", // Saturday
   263  			"01 Apr 21 13:00 +0000", // 4th month
   264  			"30 Jan 26 13:00 +0000", // 2026
   265  			"31 Jan 35 17:01 +0000", // After 5pm
   266  		},
   267  		expectError: false,
   268  	},
   269  	{
   270  		in: `
   271  ---
   272  - weekdays: ['monday:friday']
   273    times:
   274      - start_time: '09:00'
   275        end_time: '17:00'`,
   276  		intervals: []TimeInterval{
   277  			{
   278  				Weekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}},
   279  				Times:    []TimeRange{{StartMinute: 540, EndMinute: 1020}},
   280  			},
   281  		},
   282  		contains: []string{
   283  			"01 Apr 21 13:00 +0000",
   284  		},
   285  	},
   286  	{
   287  		// Invalid start time.
   288  		in: `
   289  ---
   290  - times:
   291      - start_time: '01:99'
   292        end_time: '23:59'`,
   293  		expectError: true,
   294  		err:         "couldn't parse timestamp 01:99, invalid format",
   295  	},
   296  	{
   297  		// Invalid end time.
   298  		in: `
   299  ---
   300  - times:
   301      - start_time: '00:00'
   302        end_time: '99:99'`,
   303  		expectError: true,
   304  		err:         "couldn't parse timestamp 99:99, invalid format",
   305  	},
   306  	{
   307  		// Start day before end day.
   308  		in: `
   309  ---
   310  - weekdays: ['friday:monday']`,
   311  		expectError: true,
   312  		err:         "start day cannot be before end day",
   313  	},
   314  	{
   315  		// Invalid weekdays.
   316  		in: `
   317  ---
   318  - weekdays: ['blurgsday:flurgsday']
   319  `,
   320  		expectError: true,
   321  		err:         "blurgsday is not a valid weekday",
   322  	},
   323  	{
   324  		// Numeric weekdays aren't allowed.
   325  		in: `
   326  ---
   327  - weekdays: ['1:3']
   328  `,
   329  		expectError: true,
   330  		err:         "1 is not a valid weekday",
   331  	},
   332  	{
   333  		// Negative numeric weekdays aren't allowed.
   334  		in: `
   335  ---
   336  - weekdays: ['-2:-1']
   337  `,
   338  		expectError: true,
   339  		err:         "-2 is not a valid weekday",
   340  	},
   341  	{
   342  		// 0 day of month.
   343  		in: `
   344  ---
   345  - days_of_month: ['0']
   346  `,
   347  		expectError: true,
   348  		err:         "0 is not a valid day of the month: out of range",
   349  	},
   350  	{
   351  		// Start day of month < 0.
   352  		in: `
   353  ---
   354  - days_of_month: ['-50:-20']
   355  `,
   356  		expectError: true,
   357  		err:         "-50 is not a valid day of the month: out of range",
   358  	},
   359  	{
   360  		// End day of month > 31.
   361  		in: `
   362  ---
   363  - days_of_month: ['1:50']
   364  `,
   365  		expectError: true,
   366  		err:         "50 is not a valid day of the month: out of range",
   367  	},
   368  	{
   369  		// Negative indices should work.
   370  		in: `
   371  ---
   372  - days_of_month: ['1:-1']
   373  `,
   374  		intervals: []TimeInterval{
   375  			{
   376  				DaysOfMonth: []DayOfMonthRange{{InclusiveRange{1, -1}}},
   377  			},
   378  		},
   379  		expectError: false,
   380  	},
   381  	{
   382  		// End day must be negative if begin day is negative.
   383  		in: `
   384  ---
   385  - days_of_month: ['-15:5']
   386  `,
   387  		expectError: true,
   388  		err:         "end day must be negative if start day is negative",
   389  	},
   390  	{
   391  		// Negative end date before positive positive start date.
   392  		in: `
   393  ---
   394  - days_of_month: ['10:-25']
   395  `,
   396  		expectError: true,
   397  		err:         "end day -25 is always before start day 10",
   398  	},
   399  	{
   400  		// Months should work regardless of case
   401  		in: `
   402  ---
   403  - months: ['January:december']
   404  `,
   405  		expectError: false,
   406  		intervals: []TimeInterval{
   407  			{
   408  				Months: []MonthRange{{InclusiveRange{1, 12}}},
   409  			},
   410  		},
   411  	},
   412  	{
   413  		// Time zones may be specified by location.
   414  		in: `
   415  ---
   416  - years: ['2020:2022']
   417    location: 'Australia/Sydney'
   418  `,
   419  		expectError: false,
   420  		intervals: []TimeInterval{
   421  			{
   422  				Years:    []YearRange{{InclusiveRange{2020, 2022}}},
   423  				Location: &Location{mustLoadLocation("Australia/Sydney")},
   424  			},
   425  		},
   426  	},
   427  	{
   428  		// Invalid start month.
   429  		in: `
   430  ---
   431  - months: ['martius:june']
   432  `,
   433  		expectError: true,
   434  		err:         "martius is not a valid month",
   435  	},
   436  	{
   437  		// Invalid end month.
   438  		in: `
   439  ---
   440  - months: ['march:junius']
   441  `,
   442  		expectError: true,
   443  		err:         "junius is not a valid month",
   444  	},
   445  	{
   446  		// Start month after end month.
   447  		in: `
   448  ---
   449  - months: ['december:january']
   450  `,
   451  		expectError: true,
   452  		err:         "end month january is before start month december",
   453  	},
   454  	{
   455  		// Start year after end year.
   456  		in: `
   457  ---
   458  - years: ['2022:2020']
   459  `,
   460  		expectError: true,
   461  		err:         "end year 2020 is before start year 2022",
   462  	},
   463  }
   464  
   465  func TestYamlUnmarshal(t *testing.T) {
   466  	for _, tc := range yamlUnmarshalTestCases {
   467  		var ti []TimeInterval
   468  		err := yaml.Unmarshal([]byte(tc.in), &ti)
   469  		if err != nil && !tc.expectError {
   470  			t.Errorf("Received unexpected error: %v when parsing %v", err, tc.in)
   471  		} else if err == nil && tc.expectError {
   472  			t.Errorf("Expected error when unmarshalling %s but didn't receive one", tc.in)
   473  		} else if err != nil && tc.expectError {
   474  			if err.Error() != tc.err {
   475  				t.Errorf("Incorrect error: Want %s, got %s", tc.err, err.Error())
   476  			}
   477  			continue
   478  		}
   479  		if !reflect.DeepEqual(ti, tc.intervals) {
   480  			t.Errorf("Error unmarshalling %s: Want %+v, got %+v", tc.in, tc.intervals, ti)
   481  		}
   482  		for _, ts := range tc.contains {
   483  			_t, _ := time.Parse(time.RFC822Z, ts)
   484  			isContained := false
   485  			for _, interval := range ti {
   486  				if interval.ContainsTime(_t) {
   487  					isContained = true
   488  				}
   489  			}
   490  			if !isContained {
   491  				t.Errorf("Expected intervals to contain time %s", _t)
   492  			}
   493  		}
   494  		for _, ts := range tc.excludes {
   495  			_t, _ := time.Parse(time.RFC822Z, ts)
   496  			isContained := false
   497  			for _, interval := range ti {
   498  				if interval.ContainsTime(_t) {
   499  					isContained = true
   500  				}
   501  			}
   502  			if isContained {
   503  				t.Errorf("Expected intervals to exclude time %s", _t)
   504  			}
   505  		}
   506  	}
   507  }
   508  
   509  func TestContainsTime(t *testing.T) {
   510  	for _, tc := range timeIntervalTestCases {
   511  		for _, ts := range tc.validTimeStrings {
   512  			_t, _ := time.Parse(time.RFC822Z, ts)
   513  			if !tc.timeInterval.ContainsTime(_t) {
   514  				t.Errorf("Expected period %+v to contain %+v", tc.timeInterval, _t)
   515  			}
   516  		}
   517  		for _, ts := range tc.invalidTimeStrings {
   518  			_t, _ := time.Parse(time.RFC822Z, ts)
   519  			if tc.timeInterval.ContainsTime(_t) {
   520  				t.Errorf("Period %+v not expected to contain %+v", tc.timeInterval, _t)
   521  			}
   522  		}
   523  	}
   524  }
   525  
   526  func TestParseTimeString(t *testing.T) {
   527  	for _, tc := range timeStringTestCases {
   528  		var tr TimeRange
   529  		err := yaml.Unmarshal([]byte(tc.timeString), &tr)
   530  		if err != nil && !tc.expectError {
   531  			t.Errorf("Received unexpected error: %v when parsing %v", err, tc.timeString)
   532  		} else if err == nil && tc.expectError {
   533  			t.Errorf("Expected error for invalid string %s but didn't receive one", tc.timeString)
   534  		} else if !reflect.DeepEqual(tr, tc.TimeRange) {
   535  			t.Errorf("Error parsing time string %s: Want %+v, got %+v", tc.timeString, tc.TimeRange, tr)
   536  		}
   537  	}
   538  }
   539  
   540  func TestYamlMarshal(t *testing.T) {
   541  	for _, tc := range yamlUnmarshalTestCases {
   542  		if tc.expectError {
   543  			continue
   544  		}
   545  		var ti []TimeInterval
   546  		err := yaml.Unmarshal([]byte(tc.in), &ti)
   547  		if err != nil {
   548  			t.Error(err)
   549  		}
   550  		out, err := yaml.Marshal(&ti)
   551  		if err != nil {
   552  			t.Error(err)
   553  		}
   554  		var ti2 []TimeInterval
   555  		yaml.Unmarshal(out, &ti2)
   556  		if !reflect.DeepEqual(ti, ti2) {
   557  			t.Errorf("Re-marshalling %s produced a different TimeInterval.", tc.in)
   558  		}
   559  	}
   560  }
   561  
   562  // Test JSON marshalling by marshalling a time interval
   563  // and then unmarshalling to ensure they're identical
   564  func TestJsonMarshal(t *testing.T) {
   565  	for _, tc := range yamlUnmarshalTestCases {
   566  		if tc.expectError {
   567  			continue
   568  		}
   569  		var ti []TimeInterval
   570  		err := yaml.Unmarshal([]byte(tc.in), &ti)
   571  		if err != nil {
   572  			t.Error(err)
   573  		}
   574  		out, err := json.Marshal(&ti)
   575  		if err != nil {
   576  			t.Error(err)
   577  		}
   578  		var ti2 []TimeInterval
   579  		json.Unmarshal(out, &ti2)
   580  		if !reflect.DeepEqual(ti, ti2) {
   581  			t.Errorf("Re-marshalling %s produced a different TimeInterval. Used:\n%s and got:\n%v", tc.in, out, ti2)
   582  		}
   583  	}
   584  }
   585  
   586  var completeTestCases = []struct {
   587  	in       string
   588  	contains []string
   589  	excludes []string
   590  }{
   591  	{
   592  		in: `
   593  ---
   594  weekdays: ['monday:wednesday', 'saturday', 'sunday']
   595  times:
   596    - start_time: '13:00'
   597      end_time: '15:00'
   598  days_of_month: ['1', '10', '20:-1']
   599  years: ['2020:2023']
   600  months: ['january:march']
   601  `,
   602  		contains: []string{
   603  			"10 Jan 21 13:00 +0000",
   604  			"30 Jan 21 14:24 +0000",
   605  		},
   606  		excludes: []string{
   607  			"09 Jan 21 13:00 +0000",
   608  			"20 Jan 21 12:59 +0000",
   609  			"02 Feb 21 13:00 +0000",
   610  		},
   611  	},
   612  	{
   613  		// Check for broken clamping (clamping begin date after end of month to the end of the month)
   614  		in: `
   615  ---
   616  days_of_month: ['30:31']
   617  years: ['2020:2023']
   618  months: ['february']
   619  `,
   620  		excludes: []string{
   621  			"28 Feb 21 13:00 +0000",
   622  		},
   623  	},
   624  }
   625  
   626  // Tests the entire flow from unmarshalling to containing a time
   627  func TestTimeIntervalComplete(t *testing.T) {
   628  	for _, tc := range completeTestCases {
   629  		var ti TimeInterval
   630  		if err := yaml.Unmarshal([]byte(tc.in), &ti); err != nil {
   631  			t.Error(err)
   632  		}
   633  		for _, ts := range tc.contains {
   634  			tt, err := time.Parse(time.RFC822Z, ts)
   635  			if err != nil {
   636  				t.Error(err)
   637  			}
   638  			if !ti.ContainsTime(tt) {
   639  				t.Errorf("Expected %s to contain %s", tc.in, ts)
   640  			}
   641  		}
   642  		for _, ts := range tc.excludes {
   643  			tt, err := time.Parse(time.RFC822Z, ts)
   644  			if err != nil {
   645  				t.Error(err)
   646  			}
   647  			if ti.ContainsTime(tt) {
   648  				t.Errorf("Expected %s to exclude %s", tc.in, ts)
   649  			}
   650  		}
   651  	}
   652  }
   653  
   654  // Utility function for declaring time locations in test cases. Panic if the location can't be loaded.
   655  func mustLoadLocation(name string) *time.Location {
   656  	loc, err := time.LoadLocation(name)
   657  	if err != nil {
   658  		panic(err)
   659  	}
   660  	return loc
   661  }
   662  

View as plain text