diff --git a/lib/elevator.ex b/lib/elevator.ex index 8a2294e..cc37a40 100644 --- a/lib/elevator.ex +++ b/lib/elevator.ex @@ -1,10 +1,8 @@ defmodule Elevator do @num_floors 4 + @resend_period_ms 50 + @msg_cutoff_ms 10000 @door_open_duration_ms 1000 - # ms - @resend_period 50 - @msg_ts_cutoff 10000 - @test_convergence_wait_time 3 * @resend_period def num_floors do @num_floors @@ -14,20 +12,11 @@ defmodule Elevator do @door_open_duration_ms end - def resend_period do - @resend_period + def resend_period_ms do + @resend_period_ms end - def msg_ts_cutoff do - @msg_ts_cutoff - end - - def test_convergence_wait_time do - @test_convergence_wait_time - end - - def time_to_serve_executable do - {:ok, path} = Application.fetch_env(:elevator, :time_to_serve_executable) - path + def msg_cutoff_ms do + @msg_cutoff_ms end end diff --git a/lib/elevator/cab_orders.ex b/lib/elevator/cab_orders.ex index a37fcd1..d486a4b 100644 --- a/lib/elevator/cab_orders.ex +++ b/lib/elevator/cab_orders.ex @@ -12,6 +12,7 @@ defmodule Elevator.CabOrders do GenServer.start_link(__MODULE__, arg, name: __MODULE__) end + @impl true @spec init(any()) :: {:ok, state_t()} def init(_arg \\ []) do state = %{Communicator.my_id() => %{version: 0, orders: MapSet.new()}} @@ -50,18 +51,20 @@ defmodule Elevator.CabOrders do end # --- Handle calls --- - + @impl true def handle_call(:get_my_orders, _from, state) do orders = state[Communicator.my_id()].orders {:reply, orders, state} end + @impl true def handle_call(:get_state, _from, state) do {:reply, state, state} end # --- Handle casts --- + @impl true @spec handle_cast({:receive_state, state_t()}, state_t()) :: {:noreply, state_t()} def handle_cast({:receive_state, other_state}, state) do new_state = @@ -78,6 +81,7 @@ defmodule Elevator.CabOrders do {:noreply, new_state} end + @impl true @spec handle_cast({:button_press, floor_t()}, state_t()) :: {:noreply, state_t()} def handle_cast({:button_press, floor}, state) do new_state = @@ -88,6 +92,7 @@ defmodule Elevator.CabOrders do {:noreply, new_state} end + @impl true @spec handle_cast({:arrived_at_floor, floor_t()}, state_t()) :: {:noreply, state_t()} def handle_cast({:arrived_at_floor, floor}, state) do new_state = diff --git a/lib/elevator/communicator.ex b/lib/elevator/communicator.ex index bec8baa..9ac3398 100644 --- a/lib/elevator/communicator.ex +++ b/lib/elevator/communicator.ex @@ -15,12 +15,13 @@ defmodule Elevator.Communicator do @type cab_orders_t :: Elevator.Types.cab_order_map() @type state_t :: Elevator.Types.communicator_state_map() - @type communicator_options :: [do_resend: boolean()] + @type communicator_options :: [do_resend: boolean(), do_logging: boolean()] - def start_link(arg \\ [do_resend: true]) do + def start_link(arg \\ [do_resend: true, do_logging: false]) do GenServer.start_link(__MODULE__, arg, name: __MODULE__) end + @impl true def init(opts \\ [do_resend: true, do_logging: false]) do if Keyword.get(opts, :do_resend, true) do schedule_state_broadcast() @@ -45,7 +46,6 @@ defmodule Elevator.Communicator do Returns the ID of this node. """ @spec my_id() :: node_id_t() - # TODO: decide on this def my_id, do: Node.self() @doc """ @@ -60,14 +60,15 @@ defmodule Elevator.Communicator do end @doc """ - Updates the operational key in the state map. + Updates the `operational` part of the state. + Signals to peers whether this node can serve orders. """ @spec update_operation_status(boolean()) :: :ok def update_operation_status(status) do GenServer.cast(__MODULE__, {:update_operation_status, status}) end - # Updates the timestamp when a message is recieved from a node + # Updates the timestamp when a message is received from a node @spec update_state_map(state_t(), node_id_t(), boolean()) :: state_t() defp update_state_map(state, from_node, operational) do from_node_map = %{operational: operational, timestamp: Time.utc_now()} @@ -76,13 +77,14 @@ defmodule Elevator.Communicator do # Schedules another round of state broadcasting. defp schedule_state_broadcast do - time_ms = Elevator.resend_period() + time_ms = Elevator.resend_period_ms() Process.send_after(self(), :broadcast_state, time_ms) end @doc """ Sends the cab and hall state to all connected nodes. """ + @impl true def handle_info(:broadcast_state, state) do # For periodic execution schedule_state_broadcast() @@ -104,31 +106,31 @@ defmodule Elevator.Communicator do end # Update the state map when a new node connects + @impl true def handle_info({:nodeup, node}, state) do {:noreply, update_state_map(state, node, true)} end # Delete node from state map on disconnect + @impl true def handle_info({:nodedown, node}, state) do {:noreply, %{state | connected_nodes: Map.delete(state.connected_nodes, node)}} end + @impl true def handle_info(:log_debug, state) do Process.send_after(self(), :log_debug, 1000) Logger.debug("My id: #{my_id()}") - others = who_can_serve() |> Enum.map(fn x -> "#{x}" end) |> Enum.join(", ") + others = who_can_serve() |> Enum.map(fn node -> "#{node}" end) |> Enum.join(", ") Logger.debug("Others: #{others}") {:noreply, state} end # --- Handle calls --- - def handle_call(:self, _, state) do - {:reply, my_id(), state} - end - + @impl true def handle_call(:who_can_serve, _from, state) do - cutoff_ms = Elevator.msg_ts_cutoff() + cutoff_ms = Elevator.msg_cutoff_ms() communicating_nodes = state.connected_nodes @@ -153,6 +155,7 @@ defmodule Elevator.Communicator do @doc """ Sends received hall and cab orders to respective modules, and updates timestamps for when the connected nodes last sent something. """ + @impl true @spec handle_cast( {:state_update, node_id_t(), boolean(), hall_orders_t(), cab_orders_t()}, state_t() @@ -165,7 +168,8 @@ defmodule Elevator.Communicator do {:noreply, new_state} end - @spec handle_cast({:update_operation_status, boolean()}, state_t()) :: state_t() + @impl true + @spec handle_cast({:update_operation_status, boolean()}, state_t()) :: {:noreply, state_t()} def handle_cast({:update_operation_status, status}, state) do {:noreply, %{state | operational: status}} end diff --git a/lib/elevator/fsm/action.ex b/lib/elevator/fsm/action.ex index f55f45d..12c1db0 100644 --- a/lib/elevator/fsm/action.ex +++ b/lib/elevator/fsm/action.ex @@ -9,8 +9,8 @@ defmodule Elevator.FSM.Action do alias Elevator.HallOrders alias Elevator.Decision - @motor_timeout 4000 - @action_interval 100 + @motor_timeout_ms 4000 + @action_interval_ms 100 def start_link(_arg) do pid = spawn_link(fn -> poll_action() end) @@ -32,7 +32,7 @@ defmodule Elevator.FSM.Action do poll_door_timer() check_motor_timeout() decide_and_take_action() - Process.sleep(@action_interval) + Process.sleep(@action_interval_ms) poll_action() end @@ -53,7 +53,7 @@ defmodule Elevator.FSM.Action do false last_floor_time -> - Time.diff(Time.utc_now(), last_floor_time, :millisecond) > @motor_timeout + Time.diff(Time.utc_now(), last_floor_time, :millisecond) > @motor_timeout_ms end State.set_motor_timed_out(timed_out) @@ -70,9 +70,6 @@ defmodule Elevator.FSM.Action do {new_direction, new_behavior} = Decision.next_action(orders, state) - # Logger.debug("Deciding on behavior from state:\n #{inspect(state)}\n Orders: #{inspect(orders)}") - # Logger.debug("Got behavior #{new_direction} and #{new_behavior}") - cond do state.behavior == :door_open -> CabOrders.arrived_at_floor(state.floor) diff --git a/lib/elevator/fsm/state.ex b/lib/elevator/fsm/state.ex index 95016a2..e0de1b7 100644 --- a/lib/elevator/fsm/state.ex +++ b/lib/elevator/fsm/state.ex @@ -12,7 +12,7 @@ defmodule Elevator.FSM.State do between_floors: true, obstructed: false, motor_timed_out: false, - door_open_time: Time.utc_now(), + door_open_time_ms: Time.utc_now(), last_floor_time: nil @type t :: %__MODULE__{ @@ -22,7 +22,7 @@ defmodule Elevator.FSM.State do between_floors: boolean(), obstructed: boolean(), motor_timed_out: boolean(), - door_open_time: Time.t(), + door_open_time_ms: Time.t(), last_floor_time: Time.t() | nil } @@ -48,6 +48,10 @@ defmodule Elevator.FSM.State do def set_behavior(behavior), do: GenServer.cast(__MODULE__, {:set_behavior, behavior}) + @doc """ + Opens the door if the elevator is at a floor. + Does nothing if the elevator is between floors. + """ def open_door(), do: GenServer.cast(__MODULE__, :open_door) def set_motor_timed_out(timed_out), @@ -86,18 +90,13 @@ defmodule Elevator.FSM.State do {:noreply, %{state | behavior: behavior}, {:continue, :set_outputs}} end - @impl true - def handle_cast({:set_door_open_time, door_open_time}, state) do - {:noreply, %{state | door_open_time: door_open_time}, {:continue, :set_outputs}} - end - @impl true def handle_cast(:open_door, state) do new_state = if state.between_floors do state else - %{state | behavior: :door_open, door_open_time: Time.utc_now()} + %{state | behavior: :door_open, door_open_time_ms: Time.utc_now()} end {:noreply, new_state, {:continue, :set_outputs}} diff --git a/lib/elevator/hall_orders.ex b/lib/elevator/hall_orders.ex index 3916ae3..fd44860 100644 --- a/lib/elevator/hall_orders.ex +++ b/lib/elevator/hall_orders.ex @@ -16,7 +16,7 @@ defmodule Elevator.HallOrders do @type floor :: Elevator.Types.floor() @type hall_btn :: Elevator.Types.hall_btn() - @hall_order_refresh_period 1000 + @hall_order_refresh_period_ms 1000 def start_link(arg) do GenServer.start_link(__MODULE__, arg, name: __MODULE__) @@ -39,14 +39,14 @@ defmodule Elevator.HallOrders do |> Enum.map(&{&1, {0, :idle}}) |> Enum.into(%{}) - Process.send_after(self(), :refresh_hall_orders, @hall_order_refresh_period) + Process.send_after(self(), :refresh_hall_orders, @hall_order_refresh_period_ms) {:ok, state} end @doc """ - Receiving the hall order state from another node. - Merges the states by updating the individual order status. + Receives the hall order state from another node and merges it into local state. + Each order is merged individually using the consensus algorithm in `HallOrders.Order`. """ @spec receive_state(hall_order_map()) :: :ok def receive_state(other_state), do: GenServer.cast(__MODULE__, {:receive_state, other_state}) @@ -115,6 +115,7 @@ defmodule Elevator.HallOrders do {:reply, confirmed_orders, order_map} end + @impl true def handle_call(:get_state, _, order_map) do {:reply, order_map, order_map} end @@ -149,7 +150,7 @@ defmodule Elevator.HallOrders do {old_order_version, old_order_state} = old_order_value - order_map = + new_order_map = case old_order_state do :idle -> Map.put(order_map, key, {old_order_version + 1, {:pending, MapSet.new([Node.self()])}}) @@ -158,7 +159,7 @@ defmodule Elevator.HallOrders do order_map end - new_order_value = order_map[key] + new_order_value = new_order_map[key] if old_order_value != new_order_value do Logger.debug(fn -> @@ -166,7 +167,7 @@ defmodule Elevator.HallOrders do end) end - {:noreply, order_map, {:continue, :hall_update_state}} + {:noreply, new_order_map, {:continue, :hall_update_state}} end @impl true @@ -177,7 +178,7 @@ defmodule Elevator.HallOrders do key = {floor, button_type} order_value = order_map[key] - order_map = + new_order_map = case order_value do {order_version, {:confirmed, _}} -> Map.put(order_map, key, {order_version + 1, :idle}) @@ -186,12 +187,12 @@ defmodule Elevator.HallOrders do order_map end - {:noreply, order_map} + {:noreply, new_order_map} end @impl true def handle_info(:refresh_hall_orders, order_map) do - Process.send_after(self(), :refresh_hall_orders, @hall_order_refresh_period) + Process.send_after(self(), :refresh_hall_orders, @hall_order_refresh_period_ms) {:noreply, order_map, {:continue, :hall_update_state}} end diff --git a/lib/elevator/hall_orders/cost.ex b/lib/elevator/hall_orders/cost.ex index 8cb3002..dc9cd6b 100644 --- a/lib/elevator/hall_orders/cost.ex +++ b/lib/elevator/hall_orders/cost.ex @@ -10,7 +10,7 @@ defmodule Elevator.HallOrders.Cost do alias Elevator.FSM.State require Logger - @travel_duration 2500 + @travel_duration_ms 2500 @max_simulation_steps 256 @unreachable_cost 30000 @@ -85,7 +85,8 @@ defmodule Elevator.HallOrders.Cost do fn {node1, cost1}, {node2, cost2} -> cost1 < cost2 or (cost1 == cost2 and node1 < node2) end, - fn -> {:nonode@nohost, Inf} end + # Fallback when no alive costs exist + fn -> {:nonode@nohost, :infinity} end ) min_node @@ -129,7 +130,13 @@ defmodule Elevator.HallOrders.Cost do behavior: :moving } - do_simulate(orders, next_state, target, time_ms + @travel_duration, steps_left - 1) + do_simulate( + orders, + next_state, + target, + time_ms + @travel_duration_ms, + steps_left - 1 + ) :error -> @unreachable_cost @@ -189,6 +196,7 @@ defmodule Elevator.HallOrders.Cost do orders true -> + # No reason to continue up: clear hall down when turning around clear_button(orders, floor, :hall_down) end end diff --git a/lib/elevator/hall_orders/order.ex b/lib/elevator/hall_orders/order.ex index 24c3cac..89cb5cd 100644 --- a/lib/elevator/hall_orders/order.ex +++ b/lib/elevator/hall_orders/order.ex @@ -57,9 +57,9 @@ defmodule Elevator.HallOrders.Order do end @doc """ - Maybe update a hall order based on its own state. - This may happen for example when the order autonomously transitions from pending to - confirmed when only one elevator is alive. + Advances a pending order to confirmed if the barrier set is full. + Computes and records this node's cost at the point of confirmation. + Returns `{true, new_value}` if the state changed, `{false, unchanged}` otherwise. """ @spec update_hall_order(hall_order_key(), hall_order_value(), %{ Types.floor() => MapSet.t(Types.hall_btn()) diff --git a/lib/elevator/hardware/driver.ex b/lib/elevator/hardware/driver.ex index c5a38db..ef44fa8 100644 --- a/lib/elevator/hardware/driver.ex +++ b/lib/elevator/hardware/driver.ex @@ -1,8 +1,9 @@ defmodule Elevator.Hardware.Driver do use GenServer require Logger - @call_timeout 1000 - @reconnect_interval 1000 + + @call_timeout_ms 1000 + @reconnect_interval_ms 1000 @button_map %{:hall_up => 0, :hall_down => 1, :cab => 2} @state_map %{:on => 1, :off => 0} @direction_map %{:up => 1, :down => 255, :stop => 0} @@ -33,10 +34,10 @@ defmodule Elevator.Hardware.Driver do {:error, reason} -> Logger.warning( - "Driver failed to connect (#{reason}), retrying in #{@reconnect_interval}ms..." + "Driver failed to connect (#{reason}), retrying in #{@reconnect_interval_ms}ms..." ) - Process.sleep(@reconnect_interval) + Process.sleep(@reconnect_interval_ms) connect(address, port) end end @@ -193,7 +194,7 @@ defmodule Elevator.Hardware.Driver do def handle_call({:get_order_button_state, floor, order_type}, _from, {socket, addr, port}) do :gen_tcp.send(socket, [6, @button_map[order_type], floor, 0]) - case :gen_tcp.recv(socket, 4, @call_timeout) do + case :gen_tcp.recv(socket, 4, @call_timeout_ms) do {:ok, [6, 0, 0, 0]} -> {:reply, :inactive, {socket, addr, port}} {:ok, [6, 1, 0, 0]} -> {:reply, :active, {socket, addr, port}} {:error, reason} -> {:stop, reason, {:error, reason}, {socket, addr, port}} @@ -204,7 +205,7 @@ defmodule Elevator.Hardware.Driver do def handle_call(:get_floor_sensor_state, _from, {socket, addr, port}) do :gen_tcp.send(socket, [7, 0, 0, 0]) - case :gen_tcp.recv(socket, 4, @call_timeout) do + case :gen_tcp.recv(socket, 4, @call_timeout_ms) do {:ok, [7, 0, _, 0]} -> {:reply, :between_floors, {socket, addr, port}} {:ok, [7, 1, floor, 0]} -> {:reply, floor, {socket, addr, port}} {:error, reason} -> {:stop, reason, {:error, reason}, {socket, addr, port}} @@ -215,7 +216,7 @@ defmodule Elevator.Hardware.Driver do def handle_call(:get_stop_button_state, _from, {socket, addr, port}) do :gen_tcp.send(socket, [8, 0, 0, 0]) - case :gen_tcp.recv(socket, 4, @call_timeout) do + case :gen_tcp.recv(socket, 4, @call_timeout_ms) do {:ok, [8, 0, 0, 0]} -> {:reply, :inactive, {socket, addr, port}} {:ok, [8, 1, 0, 0]} -> {:reply, :active, {socket, addr, port}} {:error, reason} -> {:stop, reason, {:error, reason}, {socket, addr, port}} @@ -226,7 +227,7 @@ defmodule Elevator.Hardware.Driver do def handle_call(:get_obstruction_switch_state, _from, {socket, addr, port}) do :gen_tcp.send(socket, [9, 0, 0, 0]) - case :gen_tcp.recv(socket, 4, @call_timeout) do + case :gen_tcp.recv(socket, 4, @call_timeout_ms) do {:ok, [9, 0, 0, 0]} -> {:reply, :inactive, {socket, addr, port}} {:ok, [9, 1, 0, 0]} -> {:reply, :active, {socket, addr, port}} {:error, reason} -> {:stop, reason, {:error, reason}, {socket, addr, port}} diff --git a/lib/elevator/hardware/input_poller.ex b/lib/elevator/hardware/input_poller.ex index 04bece5..d1ee06f 100644 --- a/lib/elevator/hardware/input_poller.ex +++ b/lib/elevator/hardware/input_poller.ex @@ -10,9 +10,9 @@ defmodule Elevator.Hardware.InputPoller do alias Elevator.HallOrders alias Elevator.Hardware.Driver - @floor_poll_interval 50 - @button_poll_interval 20 - @obstruction_poll_interval 500 + @floor_poll_interval_ms 50 + @button_poll_interval_ms 20 + @obstruction_poll_interval_ms 500 # Public API def start_link(_opts) do @@ -53,9 +53,7 @@ defmodule Elevator.Hardware.InputPoller do @impl true def handle_info(:poll_buttons, state) do schedule_button_poll() - # Polls button and notifies State if any are pressed - - prev_buttons = Map.get(state, :prev_buttons, MapSet.new()) + # Polls button and notifies Cab- and HallOrders if any are pressed current_buttons = for floor <- 0..(Elevator.num_floors() - 1), @@ -65,7 +63,7 @@ defmodule Elevator.Hardware.InputPoller do end # Only notify on new presses (in current but not in previous) - new_presses = MapSet.difference(current_buttons, prev_buttons) + new_presses = MapSet.difference(current_buttons, state.prev_buttons) Enum.each(new_presses, fn {floor, btn} -> case btn do @@ -91,14 +89,14 @@ defmodule Elevator.Hardware.InputPoller do # Schedule functions defp schedule_button_poll do - Process.send_after(self(), :poll_buttons, @button_poll_interval) + Process.send_after(self(), :poll_buttons, @button_poll_interval_ms) end defp schedule_floor_poll do - Process.send_after(self(), :poll_floor, @floor_poll_interval) + Process.send_after(self(), :poll_floor, @floor_poll_interval_ms) end defp schedule_obstruction_poll do - Process.send_after(self(), :poll_obstruction, @obstruction_poll_interval) + Process.send_after(self(), :poll_obstruction, @obstruction_poll_interval_ms) end end diff --git a/lib/elevator/hardware/outputs.ex b/lib/elevator/hardware/outputs.ex index 5af9b34..d8280e2 100644 --- a/lib/elevator/hardware/outputs.ex +++ b/lib/elevator/hardware/outputs.ex @@ -24,8 +24,9 @@ defmodule Elevator.Hardware.Outputs do set_motors(state) set_floor_light(state) - operational? = !((state.behavior == :door_open and state.obstructed) or state.motor_timed_out) - Communicator.update_operation_status(operational?) + door_blocked = state.behavior == :door_open and state.obstructed + operational = not (door_blocked or state.motor_timed_out) + Communicator.update_operation_status(operational) Task.start(fn -> orders = get_light_orders() diff --git a/test/multi/cab_orders_test.exs b/test/multi/cab_orders_test.exs index ed4ec73..c935805 100644 --- a/test/multi/cab_orders_test.exs +++ b/test/multi/cab_orders_test.exs @@ -1,6 +1,7 @@ defmodule Test.Multi.CabOrdersTest do alias Elevator.CabOrders alias Test.Utils.MultiCluster + alias Test.Utils.TestCompiled, as: TestUtils use ExUnit.Case, async: false @@ -38,13 +39,13 @@ defmodule Test.Multi.CabOrdersTest do %{node1 => %{version: 420, orders: MapSet.new([3])}} ]) - Process.sleep(Elevator.test_convergence_wait_time()) + Process.sleep(TestUtils.convergence_wait_ms()) node1_orders = :rpc.call(node1, CabOrders, :get_my_orders, []) assert node1_orders == MapSet.new([3]) end - test "elevator ingores lower version numbers", %{nodes: [node1, node2, node3]} do + test "elevator ignores lower version numbers", %{nodes: [node1, node2, node3]} do %{version: node1_version, orders: node1_orders} = :rpc.call(node1, CabOrders, :get_state, [])[node1] @@ -56,7 +57,7 @@ defmodule Test.Multi.CabOrdersTest do %{node1 => %{version: 69, orders: MapSet.new([1])}} ]) - Process.sleep(Elevator.test_convergence_wait_time()) + Process.sleep(TestUtils.convergence_wait_ms()) %{version: node1_version, orders: node1_orders} = :rpc.call(node1, CabOrders, :get_state, [])[node1] @@ -69,7 +70,7 @@ defmodule Test.Multi.CabOrdersTest do %{node1 => %{version: 67, orders: MapSet.new([1, 2])}} ]) - Process.sleep(Elevator.test_convergence_wait_time()) + Process.sleep(TestUtils.convergence_wait_ms()) %{version: node1_version, orders: node1_orders} = :rpc.call(node1, CabOrders, :get_state, [])[node1] @@ -82,7 +83,7 @@ defmodule Test.Multi.CabOrdersTest do %{node1 => %{version: 69, orders: MapSet.new([1, 2])}} ]) - Process.sleep(Elevator.test_convergence_wait_time()) + Process.sleep(TestUtils.convergence_wait_ms()) %{version: node1_version, orders: node1_orders} = :rpc.call(node1, CabOrders, :get_state, [])[node1] @@ -91,7 +92,7 @@ defmodule Test.Multi.CabOrdersTest do assert node1_orders == MapSet.new([1]) end - test "cab order states progate", %{nodes: [node1, node2, node3]} do + test "cab order states propagate", %{nodes: [node1, node2, node3]} do %{version: node1_version, orders: node1_orders} = :rpc.call(node1, CabOrders, :get_state, [])[node1] @@ -107,7 +108,7 @@ defmodule Test.Multi.CabOrdersTest do node3_orders == MapSet.new() :rpc.cast(node1, CabOrders, :button_press, [1]) - Process.sleep(Elevator.test_convergence_wait_time()) + Process.sleep(TestUtils.convergence_wait_ms()) # Ensure that node1's version and order map has propagated across all nodes %{version: node1_version, orders: node1_orders} = diff --git a/test/multi/hall_orders_test.exs b/test/multi/hall_orders_test.exs index 07bcc25..e666dfd 100644 --- a/test/multi/hall_orders_test.exs +++ b/test/multi/hall_orders_test.exs @@ -1,7 +1,8 @@ -defmodule Test.Multi.HallOrders do +defmodule Test.Multi.HallOrdersTest do alias Elevator.HallOrders alias Elevator.Communicator alias Test.Utils.MultiCluster + alias Test.Utils.TestCompiled, as: TestUtils use ExUnit.Case, async: false setup context do @@ -113,7 +114,7 @@ defmodule Test.Multi.HallOrders do test "communicator causes convergence", %{nodes: [node1, node2, node3]} do :rpc.call(node1, HallOrders, :button_press, [2, :hall_up]) - Process.sleep(Elevator.test_convergence_wait_time()) + Process.sleep(TestUtils.convergence_wait_ms()) node1_orders = :rpc.call(node1, HallOrders, :get_my_orders, []) node2_orders = :rpc.call(node2, HallOrders, :get_my_orders, []) @@ -135,7 +136,7 @@ defmodule Test.Multi.HallOrders do :rpc.call(who_arrives, HallOrders, :arrived_at_floor, [2, :up]) - Process.sleep(Elevator.test_convergence_wait_time()) + Process.sleep(TestUtils.convergence_wait_ms()) node1_state = :rpc.call(node1, HallOrders, :get_state, []) node2_state = :rpc.call(node2, HallOrders, :get_state, []) diff --git a/test/single/cost_test.exs b/test/single/cost_test.exs index 33ae667..6e680e6 100644 --- a/test/single/cost_test.exs +++ b/test/single/cost_test.exs @@ -5,7 +5,9 @@ defmodule Test.Single.CostTest do alias Elevator.FSM.State alias Elevator.HallOrders.Cost - @travel_duration 2500 + @travel_duration_ms 2500 + @unreachable_cost 30000 + @state_settle_ms 10 setup do start_supervised!({CabOrders, []}) @@ -31,19 +33,20 @@ defmodule Test.Single.CostTest do set_state(floor: 0, direction: :up, behavior: :idle) assert Cost.compute_cost({1, :hall_up}, %{}) == - @travel_duration + Elevator.door_open_duration_ms() + @travel_duration_ms + Elevator.door_open_duration_ms() end test "unknown floor yields unreachable cost" do set_state(floor: :unknown, direction: :down, behavior: :idle) - assert Cost.compute_cost({1, :hall_up}, %{}) == 30000 + assert Cost.compute_cost({1, :hall_up}, %{}) == @unreachable_cost end defp set_state(opts) do State.set_direction(Keyword.fetch!(opts, :direction)) State.set_behavior(Keyword.fetch!(opts, :behavior)) State.set_floor(Keyword.fetch!(opts, :floor)) - Process.sleep(10) + # Wait for GenServer casts to settle + Process.sleep(@state_settle_ms) end end diff --git a/test/single/hall_orders_test.exs b/test/single/hall_orders_test.exs index c36d3b7..127fa5a 100644 --- a/test/single/hall_orders_test.exs +++ b/test/single/hall_orders_test.exs @@ -1,13 +1,14 @@ -defmodule Test.Single.HallOrders do +defmodule Test.Single.HallOrdersTest do use ExUnit.Case, async: false # TODO: Maybe doctest - # doctest Elevator + + @max_continue_iterations 100 setup_all do - start_supervised!(Elevator.FSM.State) start_supervised!(Elevator.Communicator) start_supervised!(Elevator.CabOrders) start_supervised!({Elevator.HallOrders, Elevator.num_floors()}) + start_supervised!(Elevator.FSM.State) :ok end @@ -41,9 +42,9 @@ defmodule Test.Single.HallOrders do test "clear floor from pending state leaves elevator state unchanged" do {:ok, state} = Elevator.HallOrders.init(3) id = Node.self() - state = Map.put(state, {1, :hall_up}, {:pending, MapSet.new([id])}) + state = Map.put(state, {1, :hall_up}, {0, {:pending, MapSet.new([id])}}) assert {:noreply, final_state} = hallorder_cast_full({:arrived_at_floor, 1, :up}, state) - assert {:pending, _} = final_state[{1, :hall_up}] + assert {_, {:pending, _}} = final_state[{1, :hall_up}] end @tag :hall_orders_single @@ -97,7 +98,7 @@ defmodule Test.Single.HallOrders do defp hallorder_continue_full(continue_arg, state, continue_counter \\ 0) do # Prevent infinite continue loop - assert continue_counter < 100 + assert continue_counter < @max_continue_iterations ret = Elevator.HallOrders.handle_continue(continue_arg, state) diff --git a/test/utils/multi_cluster.ex b/test/utils/multi_cluster.ex index 732aabe..5bc2b40 100644 --- a/test/utils/multi_cluster.ex +++ b/test/utils/multi_cluster.ex @@ -1,6 +1,10 @@ defmodule Test.Utils.MultiCluster do @moduledoc false + @node_shutdown_timeout_ms 1000 + @cluster_wait_timeout_ms 2000 + @poll_interval_ms 50 + @spec start_three_node_cluster(non_neg_integer(), boolean()) :: %{ nodes: [node()], peers: [pid()] @@ -47,7 +51,7 @@ defmodule Test.Utils.MultiCluster do receive do {:DOWN, _, :process, ^pid, _} -> :ok after - 1000 -> :ok + @node_shutdown_timeout_ms -> :ok end end end @@ -87,12 +91,12 @@ defmodule Test.Utils.MultiCluster do {peer, node} end - defp wait_until_connected(nodes, timeout \\ 2000) do + defp wait_until_connected(nodes, timeout \\ @cluster_wait_timeout_ms) do deadline = System.monotonic_time(:millisecond) + timeout wait_connected_loop(nodes, deadline) end - defp wait_until_disconnected(nodes, timeout \\ 2000) do + defp wait_until_disconnected(nodes, timeout \\ @cluster_wait_timeout_ms) do deadline = System.monotonic_time(:millisecond) + timeout wait_disconnected_loop(nodes, deadline) end @@ -105,7 +109,7 @@ defmodule Test.Utils.MultiCluster do raise "Test timed out waiting for nodes" end - Process.sleep(50) + Process.sleep(@poll_interval_ms) wait_connected_loop(nodes, deadline) end end @@ -118,7 +122,7 @@ defmodule Test.Utils.MultiCluster do raise "Test timed out waiting for node disconnect" end - Process.sleep(50) + Process.sleep(@poll_interval_ms) wait_disconnected_loop(nodes, deadline) end end diff --git a/test/utils/test_compiled.ex b/test/utils/test_compiled.ex index 62c7da4..503f51c 100644 --- a/test/utils/test_compiled.ex +++ b/test/utils/test_compiled.ex @@ -3,6 +3,8 @@ defmodule Test.Utils.TestCompiled do This module exists because to run :rpc-calls, the called code has to be compiled, not .exs. So put code here if it is the endpoint of an RPC for testing that is not suitable for lib/. """ + def convergence_wait_ms, do: 3 * Elevator.resend_period_ms() + def start_order_modules(num_floors, do_resend) do children = [ {Elevator.Communicator, [do_resend: do_resend]},