From 6d8599d2fd05f740e0613406605265c1560a09f1 Mon Sep 17 00:00:00 2001 From: herrmannj <> Date: Thu, 31 Aug 2023 18:59:54 +0000 Subject: [PATCH] Cron.pm: release git-svn-id: https://svn.fhem.de/fhem/trunk@27915 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/lib/FHEM/Scheduler/Cron.pm | 67 +++++++++++++---- fhem/t/FHEM/90_Cron/10_Cronlib.t | 93 ++++++++++++++++-------- fhem/t/FHEM/90_Cron/11_Cronlib_Cache.cfg | 0 fhem/t/FHEM/90_Cron/11_Cronlib_Cache.t | 35 +++++++++ 4 files changed, 148 insertions(+), 47 deletions(-) create mode 100644 fhem/t/FHEM/90_Cron/11_Cronlib_Cache.cfg create mode 100644 fhem/t/FHEM/90_Cron/11_Cronlib_Cache.t diff --git a/fhem/lib/FHEM/Scheduler/Cron.pm b/fhem/lib/FHEM/Scheduler/Cron.pm index faff6a062..4ac43b7d5 100644 --- a/fhem/lib/FHEM/Scheduler/Cron.pm +++ b/fhem/lib/FHEM/Scheduler/Cron.pm @@ -50,10 +50,25 @@ sub new { $ok ||= any { sub{ my $a = shift; any { $a == $_ } (1, 3, 5, 7, 8, 10, 12) }->($_) } @{$self->{list_of_months}} if ($first == 31); if (not $ok) { $self->{error} = "day and month will never become true"; - $self->log(2, '%s', $self->{error}) if $ENV{EXTENDED_DEBUG}; + $self->log(2, '%s', $self->{error}); last VALIDATE; } } + + if (exists($param->{holidays})) { + if (defined($param->{holidays}) and + ref($param->{holidays}) eq 'ARRAY' and + all {m/^[0-6]$/} @$param->{holidays}) { + @{$self->{config}->{holidays}} = sort { $a <=> $b } @$param->{holidays}; + } else { + $self->{error} = "holidays must be a list with [0-6]"; + $self->log(2, '%s', $self->{error}); + last VALIDATE; + } + } else { + $self->{config}->{holidays} = [0,6]; #default weekend sun, sat + } + } # $self->log(2, '%s', $self->{error}) if ($self->{error} and $ENV{EXTENDED_DEBUG}); return wantarray ? ($self, $self->{error}) : $self; @@ -69,13 +84,17 @@ sub next { if ($self->{error}) { return wantarray ? (undef, $self->{error}) : undef }; # validate input - my $from_date = substr($from, 0, 8); - - - - # internal method - return $self->_next($from); - + my (@from_date, @from_time); + @from_date = ($from =~ m/^([0-9]{4})([0-9]{2})([0-9]{2})[0-9]{6}$/); + @from_time = ($from =~ m/^[0-9]{8}([0-9]{2})([0-9]{2})([0-9]{2})$/); + if (@from_date and $self->is_valid_date(@from_date) and + @from_time and $self->is_valid_time(@from_time)) { + # internal method + return $self->_next($from); + } else { + $self->log(2, 'wrong date or time spec in next call %s', $from); + return wantarray ? (undef, sprintf('wrong date or time spec in call to next() %s', $from)) : undef ; + } } sub _next { @@ -93,10 +112,17 @@ sub _next { # If there is a discrepancy between from_date and working_date, we must first perform the next_date calculation. # Otherwise, this has already been done in previous rounds and can be skipped for efficiency and speed reasons. if ($from_date ne $self->{working_date}) { - # reset pointer - undef $self->{month_ptr}; - undef $self->{mday_ptr}; - undef $self->{positional_date_cache}; + # reset pointer / cache # can happen because of sleep, hybernate, clock shift + $self->log(5, 'from_date:%s does not match working_date:%s -> clear cache ', $from_date, $self->{working_date}) if $ENV{EXTENDED_DEBUG}; + # say "before: \n".join "\n", keys %{$self}; + delete $self->{month_ptr}; + delete $self->{weekday_month_ptr}; + delete $self->{mday_ptr}; + delete $self->{next_calender_date}; + delete $self->{next_weekday_date}; + delete $self->{next_positional_date}; + delete $self->{positional_date_cache}; + # say "after: \n".join "\n", keys %{$self}; $self->{working_date} = $next_date = $self->_next_date($from_date, 1); # inclusive, the from_date is possible } else { # load from cache @@ -107,6 +133,7 @@ sub _next { # ($from_date ne $next_date)) { if ($from_time ne $self->{working_time}) { # reset pointer + $self->log(5, 'from_time:%s does not match working_time:%s -> clear cache ', $from_time, $self->{working_time}) if $ENV{EXTENDED_DEBUG}; undef $self->{hour_ptr}; undef $self->{minute_ptr}; } @@ -250,10 +277,18 @@ sub _parse_cron_text { if (($mday ne '*' and $wday ne '*' ) or ($mday eq '*' and $wday ne '*' )) { foreach my $item (@list) { + # to provide error glues if $item is malformed. $item may be modified below + my $item_txt = $item; $self->log(5, 'about to parse wday item: %s', $item) if $ENV{EXTENDED_DEBUG}; + # replace weekday abbreviation - my %w = (Sun => 0, Mon => 1, Tue => 2, Wed => 3, Thu => 4, Fr => 5, Sat => 6); - $item =~ s/(Sun|Mon|Tue|Wed|Thu|Sat)/$w{$1}/gie; + # my %w = (sun => 0, mon => 1, tue => 2, wed => 3, thu => 4, fri => 5, sat => 6); + my ($sep, $mod); + ($item, $sep, $mod) = ($item =~ m/^([^#\/]*)([#\/]*)([^#\/]*)$/); + my %w = (sun => 0, mon => 1, tue => 2, wed => 3, thu => 4, fri => 5, sat => 6); + $item =~ s/(?_parse_wday_item($item, $href_and) if ($item =~ m/^[&].*/s); if (not $res) { - $self->{error} = "syntax error in wday item: $item"; - $self->log(5, 'syntax error in wday item: %s', $item) if $ENV{EXTENDED_DEBUG}; + $self->{error} = "syntax error in wday item: $item_txt"; + $self->log(5, 'syntax error in wday item: %s', $item_txt) if $ENV{EXTENDED_DEBUG}; return; } } diff --git a/fhem/t/FHEM/90_Cron/10_Cronlib.t b/fhem/t/FHEM/90_Cron/10_Cronlib.t index ee6e42c6c..e391d16a8 100644 --- a/fhem/t/FHEM/90_Cron/10_Cronlib.t +++ b/fhem/t/FHEM/90_Cron/10_Cronlib.t @@ -27,30 +27,44 @@ my $test = [ [q(handle '$cron_text'), '1-5 * * * *', qr(^$), sub {my @r = (20230101000000); for my $h (0..23) {for my $m (1..5) {push @r, sprintf('20230101%02d%02d00', $h, $m)}}; @r}->()], [q(handle '$cron_text'), '1-5/1 * * * *', qr(^$), sub {my @r = (20230101000000); for my $h (0..23) {for my $m (1..5) {push @r, sprintf('20230101%02d%02d00', $h, $m)}}; @r}->()], [q(handle '$cron_text'), '1-5/2 * * * *', qr(^$), sub {my @r = (20230101000000); for my $h (0..23) {for my $m (1,3,5) {push @r, sprintf('20230101%02d%02d00', $h, $m)}}; @r}->()], -# [q(accept '$cron_text'), '*/1 * * * *', 0, 0, qr(^$)], -# # positive tests for minute / value range -# [q(accept '$cron_text'), '0 * * * *', 0, join (',', ((20200101120000 .. 20200101120059 ), (20200101120100 .. 20200101120159 ))), qr(^$)], -# [q(accept '$cron_text'), '00 * * * *', 0, 0, qr(^$)], -# [q(accept '$cron_text'), '1 * * * *', 0, 0, qr(^$)], -# [q(accept '$cron_text'), '01 * * * *', 0, 0, qr(^$)], -# [q(accept '$cron_text'), '1-59 * * * *', 0, 0, qr(^$)], -# [q(accept '$cron_text'), '01-059 * * * *', 0, 0, qr(^$)], -# [q(accept '$cron_text'), '1-59/1 * * * *', 0, 0, qr(^$)], -# [q(accept '$cron_text'), '01-059/01 * * * *', 0, 0, qr(^$)], -# [q(accept '$cron_text'), '1-59/59 * * * *', 0, 0, qr(^$)], -# [q(accept '$cron_text'), '01-059/059 * * * *', 0, 0, qr(^$)], -# [q(accept '$cron_text'), '10~20 * * * *', 0, 0, qr(^$)], -# # negative tests for minute / syntax -# [q(must throw an error '$cron_text'), 'a * * * *', 0, 0, qr(^syntax error in minute item:)], -# [q(must throw an error '$cron_text'), '*,a * * * *', 0, 0, qr(^syntax error in minute item:)], -# [q(must throw an error '$cron_text'), '1,a * * * *', 0, 0, qr(^syntax error in minute item:)], -# [q(must throw an error '$cron_text'), '*-5 * * * *', 0, 0, qr(^syntax error in minute item:)], -# [q(must throw an error '$cron_text'), '5-1 * * * *', 0, 0, qr(^syntax error in minute item:)], -# [q(must throw an error '$cron_text'), '60 * * * *', 0, 0, qr(^syntax error in minute item:)], -# [q(must throw an error '$cron_text'), '0-60 * * * *', 0, 0, qr(^syntax error in minute item:)], -# [q(must throw an error '$cron_text'), '0-59/60 * * * *', 0, 0, qr(^syntax error in minute item:)], -# [q(must throw an error '$cron_text'), '20~10 * * * *', 0, 0, qr(^syntax error in minute item:)], -# # negative tests for minute / value range + + # positive tests for minute + [q(accept '$cron_text'), '0 * * * *', qr(^$), join (',', ((20230101120000 .. 20230101120059 ), (20230101120100 .. 20230101120159 )))], + [q(accept '$cron_text'), '00 * * * *', qr(^$), 0], + [q(accept '$cron_text'), '1 * * * *', qr(^$), 0], + [q(accept '$cron_text'), '01 * * * *', qr(^$), 0], + [q(accept '$cron_text'), '1-59 * * * *', qr(^$), sub {my @r = (20230101000000); for my $h (0) {for my $m (1..59) {push @r, sprintf('20230101%02d%02d00', $h, $m)}}; @r}->()], + [q(accept '$cron_text'), '01-059 * * * *', qr(^$), 0], + [q(accept '$cron_text'), '1-59/1 * * * *', qr(^$), sub {my @r = (20230101000000); for my $h (0) {for my $m (1..59) {push @r, sprintf('20230101%02d%02d00', $h, $m)}}; @r}->()], + [q(accept '$cron_text'), '1-59/30 * * * *', qr(^$), sub {my @r = (20230101000000); for my $h (0) {for my $m (1,31) {push @r, sprintf('20230101%02d%02d00', $h, $m)}}; @r}->()], + [q(accept '$cron_text'), '01-059/01 * * * *', qr(^$), 0], + [q(accept '$cron_text'), '1-59/59 * * * *', qr(^$), sub {my @r = (20230101000000); for my $h (0) {for my $m (1) {push @r, sprintf('20230101%02d%02d00', $h, $m)}}; @r}->()], + [q(accept '$cron_text'), '01-059/059 * * * *', qr(^$), 0], + [q(accept '$cron_text'), '10~20 * * * *', qr(^$), 0], + [q(accept '$cron_text'), '10~20,30~40 * * * *', qr(^$), 0], + # negative tests for minute + [q(reject '$cron_text'), 'a * * * *', qr(^syntax error in minute item:), 0], + [q(reject '$cron_text'), '*,a * * * *', qr(^syntax error in minute item:), 0], + [q(reject '$cron_text'), '1,a * * * *', qr(^syntax error in minute item:), 0], + [q(reject '$cron_text'), '*-5 * * * *', qr(^syntax error in minute item:), 0], + [q(reject '$cron_text'), '5-1 * * * *', qr(^syntax error in minute item:), 0], + [q(reject '$cron_text'), '60 * * * *', qr(^syntax error in minute item:), 0], + [q(reject '$cron_text'), '0-60 * * * *', qr(^syntax error in minute item:), 0], + [q(reject '$cron_text'), '0-59/60 * * * *', qr(^syntax error in minute item:), 0], + [q(reject '$cron_text'), '20~10 * * * *', qr(^syntax error in minute item:), 0], + # positive test for hour + [q(accept '$cron_text'), '0 0 * * *', qr(^$), 20230101000000, 20230102000000, 20230103000000], + [q(accept '$cron_text'), '0 00 * * *', qr(^$), 0], + [q(accept '$cron_text'), '0 1 * * *', qr(^$), 0], + [q(accept '$cron_text'), '0 01 * * *', qr(^$), 0], + [q(accept '$cron_text'), '0 1-23 * * *', qr(^$), sub {my @r = (20230101000000); for my $h (1..23) {for my $m (0) {push @r, sprintf('20230101%02d%02d00', $h, $m)}}; @r}->()], + [q(accept '$cron_text'), '0 01-023 * * *', qr(^$), sub {my @r = (20230101000000); for my $h (1..23) {for my $m (0) {push @r, sprintf('20230101%02d%02d00', $h, $m)}}; @r}->()], + [q(accept '$cron_text'), '0 1-23/1 * * *', qr(^$), sub {my @r = (20230101000000); for my $h (1..23) {for my $m (0) {push @r, sprintf('20230101%02d%02d00', $h, $m)}}; @r}->()], + [q(accept '$cron_text'), '0 01-023/01 * * *', qr(^$), sub {my @r = (20230101000000); for my $h (1..23) {for my $m (0) {push @r, sprintf('20230101%02d%02d00', $h, $m)}}; @r}->()], + [q(accept '$cron_text'), '0 1-23/12 * * *', qr(^$), sub {my @r = (20230101000000); for my $h (1,13) {for my $m (0) {push @r, sprintf('20230101%02d%02d00', $h, $m)}}; @r}->()], + [q(accept '$cron_text'), '0 1-23/23 * * *', qr(^$), 20230101010000, 20230102010000, 20230103010000], + [q(accept '$cron_text'), '0 1~10 * * *', qr(^$), 0], + [q(accept '$cron_text'), '0 1~10,11~20 * * *', qr(^$), 0], # positive tests for weekday [q(handle '$cron_text'), '0 0 * * *', qr(^$), sub {my @r = (20230101000000); for my $d (2..31) {for my $h (0) {for my $m (0) {push @r, sprintf('202301%02d%02d%02d00', $d, $h, $m)}}}; @r}->()], @@ -60,20 +74,37 @@ my $test = [ [q(handle '$cron_text'), '0 0 * * 1-5/2', qr(^$), 20230101000000, 20230102000000, 20230104000000, 20230106000000 ], [q(handle '$cron_text'), '0 0 * * 1-5,6', qr(^$), 20230101000000, 20230102000000, 20230103000000, 20230104000000, 20230105000000, 20230106000000, 20230107000000 ], [q(handle '$cron_text'), '0 0 8 * 1-5,6', qr(^$), 20230101000000, 20230102000000, 20230103000000, 20230104000000, 20230105000000, 20230106000000, 20230107000000, 20230108000000 ], + [q(handle '$cron_text'), '0 0 8 * Mon-Fri,Sat', qr(^$), 20230101000000, 20230102000000, 20230103000000, 20230104000000, 20230105000000, 20230106000000, 20230107000000, 20230108000000 ], + [q(handle '$cron_text'), '0 0 8 * mon-fri,sat', qr(^$), 20230101000000, 20230102000000, 20230103000000, 20230104000000, 20230105000000, 20230106000000, 20230107000000, 20230108000000 ], + [q(handle '$cron_text'), '0 0 8 * MON-FRI,SAT', qr(^$), 20230101000000, 20230102000000, 20230103000000, 20230104000000, 20230105000000, 20230106000000, 20230107000000, 20230108000000 ], [q(handle '$cron_text'), '0 0 2-8 * &1-5,6', qr(^$), 20230101000000, 20230102000000, 20230103000000, 20230104000000, 20230105000000, 20230106000000, 20230107000000, 20230114000000 ], + [q(handle '$cron_text'), '0 0 2-8 * &Mon-Fri,Sat', qr(^$), 20230101000000, 20230102000000, 20230103000000, 20230104000000, 20230105000000, 20230106000000, 20230107000000, 20230114000000 ], # positional - [q(handle '$cron_text'), '0 0 * * 4#l', qr(^$), 20230101000000, 20230126000000, 20230223000000, 20230330000000, 20230427000000, 20230525000000, 20230629000000 ], + [q(handle '$cron_text'), '0 0 * 2 0#F', qr(^$), 20230201000000, 20230205000000, 20240204000000, 20250202000000, 20260201000000, 20270207000000, 20280206000000 ], + [q(handle '$cron_text'), '0 0 * 2 Sun#F', qr(^$), 20230201000000, 20230205000000, 20240204000000, 20250202000000, 20260201000000, 20270207000000, 20280206000000 ], + [q(handle '$cron_text'), '0 0 * * 4#L', qr(^$), 20230101000000, 20230126000000, 20230223000000, 20230330000000, 20230427000000, 20230525000000, 20230629000000 ], + [q(handle '$cron_text'), '0 0 * * Thu#L', qr(^$), 20230101000000, 20230126000000, 20230223000000, 20230330000000, 20230427000000, 20230525000000, 20230629000000 ], [q(handle '$cron_text'), '0 0 * 2 6#5', qr(^$), 20230201000000, 20480229000000, 20760229000000, 21160229000000, 21440229000000, 21720229000000, 22120229000000 ], [q(handle '$cron_text'), '0 0 * 2 6#-5', qr(^$), 20230201000000, 20480201000000, 20760201000000, 21160201000000, 21440201000000, 21720201000000, 22120201000000 ], + # reject + [q(reject '$cron_text'), '0 0 * 2 8', qr(^syntax error in wday item: 8$), 20230201000000 ], + [q(reject '$cron_text'), '0 0 * 2 0/Mon', qr(^syntax error in wday item: 0/Mon$), 20230201000000 ], + [q(reject '$cron_text'), '0 0 * 2 0#0', qr(^syntax error in wday item: 0#0$), 20230201000000 ], + [q(reject '$cron_text'), '0 0 * 2 0#6', qr(^syntax error in wday item: 0#6$), 20230201000000 ], + [q(reject '$cron_text'), '0 0 * 2 0#-6', qr(^syntax error in wday item: 0#-6$), 20230201000000 ], + [q(reject '$cron_text'), '0 0 * 2 0#A', qr(^syntax error in wday item: 0#A$), 20230201000000 ], + [q(reject '$cron_text'), '0 0 * 2 0#Mon', qr(^syntax error in wday item: 0#Mon$), 20230201000000 ], + [q(reject '$cron_text'), '0 0 * 2 FOO', qr(^syntax error in wday item: FOO$), 20230201000000 ], + [q(reject '$cron_text'), '0 0 * 2 Sunday', qr(^syntax error in wday item: Sunday$), 20230201000000 ], # time series - [q(Timeseries '$cron_text'), '0 12 3,4,5 2 0,2,3,4', qr(^$), 20230102150000, 20230201120000, 20230202120000, 20230203120000, 20230204120000], - [q(Feb-29 & Sunday '$cron_text'), '0 12 29 2 &7', qr(^$), 20230102150000, 20320229120000], + [q(Timeseries '$cron_text'), '0 12 3,4,5 2 0,2,3,4', qr(^$), 20230102150000, 20230201120000, 20230202120000, 20230203120000, 20230204120000], + [q(Feb-29 (leap year)'$cron_text'), '0 12 29 2 *', qr(^$), 20230101150000, 20240229120000, 20280229120000, 20320229120000], + [q(Feb-29 & Sunday '$cron_text'), '0 12 29 2 &Sun', qr(^$), 20230102150000, 20320229120000], + [q(Fri 13 '$cron_text'), '0 12 13 * &Fri', qr(^$), 20230101150000, 20230113120000, 20231013120000, 20240913120000], + [q(daylight eu '$cron_text'), '0 2 * 3,10 Sun#L', qr(^$), 20230101150000, 20230326020000, 20231029020000, 20240331020000], ]; -# print join ",", (20200101120000 .. 20200101120010 ), (20200101120100 .. 20200101120110 ); -# print "\n"; - my $cron_lib_loadable = eval{use FHEM::Scheduler::Cron;1;}; ok($cron_lib_loadable, "FHEM::Scheduler::Cron loaded"); @@ -94,7 +125,7 @@ foreach my $t (@$test) { } } $ok &&= (($err // '') =~ /$err_expected/); - ok($ok, sprintf('%s %s', eval qq{"$desc"} , $err?"(got '$err')":"(# of passes: $count)")); + ok($ok, sprintf('%s %s', eval qq{"$desc"} , $err?"(got '$err')":"(# of successful passes: $count)")); }; done_testing; diff --git a/fhem/t/FHEM/90_Cron/11_Cronlib_Cache.cfg b/fhem/t/FHEM/90_Cron/11_Cronlib_Cache.cfg new file mode 100644 index 000000000..e69de29bb diff --git a/fhem/t/FHEM/90_Cron/11_Cronlib_Cache.t b/fhem/t/FHEM/90_Cron/11_Cronlib_Cache.t new file mode 100644 index 000000000..e6a0d78a4 --- /dev/null +++ b/fhem/t/FHEM/90_Cron/11_Cronlib_Cache.t @@ -0,0 +1,35 @@ +# perl fhem.pl -t t/FHEM/90_Cron/99_Cronlib_DEBUG.t +use v5.14; + +use strict; +use warnings; +use Test::More; +use FHEM::Scheduler::Cron; + +$ENV{EXTENDED_DEBUG} = 1; + +my ($cron_obj, $err, $got); + +($cron_obj, $err) = FHEM::Scheduler::Cron->new("1 12,13 1-15 * 2#1"); +($got, $err) = $cron_obj->next(20230101120000); +ok((not $err and ($got) and ($got eq 20230101120100)), "after new: $got"); +($got, $err) = $cron_obj->next(20230101120100); +ok((not $err and ($got) and ($got eq 20230101130100)), "time cache: $got"); +($got, $err) = $cron_obj->next(20230101130100); +ok((not $err and ($got) and ($got eq 20230102120100)), "time cache: $got"); +($got, $err) = $cron_obj->next(20230102120100); +ok((not $err and ($got) and ($got eq 20230102130100)), "time cache: $got"); +($got, $err) = $cron_obj->next(20230102130100); +ok((not $err and ($got) and ($got eq 20230103120100)), "time cache: $got"); +($got, $err) = $cron_obj->next(20230103120100); +ok((not $err and ($got) and ($got eq 20230103130100)), "time cache: $got"); +($got, $err) = $cron_obj->next(20230103130100); +ok((not $err and ($got) and ($got eq 20230104120100)), "time cache: $got"); +# jumps +($got, $err) = $cron_obj->next(20230105130500); +ok((not $err and ($got) and ($got eq 20230106120100)), "jump forward: $got"); +($got, $err) = $cron_obj->next(20230101120000); +ok((not $err and ($got) and ($got eq 20230101120100)), "jump forward: $got"); + +done_testing; +exit(0);