diff --git a/.vscode/launch.json b/.vscode/launch.json index 4aa561c..06640e8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -27,7 +27,12 @@ "requireFiles": [ "test/**/test_helper.exs", "test/**/*_test.exs" + ], + "excludeModules": [ + "Circuits.I2C.Nif", + "Circuits.GPIO.Nif" ] + } ] } \ No newline at end of file diff --git a/TODO.md b/TODO.md index 151b589..43361d6 100644 --- a/TODO.md +++ b/TODO.md @@ -48,7 +48,7 @@ Even though this library is published, there are things I still want to do befor end end ``` - - [ ] Create support for a cron-manager (that allows running jobs, like the one we have in the weather station example and thereby no need to handcraft anything) (v0.4) + - [x] Create support for a cron-manager (that allows running jobs, like the one we have in the weather station example and thereby no need to handcraft anything) (v0.4) - [ ] Create support for a coordinator (that can control individual animations and effects. Effects will have to notify back their state) (v0.5) - [ ] Clustering - [ ] create an animation that consumes those (this allows to connect remote livebooks to a physical led strip. CAUTION: protect against loops!) (v0.5) diff --git a/lib/fledex.ex b/lib/fledex.ex index 3977d5c..9fff110 100644 --- a/lib/fledex.ex +++ b/lib/fledex.ex @@ -37,6 +37,7 @@ defmodule Fledex do @spec __using__(keyword) :: Macro.t() defmacro __using__(opts) do quote bind_quoted: [opts: opts] do + import Crontab.CronExpression import Fledex # import also the Leds and the color name definitions so no namespace are required import Fledex.Leds @@ -196,17 +197,52 @@ defmodule Fledex do example livebook](5_fledex_weather_example.livemd)): ```elixir - Fledex.Utils.PubSub.simple_broadcast(%{temperature: -15.2}) + Fledex.Utils.PubSub.simple_broadcast(%{temperature: -15.2}) + ``` + + Each job consists of: + + * `name`- a unique name + * `pattern`- a cron pattern (as specified in + [this cheatsheet](https://hexdocs.pm/crontab/cron_notation.html#expressions)). + Note: `Crontab.CronExpression` gets imported and therefore the sigil can directly + be used, i.e. `~e[* * * * * * * *]e` + * `options`- a keyword list with some options. The following options exist: + * `:run_once`- a boolean that indicates whether the job should be run once + at creation time. This can be important, because you might otherwise have + to wait for an extended time before the function will be executed. + * `:timezone`- The timezone the cron pattern applies to. If nothing is specified + `:utc` is assumed + * `:overlap`- This indicates whether jobs should overlap or not. An overlap can + happen when running the job takes more time than the interval between job runs. + For safety reason the default is `false`. + * `:do` - a block of code that should be executed. You can specify directly + your code here. It will be wrapped into an anonymous function. + + Example: + ```elixir + use Fledex + led_strip :nested_components2, :kino do + job :clock, ~e[@secondly]e do + date_time = DateTime.utc_now() + + Fledex.Utils.PubSub.simple_broadcast(%{ + clock_hour: date_time.hour, + clock_minute: date_time.minute, + clock_second: date_time.second + }) + end + end ``` """ - defmacro job(name, pattern, do: block) do - # IO.puts("#{inspect name}, #{inspect pattern}, #{inspect block}") + defmacro job(name, pattern, options \\ [], do: block) do ast_func = Dsl.ast_create_anonymous_func([], block) - # ast_func = {:fn, [], block} + quote do Dsl.create_job( unquote(name), unquote(pattern), + unquote(options), unquote(ast_func) ) end diff --git a/lib/fledex/animation/animator.ex b/lib/fledex/animation/animator.ex index ef29597..ef86013 100644 --- a/lib/fledex/animation/animator.ex +++ b/lib/fledex/animation/animator.ex @@ -43,13 +43,13 @@ defmodule Fledex.Animation.Animator do Both of them can be set by defining an appropriate function and setting and resetting a reference at will This module does not define any functions on its own, because the interface is defined - by `Fledex.Animation.Base`. + by `Fledex.Animation.AnimatorBase`. """ - use Fledex.Animation.Base + use Fledex.Animation.AnimatorBase require Logger - alias Fledex.Animation.Base + alias Fledex.Animation.AnimatorBase alias Fledex.Leds alias Fledex.LedStrip alias Fledex.Utils.PubSub @@ -82,8 +82,8 @@ defmodule Fledex.Animation.Animator do state = %{ triggers: %{}, type: :animation, - def_func: &Base.default_def_func/1, - options: [send_config: &Base.default_send_config_func/1], + def_func: &AnimatorBase.default_def_func/1, + options: [send_config: &AnimatorBase.default_send_config_func/1], effects: [], strip_name: strip_name, animation_name: animation_name @@ -137,7 +137,7 @@ defmodule Fledex.Animation.Animator do } = state ) do # IO.puts("Update_Leds1: Key: #{Keyword.has_key?(options, :send_config_func)}") - send_config_func = options[:send_config] || (&Base.default_send_config_func/1) + send_config_func = options[:send_config] || (&AnimatorBase.default_send_config_func/1) # IO.puts("Options: #{inspect options}") # this is for compatibility reasons. if only a send_config_func is defined # in the options list, then no options are defined. In that case we need to define @@ -200,7 +200,7 @@ defmodule Fledex.Animation.Animator do %{ type: config[:type] || state.type, triggers: Map.merge(state.triggers, config[:triggers] || state[:triggers]), - def_func: Map.get(config, :def_func, &Base.default_def_func/1), + def_func: Map.get(config, :def_func, &AnimatorBase.default_def_func/1), options: update_options(config[:options], config[:send_config_func]), effects: update_effects(state.effects, config[:effects], state.strip_name), # not to be updated diff --git a/lib/fledex/animation/base.ex b/lib/fledex/animation/animator_base.ex similarity index 71% rename from lib/fledex/animation/base.ex rename to lib/fledex/animation/animator_base.ex index 0a7ca42..eca7d21 100644 --- a/lib/fledex/animation/base.ex +++ b/lib/fledex/animation/animator_base.ex @@ -2,26 +2,26 @@ # # SPDX-License-Identifier: Apache-2.0 -defmodule Fledex.Animation.Base do - alias Fledex.Animation.Interface +defmodule Fledex.Animation.AnimatorBase do + alias Fledex.Animation.AnimatorInterface alias Fledex.Leds defmacro __using__(opts) do quote location: :keep, bind_quoted: [opts: opts] do use GenServer, opts - @behaviour Fledex.Animation.Interface + @behaviour Fledex.Animation.AnimatorInterface - alias Fledex.Animation.Base - alias Fledex.Animation.Interface + alias Fledex.Animation.AnimatorBase + alias Fledex.Animation.AnimatorInterface - # client side + # MARK: client side @doc false @spec start_link(config :: config_t, strip_name :: atom, animation_name :: atom) :: GenServer.on_start() def start_link(config, strip_name, animation_name) do {:ok, _pid} = GenServer.start_link(__MODULE__, {config, strip_name, animation_name}, - name: Interface.build_name(strip_name, :animation, animation_name) + name: AnimatorInterface.build_name(strip_name, :animator, animation_name) ) end @@ -29,7 +29,7 @@ defmodule Fledex.Animation.Base do @spec config(atom, atom, config_t) :: :ok def config(strip_name, animation_name, config) do GenServer.cast( - Interface.build_name(strip_name, :animation, animation_name), + AnimatorInterface.build_name(strip_name, :animator, animation_name), {:config, config} ) end @@ -37,12 +37,15 @@ defmodule Fledex.Animation.Base do @doc false @spec shutdown(atom, atom) :: :ok def shutdown(strip_name, animation_name) do - GenServer.stop(Interface.build_name(strip_name, :animation, animation_name), :normal) + GenServer.stop( + AnimatorInterface.build_name(strip_name, :animator, animation_name), + :normal + ) end defoverridable start_link: 3, config: 3, shutdown: 2 - # server side + # MARK: server side @impl GenServer @spec handle_call(:info, {pid, any}, state_t) :: {:reply, {:ok, map}, state_t} def handle_call(:info, _from, state) do diff --git a/lib/fledex/animation/interface.ex b/lib/fledex/animation/animator_interface.ex similarity index 87% rename from lib/fledex/animation/interface.ex rename to lib/fledex/animation/animator_interface.ex index 0f42825..44abb55 100644 --- a/lib/fledex/animation/interface.ex +++ b/lib/fledex/animation/animator_interface.ex @@ -2,14 +2,14 @@ # # SPDX-License-Identifier: Apache-2.0 -defmodule Fledex.Animation.Interface do +defmodule Fledex.Animation.AnimatorInterface do @moduledoc """ - The behaviour for animations. + The behaviour for an Animator. This behaviour is the interface expected by the `Fledex.Animation.Manager` and should be implemented as a GenServer. If you implement an animation you will have to implement those functions - but you can use `Fledex.Animation.Base` to assist you. + but you can use `Fledex.Animation.AnimatorBase` to assist you. """ @doc """ @@ -45,9 +45,9 @@ defmodule Fledex.Animation.Interface do animators that have been removed. We do not keep a reference, but only a config Therefore the animator needs to adhere to this naming convention to properly be shut down. It is the responsibility of the Animator to set the servername correctly. The - `Fledex.Animation.Base` is doing this by default. + `Fledex.Animation.AnimatorBase` is doing this by default. """ - @spec build_name(atom, :animation | :job | :coordinator, atom) :: atom + @spec build_name(atom, :animator | :job | :coordinator, atom) :: atom def build_name(strip_name, type, animation_name) when is_atom(strip_name) and is_atom(animation_name) do Module.concat(Module.concat(strip_name, type), animation_name) diff --git a/lib/fledex/animation/job_scheduler.ex b/lib/fledex/animation/job_scheduler.ex index 99be627..0651ddd 100644 --- a/lib/fledex/animation/job_scheduler.ex +++ b/lib/fledex/animation/job_scheduler.ex @@ -3,12 +3,23 @@ # SPDX-License-Identifier: Apache-2.0 # MARK: Quantum defmodule Fledex.Animation.JobScheduler do + @callback start_link() :: + {:ok, pid} + | {:error, {:already_started, pid}} + | {:error, term} + @callback stop() :: :ok + @callback new_job() :: Quantum.Job.t() + @callback add_job(Quantum.Job.t() | {Crontab.CronExpression.t(), Job.task()}) :: + :ok + @callback run_job(atom) :: :ok + @callback delete_job(atom) :: :ok + use Quantum, otp_app: __MODULE__ @impl true def config(opts \\ []) do Quantum.scheduler_config(opts, __MODULE__, __MODULE__) - |> Keyword.put(:debug_logging, false) + |> Keyword.put(:debug_logging, false) end end diff --git a/lib/fledex/animation/manager.ex b/lib/fledex/animation/manager.ex index 4feb41d..8bc2dad 100644 --- a/lib/fledex/animation/manager.ex +++ b/lib/fledex/animation/manager.ex @@ -24,7 +24,7 @@ defmodule Fledex.Animation.Manager do require Logger alias Fledex.Animation.Animator - alias Fledex.Animation.Interface + alias Fledex.Animation.AnimatorInterface alias Fledex.Animation.JobScheduler alias Fledex.LedStrip alias Quantum.Job @@ -36,7 +36,8 @@ defmodule Fledex.Animation.Manager do @typep state_t :: %{ animations: map(), coordinators: map(), - jobs: map() + jobs: map(), + impl: map() } # def child_spec(args) do @@ -110,7 +111,7 @@ defmodule Fledex.Animation.Manager do ### MARK: server side @impl GenServer @spec init(keyword) :: {:ok, state_t} - def init(_opts) do + def init(opts) do # children = [ # {DynamicSupervisor, strategy: :one_for_one, name: Manager.LedStrips}, # {DynamicSupervisor, strategy: :one_for_one, name: Manager.Animations}, @@ -118,14 +119,19 @@ defmodule Fledex.Animation.Manager do # Job # ] # Supervisor.start_link(children, strategy: :one_for_one) - JobScheduler.start_link() - state = %{ animations: %{}, coordinators: %{}, - jobs: %{} + jobs: %{}, + impls: %{ + job_scheduler: Keyword.get(opts, :job_scheduler, JobScheduler), + animator: Keyword.get(opts, :animator, Animator), + led_strip: Keyword.get(opts, :led_strip, LedStrip) + } } + state.impls.job_scheduler.start_link() + {:ok, state} end @@ -160,7 +166,7 @@ defmodule Fledex.Animation.Manager do {:reply, :ok, state} rescue - RuntimeError -> {:reply, {:error, "Animator is wrongly configured"}, state} + e in RuntimeError -> {:reply, {:error, e.message}, state} end @spec handle_call({:unregister_strip, atom}, GenServer.from(), state_t) :: @@ -174,9 +180,12 @@ defmodule Fledex.Animation.Manager do def terminate(_reason, state) do strip_names = Map.keys(state.animations) - Enum.reduce(strip_names, state, fn strip_name, state -> - unregister_strip(state, strip_name) - end) + state = + Enum.reduce(strip_names, state, fn strip_name, state -> + unregister_strip(state, strip_name) + end) + + state.impls.job_scheduler.stop() end ### MARK: private functions @@ -204,7 +213,7 @@ defmodule Fledex.Animation.Manager do @spec register_strip(state_t, atom, atom | map) :: state_t defp register_strip(state, strip_name, driver_config) do # Logger.info("registering led_strip: #{strip_name}") - {:ok, _pid} = LedStrip.start_link(strip_name, driver_config) + {:ok, _pid} = state.impls.led_strip.start_link(strip_name, driver_config) %{ state @@ -217,9 +226,14 @@ defmodule Fledex.Animation.Manager do @spec unregister_strip(state_t, atom) :: state_t defp unregister_strip(state, strip_name) do # Logger.info("unregistering led_strip_ #{strip_name}") - shutdown_coordinators(strip_name, Map.keys(state.coordinators[strip_name] || %{})) - shutdown_jobs(strip_name, Map.keys(state.jobs[strip_name] || %{})) - shutdown_animators(strip_name, Map.keys(state.animations[strip_name] || %{})) + shutdown_coordinators( + state.impls, + strip_name, + Map.keys(state.coordinators[strip_name] || %{}) + ) + + shutdown_jobs(state.impls, strip_name, Map.keys(state.jobs[strip_name] || %{})) + shutdown_animators(state.impls, strip_name, Map.keys(state.animations[strip_name] || %{})) LedStrip.stop(strip_name) %{ @@ -241,31 +255,31 @@ defmodule Fledex.Animation.Manager do {dropped, updated, created} = filter_configs(Map.get(state.animations, strip_name), configs) # Logger.info("#{inspect dropped}, #{inspect present}") - shutdown_animators(strip_name, dropped) - update_animators(strip_name, updated) - create_animators(strip_name, created) + shutdown_animators(state.impls, strip_name, dropped) + update_animators(state.impls, strip_name, updated) + create_animators(state.impls, strip_name, created) %{state | animations: Map.put(state.animations, strip_name, configs)} end - @spec shutdown_animators(atom, [atom]) :: :ok - defp shutdown_animators(strip_name, dropped_animations) do + @spec shutdown_animators(%{atom => module}, atom, [atom]) :: :ok + defp shutdown_animators(_impls, strip_name, dropped_animations) do Enum.each(dropped_animations, fn animation_name -> - GenServer.stop(Interface.build_name(strip_name, :animation, animation_name), :normal) + GenServer.stop(AnimatorInterface.build_name(strip_name, :animator, animation_name), :normal) end) end - @spec update_animators(atom, map) :: :ok - defp update_animators(strip_name, animations) do + @spec update_animators(%{atom => module}, atom, map) :: :ok + defp update_animators(impls, strip_name, animations) do Enum.each(animations, fn {animation_name, config} -> - Animator.config(strip_name, animation_name, config) + impls.animator.config(strip_name, animation_name, config) end) end - @spec create_animators(atom, map) :: :ok - defp create_animators(strip_name, created_animations) do + @spec create_animators(%{atom => module}, atom, map) :: :ok + defp create_animators(impls, strip_name, created_animations) do Enum.each(created_animations, fn {animation_name, config} -> - {:ok, _pid} = Animator.start_link(config, strip_name, animation_name) + {:ok, _pid} = impls.animator.start_link(config, strip_name, animation_name) end) end @@ -298,42 +312,45 @@ defmodule Fledex.Animation.Manager do state end - defp shutdown_coordinators(_strip_name, _coordinator_names) do + defp shutdown_coordinators(_impls, _strip_name, _coordinator_names) do end defp register_jobs(state, strip_name, jobs) do {dropped, updated, created} = filter_configs(Map.get(state.jobs, strip_name), jobs) - shutdown_jobs(strip_name, dropped) - update_jobs(strip_name, updated) - create_jobs(strip_name, created) + shutdown_jobs(state.impls, strip_name, dropped) + update_jobs(state.impls, strip_name, updated) + create_jobs(state.impls, strip_name, created) %{state | jobs: Map.put(state.jobs, strip_name, jobs)} end - defp shutdown_jobs(_strip_name, job_names) do + defp shutdown_jobs(impls, _strip_name, job_names) do Enum.each(job_names, fn job_name -> - JobScheduler.delete_job(job_name) + impls.job_scheduler.delete_job(job_name) end) end - def update_jobs(strip_name, jobs) do + defp update_jobs(impls, strip_name, jobs) do Enum.each(jobs, fn {job, job_config} -> - JobScheduler.delete_job(job) - JobScheduler.add_job(convert(job, job_config, strip_name)) + impls.job_scheduler.delete_job(job) + impls.job_scheduler.add_job(convert(impls, job, job_config, strip_name)) end) end - def create_jobs(strip_name, jobs) do + defp create_jobs(impls, strip_name, jobs) do Enum.each(jobs, fn {job, job_config} -> - JobScheduler.add_job(convert(job, job_config, strip_name)) + impls.job_scheduler.add_job(convert(impls, job, job_config, strip_name)) + if Keyword.get(job_config.options, :run_once, false), do: impls.job_scheduler.run_job(job) end) end - defp convert(job, job_config, _strip_name) do - JobScheduler.new_job() + defp convert(impls, job, job_config, _strip_name) do + impls.job_scheduler.new_job() |> Job.set_name(job) |> Job.set_schedule(job_config.pattern) |> Job.set_task(job_config.func) + |> Job.set_timezone(Keyword.get(job_config.options, :timezone, :utc)) + |> Job.set_overlap(Keyword.get(job_config.options, :overlap, false)) end end diff --git a/lib/fledex/component/clock.ex b/lib/fledex/component/clock.ex index 0c0cfc3..fbc3d48 100644 --- a/lib/fledex/component/clock.ex +++ b/lib/fledex/component/clock.ex @@ -7,7 +7,7 @@ defmodule Fledex.Component.Clock do alias Fledex.Component.Dot defp create_name(base, child) do - String.to_atom("#{inspect base}_#{inspect child}") + String.to_atom("#{inspect(base)}_#{inspect(child)}") end @impl true @@ -47,6 +47,7 @@ defmodule Fledex.Component.Clock do defp split_trigger({hour, minute} = _trigger_name), do: {hour, minute, nil} defp split_trigger({_hour, _minute, _second} = trigger_name), do: trigger_name + defp split_trigger(trigger_name) when is_atom(trigger_name) do { create_name(trigger_name, :hour), diff --git a/lib/fledex/utils/dsl.ex b/lib/fledex/utils/dsl.ex index a6a3fd6..3b2c537 100644 --- a/lib/fledex/utils/dsl.ex +++ b/lib/fledex/utils/dsl.ex @@ -98,11 +98,12 @@ defmodule Fledex.Utils.Dsl do end end - def create_job(name, pattern, function) do + def create_job(name, pattern, options, function) do %{ name => %{ type: :job, pattern: pattern, + options: options, func: function } } diff --git a/livebooks/9_fledex.jobs.livemd b/livebooks/9_fledex.jobs.livemd index 569ab44..1ccf5e4 100644 --- a/livebooks/9_fledex.jobs.livemd +++ b/livebooks/9_fledex.jobs.livemd @@ -1,3 +1,8 @@ + # 9. Fledex: Job ```elixir @@ -24,7 +29,6 @@ The example here implements our clock example with the `Fledex.Component.Clock` ```elixir alias Fledex.Component.Clock -import Crontab.CronExpression led_strip :nested_components2, :kino do component(:clock, Clock, trigger_name: {:clock_hour, :clock_minute, :clock_second}) diff --git a/mix.exs b/mix.exs index 1bed1da..bfcc870 100644 --- a/mix.exs +++ b/mix.exs @@ -91,6 +91,7 @@ defmodule Fledex.MixProject do # testing {:circuits_sim, "~> 0.1.0", only: [:dev, :test]}, + {:mox, "~> 1.1"}, # documentation {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, diff --git a/mix.lock b/mix.lock index 3d37c21..f5f3263 100644 --- a/mix.lock +++ b/mix.lock @@ -23,6 +23,7 @@ "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, + "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "quantum": {:hex, :quantum, "3.5.3", "ee38838a07761663468145f489ad93e16a79440bebd7c0f90dc1ec9850776d99", [:mix], [{:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.14 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.2", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "500fd3fa77dcd723ed9f766d4a175b684919ff7b6b8cfd9d7d0564d58eba8734"}, diff --git a/test/fledex/animation/animator_base_test.exs b/test/fledex/animation/animator_base_test.exs new file mode 100644 index 0000000..2af2067 --- /dev/null +++ b/test/fledex/animation/animator_base_test.exs @@ -0,0 +1,28 @@ +# Copyright 2023, Matthias Reik +# +# SPDX-License-Identifier: Apache-2.0 + +defmodule Fledex.Animation.AnimatorBaseTest do + use ExUnit.Case + + alias Fledex.Animation.AnimatorBase + alias Fledex.Animation.AnimatorInterface + alias Fledex.Leds + + describe "util functions" do + test "ensure correct naming" do + assert AnimatorInterface.build_name(:testA, :animator, :testB) == + :"Elixir.testA.animator.testB" + end + + test "default_def_func" do + assert AnimatorBase.default_def_func(%{}) == Leds.leds() + assert AnimatorBase.default_def_func(%{trigger_name: 10}) == Leds.leds() + end + + test "default_send_func" do + assert AnimatorBase.default_send_config_func(%{}) == %{} + assert AnimatorBase.default_send_config_func(%{trigger_name: 10}) == %{} + end + end +end diff --git a/test/fledex/animation/animator_test.exs b/test/fledex/animation/animator_test.exs index 5d1f74b..88072e5 100644 --- a/test/fledex/animation/animator_test.exs +++ b/test/fledex/animation/animator_test.exs @@ -9,7 +9,7 @@ defmodule Fledex.Animation.AnimatorTest do require Logger alias Fledex.Animation.Animator - alias Fledex.Animation.Interface + alias Fledex.Animation.AnimatorInterface alias Fledex.Effect.Rotation alias Fledex.Effect.Wanish alias Fledex.Leds @@ -307,7 +307,7 @@ defmodule Fledex.Animation.AnimatorTest do animation_name = :animation_testB {:ok, pid} = Animator.start_link(%{type: :static}, strip_name, animation_name) assert Process.alive?(pid) - GenServer.stop(Interface.build_name(strip_name, :animation, animation_name), :normal) + GenServer.stop(AnimatorInterface.build_name(strip_name, :animator, animation_name), :normal) assert not Process.alive?(pid) GenServer.stop(driver, :normal) end diff --git a/test/fledex/animation/base_test.exs b/test/fledex/animation/base_test.exs deleted file mode 100644 index 40f266c..0000000 --- a/test/fledex/animation/base_test.exs +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2023, Matthias Reik -# -# SPDX-License-Identifier: Apache-2.0 - -defmodule Fledex.Animation.BaseTest do - use ExUnit.Case - - alias Fledex.Animation.Base - alias Fledex.Animation.Interface - alias Fledex.Leds - - describe "util functions" do - test "ensure correct naming" do - assert Interface.build_name(:testA, :animation, :testB) == :"Elixir.testA.animation.testB" - end - - test "default_def_func" do - assert Base.default_def_func(%{}) == Leds.leds() - assert Base.default_def_func(%{trigger_name: 10}) == Leds.leds() - end - - test "default_send_func" do - assert Base.default_send_config_func(%{}) == %{} - assert Base.default_send_config_func(%{trigger_name: 10}) == %{} - end - end -end diff --git a/test/fledex/animation/manager_test.exs b/test/fledex/animation/manager_test.exs index 4e34c74..61a9848 100644 --- a/test/fledex/animation/manager_test.exs +++ b/test/fledex/animation/manager_test.exs @@ -7,6 +7,7 @@ defmodule Fledex.Animation.ManagerTest do alias Fledex.Animation.Manager alias Fledex.ManagerTestUtils + alias Quantum @strip_name :test_strip setup do @@ -72,7 +73,7 @@ defmodule Fledex.Animation.ManagerTest do assert config == ManagerTestUtils.get_manager_config(strip_name) Enum.each(Map.keys(config), fn key -> - assert GenServer.whereis(String.to_atom("Elixir.#{strip_name}.#{:animation}.#{key}")) != + assert GenServer.whereis(String.to_atom("Elixir.#{strip_name}.#{:animator}.#{key}")) != nil end) end @@ -84,7 +85,7 @@ defmodule Fledex.Animation.ManagerTest do } Manager.register_config(strip_name, config) - assert GenServer.whereis(String.to_atom("Elixir.#{strip_name}.#{:animation}.#{:t2}")) != nil + assert GenServer.whereis(String.to_atom("Elixir.#{strip_name}.#{:animator}.#{:t2}")) != nil config2 = %{ t1: %{type: :animation}, @@ -94,51 +95,140 @@ defmodule Fledex.Animation.ManagerTest do Manager.register_config(strip_name, config2) assert config2 == ManagerTestUtils.get_manager_config(strip_name) - assert GenServer.whereis(String.to_atom("Elixir.#{strip_name}.#{:animation}.#{:t2}")) == nil + assert GenServer.whereis(String.to_atom("Elixir.#{strip_name}.#{:animator}.#{:t2}")) == nil Enum.each(Map.keys(config2), fn key -> - assert GenServer.whereis(String.to_atom("Elixir.#{strip_name}.#{:animation}.#{key}")) != + assert GenServer.whereis(String.to_atom("Elixir.#{strip_name}.#{:animator}.#{key}")) != nil end) end end -end -defmodule Fledex.Animation.ManagerTest2 do - defmodule TestAnimator do - @type config_t :: map - @type state_t :: map - use Fledex.Animation.Base - @impl true - def start_link(_config, _strip_name, _animation_name) do - pid = Process.spawn(fn -> Process.sleep(1_000) end, [:link]) - Process.register(pid, :hello) - {:ok, pid} - end + describe "test jobs" do + import Mox + defmock(Fledex.MockJobScheduler1, for: Fledex.Animation.JobScheduler) + + test "add job" do + Fledex.MockJobScheduler1 + |> expect(:new_job, fn -> + Quantum.Job.new(Quantum.scheduler_config([], Fledex.MockJobScheduler1, JobScheduler)) + end) + |> expect(:add_job, fn _job -> :ok end) + + use Fledex + + config = + led_strip :john, :config do + job :timer, ~e[* * * * * * *]e do + :ok + end + end + + state = %{ + jobs: %{}, + animations: %{}, + coordinators: %{}, + impls: %{ + job_scheduler: Fledex.MockJobScheduler1, + led_strip: Fledex.LedStrip + } + } + + {:reply, :ok, state} = Manager.handle_call({:register_strip, :john, :none}, self(), state) - @impl true - def config(_strip_name, _animation_name, _config) do - :ok + {:reply, :ok, _state} = + Manager.handle_call({:register_config, :john, config}, self(), state) + + Mox.verify!() end - @impl true - def shutdown(_strip_name, _animation_name) do - :ok + test "update job" do + Fledex.MockJobScheduler1 + |> expect(:new_job, fn -> + Quantum.Job.new(Quantum.scheduler_config([], Fledex.MockJobScheduler1, JobScheduler)) + end) + |> expect(:delete_job, fn _name -> :ok end) + |> expect(:add_job, fn _job -> :ok end) + + use Fledex + + config = + led_strip :john, :config do + job :timer, ~e[1 * * * * * *]e do + :ok + end + end + + state = %{ + animations: %{john: %{}}, + coordinators: %{john: %{}}, + jobs: %{ + john: %{ + timer: %{ + type: :job, + pattern: ~e[* * * * * * *]e, + options: [], + func: fn -> :ok end + } + } + }, + impls: %{ + job_scheduler: Fledex.MockJobScheduler1, + led_strip: Fledex.LedStrip + } + } + + {:reply, :ok, _state} = + Manager.handle_call({:register_config, :john, config}, self(), state) + + Mox.verify!() end - @impl true - def init(init_arg) do - {:ok, init_arg} + test "delete job" do + Fledex.MockJobScheduler1 + |> expect(:delete_job, fn _name -> :ok end) + + use Fledex + + config = + led_strip :john, :config do + end + + state = %{ + animations: %{john: %{}}, + coordinators: %{john: %{}}, + jobs: %{ + john: %{ + timer: %{ + type: :job, + pattern: ~e[* * * * * * *]e, + options: [], + func: fn -> :ok end + } + } + }, + impls: %{ + job_scheduler: Fledex.MockJobScheduler1, + led_strip: Fledex.LedStrip + } + } + + {:reply, :ok, _state} = + Manager.handle_call({:register_config, :john, config}, self(), state) + + Mox.verify!() end end +end +defmodule Fledex.Animation.ManagerTest2 do use ExUnit.Case alias Fledex.Animation.Manager - describe "Animation with wrong name" do - test "register animation with a broken server_name" do - {:ok, pid} = Manager.start_link(%{test: TestAnimator}) + describe "Animation with wrong type" do + test "register animation with a broken animation type" do + {:ok, pid} = Manager.start_link() :ok = Manager.register_strip(:some_strip, :none) config = %{ @@ -149,7 +239,7 @@ defmodule Fledex.Animation.ManagerTest2 do response = Manager.register_config(:some_strip, config) - assert response == {:error, "Animator is wrongly configured"} + assert response == {:error, "An unknown type was encountered #{inspect(config)}"} Process.exit(pid, :normal) end end diff --git a/test/fledex_test.exs b/test/fledex_test.exs index 015b729..f9ba20d 100644 --- a/test/fledex_test.exs +++ b/test/fledex_test.exs @@ -18,8 +18,12 @@ defmodule Fledex.Test do use Fledex assert GenServer.whereis(Manager) != nil - # and check that Fledex, Fledex.Leds and Fledex.Color.Names are imported - # Testing for Fledex itself is difficult, since it only defines macros. + # and check that Crontab.CronExpression, Fledex, Fledex.Leds and Fledex.Color.Names + # are imported: + # from Crontab.CronExpression + assert :erlang.fun_info(&sigil_e/2) + # from Fledex + assert :erlang.fun_info(&component/3) # from Fledex.Leds assert :erlang.fun_info(&leds/1) # from Fledex.Color.Names diff --git a/test/support/manager_test_utils.ex b/test/support/manager_test_utils.ex index 3764f17..7e9df06 100644 --- a/test/support/manager_test_utils.ex +++ b/test/support/manager_test_utils.ex @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 defmodule Fledex.ManagerTestUtils do - alias Fledex.Animation.Interface + alias Fledex.Animation.AnimatorInterface alias Fledex.Animation.Manager def get_manager_config, do: get_manager_config(:animations, :all) @@ -30,7 +30,7 @@ defmodule Fledex.ManagerTestUtils do # def get_manager_config, do: get_manager_config(:animations, :all) # def get_manager_config(strip), do: get_manager_config(:animations, strip) def get_animator_config(strip_name, animation_name) do - pid = GenServer.whereis(Interface.build_name(strip_name, :animation, animation_name)) + pid = GenServer.whereis(AnimatorInterface.build_name(strip_name, :animator, animation_name)) :sys.get_state(pid) end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 20b7397..91a8716 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,7 +1,6 @@ # Copyright 2023, Matthias Reik # # SPDX-License-Identifier: Apache-2.0 - ExUnit.start( # capture_log: true )