pruning nonsense

pull/363/head
Connor Rigby 2017-08-09 20:58:26 -07:00
parent 0b0b85fc85
commit 56d7bac084
91 changed files with 60 additions and 4987 deletions

View File

@ -17,9 +17,6 @@ config :logger, :console,
# Iex needs colors too.
config :iex, :colors, enabled: true
# bot <-> firmware transports.
config :farmbot, expected_fw_version: "4.0.2"
# Rollbar
config :farmbot, rollbar_access_token: "dcd79b191ab84aa3b28259cbb80e2060"

View File

@ -1,26 +1,5 @@
use Mix.Config
config :farmbot,
config_file_name: System.get_env("CONFIG_FILE_NAME") || "default_config.json",
configurator_port: System.get_env("CONFIGURATOR_PORT") || 5000,
path: "/tmp/farmbot",
tty: {:system, "ARDUINO_TTY"}
config :farmbot, :redis,
server: System.get_env("REDIS_SERVER") || false,
port: System.get_env("REDIS_SERVER_PORT") || 6379
# Transports
mqtt_transport = Farmbot.Transport.GenMqtt
# redis_transport = Farmbot.Transport.Redis
# frontend <-> bot transports.
config :farmbot, transports: [
{mqtt_transport, name: mqtt_transport},
# {redis_transport, name: redis_transport}
config :farmbot, :init, [
]
config :wobserver,
mode: :plug,
remote_url_prefix: "/wobserver"

View File

@ -1,18 +1 @@
use Mix.Config
config :farmbot,
config_file_name: "default_config.json",
configurator_port: 5001,
path: "/tmp/farmbot_test",
config_file_name: "default_config.json",
# tty: "/dev/tnt1",
logger: false
config :farmbot_simulator, :tty, "tnt0"
config :farmbot, :redis,
server: false
config :farmbot, transports: []
# We hopefully don't need logger ¯\_(ツ)_/¯
config :logger, :console, format: ""

View File

@ -14,25 +14,23 @@ defmodule Farmbot do
def start(type, args)
def start(_, args) do
Logger.info ">> Booting Farmbot OS version: #{@version} - #{@commit}"
case Supervisor.start_link(__MODULE__, args, name: Farmbot) do
case Supervisor.start_link(__MODULE__, args, [name: Farmbot]) do
{:ok, pid} -> {:ok, pid}
{:error, reason} -> Farmbot.System.factory_reset(reason)
# {:error, reason} -> Farmbot.System.factory_reset(reason)
end
end
def init(args) do
children = [
supervisor(Registry, [:duplicate, Farmbot.Registry]),
supervisor(Farmbot.DebugLog.Supervisor, [args, [name: Farmbot.DebugLog ]]),
supervisor(Farmbot.System.Supervisor, [args, [name: Farmbot.System.Supervisor ]]),
supervisor(Farmbot.HTTP.Supervisor, [args, [name: Farmbot.HTTP.Supervisor ]]),
supervisor(Farmbot.BotState.Supervisor, [args, [name: Farmbot.BotState.Supervisor ]]),
supervisor(Farmbot.Transport.Supervisor, [args, [name: Farmbot.Transport.Supervisor ]]), # Can this be a child of the bot's state maybe?
supervisor(Farmbot.FarmEvent.Supervisor, [args, [name: Farmbot.FarmEvent.Supervisor ]]), # maybe make this a child of the database?
supervisor(Farmbot.Serial.Supervisor, [args, [name: Farmbot.Serial.Supervisor ]]),
supervisor(Farmbot.Farmware.Supervisor, [args, [name: Farmbot.Farmware.Supervisor ]]),
worker(Farmbot.ImageWatcher, [args, [name: Farmbot.ImageWatcher ]]), # this may need to move too.
# supervisor(Farmbot.HTTP.Supervisor, [args, [name: Farmbot.HTTP.Supervisor ]]),
# supervisor(Farmbot.BotState.Supervisor, [args, [name: Farmbot.BotState.Supervisor ]]),
# supervisor(Farmbot.Transport.Supervisor, [args, [name: Farmbot.Transport.Supervisor ]]), # Can this be a child of the bot's state maybe?
# supervisor(Farmbot.FarmEvent.Supervisor, [args, [name: Farmbot.FarmEvent.Supervisor ]]), # maybe make this a child of the database?
# supervisor(Farmbot.Serial.Supervisor, [args, [name: Farmbot.Serial.Supervisor ]]),
# supervisor(Farmbot.Farmware.Supervisor, [args, [name: Farmbot.Farmware.Supervisor ]]),
# worker(Farmbot.ImageWatcher, [args, [name: Farmbot.ImageWatcher ]]), # this may need to move too.
]
opts = [strategy: :one_for_one]
supervise(children, opts)

View File

@ -25,91 +25,13 @@ defmodule Farmbot.DebugLog do
enables the `debug_log/1` function.
"""
defmacro __using__(opts) do
color = Keyword.get(opts, :color)
name = Keyword.get(opts, :name)
ccolor = Keyword.get(opts, :color) || :NC
quote do
if unquote(name) do
defp get_module, do: unquote(name)
else
defp get_module, do: __MODULE__ |> Module.split() |> List.last
end
if unquote(color) do
defp debug_log(str) do
GenEvent.notify(Farmbot.DebugLog,
{get_module(), {unquote(color), str}})
end
else
defp debug_log(str) do
GenEvent.notify Farmbot.DebugLog, {get_module(), {:BLUE, str}}
end
end # if color
end # quote
end # defmacro
defmodule Handler do
@moduledoc """
Handler for DebugLogger
"""
use GenEvent
@doc false
defdelegate color(color), to: Farmbot.DebugLog
def init(state), do: {:ok, state}
def handle_event(_, :all), do: {:ok, :all}
def handle_event({module, {color, str}}, state) when is_binary(str) do
filter_me? = Map.get(state, module)
unless filter_me? do
IO.puts "#{color(color)} [#{module}]#{color(:NC)} #{str}"
end
{:ok, state}
end
def handle_call({:filter, :all}, _state) do
{:ok, :all}
end
def handle_call({:filter, module}, state) do
{:ok, :ok, Map.put(state, module, :filterme)}
end
def handle_call({:unfilter, module}, state) do
if state == :all do
{:ok, :error, state}
else
{:ok, :ok, Map.delete(state, module)}
end
end
end
@doc """
Start the Debug Logger
"""
def start_link(event_server) do
:ok = GenEvent.add_handler(event_server, Handler, %{})
{:ok, self()}
end
@doc """
Filter a module from the handler.
"""
def filter(module) do
GenEvent.call(__MODULE__, Handler, {:filter, module})
end
@doc """
Unfilter a module from the handler.
"""
def unfilter(module) do
GenEvent.call(__MODULE__, Handler, {:unfilter, module})
end
end # defmodule
import Farmbot.DebugLog
def debug_log(msg) do
module = __MODULE__ |> Module.split() |> Enum.take(-2)
IO.puts "#{color(unquote(ccolor))}[#{module}] #{msg}#{color(:NC)}"
end # debug log
end # quote
end # defmacro
end # defmodule

View File

@ -1,17 +0,0 @@
defmodule Farmbot.DebugLog.Supervisor do
@moduledoc "Internal Logger Supervisor"
use Supervisor
def start_link(args, opts) do
Supervisor.start_link(__MODULE__, args, opts)
end
def init(args) do
children = [
worker(GenEvent, [name: Farmbot.DebugLog])
worker(Farmbot.DebugLog, [Farmbot.DebugLog])
]
opts = [strategy: :one_for_one]
supervise(children, opts)
end
end

View File

@ -1,187 +0,0 @@
defmodule Farmbot.System.Network.Hostapd do
@moduledoc """
Manages an OS process of hostapd and DNSMASQ.
"""
defmodule State do
@moduledoc false
defstruct [:hostapd, :dnsmasq, :interface, :ip_addr, :manager]
end
use GenServer
require Logger
@hostapd_conf_file "hostapd.conf"
@hostapd_pid_file "hostapd.pid"
@dnsmasq_conf_file "dnsmasq.conf"
@dnsmasq_pid_file "dnsmasq.pid"
@doc ~s"""
Example:
Iex> Hostapd.start_link ip_address: "192.168.24.1",
...> manager: Farmbot.Network.Manager, interface: "wlan0"
"""
def start_link(
[interface: interface, ip_address: ip_addr, manager: manager])
do
name = Module.concat([__MODULE__, interface])
GenServer.start_link(__MODULE__,
[interface: interface, ip_address: ip_addr, manager: manager],
name: name)
end
defp setup_hostapd(interface, ip_addr) do
# HOSTAPD
# Make sure the interface is in proper condition.
:ok = hostapd_ip_settings_up(interface, ip_addr)
# build the hostapd configuration
hostapd_conf = build_hostapd_conf(interface, build_ssid())
# build a config file
File.mkdir! "/tmp/hostapd"
File.write! "/tmp/hostapd/#{@hostapd_conf_file}", hostapd_conf
hostapd_cmd = "hostapd -P /tmp/hostapd/#{@hostapd_pid_file} " <>
"/tmp/hostapd/#{@hostapd_conf_file}"
hostapd_port = Port.open({:spawn, hostapd_cmd}, [:binary])
hostapd_os_pid = hostapd_port|> Port.info() |> Keyword.get(:os_pid)
{hostapd_port, hostapd_os_pid}
end
defp setup_dnsmasq(ip_addr, interface) do
# DNSMASQ
dnsmasq_conf = build_dnsmasq_conf(ip_addr, interface)
File.mkdir!("/tmp/dnsmasq")
:ok = File.write("/tmp/dnsmasq/#{@dnsmasq_conf_file}", dnsmasq_conf)
dnsmasq_cmd = "dnsmasq -k --dhcp-lease " <>
"/tmp/dnsmasq/#{@dnsmasq_pid_file} " <>
"--conf-dir=/tmp/dnsmasq"
dnsmasq_port = Port.open({:spawn, dnsmasq_cmd}, [:binary])
dnsmasq_os_pid = dnsmasq_port|> Port.info() |> Keyword.get(:os_pid)
{dnsmasq_port, dnsmasq_os_pid}
end
def init([interface: interface, ip_address: ip_addr, manager: manager]) do
Logger.info ">> is starting hostapd on #{interface}"
# We want to know if something does.
Process.flag :trap_exit, true
{hostapd_port, hostapd_os_pid} = setup_hostapd(interface, ip_addr)
{dnsmasq_port, dnsmasq_os_pid} = setup_dnsmasq(ip_addr, interface)
state = %State{
hostapd: {hostapd_port, hostapd_os_pid},
dnsmasq: {dnsmasq_port, dnsmasq_os_pid},
interface: interface,
ip_addr: ip_addr,
manager: manager
}
{:ok, state}
end
defp hostapd_ip_settings_up(interface, ip_addr) do
:ok =
"ip" |> System.cmd(["link", "set", "#{interface}", "up"])
|> print_cmd
:ok =
"ip" |> System.cmd(["addr", "add", "#{ip_addr}/24", "dev", "#{interface}"])
|> print_cmd
:ok
end
defp hostapd_ip_settings_down(interface, ip_addr) do
:ok =
"ip" |> System.cmd(["link", "set", "#{interface}", "down"])
|> print_cmd
:ok =
"ip" |> System.cmd(["addr", "del", "#{ip_addr}/24", "dev", "#{interface}"])
|> print_cmd
:ok =
"ip" |> System.cmd(["link", "set", "#{interface}", "up"])
|> print_cmd
:ok
end
defp build_hostapd_conf(interface, ssid) do
"""
interface=#{interface}
ssid=#{ssid}
hw_mode=g
channel=6
auth_algs=1
wmm_enabled=0
"""
end
defp build_ssid do
node_str =
node() |> Atom.to_string
[name, "nerves-" <> id] =
node_str |> String.split("@")
name <> "-" <> id
end
defp build_dnsmasq_conf(ip_addr, interface) do
[a, b, c, _] = ip_addr |> String.split(".")
first_part = "#{a}.#{b}.#{c}."
"""
interface=#{interface}
dhcp-range=#{first_part}50,#{first_part}250,2h
dhcp-option=3,#{ip_addr}
dhcp-option=6,#{ip_addr}
dhcp-authoritative
address=/#/#{ip_addr}
server=/farmbot/#{ip_addr}
local=/farmbot/
domain=farmbot
"""
end
defp kill(os_pid),
do: :ok = "kill" |> System.cmd(["15", "#{os_pid}"]) |> print_cmd
defp print_cmd({_, 0}), do: :ok
defp print_cmd({res, num}) do
Logger.error ">> encountered an error (#{num}): #{res}"
:error
end
def handle_info({port, {:data, data}}, state) do
{hostapd_port, _} = state.hostapd
{dnsmasq_port, _} = state.dnsmasq
cond do
port == hostapd_port ->
handle_hostapd(data, state)
port == dnsmasq_port ->
handle_dnsmasq(data, state)
true -> {:noreply, state}
end
end
def handle_info(_thing, state), do: {:noreply, state}
defp handle_hostapd(data, state) when is_bitstring(data) do
GenEvent.notify(state.manager, {:hostapd, String.trim(data)})
{:noreply, state}
end
defp handle_hostapd(_, state), do: {:noreply, state}
defp handle_dnsmasq(data, state) when is_bitstring(data) do
GenEvent.notify(state.manager, {:dnsmasq, String.trim(data)})
{:noreply, state}
end
defp handle_dnsmasq(_, state), do: {:noreply, state}
def terminate(_, state) do
Logger.info ">> is stopping hostapd"
{_hostapd_port, hostapd_pid} = state.hostapd
{_dnsmasq_port, dnsmasq_pid} = state.dnsmasq
# Port.close hostapd_port
# Port.close dnsmasq_port
:ok = kill(hostapd_pid)
:ok = kill(dnsmasq_pid)
hostapd_ip_settings_down(state.interface, state.ip_addr)
File.rm_rf! "/tmp/hostapd"
File.rm_rf! "/tmp/dnsmasq"
end
end

View File

@ -1,56 +0,0 @@
defmodule Farmbot.System.Network.Ntp do
@moduledoc """
Sets time.
"""
require Logger
alias Farmbot.{DebugLog}
use DebugLog
@doc """
Tries to set the time from ntp.
This will try 3 times to set the time. if it fails the thrid time,
it will return an error
"""
@spec set_time(integer) :: :ok | {:error, term}
def set_time(tries \\ 0)
def set_time(tries) when tries < 4 do
Process.sleep(1000 * tries)
case :inet_res.gethostbyname('0.pool.ntp.org') do
{:ok, {:hostent, _url, _, _, _, _}} ->
do_try_set_time(tries)
{:error, err} ->
debug_log "Failed to set time (#{tries}): DNS Lookup: #{inspect err}"
set_time(tries + 1)
end
end
def set_time(_), do: {:error, :timeout}
defp do_try_set_time(tries) when tries < 4 do
# we try to set ntp time 3 times before giving up.
Logger.info ">> trying to set time (try #{tries})"
:os.cmd('ntpd -p 0.pool.ntp.org -p 1.pool.ntp.org')
wait_for_time(tries)
end
defp wait_for_time(tries, loops \\ 0)
defp wait_for_time(tries, loops) when loops > 5 do
:os.cmd('killall ntpd')
set_time(tries)
end
defp wait_for_time(tries, loops) do
case :os.system_time do
t when t > 1_474_929 ->
Logger.flush()
Logger.info ">> Time is set.", type: :success
:ok
_ ->
Process.sleep(1_000 * loops)
wait_for_time(tries, loops + 1)
end
end
end

View File

@ -1,76 +0,0 @@
defmodule Farmbot.System.FS.ConfigFileMigrations do
@moduledoc """
Migrates a state map to the current form.
# Heres how they work
* this module lists all the files in "#{:code.priv_dir(:farmbot)}/migrations"
* it sorts them in order of dates.
* those files are just elixir script files `*.exs`
* said file must export ONE module, with one function function named `run\1`
* it takes a json map, and must return that map, but updated
* when the migration runs successfully, it writes a file under the same name, but with `.migrated` at the
_end of the file name, so we know not to migrate this again.
* does this for every file in the list, passing the last one to the next one.
# example
* we needed to change "steps_per_mm" from an integer, to a map: `%{x: int, y: int, z: int}`
* so we took the version with an int, and changed it, and returned the new verion. Simple.
"""
require Logger
@save_dir Application.get_env(:farmbot, :path)
@doc """
Does the migrations
"""
def migrate(json_map) do
list_of_files = get_migrations()
Enum.reduce(list_of_files, json_map, fn(file, json) ->
migrated = "#{@save_dir}/#{file}.migrated"
# if there is no <timestamp>-<description>_migration.exs.migrated file
# run the migration
# Check the file existance.
if File.exists?(migrated) do
# if we don't run a migration, just take the accumulator
json
else
# Run the migration
Logger.info ">> running config migration: #{file}"
{{:module, m, _s, _}, _} = Code.eval_file file, migrations_dir()
# TODO(Connor): Find out why m.run can return nil?
next = m.run(json) || %{}
Farmbot.System.FS.transaction fn() ->
# write the contents of this migration, to the .migrated file.
File.write(migrated, Poison.encode!(next))
Logger.info ">> #{file} migration complete", type: :success
end, true
# merge the current acc, with the just run migration
Map.merge(json, next)
end
end)
end
@doc """
returns a list of file sorted by date
"""
@spec get_migrations :: [String.t]
def get_migrations do
migrations_dir()
|> File.ls!
|> Enum.sort(fn(migration, last) ->
[time_stampa, _desca] = String.split(migration, "-")
int_time_stampa = String.to_integer(time_stampa)
[time_stampb, _descb] = String.split(last, "-")
int_time_stampb = String.to_integer(time_stampb)
int_time_stampa <= int_time_stampb
end)
end
defp migrations_dir, do: "#{:code.priv_dir(:farmbot)}/migrations"
end

View File

@ -1,134 +0,0 @@
defmodule Farmbot.System.FS.ConfigStorage do
@moduledoc """
Loads information according to a configuration JSON file.
This does not handle the configuration file, it just holds all the
information in an easy to reach place.
"""
use GenServer
alias Farmbot.System.FS.ConfigFileMigrations, as: CFM
require Logger
alias Farmbot.Context
@config_file Application.get_env(:farmbot, :path) <> "/config.json"
@default_config_file_name Application.get_env(:farmbot, :config_file_name)
defp default_config_file,
do: "#{:code.priv_dir(:farmbot)}/configs/#{@default_config_file_name}"
def start_link(%Context{} = _ctx, opts) do
GenServer.start_link(__MODULE__, @config_file, opts)
end
def init(path) do
Logger.info ">> Config Storage init!"
# Checks if the json file exists or not
case File.read(path) do
# if it does parse it
{:ok, contents} ->
Logger.info ">> is loading its configuration file: #{path}"
f = parse_json!(contents)
migrated = CFM.migrate(f)
# NOTE(Connor):
# this will write the initial config file, the migrated one, or
# the current one at every boot.
# seems ineffecient but oh well
write!(migrated)
Logger.info ">> no migrations"
{:ok, migrated}
# if not start over with the default config file (from the priv dir)
{:error, :enoent} ->
Logger.info ">> is creating a new configuration file: #{default_config_file()}"
reset_config()
init(@config_file)
end
end
def reset_config do
Farmbot.System.FS.transaction fn() ->
File.cp!(default_config_file(), @config_file)
end
end
def read_config_file do
GenServer.call(__MODULE__, :read_config_file)
end
@doc """
Replace the configuration json with a new one.
BE CAREFUL IM NOT CHECKING THE FILE AT ALL
"""
@spec replace_config_file(binary | map) :: :ok | {:error, term}
def replace_config_file(config) when is_map(config) do
# PLEASE FIXME: This needs to be better validated.
GenServer.call(__MODULE__, {:replace_config_file, config})
end
def replace_config_file(config) when is_binary(config) do
f = parse_json!(config)
GenServer.call(__MODULE__, {:replace_config_file, f})
end
def handle_call(:read_config_file, _, state) do
read = File.read(@config_file)
{:reply, read, state}
end
def handle_call({:replace_config_file, new_state}, _, _old_state) do
write!(:ok, new_state)
end
def handle_call({:get, module, :all}, _, state) do
m = module_to_key(module)
f = state |> Map.get(m)
{:reply, {:ok, f}, state}
end
# GenServer.call(ConfigStorage, {:get, Blah, :uh})
def handle_call({:get, module, key}, _, state) do
m = module_to_key(module)
case state[m] do
nil -> {:reply, {:error, :bad_module}, state}
# this might be a HACK, or it might be clever
false -> {:reply, {:ok, nil}, state}
config -> {:reply, {:ok, config[key]}, state}
end
end
def handle_cast({:put, module, {key, val}}, state) do
m = module_to_key(module)
old = Map.get(state, m)
new = Map.put(old, key, val)
new_state = Map.put(state, m, new)
write! new_state
end
def terminate(_, _), do: nil
defp module_to_key(module),
do: module
|> Module.split
|> List.last
|> String.Casing.downcase
@spec write!(map) :: {:noreply, map}
defp write!(state) do
do_write(state)
{:noreply, state}
end
@spec write!(any, map) :: {:reply, any, map}
defp write!(reply, state) do
do_write(state)
{:reply, reply, state}
end
defp do_write(state) do
json = Poison.encode!(state)
Farmbot.System.FS.transaction fn() ->
File.write!(@config_file, json)
end
end
# tries to parse contents. raises an exception if it can't
@spec parse_json!(binary) :: map
defp parse_json!(contents), do: contents |> Poison.decode!
end

View File

@ -1,107 +0,0 @@
defmodule Farmbot.System.FS do
@moduledoc """
Handles filesystem reads and writes and formatting.
"""
require Logger
use GenStage
@path Application.get_env(:farmbot, :path)
alias Farmbot.Context
def start_link(%Context{} = _ctx, target, opts),
do: GenStage.start_link(__MODULE__, target, opts)
def init(target) do
Logger.info ">> #{target} FileSystem Init"
mod = Module.concat([Farmbot, System, target, FileSystem])
mod.fs_init
mod.mount_read_only
spawn __MODULE__, :check_update_file, []
{:producer, []}
end
def check_update_file do
update_file_path = "#{path()}/.post_update"
case File.stat(update_file_path) do
{:ok, _file} ->
Logger.info "We are in post update mode!"
result = Farmbot.System.Updates.post_install()
transaction fn ->
Logger.info "Cleaning up post update mode!", type: :busy
File.rm!(update_file_path)
end, true
if result == :reboot do
Farmbot.System.reboot()
end
_ ->
Logger.info "Not in post update mode!"
:ok
end
end
@doc ~s"""
Safely executes FileSystem commands.\n
* mounts the file system as read_write
* executes function
* mounts the file system as read_only again.
Example:
Iex> transaction fn() -> File.write("/state/bs.txt" , "hey") end
"""
@spec transaction(function) :: :ok | nil
def transaction(function, block? \\ false, timeout \\ 10_000)
def transaction(fun, false, _timeout) when is_function(fun) do
# HACK(Connor) i dont want to handle two different :add_transactions
GenServer.call(__MODULE__, {:add_transaction, fun, __MODULE__})
end
def transaction(fun, true, timeout) when is_function(fun) do
task = Task.async(fn() ->
GenServer.call(__MODULE__, {:add_transaction, fun, self()})
# FIXME(Connor) this is probably.. well terrible
receive do
thing -> thing
end
end)
case Task.yield(task, timeout) || Task.shutdown(task) do
{:ok, result} ->
result
nil ->
Logger.error "Failed to execute FS transaction in #{timeout} ms"
nil
end
end
@spec get_state :: [any]
def get_state, do: GenServer.call(__MODULE__, :get_state)
def handle_call(:get_state, _, state), do: {:reply, [], state, state}
# where there is no other queued up stuff
def handle_call({:add_transaction, fun, pid}, _from, []) do
{:reply, :ok, [{fun, pid}], []}
end
def handle_call({:add_transaction, fun, pid}, _from, queue) do
Logger.info ">> is queueing a fs transaction"
{:reply, :ok, [], [{fun, pid} | queue]}
end
def handle_demand(demand, queue) when demand > 0 do
trans = Enum.reverse(queue)
{:noreply, trans, []}
end
def handle_info(_, state), do: {:noreply, [], state}
@doc """
Returns the path where farmbot keeps its persistant data.
"""
def path, do: @path
# BEHAVIOR
@type return_type :: :ok | {:error, term}
@callback mount_read_only() :: return_type
@callback mount_read_write() :: return_type
@callback fs_init() :: return_type
end

View File

@ -1,48 +0,0 @@
defmodule Farmbot.System.FS.Worker do
@moduledoc """
Pulls transactions from the queue, does them, etc.
"""
# NOTE(Connor) This isn't working the way i want it to, but i dont have time
# to fix it right now.
# Expected behavior is for this module to get the list of transactions,
# make the filesystem read-write, do all the things
# and then make the fs read-only again.
# What actually happens is it grabs them one at a time for some reason
# Either way this seems to have fixed the race condition waiting for
# teh transaction and mucking up state.
require Logger
use GenStage
alias Farmbot.Context
def start_link(%Context{} = _ctx, target, opts) do
GenStage.start_link(__MODULE__, target, opts)
end
def init(target) do
mod = Module.concat([Farmbot, System, target, FileSystem])
{:consumer, mod, subscribe_to: [Farmbot.System.FS]}
end
def handle_events(events, _from, mod) do
mod.mount_read_write
for {transaction, cb} <- events do
transaction.()
if is_pid(cb) do
send cb, {:ok, :ok}
else
:ok
end
end
mod.mount_read_only
{:noreply, [], mod}
end
def handle_call(:get_state, _, state), do: {:reply, [], state, state}
def terminate(_, mod) do
mod.mount_read_only
:ok
end
end

View File

@ -0,0 +1,16 @@
defmodule Farmbot.System.Init do
@moduledoc "Lets write init.d in Elixir!"
@doc "OTP Spec."
@spec fb_init(atom, [term]) :: Supervisor.Spec.spec
def fb_init(module, args) do
import Supervisor.Spec
supervisor(module, args, [restart: :permanent])
end
@doc """
Start an init module.
returning {:error, reason} will factory reset the system.
"""
@callback start_link(term, Supervisor.options) :: Supervisor.supervisor
end

View File

@ -1,337 +0,0 @@
defmodule Farmbot.System.Network do
@moduledoc """
Network functionality.
"""
require Logger
alias Farmbot.System.FS.ConfigStorage, as: CS
alias Farmbot.System.Network.Ntp
alias Farmbot.{Context, Auth, HTTP, DebugLog}
use DebugLog
use GenServer
@type netman :: pid
@spec mod(atom | binary | Context.t) :: atom
defp mod(%Context{} = context), do: mod(context.network)
defp mod(target) when is_atom(target) or is_binary(target),
do: Module.concat([Farmbot, System, target, Network])
@doc """
Starts the network manager
"""
def start_link(%Context{} = context, target, opts) do
GenServer.start_link(__MODULE__, [context, target], opts)
end
def init([%Context{} = context, target]) do
Logger.info ">> is starting networking"
m = mod(target)
{:ok, _cb} = m.start_link(context)
{:ok, interface_config} = get_config("interfaces")
parse_and_start_config(context, interface_config, m)
{:ok, %{context: context, target: target}}
end
# if networking is disabled.
defp parse_and_start_config(%Context{} = context, nil, _), do: spawn(fn ->
Process.sleep(2000)
spawn fn ->
maybe_get_fpf(context)
end
maybe_log_in(context)
end)
defp parse_and_start_config(%Context{} = context, config, m) do
for {interface, settings} <- config do
m.start_interface(context, interface, settings)
end
end
case Mix.env do
:test -> defp maybe_log_in(_ctx), do: :ok
_ -> defp maybe_log_in(ctx), do: Farmbot.Auth.try_log_in(ctx.auth)
end
@doc """
Scans for wireless ssids.
"""
@spec scan(netman, binary) :: [binary]
def scan(netman, interface_name) do
GenServer.call(netman, {:scan, interface_name})
end
@doc """
Lists all network interfaces that Farmbot Detected.
"""
@spec enumerate(netman) :: [binary] | {:error, term}
def enumerate(netman) do
GenServer.call(netman, :enumerate)
end
@doc """
Restarts networking services. This will block.
"""
def restart(netman) when is_atom(netman) or is_pid(netman) do
stop_all(netman)
{:ok, interface_config} = get_config("interfaces")
m = mod(get_mod(netman)) # wtf is this?
context = get_context(netman)
parse_and_start_config(context, interface_config, m)
end
@doc """
Starts an interface
"""
def start_interface(netman, interface, settings) do
GenServer.call(netman, {:start, interface, settings}, :infinity)
end
@doc """
Stops an interface
"""
def stop_interface(netman, interface) do
GenServer.call(netman, {:stop, interface}, :infinity)
end
@doc """
Stops all interfaces
"""
def stop_all(netman) do
{:ok, interfaces} = get_config("interfaces")
if interfaces do
for {iface, _} <- interfaces do
stop_interface(netman, iface)
end
end
end
defp maybe_set_time do
{:ok, ntp} = get_config("ntp")
if ntp do
Logger.info ">> starting ntp client."
Ntp.set_time
end
:ok
end
if Mix.env() == :prod do
def read_ssh_public_key do
{:ok, public_key} = get_config("ssh")
public_key
end
else
results = case File.read("#{System.get_env("HOME")}/.ssh/id_rsa.pub") do
{:ok, bin} -> String.trim(bin)
_ -> nil
end
def read_ssh_public_key, do: unquote(results)
end
defp maybe_start_ssh do
public_key = read_ssh_public_key()
ssh_dir = '/ssh'
ssh_dir_exists? = File.exists?(ssh_dir)
shell = {Elixir.IEx, :start, []}
# shell = {Elixir.Nerves.Runtime.Shell, :start, []}
if is_binary(public_key) and ssh_dir_exists? do
File.write "#{ssh_dir}/authorized_keys", public_key
debug_log "Starting SSH."
:ok = :ssh.start()
opts =
[
{:shell, shell},
{:system_dir, ssh_dir},
{:user_dir, ssh_dir},
]
:ssh.daemon(22, opts)
:ok
else
debug_log "Not starting SSH"
:ok
end
end
defp maybe_get_fpf(%Context{} = ctx) do
# First Party Farmware is not really a network concern but here we are...
call = {:get, Configuration, "first_party_farmware"}
{:ok, fpf} = GenServer.call(CS, call)
try do
if fpf do
Logger.info ">> is installing first party Farmwares.", type: :busy
repo = Farmbot.Farmware.Installer.Repository.Farmbot
Farmbot.Farmware.Installer.enable_repo!(ctx, repo)
end
:ok
rescue
error ->
Logger.warn(">> failed to install first party farmwares: " <>
" #{inspect error}")
debug_log "#{inspect System.stacktrace()}"
:ok
end
end
defp get_context(netman) when is_atom(netman) or is_pid(netman) do
GenServer.call(netman, :get_context)
end
@doc """
Checks for nxdomain. reboots if `nxdomain`.
"""
def connection_test(%Context{} = ctx) do
Logger.info ">> doing connection test...", type: :busy
case HTTP.get(ctx, "http://neverssl.com/") do
{:ok, _} ->
Logger.info ">> connection test complete", type: :success
:ok
{:error, :nxdomain} ->
debug_log "escaping from nxdomain."
Farmbot.System.reboot
{:error, reason} ->
debug_log "not escaping."
Farmbot.System.factory_reset("Fatal Error during "
<> "connection test! #{inspect reason}")
error ->
Farmbot.System.factory_reset("Fatal Error during "
<> "connection test! #{inspect error}")
end
end
@doc """
Connected to the World Wide Web. Should be called from the
callback module.
"""
def on_connect(context, pre_fun \\ nil, post_fun \\ nil)
def on_connect(%Context{} = context, pre_fun, post_fun) do
try do
# If we were supplied a pre connect callback, do that.
if pre_fun, do: pre_fun.()
:ok = connection_test(context)
:ok = maybe_set_time()
:ok = maybe_start_ssh()
:ok = maybe_get_fpf(context)
Logger.info ">> is trying to log in."
{:ok, token} = Auth.try_log_in!(context.auth)
:ok = maybe_setup_rollbar(token)
if post_fun do
post_fun.(token)
end
{:ok, token}
rescue
exception ->
Logger.error "Farmbot failed to log in."
debug_log("#{inspect exception}")
Farmbot.System.factory_reset("#{inspect exception}")
end
end
if Mix.env == :prod do
# Only set these attributes if we are in prod pls
@access_token Application.get_env(:farmbot, :rollbar_access_token)
@commit Mix.Project.config[:commit]
@target Mix.Project.config[:target]
def maybe_setup_rollbar(token) do
if String.contains?(token.unencoded.iss, "farmbot.io") and @access_token do
Logger.info ">> Setting up rollbar!"
# Application.put_env(:rollbax, :access_token, @access_token)
# Application.put_env(:rollbax, :environment, token.unencoded.iss)
# Application.put_env(:rollbax, :enabled, true)
# Application.put_env(:rollbas, :custom, %{target: @target})
#
# Application.put_env(:farmbot, :rollbar_occurrence_data, %{
# person_id: token.unencoded.bot,
# person_email: token.unencoded.sub,
# person_username: token.unencoded.bot,
# framework: "Nerves",
# code_version: @commit,
# })
# Application.ensure_all_started(:rollbax)
:ok = ExRollbar.setup [
person_username: token.unencoded.bot,
# enable_logger: true,
person_email: token.unencoded.sub,
code_version: @commit,
access_token: @access_token,
environment: token.unencoded.iss,
person_id: token.unencoded.bot,
framework: "Nerves",
custom: %{target: @target}
]
:ok
else
Logger.info ">> Not Setting up rollbar!"
:ok
end
end
else
def maybe_setup_rollbar(_) do
Logger.info ">> Not Setting up rollbar!"
Logger.info ">> Setting up slack updater"
:ok
end
end
@spec get_config(binary) :: {:ok, any}
defp get_config(key), do: GenServer.call(CS, {:get, Network, key})
# @spec get_config() :: {:ok, false | map}
# defp get_config, do: GenServer.call(CS, {:get, Network, :all})
defp get_mod(netman), do: GenServer.call(netman, :get_mod)
# GENSERVER STUFF
def handle_call(:get_context, _, state), do: {:reply, state.context, state}
def handle_call(:get_mod, _, state), do: {:reply, state.target, state}
def handle_call({:scan, interface_name}, _, state) do
f = mod(state.target).scan(state.context, interface_name)
{:reply, f, state}
end
def handle_call(:enumerate, _, state) do
f = mod(state.target).enumerate(state.context)
{:reply, f, state}
end
def handle_call({:start, interface, settings}, _, state) do
f = mod(state.target).start_interface(state.context, interface, settings)
{:reply, f, state}
end
def handle_call({:stop, interface}, _, state) do
f = mod(state.target).stop_interface(state.context, interface)
{:reply, f, state}
end
def terminate(reason, state) do
target_pid = Process.whereis(mod(state.target))
if target_pid do
GenServer.stop(target_pid, reason)
end
end
# Behavior
@type return_type :: :ok | {:error, term}
@callback scan(Context.t, binary) :: [binary] | {:error, term}
@callback enumerate(Context.t) :: [binary] | {:error, term}
@callback start_interface(Context.t, binary, map) :: return_type
@callback stop_interface(Context.t, binary) :: return_type
@callback start_link(Context.t) :: {:ok, pid}
end

View File

@ -1,87 +0,0 @@
defmodule Redis.Client.Public do
@moduledoc """
Public api for interfacing redis.
"""
@doc """
Sends a command to redis. Blocks
"""
@spec send_redis(pid, [binary]) :: binary
def send_redis(conn, stuff), do: Redix.command!(conn, stuff)
@doc """
Input a value by a given key.
"""
@spec input_value(pid, String.t, any) :: [String.t]
def input_value(redis, key, value) when is_map(value) do
input_map(redis, %{key => value})
end
def input_value(redis, key, value) when is_list(value) do
input_list(redis, key, value)
end
def input_value(redis, key, value) when is_tuple(value) do
input_value(redis, key, Tuple.to_list(value))
end
def input_value(redis, key, value),
do: send_redis redis, ["SET", key, value]
defp input_list(redis, key, list) do
send_redis redis, ["DEL", key]
for i <- list do
if is_binary(i) or is_integer(i) do
send_redis redis, ["RPUSH", key, i]
end
end
end
@spec input_map(pid, map | struct, String.t | nil) :: [String.t]
defp input_map(redis, map, bloop \\ nil)
defp input_map(redis, %{__struct__: _} = map, bloop),
do: input_map(redis, map |> Map.from_struct, bloop)
defp input_map(redis, map, bloop) when is_map(map) do
f = Enum.map(map, fn({key, value}) ->
cond do
# When value is a map, we need to go deeper.
is_map(value) ->
handle_map_value(bloop, redis, key, value)
# When its a map we have to do some magic.
is_list(value) ->
handle_list_value(bloop, redis, key, value)
# if neither of those
true ->
handle_rest_value(bloop, redis, key, value)
end
end)
List.flatten(f)
end
defp handle_map_value(bloop, redis, key, value) do
if bloop do
input_map(redis, value, "#{bloop}.#{key}")
else
input_map(redis, value, key)
end
end
defp handle_list_value(bloop, redis, key, value) do
if bloop do
input_list(redis, "#{bloop}.#{key}", value)
else
input_list(redis, key, value)
end
end
defp handle_rest_value(bloop, redis, key, value) do
if bloop do
input_value(redis, "#{bloop}.#{key}", value)
else
input_value(redis, key, value)
end
end
end

View File

@ -1,99 +0,0 @@
defmodule Redis.Server do
@moduledoc """
Port for a redis server
"""
@config Application.get_all_env(:farmbot)[:redis]
if Mix.env() == :dev do
def should_bind, do: "bind 0.0.0.0"
else
def should_bind, do: "bind 127.0.0.1"
end
def config(path: dir), do: ~s"""
#{should_bind()}
protected-mode yes
port #{@config[:port]}
tcp-backlog 511
unixsocket /tmp/redis.sock
unixsocketperm 700
timeout 0
tcp-keepalive 0
supervised no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile ""
databases 1
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
# dbfilename /tmp/dump.rdb
dir #{dir}
slave-serve-stale-data yes
slave-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
slave-priority 100
appendonly no
appendfilename "appendonly.aof"
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
hash-max-ziplist-value 64
zset-max-ziplist-value 64
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
"""
use GenServer
use Farmbot.DebugLog
require Logger
@doc """
Start the redis server.
"""
def start_link(_, opts), do: GenServer.start_link(__MODULE__, [], opts)
def config_file do
File.write("/tmp/redis.config", config(path: Farmbot.System.FS.path))
"/tmp/redis.config"
end
def init([]) do
kill_redis()
exe = System.find_executable("redis-server")
port = Port.open({:spawn_executable, exe},
[:stream,
:binary,
:exit_status,
:hide,
:use_stdio,
:stderr_to_stdout,
args: ['#{config_file()}']])
# Port.connect(port, self())
{:ok, port}
end
def handle_info({_port, {:data, info}}, port) do
debug_log info
{:noreply, port}
end
def handle_info({_port, _info}, port) do
{:noreply, port}
end
def kill_redis do
Logger.info "trying to kill old redis"
case System.cmd("killall", ["redis-server"]) do
{_, 0} ->
Process.sleep(5000)
_ -> :ok
end
end
def terminate(_, _port) do
kill_redis()
end
end

View File

@ -1,44 +1,26 @@
defmodule Farmbot.System.Supervisor do
@moduledoc """
Supervises Platform specific stuff for Farmbot to operate
Supervises Platform specific stuff for Farmbot to operate
"""
use Supervisor
@redis_config Application.get_all_env(:farmbot)[:redis]
@target Mix.Project.config()[:target]
alias Farmbot.Context
use Supervisor
import Farmbot.System.Init
alias Farmbot.System.Network
error_msg = """
Please configure your environment's init system!
"""
def start_link(%Context{} = context, opts) do
Supervisor.start_link(__MODULE__, context, opts)
@children Application.get_env(:farmbot, :init) || Mix.raise(error_msg)
@doc "Start the System Services. This is more or less `init`."
def start_link(args, opts \\ []) do
Supervisor.start_link(__MODULE__, args, opts)
end
def init(context) do
children = [
worker(Farmbot.System.FS,
[context, @target, [name: Farmbot.System.FS]]),
worker(Farmbot.System.FS.Worker,
[context, @target, [name: Farmbot.System.FS.Worker]]),
worker(Farmbot.System.FS.ConfigStorage,
[context, [name: Farmbot.System.FS.ConfigStorage]]),
worker(Network,
[context, @target, [name: Network]]),
worker(Farmbot.FactoryResetWatcher, [context, Network, []])
]
++ maybe_redis(context)
opts = [strategy: :one_for_one]
def init(args) do
children = Enum.map(@children, fn(child) ->
fb_init(child, [args, [name: child]])
end)
opts = [strategy: :one_for_all]
supervise(children, opts)
end
@spec maybe_redis(Context.t) :: [Supervisor.Spec.spec]
defp maybe_redis(context) do
if @redis_config[:server] do
[worker(Redis.Server, [context, [name: Redis.Server]])]
else
[]
end
end
end

View File

@ -1,13 +0,0 @@
defmodule Module.concat([Farmbot, System, "host", FileSystem]) do
@moduledoc false
@behaviour Farmbot.System.FS
def fs_init do
path = Application.get_env(:farmbot, :path)
File.mkdir_p!(path)
:ok
end
def mount_read_only, do: :ok
def mount_read_write, do: :ok
end

View File

@ -1,14 +0,0 @@
defmodule Module.concat([Farmbot, System, "host", Network]) do
@moduledoc false
@behaviour Farmbot.System.Network
use GenServer
def init(_) do
{:ok, []}
end
def start_link(_), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
def scan(_, _), do: ["testssod", "test2"]
def enumerate(_), do: ["testiface0"]
def start_interface(_, _, _), do: :ok
def stop_interface(_, _), do: :ok
end

View File

@ -1,18 +0,0 @@
defmodule Module.concat([Farmbot, System, "host"]) do
@moduledoc false
@behaviour Farmbot.System
def reboot, do: :ok
def power_off, do: :ok
def factory_reset(reason) do
Farmbot.System.FS.transaction fn() ->
File.rm_rf! "#{path()}"
File.mkdir_p! "#{path()}"
File.write("#{path()}/factory_reset_reason", reason)
end, true
System.halt(0)
end
defp path, do: Farmbot.System.FS.path()
end

View File

@ -1,13 +0,0 @@
defmodule Module.concat([Farmbot, System, "host", FileSystem]) do
@moduledoc false
@behaviour Farmbot.System.FS
def fs_init do
path = Application.get_env(:farmbot, :path)
File.mkdir_p!(path)
:ok
end
def mount_read_only, do: :ok
def mount_read_write, do: :ok
end

View File

@ -1,14 +0,0 @@
defmodule Module.concat([Farmbot, System, "host", Network]) do
@moduledoc false
@behaviour Farmbot.System.Network
use GenServer
def init(_) do
{:ok, []}
end
def start_link(_), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
def scan(_, _), do: ["testssod", "test2"]
def enumerate(_), do: ["testiface0"]
def start_interface(_, _, _), do: :ok
def stop_interface(_, _), do: :ok
end

View File

@ -1,18 +0,0 @@
defmodule Module.concat([Farmbot, System, "host"]) do
@moduledoc false
@behaviour Farmbot.System
def reboot, do: :ok
def power_off, do: :ok
def factory_reset(reason) do
files = path() |> File.ls!()
Farmbot.System.FS.transaction fn() ->
File.rm_rf files
File.write("#{path()}/factory_reset_reason", reason)
end, true
:ok
end
defp path, do: Farmbot.System.FS.path()
end

View File

@ -1,87 +0,0 @@
defmodule Farmbot.System.NervesCommon.FileSystem do
@moduledoc """
Common filesystem functionality for Nerves Devices
"""
defmacro __using__(
target: _,
ro_options: ro_options,
rw_options: rw_options,
state_path: state_path,
fs_type: fs_type,
block_device: block_device)
do
quote do
require Logger
# mount -t ext4 -o ro,remount /dev/mmcblk0p3 /state
@doc false
def mount_read_only,
do: "mount" |> System.cmd(unquote(ro_options)) |> parse_cmd
@doc false
def mount_read_write,
do: "mount" |> System.cmd(unquote(rw_options)) |> parse_cmd
@doc false
def fs_init do
# check if the formatted flag exists
with {:error, _} <- File.read("#{unquote(state_path)}/.formatted") do
# If it does, format the state partition
:ok = format_state_part()
else
# If not, we are fine. continue
_ ->
File.touch "/tmp/authorized_keys"
:ok
end
:ok = tzdata_hack()
end
@doc """
This needs to happen because tzdata by default polls and downloads
things to its release dir, which is read only in nerves environments.
to fix this we bundle the file it would normally download, and package
it into farmbot, then copy it to a configured dir for tzdata to
use because /tmp is read-write, we will just copy it there at every
boot becase its easier.
"""
def tzdata_hack do
Logger.info ">> Hacking tzdata..."
# File.cp "#{:code.priv_dir(:tzdata)}/release_ets/2016c.ets", "/tmp/"
File.write "/tmp/latest_remote_poll.txt", "2017-2-14"
File.mkdir "/tmp/release_ets"
File.cp_r "#{:code.priv_dir(:farmbot)}/release_ets/2016j.ets", "/tmp/release_ets/2016j.ets"
Logger.info ">> Hacked!"
:ok
end
@doc false
def factory_reset do
:ok = format_state_part()
:ok
end
defp parse_cmd({_, 0}), do: :ok
defp parse_cmd({err, num}),
do: raise "error doing command(#{num}): #{inspect err}"
defp format_state_part do
state_path = unquote(state_path)
# Format partition
System.cmd("mkfs.#{unquote(fs_type)}", ["#{unquote(block_device)}", "-F"])
# Mount it as read/write
# NOTE(connor): is there a reason i did this in band?
System.cmd("mount", ["-t", unquote(fs_type), "-o", "rw",
unquote(block_device), state_path])
# Basically a flag that says the partition is formatted.
File.write!("#{unquote(state_path)}/.formatted", "DONT CAT ME\n")
File.mkdir_p! "#{state_path}/farmware"
File.mkdir_p! "#{state_path}/farmware/packages"
File.mkdir_p! "#{state_path}/farmware/repos"
:ok
end
end
end
end

View File

@ -1,293 +0,0 @@
defmodule Farmbot.System.NervesCommon.Network do
@doc """
Common network functionality for Nerves Devices.
"""
defmacro __using__(opts) do
modules = Keyword.get(opts, :modules, [])
callback = Keyword.get(opts, :callback)
target = Keyword.get(opts, :target)
quote do
@behaviour Farmbot.System.Network
use GenServer
require Logger
alias Nerves.InterimWiFi, as: NervesWifi
alias Nerves.{NetworkInterface, Udhcpc, WpaSupplicant, SSDPServer}
alias Farmbot.System.Network.Hostapd
alias Farmbot.Context
use Farmbot.DebugLog
def start_link(%Context{} = ctx),
do: GenServer.start_link(__MODULE__, ctx, name: __MODULE__)
def init(context) do
for module <- unquote(modules) do
{_, 0} = System.cmd("modprobe", [module])
end
# wait for a few seconds for everything to settle
Process.sleep(5000)
# execute a callback if supplied.
cb = unquote(callback)
if cb do
Logger.info ">> doing target specific setup: #{unquote(target)}"
cb.()
end
{:ok, %{logging_in: false, context: context, retries: 0}}
end
# don't start this interface
def start_interface(%Context{} = ctx, _interface, %{"default" => false}) do
:ok
end
def start_interface(%Context{} = ctx, interface, %{"default" => "dhcp", "type" => "wired"} = s) do
interface |> NervesWifi.setup([]) |> cast_start_iface(interface, s)
end
def start_interface(%Context{} = ctx, interface,
%{"default" => "dhcp",
"type" => "wireless",
"settings" => settings} = s)
do
ssid = settings["ssid"]
case settings["key_mgmt"] do
"NONE" ->
interface
|> NervesWifi.setup([ssid: ssid, key_mgmt: :NONE])
|> cast_start_iface(interface, s)
"WPA-PSK" ->
psk = settings["psk"]
interface
|> NervesWifi.setup([ssid: ssid, key_mgmt: :"WPA-PSK", psk: psk])
|> cast_start_iface(interface, s)
end
:ok
end
def start_interface(%Context{} = ctx, interface,
%{"default" => "hostapd",
"settings" => %{"ipv4_address" => ip_addr}, "type" => "wireless"} = s)
do
{:ok, manager} = GenEvent.start_link()
[interface: interface, ip_address: ip_addr, manager: manager]
|> Hostapd.start_link()
|> cast_start_iface(interface, s)
end
def cast_start_iface(blah, interface, settings) do
case blah do
{:ok, pid} ->
GenServer.cast(__MODULE__,
{:start_interface, interface, settings, pid})
:ok
{:error, :already_added} ->
:ok
{:error, reason} ->
Logger.error("Encountered an error starting " <>
"#{interface}: #{reason}")
{:error, reason}
error ->
Logger.error("Encountered an error starting " <>
"#{interface}: #{error}")
{:error, error}
end
end
def stop_interface(%Context{} = _ctx, interface) do
GenServer.call(__MODULE__, {:stop_interface, interface})
end
def scan(%Context{} = ctx, iface), do: do_scan(iface)
def do_scan(iface, retry \\ false)
def do_scan(iface, true) do
{:ok, _} = NervesWifi.setup(iface)
Process.sleep(1000)
results =
case do_iw_scan(iface) do
{:ok, f} -> f
{:error, _} ->
Logger.error ">> Could not scan on #{iface}. " <>
"The device either isn't wireless or uses the legacy WEXT driver."
[]
end
NervesWifi.teardown(iface)
results
end
def do_scan(iface, false) do
case do_iw_scan(iface) do
{:ok, results} -> results
{:error, _} -> do_scan(iface, true)
end
end
defp do_iw_scan(iface) do
case System.cmd("iw", [iface, "scan", "ap-force"]) do
{res, 0} ->
f = res |> clean_ssid
{:ok, f}
e -> {:error, e}
end
end
def enumerate(%Context{} = _ctx), do: NetworkInterface.interfaces -- ["lo"]
defp clean_ssid(hc) do
hc
|> String.replace("\t", "")
|> String.replace("\\x00", "")
|> String.split("\n")
|> Enum.filter(fn(s) -> String.contains?(s, "SSID: ") end)
|> Enum.map(fn(z) -> String.replace(z, "SSID: ", "") end)
|> Enum.filter(fn(z) -> String.length(z) != 0 end)
end
# GENSERVER STUFF
def handle_call(:logged_in, _from, state) do
{:reply, :ok, %{state | logging_in: false}}
end
def handle_call({:stop_interface, interface}, _, state) do
case state[interface] do
{settings, pid} ->
if settings["default"] == "hostapd" do
if Process.alive?(pid) do
GenServer.stop(pid, :normal)
end
{:reply, :ok, Map.delete(state, interface)}
else
:ok = Registry.unregister(NetworkInterface, interface)
:ok = Registry.unregister(Udhcpc, interface)
:ok = Registry.unregister(WpaSupplicant, interface)
Logger.warn ">> cant stop: #{interface}"
{:reply, {:error, :not_implemented}, state}
end
_ -> {:reply, {:error, :not_started}, state}
end
end
def handle_cast({:start_interface, interface, settings, pid}, state) do
{:ok, _} = Registry.register(NetworkInterface, interface, [])
{:ok, _} = Registry.register(Udhcpc, interface, [])
{:ok, _} = Registry.register(WpaSupplicant, interface, [])
{:noreply, Map.put(state, interface, {settings, pid})}
end
def handle_info({:ssdp_timer, ip, uuid} = msg, state) do
spawn fn() ->
fields = ssdp_fields(ip)
{:ok, _} = SSDPServer.publish "uuid:#{uuid}", "nerves:farmbot", fields
end
{:noreply, state}
end
def handle_info(
{Udhcpc, :bound, %{ifname: interface, ipv4_address: ip}},
%{logging_in: true} = state
) do
{:noreply, state}
end
def handle_info(
{Udhcpc, :bound, %{ifname: interface, ipv4_address: ip}},
state
) do
that = self()
spawn fn() ->
Farmbot.System.Network.on_connect(state.context,
# before callback
fn() ->
try do
{_, 0} = System.cmd("epmd", ["-daemon"])
:net_kernel.start(['farmbot@#{ip}'])
rescue
_ ->
debug_log "could not start epmd or net_kernel"
:ok
end
Logger.info ">> is waiting for linux and network and what not."
Process.sleep(5000) # ye old race linux condidtion
uuid = UUID.uuid1()
send(that, {:ssdp_timer, ip, uuid})
GenServer.call(that, :logged_in)
end,
# after callback
fn(token) ->
for {key, value} <- state do
if match?({%{"default" => "hostapd"}, _}, value) do
Logger.info "Killing #{key}"
stop_interface(state.context, key)
end
end
end)
end
{:noreply, %{state | logging_in: true}}
end
def handle_info({WpaSupplicant, {:error, :psk, :FAIL}, %{ifname: _iface}}, state) do
Farmbot.System.factory_reset("""
I could not authenticate with the access point. This could be a bad
password, or an unsupported network type.
""")
{:stop, :factory_reset, state}
end
def handle_info(
{WpaSupplicant, _event, %{ifname: _iface}},
%{retries: retries} = state
) when retries > 5 do
Farmbot.System.factory_reset("""
I could not find the wifi access point. Check that it was inputted correctly.
I tried #{retries} times and still found nothing. Maybe I'm not close enough to the access point?
""")
{:noreply, state}
end
def handle_info(
{WpaSupplicant, event, %{ifname: _iface}},
state
) when is_atom(event) do
event = event |> Atom.to_string
wrong_key? = event |> String.contains?("reason=WRONG_KEY")
not_found? = event |> String.contains?("CTRL-EVENT-NETWORK-NOT-FOUND")
if wrong_key?, do: Farmbot.System.factory_reset("""
I could not authenticate with the access point. This could be a bad
password, or an unsupported network type.
""")
if not_found? do
{:noreply, %{state | retries: state.retries + 1}}
else
{:noreply, state}
end
end
def handle_info(info, state) do
{:noreply, state}
end
def terminate(_, _state) do
# TODO STOP INTERFACES
:ok
end
defp ssdp_fields(ip) do
[
location: "http://#{ip}/ssdp",
server: "Farmbot",
node: node(),
"cache-control": "max-age=1800"
]
end
end
end
end

View File

@ -1,24 +0,0 @@
defmodule Farmbot.System.NervesCommon do
@moduledoc """
Common functionality for nerves devices.
"""
defmacro __using__(target: _) do
quote do
def reboot, do: Nerves.Firmware.reboot()
def power_off, do: Nerves.Firmware.poweroff()
def factory_reset(reason) do
Farmbot.System.FS.transaction fn() ->
File.rm_rf "#{path()}/config.json"
File.rm_rf "#{path()}/secret"
File.rm_rf "#{path()}/farmware"
File.write("#{path()}/factory_reset_reason", reason)
end, true
reboot()
end
defp path, do: Farmbot.System.FS.path()
end
end
end

View File

@ -1,63 +0,0 @@
defmodule Farmbot.System.NervesCommon.Updates do
defmacro __using__(target: _) do
quote do
@behaviour Farmbot.System.Updates
require Logger
def install(path), do: :ok = Nerves.Firmware.upgrade_and_finalize(path)
defp blerp(tries \\ 0)
defp blerp(tries) when tries > 10 do
Logger.info "No serial handler", type: :warn
:ok
end
defp blerp(tries) do
ctx = Farmbot.Context.new()
pid = Process.whereis(ctx.serial)
if is_pid(pid) do
r = Farmbot.Serial.Handler.write ctx, "F83"
if r == :timeout do
Logger.info "Got timeout waiting for serial handler. Trying again"
blerp(tries + 1)
else
:ok
end
else
Logger.info "No serial handler yet, waiting...", type: :warn
Process.sleep(5000)
blerp(tries + 1)
end
end
def post_install do
Logger.info ">> Is doing post install stuff."
:ok = blerp()
ctx = Farmbot.Context.new
r = Farmbot.Serial.Handler.write ctx, "F83"
exp = Application.get_all_env(:farmbot)[:expected_fw_version]
case r do
{:report_software_version, version} when version == exp ->
Logger.info "Firmware is already the correct version!"
:ok
other ->
# we need to flash the firmware
IO.warn "#{inspect other}"
file = "#{:code.priv_dir(:farmbot)}/firmware.hex"
Logger.info ">> Doing post update firmware flash.", type: :warn
GenServer.cast(ctx.serial, {:update_fw, file, self()})
wait_for_finish()
end
end
defp wait_for_finish do
receive do
:done -> :ok
_e -> :reboot
end
end
end
end
end

View File

@ -1,16 +0,0 @@
defmodule Module.concat([Farmbot, System, "rpi3", FileSystem]) do
@moduledoc false
@behaviour Farmbot.System.FS
@state_path Application.get_env(:farmbot, :path)
@block_device "/dev/mmcblk0p3"
@fs_type "ext4"
@ro_options ["-t", @fs_type, "-o", "ro,remount", @block_device, @state_path]
@rw_options ["-t", @fs_type, "-o", "rw,remount", @block_device, @state_path]
use Farmbot.System.NervesCommon.FileSystem,
target: "rpi3",
ro_options: @ro_options,
rw_options: @rw_options,
state_path: @state_path,
fs_type: @fs_type,
block_device: @block_device
end

View File

@ -1,9 +0,0 @@
defmodule Module.concat([Farmbot, System, "rpi3", Network]) do
use Farmbot.System.NervesCommon.Network,
target: "rpi3", modules: ["brcmfmac"], callback: fn() ->
# iw dev wlan0 interface add wlan1 type __ap
# iw_args = ~w"dev wlan0 interface add wlan1 type __ap"
# {_, 0} = System.cmd("iw", iw_args)
:ok
end
end

View File

@ -1,6 +0,0 @@
defmodule Module.concat([Farmbot, System, "rpi3"]) do
@moduledoc false
@behaviour Farmbot.System
use Farmbot.System.NervesCommon, target: "rpi3"
end

View File

@ -1,3 +0,0 @@
defmodule Module.concat([Farmbot, System, "rpi3", Updates]) do
use Farmbot.System.NervesCommon.Updates, target: "rpi3"
end

View File

@ -1,16 +0,0 @@
target = Mix.Project.config[:target]
unless target == "host" do
nerves_common_path = "lib/farmbot/system/targets/nerves_common"
nc_files = File.ls! nerves_common_path
for file <- nc_files, do: Code.eval_file(file, nerves_common_path)
end
path = case Mix.env() do
:test ->
"lib/farmbot/system/targets/#{target}_test"
_ ->
"lib/farmbot/system/targets/#{target}"
end
files = File.ls! path
for file <- files, do: Code.eval_file(file, path)

View File

@ -1,128 +0,0 @@
defmodule Farmbot.System.Updates do
@moduledoc """
Check, download, apply updates
"""
@target Mix.Project.config()[:target]
@path "/tmp/update.fw"
require Logger
alias Farmbot.System.FS
alias Farmbot.{Context, HTTP}
# TODO(connor): THIS IS A MINOR IMPROVEMENT FROM THE LAST VERSION OF THIS FILE
# BUT IT DEFINATELY NEEDS FURTHER REFACTORING.
@spec mod(atom) :: atom
defp mod(target), do: Module.concat([Farmbot, System, target, Updates])
defp releases_url(%Context{} = context) do
{:ok, token} = Farmbot.Auth.get_token(context.auth)
token.unencoded.os_update_server
end
@doc """
Checks for updates if the bot says so
"""
def do_update_check do
context = Farmbot.Context.new()
if Farmbot.BotState.get_config(context, :os_auto_update) do
check_and_download_updates(context)
else
Logger.info ">> Will not do update check!"
end
end
@doc """
Checks for updates, and if there is an update, downloads, and applies it.
"""
@spec check_and_download_updates(Context.t)
:: :ok | {:error, term} | :no_updates
def check_and_download_updates(%Context{} = ctx) do
case check_updates(ctx) do
{:update, url} ->
Logger.info ">> has found a new Operating System update! #{url}",
type: :busy
install_updates(ctx, url)
:no_updates ->
Logger.info ">> is already on the latest Operating System version!",
type: :success
:no_updates
{:error, reason} ->
Logger.error ">> encountered an error checking for updates! #{reason}"
{:error, reason}
end
end
@doc """
Checks for updates
"""
@spec check_updates(Context.t)
:: {:update, binary} | :no_updates | {:error, term}
def check_updates(%Context{} = context) do
current = Farmbot.BotState.get_os_version(context)
if String.contains?(current, "rc") do
msg = "Release Candidate Releases don't currently support updates!"
Logger.info msg, type: :warn
:no_updates
else
do_http_req(context)
end
end
@spec check_updates(Context.t)
:: {:update, binary} | :no_updates | {:error, term}
defp do_http_req(%Context{} = ctx) do
case HTTP.get(ctx, releases_url(ctx)) do
{:ok, %HTTP.Response{body: body, status_code: 200}} ->
json = Poison.decode!(body)
version = json["tag_name"]
version_without_v = String.trim_leading version, "v"
list = json["assets"]
item = Enum.find(list, fn(item) ->
item["name"] == "farmbot-#{@target}-#{version_without_v}.fw"
end)
url = item["browser_download_url"]
{:update, url}
{:error, reason} -> {:error, reason}
end
end
@doc """
Installs an update from a url
"""
@spec install_updates(Context.t, String.t) :: no_return
def install_updates(%Context{} = context, url) do
# Ignore the compiler warning here.
# "I'll fix it later i promise" -- Connor Rigby
# "i promise I will fix this one day..." -- Connor Rigby
fun = Farmbot.BotState.download_progress_fun(context, "FBOS_OTA")
path = HTTP.download_file!(context, url, @path, fun)
case File.stat(path) do
{:ok, file} ->
Logger.info "Found file: #{inspect file}", type: :success
e -> Logger.error "Could not find update file: #{inspect e}"
end
setup_post_update()
mod(@target).install(path)
Farmbot.System.reboot()
end
@doc """
Will cause `post_install/0` to be called next reboot.
"""
def setup_post_update do
FS.transaction fn ->
Logger.info "Seting up post update!", type: :busy
path = "#{FS.path()}/.post_update"
:ok = File.write(path, "DONT CAT ME\r\n")
end, true
end
@spec post_install :: no_return
def post_install, do: mod(@target).post_install()
@callback install(binary) :: no_return
@callback post_install() :: no_return
end

View File

@ -1,48 +0,0 @@
defmodule Farmbot.Transport.Redis do
@moduledoc """
Interacts with teh redis db
"""
use GenStage
require Logger
alias Farmbot.Context
@config Application.get_all_env(:farmbot)[:redis]
@ping_time 5_000
@save_time 900_000
@doc """
Starts a stage for a Redis
"""
@spec start_link(Context.t, [{atom, any}]) :: {:ok, pid}
def start_link(context, opts), do: GenStage.start_link(__MODULE__, context, opts)
def init(%Context{} = _context) do
{:ok, conn} = Redix.start_link(host: "localhost", port: @config[:port])
Process.link(conn)
Process.send_after(self(), :ping, @ping_time)
Process.send_after(self(), :save, @save_time)
{:consumer, conn, subscribe_to: [Farmbot.Transport]}
end
def handle_info({_from, {:status, stuff}}, redis) do
Redis.Client.Public.input_value(redis, "BOT_STATUS", stuff)
{:noreply, [], redis}
end
def handle_info(:ping, conn) do
Redis.Client.Public.send_redis(conn, ["PING"])
Process.send_after(self(), :ping, @ping_time)
{:noreply, [], conn}
end
def handle_info(:save, conn) do
Farmbot.System.FS.transaction fn() ->
Redis.Client.Public.send_redis(conn, ["SAVE"])
end
Process.send_after(self(), :save, @save_time)
{:noreply, [], conn}
end
def handle_info(_event, redis) do
{:noreply, [], redis}
end
end

View File

@ -1,253 +0,0 @@
defmodule Logger.Backends.FarmbotLogger do
@moduledoc """
Logger backend for logging to the frontend and dumping to the API.
Takes messages that were logged useing Logger, if they can be
jsonified, adds them too a buffer, when that buffer hits a certain
size, it tries to dump the messages onto the API.
"""
alias Farmbot.{Context, HTTP}
use GenEvent
require Logger
use Farmbot.DebugLog
@save_path Application.get_env(:farmbot, :path) <> "/logs.txt"
# ten megs. i promise
@max_file_size 1.0e+7
@filtered "[FILTERED]"
@log_amnt 5
@typedoc """
The state of the logger
"""
@type state :: %{logs: [log_message], posting: boolean, context: nil | Context.t}
@typedoc """
Type of message for ticker
"""
@type rpc_log_type
:: :success
| :busy
| :warn
| :error
| :info
| :fun
| :debug
@typedoc """
Elixir Logger level type
"""
@type logger_level
:: :info
| :warn
| :error
| :debug
@typedoc """
One day this will me more
"""
@type channel :: :toast | :email
@type log_message
:: %{message: String.t,
channels: [channel],
created_at: integer,
meta: %{type: rpc_log_type}}
def init(_) do
# ctx = Farmbot.Context.new()
debug_log "Starting Farmbot Logger"
{:ok, %{logs: [], posting: false, context: nil}}
end
# The example said ignore messages for other nodes, so im ignoring messages
# for other nodes.
def handle_event({_, gl, {Logger, _, _, _}}, state) when node(gl) != node() do
{:ok, state}
end
# The logger event.
def handle_event({_level, _, {Logger, _, _, _}}, %{context: nil} = state) do
debug_log "Ignoreing message because no context"
{:ok, state}
end
def handle_event({level, _, {Logger, message, timestamp, metadata}}, state) do
# if there is type key in the meta we need that to have priority
type = Keyword.get(metadata, :type, level)
channels = Keyword.get(metadata, :channels, [])
created_at = parse_created_at(timestamp)
san_m = sanitize(message, metadata, state)
log = build_log(san_m, created_at, type, channels)
:ok = GenServer.cast(state.context.transport, {:log, log})
logs = [log | state.logs]
if (!state.posting) and (Enum.count(logs) >= @log_amnt) do
# If not already posting, and more than 50 messages
spawn fn ->
# debug_log "going to try to post"
filterd_logs = Enum.filter(logs, fn(log) ->
log.message != @filtered
end)
do_post(state.context, filterd_logs)
end
{:ok, %{state | logs: logs, posting: true}}
else
# debug_log "not posting and logs less than 50"
{:ok, %{state | logs: logs}}
end
end
def handle_event(:flush, state), do: {:ok, %{state | logs: [], posting: false}}
def handle_call(:post_success, state) do
debug_log "Logs uploaded!"
write_file(Enum.reverse(state.logs))
{:ok, :ok, %{state | posting: false, logs: []}}
end
def handle_call(:post_fail, state) do
debug_log "Logs failed to upload!"
{:ok, :ok, %{state | posting: false}}
end
def handle_call(:messages, state) do
{:ok, Enum.reverse(state.logs), state}
end
def handle_call({:context, %Context{} = ctx}, state) do
{:ok, :ok, %{state | context: ctx}}
end
def handle_call(:get_state, state), do: {:ok, state, state}
@spec do_post(Context.t, [log_message]) :: no_return
defp do_post(%Context{} = ctx, logs) do
try do
debug_log "doing post"
{:ok, %{status_code: 200}} = HTTP.post(ctx, "/api/logs", Poison.encode!(logs))
:ok = GenEvent.call(Elixir.Logger, __MODULE__, :post_success)
rescue
e ->
debug_log "post failed: #{inspect e}"
:ok = GenEvent.call(Elixir.Logger, __MODULE__, :post_fail)
end
end
@spec write_file([log_message]) :: no_return
defp write_file(logs) do
debug_log("Writing log file!")
old = read_file()
new_file = Enum.reduce(logs, old, fn(log, acc) ->
if log.message != @filtered do
acc <> log.message <> "\r\n"
else
acc
end
end)
bin = fifo(new_file)
Farmbot.System.FS.transaction fn ->
File.write(@save_path, bin <> "\r\n")
end
end
# reads the current file.
# if the file isnt found, returns an empty string
@spec read_file :: binary
defp read_file do
case File.read(@save_path) do
{:ok, bin} -> bin
_ -> ""
end
end
# if the file is to big, fifo it
# trim lines off the file until it is smaller than @max_file_size
@spec fifo(binary) :: binary
defp fifo(new_file) when byte_size(new_file) > @max_file_size do
[_ | rest] = String.split(new_file, "\r\n")
fifo(Enum.join(rest, "\r\n"))
end
defp fifo(new_file), do: new_file
# if this backend crashes just pop it out of the logger backends.
# if we don't do this it bacomes a huge mess because of Logger
# trying to restart this module
# then this module dying again
# then printing a HUGE supervisor report
# then Logger trying to add it again etc
def terminate(_,_), do: spawn fn -> Logger.remove_backend(__MODULE__) end
@spec sanitize(binary, [any], state) :: String.t
defp sanitize(message, meta, state) do
module = Keyword.get(meta, :module)
unless meta[:nopub] do
message
|> filter_module(module)
|> filter_text(state)
end
end
@modules [
:"Elixir.Nerves.InterimWiFi",
:"Elixir.Nerves.NetworkInterface",
:"Elixir.Nerves.InterimWiFi.WiFiManager.EventHandler",
:"Elixir.Nerves.InterimWiFi.WiFiManager",
:"Elixir.Nerves.InterimWiFi.DHCPManager",
:"Elixir.Nerves.InterimWiFi.Udhcpc",
:"Elixir.Nerves.NetworkInterface.Worker",
:"Elixir.Nerves.InterimWiFi.DHCPManager.EventHandler",
:"Elixir.Nerves.WpaSupplicant",
]
for module <- @modules, do: defp filter_module(_, unquote(module)), do: @filtered
defp filter_module(message, _module), do: message
defp filter_text(">>" <> m, state) do
device = try do
Farmbot.Database.Selectors.get_device(state.context)
rescue
_e in Farmbot.Database.Selectors.Error -> %{name: "Farmbot"}
end
filter_text(device.name <> m, state)
end
defp filter_text(m, _state) when is_binary(m) do
try do
Poison.encode!(m)
m
rescue
_ -> @filtered
end
end
defp filter_text(_message, _state), do: @filtered
# Couuld probably do this inline but wheres the fun in that. its a functional
# language isn't it?
# Takes Loggers time stamp and converts it into a unix timestamp.
defp parse_created_at({{year, month, day}, {hour, minute, second, _mil}}) do
%DateTime{year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {0,0},
std_offset: 0,
time_zone: "Etc/UTC",
utc_offset: 0,
zone_abbr: "UTC"}
|> DateTime.to_unix#(:milliseconds)
end
@spec build_log(String.t, number, rpc_log_type, [channel]) :: log_message
defp build_log(message, created_at, type, channels) do
%{message: message,
created_at: created_at,
channels: channels,
meta: %{type: type, x: 0, y: 0, z: 0}}
end
end

View File

@ -1,61 +0,0 @@
defmodule Mix.Tasks.Farmbot.Discover do
@moduledoc """
Usage: mix farmbot.discover [OPTS]
## OPTS
* `--print` - print the output. (Optional)
* `--format=FORMAT` - format to print. (Optional, ignored if not printing.)
## FORMAT
* `json` - print as json.
* `human` - print in human readable form. (default)
## Examples
```
$ mix farmbot.discover --print
%{"cache-control": "max-age=1800", host: "192.168.29.103",
location: "http://192.168.29.103/ssdp", node: "farmbot@nerves-7e4b",
server: "Farmbot", service_name: "uuid:4d6ac078-0e4a-4a7a-b6cf-e951e5c959c5",
st: "nerves:farmbot"}
```
```
$ mix farmbot.discover --print --format json
[{"st":"nerves:farmbot","service_name":"uuid:4d6ac078-0e4a-4a7a-b6cf-e951e5c959c5","server":"Farmbot","node":"farmbot@nerves-7e4b","location":"http://192.168.29.103/ssdp","host":"192.168.29.103","cache-control":"max-age=1800"}]
```
"""
use Mix.Task
@shortdoc """
Discovers farmbots on the network via ssdp.
"""
def run(opts) do
devices = Nerves.SSDPClient.discover()
|> Enum.filter(fn({_, device}) -> device.st == "nerves:farmbot" end)
|> Enum.map(fn({sn, device}) -> Map.put(device, :service_name, sn) end)
switches = [print: :boolean, format: :string]
{kws, _, _} = OptionParser.parse(opts, switches: switches)
should_print? = Keyword.get(kws, :print, false)
if should_print? do
format = Keyword.get(kws, :format, "human")
do_print(devices, format)
end
devices
end
defp do_print(devices, "json") do
IO.puts Poison.encode!(devices)
end
defp do_print([device | rest], format) do
print_device(device)
do_print(rest, format)
end
defp do_print([], _), do: :ok
defp print_device(device) do
IO.inspect device
end
end

View File

@ -1,8 +0,0 @@
defmodule Mix.Tasks.Farmbot.Env do
@moduledoc false
use Mix.Task
def run(_) do
Mix.shell.info([:green, "Building initial environment for Farmbot OS"])
Code.eval_file("scripts/generate_makefile.exs", File.cwd!)
end
end

View File

@ -1,34 +0,0 @@
defmodule Mix.Tasks.Farmbot.Sign do
@moduledoc """
usage: MIX_TARGET=rpi3 mix farmbot.sign ../fwup-key.priv
"""
use Mix.Task
@shortdoc "Signs a fw image"
def run(args) do
otp_app = Mix.Project.config[:app]
target = Mix.Project.config[:target]
fw_file = Path.join(["images", "#{Mix.env()}", "#{target}", "#{otp_app}.fw"])
case args do
[priv_key_file, out_file] ->
do_run(priv_key_file, fw_file, out_file)
[priv_key_file] ->
out_file = Path.join(["images", "#{Mix.env()}", "#{target}", "#{otp_app}-signed.fw"])
do_run(priv_key_file, fw_file, out_file)
_ -> Mix.raise("""
Usage: mix farmbot.sign /tmp/fwup-key.priv /tmp/outputfile-signed.fw
""")
end
end
defp do_run(priv_key_file, unsigned_file, out_file) do
Mix.shell.info [:green, "Signing: #{unsigned_file} with: #{priv_key_file} to: #{out_file}"]
unless File.exists?(unsigned_file) do
Mix.raise "Could not find Firmware!"
end
System.cmd("fwup", ["-S", "-s", priv_key_file, "-i", unsigned_file, "-o", out_file])
end
end

View File

@ -1,83 +0,0 @@
defmodule Mix.Tasks.Farmbot.Slack do
@moduledoc "Upload a file to slack. Requires a slack key env var."
use Mix.Task
alias Farmbot.{Context, HTTP}
@shortdoc "Upload an image to farmbot slack."
def run(opts) do
Registry.start_link(:duplicate, Farmbot.Registry)
Application.ensure_all_started(:httpoison)
{keywords, uhh, _} = opts |> OptionParser.parse(switches: [signed: :boolean])
signed? = Keyword.get(keywords, :signed, false)
{:ok, _} = Farmbot.DebugLog.start_link()
ctx = Context.new()
{:ok, http} = HTTP.start_link(ctx, [])
ctx = %{ctx | http: http}
otp_app = Mix.Project.config[:app]
target = Mix.Project.config[:target]
commit = Mix.Project.config[:commit]
fw_file = Path.join(["images", "#{Mix.env()}", "#{target}",
(if signed?, do: "#{otp_app}-signed.fw", else: "#{otp_app}.fw")])
unless File.exists?(fw_file) do
Mix.raise "Could not find firmware: #{fw_file}"
end
token = System.get_env("SLACK_TOKEN") || Mix.raise "Could not find SLACK_TOKEN"
time = format_date_time(File.stat!(fw_file))
filename = "#{otp_app}-#{Mix.env()}-#{commit}#{if signed?, do: "-signed-", else: "-"}#{time}.fw"
comment = Enum.join(uhh, " ")
Mix.shell.info [:green, "Uploading: #{filename} (#{fw_file})"]
url = "https://slack.com/api/files.upload"
form_data = %{
:file => fw_file,
"token" => token,
"channels" => "embedded-systems",
"filename" => filename,
"title" => filename,
"initial_comment" => """
*New Farmbot Firmware!*
> *_Env_*: `#{Mix.env()}`
> *_Target_*: `#{target}`
> *_Commit_*: `#{commit}`
> *_Time_*: `#{time}`
#{String.trim(comment)}
"""
}
payload = Enum.map(form_data, fn({key, val}) -> {key, val} end)
real_payload = {:multipart, payload}
headers = [
{'User-Agent', 'Farmbot HTTP Adapter'}
]
%{body: body} = HTTP.post!(ctx, url, real_payload, headers)
ok? = body |> Poison.decode!() |> Map.get("ok", false)
notify_opts = ~w"""
-u normal -i #{:code.priv_dir(otp_app)}/static/farmbot_logo.png -a farmbot_build
"""
message = if ok?, do: "Upload completed", else: "Upload failed!"
System.cmd("notify-send", [notify_opts | ["Farmbot Uploader", message]] |> List.flatten())
end
defp format_date_time(%{mtime: {{yr,m,day}, {hr, min, sec}}}) do
dt = %DateTime{
hour: hr,
year: yr,
month: m,
day: day,
minute: min,
second: sec,
time_zone: "Etc/UTC",
zone_abbr: "UTC",
std_offset: 0,
utc_offset: 0
} |> Timex.local()
"#{dt.year}-#{pad(dt.month)}-#{pad(dt.day)}_#{pad(dt.hour)}#{pad(dt.minute)}"
end
defp pad(int) do
if int < 10, do: "0#{int}", else: "#{int}"
end
end

View File

@ -1,71 +0,0 @@
defmodule Mix.Tasks.Farmbot.Upload do
@moduledoc false
use Mix.Task
@shortdoc "Uploads a file to a url"
def run(opts) do
otp_app = Mix.Project.config[:app]
target = Mix.Project.config[:target]
{keywords, ip_address} =
case opts |> OptionParser.parse(switches: [signed: :boolean]) do
{keywords, [ip_address], _} -> {keywords, ip_address}
{keywords, [], _} -> {keywords, try_discover()}
end
signed_bool = Keyword.get(keywords, :signed, false)
file_name = Path.join(["images", "#{Mix.env()}", "#{target}", find_file_name(otp_app, signed_bool)])
unless File.exists?(file_name) do
Mix.raise("Could not find firmware: #{file_name}")
end
Mix.shell.info "Trying to uploading firmware: #{file_name}"
Application.ensure_all_started(:httpoison)
context = Farmbot.Context.new()
Registry.start_link(:duplicate, Farmbot.Registry)
{:ok, _} = Farmbot.DebugLog.start_link()
{:ok, http} = Farmbot.HTTP.start_link(context, [])
context = %{context | http: http}
url = "http://#{ip_address}"
ping_url = "#{url}/api/ping"
upload_url = "#{url}/api/upload_firmware"
Farmbot.HTTP.get! context, ping_url
Mix.shell.info [:green, "Connected to bot."]
payload = [
{:file, file_name}
]
# payload = [{:file, file_name}]
Mix.shell.info "Uploading..."
# headers = ["content-type", "http/form_data"]
r = Farmbot.HTTP.post! context, upload_url, {:multipart, payload}, [], timeout: :infinity
unless match?(%{status_code: 200}, r) do
Mix.raise "Failed to upload firmware: #{format r}"
end
Mix.shell.info [:green, "Finished!"]
end
defp format(%{body: body}), do: body
defp format(other), do: inspect other
defp find_file_name(otp_app, true), do: "#{otp_app}-signed.fw"
defp find_file_name(otp_app, false), do: "#{otp_app}.fw"
defp try_discover do
devices = Mix.Tasks.Farmbot.Discover.run([])
case devices do
[device] -> device.host
[_device | _more] -> do_raise("detected more than one farmbot.")
[] -> do_raise("could not detect farmbot.")
end
end
defp do_raise(msg) do
Mix.raise "#{msg} Please supply the ip address of your bot."
end
end

View File

@ -1,5 +0,0 @@
defmodule Mix.Tasks.Farmbot.Warning do
@moduledoc false
use Mix.Task
def run(_), do: Mix.raise("Please export a MIX_TARGET")
end

View File

@ -30,6 +30,7 @@ defmodule Farmbot.Mixfile do
images_path: "images/#{Mix.env()}/#{@target}",
config_path: "config/config.exs",
lockfile: "mix.lock",
elixirc_paths: elixirc_paths(Mix.env, @target),
compilers: Mix.compilers ++ maybe_use_webpack(),
aliases: aliases(@target),
deps: deps() ++ system(@target),
@ -190,7 +191,6 @@ defmodule Farmbot.Mixfile do
]
end
# TODO(connor): Build this into `:ex_webpack`
defp maybe_use_webpack() do
case System.get_env("NO_WEBPACK") do
@ -199,6 +199,13 @@ defmodule Farmbot.Mixfile do
end
end
defp elixirc_paths(:test, "host") do
["./lib", "./nerves/host", "./test/support"]
end
defp elixirc_paths(_env, target) do
["./lib", "./nerves/#{target}"]
end
# this is for cross compilation to work
# New version of nerves might not need this?

View File

@ -1,113 +0,0 @@
defmodule Farmbot.AuthTest do
use ExUnit.Case, async: false
alias Farmbot.{Context, Auth}
setup_all do
context = Context.new()
new_context = Farmbot.Test.Helpers.Context.replace_http(context)
{:ok, auth_pid} = Auth.start_link(new_context, [])
[cs_context: %{new_context | auth: auth_pid}]
end
setup %{cs_context: context} do
:ok = Auth.purge_token(context.auth)
:ok
end
test "returns nil if no token", %{cs_context: context} do
thing = Auth.get_token(context.auth)
assert thing == nil
end
test "logs in", %{cs_context: context} do
{:ok, login_token} = Farmbot.Auth.try_log_in(context.auth)
assert match?(%Farmbot.Token{}, login_token)
end
test "gets the current token", %{cs_context: context} do
good_interim(context.auth)
{:ok, login_token} = Farmbot.Auth.try_log_in(context.auth)
{:ok, token} = Auth.get_token(context.auth)
assert login_token == token
end
test "gets the current server", %{cs_context: context} do
good_interim(context.auth)
{:ok, server} = Auth.get_server(context.auth)
assert server == "http://localhost:3000"
end
# test "doesnt get a token on bad creds", %{cs_context: context} do
# bad_interim(context.auth)
# {:error, reason} = Auth.try_log_in(context.auth)
# assert reason == :bad_password
# end
test "logs in aggressivly", %{cs_context: context} do
good_interim(context.auth)
{:ok, login_token} = Farmbot.Auth.try_log_in!(context.auth)
assert match?(%Farmbot.Token{}, login_token)
end
test "logs in with creds, then with a secret", %{cs_context: context} do
good_interim(context.auth)
{:ok, login_token} = Farmbot.Auth.try_log_in(context.auth)
assert match?(%Farmbot.Token{}, login_token)
Process.sleep(100)
{:ok, new_token} = Farmbot.Auth.try_log_in(context.auth)
assert match?(%Farmbot.Token{}, new_token)
end
# test "factory resets the bot on bad log in", %{cs_context: context} do
# use_cassette "bad_login" do
# bad_interim(context.auth)
# with_mock Farmbot.System, [factory_reset: fn(_reason) -> :ok end] do
# Auth.try_log_in!(context.auth)
# assert called Farmbot.System.factory_reset(:_)
# end
# end
# end
# test "keeps a backup in case of catastrophic issues secret", %{cs_context: context}do
# Auth.purge_token(context.auth)
# use_cassette "good_login" do
# good_interim(context.auth)
# {:ok, login_token} = Farmbot.Auth.try_log_in!(context.auth)
# assert match?(%Farmbot.Token{}, login_token)
#
# assert_raise RuntimeError, fn() ->
# GenServer.stop(context.auth, :uhhhh?)
# end
# File.rm("/tmp/secret") # this is the good file
# Process.sleep(500)
#
# {:ok, token} = Farmbot.Auth.try_log_in!(context.auth)
# assert match?(%Farmbot.Token{}, token)
# end
# end
# test "forces a new token" do
# Auth.purge_token
# use_cassette "good_login" do
# good_interim(context.auth)
# {:ok, login_token} = Farmbot.Auth.try_log_in!
# assert match?(%Farmbot.Token{}, login_token)
# end
#
# use_cassette "good_login_2" do
# send Farmbot.Auth, :new_token
# {:ok, token} = Farmbot.Auth.get_token
# assert match?(%Farmbot.Token{}, token)
# end
# end
defp good_interim(auth) do
:ok = Auth.interim(auth, "admin@admin.com", "password123", "http://localhost:3000")
end
defp bad_interim(auth) do
:ok = Auth.interim(auth, "fail@fail.org", "password", "http://localhost:3000")
end
end

View File

@ -1,35 +0,0 @@
defmodule Farmbot.BotState.ConfigurationTest do
use ExUnit.Case, async: false
alias Farmbot.Context
setup_all do
cs_context = Context.new()
[cs_context: cs_context]
end
test "makes sure we dont mess state up with bad calls or casts", context do
before_call = get_state(context.cs_context)
resp = GenServer.call(context.cs_context.configuration, :do_a_barrel_roll)
after_call = get_state(context.cs_context)
assert(resp == :unhandled)
assert(after_call == before_call)
GenServer.cast(context.cs_context.configuration, :bot_net_start)
after_cast = get_state(context.cs_context)
assert(before_call == after_cast)
end
test "updates a setting inside informational settings", context do
conf = context.cs_context.configuration
old = get_state(context.cs_context)
GenServer.cast(conf, {:update_info, :i_know_this, :its_unix})
# maybe bug? change this cast to a call?
new = get_state(context.cs_context)
assert(old != new)
end
defp get_state(ctx) do
Process.sleep(10)
:sys.get_state(ctx.monitor).state |> Map.fetch!(:configuration)
end
end

View File

@ -1,146 +0,0 @@
defmodule Farmbot.BotStateTest do
use ExUnit.Case, async: false
alias Farmbot.Context
setup_all do
context = Context.new()
[cs_context: context]
end
test "Gets the current bot position", %{cs_context: context} do
[x,y,z] = Farmbot.BotState.get_current_pos(context)
assert(is_integer(x) and is_integer(y) and is_integer(z))
end
test "Sets a new position", %{cs_context: context} do
Farmbot.BotState.set_pos(context, 45, 123, -666)
[x,y,z] = Farmbot.BotState.get_current_pos(context)
assert(x == 45)
assert(y == 123)
assert(z == -666)
end
test "sets a pin mode", %{cs_context: context} do
Farmbot.BotState.set_pin_mode(context, 123, 0)
%{mode: mode, value: _} = Farmbot.BotState.get_pin(context, 123)
assert(mode == 0)
end
test "sets a pin value", %{cs_context: context} do
Farmbot.BotState.set_pin_value(context, 123, 55)
%{mode: _, value: value} = Farmbot.BotState.get_pin(context, 123)
assert(value == 55)
end
test "updates tons of configs", %{cs_context: context} do
true = Farmbot.BotState.update_config(context, "os_auto_update", false)
os_auto_update = Farmbot.BotState.get_config(context, :os_auto_update)
assert(os_auto_update == false)
true = Farmbot.BotState.update_config(context, "steps_per_mm_x", 123)
true = Farmbot.BotState.update_config(context, "steps_per_mm_y", 456)
true = Farmbot.BotState.update_config(context, "steps_per_mm_z", 789)
x = Farmbot.BotState.get_config(context, :steps_per_mm_x)
y = Farmbot.BotState.get_config(context, :steps_per_mm_y)
z = Farmbot.BotState.get_config(context, :steps_per_mm_z)
assert([x,y,z] == [123,456,789])
fail = Farmbot.BotState.update_config(context, "self_destruct_count_down", 10_000)
assert(fail == false)
end
test "gets the current os version", %{cs_context: context} do
# i just want the coverage report ok
os = Farmbot.BotState.get_os_version(context)
assert(is_bitstring(os))
end
test "sets end stops", %{cs_context: context} do
es = {1,1,1,1,1,1}
Farmbot.BotState.set_end_stops(context, es)
assert(get_hardware_part(:end_stops, context) == es)
end
test "gets all the mcu params", %{cs_context: context} do
assert get_hardware_part(:mcu_params, context) == Farmbot.BotState.get_all_mcu_params(context)
end
test "sets firmware version", %{cs_context: context} do
Farmbot.BotState.set_fw_version(context, "uhhhhh")
Process.sleep(100)
assert Farmbot.BotState.get_fw_version(context) == "uhhhhh"
end
test "sets some farmware env vars", %{cs_context: context} do
r = Farmbot.BotState.set_user_env(context, %{"SUPER_COOL_VAR" => 123})
assert(r == true)
abc = Farmbot.BotState.set_user_env(context, "DIFFERENT_COOL_VAR", "String value")
assert(abc == true)
end
test "locks the bot then unlocks it", %{cs_context: context} do
:ok = Farmbot.BotState.lock_bot(context)
stuffA = get_config_part(:informational_settings, context)[:locked]
lockedA? = Farmbot.BotState.locked?(context)
assert stuffA == true
assert stuffA == lockedA?
:ok = Farmbot.BotState.unlock_bot(context)
stuffB = get_config_part(:informational_settings, context)[:locked]
lockedB? = Farmbot.BotState.locked?(context)
assert stuffB == false
assert stuffB == lockedB?
end
test "sets sync msg to :synced", %{cs_context: context} do
context = %{context | serial: nil}
:ok = Farmbot.BotState.set_sync_msg(context, :synced)
thing = get_config_part(:informational_settings, context)[:sync_status]
assert thing == :synced
end
test "sets sync msg to :sync_now", %{cs_context: context} do
:ok = Farmbot.BotState.set_sync_msg(context, :sync_now)
thing = get_config_part(:informational_settings, context)[:sync_status]
assert thing == :sync_now
end
test "sets sync msg to :syncing", %{cs_context: context} do
:ok = Farmbot.BotState.set_sync_msg(context, :syncing)
thing = get_config_part(:informational_settings, context)[:sync_status]
assert thing == :syncing
end
test "sets sync msg to :sync_error", %{cs_context: context} do
:ok = Farmbot.BotState.set_sync_msg(context, :sync_error)
thing = get_config_part(:informational_settings, context)[:sync_status]
assert thing == :sync_error
end
test "sets sync msg to :unknown", %{cs_context: context} do
:ok = Farmbot.BotState.set_sync_msg(context, :unknown)
thing = get_config_part(:informational_settings, context)[:sync_status]
assert thing == :unknown
end
test "raises an error if wrong sync message", %{cs_context: context} do
assert_raise(FunctionClauseError, fn() ->
Farmbot.BotState.set_sync_msg(context, "some str?")
end)
end
defp get_hardware_part(part, context) do
Process.sleep(10)
:sys.get_state(context.monitor).state
|> Map.get(:hardware)
|> Map.get(part)
end
defp get_config_part(part, context) do
Process.sleep(10)
:sys.get_state(context.monitor).state
|> Map.get(:configuration)
|> Map.get(part)
end
end

View File

@ -1,98 +0,0 @@
defmodule AstTest do
use ExUnit.Case, async: false
alias Farmbot.CeleryScript.Ast, as: Ast
test "parses an ast from a stringed map" do
test_ast =
%{"args" => %{"message" => "hello world"},
"body" =>[],
"kind" => "send_message"}
ast = Ast.parse(test_ast)
assert(ast.args.message == "hello world")
assert(ast.body == [])
assert(ast.kind == "send_message")
end
test "parses an ast from a stringed map with no body" do
test_ast =
%{"args" => %{"coords" => [1,2,3]},
"kind" => "pickup_truck"}
ast = Ast.parse(test_ast)
assert(ast.args.coords == [1,2,3])
assert(ast.body == [])
assert(ast.kind == "pickup_truck")
end
test "parses an ast from a keyed map" do
test_ast_a =
%{args: %{},
kind: "play_kick_ball"}
ast_a = Ast.parse(test_ast_a)
assert(ast_a.args == %{})
assert(ast_a.body == [])
assert(ast_a.kind == "play_kick_ball")
test_ast_b =
%{args: %{},
body: [],
kind: "play_kick_ball"}
ast_b = Ast.parse(test_ast_b)
assert(ast_b.args == %{})
assert(ast_b.body == [])
assert(ast_b.kind == "play_kick_ball")
end
test "creats more ast nodes from the body" do
test_ast_body_inner = %{
args: %{},
body: [],
kind: "inner_body_node"
}
test_ast_body_main = %{
args: %{},
body: [test_ast_body_inner, test_ast_body_inner],
kind: "body_node"
}
test_ast_main =
%{args: %{},
body: [test_ast_body_main, test_ast_body_inner, test_ast_body_main],
kind: "play_kick_ball"}
main_ast = Ast.parse(test_ast_main)
assert(main_ast.args == %{})
assert(main_ast.kind == "play_kick_ball")
body = main_ast.body
assert(is_list(body))
assert(Enum.count(body) == 3)
# there may need to be a protection for a stack overflow here lol
# if you could somehow make a node that references itself or someting
# similar to that it would recurse forever
random_ast_node = Enum.random(body)
assert(random_ast_node.kind |> is_bitstring)
end
test "manually creates an ast" do
Ast.create("speak_spanish", %{words: "hello"}, body: [])
end
test "raises an error on bad data" do
maybe_ast = "some arbitrary data"
assert_raise Farmbot.CeleryScript.Error, fn() ->
Ast.parse(maybe_ast)
end
end
test "does recursion" do
map = %{"kind" => "thing",
"args" => %{"arg_a" => "string",
"arg_b" => %{"kind" => "thingb",
"args" => %{}, "body" => []}},
"body" => []}
ast = Ast.parse(map)
assert map["kind"] == ast.kind
assert map["args"]["arg_b"]["kind"] == ast.args.arg_b.kind
end
end

View File

@ -1,126 +0,0 @@
defmodule CommandTest do
use ExUnit.Case, async: false
alias Farmbot.CeleryScript.{Command, Ast, Error}
alias Farmbot.Context
alias Farmbot.Database
alias Farmbot.Database.Selectors.Error, as: SelectorError
alias Database.Syncable.Point
import Mock
setup_all do
[cs_context: Farmbot.Context.new()]
end
test "doesnt freak out on no instruction", %{cs_context: context} do
json = ~s"""
{
"kind": "bring_down_network",
"args": {"time": "now"}
}
"""
cs = Poison.decode!(json) |> Ast.parse()
assert_raise Error, fn() ->
Command.do_command(cs, context)
end
end
test "rescues from bad things", %{cs_context: context} do
with_mock Farmbot.CeleryScript.Command.Nothing, [run: fn(_,_,_) ->
raise Error, message: "Mucking About with Mocks"
end] do
assert_raise Error, fn() ->
nothing_ast = %Ast{kind: "nothing", args: %{}, body: [], comment: "hey!"}
Command.do_command(nothing_ast, context)
end
end
end
test "cant execute random data that is not a cs node", %{cs_context: context} do
assert_raise Error, fn() ->
Command.do_command("shut the door its cold in here!", context)
end
end
test "converts a coordinate to coordinates", %{cs_context: context} do
# a coordinate is already a coordinate
ast_a = %Ast{kind: "coordinate", args: %{x: 1, y: 1, z: 1}, body: []}
context = Command.ast_to_coord(context, ast_a)
{coord_a, next_context} = Farmbot.Context.pop_data(context)
assert is_map(coord_a)
assert is_map(next_context)
assert ast_a.kind == coord_a.kind
end
test "converts a tool to a coordinate" do
ctx = Context.new()
{:ok, database} = Database.start_link(ctx, [])
ctx = %{ctx | database: database}
tool_id = 66
point = %Point{id: 6, x: 1, y: 2, z: 3, pointer_type: "tool_slot", tool_id: tool_id}
Database.commit_records point, ctx, Point
tool_ast = %Ast{kind: "tool", args: %{tool_id: tool_id}, body: []}
new_context = Command.ast_to_coord(ctx, tool_ast)
{coord, _new_context2} = Context.pop_data(new_context)
assert coord.args.x == point.x
assert coord.args.y == point.y
assert coord.args.z == point.z
end
test "raises if there is no tool_slot or something" do
ctx = Context.new()
{:ok, pid} = Database.start_link(ctx, [])
ctx = %{ctx | database: pid}
tool_ast = %Ast{kind: "tool", args: %{tool_id: 905}, body: []}
assert_raise SelectorError, "Could not find tool_slot with tool_id: 905", fn() ->
Command.ast_to_coord(ctx, tool_ast)
end
end
test "gives the origin on nothing", %{cs_context: context} do
nothing = %Ast{kind: "nothing", args: %{}, body: []}
{coord, context} = Command.ast_to_coord(context, nothing) |> Farmbot.Context.pop_data
assert is_map(coord)
assert is_map(context)
assert coord.args.x == 0
assert coord.args.y == 0
assert coord.args.z == 0
end
test "gives an error on unknown asts", %{cs_context: context} do
blerp = %Ast{kind: "blerp", args: %{}, body: []}
assert_raise Error, fn ->
Command.ast_to_coord(context, blerp)
end
end
# test "doesnt implode if a sequence relies on itself" do
# use Amnesia
# use Sequence
#
# real_sequence = %Sequence{args: %{"is_outdated" => false,
# "version" => 4},
# body: [%{"args" => %{"_else" => %{"args" => %{}, "kind" => "nothing"},
# "_then" => %{"args" => %{"sequence_id" => 40000}, "kind" => "execute"},
# "lhs" => "x", "op" => "not", "rhs" => 10000}, "kind" => "_if"}],
# color: "gray", id: 40000, kind: "sequence", name: "seq_a"}
#
# Amnesia.transaction do
# real_sequence |> Sequence.write
# end
#
# sequence = %Ast{kind: "execute", args: %{sequence_id: 40000}, body: []}
#
# assert_raise(Error, "TO MUCH RECURSION", fn() ->
# Command.do_command(sequence)
# end)
# end
end

View File

@ -1,112 +0,0 @@
defmodule Farmbot.CeleryScript.IfTest do
use ExUnit.Case, async: false
alias Farmbot.CeleryScript.{Ast, Command, Error}
alias Command.If
alias Farmbot.Context
setup_all do
ctx = Context.new()
[cs_context: ctx]
end
test "raises when given a bad left hand side", %{cs_context: context} do
else_ast = %Ast{kind: "nothing", args: %{}, body: []}
then_ast = %Ast{kind: "nothing", args: %{}, body: []}
lhs = "some arbitrary property"
rhs = 1234
op = ">"
args = %{_else: else_ast, _then: then_ast, lhs: lhs, rhs: rhs, op: op}
ast = %Ast{kind: "_if", args: args, body: []}
assert_raise Error, "Got unexpected left hand side of IF: some arbitrary property", fn() ->
If.run(ast.args, ast.body, context)
end
end
test "raises if bad operator", %{cs_context: context} do
else_ast = %Ast{kind: "nothing", args: %{}, body: []}
then_ast = %Ast{kind: "nothing", args: %{}, body: []}
lhs = "x"
rhs = 1234
op = "almost greater than but not really"
args = %{_else: else_ast, _then: then_ast, lhs: lhs, rhs: rhs, op: op}
ast = %Ast{kind: "_if", args: args, body: []}
assert_raise Error, "Bad operator in if #{inspect op}", fn() ->
If.run(ast.args, ast.body, context)
end
end
test "does if with an axis", %{cs_context: context} do
[100 ,2,3] = Farmbot.BotState.set_pos(context, 100, 2, 3)
then_ast = %Ast{kind: "nothing", args: %{}, body: []}
else_ast = %Ast{kind: "this is fake and unsafe", args: %{}, body: []}
lhs = "z"
rhs = 0
op = ">"
args = %{_else: else_ast, _then: then_ast, lhs: lhs, rhs: rhs, op: op}
ast = %Ast{kind: "_if", args: args, body: []}
new_context = If.run(ast.args, ast.body, context)
{results, _new_context2} = Context.pop_data(new_context)
assert results == then_ast
refute results == else_ast
end
test "does if with a pin", %{cs_context: context} do
pin_num = 12
Farmbot.BotState.set_pin_mode(context, pin_num, 1)
Farmbot.BotState.set_pin_value(context, pin_num, 123)
then_ast = %Ast{kind: "nothing", args: %{}, body: []}
else_ast = %Ast{kind: "this is fake and unsafe", args: %{}, body: []}
lhs = "pin#{pin_num}"
op = "<"
rhs = 9000
args = %{_else: else_ast, _then: then_ast, lhs: lhs, rhs: rhs, op: op}
ast = %Ast{kind: "_if", args: args, body: []}
new_context = If.run(ast.args, ast.body, context)
{results, _new_context2} = Context.pop_data(new_context)
assert results == then_ast
refute results == else_ast
end
test "does else if", %{cs_context: context} do
[100 ,2,3] = Farmbot.BotState.set_pos(context, 100, 2, 3)
else_ast = %Ast{kind: "nothing", args: %{}, body: []}
then_ast = %Ast{kind: "this is fake and unsafe", args: %{}, body: []}
lhs = "y"
rhs = 0
op = "is"
args = %{_else: else_ast, _then: then_ast, lhs: lhs, rhs: rhs, op: op}
ast = %Ast{kind: "_if", args: args, body: []}
new_context = If.run(ast.args, ast.body, context)
{results, _new_context2} = Context.pop_data(new_context)
assert results == else_ast
refute results == then_ast
end
test "does else if with not", %{cs_context: context} do
[100 ,2,3] = Farmbot.BotState.set_pos(context, 100, 2, 3)
else_ast = %Ast{kind: "nothing", args: %{}, body: []}
then_ast = %Ast{kind: "this is fake and unsafe", args: %{}, body: []}
lhs = "y"
rhs = 2
op = "not"
args = %{_else: else_ast, _then: then_ast, lhs: lhs, rhs: rhs, op: op}
ast = %Ast{kind: "_if", args: args, body: []}
new_context = If.run(ast.args, ast.body, context)
{results, _new_context2} = Context.pop_data(new_context)
assert results == else_ast
refute results == then_ast
end
end

View File

@ -1,31 +0,0 @@
defmodule Farmbot.CeleryScript.Command.CheckUpdatesTest do
use ExUnit.Case, async: true
alias Farmbot.CeleryScript.{Ast, Command, Error}
import Mock
test "doesnt check for arduino updates anymore" do
ctx = Farmbot.Context.new()
ast = %Ast{kind: "check_updates", args: %{package: "arduino_firmware"}, body: []}
assert_raise Error, "arduino firmware is now bundled into the OS.", fn() ->
Command.do_command(ast, ctx)
end
end
test "doesnt know what to do with other strings" do
ctx = Farmbot.Context.new()
ast = %Ast{kind: "check_updates", args: %{package: "explorer.exe"}, body: []}
assert_raise Error, "unknown package: #{ast.args.package}", fn() ->
Command.do_command(ast, ctx)
end
end
test "does update check for fbos" do
# the real update thing will be checked elsewhere, this is just for the ast node.
with_mock Farmbot.System.Updates, [check_and_download_updates: fn(_) -> :ok end] do
ctx = Farmbot.Context.new()
ast = %Ast{kind: "check_updates", args: %{package: "farmbot_os"}, body: []}
Command.do_command(ast, ctx)
assert called Farmbot.System.Updates.check_and_download_updates(ctx)
end
end
end

View File

@ -1,70 +0,0 @@
defmodule Farmbot.CeleryScript.Command.ConfigUpdateTest do
use ExUnit.Case, async: false
alias Farmbot.CeleryScript.{Command, Ast}
use Farmbot.Test.Helpers.SerialTemplate, async: false
defp pair(key, val) do
%Ast{kind: "pair", args: %{label: key, value: val}, body: []}
end
describe "config_update" do
test "makes sure we have serial", %{cs_context: context} do
assert Farmbot.Serial.Handler.available?(context) == true
end
test "sets some hardware params", %{cs_context: context} do
params = [
pair("movement_timeout_x", "1000"),
pair("movement_timeout_y", "521")
]
args = %{package: "arduino_firmware"}
Command.config_update(args, params, context)
mtx = Farmbot.BotState.get_param context, "movement_timeout_x"
mty = Farmbot.BotState.get_param context, "movement_timeout_y"
assert mtx == 1000
assert mty == 521
end
test "doesnt set hardware param values to -1", %{cs_context: context} do
old = Farmbot.BotState.get_param context, "movement_timeout_x"
params = [
pair("movement_timeout_x", "-1")
]
Command.config_update(%{package: "arduino_firmware"}, params, context)
new_mtx = Farmbot.BotState.get_param context, "movement_timeout_x"
assert new_mtx != -1
assert new_mtx == old
end
test "wont put garbage in the state", %{cs_context: context} do
params = [ pair("some_garbage", "9001") ]
# ITS OVER NINE THOUSAND!!!
assert_raise RuntimeError, fn ->
Command.config_update(%{package: "arduino_firmware"}, params, context)
end
conf = Farmbot.BotState.get_param context, "some_garbage"
assert is_nil(conf)
end
test "sets some os params", %{cs_context: context} do
params = [
pair("steps_per_mm_x", 999),
pair("steps_per_mm_y", 40),
]
Command.config_update(%{package: "farmbot_os"}, params, context)
spmx = Farmbot.BotState.get_config(context, :steps_per_mm_x)
spmy = Farmbot.BotState.get_config(context, :steps_per_mm_y)
assert spmx == 999
assert spmy == 40
end
end
end

View File

@ -1,45 +0,0 @@
defmodule Farmbot.CeleryScript.Command.DataUpdateTest do
use ExUnit.Case, async: false
alias Farmbot.CeleryScript.{Command, Ast}
alias Farmbot.Database, as: DB
alias DB.Syncable.Point
alias Farmbot.Test.Helpers
alias Command.DataUpdate
setup_all do
json = Helpers.read_json("points.json") |> Poison.decode!
context = Farmbot.Context.new()
{:ok, db_pid} = DB.start_link(context, [])
context = %{context | database: db_pid}
:ok = Helpers.seed_db(context, Point, json)
[
json: json,
cs_context: context
]
end
setup context do
DB.unset_awaiting(context.cs_context, Point)
end
test "data_updates causes awaiting to be true.", context do
ast = ast("add", [pair("points", "*")])
old = DB.get_awaiting(context.cs_context, Point)
refute(old)
DataUpdate.run(ast.args, ast.body, context.cs_context)
new = DB.get_awaiting(context.cs_context, Point)
assert(new)
end
def ast(verb, pairs) do
%Ast{kind: "data_update", args: %{value: verb}, body: pairs}
end
def pair(mod, thing),
do: %Ast{kind: "pair", args: %{label: mod, value: thing}, body: []}
end

View File

@ -1,26 +0,0 @@
defmodule Farmbot.CeleryScript.Command.EmergencyLockTest do
alias Farmbot.CeleryScript.{Command, Ast, Error}
use Farmbot.Test.Helpers.SerialTemplate, async: false
describe "emergency_lock" do
test "wont lock the bot if its already locked", %{cs_context: context} do
# actually lock the bot
ast = good_ast()
Command.do_command(ast, context)
serial_state = :sys.get_state(context.serial)
assert serial_state.status == :locked
config_state = :sys.get_state(context.configuration)
assert config_state.informational_settings.sync_status == :locked
assert config_state.informational_settings.locked == true
assert_raise Error, "Bot is already locked", fn() ->
Command.do_command(ast, context)
end
end
end
defp good_ast, do: %Ast{kind: "emergency_lock", args: %{}, body: []}
end

View File

@ -1,38 +0,0 @@
defmodule Farmbot.CeleryScript.Command.EmergencyUnLockTest do
alias Farmbot.CeleryScript.{Command, Ast, Error}
use Farmbot.Test.Helpers.SerialTemplate, async: false
describe "emergency_unlock" do
test "wont lock the bot if its already locked", %{cs_context: context} do
# actually lock the bot
lock_ast = good_lock_ast()
Command.do_command(lock_ast, context)
serial_state = :sys.get_state(context.serial)
assert serial_state.status == :locked
config_state = :sys.get_state(context.configuration)
assert config_state.informational_settings.sync_status == :locked
assert config_state.informational_settings.locked == true
unlock_ast = good_unlock_ast()
Command.do_command(unlock_ast, context)
serial_state = :sys.get_state(context.serial)
assert serial_state.status == :idle
config_state = :sys.get_state(context.configuration)
assert config_state.informational_settings.sync_status == :sync_now
assert config_state.informational_settings.locked == false
assert_raise Error, "Bot is not locked", fn() ->
Command.do_command(unlock_ast, context)
end
end
end
defp good_lock_ast, do: %Ast{kind: "emergency_lock", args: %{}, body: []}
defp good_unlock_ast, do: %Ast{kind: "emergency_unlock", args: %{}, body: []}
end

View File

@ -1,31 +0,0 @@
defmodule Farmbot.CeleryScript.Command.ExplanationTest do
use ExUnit.Case, async: true
alias Farmbot.CeleryScript.{Ast, Command}
alias Command.Explanation
alias Farmbot.Context
setup_all do
ctx = Context.new()
[cs_context: ctx]
end
test "pushes explanation onto the data_stack", %{cs_context: ctx} do
assert is_map(ctx)
msg = "the cat spilled the coffee"
ast = %Ast{kind: "explanation", args: %{message: msg}, body: []}
next_context = Explanation.run(ast.args, ast.body, ctx)
assert is_map(next_context)
assert Enum.count(next_context.data_stack) == (Enum.count(ctx.data_stack) + 1)
{results, next_context2} = Context.pop_data(next_context)
assert is_map(next_context2)
assert Enum.count(next_context2.data_stack) == (Enum.count(next_context.data_stack) - 1)
assert is_map(results)
assert results.kind == ast.kind
assert results.args == ast.args
end
end

View File

@ -1,57 +0,0 @@
defmodule Farmbot.CeleryScript.Command.HomeTest do
use Farmbot.Test.Helpers.SerialTemplate, async: false
alias Farmbot.CeleryScript.Command
describe "home" do
test "makes sure we have serial", %{cs_context: context} do
assert Farmbot.Serial.Handler.available?(context) == true
end
test "homes all axises", %{cs_context: context} do
Command.home(%{axis: "all"}, [], context)
Process.sleep(500)
[x, y, z] = Farmbot.BotState.get_current_pos(context)
assert x == 0
assert y == 0
assert z == 0
Process.sleep(500)
end
test "homes x", %{cs_context: context} do
[_, _, _] = Farmbot.BotState.set_pos(context, 0,0,0)
[_x, y, z] = Farmbot.BotState.get_current_pos(context)
Command.home(%{axis: "x"}, [], context)
Process.sleep(500)
[new_x, new_y, new_z] = Farmbot.BotState.get_current_pos(context)
assert new_x == 0
assert y == new_y
assert z == new_z
end
test "homes y", %{cs_context: context} do
[_, _, _] = Farmbot.BotState.set_pos(context, 0,0,0)
[x, _y, z] = Farmbot.BotState.get_current_pos(context)
Command.home(%{axis: "y"}, [], context)
Process.sleep(500)
[new_x, new_y, new_z] = Farmbot.BotState.get_current_pos(context)
assert x == new_x
assert new_y == 0
assert z == new_z
end
test "homes z", %{cs_context: context} do
[_, _, _] = Farmbot.BotState.set_pos(context, 0,0,0)
[x, y, _z] = Farmbot.BotState.get_current_pos(context)
Command.home(%{axis: "z"}, [], context)
Process.sleep(500)
[new_x, new_y, new_z] = Farmbot.BotState.get_current_pos(context)
assert x == new_x
assert y == new_y
assert new_z == 0
end
end
end

View File

@ -1,76 +0,0 @@
defmodule Farmbot.CeleryScript.Command.MoveAbsoluteTest do
alias Farmbot.CeleryScript.{Command, Ast}
alias Farmbot.Database, as: DB
alias DB.Syncable.Point
alias Farmbot.Test.Helpers
use Helpers.SerialTemplate, async: false
describe "move_absolute" do
test "makes sure we have serial", %{cs_context: context} do
assert Farmbot.Serial.Handler.available?(context) == true
end
test "moves to a location", %{cs_context: context} do
[_curx, _cury, _curz] = Farmbot.BotState.get_current_pos(context)
location = %Ast{kind: "coordinate", args: %{x: 1000, y: 0, z: 0}, body: []}
offset = %Ast{kind: "coordinate", args: %{x: 0, y: 0, z: 0}, body: []}
Command.move_absolute(%{speed: 8000, offset: offset, location: location}, [], context)
Process.sleep(100) # wait for serial to catch up
[newx, _newy, _newz] = Farmbot.BotState.get_current_pos(context)
assert newx == 1000
end
test "moves to a location defered by an offset", %{cs_context: context} do
location = %Ast{kind: "coordinate", args: %{x: 1000, y: 0, z: 0}, body: []}
offset = %Ast{kind: "coordinate", args: %{x: 500, y: 0, z: 0}, body: []}
Command.move_absolute(%{speed: 8000, offset: offset, location: location}, [], context)
Process.sleep(100) # wait for serial to catch up
[newx, newy, newz] = Farmbot.BotState.get_current_pos(context)
assert newx == 1500
assert newy == 0
assert newz == 0
end
test "moves to a bad *plant* location", %{cs_context: context} do
[_curx, _cury, _curz] = Farmbot.BotState.get_current_pos(context)
location = %Ast{kind: "point",
args: %{pointer_type: "Plant", pointer_id: 123},
body: []}
offset = %Ast{kind: "coordinate",
args: %{x: 0, y: 0, z: 0},
body: []}
args = %{speed: 8000,
offset: offset,
location: location}
assert_raise Farmbot.Database.Selectors.Error, "does not exist.", fn ->
Command.move_absolute(args, [], context)
end
end
# this test is broke for some reason
# test "moves to a good plant", %{cs_context: context} do
# Farmbot.BotState.set_pos(context, 0, 0, 0)
# json = Helpers.read_json("points.json") |> Poison.decode!
# {:ok, db_pid} = DB.start_link(context, [])
# context = %{context | database: db_pid}
# :ok = Helpers.seed_db(context, Point, json)
# item = List.first(json)
#
# type = item["pointer_type"]
# id = item["id"]
#
# location = %Ast{kind: "point", args: %{pointer_type: type, pointer_id: id}, body: []}
# offset = %Ast{kind: "coordinate", args: %{x: 0, y: 0, z: 0}, body: []}
# args = %{speed: 8000, offset: offset, location: location}
# new_context = Command.move_absolute(args, [], context)
#
# Process.sleep(1000) # wait for serial to catch up
# [x, y, z] = Farmbot.BotState.get_current_pos(new_context)
# assert x == item["x"]
# assert y == item["y"]
# assert z == item["z"]
#
# end
end
end

View File

@ -1,23 +0,0 @@
defmodule Farmbot.CeleryScript.Command.MoveRelativeTest do
alias Farmbot.CeleryScript.Command
use Farmbot.Test.Helpers.SerialTemplate, async: false
require IEx
describe "move_absolute" do
test "makes sure we have serial", %{cs_context: context} do
assert Farmbot.Serial.Handler.available?(context) == true
end
test "moves to a location", %{cs_context: context} do
[_, _, _] = Farmbot.BotState.set_pos(context, 500,0,0)
[oldx, oldy, oldz] = Farmbot.BotState.get_current_pos(context)
Command.move_relative(%{speed: 800, x: 100, y: 0, z: 0}, [], context)
Process.sleep(600)
[newx, newy, newz] = Farmbot.BotState.get_current_pos(context)
assert newx == (oldx + 100)
assert newy == oldy
assert newz == oldz
end
end
end

View File

@ -1,18 +0,0 @@
defmodule Farmbot.CeleryScript.Command.NothingTest do
use ExUnit.Case
alias Farmbot.CeleryScript.{Command, Ast}
test "does nothing" do
json = ~s"""
{
"kind": "nothing",
"args": {}
}
"""
ast = json |> Poison.decode!() |> Ast.parse
context = Command.do_command(ast, Farmbot.Context.new())
{r, _final_context} = Farmbot.Context.pop_data(context)
assert is_map(r)
assert r.kind == "nothing"
end
end

View File

@ -1,33 +0,0 @@
defmodule Farmbot.CeleryScript.Command.PairTest do
use ExUnit.Case, async: true
alias Farmbot.CeleryScript.{Ast, Command}
alias Command.Pair
alias Farmbot.Context
setup_all do
ctx = Context.new()
[cs_context: ctx]
end
test "pushes a pair onto the data_stack", %{cs_context: ctx} do
assert is_map(ctx)
label = "the_answer_to_the_world"
value = 42
ast = %Ast{kind: "pair", args: %{label: label, value: value}, body: []}
next_context = Pair.run(ast.args, ast.body, ctx)
assert is_map(next_context)
assert Enum.count(next_context.data_stack) == (Enum.count(ctx.data_stack) + 1)
{results, next_context2} = Context.pop_data(next_context)
assert is_map(next_context2)
assert Enum.count(next_context2.data_stack) == (Enum.count(next_context.data_stack) - 1)
assert is_map(results)
assert results.kind == ast.kind
assert results.args == ast.args
end
end

View File

@ -1,16 +0,0 @@
defmodule Farmbot.CeleryScript.Command.PowerOffTest do
use ExUnit.Case, async: false
alias Farmbot.CeleryScript.{Command, Ast}
test "powers off the bot" do
json = ~s"""
{
"kind": "power_off",
"args": {}
}
"""
ast = json |> Poison.decode!() |> Ast.parse
new_context = Command.do_command(ast, Farmbot.Context.new())
assert new_context
end
end

View File

@ -1,19 +0,0 @@
defmodule Farmbot.CeleryScript.Command.ReadAllParamsTest do
use Farmbot.Test.Helpers.SerialTemplate, async: false
alias Farmbot.CeleryScript.Command
test "makes sure we have serial", %{cs_context: context} do
assert Farmbot.Serial.Handler.available?(context) == true
end
test "reads all params", %{cs_context: context} do
# old = Farmbot.BotState.get_all_mcu_params
Command.read_all_params(%{}, [], context)
Process.sleep(100)
new = Farmbot.BotState.get_all_mcu_params(context)
assert is_map(new)
assert !Enum.empty?(new)
end
end

View File

@ -1,16 +0,0 @@
defmodule Farmbot.CeleryScript.Command.RebootTest do
use ExUnit.Case, async: false
alias Farmbot.CeleryScript.{Command, Ast}
test "reboots the bot" do
json = ~s"""
{
"kind": "reboot",
"args": {}
}
"""
ast = json |> Poison.decode!() |> Ast.parse
new_context = Command.do_command(ast, Farmbot.Context.new())
assert new_context
end
end

View File

@ -1,40 +0,0 @@
defmodule Farmbot.CeleryScript.Command.RpcErrorTest do
use ExUnit.Case
alias Farmbot.CeleryScript.{Command, Ast}
setup_all do
[cs_context: Farmbot.Context.new()]
end
test "rpc ok", %{cs_context: context} do
id = "random thing that should be a uuid"
error_message = "the world exploded!"
error_json = ~s"""
{
"kind": "explanation",
"args": {"message": "#{error_message}"}
}
"""
json = ~s"""
{
"kind": "rpc_error",
"args": {"label": "#{id}"},
"body": [#{error_json}]
}
"""
ast = json |> Poison.decode! |> Ast.parse
next_context = Command.do_command(ast, context)
assert is_map(next_context)
assert next_context.__struct__ == Farmbot.Context
{resp, _final_context} = Farmbot.Context.pop_data(next_context)
assert resp.kind == "rpc_error"
assert resp.args.label == id
[message] = resp.body
assert message.kind == "explanation"
assert message.args.message == error_message
end
end

View File

@ -1,29 +0,0 @@
defmodule Farmbot.CeleryScript.Command.RpcOkTest do
use ExUnit.Case
alias Farmbot.CeleryScript.{Command, Ast}
setup_all do
[cs_context: Farmbot.Context.new()]
end
test "rpc ok", %{cs_context: context} do
id = "random thing that should be a uuid"
json = ~s"""
{
"kind": "rpc_ok",
"args": {"label": "#{id}"}
}
"""
ast = json |> Poison.decode! |> Ast.parse
assert is_map(ast)
next_context = Command.do_command(ast, context)
assert is_map(next_context)
assert next_context.__struct__ == Farmbot.Context
{resp, _final_context} = Farmbot.Context.pop_data(next_context)
assert resp.kind == "rpc_ok"
assert resp.args.label == id
end
end

View File

@ -1,31 +0,0 @@
defmodule Farmbot.CeleryScript.Command.SyncTest do
use ExUnit.Case, async: false
alias Farmbot.{Context, Database}
alias Farmbot.CeleryScript.Command.Sync
alias Farmbot.Test.Helpers
setup_all do
ctx = Context.new()
{:ok, db} = Database.start_link(ctx, [])
# {:ok, auth} = Farmbot.Auth.start_link(ctx, [])
ctx = %{ctx | database: db}
[cs_context: Helpers.login(ctx)]
end
#
# test "syncs the bot", %{cs_context: ctx} do
# db = ctx.database
# :ok = Database.flush(ctx)
#
# use_cassette "sync/corner_case" do
# before_state = :sys.get_state(db)
# before_count = Enum.count(before_state.all)
#
# Sync.run(%{}, [], ctx)
# after_state = :sys.get_state(db)
#
# after_count = Enum.count(after_state.all)
# assert(before_count < after_count)
# end
#
# end
end

View File

@ -1,26 +0,0 @@
defmodule SomeSupervisorProcess do
use Farmbot.Context.Supervisor
end
defmodule Farmbot.Context.SupervisorTest do
use ExUnit.Case, async: true
alias Farmbot.Context
setup_all do
ctx = Context.new()
[cs_context: ctx]
end
test "Builds a context tracker", %{cs_context: ctx} do
{:ok, pid} = SomeSupervisorProcess.start_link(ctx, [])
assert is_pid(pid)
assert Process.alive?(pid)
end
test "Does gud pattern matching", %{cs_context: ctx} do
assert_raise FunctionClauseError, fn() ->
not_quite_context = Map.from_struct(ctx)
SomeSupervisorProcess.start_link(not_quite_context, [])
end
end
end

View File

@ -1,21 +0,0 @@
defmodule Farmbot.Context.TrackerTest do
use ExUnit.Case, async: true
alias Farmbot.Context.Tracker
test "builds and tracks a context" do
context = Context.new()
random_key = Map.from_struct |> Map.delete(:data_stack) |> Map.keys |> Enum.random
context = %{ context | random_key => :some_cool_value}
{:ok, tracker} = Tracker.start_link(context, [])
assert is_pid(tracker)
assert is_map(context)
assert is_atom(random_key)
ctx = Tracker.get_context(tracker)
assert is_map(ctx)
assert Map.get(ctx, random_key) == :some_cool_value
assert ctx == context
end
end

View File

@ -1,43 +0,0 @@
defmodule SomeWorkerProcess do
use Farmbot.Context.Worker
end
defmodule SomeOtherProcess do
use Farmbot.Context.Worker
def init(ctx) do
{:ok, %{context: ctx, blah: :important_state} }
end
end
defmodule Farmbot.Context.WorkerTest do
use ExUnit.Case, async: true
alias Farmbot.Context
setup_all do
ctx = Context.new()
[cs_context: ctx]
end
test "Builds a context tracker", %{cs_context: ctx} do
{:ok, pid} = SomeWorkerProcess.start_link(ctx, [])
assert is_pid(pid)
state = :sys.get_state(pid)
assert state.context == ctx
end
test "Does gud pattern matching", %{cs_context: ctx} do
assert_raise FunctionClauseError, fn() ->
not_quite_context = Map.from_struct(ctx)
SomeWorkerProcess.start_link(not_quite_context, [])
end
end
test "allows overwriting `init/1` function", %{cs_context: ctx} do
{:ok, pid} = SomeOtherProcess.start_link(ctx, [])
assert is_pid(pid)
state = :sys.get_state(pid)
assert state.blah == :important_state
end
end

View File

@ -1,72 +0,0 @@
defmodule Farmbot.ContextTest do
use ExUnit.Case, async: true
alias Farmbot.Context
alias Farmbot.CeleryScript.Ast
test "creates a new context" do
ctx = Context.new()
assert is_map(ctx)
assert Map.has_key?(ctx, :data_stack)
end
test "pushes data onto the `data_stack`" do
ctx = Context.new()
ast = %Ast{kind: "solve_rubiks_cube", args: %{color: "red"}, body: []}
next = Context.push_data(ctx, ast)
assert is_map(next)
assert is_list(next.data_stack)
assert Enum.count(next.data_stack) == (Enum.count(ctx.data_stack ) + 1)
end
test "Pops data from the `data_stack`" do
ctx = Context.new()
ast_a = %Ast{kind: "play_video_games", args: %{shape: "blue"}, body: []}
ast_b = %Ast{kind: "paint_house", args: %{}, body: []}
ctx = Context.push_data(ctx, ast_a) |> Context.push_data(ast_b)
{item_b, new_context_1} = Context.pop_data(ctx)
assert item_b == ast_b
assert Enum.count(new_context_1.data_stack) == 1
{item_a, new_context_2} = Context.pop_data(new_context_1)
assert item_a == ast_a
assert Enum.count(new_context_2.data_stack) == 0
end
test "will not push data onto the data_stack that isnt an Ast Node" do
ctx = Context.new()
assert_raise(FunctionClauseError, fn() ->
almost_ast = %{kind: "some_kind", args: %{tape_player: true}, body: []}
Context.push_data(ctx, almost_ast)
end)
end
test "errors if nothing on the data stack" do
ctx = Context.new()
assert ctx.data_stack == []
assert_raise MatchError, fn() ->
{_item, _next} = Context.pop_data(ctx)
end
end
test "only accepts tagged structs as a context" do
real_ast = %Ast{kind: "this_is_valid", args: %{}, body: []}
almost_context = Context.new()
|> Context.push_data(real_ast)
|> Map.from_struct
assert is_map(almost_context)
refute Map.has_key?(almost_context, :__struct__)
assert_raise FunctionClauseError, fn() ->
Context.push_data(almost_context, real_ast)
end
assert_raise FunctionClauseError, fn() ->
Context.pop_data(almost_context)
end
end
end

View File

@ -1,198 +0,0 @@
defmodule Farmbot.DatabaseTest do
alias Farmbot.Test.Helpers
use ExUnit.Case, async: false
alias Farmbot.Database, as: DB
alias Farmbot.Context
alias DB.Syncable.Point
alias Farmbot.Test.Helpers
import Helpers, only: [tag_item: 2]
setup_all do
ctx = Context.new()
{:ok, db} = DB.start_link(ctx, [])
context = %{ctx | database: db}
[cs_context: Helpers.login(context)]
end
# HTTP STUB IS R BROKE
# test "sync" do
# ctx = Context.new()
# {:ok, db} = DB.start_link(ctx, [])
# context = %{ctx | database: db}
# :ok = DB.flush(context)
#
# use_cassette "sync/corner_case" do
# before_state = :sys.get_state(db)
# before_count = Enum.count(before_state.all)
#
# DB.sync(context)
#
# after_state = :sys.get_state(db)
# after_count = Enum.count(after_state.all)
# assert(before_count < after_count)
# end
# end
test "adds a record to the local db", %{cs_context: ctx} do
# modulename = Enum.random(DB.all_syncable_modules())
modulename = Point
plural = modulename.plural_url()
points_json = File.read!("fixture/api_fixture/points.json")
points = Poison.decode!(points_json) |> Poison.decode!
old = DB.get_all(ctx, modulename)
tagged = Enum.map(points, fn(item) ->
thing = tag_item(item, modulename)
assert(thing.__struct__ == modulename)
assert(is_number(thing.id))
thing
end)
:ok = DB.commit_records(tagged, ctx, modulename)
new = DB.get_all(ctx, modulename)
assert Enum.count(new) > Enum.count(old)
end
test "wont commit errornous things to db", %{cs_context: ctx} do
item = "random_not_json: {}, this isnt formatted_properly!"
mod = Enum.random(DB.all_syncable_modules())
error = Poison.decode(item)
old = DB.get_all(ctx, mod)
DB.commit_records(error, ctx, mod)
new = DB.get_all(ctx, mod)
assert Enum.count(new) == Enum.count(old)
end
test "gets an item out of the database", %{cs_context: ctx} do
modulename = Point
plural = modulename.plural_url()
points_json = File.read!("fixture/api_fixture/points.json")
points = Poison.decode!(points_json) |> Poison.decode!
random_item = Enum.random(points) |> tag_item(modulename)
id = random_item.id
:ok = DB.commit_records(random_item, ctx, modulename)
item = DB.get_by_id(ctx, modulename, id)
assert !is_nil(item)
assert item.body == random_item
end
test "updates an old item", %{cs_context: ctx} do
modulename = Point
plural = modulename.plural_url()
points_json = File.read!("fixture/api_fixture/points.json")
points = Poison.decode!(points_json) |> Poison.decode!
random_item = Enum.random(points) |> tag_item(modulename)
id = random_item.id
:ok = DB.commit_records(random_item, ctx, modulename)
updated = %{random_item | name: "hurdur"}
:ok = DB.commit_records(updated, ctx, modulename)
item = DB.get_by_id(ctx, modulename, id)
assert item.body == updated
end
test "toggles awaiting state for resources", %{cs_context: ctx} do
DB.set_awaiting(ctx, Point, :remove, 1)
assert(DB.get_awaiting(ctx, Point))
DB.unset_awaiting(ctx, Point)
refute(DB.get_awaiting(ctx, Point))
DB.set_awaiting(ctx, Point, :remove, 1)
assert(DB.get_awaiting(ctx, Point))
DB.unset_awaiting(ctx, Point)
refute(DB.get_awaiting(ctx, Point))
end
test "removes resources on set awaiting", %{cs_context: ctx} do
DB.set_awaiting(ctx, Point, :remove, "*")
assert(DB.get_awaiting(ctx, Point))
state = :sys.get_state(ctx.database)
assert state.by_kind[Point] == []
in_all? = Enum.any?(state.all, fn(ref) ->
match?({Point, _, _}, ref)
end)
refute in_all?
in_refs? = Enum.any?(state.refs, fn(thing) ->
match? {{Point, _, _}, _}, thing
end)
refute in_refs?
in_by_kind_and_id? = Enum.any?(Map.keys(state.by_kind_and_id), fn(thing) ->
match? {Point, _}, thing
end)
refute in_by_kind_and_id?
end
test "removes a particular item by id", %{cs_context: ctx} do
json = ~w"""
{
"id": 4999,
"created_at": "2017-05-16T16:26:13.261Z",
"updated_at": "2017-05-16T16:26:13.261Z",
"device_id": 2,
"meta": {},
"name": "Cabbage 2",
"pointer_type": "Plant",
"radius": 50,
"x": 2,
"y": 2,
"z": 2,
"openfarm_slug": "cabbage"
}
"""
modulename = Point
item = Poison.decode!(json)
thing = tag_item(item, modulename)
assert(thing.__struct__ == modulename)
assert(is_number(thing.id))
:ok = DB.commit_records([thing], ctx, modulename)
DB.set_awaiting(ctx, Point, :remove, 4999)
assert(DB.get_awaiting(ctx, Point))
state = :sys.get_state(ctx.database)
in_all? = Enum.find(state.all, fn({syncable, _, id}) ->
(syncable == Point) && (id == 4999)
end)
refute in_all?
in_refs? = Enum.any?(state.refs, fn({{syncable, _, id}, _}) ->
(syncable == Point) && (id == 4999)
end)
refute in_refs?
in_by_kind_and_id? = Enum.any?(Map.keys(state.by_kind_and_id), fn({syncable, id}) ->
(syncable == Point) && (id == 4999)
end)
refute in_by_kind_and_id?
end
end

View File

@ -1,82 +0,0 @@
alias Farmbot.Database
alias Database.Syncable
alias Syncable.Sequence
alias Syncable.Point
alias Syncable.Peripheral
alias Syncable.Regimen
alias Syncable.Tool
alias Syncable.FarmEvent
# alias Syncable.Device
alias Farmbot.Context
defmodule AllSyncablesTestHelper do
defmacro test_syncable(module, id) do
quote do
defmodule Module.concat(["#{unquote(module)}Test"]) do
use ExUnit.Case, async: false
setup_all do
context = Context.new()
[
module: unquote(module),
id: unquote(id),
cs_context: Farmbot.Test.Helpers.login(context),
]
end
test "ensures proper module sanity", %{module: mod} do
assert unquote(module) == mod
end
test "successfully fetches some stuff from the api", %{module: mod, cs_context: ctx} do
human_readable_name = Module.split(mod) |> List.last
results = mod.fetch(ctx, {__MODULE__, :callback, []})
item = Enum.random(results)
assert item.__struct__ == mod
end
unless unquote(id) == :no_show do
test "gets a particular item from the api", %{module: mod, id: id, cs_context: ctx} do
human_readable_name = Module.split(mod) |> List.last
results = mod.fetch(ctx, id, {__MODULE__, :callback, []})
refute is_error?(results)
assert results.__struct__ == mod
assert results.id == id
end
end
# test "handles errors for stuff from the api", %{module: mod, cs_context: ctx} do
# human_readable_name = Module.split(mod) |> List.last
# results = mod.fetch(ctx, -1, {__MODULE__, :callback, []})
# assert is_error?(results)
# end
def callback(results), do: results
#
# defp is_error?(results)
# defp is_error?({:error, _}), do: true
defp is_error?(_), do: false
end
end
end
end
defmodule AllSyncablesTest do
import AllSyncablesTestHelper
test_syncable Sequence, 2
test_syncable Point, 71
test_syncable Tool, 1
test_syncable FarmEvent, :no_show
test_syncable Peripheral, :no_show
test_syncable Regimen, :no_show
# test_syncable Device, -1
end

View File

@ -1,52 +0,0 @@
defmodule Database.Syncable.Fake do
use Farmbot.Database.Syncable,
model: [:foo, :bar],
endpoint: {"/fake", "/fakes"}
end
defmodule Farmbot.SyncableTest do
alias Database.Syncable.Fake
alias Farmbot.Test.Helpers
use ExUnit.Case, async: false
alias Farmbot.Context
doctest Farmbot.Database.Syncable
setup_all do
context = Context.new()
[my_fake: %Fake{},
cs_context: Helpers.login(context)]
end
test "defines a syncable", context do
assert(is_map(context.my_fake))
assert(context.my_fake.__struct__ == Fake)
end
test "singular URLs" do
assert(Fake.singular_url == "/fake")
end
test "plural URLs" do
assert(Fake.plural_url == "/fakes")
end
def get_all_by_id_callback(result) do
result
end
test "fetch all of a resource", %{cs_context: ctx} do
results = Fake.fetch(ctx, {__MODULE__, :get_all_by_id_callback, []})
item = List.first(results)
assert item.__struct__ == Fake
assert is_integer(item.id)
end
test "fetch a particular id of a resource", %{cs_context: ctx} do
item = Fake.fetch(ctx, 2, {__MODULE__, :get_all_by_id_callback, []})
assert item.__struct__ == Fake
assert is_integer(item.id)
end
end

View File

@ -1,21 +0,0 @@
defmodule Farmbot.Context.TrackerTest do
use ExUnit.Case, async: true
alias Farmbot.Context.Tracker
test "builds and tracks a context" do
context = Context.new()
random_key = Map.from_struct |> Map.delete(:data_stack) |> Map.keys |> Enum.random
context = %{ context | random_key => :some_cool_value}
{:ok, tracker} = Tracker.start_link(context, [])
assert is_pid(tracker)
assert is_map(context)
assert is_atom(random_key)
ctx = Tracker.get_context(tracker)
assert is_map(ctx)
assert Map.get(ctx, random_key) == :some_cool_value
assert ctx == context
end
end

View File

@ -1,53 +0,0 @@
defmodule Farmbot.EasterEggsTest do
@moduledoc false
use ExUnit.Case, async: true
setup_all do
write_me = test_json() |> Poison.encode!
File.write!("/tmp/test.json", write_me)
:ok
end
test "starts the server with a path to a file" do
path = "/tmp/test.json"
{:ok, pid} = Farmbot.EasterEggs.start_link({:name, :test_1}, {:path, path})
assert is_pid(pid) == true
end
test "starts the server with a json object" do
json = test_json_with_strings()
{:ok, pid} = Farmbot.EasterEggs.start_link({:name, :test_2}, {:json, json})
assert is_pid(pid) == true
end
test "adds a new json to the state" do
path = "/tmp/test.json"
{:ok, pid} = Farmbot.EasterEggs.start_link({:name, :test_3}, {:path, path})
assert is_pid(pid) == true
state1 = GenServer.call(pid, :state)
assert state1 == %{nouns: %{}, verbs: []}
new_json = %{"nouns" => [%{"somehting" => "heyo"}], "verbs" => []}
Farmbot.EasterEggs.load_json(new_json, pid)
state2 = GenServer.call(pid, :state)
assert state2 == %{nouns: %{somehting: "heyo"}, verbs: []}
end
test "logs a thing" do
path = "/tmp/test.json"
{:ok, pid} = Farmbot.EasterEggs.start_link({:name, :test_4}, {:path, path})
assert is_pid(pid) == true
state1 = GenServer.call(pid, :state)
assert state1 == %{nouns: %{}, verbs: []}
GenServer.cast pid, "hey this will get logged but logger is disabled hur dur dur"
#??? i cant actually test that?
end
def test_json do
%{nouns: [], verbs: []}
end
def test_json_with_strings do
%{"nouns" => [], "verbs" => []}
end
end

View File

@ -1,46 +0,0 @@
defmodule Farmbot.Farmware.RuntimeTest do
@moduledoc false
alias Farmbot.{Farmware, Context, CeleryScript}
alias Farmbot.Farmware.Runtime
use ExUnit.Case, async: false
setup_all do
context = Context.new()
new_context = Farmbot.Test.Helpers.Context.replace_http(context)
{:ok, auth} = Farmbot.Auth.start_link(new_context, [])
new_context1 = %{new_context | auth: auth}
Farmbot.Auth.try_log_in!(new_context1.auth)
[cs_context: new_context1]
end
test "Runs a farmware", %{cs_context: ctx} do
nothing_node = %CeleryScript.Ast{
kind: "nothing",
args: %{},
body: []
}
json_stuff = Poison.encode!(nothing_node)
fake_fw = %Farmware{
path: "/tmp/",
executable: "bash",
uuid: UUID.uuid1(),
name: "spectrometer",
url: "",
args: ["-c", "echo $BEGIN_CELERYSCRIPT #{inspect json_stuff}"],
meta: %{
min_os_version_major: "1" ,
description: "This is a fixture farmware for tests.",
language: "python",
version: "1.0.0",
author: "BLAH Blah",
zip: "hello.zip"
}
}
new_context = Runtime.execute(ctx, fake_fw)
{nothing, new_context1} = Context.pop_data(new_context)
assert nothing == nothing_node
assert new_context != new_context1
end
end

View File

@ -1,11 +0,0 @@
defmodule Farmbot.HTTPTest do
alias Farmbot.{HTTP, Context}
use ExUnit.Case, async: false
test "wont upload if an image doesnt exist" do
img_path = "#{:code.priv_dir(:farmbot)}/static/fake_farmbot_logo.png"
assert_raise(HTTP.Error, "#{img_path} not found",
fn -> HTTP.upload_file!(Context.new(), img_path) end)
end
end

View File

@ -1,25 +0,0 @@
defmodule Farmbot.ImageWatcherTest do
use ExUnit.Case, async: false
alias Farmbot.Auth
alias Farmbot.Context
setup_all do
context = Context.new()
Auth.purge_token(context.auth)
# Makes sure the dirs are empty
File.rm_rf "/tmp/images"
File.mkdir "/tmp/images"
Farmbot.ImageWatcher.force_upload(context)
:ok = Auth.interim(context.auth, "admin@admin.com", "password123", "http://localhost:3000")
{:ok, token} = Auth.try_log_in(context.auth)
%{cs_context: context}
end
# test "uploads an image automagically" do
# img_path = "#{:code.priv_dir(:farmbot)}/static/farmbot_logo.png"
# use_cassette "good_image_upload" do
# File.cp! img_path, "/tmp/images/farmbot_logo.png"
# end
# end
end

View File

@ -1,51 +0,0 @@
# defmodule Sequence.SupervisorTest do
# @moduledoc false
# use ExUnit.Case, async: true
# use Amnesia
# alias Sequence.Supervisor, as: S
# use Farmbot.Sync.Database
#
# test "starts and stops sequence" do
# {:ok, pid} = S.add_child(sequence(), Timex.now())
# S.remove_child(sequence())
# state = S.get_state
# assert is_pid(pid)
# assert state.running == nil
# end
#
# test "starts and queues a sequence" do
# sA = sequence()
# sB = %{sA | id: 123}
# now = Timex.now()
# {:ok, respA} = S.add_child(sA, now)
# {:ok, respB} = S.add_child(sB, now)
#
# S.remove_child(sA)
# S.remove_child(sB)
#
# state = S.get_state
# assert is_pid(respA)
# assert respB == :queued
# assert state.running == nil
# end
#
# test "starts, finishes a sequence, then starts the next " do
# sA = sequence()
# sB = %{sA | id: 123}
# now = Timex.now()
# {:ok, pidA} = S.add_child(sA, now)
# {:ok, :queued} = S.add_child(sB, now)
# GenServer.stop(pidA, :normal)
# state = S.get_state
# running = state.running
# {pidB, sequence} = running
# assert pidA != pidB
# end
#
# defp sequence do
# %Sequence{args: %{"is_outdated" => false,
# "version" => 4},
# body: [], color: "blue", device_id: nil, id: 186,
# kind: "sequence", name: "New Sequence"}
# end
# end

View File

@ -1,21 +0,0 @@
# defmodule Farmbot.SequenceRunnerTest do
# @moduledoc false
# use ExUnit.Case, async: true
# use Amnesia
# use Farmbot.Sync.Database
# alias Farmbot.SequenceRunner
#
# test "runs a sequence" do
# seq = sequence()
# {:ok, pid} = SequenceRunner.start_link(seq)
# assert is_pid(pid)
# end
#
# defp sequence do
# %Sequence{args: %{"is_outdated" => false,
# "version" => 4},
# body: [%{"args" => %{"message" => "Bot is at position {{ x }}, {{ y }}, {{ z }}.",
# "message_type" => "success"}, "kind" => "send_message"}], color: "blue", id: 186,
# kind: "sequence", name: "errrrp"}
# end
# end

View File

@ -1,53 +0,0 @@
defmodule Farmbot.Serial.Gcode.ParserTest do
use ExUnit.Case, async: true
test "Parses report paramater" do
bleep = Farmbot.Serial.Gcode.Parser.parse_code("R21 P0 V0 Q5")
assert(bleep == {"5", {:report_parameter_value, :param_version, 0}})
end
test "pareses a param numbered string" do
a = Farmbot.Serial.Gcode.Parser.parse_param("13")
assert(a == :movement_timeout_z)
end
test "Pareses a param in integer form" do
a = Farmbot.Serial.Gcode.Parser.parse_param(13)
assert(a == :movement_timeout_z)
end
test "Parses a param in atom form" do
a = Farmbot.Serial.Gcode.Parser.parse_param(:movement_timeout_z)
assert(a == 13)
end
test "Parses a param in string form" do
a = Farmbot.Serial.Gcode.Parser.parse_param("movement_timeout_z")
assert(a == 13)
end
test "Parses R31 and R41" do
a = Farmbot.Serial.Gcode.Parser.parse_code("R31 P0 V45 Q10")
assert a == {"10", {:report_status_value, 0, 45}}
end
test "parses end stops" do
a = Farmbot.Serial.Gcode.Parser.parse_code("R81 XA1 XB1 YA1 YB1 ZA1 ZB1 Q10")
assert a == {"10", {:report_end_stops, 1, 1, 1, 1, 1, 1}}
end
test "parses software version" do
a = Farmbot.Serial.Gcode.Parser.parse_code("R83 version string Q22")
assert a == {"22", {:report_software_version, "version string"}}
end
test "doesnt parse unhandled codes" do
a = Farmbot.Serial.Gcode.Parser.parse_code("B100")
assert a == {:unhandled_gcode, "B100"}
end
test "parses report position" do
a = Farmbot.Serial.Gcode.Parser.parse_code("R82 X1 Y2 Z3 Q10")
assert a == {"10", {:report_current_position, 1,2,3}}
end
end

View File

@ -1,19 +0,0 @@
defmodule Farmbot.Serial.HandlerTest do
alias Farmbot.Serial.Handler
@moduletag [:farmbot_serial]
use Farmbot.Test.Helpers.SerialTemplate, async: false
describe "does serial tests" do
test "checks serial availablity", %{cs_context: context} do
bool = Handler.available?(context)
assert bool == true
end
test "gets the state", %{cs_context: context} do
state = :sys.get_state(context.serial)
assert is_pid(state.nerves)
assert is_binary(state.tty)
end
end
end

View File

@ -1,26 +0,0 @@
defmodule Farmbot.System.FSTest do
use ExUnit.Case
alias Farmbot.System.FS
test "blocks and write a file" do
path = FS.path() <> "/test_file"
stuff = "HELLO WORLD"
FS.transaction fn() ->
File.write(path, stuff)
end, true
{:ok, bin} = File.read(path)
assert stuff == bin
end
test "times out" do
timeout = 1000
r = FS.transaction fn() -> Process.sleep(timeout + 100) end, true, timeout
assert is_nil r
end
test "makes coverage magically higher" do
state = FS.get_state
send FS, :uhhh
assert is_list(state)
end
end

View File

@ -1,44 +0,0 @@
defmodule TokenTest do
@moduledoc false
use ExUnit.Case, async: true
alias Farmbot.Token
test "creates a token" do
url = Faker.Internet.url
bot_name = Faker.Name.title
date_thing = 123456
email = Faker.Internet.email
decoded_json =
%{"encoded" => "asdfasdfasdfasdfasdfasdflalalalalas",
"unencoded" =>
%{"bot" => bot_name,
"exp" => date_thing,
"fw_update_server" => url,
"os_update_server" => url,
"iat" => date_thing,
"iss" => url,
"jti" => "123456",
"mqtt" => url,
"sub" => email}}
{:ok, f} = Token.create(decoded_json)
une = f.unencoded
assert(f.encoded == "asdfasdfasdfasdfasdfasdflalalalalas")
assert(une.bot == bot_name)
assert une.exp == date_thing
assert une.iat == date_thing
assert une.fw_update_server == url
assert une.os_update_server == url
assert une.sub == email
end
test "raises an execption on a bad token" do
bad_json =
%{"encoded" => "abc",
"unencoded" => "arbitrary code injection would be cool."}
assert_raise RuntimeError, fn ->
Token.create!(bad_json)
end
end
end

View File

@ -1,42 +0,0 @@
defmodule Logger.Backends.FarmbotLoggerTest do
use ExUnit.Case, async: false
alias Farmbot.{Auth, Context}
require Logger
alias Farmbot.Test.Helpers
setup_all do
backend = Logger.Backends.FarmbotLogger
on_exit(fn() ->
Logger.remove_backend(backend)
end)
context = Context.new()
Logger.flush()
{:ok, _pid} = Logger.add_backend(backend)
ok = GenEvent.call(Logger, backend, {:context, context})
[cs_context: Helpers.login(context)]
end
test "logs fifty messages, then uploads them to the api", %{cs_context: ctx} do
Logger.flush
for i <- 0..51 do
Logger.info "Farmbot can count to: #{i}"
end
Process.sleep(100)
r = Farmbot.HTTP.get! ctx, "/api/logs"
body = Poison.decode!(r.body)
assert Enum.count(body) >= 49
end
test "gets the logger state" do
Logger.flush
Logger.info "hey world"
state = GenEvent.call(Logger, Logger.Backends.FarmbotLogger, :get_state)
assert is_list(state.logs)
[log] = state.logs
assert log.message == "hey world"
end
end

View File

@ -1,40 +0,0 @@
Mix.shell.info [:green, "Compiling test helpers."]
test_lib_dir = "test/test_helpers"
File.ls!(test_lib_dir)
|> Enum.filter(fn(filename) -> Path.extname(filename) == ".exs" end)
|> Enum.all?(fn(f) ->
Mix.shell.info [:green, "Compiling #{f}"]
Code.require_file("#{test_lib_dir}/#{f}")
end) || Mix.raise "Compile error on helpers."
Mix.shell.info [:green, "Checking init and stuff"]
spawn Farmbot.Test.Helpers.Checkup, :checkup, []
Mix.shell.info [:green, "Starting ExCoveralls"]
{:ok, _} = Application.ensure_all_started(:excoveralls)
# Mix.shell.info [:green, "Starting FarmbotSimulator"]
# :ok = Application.ensure_started(:farmbot_simulator)
Process.sleep(100)
Mix.shell.info [:green, "deleting config and secret"]
File.rm_rf! "/tmp/config.json"
File.rm_rf! "/tmp/secret"
File.rm_rf! "/tmp/farmware"
Mix.shell.info [:green, "Setting up faker"]
Faker.start
Mix.shell.info [:green, "Setting up vcr"]
ExVCR.Config.cassette_library_dir("fixture/cassettes")
Mix.shell.info [:green, "removeing logger"]
Logger.remove_backend Logger.Backends.FarmbotLogger
# Farmbot.DebugLog.filter(:all)
{:ok, pid} = Farmbot.Test.SerialHelper.start_link()
Process.link(pid)
ExUnit.start

View File

@ -1,14 +0,0 @@
defmodule Farmbot.Test.Helpers.Checkup do
defp do_exit do
Mix.shell.info([:red, "Farmbot isn't alive. Not testing."])
System.halt(255)
end
def checkup do
fb_pid = Process.whereis(Farmbot.Supervisor) || do_exit()
Process.alive?(fb_pid) || do_exit()
Process.sleep(500)
checkup()
end
end

View File

@ -1,8 +0,0 @@
defmodule Farmbot.Test.Helpers.Context do
alias Farmbot.Context
def replace_http(%Context{} = context) do
{:ok, pid} = Farmbot.Test.Helpers.HTTP.start_link
%{context | http: pid}
end
end

View File

@ -1,42 +0,0 @@
defmodule Farmbot.Test.Helpers do
alias Farmbot.Database, as: DB
def login(%Farmbot.Context{} = context) do
{:ok, auth} = Farmbot.Auth.start_link(context, [])
next_context = %{context | auth: auth}
final_context = Farmbot.Test.Helpers.Context.replace_http(next_context)
Farmbot.Auth.interim(auth, "admin@admin.com", "password123", "http://localhost:3000")
Farmbot.Auth.try_log_in!(context.auth)
final_context
end
def random_file(dir \\ "fixture/api_fixture"),
do: File.ls!(dir) |> Enum.random
def read_json(:random) do
random_file() |> read_json
end
def read_json("/" <> file), do: read_json(file)
def read_json(file) do
"fixture/api_fixture/#{file}"
|> File.read!()
|> Poison.decode!
end
def seed_db(context, module, json) do
tagged = Enum.map(json, fn(item) ->
tag_item(item, module)
end)
:ok = DB.commit_records(tagged, context, module)
end
def tag_item(map, tag) do
updated_map =
map
|> Enum.map(fn({key, val}) -> {String.to_atom(key), val} end)
|> Map.new()
struct(tag, updated_map)
end
end

View File

@ -1,38 +0,0 @@
defmodule Farmbot.Test.Helpers.HTTP do
use GenServer
alias Farmbot.HTTP
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, [], opts)
end
def init([]) do
{:ok, %{}}
end
def handle_call({:request, :get, "http://localhost:3000/api/public_key", "", [], []}, _from, state) do
pub_key = File.read!("fixture/api_fixture/public_key")
response = %HTTP.Response{status_code: 200, body: pub_key, headers: []}
{:reply, {:ok, response}, state}
end
def handle_call({:request, :get, "/api/" <> resource, "", [], []}, _from, state) do
resource = String.replace(resource, "/", "_")
if "#{resource}.json" in File.ls!("fixture/api_fixture") do
json = File.read!("fixture/api_fixture/#{resource}.json") |> Poison.decode!
response = %HTTP.Response{status_code: 200, body: json, headers: []}
{:reply, {:ok, response}, state}
else
raise "Could not find: #{resource}"
end
end
def handle_call({:request, :post, "http://localhost:3000/api/tokens", _, [], []}, _from, state) do
token = File.read!("fixture/api_fixture/token.json") |> Poison.decode!
response = %HTTP.Response{status_code: 200, body: token, headers: []}
{:reply, {:ok, response}, state}
end
end

View File

@ -1,67 +0,0 @@
defmodule Farmbot.Test.SerialHelper do
use GenServer
alias Farmbot.Context
def setup_serial do
context = Context.new()
{ttya, ttyb} = slot = get_slot()
{:ok, hand} = Farmbot.Serial.Handler.start_link(context, ttyb, [])
{:ok, firm} = FirmwareSimulator.start_link(ttya, [])
context = %{context | serial: hand}
# IO.puts "claiming slot: #{inspect slot}"
{{hand, firm}, slot, context}
end
def teardown_serial({hand, firm}, slot) do
# IO.puts "releaseing slot: #{inspect slot}"
spawn fn() ->
GenServer.stop(hand, :shutdown)
GenServer.stop(firm, :shutdown)
end
done_with_slot(slot)
end
def start_link do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def get_slot do
GenServer.call(__MODULE__, :get_slot, :infinity)
end
def done_with_slot(slot) do
GenServer.call(__MODULE__, {:done_with_slot, slot})
end
def init([]) do
slots = [{"tnt0", "tnt1"}, {"tnt2", "tnt3"}, {"tnt4", "tnt5"}, {"tnt6", "tnt7"}]
slot_map = Map.new(slots, fn(slot) -> {slot, nil} end)
{:ok, %{slots: slot_map, waiting_for_slots: []}}
end
def handle_call(:get_slot, from, state) do
slot = Enum.find_value(state.slots, fn({slot, user}) ->
unless user do
slot
end
end)
if slot do
{:reply, slot, %{state | slots: %{state.slots | slot => from}}}
else
new_waiting = [from | state.waiting_for_slots]
{:noreply, %{state | waiting_for_slots: new_waiting}}
end
end
def handle_call({:done_with_slot, slot}, from, state) do
case Enum.reverse state.waiting_for_slots do
[next_in_line | rest] ->
GenServer.reply(next_in_line, slot)
{:reply, :ok, %{state | waiting_for_slots: rest, slots: %{state.slots | slot => from}}}
[] ->
{:reply, :ok, %{state | slots: %{state.slots | slot => nil}}}
end
end
end

View File

@ -1,21 +0,0 @@
defmodule Farmbot.Test.Helpers.SerialTemplate do
alias Farmbot.Serial.Handler
alias Farmbot.Test.SerialHelper
use ExUnit.CaseTemplate
defp wait_for_serial(context) do
unless Handler.available?(context) do
# IO.puts "waiting for serial..."
Process.sleep(100)
wait_for_serial(context)
end
end
setup_all do
{{hand, firm}, slot, context} = SerialHelper.setup_serial()
wait_for_serial(context)
on_exit fn() -> SerialHelper.teardown_serial({hand, firm}, slot) end
[cs_context: context, serial_handler: hand, firmware_sim: firm]
end
end