From a9a02aa9ba04e0e6eecc8d41f272400aa1ba7741 Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Mon, 26 Oct 2026 18:38:13 +0000 Subject: [PATCH] app: hand-write the User Inspect impl so coverage can instrument it `redact: true` makes Ecto derive an Inspect impl at the `schema` line. Elixir 1.20.4 emits that whole derived impl on that one line, and OTP 27's coverage instrumentation (sys_coverage) turns it into invalid Erlang ("variable '_2' unsafe in 'case'"), so `mix test --cover` aborted for the whole app before reporting anything. Minimal repro, no Ecto needed: `@derive Inspect` on a plain struct fails on Elixir 1.20.4 + OTP 27.3.4.19 and works on Elixir 1.18.3 + the same OTP. Writing the impl out by hand, one expression per line, keeps `inspect/1` output byte-identical (Ecto/Elixir render redacted fields as `#Mod<..., ...>`) and keeps redaction pinned by the existing test, and lets the coverage sensor land. Revisit when the Elixir/OTP pins change. --- app/lib/firehose/accounts/user.ex | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/app/lib/firehose/accounts/user.ex b/app/lib/firehose/accounts/user.ex index 458f208..b28d144 100644 --- a/app/lib/firehose/accounts/user.ex +++ b/app/lib/firehose/accounts/user.ex @@ -2,6 +2,16 @@ defmodule Firehose.Accounts.User do use Ecto.Schema import Ecto.Changeset + # `redact: true` below makes Ecto derive an `Inspect` impl for us. We write + # that impl out by hand instead (see the `defimpl` after the schema): + # Elixir 1.20.4 emits the derived code entirely on the `schema` line, and + # OTP 27's coverage instrumentation turns that single line into invalid + # Erlang ("variable '_2' unsafe in 'case'"), which aborts `mix test --cover` + # for the whole app. Output and redaction are identical, and the redaction is + # pinned by test/firehose/accounts_test.exs. Drop this and go back to the + # derive when the Elixir/OTP pins no longer reproduce the crash. + @derive_inspect_for_redacted_fields false + schema "users" do field :email, :string field :password, :string, virtual: true, redact: true @@ -12,6 +22,27 @@ defmodule Firehose.Accounts.User do timestamps(type: :utc_datetime) end + defimpl Inspect do + alias Firehose.Accounts.User + + # Same set as the `redact: true` field options above; keep them in sync. + @redacted_fields [:password, :hashed_password] + + def inspect(%User{} = user, opts) do + infos = + for %{field: field} = info <- User.__info__(:struct), + field not in @redacted_fields, + do: info + + Inspect.Any.inspect_as_struct( + user, + Macro.inspect_atom(:literal, User), + infos, + opts + ) + end + end + @doc """ A user changeset for registering or changing the email.