git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/cron/parser_test.go (about)

     1  package cron
     2  
     3  import (
     4  	"reflect"
     5  	"strings"
     6  	"testing"
     7  	"time"
     8  )
     9  
    10  var secondParser = NewParser(Second | Minute | Hour | Dom | Month | DowOptional | Descriptor)
    11  
    12  func TestRange(t *testing.T) {
    13  	zero := uint64(0)
    14  	ranges := []struct {
    15  		expr     string
    16  		min, max uint
    17  		expected uint64
    18  		err      string
    19  	}{
    20  		{"5", 0, 7, 1 << 5, ""},
    21  		{"0", 0, 7, 1 << 0, ""},
    22  		{"7", 0, 7, 1 << 7, ""},
    23  
    24  		{"5-5", 0, 7, 1 << 5, ""},
    25  		{"5-6", 0, 7, 1<<5 | 1<<6, ""},
    26  		{"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7, ""},
    27  
    28  		{"5-6/2", 0, 7, 1 << 5, ""},
    29  		{"5-7/2", 0, 7, 1<<5 | 1<<7, ""},
    30  		{"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7, ""},
    31  
    32  		{"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | starBit, ""},
    33  		{"*/2", 1, 3, 1<<1 | 1<<3, ""},
    34  
    35  		{"5--5", 0, 0, zero, "too many hyphens"},
    36  		{"jan-x", 0, 0, zero, "failed to parse int from"},
    37  		{"2-x", 1, 5, zero, "failed to parse int from"},
    38  		{"*/-12", 0, 0, zero, "negative number"},
    39  		{"*//2", 0, 0, zero, "too many slashes"},
    40  		{"1", 3, 5, zero, "below minimum"},
    41  		{"6", 3, 5, zero, "above maximum"},
    42  		{"5-3", 3, 5, zero, "beyond end of range"},
    43  		{"*/0", 0, 0, zero, "should be a positive number"},
    44  	}
    45  
    46  	for _, c := range ranges {
    47  		actual, err := getRange(c.expr, bounds{c.min, c.max, nil})
    48  		if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) {
    49  			t.Errorf("%s => expected %v, got %v", c.expr, c.err, err)
    50  		}
    51  		if len(c.err) == 0 && err != nil {
    52  			t.Errorf("%s => unexpected error %v", c.expr, err)
    53  		}
    54  		if actual != c.expected {
    55  			t.Errorf("%s => expected %d, got %d", c.expr, c.expected, actual)
    56  		}
    57  	}
    58  }
    59  
    60  func TestField(t *testing.T) {
    61  	fields := []struct {
    62  		expr     string
    63  		min, max uint
    64  		expected uint64
    65  	}{
    66  		{"5", 1, 7, 1 << 5},
    67  		{"5,6", 1, 7, 1<<5 | 1<<6},
    68  		{"5,6,7", 1, 7, 1<<5 | 1<<6 | 1<<7},
    69  		{"1,5-7/2,3", 1, 7, 1<<1 | 1<<5 | 1<<7 | 1<<3},
    70  	}
    71  
    72  	for _, c := range fields {
    73  		actual, _ := getField(c.expr, bounds{c.min, c.max, nil})
    74  		if actual != c.expected {
    75  			t.Errorf("%s => expected %d, got %d", c.expr, c.expected, actual)
    76  		}
    77  	}
    78  }
    79  
    80  func TestAll(t *testing.T) {
    81  	allBits := []struct {
    82  		r        bounds
    83  		expected uint64
    84  	}{
    85  		{minutes, 0xfffffffffffffff}, // 0-59: 60 ones
    86  		{hours, 0xffffff},            // 0-23: 24 ones
    87  		{dom, 0xfffffffe},            // 1-31: 31 ones, 1 zero
    88  		{months, 0x1ffe},             // 1-12: 12 ones, 1 zero
    89  		{dow, 0x7f},                  // 0-6: 7 ones
    90  	}
    91  
    92  	for _, c := range allBits {
    93  		actual := all(c.r) // all() adds the starBit, so compensate for that..
    94  		if c.expected|starBit != actual {
    95  			t.Errorf("%d-%d/%d => expected %b, got %b",
    96  				c.r.min, c.r.max, 1, c.expected|starBit, actual)
    97  		}
    98  	}
    99  }
   100  
   101  func TestBits(t *testing.T) {
   102  	bits := []struct {
   103  		min, max, step uint
   104  		expected       uint64
   105  	}{
   106  		{0, 0, 1, 0x1},
   107  		{1, 1, 1, 0x2},
   108  		{1, 5, 2, 0x2a}, // 101010
   109  		{1, 4, 2, 0xa},  // 1010
   110  	}
   111  
   112  	for _, c := range bits {
   113  		actual := getBits(c.min, c.max, c.step)
   114  		if c.expected != actual {
   115  			t.Errorf("%d-%d/%d => expected %b, got %b",
   116  				c.min, c.max, c.step, c.expected, actual)
   117  		}
   118  	}
   119  }
   120  
   121  func TestParseScheduleErrors(t *testing.T) {
   122  	var tests = []struct{ expr, err string }{
   123  		{"* 5 j * * *", "failed to parse int from"},
   124  		{"@every Xm", "failed to parse duration"},
   125  		{"@unrecognized", "unrecognized descriptor"},
   126  		{"* * * *", "expected 5 to 6 fields"},
   127  		{"", "empty spec string"},
   128  	}
   129  	for _, c := range tests {
   130  		actual, err := secondParser.Parse(c.expr)
   131  		if err == nil || !strings.Contains(err.Error(), c.err) {
   132  			t.Errorf("%s => expected %v, got %v", c.expr, c.err, err)
   133  		}
   134  		if actual != nil {
   135  			t.Errorf("expected nil schedule on error, got %v", actual)
   136  		}
   137  	}
   138  }
   139  
   140  func TestParseSchedule(t *testing.T) {
   141  	tokyo, _ := time.LoadLocation("Asia/Tokyo")
   142  	entries := []struct {
   143  		parser   Parser
   144  		expr     string
   145  		expected Schedule
   146  	}{
   147  		{secondParser, "0 5 * * * *", every5min(time.Local)},
   148  		{standardParser, "5 * * * *", every5min(time.Local)},
   149  		{secondParser, "CRON_TZ=UTC  0 5 * * * *", every5min(time.UTC)},
   150  		{standardParser, "CRON_TZ=UTC  5 * * * *", every5min(time.UTC)},
   151  		{secondParser, "CRON_TZ=Asia/Tokyo 0 5 * * * *", every5min(tokyo)},
   152  		{secondParser, "@every 5m", ConstantDelaySchedule{5 * time.Minute}},
   153  		{secondParser, "@midnight", midnight(time.Local)},
   154  		{secondParser, "TZ=UTC  @midnight", midnight(time.UTC)},
   155  		{secondParser, "TZ=Asia/Tokyo @midnight", midnight(tokyo)},
   156  		{secondParser, "@yearly", annual(time.Local)},
   157  		{secondParser, "@annually", annual(time.Local)},
   158  		{
   159  			parser: secondParser,
   160  			expr:   "* 5 * * * *",
   161  			expected: &SpecSchedule{
   162  				Second:   all(seconds),
   163  				Minute:   1 << 5,
   164  				Hour:     all(hours),
   165  				Dom:      all(dom),
   166  				Month:    all(months),
   167  				Dow:      all(dow),
   168  				Location: time.Local,
   169  			},
   170  		},
   171  	}
   172  
   173  	for _, c := range entries {
   174  		actual, err := c.parser.Parse(c.expr)
   175  		if err != nil {
   176  			t.Errorf("%s => unexpected error %v", c.expr, err)
   177  		}
   178  		if !reflect.DeepEqual(actual, c.expected) {
   179  			t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual)
   180  		}
   181  	}
   182  }
   183  
   184  func TestOptionalSecondSchedule(t *testing.T) {
   185  	parser := NewParser(SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor)
   186  	entries := []struct {
   187  		expr     string
   188  		expected Schedule
   189  	}{
   190  		{"0 5 * * * *", every5min(time.Local)},
   191  		{"5 5 * * * *", every5min5s(time.Local)},
   192  		{"5 * * * *", every5min(time.Local)},
   193  	}
   194  
   195  	for _, c := range entries {
   196  		actual, err := parser.Parse(c.expr)
   197  		if err != nil {
   198  			t.Errorf("%s => unexpected error %v", c.expr, err)
   199  		}
   200  		if !reflect.DeepEqual(actual, c.expected) {
   201  			t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual)
   202  		}
   203  	}
   204  }
   205  
   206  func TestNormalizeFields(t *testing.T) {
   207  	tests := []struct {
   208  		name     string
   209  		input    []string
   210  		options  ParseOption
   211  		expected []string
   212  	}{
   213  		{
   214  			"AllFields_NoOptional",
   215  			[]string{"0", "5", "*", "*", "*", "*"},
   216  			Second | Minute | Hour | Dom | Month | Dow | Descriptor,
   217  			[]string{"0", "5", "*", "*", "*", "*"},
   218  		},
   219  		{
   220  			"AllFields_SecondOptional_Provided",
   221  			[]string{"0", "5", "*", "*", "*", "*"},
   222  			SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor,
   223  			[]string{"0", "5", "*", "*", "*", "*"},
   224  		},
   225  		{
   226  			"AllFields_SecondOptional_NotProvided",
   227  			[]string{"5", "*", "*", "*", "*"},
   228  			SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor,
   229  			[]string{"0", "5", "*", "*", "*", "*"},
   230  		},
   231  		{
   232  			"SubsetFields_NoOptional",
   233  			[]string{"5", "15", "*"},
   234  			Hour | Dom | Month,
   235  			[]string{"0", "0", "5", "15", "*", "*"},
   236  		},
   237  		{
   238  			"SubsetFields_DowOptional_Provided",
   239  			[]string{"5", "15", "*", "4"},
   240  			Hour | Dom | Month | DowOptional,
   241  			[]string{"0", "0", "5", "15", "*", "4"},
   242  		},
   243  		{
   244  			"SubsetFields_DowOptional_NotProvided",
   245  			[]string{"5", "15", "*"},
   246  			Hour | Dom | Month | DowOptional,
   247  			[]string{"0", "0", "5", "15", "*", "*"},
   248  		},
   249  		{
   250  			"SubsetFields_SecondOptional_NotProvided",
   251  			[]string{"5", "15", "*"},
   252  			SecondOptional | Hour | Dom | Month,
   253  			[]string{"0", "0", "5", "15", "*", "*"},
   254  		},
   255  	}
   256  
   257  	for _, test := range tests {
   258  		t.Run(test.name, func(t *testing.T) {
   259  			actual, err := normalizeFields(test.input, test.options)
   260  			if err != nil {
   261  				t.Errorf("unexpected error: %v", err)
   262  			}
   263  			if !reflect.DeepEqual(actual, test.expected) {
   264  				t.Errorf("expected %v, got %v", test.expected, actual)
   265  			}
   266  		})
   267  	}
   268  }
   269  
   270  func TestNormalizeFields_Errors(t *testing.T) {
   271  	tests := []struct {
   272  		name    string
   273  		input   []string
   274  		options ParseOption
   275  		err     string
   276  	}{
   277  		{
   278  			"TwoOptionals",
   279  			[]string{"0", "5", "*", "*", "*", "*"},
   280  			SecondOptional | Minute | Hour | Dom | Month | DowOptional,
   281  			"",
   282  		},
   283  		{
   284  			"TooManyFields",
   285  			[]string{"0", "5", "*", "*"},
   286  			SecondOptional | Minute | Hour,
   287  			"",
   288  		},
   289  		{
   290  			"NoFields",
   291  			[]string{},
   292  			SecondOptional | Minute | Hour,
   293  			"",
   294  		},
   295  		{
   296  			"TooFewFields",
   297  			[]string{"*"},
   298  			SecondOptional | Minute | Hour,
   299  			"",
   300  		},
   301  	}
   302  	for _, test := range tests {
   303  		t.Run(test.name, func(t *testing.T) {
   304  			actual, err := normalizeFields(test.input, test.options)
   305  			if err == nil {
   306  				t.Errorf("expected an error, got none. results: %v", actual)
   307  			}
   308  			if !strings.Contains(err.Error(), test.err) {
   309  				t.Errorf("expected error %q, got %q", test.err, err.Error())
   310  			}
   311  		})
   312  	}
   313  }
   314  
   315  func TestStandardSpecSchedule(t *testing.T) {
   316  	entries := []struct {
   317  		expr     string
   318  		expected Schedule
   319  		err      string
   320  	}{
   321  		{
   322  			expr:     "5 * * * *",
   323  			expected: &SpecSchedule{1 << seconds.min, 1 << 5, all(hours), all(dom), all(months), all(dow), time.Local},
   324  		},
   325  		{
   326  			expr:     "@every 5m",
   327  			expected: ConstantDelaySchedule{time.Duration(5) * time.Minute},
   328  		},
   329  		{
   330  			expr: "5 j * * *",
   331  			err:  "failed to parse int from",
   332  		},
   333  		{
   334  			expr: "* * * *",
   335  			err:  "expected exactly 5 fields",
   336  		},
   337  	}
   338  
   339  	for _, c := range entries {
   340  		actual, err := ParseStandard(c.expr)
   341  		if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) {
   342  			t.Errorf("%s => expected %v, got %v", c.expr, c.err, err)
   343  		}
   344  		if len(c.err) == 0 && err != nil {
   345  			t.Errorf("%s => unexpected error %v", c.expr, err)
   346  		}
   347  		if !reflect.DeepEqual(actual, c.expected) {
   348  			t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual)
   349  		}
   350  	}
   351  }
   352  
   353  func TestNoDescriptorParser(t *testing.T) {
   354  	parser := NewParser(Minute | Hour)
   355  	_, err := parser.Parse("@every 1m")
   356  	if err == nil {
   357  		t.Error("expected an error, got none")
   358  	}
   359  }
   360  
   361  func every5min(loc *time.Location) *SpecSchedule {
   362  	return &SpecSchedule{1 << 0, 1 << 5, all(hours), all(dom), all(months), all(dow), loc}
   363  }
   364  
   365  func every5min5s(loc *time.Location) *SpecSchedule {
   366  	return &SpecSchedule{1 << 5, 1 << 5, all(hours), all(dom), all(months), all(dow), loc}
   367  }
   368  
   369  func midnight(loc *time.Location) *SpecSchedule {
   370  	return &SpecSchedule{1, 1, 1, all(dom), all(months), all(dow), loc}
   371  }
   372  
   373  func annual(loc *time.Location) *SpecSchedule {
   374  	return &SpecSchedule{
   375  		Second:   1 << seconds.min,
   376  		Minute:   1 << minutes.min,
   377  		Hour:     1 << hours.min,
   378  		Dom:      1 << dom.min,
   379  		Month:    1 << months.min,
   380  		Dow:      all(dow),
   381  		Location: loc,
   382  	}
   383  }