Skip to content

Commit c2ceeb9

Browse files
committed
Improve application error formatting
* Format (nested) supervision tree init failures * Format :gen_server exit reasons * Fixes :invalid_options text * Fixes text when when application start returns {:error, reason}
1 parent b0a5197 commit c2ceeb9

File tree

4 files changed

+262
-29
lines changed

4 files changed

+262
-29
lines changed

lib/elixir/lib/application.ex

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,8 @@ defmodule Application do
334334

335335
# {:error, reason} return value
336336
defp impl_format_reason({reason, {mod, :start, args}}) do
337-
Exception.format_exit({reason, {mod, :start, args}})
337+
Exception.format_mfa(mod, :start, args) <> " returned an error: " <>
338+
Exception.format_exit(reason)
338339
end
339340

340341
# error or exit(reason) call, use exit reason as reason.
@@ -344,8 +345,8 @@ defmodule Application do
344345

345346
# bad return value
346347
defp impl_format_reason({:bad_return, {{mod, :start, args}, return}}) do
347-
Exception.format_mfa(mod, :start, args) <> " had bad return: " <>
348-
inspect(return)
348+
Exception.format_mfa(mod, :start, args) <>
349+
" returned a bad value: " <> inspect(return)
349350
end
350351

351352
defp impl_format_reason({:already_started, app}) when is_atom(app) do
@@ -377,7 +378,7 @@ defmodule Application do
377378
end
378379

379380
defp impl_format_reason({:invalid_options, opts}) do
380-
"invalid application name: #{inspect(opts)}"
381+
"invalid application options: #{inspect(opts)}"
381382
end
382383

383384
defp impl_format_reason({:badstartspec, spec}) do

lib/elixir/lib/exception.ex

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,8 +367,24 @@ defmodule Exception do
367367
end
368368
end
369369

370-
# 2-Tuple could be an exit caused by mfa if second element is mfa,
371-
defp format_exit({reason2, {mod, fun, args}} = reason, joiner) do
370+
# :supervisor.start_link returns this error reason when it fails to init
371+
# because a child's start_link raises.
372+
defp format_exit({:shutdown,
373+
{:failed_to_start_child, child, {:EXIT, reason}}}, joiner) do
374+
format_start_child(child, reason, joiner)
375+
end
376+
377+
# :supervisor.start_link returns this error reason when it fails to init
378+
# because a child's start_link returns {:error, reason}.
379+
defp format_exit({:shutdown, {:failed_to_start_child, child, reason}},
380+
joiner) do
381+
format_start_child(child, reason, joiner)
382+
end
383+
384+
# 2-Tuple could be an exit caused by mfa if second element is mfa, args
385+
# must be a list of arguments - max length 255 due to max arity.
386+
defp format_exit({reason2, {mod, fun, args}} = reason, joiner)
387+
when length(args) < 256 do
372388
try do
373389
format_mfa(mod, fun, args)
374390
else
@@ -407,8 +423,99 @@ defmodule Exception do
407423
"no connection to #{node_name}"
408424
end
409425

426+
# :gen_server exit reasons
427+
428+
defp format_exit_reason({:already_started, pid}) do
429+
"already started: " <> inspect(pid)
430+
end
431+
432+
defp format_exit_reason({:bad_return_value, value}) do
433+
"bad return value: " <> inspect(value)
434+
end
435+
436+
defp format_exit_reason({:bad_call, request}) do
437+
"bad call: " <> inspect(request)
438+
end
439+
440+
defp format_exit_reason({:bad_cast, request}) do
441+
"bad cast: " <> inspect(request)
442+
end
443+
444+
# :supervisor.start_link error reasons
445+
446+
# If value is a list will be be formatted by mfa exit in format_exit/1
447+
defp format_exit_reason({:bad_return, {mod, :init, value}})
448+
when is_atom(mod) do
449+
format_mfa(mod, :init, 1) <> " returned a bad value: " <> inspect(value)
450+
end
451+
452+
defp format_exit_reason({:bad_start_spec, start_spec}) do
453+
"bad start spec: invalid children: " <> inspect(start_spec)
454+
end
455+
456+
defp format_exit_reason({:start_spec, start_spec}) do
457+
"bad start spec: " <> format_sup_spec(start_spec)
458+
end
459+
460+
defp format_exit_reason({:supervisor_data, data}) do
461+
"bad supervisor data: " <> format_sup_data(data)
462+
end
463+
410464
defp format_exit_reason(reason), do: inspect(reason)
411465

466+
defp format_start_child(child, reason, joiner) do
467+
"shutdown: failed to start child: " <> inspect(child) <> joiner <>
468+
"** (EXIT) " <> format_exit(reason, joiner <> <<" ">>)
469+
end
470+
471+
defp format_sup_data({:invalid_type, type}) do
472+
"invalid type: " <> inspect(type)
473+
end
474+
475+
defp format_sup_data({:invalid_strategy, strategy}) do
476+
"invalid strategy: " <> inspect(strategy)
477+
end
478+
479+
defp format_sup_data({:invalid_intensity, intensity}) do
480+
"invalid intensity: " <> inspect(intensity)
481+
end
482+
483+
defp format_sup_data({:invalid_period, period}) do
484+
"invalid period: " <> inspect(period)
485+
end
486+
487+
defp format_sup_data(other), do: inspect(other)
488+
489+
defp format_sup_spec({:invalid_child_spec, child_spec}) do
490+
"invalid child spec: " <> inspect(child_spec)
491+
end
492+
493+
defp format_sup_spec({:invalid_child_type, type}) do
494+
"invalid child type: " <> inspect(type)
495+
end
496+
497+
defp format_sup_spec({:invalid_mfa, mfa}) do
498+
"invalid mfa: " <> inspect(mfa)
499+
end
500+
501+
defp format_sup_spec({:invalid_restart_type, restart}) do
502+
"invalid restart type: " <> inspect(restart)
503+
end
504+
505+
defp format_sup_spec({:invalid_shutdown, shutdown}) do
506+
"invalid shutdown: " <> inspect(shutdown)
507+
end
508+
509+
defp format_sup_spec({:invalid_module, mod}) do
510+
"invalid module: " <> inspect(mod)
511+
end
512+
513+
defp format_sup_spec({:invalid_modules, modules}) do
514+
"invalid modules: " <> inspect(modules)
515+
end
516+
517+
defp format_sup_spec(other), do: inspect(other)
518+
412519
@doc """
413520
Receives a stacktrace entry and formats it into a string.
414521
"""

lib/elixir/test/elixir/exception_test.exs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,106 @@ defmodule Kernel.ExceptionTest do
7474
assert Exception.format_exit({:shutdown, :bye}) == "shutdown: :bye"
7575
assert Exception.format_exit({:badarg,[{:not_a_real_module, :function, 0, []}]}) ==
7676
"an exception was raised:\n ** (ArgumentError) argument error\n :not_a_real_module.function/0"
77+
assert Exception.format_exit({:bad_call, :request}) == "bad call: :request"
78+
assert Exception.format_exit({:bad_cast, :request}) == "bad cast: :request"
79+
assert Exception.format_exit({:start_spec, :unexpected}) ==
80+
"bad start spec: :unexpected"
81+
assert Exception.format_exit({:supervisor_data, :unexpected}) ==
82+
"bad supervisor data: :unexpected"
83+
end
84+
85+
defmodule Sup do
86+
def start_link(fun), do: :supervisor.start_link(__MODULE__, fun)
87+
88+
def init(fun), do: fun.()
89+
end
90+
91+
test "format_exit with supervisor errors" do
92+
trap = Process.flag(:trap_exit, true)
93+
94+
{:error, reason} = __MODULE__.Sup.start_link(fn() -> :foo end)
95+
assert Exception.format_exit(reason) ==
96+
"#{inspect(__MODULE__.Sup)}.init/1 returned a bad value: :foo"
97+
98+
return = {:ok, {:foo, []}}
99+
{:error, reason} = __MODULE__.Sup.start_link(fn() -> return end)
100+
assert Exception.format_exit(reason) ==
101+
"bad supervisor data: invalid type: :foo"
102+
103+
return = {:ok, {{:foo, 1, 1}, []}}
104+
{:error, reason} = __MODULE__.Sup.start_link(fn() -> return end)
105+
assert Exception.format_exit(reason) ==
106+
"bad supervisor data: invalid strategy: :foo"
107+
108+
return = {:ok, {{:one_for_one, :foo, 1}, []}}
109+
{:error, reason} = __MODULE__.Sup.start_link(fn() -> return end)
110+
assert Exception.format_exit(reason) ==
111+
"bad supervisor data: invalid intensity: :foo"
112+
113+
return = {:ok, {{:one_for_one, 1, :foo}, []}}
114+
{:error, reason} = __MODULE__.Sup.start_link(fn() -> return end)
115+
assert Exception.format_exit(reason) ==
116+
"bad supervisor data: invalid period: :foo"
117+
118+
return = {:ok, {{:simple_one_for_one, 1, 1}, :foo}}
119+
{:error, reason} = __MODULE__.Sup.start_link(fn() -> return end)
120+
assert Exception.format_exit(reason) ==
121+
"bad start spec: invalid children: :foo"
122+
123+
return = {:ok, {{:one_for_one, 1, 1}, [:foo]}}
124+
{:error, reason} = __MODULE__.Sup.start_link(fn() -> return end)
125+
assert Exception.format_exit(reason) ==
126+
"bad start spec: invalid child spec: :foo"
127+
128+
return = {:ok, {{:one_for_one, 1, 1},
129+
[{:child, :foo, :temporary, 1, :worker, []}]}}
130+
{:error, reason} = __MODULE__.Sup.start_link(fn() -> return end)
131+
assert Exception.format_exit(reason) ==
132+
"bad start spec: invalid mfa: :foo"
133+
134+
return = {:ok, {{:one_for_one, 1, 1},
135+
[{:child, {:m, :f, []}, :foo, 1, :worker, []}]}}
136+
{:error, reason} = __MODULE__.Sup.start_link(fn() -> return end)
137+
assert Exception.format_exit(reason) ==
138+
"bad start spec: invalid restart type: :foo"
139+
140+
return = {:ok, {{:one_for_one, 1, 1},
141+
[{:child, {:m, :f, []}, :temporary, :foo, :worker, []}]}}
142+
{:error, reason} = __MODULE__.Sup.start_link(fn() -> return end)
143+
assert Exception.format_exit(reason) ==
144+
"bad start spec: invalid shutdown: :foo"
145+
146+
return = {:ok, {{:one_for_one, 1, 1},
147+
[{:child, {:m, :f, []}, :temporary, 1, :foo, []}]}}
148+
{:error, reason} = __MODULE__.Sup.start_link(fn() -> return end)
149+
assert Exception.format_exit(reason) ==
150+
"bad start spec: invalid child type: :foo"
151+
152+
return = {:ok, {{:one_for_one, 1, 1},
153+
[{:child, {:m, :f, []}, :temporary, 1, :worker, :foo}]}}
154+
{:error, reason} = __MODULE__.Sup.start_link(fn() -> return end)
155+
assert Exception.format_exit(reason) ==
156+
"bad start spec: invalid modules: :foo"
157+
158+
return = {:ok, {{:one_for_one, 1, 1},
159+
[{:child, {:m, :f, []}, :temporary, 1, :worker, [{:foo}]}]}}
160+
{:error, reason} = __MODULE__.Sup.start_link(fn() -> return end)
161+
assert Exception.format_exit(reason) ==
162+
"bad start spec: invalid module: {:foo}"
163+
164+
return = {:ok, {{:one_for_one, 1, 1},
165+
[{:child, {Kernel, :exit, [:foo]}, :temporary, 1, :worker, []}]}}
166+
{:error, reason} = __MODULE__.Sup.start_link(fn() -> return end)
167+
assert Exception.format_exit(reason) ==
168+
"shutdown: failed to start child: :child\n ** (EXIT) :foo"
169+
170+
return = {:ok, {{:one_for_one, 1, 1},
171+
[{:child, {Kernel, :apply, [fn() -> {:error, :foo} end, []]}, :temporary, 1, :worker, []}]}}
172+
{:error, reason} = __MODULE__.Sup.start_link(fn() -> return end)
173+
assert Exception.format_exit(reason) ==
174+
"shutdown: failed to start child: :child\n ** (EXIT) :foo"
175+
176+
Process.flag(:trap_exit, trap)
77177
end
78178

79179
test "format_exit with call" do

lib/mix/test/mix/tasks/app.start_test.exs

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,7 @@ defmodule Mix.Tasks.App.StartTest do
124124
defmodule ReturnApp do
125125
use Application.Behaviour
126126

127-
def start(_type, return) do
128-
return # return
129-
end
127+
def start(_type, return), do: return
130128
end
131129

132130
@tag app: :return_sample
@@ -136,8 +134,8 @@ defmodule Mix.Tasks.App.StartTest do
136134
Process.put(:application_definition, mod: {ReturnApp, {:error, :bye}})
137135
Mix.Tasks.Compile.run []
138136

139-
assert_raise Mix.Error, "Could not start application return_sample: exited in: Mix.Tasks.App.StartTest.ReturnApp.start(:normal, {:error, :bye})\n" <>
140-
" ** (EXIT) :bye", fn ->
137+
assert_raise Mix.Error, "Could not start application return_sample: Mix.Tasks.App.StartTest.ReturnApp.start(:normal, {:error, :bye}) returned an error: :bye",
138+
fn ->
141139
Mix.Tasks.App.Start.start(:return_sample)
142140
end
143141
end
@@ -151,43 +149,70 @@ defmodule Mix.Tasks.App.StartTest do
151149
{:badarg, [{ReturnApp, :start, 2, []}] }}})
152150
Mix.Tasks.Compile.run []
153151

154-
assert_raise Mix.Error, "Could not start application return_sample: exited in: Mix.Tasks.App.StartTest.ReturnApp.start(:normal, {:error, {:badarg, [{Mix.Tasks.App.StartTest.ReturnApp, :start, 2, []}]}})\n" <>
155-
" ** (EXIT) an exception was raised:\n" <>
156-
" ** (ArgumentError) argument error\n" <>
157-
" Mix.Tasks.App.StartTest.ReturnApp.start/2", fn ->
152+
assert_raise Mix.Error, "Could not start application return_sample: Mix.Tasks.App.StartTest.ReturnApp.start(:normal, {:error, {:badarg, [{Mix.Tasks.App.StartTest.ReturnApp, :start, 2, []}]}}) returned an error: an exception was raised:\n" <>
153+
" ** (ArgumentError) argument error\n" <>
154+
" Mix.Tasks.App.StartTest.ReturnApp.start/2", fn ->
158155
Mix.Tasks.App.Start.start(:return_sample)
159156
end
160157
end
161158
end
162159

163160
@tag app: :return_sample
164-
test "start points to report on exit" do
161+
test "start points to report on bad return" do
165162
Mix.Project.push ReturnSample
166163
in_fixture "no_mixfile", fn ->
167-
Process.put(:application_definition, mod: {ReturnApp, {:EXIT, :bye}})
164+
Process.put(:application_definition, mod: {ReturnApp, :bad})
168165
Mix.Tasks.Compile.run []
169166

170-
assert_raise Mix.Error, "Could not start application return_sample: exited in: Mix.Tasks.App.StartTest.ReturnApp.start(:normal, {:EXIT, :bye})\n" <>
167+
assert_raise Mix.Error, "Could not start application return_sample: Mix.Tasks.App.StartTest.ReturnApp.start(:normal, :bad) returned a bad value: :bad",
168+
fn ->
169+
Mix.Tasks.App.Start.start(:return_sample)
170+
end
171+
end
172+
end
173+
174+
defmodule ExitSample do
175+
def project do
176+
[app: :exit_sample, version: "0.1.0"]
177+
end
178+
179+
def application do
180+
Process.get(:application_definition)
181+
end
182+
end
183+
184+
defmodule ExitApp do
185+
use Application.Behaviour
186+
187+
def start(_type, reason), do: exit(reason)
188+
end
189+
190+
@tag app: :exit_sample
191+
test "start points to report on exit" do
192+
Mix.Project.push ExitSample
193+
in_fixture "no_mixfile", fn ->
194+
Process.put(:application_definition, mod: {ExitApp, :bye})
195+
Mix.Tasks.Compile.run []
196+
197+
assert_raise Mix.Error, "Could not start application exit_sample: exited in: Mix.Tasks.App.StartTest.ExitApp.start(:normal, :bye)\n" <>
171198
" ** (EXIT) :bye",
172199
fn ->
173-
Mix.Tasks.App.Start.start(:return_sample)
200+
Mix.Tasks.App.Start.start(:exit_sample)
174201
end
175202
end
176203
end
177204

178-
@tag app: :return_sample
179-
test "start points to report on exception exit" do
180-
Mix.Project.push ReturnSample
205+
@tag app: :exit_sample
206+
test "start points to report on normal exit" do
207+
Mix.Project.push ExitSample
181208
in_fixture "no_mixfile", fn ->
182-
Process.put(:application_definition, mod: {ReturnApp, {:EXIT,
183-
{:badarg, [{ReturnApp, :start, 2, []}] }}})
209+
Process.put(:application_definition, mod: {ExitApp, :normal})
184210
Mix.Tasks.Compile.run []
185211

186-
assert_raise Mix.Error, "Could not start application return_sample: exited in: Mix.Tasks.App.StartTest.ReturnApp.start(:normal, {:EXIT, {:badarg, [{Mix.Tasks.App.StartTest.ReturnApp, :start, 2, []}]}})\n" <>
187-
" ** (EXIT) an exception was raised:\n" <>
188-
" ** (ArgumentError) argument error\n" <>
189-
" Mix.Tasks.App.StartTest.ReturnApp.start/2", fn ->
190-
Mix.Tasks.App.Start.start(:return_sample)
212+
assert_raise Mix.Error, "Could not start application exit_sample: exited in: Mix.Tasks.App.StartTest.ExitApp.start(:normal, :normal)\n" <>
213+
" ** (EXIT) normal",
214+
fn ->
215+
Mix.Tasks.App.Start.start(:exit_sample)
191216
end
192217
end
193218
end

0 commit comments

Comments
 (0)