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 }