From ff08c6622e40675f80b7ffe75ee83c39343af1e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20M=C3=A4nnchen?= Date: Fri, 20 Jan 2017 14:44:10 +0000 Subject: [PATCH] Support @reboot in parser & composer. Raise Error in Scheduler & DateChecker. (#30) --- lib/crontab/cron_expression.ex | 4 ++- lib/crontab/cron_expression/composer.ex | 7 ++++ lib/crontab/cron_expression/parser.ex | 4 +-- lib/crontab/date_checker.ex | 24 +++++++------- lib/crontab/scheduler.ex | 34 +++++++++++++++++--- test/crontab/cron_expression/parser_test.exs | 8 +++-- test/crontab/date_checker_test.exs | 7 ++++ 7 files changed, 66 insertions(+), 22 deletions(-) diff --git a/lib/crontab/cron_expression.ex b/lib/crontab/cron_expression.ex index ebaff41..866f378 100644 --- a/lib/crontab/cron_expression.ex +++ b/lib/crontab/cron_expression.ex @@ -7,6 +7,7 @@ defmodule Crontab.CronExpression do @type t :: %Crontab.CronExpression{ extended: boolean, + reboot: boolean, second: value, minute: value, hour: value, @@ -44,7 +45,8 @@ defmodule Crontab.CronExpression do The :extended attribute defines if the second is taken into account. """ - defstruct extended: false, second: [:*], minute: [:*], hour: [:*], day: [:*], month: [:*], weekday: [:*], year: [:*] + defstruct extended: false, reboot: false, second: [:*], minute: [:*], hour: [:*], day: [:*], month: [:*], + weekday: [:*], year: [:*] @doc """ Create a `%Crontab.CronExpression{}` via sigil. diff --git a/lib/crontab/cron_expression/composer.ex b/lib/crontab/cron_expression/composer.ex index 7d9e575..434e7e4 100644 --- a/lib/crontab/cron_expression/composer.ex +++ b/lib/crontab/cron_expression/composer.ex @@ -18,14 +18,21 @@ defmodule Crontab.CronExpression.Composer do iex> Crontab.CronExpression.Composer.compose %Crontab.CronExpression{minute: [9, {:-, 4, 6}, {:/, :*, 9}]} "9,4-6,*/9 * * * * *" + iex> Crontab.CronExpression.Composer.compose %Crontab.CronExpression{reboot: true} + "@reboot" + """ @spec compose(CronExpression.t) :: binary + def compose(%CronExpression{reboot: true}) do + "@reboot" + end def compose(cron_expression = %CronExpression{}) do cron_expression |> CronExpression.to_condition_list |> compose_interval |> Enum.join(" ") end + @spec compose_interval(CronExpression.condition_list) :: [binary] defp compose_interval([{_, conditions} | tail]) do part = conditions diff --git a/lib/crontab/cron_expression/parser.ex b/lib/crontab/cron_expression/parser.ex index 889e4dd..a6aad6b 100644 --- a/lib/crontab/cron_expression/parser.ex +++ b/lib/crontab/cron_expression/parser.ex @@ -8,6 +8,7 @@ defmodule Crontab.CronExpression.Parser do @type result :: {:ok, CronExpression.t} | {:error, binary} @specials %{ + reboot: %CronExpression{reboot: true}, yearly: %CronExpression{minute: [0], hour: [0], day: [1], month: [1]}, annually: %CronExpression{minute: [0], hour: [0], day: [1], month: [1]}, monthly: %CronExpression{minute: [0], hour: [0], day: [1]}, @@ -77,7 +78,7 @@ defmodule Crontab.CronExpression.Parser do @spec parse(binary, boolean) :: result def parse(cron_expression, extended \\ false) def parse("@" <> identifier, _) do - special(String.to_atom(identifier)) + special(String.to_atom(String.downcase(identifier))) end def parse(cron_expression, true) do interpret(String.split(cron_expression, " "), @extended_intervals, %CronExpression{extended: true}) @@ -235,7 +236,6 @@ defmodule Crontab.CronExpression.Parser do end @spec special(atom) :: result - defp special(:reboot), do: {:error, "Special identifier @reboot is not supported."} defp special(identifier) do if Map.has_key?(@specials, identifier) do {:ok, Map.fetch!(@specials, identifier)} diff --git a/lib/crontab/date_checker.ex b/lib/crontab/date_checker.ex index 0eb6b9c..836519c 100644 --- a/lib/crontab/date_checker.ex +++ b/lib/crontab/date_checker.ex @@ -6,32 +6,30 @@ defmodule Crontab.DateChecker do alias Crontab.CronExpression @doc """ - Check a CronExpression against a given date. + Check a condition list against a given date. ### Examples - iex> Crontab.DateChecker.matches_date? %CronExpression{minute: [{:"/", :*, 8}]}, ~N[2004-04-16 04:08:08] + iex> Crontab.DateChecker.matches_date? %Crontab.CronExpression{minute: [{:"/", :*, 8}]}, ~N[2004-04-16 04:08:08] true - iex> Crontab.DateChecker.matches_date? %CronExpression{minute: [{:"/", :*, 9}]}, ~N[2004-04-16 04:07:08] + iex> Crontab.DateChecker.matches_date? %Crontab.CronExpression{minute: [{:"/", :*, 9}]}, ~N[2004-04-16 04:07:08] false + iex> Crontab.DateChecker.matches_date? %Crontab.CronExpression{reboot: true}, ~N[2004-04-16 04:07:08] + ** (RuntimeError) Special identifier @reboot is not supported. + + iex> Crontab.DateChecker.matches_date? [{:hour, [{:"/", :*, 4}, 7]}], ~N[2004-04-16 04:07:08] + true + """ - @spec matches_date?(CronExpression.t, NaiveDateTime.t) :: boolean + @spec matches_date?(CronExpression.t, NaiveDateTime.t) :: boolean | no_return + def matches_date?(%CronExpression{reboot: true}, _), do: raise "Special identifier @reboot is not supported." def matches_date?(cron_expression = %CronExpression{}, execution_date) do cron_expression |> CronExpression.to_condition_list |> matches_date?(execution_date) end - - @doc """ - Check a condition list against a given date. - - ### Examples - - iex> Crontab.DateChecker.matches_date? [{:hour, [{:"/", :*, 4}, 7]}], ~N[2004-04-16 04:07:08] - true - """ @spec matches_date?(CronExpression.condition_list, NaiveDateTime.t) :: boolean def matches_date?(condition_list, date) def matches_date?([], _), do: true diff --git a/lib/crontab/scheduler.ex b/lib/crontab/scheduler.ex index dbc0e28..659f05b 100644 --- a/lib/crontab/scheduler.ex +++ b/lib/crontab/scheduler.ex @@ -24,9 +24,13 @@ defmodule Crontab.Scheduler do iex> Crontab.Scheduler.get_next_run_date(%Crontab.CronExpression{year: [{:/, :*, 9}]}, ~N[2002-01-13 23:00:07]) {:ok, ~N[2007-01-01 00:00:00]} + iex> Crontab.Scheduler.get_next_run_date %Crontab.CronExpression{reboot: true} + ** (RuntimeError) Special identifier @reboot is not supported. + """ @spec get_next_run_date(CronExpression.t, NaiveDateTime.t, integer) :: result - def get_next_run_date(cron_expression, date, max_runs \\ @max_runs) + def get_next_run_date(cron_expression, date \\ DateTime.to_naive(DateTime.utc_now), max_runs \\ @max_runs) + def get_next_run_date(%CronExpression{reboot: true}, _, _), do: raise "Special identifier @reboot is not supported." def get_next_run_date(cron_expression = %CronExpression{extended: false}, date, max_runs) do case get_run_date(cron_expression, clean_date(date, :seconds), max_runs, :increment) do {:ok, date} -> {:ok, date} @@ -52,9 +56,12 @@ defmodule Crontab.Scheduler do iex> Crontab.Scheduler.get_next_run_date!(%Crontab.CronExpression{year: [{:/, :*, 9}]}, ~N[2002-01-13 23:00:07]) ~N[2007-01-01 00:00:00] + iex> Crontab.Scheduler.get_next_run_date! %Crontab.CronExpression{reboot: true} + ** (RuntimeError) Special identifier @reboot is not supported. + """ @spec get_next_run_date!(CronExpression.t, NaiveDateTime.t, integer) :: NaiveDateTime.t | no_return - def get_next_run_date!(cron_expression, date, max_runs \\ @max_runs) do + def get_next_run_date!(cron_expression, date \\ DateTime.to_naive(DateTime.utc_now), max_runs \\ @max_runs) do case get_next_run_date(cron_expression, date, max_runs) do {:ok, result} -> result {:error, error} -> raise error @@ -84,6 +91,9 @@ defmodule Crontab.Scheduler do ...> %Crontab.CronExpression{year: [2017], month: [1], day: [1], hour: [0], minute: [1]}, ~N[2016-12-17 00:00:00]) {:error, [~N[2017-01-01 00:01:00]], "No compliant date was found for your interval."} + iex> Crontab.Scheduler.get_next_run_dates(3, %Crontab.CronExpression{reboot: true}) + ** (RuntimeError) Special identifier @reboot is not supported. + """ @spec get_next_run_dates(pos_integer, CronExpression.t, NaiveDateTime.t) :: {:ok | :error, [NaiveDateTime.t], binary} def get_next_run_dates(n, cron_expression, date \\ DateTime.to_naive(DateTime.utc_now)) @@ -113,6 +123,9 @@ defmodule Crontab.Scheduler do ...> %Crontab.CronExpression{year: [2017], month: [1], day: [1], hour: [0], minute: [1]}, ~N[2016-12-17 00:00:00]) ** (RuntimeError) No compliant date was found for your interval. + iex> Crontab.Scheduler.get_next_run_dates!(3, %Crontab.CronExpression{reboot: true}) + ** (RuntimeError) Special identifier @reboot is not supported. + """ @spec get_next_run_dates!(pos_integer, CronExpression.t, NaiveDateTime.t) :: [NaiveDateTime.t] | no_return def get_next_run_dates!(n, cron_expression, date \\ DateTime.to_naive(DateTime.utc_now)) @@ -153,9 +166,13 @@ defmodule Crontab.Scheduler do ...> year: [{:/, :*, 9}]}, ~N[2002-01-13 23:00:07] {:ok, ~N[1998-12-31 23:59:00]} + iex> Crontab.Scheduler.get_previous_run_date %Crontab.CronExpression{reboot: true} + ** (RuntimeError) Special identifier @reboot is not supported. + """ @spec get_previous_run_date(CronExpression.t, NaiveDateTime.t, integer) :: result - def get_previous_run_date(cron_expression, date, max_runs \\ @max_runs) + def get_previous_run_date(cron_expression, date \\ DateTime.to_naive(DateTime.utc_now), max_runs \\ @max_runs) + def get_previous_run_date(%CronExpression{reboot: true}, _, _), do: raise "Special identifier @reboot is not supported." def get_previous_run_date(cron_expression = %CronExpression{extended: false}, date, max_runs) do case get_run_date(cron_expression, date, max_runs, :decrement) do {:ok, date} -> {:ok, reset(date, :seconds)} @@ -183,10 +200,13 @@ defmodule Crontab.Scheduler do ...> year: [{:/, :*, 9}]}, ~N[2002-01-13 23:00:07] ~N[1998-12-31 23:59:00] + iex> Crontab.Scheduler.get_previous_run_date! %Crontab.CronExpression{reboot: true} + ** (RuntimeError) Special identifier @reboot is not supported. + """ @spec get_previous_run_date!(CronExpression.t, NaiveDateTime.t, integer) :: NaiveDateTime.t | no_return - def get_previous_run_date!(cron_expression, date, max_runs \\ @max_runs) do + def get_previous_run_date!(cron_expression, date \\ DateTime.to_naive(DateTime.utc_now), max_runs \\ @max_runs) do case get_previous_run_date(cron_expression, date, max_runs) do {:ok, result} -> result {:error, error} -> raise error @@ -216,6 +236,9 @@ defmodule Crontab.Scheduler do ...> %Crontab.CronExpression{year: [2016], month: [1], day: [1], hour: [0], minute: [1]}, ~N[2016-12-17 00:00:00]) {:error, [~N[2016-01-01 00:01:00]], "No compliant date was found for your interval."} + iex> Crontab.Scheduler.get_previous_run_dates(3, %Crontab.CronExpression{reboot: true}) + ** (RuntimeError) Special identifier @reboot is not supported. + """ @spec get_previous_run_dates(pos_integer, CronExpression.t, NaiveDateTime.t) :: {:ok | :error, [NaiveDateTime.t], binary} @@ -247,6 +270,9 @@ defmodule Crontab.Scheduler do ...> %Crontab.CronExpression{year: [2017], month: [1], day: [1], hour: [0], minute: [1]}, ~N[2016-12-17 00:00:00]) ** (RuntimeError) No compliant date was found for your interval. + iex> Crontab.Scheduler.get_previous_run_dates!(3, %Crontab.CronExpression{reboot: true}) + ** (RuntimeError) Special identifier @reboot is not supported. + """ @spec get_previous_run_dates!(pos_integer, CronExpression.t, NaiveDateTime.t) :: [NaiveDateTime.t] | no_return def get_previous_run_dates!(n, cron_expression, date \\ DateTime.to_naive(DateTime.utc_now)) diff --git a/test/crontab/cron_expression/parser_test.exs b/test/crontab/cron_expression/parser_test.exs index 5edb80c..ab23745 100644 --- a/test/crontab/cron_expression/parser_test.exs +++ b/test/crontab/cron_expression/parser_test.exs @@ -3,8 +3,12 @@ defmodule Crontab.CronExpression.ParserTest do doctest Crontab.CronExpression.Parser import Crontab.CronExpression.Parser - test "parse \"@reboot\" gives error" do - assert parse("@reboot") == {:error, "Special identifier @reboot is not supported."} + test "parse \"@reboot\" gives reboot" do + assert parse("@reboot") == {:ok, %Crontab.CronExpression{reboot: true, extended: false, minute: [:*], hour: [:*], day: [:*], month: [:*], weekday: [:*], year: [:*]}} + end + + test "parse \"@REBOOT\" gives reboot" do + assert parse("@REBOOT") == {:ok, %Crontab.CronExpression{reboot: true, extended: false, minute: [:*], hour: [:*], day: [:*], month: [:*], weekday: [:*], year: [:*]}} end test "parse \"@unknown\" gives error" do diff --git a/test/crontab/date_checker_test.exs b/test/crontab/date_checker_test.exs index 93267d2..d1b8dc3 100644 --- a/test/crontab/date_checker_test.exs +++ b/test/crontab/date_checker_test.exs @@ -44,4 +44,11 @@ defmodule Crontab.DateCheckerTest do assert matches_date?(:weekday, [{:"/", :*, 3}], base_date) == false assert matches_date?(:year, [{:"/", :*, 3}], base_date) == false end + + test "fail on @reboot" do + base_date = ~N[2003-04-17 04:04:08] + assert_raise RuntimeError, "Special identifier @reboot is not supported.", fn -> + matches_date?(%Crontab.CronExpression{reboot: true}, base_date) + end + end end