From 41ee08045b5e27c5c1d64e6dbe14597075ed71d1 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 9 Apr 2022 09:46:07 +0200 Subject: [PATCH 01/18] Start of cron implementation --- src/cron/cli.v | 26 ++++++++++++++++++++++++++ src/cron/cron.v | 7 +++++++ src/main.v | 2 ++ 3 files changed, 35 insertions(+) create mode 100644 src/cron/cli.v create mode 100644 src/cron/cron.v diff --git a/src/cron/cli.v b/src/cron/cli.v new file mode 100644 index 0000000..cbf5b88 --- /dev/null +++ b/src/cron/cli.v @@ -0,0 +1,26 @@ +module cron + +import cli +import env + +struct Config { +pub: + log_level string = 'WARN' + log_file string = 'vieter.log' + api_key string + address string + base_image string = 'archlinux:base-devel' +} + +pub fn cmd() cli.Command { + return cli.Command{ + name: 'cron' + description: 'Start the cron service that periodically runs builds.' + execute: fn (cmd cli.Command) ? { + config_file := cmd.flags.get_string('config-file') ? + conf := env.load(config_file) ? + + cron(conf) ? + } + } +} diff --git a/src/cron/cron.v b/src/cron/cron.v new file mode 100644 index 0000000..ac584eb --- /dev/null +++ b/src/cron/cron.v @@ -0,0 +1,7 @@ +module cron + +import git + +pub fn cron(conf Config) ? { + repos_map := git.get_repos(conf.address, conf.api_key) ? +} diff --git a/src/main.v b/src/main.v index c77e551..adaf4dc 100644 --- a/src/main.v +++ b/src/main.v @@ -5,6 +5,7 @@ import server import cli import build import git +import cron fn main() { mut app := cli.Command{ @@ -25,6 +26,7 @@ fn main() { server.cmd(), build.cmd(), git.cmd(), + cron.cmd() ] } From e890128bda68fcdf40e1818b8486f5816126b689 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 9 Apr 2022 09:50:37 +0200 Subject: [PATCH 02/18] Ran formatter --- src/build/cli.v | 4 ++-- src/cron/cli.v | 8 ++++---- src/main.v | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/build/cli.v b/src/build/cli.v index 7cdcf83..0131396 100644 --- a/src/build/cli.v +++ b/src/build/cli.v @@ -5,8 +5,8 @@ import env pub struct Config { pub: - api_key string - address string + api_key string + address string base_image string = 'archlinux:base-devel' } diff --git a/src/cron/cli.v b/src/cron/cli.v index cbf5b88..9bdec9a 100644 --- a/src/cron/cli.v +++ b/src/cron/cli.v @@ -5,10 +5,10 @@ import env struct Config { pub: - log_level string = 'WARN' - log_file string = 'vieter.log' - api_key string - address string + log_level string = 'WARN' + log_file string = 'vieter.log' + api_key string + address string base_image string = 'archlinux:base-devel' } diff --git a/src/main.v b/src/main.v index adaf4dc..7e41f25 100644 --- a/src/main.v +++ b/src/main.v @@ -26,7 +26,7 @@ fn main() { server.cmd(), build.cmd(), git.cmd(), - cron.cmd() + cron.cmd(), ] } From 6d60ea15380bdaede26216918e0922edd701f1b5 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 10 Apr 2022 16:17:50 +0200 Subject: [PATCH 03/18] Started writing cron expression parser [CI SKIP] --- Makefile | 4 +++ src/cron/cron.v | 27 ++++++++++++++++++- src/cron/expression.v | 55 ++++++++++++++++++++++++++++++++++++++ src/cron/expression_test.v | 5 ++++ src/util.v | 1 + 5 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/cron/expression.v create mode 100644 src/cron/expression_test.v diff --git a/Makefile b/Makefile index 76ab7b5..9421fb6 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,10 @@ fmt: vet: $(V) vet -W $(SRC_DIR) +.PHONY: test +test: + $(V) test $(SRC_DIR) + # Build & patch the V compiler .PHONY: v v: v/v diff --git a/src/cron/cron.v b/src/cron/cron.v index ac584eb..ccb8f9e 100644 --- a/src/cron/cron.v +++ b/src/cron/cron.v @@ -1,7 +1,32 @@ module cron import git +import datatypes +import time + +struct ScheduledBuild { + repo git.GitRepo + timestamp time.Time +} + +fn (r1 ScheduledBuild) < (r2 ScheduledBuild) bool { + return r1.timestamp < r2.timestamp +} pub fn cron(conf Config) ? { - repos_map := git.get_repos(conf.address, conf.api_key) ? + // mut queue := datatypes.MinHeap{} + // repos_map := git.get_repos(conf.address, conf.api_key) ? + + // for _, repo in repos_map { + // scheduled := ScheduledBuild{ + // repo: repo + // timestamp: 25 + // } + + // queue.insert(scheduled) + // } + + // println(queue) + exp := "10/2 5 *" + println(parse_expression(exp) ?) } diff --git a/src/cron/expression.v b/src/cron/expression.v new file mode 100644 index 0000000..8dae499 --- /dev/null +++ b/src/cron/expression.v @@ -0,0 +1,55 @@ +module cron + +import math + +struct CronExpression { + minutes []u32 + hours []u32 + days []u32 +} + +// parse_range parses a given string into a range of integers, if possible. +fn parse_range(s string, min u32, max u32) ?[]u32 { + mut out := []u32{} + mut start := min + mut interval := u32(1) + + if s != '*' { + exps := s.split('/') + + if exps.len > 1 { + interval = exps[1].u32() + } + // Here, s solely consists of a number, so that's the only value we + // should return. + else{ + return [exps[0].u32()] + } + + if exps[0] != '*' { + start = math.max(exps[0].u32(), min) + } + } + + for start <= max { + out << start + start += interval + } + + return out +} + +// min hour day month day-of-week +fn parse_expression(exp string) ?CronExpression { + parts := exp.split(' ') + + if parts.len != 3 { + return error("Expression must contain 5 space-separated parts.") + } + + return CronExpression{ + minutes: parse_range(parts[0], 0, 59) ? + hours: parse_range(parts[1], 0, 23) ? + days: parse_range(parts[2], 0, 31) ? + } +} diff --git a/src/cron/expression_test.v b/src/cron/expression_test.v new file mode 100644 index 0000000..b3b7422 --- /dev/null +++ b/src/cron/expression_test.v @@ -0,0 +1,5 @@ +module cron + +fn test_parse_star_range() { + assert parse_range('*', 0, 5) == [0, 1, 2, 3, 4, 5] +} diff --git a/src/util.v b/src/util.v index 49c9d22..228f584 100644 --- a/src/util.v +++ b/src/util.v @@ -44,6 +44,7 @@ pub fn reader_to_file(mut reader io.BufferedReader, length int, path string) ? { for to_write > 0 { // TODO don't just loop infinitely here bytes_written := file.write(buf[bytes_read - to_write..bytes_read]) or { continue } + // file.flush() to_write = to_write - bytes_written } From f92a20fcf8b4702a0702788fe70d58868c57945e Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 10 Apr 2022 16:48:37 +0200 Subject: [PATCH 04/18] Gave all modules own directory; added test CI pipeline --- .woodpecker/.test.yml | 15 +++++++++++++++ src/cron/cron.v | 4 ++-- src/cron/expression.v | 8 ++++---- src/cron/expression_test.v | 8 ++++++-- src/{ => env}/env.v | 0 src/{ => package}/package.v | 0 src/{ => response}/response.v | 0 src/{ => util}/util.v | 0 8 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 .woodpecker/.test.yml rename src/{ => env}/env.v (100%) rename src/{ => package}/package.v (100%) rename src/{ => response}/response.v (100%) rename src/{ => util}/util.v (100%) diff --git a/.woodpecker/.test.yml b/.woodpecker/.test.yml new file mode 100644 index 0000000..ec559c8 --- /dev/null +++ b/.woodpecker/.test.yml @@ -0,0 +1,15 @@ +matrix: + PLATFORM: + - linux/amd64 + - linux/arm64 + +platform: ${PLATFORM} + +pipeline: + test: + image: 'chewingbever/vlang:latest' + pull: true + commands: + - make test + when: + event: push diff --git a/src/cron/cron.v b/src/cron/cron.v index ccb8f9e..13fc22d 100644 --- a/src/cron/cron.v +++ b/src/cron/cron.v @@ -5,7 +5,7 @@ import datatypes import time struct ScheduledBuild { - repo git.GitRepo + repo git.GitRepo timestamp time.Time } @@ -27,6 +27,6 @@ pub fn cron(conf Config) ? { // } // println(queue) - exp := "10/2 5 *" + exp := '10/2 5 *' println(parse_expression(exp) ?) } diff --git a/src/cron/expression.v b/src/cron/expression.v index 8dae499..2938dd9 100644 --- a/src/cron/expression.v +++ b/src/cron/expression.v @@ -4,8 +4,8 @@ import math struct CronExpression { minutes []u32 - hours []u32 - days []u32 + hours []u32 + days []u32 } // parse_range parses a given string into a range of integers, if possible. @@ -22,7 +22,7 @@ fn parse_range(s string, min u32, max u32) ?[]u32 { } // Here, s solely consists of a number, so that's the only value we // should return. - else{ + else { return [exps[0].u32()] } @@ -44,7 +44,7 @@ fn parse_expression(exp string) ?CronExpression { parts := exp.split(' ') if parts.len != 3 { - return error("Expression must contain 5 space-separated parts.") + return error('Expression must contain 5 space-separated parts.') } return CronExpression{ diff --git a/src/cron/expression_test.v b/src/cron/expression_test.v index b3b7422..562ced2 100644 --- a/src/cron/expression_test.v +++ b/src/cron/expression_test.v @@ -1,5 +1,9 @@ module cron -fn test_parse_star_range() { - assert parse_range('*', 0, 5) == [0, 1, 2, 3, 4, 5] +fn test_parse_star_range() ? { + assert parse_range('*', 0, 5) ? == [u32(0), 1, 2, 3, 4, 5] +} + +fn test_parse_number() ? { + assert parse_range('4', 0, 5) ? == [u32(4)] } diff --git a/src/env.v b/src/env/env.v similarity index 100% rename from src/env.v rename to src/env/env.v diff --git a/src/package.v b/src/package/package.v similarity index 100% rename from src/package.v rename to src/package/package.v diff --git a/src/response.v b/src/response/response.v similarity index 100% rename from src/response.v rename to src/response/response.v diff --git a/src/util.v b/src/util/util.v similarity index 100% rename from src/util.v rename to src/util/util.v From 799fe2e4549ed545981349cdd2e193fde25fcce7 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 10 Apr 2022 16:58:55 +0200 Subject: [PATCH 05/18] Added some extra tests for parse_range --- src/cron/expression.v | 8 +++----- src/cron/expression_test.v | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/cron/expression.v b/src/cron/expression.v index 2938dd9..8781900 100644 --- a/src/cron/expression.v +++ b/src/cron/expression.v @@ -17,17 +17,15 @@ fn parse_range(s string, min u32, max u32) ?[]u32 { if s != '*' { exps := s.split('/') + start = math.min(max, math.max(exps[0].u32(), min)) + if exps.len > 1 { interval = exps[1].u32() } // Here, s solely consists of a number, so that's the only value we // should return. else { - return [exps[0].u32()] - } - - if exps[0] != '*' { - start = math.max(exps[0].u32(), min) + return [start] } } diff --git a/src/cron/expression_test.v b/src/cron/expression_test.v index 562ced2..6d293c5 100644 --- a/src/cron/expression_test.v +++ b/src/cron/expression_test.v @@ -7,3 +7,31 @@ fn test_parse_star_range() ? { fn test_parse_number() ? { assert parse_range('4', 0, 5) ? == [u32(4)] } + +fn test_parse_number_too_large() ? { + assert parse_range('10', 0, 6) ? == [u32(6)] +} + +fn test_parse_number_too_small() ? { + assert parse_range('0', 2, 6) ? == [u32(2)] +} + +fn test_parse_step_star() ? { + assert parse_range('*/4', 0, 20) ? == [u32(0), 4, 8, 12, 16, 20] +} + +fn test_parse_step_star_too_large() ? { + assert parse_range('*/21', 0, 20) ? == [u32(0)] +} + +fn test_parse_step_number() ? { + assert parse_range('5/4', 0, 20) ? == [u32(5), 9, 13, 17] +} + +fn test_parse_step_number_too_large() ? { + assert parse_range('10/4', 0, 5) ? == [u32(5)] +} + +fn test_parse_step_number_too_small() ? { + assert parse_range('2/4', 5, 10) ? == [u32(5), 9] +} From e3da3d0d7f1ad0f1d55c2d9c7e775ef3f2670ff6 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 10 Apr 2022 17:47:46 +0200 Subject: [PATCH 06/18] Can't figure out cron algo right now [CI SKIP] --- src/cron/expression.v | 32 +++++++++++++++++++++++++++++++- src/cron/expression_test.v | 7 +++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/cron/expression.v b/src/cron/expression.v index 8781900..a29bf53 100644 --- a/src/cron/expression.v +++ b/src/cron/expression.v @@ -1,6 +1,7 @@ module cron import math +import time struct CronExpression { minutes []u32 @@ -8,7 +9,32 @@ struct CronExpression { days []u32 } -// parse_range parses a given string into a range of integers, if possible. +// next calculates the earliest time this cron expression is valid. +pub fn (ce &CronExpression) next(ref &time.Time) time.Time { + res := time.Time{} + + mut day := 0 + mut hour := 0 + mut minute := 0 + + // Find the next minute + // If ref.minute is greater than + if ref.minute >= ce.minutes[ce.minutes.len - 1] || ref.minute < ce.minutes[0] { + minute = ce.minutes[0] + }else{ + for i in 0..ce.minutes.len { + if ce.minutes[i] > ref.minute { + minute = ce.minutes[i] + break + } + } + } + + return res +} + +// parse_range parses a given string into a range of sorted integers, if +// possible. fn parse_range(s string, min u32, max u32) ?[]u32 { mut out := []u32{} mut start := min @@ -29,6 +55,10 @@ fn parse_range(s string, min u32, max u32) ?[]u32 { } } + if interval == 0 { + return [] + } + for start <= max { out << start start += interval diff --git a/src/cron/expression_test.v b/src/cron/expression_test.v index 6d293c5..abd5e5f 100644 --- a/src/cron/expression_test.v +++ b/src/cron/expression_test.v @@ -1,5 +1,6 @@ module cron +// =====parse_range===== fn test_parse_star_range() ? { assert parse_range('*', 0, 5) ? == [u32(0), 1, 2, 3, 4, 5] } @@ -24,6 +25,10 @@ fn test_parse_step_star_too_large() ? { assert parse_range('*/21', 0, 20) ? == [u32(0)] } +fn test_parse_step_zero() ? { + assert parse_range('*/0', 0, 20) ? == [] +} + fn test_parse_step_number() ? { assert parse_range('5/4', 0, 20) ? == [u32(5), 9, 13, 17] } @@ -35,3 +40,5 @@ fn test_parse_step_number_too_large() ? { fn test_parse_step_number_too_small() ? { assert parse_range('2/4', 5, 10) ? == [u32(5), 9] } + + From 135b6c3d7ff5d2f383dbf0877ce16f7a1ead110e Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 11 Apr 2022 22:16:31 +0200 Subject: [PATCH 07/18] Alpha version cron 'next' function --- src/cron/cron.v | 27 +++++++- src/cron/expression.v | 128 +++++++++++++++++++++++++++++-------- src/cron/expression_test.v | 18 +++--- 3 files changed, 135 insertions(+), 38 deletions(-) diff --git a/src/cron/cron.v b/src/cron/cron.v index 13fc22d..d802dbf 100644 --- a/src/cron/cron.v +++ b/src/cron/cron.v @@ -27,6 +27,29 @@ pub fn cron(conf Config) ? { // } // println(queue) - exp := '10/2 5 *' - println(parse_expression(exp) ?) + // exp := '10/2 5 *' + // println(parse_expression(exp) ?) + ce := parse_expression('0 3 */2') ? + println(ce) + // ce := CronExpression{ + // minutes: [0] + // hours: [3] + // days: [1, 2, 3, 4, 5, 6] + // months: [1, 2] + // } + mut t := time.Time{ + year: 2022 + month: 2 + minute: 9 + hour: 13 + day: 12 + } + + // mut t := time.now() + println(t) + + for _ in 1..25 { + t = ce.next(t) ? + println(t) + } } diff --git a/src/cron/expression.v b/src/cron/expression.v index a29bf53..ff0fcd4 100644 --- a/src/cron/expression.v +++ b/src/cron/expression.v @@ -3,50 +3,117 @@ module cron import math import time +const days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + struct CronExpression { - minutes []u32 - hours []u32 - days []u32 + minutes []int + hours []int + days []int + months []int } // next calculates the earliest time this cron expression is valid. -pub fn (ce &CronExpression) next(ref &time.Time) time.Time { - res := time.Time{} +pub fn (ce &CronExpression) next(ref time.Time) ?time.Time { + mut minute_index := 0 + mut hour_index := 0 + mut day_index := 0 + mut month_index := 0 - mut day := 0 - mut hour := 0 - mut minute := 0 + for month_index < ce.months.len && ref.month > ce.months[month_index] { + month_index++ + } - // Find the next minute - // If ref.minute is greater than - if ref.minute >= ce.minutes[ce.minutes.len - 1] || ref.minute < ce.minutes[0] { - minute = ce.minutes[0] - }else{ - for i in 0..ce.minutes.len { - if ce.minutes[i] > ref.minute { - minute = ce.minutes[i] - break + if month_index < ce.months.len { + for day_index < ce.days.len && ref.day > ce.days[day_index] { + day_index++ + } + + if day_index < ce.days.len { + for hour_index < ce.hours.len && ref.hour > ce.hours[hour_index] { + hour_index++ + } + + if hour_index < ce.hours.len { + // For each unit, we calculate what the next value is + for minute_index < ce.minutes.len && ref.minute >= ce.minutes[minute_index] { + minute_index++ + } } } } - return res + + // Sometime we have to shift values one more + if minute_index == ce.minutes.len && hour_index < ce.hours.len { + hour_index += 1 + } + + if hour_index == ce.hours.len && day_index < ce.days.len { + day_index += 1 + } + + if day_index == ce.days.len && month_index < ce.months.len { + month_index += 1 + } + + mut minute := ce.minutes[minute_index % ce.minutes.len] + mut hour := ce.hours[hour_index % ce.hours.len] + mut day := ce.days[day_index % ce.days.len] + + mut reset := false + + // If the day can't be planned in the current month, we go to the next one + // and go back to day one + if day > days_in_month[ce.months[month_index % ce.months.len] - 1] { + month_index += 1 + day = ce.days[0] + + // Make sure we only plan in a month that the day occurs in + for day > days_in_month[ce.months[month_index & ce.months.len] - 1] { + month_index += 1 + + // Prevent scenario where there are no months that can be scheduled. + if month_index == 2 * ce.months.len { + return error('No schedulable moment.') + } + } + } + + + month := ce.months[month_index % ce.months.len] + mut year := ref.year + + if month_index >= ce.months.len { + year++ + } + + return time.Time{ + year: year + month: month + day: day + minute: minute + hour: hour + } +} + +fn (ce &CronExpression) next_from_now() ?time.Time { + return ce.next(time.now()) } // parse_range parses a given string into a range of sorted integers, if // possible. -fn parse_range(s string, min u32, max u32) ?[]u32 { - mut out := []u32{} +fn parse_range(s string, min int, max int) ?[]int { + mut out := []int{} mut start := min - mut interval := u32(1) + mut interval := 1 if s != '*' { exps := s.split('/') - start = math.min(max, math.max(exps[0].u32(), min)) + start = math.min(max, math.max(exps[0].int(), min)) if exps.len > 1 { - interval = exps[1].u32() + interval = exps[1].int() } // Here, s solely consists of a number, so that's the only value we // should return. @@ -69,15 +136,22 @@ fn parse_range(s string, min u32, max u32) ?[]u32 { // min hour day month day-of-week fn parse_expression(exp string) ?CronExpression { - parts := exp.split(' ') + mut parts := exp.split(' ') - if parts.len != 3 { - return error('Expression must contain 5 space-separated parts.') + if parts.len < 2 || parts.len > 4 { + return error('Expression must contain between 2 and 4 space-separated parts.') + } + + // For ease of use, we allow the user to only specify as many parts as they + // need. + for parts.len < 4 { + parts << '*' } return CronExpression{ minutes: parse_range(parts[0], 0, 59) ? hours: parse_range(parts[1], 0, 23) ? - days: parse_range(parts[2], 0, 31) ? + days: parse_range(parts[2], 1, 31) ? + months: parse_range(parts[3], 1, 12) ? } } diff --git a/src/cron/expression_test.v b/src/cron/expression_test.v index abd5e5f..9279cce 100644 --- a/src/cron/expression_test.v +++ b/src/cron/expression_test.v @@ -2,27 +2,27 @@ module cron // =====parse_range===== fn test_parse_star_range() ? { - assert parse_range('*', 0, 5) ? == [u32(0), 1, 2, 3, 4, 5] + assert parse_range('*', 0, 5) ? == [0, 1, 2, 3, 4, 5] } fn test_parse_number() ? { - assert parse_range('4', 0, 5) ? == [u32(4)] + assert parse_range('4', 0, 5) ? == [4] } fn test_parse_number_too_large() ? { - assert parse_range('10', 0, 6) ? == [u32(6)] + assert parse_range('10', 0, 6) ? == [6] } fn test_parse_number_too_small() ? { - assert parse_range('0', 2, 6) ? == [u32(2)] + assert parse_range('0', 2, 6) ? == [2] } fn test_parse_step_star() ? { - assert parse_range('*/4', 0, 20) ? == [u32(0), 4, 8, 12, 16, 20] + assert parse_range('*/4', 0, 20) ? == [0, 4, 8, 12, 16, 20] } fn test_parse_step_star_too_large() ? { - assert parse_range('*/21', 0, 20) ? == [u32(0)] + assert parse_range('*/21', 0, 20) ? == [0] } fn test_parse_step_zero() ? { @@ -30,15 +30,15 @@ fn test_parse_step_zero() ? { } fn test_parse_step_number() ? { - assert parse_range('5/4', 0, 20) ? == [u32(5), 9, 13, 17] + assert parse_range('5/4', 0, 20) ? == [5, 9, 13, 17] } fn test_parse_step_number_too_large() ? { - assert parse_range('10/4', 0, 5) ? == [u32(5)] + assert parse_range('10/4', 0, 5) ? == [5] } fn test_parse_step_number_too_small() ? { - assert parse_range('2/4', 5, 10) ? == [u32(5), 9] + assert parse_range('2/4', 5, 10) ? == [5, 9] } From 0e5f31e64907fd10c32738d0a53a73cf9218f1b5 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 11 Apr 2022 22:30:22 +0200 Subject: [PATCH 08/18] Added some much-needed documentation --- src/cron/cron.v | 2 +- src/cron/expression.v | 38 +++++++++++++++++++++++++++----------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/cron/cron.v b/src/cron/cron.v index d802dbf..6d7b7aa 100644 --- a/src/cron/cron.v +++ b/src/cron/cron.v @@ -39,7 +39,7 @@ pub fn cron(conf Config) ? { // } mut t := time.Time{ year: 2022 - month: 2 + month: 12 minute: 9 hour: 13 day: 12 diff --git a/src/cron/expression.v b/src/cron/expression.v index ff0fcd4..b3ae38d 100644 --- a/src/cron/expression.v +++ b/src/cron/expression.v @@ -12,13 +12,24 @@ struct CronExpression { months []int } -// next calculates the earliest time this cron expression is valid. +// next calculates the earliest time this cron expression is valid. It will +// always pick a moment in the future, even if ref matches completely up to the +// minute. This function conciously does not take gap years into account. pub fn (ce &CronExpression) next(ref time.Time) ?time.Time { + // For all of these values, the rule is the following: if their value is + // the length of their respective array in the CronExpression object, that + // means we've looped back around. This means that the "bigger" value has + // to be incremented by one. For example, if the minutes have looped + // around, that means that the hour has to be incremented as well. mut minute_index := 0 mut hour_index := 0 mut day_index := 0 mut month_index := 0 + // This chain is the same logic multiple times, namely that if a "bigger" + // value loops around, then the smaller value will always reset as well. + // For example, if we're going to a new day, the hour & minute will always + // be their smallest value again. for month_index < ce.months.len && ref.month > ce.months[month_index] { month_index++ } @@ -34,7 +45,9 @@ pub fn (ce &CronExpression) next(ref time.Time) ?time.Time { } if hour_index < ce.hours.len { - // For each unit, we calculate what the next value is + // Minute is the only value where we explicitely make sure we + // can't match ref's value exactly. This is to ensure we only + // return values in the future. for minute_index < ce.minutes.len && ref.minute >= ce.minutes[minute_index] { minute_index++ } @@ -42,8 +55,9 @@ pub fn (ce &CronExpression) next(ref time.Time) ?time.Time { } } - - // Sometime we have to shift values one more + // Here, we increment the "bigger" values by one if the smaller ones loop + // around. The order is important, as it allows a sort-of waterfall effect + // to occur which updates all values if required. if minute_index == ce.minutes.len && hour_index < ce.hours.len { hour_index += 1 } @@ -60,19 +74,20 @@ pub fn (ce &CronExpression) next(ref time.Time) ?time.Time { mut hour := ce.hours[hour_index % ce.hours.len] mut day := ce.days[day_index % ce.days.len] - mut reset := false - - // If the day can't be planned in the current month, we go to the next one - // and go back to day one + // Sometimes, we end up with a day that does not exist within the selected + // month, e.g. day 30 in February. When this occurs, we reset day back to + // the smallest value & loop over to the next month that does have this + // day. if day > days_in_month[ce.months[month_index % ce.months.len] - 1] { - month_index += 1 day = ce.days[0] + month_index += 1 - // Make sure we only plan in a month that the day occurs in for day > days_in_month[ce.months[month_index & ce.months.len] - 1] { month_index += 1 - // Prevent scenario where there are no months that can be scheduled. + // If for whatever reason the day value ends up being something + // that can't be scheduled in any month, we have to make sure we + // don't create an infinite loop. if month_index == 2 * ce.months.len { return error('No schedulable moment.') } @@ -83,6 +98,7 @@ pub fn (ce &CronExpression) next(ref time.Time) ?time.Time { month := ce.months[month_index % ce.months.len] mut year := ref.year + // If the month loops over, we need to increment the year. if month_index >= ce.months.len { year++ } From ab4f64b6b6997c91c637ed6861600706416ddda0 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 11 Apr 2022 22:52:06 +0200 Subject: [PATCH 09/18] Failed attempt at x,y,z cron stuff [CI SKIP] --- src/cron/cron.v | 2 +- src/cron/expression.v | 39 ++++++++++++++++++++++++++------------ src/cron/expression_test.v | 2 -- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/cron/cron.v b/src/cron/cron.v index 6d7b7aa..74e203f 100644 --- a/src/cron/cron.v +++ b/src/cron/cron.v @@ -48,7 +48,7 @@ pub fn cron(conf Config) ? { // mut t := time.now() println(t) - for _ in 1..25 { + for _ in 1 .. 25 { t = ce.next(t) ? println(t) } diff --git a/src/cron/expression.v b/src/cron/expression.v index b3ae38d..71ee9a1 100644 --- a/src/cron/expression.v +++ b/src/cron/expression.v @@ -78,11 +78,11 @@ pub fn (ce &CronExpression) next(ref time.Time) ?time.Time { // month, e.g. day 30 in February. When this occurs, we reset day back to // the smallest value & loop over to the next month that does have this // day. - if day > days_in_month[ce.months[month_index % ce.months.len] - 1] { + if day > cron.days_in_month[ce.months[month_index % ce.months.len] - 1] { day = ce.days[0] month_index += 1 - for day > days_in_month[ce.months[month_index & ce.months.len] - 1] { + for day > cron.days_in_month[ce.months[month_index & ce.months.len] - 1] { month_index += 1 // If for whatever reason the day value ends up being something @@ -94,7 +94,6 @@ pub fn (ce &CronExpression) next(ref time.Time) ?time.Time { } } - month := ce.months[month_index % ce.months.len] mut year := ref.year @@ -118,8 +117,7 @@ fn (ce &CronExpression) next_from_now() ?time.Time { // parse_range parses a given string into a range of sorted integers, if // possible. -fn parse_range(s string, min int, max int) ?[]int { - mut out := []int{} +fn parse_range(s string, min int, max int, mut bitv []bool) ? { mut start := min mut interval := 1 @@ -134,18 +132,35 @@ fn parse_range(s string, min int, max int) ?[]int { // Here, s solely consists of a number, so that's the only value we // should return. else { - return [start] + bitv[start - min - 1] = true + return } } if interval == 0 { - return [] + return } for start <= max { - out << start + bitv[start - min - 1] = true start += interval } +} + +fn parse_part(s string, min int, max int) ?[]int { + mut bitv := []bool{init: false, len: max - min + 1} + + for range in s.split(',') { + parse_range(range, min, max, mut bitv) ? + } + + mut out := []int{} + + for i in 0..max + 1 { + if bitv[i] { + out << min + i + } + } return out } @@ -165,9 +180,9 @@ fn parse_expression(exp string) ?CronExpression { } return CronExpression{ - minutes: parse_range(parts[0], 0, 59) ? - hours: parse_range(parts[1], 0, 23) ? - days: parse_range(parts[2], 1, 31) ? - months: parse_range(parts[3], 1, 12) ? + minutes: parse_part(parts[0], 0, 59) ? + hours: parse_part(parts[1], 0, 23) ? + days: parse_part(parts[2], 1, 31) ? + months: parse_part(parts[3], 1, 12) ? } } diff --git a/src/cron/expression_test.v b/src/cron/expression_test.v index 9279cce..2d58b15 100644 --- a/src/cron/expression_test.v +++ b/src/cron/expression_test.v @@ -40,5 +40,3 @@ fn test_parse_step_number_too_large() ? { fn test_parse_step_number_too_small() ? { assert parse_range('2/4', 5, 10) ? == [5, 9] } - - From 04e54b8b101a477840920f7037bf02dcd2812e95 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 12 Apr 2022 09:00:18 +0200 Subject: [PATCH 10/18] Migrated tests to new bitv-based implementation --- src/cron/expression.v | 22 ++++++----- src/cron/expression_parse_test.v | 65 ++++++++++++++++++++++++++++++++ src/cron/expression_test.v | 42 --------------------- 3 files changed, 78 insertions(+), 51 deletions(-) create mode 100644 src/cron/expression_parse_test.v delete mode 100644 src/cron/expression_test.v diff --git a/src/cron/expression.v b/src/cron/expression.v index 71ee9a1..60e1b74 100644 --- a/src/cron/expression.v +++ b/src/cron/expression.v @@ -147,6 +147,18 @@ fn parse_range(s string, min int, max int, mut bitv []bool) ? { } } +fn bitv_to_ints(bitv []bool, min int) []int { + mut out := []int{} + + for i in 0..bitv.len { + if bitv[i] { + out << min + i + } + } + + return out +} + fn parse_part(s string, min int, max int) ?[]int { mut bitv := []bool{init: false, len: max - min + 1} @@ -154,15 +166,7 @@ fn parse_part(s string, min int, max int) ?[]int { parse_range(range, min, max, mut bitv) ? } - mut out := []int{} - - for i in 0..max + 1 { - if bitv[i] { - out << min + i - } - } - - return out + return bitv_to_ints(bitv, min) } // min hour day month day-of-week diff --git a/src/cron/expression_parse_test.v b/src/cron/expression_parse_test.v new file mode 100644 index 0000000..7a4974d --- /dev/null +++ b/src/cron/expression_parse_test.v @@ -0,0 +1,65 @@ +module cron + +// parse_range_error returns the returned error message. If the result is '', +// that means the function didn't error. +fn parse_range_error(s string, min int, max int) string { + mut bitv := []bool{init: false, len: max - min + 1} + + parse_range(s, min, max, mut bitv) or { + return err.msg + } + + return '' +} + +// =====parse_range===== +fn test_parse_star_range() ? { + mut bitv := []bool{init: false, len: 6} + parse_range('*', 0, 5, mut bitv) ? + + assert bitv == [true, true, true, true, true, true] +} + +fn test_parse_number() ? { + mut bitv := []bool{init: false, len: 6} + parse_range('4', 0, 5, mut bitv) ? + + assert bitv_to_ints(bitv, 0) == [4] +} + +fn test_parse_number_too_large() ? { + assert parse_range_error('10', 0, 6) == 'Out of range.' +} + +fn test_parse_number_too_small() ? { + assert parse_range_error('0', 2, 6) == 'Out of range.' +} + +fn test_parse_step_star() ? { + mut bitv := []bool{init: false, len: 21} + parse_range('*/4', 0, 20, mut bitv) ? + + assert bitv_to_ints(bitv, 0) == [0, 4, 8, 12, 16, 20] +} + +fn test_parse_step_star_too_large() ? { + assert parse_range_error('*/21', 0, 20) == 'Step too large.' +} + +fn test_parse_step_zero() ? { + assert parse_range_error('*/0', 0, 20) == 'Step size zero not allowed.' +} + +fn test_parse_step_number() ? { + mut bitv := []bool{init: false, len: 21} + parse_range('5/4', 0, 20, mut bitv) ? + assert bitv_to_ints(bitv, 0) == [5, 9, 13, 17] +} + +fn test_parse_step_number_too_large() ? { + assert parse_range_error('10/4', 0, 5) == 'Out of range.' +} + +fn test_parse_step_number_too_small() ? { + assert parse_range_error('2/4', 5, 10) == 'Out of range.' +} diff --git a/src/cron/expression_test.v b/src/cron/expression_test.v deleted file mode 100644 index 2d58b15..0000000 --- a/src/cron/expression_test.v +++ /dev/null @@ -1,42 +0,0 @@ -module cron - -// =====parse_range===== -fn test_parse_star_range() ? { - assert parse_range('*', 0, 5) ? == [0, 1, 2, 3, 4, 5] -} - -fn test_parse_number() ? { - assert parse_range('4', 0, 5) ? == [4] -} - -fn test_parse_number_too_large() ? { - assert parse_range('10', 0, 6) ? == [6] -} - -fn test_parse_number_too_small() ? { - assert parse_range('0', 2, 6) ? == [2] -} - -fn test_parse_step_star() ? { - assert parse_range('*/4', 0, 20) ? == [0, 4, 8, 12, 16, 20] -} - -fn test_parse_step_star_too_large() ? { - assert parse_range('*/21', 0, 20) ? == [0] -} - -fn test_parse_step_zero() ? { - assert parse_range('*/0', 0, 20) ? == [] -} - -fn test_parse_step_number() ? { - assert parse_range('5/4', 0, 20) ? == [5, 9, 13, 17] -} - -fn test_parse_step_number_too_large() ? { - assert parse_range('10/4', 0, 5) ? == [5] -} - -fn test_parse_step_number_too_small() ? { - assert parse_range('2/4', 5, 10) ? == [5, 9] -} From f4bb03f488f489ba01d6efb4e8eae18f1b6da422 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 12 Apr 2022 09:48:25 +0200 Subject: [PATCH 11/18] Tests n bug fixes --- src/cron/expression.v | 50 +++++++++++++++++++++++--------- src/cron/expression_parse_test.v | 47 +++++++++++++++++++++--------- 2 files changed, 70 insertions(+), 27 deletions(-) diff --git a/src/cron/expression.v b/src/cron/expression.v index 60e1b74..46f92f9 100644 --- a/src/cron/expression.v +++ b/src/cron/expression.v @@ -1,6 +1,5 @@ module cron -import math import time const days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] @@ -121,28 +120,50 @@ fn parse_range(s string, min int, max int, mut bitv []bool) ? { mut start := min mut interval := 1 - if s != '*' { - exps := s.split('/') + exps := s.split('/') - start = math.min(max, math.max(exps[0].int(), min)) + if exps[0] != '*' { + start = exps[0].int() - if exps.len > 1 { - interval = exps[1].int() + // The builtin parsing functions return zero if the string can't be + // parsed into a number, so we have to explicitely check whether they + // actually entered zero or if it's an invalid number. + if start == 0 && exps[0] != '0' { + return error('Invalid number.') } - // Here, s solely consists of a number, so that's the only value we - // should return. - else { - bitv[start - min - 1] = true - return + + // Check whether the start value is out of range + if start < min || start > max { + return error('Out of range.') } } - if interval == 0 { + if exps.len > 1 { + interval = exps[1].int() + + // interval being zero is always invalid, but we want to check why + // it's invalid for better error messages. + if interval == 0 { + if exps[1] != '0' { + return error('Invalid number.') + }else{ + return error('Step size zero not allowed.') + } + } + + if interval > max - min { + return error('Step size too large.') + } + } + // Here, s solely consists of a number, so that's the only value we + // should return. + else if exps[0] != '*' { + bitv[start - min] = true return } for start <= max { - bitv[start - min - 1] = true + bitv[start - min] = true start += interval } } @@ -171,7 +192,8 @@ fn parse_part(s string, min int, max int) ?[]int { // min hour day month day-of-week fn parse_expression(exp string) ?CronExpression { - mut parts := exp.split(' ') + // The filter allows for multiple spaces between parts + mut parts := exp.split(' ').filter(it != '') if parts.len < 2 || parts.len > 4 { return error('Expression must contain between 2 and 4 space-separated parts.') diff --git a/src/cron/expression_parse_test.v b/src/cron/expression_parse_test.v index 7a4974d..8f22850 100644 --- a/src/cron/expression_parse_test.v +++ b/src/cron/expression_parse_test.v @@ -13,53 +13,74 @@ fn parse_range_error(s string, min int, max int) string { } // =====parse_range===== -fn test_parse_star_range() ? { +fn test_range_star_range() ? { mut bitv := []bool{init: false, len: 6} parse_range('*', 0, 5, mut bitv) ? assert bitv == [true, true, true, true, true, true] } -fn test_parse_number() ? { +fn test_range_number() ? { mut bitv := []bool{init: false, len: 6} parse_range('4', 0, 5, mut bitv) ? assert bitv_to_ints(bitv, 0) == [4] } -fn test_parse_number_too_large() ? { +fn test_range_number_too_large() ? { assert parse_range_error('10', 0, 6) == 'Out of range.' } -fn test_parse_number_too_small() ? { +fn test_range_number_too_small() ? { assert parse_range_error('0', 2, 6) == 'Out of range.' } -fn test_parse_step_star() ? { +fn test_range_number_invalid() ? { + assert parse_range_error('x', 0, 6) == 'Invalid number.' +} + +fn test_range_step_star_1() ? { mut bitv := []bool{init: false, len: 21} parse_range('*/4', 0, 20, mut bitv) ? assert bitv_to_ints(bitv, 0) == [0, 4, 8, 12, 16, 20] } -fn test_parse_step_star_too_large() ? { - assert parse_range_error('*/21', 0, 20) == 'Step too large.' +fn test_range_step_star_2() ? { + mut bitv := []bool{init: false, len: 9} + parse_range('*/3', 1, 8, mut bitv) ? + + assert bitv_to_ints(bitv, 1) == [1, 4, 7] } -fn test_parse_step_zero() ? { +fn test_range_step_star_too_large() ? { + assert parse_range_error('*/21', 0, 20) == 'Step size too large.' +} + +fn test_range_step_zero() ? { assert parse_range_error('*/0', 0, 20) == 'Step size zero not allowed.' } -fn test_parse_step_number() ? { +fn test_range_step_number() ? { mut bitv := []bool{init: false, len: 21} - parse_range('5/4', 0, 20, mut bitv) ? - assert bitv_to_ints(bitv, 0) == [5, 9, 13, 17] + parse_range('5/4', 2, 22, mut bitv) ? + + assert bitv_to_ints(bitv, 2) == [5, 9, 13, 17, 21] } -fn test_parse_step_number_too_large() ? { +fn test_range_step_number_too_large() ? { assert parse_range_error('10/4', 0, 5) == 'Out of range.' } -fn test_parse_step_number_too_small() ? { +fn test_range_step_number_too_small() ? { assert parse_range_error('2/4', 5, 10) == 'Out of range.' } + +// =====parse_part===== +fn test_part_single() ? { + assert parse_part('*', 0, 5) ? == [0, 1, 2, 3, 4, 5] +} + +fn test_part_multiple() ? { + assert parse_part('*/2,2/3', 1, 8) ? == [1, 2, 3, 5, 7, 8] +} From 2942793f40d596eef7bde4868a0ec451d0982498 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 12 Apr 2022 10:35:29 +0200 Subject: [PATCH 12/18] Added support for x-y syntax --- src/cron/cron.v | 2 +- src/cron/expression.v | 60 +++++++++++++++++++++++++------- src/cron/expression_parse_test.v | 30 +++++++++++----- 3 files changed, 70 insertions(+), 22 deletions(-) diff --git a/src/cron/cron.v b/src/cron/cron.v index 74e203f..a049eec 100644 --- a/src/cron/cron.v +++ b/src/cron/cron.v @@ -29,7 +29,7 @@ pub fn cron(conf Config) ? { // println(queue) // exp := '10/2 5 *' // println(parse_expression(exp) ?) - ce := parse_expression('0 3 */2') ? + ce := parse_expression('0 35 */2') ? println(ce) // ce := CronExpression{ // minutes: [0] diff --git a/src/cron/expression.v b/src/cron/expression.v index 46f92f9..0bc1591 100644 --- a/src/cron/expression.v +++ b/src/cron/expression.v @@ -118,17 +118,28 @@ fn (ce &CronExpression) next_from_now() ?time.Time { // possible. fn parse_range(s string, min int, max int, mut bitv []bool) ? { mut start := min + mut end := max mut interval := 1 exps := s.split('/') + if exps.len > 2 { + return error('Invalid expression.') + } + if exps[0] != '*' { - start = exps[0].int() + dash_parts := exps[0].split('-') + + if dash_parts.len > 2 { + return error('Invalid expression.') + } + + start = dash_parts[0].int() // The builtin parsing functions return zero if the string can't be // parsed into a number, so we have to explicitely check whether they // actually entered zero or if it's an invalid number. - if start == 0 && exps[0] != '0' { + if start == 0 && dash_parts[0] != '0' { return error('Invalid number.') } @@ -136,6 +147,18 @@ fn parse_range(s string, min int, max int, mut bitv []bool) ? { if start < min || start > max { return error('Out of range.') } + + if dash_parts.len == 2 { + end = dash_parts[1].int() + + if end == 0 && dash_parts[1] != '0' { + return error('Invalid number.') + } + + if end < start || end > max { + return error('Out of range.') + } + } } if exps.len > 1 { @@ -146,7 +169,7 @@ fn parse_range(s string, min int, max int, mut bitv []bool) ? { if interval == 0 { if exps[1] != '0' { return error('Invalid number.') - }else{ + } else { return error('Step size zero not allowed.') } } @@ -157,12 +180,12 @@ fn parse_range(s string, min int, max int, mut bitv []bool) ? { } // Here, s solely consists of a number, so that's the only value we // should return. - else if exps[0] != '*' { + else if exps[0] != '*' && !exps[0].contains('-') { bitv[start - min] = true return } - for start <= max { + for start <= end { bitv[start - min] = true start += interval } @@ -171,7 +194,7 @@ fn parse_range(s string, min int, max int, mut bitv []bool) ? { fn bitv_to_ints(bitv []bool, min int) []int { mut out := []int{} - for i in 0..bitv.len { + for i in 0 .. bitv.len { if bitv[i] { out << min + i } @@ -181,7 +204,7 @@ fn bitv_to_ints(bitv []bool, min int) []int { } fn parse_part(s string, min int, max int) ?[]int { - mut bitv := []bool{init: false, len: max - min + 1} + mut bitv := []bool{len: max - min + 1, init: false} for range in s.split(',') { parse_range(range, min, max, mut bitv) ? @@ -190,7 +213,8 @@ fn parse_part(s string, min int, max int) ?[]int { return bitv_to_ints(bitv, min) } -// min hour day month day-of-week +// parse_expression parses an entire cron expression string into a +// CronExpression object, if possible. fn parse_expression(exp string) ?CronExpression { // The filter allows for multiple spaces between parts mut parts := exp.split(' ').filter(it != '') @@ -205,10 +229,22 @@ fn parse_expression(exp string) ?CronExpression { parts << '*' } + mut part_results := [][]int{} + + mins := [0, 0, 1, 1] + maxs := [59, 23, 31, 12] + + // This for loop allows us to more clearly propagate the error to the user. + for i, min in mins { + part_results << parse_part(parts[i], min, maxs[i]) or { + return error('An error occurred with part $i: $err.msg') + } + } + return CronExpression{ - minutes: parse_part(parts[0], 0, 59) ? - hours: parse_part(parts[1], 0, 23) ? - days: parse_part(parts[2], 1, 31) ? - months: parse_part(parts[3], 1, 12) ? + minutes: part_results[0] + hours: part_results[1] + days: part_results[2] + months: part_results[3] } } diff --git a/src/cron/expression_parse_test.v b/src/cron/expression_parse_test.v index 8f22850..8f3ac38 100644 --- a/src/cron/expression_parse_test.v +++ b/src/cron/expression_parse_test.v @@ -3,25 +3,23 @@ module cron // parse_range_error returns the returned error message. If the result is '', // that means the function didn't error. fn parse_range_error(s string, min int, max int) string { - mut bitv := []bool{init: false, len: max - min + 1} + mut bitv := []bool{len: max - min + 1, init: false} - parse_range(s, min, max, mut bitv) or { - return err.msg - } + parse_range(s, min, max, mut bitv) or { return err.msg } return '' } // =====parse_range===== fn test_range_star_range() ? { - mut bitv := []bool{init: false, len: 6} + mut bitv := []bool{len: 6, init: false} parse_range('*', 0, 5, mut bitv) ? assert bitv == [true, true, true, true, true, true] } fn test_range_number() ? { - mut bitv := []bool{init: false, len: 6} + mut bitv := []bool{len: 6, init: false} parse_range('4', 0, 5, mut bitv) ? assert bitv_to_ints(bitv, 0) == [4] @@ -40,14 +38,14 @@ fn test_range_number_invalid() ? { } fn test_range_step_star_1() ? { - mut bitv := []bool{init: false, len: 21} + mut bitv := []bool{len: 21, init: false} parse_range('*/4', 0, 20, mut bitv) ? assert bitv_to_ints(bitv, 0) == [0, 4, 8, 12, 16, 20] } fn test_range_step_star_2() ? { - mut bitv := []bool{init: false, len: 9} + mut bitv := []bool{len: 8, init: false} parse_range('*/3', 1, 8, mut bitv) ? assert bitv_to_ints(bitv, 1) == [1, 4, 7] @@ -62,7 +60,7 @@ fn test_range_step_zero() ? { } fn test_range_step_number() ? { - mut bitv := []bool{init: false, len: 21} + mut bitv := []bool{len: 21, init: false} parse_range('5/4', 2, 22, mut bitv) ? assert bitv_to_ints(bitv, 2) == [5, 9, 13, 17, 21] @@ -76,6 +74,20 @@ fn test_range_step_number_too_small() ? { assert parse_range_error('2/4', 5, 10) == 'Out of range.' } +fn test_range_dash() ? { + mut bitv := []bool{len: 10, init: false} + parse_range('4-8', 0, 9, mut bitv) ? + + assert bitv_to_ints(bitv, 0) == [4, 5, 6, 7, 8] +} + +fn test_range_dash_step() ? { + mut bitv := []bool{len: 10, init: false} + parse_range('4-8/2', 0, 9, mut bitv) ? + + assert bitv_to_ints(bitv, 0) == [4, 6, 8] +} + // =====parse_part===== fn test_part_single() ? { assert parse_part('*', 0, 5) ? == [0, 1, 2, 3, 4, 5] From 65d6aae701d087f381ff5f11ec90f7afda5e9f2c Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 12 Apr 2022 11:10:49 +0200 Subject: [PATCH 13/18] Made sure unix value is calculated --- src/cron/cron.v | 44 +++++++++++-------------------------------- src/cron/expression.v | 4 ++-- 2 files changed, 13 insertions(+), 35 deletions(-) diff --git a/src/cron/cron.v b/src/cron/cron.v index a049eec..cd32c44 100644 --- a/src/cron/cron.v +++ b/src/cron/cron.v @@ -3,6 +3,7 @@ module cron import git import datatypes import time +import rand struct ScheduledBuild { repo git.GitRepo @@ -14,42 +15,19 @@ fn (r1 ScheduledBuild) < (r2 ScheduledBuild) bool { } pub fn cron(conf Config) ? { - // mut queue := datatypes.MinHeap{} - // repos_map := git.get_repos(conf.address, conf.api_key) ? + mut queue := datatypes.MinHeap{} - // for _, repo in repos_map { - // scheduled := ScheduledBuild{ - // repo: repo - // timestamp: 25 - // } + for _ in 0..5000 { + minute := rand.int_in_range(0, 60) ? + hour := rand.int_in_range(0, 23) ? + ce := parse_expression('$minute $hour') ? - // queue.insert(scheduled) - // } - - // println(queue) - // exp := '10/2 5 *' - // println(parse_expression(exp) ?) - ce := parse_expression('0 35 */2') ? - println(ce) - // ce := CronExpression{ - // minutes: [0] - // hours: [3] - // days: [1, 2, 3, 4, 5, 6] - // months: [1, 2] - // } - mut t := time.Time{ - year: 2022 - month: 12 - minute: 9 - hour: 13 - day: 12 + t := ce.next_from_now() ? + // println(t) + queue.insert(t) } - // mut t := time.now() - println(t) - - for _ in 1 .. 25 { - t = ce.next(t) ? - println(t) + for queue.len() > 0 { + println(queue.pop() ?) } } diff --git a/src/cron/expression.v b/src/cron/expression.v index 0bc1591..d275a42 100644 --- a/src/cron/expression.v +++ b/src/cron/expression.v @@ -101,13 +101,13 @@ pub fn (ce &CronExpression) next(ref time.Time) ?time.Time { year++ } - return time.Time{ + return time.new_time(time.Time{ year: year month: month day: day minute: minute hour: hour - } + }) } fn (ce &CronExpression) next_from_now() ?time.Time { From eb65bb8a69cebc4c687bbecee6214f57bfa61db4 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 12 Apr 2022 14:22:40 +0200 Subject: [PATCH 14/18] These bugs are gonna take a while --- src/cron/cron.v | 17 +++++------------ src/cron/expression.v | 26 ++++++++++++++----------- src/cron/expression_test.v | 39 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 23 deletions(-) create mode 100644 src/cron/expression_test.v diff --git a/src/cron/cron.v b/src/cron/cron.v index cd32c44..931d1c8 100644 --- a/src/cron/cron.v +++ b/src/cron/cron.v @@ -17,17 +17,10 @@ fn (r1 ScheduledBuild) < (r2 ScheduledBuild) bool { pub fn cron(conf Config) ? { mut queue := datatypes.MinHeap{} - for _ in 0..5000 { - minute := rand.int_in_range(0, 60) ? - hour := rand.int_in_range(0, 23) ? - ce := parse_expression('$minute $hour') ? + ce := parse_expression('0 3') ? + t := time.parse('2002-01-01 00:00:00') ? - t := ce.next_from_now() ? - // println(t) - queue.insert(t) - } - - for queue.len() > 0 { - println(queue.pop() ?) - } + println(t) + t2 := ce.next(t) ? + println(t2) } diff --git a/src/cron/expression.v b/src/cron/expression.v index d275a42..600e252 100644 --- a/src/cron/expression.v +++ b/src/cron/expression.v @@ -2,8 +2,6 @@ module cron import time -const days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] - struct CronExpression { minutes []int hours []int @@ -15,6 +13,13 @@ struct CronExpression { // always pick a moment in the future, even if ref matches completely up to the // minute. This function conciously does not take gap years into account. pub fn (ce &CronExpression) next(ref time.Time) ?time.Time { + // If the given ref matches the next cron occurence up to the minute, it + // will return that value. Because we always want to return a value in the + // future, we artifically shift the ref 60 seconds to make sure we always + // match in the future. A shift of 60 seconds is enough because the cron + // expression does not allow for accuracy smaller than one minute. + sref := ref + // For all of these values, the rule is the following: if their value is // the length of their respective array in the CronExpression object, that // means we've looped back around. This means that the "bigger" value has @@ -29,25 +34,25 @@ pub fn (ce &CronExpression) next(ref time.Time) ?time.Time { // value loops around, then the smaller value will always reset as well. // For example, if we're going to a new day, the hour & minute will always // be their smallest value again. - for month_index < ce.months.len && ref.month > ce.months[month_index] { + for month_index < ce.months.len && sref.month > ce.months[month_index] { month_index++ } if month_index < ce.months.len { - for day_index < ce.days.len && ref.day > ce.days[day_index] { + for day_index < ce.days.len && sref.day > ce.days[day_index] { day_index++ } if day_index < ce.days.len { - for hour_index < ce.hours.len && ref.hour > ce.hours[hour_index] { + for hour_index < ce.hours.len && sref.hour > ce.hours[hour_index] { hour_index++ } if hour_index < ce.hours.len { // Minute is the only value where we explicitely make sure we - // can't match ref's value exactly. This is to ensure we only + // can't match sref's value exactly. This is to ensure we only // return values in the future. - for minute_index < ce.minutes.len && ref.minute >= ce.minutes[minute_index] { + for minute_index < ce.minutes.len && sref.minute >= ce.minutes[minute_index] { minute_index++ } } @@ -60,7 +65,6 @@ pub fn (ce &CronExpression) next(ref time.Time) ?time.Time { if minute_index == ce.minutes.len && hour_index < ce.hours.len { hour_index += 1 } - if hour_index == ce.hours.len && day_index < ce.days.len { day_index += 1 } @@ -77,11 +81,11 @@ pub fn (ce &CronExpression) next(ref time.Time) ?time.Time { // month, e.g. day 30 in February. When this occurs, we reset day back to // the smallest value & loop over to the next month that does have this // day. - if day > cron.days_in_month[ce.months[month_index % ce.months.len] - 1] { + if day > time.month_days[ce.months[month_index % ce.months.len] - 1] { day = ce.days[0] month_index += 1 - for day > cron.days_in_month[ce.months[month_index & ce.months.len] - 1] { + for day > time.month_days[ce.months[month_index & ce.months.len] - 1] { month_index += 1 // If for whatever reason the day value ends up being something @@ -94,7 +98,7 @@ pub fn (ce &CronExpression) next(ref time.Time) ?time.Time { } month := ce.months[month_index % ce.months.len] - mut year := ref.year + mut year := sref.year // If the month loops over, we need to increment the year. if month_index >= ce.months.len { diff --git a/src/cron/expression_test.v b/src/cron/expression_test.v new file mode 100644 index 0000000..bc48977 --- /dev/null +++ b/src/cron/expression_test.v @@ -0,0 +1,39 @@ +module cron + +import time { new_time, Time, parse } + +fn test_next_simple() ? { + ce := parse_expression('0 3') ? + t := parse('2002-01-01 00:00:00') ? + t2 := ce.next(t) ? + + assert t2.year == 2002 + assert t2.month == 1 + assert t2.day == 1 + assert t2.hour == 3 + assert t2.minute == 0 +} + +fn test_next_identical() ? { + ce := parse_expression('0 3') ? + t := parse('2002-01-01 03:00:00') ? + t2 := ce.next(t) ? + + assert t2.year == 2002 + assert t2.month == 1 + assert t2.day == 2 + assert t2.hour == 3 + assert t2.minute == 0 +} + +fn test_next_next_day() ? { + ce := parse_expression('0 3') ? + t := parse('2002-01-01 04:00:00') ? + t2 := ce.next(t) ? + + assert t2.year == 2002 + assert t2.month == 1 + assert t2.day == 2 + assert t2.hour == 3 + assert t2.minute == 0 +} From e6033f9ab44ab7516fd251eef861e0b82c26a277 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 12 Apr 2022 20:53:20 +0200 Subject: [PATCH 15/18] Ran vfmt --- src/cron/expression_test.v | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cron/expression_test.v b/src/cron/expression_test.v index bc48977..aaead9c 100644 --- a/src/cron/expression_test.v +++ b/src/cron/expression_test.v @@ -1,6 +1,6 @@ module cron -import time { new_time, Time, parse } +import time { parse } fn test_next_simple() ? { ce := parse_expression('0 3') ? From 1116fee3fc45d9c5f5826dbfe90a4d915fc3fafa Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 12 Apr 2022 21:16:09 +0200 Subject: [PATCH 16/18] Actually possibly kinda decent cron next func --- src/cron/cron.v | 6 ----- src/cron/expression.v | 6 ++--- src/cron/expression_test.v | 53 +++++++++++++++----------------------- 3 files changed, 24 insertions(+), 41 deletions(-) diff --git a/src/cron/cron.v b/src/cron/cron.v index 931d1c8..2526326 100644 --- a/src/cron/cron.v +++ b/src/cron/cron.v @@ -1,9 +1,7 @@ module cron import git -import datatypes import time -import rand struct ScheduledBuild { repo git.GitRepo @@ -15,12 +13,8 @@ fn (r1 ScheduledBuild) < (r2 ScheduledBuild) bool { } pub fn cron(conf Config) ? { - mut queue := datatypes.MinHeap{} - ce := parse_expression('0 3') ? t := time.parse('2002-01-01 00:00:00') ? - - println(t) t2 := ce.next(t) ? println(t2) } diff --git a/src/cron/expression.v b/src/cron/expression.v index 600e252..0a35541 100644 --- a/src/cron/expression.v +++ b/src/cron/expression.v @@ -38,17 +38,17 @@ pub fn (ce &CronExpression) next(ref time.Time) ?time.Time { month_index++ } - if month_index < ce.months.len { + if month_index < ce.months.len && sref.month == ce.months[month_index] { for day_index < ce.days.len && sref.day > ce.days[day_index] { day_index++ } - if day_index < ce.days.len { + if day_index < ce.days.len && ce.days[day_index] == sref.day { for hour_index < ce.hours.len && sref.hour > ce.hours[hour_index] { hour_index++ } - if hour_index < ce.hours.len { + if hour_index < ce.hours.len && ce.hours[hour_index] == sref.hour { // Minute is the only value where we explicitely make sure we // can't match sref's value exactly. This is to ensure we only // return values in the future. diff --git a/src/cron/expression_test.v b/src/cron/expression_test.v index aaead9c..ce8526b 100644 --- a/src/cron/expression_test.v +++ b/src/cron/expression_test.v @@ -2,38 +2,27 @@ module cron import time { parse } +fn util_test_time(exp string, t1_str string, t2_str string) ? { + ce := parse_expression(exp) ? + t1 := parse(t1_str) ? + t2 := parse(t2_str) ? + + t3 := ce.next(t1) ? + + assert t2.year == t3.year + assert t2.month == t3.month + assert t2.day == t3.day + assert t2.hour == t3.hour + assert t2.minute == t3.minute +} + fn test_next_simple() ? { - ce := parse_expression('0 3') ? - t := parse('2002-01-01 00:00:00') ? - t2 := ce.next(t) ? + // Very simple + util_test_time('0 3', '2002-01-01 00:00:00', '2002-01-01 03:00:00') ? - assert t2.year == 2002 - assert t2.month == 1 - assert t2.day == 1 - assert t2.hour == 3 - assert t2.minute == 0 -} - -fn test_next_identical() ? { - ce := parse_expression('0 3') ? - t := parse('2002-01-01 03:00:00') ? - t2 := ce.next(t) ? - - assert t2.year == 2002 - assert t2.month == 1 - assert t2.day == 2 - assert t2.hour == 3 - assert t2.minute == 0 -} - -fn test_next_next_day() ? { - ce := parse_expression('0 3') ? - t := parse('2002-01-01 04:00:00') ? - t2 := ce.next(t) ? - - assert t2.year == 2002 - assert t2.month == 1 - assert t2.day == 2 - assert t2.hour == 3 - assert t2.minute == 0 + // Overlap to next day + util_test_time('0 3', '2002-01-01 03:00:00', '2002-01-02 03:00:00') ? + util_test_time('0 3', '2002-01-01 04:00:00', '2002-01-02 03:00:00') ? + + util_test_time('0 3/4', '2002-01-01 04:00:00', '2002-01-01 07:00:00') ? } From 5ce431aa4a21768d37a82919a90e9bd35e4dae40 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 12 Apr 2022 21:23:38 +0200 Subject: [PATCH 17/18] Added two more test dates; pleased v vet --- src/cron/cli.v | 1 + src/cron/cron.v | 1 + src/cron/expression_test.v | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/src/cron/cli.v b/src/cron/cli.v index 9bdec9a..8e6b0f1 100644 --- a/src/cron/cli.v +++ b/src/cron/cli.v @@ -12,6 +12,7 @@ pub: base_image string = 'archlinux:base-devel' } +// cmd returns the cli module that handles the cron daemon. pub fn cmd() cli.Command { return cli.Command{ name: 'cron' diff --git a/src/cron/cron.v b/src/cron/cron.v index 2526326..be37ffa 100644 --- a/src/cron/cron.v +++ b/src/cron/cron.v @@ -12,6 +12,7 @@ fn (r1 ScheduledBuild) < (r2 ScheduledBuild) bool { return r1.timestamp < r2.timestamp } +// cron starts a cron daemon & starts periodically scheduling builds. pub fn cron(conf Config) ? { ce := parse_expression('0 3') ? t := time.parse('2002-01-01 00:00:00') ? diff --git a/src/cron/expression_test.v b/src/cron/expression_test.v index ce8526b..0be9a64 100644 --- a/src/cron/expression_test.v +++ b/src/cron/expression_test.v @@ -25,4 +25,10 @@ fn test_next_simple() ? { util_test_time('0 3', '2002-01-01 04:00:00', '2002-01-02 03:00:00') ? util_test_time('0 3/4', '2002-01-01 04:00:00', '2002-01-01 07:00:00') ? + + // Overlap to next month + util_test_time('0 3', '2002-11-31 04:00:00', '2002-12-01 03:00:00') ? + + // Overlap to next year + util_test_time('0 3', '2002-12-31 04:00:00', '2003-01-01 03:00:00') ? } From bd0c276fd84c483a5e9bc73c3c833c6793f04615 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 12 Apr 2022 21:28:44 +0200 Subject: [PATCH 18/18] Added 'WIP' notice for cron cli --- src/cron/cron.v | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/cron/cron.v b/src/cron/cron.v index be37ffa..3ba9d0f 100644 --- a/src/cron/cron.v +++ b/src/cron/cron.v @@ -14,8 +14,5 @@ fn (r1 ScheduledBuild) < (r2 ScheduledBuild) bool { // cron starts a cron daemon & starts periodically scheduling builds. pub fn cron(conf Config) ? { - ce := parse_expression('0 3') ? - t := time.parse('2002-01-01 00:00:00') ? - t2 := ce.next(t) ? - println(t2) + println('WIP') }