diff --git a/lib/engine/alerts.ex b/lib/engine/alerts.ex index 96b74093e..8818fc6c5 100644 --- a/lib/engine/alerts.ex +++ b/lib/engine/alerts.ex @@ -1,4 +1,5 @@ defmodule Engine.Alerts do + @behaviour Engine.AlertsAPI use GenServer require Logger alias Engine.Alerts.Fetcher @@ -22,8 +23,7 @@ defmodule Engine.Alerts do GenServer.start_link(__MODULE__, opts, name: name) end - @spec max_stop_status(ets_tables(), [Fetcher.stop_id()], [Fetcher.route_id()]) :: - Fetcher.stop_status() + @impl true def max_stop_status( tables \\ %{stops_table: @stops_table, routes_table: @routes_table}, stop_ids, @@ -65,7 +65,7 @@ defmodule Engine.Alerts do end end - @spec init(Keyword.t()) :: {:ok, state()} + @impl true def init(opts) do fetch_ms = opts[:fetch_ms] || 30_000 fetcher = opts[:fetcher] || Engine.Alerts.ApiFetcher @@ -93,6 +93,7 @@ defmodule Engine.Alerts do {:ok, state} end + @impl true def handle_info(:fetch, state) do schedule_fetch(self(), state.fetch_ms) diff --git a/lib/engine/alerts_api.ex b/lib/engine/alerts_api.ex new file mode 100644 index 000000000..9508c4502 --- /dev/null +++ b/lib/engine/alerts_api.ex @@ -0,0 +1,4 @@ +defmodule Engine.AlertsAPI do + alias Engine.Alerts.Fetcher + @callback max_stop_status([Fetcher.stop_id()], [Fetcher.route_id()]) :: Fetcher.stop_status() +end diff --git a/lib/engine/config.ex b/lib/engine/config.ex index 58a37098d..bc86e5111 100644 --- a/lib/engine/config.ex +++ b/lib/engine/config.ex @@ -2,6 +2,7 @@ defmodule Engine.Config do @moduledoc """ Manages the dynamic configurable pieces of the signs such as if they are on """ + @behaviour Engine.ConfigAPI use GenServer require Logger @@ -28,7 +29,7 @@ defmodule Engine.Config do GenServer.start_link(__MODULE__, [], name: __MODULE__) end - @spec sign_config(:ets.tab(), String.t()) :: sign_config() + @impl true def sign_config(table_name \\ @table_signs, sign_id) do case :ets.lookup(table_name, sign_id) do [{^sign_id, config}] -> config @@ -36,7 +37,7 @@ defmodule Engine.Config do end end - @spec headway_config(:ets.tab(), String.t(), DateTime.t()) :: Headway.t() | nil + @impl true def headway_config(table_name \\ @table_headways, headway_group, current_time) do time_period = Headway.current_time_period(current_time) Headways.get_headway(table_name, {headway_group, time_period}) @@ -54,7 +55,7 @@ defmodule Engine.Config do schedule_update(pid, 0) end - @spec init(map()) :: {:ok, state} + @impl true def init(opts) do state = %{ table_name_signs: @table_signs, @@ -84,7 +85,7 @@ defmodule Engine.Config do ]) end - @spec handle_info(:update, state) :: {:noreply, state} + @impl true def handle_info(:update, state) do schedule_update(self()) updater = Application.get_env(:realtime_signs, :external_config_getter) diff --git a/lib/engine/config_api.ex b/lib/engine/config_api.ex new file mode 100644 index 000000000..70835d7ff --- /dev/null +++ b/lib/engine/config_api.ex @@ -0,0 +1,4 @@ +defmodule Engine.ConfigAPI do + @callback sign_config(String.t()) :: Engine.Config.sign_config() + @callback headway_config(String.t(), DateTime.t()) :: Engine.Config.Headway.t() | nil +end diff --git a/lib/engine/predictions.ex b/lib/engine/predictions.ex index 58eff3b77..614de8723 100644 --- a/lib/engine/predictions.ex +++ b/lib/engine/predictions.ex @@ -7,6 +7,7 @@ defmodule Engine.Predictions do Offers a `for_stop/1` public interface to get a list of Predictions.Prediction's for a given GTFS stop. """ + @behaviour Engine.PredictionsAPI use GenServer require Logger @@ -25,7 +26,7 @@ defmodule Engine.Predictions do end @doc "The upcoming predicted times a vehicle will be at this stop" - @spec for_stop(String.t(), 0 | 1) :: [Predictions.Prediction.t()] + @impl true def for_stop(predictions_table_id \\ @trip_updates_table, gtfs_stop_id, direction_id) do case :ets.lookup(predictions_table_id, {gtfs_stop_id, direction_id}) do [{_, :none}] -> [] @@ -34,6 +35,7 @@ defmodule Engine.Predictions do end end + @impl true def init(_) do schedule_update(self()) @@ -48,6 +50,7 @@ defmodule Engine.Predictions do }} end + @impl true def handle_info(:update, state) do schedule_update(self()) current_time = Timex.now() diff --git a/lib/engine/predictions_api.ex b/lib/engine/predictions_api.ex new file mode 100644 index 000000000..0034f39ac --- /dev/null +++ b/lib/engine/predictions_api.ex @@ -0,0 +1,3 @@ +defmodule Engine.PredictionsAPI do + @callback for_stop(String.t(), 0 | 1) :: [Predictions.Prediction.t()] +end diff --git a/lib/signs/realtime.ex b/lib/signs/realtime.ex index 991ca8d4e..b60615650 100644 --- a/lib/signs/realtime.ex +++ b/lib/signs/realtime.ex @@ -24,6 +24,7 @@ defmodule Signs.Realtime do :last_departure_engine, :config_engine, :alerts_engine, + :current_time_fn, :sign_updater, :last_update, :tick_audit, @@ -58,6 +59,7 @@ defmodule Signs.Realtime do last_departure_engine: module(), config_engine: module(), alerts_engine: module(), + current_time_fn: fun(), sign_updater: module(), last_update: DateTime.t(), tick_audit: non_neg_integer(), @@ -91,6 +93,12 @@ defmodule Signs.Realtime do last_departure_engine: last_departure_engine, config_engine: config_engine, alerts_engine: alerts_engine, + current_time_fn: + opts[:current_time_fn] || + fn -> + time_zone = Application.get_env(:realtime_signs, :time_zone) + DateTime.utc_now() |> DateTime.shift_zone!(time_zone) + end, sign_updater: sign_updater, last_update: nil, tick_audit: 60, @@ -113,8 +121,7 @@ defmodule Signs.Realtime do sign_routes = SourceConfig.sign_routes(sign.source_config) alert_status = sign.alerts_engine.max_stop_status(sign_stop_ids, sign_routes) sign_config = sign.config_engine.sign_config(sign.id) - time_zone = Application.get_env(:realtime_signs, :time_zone) - {:ok, current_time} = DateTime.utc_now() |> DateTime.shift_zone(time_zone) + current_time = sign.current_time_fn.() predictions = case sign.source_config do diff --git a/test/signs/realtime_test.exs b/test/signs/realtime_test.exs index 3e1f40cf2..6f5104dd0 100644 --- a/test/signs/realtime_test.exs +++ b/test/signs/realtime_test.exs @@ -1,79 +1,17 @@ defmodule Signs.RealtimeTest do use ExUnit.Case, async: false import ExUnit.CaptureLog + import Mox alias Content.Message.Headways.Top, as: HT alias Content.Message.Headways.Bottom, as: HB - defmodule FakePredictions do - def for_stop(_stop_id, _direction_id), do: [] - def stopped_at?(_stop_id), do: false - end - - defmodule FakePassthroughPredictions do - def for_stop("1", 0) do - [ - %Predictions.Prediction{ - stop_id: "1", - direction_id: 0, - route_id: "Red", - stopped?: false, - stops_away: 4, - destination_stop_id: "70105", - seconds_until_arrival: nil, - seconds_until_departure: nil, - seconds_until_passthrough: 30, - trip_id: "123" - } - ] - end - - def for_stop(_stop_id, _direction_id), do: [] - end - - defmodule FakeDepartureEngine do - @test_departure_time nil - - def get_last_departure(_) do - @test_departure_time - end - - def test_departure_time() do - @test_departure_time - end - end - defmodule FakeHeadways do def get_headways(_stop_id), do: {1, 5} def display_headways?(_stop_ids, _time, _buffer), do: true end - defmodule FakeConfigEngine do - def sign_config(_sign_id), do: :auto - - def headway_config(_group, _time) do - %Engine.Config.Headway{headway_id: "id", range_low: 11, range_high: 13} - end - end - - defmodule FakeUpdater do - def update_sign(id, top_msg, bottom_msg, duration, start, sign_id) do - send(self(), {:update_sign, id, top_msg, bottom_msg, duration, start, sign_id}) - end - - def send_audio(id, audio, priority, timeout, sign_id) do - send(self(), {:send_audio, id, audio, priority, timeout, sign_id}) - end - end - - defmodule FakeAlerts do - def max_stop_status(["suspended"], _routes), do: :suspension_closed_station - def max_stop_status(["suspended_transfer"], _routes), do: :suspension_transfer_station - def max_stop_status(["shuttles"], _routes), do: :shuttles_closed_station - def max_stop_status(["closure"], _routes), do: :station_closure - def max_stop_status(_stops, ["Green-B"]), do: :alert_along_route - def max_stop_status(_stops, _routes), do: :none - end + @headway_config %Engine.Config.Headway{headway_id: "id", range_low: 11, range_high: 13} @src %Signs.Utilities.SourceConfig{ stop_id: "1", @@ -84,6 +22,9 @@ defmodule Signs.RealtimeTest do announce_boarding?: false } + @fake_time DateTime.new!(~D[2023-01-01], ~T[12:00:00], "America/New_York") + def fake_time_fn, do: @fake_time + @sign %Signs.Realtime{ id: "sign_id", text_id: {"TEST", "x"}, @@ -94,20 +35,77 @@ defmodule Signs.RealtimeTest do headway_destination: :southbound }, current_content_top: %HT{destination: :southbound, vehicle_type: :train}, - current_content_bottom: %HB{range: {1, 5}, prev_departure_mins: nil}, - prediction_engine: FakePredictions, + current_content_bottom: %HB{range: {11, 13}}, + prediction_engine: Engine.Predictions.Mock, headway_engine: FakeHeadways, - last_departure_engine: FakeDepartureEngine, - config_engine: FakeConfigEngine, - alerts_engine: FakeAlerts, - sign_updater: FakeUpdater, - last_update: Timex.now(), + last_departure_engine: nil, + config_engine: Engine.Config.Mock, + alerts_engine: Engine.Alerts.Mock, + current_time_fn: &Signs.RealtimeTest.fake_time_fn/0, + sign_updater: PaEss.Updater.Mock, + last_update: @fake_time, tick_read: 1, tick_audit: 1, read_period_seconds: 100 } + @predictions [ + %Predictions.Prediction{ + stop_id: "1", + direction_id: 0, + route_id: "Red", + stopped?: false, + stops_away: 1, + destination_stop_id: "70093", + seconds_until_arrival: 120, + seconds_until_departure: 180 + }, + %Predictions.Prediction{ + stop_id: "1", + direction_id: 0, + route_id: "Red", + stopped?: false, + stops_away: 1, + destination_stop_id: "70093", + seconds_until_arrival: 240, + seconds_until_departure: 300 + } + ] + + @no_departures_predictions [ + %Predictions.Prediction{ + stop_id: "1", + direction_id: 0, + route_id: "Red", + stopped?: false, + stops_away: 1, + destination_stop_id: "70093", + seconds_until_arrival: 120 + }, + %Predictions.Prediction{ + stop_id: "1", + direction_id: 0, + route_id: "Red", + stopped?: false, + stops_away: 1, + destination_stop_id: "70093", + seconds_until_arrival: 240 + } + ] + + @no_service_audio {:canned, {"107", ["861", "21000", "864", "21000", "863"], :audio}} + + setup :verify_on_exit! + describe "run loop" do + setup do + stub(Engine.Config.Mock, :sign_config, fn _ -> :auto end) + stub(Engine.Config.Mock, :headway_config, fn _, _ -> @headway_config end) + stub(Engine.Alerts.Mock, :max_stop_status, fn _, _ -> :none end) + stub(Engine.Predictions.Mock, :for_stop, fn _, _ -> [] end) + :ok + end + test "starts up and logs unknown messages" do assert {:ok, pid} = GenServer.start_link(Signs.Realtime, @sign) @@ -122,44 +120,159 @@ defmodule Signs.RealtimeTest do end test "decrements ticks and doesn't send audio or text when sign is not expired" do - sign = %{ - @sign - | current_content_bottom: %HB{range: {11, 13}, prev_departure_mins: nil} - } - - assert {:noreply, sign} = Signs.Realtime.handle_info(:run_loop, sign) - refute_received({:send_audio, _, _, _, _}) - refute_received({:update_sign, _, _, _, _, _}) + assert {:noreply, sign} = Signs.Realtime.handle_info(:run_loop, @sign) assert sign.tick_read == 0 end - test "expires content on both lines when tick is zero" do - sign = %{ - @sign - | last_update: Timex.shift(Timex.now(), seconds: -200), - current_content_top: %HT{destination: :southbound, vehicle_type: :train}, - current_content_bottom: %HB{range: {11, 13}} - } + test "refreshes content when expired" do + expect_messages({"Southbound trains", "Every 11 to 13 min"}) + sign = %{@sign | last_update: Timex.shift(@fake_time, seconds: -200)} + Signs.Realtime.handle_info(:run_loop, sign) + end - assert {:noreply, sign} = Signs.Realtime.handle_info(:run_loop, sign) + test "announces train passing through station" do + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> + [ + %Predictions.Prediction{ + stop_id: "passthrough_audio", + direction_id: 0, + route_id: "Red", + stopped?: false, + stops_away: 4, + destination_stop_id: "70105", + seconds_until_arrival: nil, + seconds_until_departure: nil, + seconds_until_passthrough: 30, + trip_id: "123" + } + ] + end) + + expect_audios([{:canned, {"103", ["32118"], :audio_visual}}]) + assert {:noreply, sign} = Signs.Realtime.handle_info(:run_loop, @sign) + assert sign.announced_passthroughs == ["123"] + end - assert_received( - {:update_sign, _id, %HT{destination: :southbound, vehicle_type: :train}, - %HB{range: {11, 13}}, _dur, _start, _sign_id} - ) + test "when custom text is present, display it, overriding alerts" do + expect(Engine.Config.Mock, :sign_config, fn _ -> {:static_text, {"custom", "message"}} end) + expect(Engine.Alerts.Mock, :max_stop_status, fn _, _ -> :suspension_closed_station end) + expect_messages({"custom", "message"}) + expect_audios([{:ad_hoc, {"custom message", :audio}}]) + Signs.Realtime.handle_info(:run_loop, @sign) + end - refute_received({:send_audio, _, _, _, _}) + test "when sign is disabled, it's empty" do + expect(Engine.Config.Mock, :sign_config, fn _ -> :off end) + expect_messages({"", ""}) + Signs.Realtime.handle_info(:run_loop, @sign) end - test "announces train passing through station" do - sign = %{ - @sign - | prediction_engine: FakePassthroughPredictions - } + test "when sign is at a transfer station from a shuttle, and there are no predictions it's empty" do + expect(Engine.Alerts.Mock, :max_stop_status, fn _, _ -> :shuttles_transfer_station end) + expect_messages({"", ""}) + Signs.Realtime.handle_info(:run_loop, @sign) + end - assert {:noreply, sign} = Signs.Realtime.handle_info(:run_loop, sign) - assert sign.announced_passthroughs == ["123"] - assert_received({:send_audio, _, [%Content.Audio.Passthrough{}], _, _, _}) + test "when sign is at a transfer station from a suspension, and there are no predictions it's empty" do + expect(Engine.Alerts.Mock, :max_stop_status, fn _, _ -> :suspension_transfer_station end) + expect_messages({"", ""}) + Signs.Realtime.handle_info(:run_loop, @sign) + end + + test "when sign is at a transfer station, and there are no departure predictions it's empty" do + expect(Engine.Alerts.Mock, :max_stop_status, fn _, _ -> :shuttles_transfer_station end) + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> @no_departures_predictions end) + expect_messages({"", ""}) + Signs.Realtime.handle_info(:run_loop, @sign) + end + + test "when sign is at a transfer station, but there are departure predictions it shows them" do + expect(Engine.Alerts.Mock, :max_stop_status, fn _, _ -> :shuttles_transfer_station end) + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> @predictions end) + expect_messages({"Ashmont 2 min", "Ashmont 4 min"}) + Signs.Realtime.handle_info(:run_loop, @sign) + end + + test "when sign is at a station closed by shuttles and there are no departure predictions, it says so" do + expect(Engine.Alerts.Mock, :max_stop_status, fn _, _ -> :shuttles_closed_station end) + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> @no_departures_predictions end) + expect_messages({"No train service", "Use shuttle bus"}) + expect_audios([{:canned, {"199", ["864"], :audio}}]) + Signs.Realtime.handle_info(:run_loop, @sign) + end + + test "when sign is at a station closed and there are no departure predictions, but shuttles do not run at this station" do + expect(Engine.Alerts.Mock, :max_stop_status, fn _, _ -> :shuttles_closed_station end) + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> @no_departures_predictions end) + expect_messages({"No train service", ""}) + expect_audios([@no_service_audio]) + Signs.Realtime.handle_info(:run_loop, %{@sign | uses_shuttles: false}) + end + + test "when sign is at a station closed by shuttles and there are departure predictions, it shows them" do + expect(Engine.Alerts.Mock, :max_stop_status, fn _, _ -> :shuttles_closed_station end) + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> @predictions end) + expect_messages({"Ashmont 2 min", "Ashmont 4 min"}) + Signs.Realtime.handle_info(:run_loop, @sign) + end + + test "when sign is at a station closed due to suspension and there are no departure predictions, it says so" do + expect(Engine.Alerts.Mock, :max_stop_status, fn _, _ -> :suspension_closed_station end) + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> @no_departures_predictions end) + expect_messages({"No train service", ""}) + expect_audios([@no_service_audio]) + Signs.Realtime.handle_info(:run_loop, @sign) + end + + test "when sign is at a closed station and there are no departure predictions, it says so" do + expect(Engine.Alerts.Mock, :max_stop_status, fn _, _ -> :station_closure end) + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> @no_departures_predictions end) + expect_messages({"No train service", ""}) + expect_audios([@no_service_audio]) + Signs.Realtime.handle_info(:run_loop, @sign) + end + + test "when sign is at a station closed due to suspension and there are departure predictions, it shows them" do + expect(Engine.Alerts.Mock, :max_stop_status, fn _, _ -> :suspension_closed_station end) + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> @predictions end) + expect_messages({"Ashmont 2 min", "Ashmont 4 min"}) + Signs.Realtime.handle_info(:run_loop, @sign) + end + + test "when there are predictions, puts predictions on the sign" do + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> @predictions end) + expect_messages({"Ashmont 2 min", "Ashmont 4 min"}) + Signs.Realtime.handle_info(:run_loop, @sign) + end + + test "when there are no predictions and only one source config, puts headways on the sign" do + expect(Engine.Config.Mock, :headway_config, fn _, _ -> + %{@headway_config | range_high: 14} + end) + + expect_messages({"Southbound trains", "Every 11 to 14 min"}) + Signs.Realtime.handle_info(:run_loop, @sign) + end + + test "when sign is forced into headway mode but no alerts are present, displays headways" do + expect(Engine.Config.Mock, :sign_config, fn _ -> :headway end) + + expect(Engine.Config.Mock, :headway_config, fn _, _ -> + %{@headway_config | range_high: 14} + end) + + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> @predictions end) + expect_messages({"Southbound trains", "Every 11 to 14 min"}) + Signs.Realtime.handle_info(:run_loop, @sign) + end + + test "when sign is forced into headway mode but alerts are present, alert takes precedence" do + expect(Engine.Config.Mock, :sign_config, fn _ -> :headway end) + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> @predictions end) + expect(Engine.Alerts.Mock, :max_stop_status, fn _, _ -> :station_closure end) + expect_messages({"No train service", ""}) + expect_audios([@no_service_audio]) + Signs.Realtime.handle_info(:run_loop, @sign) end end @@ -323,4 +436,18 @@ defmodule Signs.RealtimeTest do assert log =~ "in_range=false" end end + + defp expect_messages(messages) do + expect(PaEss.Updater.Mock, :update_sign, fn {"TEST", "x"}, top, bottom, 145, :now, _sign_id -> + assert {Content.Message.to_string(top), Content.Message.to_string(bottom)} == messages + :ok + end) + end + + defp expect_audios(audios) do + expect(PaEss.Updater.Mock, :send_audio, fn {"TEST", ["x"]}, list, 5, 60, _sign_id -> + assert Enum.map(list, &Content.Audio.to_params(&1)) == audios + :ok + end) + end end diff --git a/test/signs/utilities/audio_test.exs b/test/signs/utilities/audio_test.exs index f7b3f18d4..3c90c8ee8 100644 --- a/test/signs/utilities/audio_test.exs +++ b/test/signs/utilities/audio_test.exs @@ -32,6 +32,7 @@ defmodule Signs.Utilities.AudioTest do last_departure_engine: nil, config_engine: Engine.Config, alerts_engine: nil, + current_time_fn: nil, sign_updater: nil, last_update: Timex.now(), tick_audit: 1, diff --git a/test/signs/utilities/headways_test.exs b/test/signs/utilities/headways_test.exs index 5c5352e7b..686dc6771 100644 --- a/test/signs/utilities/headways_test.exs +++ b/test/signs/utilities/headways_test.exs @@ -52,6 +52,7 @@ defmodule Signs.Utilities.HeadwaysTest do last_departure_engine: FakeDepartures, config_engine: Engine.Config, alerts_engine: FakeAlerts, + current_time_fn: nil, sign_updater: FakeUpdater, last_update: Timex.now(), tick_audit: 240, diff --git a/test/signs/utilities/messages_test.exs b/test/signs/utilities/messages_test.exs deleted file mode 100644 index 60e33af54..000000000 --- a/test/signs/utilities/messages_test.exs +++ /dev/null @@ -1,461 +0,0 @@ -defmodule Signs.Utilities.MessagesTest do - use ExUnit.Case, async: true - - alias Signs.Utilities.Messages - - defmodule FakeAlerts do - def max_stop_status(["suspended"], _routes), do: :suspension_closed_station - def max_stop_status(["suspended_transfer"], _routes), do: :suspension_transfer_station - def max_stop_status(["shuttles"], _routes), do: :shuttles_closed_station - def max_stop_status(["closure"], _routes), do: :station_closure - def max_stop_status(_stops, ["Green-B"]), do: :alert_along_route - def max_stop_status(_stops, _routes), do: :none - end - - defmodule FakePredictions do - def for_stop(_stop_id, _direction_id), do: [] - def stopped_at?(_stop_id), do: false - end - - @predictions [ - %Predictions.Prediction{ - stop_id: "1", - direction_id: 0, - route_id: "Red", - stopped?: false, - stops_away: 1, - destination_stop_id: "70093", - seconds_until_arrival: 120, - seconds_until_departure: 180 - }, - %Predictions.Prediction{ - stop_id: "1", - direction_id: 0, - route_id: "Red", - stopped?: false, - stops_away: 1, - destination_stop_id: "70093", - seconds_until_arrival: 240, - seconds_until_departure: 300 - } - ] - - @nd_predictions [ - %Predictions.Prediction{ - stop_id: "1", - direction_id: 0, - route_id: "Red", - stopped?: false, - stops_away: 1, - destination_stop_id: "70093", - seconds_until_arrival: 120 - }, - %Predictions.Prediction{ - stop_id: "1", - direction_id: 0, - route_id: "Red", - stopped?: false, - stops_away: 1, - destination_stop_id: "70093", - seconds_until_arrival: 240 - } - ] - - defmodule FakeDepartures do - @test_departure_time Timex.to_datetime(~N[2019-08-29 15:41:31], "America/New_York") - - def get_last_departure(_stop_id), do: @test_departure_time - - def test_departure_time(), do: @test_departure_time - end - - defmodule FakeHeadways do - def get_headways(_stop_id), do: {1, 4} - def display_headways?(_stop_ids, _time, _buffer), do: true - end - - defmodule FakeConfigEngine do - def headway_config("8-11", _time) do - %Engine.Config.Headway{headway_id: "id", range_low: 8, range_high: 11} - end - end - - @src %Signs.Utilities.SourceConfig{ - stop_id: "1", - direction_id: 0, - platform: nil, - terminal?: false, - announce_arriving?: false, - announce_boarding?: false - } - - @sign %Signs.Realtime{ - id: "sign_id", - text_id: {"TEST", "x"}, - audio_id: {"TEST", ["x"]}, - source_config: %{sources: [@src]}, - current_content_top: %Content.Message.Predictions{destination: :alewife, minutes: 4}, - current_content_bottom: %Content.Message.Predictions{destination: :ashmont, minutes: 3}, - prediction_engine: FakePredictions, - headway_engine: FakeHeadways, - last_departure_engine: FakeDepartures, - config_engine: Engine.Config, - alerts_engine: FakeAlerts, - sign_updater: FakeUpdater, - last_update: Timex.now(), - tick_audit: 240, - tick_read: 1, - read_period_seconds: 100 - } - - describe "get_messages" do - test "when custom text is present, display it, overriding alerts or disabled status" do - sign = @sign - sign_config = {:static_text, {"Test message", "Please ignore"}} - alert_status = :suspension_closed_station - - assert Messages.get_messages(@predictions, sign, sign_config, Timex.now(), alert_status) == - {Content.Message.Custom.new("Test message", :top), - Content.Message.Custom.new("Please ignore", :bottom)} - end - - test "when sign is disabled, it's empty" do - sign = @sign - sign_config = :off - alert_status = :none - - assert Messages.get_messages(@predictions, sign, sign_config, Timex.now(), alert_status) == - {Content.Message.Empty.new(), Content.Message.Empty.new()} - end - - test "when sign is at a transfer station from a shuttle, and there are no predictions it's empty" do - src = %{@src | stop_id: "no_predictions", direction_id: 1} - - sign = %{ - @sign - | source_config: %{sources: [src]}, - current_content_top: Content.Message.Empty.new(), - current_content_bottom: Content.Message.Empty.new() - } - - sign_config = :auto - alert_status = :shuttles_transfer_station - - assert Messages.get_messages(@nd_predictions, sign, sign_config, Timex.now(), alert_status) == - {Content.Message.Empty.new(), Content.Message.Empty.new()} - end - - test "when sign is at a transfer station from a suspension, and there are no predictions it's empty" do - src = %{@src | stop_id: "no_predictions", direction_id: 1} - - sign = %{ - @sign - | source_config: %{sources: [src]}, - current_content_top: {src, Content.Message.Empty.new()}, - current_content_bottom: {src, Content.Message.Empty.new()} - } - - sign_config = :auto - alert_status = :suspension_transfer_station - - assert Messages.get_messages([], sign, sign_config, Timex.now(), alert_status) == - {Content.Message.Empty.new(), Content.Message.Empty.new()} - end - - test "when sign is at a transfer station, and there are no departure predictions it's empty" do - src = %{@src | stop_id: "no_departures", direction_id: 0} - - sign = %{ - @sign - | source_config: %{sources: [src]}, - current_content_top: Content.Message.Empty.new(), - current_content_bottom: Content.Message.Empty.new() - } - - sign_config = :auto - alert_status = :shuttles_transfer_station - - assert Messages.get_messages(@nd_predictions, sign, sign_config, Timex.now(), alert_status) == - {Content.Message.Empty.new(), Content.Message.Empty.new()} - end - - test "when sign is at a transfer station, but there are departure predictions it shows them" do - sign = @sign - sign_config = :auto - alert_status = :shuttles_transfer_station - - assert Messages.get_messages(@predictions, sign, sign_config, Timex.now(), alert_status) == - { - %Content.Message.Predictions{ - destination: :ashmont, - direction_id: 0, - minutes: 2, - route_id: "Red", - stop_id: "1", - width: 18, - station_code: "TEST", - zone: "x" - }, - %Content.Message.Predictions{ - destination: :ashmont, - direction_id: 0, - minutes: 4, - route_id: "Red", - stop_id: "1", - width: 18, - station_code: "TEST", - zone: "x" - } - } - end - - test "when sign is at a station closed by shuttles and there are no departure predictions, it says so" do - src = %{@src | stop_id: "no_departures", direction_id: 0} - - sign = %{ - @sign - | source_config: %{sources: [src]}, - current_content_top: Content.Message.Empty.new(), - current_content_bottom: Content.Message.Empty.new() - } - - sign_config = :auto - alert_status = :shuttles_closed_station - - assert Messages.get_messages(@nd_predictions, sign, sign_config, Timex.now(), alert_status) == - {%Content.Message.Alert.NoService{}, %Content.Message.Alert.UseShuttleBus{}} - end - - test "when sign is at a station closed and there are no departure predictions, but shuttles do not run at this station" do - src = %{@src | stop_id: "no_departures", direction_id: 0} - - sign = %{ - @sign - | source_config: %{sources: [src]}, - current_content_top: Content.Message.Empty.new(), - current_content_bottom: Content.Message.Empty.new(), - uses_shuttles: false - } - - sign_config = :auto - alert_status = :shuttles_closed_station - - assert Messages.get_messages(@nd_predictions, sign, sign_config, Timex.now(), alert_status) == - {%Content.Message.Alert.NoService{}, Content.Message.Empty.new()} - end - - test "when sign is at a station closed by shuttles and there are departure predictions, it shows them" do - src = %{@src | stop_id: "1", direction_id: 0} - - sign = %{ - @sign - | source_config: %{sources: [src]}, - current_content_top: Content.Message.Empty.new(), - current_content_bottom: Content.Message.Empty.new() - } - - sign_config = :auto - alert_status = :shuttles_closed_station - - assert Messages.get_messages(@predictions, sign, sign_config, Timex.now(), alert_status) == - { - %Content.Message.Predictions{ - destination: :ashmont, - direction_id: 0, - minutes: 2, - route_id: "Red", - stop_id: "1", - width: 18, - station_code: "TEST", - zone: "x" - }, - %Content.Message.Predictions{ - destination: :ashmont, - direction_id: 0, - minutes: 4, - route_id: "Red", - stop_id: "1", - width: 18, - station_code: "TEST", - zone: "x" - } - } - end - - test "when sign is at a station closed due to suspension and there are no departure predictions, it says so" do - src = %{@src | stop_id: "no_departures", direction_id: 0} - - sign = %{ - @sign - | source_config: %{sources: [src]}, - current_content_top: Content.Message.Empty.new(), - current_content_bottom: Content.Message.Empty.new() - } - - alert_status = :suspension_closed_station - sign_config = :auto - - assert Messages.get_messages(@nd_predictions, sign, sign_config, Timex.now(), alert_status) == - {%Content.Message.Alert.NoService{}, Content.Message.Empty.new()} - end - - test "when sign is at a closed station and there are no departure predictions, it says so" do - src = %{@src | stop_id: "no_departures", direction_id: 0} - - sign = %{ - @sign - | source_config: %{sources: [src]}, - current_content_top: Content.Message.Empty.new(), - current_content_bottom: Content.Message.Empty.new() - } - - alert_status = :station_closure - sign_config = :auto - - assert Messages.get_messages(@nd_predictions, sign, sign_config, Timex.now(), alert_status) == - {%Content.Message.Alert.NoService{}, Content.Message.Empty.new()} - end - - test "when sign is at a station closed due to suspension and there are departure predictions, it shows them" do - src = %{@src | stop_id: "1", direction_id: 0} - - sign = %{ - @sign - | source_config: %{sources: [src]}, - current_content_top: Content.Message.Empty.new(), - current_content_bottom: Content.Message.Empty.new() - } - - alert_status = :suspension_closed_station - sign_config = :auto - - assert Messages.get_messages(@predictions, sign, sign_config, Timex.now(), alert_status) == - { - %Content.Message.Predictions{ - destination: :ashmont, - direction_id: 0, - minutes: 2, - route_id: "Red", - stop_id: "1", - width: 18, - station_code: "TEST", - zone: "x" - }, - %Content.Message.Predictions{ - destination: :ashmont, - direction_id: 0, - minutes: 4, - route_id: "Red", - stop_id: "1", - width: 18, - station_code: "TEST", - zone: "x" - } - } - end - - test "when there are predictions, puts predictions on the sign" do - sign = @sign - sign_config = :auto - alert_status = :none - - assert Messages.get_messages(@predictions, sign, sign_config, Timex.now(), alert_status) == - { - %Content.Message.Predictions{ - destination: :ashmont, - direction_id: 0, - minutes: 2, - route_id: "Red", - stop_id: "1", - width: 18, - station_code: "TEST", - zone: "x" - }, - %Content.Message.Predictions{ - destination: :ashmont, - direction_id: 0, - minutes: 4, - route_id: "Red", - stop_id: "1", - width: 18, - station_code: "TEST", - zone: "x" - } - } - end - - test "when there are no predictions and only one source config, puts headways on the sign" do - sign = %{ - @sign - | source_config: %{ - headway_group: "8-11", - headway_destination: :mattapan, - sources: [%{@src | stop_id: "no_preds"}] - }, - config_engine: FakeConfigEngine - } - - sign_config = :auto - alert_status = :none - - current_time = Timex.shift(FakeDepartures.test_departure_time(), minutes: 5) - - assert Messages.get_messages([], sign, sign_config, current_time, alert_status) == - { - %Content.Message.Headways.Top{ - destination: :mattapan, - vehicle_type: :train - }, - %Content.Message.Headways.Bottom{ - range: {8, 11}, - prev_departure_mins: nil - } - } - end - - test "when there are no predictions and more than one source config, puts nothing on the sign" do - sign = %{ - @sign - | source_config: %{ - headway_group: "headway_group", - headway_destination: :mattapan, - sources: [%{@src | stop_id: "no_preds"}, %{@src | stop_id: "no_preds"}] - } - } - - sign_config = :auto - alert_status = :none - - assert Messages.get_messages([], sign, sign_config, Timex.now(), alert_status) == - {Content.Message.Empty.new(), Content.Message.Empty.new()} - end - - test "when sign is forced into headway mode but no alerts are present, displays headways" do - sign = %{ - @sign - | config_engine: FakeConfigEngine, - source_config: %{sources: [], headway_group: "8-11", headway_destination: :mattapan} - } - - sign_config = :headway - alert_status = :none - - assert { - %Content.Message.Headways.Top{ - destination: :mattapan, - vehicle_type: :train - }, - %Content.Message.Headways.Bottom{range: {8, 11}} - } = Messages.get_messages([], sign, sign_config, Timex.now(), alert_status) - end - - test "when sign is forced into headway mode but alerts are present, alert takes precedence" do - sign = @sign - sign_config = :headway - alert_status = :station_closure - - assert {%Content.Message.Alert.NoService{}, %Content.Message.Empty{}} = - Messages.get_messages(@predictions, sign, sign_config, Timex.now(), alert_status) - end - end -end diff --git a/test/signs/utilities/predictions_test.exs b/test/signs/utilities/predictions_test.exs index a7892217a..67594d460 100644 --- a/test/signs/utilities/predictions_test.exs +++ b/test/signs/utilities/predictions_test.exs @@ -21,6 +21,7 @@ defmodule Signs.Utilities.PredictionsTest do last_departure_engine: FakeDepartures, config_engine: Engine.Config, alerts_engine: nil, + current_time_fn: nil, sign_updater: FakeUpdater, last_update: Timex.now(), tick_audit: 240, diff --git a/test/signs/utilities/reader_test.exs b/test/signs/utilities/reader_test.exs index 7a89ebf7b..c1e9bb824 100644 --- a/test/signs/utilities/reader_test.exs +++ b/test/signs/utilities/reader_test.exs @@ -39,6 +39,7 @@ defmodule Signs.Utilities.ReaderTest do last_departure_engine: FakeDepartures, config_engine: Engine.Config, alerts_engine: nil, + current_time_fn: nil, sign_updater: FakeUpdater, last_update: Timex.now(), tick_audit: 240, diff --git a/test/signs/utilities/updater_test.exs b/test/signs/utilities/updater_test.exs index 6a702a197..beac5d75d 100644 --- a/test/signs/utilities/updater_test.exs +++ b/test/signs/utilities/updater_test.exs @@ -41,6 +41,7 @@ defmodule Signs.Utilities.UpdaterTest do last_departure_engine: FakeDepartures, config_engine: Engine.Config, alerts_engine: nil, + current_time_fn: nil, sign_updater: FakeUpdater, last_update: Timex.now(), tick_audit: 240, diff --git a/test/test_helper.exs b/test/test_helper.exs index 2f2982e92..d04a6fff7 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -3,3 +3,6 @@ ExUnit.start() Mox.defmock(Engine.NetworkCheck.Mock, for: Engine.NetworkCheck) Mox.defmock(PaEss.Updater.Mock, for: PaEss.Updater) +Mox.defmock(Engine.Config.Mock, for: Engine.ConfigAPI) +Mox.defmock(Engine.Alerts.Mock, for: Engine.AlertsAPI) +Mox.defmock(Engine.Predictions.Mock, for: Engine.PredictionsAPI)