diff --git a/README.md b/README.md index 143bd36..a20966f 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,13 @@ cron.AddJob(Job{ }) ``` +## Tests + +Pre-requisites to run the tests locally: +- [Install etcd](https://etcd.io/docs/v3.4/install/) +- Launch etcd: `etcd --logger=zap` +- Proceed to run tests: `go test` + ## Release a New Version Bump new version number in `CHANGELOG.md` and `README.md`. diff --git a/cron.go b/cron.go index b887c8e..5b55183 100644 --- a/cron.go +++ b/cron.go @@ -164,6 +164,16 @@ func New(opts ...CronOpt) (*Cron, error) { return cron, nil } +// GetJob retrieves a job by name. +func (c *Cron) GetJob(jobName string) *Job { + for _, entry := range c.entries { + if entry.Job.Name == jobName { + return &entry.Job + } + } + return nil +} + // AddFunc adds a Job to the Cron to be run on the given schedule. func (c *Cron) AddJob(job Job) error { schedule, err := Parse(job.Rhythm) @@ -174,6 +184,25 @@ func (c *Cron) AddJob(job Job) error { return nil } +// DeleteJob deletes a job by name. +func (c *Cron) DeleteJob(jobName string) error { + var updatedEntries []*Entry + found := false + for _, entry := range c.entries { + if entry.Job.Name == jobName { + found = true + continue + } + // Keep the entries that don't match the specified jobName + updatedEntries = append(updatedEntries, entry) + } + if !found { + return fmt.Errorf("job not found: %s", jobName) + } + c.entries = updatedEntries + return nil +} + // Schedule adds a Job to the Cron to be run on the given schedule. func (c *Cron) Schedule(schedule Schedule, job Job) { entry := &Entry{ @@ -188,6 +217,18 @@ func (c *Cron) Schedule(schedule Schedule, job Job) { c.add <- entry } +// ListJobsByPrefix returns the list of jobs with the relevant prefix +func (c *Cron) ListJobsByPrefix(prefix string) []*Job { + var prefixJobs []*Job + for _, entry := range c.entries { + if strings.HasPrefix(entry.Job.Name, prefix) { + // Job belongs to the specified prefix + prefixJobs = append(prefixJobs, &entry.Job) + } + } + return prefixJobs +} + // Entries returns a snapshot of the cron entries. func (c *Cron) Entries() []*Entry { if c.running { diff --git a/cron_test.go b/cron_test.go index 0eafbf7..b6f9c9c 100644 --- a/cron_test.go +++ b/cron_test.go @@ -355,6 +355,125 @@ func TestJob(t *testing.T) { } } +// Add some jobs, try to ListJobsByPrefix +func TestListJobsByPrefix(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(2) + + cron, err := New() + if err != nil { + t.Fatal("unexpected error") + } + defer cron.Stop() + + // Add jobs with different prefixes + cron.AddJob(Job{Name: "prefix_test_job1", Rhythm: "* * * * * ?", Func: func(context.Context) error { wg.Done(); return nil }}) + cron.AddJob(Job{Name: "prefix_test_job2", Rhythm: "* * * * * ?", Func: func(context.Context) error { wg.Done(); return nil }}) + cron.AddJob(Job{Name: "other_job", Rhythm: "* * * * * ?", Func: func(context.Context) error { return nil }}) + + cron.Start(context.Background()) + + // Ensure only jobs with the specified prefix are returned + prefixJobs := cron.ListJobsByPrefix("prefix_test") + if len(prefixJobs) != 2 { + t.Errorf("ListJobsByPrefix did not return the correct number of jobs. Expected: 2, Actual: %d", len(prefixJobs)) + t.FailNow() + } + + select { + case <-time.After(ONE_SECOND): + t.FailNow() + case <-wait(wg): + } +} + +// Add a job, then DeleteJob +func TestDeleteJob(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(1) + + cron, err := New() + if err != nil { + t.Fatal("unexpected error") + } + defer cron.Stop() + + cron.AddJob(Job{Name: "delete_test_job", Rhythm: "* * * * * ?", Func: func(context.Context) error { wg.Done(); return nil }}) + + // Ensure the job is in the entries before deletion + foundBeforeDeletion := false + for _, entry := range cron.Entries() { + if entry.Job.Name == "delete_test_job" { + foundBeforeDeletion = true + break + } + } + + if !foundBeforeDeletion { + t.Error("Job not found in entries before deletion") + t.FailNow() + } + + cron.Start(context.Background()) + + err = cron.DeleteJob("delete_test_job") + if err != nil { + t.Errorf("Error deleting job: %v", err) + t.FailNow() + } + + // Ensure the job is no longer in the entries after deletion + foundAfterDeletion := false + for _, entry := range cron.Entries() { + if entry.Job.Name == "delete_test_job" { + foundAfterDeletion = true + break + } + } + + if foundAfterDeletion { + t.Error("DeleteJob did not remove the job from entries") + t.FailNow() + } + + // Ensure the job is not triggered after deletion + select { + case <-time.After(ONE_SECOND): + // This is expected since the job should not be triggered + case <-wait(wg): + t.Error("Job was triggered after deletion") + } +} + +// Add a job, then GetJob +func TestGetJob(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(1) + + cron, err := New() + if err != nil { + t.Fatal("unexpected error") + } + defer cron.Stop() + + jobName := "get_test_job" + cron.AddJob(Job{Name: jobName, Rhythm: "* * * * * ?", Func: func(context.Context) error { wg.Done(); return nil }}) + + cron.Start(context.Background()) + + job := cron.GetJob(jobName) + if job == nil || job.Name != jobName { + t.Error("GetJob did not return the expected job") + t.FailNow() + } + + select { + case <-time.After(ONE_SECOND): + t.FailNow() + case <-wait(wg): + } +} + // TestCron_Parallel tests that with 2 crons with the same job // They should only execute once each job event func TestCron_Parallel(t *testing.T) {