git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/cron/parser_test.go (about) 1 package cron 2 3 import ( 4 "reflect" 5 "strings" 6 "testing" 7 "time" 8 ) 9 10 var secondParser = NewParser(Second | Minute | Hour | Dom | Month | DowOptional | Descriptor) 11 12 func TestRange(t *testing.T) { 13 zero := uint64(0) 14 ranges := []struct { 15 expr string 16 min, max uint 17 expected uint64 18 err string 19 }{ 20 {"5", 0, 7, 1 << 5, ""}, 21 {"0", 0, 7, 1 << 0, ""}, 22 {"7", 0, 7, 1 << 7, ""}, 23 24 {"5-5", 0, 7, 1 << 5, ""}, 25 {"5-6", 0, 7, 1<<5 | 1<<6, ""}, 26 {"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7, ""}, 27 28 {"5-6/2", 0, 7, 1 << 5, ""}, 29 {"5-7/2", 0, 7, 1<<5 | 1<<7, ""}, 30 {"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7, ""}, 31 32 {"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | starBit, ""}, 33 {"*/2", 1, 3, 1<<1 | 1<<3, ""}, 34 35 {"5--5", 0, 0, zero, "too many hyphens"}, 36 {"jan-x", 0, 0, zero, "failed to parse int from"}, 37 {"2-x", 1, 5, zero, "failed to parse int from"}, 38 {"*/-12", 0, 0, zero, "negative number"}, 39 {"*//2", 0, 0, zero, "too many slashes"}, 40 {"1", 3, 5, zero, "below minimum"}, 41 {"6", 3, 5, zero, "above maximum"}, 42 {"5-3", 3, 5, zero, "beyond end of range"}, 43 {"*/0", 0, 0, zero, "should be a positive number"}, 44 } 45 46 for _, c := range ranges { 47 actual, err := getRange(c.expr, bounds{c.min, c.max, nil}) 48 if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) { 49 t.Errorf("%s => expected %v, got %v", c.expr, c.err, err) 50 } 51 if len(c.err) == 0 && err != nil { 52 t.Errorf("%s => unexpected error %v", c.expr, err) 53 } 54 if actual != c.expected { 55 t.Errorf("%s => expected %d, got %d", c.expr, c.expected, actual) 56 } 57 } 58 } 59 60 func TestField(t *testing.T) { 61 fields := []struct { 62 expr string 63 min, max uint 64 expected uint64 65 }{ 66 {"5", 1, 7, 1 << 5}, 67 {"5,6", 1, 7, 1<<5 | 1<<6}, 68 {"5,6,7", 1, 7, 1<<5 | 1<<6 | 1<<7}, 69 {"1,5-7/2,3", 1, 7, 1<<1 | 1<<5 | 1<<7 | 1<<3}, 70 } 71 72 for _, c := range fields { 73 actual, _ := getField(c.expr, bounds{c.min, c.max, nil}) 74 if actual != c.expected { 75 t.Errorf("%s => expected %d, got %d", c.expr, c.expected, actual) 76 } 77 } 78 } 79 80 func TestAll(t *testing.T) { 81 allBits := []struct { 82 r bounds 83 expected uint64 84 }{ 85 {minutes, 0xfffffffffffffff}, // 0-59: 60 ones 86 {hours, 0xffffff}, // 0-23: 24 ones 87 {dom, 0xfffffffe}, // 1-31: 31 ones, 1 zero 88 {months, 0x1ffe}, // 1-12: 12 ones, 1 zero 89 {dow, 0x7f}, // 0-6: 7 ones 90 } 91 92 for _, c := range allBits { 93 actual := all(c.r) // all() adds the starBit, so compensate for that.. 94 if c.expected|starBit != actual { 95 t.Errorf("%d-%d/%d => expected %b, got %b", 96 c.r.min, c.r.max, 1, c.expected|starBit, actual) 97 } 98 } 99 } 100 101 func TestBits(t *testing.T) { 102 bits := []struct { 103 min, max, step uint 104 expected uint64 105 }{ 106 {0, 0, 1, 0x1}, 107 {1, 1, 1, 0x2}, 108 {1, 5, 2, 0x2a}, // 101010 109 {1, 4, 2, 0xa}, // 1010 110 } 111 112 for _, c := range bits { 113 actual := getBits(c.min, c.max, c.step) 114 if c.expected != actual { 115 t.Errorf("%d-%d/%d => expected %b, got %b", 116 c.min, c.max, c.step, c.expected, actual) 117 } 118 } 119 } 120 121 func TestParseScheduleErrors(t *testing.T) { 122 var tests = []struct{ expr, err string }{ 123 {"* 5 j * * *", "failed to parse int from"}, 124 {"@every Xm", "failed to parse duration"}, 125 {"@unrecognized", "unrecognized descriptor"}, 126 {"* * * *", "expected 5 to 6 fields"}, 127 {"", "empty spec string"}, 128 } 129 for _, c := range tests { 130 actual, err := secondParser.Parse(c.expr) 131 if err == nil || !strings.Contains(err.Error(), c.err) { 132 t.Errorf("%s => expected %v, got %v", c.expr, c.err, err) 133 } 134 if actual != nil { 135 t.Errorf("expected nil schedule on error, got %v", actual) 136 } 137 } 138 } 139 140 func TestParseSchedule(t *testing.T) { 141 tokyo, _ := time.LoadLocation("Asia/Tokyo") 142 entries := []struct { 143 parser Parser 144 expr string 145 expected Schedule 146 }{ 147 {secondParser, "0 5 * * * *", every5min(time.Local)}, 148 {standardParser, "5 * * * *", every5min(time.Local)}, 149 {secondParser, "CRON_TZ=UTC 0 5 * * * *", every5min(time.UTC)}, 150 {standardParser, "CRON_TZ=UTC 5 * * * *", every5min(time.UTC)}, 151 {secondParser, "CRON_TZ=Asia/Tokyo 0 5 * * * *", every5min(tokyo)}, 152 {secondParser, "@every 5m", ConstantDelaySchedule{5 * time.Minute}}, 153 {secondParser, "@midnight", midnight(time.Local)}, 154 {secondParser, "TZ=UTC @midnight", midnight(time.UTC)}, 155 {secondParser, "TZ=Asia/Tokyo @midnight", midnight(tokyo)}, 156 {secondParser, "@yearly", annual(time.Local)}, 157 {secondParser, "@annually", annual(time.Local)}, 158 { 159 parser: secondParser, 160 expr: "* 5 * * * *", 161 expected: &SpecSchedule{ 162 Second: all(seconds), 163 Minute: 1 << 5, 164 Hour: all(hours), 165 Dom: all(dom), 166 Month: all(months), 167 Dow: all(dow), 168 Location: time.Local, 169 }, 170 }, 171 } 172 173 for _, c := range entries { 174 actual, err := c.parser.Parse(c.expr) 175 if err != nil { 176 t.Errorf("%s => unexpected error %v", c.expr, err) 177 } 178 if !reflect.DeepEqual(actual, c.expected) { 179 t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual) 180 } 181 } 182 } 183 184 func TestOptionalSecondSchedule(t *testing.T) { 185 parser := NewParser(SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor) 186 entries := []struct { 187 expr string 188 expected Schedule 189 }{ 190 {"0 5 * * * *", every5min(time.Local)}, 191 {"5 5 * * * *", every5min5s(time.Local)}, 192 {"5 * * * *", every5min(time.Local)}, 193 } 194 195 for _, c := range entries { 196 actual, err := parser.Parse(c.expr) 197 if err != nil { 198 t.Errorf("%s => unexpected error %v", c.expr, err) 199 } 200 if !reflect.DeepEqual(actual, c.expected) { 201 t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual) 202 } 203 } 204 } 205 206 func TestNormalizeFields(t *testing.T) { 207 tests := []struct { 208 name string 209 input []string 210 options ParseOption 211 expected []string 212 }{ 213 { 214 "AllFields_NoOptional", 215 []string{"0", "5", "*", "*", "*", "*"}, 216 Second | Minute | Hour | Dom | Month | Dow | Descriptor, 217 []string{"0", "5", "*", "*", "*", "*"}, 218 }, 219 { 220 "AllFields_SecondOptional_Provided", 221 []string{"0", "5", "*", "*", "*", "*"}, 222 SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor, 223 []string{"0", "5", "*", "*", "*", "*"}, 224 }, 225 { 226 "AllFields_SecondOptional_NotProvided", 227 []string{"5", "*", "*", "*", "*"}, 228 SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor, 229 []string{"0", "5", "*", "*", "*", "*"}, 230 }, 231 { 232 "SubsetFields_NoOptional", 233 []string{"5", "15", "*"}, 234 Hour | Dom | Month, 235 []string{"0", "0", "5", "15", "*", "*"}, 236 }, 237 { 238 "SubsetFields_DowOptional_Provided", 239 []string{"5", "15", "*", "4"}, 240 Hour | Dom | Month | DowOptional, 241 []string{"0", "0", "5", "15", "*", "4"}, 242 }, 243 { 244 "SubsetFields_DowOptional_NotProvided", 245 []string{"5", "15", "*"}, 246 Hour | Dom | Month | DowOptional, 247 []string{"0", "0", "5", "15", "*", "*"}, 248 }, 249 { 250 "SubsetFields_SecondOptional_NotProvided", 251 []string{"5", "15", "*"}, 252 SecondOptional | Hour | Dom | Month, 253 []string{"0", "0", "5", "15", "*", "*"}, 254 }, 255 } 256 257 for _, test := range tests { 258 t.Run(test.name, func(t *testing.T) { 259 actual, err := normalizeFields(test.input, test.options) 260 if err != nil { 261 t.Errorf("unexpected error: %v", err) 262 } 263 if !reflect.DeepEqual(actual, test.expected) { 264 t.Errorf("expected %v, got %v", test.expected, actual) 265 } 266 }) 267 } 268 } 269 270 func TestNormalizeFields_Errors(t *testing.T) { 271 tests := []struct { 272 name string 273 input []string 274 options ParseOption 275 err string 276 }{ 277 { 278 "TwoOptionals", 279 []string{"0", "5", "*", "*", "*", "*"}, 280 SecondOptional | Minute | Hour | Dom | Month | DowOptional, 281 "", 282 }, 283 { 284 "TooManyFields", 285 []string{"0", "5", "*", "*"}, 286 SecondOptional | Minute | Hour, 287 "", 288 }, 289 { 290 "NoFields", 291 []string{}, 292 SecondOptional | Minute | Hour, 293 "", 294 }, 295 { 296 "TooFewFields", 297 []string{"*"}, 298 SecondOptional | Minute | Hour, 299 "", 300 }, 301 } 302 for _, test := range tests { 303 t.Run(test.name, func(t *testing.T) { 304 actual, err := normalizeFields(test.input, test.options) 305 if err == nil { 306 t.Errorf("expected an error, got none. results: %v", actual) 307 } 308 if !strings.Contains(err.Error(), test.err) { 309 t.Errorf("expected error %q, got %q", test.err, err.Error()) 310 } 311 }) 312 } 313 } 314 315 func TestStandardSpecSchedule(t *testing.T) { 316 entries := []struct { 317 expr string 318 expected Schedule 319 err string 320 }{ 321 { 322 expr: "5 * * * *", 323 expected: &SpecSchedule{1 << seconds.min, 1 << 5, all(hours), all(dom), all(months), all(dow), time.Local}, 324 }, 325 { 326 expr: "@every 5m", 327 expected: ConstantDelaySchedule{time.Duration(5) * time.Minute}, 328 }, 329 { 330 expr: "5 j * * *", 331 err: "failed to parse int from", 332 }, 333 { 334 expr: "* * * *", 335 err: "expected exactly 5 fields", 336 }, 337 } 338 339 for _, c := range entries { 340 actual, err := ParseStandard(c.expr) 341 if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) { 342 t.Errorf("%s => expected %v, got %v", c.expr, c.err, err) 343 } 344 if len(c.err) == 0 && err != nil { 345 t.Errorf("%s => unexpected error %v", c.expr, err) 346 } 347 if !reflect.DeepEqual(actual, c.expected) { 348 t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual) 349 } 350 } 351 } 352 353 func TestNoDescriptorParser(t *testing.T) { 354 parser := NewParser(Minute | Hour) 355 _, err := parser.Parse("@every 1m") 356 if err == nil { 357 t.Error("expected an error, got none") 358 } 359 } 360 361 func every5min(loc *time.Location) *SpecSchedule { 362 return &SpecSchedule{1 << 0, 1 << 5, all(hours), all(dom), all(months), all(dow), loc} 363 } 364 365 func every5min5s(loc *time.Location) *SpecSchedule { 366 return &SpecSchedule{1 << 5, 1 << 5, all(hours), all(dom), all(months), all(dow), loc} 367 } 368 369 func midnight(loc *time.Location) *SpecSchedule { 370 return &SpecSchedule{1, 1, 1, all(dom), all(months), all(dow), loc} 371 } 372 373 func annual(loc *time.Location) *SpecSchedule { 374 return &SpecSchedule{ 375 Second: 1 << seconds.min, 376 Minute: 1 << minutes.min, 377 Hour: 1 << hours.min, 378 Dom: 1 << dom.min, 379 Month: 1 << months.min, 380 Dow: all(dow), 381 Location: loc, 382 } 383 }