mirror of
https://github.com/Xevion/go-ha.git
synced 2025-12-06 01:15:10 -06:00
feat: new cron trigger
This commit is contained in:
1
go.mod
1
go.mod
@@ -18,6 +18,7 @@ require (
|
|||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/kr/pretty v0.1.0 // indirect
|
github.com/kr/pretty v0.1.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/robfig/cron/v3 v3.0.0 // indirect
|
||||||
golang.org/x/net v0.42.0 // indirect
|
golang.org/x/net v0.42.0 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -17,6 +17,8 @@ github.com/nathan-osman/go-sunrise v1.1.0/go.mod h1:RcWqhT+5ShCZDev79GuWLayetpJp
|
|||||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
|
||||||
|
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
|||||||
43
internal/scheduling/cron.go
Normal file
43
internal/scheduling/cron.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package scheduling
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CronTrigger represents a trigger based on a cron expression.
|
||||||
|
type CronTrigger struct {
|
||||||
|
expression string // required for hash
|
||||||
|
schedule cron.Schedule
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCronTrigger creates a new CronTrigger from a cron expression.
|
||||||
|
func NewCronTrigger(expression string) (*CronTrigger, error) {
|
||||||
|
// Use the standard cron parser
|
||||||
|
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
||||||
|
schedule, err := parser.Parse(expression)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid cron expression: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CronTrigger{
|
||||||
|
expression: expression,
|
||||||
|
schedule: schedule,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextTime calculates the next occurrence of this cron trigger after the given time.
|
||||||
|
func (t *CronTrigger) NextTime(now time.Time) *time.Time {
|
||||||
|
next := t.schedule.Next(now)
|
||||||
|
return &next
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash returns a stable hash value for the CronTrigger.
|
||||||
|
func (t *CronTrigger) Hash() uint64 {
|
||||||
|
h := fnv.New64()
|
||||||
|
fmt.Fprintf(h, "cron:%s", t.expression)
|
||||||
|
return h.Sum64()
|
||||||
|
}
|
||||||
131
internal/scheduling/cron_test.go
Normal file
131
internal/scheduling/cron_test.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package scheduling_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Xevion/go-ha/internal/scheduling"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCronTrigger(t *testing.T) {
|
||||||
|
// Use a fixed time for consistent testing
|
||||||
|
baseTime := time.Date(2025, 8, 2, 10, 30, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cron string
|
||||||
|
now time.Time
|
||||||
|
expected time.Time
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "daily at 9am",
|
||||||
|
cron: "0 9 * * *",
|
||||||
|
now: baseTime,
|
||||||
|
expected: time.Date(2025, 8, 3, 9, 0, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "every 15 minutes",
|
||||||
|
cron: "*/15 * * * *",
|
||||||
|
now: baseTime,
|
||||||
|
expected: time.Date(2025, 8, 2, 10, 45, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "weekdays at 8am (Saturday)",
|
||||||
|
// Base time is a Saturday, so next run should be Monday
|
||||||
|
cron: "0 8 * * 1-5",
|
||||||
|
now: time.Date(2025, 8, 2, 10, 30, 0, 0, time.UTC),
|
||||||
|
expected: time.Date(2025, 8, 4, 8, 0, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "weekdays at 8am (Sunday)",
|
||||||
|
// Base time is a Sunday, so next run should be Monday
|
||||||
|
cron: "0 8 * * 1-5",
|
||||||
|
now: time.Date(2025, 8, 3, 10, 30, 0, 0, time.UTC),
|
||||||
|
expected: time.Date(2025, 8, 4, 8, 0, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "monthly on 1st",
|
||||||
|
cron: "0 0 1 * *",
|
||||||
|
now: baseTime,
|
||||||
|
expected: time.Date(2025, 9, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "specific time today",
|
||||||
|
cron: "0 14 * * *",
|
||||||
|
now: baseTime,
|
||||||
|
expected: time.Date(2025, 8, 2, 14, 0, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
trigger, err := scheduling.NewCronTrigger(tt.cron)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create cron trigger: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
next := trigger.NextTime(tt.now)
|
||||||
|
if next == nil {
|
||||||
|
t.Fatal("Expected next time, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !next.Equal(tt.expected) {
|
||||||
|
t.Errorf("Expected %v, got %v", tt.expected, *next)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTriggerInvalid(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expression string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "bad pattern",
|
||||||
|
expression: "invalid",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "requires 5 fields - too few",
|
||||||
|
expression: "4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "requires 5 fields - missing field",
|
||||||
|
expression: "0 9 * *",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "too many fields",
|
||||||
|
expression: "0 9 * * * *",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid minute",
|
||||||
|
expression: "60 9 * * *",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid hour",
|
||||||
|
expression: "0 25 * * *",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid day of month",
|
||||||
|
expression: "0 9 32 * *",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid month",
|
||||||
|
expression: "0 9 * 13 *",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid day of week",
|
||||||
|
expression: "0 9 * * 7",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := scheduling.NewCronTrigger(tt.expression)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error for invalid expression %q", tt.expression)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user