diff --git a/internal/assert/assert.go b/internal/assert/assert.go index a5eed78..ee4115d 100644 --- a/internal/assert/assert.go +++ b/internal/assert/assert.go @@ -2,17 +2,29 @@ package assert import ( "reflect" + "strings" "testing" ) +// Equal verifies equality of two objects. func Equal[T any](t *testing.T, a T, b T) { if !reflect.DeepEqual(a, b) { t.Fatalf("%v != %v", a, b) } } +// NotEqual verifies objects are not equal. func NotEqual[T any](t *testing.T, a T, b T) { if reflect.DeepEqual(a, b) { t.Fatalf("%v == %v", a, b) } } + +// ErrorContains checks whether the given error contains the specified string. +func ErrorContains(t *testing.T, err error, str string) { + if err == nil { + t.Fatalf("Error is nil") + } else if !strings.Contains(err.Error(), str) { + t.Fatalf("Error doen't contain string: %s", str) + } +} diff --git a/quartz/cron.go b/quartz/cron.go index 2c1b336..12145fa 100644 --- a/quartz/cron.go +++ b/quartz/cron.go @@ -45,7 +45,7 @@ func NewCronTrigger(expression string) (*CronTrigger, error) { // NewCronTriggerWithLoc returns a new CronTrigger with the given time.Location. func NewCronTriggerWithLoc(expression string, location *time.Location) (*CronTrigger, error) { if location == nil { - return nil, errors.New("location is nil") + return nil, illegalArgumentError("location is nil") } fields, err := validateCronExpression(expression) @@ -62,7 +62,7 @@ func NewCronTriggerWithLoc(expression string, location *time.Location) (*CronTri // full wildcard expression if lastDefined == -1 { - fields[0].values, _ = fillRange(0, 59) + fields[0].values, _ = fillRangeValues(0, 59) } return &CronTrigger{ @@ -158,13 +158,13 @@ func validateCronExpression(expression string) ([]*cronField, error) { } length := len(tokens) if length < 6 || length > 7 { - return nil, cronError("invalid expression length") + return nil, cronParseError("invalid expression length") } if length == 6 { tokens = append(tokens, "*") } if (tokens[3] != "?" && tokens[3] != "*") && (tokens[5] != "?" && tokens[5] != "*") { - return nil, cronError("day field was set twice") + return nil, cronParseError("day field set twice") } return buildCronField(tokens) @@ -223,13 +223,13 @@ func parseField(field string, min, max int, translate ...[]string) (*cronField, return &cronField{[]int{}}, nil } - // single value + // simple value i, err := strconv.Atoi(field) if err == nil { if inScope(i, min, max) { return &cronField{[]int{i}}, nil } - return nil, cronError("single min/max validation error") + return nil, cronParseError("simple field min/max validation") } // list values @@ -247,18 +247,18 @@ func parseField(field string, min, max int, translate ...[]string) (*cronField, return parseStepField(field, min, max, dict) } - // literal single value + // simple literal value if dict != nil { i := intVal(dict, field) if i >= 0 { if inScope(i, min, max) { return &cronField{[]int{i}}, nil } - return nil, cronError("cron literal min/max validation error") + return nil, cronParseError("simple literal min/max validation") } } - return nil, cronError("cron parse error") + return nil, cronParseError("parse error") } func parseListField(field string, min, max int, translate []string) (*cronField, error) { @@ -286,17 +286,16 @@ func parseListField(field string, min, max int, translate []string) (*cronField, func parseRangeField(field string, min, max int, translate []string) (*cronField, error) { t := strings.Split(field, "-") if len(t) != 2 { - return nil, cronError("parse cron range error") + return nil, cronParseError(fmt.Sprintf("invalid range field %s", field)) } from := normalize(t[0], translate) to := normalize(t[1], translate) if !inScope(from, min, max) || !inScope(to, min, max) { - return nil, cronError(fmt.Sprintf("cron range min/max validation error %d-%d", - from, to)) + return nil, cronParseError(fmt.Sprintf("range field min/max validation %d-%d", from, to)) } - rangeValues, err := fillRange(from, to) + rangeValues, err := fillRangeValues(from, to) if err != nil { return nil, err } @@ -307,7 +306,7 @@ func parseRangeField(field string, min, max int, translate []string) (*cronField func parseStepField(field string, min, max int, translate []string) (*cronField, error) { t := strings.Split(field, "/") if len(t) != 2 { - return nil, cronError("parse cron step error") + return nil, cronParseError(fmt.Sprintf("invalid step field %s", field)) } if t[0] == "*" { @@ -317,10 +316,10 @@ func parseStepField(field string, min, max int, translate []string) (*cronField, from := normalize(t[0], translate) step := atoi(t[1]) if !inScope(from, min, max) { - return nil, cronError("cron step min/max validation error") + return nil, cronParseError("step field min/max validation") } - stepValues, err := fillStep(from, step, max) + stepValues, err := fillStepValues(from, step, max) if err != nil { return nil, err } diff --git a/quartz/cron_test.go b/quartz/cron_test.go index f8a05cf..7bbe517 100644 --- a/quartz/cron_test.go +++ b/quartz/cron_test.go @@ -355,12 +355,12 @@ func TestCronHourly(t *testing.T) { func TestCronExpressionInvalidLength(t *testing.T) { _, err := quartz.NewCronTrigger("0 0 0 * *") - assert.Equal(t, err.Error(), "invalid cron expression: invalid expression length") + assert.ErrorContains(t, err, "invalid expression length") } func TestCronTriggerNilLocationError(t *testing.T) { _, err := quartz.NewCronTriggerWithLoc("@daily", nil) - assert.Equal(t, err.Error(), "location is nil") + assert.ErrorContains(t, err, "location is nil") } func TestCronExpressionDescription(t *testing.T) { diff --git a/quartz/error.go b/quartz/error.go new file mode 100644 index 0000000..14c2c28 --- /dev/null +++ b/quartz/error.go @@ -0,0 +1,31 @@ +package quartz + +import ( + "errors" + "fmt" +) + +// Errors +var ( + ErrIllegalArgument = errors.New("illegal argument") + ErrCronParse = errors.New("parse cron expression") + ErrJobNotFound = errors.New("job not found") +) + +// illegalArgumentError returns an illegal argument error with a custom +// error message, which unwraps to ErrIllegalArgument. +func illegalArgumentError(message string) error { + return fmt.Errorf("%w: %s", ErrIllegalArgument, message) +} + +// cronParseError returns a cron parse error with a custom error message, +// which unwraps to ErrCronParse. +func cronParseError(message string) error { + return fmt.Errorf("%w: %s", ErrCronParse, message) +} + +// jobNotFoundError returns a job not found error with a custom error message, +// which unwraps to ErrJobNotFound. +func jobNotFoundError(message string) error { + return fmt.Errorf("%w: %s", ErrJobNotFound, message) +} diff --git a/quartz/error_test.go b/quartz/error_test.go new file mode 100644 index 0000000..bad168d --- /dev/null +++ b/quartz/error_test.go @@ -0,0 +1,36 @@ +package quartz + +import ( + "errors" + "fmt" + "testing" + + "github.com/reugn/go-quartz/internal/assert" +) + +func TestIllegalArgumentError(t *testing.T) { + message := "argument is nil" + err := illegalArgumentError(message) + if !errors.Is(err, ErrIllegalArgument) { + t.Fatal("error must match ErrIllegalArgument") + } + assert.Equal(t, err.Error(), fmt.Sprintf("%s: %s", ErrIllegalArgument, message)) +} + +func TestCronParseError(t *testing.T) { + message := "invalid field" + err := cronParseError(message) + if !errors.Is(err, ErrCronParse) { + t.Fatal("error must match ErrCronParse") + } + assert.Equal(t, err.Error(), fmt.Sprintf("%s: %s", ErrCronParse, message)) +} + +func TestJobNotFoundError(t *testing.T) { + message := "for key" + err := jobNotFoundError(message) + if !errors.Is(err, ErrJobNotFound) { + t.Fatal("error must match ErrJobNotFound") + } + assert.Equal(t, err.Error(), fmt.Sprintf("%s: %s", ErrJobNotFound, message)) +} diff --git a/quartz/queue.go b/quartz/queue.go index 1e81ec5..df2e50b 100644 --- a/quartz/queue.go +++ b/quartz/queue.go @@ -174,7 +174,7 @@ func (jq *jobQueue) Remove(jobKey *JobKey) (ScheduledJob, error) { return heap.Remove(&jq.delegate, i).(ScheduledJob), nil } } - return nil, errors.New("no job with the given key found") + return nil, jobNotFoundError(fmt.Sprintf("for key %s", jobKey)) } // ScheduledJobs returns the slice of all scheduled jobs in the queue. diff --git a/quartz/scheduler.go b/quartz/scheduler.go index 062f2ab..9386c66 100644 --- a/quartz/scheduler.go +++ b/quartz/scheduler.go @@ -2,7 +2,7 @@ package quartz import ( "context" - "errors" + "fmt" "sync" "time" @@ -136,16 +136,16 @@ func (sched *StdScheduler) ScheduleJob( trigger Trigger, ) error { if jobDetail == nil { - return errors.New("jobDetail is nil") + return illegalArgumentError("jobDetail is nil") } if jobDetail.jobKey == nil { - return errors.New("jobDetail.jobKey is nil") + return illegalArgumentError("jobDetail.jobKey is nil") } if jobDetail.jobKey.name == "" { - return errors.New("empty key name is not allowed") + return illegalArgumentError("empty key name is not allowed") } if trigger == nil { - return errors.New("trigger is nil") + return illegalArgumentError("trigger is nil") } nextRunTime, err := trigger.NextFireTime(NowNano()) if err != nil { @@ -224,7 +224,7 @@ func (sched *StdScheduler) GetJobKeys() []*JobKey { // GetScheduledJob returns the ScheduledJob with the specified key. func (sched *StdScheduler) GetScheduledJob(jobKey *JobKey) (ScheduledJob, error) { if jobKey == nil { - return nil, errors.New("jobKey is nil") + return nil, illegalArgumentError("jobKey is nil") } scheduledJobs := sched.queue.ScheduledJobs() for _, scheduled := range scheduledJobs { @@ -232,13 +232,13 @@ func (sched *StdScheduler) GetScheduledJob(jobKey *JobKey) (ScheduledJob, error) return scheduled, nil } } - return nil, errors.New("no job with the given key found") + return nil, jobNotFoundError(fmt.Sprintf("for key %s", jobKey)) } // DeleteJob removes the Job with the specified key if present. func (sched *StdScheduler) DeleteJob(jobKey *JobKey) error { if jobKey == nil { - return errors.New("jobKey is nil") + return illegalArgumentError("jobKey is nil") } _, err := sched.queue.Remove(jobKey) if err == nil { diff --git a/quartz/scheduler_test.go b/quartz/scheduler_test.go index c2298ce..fc112a9 100644 --- a/quartz/scheduler_test.go +++ b/quartz/scheduler_test.go @@ -68,7 +68,7 @@ func TestScheduler(t *testing.T) { nonExistentJobKey := quartz.NewJobKey("NA") _, err = sched.GetScheduledJob(nonExistentJobKey) - assert.NotEqual(t, err, nil) + assert.ErrorContains(t, err, quartz.ErrJobNotFound.Error()) err = sched.DeleteJob(nonExistentJobKey) assert.NotEqual(t, err, nil) @@ -381,21 +381,21 @@ func TestSchedulerArgumentValidationErrors(t *testing.T) { assert.Equal(t, err, nil) err = sched.ScheduleJob(nil, trigger) - assert.Equal(t, err.Error(), "jobDetail is nil") + assert.ErrorContains(t, err, "jobDetail is nil") err = sched.ScheduleJob(quartz.NewJobDetail(job, nil), trigger) - assert.Equal(t, err.Error(), "jobDetail.jobKey is nil") + assert.ErrorContains(t, err, "jobDetail.jobKey is nil") err = sched.ScheduleJob(quartz.NewJobDetail(job, quartz.NewJobKey("")), trigger) - assert.Equal(t, err.Error(), "empty key name is not allowed") + assert.ErrorContains(t, err, "empty key name is not allowed") err = sched.ScheduleJob(quartz.NewJobDetail(job, quartz.NewJobKeyWithGroup("job", "")), nil) - assert.Equal(t, err.Error(), "trigger is nil") + assert.ErrorContains(t, err, "trigger is nil") err = sched.ScheduleJob(quartz.NewJobDetail(job, quartz.NewJobKey("job")), expiredTrigger) - assert.Equal(t, err.Error(), "next trigger time is in the past") + assert.ErrorContains(t, err, "next trigger time is in the past") err = sched.DeleteJob(nil) - assert.Equal(t, err.Error(), "jobKey is nil") + assert.ErrorContains(t, err, "jobKey is nil") _, err = sched.GetScheduledJob(nil) - assert.Equal(t, err.Error(), "jobKey is nil") + assert.ErrorContains(t, err, "jobKey is nil") } func TestSchedulerStartStop(t *testing.T) { diff --git a/quartz/util.go b/quartz/util.go index 0d3fd7c..192913d 100644 --- a/quartz/util.go +++ b/quartz/util.go @@ -15,7 +15,7 @@ func indexes(search []string, target []string) ([]int, error) { for _, a := range search { index := intVal(target, a) if index == -1 { - return nil, fmt.Errorf("invalid cron field: %s", a) + return nil, cronParseError(fmt.Sprintf("invalid cron field %s", a)) } searchIndexes = append(searchIndexes, index) } @@ -50,34 +50,32 @@ func sliceAtoi(sa []string) ([]int, error) { return si, nil } -func fillRange(from, to int) ([]int, error) { +func fillRangeValues(from, to int) ([]int, error) { if to < from { - return nil, cronError("fillRange") + return nil, cronParseError("fill range values") } - length := (to - from) + 1 - arr := make([]int, length) + rangeValues := make([]int, length) for i, j := from, 0; i <= to; i, j = i+1, j+1 { - arr[j] = i + rangeValues[j] = i } - return arr, nil + return rangeValues, nil } -func fillStep(from, step, max int) ([]int, error) { +func fillStepValues(from, step, max int) ([]int, error) { if max < from || step == 0 { - return nil, cronError("fillStep") + return nil, cronParseError("fill step values") } - length := ((max - from) / step) + 1 - arr := make([]int, length) + stepValues := make([]int, length) for i, j := from, 0; i <= max; i, j = i+step, j+1 { - arr[j] = i + stepValues[j] = i } - return arr, nil + return stepValues, nil } func normalize(field string, dict []string) int { @@ -97,10 +95,6 @@ func inScope(i, min, max int) bool { return false } -func cronError(cause string) error { - return fmt.Errorf("invalid cron expression: %s", cause) -} - func intVal(target []string, search string) int { uSearch := strings.ToUpper(search) for i, v := range target { @@ -118,7 +112,7 @@ func atoi(str string) int { return i } -// NowNano returns the current UTC Unix time in nanoseconds. +// NowNano returns the current Unix time in nanoseconds. func NowNano() int64 { - return time.Now().UTC().UnixNano() + return time.Now().UnixNano() }