git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/cron/cron_test.go (about) 1 package cron 2 3 import ( 4 "bytes" 5 "fmt" 6 "log" 7 "strings" 8 "sync" 9 "sync/atomic" 10 "testing" 11 "time" 12 ) 13 14 // Many tests schedule a job for every second, and then wait at most a second 15 // for it to run. This amount is just slightly larger than 1 second to 16 // compensate for a few milliseconds of runtime. 17 const OneSecond = 1*time.Second + 50*time.Millisecond 18 19 type syncWriter struct { 20 wr bytes.Buffer 21 m sync.Mutex 22 } 23 24 func (sw *syncWriter) Write(data []byte) (n int, err error) { 25 sw.m.Lock() 26 n, err = sw.wr.Write(data) 27 sw.m.Unlock() 28 return 29 } 30 31 func (sw *syncWriter) String() string { 32 sw.m.Lock() 33 defer sw.m.Unlock() 34 return sw.wr.String() 35 } 36 37 func newBufLogger(sw *syncWriter) Logger { 38 return PrintfLogger(log.New(sw, "", log.LstdFlags)) 39 } 40 41 func TestFuncPanicRecovery(t *testing.T) { 42 var buf syncWriter 43 cron := New(WithParser(secondParser), 44 WithChain(Recover(newBufLogger(&buf)))) 45 cron.Start() 46 defer cron.Stop() 47 cron.AddFunc("* * * * * ?", func() { 48 panic("YOLO") 49 }) 50 51 select { 52 case <-time.After(OneSecond): 53 if !strings.Contains(buf.String(), "YOLO") { 54 t.Error("expected a panic to be logged, got none") 55 } 56 return 57 } 58 } 59 60 type DummyJob struct{} 61 62 func (d DummyJob) Run() { 63 panic("YOLO") 64 } 65 66 func TestJobPanicRecovery(t *testing.T) { 67 var job DummyJob 68 69 var buf syncWriter 70 cron := New(WithParser(secondParser), 71 WithChain(Recover(newBufLogger(&buf)))) 72 cron.Start() 73 defer cron.Stop() 74 cron.AddJob("* * * * * ?", job) 75 76 select { 77 case <-time.After(OneSecond): 78 if !strings.Contains(buf.String(), "YOLO") { 79 t.Error("expected a panic to be logged, got none") 80 } 81 return 82 } 83 } 84 85 // Start and stop cron with no entries. 86 func TestNoEntries(t *testing.T) { 87 cron := newWithSeconds() 88 cron.Start() 89 90 select { 91 case <-time.After(OneSecond): 92 t.Fatal("expected cron will be stopped immediately") 93 case <-stop(cron): 94 } 95 } 96 97 // Start, stop, then add an entry. Verify entry doesn't run. 98 func TestStopCausesJobsToNotRun(t *testing.T) { 99 wg := &sync.WaitGroup{} 100 wg.Add(1) 101 102 cron := newWithSeconds() 103 cron.Start() 104 cron.Stop() 105 cron.AddFunc("* * * * * ?", func() { wg.Done() }) 106 107 select { 108 case <-time.After(OneSecond): 109 // No job ran! 110 case <-wait(wg): 111 t.Fatal("expected stopped cron does not run any job") 112 } 113 } 114 115 // Add a job, start cron, expect it runs. 116 func TestAddBeforeRunning(t *testing.T) { 117 wg := &sync.WaitGroup{} 118 wg.Add(1) 119 120 cron := newWithSeconds() 121 cron.AddFunc("* * * * * ?", func() { wg.Done() }) 122 cron.Start() 123 defer cron.Stop() 124 125 // Give cron 2 seconds to run our job (which is always activated). 126 select { 127 case <-time.After(OneSecond): 128 t.Fatal("expected job runs") 129 case <-wait(wg): 130 } 131 } 132 133 // Start cron, add a job, expect it runs. 134 func TestAddWhileRunning(t *testing.T) { 135 wg := &sync.WaitGroup{} 136 wg.Add(1) 137 138 cron := newWithSeconds() 139 cron.Start() 140 defer cron.Stop() 141 cron.AddFunc("* * * * * ?", func() { wg.Done() }) 142 143 select { 144 case <-time.After(OneSecond): 145 t.Fatal("expected job runs") 146 case <-wait(wg): 147 } 148 } 149 150 // Test for #34. Adding a job after calling start results in multiple job invocations 151 func TestAddWhileRunningWithDelay(t *testing.T) { 152 cron := newWithSeconds() 153 cron.Start() 154 defer cron.Stop() 155 time.Sleep(5 * time.Second) 156 var calls int64 157 cron.AddFunc("* * * * * *", func() { atomic.AddInt64(&calls, 1) }) 158 159 <-time.After(OneSecond) 160 if atomic.LoadInt64(&calls) != 1 { 161 t.Errorf("called %d times, expected 1\n", calls) 162 } 163 } 164 165 // Add a job, remove a job, start cron, expect nothing runs. 166 func TestRemoveBeforeRunning(t *testing.T) { 167 wg := &sync.WaitGroup{} 168 wg.Add(1) 169 170 cron := newWithSeconds() 171 id, _ := cron.AddFunc("* * * * * ?", func() { wg.Done() }) 172 cron.Remove(id) 173 cron.Start() 174 defer cron.Stop() 175 176 select { 177 case <-time.After(OneSecond): 178 // Success, shouldn't run 179 case <-wait(wg): 180 t.FailNow() 181 } 182 } 183 184 // Start cron, add a job, remove it, expect it doesn't run. 185 func TestRemoveWhileRunning(t *testing.T) { 186 wg := &sync.WaitGroup{} 187 wg.Add(1) 188 189 cron := newWithSeconds() 190 cron.Start() 191 defer cron.Stop() 192 id, _ := cron.AddFunc("* * * * * ?", func() { wg.Done() }) 193 cron.Remove(id) 194 195 select { 196 case <-time.After(OneSecond): 197 case <-wait(wg): 198 t.FailNow() 199 } 200 } 201 202 // Test timing with Entries. 203 func TestSnapshotEntries(t *testing.T) { 204 wg := &sync.WaitGroup{} 205 wg.Add(1) 206 207 cron := New() 208 cron.AddFunc("@every 2s", func() { wg.Done() }) 209 cron.Start() 210 defer cron.Stop() 211 212 // Cron should fire in 2 seconds. After 1 second, call Entries. 213 select { 214 case <-time.After(OneSecond): 215 cron.Entries() 216 } 217 218 // Even though Entries was called, the cron should fire at the 2 second mark. 219 select { 220 case <-time.After(OneSecond): 221 t.Error("expected job runs at 2 second mark") 222 case <-wait(wg): 223 } 224 } 225 226 // Test that the entries are correctly sorted. 227 // Add a bunch of long-in-the-future entries, and an immediate entry, and ensure 228 // that the immediate entry runs immediately. 229 // Also: Test that multiple jobs run in the same instant. 230 func TestMultipleEntries(t *testing.T) { 231 wg := &sync.WaitGroup{} 232 wg.Add(2) 233 234 cron := newWithSeconds() 235 cron.AddFunc("0 0 0 1 1 ?", func() {}) 236 cron.AddFunc("* * * * * ?", func() { wg.Done() }) 237 id1, _ := cron.AddFunc("* * * * * ?", func() { t.Fatal() }) 238 id2, _ := cron.AddFunc("* * * * * ?", func() { t.Fatal() }) 239 cron.AddFunc("0 0 0 31 12 ?", func() {}) 240 cron.AddFunc("* * * * * ?", func() { wg.Done() }) 241 242 cron.Remove(id1) 243 cron.Start() 244 cron.Remove(id2) 245 defer cron.Stop() 246 247 select { 248 case <-time.After(OneSecond): 249 t.Error("expected job run in proper order") 250 case <-wait(wg): 251 } 252 } 253 254 // Test running the same job twice. 255 func TestRunningJobTwice(t *testing.T) { 256 wg := &sync.WaitGroup{} 257 wg.Add(2) 258 259 cron := newWithSeconds() 260 cron.AddFunc("0 0 0 1 1 ?", func() {}) 261 cron.AddFunc("0 0 0 31 12 ?", func() {}) 262 cron.AddFunc("* * * * * ?", func() { wg.Done() }) 263 264 cron.Start() 265 defer cron.Stop() 266 267 select { 268 case <-time.After(2 * OneSecond): 269 t.Error("expected job fires 2 times") 270 case <-wait(wg): 271 } 272 } 273 274 func TestRunningMultipleSchedules(t *testing.T) { 275 wg := &sync.WaitGroup{} 276 wg.Add(2) 277 278 cron := newWithSeconds() 279 cron.AddFunc("0 0 0 1 1 ?", func() {}) 280 cron.AddFunc("0 0 0 31 12 ?", func() {}) 281 cron.AddFunc("* * * * * ?", func() { wg.Done() }) 282 cron.Schedule(Every(time.Minute), FuncJob(func() {})) 283 cron.Schedule(Every(time.Second), FuncJob(func() { wg.Done() })) 284 cron.Schedule(Every(time.Hour), FuncJob(func() {})) 285 286 cron.Start() 287 defer cron.Stop() 288 289 select { 290 case <-time.After(2 * OneSecond): 291 t.Error("expected job fires 2 times") 292 case <-wait(wg): 293 } 294 } 295 296 // Test that the cron is run in the local time zone (as opposed to UTC). 297 func TestLocalTimezone(t *testing.T) { 298 wg := &sync.WaitGroup{} 299 wg.Add(2) 300 301 now := time.Now() 302 // FIX: Issue #205 303 // This calculation doesn't work in seconds 58 or 59. 304 // Take the easy way out and sleep. 305 if now.Second() >= 58 { 306 time.Sleep(2 * time.Second) 307 now = time.Now() 308 } 309 spec := fmt.Sprintf("%d,%d %d %d %d %d ?", 310 now.Second()+1, now.Second()+2, now.Minute(), now.Hour(), now.Day(), now.Month()) 311 312 cron := newWithSeconds() 313 cron.AddFunc(spec, func() { wg.Done() }) 314 cron.Start() 315 defer cron.Stop() 316 317 select { 318 case <-time.After(OneSecond * 2): 319 t.Error("expected job fires 2 times") 320 case <-wait(wg): 321 } 322 } 323 324 // Test that the cron is run in the given time zone (as opposed to local). 325 func TestNonLocalTimezone(t *testing.T) { 326 wg := &sync.WaitGroup{} 327 wg.Add(2) 328 329 loc, err := time.LoadLocation("Atlantic/Cape_Verde") 330 if err != nil { 331 fmt.Printf("Failed to load time zone Atlantic/Cape_Verde: %+v", err) 332 t.Fail() 333 } 334 335 now := time.Now().In(loc) 336 // FIX: Issue #205 337 // This calculation doesn't work in seconds 58 or 59. 338 // Take the easy way out and sleep. 339 if now.Second() >= 58 { 340 time.Sleep(2 * time.Second) 341 now = time.Now().In(loc) 342 } 343 spec := fmt.Sprintf("%d,%d %d %d %d %d ?", 344 now.Second()+1, now.Second()+2, now.Minute(), now.Hour(), now.Day(), now.Month()) 345 346 cron := New(WithLocation(loc), WithParser(secondParser)) 347 cron.AddFunc(spec, func() { wg.Done() }) 348 cron.Start() 349 defer cron.Stop() 350 351 select { 352 case <-time.After(OneSecond * 2): 353 t.Error("expected job fires 2 times") 354 case <-wait(wg): 355 } 356 } 357 358 // Test that calling stop before start silently returns without 359 // blocking the stop channel. 360 func TestStopWithoutStart(t *testing.T) { 361 cron := New() 362 cron.Stop() 363 } 364 365 type testJob struct { 366 wg *sync.WaitGroup 367 name string 368 } 369 370 func (t testJob) Run() { 371 t.wg.Done() 372 } 373 374 // Test that adding an invalid job spec returns an error 375 func TestInvalidJobSpec(t *testing.T) { 376 cron := New() 377 _, err := cron.AddJob("this will not parse", nil) 378 if err == nil { 379 t.Errorf("expected an error with invalid spec, got nil") 380 } 381 } 382 383 // Test blocking run method behaves as Start() 384 func TestBlockingRun(t *testing.T) { 385 wg := &sync.WaitGroup{} 386 wg.Add(1) 387 388 cron := newWithSeconds() 389 cron.AddFunc("* * * * * ?", func() { wg.Done() }) 390 391 var unblockChan = make(chan struct{}) 392 393 go func() { 394 cron.Run() 395 close(unblockChan) 396 }() 397 defer cron.Stop() 398 399 select { 400 case <-time.After(OneSecond): 401 t.Error("expected job fires") 402 case <-unblockChan: 403 t.Error("expected that Run() blocks") 404 case <-wait(wg): 405 } 406 } 407 408 // Test that double-running is a no-op 409 func TestStartNoop(t *testing.T) { 410 var tickChan = make(chan struct{}, 2) 411 412 cron := newWithSeconds() 413 cron.AddFunc("* * * * * ?", func() { 414 tickChan <- struct{}{} 415 }) 416 417 cron.Start() 418 defer cron.Stop() 419 420 // Wait for the first firing to ensure the runner is going 421 <-tickChan 422 423 cron.Start() 424 425 <-tickChan 426 427 // Fail if this job fires again in a short period, indicating a double-run 428 select { 429 case <-time.After(time.Millisecond): 430 case <-tickChan: 431 t.Error("expected job fires exactly twice") 432 } 433 } 434 435 // Simple test using Runnables. 436 func TestJob(t *testing.T) { 437 wg := &sync.WaitGroup{} 438 wg.Add(1) 439 440 cron := newWithSeconds() 441 cron.AddJob("0 0 0 30 Feb ?", testJob{wg, "job0"}) 442 cron.AddJob("0 0 0 1 1 ?", testJob{wg, "job1"}) 443 job2, _ := cron.AddJob("* * * * * ?", testJob{wg, "job2"}) 444 cron.AddJob("1 0 0 1 1 ?", testJob{wg, "job3"}) 445 cron.Schedule(Every(5*time.Second+5*time.Nanosecond), testJob{wg, "job4"}) 446 job5 := cron.Schedule(Every(5*time.Minute), testJob{wg, "job5"}) 447 448 // Test getting an Entry pre-Start. 449 if actualName := cron.Entry(job2).Job.(testJob).name; actualName != "job2" { 450 t.Error("wrong job retrieved:", actualName) 451 } 452 if actualName := cron.Entry(job5).Job.(testJob).name; actualName != "job5" { 453 t.Error("wrong job retrieved:", actualName) 454 } 455 456 cron.Start() 457 defer cron.Stop() 458 459 select { 460 case <-time.After(OneSecond): 461 t.FailNow() 462 case <-wait(wg): 463 } 464 465 // Ensure the entries are in the right order. 466 expecteds := []string{"job2", "job4", "job5", "job1", "job3", "job0"} 467 468 var actuals []string 469 for _, entry := range cron.Entries() { 470 actuals = append(actuals, entry.Job.(testJob).name) 471 } 472 473 for i, expected := range expecteds { 474 if actuals[i] != expected { 475 t.Fatalf("Jobs not in the right order. (expected) %s != %s (actual)", expecteds, actuals) 476 } 477 } 478 479 // Test getting Entries. 480 if actualName := cron.Entry(job2).Job.(testJob).name; actualName != "job2" { 481 t.Error("wrong job retrieved:", actualName) 482 } 483 if actualName := cron.Entry(job5).Job.(testJob).name; actualName != "job5" { 484 t.Error("wrong job retrieved:", actualName) 485 } 486 } 487 488 // Issue #206 489 // Ensure that the next run of a job after removing an entry is accurate. 490 func TestScheduleAfterRemoval(t *testing.T) { 491 var wg1 sync.WaitGroup 492 var wg2 sync.WaitGroup 493 wg1.Add(1) 494 wg2.Add(1) 495 496 // The first time this job is run, set a timer and remove the other job 497 // 750ms later. Correct behavior would be to still run the job again in 498 // 250ms, but the bug would cause it to run instead 1s later. 499 500 var calls int 501 var mu sync.Mutex 502 503 cron := newWithSeconds() 504 hourJob := cron.Schedule(Every(time.Hour), FuncJob(func() {})) 505 cron.Schedule(Every(time.Second), FuncJob(func() { 506 mu.Lock() 507 defer mu.Unlock() 508 switch calls { 509 case 0: 510 wg1.Done() 511 calls++ 512 case 1: 513 time.Sleep(750 * time.Millisecond) 514 cron.Remove(hourJob) 515 calls++ 516 case 2: 517 calls++ 518 wg2.Done() 519 case 3: 520 panic("unexpected 3rd call") 521 } 522 })) 523 524 cron.Start() 525 defer cron.Stop() 526 527 // the first run might be any length of time 0 - 1s, since the schedule 528 // rounds to the second. wait for the first run to true up. 529 wg1.Wait() 530 531 select { 532 case <-time.After(2 * OneSecond): 533 t.Error("expected job fires 2 times") 534 case <-wait(&wg2): 535 } 536 } 537 538 type ZeroSchedule struct{} 539 540 func (*ZeroSchedule) Next(time.Time) time.Time { 541 return time.Time{} 542 } 543 544 // Tests that job without time does not run 545 func TestJobWithZeroTimeDoesNotRun(t *testing.T) { 546 cron := newWithSeconds() 547 var calls int64 548 cron.AddFunc("* * * * * *", func() { atomic.AddInt64(&calls, 1) }) 549 cron.Schedule(new(ZeroSchedule), FuncJob(func() { t.Error("expected zero task will not run") })) 550 cron.Start() 551 defer cron.Stop() 552 <-time.After(OneSecond) 553 if atomic.LoadInt64(&calls) != 1 { 554 t.Errorf("called %d times, expected 1\n", calls) 555 } 556 } 557 558 func TestStopAndWait(t *testing.T) { 559 t.Run("nothing running, returns immediately", func(t *testing.T) { 560 cron := newWithSeconds() 561 cron.Start() 562 ctx := cron.Stop() 563 select { 564 case <-ctx.Done(): 565 case <-time.After(time.Millisecond): 566 t.Error("context was not done immediately") 567 } 568 }) 569 570 t.Run("repeated calls to Stop", func(t *testing.T) { 571 cron := newWithSeconds() 572 cron.Start() 573 _ = cron.Stop() 574 time.Sleep(time.Millisecond) 575 ctx := cron.Stop() 576 select { 577 case <-ctx.Done(): 578 case <-time.After(time.Millisecond): 579 t.Error("context was not done immediately") 580 } 581 }) 582 583 t.Run("a couple fast jobs added, still returns immediately", func(t *testing.T) { 584 cron := newWithSeconds() 585 cron.AddFunc("* * * * * *", func() {}) 586 cron.Start() 587 cron.AddFunc("* * * * * *", func() {}) 588 cron.AddFunc("* * * * * *", func() {}) 589 cron.AddFunc("* * * * * *", func() {}) 590 time.Sleep(time.Second) 591 ctx := cron.Stop() 592 select { 593 case <-ctx.Done(): 594 case <-time.After(time.Millisecond): 595 t.Error("context was not done immediately") 596 } 597 }) 598 599 t.Run("a couple fast jobs and a slow job added, waits for slow job", func(t *testing.T) { 600 cron := newWithSeconds() 601 cron.AddFunc("* * * * * *", func() {}) 602 cron.Start() 603 cron.AddFunc("* * * * * *", func() { time.Sleep(2 * time.Second) }) 604 cron.AddFunc("* * * * * *", func() {}) 605 time.Sleep(time.Second) 606 607 ctx := cron.Stop() 608 609 // Verify that it is not done for at least 750ms 610 select { 611 case <-ctx.Done(): 612 t.Error("context was done too quickly immediately") 613 case <-time.After(750 * time.Millisecond): 614 // expected, because the job sleeping for 1 second is still running 615 } 616 617 // Verify that it IS done in the next 500ms (giving 250ms buffer) 618 select { 619 case <-ctx.Done(): 620 // expected 621 case <-time.After(1500 * time.Millisecond): 622 t.Error("context not done after job should have completed") 623 } 624 }) 625 626 t.Run("repeated calls to stop, waiting for completion and after", func(t *testing.T) { 627 cron := newWithSeconds() 628 cron.AddFunc("* * * * * *", func() {}) 629 cron.AddFunc("* * * * * *", func() { time.Sleep(2 * time.Second) }) 630 cron.Start() 631 cron.AddFunc("* * * * * *", func() {}) 632 time.Sleep(time.Second) 633 ctx := cron.Stop() 634 ctx2 := cron.Stop() 635 636 // Verify that it is not done for at least 1500ms 637 select { 638 case <-ctx.Done(): 639 t.Error("context was done too quickly immediately") 640 case <-ctx2.Done(): 641 t.Error("context2 was done too quickly immediately") 642 case <-time.After(1500 * time.Millisecond): 643 // expected, because the job sleeping for 2 seconds is still running 644 } 645 646 // Verify that it IS done in the next 1s (giving 500ms buffer) 647 select { 648 case <-ctx.Done(): 649 // expected 650 case <-time.After(time.Second): 651 t.Error("context not done after job should have completed") 652 } 653 654 // Verify that ctx2 is also done. 655 select { 656 case <-ctx2.Done(): 657 // expected 658 case <-time.After(time.Millisecond): 659 t.Error("context2 not done even though context1 is") 660 } 661 662 // Verify that a new context retrieved from stop is immediately done. 663 ctx3 := cron.Stop() 664 select { 665 case <-ctx3.Done(): 666 // expected 667 case <-time.After(time.Millisecond): 668 t.Error("context not done even when cron Stop is completed") 669 } 670 671 }) 672 } 673 674 func TestMultiThreadedStartAndStop(t *testing.T) { 675 cron := New() 676 go cron.Run() 677 time.Sleep(2 * time.Millisecond) 678 cron.Stop() 679 } 680 681 func wait(wg *sync.WaitGroup) chan bool { 682 ch := make(chan bool) 683 go func() { 684 wg.Wait() 685 ch <- true 686 }() 687 return ch 688 } 689 690 func stop(cron *Cron) chan bool { 691 ch := make(chan bool) 692 go func() { 693 cron.Stop() 694 ch <- true 695 }() 696 return ch 697 } 698 699 // newWithSeconds returns a Cron with the seconds field enabled. 700 func newWithSeconds() *Cron { 701 return New(WithParser(secondParser), WithChain()) 702 }