cron_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. package cron
  2. import (
  3. "bytes"
  4. "fmt"
  5. "log"
  6. "strings"
  7. "sync"
  8. "sync/atomic"
  9. "testing"
  10. "time"
  11. )
  12. // Many tests schedule a job for every second, and then wait at most a second
  13. // for it to run. This amount is just slightly larger than 1 second to
  14. // compensate for a few milliseconds of runtime.
  15. const OneSecond = 1*time.Second + 50*time.Millisecond
  16. type syncWriter struct {
  17. wr bytes.Buffer
  18. m sync.Mutex
  19. }
  20. func (sw *syncWriter) Write(data []byte) (n int, err error) {
  21. sw.m.Lock()
  22. n, err = sw.wr.Write(data)
  23. sw.m.Unlock()
  24. return
  25. }
  26. func (sw *syncWriter) String() string {
  27. sw.m.Lock()
  28. defer sw.m.Unlock()
  29. return sw.wr.String()
  30. }
  31. func newBufLogger(sw *syncWriter) *log.Logger {
  32. return log.New(sw, "", log.LstdFlags)
  33. }
  34. func TestFuncPanicRecovery(t *testing.T) {
  35. var buf syncWriter
  36. cron := New(WithParser(secondParser), WithPanicLogger(newBufLogger(&buf)))
  37. cron.Start()
  38. defer cron.Stop()
  39. cron.AddFunc("* * * * * ?", func() {
  40. panic("YOLO")
  41. })
  42. select {
  43. case <-time.After(OneSecond):
  44. if !strings.Contains(buf.String(), "YOLO") {
  45. t.Error("expected a panic to be logged, got none")
  46. }
  47. return
  48. }
  49. }
  50. type DummyJob struct{}
  51. func (d DummyJob) Run() {
  52. panic("YOLO")
  53. }
  54. func TestJobPanicRecovery(t *testing.T) {
  55. var job DummyJob
  56. var buf syncWriter
  57. cron := New(WithParser(secondParser), WithPanicLogger(newBufLogger(&buf)))
  58. cron.Start()
  59. defer cron.Stop()
  60. cron.AddJob("* * * * * ?", job)
  61. select {
  62. case <-time.After(OneSecond):
  63. if !strings.Contains(buf.String(), "YOLO") {
  64. t.Error("expected a panic to be logged, got none")
  65. }
  66. return
  67. }
  68. }
  69. // Start and stop cron with no entries.
  70. func TestNoEntries(t *testing.T) {
  71. cron := newWithSeconds()
  72. cron.Start()
  73. select {
  74. case <-time.After(OneSecond):
  75. t.Fatal("expected cron will be stopped immediately")
  76. case <-stop(cron):
  77. }
  78. }
  79. // Start, stop, then add an entry. Verify entry doesn't run.
  80. func TestStopCausesJobsToNotRun(t *testing.T) {
  81. wg := &sync.WaitGroup{}
  82. wg.Add(1)
  83. cron := newWithSeconds()
  84. cron.Start()
  85. cron.Stop()
  86. cron.AddFunc("* * * * * ?", func() { wg.Done() })
  87. select {
  88. case <-time.After(OneSecond):
  89. // No job ran!
  90. case <-wait(wg):
  91. t.Fatal("expected stopped cron does not run any job")
  92. }
  93. }
  94. // Add a job, start cron, expect it runs.
  95. func TestAddBeforeRunning(t *testing.T) {
  96. wg := &sync.WaitGroup{}
  97. wg.Add(1)
  98. cron := newWithSeconds()
  99. cron.AddFunc("* * * * * ?", func() { wg.Done() })
  100. cron.Start()
  101. defer cron.Stop()
  102. // Give cron 2 seconds to run our job (which is always activated).
  103. select {
  104. case <-time.After(OneSecond):
  105. t.Fatal("expected job runs")
  106. case <-wait(wg):
  107. }
  108. }
  109. // Start cron, add a job, expect it runs.
  110. func TestAddWhileRunning(t *testing.T) {
  111. wg := &sync.WaitGroup{}
  112. wg.Add(1)
  113. cron := newWithSeconds()
  114. cron.Start()
  115. defer cron.Stop()
  116. cron.AddFunc("* * * * * ?", func() { wg.Done() })
  117. select {
  118. case <-time.After(OneSecond):
  119. t.Fatal("expected job runs")
  120. case <-wait(wg):
  121. }
  122. }
  123. // Test for #34. Adding a job after calling start results in multiple job invocations
  124. func TestAddWhileRunningWithDelay(t *testing.T) {
  125. cron := newWithSeconds()
  126. cron.Start()
  127. defer cron.Stop()
  128. time.Sleep(5 * time.Second)
  129. var calls int64
  130. cron.AddFunc("* * * * * *", func() { atomic.AddInt64(&calls, 1) })
  131. <-time.After(OneSecond)
  132. if atomic.LoadInt64(&calls) != 1 {
  133. t.Errorf("called %d times, expected 1\n", calls)
  134. }
  135. }
  136. // Add a job, remove a job, start cron, expect nothing runs.
  137. func TestRemoveBeforeRunning(t *testing.T) {
  138. wg := &sync.WaitGroup{}
  139. wg.Add(1)
  140. cron := newWithSeconds()
  141. id, _ := cron.AddFunc("* * * * * ?", func() { wg.Done() })
  142. cron.Remove(id)
  143. cron.Start()
  144. defer cron.Stop()
  145. select {
  146. case <-time.After(OneSecond):
  147. // Success, shouldn't run
  148. case <-wait(wg):
  149. t.FailNow()
  150. }
  151. }
  152. // Start cron, add a job, remove it, expect it doesn't run.
  153. func TestRemoveWhileRunning(t *testing.T) {
  154. wg := &sync.WaitGroup{}
  155. wg.Add(1)
  156. cron := newWithSeconds()
  157. cron.Start()
  158. defer cron.Stop()
  159. id, _ := cron.AddFunc("* * * * * ?", func() { wg.Done() })
  160. cron.Remove(id)
  161. select {
  162. case <-time.After(OneSecond):
  163. case <-wait(wg):
  164. t.FailNow()
  165. }
  166. }
  167. // Test timing with Entries.
  168. func TestSnapshotEntries(t *testing.T) {
  169. wg := &sync.WaitGroup{}
  170. wg.Add(1)
  171. cron := New()
  172. cron.AddFunc("@every 2s", func() { wg.Done() })
  173. cron.Start()
  174. defer cron.Stop()
  175. // Cron should fire in 2 seconds. After 1 second, call Entries.
  176. select {
  177. case <-time.After(OneSecond):
  178. cron.Entries()
  179. }
  180. // Even though Entries was called, the cron should fire at the 2 second mark.
  181. select {
  182. case <-time.After(OneSecond):
  183. t.Error("expected job runs at 2 second mark")
  184. case <-wait(wg):
  185. }
  186. }
  187. // Test that the entries are correctly sorted.
  188. // Add a bunch of long-in-the-future entries, and an immediate entry, and ensure
  189. // that the immediate entry runs immediately.
  190. // Also: Test that multiple jobs run in the same instant.
  191. func TestMultipleEntries(t *testing.T) {
  192. wg := &sync.WaitGroup{}
  193. wg.Add(2)
  194. cron := newWithSeconds()
  195. cron.AddFunc("0 0 0 1 1 ?", func() {})
  196. cron.AddFunc("* * * * * ?", func() { wg.Done() })
  197. id1, _ := cron.AddFunc("* * * * * ?", func() { t.Fatal() })
  198. id2, _ := cron.AddFunc("* * * * * ?", func() { t.Fatal() })
  199. cron.AddFunc("0 0 0 31 12 ?", func() {})
  200. cron.AddFunc("* * * * * ?", func() { wg.Done() })
  201. cron.Remove(id1)
  202. cron.Start()
  203. cron.Remove(id2)
  204. defer cron.Stop()
  205. select {
  206. case <-time.After(OneSecond):
  207. t.Error("expected job run in proper order")
  208. case <-wait(wg):
  209. }
  210. }
  211. // Test running the same job twice.
  212. func TestRunningJobTwice(t *testing.T) {
  213. wg := &sync.WaitGroup{}
  214. wg.Add(2)
  215. cron := newWithSeconds()
  216. cron.AddFunc("0 0 0 1 1 ?", func() {})
  217. cron.AddFunc("0 0 0 31 12 ?", func() {})
  218. cron.AddFunc("* * * * * ?", func() { wg.Done() })
  219. cron.Start()
  220. defer cron.Stop()
  221. select {
  222. case <-time.After(2 * OneSecond):
  223. t.Error("expected job fires 2 times")
  224. case <-wait(wg):
  225. }
  226. }
  227. func TestRunningMultipleSchedules(t *testing.T) {
  228. wg := &sync.WaitGroup{}
  229. wg.Add(2)
  230. cron := newWithSeconds()
  231. cron.AddFunc("0 0 0 1 1 ?", func() {})
  232. cron.AddFunc("0 0 0 31 12 ?", func() {})
  233. cron.AddFunc("* * * * * ?", func() { wg.Done() })
  234. cron.Schedule(Every(time.Minute), FuncJob(func() {}))
  235. cron.Schedule(Every(time.Second), FuncJob(func() { wg.Done() }))
  236. cron.Schedule(Every(time.Hour), FuncJob(func() {}))
  237. cron.Start()
  238. defer cron.Stop()
  239. select {
  240. case <-time.After(2 * OneSecond):
  241. t.Error("expected job fires 2 times")
  242. case <-wait(wg):
  243. }
  244. }
  245. // Test that the cron is run in the local time zone (as opposed to UTC).
  246. func TestLocalTimezone(t *testing.T) {
  247. wg := &sync.WaitGroup{}
  248. wg.Add(2)
  249. now := time.Now()
  250. // FIX: Issue #205
  251. // This calculation doesn't work in seconds 58 or 59.
  252. // Take the easy way out and sleep.
  253. if now.Second() >= 58 {
  254. time.Sleep(2 * time.Second)
  255. now = time.Now()
  256. }
  257. spec := fmt.Sprintf("%d,%d %d %d %d %d ?",
  258. now.Second()+1, now.Second()+2, now.Minute(), now.Hour(), now.Day(), now.Month())
  259. cron := newWithSeconds()
  260. cron.AddFunc(spec, func() { wg.Done() })
  261. cron.Start()
  262. defer cron.Stop()
  263. select {
  264. case <-time.After(OneSecond * 2):
  265. t.Error("expected job fires 2 times")
  266. case <-wait(wg):
  267. }
  268. }
  269. // Test that the cron is run in the given time zone (as opposed to local).
  270. func TestNonLocalTimezone(t *testing.T) {
  271. wg := &sync.WaitGroup{}
  272. wg.Add(2)
  273. loc, err := time.LoadLocation("Atlantic/Cape_Verde")
  274. if err != nil {
  275. fmt.Printf("Failed to load time zone Atlantic/Cape_Verde: %+v", err)
  276. t.Fail()
  277. }
  278. now := time.Now().In(loc)
  279. // FIX: Issue #205
  280. // This calculation doesn't work in seconds 58 or 59.
  281. // Take the easy way out and sleep.
  282. if now.Second() >= 58 {
  283. time.Sleep(2 * time.Second)
  284. now = time.Now().In(loc)
  285. }
  286. spec := fmt.Sprintf("%d,%d %d %d %d %d ?",
  287. now.Second()+1, now.Second()+2, now.Minute(), now.Hour(), now.Day(), now.Month())
  288. cron := New(WithLocation(loc), WithParser(secondParser))
  289. cron.AddFunc(spec, func() { wg.Done() })
  290. cron.Start()
  291. defer cron.Stop()
  292. select {
  293. case <-time.After(OneSecond * 2):
  294. t.Error("expected job fires 2 times")
  295. case <-wait(wg):
  296. }
  297. }
  298. // Test that calling stop before start silently returns without
  299. // blocking the stop channel.
  300. func TestStopWithoutStart(t *testing.T) {
  301. cron := New()
  302. cron.Stop()
  303. }
  304. type testJob struct {
  305. wg *sync.WaitGroup
  306. name string
  307. }
  308. func (t testJob) Run() {
  309. t.wg.Done()
  310. }
  311. // Test that adding an invalid job spec returns an error
  312. func TestInvalidJobSpec(t *testing.T) {
  313. cron := New()
  314. _, err := cron.AddJob("this will not parse", nil)
  315. if err == nil {
  316. t.Errorf("expected an error with invalid spec, got nil")
  317. }
  318. }
  319. // Test blocking run method behaves as Start()
  320. func TestBlockingRun(t *testing.T) {
  321. wg := &sync.WaitGroup{}
  322. wg.Add(1)
  323. cron := newWithSeconds()
  324. cron.AddFunc("* * * * * ?", func() { wg.Done() })
  325. var unblockChan = make(chan struct{})
  326. go func() {
  327. cron.Run()
  328. close(unblockChan)
  329. }()
  330. defer cron.Stop()
  331. select {
  332. case <-time.After(OneSecond):
  333. t.Error("expected job fires")
  334. case <-unblockChan:
  335. t.Error("expected that Run() blocks")
  336. case <-wait(wg):
  337. }
  338. }
  339. // Test that double-running is a no-op
  340. func TestStartNoop(t *testing.T) {
  341. var tickChan = make(chan struct{}, 2)
  342. cron := newWithSeconds()
  343. cron.AddFunc("* * * * * ?", func() {
  344. tickChan <- struct{}{}
  345. })
  346. cron.Start()
  347. defer cron.Stop()
  348. // Wait for the first firing to ensure the runner is going
  349. <-tickChan
  350. cron.Start()
  351. <-tickChan
  352. // Fail if this job fires again in a short period, indicating a double-run
  353. select {
  354. case <-time.After(time.Millisecond):
  355. case <-tickChan:
  356. t.Error("expected job fires exactly twice")
  357. }
  358. }
  359. // Simple test using Runnables.
  360. func TestJob(t *testing.T) {
  361. wg := &sync.WaitGroup{}
  362. wg.Add(1)
  363. cron := newWithSeconds()
  364. cron.AddJob("0 0 0 30 Feb ?", testJob{wg, "job0"})
  365. cron.AddJob("0 0 0 1 1 ?", testJob{wg, "job1"})
  366. job2, _ := cron.AddJob("* * * * * ?", testJob{wg, "job2"})
  367. cron.AddJob("1 0 0 1 1 ?", testJob{wg, "job3"})
  368. cron.Schedule(Every(5*time.Second+5*time.Nanosecond), testJob{wg, "job4"})
  369. job5 := cron.Schedule(Every(5*time.Minute), testJob{wg, "job5"})
  370. // Test getting an Entry pre-Start.
  371. if actualName := cron.Entry(job2).Job.(testJob).name; actualName != "job2" {
  372. t.Error("wrong job retrieved:", actualName)
  373. }
  374. if actualName := cron.Entry(job5).Job.(testJob).name; actualName != "job5" {
  375. t.Error("wrong job retrieved:", actualName)
  376. }
  377. cron.Start()
  378. defer cron.Stop()
  379. select {
  380. case <-time.After(OneSecond):
  381. t.FailNow()
  382. case <-wait(wg):
  383. }
  384. // Ensure the entries are in the right order.
  385. expecteds := []string{"job2", "job4", "job5", "job1", "job3", "job0"}
  386. var actuals []string
  387. for _, entry := range cron.Entries() {
  388. actuals = append(actuals, entry.Job.(testJob).name)
  389. }
  390. for i, expected := range expecteds {
  391. if actuals[i] != expected {
  392. t.Fatalf("Jobs not in the right order. (expected) %s != %s (actual)", expecteds, actuals)
  393. }
  394. }
  395. // Test getting Entries.
  396. if actualName := cron.Entry(job2).Job.(testJob).name; actualName != "job2" {
  397. t.Error("wrong job retrieved:", actualName)
  398. }
  399. if actualName := cron.Entry(job5).Job.(testJob).name; actualName != "job5" {
  400. t.Error("wrong job retrieved:", actualName)
  401. }
  402. }
  403. // Issue #206
  404. // Ensure that the next run of a job after removing an entry is accurate.
  405. func TestScheduleAfterRemoval(t *testing.T) {
  406. var wg1 sync.WaitGroup
  407. var wg2 sync.WaitGroup
  408. wg1.Add(1)
  409. wg2.Add(1)
  410. // The first time this job is run, set a timer and remove the other job
  411. // 750ms later. Correct behavior would be to still run the job again in
  412. // 250ms, but the bug would cause it to run instead 1s later.
  413. var calls int
  414. var mu sync.Mutex
  415. cron := newWithSeconds()
  416. hourJob := cron.Schedule(Every(time.Hour), FuncJob(func() {}))
  417. cron.Schedule(Every(time.Second), FuncJob(func() {
  418. mu.Lock()
  419. defer mu.Unlock()
  420. switch calls {
  421. case 0:
  422. wg1.Done()
  423. calls++
  424. case 1:
  425. time.Sleep(750 * time.Millisecond)
  426. cron.Remove(hourJob)
  427. calls++
  428. case 2:
  429. calls++
  430. wg2.Done()
  431. case 3:
  432. panic("unexpected 3rd call")
  433. }
  434. }))
  435. cron.Start()
  436. defer cron.Stop()
  437. // the first run might be any length of time 0 - 1s, since the schedule
  438. // rounds to the second. wait for the first run to true up.
  439. wg1.Wait()
  440. select {
  441. case <-time.After(2 * OneSecond):
  442. t.Error("expected job fires 2 times")
  443. case <-wait(&wg2):
  444. }
  445. }
  446. type ZeroSchedule struct{}
  447. func (*ZeroSchedule) Next(time.Time) time.Time {
  448. return time.Time{}
  449. }
  450. // Tests that job without time does not run
  451. func TestJobWithZeroTimeDoesNotRun(t *testing.T) {
  452. cron := newWithSeconds()
  453. var calls int64
  454. cron.AddFunc("* * * * * *", func() { atomic.AddInt64(&calls, 1) })
  455. cron.Schedule(new(ZeroSchedule), FuncJob(func() { t.Error("expected zero task will not run") }))
  456. cron.Start()
  457. defer cron.Stop()
  458. <-time.After(OneSecond)
  459. if atomic.LoadInt64(&calls) != 1 {
  460. t.Errorf("called %d times, expected 1\n", calls)
  461. }
  462. }
  463. func wait(wg *sync.WaitGroup) chan bool {
  464. ch := make(chan bool)
  465. go func() {
  466. wg.Wait()
  467. ch <- true
  468. }()
  469. return ch
  470. }
  471. func stop(cron *Cron) chan bool {
  472. ch := make(chan bool)
  473. go func() {
  474. cron.Stop()
  475. ch <- true
  476. }()
  477. return ch
  478. }
  479. // newWithSeconds returns a Cron with the seconds field enabled.
  480. func newWithSeconds() *Cron {
  481. return New(WithParser(secondParser))
  482. }