Repository: quantum-elixir/quantum-core (elixir)

Issue:
Check the existence of modules and functions in the jobs configuration at startup
I noticed that with non-existing modules or functions configured in jobs, Quantom doesn't give any errors or warnings on startup. For static jos configs, should they be checked for validity?

Pull request description:
Solution: add a warning message and remove jobs with invalid tasks
Hello!

**Proposal**:

At Quantum startup, when detected that a `mfa` `task` is not a  valid function, ignore the `job` configuration and show a `warning` message.

closes #507

Patch:
diff --git a/lib/quantum.ex b/lib/quantum.ex
index 5f1bb34..3925fa2 100644
--- a/lib/quantum.ex
+++ b/lib/quantum.ex
@@ -307,29 +307,46 @@ defmodule Quantum do
       Keyword.update(config, :jobs, [], fn jobs ->
         jobs
         |> Enum.map(&Normalizer.normalize(scheduler.__new_job__([], config), &1))
-        |> remove_jobs_with_duplicate_names(scheduler)
+        |> remove_invalid_jobs(scheduler)
       end)
     end)
     |> Keyword.put_new(:supervisor_module, Quantum.Supervisor)
     |> Keyword.put_new(:name, Quantum.Supervisor)
   end
 
-  defp remove_jobs_with_duplicate_names(job_list, scheduler) do
+  defp remove_invalid_jobs(job_list, scheduler) do
     job_list
     |> Enum.reduce(%{}, fn %Job{name: name} = job, acc ->
-      if Enum.member?(Map.keys(acc), name) do
-        Logger.warn(
-          "Job with name '#{name}' of scheduler '#{scheduler}' not started due to duplicate job name"
-        )
-
-        acc
-      else
-        Map.put_new(acc, name, job)
+      cond do
+        duplicate_job?(Map.keys(acc), job) ->
+          Logger.warn(
+            "Job with name '#{name}' of scheduler '#{scheduler}' not started: duplicate job name"
+          )
+
+          acc
+
+        invalid_job_task?(job) ->
+          Logger.warn(
+            "Job with name '#{name}' of scheduler '#{scheduler}' not started: invalid task function"
+          )
+
+          acc
+
+        :else ->
+          Map.put_new(acc, name, job)
       end
     end)
     |> Map.values()
   end
 
+  defp duplicate_job?(existent_jobs, %Job{name: name}), do: Enum.member?(existent_jobs, name)
+
+  defp invalid_job_task?(%Job{task: {m, f, args}})
+       when is_atom(m) and is_atom(f) and is_list(args),
+       do: not function_exported?(m, f, length(args))
+
+  defp invalid_job_task?(_), do: false
+
   defmacro __using__(opts) do
     quote bind_quoted: [behaviour: __MODULE__, opts: opts, moduledoc: @moduledoc],
           location: :keep do


Test patch:
diff --git a/test/quantum_startup_test.exs b/test/quantum_startup_test.exs
index 54acbb1..9cc4abc 100644
--- a/test/quantum_startup_test.exs
+++ b/test/quantum_startup_test.exs
@@ -35,4 +35,27 @@ defmodule QuantumStartupTest do
       :ok = stop_supervised(Scheduler)
     end)
   end
+
+  @tag :startup
+  test "prevent unexported functions on startup" do
+    log =
+      capture_log(fn ->
+        test_jobs = [
+          {:existing_function, [schedule: ~e[2 * * * *], task: {IO, :puts, ["hey"]}]},
+          {:another_existing_function, [schedule: ~e[2 * * * *], task: {Kernel, :floor, [5.4]}]},
+          {:inexistent_function,
+           [schedule: ~e[2 * * * *], task: {UndefinedModule, :undefined_function, ["argument"]}]}
+        ]
+
+        Application.put_env(:quantum, QuantumStartupTest.Scheduler, jobs: test_jobs)
+
+        start_supervised!(Scheduler)
+
+        assert Enum.count(QuantumStartupTest.Scheduler.jobs()) == 2
+        :ok = stop_supervised(Scheduler)
+      end)
+
+    assert log =~
+             "Job with name 'inexistent_function' of scheduler 'Elixir.QuantumStartupTest.Scheduler' not started: invalid task function"
+  end
 end