github.com/TBD54566975/ftl@v0.219.0/internal/cron/cron_test.go (about)

     1  package cron
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/alecthomas/assert/v2"
     9  )
    10  
    11  func TestNonUTC(t *testing.T) {
    12  	// This cron package only works with UTC times.
    13  	// Passing in non-UTC times works fine, but the results will be in UTC.
    14  }
    15  
    16  func TestParsingAndValidationErrors(t *testing.T) {
    17  	// Rather than testing successful parsing, test them in TestNext()
    18  	for _, tt := range []struct {
    19  		str string
    20  		err string
    21  	}{
    22  		{"* * * *", "expected 5-7 components, got 4"},
    23  		{"* * * * * * * *", "expected 5-7 components, got 8"},
    24  		{"1-10,4-5/1,59-61 * * * *", "value 61 out of allowed minute range of 0-59"},
    25  		{"4-5 * * 13 *", "value 13 out of allowed month range of 1-12"},
    26  		{"4-5 * * -1 *", "1:9: unexpected token \"-\""},
    27  		{"4-5 * * 0 *", "value 0 out of allowed month range of 1-12"},
    28  		{"* * * * * 9999", "value 9999 out of allowed year range of 0-3000"},
    29  		{"* * 30 2 *", "could not find next time for pattern \"* * 30 2 *\""},
    30  		{"* * 30/0 * *", "step must be positive"},
    31  		{"* * * * * 1999", "could not find next time for pattern \"* * * * * 1999\""},
    32  		{"* * * * * * 1999", "could not find next time for pattern \"* * * * * * 1999\""},
    33  		{"* * * 29 2 * 2021", "could not find next time for pattern \"* * * 29 2 * 2021\""},
    34  	} {
    35  		t.Run(fmt.Sprintf("CronValidation:%s", tt.str), func(t *testing.T) {
    36  			_, err := Parse(tt.str)
    37  			assert.EqualError(t, err, tt.err, "Parse(%q)", tt.str)
    38  		})
    39  	}
    40  }
    41  
    42  func TestNext(t *testing.T) {
    43  	//TODO: test inputting non UTC...
    44  	for _, tt := range []struct {
    45  		str              string
    46  		inputsAndOutputs [][]time.Time
    47  	}{
    48  		{"* * * * * * *", [][]time.Time{
    49  			{
    50  				time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
    51  				time.Date(2020, 1, 1, 0, 0, 1, 0, time.UTC),
    52  			},
    53  			{ // Ticking over midnight
    54  				time.Date(2020, 1, 10, 23, 59, 59, 546, time.UTC),
    55  				time.Date(2020, 1, 11, 0, 0, 0, 0, time.UTC),
    56  			},
    57  			{ // Ticking over midnight at the end of feb, not on a leap year
    58  				time.Date(2022, 2, 28, 23, 59, 59, 666, time.UTC),
    59  				time.Date(2022, 3, 1, 0, 0, 0, 0, time.UTC),
    60  			},
    61  			{ // Ticking over midnight at the end of feb, on a leap year
    62  				time.Date(2024, 2, 28, 23, 59, 59, 666, time.UTC),
    63  				time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC),
    64  			},
    65  		}},
    66  		{"* * * * *", [][]time.Time{
    67  			{
    68  				time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
    69  				time.Date(2020, 1, 1, 0, 1, 0, 0, time.UTC),
    70  			},
    71  			{
    72  				time.Date(2020, 1, 19, 3, 34, 0, 234, time.UTC),
    73  				time.Date(2020, 1, 19, 3, 35, 0, 0, time.UTC),
    74  			},
    75  			{ // A minute over an hour
    76  				time.Date(2020, 1, 19, 3, 59, 0, 234, time.UTC),
    77  				time.Date(2020, 1, 19, 4, 0, 0, 0, time.UTC),
    78  			},
    79  			{ // A minute over midnight
    80  				time.Date(2020, 1, 10, 23, 59, 3, 546, time.UTC),
    81  				time.Date(2020, 1, 11, 0, 0, 0, 0, time.UTC),
    82  			},
    83  			{ // A minute over midnight at the end of feb, not on a leap year
    84  				time.Date(2022, 2, 28, 23, 59, 6, 666, time.UTC),
    85  				time.Date(2022, 3, 1, 0, 0, 0, 0, time.UTC),
    86  			},
    87  			{ // A minute over midnight at the end of feb, on a leap year
    88  				time.Date(2024, 2, 28, 23, 59, 55, 666, time.UTC),
    89  				time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC),
    90  			},
    91  		}},
    92  		// 6 components, should be treated as "every 10 seconds:
    93  		{"*/10 * * * * *", [][]time.Time{
    94  			{
    95  				time.Date(2020, 1, 1, 0, 0, 17, 0, time.UTC),
    96  				time.Date(2020, 1, 1, 0, 0, 20, 0, time.UTC),
    97  			},
    98  		}},
    99  		// 6 components, should be treated as "every 10 minutes, every second year"
   100  		{"*/10 * * * * 2022/2", [][]time.Time{
   101  			{
   102  				time.Date(2023, 6, 9, 18, 12, 2, 300, time.UTC),
   103  				time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
   104  			},
   105  			{
   106  				time.Date(2024, 6, 9, 18, 12, 2, 300, time.UTC),
   107  				time.Date(2024, 6, 9, 18, 20, 0, 0, time.UTC),
   108  			},
   109  		}},
   110  	} {
   111  		t.Run(fmt.Sprintf("CronSeries:%s", tt.str), func(t *testing.T) {
   112  			pattern, err := Parse(tt.str)
   113  			assert.NoError(t, err)
   114  			for _, inputAndOutput := range tt.inputsAndOutputs {
   115  				input := inputAndOutput[0]
   116  				output := inputAndOutput[1]
   117  				next, err := NextAfter(pattern, input, false)
   118  				assert.NoError(t, err)
   119  				assert.Equal(t, output, next, "NextAfter(%q, %v) = %v; want %v", tt.str, input, next, output)
   120  
   121  				outputAsInput, err := NextAfter(pattern, output, true)
   122  				assert.NoError(t, err)
   123  				assert.Equal(t, outputAsInput, output, "output of Next() should also satisfy NextAfter() with inclusive=true")
   124  			}
   125  		})
   126  	}
   127  }
   128  
   129  func TestSeries(t *testing.T) {
   130  	for _, tt := range []struct {
   131  		str           string
   132  		input         time.Time
   133  		end           time.Time
   134  		expectedCount int
   135  	}{
   136  		{
   137  			"* * * * * * *",
   138  			time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
   139  			time.Date(2020, 1, 1, 0, 0, 10, 0, time.UTC),
   140  			10,
   141  		},
   142  		{
   143  			"* * * * * * *",
   144  			time.Date(2020, 1, 1, 0, 0, 50, 0, time.UTC),
   145  			time.Date(2020, 1, 1, 0, 1, 10, 0, time.UTC),
   146  			20,
   147  		},
   148  		{ // Every 31st of the month in a year
   149  			"0 0 0 31 * * *",
   150  			time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
   151  			time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
   152  			7,
   153  		},
   154  		{ // Every 29th of Feb in the 2020s
   155  			"0 0 0 29 2 * *",
   156  			time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
   157  			time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC),
   158  			3,
   159  		},
   160  		{ // Five Mondays in Jan 2024
   161  			"0 0 0 * * 1 *",
   162  			time.Date(2023, 12, 31, 23, 59, 0, 0, time.UTC),
   163  			time.Date(2024, 1, 31, 0, 0, 0, 0, time.UTC),
   164  			5,
   165  		},
   166  		{ // Four Sundays in Jan 2024 (Sunday == 0)
   167  			"0 0 0 * * 0 *",
   168  			time.Date(2023, 12, 31, 23, 59, 0, 0, time.UTC),
   169  			time.Date(2024, 1, 31, 0, 0, 0, 0, time.UTC),
   170  			4,
   171  		},
   172  		{ // Four Sundays in Jan 2024 (sunday == 7)
   173  			"0 0 0 * * 7 *",
   174  			time.Date(2023, 12, 31, 23, 59, 0, 0, time.UTC),
   175  			time.Date(2024, 1, 31, 0, 0, 0, 0, time.UTC),
   176  			4,
   177  		},
   178  		{ // Each Mon/Wed/Friday/Sun in Jan 2024
   179  			"0 0 0 * * 1/2 *",
   180  			time.Date(2023, 12, 31, 23, 59, 0, 0, time.UTC),
   181  			time.Date(2024, 1, 31, 0, 0, 0, 0, time.UTC),
   182  			18,
   183  		},
   184  		{ // 10,11,12,13,14,17,19,24,36,48
   185  			"12/12,10-14,17-20/2 * * * * * *",
   186  			time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
   187  			time.Date(2024, 1, 1, 0, 0, 59, 100, time.UTC),
   188  			10,
   189  		},
   190  		{ // Each Mon/Wed/Friday/Sun, AND the 9th in Jan 2024
   191  			"0 0 0 9 * 1/2 *",
   192  			time.Date(2023, 12, 31, 23, 59, 0, 0, time.UTC),
   193  			time.Date(2024, 1, 31, 0, 0, 0, 0, time.UTC),
   194  			19,
   195  		},
   196  		{ // Each Mon/Wed/Friday/Sun, AND the 8th (which is a Monday anyway) in Jan 2024
   197  			"0 0 0 8 * 1/2 *",
   198  			time.Date(2023, 12, 31, 23, 59, 0, 0, time.UTC),
   199  			time.Date(2024, 1, 31, 0, 0, 0, 0, time.UTC),
   200  			18,
   201  		},
   202  		{ // Each Mon/Wed/Friday/Sun, AND every day of Jan in Jan 2024
   203  			"0 0 0 * 1 1/2 *",
   204  			time.Date(2023, 12, 31, 23, 59, 0, 0, time.UTC),
   205  			time.Date(2024, 1, 31, 0, 0, 0, 0, time.UTC),
   206  			31,
   207  		},
   208  	} {
   209  		t.Run(fmt.Sprintf("CronSeries:%s", tt.str), func(t *testing.T) {
   210  			pattern, err := Parse(tt.str)
   211  			assert.NoError(t, err)
   212  
   213  			value, err := NextAfter(pattern, tt.input, false)
   214  			assert.NoError(t, err)
   215  
   216  			count := 0
   217  			for !value.After(tt.end) {
   218  				count++
   219  
   220  				value, err = NextAfter(pattern, value, false)
   221  				assert.NoError(t, err)
   222  			}
   223  
   224  			assert.Equal(t, tt.expectedCount, count, "Count of %q between %v - %v) = %v; want %v", tt.str, tt.input, tt.end, count, tt.expectedCount)
   225  		})
   226  	}
   227  }