mirror of
https://github.com/usememos/memos.git
synced 2024-11-15 03:07:04 +08:00
384 lines
9.9 KiB
Go
384 lines
9.9 KiB
Go
//nolint:all
|
|
package cron
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var secondParser = NewParser(Second | Minute | Hour | Dom | Month | DowOptional | Descriptor)
|
|
|
|
func TestRange(t *testing.T) {
|
|
zero := uint64(0)
|
|
ranges := []struct {
|
|
expr string
|
|
min, max uint
|
|
expected uint64
|
|
err string
|
|
}{
|
|
{"5", 0, 7, 1 << 5, ""},
|
|
{"0", 0, 7, 1 << 0, ""},
|
|
{"7", 0, 7, 1 << 7, ""},
|
|
|
|
{"5-5", 0, 7, 1 << 5, ""},
|
|
{"5-6", 0, 7, 1<<5 | 1<<6, ""},
|
|
{"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7, ""},
|
|
|
|
{"5-6/2", 0, 7, 1 << 5, ""},
|
|
{"5-7/2", 0, 7, 1<<5 | 1<<7, ""},
|
|
{"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7, ""},
|
|
|
|
{"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | starBit, ""},
|
|
{"*/2", 1, 3, 1<<1 | 1<<3, ""},
|
|
|
|
{"5--5", 0, 0, zero, "too many hyphens"},
|
|
{"jan-x", 0, 0, zero, `failed to parse number: strconv.Atoi: parsing "jan": invalid syntax`},
|
|
{"2-x", 1, 5, zero, `failed to parse number: strconv.Atoi: parsing "x": invalid syntax`},
|
|
{"*/-12", 0, 0, zero, "number must be positive"},
|
|
{"*//2", 0, 0, zero, "too many slashes"},
|
|
{"1", 3, 5, zero, "below minimum"},
|
|
{"6", 3, 5, zero, "above maximum"},
|
|
{"5-3", 3, 5, zero, "beginning of range after end: 5-3"},
|
|
{"*/0", 0, 0, zero, "step cannot be zero: */0"},
|
|
}
|
|
|
|
for _, c := range ranges {
|
|
actual, err := getRange(c.expr, bounds{c.min, c.max, nil})
|
|
if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) {
|
|
t.Errorf("%s => expected %v, got %v", c.expr, c.err, err)
|
|
}
|
|
if len(c.err) == 0 && err != nil {
|
|
t.Errorf("%s => unexpected error %v", c.expr, err)
|
|
}
|
|
if actual != c.expected {
|
|
t.Errorf("%s => expected %d, got %d", c.expr, c.expected, actual)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestField(t *testing.T) {
|
|
fields := []struct {
|
|
expr string
|
|
min, max uint
|
|
expected uint64
|
|
}{
|
|
{"5", 1, 7, 1 << 5},
|
|
{"5,6", 1, 7, 1<<5 | 1<<6},
|
|
{"5,6,7", 1, 7, 1<<5 | 1<<6 | 1<<7},
|
|
{"1,5-7/2,3", 1, 7, 1<<1 | 1<<5 | 1<<7 | 1<<3},
|
|
}
|
|
|
|
for _, c := range fields {
|
|
actual, _ := getField(c.expr, bounds{c.min, c.max, nil})
|
|
if actual != c.expected {
|
|
t.Errorf("%s => expected %d, got %d", c.expr, c.expected, actual)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAll(t *testing.T) {
|
|
allBits := []struct {
|
|
r bounds
|
|
expected uint64
|
|
}{
|
|
{minutes, 0xfffffffffffffff}, // 0-59: 60 ones
|
|
{hours, 0xffffff}, // 0-23: 24 ones
|
|
{dom, 0xfffffffe}, // 1-31: 31 ones, 1 zero
|
|
{months, 0x1ffe}, // 1-12: 12 ones, 1 zero
|
|
{dow, 0x7f}, // 0-6: 7 ones
|
|
}
|
|
|
|
for _, c := range allBits {
|
|
actual := all(c.r) // all() adds the starBit, so compensate for that..
|
|
if c.expected|starBit != actual {
|
|
t.Errorf("%d-%d/%d => expected %b, got %b",
|
|
c.r.min, c.r.max, 1, c.expected|starBit, actual)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBits(t *testing.T) {
|
|
bits := []struct {
|
|
min, max, step uint
|
|
expected uint64
|
|
}{
|
|
{0, 0, 1, 0x1},
|
|
{1, 1, 1, 0x2},
|
|
{1, 5, 2, 0x2a}, // 101010
|
|
{1, 4, 2, 0xa}, // 1010
|
|
}
|
|
|
|
for _, c := range bits {
|
|
actual := getBits(c.min, c.max, c.step)
|
|
if c.expected != actual {
|
|
t.Errorf("%d-%d/%d => expected %b, got %b",
|
|
c.min, c.max, c.step, c.expected, actual)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseScheduleErrors(t *testing.T) {
|
|
var tests = []struct{ expr, err string }{
|
|
{"* 5 j * * *", `failed to parse number: strconv.Atoi: parsing "j": invalid syntax`},
|
|
{"@every Xm", "failed to parse duration"},
|
|
{"@unrecognized", "unrecognized descriptor"},
|
|
{"* * * *", "incorrect number of fields, expected 5-6"},
|
|
{"", "empty spec string"},
|
|
}
|
|
for _, c := range tests {
|
|
actual, err := secondParser.Parse(c.expr)
|
|
if err == nil || !strings.Contains(err.Error(), c.err) {
|
|
t.Errorf("%s => expected %v, got %v", c.expr, c.err, err)
|
|
}
|
|
if actual != nil {
|
|
t.Errorf("expected nil schedule on error, got %v", actual)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseSchedule(t *testing.T) {
|
|
tokyo, _ := time.LoadLocation("Asia/Tokyo")
|
|
entries := []struct {
|
|
parser Parser
|
|
expr string
|
|
expected Schedule
|
|
}{
|
|
{secondParser, "0 5 * * * *", every5min(time.Local)},
|
|
{standardParser, "5 * * * *", every5min(time.Local)},
|
|
{secondParser, "CRON_TZ=UTC 0 5 * * * *", every5min(time.UTC)},
|
|
{standardParser, "CRON_TZ=UTC 5 * * * *", every5min(time.UTC)},
|
|
{secondParser, "CRON_TZ=Asia/Tokyo 0 5 * * * *", every5min(tokyo)},
|
|
{secondParser, "@every 5m", ConstantDelaySchedule{5 * time.Minute}},
|
|
{secondParser, "@midnight", midnight(time.Local)},
|
|
{secondParser, "TZ=UTC @midnight", midnight(time.UTC)},
|
|
{secondParser, "TZ=Asia/Tokyo @midnight", midnight(tokyo)},
|
|
{secondParser, "@yearly", annual(time.Local)},
|
|
{secondParser, "@annually", annual(time.Local)},
|
|
{
|
|
parser: secondParser,
|
|
expr: "* 5 * * * *",
|
|
expected: &SpecSchedule{
|
|
Second: all(seconds),
|
|
Minute: 1 << 5,
|
|
Hour: all(hours),
|
|
Dom: all(dom),
|
|
Month: all(months),
|
|
Dow: all(dow),
|
|
Location: time.Local,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, c := range entries {
|
|
actual, err := c.parser.Parse(c.expr)
|
|
if err != nil {
|
|
t.Errorf("%s => unexpected error %v", c.expr, err)
|
|
}
|
|
if !reflect.DeepEqual(actual, c.expected) {
|
|
t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOptionalSecondSchedule(t *testing.T) {
|
|
parser := NewParser(SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor)
|
|
entries := []struct {
|
|
expr string
|
|
expected Schedule
|
|
}{
|
|
{"0 5 * * * *", every5min(time.Local)},
|
|
{"5 5 * * * *", every5min5s(time.Local)},
|
|
{"5 * * * *", every5min(time.Local)},
|
|
}
|
|
|
|
for _, c := range entries {
|
|
actual, err := parser.Parse(c.expr)
|
|
if err != nil {
|
|
t.Errorf("%s => unexpected error %v", c.expr, err)
|
|
}
|
|
if !reflect.DeepEqual(actual, c.expected) {
|
|
t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNormalizeFields(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input []string
|
|
options ParseOption
|
|
expected []string
|
|
}{
|
|
{
|
|
"AllFields_NoOptional",
|
|
[]string{"0", "5", "*", "*", "*", "*"},
|
|
Second | Minute | Hour | Dom | Month | Dow | Descriptor,
|
|
[]string{"0", "5", "*", "*", "*", "*"},
|
|
},
|
|
{
|
|
"AllFields_SecondOptional_Provided",
|
|
[]string{"0", "5", "*", "*", "*", "*"},
|
|
SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor,
|
|
[]string{"0", "5", "*", "*", "*", "*"},
|
|
},
|
|
{
|
|
"AllFields_SecondOptional_NotProvided",
|
|
[]string{"5", "*", "*", "*", "*"},
|
|
SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor,
|
|
[]string{"0", "5", "*", "*", "*", "*"},
|
|
},
|
|
{
|
|
"SubsetFields_NoOptional",
|
|
[]string{"5", "15", "*"},
|
|
Hour | Dom | Month,
|
|
[]string{"0", "0", "5", "15", "*", "*"},
|
|
},
|
|
{
|
|
"SubsetFields_DowOptional_Provided",
|
|
[]string{"5", "15", "*", "4"},
|
|
Hour | Dom | Month | DowOptional,
|
|
[]string{"0", "0", "5", "15", "*", "4"},
|
|
},
|
|
{
|
|
"SubsetFields_DowOptional_NotProvided",
|
|
[]string{"5", "15", "*"},
|
|
Hour | Dom | Month | DowOptional,
|
|
[]string{"0", "0", "5", "15", "*", "*"},
|
|
},
|
|
{
|
|
"SubsetFields_SecondOptional_NotProvided",
|
|
[]string{"5", "15", "*"},
|
|
SecondOptional | Hour | Dom | Month,
|
|
[]string{"0", "0", "5", "15", "*", "*"},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(*testing.T) {
|
|
actual, err := normalizeFields(test.input, test.options)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(actual, test.expected) {
|
|
t.Errorf("expected %v, got %v", test.expected, actual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeFields_Errors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input []string
|
|
options ParseOption
|
|
err string
|
|
}{
|
|
{
|
|
"TwoOptionals",
|
|
[]string{"0", "5", "*", "*", "*", "*"},
|
|
SecondOptional | Minute | Hour | Dom | Month | DowOptional,
|
|
"",
|
|
},
|
|
{
|
|
"TooManyFields",
|
|
[]string{"0", "5", "*", "*"},
|
|
SecondOptional | Minute | Hour,
|
|
"",
|
|
},
|
|
{
|
|
"NoFields",
|
|
[]string{},
|
|
SecondOptional | Minute | Hour,
|
|
"",
|
|
},
|
|
{
|
|
"TooFewFields",
|
|
[]string{"*"},
|
|
SecondOptional | Minute | Hour,
|
|
"",
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(*testing.T) {
|
|
actual, err := normalizeFields(test.input, test.options)
|
|
if err == nil {
|
|
t.Errorf("expected an error, got none. results: %v", actual)
|
|
}
|
|
if !strings.Contains(err.Error(), test.err) {
|
|
t.Errorf("expected error %q, got %q", test.err, err.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStandardSpecSchedule(t *testing.T) {
|
|
entries := []struct {
|
|
expr string
|
|
expected Schedule
|
|
err string
|
|
}{
|
|
{
|
|
expr: "5 * * * *",
|
|
expected: &SpecSchedule{1 << seconds.min, 1 << 5, all(hours), all(dom), all(months), all(dow), time.Local},
|
|
},
|
|
{
|
|
expr: "@every 5m",
|
|
expected: ConstantDelaySchedule{time.Duration(5) * time.Minute},
|
|
},
|
|
{
|
|
expr: "5 j * * *",
|
|
err: `failed to parse number: strconv.Atoi: parsing "j": invalid syntax`,
|
|
},
|
|
{
|
|
expr: "* * * *",
|
|
err: "incorrect number of fields",
|
|
},
|
|
}
|
|
|
|
for _, c := range entries {
|
|
actual, err := ParseStandard(c.expr)
|
|
if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) {
|
|
t.Errorf("%s => expected %v, got %v", c.expr, c.err, err)
|
|
}
|
|
if len(c.err) == 0 && err != nil {
|
|
t.Errorf("%s => unexpected error %v", c.expr, err)
|
|
}
|
|
if !reflect.DeepEqual(actual, c.expected) {
|
|
t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNoDescriptorParser(t *testing.T) {
|
|
parser := NewParser(Minute | Hour)
|
|
_, err := parser.Parse("@every 1m")
|
|
if err == nil {
|
|
t.Error("expected an error, got none")
|
|
}
|
|
}
|
|
|
|
func every5min(loc *time.Location) *SpecSchedule {
|
|
return &SpecSchedule{1 << 0, 1 << 5, all(hours), all(dom), all(months), all(dow), loc}
|
|
}
|
|
|
|
func every5min5s(loc *time.Location) *SpecSchedule {
|
|
return &SpecSchedule{1 << 5, 1 << 5, all(hours), all(dom), all(months), all(dow), loc}
|
|
}
|
|
|
|
func midnight(loc *time.Location) *SpecSchedule {
|
|
return &SpecSchedule{1, 1, 1, all(dom), all(months), all(dow), loc}
|
|
}
|
|
|
|
func annual(loc *time.Location) *SpecSchedule {
|
|
return &SpecSchedule{
|
|
Second: 1 << seconds.min,
|
|
Minute: 1 << minutes.min,
|
|
Hour: 1 << hours.min,
|
|
Dom: 1 << dom.min,
|
|
Month: 1 << months.min,
|
|
Dow: all(dow),
|
|
Location: loc,
|
|
}
|
|
}
|