Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Create a data structure to populate the system status widget #2330

Merged
merged 44 commits into from
Feb 5, 2025

Conversation

joshlarson
Copy link
Contributor

@joshlarson joshlarson commented Jan 14, 2025

@joshlarson joshlarson changed the base branch from main to jdl/system-status-alert-retrieval January 14, 2025 19:01
Copy link
Contributor

@anthonyshull anthonyshull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could probably be cleaned up a bit. One suggestion would be to write comments for all of the private functions. I find that it helps add clarity when I have to write narrative descriptions.

@joshlarson joshlarson force-pushed the jdl/system-status-flowchart branch from 9e706d9 to 0a31d30 Compare January 16, 2025 23:24
@joshlarson joshlarson force-pushed the jdl/system-status-alert-retrieval branch from a12cad9 to 42f225d Compare January 17, 2025 21:38
@joshlarson joshlarson force-pushed the jdl/system-status-flowchart branch from 0a31d30 to 58b5500 Compare January 21, 2025 18:08
Base automatically changed from jdl/system-status-alert-retrieval to main January 21, 2025 22:19
@joshlarson joshlarson force-pushed the jdl/system-status-flowchart branch from 58b5500 to c7e9dee Compare January 22, 2025 01:00
@routes ["Blue", "Mattapan", "Orange", "Red"] ++ @green_line_branches

def groups(alerts, time) do
grouped_alerts = Map.new(@routes, &{&1, alerts_for_line(alerts, &1)})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That function should be alerts_for_route if it operates per-route, no?

I also wonder if something in the Alerts.Match module might meet your needs, what if Alerts.Match.match(alerts, %InformedEntity{route: &1})?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That function should be alerts_for_route if it operates per-route, no?

Yep! Done.

I also wonder if something in the Alerts.Match module might meet your needs, what if Alerts.Match.match(alerts, %InformedEntity{route: &1})?

Ugh - I agree, but I've been struggling to actually get Alerts.Match to work, and I'm not sure exactly why. I still think it's worth using the machinery that already exists, but would you mind if I did that as a follow-up instead?

@joshlarson joshlarson marked this pull request as ready for review January 27, 2025 22:17
Copy link
Contributor

@anthonyshull anthonyshull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I don't like the implicit use of nil as a time in order to try to simplify logic. It makes for using functions that purportedly take a time, but don't really. You could utilize module attributes a lot more to hardcode a lot less.

Comment on lines 615 to 631
defp alert(opts) do
route_id = opts |> Keyword.fetch!(:route_id)
effect = opts[:effect] || Faker.Util.pick(@effects)
active_period = opts |> Keyword.fetch!(:active_period)

Alert.build(:alert,
effect: effect,
informed_entity:
InformedEntitySet.build(:informed_entity_set,
route: route_id,
entities: [
InformedEntity.build(:informed_entity, route: route_id)
]
),
active_period: active_period
)
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any code that goes toward building an alert should go in the alerts factory.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I sort of see that, although exactly what belongs in a factory versus just here eludes me at the moment.

The problem is that I don't want to have to copy-pasta the informed_entity / informed_entity_set nesting over and over again, but I'm not sure that it makes sense to pull this into a factory as-is either.

There probably is a way to do it gracefully, but unless we can get that taken care of real fast, I think I'll ask that we tackle it as a follow-up, since this work is blocking the rest of the project.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can see in this factory: https://github.com/mbta/dotcom/pull/2336/files#diff-8fbc124c966696c04346223e24b3cc686bb63d07f0f3a35316e93f8dd30c6384 how I separated _factory functions from other functions.

Comment on lines 10 to 12
@all_rail_lines ["Blue", "Green", "Orange", "Red"]
@green_line_branches ["Green-B", "Green-C", "Green-D", "Green-E"]
@heavy_rail_lines ["Blue", "Orange", "Red"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can export these out of the module.

defmodule Dotcom.SystemStatus.Groups do
  @heavy_rail_lines ~w[Blue Orange Red]
  @all_rail_lines @heavy_rails_lines ++ ~w[Green]
  
  def heavy_rail_lines, do: @heavy_rail_lines
  
  def all_rail_lines, do: @all_rail_lines
end

defmodule Dotcom.SystemStatus.GroupsTest do
  describe "groups/2" do
    test "sorts groups by heavy rail first" do
      # Exercise
      groups = Group.groups([], time_today())
      
      # Verify
      route_ids = groups |> Enum.map(&1.route_id)
      heavy_rail_lines = heavy_rail_lines()
      assert Enum.take(route_ids, Kernel.length(heavy_rail_lines)) == heavy_rail_lines
    end
  end
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that that's possible, but what is the benefit?

My problem is that it makes the tests a bit more tautological - instead of the assertion coming from the tests, the assertion comes from a mix of the source code and the tests, which means that the tests don't validate a user-facing behavior so much as validating that the code is interacting with its own internal constants.

I especially feel that way about this suggestion because the constants you mentioned aren't present in the source file, because I didn't need them. I would be exporting an internal constant that the source file doesn't even need for the sake of the tests, which is the kind of refactor that I've usually considered an antipattern.

I'll think on it for a little longer, but that's my first impression.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't necessarily have to have the heavy rail lines in a module attribute, though I think you did before. If you're getting them from somewhere else then you can get them there in the test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you're just using @lines now. So, you can export that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...though I think you did before.

This is exactly why I don't want to export the module attributes. I did a pretty hefty refactor to Groups.groups/2 that changed which module attributes existed, and that refactor would have been harder if I had been exporting them. They're implementation details, and they change during refactors.

I see that that's possible, but what is the benefit?

Bump on this question, which kind of feels like it makes all the difference.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The benefit is that the test is more communicative.

:suspension -> "Suspension"
end

time = future_start_time(alert.active_period, time)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Does Alerts.Match.any_time_match?/2 do the same thing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. This function (probably worth renaming it, tbh) gives you either :current or {:future, effect_start_time}. I think any_time_match?/2 (and its friend any_period_match?/2) return booleans, so I think at best, we would use any_time_match?/2 to figure out whether to call, effectively, this function.

Open to pairing on whether I've missed something about this, but I'd prefer to punt that to a follow-up if it's not trivially easy to do :)


# @lines is sorted in the order in which `groups/2` should sort its results
@lines ["Blue", "Orange", "Red", "Green"]
@green_line_branches ["Green-B", "Green-C", "Green-D", "Green-E"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: call GreenLine.branch_ids() for the same output! ✨

Copy link
Collaborator

@thecristen thecristen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome. The change I am requesting is just for adding @specs for the public functions. I'd love to see the type definition of what a SystemStatus is!

"Green" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false}]}]
}
"""
def subway_status(alerts, time) do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion(blocking): let's add a @spec here! this also gives us a chance to explicitly describe the shape of the output (like, what are all the possible values of time or status in status_entries?).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohhh that reminds me that perhaps the output of this should be a struct instead of a map, for more explicit type-checking, clarity, and obvious errors if it gets populated wrong?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://hexdocs.pm/elixir/1.17.3/typespecs.html#literals you can get pretty descriptive with describing maps, and having this alone would help clarity a lot! I think a map could be sufficient but a struct would be great too!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call! Will do!

The exact shape of the data is going to change again (which is probably part of why a @spec would be a good idea - to communicate these changes! 😅)

@doc """
Returns a map indicating the subway status for each of the subway lines.
"""
def subway_status() do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: let's add a @spec here too.

# Given `alerts` and `route_id`, filters out only the alerts
# applicable to the given route, using the alert's "informed
# entities".
defp alerts_for_route(route_id, alerts) do
Copy link
Collaborator

@thecristen thecristen Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion(non-blocking): expanding on my earlier suggestion (maybe?), this function body could potentially be replaced with Alerts.Match.match(alerts, %InformedEntity{route: route_id})

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll give this a try again! I'm pretty sure that this is exactly what I tried and couldn't get it to work, but I will give it another go!

@joshlarson
Copy link
Contributor Author

@thecristen follow-up PR to add @spec's:

I could pull that into this PR if you'd like, or keep it separate - I'm good either way.

@joshlarson joshlarson enabled auto-merge (squash) February 5, 2025 01:03
@joshlarson joshlarson disabled auto-merge February 5, 2025 01:04
@joshlarson joshlarson enabled auto-merge (squash) February 5, 2025 01:14
@joshlarson joshlarson dismissed anthonyshull’s stale review February 5, 2025 14:24

Verbal confirmation that the issues were addressed

@joshlarson joshlarson merged commit 25af2d0 into main Feb 5, 2025
17 checks passed
@joshlarson joshlarson deleted the jdl/system-status-flowchart branch February 5, 2025 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants