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