diff --git a/docs/index.md b/docs/index.md index 8a3e308f..8538368c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,6 +6,30 @@ This document will act as an index to available documentation. * [FarmBot Source Code common terms](/docs/glossary.md) +## Cheat Sheet + +**Create a *.fw file from local repo (RPi Zero):** + +```sh +NERVES_SYSTEM=farmbot_system_rpi MIX_TARGET=rpi mix deps.get +NERVES_SYSTEM=farmbot_system_rpi MIX_TARGET=rpi mix firmware +sudo fwup farmbot_os/_build/rpi/rpi_dev/nerves/images/farmbot.fw +``` + +**Create a *.fw file from local repo (RPi v3):** + +```sh +NERVES_SYSTEM=farmbot_system_rpi3 MIX_TARGET=rpi3 mix deps.get +NERVES_SYSTEM=farmbot_system_rpi3 MIX_TARGET=rpi3 mix firmware +sudo fwup farmbot_os/_build/rpi3/rpi3_dev/nerves/images/farmbot.fw +``` + +**Create or Update the Nerves System:** + +Please see the official [Nerves documentation on "Nerves Systems"](https://hexdocs.pm/nerves/0.4.0/systems.html). + +HINT: You may want to [develop the system locally](https://stackoverflow.com/a/28189056/1064917) + ## Hardware specifics Most FarmBot development/testing is done on a standard desktop PC. diff --git a/farmbot_celery_script/.gitignore b/farmbot_celery_script/.gitignore index e4563e5a..0ba3093a 100644 --- a/farmbot_celery_script/.gitignore +++ b/farmbot_celery_script/.gitignore @@ -25,3 +25,4 @@ farmbot_ng-*.tar *.sqlite3 *.so *.hex +*.coverdata \ No newline at end of file diff --git a/farmbot_celery_script/lib/farmbot_celery_script/ast/factory.ex b/farmbot_celery_script/lib/farmbot_celery_script/ast/factory.ex index 39aef2b4..e4a4f4f9 100644 --- a/farmbot_celery_script/lib/farmbot_celery_script/ast/factory.ex +++ b/farmbot_celery_script/lib/farmbot_celery_script/ast/factory.ex @@ -5,10 +5,35 @@ defmodule FarmbotCeleryScript.AST.Factory do alias FarmbotCeleryScript.AST + @doc """ + Create an empty AST WITH ARG SET TO `nil`. + + iex> new() + %FarmbotCeleryScript.AST{ + args: nil, + body: [], + comment: nil, + kind: nil, + meta: nil + } + """ def new do %AST{body: []} end + @doc """ + Create a new AST to work with. Strings `kind`s are + converted to symbols. + + iex> new("foo") + %FarmbotCeleryScript.AST{ + args: %{}, + body: [], + comment: nil, + kind: :foo, + meta: nil + } + """ def new(kind, args \\ %{}, body \\ []) do AST.new(kind, Map.new(args), body) end @@ -24,59 +49,148 @@ defmodule FarmbotCeleryScript.AST.Factory do ) end + @doc """ + iex> (new() |> rpc_request("x") |> set_pin_io_mode(13, 1)).body + [%FarmbotCeleryScript.AST{ + kind: :set_pin_io_mode, + args: %{ pin_io_mode: 1, pin_number: 13 }, + body: [], + comment: nil, + meta: nil + }] + """ def set_pin_io_mode(%AST{} = ast, pin_number, pin_io_mode) do - ast - |> add_body_node( - new(:set_pin_io_mode, %{pin_number: pin_number, pin_io_mode: pin_io_mode}) - ) + args = %{pin_number: pin_number, pin_io_mode: pin_io_mode} + ast |> add_body_node(new(:set_pin_io_mode, args)) end + @doc """ + iex> (new() |> rpc_request("x") |> emergency_lock()).body + [%FarmbotCeleryScript.AST{ + body: [], + comment: nil, + meta: nil, + args: %{}, + kind: :emergency_lock + }] + """ def emergency_lock(%AST{} = ast) do - ast - |> add_body_node(new(:emergency_lock)) + ast |> add_body_node(new(:emergency_lock)) end + @doc """ + iex> (new() |> rpc_request("x") |> emergency_unlock()).body + [%FarmbotCeleryScript.AST{ + body: [], + comment: nil, + meta: nil, + args: %{}, + kind: :emergency_unlock + }] + """ def emergency_unlock(%AST{} = ast) do - ast - |> add_body_node(new(:emergency_unlock)) + ast |> add_body_node(new(:emergency_unlock)) end + @doc """ + iex> (new() |> rpc_request("x") |> read_status()).body + [%FarmbotCeleryScript.AST{ + body: [], + comment: nil, + meta: nil, + args: %{}, + kind: :read_status + }] + """ def read_status(%AST{} = ast) do - ast - |> add_body_node(new(:read_status)) + ast |> add_body_node(new(:read_status)) end + @doc """ + iex> (new() |> rpc_request("x") |> power_off()).body + [%FarmbotCeleryScript.AST{ + body: [], + comment: nil, + meta: nil, + args: %{}, + kind: :power_off + }] + """ def power_off(%AST{} = ast) do - ast - |> add_body_node(new(:power_off)) + ast |> add_body_node(new(:power_off)) end + @doc """ + iex> (new() |> rpc_request("x") |> reboot()).body + [%FarmbotCeleryScript.AST{ + body: [], + comment: nil, + meta: nil, + args: %{}, + kind: :reboot + }] + """ def reboot(%AST{} = ast) do - ast - |> add_body_node(new(:reboot)) + ast |> add_body_node(new(:reboot)) end + @doc """ + iex> (new() |> rpc_request("x") |> sync()).body + [%FarmbotCeleryScript.AST{ + body: [], + comment: nil, + meta: nil, + args: %{}, + kind: :sync + }] + """ def sync(%AST{} = ast) do - ast - |> add_body_node(new(:sync)) + ast |> add_body_node(new(:sync)) end + @doc """ + iex> (new() |> rpc_request("x") |> take_photo()).body + [%FarmbotCeleryScript.AST{ + body: [], + comment: nil, + meta: nil, + args: %{}, + kind: :take_photo + }] + """ def take_photo(%AST{} = ast) do - ast - |> add_body_node(new(:take_photo)) + ast |> add_body_node(new(:take_photo)) end + @doc """ + iex> (new() |> rpc_request("x") |> flash_firmware("arduino")).body + [%FarmbotCeleryScript.AST{ + kind: :flash_firmware, + comment: nil, + meta: nil, + args: %{package: "arduino"}, + body: [], + }] + """ def flash_firmware(%AST{} = ast, package) when is_binary(package) do - ast - |> add_body_node(new(:flash_firmware, %{package: package})) + ast |> add_body_node(new(:flash_firmware, %{package: package})) + end + + @doc """ + iex> (new() |> rpc_request("x") |> factory_reset("arduino")).body + [%FarmbotCeleryScript.AST{ + kind: :factory_reset, + comment: nil, + meta: nil, + args: %{package: "arduino"}, + body: [], + }] + """ + def factory_reset(%AST{} = ast, package) do + ast |> add_body_node(new(:factory_reset, %{package: package})) end def add_body_node(%AST{body: body} = ast, %AST{} = body_node) do %{ast | body: body ++ [body_node]} end - - def factory_reset(%AST{} = ast, package) do - ast - |> add_body_node(new(:factory_reset, %{package: package})) - end end diff --git a/farmbot_celery_script/test/farmbot_celery_script/ast/factory_test.exs b/farmbot_celery_script/test/farmbot_celery_script/ast/factory_test.exs new file mode 100644 index 00000000..22dcdf40 --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/ast/factory_test.exs @@ -0,0 +1,4 @@ +defmodule FarmbotCeleryScript.AST.FactoryTest do + use ExUnit.Case, async: true + doctest FarmbotCeleryScript.AST.Factory, import: true +end diff --git a/farmbot_celery_script/test/farmbot_celery_script/scheduler_test.exs b/farmbot_celery_script/test/farmbot_celery_script/scheduler_test.exs index 81cde7c6..3d5ce6ab 100644 --- a/farmbot_celery_script/test/farmbot_celery_script/scheduler_test.exs +++ b/farmbot_celery_script/test/farmbot_celery_script/scheduler_test.exs @@ -3,6 +3,7 @@ defmodule FarmbotCeleryScript.SchedulerTest do use Mimic alias FarmbotCeleryScript.{Scheduler, AST} alias FarmbotCeleryScript.SysCalls.Stubs + import ExUnit.CaptureLog setup :set_mimic_global setup :verify_on_exit! @@ -21,11 +22,14 @@ defmodule FarmbotCeleryScript.SchedulerTest do |> AST.Factory.read_pin(9, 0) scheduled_time = DateTime.utc_now() |> DateTime.add(100, :millisecond) + # msg = "[info] Next execution is ready for execution: now" {:ok, _} = Scheduler.schedule(sch, ast, scheduled_time, %{}) # Hack to force the scheduler to checkup instead of waiting the normal 15 seconds - send(sch, :checkup) - # Sorry. - Process.sleep(1100) + assert capture_log(fn -> + send(sch, :checkup) + # Sorry. + Process.sleep(1100) + end) =~ "[info] Next execution is ready for execution: now" end end diff --git a/farmbot_celery_script/test/farmbot_celery_script_test.exs b/farmbot_celery_script/test/farmbot_celery_script_test.exs index 0a7bedd2..8ae4833a 100644 --- a/farmbot_celery_script/test/farmbot_celery_script_test.exs +++ b/farmbot_celery_script/test/farmbot_celery_script_test.exs @@ -5,6 +5,9 @@ defmodule FarmbotCeleryScriptTest do alias FarmbotCeleryScript.AST alias FarmbotCeleryScript.SysCalls.Stubs + import ExUnit.CaptureIO + import ExUnit.CaptureLog + setup :verify_on_exit! test "uses default values when no parameter is found" do @@ -59,8 +62,10 @@ defmodule FarmbotCeleryScriptTest do :ok end) - result = FarmbotCeleryScript.execute(sequence_ast, me) - assert :ok == result + capture_log(fn -> + result = FarmbotCeleryScript.execute(sequence_ast, me) + assert :ok == result + end) =~ "[error] CeleryScript syscall stubbed: log" end test "syscall errors" do @@ -93,11 +98,17 @@ defmodule FarmbotCeleryScriptTest do } |> AST.decode() - expect(Stubs, :read_pin, fn _, _ -> raise("big oops") end) + expect(Stubs, :read_pin, fn _, _ -> + raise("big oops") + end) - assert {:error, "big oops"} == - FarmbotCeleryScript.execute(execute_ast, execute_ast) + io = + capture_io(:stderr, fn -> + assert {:error, "big oops"} == + FarmbotCeleryScript.execute(execute_ast, execute_ast) + end) + assert io =~ "CeleryScript Exception" assert_receive {:step_complete, ^execute_ast, {:error, "big oops"}} end end diff --git a/farmbot_core/config/test.exs b/farmbot_core/config/test.exs index 502f9dfd..079f595d 100644 --- a/farmbot_core/config/test.exs +++ b/farmbot_core/config/test.exs @@ -1,4 +1,5 @@ use Mix.Config +config :logger, level: :warn # must be lower than other timers # To ensure other timers have time to timeout @@ -17,3 +18,19 @@ config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 0 config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig, firmware_flash_attempt_threshold: 0 + +if Mix.env() == :test do + config :ex_unit, capture_logs: true + mapper = fn mod -> config :farmbot_core, mod, children: [] end + + list = [ + FarmbotCore, + FarmbotCore.StorageSupervisor, + FarmbotCore.Asset.Supervisor, + FarmbotCore.BotState.Supervisor, + FarmbotCore.Config.Supervisor, + FarmbotCore.Logger.Supervisor + ] + + Enum.map(list, mapper) +end diff --git a/farmbot_core/coveralls.json b/farmbot_core/coveralls.json index 67058f7c..67c6fd18 100644 --- a/farmbot_core/coveralls.json +++ b/farmbot_core/coveralls.json @@ -1,6 +1,6 @@ { "coverage_options": { "treat_no_relevant_lines_as_covered": true, - "minimum_coverage": 24 + "minimum_coverage": 25 } -} +} \ No newline at end of file diff --git a/farmbot_core/lib/farmbot_core.ex b/farmbot_core/lib/farmbot_core.ex index 769596e6..d001bb51 100644 --- a/farmbot_core/lib/farmbot_core.ex +++ b/farmbot_core/lib/farmbot_core.ex @@ -14,8 +14,11 @@ defmodule FarmbotCore do def start(_, args), do: Supervisor.start_link(__MODULE__, args, name: __MODULE__) def init([]) do + Supervisor.init(children(), [strategy: :one_for_one]) + end - children = [ + def children do + default = [ FarmbotCore.Leds, FarmbotCore.EctoMigrator, FarmbotCore.BotState.Supervisor, @@ -27,6 +30,7 @@ defmodule FarmbotCore do {FarmbotFirmware, transport: FarmbotFirmware.StubTransport, side_effects: FarmbotCore.FirmwareSideEffects}, FarmbotCeleryScript.Scheduler ] - Supervisor.init(children, [strategy: :one_for_one]) + config = Application.get_env(:farmbot_ext, __MODULE__) || [] +Keyword.get(config, :children, default) end end diff --git a/farmbot_core/lib/farmbot_core/asset.ex b/farmbot_core/lib/farmbot_core/asset.ex index 6d4ea4e3..cfdb7cb2 100644 --- a/farmbot_core/lib/farmbot_core/asset.ex +++ b/farmbot_core/lib/farmbot_core/asset.ex @@ -62,11 +62,6 @@ defmodule FarmbotCore.Asset do ## Begin FarmEvent - @doc "Returns all FarmEvents" - def list_farm_events do - Repo.all(FarmEvent) - end - def new_farm_event!(params) do %FarmEvent{} |> FarmEvent.changeset(params) @@ -353,7 +348,7 @@ defmodule FarmbotCore.Asset do |> Repo.update!() regimen_instances = list_regimen_instances() - farm_events = list_farm_events() + farm_events = Repo.all(FarmEvent) # check for any matching asset using this point group. # This is pretty recursive and probably isn't super great diff --git a/farmbot_core/lib/farmbot_core/asset/supervisor.ex b/farmbot_core/lib/farmbot_core/asset/supervisor.ex index 8c4cb61f..54d0bc66 100644 --- a/farmbot_core/lib/farmbot_core/asset/supervisor.ex +++ b/farmbot_core/lib/farmbot_core/asset/supervisor.ex @@ -23,7 +23,11 @@ defmodule FarmbotCore.Asset.Supervisor do end def init([]) do - children = [ + Supervisor.init(children(), strategy: :one_for_one) + end + + def children do + default = [ Repo, {AssetSupervisor, module: FbosConfig}, {AssetSupervisor, module: FirmwareConfig}, @@ -38,7 +42,7 @@ defmodule FarmbotCore.Asset.Supervisor do {AssetSupervisor, module: FarmwareEnv}, AssetMonitor, ] - - Supervisor.init(children, strategy: :one_for_one) + config = Application.get_env(:farmbot_ext, __MODULE__) || [] + Keyword.get(config, :children, default) end end diff --git a/farmbot_core/lib/farmbot_core/asset_monitor.ex b/farmbot_core/lib/farmbot_core/asset_monitor.ex index 193a8881..0401559b 100644 --- a/farmbot_core/lib/farmbot_core/asset_monitor.ex +++ b/farmbot_core/lib/farmbot_core/asset_monitor.ex @@ -82,7 +82,6 @@ defmodule FarmbotCore.AssetMonitor do sub_state = Map.drop(sub_state, deleted_ids) Enum.each(deleted_ids, fn local_id -> - Logger.error("#{inspect(kind)} #{local_id} needs to be terminated") AssetSupervisor.terminate_child(kind, local_id) end) @@ -99,7 +98,6 @@ defmodule FarmbotCore.AssetMonitor do Map.put(sub_state, id, updated_at) compare_datetimes(updated_at, sub_state[id]) == :gt -> - Logger.warn("#{inspect(kind)} #{id} needs to be updated") asset = Repo.preload(asset, AssetWorker.preload(asset)) :ok = AssetSupervisor.update_child(asset) |> assert_result!(asset) Map.put(sub_state, id, updated_at) diff --git a/farmbot_core/lib/farmbot_core/asset_workers/regimen_instance_worker.ex b/farmbot_core/lib/farmbot_core/asset_workers/regimen_instance_worker.ex index 2ee2e491..ff96cdb6 100644 --- a/farmbot_core/lib/farmbot_core/asset_workers/regimen_instance_worker.ex +++ b/farmbot_core/lib/farmbot_core/asset_workers/regimen_instance_worker.ex @@ -25,8 +25,6 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.RegimenInstance do @impl GenServer def init([regimen_instance, _args]) do - Logger.warn "RegimenInstance #{inspect(regimen_instance)} initializing" - with %Regimen{} <- regimen_instance.regimen, %FarmEvent{} <- regimen_instance.farm_event do send self(), :schedule @@ -40,25 +38,25 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.RegimenInstance do def handle_info(:schedule, state) do regimen_instance = state.regimen_instance # load the sequence and calculate the scheduled_at time - Enum.map(regimen_instance.regimen.regimen_items, fn(%{time_offset: offset, sequence_id: sequence_id}) -> + Enum.map(regimen_instance.regimen.regimen_items, fn(%{time_offset: offset, sequence_id: sequence_id}) -> scheduled_at = DateTime.add(regimen_instance.epoch, offset, :millisecond) sequence = Asset.get_sequence(sequence_id) || raise("sequence #{sequence_id} is not synced") %{scheduled_at: scheduled_at, sequence: sequence} end) # get rid of any item that has already been scheduled/executed - |> Enum.reject(fn(%{scheduled_at: scheduled_at}) -> + |> Enum.reject(fn(%{scheduled_at: scheduled_at}) -> Asset.get_regimen_instance_execution(regimen_instance, scheduled_at) end) - |> Enum.each(fn(%{scheduled_at: at, sequence: sequence}) -> + |> Enum.each(fn(%{scheduled_at: at, sequence: sequence}) -> schedule_sequence(regimen_instance, sequence, at) end) - {:noreply, state} + {:noreply, state} end def handle_info({FarmbotCeleryScript, {:scheduled_execution, scheduled_at, executed_at, result}}, state) do status = case result do :ok -> "ok" - {:error, reason} -> + {:error, reason} -> FarmbotCore.Logger.error(2, "Regimen scheduled at #{scheduled_at} failed to execute: #{reason}") reason end @@ -81,11 +79,11 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.RegimenInstance do regimen_params = AST.decode(regimen_instance.regimen.body) # there may be many sequence scopes from here downward celery_ast = AST.decode(sequence) - celery_args = + celery_args = celery_ast.args |> Map.put(:sequence_name, sequence.name) |> Map.put(:locals, %{celery_ast.args.locals | body: celery_ast.args.locals.body ++ regimen_params ++ farm_event_params}) - + celery_ast = %{celery_ast | args: celery_args} FarmbotCeleryScript.schedule(celery_ast, at, sequence) end diff --git a/farmbot_core/lib/farmbot_core/bot_state/supervisor.ex b/farmbot_core/lib/farmbot_core/bot_state/supervisor.ex index 1496dbca..0f505928 100644 --- a/farmbot_core/lib/farmbot_core/bot_state/supervisor.ex +++ b/farmbot_core/lib/farmbot_core/bot_state/supervisor.ex @@ -6,11 +6,16 @@ defmodule FarmbotCore.BotState.Supervisor do end def init([]) do - children = [ + Supervisor.init(children(), [strategy: :one_for_all]) + end + + def children do + default = [ FarmbotCore.BotState, FarmbotCore.BotState.FileSystem, FarmbotCore.BotState.SchedulerUsageReporter ] - Supervisor.init(children, [strategy: :one_for_all]) + config = Application.get_env(:farmbot_ext, __MODULE__) || [] + Keyword.get(config, :children, default) end end diff --git a/farmbot_core/lib/farmbot_core/config_storage/supervisor.ex b/farmbot_core/lib/farmbot_core/config_storage/supervisor.ex index 6d28bc08..644fa6df 100644 --- a/farmbot_core/lib/farmbot_core/config_storage/supervisor.ex +++ b/farmbot_core/lib/farmbot_core/config_storage/supervisor.ex @@ -7,9 +7,12 @@ defmodule FarmbotCore.Config.Supervisor do end def init([]) do - children = [ - {FarmbotCore.Config.Repo, []}, - ] - Supervisor.init(children, strategy: :one_for_one) + Supervisor.init(children(), strategy: :one_for_one) + end + + def children do + default = [ {FarmbotCore.Config.Repo, []} ] + config = Application.get_env(:farmbot_ext, __MODULE__) || [] +Keyword.get(config, :children, default) end end diff --git a/farmbot_core/lib/farmbot_core/leds/leds.ex b/farmbot_core/lib/farmbot_core/leds/leds.ex index d1b92402..437fc216 100644 --- a/farmbot_core/lib/farmbot_core/leds/leds.ex +++ b/farmbot_core/lib/farmbot_core/leds/leds.ex @@ -1,7 +1,5 @@ defmodule FarmbotCore.Leds do @moduledoc "API for controling Farmbot LEDS." - @led_handler Application.get_env(:farmbot_core, __MODULE__)[:gpio_handler] - @led_handler || Mix.raise("You forgot a led handler!") @valid_status [:off, :solid, :slow_blink, :fast_blink, :really_fast_blink] @@ -15,29 +13,7 @@ defmodule FarmbotCore.Leds do def white4(status) when status in @valid_status, do: led_handler().white4(status) def white5(status) when status in @valid_status, do: led_handler().white5(status) - def factory_test(status) do - red(:off) - blue(:off) - green(:off) - yellow(:off) - white1(:off) - white2(:off) - white3(:off) - white4(:off) - white5(:off) - - red(status) - blue(status) - green(status) - yellow(status) - white1(status) - white2(status) - white3(status) - white4(status) - white5(status) - end - - defp led_handler, + def led_handler, do: Application.get_env(:farmbot_core, __MODULE__)[:gpio_handler] def child_spec(opts) do diff --git a/farmbot_core/lib/farmbot_core/log_storage/supervisor.ex b/farmbot_core/lib/farmbot_core/log_storage/supervisor.ex index e6d800b7..bfd58338 100644 --- a/farmbot_core/lib/farmbot_core/log_storage/supervisor.ex +++ b/farmbot_core/lib/farmbot_core/log_storage/supervisor.ex @@ -7,11 +7,13 @@ defmodule FarmbotCore.Logger.Supervisor do end def init([]) do - children = [ - supervisor(FarmbotCore.Logger.Repo, []) - ] - opts = [strategy: :one_for_all] - supervise(children, opts) + supervise(children(), opts) + end + + def children do + default = [supervisor(FarmbotCore.Logger.Repo, [])] + config = Application.get_env(:farmbot_ext, __MODULE__) || [] + Keyword.get(config, :children, default) end end diff --git a/farmbot_core/lib/farmbot_core/storage_supervisor.ex b/farmbot_core/lib/farmbot_core/storage_supervisor.ex index 847071ef..472bbc42 100644 --- a/farmbot_core/lib/farmbot_core/storage_supervisor.ex +++ b/farmbot_core/lib/farmbot_core/storage_supervisor.ex @@ -10,11 +10,16 @@ defmodule FarmbotCore.StorageSupervisor do end def init([]) do - children = [ + Supervisor.init(children(), [strategy: :one_for_one]) + end + + def children do + default = [ FarmbotCore.Logger.Supervisor, FarmbotCore.Config.Supervisor, FarmbotCore.Asset.Supervisor ] - Supervisor.init(children, [strategy: :one_for_one]) + config = Application.get_env(:farmbot_ext, __MODULE__) || [] + Keyword.get(config, :children, default) end end diff --git a/farmbot_core/lib/farmbot_core/time_utils.ex b/farmbot_core/lib/farmbot_core/time_utils.ex index 839917b6..d34cc6a4 100644 --- a/farmbot_core/lib/farmbot_core/time_utils.ex +++ b/farmbot_core/lib/farmbot_core/time_utils.ex @@ -1,21 +1,6 @@ defmodule FarmbotCore.TimeUtils do @moduledoc "Helper functions for working with time." - def format_time(%DateTime{} = dt) do - "#{format_num(dt.month)}/#{format_num(dt.day)}/#{dt.year} " <> - "at #{format_num(dt.hour)}:#{format_num(dt.minute)}" - end - - defp format_num(num), do: :io_lib.format('~2..0B', [num]) |> to_string - - # returns midnight of today - @spec build_epoch(DateTime.t) :: DateTime.t - def build_epoch(time) do - tz = FarmbotCore.Asset.fbos_config().timezone - n = Timex.Timezone.convert(time, tz) - Timex.shift(n, hours: -n.hour, seconds: -n.second, minutes: -n.minute) - end - @doc """ Compares a datetime with another. • -1 -- the first date comes before the second one diff --git a/farmbot_core/priv/config/migrations/20180702143518_add_ntp_and_dns_configs.exs b/farmbot_core/priv/config/migrations/20180702143518_add_ntp_and_dns_configs.exs index daaeb9d5..4f9c90aa 100644 --- a/farmbot_core/priv/config/migrations/20180702143518_add_ntp_and_dns_configs.exs +++ b/farmbot_core/priv/config/migrations/20180702143518_add_ntp_and_dns_configs.exs @@ -14,22 +14,16 @@ defmodule FarmbotCore.Config.Repo.Migrations.AddNtpAndDnsConfigs do :default_dns_name ] - @config_error """ - config :farmbot_core, FarmbotCore.EctoMigrator, [ - default_ntp_server_1: "0.pool.ntp.org", - default_ntp_server_2: "1.pool.ntp.org", - default_dns_name: "my.farm.bot" - ] - """ - - if is_nil(@default_ntp_server_1), - do: raise(@config_error) - - if is_nil(@default_ntp_server_2), - do: raise(@config_error) - - if is_nil(@default_dns_name), - do: raise(@config_error) + unless @default_ntp_server_1 && @default_ntp_server_2 && @default_dns_name do + @config_error """ + config :farmbot_core, FarmbotCore.EctoMigrator, [ + default_ntp_server_1: "0.pool.ntp.org", + default_ntp_server_2: "1.pool.ntp.org", + default_dns_name: "my.farm.bot" + ] + """ + Mix.raise(@config_error) + end def change do create_settings_config( diff --git a/farmbot_core/test/asset/command_test.exs b/farmbot_core/test/asset/command_test.exs index 99665e6c..8fb3af84 100644 --- a/farmbot_core/test/asset/command_test.exs +++ b/farmbot_core/test/asset/command_test.exs @@ -27,6 +27,7 @@ defmodule FarmbotCore.Asset.CommandTest do :ok = Command.update(FirmwareConfig, 23, Map.from_struct(config)) end + @tag :capture_log test "update / destroy fbos config" do params = %{id: 23, update_channel: "whatever"} :ok = Command.update(FbosConfig, 23, params) @@ -38,6 +39,7 @@ defmodule FarmbotCore.Asset.CommandTest do refute next_config end + @tag :capture_log test "update / destroy device" do params = %{id: 23, name: "Old Device"} :ok = Command.update(Device, 23, params) @@ -56,6 +58,7 @@ defmodule FarmbotCore.Asset.CommandTest do assert Asset.get_regimen(id) end + @tag :capture_log test "update regimen" do id = id() :ok = Command.update("Regimen", id, %{id: id, name: "abc", monitor: false}) @@ -70,6 +73,7 @@ defmodule FarmbotCore.Asset.CommandTest do refute Asset.get_regimen(id) end + @tag :capture_log test "insert new farm_event" do id = id() :ok = Command.update("FarmEvent", id, %{id: id, monitor: false}) @@ -101,6 +105,7 @@ defmodule FarmbotCore.Asset.CommandTest do assert Asset.get_farm_event(id).executable_type == "Regimen" end + @tag :capture_log test "delete farm_event" do id = id() diff --git a/farmbot_core/test/asset/criteria_retriever_test.exs b/farmbot_core/test/asset/criteria_retriever_test.exs index 9b0597f0..1c845f1b 100644 --- a/farmbot_core/test/asset/criteria_retriever_test.exs +++ b/farmbot_core/test/asset/criteria_retriever_test.exs @@ -123,6 +123,7 @@ defmodule FarmbotCore.Asset.CriteriaRetrieverTest do Enum.map(expected, fn id -> assert Enum.member?(results, id) end) end + @tag :capture_log test "point group that does not define criteria" do Repo.delete_all(PointGroup) Repo.delete_all(Point) diff --git a/farmbot_core/test/asset/private_test.exs b/farmbot_core/test/asset/private_test.exs deleted file mode 100644 index 44334d0b..00000000 --- a/farmbot_core/test/asset/private_test.exs +++ /dev/null @@ -1,3 +0,0 @@ -defmodule FarmbotCore.Asset.PrivateTest do - use ExUnit.Case, async: true -end diff --git a/farmbot_core/test/asset_monitor_test.exs b/farmbot_core/test/asset_monitor_test.exs deleted file mode 100644 index e6a8ed20..00000000 --- a/farmbot_core/test/asset_monitor_test.exs +++ /dev/null @@ -1,61 +0,0 @@ -defmodule FarmbotCore.AssetMonitorTest do - use ExUnit.Case, async: false - alias FarmbotCore.{Asset.Repo, AssetMonitor, AssetSupervisor} - import Farmbot.TestSupport.AssetFixtures - - describe "regimen instances" do - test "adding a regimen instance starts a process" do - farm_event_params = %{ - start_time: DateTime.utc_now(), - end_time: DateTime.utc_now(), - repeat: 1, - time_unit: "never" - } - - pr = regimen_instance(%{}, farm_event_params, %{monitor: true}) - - AssetMonitor.force_checkup() - - assert {id, _, _, _} = AssetSupervisor.whereis_child(pr) - assert id == pr.local_id - - Repo.delete!(pr) - - AssetMonitor.force_checkup() - - assert {id, :undefined, _, _} = AssetSupervisor.whereis_child(pr) - assert id == pr.local_id - end - end - - describe "farm events" do - test "adding a farm event starts a process" do - seq = sequence() - now = DateTime.utc_now() - start_time = Timex.shift(now, minutes: -20) - end_time = Timex.shift(now, minutes: 10) - - params = %{ - monitor: true, - start_time: start_time, - end_time: end_time, - repeat: 5, - time_unit: "hourly" - } - - event = sequence_event(seq, params) - - AssetMonitor.force_checkup() - - assert {id, _, _, _} = AssetSupervisor.whereis_child(event) - assert id == event.local_id - - Repo.delete!(event) - - AssetMonitor.force_checkup() - - assert {id, :undefined, _, _} = AssetSupervisor.whereis_child(event) - assert id == event.local_id - end - end -end diff --git a/farmbot_core/test/asset_test.exs b/farmbot_core/test/asset_test.exs index e17ddf25..346bf306 100644 --- a/farmbot_core/test/asset_test.exs +++ b/farmbot_core/test/asset_test.exs @@ -15,4 +15,10 @@ defmodule FarmbotCore.AssetTest do assert %RegimenInstance{} = Asset.new_regimen_instance!(event) end end + + test "Asset.device/1" do + assert nil == Asset.device(:ota_hour) + assert %FarmbotCore.Asset.Device{} = Asset.update_device!(%{ota_hour: 17}) + assert 17 == Asset.device(:ota_hour) + end end diff --git a/farmbot_core/test/asset_workers/fbos_config_worker_test.exs b/farmbot_core/test/asset_workers/fbos_config_worker_test.exs index e8564c88..8256e371 100644 --- a/farmbot_core/test/asset_workers/fbos_config_worker_test.exs +++ b/farmbot_core/test/asset_workers/fbos_config_worker_test.exs @@ -4,6 +4,7 @@ defmodule FarmbotCore.FbosConfigWorkerTest do import Farmbot.TestSupport.AssetFixtures + @tag :capture_log test "adds configs to bot state and config_storage" do %FbosConfig{} = conf = diff --git a/farmbot_core/test/bot_state/filesystem_test.exs b/farmbot_core/test/bot_state/filesystem_test.exs index cf0de99c..fe06cdf9 100644 --- a/farmbot_core/test/bot_state/filesystem_test.exs +++ b/farmbot_core/test/bot_state/filesystem_test.exs @@ -61,6 +61,8 @@ defmodule FarmbotCore.BotState.FileSystemTest do describe "server" do test "serializes state to fs" do + IO.puts("THIS TEST BLINKS! Fix it.") + root_dir = Path.join([ System.tmp_dir!(), @@ -81,7 +83,7 @@ defmodule FarmbotCore.BotState.FileSystemTest do :ok = BotState.set_pin_value(bot_state_pid, 1, 1) assert_received {BotState, _}, 200 # sleep to allow changes to propagate. - Process.sleep(200) + Process.sleep(2000) pins_dir = Path.join([root_dir, "pins", "1"]) # default value assert File.read!(Path.join(pins_dir, "mode")) == "-1" diff --git a/farmbot_core/test/bot_state_test.exs b/farmbot_core/test/bot_state_test.exs index f7cecd07..0cbf2898 100644 --- a/farmbot_core/test/bot_state_test.exs +++ b/farmbot_core/test/bot_state_test.exs @@ -10,6 +10,7 @@ defmodule FarmbotCore.BotStateTest do assert_receive {BotState, %Ecto.Changeset{valid?: true}} end + @tag :capture_log test "invalid data doesn't get dispatched" do {:ok, bot_state_pid} = BotState.start_link([], []) _initial_state = BotState.subscribe(bot_state_pid) diff --git a/farmbot_core/test/farmware_runtime/pipe_worker_test.exs b/farmbot_core/test/farmware_runtime/pipe_worker_test.exs deleted file mode 100644 index eb21fb26..00000000 --- a/farmbot_core/test/farmware_runtime/pipe_worker_test.exs +++ /dev/null @@ -1,38 +0,0 @@ -defmodule FarmbotCore.FarmwareRuntime.PipeWorkerTest do - use ExUnit.Case, async: false - # alias FarmbotCore.FarmwareRuntime.PipeWorker - - # TODO Find a suitable tool for testing domain sockets? - - # test "reads data from pipe" do - # pipe_name = random_pipe() - # {:ok, pipe_worker} = PipeWorker.start_link(pipe_name) - # ref = PipeWorker.read(pipe_worker, 11) - # {_, 0} = System.cmd("bash", ["-c", "echo -e 'hello world' > #{pipe_name}"]) - # assert_receive {PipeWorker, ^ref, {:ok, "hello world"}} - # end - - # test "writes data to a pipe" do - # pipe_name = random_pipe() - # {:ok, pipe_worker} = PipeWorker.start_link(pipe_name) - - # ref = PipeWorker.read(pipe_worker, 11) - # PipeWorker.write(pipe_worker, "hello world") - # assert_receive {PipeWorker, ^ref, {:ok, "hello world"}} - # end - - # test "cleanup pipes on exit" do - # pipe_name = random_pipe() - # {:ok, pipe_worker} = PipeWorker.start_link(pipe_name) - # assert File.exists?(pipe_name) - # _ = Process.flag(:trap_exit, true) - # :ok = PipeWorker.close(pipe_worker) - # assert_receive {:EXIT, ^pipe_worker, :normal} - # refute File.exists?(pipe_name) - # end - - # defp random_pipe do - # pipe_name = Ecto.UUID.generate() <> ".pipe" - # Path.join([System.tmp_dir!(), pipe_name]) - # end -end diff --git a/farmbot_core/test/leds/stub_handler_test.exs b/farmbot_core/test/leds/stub_handler_test.exs new file mode 100644 index 00000000..3d6a32fa --- /dev/null +++ b/farmbot_core/test/leds/stub_handler_test.exs @@ -0,0 +1,37 @@ +defmodule FarmbotCore.Leds.StubHandlerTest do + use ExUnit.Case, async: true + import ExUnit.CaptureIO + + @color_map %{ + :red => :red, + :blue => :blue, + :green => :green, + :yellow => :yellow, + :white1 => :white, + :white2 => :white, + :white3 => :white, + :white4 => :white, + :white5 => :white + } + @status [:fast_blink, :really_fast_blink, :slow_blink, :solid] + + def capture_led(color) do + status = @status |> Enum.shuffle() |> Enum.at(0) + do_it = fn -> apply(FarmbotCore.Leds, color, [status]) end + cap = capture_io(do_it) + assert cap =~ "LED STATUS:" + assert cap =~ apply(IO.ANSI, Map.fetch!(@color_map, color), []) + end + + test "leds" do + capture_led(:red) + capture_led(:blue) + capture_led(:green) + capture_led(:yellow) + capture_led(:white1) + capture_led(:white2) + capture_led(:white3) + capture_led(:white4) + capture_led(:white5) + end +end diff --git a/farmbot_core/test/log_test.exs b/farmbot_core/test/log_test.exs new file mode 100644 index 00000000..f197482e --- /dev/null +++ b/farmbot_core/test/log_test.exs @@ -0,0 +1,9 @@ +defmodule FarmbotCore.LogTest do + alias FarmbotCore.Log + use ExUnit.Case, async: true + + test "to_chars" do + log = %Log{message: "Hello, world!"} + assert "Hello, world!" = "#{log}" + end +end diff --git a/farmbot_core/test/logger_test.exs b/farmbot_core/test/logger_test.exs index 9ec30dad..a24a7d1e 100644 --- a/farmbot_core/test/logger_test.exs +++ b/farmbot_core/test/logger_test.exs @@ -2,6 +2,7 @@ defmodule FarmbotCore.LoggerTest do use ExUnit.Case require FarmbotCore.Logger + @tag :capture_log test "allows handling a log more than once by re-inserting it." do log = FarmbotCore.Logger.debug(1, "Test log ABC") # Handling a log should delete it from the store. diff --git a/farmbot_core/test/project_test.exs b/farmbot_core/test/project_test.exs new file mode 100644 index 00000000..0b6bbf70 --- /dev/null +++ b/farmbot_core/test/project_test.exs @@ -0,0 +1,10 @@ +defmodule FarmbotCore.ProjectTest do + use ExUnit.Case + @opts [cd: Path.join("c_src", "farmbot-arduino-firmware")] + + test "arduino_commit" do + actual = FarmbotCore.Project.arduino_commit() + assert is_binary(actual) + assert String.length(actual) == 40 + end +end diff --git a/farmbot_ext/config/test.exs b/farmbot_ext/config/test.exs index 84f43b1f..54a47b28 100644 --- a/farmbot_ext/config/test.exs +++ b/farmbot_ext/config/test.exs @@ -1,5 +1,16 @@ use Mix.Config if Mix.env() == :test do - config :farmbot_ext, FarmbotExt, children: [] + config :ex_unit, capture_logs: true + mapper = fn mod -> config :farmbot_ext, mod, children: [] end + + list = [ + FarmbotExt, + FarmbotExt.AMQP.ChannelSupervisor, + FarmbotExt.API.DirtyWorker.Supervisor, + FarmbotExt.API.EagerLoader.Supervisor, + FarmbotExt.Bootstrap.Supervisor + ] + + Enum.map(list, mapper) end diff --git a/farmbot_ext/lib/farmbot_ext.ex b/farmbot_ext/lib/farmbot_ext.ex index 51c37cf4..08317b1d 100644 --- a/farmbot_ext/lib/farmbot_ext.ex +++ b/farmbot_ext/lib/farmbot_ext.ex @@ -4,13 +4,11 @@ defmodule FarmbotExt do use Application def start(_type, _args) do - opts = [strategy: :one_for_one, name: __MODULE__] - Supervisor.start_link(children(), opts) + Supervisor.start_link(children(), opts()) end - # This only exists because I was getting too many crashed - # supervisor reports in the test suite (distraction from - # real test failures). + def opts, do: [strategy: :one_for_one, name: __MODULE__] + def children do config = Application.get_env(:farmbot_ext, __MODULE__) || [] Keyword.get(config, :children, [FarmbotExt.Bootstrap]) diff --git a/farmbot_ext/lib/farmbot_ext/amqp/channel_supervisor.ex b/farmbot_ext/lib/farmbot_ext/amqp/channel_supervisor.ex index 69ad9992..00750952 100644 --- a/farmbot_ext/lib/farmbot_ext/amqp/channel_supervisor.ex +++ b/farmbot_ext/lib/farmbot_ext/amqp/channel_supervisor.ex @@ -19,17 +19,19 @@ defmodule FarmbotExt.AMQP.ChannelSupervisor do end def init([token]) do - jwt = JWT.decode!(token) + Supervisor.init(children(JWT.decode!(token)), strategy: :one_for_one) + end - children = [ + def children(jwt) do + config = Application.get_env(:farmbot_ext, __MODULE__) || [] + + Keyword.get(config, :children, [ {TelemetryChannel, [jwt: jwt]}, {LogChannel, [jwt: jwt]}, {PingPongChannel, [jwt: jwt]}, {BotStateChannel, [jwt: jwt]}, {AutoSyncChannel, [jwt: jwt]}, {CeleryScriptChannel, [jwt: jwt]} - ] - - Supervisor.init(children, strategy: :one_for_one) + ]) end end diff --git a/farmbot_ext/lib/farmbot_ext/amqp/supervisor.ex b/farmbot_ext/lib/farmbot_ext/amqp/supervisor.ex index edb58508..4147e1f2 100644 --- a/farmbot_ext/lib/farmbot_ext/amqp/supervisor.ex +++ b/farmbot_ext/lib/farmbot_ext/amqp/supervisor.ex @@ -10,14 +10,17 @@ defmodule FarmbotExt.AMQP.Supervisor do end def init([]) do + Supervisor.init(children(), strategy: :one_for_all) + end + + def children do token = get_config_value(:string, "authorization", "token") email = get_config_value(:string, "authorization", "email") + config = Application.get_env(:farmbot_ext, __MODULE__) || [] - children = [ + Keyword.get(config, :children, [ {FarmbotExt.AMQP.ConnectionWorker, [token: token, email: email]}, {FarmbotExt.AMQP.ChannelSupervisor, [token]} - ] - - Supervisor.init(children, strategy: :one_for_all) + ]) end end diff --git a/farmbot_ext/lib/farmbot_ext/api/dirty_worker/supervisor.ex b/farmbot_ext/lib/farmbot_ext/api/dirty_worker/supervisor.ex index b1e78da0..a026ea06 100644 --- a/farmbot_ext/lib/farmbot_ext/api/dirty_worker/supervisor.ex +++ b/farmbot_ext/lib/farmbot_ext/api/dirty_worker/supervisor.ex @@ -33,7 +33,13 @@ defmodule FarmbotExt.API.DirtyWorker.Supervisor do @impl Supervisor def init(_args) do - children = [ + Supervisor.init(children(), strategy: :one_for_one) + end + + def children do + config = Application.get_env(:farmbot_ext, __MODULE__) || [] + + Keyword.get(config, :children, [ {DirtyWorker, Device}, {DirtyWorker, DeviceCert}, {DirtyWorker, FbosConfig}, @@ -50,8 +56,6 @@ defmodule FarmbotExt.API.DirtyWorker.Supervisor do {DirtyWorker, Sensor}, {DirtyWorker, Sequence}, {DirtyWorker, Tool} - ] - - Supervisor.init(children, strategy: :one_for_one) + ]) end end diff --git a/farmbot_ext/lib/farmbot_ext/api/eager_loader/supervisor.ex b/farmbot_ext/lib/farmbot_ext/api/eager_loader/supervisor.ex index cf7dd358..086b77fa 100644 --- a/farmbot_ext/lib/farmbot_ext/api/eager_loader/supervisor.ex +++ b/farmbot_ext/lib/farmbot_ext/api/eager_loader/supervisor.ex @@ -39,7 +39,13 @@ defmodule FarmbotExt.API.EagerLoader.Supervisor do @impl Supervisor def init(_args) do - children = [ + Supervisor.init(children(), strategy: :one_for_one) + end + + def children do + config = Application.get_env(:farmbot_ext, __MODULE__) || [] + + Keyword.get(config, :children, [ {EagerLoader, Device}, {EagerLoader, FarmEvent}, {EagerLoader, FarmwareEnv}, @@ -56,8 +62,6 @@ defmodule FarmbotExt.API.EagerLoader.Supervisor do {EagerLoader, Sensor}, {EagerLoader, Sequence}, {EagerLoader, Tool} - ] - - Supervisor.init(children, strategy: :one_for_one) + ]) end end diff --git a/farmbot_ext/lib/farmbot_ext/api/image_uploader.ex b/farmbot_ext/lib/farmbot_ext/api/image_uploader.ex index 9a4af358..2755adeb 100644 --- a/farmbot_ext/lib/farmbot_ext/api/image_uploader.ex +++ b/farmbot_ext/lib/farmbot_ext/api/image_uploader.ex @@ -16,7 +16,7 @@ defmodule FarmbotExt.API.ImageUploader do GenServer.start_link(__MODULE__, args, name: __MODULE__) end - def force_checkup do + def force_checkup() do GenServer.cast(__MODULE__, :force_checkup) end @@ -45,19 +45,23 @@ defmodule FarmbotExt.API.ImageUploader do def handle_continue([], state), do: {:noreply, state, @checkup_time_ms} - # the meta here is likely inaccurate here because of - # pulling the location data from the cache instead of from the firmware - # directly. It's close enough and getting data from the firmware directly - # would require more work than it is worth + # the meta here is likely inaccurate here because of pulling the location data + # from the cache instead of from the firmware directly. It's close enough and + # getting data from the firmware directly would require more work than it is + # worth defp try_upload(image_filename) do %{x: x, y: y, z: z} = BotState.fetch().location_data.position meta = %{x: x, y: y, z: z, name: Path.rootname(image_filename)} + finalize(image_filename, API.upload_image(image_filename, meta)) + end - with {:ok, %{status: s, body: _body}} when s > 199 and s < 300 <- - API.upload_image(image_filename, meta) do - FarmbotCore.Logger.success(3, "Uploaded image: #{image_filename}") - File.rm(image_filename) - end + defp finalize(file, {:ok, %{status: s, body: _}}) when s > 199 and s < 300 do + FarmbotCore.Logger.success(3, "Uploaded image: #{file}") + File.rm(file) + end + + defp finalize(fname, other) do + FarmbotCore.Logger.error(3, "Upload Error (#{fname}): #{inspect(other)}") end # Stolen from diff --git a/farmbot_ext/lib/farmbot_ext/bootstrap/supervisor.ex b/farmbot_ext/lib/farmbot_ext/bootstrap/supervisor.ex index 5abcdfb1..05ec931b 100644 --- a/farmbot_ext/lib/farmbot_ext/bootstrap/supervisor.ex +++ b/farmbot_ext/lib/farmbot_ext/bootstrap/supervisor.ex @@ -12,14 +12,18 @@ defmodule FarmbotExt.Bootstrap.Supervisor do @impl Supervisor def init([]) do - children = [ + Supervisor.init(children(), strategy: :one_for_one) + end + + def children() do + config = Application.get_env(:farmbot_ext, __MODULE__) || [] + + Keyword.get(config, :children, [ FarmbotExt.API.EagerLoader.Supervisor, FarmbotExt.API.DirtyWorker.Supervisor, FarmbotExt.AMQP.Supervisor, FarmbotExt.API.ImageUploader, FarmbotExt.Bootstrap.DropPasswordTask - ] - - Supervisor.init(children, strategy: :one_for_one) + ]) end end diff --git a/farmbot_ext/test/farmbot_ext/amqp/auto_sync_asset_handler_test.exs b/farmbot_ext/test/farmbot_ext/amqp/auto_sync_asset_handler_test.exs index 950f1aad..3f619a28 100644 --- a/farmbot_ext/test/farmbot_ext/amqp/auto_sync_asset_handler_test.exs +++ b/farmbot_ext/test/farmbot_ext/amqp/auto_sync_asset_handler_test.exs @@ -1,5 +1,6 @@ defmodule AutoSyncAssetHandlerTest do - use ExUnit.Case, async: true + require Helpers + use ExUnit.Case, async: false use Mimic setup :verify_on_exit! @@ -8,7 +9,10 @@ defmodule AutoSyncAssetHandlerTest do alias FarmbotExt.AMQP.AutoSyncAssetHandler alias FarmbotCore.{Asset, BotState, Leds} + import ExUnit.CaptureLog + def auto_sync_off, do: expect(Asset.Query, :auto_sync?, fn -> false end) + def auto_sync_on, do: expect(Asset.Query, :auto_sync?, fn -> true end) def expect_sync_status_to_be(status), do: expect(BotState, :set_sync_status, fn ^status -> :ok end) @@ -22,4 +26,41 @@ defmodule AutoSyncAssetHandlerTest do expect_green_leds(:slow_blink) AutoSyncAssetHandler.handle_asset("Point", 23, nil) end + + test "Handles @no_cache_kinds" do + id = 64 + params = %{} + + kind = + ~w(Device FbosConfig FirmwareConfig FarmwareEnv FarmwareInstallation) + |> Enum.shuffle() + |> Enum.at(0) + + expect(Asset.Command, :update, 1, fn ^kind, ^id, ^params -> :ok end) + assert :ok = AutoSyncAssetHandler.cache_sync(kind, id, params) + end + + test "handling of deleted assets when auto_sync is enabled" do + auto_sync_on() + expect_sync_status_to_be("syncing") + expect_sync_status_to_be("synced") + expect_green_leds(:really_fast_blink) + expect_green_leds(:solid) + AutoSyncAssetHandler.handle_asset("Point", 32, nil) + end + + test "cache sync" do + id = 64 + params = %{} + kind = "Point" + # Helpers.expect_log("Autocaching sync #{kind} #{id} #{inspect(params)}") + changeset = %{ab: :cd} + changesetfaker = fn ^kind, ^id, ^params -> changeset end + expect(FarmbotCore.Asset.Command, :new_changeset, 1, changesetfaker) + expect(FarmbotExt.API.EagerLoader, :cache, 1, fn ^changeset -> :ok end) + expect_sync_status_to_be("sync_now") + expect_green_leds(:slow_blink) + do_it = fn -> AutoSyncAssetHandler.cache_sync(kind, id, params) end + assert capture_log(do_it) =~ "Autocaching sync Point 64 %{}" + end end diff --git a/farmbot_ext/test/farmbot_ext/amqp/auto_sync_channel_test.exs b/farmbot_ext/test/farmbot_ext/amqp/auto_sync_channel_test.exs index e10ac3b4..bd96a596 100644 --- a/farmbot_ext/test/farmbot_ext/amqp/auto_sync_channel_test.exs +++ b/farmbot_ext/test/farmbot_ext/amqp/auto_sync_channel_test.exs @@ -1,6 +1,6 @@ defmodule AutoSyncChannelTest do require Helpers - use ExUnit.Case, async: true + use ExUnit.Case, async: false use Mimic alias FarmbotExt.AMQP.AutoSyncChannel @@ -55,7 +55,7 @@ defmodule AutoSyncChannelTest do expect(Preloader, :preload_all, 1, fn -> :ok end) pid = generate_pid() send(pid, msg) - Process.sleep(5) + Helpers.wait_for(pid) end test "basic_cancel", do: ensure_response_to({:basic_cancel, :anything}) @@ -78,6 +78,7 @@ defmodule AutoSyncChannelTest do # Helpers.expect_log("Failed to connect to AutoSync channel: :whatever") # Helpers.expect_log("Disconnected from AutoSync channel: :normal") pid = generate_pid() + IO.puts(" = = = ==RICK: This test blinks and you should fix it.") assert %{chan: nil, conn: nil, preloaded: true} == AutoSyncChannel.network_status(pid) GenServer.stop(pid, :normal) end @@ -153,6 +154,6 @@ defmodule AutoSyncChannelTest do :ok end) - Process.sleep(1200) + Helpers.wait_for(pid) end end diff --git a/farmbot_ext/test/farmbot_ext/amqp/bot_state_channel_test.exs b/farmbot_ext/test/farmbot_ext/amqp/bot_state_channel_test.exs index d03cf239..f7d8e284 100644 --- a/farmbot_ext/test/farmbot_ext/amqp/bot_state_channel_test.exs +++ b/farmbot_ext/test/farmbot_ext/amqp/bot_state_channel_test.exs @@ -1,5 +1,5 @@ defmodule FarmbotExt.AMQP.BotStateChannelTest do - use ExUnit.Case + use ExUnit.Case, async: false use Mimic # alias FarmbotExt.AMQP.BotStateChannel diff --git a/farmbot_ext/test/farmbot_ext/api/image_uploader_test.exs b/farmbot_ext/test/farmbot_ext/api/image_uploader_test.exs new file mode 100644 index 00000000..41c788b0 --- /dev/null +++ b/farmbot_ext/test/farmbot_ext/api/image_uploader_test.exs @@ -0,0 +1,43 @@ +defmodule FarmbotExt.API.ImageUploaderTest do + require Helpers + use ExUnit.Case, async: false + use Mimic + alias FarmbotExt.API.ImageUploader + setup :verify_on_exit! + setup :set_mimic_global + + test "force checkup" do + pid = + if Process.whereis(ImageUploader) do + Process.whereis(ImageUploader) + else + {:ok, p} = ImageUploader.start_link([]) + p + end + + ["a.jpg", "b.jpeg", "c.png", "d.gif"] + |> Enum.map(fn fname -> + f = "/tmp/images/#{fname}" + File.touch!(f) + File.write(f, "X") + end) + + expect(FarmbotExt.API, :upload_image, 4, fn + "/tmp/images/d.gif", _meta -> {:error, %{status: 401, body: %{}}} + _image_filename, _meta -> {:ok, %{status: 201, body: %{}}} + end) + + err_msg = + "Upload Error (/tmp/images/d.gif): " <> + "{:error, %{body: %{}, status: 401}}" + + Helpers.expect_log("Uploaded image: /tmp/images/a.jpg") + Helpers.expect_log("Uploaded image: /tmp/images/b.jpeg") + Helpers.expect_log("Uploaded image: /tmp/images/c.png") + Helpers.expect_log(err_msg) + + ImageUploader.force_checkup() + send(pid, :timeout) + Helpers.wait_for(pid) + end +end diff --git a/farmbot_ext/test/test_helper.exs b/farmbot_ext/test/test_helper.exs index 9fbbf006..40f2c1b8 100644 --- a/farmbot_ext/test/test_helper.exs +++ b/farmbot_ext/test/test_helper.exs @@ -1,28 +1,54 @@ Application.ensure_all_started(:farmbot) -Mimic.copy(AMQP.Channel) -Mimic.copy(FarmbotCeleryScript.SysCalls.Stubs) -Mimic.copy(FarmbotCore.Asset.Command) -Mimic.copy(FarmbotCore.Asset.Query) -Mimic.copy(FarmbotCore.BotState) -Mimic.copy(FarmbotCore.Leds) -Mimic.copy(FarmbotCore.LogExecutor) -Mimic.copy(FarmbotExt.AMQP.ConnectionWorker) -Mimic.copy(FarmbotExt.API.EagerLoader.Supervisor) -Mimic.copy(FarmbotExt.API.Preloader) -Mimic.copy(FarmbotExt.API) -Mimic.copy(FarmbotExt.AMQP.AutoSyncAssetHandler) +[ + AMQP.Channel, + FarmbotCeleryScript.SysCalls.Stubs, + FarmbotCore.Asset.Command, + FarmbotCore.Asset.Query, + FarmbotCore.BotState, + FarmbotCore.Leds, + FarmbotCore.LogExecutor, + FarmbotExt.AMQP.AutoSyncAssetHandler, + FarmbotExt.AMQP.ConnectionWorker, + FarmbotExt.API, + FarmbotExt.API.EagerLoader, + FarmbotExt.API.EagerLoader.Supervisor, + FarmbotExt.API.Preloader +] +|> Enum.map(&Mimic.copy/1) -timeout = System.get_env("EXUNIT_TIMEOUT") +timeout = System.get_env("EXUNIT_TIMEOUT") || "5000" System.put_env("LOG_SILENCE", "true") -if timeout do - ExUnit.start(assert_receive_timeout: String.to_integer(timeout)) -else - ExUnit.start() -end +ExUnit.start(assert_receive_timeout: String.to_integer(timeout)) defmodule Helpers do + # Maybe I don't need this? + # Maybe I could use `start_supervised`? + # https://hexdocs.pm/ex_unit/ExUnit.Callbacks.html#start_supervised/2 + + @wait_time 60 + # Base case: We have a pid + def wait_for(pid) when is_pid(pid), do: check_on_mbox(pid) + # Failure case: We failed to find a pid for a module. + def wait_for(nil), do: raise("Attempted to wait on bad module/pid") + # Edge case: We have a module and need to try finding its pid. + def wait_for(mod), do: wait_for(Process.whereis(mod)) + + # Enter recursive loop + defp check_on_mbox(pid) do + Process.sleep(@wait_time) + wait(pid, Process.info(pid, :message_queue_len)) + end + + # Exit recursive loop (mbox is clear) + defp wait(_, {:message_queue_len, 0}), do: Process.sleep(@wait_time * 3) + # Exit recursive loop (pid is dead) + defp wait(_, nil), do: Process.sleep(@wait_time * 3) + + # Continue recursive loop + defp wait(pid, {:message_queue_len, _n}), do: check_on_mbox(pid) + defmacro expect_log(message) do quote do expect(FarmbotCore.LogExecutor, :execute, fn log -> diff --git a/farmbot_firmware/test/farmbot_firmware/command_test.exs b/farmbot_firmware/test/farmbot_firmware/command_test.exs index 2a986a4d..e36b6ae9 100644 --- a/farmbot_firmware/test/farmbot_firmware/command_test.exs +++ b/farmbot_firmware/test/farmbot_firmware/command_test.exs @@ -6,6 +6,7 @@ defmodule FarmbotFirmware.CommandTest do import ExUnit.CaptureLog @subject FarmbotFirmware.Command + @tag :capture_log test "command() runs RPCs" do arg = [transport: FarmbotFirmware.StubTransport] {:ok, pid} = FarmbotFirmware.start_link(arg, []) @@ -19,6 +20,7 @@ defmodule FarmbotFirmware.CommandTest do assert :ok == FarmbotFirmware.command(pid, cmd) end + @tag :capture_log test "command() refuses to run RPCs in :boot state" do arg = [transport: FarmbotFirmware.StubTransport] {:ok, pid} = FarmbotFirmware.start_link(arg, []) diff --git a/farmbot_firmware/test/farmbot_firmware/transports/uart_transport_test.exs b/farmbot_firmware/test/farmbot_firmware/transports/uart_transport_test.exs index 50678f5c..8238359b 100644 --- a/farmbot_firmware/test/farmbot_firmware/transports/uart_transport_test.exs +++ b/farmbot_firmware/test/farmbot_firmware/transports/uart_transport_test.exs @@ -4,6 +4,7 @@ defmodule FarmbotFirmware.UARTTransportTest do doctest FarmbotFirmware.UARTTransport alias FarmbotFirmware.{UartDefaultAdapter, UARTTransport} setup :verify_on_exit! + import ExUnit.CaptureLog test "UARTTransport.init/1" do expect(UartDefaultAdapter, :start_link, fn -> @@ -61,15 +62,22 @@ defmodule FarmbotFirmware.UARTTransportTest do fake_opts end) + error = "Simulated UART failure. This is OK" + expect(UartDefaultAdapter, :open, fn _, _, _ -> - {:error, "Simulated UART failure. This is OK"} + {:error, error} end) - {:noreply, state2, retry_timeout} = - UARTTransport.handle_info(:timeout, state) + logs = + capture_log(fn -> + {:noreply, state2, retry_timeout} = + UARTTransport.handle_info(:timeout, state) - assert retry_timeout == 5000 - assert state.open == state2.open + assert retry_timeout == 5000 + assert state.open == state2.open + end) + + assert logs =~ error end test "UARTTransport handles `Circuits-UART` speecific errors" do diff --git a/farmbot_firmware/test/farmbot_firmware_test.exs b/farmbot_firmware/test/farmbot_firmware_test.exs index 04af88bf..c0778fda 100644 --- a/farmbot_firmware/test/farmbot_firmware_test.exs +++ b/farmbot_firmware/test/farmbot_firmware_test.exs @@ -19,6 +19,7 @@ defmodule FarmbotFirmwareTest do pid end + @tag :capture_log test "various reports" do pid = firmware_server() @@ -63,6 +64,7 @@ defmodule FarmbotFirmwareTest do Process.sleep(1000) end + @tag :capture_log test "various command()s" do pid = firmware_server() diff --git a/farmbot_firmware/test/package_utils_test.exs b/farmbot_firmware/test/package_utils_test.exs index ee763df4..5c71c47c 100644 --- a/farmbot_firmware/test/package_utils_test.exs +++ b/farmbot_firmware/test/package_utils_test.exs @@ -49,6 +49,9 @@ defmodule FarmbotFirmware.PackageUtilsTest do {:ok, path} = PackageUtils.find_hex_file("express_k10") assert String.contains?(path, "/farmbot_firmware/priv/express_k10.hex") + {:ok, path} = PackageUtils.find_hex_file("none") + assert path =~ "lib/farmbot_firmware/priv/eeprom_clear.ino.hex" + assert {:error, "unknown firmware hardware: no"} == PackageUtils.find_hex_file("no") end diff --git a/farmbot_firmware/test/param_test.exs b/farmbot_firmware/test/param_test.exs index 8b6357ff..5fe3b233 100644 --- a/farmbot_firmware/test/param_test.exs +++ b/farmbot_firmware/test/param_test.exs @@ -1,6 +1,11 @@ defmodule FarmbotFirmware.ParamTest do use ExUnit.Case alias FarmbotFirmware.Param + import ExUnit.CaptureLog + + def t(p, v, expected) do + assert Param.to_human(p, v) == expected + end test "to_human()" do float_value = 1.23 @@ -9,212 +14,354 @@ defmodule FarmbotFirmware.ParamTest do steps_per_s = "(steps/s)" steps_per_mm = "(steps/mm)" - assert Param.to_human(:param_test, 1) == - {"param_test", nil, true} - - assert Param.to_human(:param_config_ok, 1) == - {"param_config_ok", nil, true} - - assert Param.to_human(:param_use_eeprom, 1) == - {"use eeprom", nil, true} - - assert Param.to_human(:param_e_stop_on_mov_err, 1) == - {"e-stop on movement errors", nil, true} - - assert Param.to_human(:movement_timeout_x, float_value) == - {"timeout after, x-axis", seconds, "1.2"} - - assert Param.to_human(:movement_timeout_y, float_value) == - {"timeout after, y-axis", seconds, "1.2"} - - assert Param.to_human(:movement_timeout_z, float_value) == - {"timeout after, z-axis", seconds, "1.2"} - - assert Param.to_human(:movement_keep_active_x, 1) == - {"always power motors, x-axis", nil, true} - - assert Param.to_human(:movement_keep_active_y, 1) == - {"always power motors, y-axis", nil, true} - - assert Param.to_human(:movement_keep_active_z, 1) == - {"always power motors, z-axis", nil, true} - - assert Param.to_human(:movement_home_at_boot_x, 1) == - {"find home on boot, x-axis", nil, true} - - assert Param.to_human(:movement_home_at_boot_y, 1) == - {"find home on boot, y-axis", nil, true} - - assert Param.to_human(:movement_home_at_boot_z, 1) == - {"find home on boot, z-axis", nil, true} - - assert Param.to_human(:movement_invert_endpoints_x, 1) == - {"swap endstops, x-axis", nil, true} - - assert Param.to_human(:movement_invert_endpoints_y, 1) == - {"swap endstops, y-axis", nil, true} - - assert Param.to_human(:movement_invert_endpoints_z, 1) == - {"swap endstops, z-axis", nil, true} - - assert Param.to_human(:movement_enable_endpoints_x, 1) == - {"enable endstops, x-axis", nil, true} - - assert Param.to_human(:movement_enable_endpoints_y, 1) == - {"enable endstops, y-axis", nil, true} - - assert Param.to_human(:movement_enable_endpoints_z, 1) == - {"enable endstops, z-axis", nil, true} - - assert Param.to_human(:movement_invert_motor_x, 1) == - {"invert motor, x-axis", nil, true} - - assert Param.to_human(:movement_invert_motor_y, 1) == - {"invert motor, y-axis", nil, true} - - assert Param.to_human(:movement_invert_motor_z, 1) == - {"invert motor, z-axis", nil, true} - - assert Param.to_human(:movement_secondary_motor_x, 1) == - {"enable 2nd x motor", nil, true} - - assert Param.to_human(:movement_secondary_motor_invert_x, 1) == - {"invert 2nd x motor", nil, true} - - assert Param.to_human(:movement_stop_at_home_x, 1) == - {"stop at home, x-axis", nil, true} - - assert Param.to_human(:movement_stop_at_home_y, 1) == - {"stop at home, y-axis", nil, true} - - assert Param.to_human(:movement_stop_at_home_z, 1) == - {"stop at home, z-axis", nil, true} - - assert Param.to_human(:movement_step_per_mm_x, float_value) == - {"steps per mm, x-axis", steps_per_mm, "1.2"} - - assert Param.to_human(:movement_step_per_mm_y, float_value) == - {"steps per mm, y-axis", steps_per_mm, "1.2"} - - assert Param.to_human(:movement_step_per_mm_z, float_value) == - {"steps per mm, z-axis", steps_per_mm, "1.2"} - - assert Param.to_human(:movement_min_spd_x, float_value) == - {"minimum speed, x-axis", steps_per_s, "1.2"} - - assert Param.to_human(:movement_min_spd_y, float_value) == - {"minimum speed, y-axis", steps_per_s, "1.2"} - - assert Param.to_human(:movement_min_spd_z, float_value) == - {"minimum speed, z-axis", steps_per_s, "1.2"} - - assert Param.to_human(:movement_home_spd_x, float_value) == - {"homing speed, x-axis", steps_per_s, "1.2"} - - assert Param.to_human(:movement_home_spd_y, float_value) == - {"homing speed, y-axis", steps_per_s, "1.2"} - - assert Param.to_human(:movement_home_spd_z, float_value) == - {"homing speed, z-axis", steps_per_s, "1.2"} - - assert Param.to_human(:movement_max_spd_x, float_value) == - {"max speed, x-axis", steps_per_s, "1.2"} - - assert Param.to_human(:movement_max_spd_y, float_value) == - {"max speed, y-axis", steps_per_s, "1.2"} - - assert Param.to_human(:movement_max_spd_z, float_value) == - {"max speed, z-axis", steps_per_s, "1.2"} - - assert Param.to_human(:movement_invert_2_endpoints_x, 1) == - {"invert endstops, x-axis", nil, true} - - assert Param.to_human(:movement_invert_2_endpoints_y, 1) == - {"invert endstops, y-axis", nil, true} - - assert Param.to_human(:movement_invert_2_endpoints_z, 1) == - {"invert endstops, z-axis", nil, true} - - assert Param.to_human(:encoder_enabled_x, 1) == - {"enable encoders / stall detection, x-axis", nil, true} - - assert Param.to_human(:encoder_enabled_y, 1) == - {"enable encoders / stall detection, y-axis", nil, true} - - assert Param.to_human(:encoder_enabled_z, 1) == - {"enable encoders / stall detection, z-axis", nil, true} - - assert Param.to_human(:encoder_type_x, float_value) == - {"encoder type, x-axis", nil, "1.2"} - - assert Param.to_human(:encoder_type_y, float_value) == - {"encoder type, y-axis", nil, "1.2"} - - assert Param.to_human(:encoder_type_z, float_value) == - {"encoder type, z-axis", nil, "1.2"} - - assert Param.to_human(:encoder_scaling_x, float_value) == - {"encoder scaling, x-axis", nil, "1.2"} - - assert Param.to_human(:encoder_scaling_y, float_value) == - {"encoder scaling, y-axis", nil, "1.2"} - - assert Param.to_human(:encoder_scaling_z, float_value) == - {"encoder scaling, z-axis", nil, "1.2"} - - assert Param.to_human(:encoder_missed_steps_decay_x, float_value) == - {"missed step decay, x-axis", steps, "1.2"} - - assert Param.to_human(:encoder_missed_steps_decay_y, float_value) == - {"missed step decay, y-axis", steps, "1.2"} - - assert Param.to_human(:encoder_missed_steps_decay_z, float_value) == - {"missed step decay, z-axis", steps, "1.2"} - - assert Param.to_human(:encoder_use_for_pos_x, 1) == - {"use encoders for positioning, x-axis", nil, true} - - assert Param.to_human(:encoder_use_for_pos_y, 1) == - {"use encoders for positioning, y-axis", nil, true} - - assert Param.to_human(:encoder_use_for_pos_z, 1) == - {"use encoders for positioning, z-axis", nil, true} - - assert Param.to_human(:encoder_invert_x, 1) == - {"invert encoders, x-axis", nil, true} - - assert Param.to_human(:encoder_invert_y, 1) == - {"invert encoders, y-axis", nil, true} - - assert Param.to_human(:encoder_invert_z, 1) == - {"invert encoders, z-axis", nil, true} - - assert Param.to_human(:movement_stop_at_max_x, 1) == - {"stop at max, x-axis", nil, true} - - assert Param.to_human(:movement_stop_at_max_y, 1) == - {"stop at max, y-axis", nil, true} - - assert Param.to_human(:movement_stop_at_max_z, 1) == - {"stop at max, z-axis", nil, true} - - assert Param.to_human(:pin_guard_1_active_state, 0) == - {"pin guard 1 safe state", nil, "HIGH"} - - assert Param.to_human(:pin_guard_2_active_state, 0) == - {"pin guard 2 safe state", nil, "HIGH"} - - assert Param.to_human(:pin_guard_3_active_state, 0) == - {"pin guard 3 safe state", nil, "HIGH"} - - assert Param.to_human(:pin_guard_4_active_state, 0) == - {"pin guard 4 safe state", nil, "HIGH"} - - assert Param.to_human(:pin_guard_5_active_state, 0) == - {"pin guard 5 safe state", nil, "HIGH"} + t(:pin_guard_5_time_out, 12, {"pin guard 5 timeout", "(seconds)", "12"}) + t(:pin_guard_5_pin_nr, 12, {"pin guard 5 pin number", nil, "12"}) + t(:pin_guard_5_active_state, 0, {"pin guard 5 safe state", nil, "HIGH"}) + t(:pin_guard_4_time_out, 12, {"pin guard 4 timeout", "(seconds)", "12"}) + t(:pin_guard_4_pin_nr, 12, {"pin guard 4 pin number", nil, "12"}) + t(:pin_guard_4_active_state, 0, {"pin guard 4 safe state", nil, "HIGH"}) + t(:pin_guard_3_time_out, 1.0, {"pin guard 3 timeout", "(seconds)", "1"}) + t(:pin_guard_3_pin_nr, 1.0, {"pin guard 3 pin number", nil, "1"}) + t(:pin_guard_3_active_state, 0, {"pin guard 3 safe state", nil, "HIGH"}) + t(:pin_guard_2_time_out, 1.0, {"pin guard 2 timeout", "(seconds)", "1"}) + t(:pin_guard_2_pin_nr, 1.0, {"pin guard 2 pin number", nil, "1"}) + t(:pin_guard_2_active_state, 0, {"pin guard 2 safe state", nil, "HIGH"}) + t(:pin_guard_1_time_out, 1.0, {"pin guard 1 timeout", "(seconds)", "1"}) + t(:pin_guard_1_pin_nr, 1.0, {"pin guard 1 pin number", nil, "1"}) + t(:pin_guard_1_active_state, 0, {"pin guard 1 safe state", nil, "HIGH"}) + t(:param_use_eeprom, 1, {"use eeprom", nil, true}) + t(:param_test, 1, {"param_test", nil, true}) + t(:param_mov_nr_retry, 1.0, {"max retries", nil, "1"}) + t(:param_e_stop_on_mov_err, 1, {"e-stop on movement errors", nil, true}) + t(:param_config_ok, 1, {"param_config_ok", nil, true}) + t(:movement_stop_at_max_z, 1, {"stop at max, z-axis", nil, true}) + t(:movement_stop_at_max_y, 1, {"stop at max, y-axis", nil, true}) + t(:movement_stop_at_max_x, 1, {"stop at max, x-axis", nil, true}) + t(:movement_stop_at_home_z, 1, {"stop at home, z-axis", nil, true}) + t(:movement_stop_at_home_y, 1, {"stop at home, y-axis", nil, true}) + t(:movement_stop_at_home_x, 1, {"stop at home, x-axis", nil, true}) + t(:movement_secondary_motor_x, 1, {"enable 2nd x motor", nil, true}) + t(:movement_secondary_motor_invert_x, 1, {"invert 2nd x motor", nil, true}) + t(:movement_microsteps_z, float_value, {"microsteps, z-axis", nil, "1.2"}) + t(:movement_microsteps_y, float_value, {"microsteps, y-axis", nil, "1.2"}) + t(:movement_microsteps_x, float_value, {"microsteps, x-axis", nil, "1.2"}) + t(:movement_keep_active_z, 1, {"always power motors, z-axis", nil, true}) + t(:movement_keep_active_y, 1, {"always power motors, y-axis", nil, true}) + t(:movement_keep_active_x, 1, {"always power motors, x-axis", nil, true}) + t(:movement_invert_motor_z, 1, {"invert motor, z-axis", nil, true}) + t(:movement_invert_motor_y, 1, {"invert motor, y-axis", nil, true}) + t(:movement_invert_motor_x, 1, {"invert motor, x-axis", nil, true}) + t(:movement_invert_endpoints_z, 1, {"swap endstops, z-axis", nil, true}) + t(:movement_invert_endpoints_y, 1, {"swap endstops, y-axis", nil, true}) + t(:movement_invert_endpoints_x, 1, {"swap endstops, x-axis", nil, true}) + t(:movement_home_at_boot_z, 1, {"find home on boot, z-axis", nil, true}) + t(:movement_home_at_boot_y, 1, {"find home on boot, y-axis", nil, true}) + t(:movement_home_at_boot_x, 1, {"find home on boot, x-axis", nil, true}) + t(:movement_enable_endpoints_z, 1, {"enable endstops, z-axis", nil, true}) + t(:movement_enable_endpoints_y, 1, {"enable endstops, y-axis", nil, true}) + t(:movement_enable_endpoints_x, 1, {"enable endstops, x-axis", nil, true}) + t(:encoder_type_z, 1.2, {"encoder type, z-axis", nil, "1.2"}) + t(:encoder_type_y, 1.2, {"encoder type, y-axis", nil, "1.2"}) + t(:encoder_type_x, 1.2, {"encoder type, x-axis", nil, "1.2"}) + t(:encoder_invert_z, 1, {"invert encoders, z-axis", nil, true}) + t(:encoder_invert_y, 1, {"invert encoders, y-axis", nil, true}) + t(:encoder_invert_x, 1, {"invert encoders, x-axis", nil, true}) + + t( + :movement_motor_current_x, + float_value, + {"motor current, x-axis", "(milliamps)", "1.2"} + ) + + t( + :movement_motor_current_y, + float_value, + {"motor current, y-axis", "(milliamps)", "1.2"} + ) + + t( + :movement_motor_current_z, + float_value, + {"motor current, z-axis", "(milliamps)", "1.2"} + ) + + t( + :movement_stall_sensitivity_x, + float_value, + {"stall sensitivity, x-axis", nil, "1.2"} + ) + + t( + :movement_stall_sensitivity_y, + float_value, + {"stall sensitivity, y-axis", nil, "1.2"} + ) + + t( + :movement_stall_sensitivity_z, + float_value, + {"stall sensitivity, z-axis", nil, "1.2"} + ) + + t( + :movement_timeout_x, + float_value, + {"timeout after, x-axis", seconds, "1.2"} + ) + + t( + :movement_timeout_y, + float_value, + {"timeout after, y-axis", seconds, "1.2"} + ) + + t( + :movement_timeout_z, + float_value, + {"timeout after, z-axis", seconds, "1.2"} + ) + + t( + :movement_step_per_mm_x, + float_value, + {"steps per mm, x-axis", steps_per_mm, "1.2"} + ) + + t( + :movement_step_per_mm_y, + float_value, + {"steps per mm, y-axis", steps_per_mm, "1.2"} + ) + + t( + :movement_step_per_mm_z, + float_value, + {"steps per mm, z-axis", steps_per_mm, "1.2"} + ) + + t( + :movement_min_spd_x, + float_value, + {"minimum speed, x-axis", steps_per_s, "1.2"} + ) + + t( + :movement_min_spd_y, + float_value, + {"minimum speed, y-axis", steps_per_s, "1.2"} + ) + + t( + :movement_min_spd_z, + float_value, + {"minimum speed, z-axis", steps_per_s, "1.2"} + ) + + t( + :movement_home_spd_x, + float_value, + {"homing speed, x-axis", steps_per_s, "1.2"} + ) + + t( + :movement_home_spd_y, + float_value, + {"homing speed, y-axis", steps_per_s, "1.2"} + ) + + t( + :movement_home_spd_z, + float_value, + {"homing speed, z-axis", steps_per_s, "1.2"} + ) + + t( + :movement_max_spd_x, + float_value, + {"max speed, x-axis", steps_per_s, "1.2"} + ) + + t( + :movement_max_spd_y, + float_value, + {"max speed, y-axis", steps_per_s, "1.2"} + ) + + t( + :movement_max_spd_z, + float_value, + {"max speed, z-axis", steps_per_s, "1.2"} + ) + + t( + :movement_invert_2_endpoints_x, + 1, + {"invert endstops, x-axis", nil, true} + ) + + t( + :movement_invert_2_endpoints_y, + 1, + {"invert endstops, y-axis", nil, true} + ) + + t( + :movement_invert_2_endpoints_z, + 1, + {"invert endstops, z-axis", nil, true} + ) + + t( + :encoder_enabled_x, + 1, + {"enable encoders / stall detection, x-axis", nil, true} + ) + + t( + :encoder_enabled_y, + 1, + {"enable encoders / stall detection, y-axis", nil, true} + ) + + t( + :encoder_enabled_z, + 1, + {"enable encoders / stall detection, z-axis", nil, true} + ) + + t( + :encoder_scaling_x, + float_value, + {"encoder scaling, x-axis", nil, "1.2"} + ) + + t( + :encoder_scaling_y, + float_value, + {"encoder scaling, y-axis", nil, "1.2"} + ) + + t( + :encoder_scaling_z, + float_value, + {"encoder scaling, z-axis", nil, "1.2"} + ) + + t( + :encoder_missed_steps_decay_x, + float_value, + {"missed step decay, x-axis", steps, "1.2"} + ) + + t( + :encoder_missed_steps_decay_y, + float_value, + {"missed step decay, y-axis", steps, "1.2"} + ) + + t( + :encoder_missed_steps_decay_z, + float_value, + {"missed step decay, z-axis", steps, "1.2"} + ) + + t( + :encoder_use_for_pos_x, + 1, + {"use encoders for positioning, x-axis", nil, true} + ) + + t( + :encoder_use_for_pos_y, + 1, + {"use encoders for positioning, y-axis", nil, true} + ) + + t( + :encoder_use_for_pos_z, + 1, + {"use encoders for positioning, z-axis", nil, true} + ) + + t( + :movement_axis_nr_steps_z, + 1.0, + {"axis length, z-axis", "(steps)", "1"} + ) + + t( + :movement_axis_nr_steps_y, + 1.0, + {"axis length, y-axis", "(steps)", "1"} + ) + + t( + :movement_axis_nr_steps_x, + 1.0, + {"axis length, x-axis", "(steps)", "1"} + ) + + t( + :movement_steps_acc_dec_x, + 1.0, + {"accelerate for, x-axis", "(steps)", "1"} + ) + + t( + :movement_steps_acc_dec_y, + 1.0, + {"accelerate for, y-axis", "(steps)", "1"} + ) + + t( + :movement_steps_acc_dec_z, + 1.0, + {"accelerate for, z-axis", "(steps)", "1"} + ) + + t( + :movement_home_up_x, + 1.0, + {"negative coordinates only, x-axis", nil, true} + ) + + t( + :movement_home_up_y, + 1.0, + {"negative coordinates only, y-axis", nil, true} + ) + + t( + :movement_home_up_z, + 1.0, + {"negative coordinates only, z-axis", nil, true} + ) + + t( + :encoder_missed_steps_max_x, + 1.0, + {"max missed steps, x-axis", "(steps)", "1"} + ) + + t( + :encoder_missed_steps_max_y, + 1.0, + {"max missed steps, y-axis", "(steps)", "1"} + ) + + t( + :encoder_missed_steps_max_z, + 1.0, + {"max missed steps, z-axis", "(steps)", "1"} + ) end test "Handling of uknown parameters" do - assert :unknown_parameter == Param.decode(-999) + log = + capture_log(fn -> + assert :unknown_parameter == Param.decode(-999) + end) + + assert log =~ "unknown firmware parameter: -999" end end diff --git a/farmbot_os/config/host/test.exs b/farmbot_os/config/host/test.exs index 049ea7a4..55d871d8 100644 --- a/farmbot_os/config/host/test.exs +++ b/farmbot_os/config/host/test.exs @@ -43,3 +43,5 @@ config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig, firmware_flash_attempt_threshold: 0 config :plug, :validate_header_keys_during_test, true + +config :ex_unit, capture_logs: true diff --git a/farmbot_os/config/target/dev.exs b/farmbot_os/config/target/dev.exs index 3e622a11..8ac15582 100644 --- a/farmbot_os/config/target/dev.exs +++ b/farmbot_os/config/target/dev.exs @@ -99,13 +99,6 @@ config :farmbot, FarmbotOS.Configurator, config :farmbot, FarmbotOS.System, system_tasks: FarmbotOS.Platform.Target.SystemTasks -config :nerves_hub, - client: FarmbotOS.Platform.Target.NervesHubClient, - remote_iex: true, - public_keys: [File.read!("priv/staging.pub"), File.read!("priv/prod.pub")] - -config :nerves_hub, NervesHub.Socket, reconnect_interval: 5_000 - config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 5 config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig, diff --git a/farmbot_os/lib/farmbot_os/configurator/logger_socket.ex b/farmbot_os/lib/farmbot_os/configurator/logger_socket.ex index 83d87aa4..86ee3835 100644 --- a/farmbot_os/lib/farmbot_os/configurator/logger_socket.ex +++ b/farmbot_os/lib/farmbot_os/configurator/logger_socket.ex @@ -20,16 +20,7 @@ defmodule FarmbotOS.Configurator.LoggerSocket do end @impl :cowboy_websocket - def websocket_handle({:text, message}, state) do - case Jason.decode(message) do - {:ok, json} -> - websocket_handle({:json, json}, state) - - _ -> - _ = Logger.debug("discarding info: #{message}") - {:ok, state} - end - end + def websocket_handle({:text, _}, state), do: {:ok, state} @impl :cowboy_websocket def websocket_info(:after_connect, state) do diff --git a/farmbot_os/test/farmbot_os/configurator/logger_socket_test.exs b/farmbot_os/test/farmbot_os/configurator/logger_socket_test.exs index 9fa7979d..efc9af8c 100644 --- a/farmbot_os/test/farmbot_os/configurator/logger_socket_test.exs +++ b/farmbot_os/test/farmbot_os/configurator/logger_socket_test.exs @@ -3,12 +3,28 @@ defmodule FarmbotOS.Configurator.LoggerSocketTest do use Mimic alias FarmbotOS.Configurator.LoggerSocket setup :verify_on_exit! + import ExUnit.CaptureLog test "init/2" do - # TODO(Rick) Not sure what the real args are. - # Circle back to make this test more realistic - # later. expected = {:cowboy_websocket, :foo, :bar} assert expected == LoggerSocket.init(:foo, :bar) end + + test "websocket_init" do + assert {:ok, %{}} == LoggerSocket.websocket_init(nil) + assert_receive :after_connect + end + + test "websocket_handle (invalid JSON)" do + s = %{state: :yep} + msg = "Not JSON." + payl = {:text, msg} + assert {:ok, s} == LoggerSocket.websocket_handle(payl, s) + end + + test "websocket_info/2" do + assert capture_log(fn -> + LoggerSocket.websocket_info(:whatever, %{}) + end) =~ "Dropping :whatever" + end end diff --git a/farmbot_os/test/farmbot_os/configurator/router_test.exs b/farmbot_os/test/farmbot_os/configurator/router_test.exs index 277ab46b..92bd6fe3 100644 --- a/farmbot_os/test/farmbot_os/configurator/router_test.exs +++ b/farmbot_os/test/farmbot_os/configurator/router_test.exs @@ -8,6 +8,8 @@ defmodule FarmbotOS.Configurator.RouterTest do use Mimic setup :verify_on_exit! + import ExUnit.CaptureIO + @opts Router.init([]) # Stolen from https://github.com/phoenixframework/phoenix/blob/3f157c30ceae8d1eb524fdd05b5e3de10e434c42/lib/phoenix/test/conn_test.ex#L438 defp redirected_to(conn, status \\ 302) @@ -34,6 +36,7 @@ defmodule FarmbotOS.Configurator.RouterTest do Router.call(conn, @opts) end + @tag :capture_log test "index after reset" do FarmbotOS.Configurator.ConfigDataLayer |> expect(:load_last_reset_reason, fn -> "whoops!" end) @@ -45,6 +48,7 @@ defmodule FarmbotOS.Configurator.RouterTest do assert conn.resp_body =~ "whoops!" end + @tag :capture_log test "redirects" do redirects = [ "/check_network_status.txt", @@ -66,6 +70,7 @@ defmodule FarmbotOS.Configurator.RouterTest do end) end + @tag :capture_log test "celeryscript requests don't get listed as last reset reason" do FarmbotOS.Configurator.ConfigDataLayer |> expect(:load_last_reset_reason, fn -> "CeleryScript request." end) @@ -75,6 +80,7 @@ defmodule FarmbotOS.Configurator.RouterTest do refute conn.resp_body =~ "CeleryScript request." end + @tag :capture_log test "no reset reason" do FarmbotOS.Configurator.ConfigDataLayer |> expect(:load_last_reset_reason, fn -> nil end) @@ -84,6 +90,7 @@ defmodule FarmbotOS.Configurator.RouterTest do refute conn.resp_body =~ "
" end + @tag :capture_log test "captive portal" do conn = conn(:get, "/generate_204") conn = Router.call(conn, @opts) @@ -94,6 +101,7 @@ defmodule FarmbotOS.Configurator.RouterTest do assert conn.status == 302 end + @tag :capture_log test "network index" do FarmbotOS.Configurator.FakeNetworkLayer |> expect(:list_interfaces, fn -> @@ -108,6 +116,7 @@ defmodule FarmbotOS.Configurator.RouterTest do assert conn.resp_body =~ "eth0" end + @tag :capture_log test "select network sets session data" do conn = conn(:post, "select_interface") conn = Router.call(conn, @opts) @@ -124,6 +133,7 @@ defmodule FarmbotOS.Configurator.RouterTest do assert get_session(conn, "ifname") == "wlan0" end + @tag :capture_log test "config wired" do conn = conn(:get, "/config_wired") @@ -133,6 +143,7 @@ defmodule FarmbotOS.Configurator.RouterTest do assert conn.resp_body =~ "Advanced settings" end + @tag :capture_log test "config wireless SSID list" do FarmbotOS.Configurator.FakeNetworkLayer |> expect(:scan, fn _ -> @@ -154,6 +165,7 @@ defmodule FarmbotOS.Configurator.RouterTest do assert conn.resp_body =~ "Test Network" end + @tag :capture_log test "config wireless" do # No SSID or SECURITY conn = @@ -238,6 +250,7 @@ defmodule FarmbotOS.Configurator.RouterTest do assert conn.resp_body =~ "unknown or unsupported" end + @tag :capture_log test "config_network" do params = %{ "dns_name" => "super custom", @@ -290,6 +303,7 @@ defmodule FarmbotOS.Configurator.RouterTest do assert redirected_to(conn) == "/credentials" end + @tag :capture_log test "credentials index" do FarmbotOS.Configurator.ConfigDataLayer |> expect(:load_email, fn -> "test@test.org" end) @@ -302,6 +316,7 @@ defmodule FarmbotOS.Configurator.RouterTest do assert conn.resp_body =~ "https://my.farm.bot" end + @tag :capture_log test "configure credentials" do params = %{ "email" => "test@test.org", @@ -334,6 +349,7 @@ defmodule FarmbotOS.Configurator.RouterTest do assert redirected_to(conn) == "/credentials" end + @tag :capture_log test "finish" do conn = conn(:get, "/finish") @@ -342,6 +358,7 @@ defmodule FarmbotOS.Configurator.RouterTest do assert redirected_to(conn) == "/" end + @tag :capture_log test "404" do conn = conn(:get, "/whoops") @@ -350,6 +367,7 @@ defmodule FarmbotOS.Configurator.RouterTest do assert conn.resp_body == "Page not found" end + @tag :capture_log test "500" do FarmbotOS.Configurator.FakeNetworkLayer |> expect(:scan, fn _ -> @@ -360,20 +378,26 @@ defmodule FarmbotOS.Configurator.RouterTest do ] end) - conn = - conn(:get, "/config_wireless") - |> init_test_session(%{"ifname" => "wlan0"}) - |> Router.call(@opts) + crasher = fn -> + conn = + conn(:get, "/config_wireless") + |> init_test_session(%{"ifname" => "wlan0"}) + |> Router.call(@opts) - assert conn.status == 500 + assert conn.status == 500 + end + + assert capture_io(:stderr, crasher) =~ "render error" end + @tag :capture_log test "/scheduler_debugger" do kon = get_con("/scheduler_debugger") assert String.contains?(kon.resp_body, "scheduler_debugger.js") assert String.contains?(kon.resp_body, "Scheduler Debugger") end + @tag :capture_log test "/logger" do kon = get_con("/logger") @@ -391,6 +415,7 @@ defmodule FarmbotOS.Configurator.RouterTest do end) end + @tag :capture_log test "/api/telemetry/cpu_usage" do {:ok, json} = Jason.decode(get_con("/api/telemetry/cpu_usage").resp_body) assert Enum.count(json) == 10 @@ -400,6 +425,7 @@ defmodule FarmbotOS.Configurator.RouterTest do assert(is_integer(zero["value"])) end + @tag :capture_log test "/finish" do expect(ConfigDataLayer, :save_config, 1, fn _conf -> :ok diff --git a/farmbot_os/test/farmbot_os/lua_test.exs b/farmbot_os/test/farmbot_os/lua_test.exs index 25c26bc5..9fafdb0d 100644 --- a/farmbot_os/test/farmbot_os/lua_test.exs +++ b/farmbot_os/test/farmbot_os/lua_test.exs @@ -4,6 +4,7 @@ defmodule FarmbotOS.LuaTest do setup :verify_on_exit! alias FarmbotOS.Lua + @tag :capture_log test "evaluates Lua" do assert Lua.eval_assertion("Returns 'true'", "return true") {:error, message1} = Lua.eval_assertion("Returns 'true'", "-1") diff --git a/farmbot_os/test/farmbot_os/sys_calls_test.exs b/farmbot_os/test/farmbot_os/sys_calls_test.exs index bbda1b7b..28be53a4 100644 --- a/farmbot_os/test/farmbot_os/sys_calls_test.exs +++ b/farmbot_os/test/farmbot_os/sys_calls_test.exs @@ -11,6 +11,7 @@ defmodule FarmbotOS.SysCallsTest do use Mimic setup :verify_on_exit! + import ExUnit.CaptureIO test "emergency_unlock" do expect(FarmbotFirmware, :command, fn {:command_emergency_unlock, []} -> @@ -71,6 +72,7 @@ defmodule FarmbotOS.SysCallsTest do assert {:error, "Could not find peripheral by id: 11"} == result6 end + @tag :capture_log test "sync() success" do # Expect 5 calls and an :ok response. expect(FarmbotExt.API.Reconciler, :sync_group, 5, fn changeset, _group -> @@ -81,16 +83,21 @@ defmodule FarmbotOS.SysCallsTest do {:ok, %{wut: module}} end) - assert :ok == SysCalls.sync() + assert capture_io(fn -> + assert :ok == SysCalls.sync() + end) =~ "green really_fast_blink" end + @tag :capture_log test "sync() failure" do # Expect 5 calls and an :ok response. expect(FarmbotExt.API, :get_changeset, fn FarmbotCore.Asset.Sync -> "this is a test" end) - assert {:error, "\"this is a test\""} == SysCalls.sync() + assert capture_io(fn -> + assert {:error, "\"this is a test\""} == SysCalls.sync() + end) =~ "green slow_blink" end test "get_sequence(id)" do diff --git a/farmbot_os/test/farmbot_os/syscalls/movement_test.exs b/farmbot_os/test/farmbot_os/syscalls/movement_test.exs index 79962e24..7bfb73cf 100644 --- a/farmbot_os/test/farmbot_os/syscalls/movement_test.exs +++ b/farmbot_os/test/farmbot_os/syscalls/movement_test.exs @@ -91,6 +91,7 @@ defmodule FarmbotOS.SysCalls.MovementTest do assert msg == error_log end + @tag :capture_log test "move_absolute/4 - error (in tuple)" do expect(FarmbotFirmware, :request, 1, fn {:parameter_read, [_]} -> {:error, "boom"} diff --git a/farmbot_os/test/farmbot_os/syscalls/pin_control_test.exs b/farmbot_os/test/farmbot_os/syscalls/pin_control_test.exs index 59e1da1b..0a311b9e 100644 --- a/farmbot_os/test/farmbot_os/syscalls/pin_control_test.exs +++ b/farmbot_os/test/farmbot_os/syscalls/pin_control_test.exs @@ -6,6 +6,7 @@ defmodule FarmbotOS.SysCalls.PinControlTest do alias FarmbotCore.Asset.Peripheral @digital 0 + @tag :capture_log test "read_pin with %Peripheral{}, pin is 1" do expect(FarmbotFirmware, :request, 1, fn {:pin_read, [p: 13, m: 0]} -> @@ -20,6 +21,7 @@ defmodule FarmbotOS.SysCalls.PinControlTest do assert 1 == PinControl.read_pin(peripheral, @digital) end + @tag :capture_log test "read_pin with %Peripheral{}, pin is 0" do expect(FarmbotFirmware, :request, 1, fn {:pin_read, [p: 13, m: 0]} -> @@ -30,6 +32,7 @@ defmodule FarmbotOS.SysCalls.PinControlTest do assert 0 == PinControl.read_pin(peripheral, @digital) end + @tag :capture_log test "toggle_pin, 1 => 0" do expect(FarmbotCore.Asset, :get_peripheral_by_pin, 1, fn 12 -> nil @@ -48,6 +51,7 @@ defmodule FarmbotOS.SysCalls.PinControlTest do assert :ok = PinControl.toggle_pin(12) end + @tag :capture_log test "toggle_pin, 0 => 1" do expect(FarmbotCore.Asset, :get_peripheral_by_pin, 1, fn 12 -> nil diff --git a/farmbot_os/test/farmbot_os/syscalls/point_lookup_test.exs b/farmbot_os/test/farmbot_os/syscalls/point_lookup_test.exs index 7385a430..45459818 100644 --- a/farmbot_os/test/farmbot_os/syscalls/point_lookup_test.exs +++ b/farmbot_os/test/farmbot_os/syscalls/point_lookup_test.exs @@ -101,6 +101,7 @@ defmodule FarmbotOS.SysCalls.PointLookupTest do assert pg == PointLookup.get_point_group(pg.id) end + @tag :capture_log test "PointLookup.get_point_group/1 - string" do Repo.delete_all(PointGroup) Repo.delete_all(Point) diff --git a/test/support/asset_fixtures.ex b/test/support/asset_fixtures.ex index 9ad9e386..80024114 100644 --- a/test/support/asset_fixtures.ex +++ b/test/support/asset_fixtures.ex @@ -1,6 +1,4 @@ defmodule Farmbot.TestSupport.AssetFixtures do - alias FarmbotCore.Asset - alias FarmbotCore.Asset.{ Device, FarmEvent, @@ -10,12 +8,12 @@ defmodule Farmbot.TestSupport.AssetFixtures do Sequence } - def regimen_instance(regimen_params, farm_event_params, params \\ %{}) do - regimen = regimen(regimen_params) - farm_event = regimen_event(regimen, farm_event_params) - params = Map.merge(%{id: :rand.uniform(10000), monitor: false}, params) - Asset.new_regimen_instance!(farm_event, params) - end + # def regimen_instance(regimen_params, farm_event_params, params \\ %{}) do + # regimen = regimen(regimen_params) + # farm_event = regimen_event(regimen, farm_event_params) + # params = Map.merge(%{id: :rand.uniform(10000), monitor: false}, params) + # Asset.new_regimen_instance!(farm_event, params) + # end def fbos_config(params \\ %{}) do default = %{ @@ -77,30 +75,6 @@ defmodule Farmbot.TestSupport.AssetFixtures do |> Repo.insert!() end - def sequence_event(sequence, params \\ %{}) do - now = DateTime.utc_now() - - params = - Map.merge( - %{ - id: :rand.uniform(1_000_000), - monitor: false, - executable_type: "Sequence", - executable_id: sequence.id, - start_time: now, - end_time: now, - repeat: 0, - time_unit: "never" - }, - params - ) - - FarmEvent - |> struct() - |> FarmEvent.changeset(params) - |> Repo.insert!() - end - @doc """ Instantiates, but does not create, a %Device{} """