Merge branch 'purge-context' of github.com:ConnorRigby/farmbot_os into purge-context
This commit is contained in:
commit
fbe97119c3
|
@ -30,6 +30,7 @@ config :farmbot, ecto_repos: repos
|
|||
for repo <- [Farmbot.Repo.A, Farmbot.Repo.B] do
|
||||
config :farmbot, repo,
|
||||
adapter: Sqlite.Ecto2,
|
||||
loggers: [],
|
||||
database: "tmp/#{repo}_dev.sqlite3"
|
||||
end
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ config :farmbot, Farmbot.Repo.B,
|
|||
|
||||
config :farmbot, Farmbot.System.ConfigStorage,
|
||||
adapter: Sqlite.Ecto2,
|
||||
database: "test_tmp/farmbot_config_storage_test",
|
||||
pool: Ecto.Adapters.SQL.Sandbox
|
||||
database: "test_tmp/farmbot_config_storage_test"
|
||||
# pool: Ecto.Adapters.SQL.Sandbox
|
||||
|
||||
config :farmbot, ecto_repos: [Farmbot.Repo.A, Farmbot.Repo.B, Farmbot.System.ConfigStorage]
|
||||
|
|
|
@ -121,13 +121,15 @@ defmodule Farmbot.Bootstrap.Supervisor do
|
|||
ConfigStorage.update_config_value(:string, "authorization", "last_shutdown_reason", nil)
|
||||
|
||||
children = [
|
||||
worker(Farmbot.Bootstrap.AuthTask, []),
|
||||
supervisor(Farmbot.Firmware.Supervisor, []),
|
||||
supervisor(Farmbot.BotState.Supervisor, []),
|
||||
worker(Farmbot.Bootstrap.AuthTask, []),
|
||||
supervisor(Farmbot.Firmware.Supervisor, []),
|
||||
supervisor(Farmbot.BotState.Supervisor, []),
|
||||
supervisor(Farmbot.BotState.Transport.Supervisor, []),
|
||||
supervisor(Farmbot.HTTP.Supervisor, []),
|
||||
supervisor(Farmbot.Repo.Supervisor, []),
|
||||
supervisor(Farmbot.Farmware.Supervisor, [])
|
||||
supervisor(Farmbot.HTTP.Supervisor, []),
|
||||
supervisor(Farmbot.Repo.Supervisor, []),
|
||||
supervisor(Farmbot.Farmware.Supervisor, []),
|
||||
supervisor(Farmbot.Regimen.Supervisor, []),
|
||||
supervisor(Farmbot.FarmEvent.Supervisor, [])
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one]
|
||||
|
|
|
@ -257,7 +257,7 @@ defmodule Farmbot.BotState do
|
|||
end
|
||||
|
||||
def handle_call({:unregister_farmware, fw}, _, state) do
|
||||
new_pi = Map.delete(state.process_info.farmware, fw.name)
|
||||
new_pi = Map.delete(state.process_info.farmwares, fw.name)
|
||||
new_state = %{state | process_info: %{farmwares: new_pi}}
|
||||
{:reply, :ok, [new_state], new_state}
|
||||
end
|
||||
|
|
|
@ -88,17 +88,19 @@ defmodule Farmbot.BotState.Transport.GenMQTT.Client do
|
|||
end
|
||||
|
||||
def on_publish(["bot", _, "sync", kind, id], msg, state) do
|
||||
mod = Module.concat(["Farmbot", "Repo", kind])
|
||||
if Code.ensure_loaded?(mod) do
|
||||
body = struct(mod)
|
||||
sync_cmd = msg |> Poison.decode!(as: struct(Farmbot.Repo.SyncCmd, kind: mod, body: body, id: id))
|
||||
Farmbot.Repo.register_sync_cmd(sync_cmd)
|
||||
spawn fn() ->
|
||||
mod = Module.concat(["Farmbot", "Repo", kind])
|
||||
if Code.ensure_loaded?(mod) do
|
||||
body = struct(mod)
|
||||
sync_cmd = msg |> Poison.decode!(as: struct(Farmbot.Repo.SyncCmd, kind: mod, body: body, id: id))
|
||||
Farmbot.Repo.register_sync_cmd(sync_cmd)
|
||||
|
||||
if Farmbot.System.ConfigStorage.get_config_value(:bool, "settings", "auto_sync") do
|
||||
Farmbot.Repo.flip()
|
||||
if Farmbot.System.ConfigStorage.get_config_value(:bool, "settings", "auto_sync") do
|
||||
Farmbot.Repo.flip()
|
||||
end
|
||||
else
|
||||
Logger.warn 2, "Unknown syncable: #{mod}: #{inspect Poison.decode!(msg)}"
|
||||
end
|
||||
else
|
||||
Logger.warn 2, "Unknown syncable: #{mod}: #{inspect Poison.decode!(msg)}"
|
||||
end
|
||||
{:ok, state}
|
||||
end
|
||||
|
|
17
lib/farmbot/farm_event/execution/execution.ex
Normal file
17
lib/farmbot/farm_event/execution/execution.ex
Normal file
|
@ -0,0 +1,17 @@
|
|||
defprotocol Farmbot.FarmEvent.Execution do
|
||||
@moduledoc """
|
||||
Protocol to be implemented by any struct that can be executed by
|
||||
Farmbot.FarmEvent.Manager.
|
||||
"""
|
||||
|
||||
@typedoc "Data to be executed."
|
||||
@type data :: map
|
||||
|
||||
@doc """
|
||||
Execute an item.
|
||||
* `data` - A `Farmbot.Database.Syncable` struct that is implemented.
|
||||
* `now` - `DateTime` of execution.
|
||||
"""
|
||||
@spec execute_event(data, DateTime.t) :: any
|
||||
def execute_event(data, now)
|
||||
end
|
10
lib/farmbot/farm_event/execution/regimen.ex
Normal file
10
lib/farmbot/farm_event/execution/regimen.ex
Normal file
|
@ -0,0 +1,10 @@
|
|||
defimpl Farmbot.FarmEvent.Execution, for: Farmbot.Repo.Regimen do
|
||||
|
||||
def execute_event(regimen, now) do
|
||||
case Process.whereis(:"regimen-#{regimen.id}") do
|
||||
nil -> {:ok, _pid} = Farmbot.Regimen.Supervisor.add_child(regimen, now)
|
||||
pid -> {:ok, pid}
|
||||
end
|
||||
end
|
||||
|
||||
end
|
15
lib/farmbot/farm_event/execution/sequence.ex
Normal file
15
lib/farmbot/farm_event/execution/sequence.ex
Normal file
|
@ -0,0 +1,15 @@
|
|||
defimpl Farmbot.FarmEvent.Execution, for: Farmbot.Repo.Sequence do
|
||||
|
||||
def execute_event(sequence, _now) do
|
||||
with {:ok, ast} <- Farmbot.CeleryScript.AST.decode(sequence) do
|
||||
case Farmbot.CeleryScript.execute(ast) do
|
||||
{:ok, _} -> :ok
|
||||
{:error, reason, _} ->
|
||||
{:error, reason}
|
||||
end
|
||||
else
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
end
|
223
lib/farmbot/farm_event/manager.ex
Normal file
223
lib/farmbot/farm_event/manager.ex
Normal file
|
@ -0,0 +1,223 @@
|
|||
defmodule Farmbot.FarmEvent.Manager do
|
||||
@moduledoc """
|
||||
Manages execution of FarmEvents.
|
||||
|
||||
## Rules for FarmEvent execution.
|
||||
* Regimen
|
||||
* ignore `end_time`.
|
||||
* ignore calendar.
|
||||
* if start_time is more than 60 seconds passed due, assume it already started, and don't start it again.
|
||||
* Sequence
|
||||
* if `start_time` is late, check the calendar.
|
||||
* for each item in the calendar, check if it's event is more than 60 seconds in the past. if not, execute it.
|
||||
* if there is only one event in the calendar, ignore the `end_time`
|
||||
"""
|
||||
use GenServer
|
||||
use Farmbot.Logger
|
||||
alias Farmbot.FarmEvent.Execution
|
||||
alias Farmbot.Repo.FarmEvent
|
||||
|
||||
@checkup_time 20_000
|
||||
|
||||
## GenServer
|
||||
|
||||
defmodule State do
|
||||
@moduledoc false
|
||||
defstruct [timer: nil, last_time_index: %{}]
|
||||
end
|
||||
|
||||
@doc false
|
||||
def start_link do
|
||||
GenServer.start_link(__MODULE__, [], [name: __MODULE__])
|
||||
end
|
||||
|
||||
def init([]) do
|
||||
send self(), :checkup
|
||||
{:ok, struct(State)}
|
||||
end
|
||||
|
||||
def handle_info(:checkup, state) do
|
||||
now = get_now()
|
||||
|
||||
all_events = Farmbot.Repo.current_repo().all(Farmbot.Repo.FarmEvent)
|
||||
|
||||
# do checkup is the bulk of the work.
|
||||
{late_events, new} = do_checkup(all_events, now, state)
|
||||
|
||||
#TODO(Connor) Conditionally start events based on some state info.
|
||||
unless Enum.empty?(late_events) do
|
||||
Logger.info 3, "Time for event to run at: #{now.hour}:#{now.minute}"
|
||||
start_events(late_events, now)
|
||||
end
|
||||
|
||||
# Start a new timer.
|
||||
timer = Process.send_after self(), :checkup, @checkup_time
|
||||
{:noreply, %{new | timer: timer}}
|
||||
end
|
||||
|
||||
defp do_checkup(list, time, late_events \\ [], state)
|
||||
|
||||
defp do_checkup([], _now, late_events, state), do: {late_events, state}
|
||||
|
||||
defp do_checkup([farm_event | rest], now, late_events, state) do
|
||||
# new_late will be a executable event (Regimen or Sequence.)
|
||||
{new_late_event, last_time} = check_event(farm_event, now, state.last_time_index[farm_event.id])
|
||||
|
||||
# update state.
|
||||
new_state = %{state | last_time_index: Map.put(state.last_time_index, farm_event.id, last_time)}
|
||||
case new_late_event do
|
||||
# if `new_late_event` is nil, don't accumulate it.
|
||||
nil -> do_checkup(rest, now, late_events, new_state)
|
||||
# if there is a new event, accumulate it.
|
||||
event -> do_checkup(rest, now, [event | late_events], new_state)
|
||||
end
|
||||
end
|
||||
|
||||
defp check_event(%FarmEvent{} = f, now, last_time) do
|
||||
# Get the executable out of the database this may fail.
|
||||
# mod_list = ["Farmbot", "Repo", f.executable_type]
|
||||
mod = Module.safe_concat([f.executable_type])
|
||||
|
||||
event = lookup(mod, f.executable_id)
|
||||
|
||||
# build a local start time and end time
|
||||
start_time = Timex.parse! f.start_time, "{ISO:Extended}"
|
||||
end_time = Timex.parse! f.end_time, "{ISO:Extended}"
|
||||
# start_time = f.start_time
|
||||
# end_time = f.end_time
|
||||
# get local bool of if the event is started and finished.
|
||||
started? = Timex.after? now, start_time
|
||||
finished? = Timex.after? now, end_time
|
||||
|
||||
case f.executable_type do
|
||||
"Elixir.Farmbot.Repo.Regimen" -> maybe_start_regimen(started?, start_time, last_time, event, now)
|
||||
"Elixir.Farmbot.Repo.Sequence" -> maybe_start_sequence(started?, finished?, f, last_time, event, now)
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_start_regimen(started?, start_time, last_time, event, now)
|
||||
defp maybe_start_regimen(_started? = true, start_time, last_time, event, now) do
|
||||
case is_too_old?(now, start_time) do
|
||||
true ->
|
||||
Logger.debug 3, "regimen #{event.name} (#{event.id}) is too old to start."
|
||||
{nil, last_time}
|
||||
false ->
|
||||
Logger.debug 3, "regimen #{event.name} (#{event.id}) not to old; starting."
|
||||
{event, now}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_start_regimen(_started? = false, start_time, last_time, event, _) do
|
||||
Logger.debug 3, "regimen #{event.name} (#{event.id}) is not started yet. (#{inspect start_time}) (#{inspect Timex.now()})"
|
||||
{nil, last_time}
|
||||
end
|
||||
|
||||
defp lookup(module, sr_id) do
|
||||
case Farmbot.Repo.current_repo().get(module, sr_id) do
|
||||
nil -> raise "Could not find #{module} by id: #{sr_id}"
|
||||
item -> item
|
||||
end
|
||||
end
|
||||
|
||||
# signals the start of a sequence based on the described logic.
|
||||
defp maybe_start_sequence(started?, finished?, farm_event, last_time, event, now)
|
||||
|
||||
# We only want to check if the sequence is started, and not finished.
|
||||
defp maybe_start_sequence(_started? = true, _finished? = false, farm_event, last_time, event, now) do
|
||||
{run?, next_time} = should_run_sequence?(farm_event.calendar, last_time, now)
|
||||
case run? do
|
||||
true -> {event, next_time}
|
||||
false -> {nil, last_time}
|
||||
end
|
||||
end
|
||||
|
||||
# if `farm_event.time_unit` is "never" we can't use the `end_time`.
|
||||
# if we have no `last_time`, time to execute.
|
||||
defp maybe_start_sequence(true, _, %{time_unit: "never"} = f, _last_time = nil, event, now) do
|
||||
Logger.debug 3, "Ignoring end_time."
|
||||
case should_run_sequence?(f.calendar, nil, now) do
|
||||
{true, next} -> {event, next}
|
||||
{false, _} -> {nil, nil }
|
||||
end
|
||||
end
|
||||
|
||||
# if started is false, the event isn't ready to be executed.
|
||||
defp maybe_start_sequence(_started? = false, _fin, _farm_event, last_time, event, _now) do
|
||||
Logger.debug 3, "sequence #{event.name} (#{event.id}) is not started yet."
|
||||
{nil, last_time}
|
||||
end
|
||||
|
||||
# if the event is finished (but not a "never" time_unit), we don't execute.
|
||||
defp maybe_start_sequence(_started?, _finished? = true, _farm_event, last_time, event, _now) do
|
||||
Logger.success 3, "sequence #{event.name} (#{event.id}) is finished."
|
||||
{nil, last_time}
|
||||
end
|
||||
|
||||
# Checks if we shoudl run a sequence or not. returns {event | nil, time | nil}
|
||||
defp should_run_sequence?(calendar, last_time, now)
|
||||
|
||||
# if there is no last time, check if time is passed now within 60 seconds.
|
||||
defp should_run_sequence?([first_time | _], nil, now) do;
|
||||
|
||||
Logger.debug 3, "Checking sequence event that hasn't run before #{first_time}"
|
||||
# convert the first_time to a DateTime
|
||||
dt = Timex.parse! first_time, "{ISO:Extended}"
|
||||
# if now is after the time, we are in fact late
|
||||
if Timex.after?(now, dt) do
|
||||
{true, now}
|
||||
else
|
||||
# make sure to return nil as the last time because it stil hasnt executed yet.
|
||||
Logger.debug 3, "Sequence Event not ready yet."
|
||||
{false, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp should_run_sequence?(calendar, last_time, now) do
|
||||
# get rid of all the items that happened before last_time
|
||||
filtered_calendar = Enum.filter(calendar, fn(iso_time) ->
|
||||
dt = Timex.parse! iso_time, "{ISO:Extended}"
|
||||
# we only want this time if it happened after the last_time
|
||||
Timex.after?(dt, last_time)
|
||||
end)
|
||||
|
||||
# if after filtering, there are events that need to be run
|
||||
# check if they are older than a minute ago,
|
||||
case filtered_calendar do
|
||||
[iso_time | _] ->
|
||||
dt = Timex.parse! iso_time, "{ISO:Extended}"
|
||||
if Timex.after?(now, dt) do
|
||||
{true, dt}
|
||||
# too_old? = is_too_old?(now, dt)
|
||||
# if too_old?, do: {false, last_time}, else: {true, dt}
|
||||
else
|
||||
Logger.debug 3, "Sequence Event not ready yet."
|
||||
{false, dt}
|
||||
end
|
||||
[] ->
|
||||
Logger.debug 3, "No items in calendar."
|
||||
{false, last_time}
|
||||
end
|
||||
end
|
||||
|
||||
# Enumeration is complete.
|
||||
defp start_events([], _now), do: :ok
|
||||
|
||||
# Enumerate the events to be started.
|
||||
defp start_events([event | rest], now) do
|
||||
# Spawn to be non blocking here. Maybe link to this process?
|
||||
spawn fn() -> Execution.execute_event(event, now) end
|
||||
# Continue enumeration.
|
||||
start_events(rest, now)
|
||||
end
|
||||
|
||||
# is then more than 1 minute in the past?
|
||||
defp is_too_old?(now, then) do
|
||||
time_str_fun = fn(dt) -> "#{dt.hour}:#{dt.minute}:#{dt.second}" end
|
||||
seconds = DateTime.to_unix(now, :second) - DateTime.to_unix(then, :second)
|
||||
c = seconds > 60 # not in MS here
|
||||
Logger.debug 3, "is checking #{time_str_fun.(now)} - #{time_str_fun.(then)} = #{seconds} seconds ago. is_too_old? => #{c}"
|
||||
c
|
||||
end
|
||||
|
||||
defp get_now(), do: Timex.now()
|
||||
end
|
16
lib/farmbot/farm_event/supervisor.ex
Normal file
16
lib/farmbot/farm_event/supervisor.ex
Normal file
|
@ -0,0 +1,16 @@
|
|||
defmodule Farmbot.FarmEvent.Supervisor do
|
||||
@moduledoc false
|
||||
use Supervisor
|
||||
|
||||
def start_link do
|
||||
Supervisor.start_link(__MODULE__, [], [name: __MODULE__])
|
||||
end
|
||||
|
||||
def init([]) do
|
||||
children = [
|
||||
worker(Farmbot.FarmEvent.Manager, [])
|
||||
]
|
||||
|
||||
supervise(children, strategy: :one_for_one)
|
||||
end
|
||||
end
|
|
@ -140,7 +140,7 @@ defmodule Farmbot.Firmware do
|
|||
end
|
||||
|
||||
defp do_begin_cmd(%Current{fun: fun, args: args, from: _from} = current, state, dispatch) do
|
||||
Logger.debug 3, "Firmware command: #{fun}#{inspect(args)}"
|
||||
# Logger.debug 3, "Firmware command: #{fun}#{inspect(args)}"
|
||||
if fun == :emergency_unlock, do: Farmbot.BotState.set_sync_status(:sync_now)
|
||||
|
||||
case apply(state.handler_mod, fun, [state.handler | args]) do
|
||||
|
|
|
@ -158,7 +158,7 @@ defmodule Farmbot.Firmware.UartHandler do
|
|||
end
|
||||
|
||||
defp do_write(bin, state, dispatch \\ []) do
|
||||
Logger.debug 3, "writing: #{bin}"
|
||||
# Logger.debug 3, "writing: #{bin}"
|
||||
case UART.write(state.nerves, bin) do
|
||||
:ok -> {:reply, :ok, dispatch, %{state | current_cmd: bin}}
|
||||
err -> {:reply, err, [], %{state | current_cmd: nil}}
|
||||
|
|
164
lib/farmbot/regimen/manager.ex
Normal file
164
lib/farmbot/regimen/manager.ex
Normal file
|
@ -0,0 +1,164 @@
|
|||
defmodule Farmbot.Regimen.Manager do
|
||||
@moduledoc "Manages a Regimen"
|
||||
|
||||
use Farmbot.Logger
|
||||
use GenServer
|
||||
alias Farmbot.Repo.Regimen
|
||||
|
||||
defmodule Error do
|
||||
@moduledoc false
|
||||
defexception [:epoch, :regimen, :message]
|
||||
end
|
||||
|
||||
defmodule Item do
|
||||
@moduledoc false
|
||||
@type t :: %__MODULE__{
|
||||
name: binary,
|
||||
time_offset: integer,
|
||||
sequence: Farmbot.CeleryScript.Ast.t
|
||||
}
|
||||
|
||||
defstruct [:time_offset, :sequence, :name]
|
||||
|
||||
def parse(%{time_offset: offset, sequence_id: sequence_id})
|
||||
do
|
||||
sequence = fetch_sequence(sequence_id)
|
||||
%__MODULE__{
|
||||
name: sequence.name,
|
||||
time_offset: offset,
|
||||
sequence: Farmbot.CeleryScript.AST.decode(sequence) |> elem(1)}
|
||||
end
|
||||
|
||||
def fetch_sequence(id) do
|
||||
case Farmbot.Repo.current_repo().get(Farmbot.Repo.Sequence, id) do
|
||||
nil -> raise "Could not find sequence by id: #{inspect id}"
|
||||
obj -> obj
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def start_link(regimen, time) do
|
||||
GenServer.start_link(__MODULE__, [regimen, time], name: :"regimen-#{regimen.id}")
|
||||
end
|
||||
|
||||
def init([regimen, time]) do
|
||||
# parse and sort the regimen items
|
||||
items = filter_items(regimen)
|
||||
first_item = List.first(items)
|
||||
regimen = %{regimen | regimen_items: items}
|
||||
epoch = build_epoch(time) || raise Error,
|
||||
message: "Could not determine EPOCH because no timezone was supplied.",
|
||||
epoch: :error, regimen: regimen
|
||||
|
||||
initial_state = %{
|
||||
next_execution: nil,
|
||||
regimen: regimen,
|
||||
epoch: epoch,
|
||||
timer: nil
|
||||
}
|
||||
|
||||
if first_item do
|
||||
state = build_next_state(regimen, first_item, self(), initial_state)
|
||||
{:ok, state}
|
||||
else
|
||||
Logger.warn 2, "[#{regimen.name}] has no items on regimen."
|
||||
:ignore
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(:execute, state) do
|
||||
{item, regimen} = pop_item(state.regimen)
|
||||
if item do
|
||||
do_item(item, regimen, state)
|
||||
else
|
||||
complete(regimen, state)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(:skip, state) do
|
||||
{item, regimen} = pop_item(state.regimen)
|
||||
if item do
|
||||
do_item(nil, regimen, state)
|
||||
else
|
||||
complete(regimen, state)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
defp complete(regimen, state) do
|
||||
Logger.success 2, "[#{regimen.name}] is complete!"
|
||||
# spawn fn() ->
|
||||
# RegSup.remove_child(regimen)
|
||||
# end
|
||||
{:stop, :normal, state}
|
||||
# {:noreply, :finished}
|
||||
end
|
||||
|
||||
defp filter_items(regimen) do
|
||||
regimen.regimen_items
|
||||
|> Enum.map(&Item.parse(&1))
|
||||
|> Enum.sort(&(&1.time_offset <= &2.time_offset))
|
||||
end
|
||||
|
||||
defp do_item(item, regimen, state) do
|
||||
if item do
|
||||
Logger.busy 2, "[#{regimen.name}] is going to execute: #{item.name}"
|
||||
Farmbot.CeleryScript.execute(item.sequence)
|
||||
end
|
||||
next_item = List.first(regimen.regimen_items)
|
||||
if next_item do
|
||||
new_state = build_next_state(regimen, next_item, self(), state)
|
||||
{:noreply, new_state}
|
||||
else
|
||||
complete(regimen, state)
|
||||
end
|
||||
end
|
||||
|
||||
def build_next_state(
|
||||
%Regimen{} = regimen,
|
||||
%Item{} = nx_itm,
|
||||
pid, state)
|
||||
do
|
||||
next_dt = Timex.shift(state.epoch, milliseconds: nx_itm.time_offset)
|
||||
timezone = Farmbot.System.ConfigStorage.get_config_value(:string, "settings", "timezone")
|
||||
now = Timex.now(timezone)
|
||||
offset_from_now = Timex.diff(next_dt, now, :milliseconds)
|
||||
|
||||
timer = if (offset_from_now < 0) and (offset_from_now < -60_000) do
|
||||
Logger.info 3, "[#{regimen.name}] #{[nx_itm.name]} has been scheduled " <>
|
||||
"to happen more than one minute ago: #{offset_from_now} Skipping it."
|
||||
Process.send_after(pid, :skip, 1000)
|
||||
else
|
||||
{msg, real_offset} = ensure_not_negative(offset_from_now)
|
||||
Process.send_after(pid, msg, real_offset)
|
||||
end
|
||||
|
||||
timestr = "#{next_dt.month}/#{next_dt.day}/#{next_dt.year} " <>
|
||||
"at: #{next_dt.hour}:#{next_dt.minute} (#{offset_from_now} milliseconds)"
|
||||
|
||||
Logger.info 3, "[#{regimen.name}] next item will execute on #{timestr}"
|
||||
|
||||
%{state | timer: timer,
|
||||
regimen: regimen,
|
||||
next_execution: next_dt}
|
||||
end
|
||||
|
||||
defp ensure_not_negative(offset) when offset < -60_000, do: {:skip, 1000}
|
||||
defp ensure_not_negative(offset) when offset < 0, do: {:execute, 1000}
|
||||
defp ensure_not_negative(offset), do: {:execute, offset}
|
||||
|
||||
@spec pop_item(Regimen.t) :: {Item.t | nil, Regimen.t}
|
||||
# when there is more than one item pop the top one
|
||||
defp pop_item(%Regimen{regimen_items: [do_this_one | items ]} = r) do
|
||||
{do_this_one, %Regimen{r | regimen_items: items}}
|
||||
end
|
||||
|
||||
# returns midnight of today
|
||||
@spec build_epoch(DateTime.t) :: DateTime.t
|
||||
def build_epoch(time) do
|
||||
tz = Farmbot.System.ConfigStorage.get_config_value(:string, "settings", "timezone")
|
||||
n = Timex.Timezone.convert(time, tz)
|
||||
Timex.shift(n, hours: -n.hour, seconds: -n.second, minutes: -n.minute)
|
||||
end
|
||||
end
|
25
lib/farmbot/regimen/supervisor.ex
Normal file
25
lib/farmbot/regimen/supervisor.ex
Normal file
|
@ -0,0 +1,25 @@
|
|||
defmodule Farmbot.Regimen.Supervisor do
|
||||
@moduledoc false
|
||||
use Supervisor
|
||||
|
||||
@doc false
|
||||
def start_link do
|
||||
Supervisor.start_link(__MODULE__, [], [name: __MODULE__])
|
||||
end
|
||||
|
||||
def init([]) do
|
||||
children = []
|
||||
opts = [strategy: :one_for_one]
|
||||
supervise(children, opts)
|
||||
end
|
||||
|
||||
def add_child(regimen, time) do
|
||||
spec = worker(Farmbot.Regimen.Manager, [regimen, time], [restart: :transient, id: regimen.id])
|
||||
Supervisor.start_child(__MODULE__, spec)
|
||||
end
|
||||
|
||||
def remove_child(regimen) do
|
||||
Supervisor.terminate_child(__MODULE__, regimen.id)
|
||||
Supervisor.delete_child(__MODULE__, regimen.id)
|
||||
end
|
||||
end
|
|
@ -7,6 +7,7 @@ defmodule Farmbot.Repo.Device do
|
|||
|
||||
schema "devices" do
|
||||
field(:name, :string)
|
||||
field(:timezone, :string)
|
||||
end
|
||||
|
||||
use Farmbot.Repo.Syncable
|
||||
|
|
|
@ -7,16 +7,19 @@ defmodule Farmbot.Repo.FarmEvent do
|
|||
* A Sequence will execute.
|
||||
"""
|
||||
|
||||
alias Farmbot.Repo.JSONType
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "farm_events" do
|
||||
field(:start_time, :utc_datetime)
|
||||
field(:end_time, :utc_datetime)
|
||||
field(:start_time, :string)
|
||||
field(:end_time, :string)
|
||||
field(:repeat, :integer)
|
||||
field(:time_unit, :string)
|
||||
field(:executable_type, Farmbot.Repo.ModuleType.FarmEvent)
|
||||
field(:executable_id, :integer)
|
||||
field(:calendar, JSONType)
|
||||
end
|
||||
|
||||
use Farmbot.Repo.Syncable
|
||||
|
@ -33,7 +36,6 @@ defmodule Farmbot.Repo.FarmEvent do
|
|||
|
||||
def changeset(farm_event, params \\ %{}) do
|
||||
farm_event
|
||||
|> ensure_time([:start_time, :end_time])
|
||||
|> cast(params, @required_fields)
|
||||
|> validate_required(@required_fields)
|
||||
|> unique_constraint(:id)
|
||||
|
|
|
@ -3,15 +3,18 @@ defmodule Farmbot.Repo.Regimen do
|
|||
A Regimen is a schedule to run sequences on.
|
||||
"""
|
||||
|
||||
alias Farmbot.Repo.JSONType
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "regimens" do
|
||||
field(:name, :string)
|
||||
field(:regimen_items, JSONType)
|
||||
end
|
||||
|
||||
use Farmbot.Repo.Syncable
|
||||
@required_fields [:id, :name]
|
||||
@required_fields [:id, :name, :regimen_items]
|
||||
|
||||
def changeset(farm_event, params \\ %{}) do
|
||||
farm_event
|
||||
|
|
|
@ -33,7 +33,7 @@ defmodule Farmbot.Repo do
|
|||
|
||||
@doc "Register a diff to be stored until a flip."
|
||||
def register_sync_cmd(sync_cmd) do
|
||||
GenServer.call(__MODULE__, {:register_sync_cmd, sync_cmd})
|
||||
GenServer.call(__MODULE__, {:register_sync_cmd, sync_cmd}, :infinity)
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
@ -52,9 +52,21 @@ defmodule Farmbot.Repo do
|
|||
"A" -> [repo_a, repo_b]
|
||||
"B" -> [repo_b, repo_a]
|
||||
end
|
||||
# Copy configs
|
||||
[current, _] = repos
|
||||
copy_configs(current)
|
||||
{:ok, %{repos: repos, sync_cmds: []}}
|
||||
end
|
||||
|
||||
defp copy_configs(repo) do
|
||||
case repo.one(Farmbot.Repo.Device) do
|
||||
nil -> :ok
|
||||
%{timezone: tz} ->
|
||||
Farmbot.System.ConfigStorage.update_config_value(:string, "settings", "timezone", tz)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def terminate(_,_) do
|
||||
Farmbot.BotState.set_sync_status(:sync_error)
|
||||
end
|
||||
|
@ -80,7 +92,7 @@ defmodule Farmbot.Repo do
|
|||
Farmbot.System.ConfigStorage.update_config_value(:string, "settings", "current_repo", "A")
|
||||
end
|
||||
Farmbot.BotState.set_sync_status(:synced)
|
||||
|
||||
copy_configs(repo_b)
|
||||
{:reply, repo_b, %{state | repos: [repo_b, repo_a]}}
|
||||
end
|
||||
|
||||
|
@ -183,6 +195,7 @@ defmodule Farmbot.Repo do
|
|||
end
|
||||
|
||||
defp sync_resource(repo, resource, slug) do
|
||||
Logger.debug 3, "syncing: #{resource} (#{slug})"
|
||||
as = if resource in @singular_resources do
|
||||
struct(resource)
|
||||
else
|
||||
|
|
1
mix.exs
1
mix.exs
|
@ -106,6 +106,7 @@ defmodule Farmbot.Mixfile do
|
|||
{:inch_ex, ">= 0.0.0", only: :dev},
|
||||
{:excoveralls, "~> 0.6", only: :test},
|
||||
{:mock, "~> 0.2.0", only: :test},
|
||||
{:faker, "~> 0.9", only: :test }
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"ex_json_schema": {:hex, :ex_json_schema, "0.5.5", "d8d4c3f47b86c9e634e124d518b290dda82a8b94dcc314e45af10042fc369361", [], [], "hexpm"},
|
||||
"excoveralls": {:hex, :excoveralls, "0.7.2", "f69ede8c122ccd3b60afc775348a53fc8c39fe4278aee2f538f0d81cc5e7ff3a", [], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"faker": {:hex, :faker, "0.9.0", "b22c55967fbd3413b9e5c121e59e75a553065587cf31e5aa2271b6fae2775cde", [], [], "hexpm"},
|
||||
"fs": {:hex, :fs, "3.4.0", "6d18575c250b415b3cad559e6f97a4c822516c7bc2c10bfbb2493a8f230f5132", [], [], "hexpm"},
|
||||
"gen_mqtt": {:hex, :gen_mqtt, "0.3.1", "6ce6af7c2bcb125d5b4125c67c5ab1f29bcec2638236509bcc6abf510a6661ed", [], [{:vmq_commons, "1.0.0", [hex: :vmq_commons, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"gen_stage": {:hex, :gen_stage, "0.12.2", "e0e347cbb1ceb5f4e68a526aec4d64b54ad721f0a8b30aa9d28e0ad749419cbb", [], [], "hexpm"},
|
||||
|
|
|
@ -4,12 +4,13 @@ defmodule Farmbot.Repo.Migrations.AddFarmEventsTable do
|
|||
def change do
|
||||
create table("farm_events", primary_key: false) do
|
||||
add(:id, :integer)
|
||||
add(:start_time, :utc_datetime)
|
||||
add(:end_time, :utc_datetime)
|
||||
add(:start_time, :string)
|
||||
add(:end_time, :string)
|
||||
add(:repeat, :integer)
|
||||
add(:time_unit, :string)
|
||||
add(:executable_type, :string)
|
||||
add(:executable_id, :integer)
|
||||
add(:calendar, :string)
|
||||
end
|
||||
|
||||
create(unique_index("farm_events", [:id]))
|
||||
|
|
|
@ -5,6 +5,7 @@ defmodule Farmbot.Repo.Migrations.AddRegimensTable do
|
|||
create table("regimens", primary_key: false) do
|
||||
add(:id, :integer)
|
||||
add(:name, :string)
|
||||
add(:regimen_items, :string)
|
||||
end
|
||||
|
||||
create(unique_index("regimens", [:id]))
|
||||
|
|
|
@ -5,6 +5,7 @@ defmodule Farmbot.Repo.A.Migrations.AddDevicesTable do
|
|||
create table("devices", primary_key: false) do
|
||||
add(:id, :integer)
|
||||
add(:name, :string)
|
||||
add(:timezone, :string)
|
||||
end
|
||||
|
||||
create(unique_index("devices", [:id]))
|
||||
|
|
37
test/farmbot/bootstrap/auth_task_test.exs
Normal file
37
test/farmbot/bootstrap/auth_task_test.exs
Normal file
|
@ -0,0 +1,37 @@
|
|||
defmodule Farmbot.Bootstrap.AuthTaskTest do
|
||||
@moduledoc "Tests the timed token refresher"
|
||||
|
||||
alias Farmbot.Bootstrap.AuthTask
|
||||
alias Farmbot.System.ConfigStorage
|
||||
use ExUnit.Case, async: true
|
||||
@moduletag :farmbot_api
|
||||
|
||||
setup do
|
||||
# This is usally a timed task. Cancel the timer, so we can
|
||||
# simulate the timer firing.
|
||||
timer = :sys.get_state(AuthTask)
|
||||
if Process.read_timer(timer) do
|
||||
Process.cancel_timer(timer)
|
||||
end
|
||||
:ok
|
||||
end
|
||||
|
||||
test "refreshes token and causes side effects" do
|
||||
old_tps = Process.whereis Farmbot.BotState.Transport.Supervisor
|
||||
old_token = ConfigStorage.get_config_value(:string, "authorization", "token")
|
||||
send AuthTask, :refresh
|
||||
|
||||
# I'm sorry about this.
|
||||
Process.sleep(1000)
|
||||
|
||||
new_tps = Process.whereis Farmbot.BotState.Transport.Supervisor
|
||||
new_token = ConfigStorage.get_config_value(:string, "authorization", "token")
|
||||
assert old_tps != new_tps
|
||||
assert new_token != old_token
|
||||
|
||||
# check that the timer refreshed itself.
|
||||
assert :sys.get_state(AuthTask)
|
||||
end
|
||||
|
||||
|
||||
end
|
101
test/farmbot/bot_state/bot_state_test.exs
Normal file
101
test/farmbot/bot_state/bot_state_test.exs
Normal file
|
@ -0,0 +1,101 @@
|
|||
defmodule Farmbot.BotStateTest do
|
||||
@moduledoc "Various functions to modify the bot's state"
|
||||
|
||||
use ExUnit.Case, async: true
|
||||
alias Farmbot.BotState
|
||||
|
||||
setup_all do
|
||||
old_state = get_state()
|
||||
{:ok, %{old_state: old_state}}
|
||||
end
|
||||
|
||||
test "gets a pin value" do
|
||||
pin = BotState.get_pin_value(-1000)
|
||||
assert pin == {:error, :unknown_pin}
|
||||
end
|
||||
|
||||
test "gets current position", %{old_state: _old_state} do
|
||||
pos = BotState.get_current_pos()
|
||||
assert match?(%{x: _, y: _, z: _}, pos)
|
||||
end
|
||||
|
||||
test "Forces a push of the current state" do
|
||||
state = BotState.force_state_push
|
||||
assert state == get_state()
|
||||
end
|
||||
|
||||
test "sets busy", %{old_state: _old_state} do
|
||||
:ok = BotState.set_busy(true)
|
||||
assert match?(true, get_state(:informational_settings).busy)
|
||||
:ok = BotState.set_busy(false)
|
||||
assert match?(false, get_state(:informational_settings).busy)
|
||||
end
|
||||
|
||||
test "sets sync status :locked" do
|
||||
:ok = Farmbot.BotState.set_sync_status(:locked)
|
||||
assert match?(:locked, get_state(:informational_settings).sync_status)
|
||||
end
|
||||
|
||||
test "sets sync status :maintenance" do
|
||||
:ok = Farmbot.BotState.set_sync_status(:maintenance)
|
||||
assert match?(:maintenance, get_state(:informational_settings).sync_status)
|
||||
end
|
||||
|
||||
test "sets sync status :sync_error" do
|
||||
:ok = Farmbot.BotState.set_sync_status(:sync_error)
|
||||
assert match?(:sync_error, get_state(:informational_settings).sync_status)
|
||||
end
|
||||
|
||||
test "sets sync status :sync_now" do
|
||||
:ok = Farmbot.BotState.set_sync_status(:sync_now)
|
||||
assert match?(:sync_now, get_state(:informational_settings).sync_status)
|
||||
end
|
||||
|
||||
test "sets sync status :synced" do
|
||||
:ok = Farmbot.BotState.set_sync_status(:synced)
|
||||
assert match?(:synced, get_state(:informational_settings).sync_status)
|
||||
end
|
||||
|
||||
test "sets sync status :syncing" do
|
||||
:ok = Farmbot.BotState.set_sync_status(:syncing)
|
||||
assert match?(:syncing, get_state(:informational_settings).sync_status)
|
||||
end
|
||||
|
||||
test "sets sync status :unknown" do
|
||||
:ok = Farmbot.BotState.set_sync_status(:unknown)
|
||||
assert match?(:unknown, get_state(:informational_settings).sync_status)
|
||||
end
|
||||
|
||||
test "sets user environment" do
|
||||
val = "hey! this should be in the bot's state!"
|
||||
key = inspect self()
|
||||
|
||||
:ok = BotState.set_user_env(key, val)
|
||||
res = BotState.get_user_env
|
||||
|
||||
assert match?(%{^key => ^val}, res)
|
||||
end
|
||||
|
||||
test "registers and unregisters farmware" do
|
||||
name = "Not real farmware"
|
||||
fw = Farmbot.TestSupport.FarmwareFactory.generate(name: name)
|
||||
:ok = BotState.register_farmware(fw)
|
||||
assert match?(%{^name => %{meta: %{author: _, description: _, language: _, min_os_version_major: _, version: _},
|
||||
args: _,
|
||||
executable: _,
|
||||
path: _,
|
||||
url: _}},
|
||||
get_state(:process_info).farmwares)
|
||||
:ok = BotState.unregister_farmware(fw)
|
||||
refute Map.has_key?(get_state(:process_info).farmwares, name)
|
||||
end
|
||||
|
||||
defp get_state(key \\ nil) do
|
||||
state = :sys.get_state(Farmbot.BotState).state
|
||||
if key do
|
||||
Map.get(state, key)
|
||||
else
|
||||
state
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,60 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.ASTTest do
|
||||
@moduledoc "Tests ast nodes"
|
||||
use ExUnit.Case
|
||||
|
||||
alias Farmbot.CeleryScript.AST
|
||||
|
||||
test "parses a string key'd map" do
|
||||
ast =
|
||||
%{"kind" => "kind", "args" => %{}}
|
||||
|> AST.parse()
|
||||
|
||||
assert ast.kind == "kind"
|
||||
assert ast.args == %{}
|
||||
assert ast.body == []
|
||||
end
|
||||
|
||||
test "parses atom map" do
|
||||
ast = %{kind: "hello", args: %{}} |> AST.parse()
|
||||
|
||||
assert ast.kind == "hello"
|
||||
assert ast.args == %{}
|
||||
assert ast.body == []
|
||||
end
|
||||
|
||||
defmodule SomeUnion do
|
||||
@moduledoc "Wraps a celeryscript AST"
|
||||
defstruct [:kind, :args]
|
||||
end
|
||||
|
||||
test "parses a struct" do
|
||||
ast = %SomeUnion{kind: "whooo", args: %{}} |> AST.parse()
|
||||
assert ast.kind == "whooo"
|
||||
assert ast.args == %{}
|
||||
assert ast.body == []
|
||||
end
|
||||
|
||||
test "parses body nodes" do
|
||||
sub_ast_1 = %{"kind" => "sub1", "args" => %{}}
|
||||
sub_ast_2 = %{kind: "sub2", args: %{}}
|
||||
ast = %{kind: "hey", args: %{}, body: [sub_ast_1, sub_ast_2]} |> AST.parse()
|
||||
|
||||
assert ast.kind == "hey"
|
||||
assert ast.args == %{}
|
||||
assert Enum.at(ast.body, 0).kind == "sub1"
|
||||
assert Enum.at(ast.body, 1).kind == "sub2"
|
||||
end
|
||||
|
||||
test "changes args to atoms" do
|
||||
ast = %{kind: "124", args: %{"string_key" => :hello}} |> AST.parse()
|
||||
assert ast.kind == "124"
|
||||
assert ast.args.string_key == :hello
|
||||
assert ast.args == %{string_key: :hello}
|
||||
end
|
||||
|
||||
test "parses sub nodes in the args" do
|
||||
ast = %{kind: "main", args: %{"node" => %{kind: "sub", args: %{}}}} |> AST.parse()
|
||||
assert ast.kind == "main"
|
||||
assert ast.args.node.kind == "sub"
|
||||
end
|
||||
end
|
|
@ -1,11 +1,6 @@
|
|||
defmodule Farmbot.Firmware.Gcode.ParamTest do
|
||||
use ExUnit.Case
|
||||
|
||||
test "pareses a param numbered string" do
|
||||
a = Farmbot.Firmware.Gcode.Param.parse_param("13")
|
||||
assert(a == :movement_timeout_z)
|
||||
end
|
||||
|
||||
test "Pareses a param in integer form" do
|
||||
a = Farmbot.Firmware.Gcode.Param.parse_param(13)
|
||||
assert(a == :movement_timeout_z)
|
||||
|
@ -15,9 +10,4 @@ defmodule Farmbot.Firmware.Gcode.ParamTest do
|
|||
a = Farmbot.Firmware.Gcode.Param.parse_param(:movement_timeout_z)
|
||||
assert(a == 13)
|
||||
end
|
||||
|
||||
test "Parses a param in string form" do
|
||||
a = Farmbot.Firmware.Gcode.Param.parse_param("movement_timeout_z")
|
||||
assert(a == 13)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,7 +17,7 @@ defmodule Farmbot.Repo.FarmEventTest do
|
|||
"time_unit" => "never"
|
||||
}
|
||||
|
||||
test "inserts and transforms valid farm_event" do
|
||||
test "inserts farm_event" do
|
||||
assert(
|
||||
@farm_event_seq
|
||||
|> Poison.encode!()
|
||||
|
@ -29,23 +29,6 @@ defmodule Farmbot.Repo.FarmEventTest do
|
|||
import Ecto.Query
|
||||
id = @farm_event_seq["id"]
|
||||
[fe] = from(fe in FarmEvent, where: fe.id == ^id, select: fe) |> Repo.A.all()
|
||||
assert fe.executable_type == Repo.Sequence
|
||||
assert fe.start_time.__struct__ == DateTime
|
||||
end
|
||||
|
||||
test "raises on unimplemented executable types" do
|
||||
cs =
|
||||
@farm_event_seq
|
||||
|> Map.put("executable_type", "UnknownResource")
|
||||
|> Poison.encode!()
|
||||
|> Poison.decode!(as: %FarmEvent{})
|
||||
|> FarmEvent.changeset()
|
||||
|
||||
msg =
|
||||
~S(value `"UnknownResource"` for `Farmbot.Repo.FarmEvent.executable_type` in `insert` does not match type Farmbot.Repo.ModuleType.FarmEvent)
|
||||
|
||||
assert_raise Ecto.ChangeError, msg, fn ->
|
||||
Repo.A.insert!(cs)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,12 +2,12 @@ defmodule Farmbot.SystemTest do
|
|||
@moduledoc "Tests system functionaity."
|
||||
use ExUnit.Case
|
||||
|
||||
setup do
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Farmbot.System.ConfigStorage)
|
||||
end
|
||||
# setup do
|
||||
# :ok = Ecto.Adapters.SQL.Sandbox.checkout(Farmbot.System.ConfigStorage)
|
||||
# end
|
||||
|
||||
test "does factory reset" do
|
||||
Farmbot.System.factory_reset("hey something bad happened!")
|
||||
Farmbot.System.factory_reset({:error, "hey something bad happened!"})
|
||||
last = Farmbot.Test.SystemTasks.fetch_last()
|
||||
assert match?({:factory_reset, _}, last)
|
||||
{_, msg} = last
|
||||
|
@ -15,7 +15,7 @@ defmodule Farmbot.SystemTest do
|
|||
end
|
||||
|
||||
test "does reboot" do
|
||||
Farmbot.System.reboot("goodbye cruel world!")
|
||||
Farmbot.System.reboot({:error, "goodbye cruel world!"})
|
||||
last = Farmbot.Test.SystemTasks.fetch_last()
|
||||
assert match?({:reboot, _}, last)
|
||||
{_, msg} = last
|
||||
|
@ -23,7 +23,7 @@ defmodule Farmbot.SystemTest do
|
|||
end
|
||||
|
||||
test "does shutdown" do
|
||||
Farmbot.System.shutdown("see you soon!")
|
||||
Farmbot.System.shutdown({:error, "see you soon!"})
|
||||
last = Farmbot.Test.SystemTasks.fetch_last()
|
||||
assert match?({:shutdown, _}, last)
|
||||
{_, msg} = last
|
||||
|
|
31
test/support/factory/farmware_factory.ex
Normal file
31
test/support/factory/farmware_factory.ex
Normal file
|
@ -0,0 +1,31 @@
|
|||
defmodule Farmbot.TestSupport.FarmwareFactory do
|
||||
|
||||
def generate(opts) do
|
||||
meta = struct(Farmbot.Farmware.Meta,
|
||||
[author: Faker.App.author(), language: "python", description: ""])
|
||||
fw = struct(Farmbot.Farmware,
|
||||
[name: Faker.App.name(),
|
||||
version: Version.parse!(Faker.App.semver),
|
||||
min_os_version_major: 6,
|
||||
url: Faker.Internet.url(),
|
||||
zip: Faker.Internet.url(),
|
||||
executable: Faker.File.file_name(),
|
||||
args: [],
|
||||
config: [],
|
||||
meta: meta
|
||||
])
|
||||
|
||||
do_update(fw, opts)
|
||||
end
|
||||
|
||||
defp do_update(fw, opts)
|
||||
defp do_update(fw, [{key, val} | rest]) do
|
||||
if key in Map.keys(fw) do
|
||||
do_update(Map.put(fw, key, val), rest)
|
||||
else
|
||||
do_update(fw, rest)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_update(fw, []), do: fw
|
||||
end
|
|
@ -8,4 +8,4 @@ ExUnit.start()
|
|||
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Farmbot.Repo.A, :manual)
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Farmbot.Repo.B, :manual)
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Farmbot.System.ConfigStorage, :manual)
|
||||
# Ecto.Adapters.SQL.Sandbox.mode(Farmbot.System.ConfigStorage, :manual)
|
||||
|
|
Loading…
Reference in a new issue