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  }