diff --git a/config/test.exs b/config/test.exs index 0232e9d1d..21d6a15c9 100644 --- a/config/test.exs +++ b/config/test.exs @@ -162,3 +162,5 @@ config :screens, :screens_by_alert, config :screens, Screens.V2.ScreenData, config_cache_module: Screens.Config.MockCache, parameters_module: Screens.V2.ScreenData.MockParameters + +config :screens, Screens.V2.CandidateGenerator.DupNew, stop_module: Screens.Stops.MockStop diff --git a/lib/screens/stops/stop.ex b/lib/screens/stops/stop.ex index 55cd9e7bf..4fa64c33b 100644 --- a/lib/screens/stops/stop.ex +++ b/lib/screens/stops/stop.ex @@ -270,6 +270,7 @@ defmodule Screens.Stops.Stop do end end + @callback fetch_stop_name(id()) :: String.t() | nil def fetch_stop_name(stop_id) do Screens.Telemetry.span(~w[screens stops stop fetch_stop_name]a, %{stop_id: stop_id}, fn -> case Screens.V3Api.get_json("stops", %{"filter[id]" => stop_id}) do diff --git a/lib/screens/telemetry.ex b/lib/screens/telemetry.ex index 6bfc181e2..1a5e9148f 100644 --- a/lib/screens/telemetry.ex +++ b/lib/screens/telemetry.ex @@ -24,12 +24,18 @@ defmodule Screens.Telemetry do metadata: ~w[stop_id type_filters]a ), # DUP Candidate Generator + log_span(~w[screens v2 candidate_generator dup]a), log_span(~w[screens v2 candidate_generator dup departures_instances]a), log_span(~w[screens v2 candidate_generator dup departures get_section_data]a), log_span(~w[screens v2 candidate_generator dup departures get_sections_data]a), log_span(~w[screens v2 candidate_generator dup header_instances]a), log_span(~w[screens v2 candidate_generator dup alerts_instances]a), log_span(~w[screens v2 candidate_generator dup evergreen_content_instances]a), + # New DUP Candidate Generator + log_span(~w[screens v2 candidate_generator dup_new]a), + log_span(~w[screens v2 candidate_generator dup_new departures_instances]a), + log_span(~w[screens v2 candidate_generator dup_new evergreen_instances]a), + log_span(~w[screens v2 candidate_generator dup_new header_instances]a), # events log_event(~w[hackney_pool]a, diff --git a/lib/screens/v2/candidate_generator/dup_new.ex b/lib/screens/v2/candidate_generator/dup_new.ex index a2cc9d898..5bd55bf25 100644 --- a/lib/screens/v2/candidate_generator/dup_new.ex +++ b/lib/screens/v2/candidate_generator/dup_new.ex @@ -1,24 +1,35 @@ defmodule Screens.V2.CandidateGenerator.DupNew do @moduledoc false + alias Screens.Telemetry alias Screens.V2.CandidateGenerator - alias Screens.V2.CandidateGenerator.Dup, as: DupBase - alias Screens.V2.WidgetInstance.Placeholder + alias Screens.V2.CandidateGenerator.Widgets.Evergreen + alias __MODULE__.{Departures, Header} @behaviour CandidateGenerator + @telemetry_name ~w[screens v2 candidate_generator dup_new]a + @instance_generators [ + header_instances: &Header.instances/2, + departures_instances: &Departures.instances/2, + evergreen_instances: &Evergreen.evergreen_content_instances/2 + ] + |> Enum.map(fn {name, func} -> {@telemetry_name ++ [name], func} end) + @impl CandidateGenerator - defdelegate screen_template(), to: DupBase + defdelegate screen_template(), to: Screens.V2.CandidateGenerator.Dup @impl CandidateGenerator - def candidate_instances(_config) do - List.duplicate( - %Placeholder{ - color: :gray, - slot_names: [:full_rotation_zero, :full_rotation_one, :full_rotation_two] - }, - 3 - ) + def candidate_instances(config, now \\ DateTime.utc_now()) do + Telemetry.span(@telemetry_name, fn -> + context = Telemetry.context() + + @instance_generators + |> Task.async_stream(fn {name, func} -> + Telemetry.span(name, context, fn -> func.(config, now) end) + end) + |> Enum.flat_map(fn {:ok, instances} -> instances end) + end) end @impl CandidateGenerator diff --git a/lib/screens/v2/candidate_generator/dup_new/departures.ex b/lib/screens/v2/candidate_generator/dup_new/departures.ex new file mode 100644 index 000000000..a71b4bd55 --- /dev/null +++ b/lib/screens/v2/candidate_generator/dup_new/departures.ex @@ -0,0 +1,22 @@ +defmodule Screens.V2.CandidateGenerator.DupNew.Departures do + @moduledoc false + + alias Screens.V2.WidgetInstance.Departures, as: DeparturesWidget + alias Screens.V2.WidgetInstance.{DeparturesNoData, OvernightDepartures} + alias ScreensConfig.Screen + + @type widget :: DeparturesNoData.t() | DeparturesWidget.t() | OvernightDepartures.t() + + @spec instances(Screen.t(), DateTime.t()) :: [widget()] + def instances(config, _now) do + ~w[ + main_content_zero + main_content_one + main_content_two + main_content_reduced_zero + main_content_reduced_one + main_content_reduced_two + ]a + |> Enum.map(&%DeparturesNoData{screen: config, slot_name: &1}) + end +end diff --git a/lib/screens/v2/candidate_generator/dup_new/header.ex b/lib/screens/v2/candidate_generator/dup_new/header.ex new file mode 100644 index 000000000..f1cf79cbb --- /dev/null +++ b/lib/screens/v2/candidate_generator/dup_new/header.ex @@ -0,0 +1,24 @@ +defmodule Screens.V2.CandidateGenerator.DupNew.Header do + @moduledoc false + + alias Screens.V2.WidgetInstance.NormalHeader + alias ScreensConfig.Screen + alias ScreensConfig.V2.Dup + alias ScreensConfig.V2.Header.{CurrentStopId, CurrentStopName} + + @stop Application.compile_env( + :screens, + [Screens.V2.CandidateGenerator.DupNew, :stop_module], + Screens.Stops.Stop + ) + + @spec instances(Screen.t(), DateTime.t()) :: [NormalHeader.t()] + def instances(%Screen{app_params: %Dup{header: header_config}} = config, now) do + # Generate one header for each of the 3 rotations. + %NormalHeader{screen: config, icon: :logo, text: stop_name(header_config), time: now} + |> List.duplicate(3) + end + + defp stop_name(%CurrentStopName{stop_name: name}), do: name + defp stop_name(%CurrentStopId{stop_id: stop_id}), do: @stop.fetch_stop_name(stop_id) +end diff --git a/test/screens/v2/candidate_generator/dup_new_test.exs b/test/screens/v2/candidate_generator/dup_new_test.exs new file mode 100644 index 000000000..1d75cb9d3 --- /dev/null +++ b/test/screens/v2/candidate_generator/dup_new_test.exs @@ -0,0 +1,80 @@ +defmodule Screens.V2.CandidateGenerator.DupNewTest do + use ExUnit.Case, async: true + + alias ScreensConfig.Screen + alias ScreensConfig.V2.{Alerts, Departures, EvergreenContentItem, Header, Schedule} + alias ScreensConfig.V2.Dup, as: DupConfig + alias Screens.Stops.MockStop + alias Screens.Util.Assets + alias Screens.V2.CandidateGenerator.DupNew + alias Screens.V2.WidgetInstance.{DeparturesNoData, EvergreenContent, NormalHeader} + + import Mox + setup :verify_on_exit! + + describe "candidate_instances/2" do + @config %Screen{ + app_id: :dup_v2, + app_params: %DupConfig{ + alerts: %Alerts{stop_id: "place-abcde"}, + header: %Header.CurrentStopName{stop_name: "Test Stop"}, + primary_departures: %Departures{sections: []}, + secondary_departures: %Departures{sections: []} + }, + vendor: :outfront, + device_id: "TEST", + name: "TEST" + } + @now ~U[2024-01-15 11:45:30Z] + + test "returns expected header instances" do + expected_header = %NormalHeader{screen: @config, icon: :logo, text: "Test Stop", time: @now} + + instances = DupNew.candidate_instances(@config, @now) + + assert Enum.filter(instances, &is_struct(&1, NormalHeader)) == + List.duplicate(expected_header, 3) + end + + test "returns header with stop name determined from stop ID" do + config = put_in(@config.app_params.header, %Header.CurrentStopId{stop_id: "test_id"}) + expect(MockStop, :fetch_stop_name, fn "test_id" -> "Test Name" end) + + instances = DupNew.candidate_instances(config, @now) + + assert %NormalHeader{text: "Test Name"} = Enum.find(instances, &is_struct(&1, NormalHeader)) + end + + test "returns evergreen content when scheduled" do + schedule = %Schedule{start_dt: ~U[2024-01-01 00:00:00Z], end_dt: ~U[2024-02-01 00:00:00Z]} + + item = %EvergreenContentItem{ + asset_path: "test.png", + priority: [1], + schedule: [schedule], + slot_names: ["bottom_pane_zero"] + } + + config = put_in(@config.app_params.evergreen_content, [item]) + now_active = ~U[2024-01-10 00:00:00Z] + now_inactive = ~U[2024-02-02 00:00:00Z] + + expected_instance = %EvergreenContent{ + screen: config, + asset_url: Assets.s3_asset_url("test.png"), + now: now_active, + priority: [1], + schedule: [schedule], + slot_names: [:bottom_pane_zero] + } + + assert expected_instance in DupNew.candidate_instances(config, now_active) + assert expected_instance not in DupNew.candidate_instances(config, now_inactive) + end + + test "stub: always returns no-data state for departures" do + expected_instance = %DeparturesNoData{screen: @config, slot_name: :main_content_zero} + assert expected_instance in DupNew.candidate_instances(@config, @now) + end + end +end diff --git a/test/support/mocks.ex b/test/support/mocks.ex index 94bcf1472..145ae0ebd 100644 --- a/test/support/mocks.ex +++ b/test/support/mocks.ex @@ -1,2 +1,3 @@ Mox.defmock(Screens.Config.MockCache, for: Screens.Config.Cache) +Mox.defmock(Screens.Stops.MockStop, for: Screens.Stops.Stop) Mox.defmock(Screens.V2.ScreenData.MockParameters, for: Screens.V2.ScreenData.Parameters)