From 569abbb7b5def5521adad301a45b9b3865190053 Mon Sep 17 00:00:00 2001 From: Diogo Dourado Date: Wed, 16 Jun 2021 10:34:48 -0300 Subject: [PATCH 1/6] feat: `every` macro accepting list as arg1 + example --- lib/cronex/every.ex | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/lib/cronex/every.ex b/lib/cronex/every.ex index a9f37e0..83bdc32 100644 --- a/lib/cronex/every.ex +++ b/lib/cronex/every.ex @@ -10,16 +10,16 @@ defmodule Cronex.Every do `frequency` supports the following values: `:minute`, `:hour`, `:day`, `:month`, `:year`, `:monday`, `:tuesday`, `:wednesday`, `:thursday`, `:friday`, `:saturday`, `:sunday` - `job` must be a list with the following structure: `[do: block]`, where `block` is the code refering to a specific job + `job` must be a list with the following structure: `[do: block]`, where `block` is the code refering to a specific job ## Example every :day do - # Daily task here + # Daily task here end every :month do - # Monthly task here + # Monthly task here end """ defmacro every(frequency, [do: block] = _job) @@ -55,18 +55,18 @@ defmodule Cronex.Every do `interval` must be an integer representing the interval of frequencies that should exist between each job run - `at` must be a list with the following structure: `[at: time]`, where `time` is a string with the following format `HH:MM`, where `HH` represents the hour and `MM` the minutes at which the job should be run, this value is ignored when given in an every minute or every hour job + `at` must be a list with the following structure: `[at: time]`, where `time` is a string with the following format `HH:MM`, where `HH` represents the hour and `MM` the minutes at which the job should be run, this value is ignored when given in an every minute or every hour job `job` must be a list with the following structure: `[do: block]`, where `block` is the code corresponding to a specific job ## Example every :day, at: "10:00" do - # Daily task at 10:00 here + # Daily task at 10:00 here end every :monday, at: "12:00" do - # Monday task at 12:00 here + # Monday task at 12:00 here end every 2, :day do @@ -76,6 +76,10 @@ defmodule Cronex.Every do every 3, :week do # Every 3 weeks task end + + every [:sunday, :monday], at: "14:00" do + # Sunday and Monday task at 14:00 here + end """ defmacro every(arg1, [at: time] = _arg2, [do: block] = _job) when is_atom(arg1) and is_bitstring(time) do @@ -115,6 +119,26 @@ defmodule Cronex.Every do end end + defmacro every(arg1, [at: time], [do: block] = _job) + when is_list(arg1) and is_bitstring(time) do + days = Enum.join(arg1, "_") + job_name = String.to_atom("job_every_#{days}_at_#{time}") + + quote do + @jobs unquote(job_name) + + @doc false + def unquote(job_name)() do + Cronex.Job.new( + unquote(arg1), + unquote(time), + fn -> unquote(block) end + ) + |> Cronex.Job.validate!() + end + end + end + @doc """ `Cronex.Every.every/4` macro is used as a simple interface to add a job to the `Cronex.Table`. @@ -124,7 +148,7 @@ defmodule Cronex.Every do `frequency` supports the following values: `:minute`, `:hour`, `:day`, `:month` - `at` must be a list with the following structure: `[at: time]`, where `time` is a string with the following format `HH:MM`, where `HH` represents the hour and `MM` the minutes at which the job should be run, this value is ignored when given in an every minute or every hour job + `at` must be a list with the following structure: `[at: time]`, where `time` is a string with the following format `HH:MM`, where `HH` represents the hour and `MM` the minutes at which the job should be run, this value is ignored when given in an every minute or every hour job `job` must be a list with the following structure: `[do: block]`, where `block` is the code corresponding to a specific job From 177beb81679a8dd29e391642b2698b4bfe4c40ae Mon Sep 17 00:00:00 2001 From: Diogo Dourado Date: Wed, 16 Jun 2021 11:33:17 -0300 Subject: [PATCH 2/6] chore: parsing list of days --- lib/cronex/parser.ex | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/cronex/parser.ex b/lib/cronex/parser.ex index 147f52f..27cd757 100644 --- a/lib/cronex/parser.ex +++ b/lib/cronex/parser.ex @@ -22,6 +22,9 @@ defmodule Cronex.Parser do iex> Cronex.Parser.parse_regular_frequency(:wednesday, "12:00") {0, 12, :*, :*, 3} + iex> Cronex.Parser.parse_regular_frequency([:friday, :saturday]) + {0, 0, :*, :*, "5,6"} + iex> Cronex.Parser.parse_regular_frequency(:non_existing_day) :invalid @@ -56,6 +59,16 @@ defmodule Cronex.Parser do day_of_week = Enum.find_index(@days_of_week, &(&1 == frequency)) + 1 {minute, hour, :*, :*, day_of_week} + is_list(frequency) and Enum.all?(frequency, &Enum.member?(@days_of_week, &1)) -> + days_of_week = + frequency + |> Enum.map(fn freq -> + Enum.find_index(@days_of_week, &(&1 == freq)) + 1 + end) + |> Enum.join(",") + + {minute, hour, :*, :*, days_of_week} + true -> :invalid end @@ -64,7 +77,7 @@ defmodule Cronex.Parser do @doc """ Parses a given `interval`, `frequency` and `time` to a tuple. - `interval` is a function wich receives one argument and returns the remainder of the division of that argument by the given `interval` + `interval` is a function wich receives one argument and returns the remainder of the division of that argument by the given `interval` ## Example From de07510c05ec5e3f0f738ef42a7b3bff1a13d729 Mon Sep 17 00:00:00 2001 From: Diogo Dourado Date: Wed, 16 Jun 2021 11:33:41 -0300 Subject: [PATCH 3/6] chore: validate is_time for list of days --- lib/cronex/job.ex | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/cronex/job.ex b/lib/cronex/job.ex index 2c78597..afa7b1e 100644 --- a/lib/cronex/job.ex +++ b/lib/cronex/job.ex @@ -85,7 +85,7 @@ defmodule Cronex.Job do # TODO Process.alive? only works for local processes, improve this to support several nodes # Is time to run - # Job process is dead or non existing + # Job process is dead or non existing is_time(job.frequency) and (job.pid == nil or !Process.alive?(job.pid)) end @@ -93,7 +93,7 @@ defmodule Cronex.Job do raise ArgumentError, """ An invalid frequency was given when creating a job. - Check the docs to see the accepted frequency arguments. + Check the docs to see the accepted frequency arguments. """ end @@ -130,6 +130,11 @@ defmodule Cronex.Job do interval.(current_date_time().day - 1) == 0 end + # Every days of week job, check time and list of days of the week + defp is_time({minute, hour, :*, :*, days_of_week}) when is_list(days_of_week) do + Enum.any?(days_of_week, &is_time({minute, hour, :*, :*, &1})) + end + # Every week job, check time and day of the week defp is_time({minute, hour, :*, :*, day_of_week}) do current_date_time().minute == minute and current_date_time().hour == hour and From 5c26a734314d3417cda41c712965a9a6140fc203 Mon Sep 17 00:00:00 2001 From: Diogo Dourado Date: Wed, 16 Jun 2021 12:11:33 -0300 Subject: [PATCH 4/6] feat: Job.new for days list chore: parse_regular_frequency not joining to string test: job and parser tests for list of week days --- lib/cronex/job.ex | 7 +++++++ lib/cronex/parser.ex | 3 +-- mix.lock | 6 ++++-- test/cronex/job_test.exs | 26 ++++++++++++++++++++++++++ test/cronex/parser_test.exs | 10 ++++++++++ 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/lib/cronex/job.ex b/lib/cronex/job.ex index afa7b1e..dd50aab 100644 --- a/lib/cronex/job.ex +++ b/lib/cronex/job.ex @@ -46,6 +46,13 @@ defmodule Cronex.Job do |> Map.put(:task, task) end + def new(arg1, arg2, task) + when is_list(arg1) and is_bitstring(arg2) and is_function(task) do + %Cronex.Job{} + |> Map.put(:frequency, parse_regular_frequency(arg1, arg2)) + |> Map.put(:task, task) + end + @doc """ Creates a `%Job{}` with the given interval, frequency, time and task. diff --git a/lib/cronex/parser.ex b/lib/cronex/parser.ex index 27cd757..0c023fe 100644 --- a/lib/cronex/parser.ex +++ b/lib/cronex/parser.ex @@ -23,7 +23,7 @@ defmodule Cronex.Parser do {0, 12, :*, :*, 3} iex> Cronex.Parser.parse_regular_frequency([:friday, :saturday]) - {0, 0, :*, :*, "5,6"} + {0, 0, :*, :*, [5,6]} iex> Cronex.Parser.parse_regular_frequency(:non_existing_day) :invalid @@ -65,7 +65,6 @@ defmodule Cronex.Parser do |> Enum.map(fn freq -> Enum.find_index(@days_of_week, &(&1 == freq)) + 1 end) - |> Enum.join(",") {minute, hour, :*, :*, days_of_week} diff --git a/mix.lock b/mix.lock index f704a68..53c2778 100644 --- a/mix.lock +++ b/mix.lock @@ -1,2 +1,4 @@ -%{"earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []}, - "ex_doc": {:hex, :ex_doc, "0.14.3", "e61cec6cf9731d7d23d254266ab06ac1decbb7651c3d1568402ec535d387b6f7", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}} +%{ + "earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], [], "hexpm", "0fdcd651f9689e81cda24c8e5d06947c5aca69dbd8ce3d836b02bcd0c6004592"}, + "ex_doc": {:hex, :ex_doc, "0.14.3", "e61cec6cf9731d7d23d254266ab06ac1decbb7651c3d1568402ec535d387b6f7", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm", "6bf36498c4c67fdbe6d4ad73a112098cbcc09b147b859219b023fc2636729bf6"}, +} diff --git a/test/cronex/job_test.exs b/test/cronex/job_test.exs index 78aafce..d207dca 100644 --- a/test/cronex/job_test.exs +++ b/test/cronex/job_test.exs @@ -44,6 +44,13 @@ defmodule Cronex.JobTest do assert job == Job.validate!(job) end + test "returns the given job if frequency contains a list of week days" do + task = fn -> :ok end + job = Job.new([:sunday, :monday], "12:00", task) + + assert job == Job.validate!(job) + end + test "raises invalid frequency error when a job with an invalid frequency is given" do task = fn -> :ok end job = Job.new(:invalid_frequency, task) @@ -200,4 +207,23 @@ defmodule Cronex.JobTest do Test.DateTime.set(month: 1, day: 2, hour: 0, minute: 0) assert false == Cronex.Job.can_run?(job) end + + test "can_run?/1 with a list of week days job" do + task = fn -> :ok end + job = Job.new([:sunday, :monday], "12:00", task) + + # day_of_week == 0 (or 7) + Test.DateTime.set(year: 2021, month: 6, day: 13, hour: 12, minute: 0) + assert true == Cronex.Job.can_run?(job) + + # day_of_week == 1 + Test.DateTime.set(year: 2021, month: 6, day: 14, hour: 12, minute: 0) + assert true == Cronex.Job.can_run?(job) + + # days_of_week in 2..6 + Enum.each(15..19, fn day -> + Test.DateTime.set(day: day, hour: 12, minute: 0) + assert false == Cronex.Job.can_run?(job) + end) + end end diff --git a/test/cronex/parser_test.exs b/test/cronex/parser_test.exs index 2052257..92b71e8 100644 --- a/test/cronex/parser_test.exs +++ b/test/cronex/parser_test.exs @@ -19,4 +19,14 @@ defmodule Cronex.ParserTest do assert 0 == interval_fn.(8) assert 2 == interval_fn.(2) end + + test "parse_regular_frequency/2" do + assert {0, 0, :*, :*, day_frequency} = Cronex.Parser.parse_regular_frequency(:monday, "00:00") + assert 1 == day_frequency + + assert {30, 10, :*, :*, days_frequency} = + Cronex.Parser.parse_regular_frequency([:friday, :saturday], "10:30") + + assert [5, 6] == days_frequency + end end From 8db918a450f478f3c959d7f790fcbe58ba44e7cf Mon Sep 17 00:00:00 2001 From: Diogo Dourado Date: Wed, 16 Jun 2021 12:17:15 -0300 Subject: [PATCH 5/6] test: scheduler --- test/cronex/scheduler_test.exs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/test/cronex/scheduler_test.exs b/test/cronex/scheduler_test.exs index 376e3f1..48a0b34 100644 --- a/test/cronex/scheduler_test.exs +++ b/test/cronex/scheduler_test.exs @@ -22,6 +22,10 @@ defmodule Cronex.SchedulerTest do send(test_process(), {:ok, :every_friday}) end + every [:monday, :tuesday], at: "14:00" do + send(test_process(), {:ok, :every_monday_and_tuesday}) + end + every 2, :hour do send(test_process(), {:ok, :every_2_hour}) end @@ -46,7 +50,7 @@ defmodule Cronex.SchedulerTest do test "loads jobs from TestScheduler" do assert %{0 => %Job{}, 1 => %Job{}, 2 => %Job{}} = Cronex.Table.get_jobs(TestScheduler.table()) - assert 5 == Cronex.Table.get_jobs(TestScheduler.table()) |> map_size + assert 6 == Cronex.Table.get_jobs(TestScheduler.table()) |> map_size end test "TestScheduler starts table and task supervisor" do @@ -79,6 +83,22 @@ defmodule Cronex.SchedulerTest do refute_receive {:ok, :every_friday}, @timeout end + test "every monday and tuesday job runs on the expected time" do + # day_of_week == 1 + Test.DateTime.set(year: 2021, month: 6, day: 14, hour: 14, minute: 0) + assert_receive {:ok, :every_monday_and_tuesday}, @timeout + + Test.DateTime.set(hour: 15) + refute_receive {:ok, :every_monday_and_tuesday}, @timeout + + # day_of_week == 2 + Test.DateTime.set(year: 2021, month: 6, day: 15, hour: 14, minute: 0) + assert_receive {:ok, :every_monday_and_tuesday}, @timeout + + Test.DateTime.set(hour: 16) + refute_receive {:ok, :every_monday_and_tuesday}, @timeout + end + test "every 2 hour job runs on the expected time" do Test.DateTime.set(hour: 0, minute: 0) assert_receive {:ok, :every_2_hour}, @timeout From a3c51da0643bed2e8f19043ea159ac229e20f56c Mon Sep 17 00:00:00 2001 From: Diogo Dourado Date: Wed, 16 Jun 2021 12:23:30 -0300 Subject: [PATCH 6/6] test: less verbose when setting dates --- test/cronex/job_test.exs | 4 ++-- test/cronex/scheduler_test.exs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/cronex/job_test.exs b/test/cronex/job_test.exs index d207dca..4fe016f 100644 --- a/test/cronex/job_test.exs +++ b/test/cronex/job_test.exs @@ -217,12 +217,12 @@ defmodule Cronex.JobTest do assert true == Cronex.Job.can_run?(job) # day_of_week == 1 - Test.DateTime.set(year: 2021, month: 6, day: 14, hour: 12, minute: 0) + Test.DateTime.set(day: 14) assert true == Cronex.Job.can_run?(job) # days_of_week in 2..6 Enum.each(15..19, fn day -> - Test.DateTime.set(day: day, hour: 12, minute: 0) + Test.DateTime.set(day: day) assert false == Cronex.Job.can_run?(job) end) end diff --git a/test/cronex/scheduler_test.exs b/test/cronex/scheduler_test.exs index 48a0b34..921699a 100644 --- a/test/cronex/scheduler_test.exs +++ b/test/cronex/scheduler_test.exs @@ -92,10 +92,10 @@ defmodule Cronex.SchedulerTest do refute_receive {:ok, :every_monday_and_tuesday}, @timeout # day_of_week == 2 - Test.DateTime.set(year: 2021, month: 6, day: 15, hour: 14, minute: 0) + Test.DateTime.set(day: 15, hour: 14, minute: 0) assert_receive {:ok, :every_monday_and_tuesday}, @timeout - Test.DateTime.set(hour: 16) + Test.DateTime.set(hour: 15) refute_receive {:ok, :every_monday_and_tuesday}, @timeout end