Packages

phoenix_kit

1.7.185
1.7.211 1.7.210 1.7.209 1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib mix tasks phoenix_kit.doctor.ex
Raw

lib/mix/tasks/phoenix_kit.doctor.ex

defmodule Mix.Tasks.PhoenixKit.Doctor do
@moduledoc """
Diagnoses PhoenixKit installation, migration, and runtime issues.
Runs a comprehensive suite of checks covering database connectivity, pool
configuration, PgBouncer detection, migration state, lock conflicts, and
application configuration. Prints a clear pass/fail report with actionable
remediation steps.
## Usage
$ mix phoenix_kit.doctor
$ mix phoenix_kit.doctor --prefix=auth
## Options
* `--prefix` - Database schema prefix (default: "public")
## Checks Performed
1. **Repo Detection** — Can we find and start the Ecto repo?
2. **DB Connectivity** — Can we execute a simple query?
3. **Pool Configuration** — Pool size, checkout timeout, queue settings
4. **PgBouncer Detection** — Is PgBouncer between app and PostgreSQL?
5. **Migration State** — PhoenixKit version (COMMENT), schema_migrations alignment
6. **Pending Migrations** — Migration files not yet recorded in schema_migrations
7. **UUID Column Types** — Detects varchar uuid columns that crash Ecto on startup
8. **NULL UUIDs in FK Sources** — Detects NULL uuids that cause infinite backfill loops
9. **Orphaned FK References** — Detects orphaned rows that block FK constraint creation
10. **Lock Conflicts** — Any blocked or long-running queries?
11. **Orphaned Connections** — Idle-in-transaction or stuck connections
12. **Oban Configuration** — Queues and plugins that consume pool connections
13. **Supervisor Children** — What's running (update_mode vs full)?
14. **Update Mode** — Is update_mode active?
"""
use Mix.Task
alias PhoenixKit.Migrations.Postgres
@shortdoc "Diagnoses PhoenixKit installation, migration, and runtime issues"
@switches [prefix: :string]
@aliases [p: :prefix]
@impl Mix.Task
def run(argv) do
{opts, _argv, _errors} = OptionParser.parse(argv, switches: @switches, aliases: @aliases)
prefix = opts[:prefix] || "public"
# Start app with minimal footprint (same approach as phoenix_kit.update)
Mix.Task.run("app.config")
cap_repo_pool_size(2)
Application.put_env(:phoenix_kit, :update_mode, true)
Mix.Task.run("app.start")
header("PhoenixKit Doctor")
results = [
run_check("Repo Detection", fn -> check_repo_detection() end),
run_check("DB Connectivity", fn -> check_db_connectivity() end),
run_check("Pool Configuration", fn -> check_pool_config() end),
run_check("PgBouncer Detection", fn -> check_pgbouncer() end),
run_check("Migration State", fn -> check_migration_state(prefix) end),
run_check("Pending Migrations", fn -> check_pending_migrations() end),
run_check("UUID Column Types", fn -> check_uuid_column_types(prefix) end),
run_check("NULL UUIDs in FK Sources", fn -> check_null_uuids(prefix) end),
run_check("Orphaned FK References", fn -> check_orphaned_fk_refs(prefix) end),
run_check("Lock Conflicts", fn -> check_lock_conflicts() end),
run_check("Orphaned Connections", fn -> check_orphaned_connections() end),
run_check("Oban Configuration", fn -> check_oban_config() end),
run_check("PhoenixKit Supervisor", fn -> check_supervisor_state() end),
run_check("Update Mode", fn -> check_update_mode() end)
]
IO.puts("")
summary(results)
end
# ── Check implementations (return {:pass|:warn|:fail, detail}) ──────
defp check_repo_detection do
app = Mix.Project.config()[:app]
repos = Application.get_env(app, :ecto_repos, [])
if repos == [] do
{:fail, "No :ecto_repos configured for :#{app}"}
else
repo = hd(repos)
info =
Enum.join(
[
"app: :#{app}",
"repo: #{inspect(repo)}",
"adapter: #{inspect(repo.__adapter__())}"
],
", "
)
{:pass, info}
end
end
defp check_db_connectivity do
repo = get_repo!()
case repo.query("SELECT 1 AS ok", [], timeout: 5_000) do
{:ok, %{rows: [[1]]}} ->
{:pass, "Connected"}
{:error, %{message: msg}} ->
{:fail, "Query failed: #{msg}"}
{:error, reason} ->
{:fail, "Query failed: #{inspect(reason)}"}
end
end
defp check_pool_config do
app = Mix.Project.config()[:app]
repo = get_repo!()
config = Application.get_env(app, repo, [])
pool_size = config[:pool_size] || 10
queue_target = config[:queue_target] || 50
queue_interval = config[:queue_interval] || 1000
info =
Enum.join(
[
"pool_size: #{pool_size}",
"queue_target: #{queue_target}ms",
"queue_interval: #{queue_interval}ms"
],
", "
)
cond do
pool_size > 20 ->
{:warn, "pool_size=#{pool_size} is high — may saturate PgBouncer. #{info}"}
pool_size < 2 ->
{:warn, "pool_size=#{pool_size} is very low. #{info}"}
true ->
{:pass, info}
end
end
defp check_pgbouncer do
app = Mix.Project.config()[:app]
repo = get_repo!()
config = Application.get_env(app, repo, [])
port =
cond do
config[:port] -> config[:port]
config[:url] -> extract_port_from_url(config[:url])
true -> 5432
end
hostname = config[:hostname] || extract_host_from_url(config[:url]) || "localhost"
if port != 5432 or String.contains?(to_string(hostname), "pgbouncer") do
{:warn,
"Likely PgBouncer (port=#{port}, host=#{hostname}). " <>
"DDL migrations should use @disable_ddl_transaction true"}
else
{:pass, "Direct PostgreSQL (port=#{port}, host=#{hostname})"}
end
end
defp check_migration_state(prefix) do
repo = get_repo!()
escaped_prefix = String.replace(prefix, "'", "\\'")
# Source 1: COMMENT ON TABLE (set by each V*.up migration)
comment_version = get_comment_version(repo, escaped_prefix)
# Source 2: migrated_version_runtime (what phoenix_kit.status uses)
runtime_version =
try do
opts = %{prefix: prefix, escaped_prefix: escaped_prefix}
Postgres.migrated_version_runtime(opts)
rescue
_ -> :error
end
# Source 3: Code's latest version
latest_version = Postgres.current_version()
lines = [
"COMMENT ON TABLE: V#{comment_version}",
"migrated_version_runtime: #{if runtime_version == :error, do: "ERROR", else: "V#{runtime_version}"}",
"Code latest: V#{latest_version}"
]
info = Enum.join(lines, "\n ")
# Detect discrepancies
discrepancy =
runtime_version != :error and runtime_version != comment_version
cond do
discrepancy ->
{:warn,
"DISCREPANCY between version sources!\n #{info}\n " <>
"The COMMENT was updated by a migration that didn't commit to schema_migrations " <>
"(killed process or missing @disable_ddl_transaction true)."}
comment_version == 0 ->
{:warn, "PhoenixKit not installed.\n #{info}"}
comment_version < latest_version ->
{:warn, "Needs migration.\n #{info}"}
comment_version == latest_version ->
{:pass, info}
true ->
{:warn, "DB version > code version.\n #{info}"}
end
end
defp check_pending_migrations do
repo = get_repo!()
migrations_path = Path.join(["priv", "repo", "migrations"])
migration_files =
if File.dir?(migrations_path) do
migrations_path
|> File.ls!()
|> Enum.filter(&String.ends_with?(&1, ".exs"))
|> Enum.map(fn f ->
case Integer.parse(f) do
{version, _rest} -> {version, f}
:error -> nil
end
end)
|> Enum.reject(&is_nil/1)
|> Enum.sort()
else
[]
end
recorded =
case repo.query("SELECT version FROM schema_migrations ORDER BY version", []) do
{:ok, %{rows: rows}} -> Enum.map(rows, fn [v] -> v end) |> MapSet.new()
_ -> MapSet.new()
end
pending =
Enum.reject(migration_files, fn {version, _name} -> MapSet.member?(recorded, version) end)
phoenix_kit_pending =
Enum.filter(pending, fn {_v, name} -> String.contains?(name, "phoenix_kit") end)
# Also check for duplicate PhoenixKit migration files (same version range)
pk_files =
Enum.filter(migration_files, fn {_v, name} -> String.contains?(name, "phoenix_kit") end)
duplicates = find_duplicate_migration_ranges(pk_files)
detail_parts = []
detail_parts =
if pending != [] do
pk_names = Enum.map_join(phoenix_kit_pending, "\n ", fn {_v, n} -> n end)
detail_parts ++
[
"#{length(pending)} pending (#{length(phoenix_kit_pending)} PhoenixKit):\n #{pk_names}"
]
else
detail_parts ++ ["All #{length(migration_files)} files recorded in schema_migrations"]
end
detail_parts =
if duplicates != "" do
detail_parts ++ ["DUPLICATE ranges detected:\n #{duplicates}"]
else
detail_parts
end
detail = Enum.join(detail_parts, "\n ")
cond do
duplicates != "" ->
{:warn, detail}
pending == [] ->
{:pass, detail}
true ->
{:warn, detail}
end
end
defp find_duplicate_migration_ranges(pk_files) do
# Extract version ranges from filenames like "phoenix_kit_update_v49_to_v71.exs"
ranges =
Enum.map(pk_files, fn {_v, name} ->
case Regex.run(~r/phoenix_kit_\w+_v(\d+)_to_v(\d+)/, name) do
[_, from, to] -> {String.to_integer(from), String.to_integer(to), name}
_ -> nil
end
end)
|> Enum.reject(&is_nil/1)
# Find overlapping ranges
overlaps =
for {from1, to1, name1} <- ranges,
{from2, to2, name2} <- ranges,
name1 < name2,
max(from1, from2) < min(to1, to2),
do: "#{name1} overlaps #{name2}"
Enum.join(overlaps, "\n ")
end
# Pre-migration: check for varchar/text uuid columns that should be native uuid type.
# A varchar uuid column on phoenix_kit_settings crashes the Ecto schema loader on startup,
# blocking migrations from even running.
defp check_uuid_column_types(prefix) do
repo = get_repo!()
escaped_prefix = String.replace(prefix, "'", "\\'")
query = """
SELECT table_name, data_type
FROM information_schema.columns
WHERE table_name LIKE 'phoenix_kit_%'
AND column_name = 'uuid'
AND table_schema = '#{escaped_prefix}'
AND data_type IN ('character varying', 'text', 'character')
ORDER BY table_name
"""
case repo.query(query, [], log: false) do
{:ok, %{rows: []}} ->
{:pass, "All uuid columns are native uuid type"}
{:ok, %{rows: rows}} ->
tables =
Enum.map_join(rows, "\n ", fn [table, dtype] ->
"#{table} (#{dtype})"
end)
{:fail,
"#{length(rows)} table(s) have varchar uuid columns (will crash Ecto on load):\n #{tables}\n " <>
"Fix: ALTER TABLE <table> ALTER COLUMN uuid TYPE uuid USING uuid::uuid"}
_ ->
{:warn, "Could not check (phoenix_kit tables may not exist yet)"}
end
end
# Pre-migration: check for NULL uuid values in tables that are FK sources.
# NULL source UUIDs cause the V56 batched backfill loop to run forever.
defp check_null_uuids(prefix) do
repo = get_repo!()
escaped_prefix = String.replace(prefix, "'", "\\'")
# Key FK source tables whose uuid column must not be NULL
source_tables = [
"phoenix_kit_users",
"phoenix_kit_user_roles",
"phoenix_kit_entities",
"phoenix_kit_email_logs",
"phoenix_kit_shop_carts",
"phoenix_kit_shop_products",
"phoenix_kit_shop_categories",
"phoenix_kit_shop_shipping_methods",
"phoenix_kit_payment_options",
"phoenix_kit_billing_profiles",
"phoenix_kit_orders",
"phoenix_kit_invoices",
"phoenix_kit_payment_methods",
"phoenix_kit_subscriptions",
"phoenix_kit_subscription_types",
"phoenix_kit_subscription_plans",
"phoenix_kit_referral_codes",
"phoenix_kit_ai_endpoints",
"phoenix_kit_ai_prompts",
"phoenix_kit_sync_connections"
]
problems =
Enum.reduce(source_tables, [], fn table, acc ->
exists_query = """
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_name = '#{table}'
AND column_name = 'uuid'
AND table_schema = '#{escaped_prefix}'
)
"""
case repo.query(exists_query, [], log: false) do
{:ok, %{rows: [[true]]}} ->
table_name = prefix_table_name(table, prefix)
count_query = "SELECT count(*)::integer FROM #{table_name} WHERE uuid IS NULL"
case repo.query(count_query, [], log: false) do
{:ok, %{rows: [[count]]}} when count > 0 ->
[{table, count} | acc]
_ ->
acc
end
_ ->
acc
end
end)
if problems == [] do
{:pass, "No NULL uuids in FK source tables"}
else
detail =
Enum.map_join(Enum.reverse(problems), "\n ", fn {table, count} ->
"#{table}: #{count} rows with NULL uuid"
end)
{:fail,
"NULL uuids found (will cause infinite loop in V56 backfill):\n #{detail}\n " <>
"Fix: UPDATE <table> SET uuid = gen_random_uuid() WHERE uuid IS NULL"}
end
end
# Pre-migration: check for orphaned FK references (rows pointing to deleted parents).
# Orphaned refs cause V56's add_constraints to fail when adding FK constraints.
defp check_orphaned_fk_refs(prefix) do
repo = get_repo!()
escaped_prefix = String.replace(prefix, "'", "\\'")
# Check the most common orphaned FK pattern: user_uuid → users.uuid
fk_checks = [
{"phoenix_kit_users_tokens", "user_uuid", "phoenix_kit_users", "uuid"},
{"phoenix_kit_user_role_assignments", "user_uuid", "phoenix_kit_users", "uuid"},
{"phoenix_kit_admin_notes", "user_uuid", "phoenix_kit_users", "uuid"},
{"phoenix_kit_email_events", "email_log_uuid", "phoenix_kit_email_logs", "uuid"}
]
problems =
Enum.reduce(fk_checks, [], fn {table, fk_col, ref_table, ref_col}, acc ->
# Check both tables and columns exist
table_name = prefix_table_name(table, prefix)
ref_name = prefix_table_name(ref_table, prefix)
check_query = """
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_name = '#{table}' AND column_name = '#{fk_col}' AND table_schema = '#{escaped_prefix}'
) AND EXISTS (
SELECT FROM information_schema.columns
WHERE table_name = '#{ref_table}' AND column_name = '#{ref_col}' AND table_schema = '#{escaped_prefix}'
)
"""
case repo.query(check_query, [], log: false) do
{:ok, %{rows: [[true]]}} ->
orphan_query = """
SELECT count(*)::integer FROM #{table_name} t
WHERE t.#{fk_col} IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM #{ref_name} r WHERE r.#{ref_col} = t.#{fk_col})
"""
case repo.query(orphan_query, [], log: false) do
{:ok, %{rows: [[count]]}} when count > 0 ->
[{table, fk_col, ref_table, count} | acc]
_ ->
acc
end
_ ->
acc
end
end)
if problems == [] do
{:pass, "No orphaned FK references found"}
else
detail =
Enum.map_join(Enum.reverse(problems), "\n ", fn {table, fk_col, ref, count} ->
"#{table}.#{fk_col}#{ref}: #{count} orphaned rows"
end)
{:fail,
"Orphaned FK refs found (will block FK constraint creation):\n #{detail}\n " <>
"Fix: DELETE FROM <table> t WHERE NOT EXISTS (SELECT 1 FROM <ref> r WHERE r.uuid = t.<fk_col>)"}
end
end
defp check_lock_conflicts do
repo = get_repo!()
query = """
SELECT count(*) FROM pg_stat_activity
WHERE datname = current_database()
AND pid != pg_backend_pid()
AND wait_event_type = 'Lock'
"""
case repo.query(query, []) do
{:ok, %{rows: [[0]]}} ->
{:pass, "No lock conflicts"}
{:ok, %{rows: [[count]]}} ->
detail_query = """
SELECT pid, age(now(), query_start)::text, left(query, 80)
FROM pg_stat_activity
WHERE datname = current_database()
AND pid != pg_backend_pid()
AND wait_event_type = 'Lock'
ORDER BY query_start LIMIT 5
"""
details =
case repo.query(detail_query, []) do
{:ok, %{rows: rows}} ->
Enum.map_join(rows, "\n ", fn [pid, dur, q] ->
"PID #{pid} (#{dur}): #{q}"
end)
_ ->
"Could not fetch details"
end
{:fail, "#{count} queries waiting on locks:\n #{details}"}
_ ->
{:warn, "Could not check (may not have pg_stat_activity access)"}
end
end
defp check_orphaned_connections do
repo = get_repo!()
query = """
SELECT state, count(*)::integer, max(age(now(), state_change))::text
FROM pg_stat_activity
WHERE datname = current_database()
AND pid != pg_backend_pid()
GROUP BY state ORDER BY state
"""
case repo.query(query, []) do
{:ok, %{rows: rows}} ->
info =
Enum.map_join(rows, ", ", fn [state, count, oldest] ->
"#{state || "null"}: #{count} (oldest: #{oldest})"
end)
idle_in_tx =
Enum.find(rows, fn [state, _, _] ->
state in ["idle in transaction", "idle in transaction (aborted)"]
end)
if idle_in_tx do
[_state, count, oldest] = idle_in_tx
{:fail,
"#{count} idle-in-transaction (oldest: #{oldest}). " <>
"These block DDL. Kill: SELECT pg_terminate_backend(pid) ... All: #{info}"}
else
{:pass, info}
end
_ ->
{:warn, "Could not query pg_stat_activity"}
end
end
defp check_oban_config do
app = Mix.Project.config()[:app]
case Application.get_env(app, Oban) do
nil ->
{:pass, "Oban not configured"}
config ->
queues = Keyword.get(config, :queues, [])
plugins = Keyword.get(config, :plugins, [])
{:pass,
"#{length(queues)} queues, #{length(plugins)} plugins. Each active queue uses 1 pool connection."}
end
end
defp check_supervisor_state do
case Process.whereis(PhoenixKit.Supervisor) do
nil ->
{:warn, "PhoenixKit.Supervisor not running"}
pid ->
children = Supervisor.which_children(pid)
names = Enum.map(children, fn {id, _, _, _} -> id end)
{:pass, "#{length(children)} children: #{inspect(names)}"}
end
end
defp check_update_mode do
update_mode = Application.get_env(:phoenix_kit, :update_mode, false)
if update_mode do
{:warn, "update_mode=true (doctor runs in update_mode to minimize DB connections)"}
else
{:pass, "update_mode=false (normal operation)"}
end
end
# ── Helpers ──────────────────────────────────────────────────────────
defp get_repo! do
app = Mix.Project.config()[:app]
case Application.get_env(app, :ecto_repos, []) do
[repo | _] -> repo
[] -> raise "No :ecto_repos configured for :#{app}"
end
end
defp cap_repo_pool_size(pool_size) do
app = Mix.Project.config()[:app]
repos = Application.get_env(app, :ecto_repos, [])
Enum.each(repos, fn repo ->
current = Application.get_env(app, repo, [])
updated = Keyword.put(current, :pool_size, pool_size)
Application.put_env(app, repo, updated)
end)
# Disable Oban queues to save connections
case Application.get_env(app, Oban) do
nil ->
:ok
config ->
updated = config |> Keyword.put(:queues, []) |> Keyword.put(:plugins, [])
Application.put_env(app, Oban, updated)
end
rescue
_ -> :ok
end
defp get_comment_version(repo, escaped_prefix) do
table_query = """
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'phoenix_kit' AND table_schema = '#{escaped_prefix}'
)
"""
case repo.query(table_query, [], log: false) do
{:ok, %{rows: [[true]]}} ->
version_query = """
SELECT pg_catalog.obj_description(pg_class.oid, 'pg_class')
FROM pg_class
LEFT JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace
WHERE pg_class.relname = 'phoenix_kit'
AND pg_namespace.nspname = '#{escaped_prefix}'
"""
case repo.query(version_query, [], log: false) do
{:ok, %{rows: [[version]]}} when is_binary(version) -> String.to_integer(version)
_ -> 0
end
_ ->
0
end
end
defp prefix_table_name(table_name, "public"), do: "public.#{table_name}"
defp prefix_table_name(table_name, prefix), do: "#{prefix}.#{table_name}"
defp extract_port_from_url(nil), do: nil
defp extract_port_from_url(url) when is_binary(url) do
case URI.parse(url) do
%URI{port: port} when is_integer(port) -> port
_ -> nil
end
end
defp extract_port_from_url(_), do: nil
defp extract_host_from_url(nil), do: nil
defp extract_host_from_url(url) when is_binary(url) do
case URI.parse(url) do
%URI{host: host} when is_binary(host) -> host
_ -> nil
end
end
defp extract_host_from_url(_), do: nil
# ── Display ─────────────────────────────────────────────────────────
defp header(title) do
IO.puts("\n#{IO.ANSI.bright()}#{IO.ANSI.cyan()}#{title}#{IO.ANSI.reset()}")
IO.puts(String.duplicate("─", 60))
end
defp run_check(name, fun) do
result =
try do
fun.()
rescue
e -> {:fail, "Exception: #{Exception.message(e)}"}
end
display_check(name, result)
{name, result}
end
defp display_check(name, {:pass, detail}) do
IO.puts(" #{IO.ANSI.green()}PASS#{IO.ANSI.reset()} #{name}")
if detail, do: IO.puts(" #{IO.ANSI.faint()}#{detail}#{IO.ANSI.reset()}")
end
defp display_check(name, {:warn, detail}) do
IO.puts(" #{IO.ANSI.yellow()}WARN#{IO.ANSI.reset()} #{name}")
if detail, do: IO.puts(" #{IO.ANSI.yellow()}#{detail}#{IO.ANSI.reset()}")
end
defp display_check(name, {:fail, detail}) do
IO.puts(" #{IO.ANSI.red()}FAIL#{IO.ANSI.reset()} #{name}")
if detail, do: IO.puts(" #{IO.ANSI.red()}#{detail}#{IO.ANSI.reset()}")
end
defp summary(results) do
pass = Enum.count(results, fn {_, {status, _}} -> status == :pass end)
warn = Enum.count(results, fn {_, {status, _}} -> status == :warn end)
fail = Enum.count(results, fn {_, {status, _}} -> status == :fail end)
total = length(results)
IO.puts(
"#{IO.ANSI.bright()}Summary#{IO.ANSI.reset()}: #{pass}/#{total} passed, #{warn} warnings, #{fail} failures"
)
if fail > 0 do
IO.puts(
"#{IO.ANSI.red()}Fix the FAIL items above before running migrations.#{IO.ANSI.reset()}"
)
end
end
end