parser_test.go 9.9 KB

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