added basic cron util
This commit is contained in:
		
							parent
							
								
									d3314e1e23
								
							
						
					
					
						commit
						3b0f60fe15
					
				|  | @ -0,0 +1,165 @@ | |||
| // Package cron implements a crontab-like service to execute and schedule
 | ||||
| // repeative tasks/jobs.
 | ||||
| //
 | ||||
| // Example:
 | ||||
| //
 | ||||
| //	c := cron.New()
 | ||||
| //	c.MustAdd("dailyReport", "0 0 * * *", func() { ... })
 | ||||
| //	c.Start()
 | ||||
| package cron | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type job struct { | ||||
| 	schedule *Schedule | ||||
| 	run      func() | ||||
| } | ||||
| 
 | ||||
| // Cron is a crontab-like struct for tasks/jobs scheduling.
 | ||||
| type Cron struct { | ||||
| 	sync.RWMutex | ||||
| 
 | ||||
| 	interval time.Duration | ||||
| 	timezone *time.Location | ||||
| 	ticker   *time.Ticker | ||||
| 	jobs     map[string]*job | ||||
| } | ||||
| 
 | ||||
| // New create a new Cron struct with default tick interval of 1 minute
 | ||||
| // and timezone in UTC.
 | ||||
| //
 | ||||
| // You can change the default tick interval with Cron.SetInterval().
 | ||||
| // You can change the default timezone with Cron.SetTimezone().
 | ||||
| func New() *Cron { | ||||
| 	return &Cron{ | ||||
| 		interval: 1 * time.Minute, | ||||
| 		timezone: time.UTC, | ||||
| 		jobs:     map[string]*job{}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // SetInterval changes the current cron tick interval
 | ||||
| // (it usually should be >= 1 minute).
 | ||||
| func (c *Cron) SetInterval(d time.Duration) { | ||||
| 	// update interval
 | ||||
| 	c.Lock() | ||||
| 	wasStarted := c.ticker != nil | ||||
| 	c.interval = d | ||||
| 	c.Unlock() | ||||
| 
 | ||||
| 	// restart the ticker
 | ||||
| 	if wasStarted { | ||||
| 		c.Start() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // SetTimezone changes the current cron tick timezone.
 | ||||
| func (c *Cron) SetTimezone(l *time.Location) { | ||||
| 	c.Lock() | ||||
| 	defer c.Unlock() | ||||
| 
 | ||||
| 	c.timezone = l | ||||
| } | ||||
| 
 | ||||
| // MustAdd is similar to Add() but panic on failure.
 | ||||
| func (c *Cron) MustAdd(jobId string, cronExpr string, run func()) { | ||||
| 	if err := c.Add(jobId, cronExpr, run); err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Add registers a single cron job.
 | ||||
| //
 | ||||
| // If there is already a job with the provided id, then the old job
 | ||||
| // will be replaced with the new one.
 | ||||
| //
 | ||||
| // cronExpr is a regular cron expression, eg. "0 */3 * * *" (aka. at minute 0 past every 3rd hour).
 | ||||
| // Check cron.NewSchedule() for the supported tokens.
 | ||||
| func (c *Cron) Add(jobId string, cronExpr string, run func()) error { | ||||
| 	if run == nil { | ||||
| 		return errors.New("failed to add new cron job: run must be non-nil function") | ||||
| 	} | ||||
| 
 | ||||
| 	c.Lock() | ||||
| 	defer c.Unlock() | ||||
| 
 | ||||
| 	schedule, err := NewSchedule(cronExpr) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to add new cron job: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	c.jobs[jobId] = &job{ | ||||
| 		schedule: schedule, | ||||
| 		run:      run, | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Remove removes a single cron job by its id.
 | ||||
| func (c *Cron) Remove(jobId string) { | ||||
| 	c.Lock() | ||||
| 	defer c.Unlock() | ||||
| 
 | ||||
| 	delete(c.jobs, jobId) | ||||
| } | ||||
| 
 | ||||
| // RemoveAll removes all registered cron jobs.
 | ||||
| func (c *Cron) RemoveAll() { | ||||
| 	c.Lock() | ||||
| 	defer c.Unlock() | ||||
| 
 | ||||
| 	c.jobs = map[string]*job{} | ||||
| } | ||||
| 
 | ||||
| // Stop stops the current cron ticker (if not already).
 | ||||
| //
 | ||||
| // You can resume the ticker by calling Start().
 | ||||
| func (c *Cron) Stop() { | ||||
| 	c.Lock() | ||||
| 	defer c.Unlock() | ||||
| 
 | ||||
| 	if c.ticker == nil { | ||||
| 		return // already stopped
 | ||||
| 	} | ||||
| 
 | ||||
| 	c.ticker.Stop() | ||||
| 	c.ticker = nil | ||||
| } | ||||
| 
 | ||||
| // Start starts the cron ticker.
 | ||||
| //
 | ||||
| // Calling Start() on already started cron will restart the ticker.
 | ||||
| func (c *Cron) Start() { | ||||
| 	c.Stop() | ||||
| 
 | ||||
| 	c.Lock() | ||||
| 	defer c.Unlock() | ||||
| 
 | ||||
| 	c.ticker = time.NewTicker(c.interval) | ||||
| 
 | ||||
| 	go func() { | ||||
| 		for t := range c.ticker.C { | ||||
| 			c.runDue(t) | ||||
| 		} | ||||
| 	}() | ||||
| } | ||||
| 
 | ||||
| // runDue runs all registered jobs that are scheduled for the provided time.
 | ||||
| func (c *Cron) runDue(t time.Time) { | ||||
| 	c.RLock() | ||||
| 	defer c.RUnlock() | ||||
| 
 | ||||
| 	moment := NewMoment(t.In(c.timezone)) | ||||
| 
 | ||||
| 	for _, j := range c.jobs { | ||||
| 		if j.schedule.IsDue(moment) { | ||||
| 			go j.run() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,220 @@ | |||
| package cron | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| func TestCronNew(t *testing.T) { | ||||
| 	c := New() | ||||
| 
 | ||||
| 	expectedInterval := 1 * time.Minute | ||||
| 	if c.interval != expectedInterval { | ||||
| 		t.Fatalf("Expected default interval %v, got %v", expectedInterval, c.interval) | ||||
| 	} | ||||
| 
 | ||||
| 	expectedTimezone := time.UTC | ||||
| 	if c.timezone.String() != expectedTimezone.String() { | ||||
| 		t.Fatalf("Expected default timezone %v, got %v", expectedTimezone, c.timezone) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(c.jobs) != 0 { | ||||
| 		t.Fatalf("Expected no jobs by default, got \n%v", c.jobs) | ||||
| 	} | ||||
| 
 | ||||
| 	if c.ticker != nil { | ||||
| 		t.Fatal("Expected the ticker NOT to be initialized") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestCronSetInterval(t *testing.T) { | ||||
| 	c := New() | ||||
| 
 | ||||
| 	interval := 2 * time.Minute | ||||
| 
 | ||||
| 	c.SetInterval(interval) | ||||
| 
 | ||||
| 	if c.interval != interval { | ||||
| 		t.Fatalf("Expected interval %v, got %v", interval, c.interval) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestCronSetTimezone(t *testing.T) { | ||||
| 	c := New() | ||||
| 
 | ||||
| 	timezone, _ := time.LoadLocation("Asia/Tokyo") | ||||
| 
 | ||||
| 	c.SetTimezone(timezone) | ||||
| 
 | ||||
| 	if c.timezone.String() != timezone.String() { | ||||
| 		t.Fatalf("Expected timezone %v, got %v", timezone, c.timezone) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestCronAddAndRemove(t *testing.T) { | ||||
| 	c := New() | ||||
| 
 | ||||
| 	if err := c.Add("test0", "* * * * *", nil); err == nil { | ||||
| 		t.Fatal("Expected nil function error") | ||||
| 	} | ||||
| 
 | ||||
| 	if err := c.Add("test1", "invalid", func() {}); err == nil { | ||||
| 		t.Fatal("Expected invalid cron expression error") | ||||
| 	} | ||||
| 
 | ||||
| 	if err := c.Add("test2", "* * * * *", func() {}); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := c.Add("test3", "* * * * *", func() {}); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := c.Add("test4", "* * * * *", func() {}); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// overwrite test2
 | ||||
| 	if err := c.Add("test2", "1 2 3 4 5", func() {}); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := c.Add("test5", "1 2 3 4 5", func() {}); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// mock job deletion
 | ||||
| 	c.Remove("test4") | ||||
| 
 | ||||
| 	// try to remove non-existing (should be no-op)
 | ||||
| 	c.Remove("missing") | ||||
| 
 | ||||
| 	// check job keys
 | ||||
| 	{ | ||||
| 		expectedKeys := []string{"test3", "test2", "test5"} | ||||
| 
 | ||||
| 		if v := len(c.jobs); v != len(expectedKeys) { | ||||
| 			t.Fatalf("Expected %d jobs, got %d", len(expectedKeys), v) | ||||
| 		} | ||||
| 
 | ||||
| 		for _, k := range expectedKeys { | ||||
| 			if c.jobs[k] == nil { | ||||
| 				t.Fatalf("Expected job with key %s, got nil", k) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// check the jobs schedule
 | ||||
| 	{ | ||||
| 		expectedSchedules := map[string]string{ | ||||
| 			"test2": `{"minutes":{"1":{}},"hours":{"2":{}},"days":{"3":{}},"months":{"4":{}},"daysOfWeek":{"5":{}}}`, | ||||
| 			"test3": `{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`, | ||||
| 			"test5": `{"minutes":{"1":{}},"hours":{"2":{}},"days":{"3":{}},"months":{"4":{}},"daysOfWeek":{"5":{}}}`, | ||||
| 		} | ||||
| 		for k, v := range expectedSchedules { | ||||
| 			raw, err := json.Marshal(c.jobs[k].schedule) | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 
 | ||||
| 			if string(raw) != v { | ||||
| 				t.Fatalf("Expected %q schedule \n%s, \ngot \n%s", k, v, raw) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestCronMustAdd(t *testing.T) { | ||||
| 	c := New() | ||||
| 
 | ||||
| 	defer func() { | ||||
| 		if r := recover(); r == nil { | ||||
| 			t.Errorf("test1 didn't panic") | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	c.MustAdd("test1", "* * * * *", nil) | ||||
| 
 | ||||
| 	c.MustAdd("test2", "* * * * *", func() {}) | ||||
| 
 | ||||
| 	if _, ok := c.jobs["test2"]; !ok { | ||||
| 		t.Fatal("Couldn't find job test2") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestCronRemoveAll(t *testing.T) { | ||||
| 	c := New() | ||||
| 
 | ||||
| 	if err := c.Add("test1", "* * * * *", func() {}); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := c.Add("test2", "* * * * *", func() {}); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := c.Add("test3", "* * * * *", func() {}); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if v := len(c.jobs); v != 3 { | ||||
| 		t.Fatalf("Expected %d jobs, got %d", 3, v) | ||||
| 	} | ||||
| 
 | ||||
| 	c.RemoveAll() | ||||
| 
 | ||||
| 	if v := len(c.jobs); v != 0 { | ||||
| 		t.Fatalf("Expected %d jobs, got %d", 0, v) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestCronStartStop(t *testing.T) { | ||||
| 	c := New() | ||||
| 
 | ||||
| 	c.SetInterval(1 * time.Second) | ||||
| 
 | ||||
| 	test1 := 0 | ||||
| 	test2 := 0 | ||||
| 
 | ||||
| 	c.Add("test1", "* * * * *", func() { | ||||
| 		test1++ | ||||
| 	}) | ||||
| 
 | ||||
| 	c.Add("test2", "* * * * *", func() { | ||||
| 		test2++ | ||||
| 	}) | ||||
| 
 | ||||
| 	expectedCalls := 3 | ||||
| 
 | ||||
| 	// call twice Start to check if the previous ticker will be reseted
 | ||||
| 	c.Start() | ||||
| 	c.Start() | ||||
| 
 | ||||
| 	time.Sleep(3250 * time.Millisecond) | ||||
| 
 | ||||
| 	// call twice Stop to ensure that the second stop is no-op
 | ||||
| 	c.Stop() | ||||
| 	c.Stop() | ||||
| 
 | ||||
| 	if test1 != expectedCalls { | ||||
| 		t.Fatalf("Expected %d test1, got %d", expectedCalls, test1) | ||||
| 	} | ||||
| 	if test2 != expectedCalls { | ||||
| 		t.Fatalf("Expected %d test2, got %d", expectedCalls, test2) | ||||
| 	} | ||||
| 
 | ||||
| 	// resume for ~5 seconds
 | ||||
| 	c.Start() | ||||
| 	time.Sleep(5250 * time.Millisecond) | ||||
| 	c.Stop() | ||||
| 
 | ||||
| 	expectedCalls += 5 | ||||
| 
 | ||||
| 	if test1 != expectedCalls { | ||||
| 		t.Fatalf("Expected %d test1, got %d", expectedCalls, test1) | ||||
| 	} | ||||
| 	if test2 != expectedCalls { | ||||
| 		t.Fatalf("Expected %d test2, got %d", expectedCalls, test2) | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,194 @@ | |||
| package cron | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| // Moment represents a parsed single time moment.
 | ||||
| type Moment struct { | ||||
| 	Minute    int `json:"minute"` | ||||
| 	Hour      int `json:"hour"` | ||||
| 	Day       int `json:"day"` | ||||
| 	Month     int `json:"month"` | ||||
| 	DayOfWeek int `json:"dayOfWeek"` | ||||
| } | ||||
| 
 | ||||
| // NewMoment creates a new Moment from the specified time.
 | ||||
| func NewMoment(t time.Time) *Moment { | ||||
| 	return &Moment{ | ||||
| 		Minute:    t.Minute(), | ||||
| 		Hour:      t.Hour(), | ||||
| 		Day:       t.Day(), | ||||
| 		Month:     int(t.Month()), | ||||
| 		DayOfWeek: int(t.Weekday()), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Schedule stores parsed information for each time component when a cron job should run.
 | ||||
| type Schedule struct { | ||||
| 	Minutes    map[int]struct{} `json:"minutes"` | ||||
| 	Hours      map[int]struct{} `json:"hours"` | ||||
| 	Days       map[int]struct{} `json:"days"` | ||||
| 	Months     map[int]struct{} `json:"months"` | ||||
| 	DaysOfWeek map[int]struct{} `json:"daysOfWeek"` | ||||
| } | ||||
| 
 | ||||
| // IsDue checks whether the provided Moment satisfies the current Schedule.
 | ||||
| func (s *Schedule) IsDue(m *Moment) bool { | ||||
| 	if _, ok := s.Minutes[m.Minute]; !ok { | ||||
| 		return false | ||||
| 	} | ||||
| 
 | ||||
| 	if _, ok := s.Hours[m.Hour]; !ok { | ||||
| 		return false | ||||
| 	} | ||||
| 
 | ||||
| 	if _, ok := s.Days[m.Day]; !ok { | ||||
| 		return false | ||||
| 	} | ||||
| 
 | ||||
| 	if _, ok := s.DaysOfWeek[m.DayOfWeek]; !ok { | ||||
| 		return false | ||||
| 	} | ||||
| 
 | ||||
| 	if _, ok := s.Months[m.Month]; !ok { | ||||
| 		return false | ||||
| 	} | ||||
| 
 | ||||
| 	return true | ||||
| } | ||||
| 
 | ||||
| // NewSchedule creates a new Schedule from a cron expression.
 | ||||
| //
 | ||||
| // A cron expression is consisted of 5 segments separated by space,
 | ||||
| // representing: minute, hour, day of the month, month and day of the week.
 | ||||
| //
 | ||||
| // Each segment could be in the following formats:
 | ||||
| //   - wildcard: *
 | ||||
| //   - range:    1-30
 | ||||
| //   - step:     */n or 1-30/n
 | ||||
| //   - list:     1,2,3,10-20/n
 | ||||
| func NewSchedule(cronExpr string) (*Schedule, error) { | ||||
| 	segments := strings.Split(cronExpr, " ") | ||||
| 	if len(segments) != 5 { | ||||
| 		return nil, errors.New("invalid cron expression - must have exactly 5 space separated segments") | ||||
| 	} | ||||
| 
 | ||||
| 	minutes, err := parseCronSegment(segments[0], 0, 59) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	hours, err := parseCronSegment(segments[1], 0, 23) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	days, err := parseCronSegment(segments[2], 1, 31) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	months, err := parseCronSegment(segments[3], 1, 12) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	daysOfWeek, err := parseCronSegment(segments[4], 0, 6) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &Schedule{ | ||||
| 		Minutes:    minutes, | ||||
| 		Hours:      hours, | ||||
| 		Days:       days, | ||||
| 		Months:     months, | ||||
| 		DaysOfWeek: daysOfWeek, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // parseCronSegment parses a single cron expression segment and
 | ||||
| // returns its time schedule slots.
 | ||||
| func parseCronSegment(segment string, min int, max int) (map[int]struct{}, error) { | ||||
| 	slots := map[int]struct{}{} | ||||
| 
 | ||||
| 	list := strings.Split(segment, ",") | ||||
| 	for _, p := range list { | ||||
| 		stepParts := strings.Split(p, "/") | ||||
| 
 | ||||
| 		// step (*/n, 1-30/n)
 | ||||
| 		var step int | ||||
| 		switch len(stepParts) { | ||||
| 		case 1: | ||||
| 			step = 1 | ||||
| 		case 2: | ||||
| 			parsedStep, err := strconv.Atoi(stepParts[1]) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			if parsedStep < 1 || parsedStep > max { | ||||
| 				return nil, fmt.Errorf("invalid segment step boundary - the step must be between 1 and the %d", max) | ||||
| 			} | ||||
| 			step = parsedStep | ||||
| 		default: | ||||
| 			return nil, errors.New("invalid segment step format - must be in the format */n or 1-30/n") | ||||
| 		} | ||||
| 
 | ||||
| 		// find the min and max range of the segment part
 | ||||
| 		var rangeMin, rangeMax int | ||||
| 		if stepParts[0] == "*" { | ||||
| 			rangeMin = min | ||||
| 			rangeMax = max | ||||
| 		} else { | ||||
| 			// single digit (1) or range (1-30)
 | ||||
| 			rangeParts := strings.Split(stepParts[0], "-") | ||||
| 			switch len(rangeParts) { | ||||
| 			case 1: | ||||
| 				if step != 1 { | ||||
| 					return nil, errors.New("invalid segement step - step > 1 could be used only with the wildcard or range format") | ||||
| 				} | ||||
| 				parsed, err := strconv.Atoi(rangeParts[0]) | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 				if parsed < min || parsed > max { | ||||
| 					return nil, errors.New("invalid segment value - must be between the min and max of the segment") | ||||
| 				} | ||||
| 				rangeMin = parsed | ||||
| 				rangeMax = rangeMin | ||||
| 			case 2: | ||||
| 				parsedMin, err := strconv.Atoi(rangeParts[0]) | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 				if parsedMin < min || parsedMin > max { | ||||
| 					return nil, fmt.Errorf("invalid segment range minimum - must be between %d and %d", min, max) | ||||
| 				} | ||||
| 				rangeMin = parsedMin | ||||
| 
 | ||||
| 				parsedMax, err := strconv.Atoi(rangeParts[1]) | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 				if parsedMax < parsedMin || parsedMax > max { | ||||
| 					return nil, fmt.Errorf("invalid segment range maximum - must be between %d and %d", rangeMin, max) | ||||
| 				} | ||||
| 				rangeMax = parsedMax | ||||
| 			default: | ||||
| 				return nil, errors.New("invalid segment range format - the range must have 1 or 2 parts") | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		// fill the slots
 | ||||
| 		for i := rangeMin; i <= rangeMax; i += step { | ||||
| 			slots[i] = struct{}{} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return slots, nil | ||||
| } | ||||
|  | @ -0,0 +1,361 @@ | |||
| package cron_test | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/pocketbase/pocketbase/tools/cron" | ||||
| ) | ||||
| 
 | ||||
| func TestNewMoment(t *testing.T) { | ||||
| 	date, err := time.Parse("2006-01-02 15:04", "2023-05-09 15:20") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	m := cron.NewMoment(date) | ||||
| 
 | ||||
| 	if m.Minute != 20 { | ||||
| 		t.Fatalf("Expected m.Minute %d, got %d", 20, m.Minute) | ||||
| 	} | ||||
| 
 | ||||
| 	if m.Hour != 15 { | ||||
| 		t.Fatalf("Expected m.Hour %d, got %d", 15, m.Hour) | ||||
| 	} | ||||
| 
 | ||||
| 	if m.Day != 9 { | ||||
| 		t.Fatalf("Expected m.Day %d, got %d", 9, m.Day) | ||||
| 	} | ||||
| 
 | ||||
| 	if m.Month != 5 { | ||||
| 		t.Fatalf("Expected m.Month %d, got %d", 5, m.Month) | ||||
| 	} | ||||
| 
 | ||||
| 	if m.DayOfWeek != 2 { | ||||
| 		t.Fatalf("Expected m.DayOfWeek %d, got %d", 2, m.DayOfWeek) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestNewSchedule(t *testing.T) { | ||||
| 	scenarios := []struct { | ||||
| 		cronExpr       string | ||||
| 		expectError    bool | ||||
| 		expectSchedule string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			"invalid", | ||||
| 			true, | ||||
| 			"", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * * *", | ||||
| 			true, | ||||
| 			"", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * * * * *", | ||||
| 			true, | ||||
| 			"", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"2/3 * * * *", | ||||
| 			true, | ||||
| 			"", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * * * *", | ||||
| 			false, | ||||
| 			`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"*/2 */3 */5 */4 */2", | ||||
| 			false, | ||||
| 			`{"minutes":{"0":{},"10":{},"12":{},"14":{},"16":{},"18":{},"2":{},"20":{},"22":{},"24":{},"26":{},"28":{},"30":{},"32":{},"34":{},"36":{},"38":{},"4":{},"40":{},"42":{},"44":{},"46":{},"48":{},"50":{},"52":{},"54":{},"56":{},"58":{},"6":{},"8":{}},"hours":{"0":{},"12":{},"15":{},"18":{},"21":{},"3":{},"6":{},"9":{}},"days":{"1":{},"11":{},"16":{},"21":{},"26":{},"31":{},"6":{}},"months":{"1":{},"5":{},"9":{}},"daysOfWeek":{"0":{},"2":{},"4":{},"6":{}}}`, | ||||
| 		}, | ||||
| 
 | ||||
| 		// minute segment
 | ||||
| 		{ | ||||
| 			"-1 * * * *", | ||||
| 			true, | ||||
| 			"", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"60 * * * *", | ||||
| 			true, | ||||
| 			"", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"0 * * * *", | ||||
| 			false, | ||||
| 			`{"minutes":{"0":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"59 * * * *", | ||||
| 			false, | ||||
| 			`{"minutes":{"59":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"1,2,5,7,40-50/2 * * * *", | ||||
| 			false, | ||||
| 			`{"minutes":{"1":{},"2":{},"40":{},"42":{},"44":{},"46":{},"48":{},"5":{},"50":{},"7":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`, | ||||
| 		}, | ||||
| 
 | ||||
| 		// hour hour segment
 | ||||
| 		{ | ||||
| 			"* -1 * * *", | ||||
| 			true, | ||||
| 			"", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* 24 * * *", | ||||
| 			true, | ||||
| 			"", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* 0 * * *", | ||||
| 			false, | ||||
| 			`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* 23 * * *", | ||||
| 			false, | ||||
| 			`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"23":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* 3,4,8-16/3,7 * * *", | ||||
| 			false, | ||||
| 			`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"11":{},"14":{},"3":{},"4":{},"7":{},"8":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`, | ||||
| 		}, | ||||
| 
 | ||||
| 		// day segment
 | ||||
| 		{ | ||||
| 			"* * 0 * *", | ||||
| 			true, | ||||
| 			"", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * 32 * *", | ||||
| 			true, | ||||
| 			"", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * 1 * *", | ||||
| 			false, | ||||
| 			`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * 31 * *", | ||||
| 			false, | ||||
| 			`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"31":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * 5,6,20-30/3,1 * *", | ||||
| 			false, | ||||
| 			`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"20":{},"23":{},"26":{},"29":{},"5":{},"6":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`, | ||||
| 		}, | ||||
| 
 | ||||
| 		// month segment
 | ||||
| 		{ | ||||
| 			"* * * 0 *", | ||||
| 			true, | ||||
| 			"", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * * 13 *", | ||||
| 			true, | ||||
| 			"", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * * 1 *", | ||||
| 			false, | ||||
| 			`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * * 12 *", | ||||
| 			false, | ||||
| 			`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"12":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * * 1,4,5-10/2 *", | ||||
| 			false, | ||||
| 			`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"4":{},"5":{},"7":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`, | ||||
| 		}, | ||||
| 
 | ||||
| 		// day of week
 | ||||
| 		{ | ||||
| 			"* * * * -1", | ||||
| 			true, | ||||
| 			"", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * * * 7", | ||||
| 			true, | ||||
| 			"", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * * * 0", | ||||
| 			false, | ||||
| 			`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{}}}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * * * 6", | ||||
| 			false, | ||||
| 			`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"6":{}}}`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * * * 1,2-5/2", | ||||
| 			false, | ||||
| 			`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"1":{},"2":{},"4":{}}}`, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, s := range scenarios { | ||||
| 		schedule, err := cron.NewSchedule(s.cronExpr) | ||||
| 
 | ||||
| 		hasErr := err != nil | ||||
| 		if hasErr != s.expectError { | ||||
| 			t.Fatalf("[%s] Expected hasErr to be %v, got %v (%v)", s.cronExpr, s.expectError, hasErr, err) | ||||
| 		} | ||||
| 
 | ||||
| 		if hasErr { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		encoded, err := json.Marshal(schedule) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("[%s] Failed to marshalize the result schedule: %v", s.cronExpr, err) | ||||
| 		} | ||||
| 		encodedStr := string(encoded) | ||||
| 
 | ||||
| 		if encodedStr != s.expectSchedule { | ||||
| 			t.Fatalf("[%s] Expected \n%s, \ngot \n%s", s.cronExpr, s.expectSchedule, encodedStr) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestScheduleIsDue(t *testing.T) { | ||||
| 	scenarios := []struct { | ||||
| 		cronExpr string | ||||
| 		moment   *cron.Moment | ||||
| 		expected bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			"* * * * *", | ||||
| 			&cron.Moment{}, | ||||
| 			false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * * * *", | ||||
| 			&cron.Moment{ | ||||
| 				Minute:    1, | ||||
| 				Hour:      1, | ||||
| 				Day:       1, | ||||
| 				Month:     1, | ||||
| 				DayOfWeek: 1, | ||||
| 			}, | ||||
| 			true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"5 * * * *", | ||||
| 			&cron.Moment{ | ||||
| 				Minute:    1, | ||||
| 				Hour:      1, | ||||
| 				Day:       1, | ||||
| 				Month:     1, | ||||
| 				DayOfWeek: 1, | ||||
| 			}, | ||||
| 			false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"5 * * * *", | ||||
| 			&cron.Moment{ | ||||
| 				Minute:    5, | ||||
| 				Hour:      1, | ||||
| 				Day:       1, | ||||
| 				Month:     1, | ||||
| 				DayOfWeek: 1, | ||||
| 			}, | ||||
| 			true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* 2-6 * * 2,3", | ||||
| 			&cron.Moment{ | ||||
| 				Minute:    1, | ||||
| 				Hour:      2, | ||||
| 				Day:       1, | ||||
| 				Month:     1, | ||||
| 				DayOfWeek: 1, | ||||
| 			}, | ||||
| 			false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* 2-6 * * 2,3", | ||||
| 			&cron.Moment{ | ||||
| 				Minute:    1, | ||||
| 				Hour:      2, | ||||
| 				Day:       1, | ||||
| 				Month:     1, | ||||
| 				DayOfWeek: 3, | ||||
| 			}, | ||||
| 			true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * 1,2,5,15-18 * *", | ||||
| 			&cron.Moment{ | ||||
| 				Minute:    1, | ||||
| 				Hour:      1, | ||||
| 				Day:       6, | ||||
| 				Month:     1, | ||||
| 				DayOfWeek: 1, | ||||
| 			}, | ||||
| 			false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * 1,2,5,15-18/2 * *", | ||||
| 			&cron.Moment{ | ||||
| 				Minute:    1, | ||||
| 				Hour:      1, | ||||
| 				Day:       2, | ||||
| 				Month:     1, | ||||
| 				DayOfWeek: 1, | ||||
| 			}, | ||||
| 			true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * 1,2,5,15-18/2 * *", | ||||
| 			&cron.Moment{ | ||||
| 				Minute:    1, | ||||
| 				Hour:      1, | ||||
| 				Day:       18, | ||||
| 				Month:     1, | ||||
| 				DayOfWeek: 1, | ||||
| 			}, | ||||
| 			false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"* * 1,2,5,15-18/2 * *", | ||||
| 			&cron.Moment{ | ||||
| 				Minute:    1, | ||||
| 				Hour:      1, | ||||
| 				Day:       17, | ||||
| 				Month:     1, | ||||
| 				DayOfWeek: 1, | ||||
| 			}, | ||||
| 			true, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for i, s := range scenarios { | ||||
| 		schedule, err := cron.NewSchedule(s.cronExpr) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("[%d-%s] Unexpected cron error: %v", i, s.cronExpr, err) | ||||
| 		} | ||||
| 
 | ||||
| 		result := schedule.IsDue(s.moment) | ||||
| 
 | ||||
| 		if result != s.expected { | ||||
| 			t.Fatalf("[%d-%s] Expected %v, got %v", i, s.cronExpr, s.expected, result) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
		Loading…
	
		Reference in New Issue