wip: refactor for new config format
This commit is contained in:
@@ -10,6 +10,28 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
serviceDir = "service"
|
||||
)
|
||||
|
||||
var (
|
||||
configDir string
|
||||
)
|
||||
|
||||
func setup(t *testing.T) {
|
||||
var err error
|
||||
if configDir, err = os.MkdirTemp("", "wingmate-*-test"); err != nil {
|
||||
t.Fatal("setup", err)
|
||||
}
|
||||
viper.Set(EnvConfigPath, configDir)
|
||||
}
|
||||
|
||||
func tear(t *testing.T) {
|
||||
if err := os.RemoveAll(configDir); err != nil {
|
||||
t.Fatal("tear", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
|
||||
type testEntry struct {
|
||||
@@ -17,26 +39,6 @@ func TestRead(t *testing.T) {
|
||||
testFunc func(t *testing.T)
|
||||
}
|
||||
|
||||
var (
|
||||
configDir string
|
||||
err error
|
||||
)
|
||||
|
||||
const serviceDir = "service"
|
||||
|
||||
setup := func(t *testing.T) {
|
||||
if configDir, err = os.MkdirTemp("", "wingmate-*-test"); err != nil {
|
||||
t.Fatal("setup", err)
|
||||
}
|
||||
viper.Set(EnvConfigPath, configDir)
|
||||
}
|
||||
|
||||
tear := func(t *testing.T) {
|
||||
if err = os.RemoveAll(configDir); err != nil {
|
||||
t.Fatal("tear", err)
|
||||
}
|
||||
}
|
||||
|
||||
mkSvcDir := func(t *testing.T) {
|
||||
if err := os.MkdirAll(path.Join(configDir, serviceDir), 0755); err != nil {
|
||||
t.Fatal("create dir", err)
|
||||
|
||||
@@ -4,46 +4,34 @@ import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
)
|
||||
|
||||
type CronExactSpec interface {
|
||||
CronTimeSpec
|
||||
Value() uint8
|
||||
}
|
||||
|
||||
type CronMultipleOccurrenceSpec interface {
|
||||
CronTimeSpec
|
||||
Values() []uint8
|
||||
}
|
||||
|
||||
type CronTimeSpec interface {
|
||||
Type() wingmate.CronTimeType
|
||||
Match(uint8) bool
|
||||
//Type() wingmate.CronTimeType
|
||||
//Match(uint8) bool
|
||||
}
|
||||
|
||||
type Cron struct {
|
||||
minute CronTimeSpec
|
||||
hour CronTimeSpec
|
||||
dom CronTimeSpec
|
||||
month CronTimeSpec
|
||||
dow CronTimeSpec
|
||||
command string
|
||||
lastRun time.Time
|
||||
hasRun bool
|
||||
Minute CronTimeSpec
|
||||
Hour CronTimeSpec
|
||||
DoM CronTimeSpec
|
||||
Month CronTimeSpec
|
||||
DoW CronTimeSpec
|
||||
Command string
|
||||
}
|
||||
|
||||
type cronField int
|
||||
|
||||
const (
|
||||
CrontabEntryRegex = `^\s*(?P<minute>\S+)\s+(?P<hour>\S+)\s+(?P<dom>\S+)\s+(?P<month>\S+)\s+(?P<dow>\S+)\s+(?P<command>\S.*\S)\s*$`
|
||||
CrontabSubmatchLen = 7
|
||||
CrontabEntryRegexPattern = `^\s*(?P<minute>\S+)\s+(?P<hour>\S+)\s+(?P<dom>\S+)\s+(?P<month>\S+)\s+(?P<dow>\S+)\s+(?P<command>\S.*\S)\s*$`
|
||||
CrontabCommentLineRegexPattern = `^\s*#.*$`
|
||||
CrontabCommentSuffixRegexPattern = `^\s*([^#]+)#.*$`
|
||||
CrontabSubMatchLen = 7
|
||||
|
||||
minute cronField = iota
|
||||
hour
|
||||
@@ -52,21 +40,22 @@ const (
|
||||
dow
|
||||
)
|
||||
|
||||
var (
|
||||
crontabEntryRegex = regexp.MustCompile(CrontabEntryRegexPattern)
|
||||
crontabCommentLineRegex = regexp.MustCompile(CrontabCommentLineRegexPattern)
|
||||
crontabCommentSuffixRegex = regexp.MustCompile(CrontabCommentSuffixRegexPattern)
|
||||
)
|
||||
|
||||
func readCrontab(path string) ([]*Cron, error) {
|
||||
var (
|
||||
file *os.File
|
||||
err error
|
||||
scanner *bufio.Scanner
|
||||
line string
|
||||
re *regexp.Regexp
|
||||
parts []string
|
||||
retval []*Cron
|
||||
)
|
||||
|
||||
if re, err = regexp.Compile(CrontabEntryRegex); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if file, err = os.Open(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -79,41 +68,48 @@ func readCrontab(path string) ([]*Cron, error) {
|
||||
for scanner.Scan() {
|
||||
line = scanner.Text()
|
||||
|
||||
parts = re.FindStringSubmatch(line)
|
||||
if len(parts) != CrontabSubmatchLen {
|
||||
if crontabCommentLineRegex.MatchString(line) {
|
||||
continue
|
||||
}
|
||||
|
||||
parts = crontabCommentSuffixRegex.FindStringSubmatch(line)
|
||||
if len(parts) == 2 {
|
||||
line = parts[1]
|
||||
}
|
||||
|
||||
parts = crontabEntryRegex.FindStringSubmatch(line)
|
||||
if len(parts) != CrontabSubMatchLen {
|
||||
wingmate.Log().Error().Msgf("invalid entry %s", line)
|
||||
continue
|
||||
}
|
||||
|
||||
c := &Cron{
|
||||
hasRun: false,
|
||||
}
|
||||
c := &Cron{}
|
||||
if err = c.setField(minute, parts[1]); err != nil {
|
||||
wingmate.Log().Error().Msgf("error parsing minute field %+v", err)
|
||||
wingmate.Log().Error().Msgf("error parsing Minute field %+v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = c.setField(hour, parts[2]); err != nil {
|
||||
wingmate.Log().Error().Msgf("error parsing hour field %+v", err)
|
||||
wingmate.Log().Error().Msgf("error parsing Hour field %+v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = c.setField(dom, parts[3]); err != nil {
|
||||
wingmate.Log().Error().Msgf("error parsing day of month field %+v", err)
|
||||
wingmate.Log().Error().Msgf("error parsing Day of Month field %+v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = c.setField(month, parts[4]); err != nil {
|
||||
wingmate.Log().Error().Msgf("error parsing month field %+v", err)
|
||||
wingmate.Log().Error().Msgf("error parsing Month field %+v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = c.setField(dow, parts[5]); err != nil {
|
||||
wingmate.Log().Error().Msgf("error parsing day of week field %+v", err)
|
||||
wingmate.Log().Error().Msgf("error parsing Day of Week field %+v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
c.command = parts[6]
|
||||
c.Command = parts[6]
|
||||
|
||||
retval = append(retval, c)
|
||||
}
|
||||
@@ -121,35 +117,6 @@ func readCrontab(path string) ([]*Cron, error) {
|
||||
return retval, nil
|
||||
}
|
||||
|
||||
func (c *Cron) Command() string {
|
||||
return c.command
|
||||
}
|
||||
|
||||
func (c *Cron) TimeToRun(now time.Time) bool {
|
||||
if c.minute.Match(uint8(now.Minute())) &&
|
||||
c.hour.Match(uint8(now.Hour())) &&
|
||||
c.dom.Match(uint8(now.Day())) &&
|
||||
c.month.Match(uint8(now.Month())) &&
|
||||
c.dow.Match(uint8(now.Weekday())) {
|
||||
|
||||
if c.hasRun {
|
||||
if now.Sub(c.lastRun) <= time.Minute && now.Minute() == c.lastRun.Minute() {
|
||||
return false
|
||||
} else {
|
||||
c.lastRun = now
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
|
||||
c.lastRun = now
|
||||
c.hasRun = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type fieldRange struct {
|
||||
min int
|
||||
max int
|
||||
@@ -182,25 +149,25 @@ func (c *Cron) setField(field cronField, input string) error {
|
||||
switch field {
|
||||
case minute:
|
||||
fr = newRange(0, 59)
|
||||
cField = &c.minute
|
||||
cField = &c.Minute
|
||||
case hour:
|
||||
fr = newRange(0, 23)
|
||||
cField = &c.hour
|
||||
cField = &c.Hour
|
||||
case dom:
|
||||
fr = newRange(1, 31)
|
||||
cField = &c.dom
|
||||
cField = &c.DoM
|
||||
case month:
|
||||
fr = newRange(1, 12)
|
||||
cField = &c.month
|
||||
cField = &c.Month
|
||||
case dow:
|
||||
fr = newRange(0, 6)
|
||||
cField = &c.dow
|
||||
cField = &c.DoW
|
||||
default:
|
||||
return errors.New("invalid cron field descriptor")
|
||||
}
|
||||
|
||||
if input == "*" {
|
||||
*cField = &specAny{}
|
||||
*cField = &SpecAny{}
|
||||
} else if strings.HasPrefix(input, "*/") {
|
||||
if parsed64, err = strconv.ParseUint(input[2:], 10, 8); err != nil {
|
||||
return fmt.Errorf("error parse field %+v with input %s: %w", field, input, err)
|
||||
@@ -217,7 +184,7 @@ func (c *Cron) setField(field cronField, input string) error {
|
||||
current += parsed
|
||||
}
|
||||
|
||||
*cField = &specMultiOccurrence{
|
||||
*cField = &SpecMultiOccurrence{
|
||||
values: multi,
|
||||
}
|
||||
} else {
|
||||
@@ -237,7 +204,7 @@ func (c *Cron) setField(field cronField, input string) error {
|
||||
multi = append(multi, parsed)
|
||||
}
|
||||
|
||||
*cField = &specMultiOccurrence{
|
||||
*cField = &SpecMultiOccurrence{
|
||||
values: multi,
|
||||
}
|
||||
} else {
|
||||
@@ -250,7 +217,7 @@ func (c *Cron) setField(field cronField, input string) error {
|
||||
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input)
|
||||
}
|
||||
|
||||
*cField = &specExact{
|
||||
*cField = &SpecExact{
|
||||
value: parsed,
|
||||
}
|
||||
}
|
||||
@@ -259,51 +226,21 @@ func (c *Cron) setField(field cronField, input string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type specAny struct{}
|
||||
type SpecAny struct{}
|
||||
|
||||
func (a *specAny) Type() wingmate.CronTimeType {
|
||||
return wingmate.Any
|
||||
}
|
||||
|
||||
func (a *specAny) Match(u uint8) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type specExact struct {
|
||||
type SpecExact struct {
|
||||
value uint8
|
||||
}
|
||||
|
||||
func (e *specExact) Type() wingmate.CronTimeType {
|
||||
return wingmate.Exact
|
||||
}
|
||||
|
||||
func (e *specExact) Match(u uint8) bool {
|
||||
return u == e.value
|
||||
}
|
||||
|
||||
func (e *specExact) Value() uint8 {
|
||||
func (e *SpecExact) Value() uint8 {
|
||||
return e.value
|
||||
}
|
||||
|
||||
type specMultiOccurrence struct {
|
||||
type SpecMultiOccurrence struct {
|
||||
values []uint8
|
||||
}
|
||||
|
||||
func (m *specMultiOccurrence) Type() wingmate.CronTimeType {
|
||||
return wingmate.MultipleOccurrence
|
||||
}
|
||||
|
||||
func (m *specMultiOccurrence) Match(u uint8) bool {
|
||||
for _, v := range m.values {
|
||||
if v == u {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *specMultiOccurrence) Values() []uint8 {
|
||||
func (m *SpecMultiOccurrence) Values() []uint8 {
|
||||
out := make([]uint8, len(m.values))
|
||||
copy(out, m.values)
|
||||
return out
|
||||
|
||||
128
config/crontab_test.go
Normal file
128
config/crontab_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
crontabFileName = "crontab"
|
||||
)
|
||||
|
||||
func TestCrontab(t *testing.T) {
|
||||
type testEntry struct {
|
||||
name string
|
||||
crontab string
|
||||
wantErr bool
|
||||
}
|
||||
|
||||
_ = wingmate.NewLog(os.Stderr)
|
||||
tests := []testEntry{
|
||||
{
|
||||
name: "positive",
|
||||
crontab: crontabTestCase0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "with comment",
|
||||
crontab: crontabTestCase1,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "various values",
|
||||
crontab: crontabTestCase2,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "failed to parse",
|
||||
crontab: crontabTestCase3,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setup(t)
|
||||
defer tear(t)
|
||||
|
||||
writeCrontab(t, tt.crontab)
|
||||
|
||||
cfg, err := Read()
|
||||
if tt.wantErr != (err != nil) {
|
||||
t.Fatalf("wantErr is %v but err is %+v", tt.wantErr, err)
|
||||
}
|
||||
|
||||
t.Logf("cfg: %+v", cfg)
|
||||
for _, c := range cfg.Cron {
|
||||
t.Logf("%+v", c)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeCrontab(t *testing.T, content string) {
|
||||
var (
|
||||
f *os.File
|
||||
err error
|
||||
)
|
||||
|
||||
if f, err = os.Create(filepath.Join(configDir, crontabFileName)); err != nil {
|
||||
t.Fatal("create crontab file", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
|
||||
if _, err = f.Write([]byte(content)); err != nil {
|
||||
t.Fatal("writing crontab file", err)
|
||||
}
|
||||
}
|
||||
|
||||
const crontabTestCase0 = `* * * * * /path/to/executable`
|
||||
const crontabTestCase1 = `# this is a comment
|
||||
## comment with space
|
||||
* * * * * /path/to/executable
|
||||
* * * * * /path/to/executable # comment as a suffix
|
||||
`
|
||||
|
||||
const crontabTestCase2 = `# first comment
|
||||
*/5 13 3,5,7 * * /path/to/executable`
|
||||
|
||||
const crontabTestCase3 = `a 13 3,5,7 * * /path/to/executable
|
||||
*/5 a 3,5,7 * * /path/to/executable
|
||||
*/5 13 a * * /path/to/executable
|
||||
*/5 13 3,5,7 a * /path/to/executable
|
||||
*/5 13 3,5,7 * a /path/to/executable
|
||||
*/x 13 3,5,7 * a /path/to/executable
|
||||
76 13 3,5,7 * a /path/to/executable
|
||||
*/75 13 3,5,7 * a /path/to/executable
|
||||
*/5 13 3,x,7 * a /path/to/executable
|
||||
*/5 13 3,5,67 * a /path/to/executable
|
||||
*/5 13 * * /path/to/executable
|
||||
*/5 13 3,5,7 * * /path/to/executable`
|
||||
|
||||
func TestSpecExact(t *testing.T) {
|
||||
var val uint8 = 45
|
||||
s := SpecExact{
|
||||
value: val,
|
||||
}
|
||||
|
||||
assert.Equal(t, val, s.Value())
|
||||
}
|
||||
|
||||
func TestSpecMulti(t *testing.T) {
|
||||
val := []uint8{3, 5, 7, 15}
|
||||
s := SpecMultiOccurrence{
|
||||
values: val,
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, val, s.Values())
|
||||
}
|
||||
|
||||
func TestInvalidField(t *testing.T) {
|
||||
c := &Cron{}
|
||||
assert.NotNil(t, c.setField(cronField(99), "x"))
|
||||
}
|
||||
Reference in New Issue
Block a user